fix(dashboard): Cell behaviour in DataGrid (#8183)

This commit is contained in:
Kasper Fabricius Kristensen
2024-07-30 18:18:07 +02:00
committed by GitHub
parent fffd4f2b3b
commit 2967221e73
39 changed files with 2107 additions and 1034 deletions

View File

@@ -1,7 +1,8 @@
import { Checkbox } from "@medusajs/ui"
import { Controller } from "react-hook-form"
import { Controller, ControllerRenderProps } from "react-hook-form"
import { useCombinedRefs } from "../../../hooks/use-combined-refs"
import { useDataGridCell } from "../hooks"
import { DataGridCellProps } from "../types"
import { DataGridCellProps, InputProps } from "../types"
import { DataGridCellContainer } from "./data-grid-cell-container"
export const DataGridBooleanCell = <TData, TValue = any>({
@@ -9,28 +10,63 @@ export const DataGridBooleanCell = <TData, TValue = any>({
context,
disabled,
}: DataGridCellProps<TData, TValue> & { disabled?: boolean }) => {
const { control, attributes, container, onChange } = useDataGridCell({
const { control, renderProps } = useDataGridCell({
field,
context,
type: "select",
})
const { container, input } = renderProps
return (
<Controller
control={control}
name={field}
render={({ field: { value, onChange: _, ...field } }) => {
render={({ field }) => {
return (
<DataGridCellContainer {...container}>
<Checkbox
checked={value}
onCheckedChange={(next) => onChange(next, value)}
{...field}
{...attributes}
disabled={disabled}
/>
<Inner field={field} inputProps={input} disabled={disabled} />
</DataGridCellContainer>
)
}}
/>
)
}
const Inner = ({
field,
inputProps,
disabled,
}: {
field: ControllerRenderProps<any, string>
inputProps: InputProps
disabled?: boolean
}) => {
const { ref, value, onBlur, name, disabled: fieldDisabled } = field
const {
ref: inputRef,
onBlur: onInputBlur,
onChange,
onFocus,
...attributes
} = inputProps
const combinedRefs = useCombinedRefs(ref, inputRef)
return (
<Checkbox
disabled={disabled || fieldDisabled}
name={name}
checked={value}
onCheckedChange={(newValue) => onChange(newValue === true, value)}
onFocus={onFocus}
onBlur={() => {
onBlur()
onInputBlur()
}}
ref={combinedRefs}
tabIndex={-1}
{...attributes}
/>
)
}

View File

@@ -1,36 +1,41 @@
import { clx } from "@medusajs/ui"
import { PropsWithChildren } from "react"
import { DataGridCellContainerProps } from "../types"
type ContainerProps = PropsWithChildren<DataGridCellContainerProps>
import { DataGridCellContainerProps } from "../types"
export const DataGridCellContainer = ({
isAnchor,
isSelected,
isDragSelected,
showOverlay,
placeholder,
overlay,
wrapper,
innerProps,
overlayProps,
children,
}: ContainerProps) => {
}: DataGridCellContainerProps) => {
return (
<div className="static size-full">
<div className="flex size-full items-start outline-none" tabIndex={-1}>
<div {...wrapper} className="relative size-full min-w-0 flex-1">
<div className="relative z-[1] flex size-full items-center justify-center">
<RenderChildren isAnchor={isAnchor} placeholder={placeholder}>
{children}
</RenderChildren>
</div>
{!isAnchor && (
<div
{...overlay}
tabIndex={-1}
className="absolute inset-0 z-[2] size-full"
/>
)}
</div>
<div
className={clx("bg-ui-bg-base relative size-full outline-none", {
"ring-ui-bg-interactive ring-2 ring-inset": isAnchor,
"bg-ui-bg-highlight [&:has([data-field]:focus)]:bg-ui-bg-base":
isSelected || isAnchor,
"bg-ui-bg-subtle": isDragSelected && !isAnchor,
})}
tabIndex={0}
{...innerProps}
>
<div className="relative z-[1] flex size-full items-center justify-center">
<RenderChildren isAnchor={isAnchor} placeholder={placeholder}>
{children}
</RenderChildren>
</div>
{/* {showDragHandle && (
<div className="bg-ui-bg-interactive absolute -bottom-[1.5px] -right-[1.5px] size-[3px]" />
)} */}
{showOverlay && (
<div
{...overlayProps}
data-cell-overlay="true"
className="absolute inset-0 z-[2] size-full"
/>
)}
</div>
)
}

View File

@@ -1,116 +1,149 @@
import { TrianglesMini } from "@medusajs/icons"
import { clx } from "@medusajs/ui"
import { ComponentPropsWithoutRef, forwardRef, memo } from "react"
import { Controller } from "react-hook-form"
// Not currently used, re-implement or delete depending on whether there is a need for it in the future.
import { countries } from "../../../lib/data/countries"
import { useDataGridCell } from "../hooks"
import { DataGridCellProps } from "../types"
import { DataGridCellContainer } from "./data-grid-cell-container"
// import { TrianglesMini } from "@medusajs/icons"
// import { clx } from "@medusajs/ui"
// import { ComponentPropsWithoutRef, forwardRef, memo } from "react"
// import { Controller, ControllerRenderProps } from "react-hook-form"
export const DataGridCountrySelectCell = <TData, TValue = any>({
field,
context,
}: DataGridCellProps<TData, TValue>) => {
const { control, attributes, container, onChange } = useDataGridCell({
field,
context,
})
// import { useCombinedRefs } from "../../../hooks/use-combined-refs"
// import { countries } from "../../../lib/data/countries"
// import { useDataGridCell } from "../hooks"
// import { DataGridCellProps, InputProps } from "../types"
// import { DataGridCellContainer } from "./data-grid-cell-container"
return (
<Controller
control={control}
name={field}
render={({ field: { value, onChange: _, disabled, ...field } }) => {
return (
<DataGridCellContainer
{...container}
placeholder={
<DataGridCountryCellPlaceholder
value={value}
disabled={disabled}
attributes={attributes}
/>
}
>
<MemoizedDataGridCountryCell
value={value}
onChange={(e) => onChange(e.target.value, value)}
disabled={disabled}
{...attributes}
{...field}
/>
</DataGridCellContainer>
)
}}
/>
)
}
// export const DataGridCountrySelectCell = <TData, TValue = any>({
// field,
// context,
// }: DataGridCellProps<TData, TValue>) => {
// const { control, renderProps } = useDataGridCell({
// field,
// context,
// type: "select",
// })
const DataGridCountryCellPlaceholder = ({
value,
disabled,
attributes,
}: {
value?: string
disabled?: boolean
attributes: Record<string, any>
}) => {
const country = countries.find((c) => c.iso_2 === value)
// const { container, input } = renderProps
return (
<div className="relative flex size-full" {...attributes}>
<TrianglesMini
className={clx(
"text-ui-fg-muted transition-fg pointer-events-none absolute right-4 top-1/2 -translate-y-1/2",
{
"text-ui-fg-disabled": disabled,
}
)}
/>
<div
className={clx(
"txt-compact-small w-full appearance-none bg-transparent px-4 py-2.5 outline-none"
)}
>
{country?.display_name}
</div>
</div>
)
}
// return (
// <Controller
// control={control}
// name={field}
// render={({ field: { value, onChange: _, disabled, ...field } }) => {
// return (
// <DataGridCellContainer
// {...container}
// placeholder={
// <DataGridCountryCellPlaceholder
// value={value}
// disabled={disabled}
// attributes={attributes}
// />
// }
// >
// <MemoizedDataGridCountryCell
// value={value}
// onChange={(e) => onChange(e.target.value, value)}
// disabled={disabled}
// {...attributes}
// {...field}
// />
// </DataGridCellContainer>
// )
// }}
// />
// )
// }
const DataGridCountryCellImpl = forwardRef<
HTMLSelectElement,
ComponentPropsWithoutRef<"select">
>(({ disabled, className, ...props }, ref) => {
return (
<div className="relative flex size-full">
<TrianglesMini
className={clx(
"text-ui-fg-muted transition-fg pointer-events-none absolute right-4 top-1/2 -translate-y-1/2",
{
"text-ui-fg-disabled": disabled,
}
)}
/>
<select
{...props}
ref={ref}
className={clx(
"txt-compact-small w-full appearance-none bg-transparent px-4 py-2.5 outline-none",
className
)}
>
<option value=""></option>
{countries.map((country) => (
<option key={country.iso_2} value={country.iso_2}>
{country.display_name}
</option>
))}
</select>
</div>
)
})
DataGridCountryCellImpl.displayName = "DataGridCountryCell"
// const Inner = ({
// field,
// inputProps,
// }: {
// field: ControllerRenderProps<any, string>
// inputProps: InputProps
// }) => {
// const { value, onChange, onBlur, ref, ...rest } = field
// const { ref: inputRef, onBlur: onInputBlur, ...input } = inputProps
const MemoizedDataGridCountryCell = memo(DataGridCountryCellImpl)
// const combinedRefs = useCombinedRefs(inputRef, ref)
// return (
// <MemoizedDataGridCountryCell
// value={value}
// onChange={(e) => onChange(e.target.value, value)}
// onBlur={() => {
// onBlur()
// onInputBlur()
// }}
// ref={combinedRefs}
// {...input}
// {...rest}
// />
// )
// }
// const DataGridCountryCellPlaceholder = ({
// value,
// disabled,
// attributes,
// }: {
// value?: string
// disabled?: boolean
// attributes: Record<string, any>
// }) => {
// const country = countries.find((c) => c.iso_2 === value)
// return (
// <div className="relative flex size-full" {...attributes}>
// <TrianglesMini
// className={clx(
// "text-ui-fg-muted transition-fg pointer-events-none absolute right-4 top-1/2 -translate-y-1/2",
// {
// "text-ui-fg-disabled": disabled,
// }
// )}
// />
// <div
// className={clx(
// "txt-compact-small w-full appearance-none bg-transparent px-4 py-2.5 outline-none"
// )}
// >
// {country?.display_name}
// </div>
// </div>
// )
// }
// const DataGridCountryCellImpl = forwardRef<
// HTMLSelectElement,
// ComponentPropsWithoutRef<"select">
// >(({ disabled, className, ...props }, ref) => {
// return (
// <div className="relative flex size-full">
// <TrianglesMini
// className={clx(
// "text-ui-fg-muted transition-fg pointer-events-none absolute right-4 top-1/2 -translate-y-1/2",
// {
// "text-ui-fg-disabled": disabled,
// }
// )}
// />
// <select
// {...props}
// ref={ref}
// className={clx(
// "txt-compact-small w-full appearance-none bg-transparent px-4 py-2.5 outline-none",
// className
// )}
// >
// <option value=""></option>
// {countries.map((country) => (
// <option key={country.iso_2} value={country.iso_2}>
// {country.display_name}
// </option>
// ))}
// </select>
// </div>
// )
// })
// DataGridCountryCellImpl.displayName = "DataGridCountryCell"
// const MemoizedDataGridCountryCell = memo(DataGridCountryCellImpl)

View File

@@ -1,9 +1,14 @@
import CurrencyInput from "react-currency-input-field"
import { Controller } from "react-hook-form"
import CurrencyInput, {
CurrencyInputProps,
formatValue,
} from "react-currency-input-field"
import { Controller, ControllerRenderProps } from "react-hook-form"
import { currencies } from "../../../lib/data/currencies"
import { useCallback, useEffect, useState } from "react"
import { useCombinedRefs } from "../../../hooks/use-combined-refs"
import { CurrencyInfo, currencies } from "../../../lib/data/currencies"
import { useDataGridCell } from "../hooks"
import { DataGridCellProps } from "../types"
import { DataGridCellProps, InputProps } from "../types"
import { DataGridCellContainer } from "./data-grid-cell-container"
interface DataGridCurrencyCellProps<TData, TValue = any>
@@ -16,39 +21,122 @@ export const DataGridCurrencyCell = <TData, TValue = any>({
context,
code,
}: DataGridCurrencyCellProps<TData, TValue>) => {
const { control, attributes, container } = useDataGridCell({
const { control, renderProps } = useDataGridCell({
field,
context,
type: "number",
})
const { container, input } = renderProps
const currency = currencies[code.toUpperCase()]
return (
<Controller
control={control}
name={field}
render={({ field: { value, onChange, ...field } }) => {
render={({ field }) => {
return (
<DataGridCellContainer {...container}>
<div className="flex size-full items-center gap-2 px-4 py-2.5">
<span className="txt-compact-small text-ui-fg-muted" aria-hidden>
{currency.symbol_native}
</span>
<CurrencyInput
{...field}
{...attributes}
className="txt-compact-small w-full flex-1 appearance-none bg-transparent text-right outline-none"
value={value}
onValueChange={(_value, _name, values) =>
onChange(values?.value)
}
decimalScale={currency.decimal_digits}
decimalsLimit={currency.decimal_digits}
/>
</div>
<Inner field={field} inputProps={input} currencyInfo={currency} />
</DataGridCellContainer>
)
}}
/>
)
}
const Inner = ({
field,
inputProps,
currencyInfo,
}: {
field: ControllerRenderProps<any, string>
inputProps: InputProps
currencyInfo: CurrencyInfo
}) => {
const { value, onChange: _, onBlur, ref, ...rest } = field
const {
ref: inputRef,
onBlur: onInputBlur,
onFocus,
onChange,
...attributes
} = inputProps
const formatter = useCallback(
(value?: string | number) => {
const ensuredValue =
typeof value === "number" ? value.toString() : value || ""
return formatValue({
value: ensuredValue,
decimalScale: currencyInfo.decimal_digits,
disableGroupSeparators: true,
decimalSeparator: ".",
})
},
[currencyInfo]
)
const [localValue, setLocalValue] = useState<string | number>(value || "")
const handleValueChange: CurrencyInputProps["onValueChange"] = (
value,
_name,
_values
) => {
if (!value) {
setLocalValue("")
return
}
setLocalValue(value)
}
useEffect(() => {
let update = value
// The component we use is a bit fidly when the value is updated externally
// so we need to ensure a format that will result in the cell being formatted correctly
// according to the users locale on the next render.
if (!isNaN(Number(value))) {
update = formatter(update)
}
setLocalValue(update)
}, [value, formatter])
const combinedRed = useCombinedRefs(inputRef, ref)
return (
<div className="relative flex size-full items-center">
<span
className="txt-compact-small text-ui-fg-muted pointer-events-none absolute left-4 w-fit min-w-4"
aria-hidden
>
{currencyInfo.symbol_native}
</span>
<CurrencyInput
{...rest}
{...attributes}
ref={combinedRed}
className="txt-compact-small w-full flex-1 cursor-default appearance-none bg-transparent py-2.5 pl-12 pr-4 text-right outline-none"
value={localValue || undefined}
onValueChange={handleValueChange}
formatValueOnBlur
onBlur={() => {
onBlur()
onInputBlur()
onChange(localValue, value)
}}
onFocus={onFocus}
decimalScale={currencyInfo.decimal_digits}
decimalsLimit={currencyInfo.decimal_digits}
autoComplete="off"
tabIndex={-1}
/>
</div>
)
}

View File

@@ -1,5 +1,9 @@
import { clx } from "@medusajs/ui"
import { useEffect, useState } from "react"
import { Controller, ControllerRenderProps } from "react-hook-form"
import { useCombinedRefs } from "../../../hooks/use-combined-refs"
import { useDataGridCell } from "../hooks"
import { DataGridCellProps } from "../types"
import { DataGridCellProps, InputProps } from "../types"
import { DataGridCellContainer } from "./data-grid-cell-container"
export const DataGridNumberCell = <TData, TValue = any>({
@@ -11,32 +15,82 @@ export const DataGridNumberCell = <TData, TValue = any>({
max?: number
placeholder?: string
}) => {
const { register, attributes, container } = useDataGridCell({
const { control, renderProps } = useDataGridCell({
field,
context,
type: "number",
})
return (
<DataGridCellContainer {...container}>
<input
{...attributes}
type="number"
{...register(field, {
valueAsNumber: true,
onChange: (e) => {
if (e.target.value) {
const parsedValue = Number(e.target.value)
if (Number.isNaN(parsedValue)) {
return undefined
}
const { container, input } = renderProps
return parsedValue
}
},
})}
className="h-full w-full bg-transparent p-2 text-right [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
{...rest}
/>
</DataGridCellContainer>
return (
<Controller
control={control}
name={field}
render={({ field }) => {
return (
<DataGridCellContainer {...container}>
<Inner field={field} inputProps={input} {...rest} />
</DataGridCellContainer>
)
}}
/>
)
}
const Inner = ({
field,
inputProps,
...props
}: {
field: ControllerRenderProps<any, string>
inputProps: InputProps
min?: number
max?: number
placeholder?: string
}) => {
const { ref, value, onChange: _, onBlur, ...fieldProps } = field
const {
ref: inputRef,
onChange,
onBlur: onInputBlur,
onFocus,
...attributes
} = inputProps
const [localValue, setLocalValue] = useState(value)
useEffect(() => {
setLocalValue(value)
}, [value])
const combinedRefs = useCombinedRefs(inputRef, ref)
return (
<div className="size-full">
<input
ref={combinedRefs}
value={localValue}
onChange={(e) => setLocalValue(e.target.value)}
onBlur={() => {
onBlur()
onInputBlur()
// We propagate the change to the field only when the input is blurred
onChange(localValue, value)
}}
onFocus={onFocus}
type="number"
inputMode="decimal"
className={clx(
"txt-compact-small size-full bg-transparent px-4 py-2.5 outline-none",
"placeholder:text-ui-fg-muted"
)}
tabIndex={-1}
{...props}
{...fieldProps}
{...attributes}
/>
</div>
)
}

View File

@@ -6,8 +6,8 @@ export const DataGridReadOnlyCell = ({
children,
}: DataGridReadOnlyCellProps) => {
return (
<div className="bg-ui-bg-subtle txt-compact-small text-ui-fg-subtle flex size-full cursor-not-allowed items-center px-4 py-2.5 outline-none">
<span>{children}</span>
<div className="bg-ui-bg-subtle txt-compact-small text-ui-fg-subtle flex size-full cursor-not-allowed items-center overflow-hidden px-4 py-2.5 outline-none">
<span className="truncate">{children}</span>
</div>
)
}

View File

@@ -1,53 +1,55 @@
import { Select, clx } from "@medusajs/ui"
import { Controller } from "react-hook-form"
import { useDataGridCell } from "../hooks"
import { DataGridCellProps } from "../types"
import { DataGridCellContainer } from "./data-grid-cell-container"
// Not currently used, re-implement or delete depending on whether there is a need for it in the future.
interface DataGridSelectCellProps<TData, TValue = any>
extends DataGridCellProps<TData, TValue> {
options: { label: string; value: string }[]
}
// import { Select, clx } from "@medusajs/ui"
// import { Controller } from "react-hook-form"
// import { useDataGridCell } from "../hooks"
// import { DataGridCellProps } from "../types"
// import { DataGridCellContainer } from "./data-grid-cell-container"
export const DataGridSelectCell = <TData, TValue = any>({
context,
options,
field,
}: DataGridSelectCellProps<TData, TValue>) => {
const { control, attributes, container } = useDataGridCell({
field,
context,
})
// interface DataGridSelectCellProps<TData, TValue = any>
// extends DataGridCellProps<TData, TValue> {
// options: { label: string; value: string }[]
// }
return (
<Controller
control={control}
name={field}
render={({ field: { onChange, ref, ...field } }) => {
return (
<DataGridCellContainer {...container}>
<Select {...field} onValueChange={onChange}>
<Select.Trigger
{...attributes}
ref={ref}
className={clx(
"h-full w-full rounded-none bg-transparent px-4 py-2.5 shadow-none",
"hover:bg-transparent focus:shadow-none data-[state=open]:!shadow-none"
)}
>
<Select.Value />
</Select.Trigger>
<Select.Content>
{options.map((option) => (
<Select.Item key={option.value} value={option.value}>
{option.label}
</Select.Item>
))}
</Select.Content>
</Select>
</DataGridCellContainer>
)
}}
/>
)
}
// export const DataGridSelectCell = <TData, TValue = any>({
// context,
// options,
// field,
// }: DataGridSelectCellProps<TData, TValue>) => {
// const { control, attributes, container } = useDataGridCell({
// field,
// context,
// })
// return (
// <Controller
// control={control}
// name={field}
// render={({ field: { onChange, ref, ...field } }) => {
// return (
// <DataGridCellContainer {...container}>
// <Select {...field} onValueChange={onChange}>
// <Select.Trigger
// {...attributes}
// ref={ref}
// className={clx(
// "h-full w-full rounded-none bg-transparent px-4 py-2.5 shadow-none",
// "hover:bg-transparent focus:shadow-none data-[state=open]:!shadow-none"
// )}
// >
// <Select.Value />
// </Select.Trigger>
// <Select.Content>
// {options.map((option) => (
// <Select.Item key={option.value} value={option.value}>
// {option.label}
// </Select.Item>
// ))}
// </Select.Content>
// </Select>
// </DataGridCellContainer>
// )
// }}
// />
// )
// }

View File

@@ -1,41 +1,77 @@
import { clx } from "@medusajs/ui"
import { Controller } from "react-hook-form"
import { Controller, ControllerRenderProps } from "react-hook-form"
import { useEffect, useState } from "react"
import { useCombinedRefs } from "../../../hooks/use-combined-refs"
import { useDataGridCell } from "../hooks"
import { DataGridCellProps } from "../types"
import { DataGridCellProps, InputProps } from "../types"
import { DataGridCellContainer } from "./data-grid-cell-container"
export const DataGridTextCell = <TData, TValue = any>({
field,
context,
}: DataGridCellProps<TData, TValue>) => {
const { control, attributes, container, onChange } = useDataGridCell({
const { control, renderProps } = useDataGridCell({
field,
context,
type: "text",
})
const { container, input } = renderProps
return (
<Controller
control={control}
name={field}
render={({ field: { value, onChange: _, ...field } }) => {
render={({ field }) => {
return (
<DataGridCellContainer {...container}>
<input
className={clx(
"txt-compact-small text-ui-fg-subtle flex size-full cursor-pointer items-center justify-center bg-transparent px-4 py-2.5 outline-none",
"focus:cursor-text"
)}
autoComplete="off"
tabIndex={-1}
value={value}
onChange={(e) => onChange(e.target.value, value)}
{...attributes}
{...field}
/>
<Inner field={field} inputProps={input} />
</DataGridCellContainer>
)
}}
/>
)
}
const Inner = ({
field,
inputProps,
}: {
field: ControllerRenderProps<any, string>
inputProps: InputProps
}) => {
const { onChange: _, onBlur, ref, value, ...rest } = field
const { ref: inputRef, onBlur: onInputBlur, onChange, ...input } = inputProps
const [localValue, setLocalValue] = useState(value)
useEffect(() => {
setLocalValue(value)
}, [value])
const combinedRefs = useCombinedRefs(inputRef, ref)
return (
<input
className={clx(
"txt-compact-small text-ui-fg-subtle flex size-full cursor-pointer items-center justify-center bg-transparent px-4 py-2.5 outline-none",
"focus:cursor-text"
)}
autoComplete="off"
tabIndex={-1}
value={localValue}
onChange={(e) => setLocalValue(e.target.value)}
ref={combinedRefs}
onBlur={() => {
onBlur()
onInputBlur()
// We propagate the change to the field only when the input is blurred
onChange(localValue, value)
}}
{...input}
{...rest}
/>
)
}