Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a134d8a
wip(dashboard): setup variant media form
fPolic Oct 2, 2025
121a34c
wip: cleanup table and images, wip check handler
fPolic Oct 3, 2025
07687c9
feat: proper sidebar functionallity
fPolic Oct 3, 2025
877792c
fefat: add js-sdk and hooks
fPolic Oct 3, 2025
3f78c19
feat: allow only one selection
fPolic Oct 7, 2025
bb77adf
wip: lazy load variants in the table
fPolic Oct 7, 2025
7b2e6ca
feat: new variants management for images on product details
fPolic Oct 7, 2025
3fb114a
chore: refactor
fPolic Oct 7, 2025
4555add
wip: variant details page work
fPolic Oct 7, 2025
9cdc91c
fix: cleanup media section, fix issues and types
fPolic Oct 8, 2025
85290be
feat: correct scoped images, cleanup in edit modal
fPolic Oct 8, 2025
e7316b3
Merge branch 'feat/scoped-variant-images' into feat/scoped-variant-im…
fPolic Oct 8, 2025
c4f191d
feat: js sdk and hooks, filter out product images on variant details,…
fPolic Oct 8, 2025
df26de4
chore: cleanup
fPolic Oct 8, 2025
8bd3b51
refacto: rename route
fPolic Oct 8, 2025
2d4768e
Merge branch 'feat/scoped-variant-images' into feat/scoped-variant-im…
fPolic Oct 8, 2025
a81a479
feat: thumbnail functionallity
fPolic Oct 8, 2025
e4be0a4
Merge branch 'feat/scoped-variant-images' into feat/scoped-variant-im…
fPolic Oct 8, 2025
be34ec8
Merge branch 'feat/scoped-variant-images' into feat/scoped-variant-im…
fPolic Oct 8, 2025
8ec2835
fix: refresh checked after revalidation load
fPolic Oct 8, 2025
c69951b
Merge branch 'feat/scoped-variant-images' into feat/scoped-variant-im…
fPolic Oct 9, 2025
b1ebf8d
Merge branch 'feat/scoped-variant-images' into feat/scoped-variant-im…
fPolic Oct 9, 2025
fc81447
Merge branch 'feat/scoped-variant-images' into feat/scoped-variant-im…
fPolic Oct 10, 2025
c37abff
Merge branch 'feat/scoped-variant-images' into feat/scoped-variant-im…
fPolic Oct 13, 2025
30ad99e
Merge branch 'feat/scoped-variant-images' into feat/scoped-variant-im…
fPolic Oct 14, 2025
35dee02
fix: rm unused, refactor type
fPolic Oct 14, 2025
819cfc6
Merge branch 'feat/scoped-variant-images' into feat/scoped-variant-im…
fPolic Oct 21, 2025
1d22a5c
Create thirty-clocks-refuse.md
olivermrbl Oct 22, 2025
43e7541
feat: new add remove variant media layout
fPolic Oct 22, 2025
2073f6b
feat: new image add UX
fPolic Oct 22, 2025
9ebdcb3
Merge branch 'feat/scoped-variant-images' into feat/scoped-variant-im…
fPolic Oct 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/thirty-clocks-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@medusajs/js-sdk": patch
"@medusajs/types": patch
"@medusajs/dashboard": patch
---

feat(dashboard): variant images management UI
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,13 @@ export function getRouteMap({
lazy: () =>
import("../../routes/products/product-media"),
},
{
path: "images/:image_id/variants",
lazy: () =>
import(
"../../routes/products/product-image-variants-edit"
),
},
{
path: "prices",
lazy: () =>
Expand Down Expand Up @@ -198,6 +205,13 @@ export function getRouteMap({
"../../routes/product-variants/product-variant-manage-inventory-items"
),
},
{
path: "media",
lazy: () =>
import(
"../../routes/product-variants/product-variant-media"
),
},
{
path: "metadata/edit",
lazy: () =>
Expand Down
54 changes: 54 additions & 0 deletions packages/admin/dashboard/src/hooks/api/products.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -419,3 +419,57 @@ export const useConfirmImportProducts = (
...options,
})
}

export const useBatchImageVariants = (
productId: string,
imageId: string,
options?: UseMutationOptions<
HttpTypes.AdminBatchImageVariantResponse,
FetchError,
HttpTypes.AdminBatchImageVariantRequest
>
) => {
return useMutation({
mutationFn: (payload) =>
sdk.admin.product.batchImageVariants(productId, imageId, payload),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({
queryKey: productsQueryKeys.detail(productId),
})
queryClient.invalidateQueries({ queryKey: variantsQueryKeys.lists() })
queryClient.invalidateQueries({ queryKey: variantsQueryKeys.details() })

options?.onSuccess?.(data, variables, context)
},
...options,
})
}

