feat(admin-ui): metadata for categories in admin ui and refactor to react-hook-form (#5697)
* WIP * add metadata to category add and edit ui * remove console.log * lint * remove console.log * requested changes getDefaultCategoryValues util * change placeholder and wrap handleSubmit * Update edit-product-category.tsx --------- Co-authored-by: lulu <lucjan.grzesik@gmail.com> Co-authored-by: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Co-authored-by: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com> Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
3 files changed
+268
-111
No files matched your search
+130
-46
@@ -1,5 +1,3 @@
|
|||||||
import { useState } from "react"
|
|
||||||
|
|
||||||
import { ProductCategory } from "@medusajs/medusa"
|
import { ProductCategory } from "@medusajs/medusa"
|
||||||
import {
|
import {
|
||||||
adminProductCategoryKeys,
|
adminProductCategoryKeys,
|
||||||
@@ -17,23 +15,64 @@ import { NextSelect } from "../../../components/molecules/select/next-select"
|
|||||||
import useNotification from "../../../hooks/use-notification"
|
import useNotification from "../../../hooks/use-notification"
|
||||||
import { getErrorMessage } from "../../../utils/error-messages"
|
import { getErrorMessage } from "../../../utils/error-messages"
|
||||||
import TreeCrumbs from "../components/tree-crumbs"
|
import TreeCrumbs from "../components/tree-crumbs"
|
||||||
|
import MetadataForm, {
|
||||||
|
getSubmittableMetadata,
|
||||||
|
MetadataFormType,
|
||||||
|
} from "../../../components/forms/general/metadata-form"
|
||||||
|
import { Controller, useForm } from "react-hook-form"
|
||||||
|
import { nestedForm } from "../../../utils/nested-form"
|
||||||
|
import { TFunction } from "i18next"
|
||||||
|
import { getDefaultCategoryValues } from "../utils"
|
||||||
|
|
||||||
const visibilityOptions = (t) => [
|
export enum CategoryStatus {
|
||||||
|
Active = "active",
|
||||||
|
Inactive = "inactive",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CategoryVisibility {
|
||||||
|
Public = "public",
|
||||||
|
Private = "private",
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibilityOptions = (
|
||||||
|
t: TFunction<"translation", undefined, "translation">
|
||||||
|
) => [
|
||||||
{
|
{
|
||||||
label: t("modals-public", "Public"),
|
label: t("modals-public", "Public"),
|
||||||
value: "public",
|
value: CategoryVisibility.Public,
|
||||||
},
|
},
|
||||||
{ label: t("modals-private", "Private"), value: "private" },
|
{ label: t("modals-private", "Private"), value: CategoryVisibility.Private },
|
||||||
]
|
]
|
||||||
|
|
||||||
const statusOptions = (t) => [
|
const statusOptions = (
|
||||||
{ label: t("modals-active", "Active"), value: "active" },
|
t: TFunction<"translation", undefined, "translation">
|
||||||
{ label: t("modals-inactive", "Inactive"), value: "inactive" },
|
) => [
|
||||||
|
{ label: t("modals-active", "Active"), value: CategoryStatus.Active },
|
||||||
|
{ label: t("modals-inactive", "Inactive"), value: CategoryStatus.Inactive },
|
||||||
]
|
]
|
||||||
|
|
||||||
type CreateProductCategoryProps = {
|
type CreateProductCategoryProps = {
|
||||||
closeModal: () => void
|
closeModal: () => void
|
||||||
parentCategory?: ProductCategory
|
parentCategory?: ProductCategory
|
||||||
|
categories: ProductCategory[]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export type CategoryFormData = {
|
||||||
|
name: string
|
||||||
|
handle: string | undefined
|
||||||
|
description: string | undefined
|
||||||
|
metadata: MetadataFormType
|
||||||
|
is_active: {
|
||||||
|
value: CategoryStatus
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
is_public: {
|
||||||
|
value: CategoryVisibility
|
||||||
|
label: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,23 +84,33 @@ function CreateProductCategory(props: CreateProductCategoryProps) {
|
|||||||
const notification = useNotification()
|
const notification = useNotification()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
const [name, setName] = useState("")
|
const form = useForm<CategoryFormData>({
|
||||||
const [handle, setHandle] = useState("")
|
defaultValues: getDefaultCategoryValues(t),
|
||||||
const [description, setDescription] = useState("")
|
mode: "onChange",
|
||||||
const [isActive, setIsActive] = useState(true)
|
})
|
||||||
const [isPublic, setIsPublic] = useState(true)
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
watch,
|
||||||
|
reset,
|
||||||
|
control,
|
||||||
|
formState: { errors, isDirty, isValid, isSubmitting },
|
||||||
|
} = form
|
||||||
|
const name = watch("name", "")
|
||||||
|
|
||||||
const { mutateAsync: createProductCategory } = useAdminCreateProductCategory()
|
const { mutateAsync: createProductCategory } =
|
||||||
|
useAdminCreateProductCategory()
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const submit = handleSubmit(async (data) => {
|
||||||
try {
|
try {
|
||||||
await createProductCategory({
|
await createProductCategory({
|
||||||
name,
|
name: data.name,
|
||||||
handle,
|
handle: data.handle,
|
||||||
description,
|
description: data.description,
|
||||||
is_active: isActive,
|
is_active: data.is_active.value === CategoryStatus.Active,
|
||||||
is_internal: !isPublic,
|
is_internal: data.is_public.value === CategoryVisibility.Private,
|
||||||
parent_category_id: parentCategory?.id ?? null,
|
parent_category_id: parentCategory?.id ?? null,
|
||||||
|
metadata: getSubmittableMetadata(data.metadata),
|
||||||
})
|
})
|
||||||
// TODO: temporary here, investigate why `useAdminCreateProductCategory` doesn't invalidate this
|
// TODO: temporary here, investigate why `useAdminCreateProductCategory` doesn't invalidate this
|
||||||
await queryClient.invalidateQueries(adminProductCategoryKeys.lists())
|
await queryClient.invalidateQueries(adminProductCategoryKeys.lists())
|
||||||
@@ -83,7 +132,7 @@ function CreateProductCategory(props: CreateProductCategoryProps) {
|
|||||||
)
|
)
|
||||||
notification(t("modals-error", "Error"), errorMessage, "error")
|
notification(t("modals-error", "Error"), errorMessage, "error")
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FocusModal>
|
<FocusModal>
|
||||||
@@ -96,8 +145,8 @@ function CreateProductCategory(props: CreateProductCategoryProps) {
|
|||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={onSubmit}
|
disabled={!isDirty || !isValid || isSubmitting}
|
||||||
disabled={!name}
|
onClick={submit}
|
||||||
className="rounded-rounded"
|
className="rounded-rounded"
|
||||||
>
|
>
|
||||||
{t("modals-save-category", "Save category")}
|
{t("modals-save-category", "Save category")}
|
||||||
@@ -105,7 +154,6 @@ function CreateProductCategory(props: CreateProductCategoryProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FocusModal.Header>
|
</FocusModal.Header>
|
||||||
|
|
||||||
<FocusModal.Main className="no-scrollbar flex w-full justify-center">
|
<FocusModal.Main className="no-scrollbar flex w-full justify-center">
|
||||||
<div className="small:w-4/5 medium:w-7/12 large:w-6/12 my-16 max-w-[700px]">
|
<div className="small:w-4/5 medium:w-7/12 large:w-6/12 my-16 max-w-[700px]">
|
||||||
<h1 className="inter-xlarge-semibold text-grey-90 pb-6">
|
<h1 className="inter-xlarge-semibold text-grey-90 pb-6">
|
||||||
@@ -134,61 +182,97 @@ function CreateProductCategory(props: CreateProductCategoryProps) {
|
|||||||
<div className="mb-8 flex justify-between gap-6">
|
<div className="mb-8 flex justify-between gap-6">
|
||||||
<InputField
|
<InputField
|
||||||
required
|
required
|
||||||
label={t("modals-name", "Name")}
|
label={t("modals-name", "Name") as string}
|
||||||
type="string"
|
type="string"
|
||||||
name="name"
|
|
||||||
value={name}
|
|
||||||
className="w-[338px]"
|
className="w-[338px]"
|
||||||
placeholder={t(
|
placeholder={
|
||||||
|
t(
|
||||||
"modals-give-this-category-a-name",
|
"modals-give-this-category-a-name",
|
||||||
"Give this category a name"
|
"Give this category a name"
|
||||||
)}
|
) as string
|
||||||
onChange={(ev) => setName(ev.target.value)}
|
}
|
||||||
|
{...register("name", { required: true })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InputField
|
<InputField
|
||||||
label={t("modals-handle", "Handle")}
|
label={t("modals-handle", "Handle") as string}
|
||||||
type="string"
|
type="string"
|
||||||
name="handle"
|
|
||||||
value={handle}
|
|
||||||
className="w-[338px]"
|
className="w-[338px]"
|
||||||
placeholder={t("modals-custom-handle", "Custom handle")}
|
placeholder={
|
||||||
onChange={(ev) => setHandle(ev.target.value)}
|
t("modals-custom-handle", "Custom handle") as string
|
||||||
|
}
|
||||||
|
{...register("handle")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<TextArea
|
<TextArea
|
||||||
label={t("modals-description", "Description")}
|
label={t("modals-description", "Description")}
|
||||||
name="description"
|
placeholder={
|
||||||
value={description}
|
t(
|
||||||
placeholder={t(
|
|
||||||
"modals-give-this-category-a-description",
|
"modals-give-this-category-a-description",
|
||||||
"Give this category a description"
|
"Give this category a description"
|
||||||
)}
|
) as string
|
||||||
onChange={(ev) => setDescription(ev.target.value)}
|
}
|
||||||
|
{...register("description")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-8 flex justify-between gap-6">
|
<div className="mb-8 flex justify-between gap-6">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
<Controller
|
||||||
|
name={"is_active"}
|
||||||
|
control={control}
|
||||||
|
rules={{ required: true }}
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
<NextSelect
|
<NextSelect
|
||||||
label={t("modals-status", "Status")}
|
{...field}
|
||||||
|
label={t("modals-status", "Status") as string}
|
||||||
|
placeholder="Choose status"
|
||||||
options={statusOptions(t)}
|
options={statusOptions(t)}
|
||||||
value={statusOptions(t)[isActive ? 0 : 1]}
|
value={
|
||||||
onChange={(o) => setIsActive(o.value === "active")}
|
statusOptions(t)[
|
||||||
|
field.value?.value === CategoryStatus.Active ? 0 : 1
|
||||||
|
]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
<Controller
|
||||||
|
name={"is_public"}
|
||||||
|
control={control}
|
||||||
|
rules={{ required: true }}
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
<NextSelect
|
<NextSelect
|
||||||
label={t("modals-visibility", "Visibility")}
|
{...field}
|
||||||
|
label={
|
||||||
|
t("modals-visibility", "Visibility") as string
|
||||||
|
}
|
||||||
|
placeholder="Choose visibility"
|
||||||
options={visibilityOptions(t)}
|
options={visibilityOptions(t)}
|
||||||
value={visibilityOptions(t)[isPublic ? 0 : 1]}
|
value={
|
||||||
onChange={(o) => setIsPublic(o.value === "public")}
|
visibilityOptions(t)[
|
||||||
|
field.value.value === CategoryVisibility.Public ? 0 : 1
|
||||||
|
]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-xlarge">
|
||||||
|
<h2 className="inter-base-semibold mb-base">
|
||||||
|
{t("collection-modal-metadata", "Metadata")}
|
||||||
|
</h2>
|
||||||
|
<MetadataForm form={nestedForm(form, "metadata")} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FocusModal.Main>
|
</FocusModal.Main>
|
||||||
</FocusModal>
|
</FocusModal>
|
||||||
|
|||||||
+106
-65
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from "react"
|
import { useEffect } from "react"
|
||||||
|
|
||||||
import { ProductCategory } from "@medusajs/medusa"
|
import { ProductCategory } from "@medusajs/medusa"
|
||||||
import { useAdminUpdateProductCategory } from "medusa-react"
|
import { useAdminUpdateProductCategory } from "medusa-react"
|
||||||
@@ -15,26 +15,33 @@ import useNotification from "../../../hooks/use-notification"
|
|||||||
import { Option } from "../../../types/shared"
|
import { Option } from "../../../types/shared"
|
||||||
import { getErrorMessage } from "../../../utils/error-messages"
|
import { getErrorMessage } from "../../../utils/error-messages"
|
||||||
import TreeCrumbs from "../components/tree-crumbs"
|
import TreeCrumbs from "../components/tree-crumbs"
|
||||||
|
import MetadataForm, {
|
||||||
|
getSubmittableMetadata,
|
||||||
|
} from "../../../components/forms/general/metadata-form"
|
||||||
|
import { Controller, useForm } from "react-hook-form"
|
||||||
|
import { nestedForm } from "../../../utils/nested-form"
|
||||||
|
import { CategoryFormData, CategoryStatus, CategoryVisibility } from "./add-product-category"
|
||||||
|
import { getDefaultCategoryValues } from "../utils"
|
||||||
|
|
||||||
const visibilityOptions: (t: TFunction) => Option[] = (t) => [
|
const visibilityOptions: (t: TFunction) => Option[] = (t) => [
|
||||||
{
|
{
|
||||||
label: "Public",
|
label: "Public",
|
||||||
value: "public",
|
value: CategoryVisibility.Public,
|
||||||
},
|
},
|
||||||
{ label: "Private", value: "private" },
|
{ label: "Private", value: CategoryVisibility.Private },
|
||||||
]
|
]
|
||||||
|
|
||||||
const statusOptions: (t: TFunction) => Option[] = (t) => [
|
const statusOptions: (t: TFunction) => Option[] = (t) => [
|
||||||
{ label: "Active", value: "active" },
|
{ label: "Active", value: CategoryStatus.Active },
|
||||||
{ label: "Inactive", value: "inactive" },
|
{ label: "Inactive", value: CategoryStatus.Inactive },
|
||||||
]
|
]
|
||||||
|
|
||||||
type EditProductCategoriesSideModalProps = {
|
type EditProductCategoriesSideModalProps = {
|
||||||
activeCategory: ProductCategory
|
activeCategory: ProductCategory
|
||||||
close: () => void
|
close: () => void
|
||||||
isVisible: boolean
|
isVisible: boolean
|
||||||
|
categories: ProductCategory[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modal for editing product categories
|
* Modal for editing product categories
|
||||||
*/
|
*/
|
||||||
@@ -43,12 +50,6 @@ function EditProductCategoriesSideModal(
|
|||||||
) {
|
) {
|
||||||
const { isVisible, close, activeCategory, categories } = props
|
const { isVisible, close, activeCategory, categories } = props
|
||||||
|
|
||||||
const [name, setName] = useState("")
|
|
||||||
const [handle, setHandle] = useState("")
|
|
||||||
const [description, setDescription] = useState("")
|
|
||||||
const [isActive, setIsActive] = useState(true)
|
|
||||||
const [isPublic, setIsPublic] = useState(true)
|
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const notification = useNotification()
|
const notification = useNotification()
|
||||||
|
|
||||||
@@ -56,24 +57,37 @@ function EditProductCategoriesSideModal(
|
|||||||
activeCategory?.id
|
activeCategory?.id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const form = useForm<CategoryFormData>({
|
||||||
|
defaultValues: getDefaultCategoryValues(t, activeCategory),
|
||||||
|
mode: "onChange",
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
watch,
|
||||||
|
reset,
|
||||||
|
control,
|
||||||
|
formState: { errors, isDirty, isValid, isSubmitting },
|
||||||
|
} = form
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeCategory) {
|
if (activeCategory) {
|
||||||
setName(activeCategory.name)
|
reset(getDefaultCategoryValues(t, activeCategory))
|
||||||
setHandle(activeCategory.handle)
|
|
||||||
setDescription(activeCategory.description)
|
|
||||||
setIsActive(activeCategory.is_active)
|
|
||||||
setIsPublic(!activeCategory.is_internal)
|
|
||||||
}
|
}
|
||||||
}, [activeCategory])
|
}, [activeCategory, reset])
|
||||||
|
|
||||||
const onSave = async () => {
|
const onSave = async (data: CategoryFormData) => {
|
||||||
try {
|
try {
|
||||||
await updateCategory({
|
await updateCategory({
|
||||||
name,
|
name: data.name,
|
||||||
handle,
|
handle: data.handle,
|
||||||
description,
|
description: data.description,
|
||||||
is_active: isActive,
|
is_active: data.is_active.value === CategoryStatus.Active,
|
||||||
is_internal: !isPublic,
|
is_internal: data.is_public.value === CategoryVisibility.Private,
|
||||||
|
metadata: getSubmittableMetadata(data.metadata),
|
||||||
})
|
})
|
||||||
|
|
||||||
notification(
|
notification(
|
||||||
@@ -102,21 +116,32 @@ function EditProductCategoriesSideModal(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SideModal close={onClose} isVisible={!!isVisible}>
|
<SideModal close={onClose} isVisible={!!isVisible}>
|
||||||
<div className="flex h-full flex-col justify-between">
|
<div className="flex h-full flex-col justify-between overflow-auto">
|
||||||
{/* === HEADER === */}
|
{/* === HEADER === */}
|
||||||
<div className="flex items-center justify-between p-6">
|
<div className="flex items-center justify-between p-6">
|
||||||
<h3 className="inter-large-semibold flex items-center gap-2 text-xl text-gray-900">
|
|
||||||
{t("modals-edit-product-category", "Edit product category")}
|
|
||||||
</h3>
|
|
||||||
<Button
|
<Button
|
||||||
|
size="small"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="h-8 w-8 p-2"
|
className="h-8 w-8 p-2"
|
||||||
onClick={props.close}
|
onClick={props.close}
|
||||||
>
|
>
|
||||||
<CrossIcon size={20} className="text-grey-50" />
|
<CrossIcon size={20} className="text-grey-50" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<div className="gap-x-small flex">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="primary"
|
||||||
|
disabled={!isDirty || !isValid || isSubmitting}
|
||||||
|
onClick={handleSubmit(onSave)}
|
||||||
|
className="rounded-rounded"
|
||||||
|
>
|
||||||
|
{t("modals-save-category", "Save category")}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 className="inter-large-semibold flex items-center gap-2 text-xl text-gray-900 px-6">
|
||||||
|
{t("modals-edit-product-category", "Edit product category")}
|
||||||
|
</h3>
|
||||||
{/* === DIVIDER === */}
|
{/* === DIVIDER === */}
|
||||||
<div className="block h-[1px] bg-gray-200" />
|
<div className="block h-[1px] bg-gray-200" />
|
||||||
|
|
||||||
@@ -129,68 +154,84 @@ function EditProductCategoriesSideModal(
|
|||||||
<div className="flex-grow px-6">
|
<div className="flex-grow px-6">
|
||||||
<InputField
|
<InputField
|
||||||
required
|
required
|
||||||
label={t("modals-name", "Name")}
|
label={t("modals-name", "Name") as string}
|
||||||
type="string"
|
type="string"
|
||||||
name="name"
|
|
||||||
value={name}
|
|
||||||
className="my-6"
|
className="my-6"
|
||||||
placeholder={t(
|
placeholder={
|
||||||
|
t(
|
||||||
"modals-give-this-category-a-name",
|
"modals-give-this-category-a-name",
|
||||||
"Give this category a name"
|
"Give this category a name"
|
||||||
)}
|
) as string
|
||||||
onChange={(ev) => setName(ev.target.value)}
|
}
|
||||||
|
{...register("name", { required: true })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InputField
|
<InputField
|
||||||
required
|
label={t("modals-handle", "Handle") as string}
|
||||||
label={t("modals-handle", "Handle")}
|
|
||||||
type="string"
|
|
||||||
name="handle"
|
|
||||||
value={handle}
|
|
||||||
className="my-6"
|
className="my-6"
|
||||||
placeholder={t("modals-custom-handle", "Custom handle")}
|
type="string"
|
||||||
onChange={(ev) => setHandle(ev.target.value)}
|
placeholder={
|
||||||
|
t("modals-custom-handle", "Custom handle") as string
|
||||||
|
}
|
||||||
|
{...register("handle")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextArea
|
<TextArea
|
||||||
label={t("modals-description", "Description")}
|
label={t("modals-description", "Description")}
|
||||||
name="description"
|
|
||||||
value={description}
|
|
||||||
className="my-6"
|
className="my-6"
|
||||||
placeholder={t(
|
placeholder={
|
||||||
|
t(
|
||||||
"modals-give-this-category-a-description",
|
"modals-give-this-category-a-description",
|
||||||
"Give this category a description"
|
"Give this category a description"
|
||||||
)}
|
) as string
|
||||||
onChange={(ev) => setDescription(ev.target.value)}
|
}
|
||||||
|
{...register("description")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="is_active"
|
||||||
|
control={control}
|
||||||
|
rules={{ required: true }}
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
<NextSelect
|
<NextSelect
|
||||||
label={t("modals-status", "Status")}
|
{...field}
|
||||||
|
label={t("modals-status", "Status") as string}
|
||||||
|
placeholder="Choose status"
|
||||||
options={statusOptions(t)}
|
options={statusOptions(t)}
|
||||||
value={statusOptions(t)[isActive ? 0 : 1]}
|
value={
|
||||||
onChange={(o) => setIsActive(o.value === "active")}
|
statusOptions(t)[field.value?.value === CategoryStatus.Active ? 0 : 1]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="is_public"
|
||||||
|
control={control}
|
||||||
|
rules={{ required: true }}
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
<NextSelect
|
<NextSelect
|
||||||
|
{...field}
|
||||||
className="my-6"
|
className="my-6"
|
||||||
label={t("modals-visibility", "Visibility")}
|
label={t("modals-visibility", "Visibility") as string}
|
||||||
|
placeholder="Choose visibility"
|
||||||
options={visibilityOptions(t)}
|
options={visibilityOptions(t)}
|
||||||
value={visibilityOptions(t)[isPublic ? 0 : 1]}
|
value={
|
||||||
onChange={(o) => setIsPublic(o.value === "public")}
|
visibilityOptions(t)[field.value.value === CategoryVisibility.Public ? 0 : 1]
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="mt-small mb-xlarge">
|
||||||
|
<h2 className="inter-base-semibold mb-base">
|
||||||
|
{t("collection-modal-metadata", "Metadata")}
|
||||||
|
</h2>
|
||||||
|
<MetadataForm form={nestedForm(form, "metadata")} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* === DIVIDER === */}
|
|
||||||
<div className="block h-[1px] bg-gray-200" />
|
|
||||||
|
|
||||||
{/* === FOOTER === */}
|
|
||||||
<div className="flex justify-end gap-2 p-3">
|
|
||||||
<Button size="small" variant="ghost" onClick={onClose}>
|
|
||||||
{t("modals-cancel", "Cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button size="small" variant="primary" onClick={onSave}>
|
|
||||||
{t("modals-save-and-close", "Save and close")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SideModal>
|
</SideModal>
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { ProductCategory } from "@medusajs/medusa"
|
||||||
|
import { TFunction } from "i18next"
|
||||||
|
import { CategoryFormData, CategoryStatus, CategoryVisibility } from "../modals/add-product-category"
|
||||||
export const flattenCategoryTree = (rootCategories) => {
|
export const flattenCategoryTree = (rootCategories) => {
|
||||||
return rootCategories.reduce((acc, category) => {
|
return rootCategories.reduce((acc, category) => {
|
||||||
if (category?.category_children.length) {
|
if (category?.category_children.length) {
|
||||||
@@ -29,3 +32,32 @@ export const getAncestors = (targetNode, nodes, acc = []) => {
|
|||||||
|
|
||||||
return acc
|
return acc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getDefaultCategoryValues = (t: TFunction, category?: ProductCategory): CategoryFormData => {
|
||||||
|
return {
|
||||||
|
name: category?.name || "",
|
||||||
|
handle: category?.handle || "",
|
||||||
|
description: category?.description || "",
|
||||||
|
metadata: {
|
||||||
|
entries: Object.entries(category?.metadata || {}).map(
|
||||||
|
([key, value]) => ({
|
||||||
|
key,
|
||||||
|
value: value as string,
|
||||||
|
state: "existing",
|
||||||
|
})
|
||||||
|
),
|
||||||
|
},
|
||||||
|
is_active: {
|
||||||
|
value: category?.is_active ? (category?.is_active ? CategoryStatus.Active : CategoryStatus.Inactive) : CategoryStatus.Active,
|
||||||
|
label: category?.is_active
|
||||||
|
? t("modals-active", "Public") as string
|
||||||
|
: t("modals-inactive", "Inactive") as string,
|
||||||
|
},
|
||||||
|
is_public: {
|
||||||
|
value: category?.is_internal ? (category?.is_internal ? CategoryVisibility.Private : CategoryVisibility.Public) : CategoryVisibility.Public,
|
||||||
|
label: category?.is_internal
|
||||||
|
? t("modals-private", "Private") as string
|
||||||
|
: t("modals-public", "Public") as string,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user