Skip to content

Commit 186e9c5

Browse files
fPolicolivermrbl
andauthored
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 <[email protected]>
1 parent 5a1e2ca commit 186e9c5

File tree

22 files changed

+1199
-8
lines changed

22 files changed

+1199
-8
lines changed

.changeset/thirty-clocks-refuse.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@medusajs/js-sdk": patch
3+
"@medusajs/types": patch
4+
"@medusajs/dashboard": patch
5+
---
6+
7+
feat(dashboard): variant images management UI

packages/admin/dashboard/src/dashboard-app/routes/get-route.map.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,13 @@ export function getRouteMap({
124124
lazy: () =>
125125
import("../../routes/products/product-media"),
126126
},
127+
{
128+
path: "images/:image_id/variants",
129+
lazy: () =>
130+
import(
131+
"../../routes/products/product-image-variants-edit"
132+
),
133+
},
127134
{
128135
path: "prices",
129136
lazy: () =>
@@ -198,6 +205,13 @@ export function getRouteMap({
198205
"../../routes/product-variants/product-variant-manage-inventory-items"
199206
),
200207
},
208+
{
209+
path: "media",
210+
lazy: () =>
211+
import(
212+
"../../routes/product-variants/product-variant-media"
213+
),
214+
},
201215
{
202216
path: "metadata/edit",
203217
lazy: () =>

packages/admin/dashboard/src/hooks/api/products.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,3 +419,57 @@ export const useConfirmImportProducts = (
419419
...options,
420420
})
421421
}
422+
423+
export const useBatchImageVariants = (
424+
productId: string,
425+
imageId: string,
426+
options?: UseMutationOptions<
427+
HttpTypes.AdminBatchImageVariantResponse,
428+
FetchError,
429+
HttpTypes.AdminBatchImageVariantRequest
430+
>
431+
) => {
432+
return useMutation({
433+
mutationFn: (payload) =>
434+
sdk.admin.product.batchImageVariants(productId, imageId, payload),
435+
onSuccess: (data, variables, context) => {
436+
queryClient.invalidateQueries({
437+
queryKey: productsQueryKeys.detail(productId),
438+
})
439+
queryClient.invalidateQueries({ queryKey: variantsQueryKeys.lists() })
440+
queryClient.invalidateQueries({ queryKey: variantsQueryKeys.details() })
441+
442+
options?.onSuccess?.(data, variables, context)
443+
},
444+
...options,
445+
})
446+
}
447+
448+
export const useBatchVariantImages = (
449+
productId: string,
450+
variantId: string,
451+
options?: UseMutationOptions<
452+
HttpTypes.AdminBatchVariantImagesResponse,
453+
FetchError,
454+
HttpTypes.AdminBatchVariantImagesRequest
455+
>
456+
) => {
457+
return useMutation({
458+
mutationFn: (payload) =>
459+
sdk.admin.product.batchVariantImages(productId, variantId, payload),
460+
onSuccess: (data, variables, context) => {
461+
queryClient.invalidateQueries({
462+
queryKey: productsQueryKeys.detail(productId),
463+
})
464+
queryClient.invalidateQueries({
465+
queryKey: variantsQueryKeys.list({ productId }),
466+
})
467+
queryClient.invalidateQueries({
468+
queryKey: variantsQueryKeys.detail(variantId),
469+
})
470+
471+
options?.onSuccess?.(data, variables, context)
472+
},
473+
...options,
474+
})
475+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { HttpTypes } from "@medusajs/types"
2+
import { useQueryParams } from "../../use-query-params"
3+
4+
type UseProductTagTableQueryProps = {
5+
prefix?: string
6+
pageSize?: number
7+
}
8+
9+
export const useProductVariantTableQuery = ({
10+
prefix,
11+
pageSize = 20,
12+
}: UseProductTagTableQueryProps) => {
13+
const queryObject = useQueryParams(
14+
["offset", "q", "order", "created_at", "updated_at"],
15+
prefix
16+
)
17+
18+
const { offset, q, order, created_at, updated_at } = queryObject
19+
const searchParams: HttpTypes.AdminProductTagListParams = {
20+
limit: pageSize,
21+
offset: offset ? Number(offset) : 0,
22+
order,
23+
created_at: created_at ? JSON.parse(created_at) : undefined,
24+
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
25+
q,
26+
}
27+
28+
return {
29+
searchParams,
30+
raw: queryObject,
31+
}
32+
}

packages/admin/dashboard/src/i18n/translations/$schema.json

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,12 @@
450450
"idCopiedToClipboard": {
451451
"type": "string"
452452
},
453+
"editVariantImages": {
454+
"type": "string"
455+
},
456+
"editImages": {
457+
"type": "string"
458+
},
453459
"addReason": {
454460
"type": "string"
455461
},
@@ -530,6 +536,8 @@
530536
"continue",
531537
"continueWithEmail",
532538
"idCopiedToClipboard",
539+
"editVariantImages",
540+
"editImages",
533541
"addReason",
534542
"addNote",
535543
"reset",
@@ -1912,6 +1920,9 @@
19121920
"editHint": {
19131921
"type": "string"
19141922
},
1923+
"manageImageVariants": {
1924+
"type": "string"
1925+
},
19151926
"makeThumbnail": {
19161927
"type": "string"
19171928
},
@@ -1969,11 +1980,27 @@
19691980
},
19701981
"successToast": {
19711982
"type": "string"
1983+
},
1984+
"variantImages": {
1985+
"type": "string"
1986+
},
1987+
"showAvailableImages": {
1988+
"type": "string"
1989+
},
1990+
"availableImages": {
1991+
"type": "string"
1992+
},
1993+
"selectToAdd": {
1994+
"type": "string"
1995+
},
1996+
"removeSelected": {
1997+
"type": "string"
19721998
}
19731999
},
19742000
"required": [
19752001
"label",
19762002
"editHint",
2003+
"manageImageVariants",
19772004
"makeThumbnail",
19782005
"uploadImagesLabel",
19792006
"uploadImagesHint",
@@ -1988,7 +2015,57 @@
19882015
"downloadImageLabel",
19892016
"deleteImageLabel",
19902017
"emptyState",
1991-
"successToast"
2018+
"successToast",
2019+
"variantImages",
2020+
"showAvailableImages",
2021+
"availableImages",
2022+
"selectToAdd",
2023+
"removeSelected"
2024+
],
2025+
"additionalProperties": false
2026+
},
2027+
"variantMedia": {
2028+
"type": "object",
2029+
"properties": {
2030+
"label": {
2031+
"type": "string"
2032+
},
2033+
"manageVariants": {
2034+
"type": "string"
2035+
},
2036+
"addToMultipleVariants": {
2037+
"type": "string"
2038+
},
2039+
"manageVariantsDescription": {
2040+
"type": "string"
2041+
},
2042+
"successToast": {
2043+
"type": "string"
2044+
},
2045+
"emptyState": {
2046+
"type": "object",
2047+
"properties": {
2048+
"header": {
2049+
"type": "string"
2050+
},
2051+
"description": {
2052+
"type": "string"
2053+
},
2054+
"action": {
2055+
"type": "string"
2056+
}
2057+
},
2058+
"required": ["header", "description", "action"],
2059+
"additionalProperties": false
2060+
}
2061+
},
2062+
"required": [
2063+
"label",
2064+
"manageVariants",
2065+
"addToMultipleVariants",
2066+
"manageVariantsDescription",
2067+
"successToast",
2068+
"emptyState"
19922069
],
19932070
"additionalProperties": false
19942071
},
@@ -2736,6 +2813,7 @@
27362813
"editOptions",
27372814
"editPrices",
27382815
"media",
2816+
"variantMedia",
27392817
"discountableHint",
27402818
"noSalesChannels",
27412819
"variantCount_one",
@@ -7678,6 +7756,7 @@
76787756
"campaign",
76797757
"method",
76807758
"allocation",
7759+
"allocationTooltip",
76817760
"addCondition",
76827761
"clearAll",
76837762
"taxInclusive",