export const useBatchVariantImages = (
productId: string,
variantId: string,
options?: UseMutationOptions<
HttpTypes.AdminBatchVariantImagesResponse,
FetchError,
HttpTypes.AdminBatchVariantImagesRequest
>
) => {
return useMutation({
mutationFn: (payload) =>
sdk.admin.product.batchVariantImages(productId, variantId, payload),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({
queryKey: productsQueryKeys.detail(productId),
})
queryClient.invalidateQueries({
queryKey: variantsQueryKeys.list({ productId }),
})
queryClient.invalidateQueries({
queryKey: variantsQueryKeys.detail(variantId),
})

options?.onSuccess?.(data, variables, context)
},
...options,
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { HttpTypes } from "@medusajs/types"
import { useQueryParams } from "../../use-query-params"

type UseProductTagTableQueryProps = {
prefix?: string
pageSize?: number
}

export const useProductVariantTableQuery = ({
prefix,
pageSize = 20,
}: UseProductTagTableQueryProps) => {
const queryObject = useQueryParams(
["offset", "q", "order", "created_at", "updated_at"],
prefix
)

const { offset, q, order, created_at, updated_at } = queryObject
const searchParams: HttpTypes.AdminProductTagListParams = {
limit: pageSize,
offset: offset ? Number(offset) : 0,
order,
created_at: created_at ? JSON.parse(created_at) : undefined,
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
q,
}

return {
searchParams,
raw: queryObject,
}
}
81 changes: 80 additions & 1 deletion packages/admin/dashboard/src/i18n/translations/$schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,12 @@
"idCopiedToClipboard": {
"type": "string"
},
"editVariantImages": {
"type": "string"
},
"editImages": {
"type": "string"
},
"addReason": {
"type": "string"
},
Expand Down Expand Up @@ -530,6 +536,8 @@
"continue",
"continueWithEmail",
"idCopiedToClipboard",
"editVariantImages",
"editImages",
"addReason",
"addNote",
"reset",
Expand Down Expand Up @@ -1912,6 +1920,9 @@
"editHint": {
"type": "string"
},
"manageImageVariants": {
"type": "string"
},
"makeThumbnail": {
"type": "string"
},
Expand Down Expand Up @@ -1969,11 +1980,27 @@
},
"successToast": {
"type": "string"
},
"variantImages": {
"type": "string"
},
"showAvailableImages": {
"type": "string"
},
"availableImages": {
"type": "string"
},
"selectToAdd": {
"type": "string"
},
"removeSelected": {
"type": "string"
}
},
"required": [
"label",
"editHint",
"manageImageVariants",
"makeThumbnail",
"uploadImagesLabel",
"uploadImagesHint",
Expand All @@ -1988,7 +2015,57 @@
"downloadImageLabel",
"deleteImageLabel",
"emptyState",
"successToast"
"successToast",
"variantImages",
"showAvailableImages",
"availableImages",
"selectToAdd",
"removeSelected"
],
"additionalProperties": false
},
"variantMedia": {
"type": "object",
"properties": {
"label": {
"type": "string"
},
"manageVariants": {
"type": "string"
},
"addToMultipleVariants": {
"type": "string"
},
"manageVariantsDescription": {
"type": "string"
},
"successToast": {
"type": "string"
},
"emptyState": {
"type": "object",
"properties": {
"header": {
"type": "string"
},
"description": {
"type": "string"
},
"action": {
"type": "string"
}
},
"required": ["header", "description", "action"],
"additionalProperties": false
}
},
"required": [
"label",
"manageVariants",
"addToMultipleVariants",
"manageVariantsDescription",
"successToast",
"emptyState"
],
"additionalProperties": false
},
Expand Down Expand Up @@ -2736,6 +2813,7 @@
"editOptions",
"editPrices",
"media",
"variantMedia",
"discountableHint",
"noSalesChannels",
"variantCount_one",
Expand Down Expand Up @@ -7678,6 +7756,7 @@
"campaign",
"method",
"allocation",
"allocationTooltip",
"addCondition",
"clearAll",
"taxInclusive",
Expand Down
24 changes: 22 additions & 2 deletions packages/admin/dashboard/src/i18n/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@
"continue": "Continue",
"continueWithEmail": "Continue with Email",
"idCopiedToClipboard": "ID copied to clipboard",
"editVariantImages": "Edit variant images",
"editImages": "Edit images",
"addReason": "Add Reason",
"addNote": "Add Note",
"reset": "Reset",
Expand Down Expand Up @@ -506,6 +508,7 @@
"media": {
"label": "Media",
"editHint": "Add media to the product to showcase it in your storefront.",
"manageImageVariants": "Manage associated variants",
"makeThumbnail": "Make thumbnail",
"uploadImagesLabel": "Upload images",
"uploadImagesHint": "Drag and drop images here or click to upload.",
Expand All @@ -521,10 +524,27 @@
"deleteImageLabel": "Delete current image",
"emptyState": {
"header": "No media yet",
"description": "Add media to the product to showcase it in your storefront.",
"description": "Add media to showcase it in your storefront.",
"action": "Add media"
},
"successToast": "Media was successfully updated."
"successToast": "Media was successfully updated.",
"variantImages": "Variant images",
"showAvailableImages": "Show available images",
"availableImages": "Available images",
"selectToAdd": "Select to add to variant",
"removeSelected": "Remove Selected"
},
"variantMedia": {
"label": "Variant Media",
"manageVariants": "Manage variants",
"addToMultipleVariants": "Add to multiple variants",
"manageVariantsDescription": "Manage associated variants for the image",
"successToast": "Image variants successfully updated.",
"emptyState": {
"header": "No media yet",
"description": "Add media to the variant to showcase it in your storefront.",
"action": "Add media"
}
},
"discountableHint": "When unchecked, discounts will not be applied to this product.",
"noSalesChannels": "Not available in any sales channels",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { VariantMediaSection } from "./variant-media-section"
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Container, Heading, Text, Tooltip } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { HttpTypes } from "@medusajs/types"
import { PencilSquare, ThumbnailBadge } from "@medusajs/icons"

