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:
@@ -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."
|
||||||
|
|||||||
+40
-24
@@ -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[]>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user