packages/admin/dashboard/src/i18n/translations/en.json

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@
122122
"continue": "Continue",
123123
"continueWithEmail": "Continue with Email",
124124
"idCopiedToClipboard": "ID copied to clipboard",
125+
"editVariantImages": "Edit variant images",
126+
"editImages": "Edit images",
125127
"addReason": "Add Reason",
126128
"addNote": "Add Note",
127129
"reset": "Reset",
@@ -506,6 +508,7 @@
506508
"media": {
507509
"label": "Media",
508510
"editHint": "Add media to the product to showcase it in your storefront.",
511+
"manageImageVariants": "Manage associated variants",
509512
"makeThumbnail": "Make thumbnail",
510513
"uploadImagesLabel": "Upload images",
511514
"uploadImagesHint": "Drag and drop images here or click to upload.",
@@ -521,10 +524,27 @@
521524
"deleteImageLabel": "Delete current image",
522525
"emptyState": {
523526
"header": "No media yet",
524-
"description": "Add media to the product to showcase it in your storefront.",
527+
"description": "Add media to showcase it in your storefront.",
525528
"action": "Add media"
526529
},
527-
"successToast": "Media was successfully updated."
530+
"successToast": "Media was successfully updated.",
531+
"variantImages": "Variant images",
532+
"showAvailableImages": "Show available images",
533+
"availableImages": "Available images",
534+
"selectToAdd": "Select to add to variant",
535+
"removeSelected": "Remove Selected"
536+
},
537+
"variantMedia": {
538+
"label": "Variant Media",
539+
"manageVariants": "Manage variants",
540+
"addToMultipleVariants": "Add to multiple variants",
541+
"manageVariantsDescription": "Manage associated variants for the image",
542+
"successToast": "Image variants successfully updated.",
543+
"emptyState": {
544+
"header": "No media yet",
545+
"description": "Add media to the variant to showcase it in your storefront.",
546+
"action": "Add media"
547+
}
528548
},
529549
"discountableHint": "When unchecked, discounts will not be applied to this product.",
530550
"noSalesChannels": "Not available in any sales channels",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { VariantMediaSection } from "./variant-media-section"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { Container, Heading, Text, Tooltip } from "@medusajs/ui"
2+
import { useTranslation } from "react-i18next"
3+
import { HttpTypes } from "@medusajs/types"
4+
import { PencilSquare, ThumbnailBadge } from "@medusajs/icons"
5+
6+
import { ActionMenu } from "../../../../../components/common/action-menu"
7+
8+
type VariantMediaSectionProps = {
9+
variant: HttpTypes.AdminProductVariant
10+
}
11+
12+
export const VariantMediaSection = ({ variant }: VariantMediaSectionProps) => {
13+
const { t } = useTranslation()
14+
15+
// show only variant scoped images
16+
const media = (variant.images || []).filter((image) =>
17+
image.variants?.some((variant) => variant.id === variant.id)
18+
)
19+
20+
return (
21+
<Container className="divide-y p-0">
22+
<div className="flex items-center justify-between px-6 py-4">
23+
<Heading level="h2">{t("products.media.label")}</Heading>
24+
<ActionMenu
25+
groups={[
26+
{
27+
actions: [
28+
{
29+
label: t("actions.editImages"),
30+
to: "media",
31+
icon: <PencilSquare />,
32+
},
33+
],
34+
},
35+
]}
36+
/>
37+
</div>
38+
{media.length > 0 ? (
39+
<div className="grid grid-cols-[repeat(auto-fill,minmax(96px,1fr))] gap-4 px-6 py-4">
40+
{media.map((i) => {
41+
return (
42+
<div
43+
className="shadow-elevation-card-rest hover:shadow-elevation-card-hover transition-fg group relative aspect-square size-full overflow-hidden rounded-[8px]"
44+
key={i.id}
45+
>
46+
{i.url === variant.thumbnail && (
47+
<div className="absolute left-2 top-2">
48+
<Tooltip content={t("products.media.thumbnailTooltip")}>
49+
<ThumbnailBadge />
50+
</Tooltip>
51+
</div>
52+
)}
53+
<img src={i.url} className="size-full object-cover" />
54+
</div>
55+
)
56+
})}
57+
</div>
58+
) : (
59+
<div className="flex flex-col items-center gap-y-4 pb-8 pt-6">
60+
<div className="flex flex-col items-center">
61+
<Text
62+
size="small"
63+
leading="compact"
64+
weight="plus"
65+
className="text-ui-fg-subtle"
66+
>
67+
{t("products.media.emptyState.header")}
68+
</Text>
69+
<Text size="small" className="text-ui-fg-muted">
70+
{t("products.media.emptyState.description")}
71+
</Text>
72+
</div>
73+
</div>
74+
)}
75+
</Container>
76+
)
77+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
export const VARIANT_DETAIL_FIELDS =
2-
"*inventory_items,*inventory_items.inventory,*inventory_items.inventory.location_levels,*options,*options.option,*prices,*prices.price_rules"
2+
"*inventory_items,*inventory_items.inventory,*inventory_items.inventory.location_levels,*options,*options.option,*prices,*prices.price_rules,+images.id,+images.url,+images.variants.id"

0 commit comments

Comments
 (0)