import { ActionMenu } from "../../../../../components/common/action-menu"

type VariantMediaSectionProps = {
variant: HttpTypes.AdminProductVariant
}

export const VariantMediaSection = ({ variant }: VariantMediaSectionProps) => {
const { t } = useTranslation()

// show only variant scoped images
const media = (variant.images || []).filter((image) =>
image.variants?.some((variant) => variant.id === variant.id)
)

return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">{t("products.media.label")}</Heading>
<ActionMenu
groups={[
{
actions: [
{
label: t("actions.editImages"),
to: "media",
icon: <PencilSquare />,
},
],
},
]}
/>
</div>
{media.length > 0 ? (
<div className="grid grid-cols-[repeat(auto-fill,minmax(96px,1fr))] gap-4 px-6 py-4">
{media.map((i) => {
return (
<div
className="shadow-elevation-card-rest hover:shadow-elevation-card-hover transition-fg group relative aspect-square size-full overflow-hidden rounded-[8px]"
key={i.id}
>
{i.url === variant.thumbnail && (
<div className="absolute left-2 top-2">
<Tooltip content={t("products.media.thumbnailTooltip")}>
<ThumbnailBadge />
</Tooltip>
</div>
)}
<img src={i.url} className="size-full object-cover" />
</div>
)
})}
</div>
) : (
<div className="flex flex-col items-center gap-y-4 pb-8 pt-6">
<div className="flex flex-col items-center">
<Text
size="small"
leading="compact"
weight="plus"
className="text-ui-fg-subtle"
>
{t("products.media.emptyState.header")}
</Text>
<Text size="small" className="text-ui-fg-muted">
{t("products.media.emptyState.description")}
</Text>
</div>
</div>
)}
</Container>
)
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export const VARIANT_DETAIL_FIELDS =
"*inventory_items,*inventory_items.inventory,*inventory_items.inventory.location_levels,*options,*options.option,*prices,*prices.price_rules"
"*inventory_items,*inventory_items.inventory,*inventory_items.inventory.location_levels,*options,*options.option,*prices,*prices.price_rules,+images.id,+images.url,+images.variants.id"
Loading