feat: Add support for uploading media when creating a product (#7567)

This commit is contained in:
Stevche Radevski
2024-05-31 15:34:16 +00:00
committed by GitHub
parent 294ec36cc3
commit 97327670a5
12 changed files with 392 additions and 284 deletions
@@ -0,0 +1 @@
export * from "./media-grid-view"
@@ -0,0 +1,125 @@
import { CheckMini, Spinner, ThumbnailBadge } from "@medusajs/icons"
import { Tooltip, clx } from "@medusajs/ui"
import { AnimatePresence, motion } from "framer-motion"
import { useCallback, useState } from "react"
import { useTranslation } from "react-i18next"
interface MediaView {
id?: string
field_id: string
url: string
isThumbnail: boolean
}
interface MediaGridProps {
media: MediaView[]
selection: Record<string, boolean>
onCheckedChange: (id: string) => (value: boolean) => void
}
export const MediaGrid = ({
media,
selection,
onCheckedChange,
}: MediaGridProps) => {
return (
<div className="bg-ui-bg-subtle size-full overflow-auto">
<div className="grid h-fit auto-rows-auto grid-cols-4 gap-6 p-6">
{media.map((m) => {
return (
<MediaGridItem
onCheckedChange={onCheckedChange(m.id!)}
checked={!!selection[m.id!]}
key={m.field_id}
media={m}
/>
)
})}
</div>
</div>
)
}
interface MediaGridItemProps {
media: MediaView
checked: boolean
onCheckedChange: (value: boolean) => void
}
const MediaGridItem = ({
media,
checked,
onCheckedChange,
}: MediaGridItemProps) => {
const [isLoading, setIsLoading] = useState(true)
const { t } = useTranslation()
const handleToggle = useCallback(() => {
onCheckedChange(!checked)
}, [checked, onCheckedChange])
return (
<button
type="button"
onClick={handleToggle}
className="shadow-elevation-card-rest hover:shadow-elevation-card-hover focus-visible:shadow-borders-focus bg-ui-bg-subtle-hover group relative aspect-square h-auto max-w-full overflow-hidden rounded-lg outline-none"
>
{media.isThumbnail && (
<div className="absolute left-2 top-2">
<Tooltip content={t("products.media.thumbnailTooltip")}>
<ThumbnailBadge />
</Tooltip>
</div>
)}
<div
className={clx(
"transition-fg absolute right-2 top-2 opacity-0 group-focus-within:opacity-100 group-hover:opacity-100 group-focus:opacity-100",
{
"opacity-100": checked,
}
)}
>
<div
className={clx(
"group relative inline-flex h-4 w-4 items-center justify-center outline-none "
)}
>
<div
className={clx(
"text-ui-fg-on-inverted bg-ui-bg-component shadow-borders-base [&_path]:shadow-details-contrast-on-bg-interactive group-disabled:text-ui-fg-disabled group-disabled:!bg-ui-bg-disabled group-disabled:!shadow-borders-base transition-fg h-[14px] w-[14px] rounded-[3px]",
{
"bg-ui-bg-interactive group-hover:bg-ui-bg-interactive shadow-borders-interactive-with-shadow":
checked,
}
)}
>
{checked && (
<div className="absolute inset-0">
<CheckMini />
</div>
)}
</div>
</div>
</div>
<AnimatePresence>
{isLoading && (
<motion.div
initial={{ opacity: 1 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0, transition: { duration: 0.5 } }}
className="bg-ui-bg-subtle-hover absolute inset-0 flex items-center justify-center"
>
<Spinner className="text-ui-fg-subtle animate-spin" />
</motion.div>
)}
</AnimatePresence>
<img
src={media.url}
onLoad={() => setIsLoading(false)}
alt=""
className="size-full object-cover object-center"
/>
</button>
)
}
@@ -0,0 +1 @@
export * from "./upload-media-form-item"
@@ -0,0 +1,104 @@
import { useTranslation } from "react-i18next"
import { Form } from "../../../../../components/common/form"
import {
FileType,
FileUpload,
} from "../../../../../components/common/file-upload"
import { UseFormReturn } from "react-hook-form"
import {
EditProductMediaSchemaType,
ProductCreateSchemaType,
} from "../../../product-create/types"
import { MediaSchema } from "../../../product-create/constants"
import { z } from "zod"
type Media = z.infer<typeof MediaSchema>
const SUPPORTED_FORMATS = [
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
"image/heic",
"image/svg+xml",
]
const SUPPORTED_FORMATS_FILE_EXTENSIONS = [
".jpeg",
".png",
".gif",
".webp",
".heic",
".svg",
]
export const UploadMediaFormItem = ({
form,
append,
}: {
form:
| UseFormReturn<ProductCreateSchemaType>
| UseFormReturn<EditProductMediaSchemaType>
append: (value: Media) => void
}) => {
const { t } = useTranslation()
const hasInvalidFiles = (fileList: FileType[]) => {
const invalidFile = fileList.find(
(f) => !SUPPORTED_FORMATS.includes(f.file.type)
)
if (invalidFile) {
form.setError("media", {
type: "invalid_file",
message: t("products.media.invalidFileType", {
name: invalidFile.file.name,
types: SUPPORTED_FORMATS_FILE_EXTENSIONS.join(", "),
}),
})
return true
}
return false
}
return (
<Form.Field
control={
form.control as UseFormReturn<EditProductMediaSchemaType>["control"]
}
name="media"
render={() => {
return (
<Form.Item>
<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.media}
formats={SUPPORTED_FORMATS}
onUploaded={(files) => {
form.clearErrors("media")
if (hasInvalidFiles(files)) {
return
}
// TODO: For now all files that get uploaded are not thumbnails, revisit this logic
files.forEach((f) => append({ ...f, isThumbnail: false }))
}}
/>
</Form.Control>
<Form.ErrorMessage />
</div>
</Form.Item>
)
}}
/>
)
}
@@ -1,69 +1,80 @@
import { Heading } from "@medusajs/ui"
import { UseFormReturn } from "react-hook-form"
import { CommandBar, Heading } from "@medusajs/ui"
import { UseFormReturn, useFieldArray } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { FileUpload } from "../../../../../../../components/common/file-upload"
import { Form } from "../../../../../../../components/common/form"
import { ProductCreateSchemaType } from "../../../../types"
import { MediaGrid } from "../../../../../common/components/media-grid-view"
import { useCallback, useState } from "react"
import { UploadMediaFormItem } from "../../../../../common/components/upload-media-form-item"
type ProductCreateMediaSectionProps = {
form: UseFormReturn<ProductCreateSchemaType>
}
const SUPPORTED_FORMATS = [
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
"image/heic",
"image/svg+xml",
]
export const ProductCreateMediaSection = ({
form,
}: ProductCreateMediaSectionProps) => {
const { t } = useTranslation()
const [selection, setSelection] = useState<Record<string, true>>({})
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 handleCheckedChange = useCallback(
(id: string) => {
return (val: boolean) => {
if (!val) {
const { [id]: _, ...rest } = selection
setSelection(rest)
} else {
setSelection((prev) => ({ ...prev, [id]: true }))
}
}
},
[selection]
)
return (
<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="images"
render={() => {
return (
<Form.Item>
<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={(files) => {
form.clearErrors("images")
// if (hasInvalidFiles(files)) {
// return
// }
// files.forEach((f) => append(f))
}}
/>
</Form.Control>
<Form.ErrorMessage />
</div>
</Form.Item>
)
}}
/>
<UploadMediaFormItem form={form} append={append} />
</div>
{fields?.length ? (
<MediaGrid
media={fields}
selection={selection}
onCheckedChange={handleCheckedChange}
/>
) : null}
<CommandBar open={!!selectionCount}>
<CommandBar.Bar>
<CommandBar.Value>
{t("general.countSelected", {
count: selectionCount,
})}
</CommandBar.Value>
<CommandBar.Seperator />
<CommandBar.Command
action={handleDelete}
label={t("actions.delete")}
shortcut="d"
/>
</CommandBar.Bar>
</CommandBar>
</div>
)
}
@@ -6,6 +6,7 @@ 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"
type ProductAttributesProps = {
form: UseFormReturn<ProductCreateSchemaType>
@@ -19,6 +20,8 @@ export const ProductCreateDetailsForm = ({ form }: ProductAttributesProps) => {
<ProductCreateGeneralSection form={form} />
<Divider />
<ProductCreateVariantsSection form={form} />
<Divider />
<ProductCreateMediaSection form={form} />
</div>
</div>
)
@@ -19,6 +19,8 @@ import { ProductCreateOrganizeForm } from "../product-create-organize-form"
import { ProductCreateInventoryKitForm } from "../product-create-inventory-kit-form"
import { ProductCreateVariantsForm } from "../product-create-variants-form"
import { isFetchError } from "../../../../../lib/is-fetch-error"
import { sdk } from "../../../../../lib/client"
import { HttpTypes } from "@medusajs/types"
enum Tab {
DETAILS = "details",
@@ -80,13 +82,41 @@ export const ProductCreateForm = () => {
const isDraftSubmission = submitter.dataset.name === SAVE_DRAFT_BUTTON
const payload = { ...values }
const media = values.media || []
const payload = { ...values, media: undefined }
let uploadedMedia: (HttpTypes.AdminFile & { isThumbnail: boolean })[] = []
try {
if (media.length) {
const thumbnailReq = media.find((m) => m.isThumbnail)
const otherMediaReq = media.filter((m) => !m.isThumbnail)
const fileReqs = []
if (thumbnailReq) {
fileReqs.push(
sdk.admin.uploads
.create({ files: [thumbnailReq.file] })
.then((r) => r.files.map((f) => ({ ...f, isThumbnail: true })))
)
}
if (otherMediaReq?.length) {
fileReqs.push(
sdk.admin.uploads
.create({
files: otherMediaReq.map((m) => m.file),
})
.then((r) => r.files.map((f) => ({ ...f, isThumbnail: false })))
)
}
uploadedMedia = (await Promise.all(fileReqs)).flat()
}
const { product } = await mutateAsync(
normalizeProductFormValues({
// TODO: workflow should handle inventory creation
...payload,
media: uploadedMedia,
status: (isDraftSubmission ? "draft" : "published") as any,
})
)
@@ -2,6 +2,13 @@ import { z } from "zod"
import { decorateVariantsWithDefaultValues } from "./utils.ts"
import { optionalInt } from "../../../lib/validation.ts"
export const MediaSchema = z.object({
id: z.string().optional(),
url: z.string(),
isThumbnail: z.boolean(),
file: z.any().nullable(), // File
})
export const ProductCreateSchema = z
.object({
title: z.string().min(1),
@@ -69,8 +76,7 @@ export const ProductCreateSchema = z
})
)
.min(1),
images: z.array(z.string()).optional(),
thumbnail: z.string().optional(),
media: z.array(MediaSchema).optional(),
})
.superRefine((data, ctx) => {
if (data.variants.every((v) => !v.should_create)) {
@@ -82,6 +88,10 @@ export const ProductCreateSchema = z
}
})
export const EditProductMediaSchema = z.object({
media: z.array(MediaSchema),
})
export const PRODUCT_CREATE_FORM_DEFAULTS: Partial<
z.infer<typeof ProductCreateSchema>
> = {
@@ -107,8 +117,7 @@ export const PRODUCT_CREATE_FORM_DEFAULTS: Partial<
},
]),
enable_variants: false,
images: [],
thumbnail: "",
media: [],
categories: [],
collection_id: "",
description: "",
@@ -1,4 +1,6 @@
import { z } from "zod"
import { ProductCreateSchema } from "./constants"
import { EditProductMediaSchema, ProductCreateSchema } from "./constants"
export type ProductCreateSchemaType = z.infer<typeof ProductCreateSchema>
export type EditProductMediaSchemaType = z.infer<typeof EditProductMediaSchema>
@@ -6,6 +6,11 @@ import { castNumber } from "../../../lib/cast-number.ts"
export const normalizeProductFormValues = (
values: ProductCreateSchemaType & { status: CreateProductDTO["status"] }
) => {
const thumbnail = values.media?.find((media) => media.isThumbnail)?.url
const images = values.media
?.filter((media) => !media.isThumbnail)
.map((media) => ({ url: media.url }))
return {
status: values.status,
is_giftcard: false,
@@ -15,9 +20,7 @@ export const normalizeProductFormValues = (
sales_channels: values?.sales_channels?.length
? values.sales_channels?.map((sc) => ({ id: sc.id }))
: undefined,
images: values.images?.length
? values.images.map((url) => ({ url }))
: undefined,
images,
collection_id: values.collection_id || undefined,
categories: values.categories.map((id) => ({ id })),
type_id: values.type_id || undefined,
@@ -26,7 +29,7 @@ export const normalizeProductFormValues = (
material: values.material || undefined,
mid_code: values.mid_code || undefined,
hs_code: values.hs_code || undefined,
thumbnail: values.thumbnail || undefined,
thumbnail,
title: values.title,
subtitle: values.subtitle || undefined,
description: values.description || undefined,
@@ -1,67 +1,38 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { CheckMini, Spinner, ThumbnailBadge } from "@medusajs/icons"
import { Image, Product } from "@medusajs/medusa"
import { Button, CommandBar, Tooltip, clx, toast } from "@medusajs/ui"
import { AnimatePresence, motion } from "framer-motion"
import { Button, CommandBar } from "@medusajs/ui"
import { Fragment, useCallback, useState } from "react"
import { useFieldArray, useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { z } from "zod"
import { Link } from "react-router-dom"
import {
FileType,
FileUpload,
} from "../../../../../components/common/file-upload"
import { Form } from "../../../../../components/common/form"
import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
import { useUpdateProduct } from "../../../../../hooks/api/products"
import { sdk } from "../../../../../lib/client"
import {
EditProductMediaSchema,
MediaSchema,
} from "../../../product-create/constants"
import { HttpTypes } from "@medusajs/types"
import { MediaGrid } from "../../../common/components/media-grid-view"
import { UploadMediaFormItem } from "../../../common/components/upload-media-form-item"
import { EditProductMediaSchemaType } from "../../../product-create/types"
type ProductMediaViewProps = {
product: Product
product: HttpTypes.AdminProduct
}
const MediaSchema = z.object({
id: z.string(),
url: z.string(),
isThumbnail: z.boolean(),
file: z.any().nullable(), // File
})
const SUPPORTED_FORMATS = [
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
"image/heic",
"image/svg+xml",
]
const SUPPORTED_FORMATS_FILE_EXTENSIONS = [
".jpeg",
".png",
".gif",
".webp",
".heic",
".svg",
]
type Media = z.infer<typeof MediaSchema>
const EditProductMediaSchema = z.object({
media: z.array(MediaSchema),
})
export const EditProductMediaForm = ({ product }: ProductMediaViewProps) => {
const [selection, setSelection] = useState<Record<string, true>>({})
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<z.infer<typeof EditProductMediaSchema>>({
const form = useForm<EditProductMediaSchemaType>({
defaultValues: {
media: getDefaultValues(product.images, product.thumbnail),
},
@@ -74,47 +45,40 @@ export const EditProductMediaForm = ({ product }: ProductMediaViewProps) => {
keyName: "field_id",
})
const { mutateAsync, isPending } = useUpdateProduct(product.id)
const { mutateAsync, isPending } = useUpdateProduct(product.id!)
const handleSubmit = form.handleSubmit(async ({ media }) => {
const urls = media.map((m) => m.url)
const filesToUpload = media
.map((m, index) => ({ file: m.file, index }))
.filter((m) => m.file)
.map((m, i) => ({ file: m.file, index: i }))
.filter((m) => !!m.file)
let uploaded: HttpTypes.AdminFile[] = []
if (filesToUpload.length) {
const files = filesToUpload.map((m) => m.file) as File[]
const uploads = await sdk.admin.uploads
.create({ files })
.then((res) => {
return res.files
})
const { files: uploads } = await sdk.admin.uploads
.create({ files: filesToUpload.map((m) => m.file) })
.catch(() => {
form.setError("media", {
type: "invalid_file",
message: t("products.media.failedToUpload"),
})
return { files: [] }
})
if (!uploads) {
return
}
// Insert the URLs of the uploaded files back into the urls array
uploads.forEach((upload, i) => {
const originalIndex = filesToUpload[i].index
urls[originalIndex] = upload.url
})
uploaded = uploads
}
const thumbnailIndex = media.findIndex((m) => m.isThumbnail)
const thumbnail = thumbnailIndex > -1 ? urls[thumbnailIndex] : null
const withUpdatedUrls = media.map((entry, i) => {
const toUploadIndex = filesToUpload.findIndex((m) => m.index === i)
if (toUploadIndex > -1) {
return { ...entry, url: uploaded[toUploadIndex]?.url }
}
return entry
})
const thumbnail = withUpdatedUrls.find((m) => m.isThumbnail)?.url
await mutateAsync(
{
images: urls.map((url) => ({ url })),
images: withUpdatedUrls.map((file) => ({ url: file.url })),
// Set thumbnail to empty string if no thumbnail is selected, as the API does not accept null
thumbnail: thumbnail || "",
},
@@ -126,26 +90,6 @@ export const EditProductMediaForm = ({ product }: ProductMediaViewProps) => {
)
})
const hasInvalidFiles = (fileList: FileType[]) => {
const invalidFile = fileList.find(
(f) => !SUPPORTED_FORMATS.includes(f.file.type)
)
if (invalidFile) {
form.setError("media", {
type: "invalid_file",
message: t("products.media.invalidFileType", {
name: invalidFile.file.name,
types: SUPPORTED_FORMATS_FILE_EXTENSIONS.join(", "),
}),
})
return true
}
return false
}
const handleCheckedChange = useCallback(
(id: string) => {
return (val: boolean) => {
@@ -221,58 +165,13 @@ export const EditProductMediaForm = ({ product }: ProductMediaViewProps) => {
</RouteFocusModal.Header>
<RouteFocusModal.Body className="flex flex-col overflow-hidden">
<div className="flex size-full flex-col-reverse lg:grid lg:grid-cols-[1fr_560px]">
<div className="bg-ui-bg-subtle size-full overflow-auto">
<div className="grid h-fit auto-rows-auto grid-cols-4 gap-6 p-6">
{fields.map((m) => {
return (
<GridItem
onCheckedChange={handleCheckedChange(m.id)}
checked={!!selection[m.id]}
key={m.field_id}
media={m}
/>
)
})}
</div>
</div>
<MediaGrid
media={fields}
onCheckedChange={handleCheckedChange}
selection={selection}
/>
<div className="bg-ui-bg-base border-b px-6 py-4 lg:border-b-0 lg:border-l">
<Form.Field
control={form.control}
name="media"
render={() => {
return (
<Form.Item>
<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.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>
</Form.Item>
)
}}
/>
<UploadMediaFormItem form={form} append={append} />
</div>
</div>
</RouteFocusModal.Body>
@@ -306,93 +205,14 @@ export const EditProductMediaForm = ({ product }: ProductMediaViewProps) => {
)
}
const GridItem = ({
media,
checked,
onCheckedChange,
}: {
media: Media
checked: boolean
onCheckedChange: (value: boolean) => void
}) => {
const [isLoading, setIsLoading] = useState(true)
const { t } = useTranslation()
const handleToggle = useCallback(() => {
onCheckedChange(!checked)
}, [checked, onCheckedChange])
return (
<button
type="button"
onClick={handleToggle}
className="shadow-elevation-card-rest hover:shadow-elevation-card-hover focus-visible:shadow-borders-focus bg-ui-bg-subtle-hover group relative aspect-square h-auto max-w-full overflow-hidden rounded-lg outline-none"
>
{media.isThumbnail && (
<div className="absolute left-2 top-2">
<Tooltip content={t("products.media.thumbnailTooltip")}>
<ThumbnailBadge />
</Tooltip>
</div>
)}
<div
className={clx(
"transition-fg absolute right-2 top-2 opacity-0 group-focus-within:opacity-100 group-hover:opacity-100 group-focus:opacity-100",
{
"opacity-100": checked,
}
)}
>
<div
className={clx(
"group relative inline-flex h-5 w-5 items-center justify-center outline-none "
)}
>
<div
className={clx(
"text-ui-fg-on-inverted bg-ui-bg-component shadow-borders-base [&_path]:shadow-details-contrast-on-bg-interactive group-disabled:text-ui-fg-disabled group-disabled:!bg-ui-bg-disabled group-disabled:!shadow-borders-base transition-fg h-[14px] w-[14px] rounded-[3px]",
{
"bg-ui-bg-interactive group-hover:bg-ui-bg-interactive shadow-borders-interactive-with-shadow":
checked,
}
)}
>
{checked && (
<div className="absolute inset-0">
<CheckMini />
</div>
)}
</div>
</div>
</div>
<AnimatePresence>
{isLoading && (
<motion.div
initial={{ opacity: 1 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0, transition: { duration: 0.5 } }}
className="bg-ui-bg-subtle-hover absolute inset-0 flex items-center justify-center"
>
<Spinner className="text-ui-fg-subtle animate-spin" />
</motion.div>
)}
</AnimatePresence>
<img
src={media.url}
onLoad={() => setIsLoading(false)}
alt=""
className="size-full object-cover object-center"
/>
</button>
)
}
const getDefaultValues = (images: Image[] | null, thumbnail: string | null) => {
const getDefaultValues = (
images: HttpTypes.AdminProductImage[] | undefined,
thumbnail: string | undefined
) => {
const media: Media[] =
images?.map((image) => ({
id: image.id,
url: image.url,
id: image.id!,
url: image.url!,
isThumbnail: image.url === thumbnail,
file: null,
})) || []
@@ -1,12 +1,11 @@
import { Product } from "@medusajs/medusa"
import { useSearchParams } from "react-router-dom"
import { EditProductMediaForm } from "../edit-product-media-form"
import { ProductMediaGallery } from "../product-media-gallery"
import { ProductMediaViewContext } from "./product-media-view-context"
import { HttpTypes } from "@medusajs/types"
type ProductMediaViewProps = {
product: Product
product: HttpTypes.AdminProduct
}
enum View {
@@ -45,7 +44,7 @@ export const ProductMediaView = ({ product }: ProductMediaViewProps) => {
)
}
const renderView = (view: View, product: Product) => {
const renderView = (view: View, product: HttpTypes.AdminProduct) => {
switch (view) {
case View.GALLERY:
return <ProductMediaGallery product={product} />