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:
Kasper Fabricius Kristensen
2024-06-06 08:09:38 +00:00
committed by GitHub
parent cc0452d29e
commit 1f203774cb
6 changed files with 107 additions and 123 deletions
@@ -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>
)
}}
/>
)
}
@@ -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 && (
@@ -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}
@@ -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>
@@ -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>