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
co-authored by Oli Juhl
parent bafd006094
commit 4757281677
57 changed files with 3323 additions and 80 deletions
@@ -0,0 +1,26 @@
import { batchImageVariantsWorkflow } from "@medusajs/core-flows"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import { HttpTypes } from "@medusajs/framework/types"
export const POST = async (
req: AuthenticatedMedusaRequest<HttpTypes.AdminBatchImageVariantRequest>,
res: MedusaResponse<HttpTypes.AdminBatchImageVariantResponse>
) => {
const imageId = req.params.image_id
const { result } = await batchImageVariantsWorkflow(req.scope).run({
input: {
image_id: imageId,
add: req.validatedBody.add,
remove: req.validatedBody.remove,
},
})
res.status(200).json({
added: result.added,
removed: result.removed,
})
}
@@ -0,0 +1,26 @@
import { batchVariantImagesWorkflow } from "@medusajs/core-flows"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import { HttpTypes } from "@medusajs/framework/types"
export const POST = async (
req: AuthenticatedMedusaRequest<HttpTypes.AdminBatchVariantImagesRequest>,
res: MedusaResponse<HttpTypes.AdminBatchVariantImagesResponse>
) => {
const variantId = req.params.variant_id
const { result } = await batchVariantImagesWorkflow(req.scope).run({
input: {
variant_id: variantId,
add: req.validatedBody.add,
remove: req.validatedBody.remove,
},
})
res.status(200).json({
added: result.added,
removed: result.removed,
})
}
@@ -13,6 +13,8 @@ import { maybeApplyPriceListsFilter } from "./utils"
import {
AdminBatchCreateVariantInventoryItem,
AdminBatchDeleteVariantInventoryItem,
AdminBatchImageVariant,
AdminBatchVariantImages,
AdminBatchUpdateProduct,
AdminBatchUpdateProductVariant,
AdminBatchUpdateVariantInventoryItem,
@@ -180,6 +182,22 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
),
],
},
{
method: ["POST"],
matcher: "/admin/products/:id/images/:image_id/variants/batch",
bodyParser: {
sizeLimit: DEFAULT_BATCH_ENDPOINTS_SIZE_LIMIT,
},
middlewares: [validateAndTransformBody(AdminBatchImageVariant)],
},
{
method: ["POST"],
matcher: "/admin/products/:id/variants/:variant_id/images/batch",
bodyParser: {
sizeLimit: DEFAULT_BATCH_ENDPOINTS_SIZE_LIMIT,
},
middlewares: [validateAndTransformBody(AdminBatchVariantImages)],
},
// Note: New endpoint in v2
{
method: ["GET"],
@@ -1,6 +1,7 @@
export const defaultAdminProductsVariantFields = [
"id",
"product_id",
"thumbnail",
"title",
"sku",
"allow_backorder",
@@ -191,6 +191,7 @@ export const UpdateProductVariant = z
barcode: z.string().nullish(),
hs_code: z.string().nullish(),
mid_code: z.string().nullish(),
thumbnail: z.string().nullish(),
allow_backorder: booleanString().optional(),
manage_inventory: booleanString().optional(),
variant_rank: z.number().optional(),
@@ -351,6 +352,20 @@ export type AdminBatchVariantInventoryItemsType = BatchMethodRequest<
AdminBatchDeleteVariantInventoryItemType
>
export const AdminBatchImageVariant = z.object({
add: z.array(z.string()).optional(),
remove: z.array(z.string()).optional(),
}) satisfies ZodType<HttpTypes.AdminBatchImageVariantRequest>
export type AdminBatchImageVariantType = z.infer<typeof AdminBatchImageVariant>
export const AdminBatchVariantImages = z.object({
add: z.array(z.string()).optional(),
remove: z.array(z.string()).optional(),
}) satisfies ZodType<HttpTypes.AdminBatchVariantImagesRequest>
export type AdminBatchVariantImagesType = z.infer<
typeof AdminBatchVariantImages
>
export const AdminImportProducts = z.object({
file_key: z.string(),
originalname: z.string(),