From 06b2f0a8dcce179983205baf80cf74114cc2c359 Mon Sep 17 00:00:00 2001 From: Stevche Radevski Date: Thu, 4 Apr 2024 12:27:33 +0200 Subject: [PATCH] feat: Add some of the missing pieces in the create product form (#6918) The changes are still partial, there is more work to be done to have everything function properly. Also, refactored the file upload from the product media form to a separate component --- .../public/locales/en-US/translation.json | 61 ++- .../common/file-upload/file-upload.tsx | 135 +++++++ .../components/common/file-upload/index.ts | 1 + .../router-provider/router-provider.tsx | 2 +- .../create-product-details.tsx | 352 +++++++++++++++--- .../create-product-form.tsx | 28 +- .../edit-product-form/edit-product-form.tsx | 4 +- .../edit-product-media-form.tsx | 184 ++------- 8 files changed, 545 insertions(+), 222 deletions(-) create mode 100644 packages/admin-next/dashboard/src/components/common/file-upload/file-upload.tsx create mode 100644 packages/admin-next/dashboard/src/components/common/file-upload/index.ts diff --git a/packages/admin-next/dashboard/public/locales/en-US/translation.json b/packages/admin-next/dashboard/public/locales/en-US/translation.json index e5b8b9b75d..d5eeb6458b 100644 --- a/packages/admin-next/dashboard/public/locales/en-US/translation.json +++ b/packages/admin-next/dashboard/public/locales/en-US/translation.json @@ -55,6 +55,7 @@ }, "actions": { "save": "Save", + "saveAsDraft": "Save as draft", "create": "Create", "delete": "Delete", "remove": "Remove", @@ -151,7 +152,7 @@ "attributes": "Attributes", "editProduct": "Edit Product", "editAttributes": "Edit Attributes", - "organization": "Organization", + "organization": "Organize", "editOrganization": "Edit Organization", "options": "Options", "editOptions": "Edit Options", @@ -171,10 +172,7 @@ "deleteImageLabel": "Delete current image", "noMediaLabel": "The product has no associated media." }, - "titleHint": "Give your product a short and clear title.<0/>50-60 characters is the recommended length for search engines.", - "descriptionHint": "Give your product a short and clear description.<0/>120-160 characters is the recommended length for search engines.", "discountableHint": "When unchecked discounts will not be applied to this product.", - "handleTooltip": "The handle is used to reference the product in your storefront. If not specified, the handle will be generated from the product title.", "availableInSalesChannels": "Available in <0>{{x}} of <1>{{y}} sales channels", "noSalesChannels": "Not available in any sales channels", "variantCount_one": "{{count}} variant", @@ -186,6 +184,61 @@ "proposed": "Proposed", "rejected": "Rejected" }, + "fields": { + "title": { + "label": "Title", + "hint": "Give your product a short and clear title.<0/>50-60 characters is the recommended length for search engines." + }, + "subtitle": { + "label": "Subtitle" + }, + "handle": { + "label": "Handle", + "tooltip": "The handle is used to reference the product in your storefront. If not specified, the handle will be generated from the product title." + }, + "description": { + "label": "Description", + "hint": "Give your product a short and clear description.<0/>120-160 characters is the recommended length for search engines." + }, + "discountable": { + "label": "Discountable", + "hint": "When unchecked discounts will not be applied to this product" + }, + "type": { + "label": "Type" + }, + "collection": { + "label": "Collection" + }, + "categories": { + "label": "Categories" + }, + "tags": { + "label": "Tags" + }, + "sales_channels": { + "label": "Sales channels", + "hint": "This product will only be available in the default sales channel if left untouched" + }, + "countryOrigin": { + "label": "Country of origin" + }, + "material": { + "label": "Material" + }, + "width": { + "label": "Width" + }, + "length": { + "label": "Length" + }, + "height": { + "label": "Height" + }, + "weight": { + "label": "Weight" + } + }, "variant": { "edit": { "header": "Edit Variant" diff --git a/packages/admin-next/dashboard/src/components/common/file-upload/file-upload.tsx b/packages/admin-next/dashboard/src/components/common/file-upload/file-upload.tsx new file mode 100644 index 0000000000..f1b2ab6947 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/file-upload/file-upload.tsx @@ -0,0 +1,135 @@ +import { ArrowDownTray } from "@medusajs/icons" +import { Text, clx } from "@medusajs/ui" +import { ChangeEvent, DragEvent, useRef, useState } from "react" + +export interface FileType { + id: string + url: string + file: File +} + +export interface FileUploadProps { + label: string + hint?: string + hasError?: boolean + formats: string[] + onUploaded: (files: FileType[]) => void +} + +export const FileUpload = ({ + label, + hint, + hasError, + formats, + onUploaded, +}: FileUploadProps) => { + const [isDragOver, setIsDragOver] = useState(false) + const inputRef = useRef(null) + const dropZoneRef = useRef(null) + + const handleOpenFileSelector = () => { + inputRef.current?.click() + } + + const handleDragEnter = (event: DragEvent) => { + event.preventDefault() + event.stopPropagation() + + const files = event.dataTransfer?.files + if (!files) { + return + } + + setIsDragOver(true) + } + + const handleDragLeave = (event: DragEvent) => { + event.preventDefault() + event.stopPropagation() + + if ( + !dropZoneRef.current || + dropZoneRef.current.contains(event.relatedTarget as Node) + ) { + return + } + + setIsDragOver(false) + } + + const handleUploaded = (files: FileList | null) => { + if (!files) { + return + } + + const fileList = Array.from(files) + const fileObj = fileList.map((file) => { + const previewUrl = URL.createObjectURL(file) + return { + id: crypto.randomUUID(), + url: previewUrl, + file, + } + }) + + onUploaded(fileObj) + } + + const handleDrop = (event: DragEvent) => { + event.preventDefault() + event.stopPropagation() + + setIsDragOver(false) + + handleUploaded(event.dataTransfer?.files) + } + + const handleFileChange = async (event: ChangeEvent) => { + handleUploaded(event.target.files) + } + + return ( +
+ + +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/common/file-upload/index.ts b/packages/admin-next/dashboard/src/components/common/file-upload/index.ts new file mode 100644 index 0000000000..622ec7e684 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/file-upload/index.ts @@ -0,0 +1 @@ +export * from "./file-upload" diff --git a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx index ed6899ce29..da09efa75f 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx @@ -6,7 +6,7 @@ import { import { v1Routes } from "./v1" import { v2Routes } from "./v2" -const V2_ENABLED = import.meta.env.VITE_MEDUSA_V2 || false +const V2_ENABLED = import.meta.env.VITE_MEDUSA_V2 === "true" const router = createBrowserRouter(V2_ENABLED ? v2Routes : v1Routes) diff --git a/packages/admin-next/dashboard/src/routes/products/product-create/components/create-product-form/create-product-details.tsx b/packages/admin-next/dashboard/src/routes/products/product-create/components/create-product-form/create-product-details.tsx index dd6a84c1ab..d0f3163e11 100644 --- a/packages/admin-next/dashboard/src/routes/products/product-create/components/create-product-form/create-product-details.tsx +++ b/packages/admin-next/dashboard/src/routes/products/product-create/components/create-product-form/create-product-details.tsx @@ -1,10 +1,25 @@ -import { Button, Checkbox, Heading, Input, Text, Textarea } from "@medusajs/ui" +import { + Button, + Checkbox, + Heading, + Input, + Select, + Switch, + Text, + Textarea, +} from "@medusajs/ui" import { Trans, useTranslation } from "react-i18next" import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels" import { SalesChannel } from "@medusajs/medusa" import { RowSelectionState, createColumnHelper } from "@tanstack/react-table" -import { useAdminSalesChannels } from "medusa-react" +import { + useAdminCollections, + useAdminProductCategories, + useAdminProductTags, + useAdminProductTypes, + useAdminSalesChannels, +} from "medusa-react" import { Fragment, useMemo, useState } from "react" import { CountrySelect } from "../../../../../components/common/country-select" import { Form } from "../../../../../components/common/form" @@ -15,14 +30,38 @@ import { useSalesChannelTableFilters } from "../../../../../hooks/table/filters/ import { useSalesChannelTableQuery } from "../../../../../hooks/table/query/use-sales-channel-table-query" import { useDataTable } from "../../../../../hooks/use-data-table" import { CreateProductFormReturn } from "./create-product-form" +import { Combobox } from "../../../../../components/common/combobox" +import { FileUpload } from "../../../../../components/common/file-upload" type CreateProductPropsProps = { form: CreateProductFormReturn } +const SUPPORTED_FORMATS = [ + "image/jpeg", + "image/png", + "image/gif", + "image/webp", + "image/heic", + "image/svg+xml", +] + export const CreateProductDetails = ({ form }: CreateProductPropsProps) => { const { t } = useTranslation() const [open, onOpenChange] = useState(false) + const { product_types, isLoading: isLoadingTypes } = useAdminProductTypes() + const { product_tags, isLoading: isLoadingTags } = useAdminProductTags() + const { collections, isLoading: isLoadingCollections } = useAdminCollections() + const { sales_channels, isLoading: isLoadingSalesChannels } = + useAdminSalesChannels() + const { product_categories, isLoading: isLoadingCategories } = + useAdminProductCategories() + + // const { append } = useFieldArray({ + // name: "images", + // control: form.control, + // // keyName: "field_id", + // }) return ( { render={({ field }) => { return ( - {t("fields.title")} + + {t("products.fields.title.label")} + @@ -66,7 +107,7 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => { return ( - {t("fields.subtitle")} + {t("products.fields.subtitle.label")} @@ -76,17 +117,13 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => { }} /> - + ]} /> - +
{ return ( {t("fields.handle")} @@ -108,22 +145,6 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => { ) }} /> - { - return ( - - - {t("fields.material")} - - - - - - ) - }} - />
{ return ( - {t("fields.description")} + {t("products.fields.description.label")}