fix(): Deleted default sales channel should be prevented (#10193)

FIXES CMRC-722

**What**
- It should not be allowed to delete a default sales channel
- The admin does not allow to delete a sales channel use as the default for the store
This commit is contained in:
Adrien de Peretti
2024-12-05 17:19:45 +01:00
committed by GitHub
parent 2b455b15a6
commit 559fc6587a
9 changed files with 510 additions and 1563 deletions

View File

@@ -0,0 +1,7 @@
---
"@medusajs/core-flows": patch
"@medusajs/types": patch
"@medusajs/dashboard": patch
---
fix(): Deleted default sales channel should be prevented

View File

@@ -3,6 +3,7 @@ import {
adminHeaders,
createAdminUser,
} from "../../../../helpers/create-admin-user"
import { Modules } from "@medusajs/framework/utils"
jest.setTimeout(60000)
@@ -10,9 +11,10 @@ medusaIntegrationTestRunner({
testSuite: ({ dbConnection, getContainer, api }) => {
let salesChannel1
let salesChannel2
let container
beforeEach(async () => {
const container = getContainer()
container = getContainer()
await createAdminUser(dbConnection, adminHeaders, container)
salesChannel1 = (
@@ -245,6 +247,34 @@ medusaIntegrationTestRunner({
})
describe("DELETE /admin/sales-channels/:id", () => {
it("should fail to delete the requested sales channel if it is used as a default sales channel", async () => {
const salesChannel = (
await api.post(
"/admin/sales-channels",
{ name: "Test channel", description: "Test" },
adminHeaders
)
).data.sales_channel
const storeModule = container.resolve(Modules.STORE)
await storeModule.createStores({
name: "New store",
supported_currencies: [
{ currency_code: "usd", is_default: true },
{ currency_code: "dkk" },
],
default_sales_channel_id: salesChannel.id,
})
const errorResponse = await api
.delete(`/admin/sales-channels/${salesChannel.id}`, adminHeaders)
.catch((err) => err)
expect(errorResponse.response.data.message).toEqual(
`Cannot delete default sales channels: ${salesChannel.id}`
)
})
it("should delete the requested sales channel", async () => {
const toDelete = (
await api.get(
@@ -268,17 +298,19 @@ medusaIntegrationTestRunner({
object: "sales-channel",
})
await api
const err = await api
.get(
`/admin/sales-channels/${salesChannel1.id}?fields=id,deleted_at`,
adminHeaders
)
.catch((err) => {
expect(err.response.data.type).toEqual("not_found")
expect(err.response.data.message).toEqual(
`Sales channel with id: ${salesChannel1.id} not found`
)
return err
})
expect(err.response.data.type).toEqual("not_found")
expect(err.response.data.message).toEqual(
`Sales channel with id: ${salesChannel1.id} not found`
)
})
it("should successfully delete channel associations", async () => {

File diff suppressed because it is too large Load Diff

View File

@@ -2334,6 +2334,9 @@
"update": "Sales channel updated successfully",
"delete": "Sales channel deleted successfully"
},
"tooltip": {
"cannotDeleteDefault": "Cannot delete default sales channel"
},
"products": {
"list": {
"noRecordsMessage": "There are no products in the sales channel."

View File

@@ -13,8 +13,12 @@ import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { ActionMenu } from "../../../../components/common/action-menu"
import {
ActionGroup,
ActionMenu,
} from "../../../../components/common/action-menu"
import { DataTable } from "../../../../components/table/data-table"
import { useStore } from "../../../../hooks/api"
import {
useDeleteSalesChannel,
useSalesChannels,
@@ -29,9 +33,12 @@ const PAGE_SIZE = 20
export const SalesChannelListTable = () => {
const { t } = useTranslation()
const { store } = useStore()
const { raw, searchParams } = useSalesChannelTableQuery({
pageSize: PAGE_SIZE,
})
const {
sales_channels,
count,
@@ -40,13 +47,22 @@ export const SalesChannelListTable = () => {
error,
} = useSalesChannels(searchParams, {
placeholderData: keepPreviousData,
})
}) as Omit<ReturnType<typeof useSalesChannels>, "sales_channels"> & {
sales_channels: (HttpTypes.AdminSalesChannel & { is_default?: boolean })[]
}
const columns = useColumns()
const filters = useSalesChannelTableFilters()
const sales_channels_data =
sales_channels?.map((sales_channel) => {
sales_channel.is_default =
store?.default_sales_channel_id === sales_channel.id
return sales_channel
}) ?? []
const { table } = useDataTable({
data: sales_channels ?? [],
data: sales_channels_data,
columns,
count,
enablePagination: true,
@@ -97,7 +113,7 @@ export const SalesChannelListTable = () => {
const SalesChannelActions = ({
salesChannel,
}: {
salesChannel: HttpTypes.AdminSalesChannel
salesChannel: HttpTypes.AdminSalesChannel & { is_default?: boolean }
}) => {
const { t } = useTranslation()
const prompt = usePrompt()
@@ -129,30 +145,30 @@ const SalesChannelActions = ({
})
}
return (
<ActionMenu
groups={[
const disabledTooltip = salesChannel.is_default
? t("salesChannels.tooltip.cannotDeleteDefault")
: undefined
const groups: ActionGroup[] = [
{
actions: [
{
actions: [
{
icon: <PencilSquare />,
label: t("actions.edit"),
to: `/settings/sales-channels/${salesChannel.id}/edit`,
},
],
icon: <PencilSquare />,
label: t("actions.edit"),
to: `/settings/sales-channels/${salesChannel.id}/edit`,
},
{
actions: [
{
icon: <Trash />,
label: t("actions.delete"),
onClick: handleDelete,
},
],
icon: <Trash />,
label: t("actions.delete"),
onClick: handleDelete,
disabled: salesChannel.is_default,
disabledTooltip,
},
]}
/>
)
],
},
]
return <ActionMenu groups={groups} />
}
const columnHelper = createColumnHelper<HttpTypes.AdminSalesChannel>()

View File

@@ -0,0 +1,36 @@
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { MedusaError, Modules } from "@medusajs/framework/utils"
export const canDeleteSalesChannelsOrThrowStepId =
"can-delete-sales-channels-or-throw-step"
export const canDeleteSalesChannelsOrThrowStep = createStep(
canDeleteSalesChannelsOrThrowStepId,
async ({ ids }: { ids: string | string[] }, { container }) => {
const salesChannelIdsToDelete = Array.isArray(ids) ? ids : [ids]
const storeModule = await container.resolve(Modules.STORE)
const stores = await storeModule.listStores(
{
default_sales_channel_id: salesChannelIdsToDelete,
},
{
select: ["default_sales_channel_id"],
}
)
const defaultSalesChannelIds = stores.map((s) => s.default_sales_channel_id)
if (defaultSalesChannelIds.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Cannot delete default sales channels: ${defaultSalesChannelIds.join(
", "
)}`
)
}
return new StepResponse(true)
}
)

View File

@@ -6,3 +6,4 @@ export * from "./detach-products-from-sales-channels"
export * from "./update-sales-channels"
export * from "./associate-locations-with-channels"
export * from "./detach-locations-from-channels"
export * from "./can-delete-sales-channels"

View File

@@ -7,6 +7,7 @@ import {
import { emitEventStep } from "../../common"
import { removeRemoteLinkStep } from "../../common/steps/remove-remote-links"
import { deleteSalesChannelsStep } from "../steps/delete-sales-channels"
import { canDeleteSalesChannelsOrThrowStep } from "../steps"
export type DeleteSalesChannelsWorkflowInput = { ids: string[] }
@@ -19,6 +20,7 @@ export const deleteSalesChannelsWorkflow = createWorkflow(
(
input: WorkflowData<DeleteSalesChannelsWorkflowInput>
): WorkflowData<void> => {
canDeleteSalesChannelsOrThrowStep({ ids: input.ids })
deleteSalesChannelsStep(input.ids)
removeRemoteLinkStep({

View File

@@ -1,4 +1,4 @@
import { BaseFilterable } from "../../dal"
import { BaseFilterable, OperatorMap } from "../../dal"
export interface StoreCurrencyDTO {
/**
@@ -99,4 +99,8 @@ export interface FilterableStoreProps
* Filter stores by their names.
*/
name?: string | string[]
/**
* Filter stores by their associated default sales channel's ID.
*/
default_sales_channel_id?: string | string[] | OperatorMap<string | string[]>
}