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
This commit is contained in:
1 parent
483bf98a49
commit
06b2f0a8dc
8 files changed
+545
-222
No files matched your search
@@ -55,6 +55,7 @@
|
|||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
|
"saveAsDraft": "Save as draft",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
@@ -151,7 +152,7 @@
|
|||||||
"attributes": "Attributes",
|
"attributes": "Attributes",
|
||||||
"editProduct": "Edit Product",
|
"editProduct": "Edit Product",
|
||||||
"editAttributes": "Edit Attributes",
|
"editAttributes": "Edit Attributes",
|
||||||
"organization": "Organization",
|
"organization": "Organize",
|
||||||
"editOrganization": "Edit Organization",
|
"editOrganization": "Edit Organization",
|
||||||
"options": "Options",
|
"options": "Options",
|
||||||
"editOptions": "Edit Options",
|
"editOptions": "Edit Options",
|
||||||
@@ -171,10 +172,7 @@
|
|||||||
"deleteImageLabel": "Delete current image",
|
"deleteImageLabel": "Delete current image",
|
||||||
"noMediaLabel": "The product has no associated media."
|
"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.",
|
"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}}</0> of <1>{{y}}</1> sales channels",
|
"availableInSalesChannels": "Available in <0>{{x}}</0> of <1>{{y}}</1> sales channels",
|
||||||
"noSalesChannels": "Not available in any sales channels",
|
"noSalesChannels": "Not available in any sales channels",
|
||||||
"variantCount_one": "{{count}} variant",
|
"variantCount_one": "{{count}} variant",
|
||||||
@@ -186,6 +184,61 @@
|
|||||||
"proposed": "Proposed",
|
"proposed": "Proposed",
|
||||||
"rejected": "Rejected"
|
"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": {
|
"variant": {
|
||||||
"edit": {
|
"edit": {
|
||||||
"header": "Edit Variant"
|
"header": "Edit Variant"
|
||||||
|
|||||||
@@ -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<boolean>(false)
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const dropZoneRef = useRef<HTMLButtonElement>(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<HTMLInputElement>) => {
|
||||||
|
handleUploaded(event.target.files)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
ref={dropZoneRef}
|
||||||
|
type="button"
|
||||||
|
onClick={handleOpenFileSelector}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onDragOver={(e) => e.preventDefault()}
|
||||||
|
onDragEnter={handleDragEnter}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
className={clx(
|
||||||
|
"bg-ui-bg-component border-ui-border-strong transition-fg group flex w-full flex-col items-center gap-y-2 rounded-lg border border-dashed p-8",
|
||||||
|
"hover:border-ui-border-interactive focus:border-ui-border-interactive",
|
||||||
|
"focus:shadow-borders-focus outline-none focus:border-solid",
|
||||||
|
{
|
||||||
|
"!border-ui-border-error": hasError,
|
||||||
|
"!border-ui-border-interactive": isDragOver,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="text-ui-fg-subtle group-disabled:text-ui-fg-disabled flex items-center gap-x-2">
|
||||||
|
<ArrowDownTray />
|
||||||
|
<Text>{label}</Text>
|
||||||
|
</div>
|
||||||
|
{!!hint && (
|
||||||
|
<Text
|
||||||
|
size="small"
|
||||||
|
leading="compact"
|
||||||
|
className="text-ui-fg-muted group-disabled:text-ui-fg-disabled"
|
||||||
|
>
|
||||||
|
{hint}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
hidden
|
||||||
|
ref={inputRef}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
type="file"
|
||||||
|
accept={formats.join(",")}
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./file-upload"
|
||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
import { v1Routes } from "./v1"
|
import { v1Routes } from "./v1"
|
||||||
import { v2Routes } from "./v2"
|
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)
|
const router = createBrowserRouter(V2_ENABLED ? v2Routes : v1Routes)
|
||||||
|
|
||||||
|
|||||||
+308
-44
@@ -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 { Trans, useTranslation } from "react-i18next"
|
||||||
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"
|
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"
|
||||||
|
|
||||||
import { SalesChannel } from "@medusajs/medusa"
|
import { SalesChannel } from "@medusajs/medusa"
|
||||||
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
|
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 { Fragment, useMemo, useState } from "react"
|
||||||
import { CountrySelect } from "../../../../../components/common/country-select"
|
import { CountrySelect } from "../../../../../components/common/country-select"
|
||||||
import { Form } from "../../../../../components/common/form"
|
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 { useSalesChannelTableQuery } from "../../../../../hooks/table/query/use-sales-channel-table-query"
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
import { CreateProductFormReturn } from "./create-product-form"
|
import { CreateProductFormReturn } from "./create-product-form"
|
||||||
|
import { Combobox } from "../../../../../components/common/combobox"
|
||||||
|
import { FileUpload } from "../../../../../components/common/file-upload"
|
||||||
|
|
||||||
type CreateProductPropsProps = {
|
type CreateProductPropsProps = {
|
||||||
form: CreateProductFormReturn
|
form: CreateProductFormReturn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SUPPORTED_FORMATS = [
|
||||||
|
"image/jpeg",
|
||||||
|
"image/png",
|
||||||
|
"image/gif",
|
||||||
|
"image/webp",
|
||||||
|
"image/heic",
|
||||||
|
"image/svg+xml",
|
||||||
|
]
|
||||||
|
|
||||||
export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
|
export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [open, onOpenChange] = useState(false)
|
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 (
|
return (
|
||||||
<PanelGroup
|
<PanelGroup
|
||||||
@@ -51,7 +90,9 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Form.Label>{t("fields.title")}</Form.Label>
|
<Form.Label>
|
||||||
|
{t("products.fields.title.label")}
|
||||||
|
</Form.Label>
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
@@ -66,7 +107,7 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
|
|||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Form.Label optional>
|
<Form.Label optional>
|
||||||
{t("fields.subtitle")}
|
{t("products.fields.subtitle.label")}
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
@@ -76,17 +117,13 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Text
|
<Form.Hint>
|
||||||
size="small"
|
|
||||||
leading="compact"
|
|
||||||
className="text-ui-fg-subtle"
|
|
||||||
>
|
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="products.titleHint"
|
i18nKey="products.fields.title.hint"
|
||||||
t={t}
|
t={t}
|
||||||
components={[<br key="break" />]}
|
components={[<br key="break" />]}
|
||||||
/>
|
/>
|
||||||
</Text>
|
</Form.Hint>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-x-4">
|
<div className="grid grid-cols-2 gap-x-4">
|
||||||
<Form.Field
|
<Form.Field
|
||||||
@@ -96,7 +133,7 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
|
|||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Form.Label
|
<Form.Label
|
||||||
tooltip={t("products.handleTooltip")}
|
tooltip={t("products.fields.handle.tooltip")}
|
||||||
optional
|
optional
|
||||||
>
|
>
|
||||||
{t("fields.handle")}
|
{t("fields.handle")}
|
||||||
@@ -108,22 +145,6 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="material"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label optional>
|
|
||||||
{t("fields.material")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Input {...field} />
|
|
||||||
</Form.Control>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -132,14 +153,14 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
|
|||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Form.Label optional>
|
<Form.Label optional>
|
||||||
{t("fields.description")}
|
{t("products.fields.description.label")}
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<Textarea {...field} />
|
<Textarea {...field} />
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
<Form.Hint>
|
<Form.Hint>
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey={"products.descriptionHint"}
|
i18nKey={"products.fields.description.hint"}
|
||||||
components={[<br key="break" />]}
|
components={[<br key="break" />]}
|
||||||
/>
|
/>
|
||||||
</Form.Hint>
|
</Form.Hint>
|
||||||
@@ -150,16 +171,224 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div id="organize" className="flex flex-col gap-y-8">
|
<div id="organize" className="flex flex-col gap-y-8">
|
||||||
<Heading level="h2">{t("products.organization")}</Heading>
|
<Heading level="h2">{t("products.organization")}</Heading>
|
||||||
<Button
|
<div className="grid grid-cols-1 gap-x-4">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="discountable"
|
||||||
|
render={({ field: { value, onChange, ...field } }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Form.Label optional>
|
||||||
|
{t("products.fields.discountable.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Switch
|
||||||
|
{...field}
|
||||||
|
checked={!!value}
|
||||||
|
onCheckedChange={onChange}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Hint>
|
||||||
|
<Trans i18nKey={"products.fields.discountable.hint"} />
|
||||||
|
</Form.Hint>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-x-4">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="type_id"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>
|
||||||
|
{t("products.fields.type.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Select disabled={isLoadingTypes} {...field}>
|
||||||
|
<Select.Trigger ref={field.ref}>
|
||||||
|
<Select.Value />
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
{(product_types ?? []).map((type) => (
|
||||||
|
<Select.Item key={type.id} value={type.id}>
|
||||||
|
{type.value}
|
||||||
|
</Select.Item>
|
||||||
|
))}
|
||||||
|
</Select.Content>
|
||||||
|
</Select>
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="collection_id"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>
|
||||||
|
{t("products.fields.collection.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Select disabled={isLoadingCollections} {...field}>
|
||||||
|
<Select.Trigger ref={field.ref}>
|
||||||
|
<Select.Value />
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
{(collections ?? []).map((collection) => (
|
||||||
|
<Select.Item
|
||||||
|
key={collection.id}
|
||||||
|
value={collection.id}
|
||||||
|
>
|
||||||
|
{collection.title}
|
||||||
|
</Select.Item>
|
||||||
|
))}
|
||||||
|
</Select.Content>
|
||||||
|
</Select>
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-x-4">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="category_ids"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>
|
||||||
|
{t("products.fields.categories.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Combobox
|
||||||
|
disabled={isLoadingCategories}
|
||||||
|
options={(product_categories ?? []).map(
|
||||||
|
(category) => ({
|
||||||
|
label: category.name,
|
||||||
|
value: category.id,
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="tags"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>
|
||||||
|
{t("products.fields.tags.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Combobox
|
||||||
|
disabled={isLoadingTags}
|
||||||
|
options={(product_tags ?? []).map((tag) => ({
|
||||||
|
label: tag.value,
|
||||||
|
value: tag.id,
|
||||||
|
}))}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* TODO: Align to match designs */}
|
||||||
|
<div className="grid grid-cols-1 gap-x-4">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="sales_channels"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>
|
||||||
|
{t("products.fields.sales_channels.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Hint>
|
||||||
|
<Trans
|
||||||
|
i18nKey={"products.fields.sales_channels.hint"}
|
||||||
|
/>
|
||||||
|
</Form.Hint>
|
||||||
|
<Form.Control>
|
||||||
|
<Combobox
|
||||||
|
disabled={isLoadingSalesChannels}
|
||||||
|
options={(sales_channels ?? []).map(
|
||||||
|
(salesChannel) => ({
|
||||||
|
label: salesChannel.name,
|
||||||
|
value: salesChannel.id,
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* <Button
|
||||||
size="small"
|
size="small"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => onOpenChange(!open)}
|
onClick={() => onOpenChange(!open)}
|
||||||
>
|
>
|
||||||
{t("actions.edit")}
|
{t("actions.edit")}
|
||||||
</Button>
|
</Button> */}
|
||||||
</div>
|
</div>
|
||||||
|
<div id="variants" className="flex flex-col gap-y-8">
|
||||||
|
<Heading level="h2">{t("products.variants")}</Heading>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="attributes" className="flex flex-col gap-y-8">
|
<div id="attributes" className="flex flex-col gap-y-8">
|
||||||
<Heading level="h2">{t("products.attributes")}</Heading>
|
<Heading level="h2">{t("products.attributes")}</Heading>
|
||||||
|
<div className="grid grid-cols-2 gap-x-4 gap-y-8">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="origin_country"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>
|
||||||
|
{t("products.fields.countryOrigin.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<CountrySelect {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="material"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>
|
||||||
|
{t("products.fields.material.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-x-4 gap-y-8">
|
<div className="grid grid-cols-2 gap-x-4 gap-y-8">
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -167,7 +396,9 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Form.Label optional>{t("fields.width")}</Form.Label>
|
<Form.Label optional>
|
||||||
|
{t("products.fields.width.label")}
|
||||||
|
</Form.Label>
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<Input {...field} type="number" min={0} />
|
<Input {...field} type="number" min={0} />
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
@@ -181,7 +412,9 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Form.Label optional>{t("fields.length")}</Form.Label>
|
<Form.Label optional>
|
||||||
|
{t("products.fields.length.label")}
|
||||||
|
</Form.Label>
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<Input {...field} type="number" min={0} />
|
<Input {...field} type="number" min={0} />
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
@@ -195,7 +428,9 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Form.Label optional>{t("fields.height")}</Form.Label>
|
<Form.Label optional>
|
||||||
|
{t("products.fields.height.label")}
|
||||||
|
</Form.Label>
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<Input {...field} type="number" min={0} />
|
<Input {...field} type="number" min={0} />
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
@@ -209,7 +444,9 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Form.Label optional>{t("fields.weight")}</Form.Label>
|
<Form.Label optional>
|
||||||
|
{t("products.fields.weight.label")}
|
||||||
|
</Form.Label>
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<Input {...field} type="number" min={0} />
|
<Input {...field} type="number" min={0} />
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
@@ -217,18 +454,45 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
{/* TODO: Add missing attribute fields */}
|
||||||
|
</div>
|
||||||
|
<div id="media" className="flex flex-col gap-y-8">
|
||||||
|
<Heading level="h2">{t("products.media.label")}</Heading>
|
||||||
|
<div className="grid grid-cols-1 gap-x-4 gap-y-8">
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="origin_country"
|
name="images"
|
||||||
render={({ field }) => {
|
render={() => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Form.Label optional>
|
<div className="flex flex-col gap-y-4">
|
||||||
{t("fields.countryOfOrigin")}
|
<div className="flex flex-col gap-y-1">
|
||||||
</Form.Label>
|
<Form.Label optional>
|
||||||
<Form.Control>
|
{t("products.media.label")}
|
||||||
<CountrySelect {...field} />
|
</Form.Label>
|
||||||
</Form.Control>
|
<Form.Hint>
|
||||||
|
{t("products.media.editHint")}
|
||||||
|
</Form.Hint>
|
||||||
|
</div>
|
||||||
|
<Form.Control>
|
||||||
|
<FileUpload
|
||||||
|
label={t("products.media.uploadImagesLabel")}
|
||||||
|
hint={t("products.media.uploadImagesHint")}
|
||||||
|
hasError={!!form.formState.errors.images}
|
||||||
|
formats={SUPPORTED_FORMATS}
|
||||||
|
onUploaded={() => {
|
||||||
|
form.clearErrors("images")
|
||||||
|
// if (hasInvalidFiles(files)) {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// files.forEach((f) => append(f))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</div>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
|||||||
+15
-13
@@ -15,15 +15,19 @@ const CreateProductSchema = zod.object({
|
|||||||
title: zod.string(),
|
title: zod.string(),
|
||||||
subtitle: zod.string().optional(),
|
subtitle: zod.string().optional(),
|
||||||
handle: zod.string().optional(),
|
handle: zod.string().optional(),
|
||||||
material: zod.string().optional(),
|
|
||||||
description: zod.string().optional(),
|
description: zod.string().optional(),
|
||||||
discountable: zod.boolean(),
|
discountable: zod.boolean(),
|
||||||
|
type_id: zod.string().optional(),
|
||||||
|
collection_id: zod.string().optional(),
|
||||||
|
category_ids: zod.array(zod.string()).optional(),
|
||||||
|
tags: zod.array(zod.string()).optional(),
|
||||||
sales_channels: zod.array(zod.string()).optional(),
|
sales_channels: zod.array(zod.string()).optional(),
|
||||||
|
origin_country: zod.string().optional(),
|
||||||
|
material: zod.string().optional(),
|
||||||
width: zod.string().optional(),
|
width: zod.string().optional(),
|
||||||
length: zod.string().optional(),
|
length: zod.string().optional(),
|
||||||
height: zod.string().optional(),
|
height: zod.string().optional(),
|
||||||
weight: zod.string().optional(),
|
weight: zod.string().optional(),
|
||||||
origin_country: zod.string().optional(),
|
|
||||||
mid_code: zod.string().optional(),
|
mid_code: zod.string().optional(),
|
||||||
hs_code: zod.string().optional(),
|
hs_code: zod.string().optional(),
|
||||||
variants: zod.array(
|
variants: zod.array(
|
||||||
@@ -31,6 +35,8 @@ const CreateProductSchema = zod.object({
|
|||||||
variant_rank: zod.number(),
|
variant_rank: zod.number(),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
images: zod.array(zod.string()).optional(),
|
||||||
|
thumbnail: zod.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
type Schema = zod.infer<typeof CreateProductSchema>
|
type Schema = zod.infer<typeof CreateProductSchema>
|
||||||
@@ -45,18 +51,12 @@ export const CreateProductForm = () => {
|
|||||||
title: "",
|
title: "",
|
||||||
subtitle: "",
|
subtitle: "",
|
||||||
handle: "",
|
handle: "",
|
||||||
material: "",
|
|
||||||
description: "",
|
description: "",
|
||||||
discountable: true,
|
discountable: true,
|
||||||
height: "",
|
tags: [],
|
||||||
length: "",
|
|
||||||
weight: "",
|
|
||||||
width: "",
|
|
||||||
origin_country: "",
|
|
||||||
mid_code: "",
|
|
||||||
hs_code: "",
|
|
||||||
sales_channels: [],
|
sales_channels: [],
|
||||||
variants: [],
|
variants: [],
|
||||||
|
images: [],
|
||||||
},
|
},
|
||||||
resolver: zodResolver(CreateProductSchema),
|
resolver: zodResolver(CreateProductSchema),
|
||||||
})
|
})
|
||||||
@@ -66,13 +66,15 @@ export const CreateProductForm = () => {
|
|||||||
const handleSubmit = form.handleSubmit(async (values) => {
|
const handleSubmit = form.handleSubmit(async (values) => {
|
||||||
await mutateAsync(
|
await mutateAsync(
|
||||||
{
|
{
|
||||||
title: values.title,
|
...values,
|
||||||
discountable: values.discountable,
|
|
||||||
is_giftcard: false,
|
is_giftcard: false,
|
||||||
|
tags: values.tags?.map((tag) => ({ value: tag })),
|
||||||
|
sales_channels: values.sales_channels?.map((sc) => ({ id: sc })),
|
||||||
width: values.width ? parseFloat(values.width) : undefined,
|
width: values.width ? parseFloat(values.width) : undefined,
|
||||||
length: values.length ? parseFloat(values.length) : undefined,
|
length: values.length ? parseFloat(values.length) : undefined,
|
||||||
height: values.height ? parseFloat(values.height) : undefined,
|
height: values.height ? parseFloat(values.height) : undefined,
|
||||||
weight: values.weight ? parseFloat(values.weight) : undefined,
|
weight: values.weight ? parseFloat(values.weight) : undefined,
|
||||||
|
variants: values.variants.map((v) => ({ title: "", prices: [] })),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: ({ product }) => {
|
onSuccess: ({ product }) => {
|
||||||
@@ -93,7 +95,7 @@ export const CreateProductForm = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</RouteFocusModal.Close>
|
</RouteFocusModal.Close>
|
||||||
<Button type="submit" size="small" isLoading={isLoading}>
|
<Button type="submit" size="small" isLoading={isLoading}>
|
||||||
{t("actions.save")}
|
{t("actions.saveAsDraft")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</RouteFocusModal.Header>
|
</RouteFocusModal.Header>
|
||||||
|
|||||||
+2
-2
@@ -140,7 +140,7 @@ export const EditProductForm = ({ product }: EditProductFormProps) => {
|
|||||||
className="text-ui-fg-subtle"
|
className="text-ui-fg-subtle"
|
||||||
>
|
>
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="products.titleHint"
|
i18nKey="products.fields.title.hint"
|
||||||
t={t}
|
t={t}
|
||||||
components={[<br key="break" />]}
|
components={[<br key="break" />]}
|
||||||
/>
|
/>
|
||||||
@@ -204,7 +204,7 @@ export const EditProductForm = ({ product }: EditProductFormProps) => {
|
|||||||
<Form.ErrorMessage />
|
<Form.ErrorMessage />
|
||||||
<Form.Hint>
|
<Form.Hint>
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="products.descriptionHint"
|
i18nKey="products.fields.description.hint"
|
||||||
t={t}
|
t={t}
|
||||||
components={[<br key="break" />]}
|
components={[<br key="break" />]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+26
-158
@@ -1,22 +1,10 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import {
|
import { CheckMini, Spinner, ThumbnailBadge } from "@medusajs/icons"
|
||||||
ArrowDownTray,
|
|
||||||
CheckMini,
|
|
||||||
Spinner,
|
|
||||||
ThumbnailBadge,
|
|
||||||
} from "@medusajs/icons"
|
|
||||||
import { Image, Product } from "@medusajs/medusa"
|
import { Image, Product } from "@medusajs/medusa"
|
||||||
import { Button, CommandBar, Text, Tooltip, clx } from "@medusajs/ui"
|
import { Button, CommandBar, Tooltip, clx } from "@medusajs/ui"
|
||||||
import { AnimatePresence, motion } from "framer-motion"
|
import { AnimatePresence, motion } from "framer-motion"
|
||||||
import { useAdminUpdateProduct, useMedusa } from "medusa-react"
|
import { useAdminUpdateProduct, useMedusa } from "medusa-react"
|
||||||
import {
|
import { Fragment, useCallback, useState } from "react"
|
||||||
ChangeEvent,
|
|
||||||
DragEvent,
|
|
||||||
Fragment,
|
|
||||||
useCallback,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react"
|
|
||||||
import { useFieldArray, useForm } from "react-hook-form"
|
import { useFieldArray, useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
@@ -27,6 +15,10 @@ import {
|
|||||||
RouteFocusModal,
|
RouteFocusModal,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../components/route-modal"
|
} from "../../../../../components/route-modal"
|
||||||
|
import {
|
||||||
|
FileType,
|
||||||
|
FileUpload,
|
||||||
|
} from "../../../../../components/common/file-upload"
|
||||||
|
|
||||||
type ProductMediaViewProps = {
|
type ProductMediaViewProps = {
|
||||||
product: Product
|
product: Product
|
||||||
@@ -65,11 +57,6 @@ const EditProductMediaSchema = z.object({
|
|||||||
|
|
||||||
export const EditProductMediaForm = ({ product }: ProductMediaViewProps) => {
|
export const EditProductMediaForm = ({ product }: ProductMediaViewProps) => {
|
||||||
const [selection, setSelection] = useState<Record<string, true>>({})
|
const [selection, setSelection] = useState<Record<string, true>>({})
|
||||||
const [isDragOver, setIsDragOver] = useState<boolean>(false)
|
|
||||||
|
|
||||||
const dropZoneRef = useRef<HTMLButtonElement>(null)
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { handleSuccess } = useRouteModal()
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
@@ -137,20 +124,16 @@ export const EditProductMediaForm = ({ product }: ProductMediaViewProps) => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleOpenFileSelector = () => {
|
const hasInvalidFiles = (fileList: FileType[]) => {
|
||||||
inputRef.current?.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasInvalidFiles = (fileList: File[]) => {
|
|
||||||
const invalidFile = fileList.find(
|
const invalidFile = fileList.find(
|
||||||
(f) => !SUPPORTED_FORMATS.includes(f.type)
|
(f) => !SUPPORTED_FORMATS.includes(f.file.type)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (invalidFile) {
|
if (invalidFile) {
|
||||||
form.setError("media", {
|
form.setError("media", {
|
||||||
type: "invalid_file",
|
type: "invalid_file",
|
||||||
message: t("products.media.invalidFileType", {
|
message: t("products.media.invalidFileType", {
|
||||||
name: invalidFile.name,
|
name: invalidFile.file.name,
|
||||||
types: SUPPORTED_FORMATS_FILE_EXTENSIONS.join(", "),
|
types: SUPPORTED_FORMATS_FILE_EXTENSIONS.join(", "),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@@ -161,94 +144,6 @@ export const EditProductMediaForm = ({ product }: ProductMediaViewProps) => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFileChange = async (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const files = event.target.files
|
|
||||||
if (files) {
|
|
||||||
const fileList = Array.from(files)
|
|
||||||
|
|
||||||
if (hasInvalidFiles(fileList)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
form.clearErrors("media")
|
|
||||||
|
|
||||||
fileList.forEach((file) => {
|
|
||||||
const preview = URL.createObjectURL(file)
|
|
||||||
|
|
||||||
append({
|
|
||||||
id: crypto.randomUUID(),
|
|
||||||
url: preview,
|
|
||||||
isThumbnail: false,
|
|
||||||
file,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDragEnter = (event: DragEvent) => {
|
|
||||||
event.preventDefault()
|
|
||||||
event.stopPropagation()
|
|
||||||
|
|
||||||
const files = event.dataTransfer.files
|
|
||||||
|
|
||||||
if (files) {
|
|
||||||
const fileList = Array.from(files)
|
|
||||||
|
|
||||||
console.log(fileList.map((f) => f.type))
|
|
||||||
|
|
||||||
if (hasInvalidFiles(fileList)) {
|
|
||||||
console.log("invalid files")
|
|
||||||
setIsDragOver(false)
|
|
||||||
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 handleDrop = (event: DragEvent) => {
|
|
||||||
event.preventDefault()
|
|
||||||
event.stopPropagation()
|
|
||||||
|
|
||||||
setIsDragOver(false)
|
|
||||||
form.clearErrors("media")
|
|
||||||
|
|
||||||
const files = event.dataTransfer.files
|
|
||||||
|
|
||||||
if (files) {
|
|
||||||
const fileList = Array.from(files)
|
|
||||||
|
|
||||||
if (hasInvalidFiles(fileList)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fileList.forEach((file) => {
|
|
||||||
const previewUrl = URL.createObjectURL(file)
|
|
||||||
|
|
||||||
append({
|
|
||||||
id: crypto.randomUUID(),
|
|
||||||
url: previewUrl,
|
|
||||||
isThumbnail: false,
|
|
||||||
file,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCheckedChange = useCallback(
|
const handleCheckedChange = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
return (val: boolean) => {
|
return (val: boolean) => {
|
||||||
@@ -353,49 +248,22 @@ export const EditProductMediaForm = ({ product }: ProductMediaViewProps) => {
|
|||||||
<Form.Hint>{t("products.media.editHint")}</Form.Hint>
|
<Form.Hint>{t("products.media.editHint")}</Form.Hint>
|
||||||
</div>
|
</div>
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<div>
|
<FileUpload
|
||||||
<button
|
label={t("products.media.uploadImagesLabel")}
|
||||||
ref={dropZoneRef}
|
hint={t("products.media.uploadImagesHint")}
|
||||||
type="button"
|
hasError={!!form.formState.errors.media}
|
||||||
onClick={handleOpenFileSelector}
|
formats={SUPPORTED_FORMATS}
|
||||||
onDrop={handleDrop}
|
onUploaded={(files) => {
|
||||||
onDragOver={(e) => e.preventDefault()}
|
form.clearErrors("media")
|
||||||
onDragEnter={handleDragEnter}
|
if (hasInvalidFiles(files)) {
|
||||||
onDragLeave={handleDragLeave}
|
return
|
||||||
className={clx(
|
}
|
||||||
"bg-ui-bg-component border-ui-border-strong transition-fg group flex w-full flex-col items-center gap-y-2 rounded-lg border border-dashed p-8",
|
|
||||||
"hover:border-ui-border-interactive focus:border-ui-border-interactive",
|
files.forEach((f) =>
|
||||||
"focus:shadow-borders-focus outline-none focus:border-solid",
|
append({ ...f, isThumbnail: false })
|
||||||
{
|
)
|
||||||
"!border-ui-border-error":
|
}}
|
||||||
!!form.formState.errors.media,
|
/>
|
||||||
"!border-ui-border-interactive": isDragOver,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="text-ui-fg-subtle group-disabled:text-ui-fg-disabled flex items-center gap-x-2">
|
|
||||||
<ArrowDownTray />
|
|
||||||
<Text>
|
|
||||||
{t("products.media.uploadImagesLabel")}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<Text
|
|
||||||
size="small"
|
|
||||||
leading="compact"
|
|
||||||
className="text-ui-fg-muted group-disabled:text-ui-fg-disabled"
|
|
||||||
>
|
|
||||||
{t("products.media.uploadImagesHint")}
|
|
||||||
</Text>
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
hidden
|
|
||||||
ref={inputRef}
|
|
||||||
onChange={handleFileChange}
|
|
||||||
type="file"
|
|
||||||
accept={SUPPORTED_FORMATS.join(",")}
|
|
||||||
multiple
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
<Form.ErrorMessage />
|
<Form.ErrorMessage />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user