feat(core-flows,product,types): scoped variant images (#13623)

* wip(product): variant images

* fix: return type

* wip: repo and list approach

* fix: redo repo method, make test pass

* fix: change getVariantImages impl

* feat: update test

* feat: API and core flows layer

* wip: integration spec

* fix: deterministic test

* chore: refactor and simplify, cleanup, remove repo method

* wip: batch add all images to all vairants

* fix: remove, expand testing

* refactor: pass variants instead of refetch

* chore: expand integration test

* feat: test multi assign route

* fix: remove `/admin/products/:id/variants/images` route

* feat: batch images to variant endpoint

* fix: length assertion

* feat: variant thumbnail

* fix: send variant thumbnail by default

* fix: product export test assertion

* fix: test

* feat: variant thumbnail on line item

* fix: add missing list and count method, update types

* feat: optimise variant images lookups

* feat: thumbnail management in core flows

* fix: typos, type, build

* feat: cascade delete to pivot table, rm unused unused fields

* feat(dashboard): variant images management UI (#13670)

* wip(dashboard): setup variant media form

* wip: cleanup table and images, wip check handler

* feat: proper sidebar functionallity

* fefat: add js-sdk and hooks

* feat: allow only one selection

* wip: lazy load variants in the table

* feat: new variants management for images on product details

* chore: refactor

* wip: variant details page work

* fix: cleanup media section, fix issues and types

* feat: correct scoped images, cleanup in edit modal

* feat: js sdk and hooks, filter out product images on variant details, labels, add API call and wrap UI

* chore: cleanup

* refacto: rename route

* feat: thumbnail functionallity

* fix: refresh checked after revalidation load

* fix: rm unused, refactor type

* Create thirty-clocks-refuse.md

* feat: new add remove variant media layout

* feat: new image add UX

---------

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>

* fix: table name in migration

* chore: update changesets

---------

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Frane Polić
2025-10-26 15:15:40 +01:00
committed by GitHub
parent bafd006094
commit 4757281677
57 changed files with 3323 additions and 80 deletions

View File

@@ -1565,6 +1565,146 @@ medusaIntegrationTestRunner({
)
})
it("should add one item with variant thumbnail and one item with product thumbnail", async () => {
const salesChannel = await scModuleService.createSalesChannels({
name: "Webshop",
})
const location = await stockLocationModule.createStockLocations({
name: "Warehouse",
})
let cart = await cartModuleService.createCarts({
currency_code: "usd",
sales_channel_id: salesChannel.id,
})
await remoteLink.create([
{
[Modules.SALES_CHANNEL]: {
sales_channel_id: salesChannel.id,
},
[Modules.STOCK_LOCATION]: {
stock_location_id: location.id,
},
},
])
const [product1, product2] = await productModule.createProducts([
{
title: "Test product 1",
status: ProductStatus.PUBLISHED,
thumbnail: "product-thumbnail-1",
variants: [
{
title: "Test variant 1",
manage_inventory: false,
},
],
},
{
title: "Test product 2",
status: ProductStatus.PUBLISHED,
thumbnail: "product-thumbnail-2",
variants: [
{
title: "Test variant 2",
manage_inventory: false,
thumbnail: "variant-thumbnail-2",
},
],
},
])
const priceSet1 = await pricingModule.createPriceSets({
prices: [
{
amount: 30,
currency_code: "usd",
},
],
})
const priceSet2 = await pricingModule.createPriceSets({
prices: [
{
amount: 30,
currency_code: "usd",
},
],
})
await pricingModule.createPricePreferences({
attribute: "currency_code",
value: "usd",
is_tax_inclusive: true,
})
await remoteLink.create([
{
[Modules.PRODUCT]: {
variant_id: product1.variants[0].id,
},
[Modules.PRICING]: {
price_set_id: priceSet1.id,
},
},
])
await remoteLink.create([
{
[Modules.PRODUCT]: {
variant_id: product2.variants[0].id,
},
[Modules.PRICING]: {
price_set_id: priceSet2.id,
},
},
])
cart = await cartModuleService.retrieveCart(cart.id, {
select: ["id", "region_id", "currency_code", "sales_channel_id"],
})
await addToCartWorkflow(appContainer).run({
input: {
items: [
{
variant_id: product1.variants[0].id,
quantity: 1,
},
{
variant_id: product2.variants[0].id,
quantity: 1,
},
],
cart_id: cart.id,
},
})
cart = await cartModuleService.retrieveCart(cart.id, {
relations: ["items"],
})
expect(cart.items).toHaveLength(2)
expect(cart.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
variant_id: product1.variants.find(
(v) => v.title === "Test variant 1"
)!.id,
thumbnail: "product-thumbnail-1",
}),
expect.objectContaining({
variant_id: product2.variants.find(
(v) => v.title === "Test variant 2"
)!.id,
thumbnail: "variant-thumbnail-2",
}),
])
)
})
it("should add custom item to cart", async () => {
const salesChannel = await scModuleService.createSalesChannels({
name: "Webshop",

View File

@@ -0,0 +1,134 @@
import {
batchImageVariantsWorkflow,
batchVariantImagesWorkflow,
} from "@medusajs/core-flows"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { IProductModuleService } from "@medusajs/types"
import { Modules } from "@medusajs/utils"
jest.setTimeout(50000)
const env = {}
medusaIntegrationTestRunner({
env,
testSuite: ({ getContainer }) => {
describe("Workflows: Batch variant image management", () => {
let appContainer
let productModule: IProductModuleService
beforeAll(async () => {
appContainer = getContainer()
productModule = appContainer.resolve(Modules.PRODUCT)
})
const createVariantWithImage = async (
imageUrl: string,
suffix: string
) => {
const [createdProduct] = await productModule.createProducts([
{
title: `test-product-${suffix}`,
images: [{ url: imageUrl }],
variants: [
{
title: `test-variant-${suffix}`,
sku: `test-variant-sku-${suffix}`,
},
],
},
])
const [product] = await productModule.listProducts(
{ id: createdProduct.id },
{
relations: ["variants", "images"],
}
)
const variant = product.variants[0]!
const image = product.images[0]!
await productModule.updateProductVariants(variant.id, {
thumbnail: imageUrl,
})
await productModule.addImageToVariant([
{
variant_id: variant.id,
image_id: image.id,
},
])
const [variantWithThumbnail] = await productModule.listProductVariants(
{ id: variant.id },
{
select: ["id", "thumbnail"],
}
)
expect(variantWithThumbnail.thumbnail).toEqual(imageUrl)
return {
variantId: variant.id,
imageId: image.id,
imageUrl,
}
}
it("clears the variant thumbnail when removing images via batchVariantImagesWorkflow", async () => {
const imageUrl = "https://test-image-url.com/image-1.png"
const { variantId, imageId } = await createVariantWithImage(
imageUrl,
"variant-workflow"
)
const workflow = batchVariantImagesWorkflow(appContainer)
const { result } = await workflow.run({
input: {
variant_id: variantId,
remove: [imageId],
},
})
expect(result.removed).toEqual([imageId])
const [updatedVariant] = await productModule.listProductVariants(
{ id: variantId },
{
select: ["id", "thumbnail"],
}
)
expect(updatedVariant.thumbnail).toBeNull()
})
it("clears the variant thumbnail when removing variants via batchImageVariantsWorkflow", async () => {
const imageUrl = "https://test-image-url.com/image-2.png"
const { variantId, imageId } = await createVariantWithImage(
imageUrl,
"image-workflow"
)
const workflow = batchImageVariantsWorkflow(appContainer)
const { result } = await workflow.run({
input: {
image_id: imageId,
remove: [variantId],
},
})
expect(result.removed).toEqual([variantId])
const [updatedVariant] = await productModule.listProductVariants(
{ id: variantId },
{
select: ["id", "thumbnail"],
}
)
expect(updatedVariant.thumbnail).toBeNull()
})
})
},
})