feat(admin-ui, medusa): admin UI metadata (#3644)

This commit is contained in:
Kasper Fabricius Kristensen
2023-03-31 12:07:24 +02:00
committed by GitHub
parent 4f4ccee7fb
commit 4342ac884b
59 changed files with 1904 additions and 963 deletions

View File

@@ -11,7 +11,7 @@ import InputHeader from "../../fundamentals/input-header"
import CustomHeader from "./custom-header"
import { DateTimePickerProps } from "./types"
const getDateClassname = (d, tempDate) => {
const getDateClassname = (d: Date, tempDate: Date) => {
return moment(d).format("YY,MM,DD") === moment(tempDate).format("YY,MM,DD")
? "date chosen"
: `date ${
@@ -29,12 +29,18 @@ const DatePicker: React.FC<DateTimePickerProps> = ({
tooltipContent,
tooltip,
}) => {
const [tempDate, setTempDate] = useState(date)
const [tempDate, setTempDate] = useState<Date | null>(date || null)
const [isOpen, setIsOpen] = useState(false)
useEffect(() => setTempDate(date), [isOpen])
const submitDate = () => {
if (!tempDate || !date) {
onSubmitDate(null)
setIsOpen(false)
return
}
// update only date, month and year
const newDate = new Date(date.getTime())
newDate.setUTCDate(tempDate.getUTCDate())
@@ -68,7 +74,9 @@ const DatePicker: React.FC<DateTimePickerProps> = ({
<ArrowDownIcon size={16} />
</div>
<label className="w-full text-left">
{moment(date).format("ddd, DD MMM YYYY")}
{date
? moment(date).format("ddd, DD MMM YYYY")
: "---, -- -- ----"}
</label>
</InputContainer>
</button>
@@ -78,7 +86,10 @@ const DatePicker: React.FC<DateTimePickerProps> = ({
sideOffset={8}
className="rounded-rounded border-grey-20 bg-grey-0 shadow-dropdown w-full border px-8"
>
<CalendarComponent date={tempDate} onChange={setTempDate} />
<CalendarComponent
date={tempDate}
onChange={(date) => setTempDate(date)}
/>
<div className="mb-8 mt-5 flex w-full">
<Button
variant="ghost"
@@ -101,7 +112,18 @@ const DatePicker: React.FC<DateTimePickerProps> = ({
)
}
export const CalendarComponent = ({ date, onChange }) => (
type CalendarComponentProps = {
date: Date | null
onChange: (
date: Date | null,
event: React.SyntheticEvent<any, Event> | undefined
) => void
}
export const CalendarComponent = ({
date,
onChange,
}: CalendarComponentProps) => (
<ReactDatePicker
selected={date}
inline

View File

@@ -70,7 +70,7 @@ const TimePicker: React.FC<DateTimePickerProps> = ({
<ClockIcon size={16} />
<span className="mx-1">UTC</span>
<span className="text-grey-90">
{moment.utc(date).format("HH:mm")}
{date ? moment.utc(date).format("HH:mm") : "--:--"}
</span>
</div>
</InputContainer>
@@ -84,13 +84,13 @@ const TimePicker: React.FC<DateTimePickerProps> = ({
<NumberScroller
numbers={hourNumbers}
selected={selectedHour}
onSelect={(n) => setSelectedHour(n)}
onSelect={(n) => setSelectedHour(n as number)}
className="pr-4"
/>
<NumberScroller
numbers={minuteNumbers}
selected={selectedMinute}
onSelect={(n) => setSelectedMinute(n)}
onSelect={(n) => setSelectedMinute(n as number)}
/>
<div className="to-grey-0 h-xlarge absolute bottom-4 left-0 right-0 z-10 bg-gradient-to-b from-transparent" />
</PopoverPrimitive.Content>

View File

@@ -1,6 +1,6 @@
import { InputHeaderProps } from "../../fundamentals/input-header"
export type DateTimePickerProps = {
date: Date
onSubmitDate: (newDate: Date) => void
date: Date | null
onSubmitDate: (newDate: Date | null) => void
} & InputHeaderProps

View File

@@ -0,0 +1,34 @@
import FormValidator from "../../../../utils/form-validator"
import { NestedForm } from "../../../../utils/nested-form"
import InputField from "../../../molecules/input"
export type CustomerGroupGeneralFormType = {
name: string
}
type CustomerGroupGeneralFormProps = {
form: NestedForm<CustomerGroupGeneralFormType>
}
export const CustomerGroupGeneralForm = ({
form,
}: CustomerGroupGeneralFormProps) => {
const {
register,
path,
formState: { errors },
} = form
return (
<div>
<InputField
label="Name"
required
{...register(path("name"), {
required: FormValidator.required("Name"),
})}
errors={errors}
/>
</div>
)
}

View File

@@ -0,0 +1,159 @@
import { Controller } from "react-hook-form"
import { NestedForm } from "../../../../utils/nested-form"
import DatePicker from "../../../atoms/date-picker/date-picker"
import TimePicker from "../../../atoms/date-picker/time-picker"
import AvailabilityDuration from "../../../molecules/availability-duration"
import InputField from "../../../molecules/input"
import SwitchableItem from "../../../molecules/switchable-item"
export type DiscountConfigurationFormType = {
starts_at: Date
ends_at: Date | null
usage_limit: number | null
valid_duration: string | null
}
type DiscountConfigurationFormProps = {
form: NestedForm<DiscountConfigurationFormType>
isDynamic?: boolean
}
const DiscountConfigurationForm = ({
form,
isDynamic,
}: DiscountConfigurationFormProps) => {
const { control, path } = form
return (
<div>
<div className="gap-y-large flex flex-col">
<Controller
name={path("starts_at")}
control={control}
render={({ field: { onChange, value } }) => {
return (
<SwitchableItem
open={!!value}
onSwitch={() => {
if (value) {
onChange(null)
} else {
onChange(new Date())
}
}}
title="Discount has a start date?"
description="Schedule the discount to activate in the future."
>
<div className="gap-x-xsmall flex items-center">
<DatePicker
date={value!}
label="Start date"
onSubmitDate={onChange}
/>
<TimePicker
label="Start time"
date={value!}
onSubmitDate={onChange}
/>
</div>
</SwitchableItem>
)
}}
/>
<Controller
name={path("ends_at")}
control={control}
render={({ field: { value, onChange } }) => {
return (
<SwitchableItem
open={!!value}
onSwitch={() => {
if (value) {
onChange(null)
} else {
onChange(
new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000)
)
}
}}
title="Discount has an expiry date?"
description="Schedule the discount to deactivate in the future."
>
<div className="gap-x-xsmall flex items-center">
<DatePicker
date={value!}
label="Expiry date"
onSubmitDate={onChange}
/>
<TimePicker
label="Expiry time"
date={value!}
onSubmitDate={onChange}
/>
</div>
</SwitchableItem>
)
}}
/>
<Controller
name={path("usage_limit")}
control={control}
render={({ field: { value, onChange } }) => {
return (
<SwitchableItem
open={!!value}
onSwitch={() => {
if (value) {
onChange(null)
} else {
onChange(10)
}
}}
title="Limit the number of redemtions?"
description="Limit applies across all customers, not per customer."
>
<InputField
label="Number of redemptions"
type="number"
placeholder="5"
min={1}
defaultValue={value ?? undefined}
onChange={(value) => onChange(value.target.valueAsNumber)}
/>
</SwitchableItem>
)
}}
/>
{isDynamic && (
<Controller
name={path("valid_duration")}
control={control}
render={({ field: { onChange, value } }) => {
return (
<SwitchableItem
open={!!value}
onSwitch={() => {
if (value) {
onChange(null)
} else {
onChange("P0Y0M0DT00H00M")
}
}}
title="Availability duration?"
description="Set the duration of the discount."
>
<AvailabilityDuration
value={value ?? undefined}
onChange={onChange}
/>
</SwitchableItem>
)
}}
/>
)}
</div>
</div>
)
}
export default DiscountConfigurationForm

View File

@@ -0,0 +1,192 @@
import clsx from "clsx"
import { useAdminRegions } from "medusa-react"
import { useMemo } from "react"
import { Controller, useWatch } from "react-hook-form"
import { Option } from "../../../../types/shared"
import FormValidator from "../../../../utils/form-validator"
import { NestedForm } from "../../../../utils/nested-form"
import InputError from "../../../atoms/input-error"
import IconTooltip from "../../../molecules/icon-tooltip"
import IndeterminateCheckbox from "../../../molecules/indeterminate-checkbox"
import InputField from "../../../molecules/input"
import { NextSelect } from "../../../molecules/select/next-select"
import TextArea from "../../../molecules/textarea"
import PriceFormInput from "../../general/prices-form/price-form-input"
type DiscountRegionOption = Option & {
currency_code: string
}
enum DiscountRuleType {
FIXED = "fixed",
PERCENTAGE = "percentage",
FREE_SHIPPING = "free_shipping",
}
export type DiscountGeneralFormType = {
region_ids: DiscountRegionOption[]
code: string
value?: number
description: string
is_dynamic?: boolean
}
type DiscountGeneralFormProps = {
form: NestedForm<DiscountGeneralFormType>
type: DiscountRuleType
isEdit?: boolean
}
const DiscountGeneralForm = ({
form,
type,
isEdit,
}: DiscountGeneralFormProps) => {
const {
register,
path,
control,
formState: { errors },
} = form
const { regions } = useAdminRegions()
const regionOptions = useMemo(() => {
return (
regions?.map((r) => ({
value: r.id,
label: r.name,
currency_code: r.currency_code,
})) || []
)
}, [regions])
const selectedRegionCurrency = useWatch({
control,
name: path("region_ids.0.currency_code"),
defaultValue: "usd",
})
return (
<div className="gap-y-large flex flex-col">
<Controller
name={path("region_ids")}
control={control}
rules={{
required: FormValidator.required("Regions"),
}}
render={({ field: { value, onChange, name, ref } }) => {
return (
<NextSelect
name={name}
ref={ref}
value={value}
onChange={(value) => {
onChange(type === DiscountRuleType.FIXED ? [value] : value)
}}
label="Choose valid regions"
isMulti={type !== DiscountRuleType.FIXED}
selectAll={type !== DiscountRuleType.FIXED}
isSearchable
required
options={regionOptions}
errors={errors}
/>
)
}}
/>
<div>
<div
className={clsx("gap-small grid", {
"grid-cols-2":
type === DiscountRuleType.FIXED ||
type === DiscountRuleType.PERCENTAGE,
"grid-cols-1": type === DiscountRuleType.FREE_SHIPPING,
})}
>
<InputField
label="Code"
required
errors={errors}
{...register(path("code"), {
required: FormValidator.required("Code"),
})}
/>
{type === DiscountRuleType.FIXED ? (
<Controller
name={path("value")}
rules={{
required: FormValidator.required("Amount"),
shouldUnregister: true,
}}
render={({ field: { value, onChange } }) => {
return (
<PriceFormInput
label="Amount"
amount={value}
onChange={onChange}
currencyCode={selectedRegionCurrency}
errors={errors}
/>
)
}}
/>
) : type === DiscountRuleType.PERCENTAGE ? (
<InputField
label="Percentage"
placeholder="Percentage"
errors={errors}
prefix="%"
required
{...register(path("value"), {
valueAsNumber: true,
required: FormValidator.required("Percentage"),
shouldUnregister: true,
})}
/>
) : null}
</div>
<p className="inter-small-regular text-grey-50 mt-small max-w-[60%]">
The code your customers will enter during checkout. This will appear
on your customer&apos;s invoice. Uppercase letters and numbers only.
</p>
</div>
<div>
<TextArea
label="Description"
errors={errors}
required
{...register(path("description"), {
required: FormValidator.required("Description"),
})}
/>
</div>
{!isEdit && (
<div>
<Controller
name={path("is_dynamic")}
control={control}
render={({ field: { value, onChange, ref } }) => {
return (
<div>
<div className="flex items-center">
<IndeterminateCheckbox
checked={value}
onChange={onChange}
ref={ref}
/>
<p className="ml-small mr-xsmall">Template discount</p>
<IconTooltip content="Template discounts allow you to define a set of rules that can be used across a group of discounts. This is useful in campaigns that should generate unique codes for each user, but where the rules for all unique codes should be the same." />
</div>
<InputError errors={errors} name={path("is_dynamic")} />
</div>
)
}}
/>
</div>
)}
</div>
)
}
export default DiscountGeneralForm

View File

@@ -0,0 +1,87 @@
import FormValidator from "../../../../utils/form-validator"
import { NestedForm } from "../../../../utils/nested-form"
import InputField from "../../../molecules/input"
export type AddressContactFormType = {
first_name: string
last_name: string
company: string | null
phone: string | null
}
type AddressContactFormProps = {
requireFields?: Partial<Record<keyof AddressContactFormType, boolean>>
form: NestedForm<AddressContactFormType>
}
/**
* Re-usable form for address contact information, used to create and edit addresses.
* Fields are optional, but can be required by passing in a requireFields object.
*/
const AddressContactForm = ({
form,
requireFields,
}: AddressContactFormProps) => {
const {
path,
register,
formState: { errors },
} = form
return (
<div>
<div className="gap-large grid grid-cols-2">
<InputField
{...register(path("first_name"), {
required: requireFields?.first_name
? FormValidator.required("First name")
: false,
pattern: FormValidator.whiteSpaceRule("First name"),
})}
placeholder="First Name"
label="First Name"
required={requireFields?.first_name}
errors={errors}
/>
<InputField
{...form.register(path("last_name"), {
required: requireFields?.last_name
? FormValidator.required("Last name")
: false,
pattern: FormValidator.whiteSpaceRule("Last name"),
})}
placeholder="Last Name"
label="Last Name"
required={requireFields?.last_name}
errors={errors}
/>
<InputField
{...form.register(path("company"), {
pattern: FormValidator.whiteSpaceRule("Company"),
required: requireFields?.company
? FormValidator.required("Company")
: false,
})}
placeholder="Company"
required={requireFields?.company}
label="Company"
errors={errors}
/>
<InputField
{...form.register(path("phone"), {
pattern: FormValidator.whiteSpaceRule("Phone"),
required: requireFields?.phone
? FormValidator.required("Phone")
: false,
})}
required={requireFields?.phone}
placeholder="Phone"
label="Phone"
errors={errors}
/>
</div>
</div>
)
}
export default AddressContactForm

View File

@@ -0,0 +1,129 @@
import { Controller } from "react-hook-form"
import { Option } from "../../../../types/shared"
import FormValidator from "../../../../utils/form-validator"
import { NestedForm } from "../../../../utils/nested-form"
import InputField from "../../../molecules/input"
import { NextSelect } from "../../../molecules/select/next-select"
export type AddressLocationFormType = {
address_1: string
address_2: string | null
city: string
province: string | null
country_code: Option
postal_code: string
}
type AddressLocationFormProps = {
requireFields?: Partial<Record<keyof AddressLocationFormType, boolean>>
countryOptions?: Option[]
form: NestedForm<AddressLocationFormType>
}
/**
* Re-usable form for address location information, used to create and edit addresses.
* Fields are optional, but can be required by passing in a requireFields object.
*/
const AddressLocationForm = ({
form,
countryOptions,
requireFields,
}: AddressLocationFormProps) => {
const {
register,
path,
formState: { errors },
control,
} = form
return (
<div className="gap-large grid grid-cols-2">
<InputField
{...register(path("address_1"), {
required: requireFields?.address_1
? FormValidator.required("Address 1")
: false,
pattern: FormValidator.whiteSpaceRule("Address 1"),
})}
placeholder="Address 1"
label="Address 1"
required={requireFields?.address_1}
errors={errors}
/>
<InputField
{...register(path("address_2"), {
pattern: FormValidator.whiteSpaceRule("Address 2"),
required: requireFields?.address_2
? FormValidator.required("Address 2")
: false,
})}
placeholder="Address 2"
required={requireFields?.address_2}
label="Address 2"
errors={errors}
/>
<InputField
{...register(path("postal_code"), {
required: requireFields?.postal_code
? FormValidator.required("Postal code")
: false,
pattern: FormValidator.whiteSpaceRule("Postal code"),
})}
placeholder="Postal code"
label="Postal code"
required={requireFields?.postal_code}
autoComplete="off"
errors={errors}
/>
<InputField
placeholder="City"
label="City"
{...register(path("city"), {
required: requireFields?.city
? FormValidator.required("City")
: false,
pattern: FormValidator.whiteSpaceRule("City"),
})}
required={requireFields?.city}
errors={errors}
/>
<InputField
{...register(path("province"), {
pattern: FormValidator.whiteSpaceRule("Province"),
required: requireFields?.province
? FormValidator.required("Province")
: false,
})}
placeholder="Province"
label="Province"
required={requireFields?.province}
errors={errors}
/>
<Controller
control={control}
name={path("country_code")}
rules={{
required: requireFields?.country_code
? FormValidator.required("Country")
: false,
}}
render={({ field: { value, onChange } }) => {
return (
<NextSelect
label="Country"
required={requireFields?.country_code}
value={value}
options={countryOptions}
onChange={onChange}
name={path("country_code")}
errors={errors}
isClearable={!requireFields?.country_code}
/>
)
}}
/>
</div>
)
}
export default AddressLocationForm

View File

@@ -0,0 +1,371 @@
import * as DropdownMenu from "@radix-ui/react-dropdown-menu"
import clsx from "clsx"
import React, { useMemo } from "react"
import { useFieldArray, useWatch } from "react-hook-form"
import { NestedForm } from "../../../../utils/nested-form"
import Button from "../../../fundamentals/button"
import ArrowDownIcon from "../../../fundamentals/icons/arrow-down-icon"
import ArrowUpIcon from "../../../fundamentals/icons/arrow-up-icon"
import DuplicateIcon from "../../../fundamentals/icons/duplicate-icon"
import EllipsisVerticalIcon from "../../../fundamentals/icons/ellipsis-vertical-icon"
import TrashIcon from "../../../fundamentals/icons/trash-icon"
import WarningCircleIcon from "../../../fundamentals/icons/warning-circle"
import XCircleIcon from "../../../fundamentals/icons/x-circle-icon"
export type MetadataField = {
key: string
value: string
state?: "existing" | "new"
}
export type MetadataFormType = {
entries: MetadataField[]
deleted?: string[]
ignored?: Record<string, any>
}
type MetadataProps = {
form: NestedForm<MetadataFormType>
}
const MetadataForm = ({ form }: MetadataProps) => {
const { control, path, register, setValue, getValues } = form
const { fields, remove, insert } = useFieldArray({
control,
name: path("entries"),
keyName: "fieldKey",
})
const handleInsertAbove = (index: number) => {
insert(index, { key: "", value: "" })
}
const handleInsertBelow = (index: number) => {
insert(index + 1, { key: "", value: "" })
}
const handleDelete = (index: number) => {
if (fields[index].state === "existing") {
const deleted = getValues(path(`deleted`)) || []
setValue(path(`deleted`), [...deleted, fields[index].key], {
shouldDirty: true,
})
}
if (index === 0 && fields.length === 1) {
setValue(path(`entries.${index}.value`), "", {
shouldDirty: true,
})
setValue(path(`entries.${index}.key`), "", {
shouldDirty: true,
})
} else {
remove(index)
}
}
const handleDuplicate = (index: number) => {
insert(index + 1, { ...fields[index], state: undefined })
}
const handleClearContents = (index: number) => {
setValue(path(`entries.${index}.value`), "", {
shouldDirty: true,
})
setValue(path(`entries.${index}.key`), "", {
shouldDirty: true,
})
}
const subscriber = useWatch({
control,
name: path("entries"),
})
const ignoredSubscriber = useWatch({
control,
name: path("ignored"),
defaultValue: {},
})
const ignoredLength = useMemo(() => {
return Object.keys(ignoredSubscriber || {}).length
}, [ignoredSubscriber])
// If there is only one row and it is empty or there are no rows, disable the delete button
const isDisabled = useMemo(() => {
if (!subscriber?.length) {
return true
}
if (subscriber?.length === 1) {
return (
(subscriber[0].key === "" && subscriber[0].value === "") ||
(subscriber[0].key === undefined && subscriber[0].value === undefined)
)
}
return false
}, [subscriber])
const rowClasses =
"divide-grey-20 grid grid-cols-[165px_1fr] divide-x divide-solid [&>div]:px-base [&>div]:py-xsmall"
return (
<>
<div className="rounded-rounded border-grey-20 divide-grey-20 inter-base-regular divide-y border">
<div
className={clsx(
"inter-small-semibold bg-grey-5 rounded-t-rounded",
rowClasses
)}
>
<div>
<p>Key</p>
</div>
<div className="">
<p>Value</p>
</div>
</div>
<div className="divide-grey-20 divide-y">
{!fields.length ? (
<MetadataRow
onClearContents={() => handleClearContents(0)}
onDelete={() => handleDelete(0)}
onDuplicate={() => handleDuplicate(0)}
onInsertAbove={() => handleInsertAbove(0)}
onInsertBelow={() => handleInsertBelow(0)}
isDisabled={isDisabled}
>
<div>
<MetadataInput
{...register(path(`entries.${0}.key`))}
placeholder="Key"
/>
</div>
<div>
<MetadataInput
{...register(path(`entries.${0}.value`))}
placeholder="Value"
/>
</div>
</MetadataRow>
) : (
fields.map((field, index) => {
return (
<MetadataRow
key={field.fieldKey}
onClearContents={() => handleClearContents(index)}
onDelete={() => handleDelete(index)}
onDuplicate={() => handleDuplicate(index)}
onInsertAbove={() => handleInsertAbove(index)}
onInsertBelow={() => handleInsertBelow(index)}
isDisabled={isDisabled}
>
<div>
<MetadataInput
{...register(path(`entries.${index}.key`))}
placeholder="Key"
/>
</div>
<div>
<MetadataInput
{...register(path(`entries.${index}.value`))}
placeholder="Value"
/>
</div>
</MetadataRow>
)
})
)}
</div>
</div>
{ignoredLength > 0 && (
<div className="rounded-rounded p-base gap-x-base mt-base flex items-start border border-[#FFD386] bg-[#FFECBC]">
<div>
<WarningCircleIcon
fillType="solid"
size={20}
className="text-[#FFB224]"
/>
</div>
<div>
<p className="inter-small-regular text-[#AD5700]">
This entities metadata contains complex values that we currently
don&apos;t support editing through the admin UI. Due to this{" "}
{Object.keys(ignoredLength)} keys are currently not being
displayed. You can still edit these values using the API.
</p>
</div>
</div>
)}
</>
)
}
export default MetadataForm
const MetadataInput = React.forwardRef<
HTMLInputElement,
React.ComponentPropsWithoutRef<"input">
>(({ className, ...props }, ref) => {
return (
<input
ref={ref}
className={clsx(
"placeholder:text-grey-40 placeholder:inter-base-regular w-full appearance-none outline-none",
className
)}
{...props}
/>
)
})
MetadataInput.displayName = "MetadataInput"
type MetadataRowProps = React.PropsWithChildren<{
onDuplicate: () => void
onInsertAbove: () => void
onInsertBelow: () => void
onDelete: () => void
onClearContents: () => void
isDisabled?: boolean
}>
const MetadataRow = ({
onDuplicate,
onInsertAbove,
onInsertBelow,
onDelete,
onClearContents,
isDisabled = false,
children,
}: MetadataRowProps) => {
const itemClasses =
"px-base py-[6px] outline-none flex items-center gap-x-xsmall hover:bg-grey-5 focus:bg-grey-10 transition-colors cursor-pointer"
return (
<div className="last-of-type:rounded-b-rounded group/metadata relative">
<div className="divide-grey-20 [&>div]:px-base [&>div]:py-xsmall grid grid-cols-[165px_1fr] divide-x divide-solid">
{children}
</div>
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<Button
variant="secondary"
size="small"
className={clsx(
"h-xlarge w-large -right-small radix-state-open:opacity-100 absolute inset-y-1/2 -translate-y-1/2 transform opacity-0 transition-opacity group-hover/metadata:opacity-100"
)}
>
<EllipsisVerticalIcon />
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content asChild sideOffset={8}>
<div className="bg-grey-0 shadow-dropdown border-grey-20 rounded-rounded z-50 overflow-hidden border">
<DropdownMenu.Item onClick={onInsertAbove} className={itemClasses}>
<ArrowUpIcon size={20} />
<span>Insert above</span>
</DropdownMenu.Item>
<DropdownMenu.Item onClick={onInsertBelow} className={itemClasses}>
<ArrowDownIcon size={20} />
<span>Insert below</span>
</DropdownMenu.Item>
<DropdownMenu.Item onClick={onDuplicate} className={itemClasses}>
<DuplicateIcon size={20} />
<span>Duplicate</span>
</DropdownMenu.Item>
<DropdownMenu.Item
onClick={onClearContents}
className={itemClasses}
>
<XCircleIcon size={20} />
<span>Clear contents</span>
</DropdownMenu.Item>
<DropdownMenu.DropdownMenuSeparator className="bg-grey-20 h-px w-full" />
<DropdownMenu.Item
onClick={onDelete}
disabled={isDisabled}
className={clsx(
{
"text-grey-40": isDisabled,
"text-rose-50": !isDisabled,
},
itemClasses,
"hover:bg-grey-0"
)}
>
<TrashIcon size={20} />
<span>Delete</span>
</DropdownMenu.Item>
</div>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
)
}
export const getSubmittableMetadata = (
data: MetadataFormType
): Record<string, unknown> => {
const metadata = data.entries.reduce((acc, { key, value }) => {
if (key) {
acc[key] = value
}
return acc
}, {} as Record<string, unknown>)
if (data.deleted?.length) {
data.deleted.forEach((key) => {
metadata[key] = ""
})
}
// Preserve complex values that we don't support editing through the UI
if (data.ignored) {
Object.entries(data.ignored).forEach(([key, value]) => {
metadata[key] = value
})
}
return metadata
}
export const getMetadataFormValues = (
metadata?: Record<string, any> | null
): MetadataFormType => {
const data: MetadataFormType = {
entries: [],
deleted: [],
ignored: {},
}
if (metadata) {
Object.entries(metadata).forEach(([key, value]) => {
if (isPrimitive(value)) {
data.entries.push({
key,
value: value as string,
state: "existing",
})
} else {
data.ignored![key] = value
}
})
}
return data
}
const isPrimitive = (value: any): boolean => {
return (
value === null ||
value === undefined ||
typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean"
)
}

View File

@@ -1,8 +1,9 @@
import clsx from "clsx"
import { useEffect, useState } from "react"
import { useCallback, useEffect, useState } from "react"
import AmountField from "react-currency-input-field"
import { currencies } from "../../../../utils/currencies"
import InputError from "../../../atoms/input-error"
import InputHeader from "../../../fundamentals/input-header"
type Props = {
currencyCode: string
@@ -10,6 +11,8 @@ type Props = {
onChange: (amount?: number) => void
errors?: { [x: string]: unknown }
name?: string
label?: string
required?: boolean
}
const PriceFormInput = ({
@@ -18,13 +21,18 @@ const PriceFormInput = ({
errors,
amount,
onChange,
label,
required,
}: Props) => {
const { symbol_native, decimal_digits } =
currencies[currencyCode.toUpperCase()]
const getFormattedValue = (value: number) => {
return `${value / 10 ** decimal_digits}`
}
const getFormattedValue = useCallback(
(value: number) => {
return `${value / 10 ** decimal_digits}`
},
[decimal_digits]
)
const [formattedValue, setFormattedValue] = useState<string | undefined>(
amount !== null && amount !== undefined
@@ -36,7 +44,7 @@ const PriceFormInput = ({
if (amount) {
setFormattedValue(getFormattedValue(amount))
}
}, [amount, decimal_digits])
}, [amount, decimal_digits, getFormattedValue])
const onAmountChange = (value?: string, floatValue?: number | null) => {
if (typeof floatValue === "number") {
@@ -52,6 +60,7 @@ const PriceFormInput = ({
return (
<div>
{label && <InputHeader {...{ label, required }} className="mb-xsmall" />}
<div
className={clsx(
"bg-grey-5 border-gray-20 px-small py-xsmall rounded-rounded focus-within:shadow-input focus-within:border-violet-60 flex h-10 w-full items-center border",

View File

@@ -1,11 +1,12 @@
import { Controller, useFieldArray, useWatch } from "react-hook-form"
import { currencies } from "../../../../utils/currencies"
import { NestedForm } from "../../../../utils/nested-form"
import { nestedForm, NestedForm } from "../../../../utils/nested-form"
import IncludesTaxTooltip from "../../../atoms/includes-tax-tooltip"
import InputError from "../../../atoms/input-error"
import Switch from "../../../atoms/switch"
import CoinsIcon from "../../../fundamentals/icons/coins-icon"
import IconTooltip from "../../../molecules/icon-tooltip"
import MetadataForm, { MetadataFormType } from "../../general/metadata-form"
import PriceFormInput from "../../general/prices-form/price-form-input"
type DenominationType = {
@@ -18,6 +19,7 @@ export type DenominationFormType = {
defaultDenomination: DenominationType
currencyDenominations: DenominationType[]
useSameValue: boolean
metadata: MetadataFormType
}
type Props = {
@@ -51,7 +53,7 @@ const DenominationForm = ({ form }: Props) => {
<div className="gap-y-xlarge flex flex-col">
<div>
<div className="gap-x-2xsmall flex items-center">
<h2 className="inter-large-semibold">Default currency</h2>
<h2 className="inter-base-semibold">Default currency</h2>
<IconTooltip
type="info"
content="The denomination in your store's default currency"
@@ -170,6 +172,10 @@ const DenominationForm = ({ form }: Props) => {
</div>
</div>
)}
<div>
<h2 className="inter-base-semibold mb-base">Metadata</h2>
<MetadataForm form={nestedForm(form, "metadata")} />
</div>
</div>
)
}

View File

@@ -3,13 +3,14 @@ import { useMemo } from "react"
import {
useAdminCollections,
useAdminProductCategories,
useAdminProductTypes
useAdminProductTypes,
} from "medusa-react"
import { NestedMultiselectOption } from "../../../../domain/categories/components/multiselect"
import { transformCategoryToNestedFormOptions } from "../../../../domain/categories/utils/transform-response"
import {
FeatureFlag, useFeatureFlag
FeatureFlag,
useFeatureFlag,
} from "../../../../providers/feature-flag-provider"
const useOrganizeData = () => {
@@ -19,12 +20,15 @@ const useOrganizeData = () => {
refetchOnWindowFocus: true,
})
const { collections } = useAdminCollections()
const { product_categories: categories = [] } =
isFeatureEnabled(FeatureFlag.PRODUCT_CATEGORIES) &&
useAdminProductCategories({
const { product_categories: categories = [] } = useAdminProductCategories(
{
parent_category_id: "null",
include_descendants_tree: true,
})
},
{
enabled: isFeatureEnabled(FeatureFlag.PRODUCT_CATEGORIES),
}
)
const productTypeOptions = useMemo(() => {
return (

View File

@@ -11,6 +11,7 @@ import { nestedForm } from "../../../../../utils/nested-form"
import IconTooltip from "../../../../molecules/icon-tooltip"
import InputField from "../../../../molecules/input"
import Accordion from "../../../../organisms/accordion"
import MetadataForm, { MetadataFormType } from "../../../general/metadata-form"
import { PricesFormType } from "../../../general/prices-form"
import VariantPricesForm from "../variant-prices-form"
@@ -29,6 +30,7 @@ export type EditFlowVariantFormType = {
}[]
customs: CustomsFormType
dimensions: DimensionsFormType
metadata: MetadataFormType
}
type Props = {
@@ -125,6 +127,13 @@ const EditFlowVariantForm = ({ form, isEdit }: Props) => {
</div>
)}
</Accordion.Item>
<Accordion.Item title="Metadata" value="metadata">
<p className="inter-base-regular text-grey-50 mb-base">
Metadata can be used to store additional information about the
variant.
</p>
<MetadataForm form={nestedForm(form, "metadata")} />
</Accordion.Item>
</Accordion>
)
}

View File

@@ -0,0 +1,26 @@
import IconProps from "../types/icon-type"
const EllipsisVerticalIcon = ({
size = 20,
color = "currentColor",
...attributes
}: IconProps) => {
return (
<svg
width={size}
height={size}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...attributes}
>
<path
d="M10 3.75C10.1989 3.75 10.3897 3.82902 10.5303 3.96967C10.671 4.11032 10.75 4.30109 10.75 4.5C10.75 4.69891 10.671 4.88968 10.5303 5.03033C10.3897 5.17098 10.1989 5.25 10 5.25C9.80109 5.25 9.61032 5.17098 9.46967 5.03033C9.32902 4.88968 9.25 4.69891 9.25 4.5C9.25 4.30109 9.32902 4.11032 9.46967 3.96967C9.61032 3.82902 9.80109 3.75 10 3.75ZM10 9.25C10.1989 9.25 10.3897 9.32902 10.5303 9.46967C10.671 9.61032 10.75 9.80109 10.75 10C10.75 10.1989 10.671 10.3897 10.5303 10.5303C10.3897 10.671 10.1989 10.75 10 10.75C9.80109 10.75 9.61032 10.671 9.46967 10.5303C9.32902 10.3897 9.25 10.1989 9.25 10C9.25 9.80109 9.32902 9.61032 9.46967 9.46967C9.61032 9.32902 9.80109 9.25 10 9.25ZM10.5303 14.9697C10.671 15.1103 10.75 15.3011 10.75 15.5C10.75 15.6989 10.671 15.8897 10.5303 16.0303C10.3897 16.171 10.1989 16.25 10 16.25C9.80109 16.25 9.61032 16.171 9.46967 16.0303C9.32902 15.8897 9.25 15.6989 9.25 15.5C9.25 15.3011 9.32902 15.1103 9.46967 14.9697C9.61032 14.829 9.80109 14.75 10 14.75C10.1989 14.75 10.3897 14.829 10.5303 14.9697Z"
stroke={color}
strokeWidth="1.5"
/>
</svg>
)
}
export default EllipsisVerticalIcon

View File

@@ -1,4 +1,3 @@
import InputHeader, { InputHeaderProps } from "../../fundamentals/input-header"
import React, {
ChangeEventHandler,
FocusEventHandler,
@@ -6,11 +5,12 @@ import React, {
useImperativeHandle,
useRef,
} from "react"
import InputHeader, { InputHeaderProps } from "../../fundamentals/input-header"
import clsx from "clsx"
import InputError from "../../atoms/input-error"
import MinusIcon from "../../fundamentals/icons/minus-icon"
import PlusIcon from "../../fundamentals/icons/plus-icon"
import clsx from "clsx"
export type InputProps = Omit<React.ComponentPropsWithRef<"input">, "prefix"> &
InputHeaderProps & {
@@ -26,7 +26,6 @@ export type InputProps = Omit<React.ComponentPropsWithRef<"input">, "prefix"> &
props?: React.HTMLAttributes<HTMLDivElement>
}
// eslint-disable-next-line react/display-name
const InputField = React.forwardRef<HTMLInputElement, InputProps>(
(
{
@@ -161,4 +160,6 @@ const InputField = React.forwardRef<HTMLInputElement, InputProps>(
}
)
InputField.displayName = "InputField"
export default InputField

View File

@@ -85,7 +85,7 @@ const Item: React.FC<AccordionItemProps> = ({
<AccordionPrimitive.Content
forceMount={forceMountContent}
className={clsx(
"radix-state-closed:animate-accordion-close radix-state-open:animate-accordion-open overflow-hidden px-1"
"radix-state-closed:animate-accordion-close radix-state-open:animate-accordion-open radix-state-closed:pointer-events-none px-1"
)}
>
<div className="inter-base-regular group-radix-state-closed:animate-accordion-close">

View File

@@ -18,6 +18,8 @@ type BodyCardProps = {
status?: React.ReactNode
customHeader?: React.ReactNode
compact?: boolean
footerMinHeight?: number
setBorders?: boolean
} & React.HTMLAttributes<HTMLDivElement>
const BodyCard: React.FC<BodyCardProps> = ({
@@ -94,7 +96,7 @@ const BodyCard: React.FC<BodyCardProps> = ({
{children && (
<div
className={clsx("flex flex-col", {
"my-large grow": !compact,
grow: !compact,
})}
>
{children}

View File

@@ -109,7 +109,7 @@ const EditDenominationsModal = ({
<Modal open={open} handleClose={handleClose}>
<Modal.Body>
<Modal.Header handleClose={handleClose}>
<h1 className="inter-xlarge-semibold">Edit Denominations</h1>
<h1 className="inter-xlarge-semibold">Edit Denomination</h1>
</Modal.Header>
<form onSubmit={onSubmit}>
<Modal.Content>

View File

@@ -6,13 +6,18 @@ import OrganizeForm, {
OrganizeFormType,
} from "../../forms/product/organize-form"
import Button from "../../fundamentals/button"
import Modal from "../../molecules/modal"
import { Product } from "@medusajs/medusa"
import { nestedForm } from "../../../utils/nested-form"
import useEditProductActions from "../../../hooks/use-edit-product-actions"
import { useEffect } from "react"
import { useForm } from "react-hook-form"
import useEditProductActions from "../../../hooks/use-edit-product-actions"
import { nestedForm } from "../../../utils/nested-form"
import MetadataForm, {
getMetadataFormValues,
getSubmittableMetadata,
MetadataFormType,
} from "../../forms/general/metadata-form"
import Button from "../../fundamentals/button"
import Modal from "../../molecules/modal"
type Props = {
product: Product
@@ -24,6 +29,7 @@ type GeneralFormWrapper = {
general: GeneralFormType
organize: OrganizeFormType
discountable: DiscountableFormType
metadata: MetadataFormType
}
const GeneralModal = ({ product, open, onClose }: Props) => {
@@ -76,6 +82,7 @@ const GeneralModal = ({ product, open, onClose }: Props) => {
categories: data.organize.categories?.map((id) => ({ id })),
discountable: data.discountable.value,
metadata: getSubmittableMetadata(data.metadata),
},
onReset
)
@@ -105,6 +112,10 @@ const GeneralModal = ({ product, open, onClose }: Props) => {
form={nestedForm(form, "discountable")}
isGiftCard={product.is_giftcard}
/>
<div className="mt-xlarge">
<h2 className="inter-base-semibold mb-base">Metadata</h2>
<MetadataForm form={nestedForm(form, "metadata")} />
</div>
</Modal.Content>
<Modal.Footer>
<div className="flex w-full justify-end gap-x-2">
@@ -155,6 +166,7 @@ const getDefaultValues = (product: Product): GeneralFormWrapper => {
discountable: {
value: product.discountable,
},
metadata: getMetadataFormValues(product.metadata),
}
}

View File

@@ -141,6 +141,18 @@ const ProductDetails = ({ product }: Props) => {
title="Discountable"
value={product.discountable ? "True" : "False"}
/>
<Detail
title="Metadata"
value={
Object.entries(product.metadata || {}).length > 0
? `${Object.entries(product.metadata || {}).length} ${
Object.keys(product.metadata || {}).length === 1
? "item"
: "items"
}`
: undefined
}
/>
</div>
)
}

View File

@@ -1,15 +1,16 @@
import { AdminPostProductsProductVariantsReq, Product } from "@medusajs/medusa"
import { useContext, useEffect } from "react"
import { useEffect } from "react"
import EditFlowVariantForm, {
EditFlowVariantFormType,
} from "../../forms/product/variant-form/edit-flow-variant-form"
import LayeredModal, {
LayeredModalContext,
useLayeredModal,
} from "../../molecules/modal/layered-modal"
import { useMedusa } from "medusa-react"
import { useForm } from "react-hook-form"
import useEditProductActions from "../../../hooks/use-edit-product-actions"
import { getSubmittableMetadata } from "../../forms/general/metadata-form"
import Button from "../../fundamentals/button"
import Modal from "../../molecules/modal"
@@ -20,7 +21,8 @@ type Props = {
}
const AddVariantModal = ({ open, onClose, product }: Props) => {
const context = useContext(LayeredModalContext)
const context = useLayeredModal()
const { client } = useMedusa()
const form = useForm<EditFlowVariantFormType>({
defaultValues: getDefaultValues(product),
@@ -160,6 +162,10 @@ const getDefaultValues = (product: Product): EditFlowVariantFormType => {
hs_code: null,
origin_country: null,
},
metadata: {
entries: [],
deleted: [],
},
}
}
@@ -192,6 +198,7 @@ export const createAddPayload = (
: null,
// @ts-ignore
prices: priceArray,
metadata: getSubmittableMetadata(data.metadata),
title: data.general.title || `${options?.map((o) => o.value).join(" / ")}`,
options: options.map((option) => ({
option_id: option.id,

View File

@@ -11,6 +11,7 @@ import { useContext } from "react"
import { useForm } from "react-hook-form"
import useEditProductActions from "../../../hooks/use-edit-product-actions"
import { countries } from "../../../utils/countries"
import { getMetadataFormValues } from "../../forms/general/metadata-form"
import Button from "../../fundamentals/button"
import Modal from "../../molecules/modal"
import { createAddPayload } from "./add-variant-modal"
@@ -204,6 +205,7 @@ export const getEditVariantDefaultValues = (
})),
},
options,
metadata: getMetadataFormValues(variant.metadata),
}
}

View File

@@ -10,6 +10,7 @@ import EditFlowVariantForm, {
import { useForm } from "react-hook-form"
import useEditProductActions from "../../../../hooks/use-edit-product-actions"
import { getSubmittableMetadata } from "../../../forms/general/metadata-form"
import Button from "../../../fundamentals/button"
import Modal from "../../../molecules/modal"
import { LayeredModalContext } from "../../../molecules/modal/layered-modal"
@@ -117,6 +118,7 @@ export const createUpdatePayload = (
...stock,
...dimensions,
...customs,
metadata: getSubmittableMetadata(data.metadata),
// @ts-ignore
origin_country: customs?.origin_country
? customs.origin_country.value

View File

@@ -78,12 +78,12 @@ const EditVariantsModal = ({ open, onClose, product }: Props) => {
/>
)
},
[product]
[moveCard, product]
)
const handleFormReset = () => {
const handleFormReset = useCallback(() => {
reset(getDefaultValues(product))
}
}, [product, reset])
const resetAndClose = () => {
handleFormReset()
@@ -93,7 +93,7 @@ const EditVariantsModal = ({ open, onClose, product }: Props) => {
useEffect(() => {
handleFormReset()
}, [product])
}, [product, handleFormReset])
const onSubmit = handleSubmit((data) => {
onUpdate(

View File

@@ -28,7 +28,7 @@ function RawJSON(props: RawJSONProps) {
return (
<BodyCard className={"mb-4 h-auto min-h-0 w-full"} title={title}>
<div className="mt-4 flex flex-grow items-center">
<div className="flex flex-grow items-center">
<JSONView data={data} />
</div>
</BodyCard>

View File

@@ -1,7 +1,8 @@
import { Controller } from "react-hook-form"
import { Option } from "../../types/shared"
import FormValidator from "../../utils/form-validator"
import { NestedForm } from "../../utils/nested-form"
import { nestedForm, NestedForm } from "../../utils/nested-form"
import MetadataForm, { MetadataFormType } from "../forms/general/metadata-form"
import Input from "../molecules/input"
import { NextSelect } from "../molecules/select/next-select"
@@ -16,6 +17,7 @@ export type AddressPayload = {
country_code: Option
postal_code: string
phone: string | null
metadata: MetadataFormType
}
export enum AddressType {
@@ -173,6 +175,10 @@ const AddressForm = ({
}}
/>
</div>
<div className="mt-xlarge gap-y-base flex flex-col">
<span className="inter-base-semibold">Metadata</span>
<MetadataForm form={nestedForm(form, "metadata")} />
</div>
</div>
)
}

View File

@@ -3,15 +3,20 @@ import {
useAdminCreateCollection,
useAdminUpdateCollection,
} from "medusa-react"
import React, { useEffect, useState } from "react"
import React, { useEffect } from "react"
import { useForm } from "react-hook-form"
import useNotification from "../../../hooks/use-notification"
import { getErrorMessage } from "../../../utils/error-messages"
import { nestedForm } from "../../../utils/nested-form"
import MetadataForm, {
getSubmittableMetadata,
MetadataFormType,
} from "../../forms/general/metadata-form"
import Button from "../../fundamentals/button"
import IconTooltip from "../../molecules/icon-tooltip"
import InputField from "../../molecules/input"
import Modal from "../../molecules/modal"
import Metadata, { MetadataField } from "../../organisms/metadata"
import { MetadataField } from "../../organisms/metadata"
type CollectionModalProps = {
onClose: () => void
@@ -23,6 +28,7 @@ type CollectionModalProps = {
type CollectionModalFormData = {
title: string
handle: string | undefined
metadata: MetadataFormType
}
const CollectionModal: React.FC<CollectionModalProps> = ({
@@ -35,51 +41,54 @@ const CollectionModal: React.FC<CollectionModalProps> = ({
)
const { mutate: create, isLoading: creating } = useAdminCreateCollection()
const { register, handleSubmit, reset } = useForm<CollectionModalFormData>()
const form = useForm<CollectionModalFormData>({
defaultValues: {
title: collection?.title,
handle: collection?.handle,
metadata: {
entries: Object.entries(collection?.metadata || {}).map(
([key, value]) => ({
key,
value: value as string,
state: "existing",
})
),
},
},
})
const { register, handleSubmit, reset } = form
useEffect(() => {
if (collection) {
reset({
title: collection.title,
handle: collection.handle,
metadata: {
entries: Object.entries(collection.metadata || {}).map(
([key, value]) => ({
key,
value: value as string,
state: "existing",
})
),
},
})
}
}, [collection, reset])
const notification = useNotification()
const [metadata, setMetadata] = useState<MetadataField[]>([])
if (isEdit && !collection) {
throw new Error("Collection is required for edit")
}
useEffect(() => {
register("title", { required: true })
register("handle")
}, [])
useEffect(() => {
if (isEdit && collection) {
reset({
title: collection.title,
handle: collection.handle,
})
if (collection.metadata) {
Object.entries(collection.metadata).map(([key, value]) => {
if (typeof value === "string") {
const newMeta = metadata
newMeta.push({ key, value })
setMetadata(newMeta)
}
})
}
}
}, [collection, isEdit])
const submit = (data: CollectionModalFormData) => {
if (isEdit) {
update(
{
title: data.title,
handle: data.handle,
metadata: metadata.reduce((acc, next) => {
return {
...acc,
[next.key]: next.value,
}
}, {}),
metadata: getSubmittableMetadata(data.metadata),
},
{
onSuccess: () => {
@@ -100,12 +109,7 @@ const CollectionModal: React.FC<CollectionModalProps> = ({
{
title: data.title,
handle: data.handle,
metadata: metadata.reduce((acc, next) => {
return {
...acc,
[next.key]: next.value,
}
}, {}),
metadata: getSubmittableMetadata(data.metadata),
},
{
onSuccess: () => {
@@ -159,8 +163,9 @@ const CollectionModal: React.FC<CollectionModalProps> = ({
/>
</div>
</div>
<div className="mt-xlarge w-full">
<Metadata setMetadata={setMetadata} metadata={metadata} />
<div className="mt-xlarge">
<h2 className="inter-base-semibold mb-base">Metadata</h2>
<MetadataForm form={nestedForm(form, "metadata")} />
</div>
</Modal.Content>
<Modal.Footer>

View File

@@ -1,6 +1,5 @@
import { CustomerGroup } from "@medusajs/medusa"
import { useAdminCustomerGroups } from "medusa-react"
import { useContext } from "react"
import { useNavigate } from "react-router-dom"
import {
HeaderGroup,
@@ -11,13 +10,11 @@ import {
useSortBy,
useTable,
} from "react-table"
import CustomerGroupContext, {
CustomerGroupContextContainer,
} from "../../../domain/customers/groups/context/customer-group-context"
import useQueryFilters from "../../../hooks/use-query-filters"
import useSetSearchParams from "../../../hooks/use-set-search-params"
import DetailsIcon from "../../fundamentals/details-icon"
import EditIcon from "../../fundamentals/icons/edit-icon"
import TrashIcon from "../../fundamentals/icons/trash-icon"
import { ActionType } from "../../molecules/actionables"
import Table from "../../molecules/table"
import TableContainer from "../../organisms/table-container"
import { CUSTOMER_GROUPS_TABLE_COLUMNS } from "./config"
@@ -92,19 +89,19 @@ function CustomerGroupsTableRow(props: CustomerGroupsTableRowProps) {
const { row } = props
const navigate = useNavigate()
const { showModal } = useContext(CustomerGroupContext)
const actions = [
{
label: "Edit",
onClick: showModal,
icon: <EditIcon size={20} />,
},
const actions: ActionType[] = [
{
label: "Details",
onClick: () => navigate(row.original.id),
icon: <DetailsIcon size={20} />,
},
{
label: "Delete",
onClick: () => {},
icon: <TrashIcon size={20} />,
variant: "danger",
},
]
return (
@@ -223,11 +220,7 @@ function CustomerGroupsTable(props: CustomerGroupsTableProps) {
<Table.Body {...table.getTableBodyProps()}>
{table.rows.map((row) => {
table.prepareRow(row)
return (
<CustomerGroupContextContainer key={row.id} group={row.original}>
<CustomerGroupsTableRow row={row} />
</CustomerGroupContextContainer>
)
return <CustomerGroupsTableRow row={row} key={row.id} />
})}
</Table.Body>
</Table>

View File

@@ -73,7 +73,7 @@ export const useCustomerOrdersColumns = (): Column<Order>[] => {
}
return (
<div className="gap-x-2xsmall flex">
<div className="gap-x-2xsmall flex items-center">
<div ref={containerRef} className="gap-x-xsmall flex">
{visibleItems.map((item) => {
return (