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:
committed by
GitHub
parent
2b455b15a6
commit
559fc6587a
7
.changeset/honest-cars-knock.md
Normal file
7
.changeset/honest-cars-knock.md
Normal 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,
|
||||
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
@@ -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."
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -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 "./associate-locations-with-channels"
|
||||
export * from "./detach-locations-from-channels"
|
||||
export * from "./can-delete-sales-channels"
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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[]>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user