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:
Stevche Radevski
2024-04-04 12:27:33 +02:00
committed by GitHub
parent 483bf98a49
commit 06b2f0a8dc
8 changed files with 545 additions and 222 deletions

View File

@@ -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}}</0> of <1>{{y}}</1> 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"

View File

@@ -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>
)
}

View File

@@ -0,0 +1 @@
export * from "./file-upload"

View File

@@ -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)

View File

@@ -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 (
<PanelGroup
@@ -51,7 +90,9 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.title")}</Form.Label>
<Form.Label>
{t("products.fields.title.label")}
</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
@@ -66,7 +107,7 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
return (
<Form.Item>
<Form.Label optional>
{t("fields.subtitle")}
{t("products.fields.subtitle.label")}
</Form.Label>
<Form.Control>
<Input {...field} />
@@ -76,17 +117,13 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
}}
/>
</div>
<Text
size="small"
leading="compact"
className="text-ui-fg-subtle"
>
<Form.Hint>
<Trans
i18nKey="products.titleHint"
i18nKey="products.fields.title.hint"
t={t}
components={[<br key="break" />]}
/>
</Text>
</Form.Hint>
</div>
<div className="grid grid-cols-2 gap-x-4">
<Form.Field
@@ -96,7 +133,7 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
return (
<Form.Item>
<Form.Label
tooltip={t("products.handleTooltip")}
tooltip={t("products.fields.handle.tooltip")}
optional
>
{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>
<Form.Field
control={form.control}
@@ -132,14 +153,14 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
return (
<Form.Item>
<Form.Label optional>
{t("fields.description")}
{t("products.fields.description.label")}
</Form.Label>
<Form.Control>
<Textarea {...field} />
</Form.Control>
<Form.Hint>
<Trans
i18nKey={"products.descriptionHint"}
i18nKey={"products.fields.description.hint"}
components={[<br key="break" />]}
/>
</Form.Hint>
@@ -150,16 +171,224 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
</div>
<div id="organize" className="flex flex-col gap-y-8">
<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"
variant="secondary"
onClick={() => onOpenChange(!open)}
>
{t("actions.edit")}
</Button>
</Button> */}
</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">
<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">
<Form.Field
control={form.control}
@@ -167,7 +396,9 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
render={({ field }) => {
return (
<Form.Item>
<Form.Label optional>{t("fields.width")}</Form.Label>
<Form.Label optional>
{t("products.fields.width.label")}
</Form.Label>
<Form.Control>
<Input {...field} type="number" min={0} />
</Form.Control>
@@ -181,7 +412,9 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
render={({ field }) => {
return (
<Form.Item>
<Form.Label optional>{t("fields.length")}</Form.Label>
<Form.Label optional>
{t("products.fields.length.label")}
</Form.Label>
<Form.Control>
<Input {...field} type="number" min={0} />
</Form.Control>
@@ -195,7 +428,9 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
render={({ field }) => {
return (
<Form.Item>
<Form.Label optional>{t("fields.height")}</Form.Label>
<Form.Label optional>
{t("products.fields.height.label")}
</Form.Label>
<Form.Control>
<Input {...field} type="number" min={0} />
</Form.Control>
@@ -209,7 +444,9 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
render={({ field }) => {
return (
<Form.Item>
<Form.Label optional>{t("fields.weight")}</Form.Label>
<Form.Label optional>
{t("products.fields.weight.label")}
</Form.Label>
<Form.Control>
<Input {...field} type="number" min={0} />
</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
control={form.control}
name="origin_country"
render={({ field }) => {
name="images"
render={() => {
return (
<Form.Item>
<Form.Label optional>
{t("fields.countryOfOrigin")}
</Form.Label>
<Form.Control>
<CountrySelect {...field} />
</Form.Control>
<div className="flex flex-col gap-y-4">
<div className="flex flex-col gap-y-1">
<Form.Label optional>
{t("products.media.label")}
</Form.Label>
<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>
)
}}

View File

@@ -15,15 +15,19 @@ const CreateProductSchema = zod.object({
title: zod.string(),
subtitle: zod.string().optional(),
handle: zod.string().optional(),
material: zod.string().optional(),
description: zod.string().optional(),
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(),
origin_country: zod.string().optional(),
material: zod.string().optional(),
width: zod.string().optional(),
length: zod.string().optional(),
height: zod.string().optional(),
weight: zod.string().optional(),
origin_country: zod.string().optional(),
mid_code: zod.string().optional(),
hs_code: zod.string().optional(),
variants: zod.array(
@@ -31,6 +35,8 @@ const CreateProductSchema = zod.object({
variant_rank: zod.number(),
})
),
images: zod.array(zod.string()).optional(),
thumbnail: zod.string().optional(),
})
type Schema = zod.infer<typeof CreateProductSchema>
@@ -45,18 +51,12 @@ export const CreateProductForm = () => {
title: "",
subtitle: "",
handle: "",
material: "",
description: "",
discountable: true,
height: "",
length: "",
weight: "",
width: "",
origin_country: "",
mid_code: "",
hs_code: "",
tags: [],
sales_channels: [],
variants: [],
images: [],
},
resolver: zodResolver(CreateProductSchema),
})
@@ -66,13 +66,15 @@ export const CreateProductForm = () => {
const handleSubmit = form.handleSubmit(async (values) => {
await mutateAsync(
{
title: values.title,
discountable: values.discountable,
...values,
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,
length: values.length ? parseFloat(values.length) : undefined,
height: values.height ? parseFloat(values.height) : undefined,
weight: values.weight ? parseFloat(values.weight) : undefined,
variants: values.variants.map((v) => ({ title: "", prices: [] })),
},
{
onSuccess: ({ product }) => {
@@ -93,7 +95,7 @@ export const CreateProductForm = () => {
</Button>
</RouteFocusModal.Close>
<Button type="submit" size="small" isLoading={isLoading}>
{t("actions.save")}
{t("actions.saveAsDraft")}
</Button>
</div>
</RouteFocusModal.Header>

View File

@@ -140,7 +140,7 @@ export const EditProductForm = ({ product }: EditProductFormProps) => {
className="text-ui-fg-subtle"
>
<Trans
i18nKey="products.titleHint"
i18nKey="products.fields.title.hint"
t={t}
components={[<br key="break" />]}
/>
@@ -204,7 +204,7 @@ export const EditProductForm = ({ product }: EditProductFormProps) => {
<Form.ErrorMessage />
<Form.Hint>
<Trans
i18nKey="products.descriptionHint"
i18nKey="products.fields.description.hint"
t={t}
components={[<br key="break" />]}
/>

View File

@@ -1,22 +1,10 @@
import { zodResolver } from "@hookform/resolvers/zod"
import {
ArrowDownTray,
CheckMini,
Spinner,
ThumbnailBadge,
} from "@medusajs/icons"
import { CheckMini, Spinner, ThumbnailBadge } from "@medusajs/icons"
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 { useAdminUpdateProduct, useMedusa } from "medusa-react"
import {
ChangeEvent,
DragEvent,
Fragment,
useCallback,
useRef,
useState,
} from "react"
import { Fragment, useCallback, useState } from "react"
import { useFieldArray, useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { z } from "zod"
@@ -27,6 +15,10 @@ import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
import {
FileType,
FileUpload,
} from "../../../../../components/common/file-upload"
type ProductMediaViewProps = {
product: Product
@@ -65,11 +57,6 @@ const EditProductMediaSchema = z.object({
export const EditProductMediaForm = ({ product }: ProductMediaViewProps) => {
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 { handleSuccess } = useRouteModal()
@@ -137,20 +124,16 @@ export const EditProductMediaForm = ({ product }: ProductMediaViewProps) => {
)
})
const handleOpenFileSelector = () => {
inputRef.current?.click()
}
const hasInvalidFiles = (fileList: File[]) => {
const hasInvalidFiles = (fileList: FileType[]) => {
const invalidFile = fileList.find(
(f) => !SUPPORTED_FORMATS.includes(f.type)
(f) => !SUPPORTED_FORMATS.includes(f.file.type)
)
if (invalidFile) {
form.setError("media", {
type: "invalid_file",
message: t("products.media.invalidFileType", {
name: invalidFile.name,
name: invalidFile.file.name,
types: SUPPORTED_FORMATS_FILE_EXTENSIONS.join(", "),
}),
})
@@ -161,94 +144,6 @@ export const EditProductMediaForm = ({ product }: ProductMediaViewProps) => {
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(
(id: string) => {
return (val: boolean) => {
@@ -353,49 +248,22 @@ export const EditProductMediaForm = ({ product }: ProductMediaViewProps) => {
<Form.Hint>{t("products.media.editHint")}</Form.Hint>
</div>
<Form.Control>
<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":
!!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>
<FileUpload
label={t("products.media.uploadImagesLabel")}
hint={t("products.media.uploadImagesHint")}
hasError={!!form.formState.errors.media}
formats={SUPPORTED_FORMATS}
onUploaded={(files) => {
form.clearErrors("media")
if (hasInvalidFiles(files)) {
return
}
files.forEach((f) =>
append({ ...f, isThumbnail: false })
)
}}
/>
</Form.Control>
<Form.ErrorMessage />
</div>