fix(dashboard): Cell behaviour in DataGrid (#8183)
This commit is contained in:
@@ -39,10 +39,11 @@
|
|||||||
"@radix-ui/react-hover-card": "1.1.1",
|
"@radix-ui/react-hover-card": "1.1.1",
|
||||||
"@tanstack/react-query": "^5.28.14",
|
"@tanstack/react-query": "^5.28.14",
|
||||||
"@tanstack/react-table": "8.10.7",
|
"@tanstack/react-table": "8.10.7",
|
||||||
"@tanstack/react-virtual": "^3.0.4",
|
"@tanstack/react-virtual": "^3.8.3",
|
||||||
"@uiw/react-json-view": "^2.0.0-alpha.17",
|
"@uiw/react-json-view": "^2.0.0-alpha.17",
|
||||||
"cmdk": "^0.2.0",
|
"cmdk": "^0.2.0",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
|
"focus-trap-react": "^10.2.3",
|
||||||
"framer-motion": "^11.0.3",
|
"framer-motion": "^11.0.3",
|
||||||
"i18next": "23.7.11",
|
"i18next": "23.7.11",
|
||||||
"i18next-browser-languagedetector": "7.2.0",
|
"i18next-browser-languagedetector": "7.2.0",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export const IncludesTaxTooltip = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
maxWidth={999}
|
||||||
content={
|
content={
|
||||||
includesTax
|
includesTax
|
||||||
? t("general.includesTaxTooltip")
|
? 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 { Control, FieldValues, Path, UseFormRegister } from "react-hook-form"
|
||||||
import { CellCoords } from "./types"
|
import { CellCoords } from "./types"
|
||||||
|
|
||||||
type DataGridContextType<TForm extends FieldValues> = {
|
type DataGridContextType<TForm extends FieldValues> = {
|
||||||
|
// Grid state
|
||||||
anchor: CellCoords | null
|
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>
|
register: UseFormRegister<TForm>
|
||||||
control: Control<TForm>
|
control: Control<TForm>
|
||||||
onRegisterCell: (coordinates: CellCoords) => void
|
getInputChangeHandler: (field: Path<TForm>) => (next: any, prev: any) => void
|
||||||
onUnregisterCell: (coordinates: CellCoords) => void
|
// Wrapper handlers
|
||||||
getMouseDownHandler: (
|
getWrapperFocusHandler: (
|
||||||
coordinates: CellCoords
|
coordinates: CellCoords
|
||||||
) => (e: MouseEvent<HTMLElement>) => void
|
) => (e: FocusEvent<HTMLElement>) => void
|
||||||
getMouseOverHandler: (
|
getWrapperMouseOverHandler: (
|
||||||
coordinates: CellCoords
|
coordinates: CellCoords
|
||||||
) => ((e: MouseEvent<HTMLElement>) => void) | undefined
|
) => ((e: MouseEvent<HTMLElement>) => void) | undefined
|
||||||
getOnChangeHandler: (field: Path<TForm>) => (next: any, prev: any) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DataGridContext = createContext<DataGridContextType<any> | null>(
|
export const DataGridContext = createContext<DataGridContextType<any> | null>(
|
||||||
|
|||||||
+47
-11
@@ -1,7 +1,8 @@
|
|||||||
import { Checkbox } from "@medusajs/ui"
|
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 { useDataGridCell } from "../hooks"
|
||||||
import { DataGridCellProps } from "../types"
|
import { DataGridCellProps, InputProps } from "../types"
|
||||||
import { DataGridCellContainer } from "./data-grid-cell-container"
|
import { DataGridCellContainer } from "./data-grid-cell-container"
|
||||||
|
|
||||||
export const DataGridBooleanCell = <TData, TValue = any>({
|
export const DataGridBooleanCell = <TData, TValue = any>({
|
||||||
@@ -9,28 +10,63 @@ export const DataGridBooleanCell = <TData, TValue = any>({
|
|||||||
context,
|
context,
|
||||||
disabled,
|
disabled,
|
||||||
}: DataGridCellProps<TData, TValue> & { disabled?: boolean }) => {
|
}: DataGridCellProps<TData, TValue> & { disabled?: boolean }) => {
|
||||||
const { control, attributes, container, onChange } = useDataGridCell({
|
const { control, renderProps } = useDataGridCell({
|
||||||
field,
|
field,
|
||||||
context,
|
context,
|
||||||
|
type: "select",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { container, input } = renderProps
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={field}
|
name={field}
|
||||||
render={({ field: { value, onChange: _, ...field } }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<DataGridCellContainer {...container}>
|
<DataGridCellContainer {...container}>
|
||||||
<Checkbox
|
<Inner field={field} inputProps={input} disabled={disabled} />
|
||||||
checked={value}
|
|
||||||
onCheckedChange={(next) => onChange(next, value)}
|
|
||||||
{...field}
|
|
||||||
{...attributes}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
</DataGridCellContainer>
|
</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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
+21
-16
@@ -1,37 +1,42 @@
|
|||||||
|
import { clx } from "@medusajs/ui"
|
||||||
import { PropsWithChildren } from "react"
|
import { PropsWithChildren } from "react"
|
||||||
import { DataGridCellContainerProps } from "../types"
|
|
||||||
|
|
||||||
type ContainerProps = PropsWithChildren<DataGridCellContainerProps>
|
import { DataGridCellContainerProps } from "../types"
|
||||||
|
|
||||||
export const DataGridCellContainer = ({
|
export const DataGridCellContainer = ({
|
||||||
isAnchor,
|
isAnchor,
|
||||||
|
isSelected,
|
||||||
|
isDragSelected,
|
||||||
|
showOverlay,
|
||||||
placeholder,
|
placeholder,
|
||||||
overlay,
|
innerProps,
|
||||||
wrapper,
|
overlayProps,
|
||||||
children,
|
children,
|
||||||
}: ContainerProps) => {
|
}: DataGridCellContainerProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="static size-full">
|
<div
|
||||||
<div className="flex size-full items-start outline-none" tabIndex={-1}>
|
className={clx("bg-ui-bg-base relative size-full outline-none", {
|
||||||
<div {...wrapper} className="relative size-full min-w-0 flex-1">
|
"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">
|
<div className="relative z-[1] flex size-full items-center justify-center">
|
||||||
<RenderChildren isAnchor={isAnchor} placeholder={placeholder}>
|
<RenderChildren isAnchor={isAnchor} placeholder={placeholder}>
|
||||||
{children}
|
{children}
|
||||||
</RenderChildren>
|
</RenderChildren>
|
||||||
</div>
|
</div>
|
||||||
{!isAnchor && (
|
{showOverlay && (
|
||||||
<div
|
<div
|
||||||
{...overlay}
|
{...overlayProps}
|
||||||
tabIndex={-1}
|
data-cell-overlay="true"
|
||||||
className="absolute inset-0 z-[2] size-full"
|
className="absolute inset-0 z-[2] size-full"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{/* {showDragHandle && (
|
|
||||||
<div className="bg-ui-bg-interactive absolute -bottom-[1.5px] -right-[1.5px] size-[3px]" />
|
|
||||||
)} */}
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+142
-109
@@ -1,116 +1,149 @@
|
|||||||
import { TrianglesMini } from "@medusajs/icons"
|
// Not currently used, re-implement or delete depending on whether there is a need for it in the future.
|
||||||
import { clx } from "@medusajs/ui"
|
|
||||||
import { ComponentPropsWithoutRef, forwardRef, memo } from "react"
|
|
||||||
import { Controller } from "react-hook-form"
|
|
||||||
|
|
||||||
import { countries } from "../../../lib/data/countries"
|
// import { TrianglesMini } from "@medusajs/icons"
|
||||||
import { useDataGridCell } from "../hooks"
|
// import { clx } from "@medusajs/ui"
|
||||||
import { DataGridCellProps } from "../types"
|
// import { ComponentPropsWithoutRef, forwardRef, memo } from "react"
|
||||||
import { DataGridCellContainer } from "./data-grid-cell-container"
|
// import { Controller, ControllerRenderProps } from "react-hook-form"
|
||||||
|
|
||||||
export const DataGridCountrySelectCell = <TData, TValue = any>({
|
// import { useCombinedRefs } from "../../../hooks/use-combined-refs"
|
||||||
field,
|
// import { countries } from "../../../lib/data/countries"
|
||||||
context,
|
// import { useDataGridCell } from "../hooks"
|
||||||
}: DataGridCellProps<TData, TValue>) => {
|
// import { DataGridCellProps, InputProps } from "../types"
|
||||||
const { control, attributes, container, onChange } = useDataGridCell({
|
// import { DataGridCellContainer } from "./data-grid-cell-container"
|
||||||
field,
|
|
||||||
context,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
// export const DataGridCountrySelectCell = <TData, TValue = any>({
|
||||||
<Controller
|
// field,
|
||||||
control={control}
|
// context,
|
||||||
name={field}
|
// }: DataGridCellProps<TData, TValue>) => {
|
||||||
render={({ field: { value, onChange: _, disabled, ...field } }) => {
|
// const { control, renderProps } = useDataGridCell({
|
||||||
return (
|
// field,
|
||||||
<DataGridCellContainer
|
// context,
|
||||||
{...container}
|
// type: "select",
|
||||||
placeholder={
|
// })
|
||||||
<DataGridCountryCellPlaceholder
|
|
||||||
value={value}
|
|
||||||
disabled={disabled}
|
|
||||||
attributes={attributes}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<MemoizedDataGridCountryCell
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange(e.target.value, value)}
|
|
||||||
disabled={disabled}
|
|
||||||
{...attributes}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</DataGridCellContainer>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const DataGridCountryCellPlaceholder = ({
|
// const { container, input } = renderProps
|
||||||
value,
|
|
||||||
disabled,
|
|
||||||
attributes,
|
|
||||||
}: {
|
|
||||||
value?: string
|
|
||||||
disabled?: boolean
|
|
||||||
attributes: Record<string, any>
|
|
||||||
}) => {
|
|
||||||
const country = countries.find((c) => c.iso_2 === value)
|
|
||||||
|
|
||||||
return (
|
// return (
|
||||||
<div className="relative flex size-full" {...attributes}>
|
// <Controller
|
||||||
<TrianglesMini
|
// control={control}
|
||||||
className={clx(
|
// name={field}
|
||||||
"text-ui-fg-muted transition-fg pointer-events-none absolute right-4 top-1/2 -translate-y-1/2",
|
// render={({ field: { value, onChange: _, disabled, ...field } }) => {
|
||||||
{
|
// return (
|
||||||
"text-ui-fg-disabled": disabled,
|
// <DataGridCellContainer
|
||||||
}
|
// {...container}
|
||||||
)}
|
// placeholder={
|
||||||
/>
|
// <DataGridCountryCellPlaceholder
|
||||||
<div
|
// value={value}
|
||||||
className={clx(
|
// disabled={disabled}
|
||||||
"txt-compact-small w-full appearance-none bg-transparent px-4 py-2.5 outline-none"
|
// attributes={attributes}
|
||||||
)}
|
// />
|
||||||
>
|
// }
|
||||||
{country?.display_name}
|
// >
|
||||||
</div>
|
// <MemoizedDataGridCountryCell
|
||||||
</div>
|
// value={value}
|
||||||
)
|
// onChange={(e) => onChange(e.target.value, value)}
|
||||||
}
|
// disabled={disabled}
|
||||||
|
// {...attributes}
|
||||||
|
// {...field}
|
||||||
|
// />
|
||||||
|
// </DataGridCellContainer>
|
||||||
|
// )
|
||||||
|
// }}
|
||||||
|
// />
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
const DataGridCountryCellImpl = forwardRef<
|
// const Inner = ({
|
||||||
HTMLSelectElement,
|
// field,
|
||||||
ComponentPropsWithoutRef<"select">
|
// inputProps,
|
||||||
>(({ disabled, className, ...props }, ref) => {
|
// }: {
|
||||||
return (
|
// field: ControllerRenderProps<any, string>
|
||||||
<div className="relative flex size-full">
|
// inputProps: InputProps
|
||||||
<TrianglesMini
|
// }) => {
|
||||||
className={clx(
|
// const { value, onChange, onBlur, ref, ...rest } = field
|
||||||
"text-ui-fg-muted transition-fg pointer-events-none absolute right-4 top-1/2 -translate-y-1/2",
|
// const { ref: inputRef, onBlur: onInputBlur, ...input } = inputProps
|
||||||
{
|
|
||||||
"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)
|
// 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 CurrencyInput, {
|
||||||
import { Controller } from "react-hook-form"
|
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 { useDataGridCell } from "../hooks"
|
||||||
import { DataGridCellProps } from "../types"
|
import { DataGridCellProps, InputProps } from "../types"
|
||||||
import { DataGridCellContainer } from "./data-grid-cell-container"
|
import { DataGridCellContainer } from "./data-grid-cell-container"
|
||||||
|
|
||||||
interface DataGridCurrencyCellProps<TData, TValue = any>
|
interface DataGridCurrencyCellProps<TData, TValue = any>
|
||||||
@@ -16,39 +21,122 @@ export const DataGridCurrencyCell = <TData, TValue = any>({
|
|||||||
context,
|
context,
|
||||||
code,
|
code,
|
||||||
}: DataGridCurrencyCellProps<TData, TValue>) => {
|
}: DataGridCurrencyCellProps<TData, TValue>) => {
|
||||||
const { control, attributes, container } = useDataGridCell({
|
const { control, renderProps } = useDataGridCell({
|
||||||
field,
|
field,
|
||||||
context,
|
context,
|
||||||
|
type: "number",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { container, input } = renderProps
|
||||||
|
|
||||||
const currency = currencies[code.toUpperCase()]
|
const currency = currencies[code.toUpperCase()]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={field}
|
name={field}
|
||||||
render={({ field: { value, onChange, ...field } }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<DataGridCellContainer {...container}>
|
<DataGridCellContainer {...container}>
|
||||||
<div className="flex size-full items-center gap-2 px-4 py-2.5">
|
<Inner field={field} inputProps={input} currencyInfo={currency} />
|
||||||
<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>
|
|
||||||
</DataGridCellContainer>
|
</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
+75
-21
@@ -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 { useDataGridCell } from "../hooks"
|
||||||
import { DataGridCellProps } from "../types"
|
import { DataGridCellProps, InputProps } from "../types"
|
||||||
import { DataGridCellContainer } from "./data-grid-cell-container"
|
import { DataGridCellContainer } from "./data-grid-cell-container"
|
||||||
|
|
||||||
export const DataGridNumberCell = <TData, TValue = any>({
|
export const DataGridNumberCell = <TData, TValue = any>({
|
||||||
@@ -11,32 +15,82 @@ export const DataGridNumberCell = <TData, TValue = any>({
|
|||||||
max?: number
|
max?: number
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
}) => {
|
}) => {
|
||||||
const { register, attributes, container } = useDataGridCell({
|
const { control, renderProps } = useDataGridCell({
|
||||||
field,
|
field,
|
||||||
context,
|
context,
|
||||||
|
type: "number",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { container, input } = renderProps
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={field}
|
||||||
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<DataGridCellContainer {...container}>
|
<DataGridCellContainer {...container}>
|
||||||
<input
|
<Inner field={field} inputProps={input} {...rest} />
|
||||||
{...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
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
</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>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -6,8 +6,8 @@ export const DataGridReadOnlyCell = ({
|
|||||||
children,
|
children,
|
||||||
}: DataGridReadOnlyCellProps) => {
|
}: DataGridReadOnlyCellProps) => {
|
||||||
return (
|
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">
|
<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>{children}</span>
|
<span className="truncate">{children}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+52
-50
@@ -1,53 +1,55 @@
|
|||||||
import { Select, clx } from "@medusajs/ui"
|
// Not currently used, re-implement or delete depending on whether there is a need for it in the future.
|
||||||
import { Controller } from "react-hook-form"
|
|
||||||
import { useDataGridCell } from "../hooks"
|
|
||||||
import { DataGridCellProps } from "../types"
|
|
||||||
import { DataGridCellContainer } from "./data-grid-cell-container"
|
|
||||||
|
|
||||||
interface DataGridSelectCellProps<TData, TValue = any>
|
// import { Select, clx } from "@medusajs/ui"
|
||||||
extends DataGridCellProps<TData, TValue> {
|
// import { Controller } from "react-hook-form"
|
||||||
options: { label: string; value: string }[]
|
// import { useDataGridCell } from "../hooks"
|
||||||
}
|
// import { DataGridCellProps } from "../types"
|
||||||
|
// import { DataGridCellContainer } from "./data-grid-cell-container"
|
||||||
|
|
||||||
export const DataGridSelectCell = <TData, TValue = any>({
|
// interface DataGridSelectCellProps<TData, TValue = any>
|
||||||
context,
|
// extends DataGridCellProps<TData, TValue> {
|
||||||
options,
|
// options: { label: string; value: string }[]
|
||||||
field,
|
// }
|
||||||
}: DataGridSelectCellProps<TData, TValue>) => {
|
|
||||||
const { control, attributes, container } = useDataGridCell({
|
|
||||||
field,
|
|
||||||
context,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
// export const DataGridSelectCell = <TData, TValue = any>({
|
||||||
<Controller
|
// context,
|
||||||
control={control}
|
// options,
|
||||||
name={field}
|
// field,
|
||||||
render={({ field: { onChange, ref, ...field } }) => {
|
// }: DataGridSelectCellProps<TData, TValue>) => {
|
||||||
return (
|
// const { control, attributes, container } = useDataGridCell({
|
||||||
<DataGridCellContainer {...container}>
|
// field,
|
||||||
<Select {...field} onValueChange={onChange}>
|
// context,
|
||||||
<Select.Trigger
|
// })
|
||||||
{...attributes}
|
|
||||||
ref={ref}
|
// return (
|
||||||
className={clx(
|
// <Controller
|
||||||
"h-full w-full rounded-none bg-transparent px-4 py-2.5 shadow-none",
|
// control={control}
|
||||||
"hover:bg-transparent focus:shadow-none data-[state=open]:!shadow-none"
|
// name={field}
|
||||||
)}
|
// render={({ field: { onChange, ref, ...field } }) => {
|
||||||
>
|
// return (
|
||||||
<Select.Value />
|
// <DataGridCellContainer {...container}>
|
||||||
</Select.Trigger>
|
// <Select {...field} onValueChange={onChange}>
|
||||||
<Select.Content>
|
// <Select.Trigger
|
||||||
{options.map((option) => (
|
// {...attributes}
|
||||||
<Select.Item key={option.value} value={option.value}>
|
// ref={ref}
|
||||||
{option.label}
|
// className={clx(
|
||||||
</Select.Item>
|
// "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.Content>
|
// )}
|
||||||
</Select>
|
// >
|
||||||
</DataGridCellContainer>
|
// <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>
|
||||||
|
// )
|
||||||
|
// }}
|
||||||
|
// />
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|||||||
+47
-11
@@ -1,26 +1,58 @@
|
|||||||
import { clx } from "@medusajs/ui"
|
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 { useDataGridCell } from "../hooks"
|
||||||
import { DataGridCellProps } from "../types"
|
import { DataGridCellProps, InputProps } from "../types"
|
||||||
import { DataGridCellContainer } from "./data-grid-cell-container"
|
import { DataGridCellContainer } from "./data-grid-cell-container"
|
||||||
|
|
||||||
export const DataGridTextCell = <TData, TValue = any>({
|
export const DataGridTextCell = <TData, TValue = any>({
|
||||||
field,
|
field,
|
||||||
context,
|
context,
|
||||||
}: DataGridCellProps<TData, TValue>) => {
|
}: DataGridCellProps<TData, TValue>) => {
|
||||||
const { control, attributes, container, onChange } = useDataGridCell({
|
const { control, renderProps } = useDataGridCell({
|
||||||
field,
|
field,
|
||||||
context,
|
context,
|
||||||
|
type: "text",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { container, input } = renderProps
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={field}
|
name={field}
|
||||||
render={({ field: { value, onChange: _, ...field } }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<DataGridCellContainer {...container}>
|
<DataGridCellContainer {...container}>
|
||||||
|
<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
|
<input
|
||||||
className={clx(
|
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",
|
"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",
|
||||||
@@ -28,14 +60,18 @@ export const DataGridTextCell = <TData, TValue = any>({
|
|||||||
)}
|
)}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
value={value}
|
value={localValue}
|
||||||
onChange={(e) => onChange(e.target.value, value)}
|
onChange={(e) => setLocalValue(e.target.value)}
|
||||||
{...attributes}
|
ref={combinedRefs}
|
||||||
{...field}
|
onBlur={() => {
|
||||||
/>
|
onBlur()
|
||||||
</DataGridCellContainer>
|
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 { HttpTypes } from "@medusajs/types"
|
||||||
import { DataGridCurrencyCell } from "../data-grid-cells/data-grid-currency-cell"
|
import { CellContext, ColumnDef } from "@tanstack/react-table"
|
||||||
import { createDataGridHelper } from "../utils"
|
|
||||||
import { IncludesTaxTooltip } from "../../../components/common/tax-badge/tax-badge"
|
|
||||||
import { TFunction } from "i18next"
|
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 { DataGridReadOnlyCell } from "../data-grid-cells/data-grid-readonly-cell"
|
||||||
|
import { createDataGridHelper } from "../utils"
|
||||||
|
|
||||||
const columnHelper = createDataGridHelper<string | HttpTypes.AdminRegion>()
|
export const getPriceColumns = <TData,>({
|
||||||
|
|
||||||
export const getPriceColumns = ({
|
|
||||||
currencies,
|
currencies,
|
||||||
regions,
|
regions,
|
||||||
pricePreferences,
|
pricePreferences,
|
||||||
@@ -19,15 +17,12 @@ export const getPriceColumns = ({
|
|||||||
currencies?: string[]
|
currencies?: string[]
|
||||||
regions?: HttpTypes.AdminRegion[]
|
regions?: HttpTypes.AdminRegion[]
|
||||||
pricePreferences?: HttpTypes.AdminPricePreference[]
|
pricePreferences?: HttpTypes.AdminPricePreference[]
|
||||||
isReadyOnly?: (
|
isReadyOnly?: (context: CellContext<TData, unknown>) => boolean
|
||||||
context: CellContext<string | HttpTypes.AdminRegion, unknown>
|
getFieldName: (context: CellContext<TData, unknown>, value: string) => string
|
||||||
) => boolean
|
|
||||||
getFieldName: (
|
|
||||||
context: CellContext<string | HttpTypes.AdminRegion, unknown>,
|
|
||||||
value: string
|
|
||||||
) => string
|
|
||||||
t: TFunction
|
t: TFunction
|
||||||
}) => {
|
}): ColumnDef<TData, unknown>[] => {
|
||||||
|
const columnHelper = createDataGridHelper<TData>()
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...(currencies?.map((currency) => {
|
...(currencies?.map((currency) => {
|
||||||
const preference = pricePreferences?.find(
|
const preference = pricePreferences?.find(
|
||||||
@@ -60,6 +55,7 @@ export const getPriceColumns = ({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
type: "string",
|
||||||
})
|
})
|
||||||
}) ?? []),
|
}) ?? []),
|
||||||
...(regions?.map((region) => {
|
...(regions?.map((region) => {
|
||||||
@@ -98,6 +94,7 @@ export const getPriceColumns = ({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
type: "string",
|
||||||
})
|
})
|
||||||
}) ?? []),
|
}) ?? []),
|
||||||
]
|
]
|
||||||
|
|||||||
+750
-279
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,18 @@
|
|||||||
import { CellContext } from "@tanstack/react-table"
|
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 { DataGridContext } from "./context"
|
||||||
import {
|
import {
|
||||||
CellCoords,
|
CellCoords,
|
||||||
DataGridCellContainerProps,
|
|
||||||
DataGridCellContext,
|
DataGridCellContext,
|
||||||
|
DataGridCellRenderProps,
|
||||||
} from "./types"
|
} from "./types"
|
||||||
import { generateCellId, isCellMatch } from "./utils"
|
import { generateCellId, isCellMatch } from "./utils"
|
||||||
|
|
||||||
@@ -23,11 +31,16 @@ const useDataGridContext = () => {
|
|||||||
type UseDataGridCellProps<TData, TValue> = {
|
type UseDataGridCellProps<TData, TValue> = {
|
||||||
field: string
|
field: string
|
||||||
context: CellContext<TData, TValue>
|
context: CellContext<TData, TValue>
|
||||||
|
type: "text" | "number" | "select"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const textCharacterRegex = /^.$/u
|
||||||
|
const numberCharacterRegex = /^[0-9]$/u
|
||||||
|
|
||||||
export const useDataGridCell = <TData, TValue>({
|
export const useDataGridCell = <TData, TValue>({
|
||||||
field,
|
field,
|
||||||
context,
|
context,
|
||||||
|
type,
|
||||||
}: UseDataGridCellProps<TData, TValue>) => {
|
}: UseDataGridCellProps<TData, TValue>) => {
|
||||||
const { rowIndex, columnIndex } = context as DataGridCellContext<
|
const { rowIndex, columnIndex } = context as DataGridCellContext<
|
||||||
TData,
|
TData,
|
||||||
@@ -44,45 +57,167 @@ export const useDataGridCell = <TData, TValue>({
|
|||||||
register,
|
register,
|
||||||
control,
|
control,
|
||||||
anchor,
|
anchor,
|
||||||
onRegisterCell,
|
selection,
|
||||||
onUnregisterCell,
|
dragSelection,
|
||||||
getMouseOverHandler,
|
setIsEditing,
|
||||||
getMouseDownHandler,
|
setIsSelecting,
|
||||||
getOnChangeHandler,
|
setRangeEnd,
|
||||||
|
getWrapperFocusHandler,
|
||||||
|
getWrapperMouseOverHandler,
|
||||||
|
getInputChangeHandler,
|
||||||
|
registerCell,
|
||||||
} = useDataGridContext()
|
} = useDataGridContext()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onRegisterCell(coords)
|
registerCell(coords, field)
|
||||||
|
}, [coords, field, registerCell])
|
||||||
|
|
||||||
return () => {
|
const [showOverlay, setShowOverlay] = useState(true)
|
||||||
onUnregisterCell(coords)
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}, [coords, onRegisterCell, onUnregisterCell])
|
|
||||||
|
|
||||||
const container: DataGridCellContainerProps = {
|
|
||||||
isAnchor: anchor ? isCellMatch(coords, anchor) : false,
|
|
||||||
wrapper: {
|
|
||||||
onMouseDown: getMouseDownHandler(coords),
|
|
||||||
onMouseOver: getMouseOverHandler(coords),
|
|
||||||
},
|
|
||||||
overlay: {
|
|
||||||
onClick: () => {},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const attributes = {
|
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()
|
||||||
|
}
|
||||||
|
}, [isAnchor])
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
ref: inputRef,
|
||||||
|
onBlur: handleInputBlur,
|
||||||
|
onFocus: handleInputFocus,
|
||||||
|
onChange: getInputChangeHandler(field),
|
||||||
"data-row": coords.row,
|
"data-row": coords.row,
|
||||||
"data-col": coords.col,
|
"data-col": coords.col,
|
||||||
"data-cell-id": id,
|
"data-cell-id": id,
|
||||||
"data-field": field,
|
"data-field": field,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
register,
|
register,
|
||||||
control,
|
control,
|
||||||
attributes,
|
renderProps,
|
||||||
container,
|
|
||||||
onChange: getOnChangeHandler(field),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,151 +1,211 @@
|
|||||||
import { Command } from "../../hooks/use-command-history"
|
import { Command } from "../../hooks/use-command-history"
|
||||||
|
import { CellCoords } from "./types"
|
||||||
|
|
||||||
/**
|
export class Matrix {
|
||||||
* A sorted set implementation that uses binary search to find the insertion index.
|
private cells: (string | null)[][]
|
||||||
*/
|
|
||||||
export class SortedSet<T> {
|
|
||||||
private items: T[] = []
|
|
||||||
|
|
||||||
constructor(initialItems?: T[]) {
|
constructor(rows: number, cols: number) {
|
||||||
if (initialItems) {
|
this.cells = Array.from({ length: rows }, () => Array(cols).fill(null))
|
||||||
this.insertMultiple(initialItems)
|
}
|
||||||
|
|
||||||
|
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 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 null
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.items[index - 1]
|
// 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]
|
||||||
}
|
}
|
||||||
|
|
||||||
getNext(value: T): T | null {
|
|
||||||
const index = this.findInsertionIndex(value)
|
|
||||||
if (index === this.items.length - 1) {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.items[index + 1]
|
getIsCellSelected(
|
||||||
|
cell: CellCoords | null,
|
||||||
|
start: CellCoords | null,
|
||||||
|
end: CellCoords | null
|
||||||
|
): boolean {
|
||||||
|
if (!cell || !start || !end) {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
getFirst(): T | null {
|
if (start.col !== end.col) {
|
||||||
if (this.items.length === 0) {
|
throw new Error("Selection must be in the same column")
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.items[0]
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
getLast(): T | null {
|
getValidMovement(
|
||||||
if (this.items.length === 0) {
|
row: number,
|
||||||
return null
|
col: number,
|
||||||
}
|
direction: string,
|
||||||
|
metaKey: boolean = false
|
||||||
|
): CellCoords {
|
||||||
|
const [dRow, dCol] = this._getDirectionDeltas(direction)
|
||||||
|
|
||||||
return this.items[this.items.length - 1]
|
if (metaKey) {
|
||||||
}
|
return this._getLastValidCellInDirection(row, col, dRow, dCol)
|
||||||
|
|
||||||
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 {
|
} else {
|
||||||
right = mid - 1
|
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 left
|
|
||||||
|
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 = {
|
export type BulkUpdateCommandArgs = {
|
||||||
selection: Record<string, boolean>
|
fields: string[]
|
||||||
next: string[]
|
next: string[]
|
||||||
prev: string[]
|
prev: string[]
|
||||||
setter: (selection: Record<string, boolean>, values: string[]) => void
|
setter: (fields: string[], values: string[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DeleteCommand implements Command {
|
export class BulkUpdateCommand implements Command {
|
||||||
private _selection: Record<string, boolean>
|
private _fields: string[]
|
||||||
|
|
||||||
private _prev: string[]
|
private _prev: string[]
|
||||||
private _next: string[]
|
private _next: string[]
|
||||||
|
|
||||||
private _setter: (
|
private _setter: (string: string[], values: string[]) => void
|
||||||
selection: Record<string, boolean>,
|
|
||||||
values: string[]
|
|
||||||
) => void
|
|
||||||
|
|
||||||
constructor({ selection, prev, next, setter }: PasteCommandArgs) {
|
constructor({ fields, prev, next, setter }: BulkUpdateCommandArgs) {
|
||||||
this._selection = selection
|
this._fields = fields
|
||||||
this._prev = prev
|
this._prev = prev
|
||||||
this._next = next
|
this._next = next
|
||||||
this._setter = setter
|
this._setter = setter
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): void {
|
||||||
this._setter(this._selection, this._next)
|
this._setter(this._fields, this._next)
|
||||||
}
|
}
|
||||||
undo(): void {
|
undo(): void {
|
||||||
this._setter(this._selection, this._prev)
|
this._setter(this._fields, 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)
|
|
||||||
}
|
}
|
||||||
redo(): void {
|
redo(): void {
|
||||||
this.execute()
|
this.execute()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CellContext } from "@tanstack/react-table"
|
import { CellContext } from "@tanstack/react-table"
|
||||||
import { MouseEvent, ReactNode } from "react"
|
import React, { PropsWithChildren, ReactNode, RefObject } from "react"
|
||||||
|
|
||||||
export type CellCoords = {
|
export type CellCoords = {
|
||||||
row: number
|
row: number
|
||||||
@@ -28,16 +28,42 @@ export interface DataGridCellContext<TData = unknown, TValue = any>
|
|||||||
rowIndex: number
|
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
|
isAnchor: boolean
|
||||||
|
isSelected: boolean
|
||||||
|
isDragSelected: boolean
|
||||||
placeholder?: ReactNode
|
placeholder?: ReactNode
|
||||||
wrapper: {
|
showOverlay: boolean
|
||||||
onMouseDown: (e: MouseEvent<HTMLElement>) => void
|
|
||||||
onMouseOver: ((e: MouseEvent<HTMLElement>) => void) | undefined
|
|
||||||
}
|
|
||||||
overlay: {
|
|
||||||
onClick: () => void
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DataGridColumnType = "string" | "number" | "boolean"
|
export type DataGridColumnType = "string" | "number" | "boolean"
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export function getFieldsInRange(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function convertArrayToPrimitive<
|
export function convertArrayToPrimitive<
|
||||||
T extends "boolean" | "number" | "string"
|
T extends "boolean" | "number" | "string",
|
||||||
>(values: string[], type: T) {
|
>(values: string[], type: T) {
|
||||||
const convertedValues: any[] = []
|
const convertedValues: any[] = []
|
||||||
|
|
||||||
@@ -222,10 +222,10 @@ export function getColumnName(column: Column<any, any>): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getColumnType(
|
export function getColumnType(
|
||||||
cellId: string,
|
cell: CellCoords,
|
||||||
columns: Column<any, any>[]
|
columns: Column<any, any>[]
|
||||||
): DataGridColumnType {
|
): DataGridColumnType {
|
||||||
const { col } = parseCellId(cellId)
|
const { col } = cell
|
||||||
|
|
||||||
const column = columns[col]
|
const column = columns[col]
|
||||||
|
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ const Header = () => {
|
|||||||
) : (
|
) : (
|
||||||
<Skeleton className="h-6 w-6 rounded-md" />
|
<Skeleton className="h-6 w-6 rounded-md" />
|
||||||
)}
|
)}
|
||||||
<div className="block overflow-hidden">
|
<div className="block overflow-hidden text-left">
|
||||||
{name ? (
|
{name ? (
|
||||||
<Text
|
<Text
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
+4
-4
@@ -131,14 +131,14 @@ const SettingsSidebar = () => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="flex flex-1 flex-col justify-between overflow-y-auto">
|
<aside className="relative 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">
|
||||||
<div className="bg-ui-bg-subtle sticky top-0 z-[1]">
|
|
||||||
<Header />
|
<Header />
|
||||||
<div className="flex items-center justify-center px-3">
|
<div className="flex items-center justify-center px-3">
|
||||||
<Divider variant="dashed" />
|
<Divider variant="dashed" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-1 flex-col">
|
||||||
<div className="flex flex-1 flex-col overflow-y-auto">
|
<div className="flex flex-1 flex-col overflow-y-auto">
|
||||||
<CollapsibleSection
|
<CollapsibleSection
|
||||||
label={t("app.nav.settings.general")}
|
label={t("app.nav.settings.general")}
|
||||||
@@ -229,7 +229,7 @@ const CollapsibleSection = ({
|
|||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
<Collapsible.Trigger asChild>
|
<Collapsible.Trigger asChild>
|
||||||
<IconButton size="2xsmall" variant="transparent">
|
<IconButton size="2xsmall" variant="transparent" className="static">
|
||||||
<MinusMini className="text-ui-fg-muted" />
|
<MinusMini className="text-ui-fg-muted" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Collapsible.Trigger>
|
</Collapsible.Trigger>
|
||||||
|
|||||||
+25
-3
@@ -2,6 +2,7 @@ import { FocusModal, clx } from "@medusajs/ui"
|
|||||||
import { PropsWithChildren, useEffect, useState } from "react"
|
import { PropsWithChildren, useEffect, useState } from "react"
|
||||||
import { useNavigate } from "react-router-dom"
|
import { useNavigate } from "react-router-dom"
|
||||||
import { RouteModalForm } from "../route-modal-form"
|
import { RouteModalForm } from "../route-modal-form"
|
||||||
|
import { useRouteModal } from "../route-modal-provider"
|
||||||
import { RouteModalProvider } from "../route-modal-provider/route-provider"
|
import { RouteModalProvider } from "../route-modal-provider/route-provider"
|
||||||
import { StackedModalProvider } from "../stacked-modal-provider"
|
import { StackedModalProvider } from "../stacked-modal-provider"
|
||||||
|
|
||||||
@@ -41,16 +42,37 @@ const Root = ({ prev = "..", children }: RouteFocusModalProps) => {
|
|||||||
<FocusModal open={open} onOpenChange={handleOpenChange}>
|
<FocusModal open={open} onOpenChange={handleOpenChange}>
|
||||||
<RouteModalProvider prev={prev}>
|
<RouteModalProvider prev={prev}>
|
||||||
<StackedModalProvider onOpenChange={onStackedModalOpen}>
|
<StackedModalProvider onOpenChange={onStackedModalOpen}>
|
||||||
|
<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
|
<FocusModal.Content
|
||||||
|
onEscapeKeyDown={
|
||||||
|
shouldPreventClose
|
||||||
|
? (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
className={clx({
|
className={clx({
|
||||||
"!bg-ui-bg-disabled !inset-x-5 !inset-y-3": stackedModalOpen,
|
"!bg-ui-bg-disabled !inset-x-5 !inset-y-3": stackedModalOpen,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</FocusModal.Content>
|
</FocusModal.Content>
|
||||||
</StackedModalProvider>
|
|
||||||
</RouteModalProvider>
|
|
||||||
</FocusModal>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
@@ -2,6 +2,10 @@ import { createContext } from "react"
|
|||||||
|
|
||||||
type RouteModalProviderState = {
|
type RouteModalProviderState = {
|
||||||
handleSuccess: (path?: string) => void
|
handleSuccess: (path?: string) => void
|
||||||
|
setCloseOnEscape: (value: boolean) => void
|
||||||
|
__internal: {
|
||||||
|
closeOnEscape: boolean
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RouteModalProviderContext =
|
export const RouteModalProviderContext =
|
||||||
|
|||||||
+18
-4
@@ -1,4 +1,4 @@
|
|||||||
import { PropsWithChildren } from "react"
|
import { PropsWithChildren, useCallback, useMemo, useState } from "react"
|
||||||
import { useNavigate } from "react-router-dom"
|
import { useNavigate } from "react-router-dom"
|
||||||
import { RouteModalProviderContext } from "./route-modal-context"
|
import { RouteModalProviderContext } from "./route-modal-context"
|
||||||
|
|
||||||
@@ -12,13 +12,27 @@ export const RouteModalProvider = ({
|
|||||||
}: RouteModalProviderProps) => {
|
}: RouteModalProviderProps) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const handleSuccess = (path?: string) => {
|
const [closeOnEscape, setCloseOnEscape] = useState(true)
|
||||||
|
|
||||||
|
const handleSuccess = useCallback(
|
||||||
|
(path?: string) => {
|
||||||
const to = path || prev
|
const to = path || prev
|
||||||
navigate(to, { replace: true, state: { isSubmitSuccessful: true } })
|
navigate(to, { replace: true, state: { isSubmitSuccessful: true } })
|
||||||
}
|
},
|
||||||
|
[navigate, prev]
|
||||||
|
)
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
handleSuccess,
|
||||||
|
setCloseOnEscape,
|
||||||
|
__internal: { closeOnEscape },
|
||||||
|
}),
|
||||||
|
[handleSuccess, setCloseOnEscape, closeOnEscape]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteModalProviderContext.Provider value={{ handleSuccess }}>
|
<RouteModalProviderContext.Provider value={value}>
|
||||||
{children}
|
{children}
|
||||||
</RouteModalProviderContext.Provider>
|
</RouteModalProviderContext.Provider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { MutableRefObject, Ref, RefCallback } from "react"
|
||||||
|
|
||||||
|
// Utility function to set multiple refs
|
||||||
|
function setRef<T>(ref: Ref<T> | undefined, value: T) {
|
||||||
|
if (typeof ref === "function") {
|
||||||
|
ref(value)
|
||||||
|
} else if (ref && "current" in ref) {
|
||||||
|
;(ref as MutableRefObject<T>).current = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combining multiple refs into one
|
||||||
|
export const useCombinedRefs = <T,>(
|
||||||
|
...refs: (Ref<T> | undefined)[]
|
||||||
|
): RefCallback<T> => {
|
||||||
|
return (value: T) => {
|
||||||
|
refs.forEach((ref) => setRef(ref, value))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -210,6 +210,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dataGrid": {
|
||||||
|
"editColumns": "Edit columns",
|
||||||
|
"shortcuts": {
|
||||||
|
"label": "Shortcuts",
|
||||||
|
"commands": {
|
||||||
|
"undo": "Undo",
|
||||||
|
"redo": "Redo",
|
||||||
|
"edit": "Edit the current cell"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"date": {
|
"date": {
|
||||||
"today": "Today",
|
"today": "Today",
|
||||||
|
|||||||
+14
-6
@@ -1,12 +1,13 @@
|
|||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
|
|
||||||
import { StockLocationDTO } from "@medusajs/types"
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import { UseFormReturn } from "react-hook-form"
|
import { UseFormReturn } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { DataGridNumberCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-number-cell"
|
||||||
|
import { DataGridReadOnlyCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-readonly-cell"
|
||||||
import { DataGridRoot } from "../../../../../components/data-grid/data-grid-root"
|
import { DataGridRoot } from "../../../../../components/data-grid/data-grid-root"
|
||||||
import { createDataGridHelper } from "../../../../../components/data-grid/utils"
|
import { createDataGridHelper } from "../../../../../components/data-grid/utils"
|
||||||
import { DataGridReadOnlyCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-readonly-cell"
|
import { useRouteModal } from "../../../../../components/modals"
|
||||||
import { DataGridNumberCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-number-cell"
|
|
||||||
import { useStockLocations } from "../../../../../hooks/api/stock-locations"
|
import { useStockLocations } from "../../../../../hooks/api/stock-locations"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -15,21 +16,27 @@ type Props = {
|
|||||||
|
|
||||||
export const CreateInventoryAvailabilityForm = ({ form }: Props) => {
|
export const CreateInventoryAvailabilityForm = ({ form }: Props) => {
|
||||||
const { isPending, stock_locations = [] } = useStockLocations({ limit: 999 })
|
const { isPending, stock_locations = [] } = useStockLocations({ limit: 999 })
|
||||||
|
const { setCloseOnEscape } = useRouteModal()
|
||||||
|
|
||||||
const columns = useColumns()
|
const columns = useColumns()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex size-full flex-col divide-y overflow-hidden">
|
<div className="size-full">
|
||||||
{isPending ? (
|
{isPending ? (
|
||||||
<div>Loading...</div>
|
<div>Loading...</div>
|
||||||
) : (
|
) : (
|
||||||
<DataGridRoot columns={columns} data={stock_locations} state={form} />
|
<DataGridRoot
|
||||||
|
columns={columns}
|
||||||
|
data={stock_locations}
|
||||||
|
state={form}
|
||||||
|
onEditingChange={(editing) => setCloseOnEscape(!editing)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnHelper = createDataGridHelper<StockLocationDTO>()
|
const columnHelper = createDataGridHelper<HttpTypes.AdminStockLocation>()
|
||||||
|
|
||||||
const useColumns = () => {
|
const useColumns = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -65,6 +72,7 @@ const useColumns = () => {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
disableHiding: true,
|
disableHiding: true,
|
||||||
|
type: "number",
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
[t]
|
[t]
|
||||||
|
|||||||
+4
-7
@@ -59,9 +59,7 @@ const CreateInventoryItemSchema = zod.object({
|
|||||||
// metadata: zod.record(zod.string(), zod.unknown()).optional(),
|
// metadata: zod.record(zod.string(), zod.unknown()).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
type CreateInventoryItemFormProps = {}
|
export function CreateInventoryItemForm() {
|
||||||
|
|
||||||
export function CreateInventoryItemForm({}: CreateInventoryItemFormProps) {
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { handleSuccess } = useRouteModal()
|
const { handleSuccess } = useRouteModal()
|
||||||
const [tab, setTab] = React.useState<Tab>(Tab.DETAILS)
|
const [tab, setTab] = React.useState<Tab>(Tab.DETAILS)
|
||||||
@@ -237,12 +235,12 @@ export function CreateInventoryItemForm({}: CreateInventoryItemFormProps) {
|
|||||||
|
|
||||||
<RouteFocusModal.Body
|
<RouteFocusModal.Body
|
||||||
className={clx(
|
className={clx(
|
||||||
"flex h-full w-fit flex-col items-center divide-y overflow-hidden",
|
"flex h-full w-full flex-col items-center divide-y overflow-hidden",
|
||||||
{ "mx-auto": tab === Tab.DETAILS }
|
{ "mx-auto": tab === Tab.DETAILS }
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ProgressTabs.Content value={Tab.DETAILS} className="h-full w-full">
|
<ProgressTabs.Content value={Tab.DETAILS} className="h-full w-full">
|
||||||
<div className="container mx-auto w-[720px] px-1 py-8">
|
<div className="mx-auto w-[720px] px-1 py-8">
|
||||||
<Heading level="h2" className="mb-12 mt-8 text-2xl">
|
<Heading level="h2" className="mb-12 mt-8 text-2xl">
|
||||||
{t("inventory.create.title")}
|
{t("inventory.create.title")}
|
||||||
</Heading>
|
</Heading>
|
||||||
@@ -534,8 +532,7 @@ export function CreateInventoryItemForm({}: CreateInventoryItemFormProps) {
|
|||||||
|
|
||||||
<ProgressTabs.Content
|
<ProgressTabs.Content
|
||||||
value={Tab.AVAILABILITY}
|
value={Tab.AVAILABILITY}
|
||||||
className="h-full w-full"
|
className="size-full"
|
||||||
style={{ width: "100vw" }}
|
|
||||||
>
|
>
|
||||||
<CreateInventoryAvailabilityForm form={form} />
|
<CreateInventoryAvailabilityForm form={form} />
|
||||||
</ProgressTabs.Content>
|
</ProgressTabs.Content>
|
||||||
|
|||||||
+10
-2
@@ -2,11 +2,12 @@ import { useMemo } from "react"
|
|||||||
import { UseFormReturn } from "react-hook-form"
|
import { UseFormReturn } from "react-hook-form"
|
||||||
|
|
||||||
import { DataGridRoot } from "../../../../../components/data-grid/data-grid-root"
|
import { DataGridRoot } from "../../../../../components/data-grid/data-grid-root"
|
||||||
|
import { useRouteModal } from "../../../../../components/modals"
|
||||||
|
import { usePricePreferences } from "../../../../../hooks/api/price-preferences"
|
||||||
import { useRegions } from "../../../../../hooks/api/regions"
|
import { useRegions } from "../../../../../hooks/api/regions"
|
||||||
import { useStore } from "../../../../../hooks/api/store"
|
import { useStore } from "../../../../../hooks/api/store"
|
||||||
import { useShippingOptionPriceColumns } from "../../../common/hooks/use-shipping-option-price-columns"
|
import { useShippingOptionPriceColumns } from "../../../common/hooks/use-shipping-option-price-columns"
|
||||||
import { CreateShippingOptionSchema } from "./schema"
|
import { CreateShippingOptionSchema } from "./schema"
|
||||||
import { usePricePreferences } from "../../../../../hooks/api/price-preferences"
|
|
||||||
|
|
||||||
type PricingPricesFormProps = {
|
type PricingPricesFormProps = {
|
||||||
form: UseFormReturn<CreateShippingOptionSchema>
|
form: UseFormReturn<CreateShippingOptionSchema>
|
||||||
@@ -39,6 +40,8 @@ export const CreateShippingOptionsPricesForm = ({
|
|||||||
|
|
||||||
const { price_preferences: pricePreferences } = usePricePreferences({})
|
const { price_preferences: pricePreferences } = usePricePreferences({})
|
||||||
|
|
||||||
|
const { setCloseOnEscape } = useRouteModal()
|
||||||
|
|
||||||
const columns = useShippingOptionPriceColumns({
|
const columns = useShippingOptionPriceColumns({
|
||||||
currencies,
|
currencies,
|
||||||
regions,
|
regions,
|
||||||
@@ -62,7 +65,12 @@ export const CreateShippingOptionsPricesForm = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex size-full flex-col divide-y overflow-hidden">
|
<div className="flex size-full flex-col divide-y overflow-hidden">
|
||||||
<DataGridRoot data={data} columns={columns} state={form} />
|
<DataGridRoot
|
||||||
|
data={data}
|
||||||
|
columns={columns}
|
||||||
|
state={form}
|
||||||
|
onEditingChange={(editing) => setCloseOnEscape(!editing)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-2
@@ -12,12 +12,12 @@ import {
|
|||||||
RouteFocusModal,
|
RouteFocusModal,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../components/modals/index"
|
} from "../../../../../components/modals/index"
|
||||||
|
import { usePricePreferences } from "../../../../../hooks/api/price-preferences"
|
||||||
import { useRegions } from "../../../../../hooks/api/regions"
|
import { useRegions } from "../../../../../hooks/api/regions"
|
||||||
import { useUpdateShippingOptions } from "../../../../../hooks/api/shipping-options"
|
import { useUpdateShippingOptions } from "../../../../../hooks/api/shipping-options"
|
||||||
import { useStore } from "../../../../../hooks/api/store"
|
import { useStore } from "../../../../../hooks/api/store"
|
||||||
import { castNumber } from "../../../../../lib/cast-number"
|
import { castNumber } from "../../../../../lib/cast-number"
|
||||||
import { useShippingOptionPriceColumns } from "../../../common/hooks/use-shipping-option-price-columns"
|
import { useShippingOptionPriceColumns } from "../../../common/hooks/use-shipping-option-price-columns"
|
||||||
import { usePricePreferences } from "../../../../../hooks/api/price-preferences"
|
|
||||||
|
|
||||||
const getInitialCurrencyPrices = (
|
const getInitialCurrencyPrices = (
|
||||||
prices: HttpTypes.AdminShippingOptionPrice[]
|
prices: HttpTypes.AdminShippingOptionPrice[]
|
||||||
@@ -111,6 +111,8 @@ export function EditShippingOptionsPricingForm({
|
|||||||
|
|
||||||
const { price_preferences: pricePreferences } = usePricePreferences({})
|
const { price_preferences: pricePreferences } = usePricePreferences({})
|
||||||
|
|
||||||
|
const { setCloseOnEscape } = useRouteModal()
|
||||||
|
|
||||||
const columns = useShippingOptionPriceColumns({
|
const columns = useShippingOptionPriceColumns({
|
||||||
currencies,
|
currencies,
|
||||||
regions,
|
regions,
|
||||||
@@ -234,7 +236,12 @@ export function EditShippingOptionsPricingForm({
|
|||||||
|
|
||||||
<RouteFocusModal.Body>
|
<RouteFocusModal.Body>
|
||||||
<div className="flex size-full flex-col divide-y overflow-hidden">
|
<div className="flex size-full flex-col divide-y overflow-hidden">
|
||||||
<DataGridRoot data={data} columns={columns} state={form} />
|
<DataGridRoot
|
||||||
|
data={data}
|
||||||
|
columns={columns}
|
||||||
|
state={form}
|
||||||
|
onEditingChange={(editing) => setCloseOnEscape(!editing)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</RouteFocusModal.Body>
|
</RouteFocusModal.Body>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
+6
-5
@@ -1,13 +1,13 @@
|
|||||||
import { HttpTypes, StoreCurrencyDTO } from "@medusajs/types"
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import { ColumnDef } from "@tanstack/react-table"
|
import { ColumnDef } from "@tanstack/react-table"
|
||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
import { Thumbnail } from "../../../../components/common/thumbnail"
|
import { Thumbnail } from "../../../../components/common/thumbnail"
|
||||||
import { DataGridReadOnlyCell } from "../../../../components/data-grid/data-grid-cells/data-grid-readonly-cell"
|
import { DataGridReadOnlyCell } from "../../../../components/data-grid/data-grid-cells/data-grid-readonly-cell"
|
||||||
|
import { getPriceColumns } from "../../../../components/data-grid/data-grid-columns/price-columns"
|
||||||
import { createDataGridHelper } from "../../../../components/data-grid/utils"
|
import { createDataGridHelper } from "../../../../components/data-grid/utils"
|
||||||
import { isProductRow } from "../utils"
|
import { isProductRow } from "../utils"
|
||||||
import { getPriceColumns } from "../../../../components/data-grid/data-grid-columns/price-columns"
|
|
||||||
|
|
||||||
const columnHelper = createDataGridHelper<
|
const columnHelper = createDataGridHelper<
|
||||||
HttpTypes.AdminProduct | HttpTypes.AdminProductVariant
|
HttpTypes.AdminProduct | HttpTypes.AdminProductVariant
|
||||||
@@ -18,7 +18,7 @@ export const usePriceListGridColumns = ({
|
|||||||
regions = [],
|
regions = [],
|
||||||
pricePreferences = [],
|
pricePreferences = [],
|
||||||
}: {
|
}: {
|
||||||
currencies?: StoreCurrencyDTO[]
|
currencies?: HttpTypes.AdminStoreCurrency[]
|
||||||
regions?: HttpTypes.AdminRegion[]
|
regions?: HttpTypes.AdminRegion[]
|
||||||
pricePreferences?: HttpTypes.AdminPricePreference[]
|
pricePreferences?: HttpTypes.AdminPricePreference[]
|
||||||
}) => {
|
}) => {
|
||||||
@@ -33,7 +33,6 @@ export const usePriceListGridColumns = ({
|
|||||||
header: t("fields.title"),
|
header: t("fields.title"),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const entity = row.original
|
const entity = row.original
|
||||||
|
|
||||||
if (isProductRow(entity)) {
|
if (isProductRow(entity)) {
|
||||||
return (
|
return (
|
||||||
<DataGridReadOnlyCell>
|
<DataGridReadOnlyCell>
|
||||||
@@ -55,7 +54,9 @@ export const usePriceListGridColumns = ({
|
|||||||
},
|
},
|
||||||
disableHiding: true,
|
disableHiding: true,
|
||||||
}),
|
}),
|
||||||
...getPriceColumns({
|
...getPriceColumns<
|
||||||
|
HttpTypes.AdminProduct | HttpTypes.AdminProductVariant
|
||||||
|
>({
|
||||||
currencies: currencies.map((c) => c.currency_code),
|
currencies: currencies.map((c) => c.currency_code),
|
||||||
regions,
|
regions,
|
||||||
pricePreferences,
|
pricePreferences,
|
||||||
|
|||||||
+4
@@ -2,6 +2,7 @@ import { HttpTypes } from "@medusajs/types"
|
|||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
import { UseFormReturn, useWatch } from "react-hook-form"
|
import { UseFormReturn, useWatch } from "react-hook-form"
|
||||||
import { DataGridRoot } from "../../../../../components/data-grid/data-grid-root"
|
import { DataGridRoot } from "../../../../../components/data-grid/data-grid-root"
|
||||||
|
import { useRouteModal } from "../../../../../components/modals"
|
||||||
import { useProducts } from "../../../../../hooks/api/products"
|
import { useProducts } from "../../../../../hooks/api/products"
|
||||||
import { usePriceListGridColumns } from "../../../common/hooks/use-price-list-grid-columns"
|
import { usePriceListGridColumns } from "../../../common/hooks/use-price-list-grid-columns"
|
||||||
import { PriceListCreateProductVariantsSchema } from "../../../common/schemas"
|
import { PriceListCreateProductVariantsSchema } from "../../../common/schemas"
|
||||||
@@ -37,6 +38,8 @@ export const PriceListPricesForm = ({
|
|||||||
fields: "title,thumbnail,*variants",
|
fields: "title,thumbnail,*variants",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { setCloseOnEscape } = useRouteModal()
|
||||||
|
|
||||||
const { setValue } = form
|
const { setValue } = form
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -83,6 +86,7 @@ export const PriceListPricesForm = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
state={form}
|
state={form}
|
||||||
|
onEditingChange={(editing) => setCloseOnEscape(!editing)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
+4
@@ -3,6 +3,7 @@ import { useEffect } from "react"
|
|||||||
import { UseFormReturn, useWatch } from "react-hook-form"
|
import { UseFormReturn, useWatch } from "react-hook-form"
|
||||||
|
|
||||||
import { DataGridRoot } from "../../../../../components/data-grid/data-grid-root"
|
import { DataGridRoot } from "../../../../../components/data-grid/data-grid-root"
|
||||||
|
import { useRouteModal } from "../../../../../components/modals"
|
||||||
import { useProducts } from "../../../../../hooks/api/products"
|
import { useProducts } from "../../../../../hooks/api/products"
|
||||||
import { usePriceListGridColumns } from "../../../common/hooks/use-price-list-grid-columns"
|
import { usePriceListGridColumns } from "../../../common/hooks/use-price-list-grid-columns"
|
||||||
import { PriceListCreateProductVariantsSchema } from "../../../common/schemas"
|
import { PriceListCreateProductVariantsSchema } from "../../../common/schemas"
|
||||||
@@ -40,6 +41,8 @@ export const PriceListPricesAddPricesForm = ({
|
|||||||
|
|
||||||
const { setValue } = form
|
const { setValue } = form
|
||||||
|
|
||||||
|
const { setCloseOnEscape } = useRouteModal()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoading && products) {
|
if (!isLoading && products) {
|
||||||
products.forEach((product) => {
|
products.forEach((product) => {
|
||||||
@@ -84,6 +87,7 @@ export const PriceListPricesAddPricesForm = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
state={form}
|
state={form}
|
||||||
|
onEditingChange={(editing) => setCloseOnEscape(!editing)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
+8
-16
@@ -40,7 +40,7 @@ export const PriceListPricesEditForm = ({
|
|||||||
pricePreferences,
|
pricePreferences,
|
||||||
}: PriceListPricesEditFormProps) => {
|
}: PriceListPricesEditFormProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { handleSuccess } = useRouteModal()
|
const { handleSuccess, setCloseOnEscape } = useRouteModal()
|
||||||
|
|
||||||
const initialValue = useRef(initRecord(priceList, products))
|
const initialValue = useRef(initRecord(priceList, products))
|
||||||
|
|
||||||
@@ -101,6 +101,7 @@ export const PriceListPricesEditForm = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
state={form}
|
state={form}
|
||||||
|
onEditingChange={(editing) => setCloseOnEscape(!editing)}
|
||||||
/>
|
/>
|
||||||
</RouteFocusModal.Body>
|
</RouteFocusModal.Body>
|
||||||
<RouteFocusModal.Footer>
|
<RouteFocusModal.Footer>
|
||||||
@@ -184,13 +185,10 @@ function convertToPriceArray(
|
|||||||
) {
|
) {
|
||||||
const prices: PriceObject[] = []
|
const prices: PriceObject[] = []
|
||||||
|
|
||||||
const regionCurrencyMap = regions.reduce(
|
const regionCurrencyMap = regions.reduce((map, region) => {
|
||||||
(map, region) => {
|
|
||||||
map[region.id] = region.currency_code
|
map[region.id] = region.currency_code
|
||||||
return map
|
return map
|
||||||
},
|
}, {} as Record<string, string>)
|
||||||
{} as Record<string, string>
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const [_productId, product] of Object.entries(data || {})) {
|
for (const [_productId, product] of Object.entries(data || {})) {
|
||||||
const { variants } = product || {}
|
const { variants } = product || {}
|
||||||
@@ -242,21 +240,15 @@ function comparePrices(initialPrices: PriceObject[], newPrices: PriceObject[]) {
|
|||||||
const pricesToCreate: HttpTypes.AdminCreatePriceListPrice[] = []
|
const pricesToCreate: HttpTypes.AdminCreatePriceListPrice[] = []
|
||||||
const pricesToDelete: string[] = []
|
const pricesToDelete: string[] = []
|
||||||
|
|
||||||
const initialPriceMap = initialPrices.reduce(
|
const initialPriceMap = initialPrices.reduce((map, price) => {
|
||||||
(map, price) => {
|
|
||||||
map[createMapKey(price)] = price
|
map[createMapKey(price)] = price
|
||||||
return map
|
return map
|
||||||
},
|
}, {} as Record<string, (typeof initialPrices)[0]>)
|
||||||
{} as Record<string, (typeof initialPrices)[0]>
|
|
||||||
)
|
|
||||||
|
|
||||||
const newPriceMap = newPrices.reduce(
|
const newPriceMap = newPrices.reduce((map, price) => {
|
||||||
(map, price) => {
|
|
||||||
map[createMapKey(price)] = price
|
map[createMapKey(price)] = price
|
||||||
return map
|
return map
|
||||||
},
|
}, {} as Record<string, (typeof newPrices)[0]>)
|
||||||
{} as Record<string, (typeof newPrices)[0]>
|
|
||||||
)
|
|
||||||
|
|
||||||
const keys = new Set([
|
const keys = new Set([
|
||||||
...Object.keys(initialPriceMap),
|
...Object.keys(initialPriceMap),
|
||||||
|
|||||||
+6
@@ -41,6 +41,12 @@ export const PriceListPricesEdit = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteFocusModal>
|
<RouteFocusModal>
|
||||||
|
<RouteFocusModal.Title asChild>
|
||||||
|
<span className="sr-only">Edit Prices for {price_list?.title}</span>
|
||||||
|
</RouteFocusModal.Title>
|
||||||
|
<RouteFocusModal.Description className="sr-only">
|
||||||
|
Update prices for products in the price list
|
||||||
|
</RouteFocusModal.Description>
|
||||||
{ready && (
|
{ready && (
|
||||||
<PriceListPricesEditForm
|
<PriceListPricesEditForm
|
||||||
priceList={price_list}
|
priceList={price_list}
|
||||||
|
|||||||
@@ -1,39 +1,31 @@
|
|||||||
import { CurrencyDTO, HttpTypes } from "@medusajs/types"
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
|
|
||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
import { UseFormReturn, useWatch } from "react-hook-form"
|
import { UseFormReturn, useWatch } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { getPriceColumns } from "../../../components/data-grid/data-grid-columns/price-columns"
|
||||||
|
import { DataGridRoot } from "../../../components/data-grid/data-grid-root/data-grid-root"
|
||||||
|
import { createDataGridHelper } from "../../../components/data-grid/utils.ts"
|
||||||
import { ReadonlyCell } from "../../../components/grid/grid-cells/common/readonly-cell"
|
import { ReadonlyCell } from "../../../components/grid/grid-cells/common/readonly-cell"
|
||||||
import { useCurrencies } from "../../../hooks/api/currencies"
|
import { useRouteModal } from "../../../components/modals/index.ts"
|
||||||
import { useStore } from "../../../hooks/api/store"
|
import { usePricePreferences } from "../../../hooks/api/price-preferences"
|
||||||
import { ProductCreateSchema } from "../product-create/constants"
|
|
||||||
import { useRegions } from "../../../hooks/api/regions.tsx"
|
import { useRegions } from "../../../hooks/api/regions.tsx"
|
||||||
import { usePricePreferences } from "../../../hooks/api/price-preferences.tsx"
|
import { useStore } from "../../../hooks/api/store"
|
||||||
import { getPriceColumns } from "../../../components/data-grid/data-grid-columns/price-columns.tsx"
|
import { ProductCreateSchemaType } from "../product-create/types.ts"
|
||||||
import { DataGridRoot } from "../../../components/data-grid/data-grid-root/data-grid-root.tsx"
|
|
||||||
|
|
||||||
type VariantPricingFormProps = {
|
type VariantPricingFormProps = {
|
||||||
form: UseFormReturn<ProductCreateSchema>
|
form: UseFormReturn<ProductCreateSchemaType>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VariantPricingForm = ({ form }: VariantPricingFormProps) => {
|
export const VariantPricingForm = ({ form }: VariantPricingFormProps) => {
|
||||||
const { store, isLoading: isStoreLoading } = useStore()
|
const { store } = useStore()
|
||||||
const { currencies, isLoading: isCurrenciesLoading } = useCurrencies(
|
|
||||||
{
|
|
||||||
code: store?.supported_currencies?.map((c) => c.currency_code),
|
|
||||||
limit: store?.supported_currencies?.length,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!store,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const { regions } = useRegions({ limit: 9999 })
|
const { regions } = useRegions({ limit: 9999 })
|
||||||
|
const { setCloseOnEscape } = useRouteModal()
|
||||||
|
|
||||||
const { price_preferences: pricePreferences } = usePricePreferences({})
|
const { price_preferences: pricePreferences } = usePricePreferences({})
|
||||||
|
|
||||||
const columns = useVariantPriceGridColumns({
|
const columns = useVariantPriceGridColumns({
|
||||||
currencies,
|
currencies: store?.supported_currencies,
|
||||||
regions,
|
regions,
|
||||||
pricePreferences,
|
pricePreferences,
|
||||||
})
|
})
|
||||||
@@ -44,33 +36,31 @@ export const VariantPricingForm = ({ form }: VariantPricingFormProps) => {
|
|||||||
}) as any
|
}) as any
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex size-full flex-col divide-y overflow-hidden">
|
|
||||||
<DataGridRoot
|
<DataGridRoot
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={variants}
|
data={variants}
|
||||||
isLoading={isStoreLoading || isCurrenciesLoading}
|
|
||||||
state={form}
|
state={form}
|
||||||
|
onEditingChange={(editing) => setCloseOnEscape(!editing)}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<HttpTypes.AdminProductVariant>()
|
const columnHelper = createDataGridHelper<HttpTypes.AdminProductVariant>()
|
||||||
|
|
||||||
export const useVariantPriceGridColumns = ({
|
const useVariantPriceGridColumns = ({
|
||||||
currencies = [],
|
currencies = [],
|
||||||
regions = [],
|
regions = [],
|
||||||
pricePreferences = [],
|
pricePreferences = [],
|
||||||
}: {
|
}: {
|
||||||
currencies?: CurrencyDTO[]
|
currencies?: HttpTypes.AdminStore["supported_currencies"]
|
||||||
regions?: HttpTypes.AdminRegion[]
|
regions?: HttpTypes.AdminRegion[]
|
||||||
pricePreferences?: HttpTypes.AdminPricePreference[]
|
pricePreferences?: HttpTypes.AdminPricePreference[]
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const colDefs: ColumnDef<HttpTypes.AdminProductVariant>[] = useMemo(() => {
|
return useMemo(() => {
|
||||||
return [
|
return [
|
||||||
columnHelper.display({
|
columnHelper.column({
|
||||||
id: t("fields.title"),
|
id: t("fields.title"),
|
||||||
header: t("fields.title"),
|
header: t("fields.title"),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
@@ -78,16 +68,15 @@ export const useVariantPriceGridColumns = ({
|
|||||||
return (
|
return (
|
||||||
<ReadonlyCell>
|
<ReadonlyCell>
|
||||||
<div className="flex h-full w-full items-center gap-x-2 overflow-hidden">
|
<div className="flex h-full w-full items-center gap-x-2 overflow-hidden">
|
||||||
<span title={entity.title} className="truncate">
|
<span className="truncate">{entity.title}</span>
|
||||||
{entity.title}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</ReadonlyCell>
|
</ReadonlyCell>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
disableHiding: true,
|
||||||
}),
|
}),
|
||||||
...getPriceColumns({
|
...getPriceColumns<HttpTypes.AdminProductVariant>({
|
||||||
currencies: currencies.map((c) => c.code),
|
currencies: currencies.map((c) => c.currency_code),
|
||||||
regions,
|
regions,
|
||||||
pricePreferences,
|
pricePreferences,
|
||||||
getFieldName: (context, value) => {
|
getFieldName: (context, value) => {
|
||||||
@@ -100,6 +89,4 @@ export const useVariantPriceGridColumns = ({
|
|||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
}, [t, currencies, regions, pricePreferences])
|
}, [t, currencies, regions, pricePreferences])
|
||||||
|
|
||||||
return colDefs
|
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-32
@@ -73,8 +73,8 @@ export const ProductCreateForm = ({
|
|||||||
return regions.reduce((acc, reg) => {
|
return regions.reduce((acc, reg) => {
|
||||||
acc[reg.id] = reg.currency_code
|
acc[reg.id] = reg.currency_code
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {} as Record<string, string>)
|
||||||
}, regions)
|
}, [regions])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: Important to revisit this - use variants watch so high in the tree can cause needless rerenders of the entire page
|
* TODO: Important to revisit this - use variants watch so high in the tree can cause needless rerenders of the entire page
|
||||||
@@ -212,10 +212,6 @@ export const ProductCreateForm = ({
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
onNext(tab)
|
onNext(tab)
|
||||||
} else {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
handleSubmit(e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -236,24 +232,26 @@ export const ProductCreateForm = ({
|
|||||||
className="flex h-full flex-col overflow-hidden"
|
className="flex h-full flex-col overflow-hidden"
|
||||||
>
|
>
|
||||||
<RouteFocusModal.Header>
|
<RouteFocusModal.Header>
|
||||||
<div className="flex w-full items-center justify-between gap-x-4">
|
<div className="-my-2 w-full border-l">
|
||||||
<div className="-my-2 w-fit border-l">
|
<ProgressTabs.List className="justify-start-start flex w-full items-center">
|
||||||
<ProgressTabs.List className="grid w-full grid-cols-4">
|
|
||||||
<ProgressTabs.Trigger
|
<ProgressTabs.Trigger
|
||||||
status={tabState[Tab.DETAILS]}
|
status={tabState[Tab.DETAILS]}
|
||||||
value={Tab.DETAILS}
|
value={Tab.DETAILS}
|
||||||
|
className="max-w-[200px] truncate"
|
||||||
>
|
>
|
||||||
{t("products.create.tabs.details")}
|
{t("products.create.tabs.details")}
|
||||||
</ProgressTabs.Trigger>
|
</ProgressTabs.Trigger>
|
||||||
<ProgressTabs.Trigger
|
<ProgressTabs.Trigger
|
||||||
status={tabState[Tab.ORGANIZE]}
|
status={tabState[Tab.ORGANIZE]}
|
||||||
value={Tab.ORGANIZE}
|
value={Tab.ORGANIZE}
|
||||||
|
className="max-w-[200px] truncate"
|
||||||
>
|
>
|
||||||
{t("products.create.tabs.organize")}
|
{t("products.create.tabs.organize")}
|
||||||
</ProgressTabs.Trigger>
|
</ProgressTabs.Trigger>
|
||||||
<ProgressTabs.Trigger
|
<ProgressTabs.Trigger
|
||||||
status={tabState[Tab.VARIANTS]}
|
status={tabState[Tab.VARIANTS]}
|
||||||
value={Tab.VARIANTS}
|
value={Tab.VARIANTS}
|
||||||
|
className="max-w-[200px] truncate"
|
||||||
>
|
>
|
||||||
{t("products.create.tabs.variants")}
|
{t("products.create.tabs.variants")}
|
||||||
</ProgressTabs.Trigger>
|
</ProgressTabs.Trigger>
|
||||||
@@ -261,35 +259,13 @@ export const ProductCreateForm = ({
|
|||||||
<ProgressTabs.Trigger
|
<ProgressTabs.Trigger
|
||||||
status={tabState[Tab.INVENTORY]}
|
status={tabState[Tab.INVENTORY]}
|
||||||
value={Tab.INVENTORY}
|
value={Tab.INVENTORY}
|
||||||
|
className="max-w-[200px] truncate"
|
||||||
>
|
>
|
||||||
{t("products.create.tabs.inventory")}
|
{t("products.create.tabs.inventory")}
|
||||||
</ProgressTabs.Trigger>
|
</ProgressTabs.Trigger>
|
||||||
)}
|
)}
|
||||||
</ProgressTabs.List>
|
</ProgressTabs.List>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-end gap-x-2">
|
|
||||||
<RouteFocusModal.Close asChild>
|
|
||||||
<Button variant="secondary" size="small">
|
|
||||||
{t("actions.cancel")}
|
|
||||||
</Button>
|
|
||||||
</RouteFocusModal.Close>
|
|
||||||
<Button
|
|
||||||
data-name={SAVE_DRAFT_BUTTON}
|
|
||||||
size="small"
|
|
||||||
type="submit"
|
|
||||||
isLoading={isPending}
|
|
||||||
className="whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{t("actions.saveAsDraft")}
|
|
||||||
</Button>
|
|
||||||
<PrimaryButton
|
|
||||||
tab={tab}
|
|
||||||
next={onNext}
|
|
||||||
isLoading={isPending}
|
|
||||||
showInventoryTab={showInventoryTab}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</RouteFocusModal.Header>
|
</RouteFocusModal.Header>
|
||||||
<RouteFocusModal.Body className="size-full overflow-hidden">
|
<RouteFocusModal.Body className="size-full overflow-hidden">
|
||||||
<ProgressTabs.Content
|
<ProgressTabs.Content
|
||||||
@@ -320,6 +296,30 @@ export const ProductCreateForm = ({
|
|||||||
)}
|
)}
|
||||||
</RouteFocusModal.Body>
|
</RouteFocusModal.Body>
|
||||||
</ProgressTabs>
|
</ProgressTabs>
|
||||||
|
<RouteFocusModal.Footer>
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
<RouteFocusModal.Close asChild>
|
||||||
|
<Button variant="secondary" size="small">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteFocusModal.Close>
|
||||||
|
<Button
|
||||||
|
data-name={SAVE_DRAFT_BUTTON}
|
||||||
|
size="small"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isPending}
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{t("actions.saveAsDraft")}
|
||||||
|
</Button>
|
||||||
|
<PrimaryButton
|
||||||
|
tab={tab}
|
||||||
|
next={onNext}
|
||||||
|
isLoading={isPending}
|
||||||
|
showInventoryTab={showInventoryTab}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</RouteFocusModal.Footer>
|
||||||
</form>
|
</form>
|
||||||
</RouteFocusModal.Form>
|
</RouteFocusModal.Form>
|
||||||
</RouteFocusModal>
|
</RouteFocusModal>
|
||||||
|
|||||||
+17
-9
@@ -2,16 +2,17 @@ import { HttpTypes } from "@medusajs/types"
|
|||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
import { UseFormReturn, useWatch } from "react-hook-form"
|
import { UseFormReturn, useWatch } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { ProductCreateSchemaType } from "../../types"
|
import { DataGridBooleanCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-boolean-cell"
|
||||||
import { useStore } from "../../../../../hooks/api/store"
|
|
||||||
import { DataGridRoot } from "../../../../../components/data-grid/data-grid-root"
|
|
||||||
import { DataGridReadOnlyCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-readonly-cell"
|
import { DataGridReadOnlyCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-readonly-cell"
|
||||||
import { DataGridTextCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-text-cell"
|
import { DataGridTextCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-text-cell"
|
||||||
import { createDataGridHelper } from "../../../../../components/data-grid/utils"
|
|
||||||
import { DataGridBooleanCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-boolean-cell"
|
|
||||||
import { useRegions } from "../../../../../hooks/api/regions"
|
|
||||||
import { usePricePreferences } from "../../../../../hooks/api/price-preferences"
|
|
||||||
import { getPriceColumns } from "../../../../../components/data-grid/data-grid-columns/price-columns"
|
import { getPriceColumns } from "../../../../../components/data-grid/data-grid-columns/price-columns"
|
||||||
|
import { DataGridRoot } from "../../../../../components/data-grid/data-grid-root"
|
||||||
|
import { createDataGridHelper } from "../../../../../components/data-grid/utils"
|
||||||
|
import { useRouteModal } from "../../../../../components/modals"
|
||||||
|
import { usePricePreferences } from "../../../../../hooks/api/price-preferences"
|
||||||
|
import { useRegions } from "../../../../../hooks/api/regions"
|
||||||
|
import { useStore } from "../../../../../hooks/api/store"
|
||||||
|
import { ProductCreateSchemaType } from "../../types"
|
||||||
|
|
||||||
type ProductCreateVariantsFormProps = {
|
type ProductCreateVariantsFormProps = {
|
||||||
form: UseFormReturn<ProductCreateSchemaType>
|
form: UseFormReturn<ProductCreateSchemaType>
|
||||||
@@ -26,6 +27,8 @@ export const ProductCreateVariantsForm = ({
|
|||||||
|
|
||||||
const { price_preferences: pricePreferences } = usePricePreferences({})
|
const { price_preferences: pricePreferences } = usePricePreferences({})
|
||||||
|
|
||||||
|
const { setCloseOnEscape } = useRouteModal()
|
||||||
|
|
||||||
const currencyCodes = useMemo(
|
const currencyCodes = useMemo(
|
||||||
() => store?.supported_currencies?.map((c) => c.currency_code) || [],
|
() => store?.supported_currencies?.map((c) => c.currency_code) || [],
|
||||||
[store]
|
[store]
|
||||||
@@ -67,7 +70,12 @@ export const ProductCreateVariantsForm = ({
|
|||||||
{isPending && !store ? (
|
{isPending && !store ? (
|
||||||
<div>Loading...</div>
|
<div>Loading...</div>
|
||||||
) : (
|
) : (
|
||||||
<DataGridRoot columns={columns} data={variantData} state={form} />
|
<DataGridRoot
|
||||||
|
columns={columns}
|
||||||
|
data={variantData}
|
||||||
|
state={form}
|
||||||
|
onEditingChange={(editing) => setCloseOnEscape(!editing)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -180,7 +188,7 @@ const useColumns = ({
|
|||||||
type: "boolean",
|
type: "boolean",
|
||||||
}),
|
}),
|
||||||
|
|
||||||
...getPriceColumns({
|
...getPriceColumns<HttpTypes.AdminProductVariant>({
|
||||||
currencies,
|
currencies,
|
||||||
regions,
|
regions,
|
||||||
pricePreferences,
|
pricePreferences,
|
||||||
|
|||||||
@@ -72,8 +72,7 @@ export const PricingEdit = ({
|
|||||||
|
|
||||||
const { mutateAsync, isPending } = useUpdateProductVariantsBatch(product.id)
|
const { mutateAsync, isPending } = useUpdateProductVariantsBatch(product.id)
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(
|
const handleSubmit = form.handleSubmit(async (values) => {
|
||||||
async (values) => {
|
|
||||||
const reqData = values.variants.map((variant, ind) => ({
|
const reqData = values.variants.map((variant, ind) => ({
|
||||||
id: variants[ind].id,
|
id: variants[ind].id,
|
||||||
prices: Object.entries(variant.prices || {}).map(
|
prices: Object.entries(variant.prices || {}).map(
|
||||||
@@ -122,18 +121,17 @@ export const PricingEdit = ({
|
|||||||
handleSuccess("..")
|
handleSuccess("..")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
})
|
||||||
(err) => {
|
|
||||||
console.log(err)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteFocusModal.Form form={form}>
|
<RouteFocusModal.Form form={form}>
|
||||||
<form onSubmit={handleSubmit} className="flex h-full flex-col">
|
<form onSubmit={handleSubmit} className="flex size-full flex-col">
|
||||||
<RouteFocusModal.Header>
|
<RouteFocusModal.Header />
|
||||||
|
<RouteFocusModal.Body className="flex flex-col overflow-hidden">
|
||||||
|
<VariantPricingForm form={form as any} />
|
||||||
|
</RouteFocusModal.Body>
|
||||||
|
<RouteFocusModal.Footer>
|
||||||
<div className="flex w-full items-center justify-end gap-x-2">
|
<div className="flex w-full items-center justify-end gap-x-2">
|
||||||
<div className="flex items-center gap-x-4">
|
|
||||||
<RouteFocusModal.Close asChild>
|
<RouteFocusModal.Close asChild>
|
||||||
<Button variant="secondary" size="small">
|
<Button variant="secondary" size="small">
|
||||||
{t("actions.cancel")}
|
{t("actions.cancel")}
|
||||||
@@ -148,11 +146,7 @@ export const PricingEdit = ({
|
|||||||
{t("actions.save")}
|
{t("actions.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</RouteFocusModal.Footer>
|
||||||
</RouteFocusModal.Header>
|
|
||||||
<RouteFocusModal.Body>
|
|
||||||
<VariantPricingForm form={form as any} />
|
|
||||||
</RouteFocusModal.Body>
|
|
||||||
</form>
|
</form>
|
||||||
</RouteFocusModal.Form>
|
</RouteFocusModal.Form>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4483,7 +4483,7 @@ __metadata:
|
|||||||
"@radix-ui/react-hover-card": 1.1.1
|
"@radix-ui/react-hover-card": 1.1.1
|
||||||
"@tanstack/react-query": ^5.28.14
|
"@tanstack/react-query": ^5.28.14
|
||||||
"@tanstack/react-table": 8.10.7
|
"@tanstack/react-table": 8.10.7
|
||||||
"@tanstack/react-virtual": ^3.0.4
|
"@tanstack/react-virtual": ^3.8.3
|
||||||
"@types/node": ^20.11.15
|
"@types/node": ^20.11.15
|
||||||
"@types/react": ^18.2.79
|
"@types/react": ^18.2.79
|
||||||
"@types/react-dom": ^18.2.25
|
"@types/react-dom": ^18.2.25
|
||||||
@@ -4492,6 +4492,7 @@ __metadata:
|
|||||||
autoprefixer: ^10.4.17
|
autoprefixer: ^10.4.17
|
||||||
cmdk: ^0.2.0
|
cmdk: ^0.2.0
|
||||||
date-fns: ^3.6.0
|
date-fns: ^3.6.0
|
||||||
|
focus-trap-react: ^10.2.3
|
||||||
framer-motion: ^11.0.3
|
framer-motion: ^11.0.3
|
||||||
i18next: 23.7.11
|
i18next: 23.7.11
|
||||||
i18next-browser-languagedetector: 7.2.0
|
i18next-browser-languagedetector: 7.2.0
|
||||||
@@ -11498,15 +11499,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@tanstack/react-virtual@npm:^3.0.4":
|
"@tanstack/react-virtual@npm:^3.8.3":
|
||||||
version: 3.5.0
|
version: 3.8.3
|
||||||
resolution: "@tanstack/react-virtual@npm:3.5.0"
|
resolution: "@tanstack/react-virtual@npm:3.8.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@tanstack/virtual-core": 3.5.0
|
"@tanstack/virtual-core": 3.8.3
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
checksum: 0e0389dcae02cb3a6dc31644be6fedc9c635f66b747136149b065351ef23995e131b01c85348875a8c8c7bade28a2bb8bbc9c7b66a995051d6be08aec60d07c4
|
checksum: 36a80c4fd1516d29a36ce4f97e0b1d05c0c715e779d12b70b9c9b7ebb8296e928fd5ca1d2a336467fa00d07721bb679845cdaa6f28de08b47402534d8bfed0d9
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -11517,10 +11518,10 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@tanstack/virtual-core@npm:3.5.0":
|
"@tanstack/virtual-core@npm:3.8.3":
|
||||||
version: 3.5.0
|
version: 3.8.3
|
||||||
resolution: "@tanstack/virtual-core@npm:3.5.0"
|
resolution: "@tanstack/virtual-core@npm:3.8.3"
|
||||||
checksum: 21a12049df81ce282054e8406cb74dd21d867aac077f31568ffdb08acd9aa171ae0666ceb43654895e73907acf5db02c7d651a66960da8acceeb2e034885e605
|
checksum: 3509a578e7c8ced00eef5574ca16aae466a4d066204d3c558f027d13656b2950364fdb99c07a834557933b3b81d531e029f952766c36016e386c24e8be978af5
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -18392,6 +18393,29 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"focus-trap-react@npm:^10.2.3":
|
||||||
|
version: 10.2.3
|
||||||
|
resolution: "focus-trap-react@npm:10.2.3"
|
||||||
|
dependencies:
|
||||||
|
focus-trap: ^7.5.4
|
||||||
|
tabbable: ^6.2.0
|
||||||
|
peerDependencies:
|
||||||
|
prop-types: ^15.8.1
|
||||||
|
react: ">=16.3.0"
|
||||||
|
react-dom: ">=16.3.0"
|
||||||
|
checksum: 18527771cacc1083ecdf472276a4c46581a6289dc98b6caf1c31641091732933cbffdec78e4c86c2c1568ac4f82a83c367d6a4d7e0e84dc96b239d2d4c12c071
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"focus-trap@npm:^7.5.4":
|
||||||
|
version: 7.5.4
|
||||||
|
resolution: "focus-trap@npm:7.5.4"
|
||||||
|
dependencies:
|
||||||
|
tabbable: ^6.2.0
|
||||||
|
checksum: c09e12b957862b2608977ff90de782645f99c3555cc5d93977240c179befa8723b9b1183e93890b4ad9d364d52a1af36416e63a728522ecce656a447d9ddd945
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"follow-redirects@npm:^1.14.0, follow-redirects@npm:^1.14.4, follow-redirects@npm:^1.15.6":
|
"follow-redirects@npm:^1.14.0, follow-redirects@npm:^1.14.4, follow-redirects@npm:^1.15.6":
|
||||||
version: 1.15.6
|
version: 1.15.6
|
||||||
resolution: "follow-redirects@npm:1.15.6"
|
resolution: "follow-redirects@npm:1.15.6"
|
||||||
@@ -28970,6 +28994,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"tabbable@npm:^6.2.0":
|
||||||
|
version: 6.2.0
|
||||||
|
resolution: "tabbable@npm:6.2.0"
|
||||||
|
checksum: ced8b38f05f2de62cd46836d77c2646c42b8c9713f5bd265daf0e78ff5ac73d3ba48a7ca45f348bafeef29b23da7187c72250742d37627883ef89cbd7fa76898
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"table@npm:^6.0.9":
|
"table@npm:^6.0.9":
|
||||||
version: 6.8.2
|
version: 6.8.2
|
||||||
resolution: "table@npm:6.8.2"
|
resolution: "table@npm:6.8.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user