From d5fc46b22240d73d94d4040189f98da0126773bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frane=20Poli=C4=87?= <16856471+fPolic@users.noreply.github.com> Date: Thu, 30 Oct 2025 15:21:27 +0100 Subject: [PATCH] feat(dashboard): variant images list thumbnail + refactor form state management (#13905) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary **What** — What changes are introduced in this PR? - show thumbnail on the product variant list - refactor variant image editor state management - await revalidation before rendering form **Testing** — How have these changes been tested, or how can the reviewer test the feature? Manual testing --- ## Checklist Please ensure the following before requesting a review: - [x] I have added a **changeset** for this PR - Every non-breaking change should be marked as a **patch** - To add a changeset, run `yarn changeset` and follow the prompts - [ ] The changes are covered by relevant **tests** - [x] I have verified the code works as intended locally - [ ] I have linked the related issue(s) if applicable --- .changeset/serious-doors-pump.md | 5 ++ .../dashboard/src/i18n/translations/en.json | 4 +- .../edit-product-variant-media-form.tsx | 59 ++++++++----------- .../product-variant-media.tsx | 8 ++- .../product-variant-section.tsx | 48 +++++++-------- 5 files changed, 58 insertions(+), 66 deletions(-) create mode 100644 .changeset/serious-doors-pump.md diff --git a/.changeset/serious-doors-pump.md b/.changeset/serious-doors-pump.md new file mode 100644 index 0000000000..21b4c314ee --- /dev/null +++ b/.changeset/serious-doors-pump.md @@ -0,0 +1,5 @@ +--- +"@medusajs/dashboard": patch +--- + +feat(dashboard): variant images list thumbnail + refactor form state management diff --git a/packages/admin/dashboard/src/i18n/translations/en.json b/packages/admin/dashboard/src/i18n/translations/en.json index 6b102051c2..90a06cbe3a 100644 --- a/packages/admin/dashboard/src/i18n/translations/en.json +++ b/packages/admin/dashboard/src/i18n/translations/en.json @@ -530,8 +530,8 @@ "successToast": "Media was successfully updated.", "variantImages": "Variant images", "showAvailableImages": "Show available images", - "availableImages": "Available images", - "selectToAdd": "Select to add to variant", + "availableImages": "Select images", + "selectToAdd": "Add product images to the variant. To add new images, add them to the product first.", "removeSelected": "Remove Selected" }, "variantMedia": { diff --git a/packages/admin/dashboard/src/routes/product-variants/product-variant-media/components/edit-product-variant-media-form/edit-product-variant-media-form.tsx b/packages/admin/dashboard/src/routes/product-variants/product-variant-media/components/edit-product-variant-media-form/edit-product-variant-media-form.tsx index f87f474340..167d2f2fda 100644 --- a/packages/admin/dashboard/src/routes/product-variants/product-variant-media/components/edit-product-variant-media-form/edit-product-variant-media-form.tsx +++ b/packages/admin/dashboard/src/routes/product-variants/product-variant-media/components/edit-product-variant-media-form/edit-product-variant-media-form.tsx @@ -51,24 +51,9 @@ export const EditProductVariantMediaForm = ({ (image) => !image.variants?.some((variant) => variant.id === variant.id) ) - const [variantImages, setVariantImages] = useState>(() => - allVariantImages.reduce( - // @eslint-disable-next-line - (acc: Record, image) => { - acc[image.id] = true - return acc - }, - {} - ) - ) - const [selection, setSelection] = useState>({}) const [isSidebarOpen, setIsSidebarOpen] = useState(false) - const availableImages = unassociatedImages.filter( - (image) => !variantImages[image.id!] - ) - const form = useForm({ defaultValues: { image_ids: allVariantImages.map((image) => image.id!), @@ -77,6 +62,11 @@ export const EditProductVariantMediaForm = ({ resolver: zodResolver(MediaSchema), }) + const formImageIds = form.watch("image_ids") + const availableImages = unassociatedImages.filter( + (image) => !formImageIds.includes(image.id!) + ) + const { mutateAsync: updateVariant } = useUpdateProductVariant( variant.product_id!, variant.id! @@ -88,10 +78,8 @@ export const EditProductVariantMediaForm = ({ ) const handleSubmit = form.handleSubmit(async (data) => { - const currentVariantImageIds = data.image_ids - const newVariantImageIds = Object.keys(variantImages).filter( - (id) => variantImages[id] - ) + const currentVariantImageIds = allVariantImages.map((image) => image.id!) + const newVariantImageIds = data.image_ids const imagesToAdd = newVariantImageIds.filter( (id) => !currentVariantImageIds.includes(id) @@ -134,10 +122,11 @@ export const EditProductVariantMediaForm = ({ }) const handleAddImageToVariant = (imageId: string) => { - setVariantImages((prev) => ({ - ...prev, - [imageId]: true, - })) + const currentImageIds = form.getValues("image_ids") + form.setValue("image_ids", [...currentImageIds, imageId], { + shouldDirty: true, + shouldTouch: true, + }) } const handleCheckedChange = useCallback( @@ -163,7 +152,10 @@ export const EditProductVariantMediaForm = ({ const selectedImage = allProductImages.find((image) => image.id === ids[0]) if (selectedImage) { - form.setValue("thumbnail", selectedImage.url) + form.setValue("thumbnail", selectedImage.url, { + shouldDirty: selectedImage.url !== variant.thumbnail, + shouldTouch: selectedImage.url !== variant.thumbnail, + }) } } @@ -173,12 +165,13 @@ export const EditProductVariantMediaForm = ({ return } - setVariantImages((prev) => { - const newVariantImages = { ...prev } - selectedIds.forEach((id) => { - delete newVariantImages[id] - }) - return newVariantImages + const currentImageIds = form.getValues("image_ids") + const newImageIds = currentImageIds.filter( + (id) => !selectedIds.includes(id) + ) + form.setValue("image_ids", newImageIds, { + shouldDirty: true, + shouldTouch: true, }) setSelection({}) @@ -218,7 +211,7 @@ export const EditProductVariantMediaForm = ({
{allProductImages - .filter((image) => variantImages[image.id!]) + .filter((image) => formImageIds.includes(image.id!)) .map((image) => ( {t("products.media.availableImages")} -

+

{t("products.media.selectToAdd")}

@@ -272,7 +265,7 @@ export const EditProductVariantMediaForm = ({

{t("products.media.availableImages")}

-

+

{t("products.media.selectToAdd")}

diff --git a/packages/admin/dashboard/src/routes/product-variants/product-variant-media/product-variant-media.tsx b/packages/admin/dashboard/src/routes/product-variants/product-variant-media/product-variant-media.tsx index 02bdd2a9d1..a3793607e9 100644 --- a/packages/admin/dashboard/src/routes/product-variants/product-variant-media/product-variant-media.tsx +++ b/packages/admin/dashboard/src/routes/product-variants/product-variant-media/product-variant-media.tsx @@ -12,13 +12,15 @@ type ProductMediaVariantsReponse = HttpTypes.AdminProductVariant & { export const ProductVariantMedia = () => { const { id, variant_id } = useParams() - const { variant, isLoading, isError, error } = useProductVariant( + const { variant, isFetching, isError, error } = useProductVariant( id!, variant_id!, - { fields: "*product,*product.images,*images,+images.variants.id" } + { + fields: "*product,*product.images,*images,+images.variants.id", + } ) - const ready = !isLoading && variant + const ready = !isFetching && variant if (isError) { throw error diff --git a/packages/admin/dashboard/src/routes/products/product-detail/components/product-variant-section/product-variant-section.tsx b/packages/admin/dashboard/src/routes/products/product-detail/components/product-variant-section/product-variant-section.tsx index 0732b9628c..c3604dc113 100644 --- a/packages/admin/dashboard/src/routes/products/product-detail/components/product-variant-section/product-variant-section.tsx +++ b/packages/admin/dashboard/src/routes/products/product-detail/components/product-variant-section/product-variant-section.tsx @@ -18,7 +18,6 @@ import { useTranslation } from "react-i18next" import { CellContext } from "@tanstack/react-table" import { useNavigate, useSearchParams } from "react-router-dom" import { DataTable } from "../../../../../components/data-table" -import { useDataTableDateColumns } from "../../../../../components/data-table/helpers/general/use-data-table-date-columns" import { useDataTableDateFilters } from "../../../../../components/data-table/helpers/general/use-data-table-date-filters" import { useDeleteVariantLazy, @@ -26,6 +25,7 @@ import { } from "../../../../../hooks/api/products" import { useQueryParams } from "../../../../../hooks/use-query-params" import { PRODUCT_VARIANT_IDS_KEY } from "../../../common/constants" +import { Thumbnail } from "../../../../../components/common/thumbnail" type ProductVariantSectionProps = { product: HttpTypes.AdminProduct @@ -39,26 +39,11 @@ export const ProductVariantSection = ({ }: ProductVariantSectionProps) => { const { t } = useTranslation() - const { - q, - order, - offset, - allow_backorder, - manage_inventory, - created_at, - updated_at, - } = useQueryParams( - [ - "q", - "order", - "offset", - "manage_inventory", - "allow_backorder", - "created_at", - "updated_at", - ], - PREFIX - ) + const { q, order, offset, allow_backorder, manage_inventory } = + useQueryParams( + ["q", "order", "offset", "manage_inventory", "allow_backorder"], + PREFIX + ) const columns = useColumns(product) const filters = useFilters() @@ -77,10 +62,8 @@ export const ProductVariantSection = ({ manage_inventory: manage_inventory ? JSON.parse(manage_inventory) : undefined, - created_at: created_at ? JSON.parse(created_at) : undefined, - updated_at: updated_at ? JSON.parse(updated_at) : undefined, fields: - "title,sku,*options,created_at,updated_at,*inventory_items.inventory.location_levels,inventory_quantity,manage_inventory", + "title,sku,thumbnail,*options,created_at,*inventory_items.inventory.location_levels,inventory_quantity,manage_inventory", }, { placeholderData: keepPreviousData, @@ -163,8 +146,6 @@ const useColumns = (product: HttpTypes.AdminProduct) => { return filtered }, [searchParams]) - const dateColumns = useDataTableDateColumns() - const handleDelete = useCallback( async (id: string, title: string) => { const res = await prompt({ @@ -349,6 +330,18 @@ const useColumns = (product: HttpTypes.AdminProduct) => { return useMemo(() => { return [ + columnHelper.accessor("thumbnail", { + header: "", + headerAlign: "center", + maxSize: 72, + cell: ({ row }) => { + return ( +
+ +
+ ) + }, + }), columnHelper.accessor("title", { header: t("fields.title"), enableSorting: true, @@ -387,12 +380,11 @@ const useColumns = (product: HttpTypes.AdminProduct) => { }, maxSize: 250, }), - ...dateColumns, columnHelper.action({ actions: getActions, }), ] - }, [t, optionColumns, dateColumns, getActions, getInventory]) + }, [t, optionColumns, getActions, getInventory]) } const filterHelper =