feat(dashboard): SwitchBox component (#7607)
**What** - In the latest design changes, the Switch component needs to be wrapped in a container with a label and description. It doesn't make sense to add this "variant" to `@medusajs/ui`, so I have created a local component that we can use every time the design calls for this setup, to avoid re-implementing it multiple times, and the design being inconsistent across usages. - Updates the Product domain forms to use the component. There are other places that needs to be updated to use it, but we can tackle those in our clean up tasks. I have checked with Ludvig and this design should be used everywhere we previously had the design shown in the first image. <img width="604" alt="image" src="https://github.com/medusajs/medusa/assets/45367945/053e40c4-fa8f-4cac-96b3-659deeb6d760"> <img width="745" alt="image" src="https://github.com/medusajs/medusa/assets/45367945/890eb28f-83ac-4fa6-968f-cc204f1acd1e">
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export * from "./switch-box"
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Switch } from "@medusajs/ui"
|
||||
import { ReactNode } from "react"
|
||||
import { ControllerProps, FieldPath, FieldValues } from "react-hook-form"
|
||||
|
||||
import { Form } from "../../common/form"
|
||||
|
||||
interface HeadlessControllerProps<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
> extends Omit<ControllerProps<TFieldValues, TName>, "render"> {}
|
||||
|
||||
interface SwitchBoxProps<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
> extends HeadlessControllerProps<TFieldValues, TName>{
|
||||
label: string
|
||||
description: string
|
||||
optional?: boolean
|
||||
tooltip?: ReactNode
|
||||
/**
|
||||
* Callback for performing additional actions when the checked state changes.
|
||||
* This does not intercept the form control, it is only used for injecting side-effects.
|
||||
*/
|
||||
onCheckedChange?: (checked: boolean) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for the Switch component to be used with `react-hook-form`.
|
||||
*
|
||||
* Use this component whenever a design calls for wrapping the Switch component
|
||||
* in a container with a label and description.
|
||||
*/
|
||||
export const SwitchBox = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
>({
|
||||
label,
|
||||
description,
|
||||
optional = false,
|
||||
tooltip,
|
||||
onCheckedChange,
|
||||
...props
|
||||
}: SwitchBoxProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<Form.Field
|
||||
{...props}
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="bg-ui-bg-component shadow-elevation-card-rest flex items-start gap-x-3 rounded-lg p-3">
|
||||
<Form.Control>
|
||||
<Switch {...field} checked={value} onCheckedChange={(e) => {
|
||||
onCheckedChange?.(e)
|
||||
onChange(e)
|
||||
}} />
|
||||
</Form.Control>
|
||||
<div>
|
||||
<Form.Label optional={optional} tooltip={tooltip}>
|
||||
{label}
|
||||
</Form.Label>
|
||||
<Form.Hint>{description}</Form.Hint>
|
||||
</div>
|
||||
</div>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
+16
-38
@@ -23,6 +23,7 @@ import { useTranslation } from "react-i18next"
|
||||
|
||||
import { Form } from "../../../../../../../components/common/form"
|
||||
import { SortableList } from "../../../../../../../components/common/sortable-list"
|
||||
import { SwitchBox } from "../../../../../../../components/common/switch-box"
|
||||
import { ChipInput } from "../../../../../../../components/inputs/chip-input"
|
||||
import { ProductCreateSchemaType } from "../../../../types"
|
||||
import { decorateVariantsWithDefaultValues } from "../../../../utils"
|
||||
@@ -285,46 +286,23 @@ export const ProductCreateVariantsSection = ({
|
||||
return (
|
||||
<div id="variants" className="flex flex-col gap-y-8">
|
||||
<Heading level="h2">{t("products.create.variants.header")}</Heading>
|
||||
<Form.Field
|
||||
<SwitchBox
|
||||
control={form.control}
|
||||
name="enable_variants"
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="shadow-elevation-card-rest bg-ui-bg-field flex flex-row gap-x-4 rounded-xl p-2">
|
||||
<Form.Control>
|
||||
<Switch
|
||||
checked={value}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
form.setValue("options", [
|
||||
{
|
||||
title: "",
|
||||
values: [],
|
||||
},
|
||||
])
|
||||
form.setValue("variants", [])
|
||||
} else {
|
||||
createDefaultOptionAndVariant()
|
||||
}
|
||||
|
||||
onChange(!!checked)
|
||||
}}
|
||||
{...field}
|
||||
className="mt-1"
|
||||
/>
|
||||
</Form.Control>
|
||||
<div className="flex flex-col">
|
||||
<Form.Label>
|
||||
{t("products.create.variants.subHeadingTitle")}
|
||||
</Form.Label>
|
||||
<Form.Hint>
|
||||
{t("products.create.variants.subHeadingDescription")}
|
||||
</Form.Hint>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Item>
|
||||
)
|
||||
label={t("products.create.variants.subHeadingTitle")}
|
||||
description={t("products.create.variants.subHeadingDescription")}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
form.setValue("options", [
|
||||
{
|
||||
title: "",
|
||||
values: [],
|
||||
},
|
||||
])
|
||||
form.setValue("variants", [])
|
||||
} else {
|
||||
createDefaultOptionAndVariant()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{watchedAreVariantsEnabled && (
|
||||
|
||||
+9
-31
@@ -1,9 +1,10 @@
|
||||
import { Button, Heading, Switch } from "@medusajs/ui"
|
||||
import { Button, Heading } from "@medusajs/ui"
|
||||
import { UseFormReturn, useFieldArray } from "react-hook-form"
|
||||
import { Trans, useTranslation } from "react-i18next"
|
||||
|
||||
import { ChipGroup } from "../../../../../../../components/common/chip-group"
|
||||
import { Form } from "../../../../../../../components/common/form"
|
||||
import { SwitchBox } from "../../../../../../../components/common/switch-box"
|
||||
import { Combobox } from "../../../../../../../components/inputs/combobox"
|
||||
import { useComboboxData } from "../../../../../../../hooks/use-combobox-data"
|
||||
import { client, sdk } from "../../../../../../../lib/client"
|
||||
@@ -64,36 +65,13 @@ export const ProductCreateOrganizationSection = ({
|
||||
return (
|
||||
<div id="organize" className="flex flex-col gap-y-8">
|
||||
<Heading>{t("products.organization.header")}</Heading>
|
||||
<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="shadow-elevation-card-rest bg-ui-bg-field flex flex-row gap-x-4 rounded-xl p-2">
|
||||
<Form.Control>
|
||||
<Switch
|
||||
{...field}
|
||||
checked={!!value}
|
||||
onCheckedChange={onChange}
|
||||
className="mt-1"
|
||||
/>
|
||||
</Form.Control>
|
||||
<div className="flex flex-col">
|
||||
<Form.Label>
|
||||
{t("products.fields.discountable.label")}
|
||||
</Form.Label>
|
||||
<Form.Hint>
|
||||
{t("products.fields.discountable.hint")}
|
||||
</Form.Hint>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<SwitchBox
|
||||
control={form.control}
|
||||
name="discountable"
|
||||
label={t("products.fields.discountable.label")}
|
||||
description={t("products.fields.discountable.hint")}
|
||||
optional
|
||||
/>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
|
||||
+3
-7
@@ -1,12 +1,8 @@
|
||||
import { Heading } from "@medusajs/ui"
|
||||
import { useState } from "react"
|
||||
import { UseFormReturn } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { Divider } from "../../../../../components/common/divider"
|
||||
import { SplitView } from "../../../../../components/layout/split-view"
|
||||
import { ProductCreateSchemaType } from "../../types"
|
||||
import { ProductCreateAttributeSection } from "./components/product-create-organize-attribute-section"
|
||||
import { ProductCreateDetailsContext } from "./components/product-create-organize-context"
|
||||
import { ProductCreateOrganizationSection } from "./components/product-create-organize-section"
|
||||
import { ProductCreateSalesChannelDrawer } from "./components/product-create-sales-channel-drawer"
|
||||
@@ -28,9 +24,9 @@ export const ProductCreateOrganizeForm = ({ form }: ProductAttributesProps) => {
|
||||
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
||||
<ProductCreateOrganizationSection form={form} />
|
||||
|
||||
{/*TODO: WHERE DO WE SET PRODUCT ATTRIBUTES? -> the plan is to moved that to Inventory UI */}
|
||||
{/*<Divider />*/}
|
||||
{/*<ProductCreateAttributeSection form={form} />*/}
|
||||
{/* TODO: WHERE DO WE SET PRODUCT ATTRIBUTES? -> the plan is to moved that to Inventory UI */}
|
||||
{/* <Divider />*/}
|
||||
{/* <ProductCreateAttributeSection form={form} />*/}
|
||||
</div>
|
||||
</div>
|
||||
</SplitView.Content>
|
||||
|
||||
+8
-47
@@ -1,12 +1,13 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { Product } from "@medusajs/medusa"
|
||||
import { ProductStatus } from "@medusajs/types"
|
||||
import { Button, Input, Select, Switch, Text, Textarea } from "@medusajs/ui"
|
||||
import { Button, Input, Select, Text, Textarea } from "@medusajs/ui"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { Trans, useTranslation } from "react-i18next"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { SwitchBox } from "../../../../../components/common/switch-box"
|
||||
import {
|
||||
RouteDrawer,
|
||||
useRouteModal,
|
||||
@@ -74,8 +75,8 @@ export const EditProductForm = ({ product }: EditProductFormProps) => {
|
||||
onSubmit={handleSubmit}
|
||||
className="flex flex-1 flex-col overflow-hidden"
|
||||
>
|
||||
<RouteDrawer.Body className="flex max-w-full flex-1 flex-col gap-y-8 overflow-y-auto">
|
||||
<div className="flex h-full flex-col gap-y-8">
|
||||
<RouteDrawer.Body className="flex flex-1 flex-col gap-y-8 overflow-y-auto">
|
||||
<div className="flex flex-col gap-y-8">
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
@@ -142,19 +143,6 @@ export const EditProductForm = ({ product }: EditProductFormProps) => {
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
className="text-ui-fg-subtle"
|
||||
>
|
||||
<Trans
|
||||
i18nKey="products.fields.title.hint"
|
||||
t={t}
|
||||
components={[<br key="break" />]}
|
||||
/>
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="handle"
|
||||
@@ -210,43 +198,16 @@ export const EditProductForm = ({ product }: EditProductFormProps) => {
|
||||
<Textarea {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
<Form.Hint>
|
||||
<Trans
|
||||
i18nKey="products.fields.description.hint"
|
||||
t={t}
|
||||
components={[<br key="break" />]}
|
||||
/>
|
||||
</Form.Hint>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Form.Field
|
||||
<SwitchBox
|
||||
control={form.control}
|
||||
name="discountable"
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="flex items-center justify-between">
|
||||
<Form.Label optional>
|
||||
{t("fields.discountable")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Switch
|
||||
{...field}
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
/>
|
||||
</Form.Control>
|
||||
</div>
|
||||
<Form.Hint className="!mt-1">
|
||||
{t("products.discountableHint")}
|
||||
</Form.Hint>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
label={t("fields.discountable")}
|
||||
description={t("products.discountableHint")}
|
||||
/>
|
||||
</div>
|
||||
</RouteDrawer.Body>
|
||||
|
||||
Reference in New Issue
Block a user