From bf258abf4ff90a4609039799c0a99c8bf08b742a Mon Sep 17 00:00:00 2001 From: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com> Date: Mon, 29 Jul 2024 18:30:43 +0200 Subject: [PATCH] minor fixes to product create form --- .../dashboard/src/i18n/translations/en.json | 4 +- .../upload-media-form-item.tsx | 16 +- .../product-create-general-section.tsx | 2 +- .../product-create-details-media-section.tsx | 211 +++++++++++----- ...product-create-details-variant-section.tsx | 225 +++++++++--------- .../product-create-details-form.tsx | 9 +- .../edit-product-media-form.tsx | 2 +- 7 files changed, 296 insertions(+), 173 deletions(-) diff --git a/packages/admin-next/dashboard/src/i18n/translations/en.json b/packages/admin-next/dashboard/src/i18n/translations/en.json index fcf93644d06b7..6bbb63f54536f 100644 --- a/packages/admin-next/dashboard/src/i18n/translations/en.json +++ b/packages/admin-next/dashboard/src/i18n/translations/en.json @@ -321,7 +321,8 @@ "productVariants": { "label": "Product variants", "hint": "This ranking will affect the variants' order in your storefront.", - "alert": "Add options to create variants." + "alert": "Add options to create variants.", + "tip": "Variants left unchecked won't be created. You can always create and edit variants afterwards but this list fits the variations in your product options." }, "productOptions": { "label": "Product options", @@ -340,6 +341,7 @@ "media": { "label": "Media", "editHint": "Add media to the product to showcase it in your storefront.", + "makeThumbnail": "Make thumbnail", "uploadImagesLabel": "Upload images", "uploadImagesHint": "Drag and drop images here or click to upload.", "invalidFileType": "'{{name}}' is not a supported file type. Supported file types are: {{types}}.", diff --git a/packages/admin-next/dashboard/src/routes/products/common/components/upload-media-form-item/upload-media-form-item.tsx b/packages/admin-next/dashboard/src/routes/products/common/components/upload-media-form-item/upload-media-form-item.tsx index 0d8f08d657c1e..883ef812e2e58 100644 --- a/packages/admin-next/dashboard/src/routes/products/common/components/upload-media-form-item/upload-media-form-item.tsx +++ b/packages/admin-next/dashboard/src/routes/products/common/components/upload-media-form-item/upload-media-form-item.tsx @@ -1,16 +1,16 @@ +import { UseFormReturn } from "react-hook-form" import { useTranslation } from "react-i18next" -import { Form } from "../../../../../components/common/form" +import { z } from "zod" import { FileType, FileUpload, } from "../../../../../components/common/file-upload" -import { UseFormReturn } from "react-hook-form" +import { Form } from "../../../../../components/common/form" +import { MediaSchema } from "../../../product-create/constants" import { EditProductMediaSchemaType, ProductCreateSchemaType, } from "../../../product-create/types" -import { MediaSchema } from "../../../product-create/constants" -import { z } from "zod" type Media = z.infer @@ -35,11 +35,13 @@ const SUPPORTED_FORMATS_FILE_EXTENSIONS = [ export const UploadMediaFormItem = ({ form, append, + showHint = true, }: { form: | UseFormReturn | UseFormReturn append: (value: Media) => void + showHint?: boolean }) => { const { t } = useTranslation() @@ -72,10 +74,12 @@ export const UploadMediaFormItem = ({ render={() => { return ( -
+
{t("products.media.label")} - {t("products.media.editHint")} + {showHint && ( + {t("products.media.editHint")} + )}
+
@@ -13,68 +14,172 @@ type ProductCreateMediaSectionProps = { export const ProductCreateMediaSection = ({ form, }: ProductCreateMediaSectionProps) => { - const { t } = useTranslation() - const [selection, setSelection] = useState>({}) - const selectionCount = Object.keys(selection).length - const { fields, append, remove } = useFieldArray({ name: "media", control: form.control, keyName: "field_id", }) - const handleDelete = () => { - const ids = Object.keys(selection) - const indices = ids.map((id) => fields.findIndex((m) => m.id === id)) - - remove(indices) - setSelection({}) + const getOnDelete = (index: number) => { + return () => { + remove(index) + } } - const handleCheckedChange = useCallback( - (id: string) => { - return (val: boolean) => { - if (!val) { - const { [id]: _, ...rest } = selection - setSelection(rest) - } else { - setSelection((prev) => ({ ...prev, [id]: true })) + const getMakeThumbnail = (index: number) => { + return () => { + const newFields = fields.map((field, i) => { + return { + ...field, + isThumbnail: i === index, } - } - }, - [selection] + }) + + form.setValue("media", newFields, { + shouldDirty: true, + shouldTouch: true, + }) + } + } + + const getItemHandlers = (index: number) => { + return { + onDelete: getOnDelete(index), + onMakeThumbnail: getMakeThumbnail(index), + } + } + + return ( +
+ +
    + {fields.map((field, index) => { + const { onDelete, onMakeThumbnail } = getItemHandlers(index) + + return ( + + ) + })} +
+
) +} + +type MediaField = { + isThumbnail: boolean + url: string + id?: string | undefined + file?: File + field_id: string +} + +type MediaItemProps = { + field: MediaField + onDelete: () => void + onMakeThumbnail: () => void +} + +const MediaItem = ({ field, onDelete, onMakeThumbnail }: MediaItemProps) => { + const { t } = useTranslation() + + if (!field.file) { + return null + } return ( -
- {t("products.media.label")} -
- +
  • +
    +
    + +
    +
    + + {field.file.name} + +
    + {field.isThumbnail && } + + {formatFileSize(field.file.size)} + +
    +
    - {fields?.length ? ( - + , + onClick: onMakeThumbnail, + }, + ], + }, + { + actions: [ + { + icon: , + label: t("actions.delete"), + onClick: onDelete, + }, + ], + }, + ]} /> - ) : null} - - - - - {t("general.countSelected", { - count: selectionCount, - })} - - - - - - -
  • + + + +
    + + ) +} + +const ThumbnailPreview = ({ file }: { file?: File | null }) => { + const [thumbnailUrl, setThumbnailUrl] = useState(null) + + useEffect(() => { + if (file) { + const objectUrl = URL.createObjectURL(file) + setThumbnailUrl(objectUrl) + + return () => URL.revokeObjectURL(objectUrl) + } + }, [file]) + + if (!thumbnailUrl) { + return null + } + + return ( + + ) +} + +function formatFileSize(bytes: number, decimalPlaces: number = 2): string { + if (bytes === 0) { + return "0 Bytes" + } + + const k = 1024 + const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + + return ( + parseFloat((bytes / Math.pow(k, i)).toFixed(decimalPlaces)) + " " + sizes[i] ) } diff --git a/packages/admin-next/dashboard/src/routes/products/product-create/components/product-create-details-form/components/product-create-details-variant-section/product-create-details-variant-section.tsx b/packages/admin-next/dashboard/src/routes/products/product-create/components/product-create-details-form/components/product-create-details-variant-section/product-create-details-variant-section.tsx index be1dd439395d0..cd18dbf7b25a9 100644 --- a/packages/admin-next/dashboard/src/routes/products/product-create/components/product-create-details-form/components/product-create-details-variant-section/product-create-details-variant-section.tsx +++ b/packages/admin-next/dashboard/src/routes/products/product-create/components/product-create-details-form/components/product-create-details-variant-section/product-create-details-variant-section.tsx @@ -8,7 +8,6 @@ import { IconButton, Input, Label, - Switch, Text, clx, } from "@medusajs/ui" @@ -22,6 +21,7 @@ import { import { useTranslation } from "react-i18next" import { Form } from "../../../../../../../components/common/form" +import { InlineTip } from "../../../../../../../components/common/inline-tip" import { SortableList } from "../../../../../../../components/common/sortable-list" import { SwitchBox } from "../../../../../../../components/common/switch-box" import { ChipInput } from "../../../../../../../components/inputs/chip-input" @@ -285,26 +285,28 @@ export const ProductCreateVariantsSection = ({ return (
    - {t("products.create.variants.header")} - { - if (checked) { - form.setValue("options", [ - { - title: "", - values: [], - }, - ]) - form.setValue("variants", []) - } else { - createDefaultOptionAndVariant() - } - }} - /> +
    + {t("products.create.variants.header")} + { + if (checked) { + form.setValue("options", [ + { + title: "", + values: [], + }, + ]) + form.setValue("variants", []) + } else { + createDefaultOptionAndVariant() + } + }} + /> +
    {watchedAreVariantsEnabled && ( <>
    @@ -314,7 +316,7 @@ export const ProductCreateVariantsSection = ({ render={() => { return ( -
    +
    @@ -426,94 +428,103 @@ export const ProductCreateVariantsSection = ({ }} />
    -
    -
    - - {t("products.create.variants.productVariants.hint")} -
    - {!showInvalidOptionsMessage && showInvalidVariantsMessage && ( - - {t("products.create.errors.variants")} - - )} - {variants.fields.length > 0 ? ( -
    -
    -
    - -
    -
    - {watchedOptions.map((option, index) => ( -
    - - {option.title} - +
    +
    +
    + + + {t("products.create.variants.productVariants.hint")} + +
    + {!showInvalidOptionsMessage && showInvalidVariantsMessage && ( + + {t("products.create.errors.variants")} + + )} + {variants.fields.length > 0 ? ( +
    +
    +
    +
    - ))} -
    - { - return ( - -
    + {watchedOptions.map((option, index) => ( +
    + + {option.title} + +
    + ))} +
    + { + return ( + - { - return ( - - - - - - ) +
    - - {Object.values(item.options).map((value, index) => ( - - {value} - - ))} -
    -
    - ) - }} - /> -
    - ) : ( - - {t("products.create.variants.productVariants.alert")} - - )} + > + { + return ( + + + + + + ) + }} + /> + + {Object.values(item.options).map((value, index) => ( + + {value} + + ))} +
    + + ) + }} + /> +
    + ) : ( + + {t("products.create.variants.productVariants.alert")} + + )} + {variants.fields.length > 0 && ( + + {t("products.create.variants.productVariants.tip")} + + )} +
    )} diff --git a/packages/admin-next/dashboard/src/routes/products/product-create/components/product-create-details-form/product-create-details-form.tsx b/packages/admin-next/dashboard/src/routes/products/product-create/components/product-create-details-form/product-create-details-form.tsx index 6f7afe4483eff..95aa14c41728c 100644 --- a/packages/admin-next/dashboard/src/routes/products/product-create/components/product-create-details-form/product-create-details-form.tsx +++ b/packages/admin-next/dashboard/src/routes/products/product-create/components/product-create-details-form/product-create-details-form.tsx @@ -5,8 +5,8 @@ import { useTranslation } from "react-i18next" import { Divider } from "../../../../../components/common/divider" import { ProductCreateSchemaType } from "../../types" import { ProductCreateGeneralSection } from "./components/product-create-details-general-section" -import { ProductCreateVariantsSection } from "./components/product-create-details-variant-section" import { ProductCreateMediaSection } from "./components/product-create-details-media-section" +import { ProductCreateVariantsSection } from "./components/product-create-details-variant-section" type ProductAttributesProps = { form: UseFormReturn @@ -17,11 +17,12 @@ export const ProductCreateDetailsForm = ({ form }: ProductAttributesProps) => {
    - +
    + + +
    - -
    ) diff --git a/packages/admin-next/dashboard/src/routes/products/product-media/components/edit-product-media-form/edit-product-media-form.tsx b/packages/admin-next/dashboard/src/routes/products/product-media/components/edit-product-media-form/edit-product-media-form.tsx index 1e3955d8032e6..1ac331cc332b6 100644 --- a/packages/admin-next/dashboard/src/routes/products/product-media/components/edit-product-media-form/edit-product-media-form.tsx +++ b/packages/admin-next/dashboard/src/routes/products/product-media/components/edit-product-media-form/edit-product-media-form.tsx @@ -187,7 +187,7 @@ export const EditProductMediaForm = ({ product }: ProductMediaViewProps) => {