feat: add sales channel management (#6761)

Add V2 sales channel management to admin

`@medusajs/medusa`
- Add `POST /admin/sales-channels/:id/products/batch/remove`
- Refactor cross-module filter middleware to comply with the latest convention

`@medusajs/admin-next`
- Add all sales channel routes
- Moves the following sales channel UI to shared components in `modules/sales-channel`:
  - sales-channel-list
  - sales-channel-edit
  - sales-channel-details
    - sales-channel-general-section
  - sales-channel-create

The sales-channel-product-section is not shared because the API in V2 will change.
The sales-channel-add-products component is not shared because the API in V2 will change.

`@medusajs/core-flows`
- Add `detachProductsFromSalesChannelsStep`
- Add `removeProductsFromSalesChannelsWorkflow`
This commit is contained in:
Oli Juhl
2024-04-02 15:38:33 +02:00
committed by GitHub
parent 3dee91426e
commit 7895ff3849
40 changed files with 1184 additions and 58 deletions

View File

@@ -1,4 +1,4 @@
const { ModuleRegistrationName } = require("@medusajs/modules-sdk")
const { ModuleRegistrationName, Modules } = require("@medusajs/modules-sdk")
const { medusaIntegrationTestRunner } = require("medusa-test-utils")
const { createAdminUser } = require("../../../helpers/create-admin-user")
const { breaking } = require("../../../helpers/breaking")
@@ -24,6 +24,7 @@ medusaIntegrationTestRunner({
let salesChannelService
let productService
let remoteQuery
let remoteLink
beforeAll(() => {
;({
@@ -46,6 +47,7 @@ medusaIntegrationTestRunner({
)
productService = container.resolve(ModuleRegistrationName.PRODUCT)
remoteQuery = container.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK)
})
describe("GET /admin/sales-channels/:id", () => {
@@ -614,60 +616,126 @@ medusaIntegrationTestRunner({
})
describe("DELETE /admin/sales-channels/:id/products/batch", () => {
// BREAKING CHANGE: Endpoint has changed
// from: DELETE /admin/sales-channels/:id/products/batch
// to: POST /admin/sales-channels/:id/products/batch/remove
let salesChannel
let product
beforeEach(async () => {
product = await simpleProductFactory(dbConnection, {
id: "product_1",
title: "test title",
})
salesChannel = await simpleSalesChannelFactory(dbConnection, {
name: "test name",
description: "test description",
products: [product],
})
;({ salesChannel, product } = await breaking(
async () => {
const product = await simpleProductFactory(dbConnection, {
id: "product_1",
title: "test title",
})
const salesChannel = await simpleSalesChannelFactory(dbConnection, {
name: "test name",
description: "test description",
products: [product],
})
return { salesChannel, product }
},
async () => {
const salesChannel = await salesChannelService.create({
name: "test name",
description: "test description",
})
const product = await productService.create({
title: "test title",
})
await remoteLink.create({
[Modules.PRODUCT]: {
product_id: product.id,
},
[Modules.SALES_CHANNEL]: {
sales_channel_id: salesChannel.id,
},
})
return { salesChannel, product }
}
))
})
it("should remove products from a sales channel", async () => {
let attachedProduct = await dbConnection.manager.findOne(Product, {
where: { id: product.id },
relations: ["sales_channels"],
})
const attachedProduct = await breaking(
async () => {
return await dbConnection.manager.findOne(Product, {
where: { id: product.id },
relations: ["sales_channels"],
})
},
async () => {
const [product] = await remoteQuery({
products: {
fields: ["id"],
sales_channels: {
fields: ["id", "name", "description", "is_disabled"],
},
},
})
expect(attachedProduct.sales_channels.length).toBe(2)
return product
}
)
expect(attachedProduct.sales_channels.length).toBe(
breaking(
() => 2,
() => 1 // Comment: The product factory from v1 adds products to the default channel
)
)
expect(attachedProduct.sales_channels).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
name: "test name",
description: "test description",
is_disabled: false,
}),
expect.objectContaining({
id: expect.any(String),
is_disabled: false,
}),
])
expect.arrayContaining(
breaking(
() => [
expect.objectContaining({
id: expect.any(String),
name: "test name",
description: "test description",
is_disabled: false,
}),
expect.objectContaining({
id: expect.any(String),
is_disabled: false,
}),
],
() => [
expect.objectContaining({
id: expect.any(String),
name: "test name",
description: "test description",
is_disabled: false,
}),
]
)
)
)
const payload = {
product_ids: [{ id: product.id }],
product_ids: breaking(
() => [{ id: product.id }],
() => [product.id]
),
}
await api.delete(
`/admin/sales-channels/${salesChannel.id}/products/batch`,
{
...adminReqConfig,
data: payload,
}
)
// Validate idempotency
const response = await api.delete(
`/admin/sales-channels/${salesChannel.id}/products/batch`,
{
...adminReqConfig,
data: payload,
const response = await breaking(
async () => {
return await api.delete(
`/admin/sales-channels/${salesChannel.id}/products/batch`,
{ ...adminReqConfig, data: payload }
)
},
async () => {
return await api.post(
`/admin/sales-channels/${salesChannel.id}/products/batch/remove`,
payload,
adminReqConfig
)
}
)
@@ -681,17 +749,42 @@ medusaIntegrationTestRunner({
})
)
attachedProduct = await dbConnection.manager.findOne(Product, {
where: { id: product.id },
relations: ["sales_channels"],
})
const removedProduct = await breaking(
async () => {
return await dbConnection.manager.findOne(Product, {
where: { id: product.id },
relations: ["sales_channels"],
})
},
async () => {
const [product] = await remoteQuery({
products: {
fields: ["id"],
sales_channels: {
fields: ["id", "name", "description", "is_disabled"],
},
},
})
return product
}
)
// default sales channel
expect(attachedProduct.sales_channels.length).toBe(1)
expect(removedProduct.sales_channels.length).toBe(
breaking(
() => 1,
() => 0 // Comment: The product factory from v1 adds products to the default channel
)
)
})
})
describe("POST /admin/sales-channels/:id/products/batch", () => {
// BREAKING CHANGE: Endpoint has changed
// from: /admin/sales-channels/:id/products/batch
// to: /admin/sales-channels/:id/products/batch/add
let { salesChannel, product } = {}
beforeEach(async () => {