fix(dashboard): Cell behaviour in DataGrid (#8183)
This commit is contained in:
@@ -13,6 +13,7 @@ export const IncludesTaxTooltip = ({
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
maxWidth={999}
|
||||
content={
|
||||
includesTax
|
||||
? t("general.includesTaxTooltip")
|
||||
|
||||
@@ -1,20 +1,31 @@
|
||||
import { MouseEvent, createContext } from "react"
|
||||
import { FocusEvent, MouseEvent, createContext } from "react"
|
||||
import { Control, FieldValues, Path, UseFormRegister } from "react-hook-form"
|
||||
import { CellCoords } from "./types"
|
||||
|
||||
type DataGridContextType<TForm extends FieldValues> = {
|
||||
// Grid state
|
||||
anchor: CellCoords | null
|
||||
selection: Record<string, boolean>
|
||||
dragSelection: Record<string, boolean>
|
||||
// Cell handlers
|
||||
registerCell: (coords: CellCoords, key: string) => void
|
||||
getIsCellSelected: (coords: CellCoords) => boolean
|
||||
getIsCellDragSelected: (coords: CellCoords) => boolean
|
||||
// Grid handlers
|
||||
setIsEditing: (value: boolean) => void
|
||||
setIsSelecting: (value: boolean) => void
|
||||
setRangeEnd: (coords: CellCoords) => void
|
||||
// Form state and handlers
|
||||
register: UseFormRegister<TForm>
|
||||
control: Control<TForm>
|
||||
onRegisterCell: (coordinates: CellCoords) => void
|
||||
onUnregisterCell: (coordinates: CellCoords) => void
|
||||
getMouseDownHandler: (
|
||||
getInputChangeHandler: (field: Path<TForm>) => (next: any, prev: any) => void
|
||||
// Wrapper handlers
|
||||
getWrapperFocusHandler: (
|
||||
coordinates: CellCoords
|
||||
) => (e: MouseEvent<HTMLElement>) => void
|
||||
getMouseOverHandler: (
|
||||
) => (e: FocusEvent<HTMLElement>) => void
|
||||
getWrapperMouseOverHandler: (
|
||||
coordinates: CellCoords
|
||||
) => ((e: MouseEvent<HTMLElement>) => void) | undefined
|
||||
getOnChangeHandler: (field: Path<TForm>) => (next: any, prev: any) => void
|
||||
}
|
||||
|
||||
export const DataGridContext = createContext<DataGridContextType<any> | null>(
|
||||
|
||||
+47
-11
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
+29
-24
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+142
-109
@@ -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)
|
||||
|
||||
+110
-22
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+76
-22
@@ -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}
|
||||
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}
|
||||
/>
|
||||
</DataGridCellContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+2
-2
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+52
-50
@@ -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>
|
||||
// )
|
||||
// }}
|
||||
// />
|
||||
// )
|
||||
// }
|
||||
|
||||
+52
-16
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
+12
-15
@@ -1,14 +1,12 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { DataGridCurrencyCell } from "../data-grid-cells/data-grid-currency-cell"
|
||||
import { createDataGridHelper } from "../utils"
|
||||
import { IncludesTaxTooltip } from "../../../components/common/tax-badge/tax-badge"
|
||||
import { CellContext, ColumnDef } from "@tanstack/react-table"
|
||||
import { TFunction } from "i18next"
|
||||
import { CellContext } from "@tanstack/react-table"
|
||||
import { IncludesTaxTooltip } from "../../../components/common/tax-badge/tax-badge"
|
||||
import { DataGridCurrencyCell } from "../data-grid-cells/data-grid-currency-cell"
|
||||
import { DataGridReadOnlyCell } from "../data-grid-cells/data-grid-readonly-cell"
|
||||
import { createDataGridHelper } from "../utils"
|
||||
|
||||
const columnHelper = createDataGridHelper<string | HttpTypes.AdminRegion>()
|
||||
|
||||
export const getPriceColumns = ({
|
||||
export const getPriceColumns = <TData,>({
|
||||
currencies,
|
||||
regions,
|
||||
pricePreferences,
|
||||
@@ -19,15 +17,12 @@ export const getPriceColumns = ({
|
||||
currencies?: string[]
|
||||
regions?: HttpTypes.AdminRegion[]
|
||||
pricePreferences?: HttpTypes.AdminPricePreference[]
|
||||
isReadyOnly?: (
|
||||
context: CellContext<string | HttpTypes.AdminRegion, unknown>
|
||||
) => boolean
|
||||
getFieldName: (
|
||||
context: CellContext<string | HttpTypes.AdminRegion, unknown>,
|
||||
value: string
|
||||
) => string
|
||||
isReadyOnly?: (context: CellContext<TData, unknown>) => boolean
|
||||
getFieldName: (context: CellContext<TData, unknown>, value: string) => string
|
||||
t: TFunction
|
||||
}) => {
|
||||
}): ColumnDef<TData, unknown>[] => {
|
||||
const columnHelper = createDataGridHelper<TData>()
|
||||
|
||||
return [
|
||||
...(currencies?.map((currency) => {
|
||||
const preference = pricePreferences?.find(
|
||||
@@ -60,6 +55,7 @@ export const getPriceColumns = ({
|
||||
/>
|
||||
)
|
||||
},
|
||||
type: "string",
|
||||
})
|
||||
}) ?? []),
|
||||
...(regions?.map((region) => {
|
||||
@@ -98,6 +94,7 @@ export const getPriceColumns = ({
|
||||
/>
|
||||
)
|
||||
},
|
||||
type: "string",
|
||||
})
|
||||
}) ?? []),
|
||||
]
|
||||
|
||||
+808
-337
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,18 @@
|
||||
import { CellContext } from "@tanstack/react-table"
|
||||
import { useContext, useEffect, useMemo } from "react"
|
||||
import React, {
|
||||
MouseEvent,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react"
|
||||
import { DataGridContext } from "./context"
|
||||
import {
|
||||
CellCoords,
|
||||
DataGridCellContainerProps,
|
||||
DataGridCellContext,
|
||||
DataGridCellRenderProps,
|
||||
} from "./types"
|
||||
import { generateCellId, isCellMatch } from "./utils"
|
||||
|
||||
@@ -23,11 +31,16 @@ const useDataGridContext = () => {
|
||||
type UseDataGridCellProps<TData, TValue> = {
|
||||
field: string
|
||||
context: CellContext<TData, TValue>
|
||||
type: "text" | "number" | "select"
|
||||
}
|
||||
|
||||
const textCharacterRegex = /^.$/u
|
||||
const numberCharacterRegex = /^[0-9]$/u
|
||||
|
||||
export const useDataGridCell = <TData, TValue>({
|
||||
field,
|
||||
context,
|
||||
type,
|
||||
}: UseDataGridCellProps<TData, TValue>) => {
|
||||
const { rowIndex, columnIndex } = context as DataGridCellContext<
|
||||
TData,
|
||||
@@ -44,45 +57,167 @@ export const useDataGridCell = <TData, TValue>({
|
||||
register,
|
||||
control,
|
||||
anchor,
|
||||
onRegisterCell,
|
||||
onUnregisterCell,
|
||||
getMouseOverHandler,
|
||||
getMouseDownHandler,
|
||||
getOnChangeHandler,
|
||||
selection,
|
||||
dragSelection,
|
||||
setIsEditing,
|
||||
setIsSelecting,
|
||||
setRangeEnd,
|
||||
getWrapperFocusHandler,
|
||||
getWrapperMouseOverHandler,
|
||||
getInputChangeHandler,
|
||||
registerCell,
|
||||
} = useDataGridContext()
|
||||
|
||||
useEffect(() => {
|
||||
onRegisterCell(coords)
|
||||
registerCell(coords, field)
|
||||
}, [coords, field, registerCell])
|
||||
|
||||
return () => {
|
||||
onUnregisterCell(coords)
|
||||
const [showOverlay, setShowOverlay] = useState(true)
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const inputRef = useRef<HTMLElement>(null)
|
||||
|
||||
const handleOverlayMouseDown = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
if (e.detail === 2) {
|
||||
if (inputRef.current) {
|
||||
setShowOverlay(false)
|
||||
|
||||
inputRef.current.focus()
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (e.shiftKey) {
|
||||
setRangeEnd(coords)
|
||||
return
|
||||
}
|
||||
|
||||
if (containerRef.current) {
|
||||
setIsSelecting(true)
|
||||
containerRef.current.focus()
|
||||
}
|
||||
},
|
||||
[setIsSelecting, setRangeEnd, coords]
|
||||
)
|
||||
|
||||
const handleInputBlur = useCallback(() => {
|
||||
setShowOverlay(true)
|
||||
setIsEditing(false)
|
||||
}, [setIsEditing])
|
||||
|
||||
const handleInputFocus = useCallback(() => {
|
||||
setShowOverlay(false)
|
||||
setIsEditing(true)
|
||||
}, [setIsEditing])
|
||||
|
||||
const validateKeyStroke = useCallback(
|
||||
(key: string) => {
|
||||
if (type === "number") {
|
||||
return numberCharacterRegex.test(key)
|
||||
}
|
||||
|
||||
if (type === "text") {
|
||||
return textCharacterRegex.test(key)
|
||||
}
|
||||
|
||||
// KeyboardEvents should not be forwareded to other types of cells
|
||||
return false
|
||||
},
|
||||
[type]
|
||||
)
|
||||
|
||||
const handleContainerKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (!inputRef.current || !validateKeyStroke(e.key) || !showOverlay) {
|
||||
return
|
||||
}
|
||||
|
||||
// Allow the user to undo/redo
|
||||
if (e.key.toLowerCase() === "z" && (e.ctrlKey || e.metaKey)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Allow the user to copy
|
||||
if (e.key.toLowerCase() === "c" && (e.ctrlKey || e.metaKey)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Allow the user to paste
|
||||
if (e.key.toLowerCase() === "v" && (e.ctrlKey || e.metaKey)) {
|
||||
return
|
||||
}
|
||||
|
||||
const event = new KeyboardEvent(e.type, e.nativeEvent)
|
||||
|
||||
inputRef.current.focus()
|
||||
setShowOverlay(false)
|
||||
|
||||
// if the inputRef can use .select() then we can use it here
|
||||
if (inputRef.current instanceof HTMLInputElement) {
|
||||
inputRef.current.select()
|
||||
}
|
||||
|
||||
inputRef.current.dispatchEvent(event)
|
||||
},
|
||||
[showOverlay, validateKeyStroke]
|
||||
)
|
||||
|
||||
const isAnchor = useMemo(() => {
|
||||
return anchor ? isCellMatch(coords, anchor) : false
|
||||
}, [anchor, coords])
|
||||
|
||||
const isSelected = useMemo(() => {
|
||||
return selection[id] || false
|
||||
}, [selection, id])
|
||||
|
||||
const isDragSelected = useMemo(() => {
|
||||
return dragSelection[id] || false
|
||||
}, [dragSelection, id])
|
||||
|
||||
useEffect(() => {
|
||||
if (isAnchor && !containerRef.current?.contains(document.activeElement)) {
|
||||
containerRef.current?.focus()
|
||||
}
|
||||
}, [coords, onRegisterCell, onUnregisterCell])
|
||||
}, [isAnchor])
|
||||
|
||||
const container: DataGridCellContainerProps = {
|
||||
isAnchor: anchor ? isCellMatch(coords, anchor) : false,
|
||||
wrapper: {
|
||||
onMouseDown: getMouseDownHandler(coords),
|
||||
onMouseOver: getMouseOverHandler(coords),
|
||||
const renderProps: DataGridCellRenderProps = {
|
||||
container: {
|
||||
isAnchor,
|
||||
isSelected,
|
||||
isDragSelected,
|
||||
showOverlay,
|
||||
innerProps: {
|
||||
ref: containerRef,
|
||||
onMouseOver: getWrapperMouseOverHandler(coords),
|
||||
onKeyDown: handleContainerKeyDown,
|
||||
onFocus: getWrapperFocusHandler(coords),
|
||||
"data-container-id": id,
|
||||
},
|
||||
overlayProps: {
|
||||
onMouseDown: handleOverlayMouseDown,
|
||||
},
|
||||
},
|
||||
overlay: {
|
||||
onClick: () => {},
|
||||
input: {
|
||||
ref: inputRef,
|
||||
onBlur: handleInputBlur,
|
||||
onFocus: handleInputFocus,
|
||||
onChange: getInputChangeHandler(field),
|
||||
"data-row": coords.row,
|
||||
"data-col": coords.col,
|
||||
"data-cell-id": id,
|
||||
"data-field": field,
|
||||
},
|
||||
}
|
||||
|
||||
const attributes = {
|
||||
"data-row": coords.row,
|
||||
"data-col": coords.col,
|
||||
"data-cell-id": id,
|
||||
"data-field": field,
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
register,
|
||||
control,
|
||||
attributes,
|
||||
container,
|
||||
onChange: getOnChangeHandler(field),
|
||||
renderProps,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,151 +1,211 @@
|
||||
import { Command } from "../../hooks/use-command-history"
|
||||
import { CellCoords } from "./types"
|
||||
|
||||
/**
|
||||
* A sorted set implementation that uses binary search to find the insertion index.
|
||||
*/
|
||||
export class SortedSet<T> {
|
||||
private items: T[] = []
|
||||
export class Matrix {
|
||||
private cells: (string | null)[][]
|
||||
|
||||
constructor(initialItems?: T[]) {
|
||||
if (initialItems) {
|
||||
this.insertMultiple(initialItems)
|
||||
}
|
||||
constructor(rows: number, cols: number) {
|
||||
this.cells = Array.from({ length: rows }, () => Array(cols).fill(null))
|
||||
}
|
||||
|
||||
insert(value: T): void {
|
||||
const insertionIndex = this.findInsertionIndex(value)
|
||||
|
||||
if (this.items[insertionIndex] !== value) {
|
||||
this.items.splice(insertionIndex, 0, value)
|
||||
}
|
||||
}
|
||||
|
||||
remove(value: T): void {
|
||||
const index = this.findInsertionIndex(value)
|
||||
|
||||
if (this.items[index] === value) {
|
||||
this.items.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
getPrev(value: T): T | null {
|
||||
const index = this.findInsertionIndex(value)
|
||||
if (index === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.items[index - 1]
|
||||
}
|
||||
|
||||
getNext(value: T): T | null {
|
||||
const index = this.findInsertionIndex(value)
|
||||
if (index === this.items.length - 1) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.items[index + 1]
|
||||
}
|
||||
|
||||
getFirst(): T | null {
|
||||
if (this.items.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.items[0]
|
||||
}
|
||||
|
||||
getLast(): T | null {
|
||||
if (this.items.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.items[this.items.length - 1]
|
||||
}
|
||||
|
||||
toArray(): T[] {
|
||||
return [...this.items]
|
||||
}
|
||||
|
||||
private insertMultiple(values: T[]): void {
|
||||
values.forEach((value) => this.insert(value))
|
||||
}
|
||||
|
||||
private findInsertionIndex(value: T): number {
|
||||
let left = 0
|
||||
let right = this.items.length - 1
|
||||
while (left <= right) {
|
||||
const mid = Math.floor((left + right) / 2)
|
||||
if (this.items[mid] === value) {
|
||||
return mid
|
||||
} else if (this.items[mid] < value) {
|
||||
left = mid + 1
|
||||
} else {
|
||||
right = mid - 1
|
||||
getFirstNavigableCell(): CellCoords | null {
|
||||
for (let row = 0; row < this.cells.length; row++) {
|
||||
for (let col = 0; col < this.cells[0].length; col++) {
|
||||
if (this.cells[row][col] !== null) {
|
||||
return { row, col }
|
||||
}
|
||||
}
|
||||
}
|
||||
return left
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Register a navigable cell with a unique key
|
||||
registerField(row: number, col: number, key: string) {
|
||||
if (this._isValidPosition(row, col)) {
|
||||
this.cells[row][col] = key
|
||||
}
|
||||
}
|
||||
|
||||
getFieldsInSelection(
|
||||
start: CellCoords | null,
|
||||
end: CellCoords | null
|
||||
): string[] {
|
||||
const keys: string[] = []
|
||||
|
||||
if (!start || !end) {
|
||||
return keys
|
||||
}
|
||||
|
||||
if (start.col !== end.col) {
|
||||
throw new Error("Selection must be in the same column")
|
||||
}
|
||||
|
||||
const startRow = Math.min(start.row, end.row)
|
||||
const endRow = Math.max(start.row, end.row)
|
||||
const col = start.col
|
||||
|
||||
for (let row = startRow; row <= endRow; row++) {
|
||||
if (this._isValidPosition(row, col) && this.cells[row][col] !== null) {
|
||||
keys.push(this.cells[row][col] as string)
|
||||
}
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
getCellKey(cell: CellCoords): string | null {
|
||||
if (this._isValidPosition(cell.row, cell.col)) {
|
||||
return this.cells[cell.row][cell.col]
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
getIsCellSelected(
|
||||
cell: CellCoords | null,
|
||||
start: CellCoords | null,
|
||||
end: CellCoords | null
|
||||
): boolean {
|
||||
if (!cell || !start || !end) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (start.col !== end.col) {
|
||||
throw new Error("Selection must be in the same column")
|
||||
}
|
||||
|
||||
const startRow = Math.min(start.row, end.row)
|
||||
const endRow = Math.max(start.row, end.row)
|
||||
const col = start.col
|
||||
|
||||
return cell.col === col && cell.row >= startRow && cell.row <= endRow
|
||||
}
|
||||
|
||||
getValidMovement(
|
||||
row: number,
|
||||
col: number,
|
||||
direction: string,
|
||||
metaKey: boolean = false
|
||||
): CellCoords {
|
||||
const [dRow, dCol] = this._getDirectionDeltas(direction)
|
||||
|
||||
if (metaKey) {
|
||||
return this._getLastValidCellInDirection(row, col, dRow, dCol)
|
||||
} else {
|
||||
let newRow = row + dRow
|
||||
let newCol = col + dCol
|
||||
|
||||
if (
|
||||
newRow < 0 ||
|
||||
newRow >= this.cells.length ||
|
||||
newCol < 0 ||
|
||||
newCol >= this.cells[0].length
|
||||
) {
|
||||
return { row, col }
|
||||
}
|
||||
|
||||
while (
|
||||
this._isValidPosition(newRow, newCol) &&
|
||||
this.cells[newRow][newCol] === null
|
||||
) {
|
||||
newRow += dRow
|
||||
newCol += dCol
|
||||
|
||||
if (
|
||||
newRow < 0 ||
|
||||
newRow >= this.cells.length ||
|
||||
newCol < 0 ||
|
||||
newCol >= this.cells[0].length
|
||||
) {
|
||||
return { row, col }
|
||||
}
|
||||
}
|
||||
|
||||
return this._isValidPosition(newRow, newCol)
|
||||
? { row: newRow, col: newCol }
|
||||
: { row, col }
|
||||
}
|
||||
}
|
||||
|
||||
private _isValidPosition(row: number, col: number): boolean {
|
||||
return (
|
||||
row >= 0 &&
|
||||
row < this.cells.length &&
|
||||
col >= 0 &&
|
||||
col < this.cells[0].length
|
||||
)
|
||||
}
|
||||
|
||||
private _getDirectionDeltas(direction: string): [number, number] {
|
||||
switch (direction) {
|
||||
case "ArrowUp":
|
||||
return [-1, 0]
|
||||
case "ArrowDown":
|
||||
return [1, 0]
|
||||
case "ArrowLeft":
|
||||
return [0, -1]
|
||||
case "ArrowRight":
|
||||
return [0, 1]
|
||||
default:
|
||||
return [0, 0]
|
||||
}
|
||||
}
|
||||
|
||||
private _getLastValidCellInDirection(
|
||||
row: number,
|
||||
col: number,
|
||||
dRow: number,
|
||||
dCol: number
|
||||
): CellCoords {
|
||||
let newRow = row
|
||||
let newCol = col
|
||||
let lastValidRow = row
|
||||
let lastValidCol = col
|
||||
|
||||
while (this._isValidPosition(newRow + dRow, newCol + dCol)) {
|
||||
newRow += dRow
|
||||
newCol += dCol
|
||||
if (this.cells[newRow][newCol] !== null) {
|
||||
lastValidRow = newRow
|
||||
lastValidCol = newCol
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
row: lastValidRow,
|
||||
col: lastValidCol,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type PasteCommandArgs = {
|
||||
selection: Record<string, boolean>
|
||||
export type BulkUpdateCommandArgs = {
|
||||
fields: string[]
|
||||
next: string[]
|
||||
prev: string[]
|
||||
setter: (selection: Record<string, boolean>, values: string[]) => void
|
||||
setter: (fields: string[], values: string[]) => void
|
||||
}
|
||||
|
||||
export class DeleteCommand implements Command {
|
||||
private _selection: Record<string, boolean>
|
||||
export class BulkUpdateCommand implements Command {
|
||||
private _fields: string[]
|
||||
|
||||
private _prev: string[]
|
||||
private _next: string[]
|
||||
|
||||
private _setter: (
|
||||
selection: Record<string, boolean>,
|
||||
values: string[]
|
||||
) => void
|
||||
private _setter: (string: string[], values: string[]) => void
|
||||
|
||||
constructor({ selection, prev, next, setter }: PasteCommandArgs) {
|
||||
this._selection = selection
|
||||
constructor({ fields, prev, next, setter }: BulkUpdateCommandArgs) {
|
||||
this._fields = fields
|
||||
this._prev = prev
|
||||
this._next = next
|
||||
this._setter = setter
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
this._setter(this._selection, this._next)
|
||||
this._setter(this._fields, this._next)
|
||||
}
|
||||
undo(): void {
|
||||
this._setter(this._selection, this._prev)
|
||||
}
|
||||
redo(): void {
|
||||
this.execute()
|
||||
}
|
||||
}
|
||||
|
||||
export class PasteCommand implements Command {
|
||||
private _selection: Record<string, boolean>
|
||||
|
||||
private _prev: string[]
|
||||
private _next: string[]
|
||||
|
||||
private _setter: (
|
||||
selection: Record<string, boolean>,
|
||||
values: string[]
|
||||
) => void
|
||||
|
||||
constructor({ selection, prev, next, setter }: PasteCommandArgs) {
|
||||
this._selection = selection
|
||||
this._prev = prev
|
||||
this._next = next
|
||||
this._setter = setter
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
this._setter(this._selection, this._next)
|
||||
}
|
||||
undo(): void {
|
||||
this._setter(this._selection, this._prev)
|
||||
this._setter(this._fields, this._prev)
|
||||
}
|
||||
redo(): void {
|
||||
this.execute()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CellContext } from "@tanstack/react-table"
|
||||
import { MouseEvent, ReactNode } from "react"
|
||||
import React, { PropsWithChildren, ReactNode, RefObject } from "react"
|
||||
|
||||
export type CellCoords = {
|
||||
row: number
|
||||
@@ -28,16 +28,42 @@ export interface DataGridCellContext<TData = unknown, TValue = any>
|
||||
rowIndex: number
|
||||
}
|
||||
|
||||
export interface DataGridCellContainerProps {
|
||||
export interface DataGridCellRenderProps {
|
||||
container: DataGridCellContainerProps
|
||||
input: InputProps
|
||||
}
|
||||
|
||||
export interface InputProps {
|
||||
ref: RefObject<HTMLElement>
|
||||
onBlur: () => void
|
||||
onFocus: () => void
|
||||
onChange: (next: any, prev: any) => void
|
||||
"data-row": number
|
||||
"data-col": number
|
||||
"data-cell-id": string
|
||||
"data-field": string
|
||||
}
|
||||
|
||||
interface InnerProps {
|
||||
ref: RefObject<HTMLDivElement>
|
||||
onMouseOver: ((e: React.MouseEvent<HTMLElement>) => void) | undefined
|
||||
onKeyDown: (e: React.KeyboardEvent<HTMLDivElement>) => void
|
||||
onFocus: (e: React.FocusEvent<HTMLElement>) => void
|
||||
"data-container-id": string
|
||||
}
|
||||
|
||||
interface OverlayProps {
|
||||
onMouseDown: (e: React.MouseEvent<HTMLElement>) => void
|
||||
}
|
||||
|
||||
export interface DataGridCellContainerProps extends PropsWithChildren<{}> {
|
||||
innerProps: InnerProps
|
||||
overlayProps: OverlayProps
|
||||
isAnchor: boolean
|
||||
isSelected: boolean
|
||||
isDragSelected: boolean
|
||||
placeholder?: ReactNode
|
||||
wrapper: {
|
||||
onMouseDown: (e: MouseEvent<HTMLElement>) => void
|
||||
onMouseOver: ((e: MouseEvent<HTMLElement>) => void) | undefined
|
||||
}
|
||||
overlay: {
|
||||
onClick: () => void
|
||||
}
|
||||
showOverlay: boolean
|
||||
}
|
||||
|
||||
export type DataGridColumnType = "string" | "number" | "boolean"
|
||||
|
||||
@@ -97,7 +97,7 @@ export function getFieldsInRange(
|
||||
}
|
||||
|
||||
export function convertArrayToPrimitive<
|
||||
T extends "boolean" | "number" | "string"
|
||||
T extends "boolean" | "number" | "string",
|
||||
>(values: string[], type: T) {
|
||||
const convertedValues: any[] = []
|
||||
|
||||
@@ -222,10 +222,10 @@ export function getColumnName(column: Column<any, any>): string {
|
||||
}
|
||||
|
||||
export function getColumnType(
|
||||
cellId: string,
|
||||
cell: CellCoords,
|
||||
columns: Column<any, any>[]
|
||||
): DataGridColumnType {
|
||||
const { col } = parseCellId(cellId)
|
||||
const { col } = cell
|
||||
|
||||
const column = columns[col]
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ const Header = () => {
|
||||
) : (
|
||||
<Skeleton className="h-6 w-6 rounded-md" />
|
||||
)}
|
||||
<div className="block overflow-hidden">
|
||||
<div className="block overflow-hidden text-left">
|
||||
{name ? (
|
||||
<Text
|
||||
size="small"
|
||||
|
||||
+8
-8
@@ -131,14 +131,14 @@ const SettingsSidebar = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<aside className="flex flex-1 flex-col justify-between overflow-y-auto">
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="bg-ui-bg-subtle sticky top-0 z-[1]">
|
||||
<Header />
|
||||
<div className="flex items-center justify-center px-3">
|
||||
<Divider variant="dashed" />
|
||||
</div>
|
||||
<aside className="relative flex flex-1 flex-col justify-between overflow-y-auto">
|
||||
<div className="bg-ui-bg-subtle sticky top-0">
|
||||
<Header />
|
||||
<div className="flex items-center justify-center px-3">
|
||||
<Divider variant="dashed" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="flex flex-1 flex-col overflow-y-auto">
|
||||
<CollapsibleSection
|
||||
label={t("app.nav.settings.general")}
|
||||
@@ -229,7 +229,7 @@ const CollapsibleSection = ({
|
||||
{label}
|
||||
</Text>
|
||||
<Collapsible.Trigger asChild>
|
||||
<IconButton size="2xsmall" variant="transparent">
|
||||
<IconButton size="2xsmall" variant="transparent" className="static">
|
||||
<MinusMini className="text-ui-fg-muted" />
|
||||
</IconButton>
|
||||
</Collapsible.Trigger>
|
||||
|
||||
+29
-7
@@ -2,6 +2,7 @@ import { FocusModal, clx } from "@medusajs/ui"
|
||||
import { PropsWithChildren, useEffect, useState } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { RouteModalForm } from "../route-modal-form"
|
||||
import { useRouteModal } from "../route-modal-provider"
|
||||
import { RouteModalProvider } from "../route-modal-provider/route-provider"
|
||||
import { StackedModalProvider } from "../stacked-modal-provider"
|
||||
|
||||
@@ -41,19 +42,40 @@ const Root = ({ prev = "..", children }: RouteFocusModalProps) => {
|
||||
<FocusModal open={open} onOpenChange={handleOpenChange}>
|
||||
<RouteModalProvider prev={prev}>
|
||||
<StackedModalProvider onOpenChange={onStackedModalOpen}>
|
||||
<FocusModal.Content
|
||||
className={clx({
|
||||
"!bg-ui-bg-disabled !inset-x-5 !inset-y-3": stackedModalOpen,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</FocusModal.Content>
|
||||
<Content stackedModalOpen={stackedModalOpen}>{children}</Content>
|
||||
</StackedModalProvider>
|
||||
</RouteModalProvider>
|
||||
</FocusModal>
|
||||
)
|
||||
}
|
||||
|
||||
type ContentProps = PropsWithChildren<{
|
||||
stackedModalOpen: boolean
|
||||
}>
|
||||
|
||||
const Content = ({ stackedModalOpen, children }: ContentProps) => {
|
||||
const { __internal } = useRouteModal()
|
||||
|
||||
const shouldPreventClose = !__internal.closeOnEscape
|
||||
|
||||
return (
|
||||
<FocusModal.Content
|
||||
onEscapeKeyDown={
|
||||
shouldPreventClose
|
||||
? (e) => {
|
||||
e.preventDefault()
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
className={clx({
|
||||
"!bg-ui-bg-disabled !inset-x-5 !inset-y-3": stackedModalOpen,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</FocusModal.Content>
|
||||
)
|
||||
}
|
||||
|
||||
const Header = FocusModal.Header
|
||||
const Title = FocusModal.Title
|
||||
const Description = FocusModal.Description
|
||||
|
||||
+4
@@ -2,6 +2,10 @@ import { createContext } from "react"
|
||||
|
||||
type RouteModalProviderState = {
|
||||
handleSuccess: (path?: string) => void
|
||||
setCloseOnEscape: (value: boolean) => void
|
||||
__internal: {
|
||||
closeOnEscape: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const RouteModalProviderContext =
|
||||
|
||||
+20
-6
@@ -1,4 +1,4 @@
|
||||
import { PropsWithChildren } from "react"
|
||||
import { PropsWithChildren, useCallback, useMemo, useState } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { RouteModalProviderContext } from "./route-modal-context"
|
||||
|
||||
@@ -12,13 +12,27 @@ export const RouteModalProvider = ({
|
||||
}: RouteModalProviderProps) => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleSuccess = (path?: string) => {
|
||||
const to = path || prev
|
||||
navigate(to, { replace: true, state: { isSubmitSuccessful: true } })
|
||||
}
|
||||
const [closeOnEscape, setCloseOnEscape] = useState(true)
|
||||
|
||||
const handleSuccess = useCallback(
|
||||
(path?: string) => {
|
||||
const to = path || prev
|
||||
navigate(to, { replace: true, state: { isSubmitSuccessful: true } })
|
||||
},
|
||||
[navigate, prev]
|
||||
)
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
handleSuccess,
|
||||
setCloseOnEscape,
|
||||
__internal: { closeOnEscape },
|
||||
}),
|
||||
[handleSuccess, setCloseOnEscape, closeOnEscape]
|
||||
)
|
||||
|
||||
return (
|
||||
<RouteModalProviderContext.Provider value={{ handleSuccess }}>
|
||||
<RouteModalProviderContext.Provider value={value}>
|
||||
{children}
|
||||
</RouteModalProviderContext.Provider>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user