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 16:19:45 +00:00
committed by GitHub
parent 2b455b15a6
commit 559fc6587a
9 changed files with 510 additions and 1563 deletions
+7
View File
@@ -0,0 +1,7 @@
---
"@medusajs/core-flows": patch
"@medusajs/types": patch
"@medusajs/dashboard": patch
---
fix(): Deleted default sales channel should be prevented
@@ -3,6 +3,7 @@ import {
adminHeaders, adminHeaders,
createAdminUser, createAdminUser,
} from "../../../../helpers/create-admin-user" } from "../../../../helpers/create-admin-user"
import { Modules } from "@medusajs/framework/utils"
jest.setTimeout(60000) jest.setTimeout(60000)
@@ -10,9 +11,10 @@ medusaIntegrationTestRunner({
testSuite: ({ dbConnection, getContainer, api }) => { testSuite: ({ dbConnection, getContainer, api }) => {
let salesChannel1 let salesChannel1
let salesChannel2 let salesChannel2
let container
beforeEach(async () => { beforeEach(async () => {
const container = getContainer() container = getContainer()
await createAdminUser(dbConnection, adminHeaders, container) await createAdminUser(dbConnection, adminHeaders, container)
salesChannel1 = ( salesChannel1 = (
@@ -245,6 +247,34 @@ medusaIntegrationTestRunner({
}) })
describe("DELETE /admin/sales-channels/:id", () => { 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 () => { it("should delete the requested sales channel", async () => {
const toDelete = ( const toDelete = (
await api.get( await api.get(
@@ -268,17 +298,19 @@ medusaIntegrationTestRunner({
object: "sales-channel", object: "sales-channel",
}) })
await api const err = await api
.get( .get(
`/admin/sales-channels/${salesChannel1.id}?fields=id,deleted_at`, `/admin/sales-channels/${salesChannel1.id}?fields=id,deleted_at`,
adminHeaders adminHeaders
) )
.catch((err) => { .catch((err) => {
expect(err.response.data.type).toEqual("not_found") return err
expect(err.response.data.message).toEqual(
`Sales channel with id: ${salesChannel1.id} not found`
)
}) })
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 () => { it("should successfully delete channel associations", async () => {
File diff suppressed because it is too large Load Diff
@@ -2334,6 +2334,9 @@
"update": "Sales channel updated successfully", "update": "Sales channel updated successfully",
"delete": "Sales channel deleted successfully" "delete": "Sales channel deleted successfully"
}, },
"tooltip": {
"cannotDeleteDefault": "Cannot delete default sales channel"
},
"products": { "products": {
"list": { "list": {
"noRecordsMessage": "There are no products in the sales channel." "noRecordsMessage": "There are no products in the sales channel."
@@ -13,8 +13,12 @@ import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react" import { useMemo } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom" 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 { DataTable } from "../../../../components/table/data-table"
import { useStore } from "../../../../hooks/api"
import { import {
useDeleteSalesChannel, useDeleteSalesChannel,
useSalesChannels, useSalesChannels,
@@ -29,9 +33,12 @@ const PAGE_SIZE = 20
export const SalesChannelListTable = () => { export const SalesChannelListTable = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { store } = useStore()
const { raw, searchParams } = useSalesChannelTableQuery({ const { raw, searchParams } = useSalesChannelTableQuery({
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
}) })
const { const {
sales_channels, sales_channels,
count, count,
@@ -40,13 +47,22 @@ export const SalesChannelListTable = () => {
error, error,
} = useSalesChannels(searchParams, { } = useSalesChannels(searchParams, {
placeholderData: keepPreviousData, placeholderData: keepPreviousData,
}) }) as Omit<ReturnType<typeof useSalesChannels>, "sales_channels"> & {
sales_channels: (HttpTypes.AdminSalesChannel & { is_default?: boolean })[]
}
const columns = useColumns() const columns = useColumns()
const filters = useSalesChannelTableFilters() 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({ const { table } = useDataTable({
data: sales_channels ?? [], data: sales_channels_data,
columns, columns,
count, count,
enablePagination: true, enablePagination: true,
@@ -97,7 +113,7 @@ export const SalesChannelListTable = () => {
const SalesChannelActions = ({ const SalesChannelActions = ({
salesChannel, salesChannel,
}: { }: {
salesChannel: HttpTypes.AdminSalesChannel salesChannel: HttpTypes.AdminSalesChannel & { is_default?: boolean }
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const prompt = usePrompt() const prompt = usePrompt()
@@ -129,30 +145,30 @@ const SalesChannelActions = ({
}) })
} }
return ( const disabledTooltip = salesChannel.is_default
<ActionMenu ? t("salesChannels.tooltip.cannotDeleteDefault")
groups={[ : undefined
const groups: ActionGroup[] = [
{
actions: [
{ {
actions: [ icon: <PencilSquare />,
{ label: t("actions.edit"),
icon: <PencilSquare />, to: `/settings/sales-channels/${salesChannel.id}/edit`,
label: t("actions.edit"),
to: `/settings/sales-channels/${salesChannel.id}/edit`,
},
],
}, },
{ {
actions: [ icon: <Trash />,
{ label: t("actions.delete"),
icon: <Trash />, onClick: handleDelete,
label: t("actions.delete"), disabled: salesChannel.is_default,
onClick: handleDelete, disabledTooltip,
},
],
}, },
]} ],
/> },
) ]
return <ActionMenu groups={groups} />
} }
const columnHelper = createColumnHelper<HttpTypes.AdminSalesChannel>() const columnHelper = createColumnHelper<HttpTypes.AdminSalesChannel>()
@@ -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)
}
)
@@ -6,3 +6,4 @@ export * from "./detach-products-from-sales-channels"
export * from "./update-sales-channels" export * from "./update-sales-channels"
export * from "./associate-locations-with-channels" export * from "./associate-locations-with-channels"
export * from "./detach-locations-from-channels" export * from "./detach-locations-from-channels"
export * from "./can-delete-sales-channels"
@@ -7,6 +7,7 @@ import {
import { emitEventStep } from "../../common" import { emitEventStep } from "../../common"
import { removeRemoteLinkStep } from "../../common/steps/remove-remote-links" import { removeRemoteLinkStep } from "../../common/steps/remove-remote-links"
import { deleteSalesChannelsStep } from "../steps/delete-sales-channels" import { deleteSalesChannelsStep } from "../steps/delete-sales-channels"
import { canDeleteSalesChannelsOrThrowStep } from "../steps"
export type DeleteSalesChannelsWorkflowInput = { ids: string[] } export type DeleteSalesChannelsWorkflowInput = { ids: string[] }
@@ -19,6 +20,7 @@ export const deleteSalesChannelsWorkflow = createWorkflow(
( (
input: WorkflowData<DeleteSalesChannelsWorkflowInput> input: WorkflowData<DeleteSalesChannelsWorkflowInput>
): WorkflowData<void> => { ): WorkflowData<void> => {
canDeleteSalesChannelsOrThrowStep({ ids: input.ids })
deleteSalesChannelsStep(input.ids) deleteSalesChannelsStep(input.ids)
removeRemoteLinkStep({ removeRemoteLinkStep({
@@ -1,4 +1,4 @@
import { BaseFilterable } from "../../dal" import { BaseFilterable, OperatorMap } from "../../dal"
export interface StoreCurrencyDTO { export interface StoreCurrencyDTO {
/** /**
@@ -99,4 +99,8 @@ export interface FilterableStoreProps
* Filter stores by their names. * Filter stores by their names.
*/ */
name?: string | string[] name?: string | string[]
/**
* Filter stores by their associated default sales channel's ID.
*/
default_sales_channel_id?: string | string[] | OperatorMap<string | string[]>
} }