feat(dashboard): variant images list thumbnail + refactor form state management (#13905)
## 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
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -51,24 +51,9 @@ export const EditProductVariantMediaForm = ({
|
||||
(image) => !image.variants?.some((variant) => variant.id === variant.id)
|
||||
)
|
||||
|
||||
const [variantImages, setVariantImages] = useState<Record<string, true>>(() =>
|
||||
allVariantImages.reduce(
|
||||
// @eslint-disable-next-line
|
||||
(acc: Record<string, true>, image) => {
|
||||
acc[image.id] = true
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
)
|
||||
|
||||
const [selection, setSelection] = useState<Record<string, true>>({})
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false)
|
||||
|
||||
const availableImages = unassociatedImages.filter(
|
||||
(image) => !variantImages[image.id!]
|
||||
)
|
||||
|
||||
const form = useForm<MediaSchemaType>({
|
||||
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 = ({
|
||||
</div>
|
||||
<div className="grid h-fit auto-rows-auto grid-cols-2 gap-4 p-4 sm:grid-cols-3 lg:grid-cols-6 lg:gap-6 lg:p-6">
|
||||
{allProductImages
|
||||
.filter((image) => variantImages[image.id!])
|
||||
.filter((image) => formImageIds.includes(image.id!))
|
||||
.map((image) => (
|
||||
<MediaGridItem
|
||||
key={image.id}
|
||||
@@ -238,7 +231,7 @@ export const EditProductVariantMediaForm = ({
|
||||
<h3 className="ui-fg-base ">
|
||||
{t("products.media.availableImages")}
|
||||
</h3>
|
||||
<p className="text-ui-fg-dimmed mt-1 text-sm">
|
||||
<p className="text-ui-fg-muted mt-1 text-sm">
|
||||
{t("products.media.selectToAdd")}
|
||||
</p>
|
||||
</div>
|
||||
@@ -272,7 +265,7 @@ export const EditProductVariantMediaForm = ({
|
||||
<h3 className="ui-fg-base text-sm font-medium">
|
||||
{t("products.media.availableImages")}
|
||||
</h3>
|
||||
<p className="ui-fg-muted mt-1 text-xs">
|
||||
<p className="text-ui-fg-muted mt-1 pr-2 text-xs">
|
||||
{t("products.media.selectToAdd")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<HttpTypes.AdminProductVariant>()
|
||||
|
||||
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 (
|
||||
<div className="flex items-center pl-[1px]">
|
||||
<Thumbnail src={row.original.thumbnail} />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}),
|
||||
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 =
|
||||
|
||||
Reference in New Issue
Block a user