fix(dashboard): Fix spacing, media, and missing tip in product create form (#8338)
Resolves CC-146, CC-109
This commit is contained in:
@@ -334,7 +334,8 @@
|
|||||||
"productVariants": {
|
"productVariants": {
|
||||||
"label": "Product variants",
|
"label": "Product variants",
|
||||||
"hint": "This ranking will affect the variants' order in your storefront.",
|
"hint": "This ranking will affect the variants' order in your storefront.",
|
||||||
"alert": "Add options to create variants."
|
"alert": "Add options to create variants.",
|
||||||
|
"tip": "Variants left unchecked won't be created. You can always create and edit variants afterwards but this list fits the variations in your product options."
|
||||||
},
|
},
|
||||||
"productOptions": {
|
"productOptions": {
|
||||||
"label": "Product options",
|
"label": "Product options",
|
||||||
@@ -388,6 +389,7 @@
|
|||||||
"media": {
|
"media": {
|
||||||
"label": "Media",
|
"label": "Media",
|
||||||
"editHint": "Add media to the product to showcase it in your storefront.",
|
"editHint": "Add media to the product to showcase it in your storefront.",
|
||||||
|
"makeThumbnail": "Make thumbnail",
|
||||||
"uploadImagesLabel": "Upload images",
|
"uploadImagesLabel": "Upload images",
|
||||||
"uploadImagesHint": "Drag and drop images here or click to upload.",
|
"uploadImagesHint": "Drag and drop images here or click to upload.",
|
||||||
"invalidFileType": "'{{name}}' is not a supported file type. Supported file types are: {{types}}.",
|
"invalidFileType": "'{{name}}' is not a supported file type. Supported file types are: {{types}}.",
|
||||||
|
|||||||
+10
-6
@@ -1,16 +1,16 @@
|
|||||||
|
import { UseFormReturn } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { z } from "zod"
|
||||||
import {
|
import {
|
||||||
FileType,
|
FileType,
|
||||||
FileUpload,
|
FileUpload,
|
||||||
} from "../../../../../components/common/file-upload"
|
} from "../../../../../components/common/file-upload"
|
||||||
import { UseFormReturn } from "react-hook-form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import { MediaSchema } from "../../../product-create/constants"
|
||||||
import {
|
import {
|
||||||
EditProductMediaSchemaType,
|
EditProductMediaSchemaType,
|
||||||
ProductCreateSchemaType,
|
ProductCreateSchemaType,
|
||||||
} from "../../../product-create/types"
|
} from "../../../product-create/types"
|
||||||
import { MediaSchema } from "../../../product-create/constants"
|
|
||||||
import { z } from "zod"
|
|
||||||
|
|
||||||
type Media = z.infer<typeof MediaSchema>
|
type Media = z.infer<typeof MediaSchema>
|
||||||
|
|
||||||
@@ -35,11 +35,13 @@ const SUPPORTED_FORMATS_FILE_EXTENSIONS = [
|
|||||||
export const UploadMediaFormItem = ({
|
export const UploadMediaFormItem = ({
|
||||||
form,
|
form,
|
||||||
append,
|
append,
|
||||||
|
showHint = true,
|
||||||
}: {
|
}: {
|
||||||
form:
|
form:
|
||||||
| UseFormReturn<ProductCreateSchemaType>
|
| UseFormReturn<ProductCreateSchemaType>
|
||||||
| UseFormReturn<EditProductMediaSchemaType>
|
| UseFormReturn<EditProductMediaSchemaType>
|
||||||
append: (value: Media) => void
|
append: (value: Media) => void
|
||||||
|
showHint?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
@@ -72,10 +74,12 @@ export const UploadMediaFormItem = ({
|
|||||||
render={() => {
|
render={() => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<div className="flex flex-col gap-y-4">
|
<div className="flex flex-col gap-y-2">
|
||||||
<div className="flex flex-col gap-y-1">
|
<div className="flex flex-col gap-y-1">
|
||||||
<Form.Label optional>{t("products.media.label")}</Form.Label>
|
<Form.Label optional>{t("products.media.label")}</Form.Label>
|
||||||
<Form.Hint>{t("products.media.editHint")}</Form.Hint>
|
{showHint && (
|
||||||
|
<Form.Hint>{t("products.media.editHint")}</Form.Hint>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<FileUpload
|
<FileUpload
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ export const ProductCreateGeneralSection = ({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="general" className="flex flex-col gap-y-8">
|
<div id="general" className="flex flex-col gap-y-6">
|
||||||
<div className="flex flex-col gap-y-2">
|
<div className="flex flex-col gap-y-2">
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||||
<Form.Field
|
<Form.Field
|
||||||
|
|||||||
+159
-54
@@ -1,10 +1,11 @@
|
|||||||
import { CommandBar, Heading } from "@medusajs/ui"
|
import { StackPerspective, ThumbnailBadge, Trash, XMark } from "@medusajs/icons"
|
||||||
|
import { IconButton, Text } from "@medusajs/ui"
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
import { UseFormReturn, useFieldArray } from "react-hook-form"
|
import { UseFormReturn, useFieldArray } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { ProductCreateSchemaType } from "../../../../types"
|
import { ActionMenu } from "../../../../../../../components/common/action-menu"
|
||||||
import { MediaGrid } from "../../../../../common/components/media-grid-view"
|
|
||||||
import { useCallback, useState } from "react"
|
|
||||||
import { UploadMediaFormItem } from "../../../../../common/components/upload-media-form-item"
|
import { UploadMediaFormItem } from "../../../../../common/components/upload-media-form-item"
|
||||||
|
import { ProductCreateSchemaType } from "../../../../types"
|
||||||
|
|
||||||
type ProductCreateMediaSectionProps = {
|
type ProductCreateMediaSectionProps = {
|
||||||
form: UseFormReturn<ProductCreateSchemaType>
|
form: UseFormReturn<ProductCreateSchemaType>
|
||||||
@@ -13,68 +14,172 @@ type ProductCreateMediaSectionProps = {
|
|||||||
export const ProductCreateMediaSection = ({
|
export const ProductCreateMediaSection = ({
|
||||||
form,
|
form,
|
||||||
}: ProductCreateMediaSectionProps) => {
|
}: ProductCreateMediaSectionProps) => {
|
||||||
const { t } = useTranslation()
|
|
||||||
const [selection, setSelection] = useState<Record<string, true>>({})
|
|
||||||
const selectionCount = Object.keys(selection).length
|
|
||||||
|
|
||||||
const { fields, append, remove } = useFieldArray({
|
const { fields, append, remove } = useFieldArray({
|
||||||
name: "media",
|
name: "media",
|
||||||
control: form.control,
|
control: form.control,
|
||||||
keyName: "field_id",
|
keyName: "field_id",
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleDelete = () => {
|
const getOnDelete = (index: number) => {
|
||||||
const ids = Object.keys(selection)
|
return () => {
|
||||||
const indices = ids.map((id) => fields.findIndex((m) => m.id === id))
|
remove(index)
|
||||||
|
}
|
||||||
remove(indices)
|
|
||||||
setSelection({})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCheckedChange = useCallback(
|
const getMakeThumbnail = (index: number) => {
|
||||||
(id: string) => {
|
return () => {
|
||||||
return (val: boolean) => {
|
const newFields = fields.map((field, i) => {
|
||||||
if (!val) {
|
return {
|
||||||
const { [id]: _, ...rest } = selection
|
...field,
|
||||||
setSelection(rest)
|
isThumbnail: i === index,
|
||||||
} else {
|
|
||||||
setSelection((prev) => ({ ...prev, [id]: true }))
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
},
|
|
||||||
[selection]
|
form.setValue("media", newFields, {
|
||||||
)
|
shouldDirty: true,
|
||||||
|
shouldTouch: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getItemHandlers = (index: number) => {
|
||||||
|
return {
|
||||||
|
onDelete: getOnDelete(index),
|
||||||
|
onMakeThumbnail: getMakeThumbnail(index),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="media" className="flex flex-col gap-y-8">
|
<div id="media" className="flex flex-col gap-y-2">
|
||||||
<Heading level="h2">{t("products.media.label")}</Heading>
|
<UploadMediaFormItem form={form} append={append} showHint={false} />
|
||||||
<div className="grid grid-cols-1 gap-x-4 gap-y-8">
|
<ul className="flex flex-col gap-y-2">
|
||||||
<UploadMediaFormItem form={form} append={append} />
|
{fields.map((field, index) => {
|
||||||
</div>
|
const { onDelete, onMakeThumbnail } = getItemHandlers(index)
|
||||||
{fields?.length ? (
|
|
||||||
<MediaGrid
|
|
||||||
media={fields}
|
|
||||||
selection={selection}
|
|
||||||
onCheckedChange={handleCheckedChange}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<CommandBar open={!!selectionCount}>
|
return (
|
||||||
<CommandBar.Bar>
|
<MediaItem
|
||||||
<CommandBar.Value>
|
key={field.id}
|
||||||
{t("general.countSelected", {
|
field={field}
|
||||||
count: selectionCount,
|
onDelete={onDelete}
|
||||||
})}
|
onMakeThumbnail={onMakeThumbnail}
|
||||||
</CommandBar.Value>
|
/>
|
||||||
<CommandBar.Seperator />
|
)
|
||||||
|
})}
|
||||||
<CommandBar.Command
|
</ul>
|
||||||
action={handleDelete}
|
|
||||||
label={t("actions.delete")}
|
|
||||||
shortcut="d"
|
|
||||||
/>
|
|
||||||
</CommandBar.Bar>
|
|
||||||
</CommandBar>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MediaField = {
|
||||||
|
isThumbnail: boolean
|
||||||
|
url: string
|
||||||
|
id?: string | undefined
|
||||||
|
file?: File
|
||||||
|
field_id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type MediaItemProps = {
|
||||||
|
field: MediaField
|
||||||
|
onDelete: () => void
|
||||||
|
onMakeThumbnail: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const MediaItem = ({ field, onDelete, onMakeThumbnail }: MediaItemProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
if (!field.file) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className="bg-ui-bg-component shadow-elevation-card-rest flex items-center justify-between rounded-lg px-3 py-2">
|
||||||
|
<div className="flex items-center gap-x-3">
|
||||||
|
<div className="bg-ui-bg-base h-10 w-[30px] overflow-hidden rounded-md">
|
||||||
|
<ThumbnailPreview file={field.file} />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
{field.file.name}
|
||||||
|
</Text>
|
||||||
|
<div className="flex items-center gap-x-1">
|
||||||
|
{field.isThumbnail && <ThumbnailBadge />}
|
||||||
|
<Text size="xsmall" leading="compact" className="text-ui-fg-subtle">
|
||||||
|
{formatFileSize(field.file.size)}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-x-1">
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("products.media.makeThumbnail"),
|
||||||
|
icon: <StackPerspective />,
|
||||||
|
onClick: onMakeThumbnail,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: <Trash />,
|
||||||
|
label: t("actions.delete"),
|
||||||
|
onClick: onDelete,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
type="button"
|
||||||
|
size="small"
|
||||||
|
variant="transparent"
|
||||||
|
onClick={onDelete}
|
||||||
|
>
|
||||||
|
<XMark />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThumbnailPreview = ({ file }: { file?: File | null }) => {
|
||||||
|
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (file) {
|
||||||
|
const objectUrl = URL.createObjectURL(file)
|
||||||
|
setThumbnailUrl(objectUrl)
|
||||||
|
|
||||||
|
return () => URL.revokeObjectURL(objectUrl)
|
||||||
|
}
|
||||||
|
}, [file])
|
||||||
|
|
||||||
|
if (!thumbnailUrl) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={thumbnailUrl}
|
||||||
|
alt=""
|
||||||
|
className="size-full object-cover object-center"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFileSize(bytes: number, decimalPlaces: number = 2): string {
|
||||||
|
if (bytes === 0) {
|
||||||
|
return "0 Bytes"
|
||||||
|
}
|
||||||
|
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
|
||||||
|
return (
|
||||||
|
parseFloat((bytes / Math.pow(k, i)).toFixed(decimalPlaces)) + " " + sizes[i]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
+120
-109
@@ -8,7 +8,6 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
Switch,
|
|
||||||
Text,
|
Text,
|
||||||
clx,
|
clx,
|
||||||
} from "@medusajs/ui"
|
} from "@medusajs/ui"
|
||||||
@@ -22,6 +21,7 @@ import {
|
|||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
import { Form } from "../../../../../../../components/common/form"
|
import { Form } from "../../../../../../../components/common/form"
|
||||||
|
import { InlineTip } from "../../../../../../../components/common/inline-tip"
|
||||||
import { SortableList } from "../../../../../../../components/common/sortable-list"
|
import { SortableList } from "../../../../../../../components/common/sortable-list"
|
||||||
import { SwitchBox } from "../../../../../../../components/common/switch-box"
|
import { SwitchBox } from "../../../../../../../components/common/switch-box"
|
||||||
import { ChipInput } from "../../../../../../../components/inputs/chip-input"
|
import { ChipInput } from "../../../../../../../components/inputs/chip-input"
|
||||||
@@ -285,26 +285,28 @@ export const ProductCreateVariantsSection = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="variants" className="flex flex-col gap-y-8">
|
<div id="variants" className="flex flex-col gap-y-8">
|
||||||
<Heading level="h2">{t("products.create.variants.header")}</Heading>
|
<div className="flex flex-col gap-y-6">
|
||||||
<SwitchBox
|
<Heading level="h2">{t("products.create.variants.header")}</Heading>
|
||||||
control={form.control}
|
<SwitchBox
|
||||||
name="enable_variants"
|
control={form.control}
|
||||||
label={t("products.create.variants.subHeadingTitle")}
|
name="enable_variants"
|
||||||
description={t("products.create.variants.subHeadingDescription")}
|
label={t("products.create.variants.subHeadingTitle")}
|
||||||
onCheckedChange={(checked) => {
|
description={t("products.create.variants.subHeadingDescription")}
|
||||||
if (checked) {
|
onCheckedChange={(checked) => {
|
||||||
form.setValue("options", [
|
if (checked) {
|
||||||
{
|
form.setValue("options", [
|
||||||
title: "",
|
{
|
||||||
values: [],
|
title: "",
|
||||||
},
|
values: [],
|
||||||
])
|
},
|
||||||
form.setValue("variants", [])
|
])
|
||||||
} else {
|
form.setValue("variants", [])
|
||||||
createDefaultOptionAndVariant()
|
} else {
|
||||||
}
|
createDefaultOptionAndVariant()
|
||||||
}}
|
}
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{watchedAreVariantsEnabled && (
|
{watchedAreVariantsEnabled && (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-y-6">
|
<div className="flex flex-col gap-y-6">
|
||||||
@@ -314,7 +316,7 @@ export const ProductCreateVariantsSection = ({
|
|||||||
render={() => {
|
render={() => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<div className="flex flex-col gap-y-4">
|
<div className="flex flex-col gap-y-6">
|
||||||
<div className="flex items-start justify-between gap-x-4">
|
<div className="flex items-start justify-between gap-x-4">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<Form.Label>
|
<Form.Label>
|
||||||
@@ -426,94 +428,103 @@ export const ProductCreateVariantsSection = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 gap-x-4 gap-y-4">
|
<div className="grid grid-cols-1 gap-x-4 gap-y-8">
|
||||||
<div className="flex flex-col gap-y-1">
|
<div className="flex flex-col gap-y-6">
|
||||||
<Label weight="plus">
|
<div className="flex flex-col">
|
||||||
{t("products.create.variants.productVariants.label")}
|
<Label weight="plus">
|
||||||
</Label>
|
{t("products.create.variants.productVariants.label")}
|
||||||
<Hint>{t("products.create.variants.productVariants.hint")}</Hint>
|
</Label>
|
||||||
</div>
|
<Hint>
|
||||||
{!showInvalidOptionsMessage && showInvalidVariantsMessage && (
|
{t("products.create.variants.productVariants.hint")}
|
||||||
<Alert dismissible variant="error">
|
</Hint>
|
||||||
{t("products.create.errors.variants")}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
{variants.fields.length > 0 ? (
|
|
||||||
<div className="overflow-hidden rounded-xl border">
|
|
||||||
<div
|
|
||||||
className="bg-ui-bg-component text-ui-fg-subtle grid items-center gap-3 border-b px-6 py-3.5"
|
|
||||||
style={{
|
|
||||||
gridTemplateColumns: `20px 28px repeat(${watchedOptions.length}, 1fr)`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<Checkbox
|
|
||||||
checked={getCheckboxState(watchedVariants)}
|
|
||||||
onCheckedChange={onCheckboxChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div />
|
|
||||||
{watchedOptions.map((option, index) => (
|
|
||||||
<div key={index}>
|
|
||||||
<Text size="small" leading="compact" weight="plus">
|
|
||||||
{option.title}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<SortableList
|
|
||||||
items={variants.fields}
|
|
||||||
onChange={handleRankChange}
|
|
||||||
renderItem={(item, index) => {
|
|
||||||
return (
|
|
||||||
<SortableList.Item
|
|
||||||
id={item.id}
|
|
||||||
className={clx("bg-ui-bg-base border-b", {
|
|
||||||
"border-b-0": index === variants.fields.length - 1,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="text-ui-fg-subtle grid w-full items-center gap-3 px-6 py-3.5"
|
|
||||||
style={{
|
|
||||||
gridTemplateColumns: `20px 28px repeat(${watchedOptions.length}, 1fr)`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name={`variants.${index}.should_create` as const}
|
|
||||||
render={({
|
|
||||||
field: { value, onChange, ...field },
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Control>
|
|
||||||
<Checkbox
|
|
||||||
{...field}
|
|
||||||
checked={value}
|
|
||||||
onCheckedChange={onChange}
|
|
||||||
/>
|
|
||||||
</Form.Control>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<SortableList.DragHandle />
|
|
||||||
{Object.values(item.options).map((value, index) => (
|
|
||||||
<Text key={index} size="small" leading="compact">
|
|
||||||
{value}
|
|
||||||
</Text>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</SortableList.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
{!showInvalidOptionsMessage && showInvalidVariantsMessage && (
|
||||||
<Alert>
|
<Alert dismissible variant="error">
|
||||||
{t("products.create.variants.productVariants.alert")}
|
{t("products.create.errors.variants")}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
{variants.fields.length > 0 ? (
|
||||||
|
<div className="overflow-hidden rounded-xl border">
|
||||||
|
<div
|
||||||
|
className="bg-ui-bg-component text-ui-fg-subtle grid items-center gap-3 border-b px-6 py-2.5"
|
||||||
|
style={{
|
||||||
|
gridTemplateColumns: `20px 28px repeat(${watchedOptions.length}, 1fr)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Checkbox
|
||||||
|
checked={getCheckboxState(watchedVariants)}
|
||||||
|
onCheckedChange={onCheckboxChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div />
|
||||||
|
{watchedOptions.map((option, index) => (
|
||||||
|
<div key={index}>
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
{option.title}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<SortableList
|
||||||
|
items={variants.fields}
|
||||||
|
onChange={handleRankChange}
|
||||||
|
renderItem={(item, index) => {
|
||||||
|
return (
|
||||||
|
<SortableList.Item
|
||||||
|
id={item.id}
|
||||||
|
className={clx("bg-ui-bg-base border-b", {
|
||||||
|
"border-b-0": index === variants.fields.length - 1,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="text-ui-fg-subtle grid w-full items-center gap-3 px-6 py-2.5"
|
||||||
|
style={{
|
||||||
|
gridTemplateColumns: `20px 28px repeat(${watchedOptions.length}, 1fr)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name={`variants.${index}.should_create` as const}
|
||||||
|
render={({
|
||||||
|
field: { value, onChange, ...field },
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Control>
|
||||||
|
<Checkbox
|
||||||
|
{...field}
|
||||||
|
checked={value}
|
||||||
|
onCheckedChange={onChange}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SortableList.DragHandle />
|
||||||
|
{Object.values(item.options).map((value, index) => (
|
||||||
|
<Text key={index} size="small" leading="compact">
|
||||||
|
{value}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</SortableList.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Alert>
|
||||||
|
{t("products.create.variants.productVariants.alert")}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
{variants.fields.length > 0 && (
|
||||||
|
<InlineTip variant="tip">
|
||||||
|
{t("products.create.variants.productVariants.tip")}
|
||||||
|
</InlineTip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+5
-4
@@ -5,8 +5,8 @@ import { useTranslation } from "react-i18next"
|
|||||||
import { Divider } from "../../../../../components/common/divider"
|
import { Divider } from "../../../../../components/common/divider"
|
||||||
import { ProductCreateSchemaType } from "../../types"
|
import { ProductCreateSchemaType } from "../../types"
|
||||||
import { ProductCreateGeneralSection } from "./components/product-create-details-general-section"
|
import { ProductCreateGeneralSection } from "./components/product-create-details-general-section"
|
||||||
import { ProductCreateVariantsSection } from "./components/product-create-details-variant-section"
|
|
||||||
import { ProductCreateMediaSection } from "./components/product-create-details-media-section"
|
import { ProductCreateMediaSection } from "./components/product-create-details-media-section"
|
||||||
|
import { ProductCreateVariantsSection } from "./components/product-create-details-variant-section"
|
||||||
|
|
||||||
type ProductAttributesProps = {
|
type ProductAttributesProps = {
|
||||||
form: UseFormReturn<ProductCreateSchemaType>
|
form: UseFormReturn<ProductCreateSchemaType>
|
||||||
@@ -17,11 +17,12 @@ export const ProductCreateDetailsForm = ({ form }: ProductAttributesProps) => {
|
|||||||
<div className="flex flex-col items-center p-16">
|
<div className="flex flex-col items-center p-16">
|
||||||
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
||||||
<Header />
|
<Header />
|
||||||
<ProductCreateGeneralSection form={form} />
|
<div className="flex flex-col gap-y-6">
|
||||||
|
<ProductCreateGeneralSection form={form} />
|
||||||
|
<ProductCreateMediaSection form={form} />
|
||||||
|
</div>
|
||||||
<Divider />
|
<Divider />
|
||||||
<ProductCreateVariantsSection form={form} />
|
<ProductCreateVariantsSection form={form} />
|
||||||
<Divider />
|
|
||||||
<ProductCreateMediaSection form={form} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
@@ -187,7 +187,7 @@ export const EditProductMediaForm = ({ product }: ProductMediaViewProps) => {
|
|||||||
<Fragment>
|
<Fragment>
|
||||||
<CommandBar.Command
|
<CommandBar.Command
|
||||||
action={handlePromoteToThumbnail}
|
action={handlePromoteToThumbnail}
|
||||||
label={"Make thumbnail"}
|
label={t("products.media.makeThumbnail")}
|
||||||
shortcut="t"
|
shortcut="t"
|
||||||
/>
|
/>
|
||||||
<CommandBar.Seperator />
|
<CommandBar.Seperator />
|
||||||
|
|||||||
Reference in New Issue
Block a user