Skip to content

Commit

Permalink
fix(dashboard): Fix spacing, media, and missing tip in product create…
Browse files Browse the repository at this point in the history
… form (#8338)

Resolves CC-146, CC-109
  • Loading branch information
kasperkristensen authored Jul 31, 2024
1 parent 9de1d8c commit 4fda46d
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 173 deletions.
4 changes: 3 additions & 1 deletion packages/admin-next/dashboard/src/i18n/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,8 @@
"productVariants": {
"label": "Product variants",
"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": {
"label": "Product options",
Expand Down Expand Up @@ -388,6 +389,7 @@
"media": {
"label": "Media",
"editHint": "Add media to the product to showcase it in your storefront.",
"makeThumbnail": "Make thumbnail",
"uploadImagesLabel": "Upload images",
"uploadImagesHint": "Drag and drop images here or click to upload.",
"invalidFileType": "'{{name}}' is not a supported file type. Supported file types are: {{types}}.",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { UseFormReturn } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { Form } from "../../../../../components/common/form"
import { z } from "zod"
import {
FileType,
FileUpload,
} from "../../../../../components/common/file-upload"
import { UseFormReturn } from "react-hook-form"
import { Form } from "../../../../../components/common/form"
import { MediaSchema } from "../../../product-create/constants"
import {
EditProductMediaSchemaType,
ProductCreateSchemaType,
} from "../../../product-create/types"
import { MediaSchema } from "../../../product-create/constants"
import { z } from "zod"

type Media = z.infer<typeof MediaSchema>

Expand All @@ -35,11 +35,13 @@ const SUPPORTED_FORMATS_FILE_EXTENSIONS = [
export const UploadMediaFormItem = ({
form,
append,
showHint = true,
}: {
form:
| UseFormReturn<ProductCreateSchemaType>
| UseFormReturn<EditProductMediaSchemaType>
append: (value: Media) => void
showHint?: boolean
}) => {
const { t } = useTranslation()

Expand Down Expand Up @@ -72,10 +74,12 @@ export const UploadMediaFormItem = ({
render={() => {
return (
<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">
<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>
<Form.Control>
<FileUpload
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const ProductCreateGeneralSection = ({
const { t } = useTranslation()

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="grid grid-cols-1 gap-4 md:grid-cols-3">
<Form.Field
Expand Down
Original file line number Diff line number Diff line change
@@ -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 { useTranslation } from "react-i18next"
import { ProductCreateSchemaType } from "../../../../types"
import { MediaGrid } from "../../../../../common/components/media-grid-view"
import { useCallback, useState } from "react"
import { ActionMenu } from "../../../../../../../components/common/action-menu"
import { UploadMediaFormItem } from "../../../../../common/components/upload-media-form-item"
import { ProductCreateSchemaType } from "../../../../types"

type ProductCreateMediaSectionProps = {
form: UseFormReturn<ProductCreateSchemaType>
Expand All @@ -13,68 +14,172 @@ type ProductCreateMediaSectionProps = {
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 getOnDelete = (index: number) => {
return () => {
remove(index)
}
}

const handleCheckedChange = useCallback(
(id: string) => {
return (val: boolean) => {
if (!val) {
const { [id]: _, ...rest } = selection
setSelection(rest)
} else {
setSelection((prev) => ({ ...prev, [id]: true }))
const getMakeThumbnail = (index: number) => {
return () => {
const newFields = fields.map((field, i) => {
return {
...field,
isThumbnail: i === index,
}
}
},
[selection]
})

form.setValue("media", newFields, {
shouldDirty: true,
shouldTouch: true,
})
}
}

const getItemHandlers = (index: number) => {
return {
onDelete: getOnDelete(index),
onMakeThumbnail: getMakeThumbnail(index),
}
}

return (
<div id="media" className="flex flex-col gap-y-2">
<UploadMediaFormItem form={form} append={append} showHint={false} />
<ul className="flex flex-col gap-y-2">
{fields.map((field, index) => {
const { onDelete, onMakeThumbnail } = getItemHandlers(index)

return (
<MediaItem
key={field.id}
field={field}
onDelete={onDelete}
onMakeThumbnail={onMakeThumbnail}
/>
)
})}
</ul>
</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 (
<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">
<UploadMediaFormItem form={form} append={append} />
<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>
{fields?.length ? (
<MediaGrid
media={fields}
selection={selection}
onCheckedChange={handleCheckedChange}
<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,
},
],
},
]}
/>
) : 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>
<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]
)
}
Loading

0 comments on commit 4fda46d

Please sign in to comment.