feat(dashboard): DataGrid improvements [4/4] (#8798)
**What** - Changes cell state strategy from tracking from lazy to eager. This has required some changes to the API of the DataGrid component, and createDataGridColumnHelper function. - Displays error messages in both affected cells and their rows. The row indicator also provides an option to quickly jump to an error. - Allows the user to hide all rows and columns that don't have errors, to help quickly get an overview of the errors in a large grid. - The first column of a DataGrid is now pinned, making it easier for a user to tell which entity they are editing. - Fixes and improvements to column visibility menu. - Adds a shortcuts modal that explains the different available keyboard commands. - Updates `@tanstack/react-table` to the latest version. Resolves CC-269
This commit is contained in:
1 parent
d8fdf4d0b2
commit
b8572165cb
68 files changed
+4116
-2381
No files matched your search
@@ -31,13 +31,14 @@
|
|||||||
"@ariakit/react": "^0.4.1",
|
"@ariakit/react": "^0.4.1",
|
||||||
"@dnd-kit/core": "^6.1.0",
|
"@dnd-kit/core": "^6.1.0",
|
||||||
"@dnd-kit/sortable": "^8.0.0",
|
"@dnd-kit/sortable": "^8.0.0",
|
||||||
|
"@hookform/error-message": "^2.0.1",
|
||||||
"@hookform/resolvers": "3.4.2",
|
"@hookform/resolvers": "3.4.2",
|
||||||
"@medusajs/icons": "1.2.1",
|
"@medusajs/icons": "1.2.1",
|
||||||
"@medusajs/js-sdk": "0.0.1",
|
"@medusajs/js-sdk": "0.0.1",
|
||||||
"@medusajs/ui": "3.0.0",
|
"@medusajs/ui": "3.0.0",
|
||||||
"@radix-ui/react-collapsible": "1.1.0",
|
"@radix-ui/react-collapsible": "1.1.0",
|
||||||
"@tanstack/react-query": "^5.28.14",
|
"@tanstack/react-query": "^5.28.14",
|
||||||
"@tanstack/react-table": "8.10.7",
|
"@tanstack/react-table": "8.20.5",
|
||||||
"@tanstack/react-virtual": "^3.8.3",
|
"@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",
|
||||||
@@ -55,6 +56,7 @@
|
|||||||
"react-country-flag": "^3.1.0",
|
"react-country-flag": "^3.1.0",
|
||||||
"react-currency-input-field": "^3.6.11",
|
"react-currency-input-field": "^3.6.11",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-helmet-async": "^2.0.5",
|
||||||
"react-hook-form": "7.49.1",
|
"react-hook-form": "7.49.1",
|
||||||
"react-i18next": "13.5.0",
|
"react-i18next": "13.5.0",
|
||||||
"react-jwt": "^1.2.0",
|
"react-jwt": "^1.2.0",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Toaster, TooltipProvider } from "@medusajs/ui"
|
import { Toaster, TooltipProvider } from "@medusajs/ui"
|
||||||
import { QueryClientProvider } from "@tanstack/react-query"
|
import { QueryClientProvider } from "@tanstack/react-query"
|
||||||
|
import { HelmetProvider } from "react-helmet-async"
|
||||||
|
|
||||||
import { I18n } from "./components/utilities/i18n"
|
import { I18n } from "./components/utilities/i18n"
|
||||||
import { queryClient } from "./lib/query-client"
|
import { queryClient } from "./lib/query-client"
|
||||||
@@ -11,17 +12,19 @@ import "./index.css"
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<HelmetProvider>
|
||||||
<ThemeProvider>
|
<QueryClientProvider client={queryClient}>
|
||||||
<I18n />
|
<ThemeProvider>
|
||||||
<TooltipProvider>
|
<I18n />
|
||||||
<I18nProvider>
|
<TooltipProvider>
|
||||||
<RouterProvider />
|
<I18nProvider>
|
||||||
</I18nProvider>
|
<RouterProvider />
|
||||||
</TooltipProvider>
|
</I18nProvider>
|
||||||
<Toaster />
|
</TooltipProvider>
|
||||||
</ThemeProvider>
|
<Toaster />
|
||||||
</QueryClientProvider>
|
</ThemeProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
|
</HelmetProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-6
@@ -1,20 +1,19 @@
|
|||||||
import { Checkbox } from "@medusajs/ui"
|
import { Checkbox } from "@medusajs/ui"
|
||||||
import { Controller, ControllerRenderProps } from "react-hook-form"
|
import { Controller, ControllerRenderProps } from "react-hook-form"
|
||||||
|
|
||||||
import { useCombinedRefs } from "../../../hooks/use-combined-refs"
|
import { useCombinedRefs } from "../../../hooks/use-combined-refs"
|
||||||
import { useDataGridCell } from "../hooks"
|
import { useDataGridCell, useDataGridCellError } from "../hooks"
|
||||||
import { DataGridCellProps, InputProps } 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>({
|
||||||
field,
|
|
||||||
context,
|
context,
|
||||||
disabled,
|
disabled,
|
||||||
}: DataGridCellProps<TData, TValue> & { disabled?: boolean }) => {
|
}: DataGridCellProps<TData, TValue> & { disabled?: boolean }) => {
|
||||||
const { control, renderProps } = useDataGridCell({
|
const { field, control, renderProps } = useDataGridCell({
|
||||||
field,
|
|
||||||
context,
|
context,
|
||||||
type: "boolean",
|
|
||||||
})
|
})
|
||||||
|
const errorProps = useDataGridCellError({ context })
|
||||||
|
|
||||||
const { container, input } = renderProps
|
const { container, input } = renderProps
|
||||||
|
|
||||||
@@ -24,7 +23,7 @@ export const DataGridBooleanCell = <TData, TValue = any>({
|
|||||||
name={field}
|
name={field}
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<DataGridCellContainer {...container}>
|
<DataGridCellContainer {...container} {...errorProps}>
|
||||||
<Inner field={field} inputProps={input} disabled={disabled} />
|
<Inner field={field} inputProps={input} disabled={disabled} />
|
||||||
</DataGridCellContainer>
|
</DataGridCellContainer>
|
||||||
)
|
)
|
||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
import { ErrorMessage } from "@hookform/error-message"
|
||||||
|
import { ExclamationCircle } from "@medusajs/icons"
|
||||||
|
import { Tooltip, clx } from "@medusajs/ui"
|
||||||
|
import { PropsWithChildren } from "react"
|
||||||
|
import { get } from "react-hook-form"
|
||||||
|
|
||||||
|
import { DataGridCellContainerProps, DataGridErrorRenderProps } from "../types"
|
||||||
|
import { DataGridRowErrorIndicator } from "./data-grid-row-error-indicator"
|
||||||
|
|
||||||
|
export const DataGridCellContainer = ({
|
||||||
|
isAnchor,
|
||||||
|
isSelected,
|
||||||
|
isDragSelected,
|
||||||
|
field,
|
||||||
|
showOverlay,
|
||||||
|
placeholder,
|
||||||
|
innerProps,
|
||||||
|
overlayProps,
|
||||||
|
children,
|
||||||
|
errors,
|
||||||
|
rowErrors,
|
||||||
|
}: DataGridCellContainerProps & DataGridErrorRenderProps<any>) => {
|
||||||
|
const error = get(errors, field)
|
||||||
|
const hasError = !!error
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clx(
|
||||||
|
"bg-ui-bg-base group/cell relative flex size-full items-center gap-x-2 px-4 py-2.5 outline-none",
|
||||||
|
{
|
||||||
|
"bg-ui-tag-red-bg text-ui-tag-red-text":
|
||||||
|
hasError && !isAnchor && !isSelected && !isDragSelected,
|
||||||
|
"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={-1}
|
||||||
|
{...innerProps}
|
||||||
|
>
|
||||||
|
<ErrorMessage
|
||||||
|
name={field}
|
||||||
|
errors={errors}
|
||||||
|
render={({ message }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Tooltip content={message} delayDuration={0}>
|
||||||
|
<ExclamationCircle className="text-ui-tag-red-icon z-[3]" />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="relative z-[1] flex size-full items-center justify-center">
|
||||||
|
<RenderChildren isAnchor={isAnchor} placeholder={placeholder}>
|
||||||
|
{children}
|
||||||
|
</RenderChildren>
|
||||||
|
</div>
|
||||||
|
<DataGridRowErrorIndicator rowErrors={rowErrors} />
|
||||||
|
{showOverlay && (
|
||||||
|
<div
|
||||||
|
{...overlayProps}
|
||||||
|
data-cell-overlay="true"
|
||||||
|
className="absolute inset-0 z-[2] size-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const RenderChildren = ({
|
||||||
|
isAnchor,
|
||||||
|
placeholder,
|
||||||
|
children,
|
||||||
|
}: PropsWithChildren<
|
||||||
|
Pick<DataGridCellContainerProps, "isAnchor" | "placeholder">
|
||||||
|
>) => {
|
||||||
|
if (!isAnchor && placeholder) {
|
||||||
|
return placeholder
|
||||||
|
}
|
||||||
|
|
||||||
|
return children
|
||||||
|
}
|
||||||
File renamed without changes.
+6
-8
@@ -7,7 +7,7 @@ import { Controller, ControllerRenderProps } from "react-hook-form"
|
|||||||
import { useCallback, useEffect, useState } from "react"
|
import { useCallback, useEffect, useState } from "react"
|
||||||
import { useCombinedRefs } from "../../../hooks/use-combined-refs"
|
import { useCombinedRefs } from "../../../hooks/use-combined-refs"
|
||||||
import { CurrencyInfo, currencies } from "../../../lib/data/currencies"
|
import { CurrencyInfo, currencies } from "../../../lib/data/currencies"
|
||||||
import { useDataGridCell } from "../hooks"
|
import { useDataGridCell, useDataGridCellError } from "../hooks"
|
||||||
import { DataGridCellProps, InputProps } from "../types"
|
import { DataGridCellProps, InputProps } from "../types"
|
||||||
import { DataGridCellContainer } from "./data-grid-cell-container"
|
import { DataGridCellContainer } from "./data-grid-cell-container"
|
||||||
|
|
||||||
@@ -17,15 +17,13 @@ interface DataGridCurrencyCellProps<TData, TValue = any>
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const DataGridCurrencyCell = <TData, TValue = any>({
|
export const DataGridCurrencyCell = <TData, TValue = any>({
|
||||||
field,
|
|
||||||
context,
|
context,
|
||||||
code,
|
code,
|
||||||
}: DataGridCurrencyCellProps<TData, TValue>) => {
|
}: DataGridCurrencyCellProps<TData, TValue>) => {
|
||||||
const { control, renderProps } = useDataGridCell({
|
const { field, control, renderProps } = useDataGridCell({
|
||||||
field,
|
|
||||||
context,
|
context,
|
||||||
type: "number",
|
|
||||||
})
|
})
|
||||||
|
const errorProps = useDataGridCellError({ context })
|
||||||
|
|
||||||
const { container, input } = renderProps
|
const { container, input } = renderProps
|
||||||
|
|
||||||
@@ -37,7 +35,7 @@ export const DataGridCurrencyCell = <TData, TValue = any>({
|
|||||||
name={field}
|
name={field}
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<DataGridCellContainer {...container}>
|
<DataGridCellContainer {...container} {...errorProps}>
|
||||||
<Inner field={field} inputProps={input} currencyInfo={currency} />
|
<Inner field={field} inputProps={input} currencyInfo={currency} />
|
||||||
</DataGridCellContainer>
|
</DataGridCellContainer>
|
||||||
)
|
)
|
||||||
@@ -112,7 +110,7 @@ const Inner = ({
|
|||||||
return (
|
return (
|
||||||
<div className="relative flex size-full items-center">
|
<div className="relative flex size-full items-center">
|
||||||
<span
|
<span
|
||||||
className="txt-compact-small text-ui-fg-muted pointer-events-none absolute left-4 w-fit min-w-4"
|
className="txt-compact-small text-ui-fg-muted pointer-events-none absolute left-0 w-fit min-w-4"
|
||||||
aria-hidden
|
aria-hidden
|
||||||
>
|
>
|
||||||
{currencyInfo.symbol_native}
|
{currencyInfo.symbol_native}
|
||||||
@@ -121,7 +119,7 @@ const Inner = ({
|
|||||||
{...rest}
|
{...rest}
|
||||||
{...attributes}
|
{...attributes}
|
||||||
ref={combinedRed}
|
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"
|
className="txt-compact-small w-full flex-1 cursor-default appearance-none bg-transparent pl-8 text-right outline-none"
|
||||||
value={localValue || undefined}
|
value={localValue || undefined}
|
||||||
onValueChange={handleValueChange}
|
onValueChange={handleValueChange}
|
||||||
formatValueOnBlur
|
formatValueOnBlur
|
||||||
+231
@@ -0,0 +1,231 @@
|
|||||||
|
import { XMark } from "@medusajs/icons"
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
clx,
|
||||||
|
Heading,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Kbd,
|
||||||
|
Text,
|
||||||
|
} from "@medusajs/ui"
|
||||||
|
import * as Dialog from "@radix-ui/react-dialog"
|
||||||
|
import { useMemo, useState } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
const useDataGridShortcuts = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const shortcuts = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
label: t("dataGrid.shortcuts.commands.undo"),
|
||||||
|
keys: {
|
||||||
|
Mac: ["⌘", "Z"],
|
||||||
|
Windows: ["Ctrl", "Z"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("dataGrid.shortcuts.commands.redo"),
|
||||||
|
keys: {
|
||||||
|
Mac: ["⇧", "⌘", "Z"],
|
||||||
|
Windows: ["Shift", "Ctrl", "Z"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("dataGrid.shortcuts.commands.copy"),
|
||||||
|
keys: {
|
||||||
|
Mac: ["⌘", "C"],
|
||||||
|
Windows: ["Ctrl", "C"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("dataGrid.shortcuts.commands.paste"),
|
||||||
|
keys: {
|
||||||
|
Mac: ["⌘", "V"],
|
||||||
|
Windows: ["Ctrl", "V"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("dataGrid.shortcuts.commands.edit"),
|
||||||
|
keys: {
|
||||||
|
Mac: ["↵"],
|
||||||
|
Windows: ["Enter"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("dataGrid.shortcuts.commands.delete"),
|
||||||
|
keys: {
|
||||||
|
Mac: ["⌫"],
|
||||||
|
Windows: ["Backspace"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("dataGrid.shortcuts.commands.clear"),
|
||||||
|
keys: {
|
||||||
|
Mac: ["Space"],
|
||||||
|
Windows: ["Space"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("dataGrid.shortcuts.commands.moveUp"),
|
||||||
|
keys: {
|
||||||
|
Mac: ["↑"],
|
||||||
|
Windows: ["↑"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("dataGrid.shortcuts.commands.moveDown"),
|
||||||
|
keys: {
|
||||||
|
Mac: ["↓"],
|
||||||
|
Windows: ["↓"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("dataGrid.shortcuts.commands.moveLeft"),
|
||||||
|
keys: {
|
||||||
|
Mac: ["←"],
|
||||||
|
Windows: ["←"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("dataGrid.shortcuts.commands.moveRight"),
|
||||||
|
keys: {
|
||||||
|
Mac: ["→"],
|
||||||
|
Windows: ["→"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("dataGrid.shortcuts.commands.moveTop"),
|
||||||
|
keys: {
|
||||||
|
Mac: ["⌘", "↑"],
|
||||||
|
Windows: ["Ctrl", "↑"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("dataGrid.shortcuts.commands.moveBottom"),
|
||||||
|
keys: {
|
||||||
|
Mac: ["⌘", "↓"],
|
||||||
|
Windows: ["Ctrl", "↓"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("dataGrid.shortcuts.commands.selectDown"),
|
||||||
|
keys: {
|
||||||
|
Mac: ["⇧", "↓"],
|
||||||
|
Windows: ["Shift", "↓"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("dataGrid.shortcuts.commands.selectUp"),
|
||||||
|
keys: {
|
||||||
|
Mac: ["⇧", "↑"],
|
||||||
|
Windows: ["Shift", "↑"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("dataGrid.shortcuts.commands.selectColumnDown"),
|
||||||
|
keys: {
|
||||||
|
Mac: ["⇧", "⌘", "↓"],
|
||||||
|
Windows: ["Shift", "Ctrl", "↓"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("dataGrid.shortcuts.commands.selectColumnUp"),
|
||||||
|
keys: {
|
||||||
|
Mac: ["⇧", "⌘", "↑"],
|
||||||
|
Windows: ["Shift", "Ctrl", "↑"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[t]
|
||||||
|
)
|
||||||
|
|
||||||
|
return shortcuts
|
||||||
|
}
|
||||||
|
|
||||||
|
type DataGridKeyboardShortcutModalProps = {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DataGridKeyboardShortcutModal = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: DataGridKeyboardShortcutModalProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [searchValue, onSearchValueChange] = useState("")
|
||||||
|
const shortcuts = useDataGridShortcuts()
|
||||||
|
|
||||||
|
const searchResults = useMemo(() => {
|
||||||
|
return shortcuts.filter((shortcut) =>
|
||||||
|
shortcut.label.toLowerCase().includes(searchValue.toLowerCase())
|
||||||
|
)
|
||||||
|
}, [searchValue, shortcuts])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.Root open={open} onOpenChange={onOpenChange}>
|
||||||
|
<Dialog.Trigger asChild>
|
||||||
|
<Button size="small" variant="secondary">
|
||||||
|
{t("dataGrid.shortcuts.label")}
|
||||||
|
</Button>
|
||||||
|
</Dialog.Trigger>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay
|
||||||
|
className={clx(
|
||||||
|
"bg-ui-bg-overlay fixed inset-0",
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Dialog.Content className="bg-ui-bg-subtle shadow-elevation-modal fixed left-[50%] top-[50%] flex h-full max-h-[612px] w-full max-w-[560px] translate-x-[-50%] translate-y-[-50%] flex-col divide-y overflow-hidden rounded-lg outline-none">
|
||||||
|
<div className="flex flex-col gap-y-3 px-6 py-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Dialog.Title asChild>
|
||||||
|
<Heading>{t("app.menus.user.shortcuts")}</Heading>
|
||||||
|
</Dialog.Title>
|
||||||
|
<Dialog.Description className="sr-only"></Dialog.Description>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<Kbd>esc</Kbd>
|
||||||
|
<Dialog.Close asChild>
|
||||||
|
<IconButton variant="transparent" size="small">
|
||||||
|
<XMark />
|
||||||
|
</IconButton>
|
||||||
|
</Dialog.Close>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
type="search"
|
||||||
|
value={searchValue}
|
||||||
|
autoFocus
|
||||||
|
onChange={(e) => onSearchValueChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col divide-y overflow-y-auto">
|
||||||
|
{searchResults.map((shortcut, index) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="text-ui-fg-subtle flex items-center justify-between px-6 py-3"
|
||||||
|
>
|
||||||
|
<Text size="small">{shortcut.label}</Text>
|
||||||
|
<div className="flex items-center gap-x-1">
|
||||||
|
{shortcut.keys.Mac?.map((key, index) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-x-1" key={index}>
|
||||||
|
<Kbd>{key}</Kbd>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
+5
-7
@@ -2,12 +2,11 @@ import { clx } from "@medusajs/ui"
|
|||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { Controller, ControllerRenderProps } from "react-hook-form"
|
import { Controller, ControllerRenderProps } from "react-hook-form"
|
||||||
import { useCombinedRefs } from "../../../hooks/use-combined-refs"
|
import { useCombinedRefs } from "../../../hooks/use-combined-refs"
|
||||||
import { useDataGridCell } from "../hooks"
|
import { useDataGridCell, useDataGridCellError } from "../hooks"
|
||||||
import { DataGridCellProps, InputProps } 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>({
|
||||||
field,
|
|
||||||
context,
|
context,
|
||||||
...rest
|
...rest
|
||||||
}: DataGridCellProps<TData, TValue> & {
|
}: DataGridCellProps<TData, TValue> & {
|
||||||
@@ -15,11 +14,10 @@ export const DataGridNumberCell = <TData, TValue = any>({
|
|||||||
max?: number
|
max?: number
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
}) => {
|
}) => {
|
||||||
const { control, renderProps } = useDataGridCell({
|
const { field, control, renderProps } = useDataGridCell({
|
||||||
field,
|
|
||||||
context,
|
context,
|
||||||
type: "number",
|
|
||||||
})
|
})
|
||||||
|
const errorProps = useDataGridCellError({ context })
|
||||||
|
|
||||||
const { container, input } = renderProps
|
const { container, input } = renderProps
|
||||||
|
|
||||||
@@ -29,7 +27,7 @@ export const DataGridNumberCell = <TData, TValue = any>({
|
|||||||
name={field}
|
name={field}
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<DataGridCellContainer {...container}>
|
<DataGridCellContainer {...container} {...errorProps}>
|
||||||
<Inner field={field} inputProps={input} {...rest} />
|
<Inner field={field} inputProps={input} {...rest} />
|
||||||
</DataGridCellContainer>
|
</DataGridCellContainer>
|
||||||
)
|
)
|
||||||
@@ -83,7 +81,7 @@ const Inner = ({
|
|||||||
type="number"
|
type="number"
|
||||||
inputMode="decimal"
|
inputMode="decimal"
|
||||||
className={clx(
|
className={clx(
|
||||||
"txt-compact-small size-full bg-transparent px-4 py-2.5 outline-none",
|
"txt-compact-small size-full bg-transparent outline-none",
|
||||||
"placeholder:text-ui-fg-muted"
|
"placeholder:text-ui-fg-muted"
|
||||||
)}
|
)}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
import { PropsWithChildren } from "react"
|
||||||
|
|
||||||
|
import { useDataGridCellError } from "../hooks"
|
||||||
|
import { DataGridCellProps } from "../types"
|
||||||
|
import { DataGridRowErrorIndicator } from "./data-grid-row-error-indicator"
|
||||||
|
|
||||||
|
type DataGridReadonlyCellProps<TData, TValue = any> = DataGridCellProps<
|
||||||
|
TData,
|
||||||
|
TValue
|
||||||
|
> &
|
||||||
|
PropsWithChildren
|
||||||
|
|
||||||
|
export const DataGridReadonlyCell = <TData, TValue = any>({
|
||||||
|
context,
|
||||||
|
children,
|
||||||
|
}: DataGridReadonlyCellProps<TData, TValue>) => {
|
||||||
|
const { rowErrors } = useDataGridCellError({ context })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-ui-bg-subtle txt-compact-small text-ui-fg-subtle flex size-full cursor-not-allowed items-center justify-between overflow-hidden px-4 py-2.5 outline-none">
|
||||||
|
<span className="truncate">{children}</span>
|
||||||
|
<DataGridRowErrorIndicator rowErrors={rowErrors} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,955 @@
|
|||||||
|
import {
|
||||||
|
Adjustments,
|
||||||
|
AdjustmentsDone,
|
||||||
|
ExclamationCircle,
|
||||||
|
} from "@medusajs/icons"
|
||||||
|
import { Button, DropdownMenu, clx } from "@medusajs/ui"
|
||||||
|
import {
|
||||||
|
Cell,
|
||||||
|
CellContext,
|
||||||
|
Column,
|
||||||
|
ColumnDef,
|
||||||
|
Row,
|
||||||
|
VisibilityState,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table"
|
||||||
|
import { VirtualItem, useVirtualizer } from "@tanstack/react-virtual"
|
||||||
|
import FocusTrap from "focus-trap-react"
|
||||||
|
import {
|
||||||
|
CSSProperties,
|
||||||
|
MouseEvent,
|
||||||
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react"
|
||||||
|
import { FieldValues, UseFormReturn } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { useCommandHistory } from "../../../hooks/use-command-history"
|
||||||
|
import { ConditionalTooltip } from "../../common/conditional-tooltip"
|
||||||
|
import { DataGridContext } from "../context"
|
||||||
|
import {
|
||||||
|
useDataGridCellHandlers,
|
||||||
|
useDataGridCellMetadata,
|
||||||
|
useDataGridCellSnapshot,
|
||||||
|
useDataGridClipboardEvents,
|
||||||
|
useDataGridColumnVisibility,
|
||||||
|
useDataGridErrorHighlighting,
|
||||||
|
useDataGridFormHandlers,
|
||||||
|
useDataGridKeydownEvent,
|
||||||
|
useDataGridMouseUpEvent,
|
||||||
|
useDataGridNavigation,
|
||||||
|
useDataGridQueryTool,
|
||||||
|
} from "../hooks"
|
||||||
|
import { DataGridMatrix } from "../models"
|
||||||
|
import { DataGridCoordinates, GridColumnOption } from "../types"
|
||||||
|
import { generateCellId, isCellMatch } from "../utils"
|
||||||
|
import { DataGridKeyboardShortcutModal } from "./data-grid-keyboard-shortcut-modal"
|
||||||
|
|
||||||
|
export interface DataGridRootProps<
|
||||||
|
TData,
|
||||||
|
TFieldValues extends FieldValues = FieldValues
|
||||||
|
> {
|
||||||
|
data?: TData[]
|
||||||
|
columns: ColumnDef<TData>[]
|
||||||
|
state: UseFormReturn<TFieldValues>
|
||||||
|
getSubRows?: (row: TData) => TData[] | undefined
|
||||||
|
onEditingChange?: (isEditing: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ROW_HEIGHT = 40
|
||||||
|
|
||||||
|
const getCommonPinningStyles = <TData,>(
|
||||||
|
column: Column<TData>
|
||||||
|
): CSSProperties => {
|
||||||
|
const isPinned = column.getIsPinned()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Since our border colors are semi-transparent, we need to set a custom border color
|
||||||
|
* that looks the same as the actual border color, but has 100% opacity.
|
||||||
|
*
|
||||||
|
* We do this by checking if the current theme is dark mode, and then setting the border color
|
||||||
|
* to the corresponding color.
|
||||||
|
*/
|
||||||
|
const isDarkMode = document.documentElement.classList.contains("dark")
|
||||||
|
const BORDER_COLOR = isDarkMode ? "rgb(50,50,53)" : "rgb(228,228,231)"
|
||||||
|
|
||||||
|
return {
|
||||||
|
position: isPinned ? "sticky" : "relative",
|
||||||
|
width: column.getSize(),
|
||||||
|
zIndex: isPinned ? 1 : 0,
|
||||||
|
borderBottom: isPinned ? `1px solid ${BORDER_COLOR}` : undefined,
|
||||||
|
borderRight: isPinned ? `1px solid ${BORDER_COLOR}` : undefined,
|
||||||
|
left: isPinned === "left" ? `${column.getStart("left")}px` : undefined,
|
||||||
|
right: isPinned === "right" ? `${column.getAfter("right")}px` : undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO:
|
||||||
|
* - [Minor] Add shortcuts overview modal.
|
||||||
|
* - [Minor] Extend the commands to also support modifying the anchor and rangeEnd, to restore the previous focus after undo/redo.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const DataGridRoot = <
|
||||||
|
TData,
|
||||||
|
TFieldValues extends FieldValues = FieldValues
|
||||||
|
>({
|
||||||
|
data = [],
|
||||||
|
columns,
|
||||||
|
state,
|
||||||
|
getSubRows,
|
||||||
|
onEditingChange,
|
||||||
|
}: DataGridRootProps<TData, TFieldValues>) => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const { redo, undo, execute } = useCommandHistory()
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
control,
|
||||||
|
getValues,
|
||||||
|
setValue,
|
||||||
|
formState: { errors },
|
||||||
|
} = state
|
||||||
|
|
||||||
|
const [trapActive, setTrapActive] = useState(false)
|
||||||
|
|
||||||
|
const [anchor, setAnchor] = useState<DataGridCoordinates | null>(null)
|
||||||
|
const [rangeEnd, setRangeEnd] = useState<DataGridCoordinates | null>(null)
|
||||||
|
const [dragEnd, setDragEnd] = useState<DataGridCoordinates | null>(null)
|
||||||
|
|
||||||
|
const [isSelecting, setIsSelecting] = useState(false)
|
||||||
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
|
|
||||||
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
|
|
||||||
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
|
||||||
|
const [rowVisibility, setRowVisibility] = useState<VisibilityState>({})
|
||||||
|
|
||||||
|
const grid = useReactTable({
|
||||||
|
data: data,
|
||||||
|
columns,
|
||||||
|
initialState: {
|
||||||
|
columnPinning: {
|
||||||
|
left: [columns[0].id!],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
columnVisibility,
|
||||||
|
},
|
||||||
|
onColumnVisibilityChange: setColumnVisibility,
|
||||||
|
getSubRows,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
defaultColumn: {
|
||||||
|
size: 200,
|
||||||
|
maxSize: 400,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { flatRows } = grid.getRowModel()
|
||||||
|
const flatColumns = grid.getAllFlatColumns()
|
||||||
|
|
||||||
|
const visibleRows = useMemo(
|
||||||
|
() => flatRows.filter((_, index) => rowVisibility?.[index] !== false),
|
||||||
|
[flatRows, rowVisibility]
|
||||||
|
)
|
||||||
|
const visibleColumns = grid.getVisibleLeafColumns()
|
||||||
|
|
||||||
|
const rowVirtualizer = useVirtualizer({
|
||||||
|
count: visibleRows.length,
|
||||||
|
estimateSize: () => ROW_HEIGHT,
|
||||||
|
getScrollElement: () => containerRef.current,
|
||||||
|
overscan: 5,
|
||||||
|
rangeExtractor: (range) => {
|
||||||
|
const toRender = new Set(
|
||||||
|
Array.from(
|
||||||
|
{ length: range.endIndex - range.startIndex + 1 },
|
||||||
|
(_, i) => range.startIndex + i
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (anchor && visibleRows[anchor.row]) {
|
||||||
|
toRender.add(anchor.row)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rangeEnd && visibleRows[rangeEnd.row]) {
|
||||||
|
toRender.add(rangeEnd.row)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(toRender).sort((a, b) => a - b)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const virtualRows = rowVirtualizer.getVirtualItems()
|
||||||
|
|
||||||
|
const columnVirtualizer = useVirtualizer({
|
||||||
|
count: visibleColumns.length,
|
||||||
|
estimateSize: (index) => visibleColumns[index].getSize(),
|
||||||
|
getScrollElement: () => containerRef.current,
|
||||||
|
horizontal: true,
|
||||||
|
overscan: 3,
|
||||||
|
rangeExtractor: (range) => {
|
||||||
|
const startIndex = range.startIndex
|
||||||
|
const endIndex = range.endIndex
|
||||||
|
|
||||||
|
const toRender = new Set(
|
||||||
|
Array.from(
|
||||||
|
{ length: endIndex - startIndex + 1 },
|
||||||
|
(_, i) => startIndex + i
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (anchor && visibleColumns[anchor.col]) {
|
||||||
|
toRender.add(anchor.col)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rangeEnd && visibleColumns[rangeEnd.col]) {
|
||||||
|
toRender.add(rangeEnd.col)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The first column is pinned, so we always render it
|
||||||
|
toRender.add(0)
|
||||||
|
|
||||||
|
return Array.from(toRender).sort((a, b) => a - b)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const virtualColumns = columnVirtualizer.getVirtualItems()
|
||||||
|
|
||||||
|
let virtualPaddingLeft: number | undefined
|
||||||
|
let virtualPaddingRight: number | undefined
|
||||||
|
|
||||||
|
if (columnVirtualizer && virtualColumns?.length) {
|
||||||
|
virtualPaddingLeft = virtualColumns[0]?.start ?? 0
|
||||||
|
virtualPaddingRight =
|
||||||
|
columnVirtualizer.getTotalSize() -
|
||||||
|
(virtualColumns[virtualColumns.length - 1]?.end ?? 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const matrix = useMemo(
|
||||||
|
() => new DataGridMatrix<TData, TFieldValues>(flatRows, columns),
|
||||||
|
[flatRows, columns]
|
||||||
|
)
|
||||||
|
const queryTool = useDataGridQueryTool(containerRef)
|
||||||
|
|
||||||
|
const setSingleRange = useCallback(
|
||||||
|
(coordinates: DataGridCoordinates | null) => {
|
||||||
|
setAnchor(coordinates)
|
||||||
|
setRangeEnd(coordinates)
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const { errorCount, isHighlighted, toggleErrorHighlighting } =
|
||||||
|
useDataGridErrorHighlighting(matrix, grid, errors)
|
||||||
|
|
||||||
|
const handleToggleErrorHighlighting = useCallback(() => {
|
||||||
|
toggleErrorHighlighting(
|
||||||
|
rowVisibility,
|
||||||
|
columnVisibility,
|
||||||
|
setRowVisibility,
|
||||||
|
setColumnVisibility
|
||||||
|
)
|
||||||
|
}, [toggleErrorHighlighting, rowVisibility, columnVisibility])
|
||||||
|
|
||||||
|
const {
|
||||||
|
columnOptions,
|
||||||
|
handleToggleColumn,
|
||||||
|
handleResetColumns,
|
||||||
|
isDisabled: isColumsDisabled,
|
||||||
|
} = useDataGridColumnVisibility(grid, matrix)
|
||||||
|
|
||||||
|
const handleToggleColumnVisibility = useCallback(
|
||||||
|
(index: number) => {
|
||||||
|
return handleToggleColumn(index)
|
||||||
|
},
|
||||||
|
[handleToggleColumn]
|
||||||
|
)
|
||||||
|
|
||||||
|
const { navigateToField, scrollToCoordinates } = useDataGridNavigation<
|
||||||
|
TData,
|
||||||
|
TFieldValues
|
||||||
|
>({
|
||||||
|
matrix,
|
||||||
|
queryTool,
|
||||||
|
anchor,
|
||||||
|
columnVirtualizer,
|
||||||
|
rowVirtualizer,
|
||||||
|
flatColumns,
|
||||||
|
setColumnVisibility,
|
||||||
|
setSingleRange,
|
||||||
|
visibleColumns,
|
||||||
|
visibleRows,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { createSnapshot, restoreSnapshot } = useDataGridCellSnapshot<
|
||||||
|
TData,
|
||||||
|
TFieldValues
|
||||||
|
>({
|
||||||
|
matrix,
|
||||||
|
form: state,
|
||||||
|
})
|
||||||
|
|
||||||
|
const onEditingChangeHandler = useCallback(
|
||||||
|
(value: boolean) => {
|
||||||
|
if (onEditingChange) {
|
||||||
|
onEditingChange(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
createSnapshot(anchor)
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsEditing(value)
|
||||||
|
},
|
||||||
|
[anchor, createSnapshot, onEditingChange]
|
||||||
|
)
|
||||||
|
|
||||||
|
const { getSelectionValues, setSelectionValues } = useDataGridFormHandlers<
|
||||||
|
TData,
|
||||||
|
TFieldValues
|
||||||
|
>({
|
||||||
|
matrix,
|
||||||
|
form: state,
|
||||||
|
anchor,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { handleKeyDownEvent } = useDataGridKeydownEvent<TData, TFieldValues>({
|
||||||
|
matrix,
|
||||||
|
queryTool,
|
||||||
|
anchor,
|
||||||
|
rangeEnd,
|
||||||
|
isEditing,
|
||||||
|
setRangeEnd,
|
||||||
|
getSelectionValues,
|
||||||
|
getValues,
|
||||||
|
setSelectionValues,
|
||||||
|
onEditingChangeHandler,
|
||||||
|
restoreSnapshot,
|
||||||
|
setSingleRange,
|
||||||
|
scrollToCoordinates,
|
||||||
|
execute,
|
||||||
|
undo,
|
||||||
|
redo,
|
||||||
|
setValue,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { handleMouseUpEvent } = useDataGridMouseUpEvent<TData, TFieldValues>({
|
||||||
|
matrix,
|
||||||
|
anchor,
|
||||||
|
dragEnd,
|
||||||
|
setDragEnd,
|
||||||
|
isDragging,
|
||||||
|
setIsDragging,
|
||||||
|
setRangeEnd,
|
||||||
|
setIsSelecting,
|
||||||
|
getSelectionValues,
|
||||||
|
setSelectionValues,
|
||||||
|
execute,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { handleCopyEvent, handlePasteEvent } = useDataGridClipboardEvents<
|
||||||
|
TData,
|
||||||
|
TFieldValues
|
||||||
|
>({
|
||||||
|
matrix,
|
||||||
|
isEditing,
|
||||||
|
anchor,
|
||||||
|
rangeEnd,
|
||||||
|
getSelectionValues,
|
||||||
|
setSelectionValues,
|
||||||
|
execute,
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
getWrapperFocusHandler,
|
||||||
|
getInputChangeHandler,
|
||||||
|
getOverlayMouseDownHandler,
|
||||||
|
getWrapperMouseOverHandler,
|
||||||
|
getIsCellDragSelected,
|
||||||
|
getIsCellSelected,
|
||||||
|
onDragToFillStart,
|
||||||
|
} = useDataGridCellHandlers<TData, TFieldValues>({
|
||||||
|
matrix,
|
||||||
|
anchor,
|
||||||
|
rangeEnd,
|
||||||
|
setRangeEnd,
|
||||||
|
isDragging,
|
||||||
|
setIsDragging,
|
||||||
|
isSelecting,
|
||||||
|
setIsSelecting,
|
||||||
|
setSingleRange,
|
||||||
|
dragEnd,
|
||||||
|
setDragEnd,
|
||||||
|
setValue,
|
||||||
|
execute,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { getCellErrorMetadata, getCellMetadata } = useDataGridCellMetadata<
|
||||||
|
TData,
|
||||||
|
TFieldValues
|
||||||
|
>({
|
||||||
|
matrix,
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Effects */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register all handlers for the grid.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
const container = containerRef.current
|
||||||
|
|
||||||
|
if (
|
||||||
|
!container ||
|
||||||
|
!container.contains(document.activeElement) ||
|
||||||
|
!trapActive
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
container.addEventListener("keydown", handleKeyDownEvent)
|
||||||
|
container.addEventListener("mouseup", handleMouseUpEvent)
|
||||||
|
|
||||||
|
// Copy and paste event listeners need to be added to the window
|
||||||
|
window.addEventListener("copy", handleCopyEvent)
|
||||||
|
window.addEventListener("paste", handlePasteEvent)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
container.removeEventListener("keydown", handleKeyDownEvent)
|
||||||
|
container.removeEventListener("mouseup", handleMouseUpEvent)
|
||||||
|
|
||||||
|
window.removeEventListener("copy", handleCopyEvent)
|
||||||
|
window.removeEventListener("paste", handlePasteEvent)
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
trapActive,
|
||||||
|
handleKeyDownEvent,
|
||||||
|
handleMouseUpEvent,
|
||||||
|
handleCopyEvent,
|
||||||
|
handlePasteEvent,
|
||||||
|
])
|
||||||
|
|
||||||
|
const [isHeaderInteractionActive, setIsHeaderInteractionActive] =
|
||||||
|
useState(false)
|
||||||
|
|
||||||
|
const handleHeaderInteractionChange = useCallback((isActive: boolean) => {
|
||||||
|
setIsHeaderInteractionActive(isActive)
|
||||||
|
setTrapActive(!isActive)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto corrective effect for ensuring we always
|
||||||
|
* have a range end.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (!anchor) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rangeEnd) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setRangeEnd(anchor)
|
||||||
|
}, [anchor, rangeEnd])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure that we set a anchor on first render.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (!anchor && matrix) {
|
||||||
|
const coords = matrix.getFirstNavigableCell()
|
||||||
|
|
||||||
|
if (coords) {
|
||||||
|
setSingleRange(coords)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [anchor, matrix, setSingleRange])
|
||||||
|
|
||||||
|
const values = useMemo(
|
||||||
|
() => ({
|
||||||
|
anchor,
|
||||||
|
control,
|
||||||
|
trapActive,
|
||||||
|
errors,
|
||||||
|
setIsSelecting,
|
||||||
|
setIsEditing: onEditingChangeHandler,
|
||||||
|
setSingleRange,
|
||||||
|
setRangeEnd,
|
||||||
|
getWrapperFocusHandler,
|
||||||
|
getInputChangeHandler,
|
||||||
|
getOverlayMouseDownHandler,
|
||||||
|
getWrapperMouseOverHandler,
|
||||||
|
register,
|
||||||
|
getIsCellSelected,
|
||||||
|
getIsCellDragSelected,
|
||||||
|
getCellMetadata,
|
||||||
|
getCellErrorMetadata,
|
||||||
|
navigateToField,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
anchor,
|
||||||
|
control,
|
||||||
|
trapActive,
|
||||||
|
errors,
|
||||||
|
setIsSelecting,
|
||||||
|
onEditingChangeHandler,
|
||||||
|
setSingleRange,
|
||||||
|
setRangeEnd,
|
||||||
|
getWrapperFocusHandler,
|
||||||
|
getInputChangeHandler,
|
||||||
|
getOverlayMouseDownHandler,
|
||||||
|
getWrapperMouseOverHandler,
|
||||||
|
register,
|
||||||
|
getIsCellSelected,
|
||||||
|
getIsCellDragSelected,
|
||||||
|
getCellMetadata,
|
||||||
|
getCellErrorMetadata,
|
||||||
|
navigateToField,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataGridContext.Provider value={values}>
|
||||||
|
<div className="bg-ui-bg-subtle flex size-full flex-col">
|
||||||
|
<DataGridHeader
|
||||||
|
columnOptions={columnOptions}
|
||||||
|
isDisabled={isColumsDisabled}
|
||||||
|
onToggleColumn={handleToggleColumnVisibility}
|
||||||
|
errorCount={errorCount}
|
||||||
|
onToggleErrorHighlighting={handleToggleErrorHighlighting}
|
||||||
|
onResetColumns={handleResetColumns}
|
||||||
|
isHighlighted={isHighlighted}
|
||||||
|
onHeaderInteractionChange={handleHeaderInteractionChange}
|
||||||
|
/>
|
||||||
|
<FocusTrap
|
||||||
|
active={trapActive && !isHeaderInteractionActive}
|
||||||
|
focusTrapOptions={{
|
||||||
|
initialFocus: () => {
|
||||||
|
if (!anchor) {
|
||||||
|
const coords = matrix.getFirstNavigableCell()
|
||||||
|
|
||||||
|
if (!coords) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = generateCellId(coords)
|
||||||
|
|
||||||
|
return containerRef.current?.querySelector(
|
||||||
|
`[data-container-id="${id}"]`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = generateCellId(anchor)
|
||||||
|
|
||||||
|
const anchorContainer = containerRef.current?.querySelector(
|
||||||
|
`[data-container-id="${id}`
|
||||||
|
) as HTMLElement | null
|
||||||
|
|
||||||
|
return anchorContainer ?? undefined
|
||||||
|
},
|
||||||
|
onActivate: () => setTrapActive(true),
|
||||||
|
onDeactivate: () => setTrapActive(false),
|
||||||
|
fallbackFocus: () => {
|
||||||
|
if (!anchor) {
|
||||||
|
const coords = matrix.getFirstNavigableCell()
|
||||||
|
|
||||||
|
if (!coords) {
|
||||||
|
return containerRef.current!
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = generateCellId(coords)
|
||||||
|
|
||||||
|
const firstCell = containerRef.current?.querySelector(
|
||||||
|
`[data-container-id="${id}"]`
|
||||||
|
) as HTMLElement | null
|
||||||
|
|
||||||
|
if (firstCell) {
|
||||||
|
return firstCell
|
||||||
|
}
|
||||||
|
|
||||||
|
return containerRef.current!
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = generateCellId(anchor)
|
||||||
|
|
||||||
|
const anchorContainer = containerRef.current?.querySelector(
|
||||||
|
`[data-container-id="${id}`
|
||||||
|
) as HTMLElement | null
|
||||||
|
|
||||||
|
if (anchorContainer) {
|
||||||
|
return anchorContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
return containerRef.current!
|
||||||
|
},
|
||||||
|
allowOutsideClick: true,
|
||||||
|
escapeDeactivates: false,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="size-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
tabIndex={-1}
|
||||||
|
onFocus={() => !trapActive && setTrapActive(true)}
|
||||||
|
className="relative h-full select-none overflow-auto outline-none"
|
||||||
|
>
|
||||||
|
<div role="grid" className="text-ui-fg-subtle grid">
|
||||||
|
<div
|
||||||
|
role="rowgroup"
|
||||||
|
className="txt-compact-small-plus bg-ui-bg-subtle sticky top-0 z-[1] grid"
|
||||||
|
>
|
||||||
|
{grid.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<div
|
||||||
|
role="row"
|
||||||
|
key={headerGroup.id}
|
||||||
|
className="flex h-10 w-full"
|
||||||
|
>
|
||||||
|
{virtualPaddingLeft ? (
|
||||||
|
<div
|
||||||
|
role="presentation"
|
||||||
|
style={{ display: "flex", width: virtualPaddingLeft }}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{virtualColumns.reduce((acc, vc, index, array) => {
|
||||||
|
const header = headerGroup.headers[vc.index]
|
||||||
|
const previousVC = array[index - 1]
|
||||||
|
|
||||||
|
if (previousVC && vc.index !== previousVC.index + 1) {
|
||||||
|
// If there's a gap between the current and previous virtual columns
|
||||||
|
acc.push(
|
||||||
|
<div
|
||||||
|
key={`padding-${previousVC.index}-${vc.index}`}
|
||||||
|
role="presentation"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
width: `${vc.start - previousVC.end}px`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
acc.push(
|
||||||
|
<div
|
||||||
|
key={header.id}
|
||||||
|
role="columnheader"
|
||||||
|
data-column-index={vc.index}
|
||||||
|
style={{
|
||||||
|
width: header.getSize(),
|
||||||
|
...getCommonPinningStyles(header.column),
|
||||||
|
}}
|
||||||
|
className="bg-ui-bg-base txt-compact-small-plus flex items-center border-b border-r px-4 py-2.5"
|
||||||
|
>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, [] as ReactNode[])}
|
||||||
|
{virtualPaddingRight ? (
|
||||||
|
<div
|
||||||
|
role="presentation"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
width: virtualPaddingRight,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
role="rowgroup"
|
||||||
|
className="relative grid"
|
||||||
|
style={{
|
||||||
|
height: `${rowVirtualizer.getTotalSize()}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{virtualRows.map((virtualRow) => {
|
||||||
|
const row = visibleRows[virtualRow.index] as Row<TData>
|
||||||
|
const rowIndex = flatRows.findIndex((r) => r.id === row.id)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataGridRow
|
||||||
|
key={row.id}
|
||||||
|
row={row}
|
||||||
|
rowIndex={rowIndex}
|
||||||
|
virtualRow={virtualRow}
|
||||||
|
flatColumns={flatColumns}
|
||||||
|
virtualColumns={virtualColumns}
|
||||||
|
anchor={anchor}
|
||||||
|
virtualPaddingLeft={virtualPaddingLeft}
|
||||||
|
virtualPaddingRight={virtualPaddingRight}
|
||||||
|
onDragToFillStart={onDragToFillStart}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FocusTrap>
|
||||||
|
</div>
|
||||||
|
</DataGridContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DataGridHeaderProps = {
|
||||||
|
columnOptions: GridColumnOption[]
|
||||||
|
isDisabled: boolean
|
||||||
|
onToggleColumn: (index: number) => (value: boolean) => void
|
||||||
|
onResetColumns: () => void
|
||||||
|
isHighlighted: boolean
|
||||||
|
errorCount: number
|
||||||
|
onToggleErrorHighlighting: () => void
|
||||||
|
onHeaderInteractionChange: (isActive: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const DataGridHeader = ({
|
||||||
|
columnOptions,
|
||||||
|
isDisabled,
|
||||||
|
onToggleColumn,
|
||||||
|
onResetColumns,
|
||||||
|
isHighlighted,
|
||||||
|
errorCount,
|
||||||
|
onToggleErrorHighlighting,
|
||||||
|
onHeaderInteractionChange,
|
||||||
|
}: DataGridHeaderProps) => {
|
||||||
|
const [shortcutsOpen, setShortcutsOpen] = useState(false)
|
||||||
|
const [columnsOpen, setColumnsOpen] = useState(false)
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
// Since all columns are checked by default, we can check if any column is unchecked
|
||||||
|
const hasChanged = columnOptions.some((column) => !column.checked)
|
||||||
|
|
||||||
|
const handleShortcutsOpenChange = (value: boolean) => {
|
||||||
|
onHeaderInteractionChange(value)
|
||||||
|
setShortcutsOpen(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleColumnsOpenChange = (value: boolean) => {
|
||||||
|
onHeaderInteractionChange(value)
|
||||||
|
setColumnsOpen(value)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="bg-ui-bg-base flex items-center justify-between border-b p-4">
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<DropdownMenu open={columnsOpen} onOpenChange={handleColumnsOpenChange}>
|
||||||
|
<ConditionalTooltip
|
||||||
|
showTooltip={isDisabled}
|
||||||
|
content={t("dataGrid.columns.disabled")}
|
||||||
|
>
|
||||||
|
<DropdownMenu.Trigger asChild disabled={isDisabled}>
|
||||||
|
<Button size="small" variant="secondary">
|
||||||
|
{hasChanged ? <AdjustmentsDone /> : <Adjustments />}
|
||||||
|
{t("dataGrid.columns.view")}
|
||||||
|
</Button>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
</ConditionalTooltip>
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
{columnOptions.map((column, index) => {
|
||||||
|
const { checked, disabled, id, name } = column
|
||||||
|
|
||||||
|
if (disabled) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu.CheckboxItem
|
||||||
|
key={id}
|
||||||
|
checked={checked}
|
||||||
|
onCheckedChange={onToggleColumn(index)}
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</DropdownMenu.CheckboxItem>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu>
|
||||||
|
{hasChanged && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="transparent"
|
||||||
|
type="button"
|
||||||
|
onClick={onResetColumns}
|
||||||
|
className="text-ui-fg-muted hover:text-ui-fg-subtle"
|
||||||
|
>
|
||||||
|
{t("dataGrid.columns.resetToDefault")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
{errorCount > 0 && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
onClick={onToggleErrorHighlighting}
|
||||||
|
className={clx({
|
||||||
|
"bg-ui-button-neutral-pressed": isHighlighted,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<ExclamationCircle className="text-ui-fg-subtle" />
|
||||||
|
<span>
|
||||||
|
{t("dataGrid.errors.count", {
|
||||||
|
count: errorCount,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<DataGridKeyboardShortcutModal
|
||||||
|
open={shortcutsOpen}
|
||||||
|
onOpenChange={handleShortcutsOpenChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DataGridCellProps<TData> = {
|
||||||
|
cell: Cell<TData, unknown>
|
||||||
|
columnIndex: number
|
||||||
|
rowIndex: number
|
||||||
|
anchor: DataGridCoordinates | null
|
||||||
|
onDragToFillStart: (e: MouseEvent<HTMLElement>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const DataGridCell = <TData,>({
|
||||||
|
cell,
|
||||||
|
columnIndex,
|
||||||
|
rowIndex,
|
||||||
|
anchor,
|
||||||
|
onDragToFillStart,
|
||||||
|
}: DataGridCellProps<TData>) => {
|
||||||
|
const coords: DataGridCoordinates = {
|
||||||
|
row: rowIndex,
|
||||||
|
col: columnIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAnchor = isCellMatch(coords, anchor)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="gridcell"
|
||||||
|
aria-rowindex={rowIndex}
|
||||||
|
aria-colindex={columnIndex}
|
||||||
|
style={{
|
||||||
|
width: cell.column.getSize(),
|
||||||
|
...getCommonPinningStyles(cell.column),
|
||||||
|
}}
|
||||||
|
data-row-index={rowIndex}
|
||||||
|
data-column-index={columnIndex}
|
||||||
|
className={clx(
|
||||||
|
"relative flex items-center border-b border-r p-0 outline-none"
|
||||||
|
)}
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
<div className="relative h-full w-full">
|
||||||
|
{flexRender(cell.column.columnDef.cell, {
|
||||||
|
...cell.getContext(),
|
||||||
|
columnIndex,
|
||||||
|
rowIndex: rowIndex,
|
||||||
|
} as CellContext<TData, any>)}
|
||||||
|
{isAnchor && (
|
||||||
|
<div
|
||||||
|
onMouseDown={onDragToFillStart}
|
||||||
|
className="bg-ui-fg-interactive absolute bottom-0 right-0 z-[3] size-1.5 cursor-ns-resize"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DataGridRowProps<TData> = {
|
||||||
|
row: Row<TData>
|
||||||
|
rowIndex: number
|
||||||
|
virtualRow: VirtualItem<Element>
|
||||||
|
virtualPaddingLeft?: number
|
||||||
|
virtualPaddingRight?: number
|
||||||
|
virtualColumns: VirtualItem<Element>[]
|
||||||
|
flatColumns: Column<TData, unknown>[]
|
||||||
|
anchor: DataGridCoordinates | null
|
||||||
|
onDragToFillStart: (e: MouseEvent<HTMLElement>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const DataGridRow = <TData,>({
|
||||||
|
row,
|
||||||
|
rowIndex,
|
||||||
|
virtualRow,
|
||||||
|
virtualPaddingLeft,
|
||||||
|
virtualPaddingRight,
|
||||||
|
virtualColumns,
|
||||||
|
flatColumns,
|
||||||
|
anchor,
|
||||||
|
onDragToFillStart,
|
||||||
|
}: DataGridRowProps<TData>) => {
|
||||||
|
const visibleCells = row.getVisibleCells()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="row"
|
||||||
|
aria-rowindex={virtualRow.index}
|
||||||
|
style={{
|
||||||
|
transform: `translateY(${virtualRow.start}px)`,
|
||||||
|
}}
|
||||||
|
className="bg-ui-bg-subtle txt-compact-small absolute flex h-10 w-full"
|
||||||
|
>
|
||||||
|
{virtualPaddingLeft ? (
|
||||||
|
<div
|
||||||
|
role="presentation"
|
||||||
|
style={{ display: "flex", width: virtualPaddingLeft }}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{virtualColumns.reduce((acc, vc, index, array) => {
|
||||||
|
const cell = visibleCells[vc.index]
|
||||||
|
const column = cell.column
|
||||||
|
const columnIndex = flatColumns.findIndex((c) => c.id === column.id)
|
||||||
|
const previousVC = array[index - 1]
|
||||||
|
|
||||||
|
if (previousVC && vc.index !== previousVC.index + 1) {
|
||||||
|
// If there's a gap between the current and previous virtual columns
|
||||||
|
acc.push(
|
||||||
|
<div
|
||||||
|
key={`padding-${previousVC.index}-${vc.index}`}
|
||||||
|
role="presentation"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
width: `${vc.start - previousVC.end}px`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
acc.push(
|
||||||
|
<DataGridCell
|
||||||
|
key={cell.id}
|
||||||
|
cell={cell}
|
||||||
|
columnIndex={columnIndex}
|
||||||
|
rowIndex={rowIndex}
|
||||||
|
anchor={anchor}
|
||||||
|
onDragToFillStart={onDragToFillStart}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, [] as ReactNode[])}
|
||||||
|
{virtualPaddingRight ? (
|
||||||
|
<div
|
||||||
|
role="presentation"
|
||||||
|
style={{ display: "flex", width: virtualPaddingRight }}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
import { Badge, Tooltip } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { DataGridRowError } from "../types"
|
||||||
|
|
||||||
|
type DataGridRowErrorIndicatorProps = {
|
||||||
|
rowErrors: DataGridRowError[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DataGridRowErrorIndicator = ({
|
||||||
|
rowErrors,
|
||||||
|
}: DataGridRowErrorIndicatorProps) => {
|
||||||
|
const rowErrorCount = rowErrors ? rowErrors.length : 0
|
||||||
|
|
||||||
|
if (!rowErrors || rowErrorCount <= 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
<ul className="flex flex-col gap-y-3">
|
||||||
|
{rowErrors.map((error, index) => (
|
||||||
|
<DataGridRowErrorLine key={index} error={error} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
delayDuration={0}
|
||||||
|
>
|
||||||
|
<Badge color="red" size="2xsmall" className="cursor-default">
|
||||||
|
{rowErrorCount}
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const DataGridRowErrorLine = ({
|
||||||
|
error,
|
||||||
|
}: {
|
||||||
|
error: { message: string; to: () => void }
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className="txt-compact-small flex flex-col items-start">
|
||||||
|
{error.message}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={error.to}
|
||||||
|
className="text-ui-fg-interactive hover:text-ui-fg-interactive-hover transition-fg"
|
||||||
|
>
|
||||||
|
{t("dataGrid.errors.fixError")}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
File renamed without changes.
+63
@@ -0,0 +1,63 @@
|
|||||||
|
import { ColumnDef } from "@tanstack/react-table"
|
||||||
|
import { Skeleton } from "../../common/skeleton"
|
||||||
|
|
||||||
|
type DataGridSkeletonProps<TData> = {
|
||||||
|
columns: ColumnDef<TData>[]
|
||||||
|
rows?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DataGridSkeleton = <TData,>({
|
||||||
|
columns,
|
||||||
|
rows: rowCount = 10,
|
||||||
|
}: DataGridSkeletonProps<TData>) => {
|
||||||
|
const rows = Array.from({ length: rowCount }, (_, i) => i)
|
||||||
|
|
||||||
|
const colCount = columns.length
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-ui-bg-subtle size-full">
|
||||||
|
<div className="bg-ui-bg-base border-b p-4">
|
||||||
|
<div className="bg-ui-button-neutral h-7 w-[116px] animate-pulse rounded-md" />
|
||||||
|
</div>
|
||||||
|
<div className="bg-ui-bg-subtle size-full overflow-auto">
|
||||||
|
<div
|
||||||
|
className="grid"
|
||||||
|
style={{
|
||||||
|
gridTemplateColumns: `repeat(${colCount}, 1fr)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{columns.map((_col, i) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="bg-ui-bg-base flex h-10 w-[200px] items-center border-b border-r px-4 py-2.5 last:border-r-0"
|
||||||
|
>
|
||||||
|
<Skeleton className="h-[14px] w-[164px]" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{rows.map((_, j) => (
|
||||||
|
<div
|
||||||
|
className="grid"
|
||||||
|
style={{ gridTemplateColumns: `repeat(${colCount}, 1fr)` }}
|
||||||
|
key={j}
|
||||||
|
>
|
||||||
|
{columns.map((_col, k) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={k}
|
||||||
|
className="bg-ui-bg-base flex h-10 w-[200px] items-center border-b border-r px-4 py-2.5 last:border-r-0"
|
||||||
|
>
|
||||||
|
<Skeleton className="h-[14px] w-[164px]" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+6
-8
@@ -1,21 +1,19 @@
|
|||||||
import { clx } from "@medusajs/ui"
|
import { clx } from "@medusajs/ui"
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
import { Controller, ControllerRenderProps } from "react-hook-form"
|
import { Controller, ControllerRenderProps } from "react-hook-form"
|
||||||
|
|
||||||
import { useEffect, useState } from "react"
|
|
||||||
import { useCombinedRefs } from "../../../hooks/use-combined-refs"
|
import { useCombinedRefs } from "../../../hooks/use-combined-refs"
|
||||||
import { useDataGridCell } from "../hooks"
|
import { useDataGridCell, useDataGridCellError } from "../hooks"
|
||||||
import { DataGridCellProps, InputProps } 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,
|
|
||||||
context,
|
context,
|
||||||
}: DataGridCellProps<TData, TValue>) => {
|
}: DataGridCellProps<TData, TValue>) => {
|
||||||
const { control, renderProps } = useDataGridCell({
|
const { field, control, renderProps } = useDataGridCell({
|
||||||
field,
|
|
||||||
context,
|
context,
|
||||||
type: "text",
|
|
||||||
})
|
})
|
||||||
|
const errorProps = useDataGridCellError({ context })
|
||||||
|
|
||||||
const { container, input } = renderProps
|
const { container, input } = renderProps
|
||||||
|
|
||||||
@@ -25,7 +23,7 @@ export const DataGridTextCell = <TData, TValue = any>({
|
|||||||
name={field}
|
name={field}
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<DataGridCellContainer {...container}>
|
<DataGridCellContainer {...container} {...errorProps}>
|
||||||
<Inner field={field} inputProps={input} />
|
<Inner field={field} inputProps={input} />
|
||||||
</DataGridCellContainer>
|
</DataGridCellContainer>
|
||||||
)
|
)
|
||||||
@@ -55,7 +53,7 @@ const Inner = ({
|
|||||||
return (
|
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 outline-none",
|
||||||
"focus:cursor-text"
|
"focus:cursor-text"
|
||||||
)}
|
)}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
+2
-1
@@ -2,5 +2,6 @@ export { DataGridBooleanCell } from "./data-grid-boolean-cell"
|
|||||||
export { DataGridCurrencyCell } from "./data-grid-currency-cell"
|
export { DataGridCurrencyCell } from "./data-grid-currency-cell"
|
||||||
export { DataGridNumberCell } from "./data-grid-number-cell"
|
export { DataGridNumberCell } from "./data-grid-number-cell"
|
||||||
export { DataGridReadonlyCell as DataGridReadOnlyCell } from "./data-grid-readonly-cell"
|
export { DataGridReadonlyCell as DataGridReadOnlyCell } from "./data-grid-readonly-cell"
|
||||||
|
export { DataGridRoot, type DataGridRootProps } from "./data-grid-root"
|
||||||
|
export { DataGridSkeleton } from "./data-grid-skeleton"
|
||||||
export { DataGridTextCell } from "./data-grid-text-cell"
|
export { DataGridTextCell } from "./data-grid-text-cell"
|
||||||
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { FocusEvent, MouseEvent, createContext } from "react"
|
|
||||||
import { Control, FieldValues, Path, UseFormRegister } from "react-hook-form"
|
|
||||||
import { CellCoords, CellType } from "./types"
|
|
||||||
|
|
||||||
type DataGridContextType<TForm extends FieldValues> = {
|
|
||||||
// Grid state
|
|
||||||
anchor: CellCoords | null
|
|
||||||
trapActive: boolean
|
|
||||||
// Cell handlers
|
|
||||||
registerCell: (coords: CellCoords, field: string, type: CellType) => void
|
|
||||||
getIsCellSelected: (coords: CellCoords) => boolean
|
|
||||||
getIsCellDragSelected: (coords: CellCoords) => boolean
|
|
||||||
// Grid handlers
|
|
||||||
setIsEditing: (value: boolean) => void
|
|
||||||
setIsSelecting: (value: boolean) => void
|
|
||||||
setRangeEnd: (coords: CellCoords) => void
|
|
||||||
setSingleRange: (coords: CellCoords) => void
|
|
||||||
// Form state and handlers
|
|
||||||
register: UseFormRegister<TForm>
|
|
||||||
control: Control<TForm>
|
|
||||||
getInputChangeHandler: (field: Path<TForm>) => (next: any, prev: any) => void
|
|
||||||
// Wrapper handlers
|
|
||||||
getWrapperFocusHandler: (
|
|
||||||
coordinates: CellCoords
|
|
||||||
) => (e: FocusEvent<HTMLElement>) => void
|
|
||||||
getWrapperMouseOverHandler: (
|
|
||||||
coordinates: CellCoords
|
|
||||||
) => ((e: MouseEvent<HTMLElement>) => void) | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DataGridContext = createContext<DataGridContextType<any> | null>(
|
|
||||||
null
|
|
||||||
)
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { FocusEvent, MouseEvent, createContext } from "react"
|
||||||
|
import {
|
||||||
|
Control,
|
||||||
|
FieldErrors,
|
||||||
|
FieldValues,
|
||||||
|
Path,
|
||||||
|
UseFormRegister,
|
||||||
|
} from "react-hook-form"
|
||||||
|
import { CellErrorMetadata, CellMetadata, DataGridCoordinates } from "../types"
|
||||||
|
|
||||||
|
type DataGridContextType<TFieldValues extends FieldValues> = {
|
||||||
|
// Grid state
|
||||||
|
anchor: DataGridCoordinates | null
|
||||||
|
trapActive: boolean
|
||||||
|
errors: FieldErrors<TFieldValues>
|
||||||
|
// Cell handlers
|
||||||
|
getIsCellSelected: (coords: DataGridCoordinates) => boolean
|
||||||
|
getIsCellDragSelected: (coords: DataGridCoordinates) => boolean
|
||||||
|
// Grid handlers
|
||||||
|
setIsEditing: (value: boolean) => void
|
||||||
|
setIsSelecting: (value: boolean) => void
|
||||||
|
setRangeEnd: (coords: DataGridCoordinates) => void
|
||||||
|
setSingleRange: (coords: DataGridCoordinates) => void
|
||||||
|
// Form state and handlers
|
||||||
|
register: UseFormRegister<TFieldValues>
|
||||||
|
control: Control<TFieldValues>
|
||||||
|
getInputChangeHandler: (
|
||||||
|
field: Path<TFieldValues>
|
||||||
|
) => (next: any, prev: any) => void
|
||||||
|
// Wrapper handlers
|
||||||
|
getWrapperFocusHandler: (
|
||||||
|
coordinates: DataGridCoordinates
|
||||||
|
) => (e: FocusEvent<HTMLElement>) => void
|
||||||
|
getWrapperMouseOverHandler: (
|
||||||
|
coordinates: DataGridCoordinates
|
||||||
|
) => ((e: MouseEvent<HTMLElement>) => void) | undefined
|
||||||
|
getCellMetadata: (coords: DataGridCoordinates) => CellMetadata
|
||||||
|
getCellErrorMetadata: (coords: DataGridCoordinates) => CellErrorMetadata
|
||||||
|
navigateToField: (field: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DataGridContext = createContext<DataGridContextType<any> | null>(
|
||||||
|
null
|
||||||
|
)
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./data-grid-context"
|
||||||
|
export * from "./use-data-grid-context"
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
import { useContext } from "react"
|
||||||
|
import { DataGridContext } from "./data-grid-context"
|
||||||
|
|
||||||
|
export const useDataGridContext = () => {
|
||||||
|
const context = useContext(DataGridContext)
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
"useDataGridContext must be used within a DataGridContextProvider"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
-55
@@ -1,55 +0,0 @@
|
|||||||
import { clx } from "@medusajs/ui"
|
|
||||||
import { PropsWithChildren } from "react"
|
|
||||||
|
|
||||||
import { DataGridCellContainerProps } from "../types"
|
|
||||||
|
|
||||||
export const DataGridCellContainer = ({
|
|
||||||
isAnchor,
|
|
||||||
isSelected,
|
|
||||||
isDragSelected,
|
|
||||||
showOverlay,
|
|
||||||
placeholder,
|
|
||||||
innerProps,
|
|
||||||
overlayProps,
|
|
||||||
children,
|
|
||||||
}: DataGridCellContainerProps) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={clx("bg-ui-bg-base relative size-full outline-none", {
|
|
||||||
"ring-ui-bg-interactive ring-2 ring-inset": isAnchor,
|
|
||||||
"bg-ui-bg-highlight [&:has([data-field]:focus)]:bg-ui-bg-base":
|
|
||||||
isSelected || isAnchor,
|
|
||||||
"bg-ui-bg-subtle": isDragSelected && !isAnchor,
|
|
||||||
})}
|
|
||||||
tabIndex={0}
|
|
||||||
{...innerProps}
|
|
||||||
>
|
|
||||||
<div className="relative z-[1] flex size-full items-center justify-center">
|
|
||||||
<RenderChildren isAnchor={isAnchor} placeholder={placeholder}>
|
|
||||||
{children}
|
|
||||||
</RenderChildren>
|
|
||||||
</div>
|
|
||||||
{showOverlay && (
|
|
||||||
<div
|
|
||||||
{...overlayProps}
|
|
||||||
data-cell-overlay="true"
|
|
||||||
className="absolute inset-0 z-[2] size-full"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const RenderChildren = ({
|
|
||||||
isAnchor,
|
|
||||||
placeholder,
|
|
||||||
children,
|
|
||||||
}: PropsWithChildren<
|
|
||||||
Pick<DataGridCellContainerProps, "isAnchor" | "placeholder">
|
|
||||||
>) => {
|
|
||||||
if (!isAnchor && placeholder) {
|
|
||||||
return placeholder
|
|
||||||
}
|
|
||||||
|
|
||||||
return children
|
|
||||||
}
|
|
||||||
-13
@@ -1,13 +0,0 @@
|
|||||||
import { PropsWithChildren } from "react"
|
|
||||||
|
|
||||||
type DataGridReadonlyCellProps = PropsWithChildren
|
|
||||||
|
|
||||||
export const DataGridReadonlyCell = ({
|
|
||||||
children,
|
|
||||||
}: DataGridReadonlyCellProps) => {
|
|
||||||
return (
|
|
||||||
<div className="bg-ui-bg-subtle txt-compact-small text-ui-fg-subtle flex size-full cursor-not-allowed items-center overflow-hidden px-4 py-2.5 outline-none">
|
|
||||||
<span className="truncate">{children}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1,52 +0,0 @@
|
|||||||
import { Table } from "@medusajs/ui"
|
|
||||||
import { ColumnDef } from "@tanstack/react-table"
|
|
||||||
import { Skeleton } from "../common/skeleton"
|
|
||||||
|
|
||||||
type DataGridSkeletonProps<TData> = {
|
|
||||||
columns: ColumnDef<TData>[]
|
|
||||||
rows?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DataGridSkeleton = <TData,>({
|
|
||||||
columns,
|
|
||||||
rows: rowCount = 10,
|
|
||||||
}: DataGridSkeletonProps<TData>) => {
|
|
||||||
const rows = Array.from({ length: rowCount }, (_, i) => i)
|
|
||||||
|
|
||||||
const colCount = columns.length
|
|
||||||
const colWidth = 100 / colCount
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Table>
|
|
||||||
<Table.Header>
|
|
||||||
<Table.Row>
|
|
||||||
{columns.map((_col, i) => {
|
|
||||||
return (
|
|
||||||
<Table.HeaderCell
|
|
||||||
key={i}
|
|
||||||
style={{
|
|
||||||
width: `${colWidth}%`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Skeleton className="h-7" />
|
|
||||||
</Table.HeaderCell>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Table.Row>
|
|
||||||
</Table.Header>
|
|
||||||
<Table.Body>
|
|
||||||
{rows.map((_, j) => (
|
|
||||||
<Table.Row key={j}>
|
|
||||||
{columns.map((_col, k) => {
|
|
||||||
return (
|
|
||||||
<Table.Cell key={k}>
|
|
||||||
<Skeleton className="h-7" />
|
|
||||||
</Table.Cell>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Table.Row>
|
|
||||||
))}
|
|
||||||
</Table.Body>
|
|
||||||
</Table>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -5,10 +5,11 @@ import {
|
|||||||
DataGridCurrencyCell,
|
DataGridCurrencyCell,
|
||||||
DataGridNumberCell,
|
DataGridNumberCell,
|
||||||
DataGridReadOnlyCell,
|
DataGridReadOnlyCell,
|
||||||
|
DataGridRoot,
|
||||||
|
DataGridSkeleton,
|
||||||
DataGridTextCell,
|
DataGridTextCell,
|
||||||
} from "./data-grid-cells"
|
type DataGridRootProps,
|
||||||
import { DataGridRoot, DataGridRootProps } from "./data-grid-root"
|
} from "./components"
|
||||||
import { DataGridSkeleton } from "./data-grid-skeleton"
|
|
||||||
|
|
||||||
interface DataGridProps<TData, TFieldValues extends FieldValues = FieldValues>
|
interface DataGridProps<TData, TFieldValues extends FieldValues = FieldValues>
|
||||||
extends DataGridRootProps<TData, TFieldValues> {
|
extends DataGridRootProps<TData, TFieldValues> {
|
||||||
@@ -20,7 +21,12 @@ const _DataGrid = <TData, TFieldValues extends FieldValues = FieldValues>({
|
|||||||
...props
|
...props
|
||||||
}: DataGridProps<TData, TFieldValues>) => {
|
}: DataGridProps<TData, TFieldValues>) => {
|
||||||
return isLoading ? (
|
return isLoading ? (
|
||||||
<DataGridSkeleton columns={props.columns} />
|
<DataGridSkeleton
|
||||||
|
columns={props.columns}
|
||||||
|
rows={
|
||||||
|
props.data?.length && props.data.length > 0 ? props.data.length : 10
|
||||||
|
}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<DataGridRoot {...props} />
|
<DataGridRoot {...props} />
|
||||||
)
|
)
|
||||||
|
|||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
import {
|
||||||
|
CellContext,
|
||||||
|
ColumnDefTemplate,
|
||||||
|
createColumnHelper,
|
||||||
|
HeaderContext,
|
||||||
|
} from "@tanstack/react-table"
|
||||||
|
import { FieldValues } from "react-hook-form"
|
||||||
|
|
||||||
|
import { DataGridColumnType, FieldFunction } from "../types"
|
||||||
|
|
||||||
|
type DataGridHelperColumnsProps<TData, TFieldValues extends FieldValues> = {
|
||||||
|
/**
|
||||||
|
* The id of the column.
|
||||||
|
*/
|
||||||
|
id: string
|
||||||
|
/**
|
||||||
|
* The name of the column, shown in the column visibility menu.
|
||||||
|
*/
|
||||||
|
name?: string
|
||||||
|
/**
|
||||||
|
* The header template for the column.
|
||||||
|
*/
|
||||||
|
header: ColumnDefTemplate<HeaderContext<TData, unknown>> | undefined
|
||||||
|
/**
|
||||||
|
* The cell template for the column.
|
||||||
|
*/
|
||||||
|
cell: ColumnDefTemplate<CellContext<TData, unknown>> | undefined
|
||||||
|
/**
|
||||||
|
* Callback to set the field path for each cell in the column.
|
||||||
|
* If a callback is not provided, or returns null, the cell will not be editable.
|
||||||
|
*/
|
||||||
|
field?: FieldFunction<TData, TFieldValues>
|
||||||
|
/**
|
||||||
|
* Whether the column cannot be hidden by the user.
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
disableHiding?: boolean
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
field: FieldFunction<TData, TFieldValues>
|
||||||
|
type: DataGridColumnType
|
||||||
|
}
|
||||||
|
| { field?: null | undefined; type?: never }
|
||||||
|
)
|
||||||
|
|
||||||
|
export function createDataGridHelper<
|
||||||
|
TData,
|
||||||
|
TFieldValues extends FieldValues
|
||||||
|
>() {
|
||||||
|
const columnHelper = createColumnHelper<TData>()
|
||||||
|
|
||||||
|
return {
|
||||||
|
column: ({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
header,
|
||||||
|
cell,
|
||||||
|
disableHiding = false,
|
||||||
|
field,
|
||||||
|
type,
|
||||||
|
}: DataGridHelperColumnsProps<TData, TFieldValues>) =>
|
||||||
|
columnHelper.display({
|
||||||
|
id,
|
||||||
|
header,
|
||||||
|
cell,
|
||||||
|
enableHiding: !disableHiding,
|
||||||
|
meta: {
|
||||||
|
name,
|
||||||
|
field,
|
||||||
|
type,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
+53
-24
@@ -1,27 +1,43 @@
|
|||||||
import { HttpTypes } from "@medusajs/types"
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import { CellContext, ColumnDef } from "@tanstack/react-table"
|
import { ColumnDef } from "@tanstack/react-table"
|
||||||
import { TFunction } from "i18next"
|
import { TFunction } from "i18next"
|
||||||
|
import { FieldPath, FieldValues } from "react-hook-form"
|
||||||
import { IncludesTaxTooltip } from "../../common/tax-badge/tax-badge"
|
import { IncludesTaxTooltip } from "../../common/tax-badge/tax-badge"
|
||||||
import { DataGridCurrencyCell } from "../data-grid-cells/data-grid-currency-cell"
|
import { DataGridCurrencyCell } from "../components/data-grid-currency-cell"
|
||||||
import { DataGridReadonlyCell } from "../data-grid-cells/data-grid-readonly-cell"
|
import { DataGridReadonlyCell } from "../components/data-grid-readonly-cell"
|
||||||
import { createDataGridHelper } from "../utils"
|
import { FieldContext } from "../types"
|
||||||
|
import { createDataGridHelper } from "./create-data-grid-column-helper"
|
||||||
|
|
||||||
export const createDataGridPriceColumns = <TData,>({
|
type CreateDataGridPriceColumnsProps<
|
||||||
|
TData,
|
||||||
|
TFieldValues extends FieldValues
|
||||||
|
> = {
|
||||||
|
currencies?: string[]
|
||||||
|
regions?: HttpTypes.AdminRegion[]
|
||||||
|
pricePreferences?: HttpTypes.AdminPricePreference[]
|
||||||
|
isReadyOnly?: (context: FieldContext<TData>) => boolean
|
||||||
|
getFieldName: (
|
||||||
|
context: FieldContext<TData>,
|
||||||
|
value: string
|
||||||
|
) => FieldPath<TFieldValues> | null
|
||||||
|
t: TFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createDataGridPriceColumns = <
|
||||||
|
TData,
|
||||||
|
TFieldValues extends FieldValues
|
||||||
|
>({
|
||||||
currencies,
|
currencies,
|
||||||
regions,
|
regions,
|
||||||
pricePreferences,
|
pricePreferences,
|
||||||
isReadyOnly,
|
isReadyOnly,
|
||||||
getFieldName,
|
getFieldName,
|
||||||
t,
|
t,
|
||||||
}: {
|
}: CreateDataGridPriceColumnsProps<TData, TFieldValues>): ColumnDef<
|
||||||
currencies?: string[]
|
TData,
|
||||||
regions?: HttpTypes.AdminRegion[]
|
unknown
|
||||||
pricePreferences?: HttpTypes.AdminPricePreference[]
|
>[] => {
|
||||||
isReadyOnly?: (context: CellContext<TData, unknown>) => boolean
|
const columnHelper = createDataGridHelper<TData, TFieldValues>()
|
||||||
getFieldName: (context: CellContext<TData, unknown>, value: string) => string
|
|
||||||
t: TFunction
|
|
||||||
}): ColumnDef<TData, unknown>[] => {
|
|
||||||
const columnHelper = createDataGridHelper<TData>()
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...(currencies?.map((currency) => {
|
...(currencies?.map((currency) => {
|
||||||
@@ -34,6 +50,16 @@ export const createDataGridPriceColumns = <TData,>({
|
|||||||
name: t("fields.priceTemplate", {
|
name: t("fields.priceTemplate", {
|
||||||
regionOrCurrency: currency.toUpperCase(),
|
regionOrCurrency: currency.toUpperCase(),
|
||||||
}),
|
}),
|
||||||
|
field: (context) => {
|
||||||
|
const isReadyOnlyValue = isReadyOnly?.(context)
|
||||||
|
|
||||||
|
if (isReadyOnlyValue) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return getFieldName(context, currency)
|
||||||
|
},
|
||||||
|
type: "number",
|
||||||
header: () => (
|
header: () => (
|
||||||
<div className="flex w-full items-center justify-between gap-3">
|
<div className="flex w-full items-center justify-between gap-3">
|
||||||
{t("fields.priceTemplate", {
|
{t("fields.priceTemplate", {
|
||||||
@@ -44,16 +70,10 @@ export const createDataGridPriceColumns = <TData,>({
|
|||||||
),
|
),
|
||||||
cell: (context) => {
|
cell: (context) => {
|
||||||
if (isReadyOnly?.(context)) {
|
if (isReadyOnly?.(context)) {
|
||||||
return <DataGridReadonlyCell />
|
return <DataGridReadonlyCell context={context} />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <DataGridCurrencyCell code={currency} context={context} />
|
||||||
<DataGridCurrencyCell
|
|
||||||
code={currency}
|
|
||||||
context={context}
|
|
||||||
field={getFieldName(context, currency)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}) ?? []),
|
}) ?? []),
|
||||||
@@ -67,6 +87,16 @@ export const createDataGridPriceColumns = <TData,>({
|
|||||||
name: t("fields.priceTemplate", {
|
name: t("fields.priceTemplate", {
|
||||||
regionOrCurrency: region.name,
|
regionOrCurrency: region.name,
|
||||||
}),
|
}),
|
||||||
|
field: (context) => {
|
||||||
|
const isReadyOnlyValue = isReadyOnly?.(context)
|
||||||
|
|
||||||
|
if (isReadyOnlyValue) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return getFieldName(context, region.id)
|
||||||
|
},
|
||||||
|
type: "number",
|
||||||
header: () => (
|
header: () => (
|
||||||
<div className="flex w-full items-center justify-between gap-3">
|
<div className="flex w-full items-center justify-between gap-3">
|
||||||
{t("fields.priceTemplate", {
|
{t("fields.priceTemplate", {
|
||||||
@@ -77,7 +107,7 @@ export const createDataGridPriceColumns = <TData,>({
|
|||||||
),
|
),
|
||||||
cell: (context) => {
|
cell: (context) => {
|
||||||
if (isReadyOnly?.(context)) {
|
if (isReadyOnly?.(context)) {
|
||||||
return <DataGridReadonlyCell />
|
return <DataGridReadonlyCell context={context} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const currency = currencies?.find((c) => c === region.currency_code)
|
const currency = currencies?.find((c) => c === region.currency_code)
|
||||||
@@ -89,7 +119,6 @@ export const createDataGridPriceColumns = <TData,>({
|
|||||||
<DataGridCurrencyCell
|
<DataGridCurrencyCell
|
||||||
code={region.currency_code}
|
code={region.currency_code}
|
||||||
context={context}
|
context={context}
|
||||||
field={getFieldName(context, region.id)}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
+1
@@ -1 +1,2 @@
|
|||||||
|
export * from "./create-data-grid-column-helper"
|
||||||
export * from "./create-data-grid-price-columns"
|
export * from "./create-data-grid-price-columns"
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
export * from "./use-data-grid-cell"
|
||||||
|
export * from "./use-data-grid-cell-error"
|
||||||
|
export * from "./use-data-grid-cell-handlers"
|
||||||
|
export * from "./use-data-grid-cell-metadata"
|
||||||
|
export * from "./use-data-grid-cell-snapshot"
|
||||||
|
export * from "./use-data-grid-clipboard-events"
|
||||||
|
export * from "./use-data-grid-column-visibility"
|
||||||
|
export * from "./use-data-grid-error-highlighting"
|
||||||
|
export * from "./use-data-grid-form-handlers"
|
||||||
|
export * from "./use-data-grid-keydown-event"
|
||||||
|
export * from "./use-data-grid-mouse-up-event"
|
||||||
|
export * from "./use-data-grid-navigation"
|
||||||
|
export * from "./use-data-grid-query-tool"
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
import { CellContext } from "@tanstack/react-table"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
import { FieldError, FieldErrors, get } from "react-hook-form"
|
||||||
|
|
||||||
|
import { useDataGridContext } from "../context"
|
||||||
|
import { DataGridCellContext, DataGridRowError } from "../types"
|
||||||
|
|
||||||
|
type UseDataGridCellErrorOptions<TData, TValue> = {
|
||||||
|
context: CellContext<TData, TValue>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDataGridCellError = <TextData, TValue>({
|
||||||
|
context,
|
||||||
|
}: UseDataGridCellErrorOptions<TextData, TValue>) => {
|
||||||
|
const { errors, getCellErrorMetadata, navigateToField } = useDataGridContext()
|
||||||
|
|
||||||
|
const { rowIndex, columnIndex } = context as DataGridCellContext<
|
||||||
|
TextData,
|
||||||
|
TValue
|
||||||
|
>
|
||||||
|
|
||||||
|
const { accessor, field } = useMemo(() => {
|
||||||
|
return getCellErrorMetadata({ row: rowIndex, col: columnIndex })
|
||||||
|
}, [rowIndex, columnIndex, getCellErrorMetadata])
|
||||||
|
|
||||||
|
const rowErrorsObject: FieldErrors | undefined =
|
||||||
|
accessor && columnIndex === 0 ? get(errors, accessor) : undefined
|
||||||
|
|
||||||
|
const rowErrors: DataGridRowError[] = []
|
||||||
|
|
||||||
|
function collectErrors(
|
||||||
|
errorObject: FieldErrors | FieldError | undefined,
|
||||||
|
baseAccessor: string
|
||||||
|
) {
|
||||||
|
if (!errorObject) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFieldError(errorObject)) {
|
||||||
|
// Handle a single FieldError directly
|
||||||
|
const message = errorObject.message
|
||||||
|
|
||||||
|
const to = () => navigateToField(baseAccessor)
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
rowErrors.push({ message, to })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Traverse nested objects
|
||||||
|
Object.keys(errorObject).forEach((key) => {
|
||||||
|
const nestedError = errorObject[key]
|
||||||
|
const fieldAccessor = `${baseAccessor}.${key}`
|
||||||
|
|
||||||
|
if (nestedError && typeof nestedError === "object") {
|
||||||
|
collectErrors(nestedError, fieldAccessor)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rowErrorsObject && accessor) {
|
||||||
|
collectErrors(rowErrorsObject, accessor)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cellError: FieldError | undefined = field
|
||||||
|
? get(errors, field)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
errors,
|
||||||
|
rowErrors,
|
||||||
|
cellError,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFieldError(errors: FieldErrors | FieldError): errors is FieldError {
|
||||||
|
return typeof errors === "object" && "message" in errors && "type" in errors
|
||||||
|
}
|
||||||
+153
@@ -0,0 +1,153 @@
|
|||||||
|
import { FocusEvent, MouseEvent, useCallback } from "react"
|
||||||
|
import { FieldValues, UseFormSetValue } from "react-hook-form"
|
||||||
|
import { DataGridMatrix, DataGridUpdateCommand } from "../models"
|
||||||
|
import { DataGridCoordinates } from "../types"
|
||||||
|
|
||||||
|
type UseDataGridCellHandlersOptions<TData, TFieldValues extends FieldValues> = {
|
||||||
|
matrix: DataGridMatrix<TData, TFieldValues>
|
||||||
|
anchor: DataGridCoordinates | null
|
||||||
|
rangeEnd: DataGridCoordinates | null
|
||||||
|
setRangeEnd: (coords: DataGridCoordinates | null) => void
|
||||||
|
isSelecting: boolean
|
||||||
|
setIsSelecting: (isSelecting: boolean) => void
|
||||||
|
isDragging: boolean
|
||||||
|
setIsDragging: (isDragging: boolean) => void
|
||||||
|
setSingleRange: (coords: DataGridCoordinates) => void
|
||||||
|
dragEnd: DataGridCoordinates | null
|
||||||
|
setDragEnd: (coords: DataGridCoordinates | null) => void
|
||||||
|
setValue: UseFormSetValue<TFieldValues>
|
||||||
|
execute: (command: DataGridUpdateCommand) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDataGridCellHandlers = <
|
||||||
|
TData,
|
||||||
|
TFieldValues extends FieldValues
|
||||||
|
>({
|
||||||
|
matrix,
|
||||||
|
anchor,
|
||||||
|
rangeEnd,
|
||||||
|
setRangeEnd,
|
||||||
|
isDragging,
|
||||||
|
setIsDragging,
|
||||||
|
isSelecting,
|
||||||
|
setIsSelecting,
|
||||||
|
setSingleRange,
|
||||||
|
dragEnd,
|
||||||
|
setDragEnd,
|
||||||
|
setValue,
|
||||||
|
execute,
|
||||||
|
}: UseDataGridCellHandlersOptions<TData, TFieldValues>) => {
|
||||||
|
const getWrapperFocusHandler = useCallback(
|
||||||
|
(coords: DataGridCoordinates) => {
|
||||||
|
return (_e: FocusEvent<HTMLElement>) => {
|
||||||
|
setSingleRange(coords)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setSingleRange]
|
||||||
|
)
|
||||||
|
|
||||||
|
const getOverlayMouseDownHandler = useCallback(
|
||||||
|
(coords: DataGridCoordinates) => {
|
||||||
|
return (e: MouseEvent<HTMLElement>) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (e.shiftKey) {
|
||||||
|
setRangeEnd(coords)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSelecting(true)
|
||||||
|
|
||||||
|
setSingleRange(coords)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setIsSelecting, setRangeEnd, setSingleRange]
|
||||||
|
)
|
||||||
|
|
||||||
|
const getWrapperMouseOverHandler = useCallback(
|
||||||
|
(coords: DataGridCoordinates) => {
|
||||||
|
if (!isDragging && !isSelecting) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return (_e: MouseEvent<HTMLElement>) => {
|
||||||
|
/**
|
||||||
|
* If the column is not the same as the anchor col,
|
||||||
|
* we don't want to select the cell.
|
||||||
|
*/
|
||||||
|
if (anchor?.col !== coords.col) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSelecting) {
|
||||||
|
setRangeEnd(coords)
|
||||||
|
} else {
|
||||||
|
setDragEnd(coords)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[anchor?.col, isDragging, isSelecting, setDragEnd, setRangeEnd]
|
||||||
|
)
|
||||||
|
|
||||||
|
const getInputChangeHandler = useCallback(
|
||||||
|
// Using `any` here as the generic type of Path<TFieldValues> will
|
||||||
|
// not be inferred correctly.
|
||||||
|
(field: any) => {
|
||||||
|
return (next: any, prev: any) => {
|
||||||
|
const command = new DataGridUpdateCommand({
|
||||||
|
next,
|
||||||
|
prev,
|
||||||
|
setter: (value) => {
|
||||||
|
setValue(field, value, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
execute(command)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setValue, execute]
|
||||||
|
)
|
||||||
|
|
||||||
|
const onDragToFillStart = useCallback(
|
||||||
|
(_e: MouseEvent<HTMLElement>) => {
|
||||||
|
setIsDragging(true)
|
||||||
|
},
|
||||||
|
[setIsDragging]
|
||||||
|
)
|
||||||
|
|
||||||
|
const getIsCellSelected = useCallback(
|
||||||
|
(cell: DataGridCoordinates | null) => {
|
||||||
|
if (!cell || !anchor || !rangeEnd) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return matrix.getIsCellSelected(cell, anchor, rangeEnd)
|
||||||
|
},
|
||||||
|
[anchor, rangeEnd, matrix]
|
||||||
|
)
|
||||||
|
|
||||||
|
const getIsCellDragSelected = useCallback(
|
||||||
|
(cell: DataGridCoordinates | null) => {
|
||||||
|
if (!cell || !anchor || !dragEnd) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return matrix.getIsCellSelected(cell, anchor, dragEnd)
|
||||||
|
},
|
||||||
|
[anchor, dragEnd, matrix]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
getWrapperFocusHandler,
|
||||||
|
getOverlayMouseDownHandler,
|
||||||
|
getWrapperMouseOverHandler,
|
||||||
|
getInputChangeHandler,
|
||||||
|
getIsCellSelected,
|
||||||
|
getIsCellDragSelected,
|
||||||
|
onDragToFillStart,
|
||||||
|
}
|
||||||
|
}
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
import { useCallback } from "react"
|
||||||
|
import { FieldValues } from "react-hook-form"
|
||||||
|
import { DataGridMatrix } from "../models"
|
||||||
|
import { CellErrorMetadata, CellMetadata, DataGridCoordinates } from "../types"
|
||||||
|
import { generateCellId } from "../utils"
|
||||||
|
|
||||||
|
type UseDataGridCellMetadataOptions<TData, TFieldValues extends FieldValues> = {
|
||||||
|
matrix: DataGridMatrix<TData, TFieldValues>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDataGridCellMetadata = <
|
||||||
|
TData,
|
||||||
|
TFieldValues extends FieldValues
|
||||||
|
>({
|
||||||
|
matrix,
|
||||||
|
}: UseDataGridCellMetadataOptions<TData, TFieldValues>) => {
|
||||||
|
/**
|
||||||
|
* Creates metadata for a cell.
|
||||||
|
*/
|
||||||
|
const getCellMetadata = useCallback(
|
||||||
|
(coords: DataGridCoordinates): CellMetadata => {
|
||||||
|
const { row, col } = coords
|
||||||
|
|
||||||
|
const id = generateCellId(coords)
|
||||||
|
const field = matrix.getCellField(coords)
|
||||||
|
const type = matrix.getCellType(coords)
|
||||||
|
|
||||||
|
if (!field || !type) {
|
||||||
|
throw new Error(`'field' or 'type' is null for cell ${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputAttributes = {
|
||||||
|
"data-row": row,
|
||||||
|
"data-col": col,
|
||||||
|
"data-cell-id": id,
|
||||||
|
"data-field": field,
|
||||||
|
}
|
||||||
|
|
||||||
|
const innerAttributes = {
|
||||||
|
"data-container-id": id,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
field,
|
||||||
|
type,
|
||||||
|
inputAttributes,
|
||||||
|
innerAttributes,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[matrix]
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates error metadata for a cell. This is used to display error messages
|
||||||
|
* in the cell, and its containing row.
|
||||||
|
*/
|
||||||
|
const getCellErrorMetadata = useCallback(
|
||||||
|
(coords: DataGridCoordinates): CellErrorMetadata => {
|
||||||
|
const accessor = matrix.getRowAccessor(coords.row)
|
||||||
|
const field = matrix.getCellField(coords)
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessor,
|
||||||
|
field,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[matrix]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
getCellMetadata,
|
||||||
|
getCellErrorMetadata,
|
||||||
|
}
|
||||||
|
}
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
import { useCallback, useState } from "react"
|
||||||
|
import { FieldValues, Path, UseFormReturn } from "react-hook-form"
|
||||||
|
import { DataGridMatrix } from "../models"
|
||||||
|
import { DataGridCellSnapshot, DataGridCoordinates } from "../types"
|
||||||
|
|
||||||
|
type UseDataGridCellSnapshotOptions<TData, TFieldValues extends FieldValues> = {
|
||||||
|
matrix: DataGridMatrix<TData, TFieldValues>
|
||||||
|
form: UseFormReturn<TFieldValues>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDataGridCellSnapshot = <
|
||||||
|
TData,
|
||||||
|
TFieldValues extends FieldValues
|
||||||
|
>({
|
||||||
|
matrix,
|
||||||
|
form,
|
||||||
|
}: UseDataGridCellSnapshotOptions<TData, TFieldValues>) => {
|
||||||
|
const [snapshot, setSnapshot] = useState<DataGridCellSnapshot<TFieldValues> | null>(
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
const { getValues, setValue } = form
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a snapshot of the current cell value.
|
||||||
|
*/
|
||||||
|
const createSnapshot = useCallback(
|
||||||
|
(cell: DataGridCoordinates | null) => {
|
||||||
|
if (!cell) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const field = matrix.getCellField(cell)
|
||||||
|
|
||||||
|
if (!field) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = getValues(field as Path<TFieldValues>)
|
||||||
|
|
||||||
|
setSnapshot({ field, value })
|
||||||
|
},
|
||||||
|
[getValues, matrix]
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores the cell value from the snapshot if it exists.
|
||||||
|
*/
|
||||||
|
const restoreSnapshot = useCallback(() => {
|
||||||
|
if (!snapshot) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { field, value } = snapshot
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setValue(field as Path<TFieldValues>, value)
|
||||||
|
})
|
||||||
|
}, [setValue, snapshot])
|
||||||
|
|
||||||
|
return {
|
||||||
|
createSnapshot,
|
||||||
|
restoreSnapshot,
|
||||||
|
}
|
||||||
|
}
|
||||||
+32
-68
@@ -1,59 +1,24 @@
|
|||||||
import { CellContext } from "@tanstack/react-table"
|
import { CellContext } from "@tanstack/react-table"
|
||||||
import React, {
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||||
MouseEvent,
|
|
||||||
useCallback,
|
import { useDataGridContext } from "../context"
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react"
|
|
||||||
import { DataGridContext } from "./context"
|
|
||||||
import { GridQueryTool } from "./models"
|
|
||||||
import {
|
import {
|
||||||
CellCoords,
|
|
||||||
DataGridCellContext,
|
DataGridCellContext,
|
||||||
DataGridCellRenderProps,
|
DataGridCellRenderProps,
|
||||||
} from "./types"
|
DataGridCoordinates,
|
||||||
import { generateCellId, isCellMatch } from "./utils"
|
} from "../types"
|
||||||
|
import { isCellMatch } from "../utils"
|
||||||
|
|
||||||
const useDataGridContext = () => {
|
type UseDataGridCellOptions<TData, TValue> = {
|
||||||
const context = useContext(DataGridContext)
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error(
|
|
||||||
"useDataGridContext must be used within a DataGridContextProvider"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
type UseDataGridCellProps<TData, TValue> = {
|
|
||||||
field: string
|
|
||||||
context: CellContext<TData, TValue>
|
context: CellContext<TData, TValue>
|
||||||
type: "text" | "number" | "select" | "boolean"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const textCharacterRegex = /^.$/u
|
const textCharacterRegex = /^.$/u
|
||||||
const numberCharacterRegex = /^[0-9]$/u
|
const numberCharacterRegex = /^[0-9]$/u
|
||||||
|
|
||||||
export const useDataGridCell = <TData, TValue>({
|
export const useDataGridCell = <TData, TValue>({
|
||||||
field,
|
|
||||||
context,
|
context,
|
||||||
type,
|
}: UseDataGridCellOptions<TData, TValue>) => {
|
||||||
}: UseDataGridCellProps<TData, TValue>) => {
|
|
||||||
const { rowIndex, columnIndex } = context as DataGridCellContext<
|
|
||||||
TData,
|
|
||||||
TValue
|
|
||||||
>
|
|
||||||
|
|
||||||
const coords: CellCoords = useMemo(
|
|
||||||
() => ({ row: rowIndex, col: columnIndex }),
|
|
||||||
[rowIndex, columnIndex]
|
|
||||||
)
|
|
||||||
const id = generateCellId(coords)
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
control,
|
control,
|
||||||
@@ -67,12 +32,22 @@ export const useDataGridCell = <TData, TValue>({
|
|||||||
getInputChangeHandler,
|
getInputChangeHandler,
|
||||||
getIsCellSelected,
|
getIsCellSelected,
|
||||||
getIsCellDragSelected,
|
getIsCellDragSelected,
|
||||||
registerCell,
|
getCellMetadata,
|
||||||
} = useDataGridContext()
|
} = useDataGridContext()
|
||||||
|
|
||||||
useEffect(() => {
|
const { rowIndex, columnIndex } = context as DataGridCellContext<
|
||||||
registerCell(coords, field, type)
|
TData,
|
||||||
}, [coords, field, type, registerCell])
|
TValue
|
||||||
|
>
|
||||||
|
|
||||||
|
const coords: DataGridCoordinates = useMemo(
|
||||||
|
() => ({ row: rowIndex, col: columnIndex }),
|
||||||
|
[rowIndex, columnIndex]
|
||||||
|
)
|
||||||
|
|
||||||
|
const { id, field, type, innerAttributes, inputAttributes } = useMemo(() => {
|
||||||
|
return getCellMetadata(coords)
|
||||||
|
}, [coords, getCellMetadata])
|
||||||
|
|
||||||
const [showOverlay, setShowOverlay] = useState(true)
|
const [showOverlay, setShowOverlay] = useState(true)
|
||||||
|
|
||||||
@@ -80,7 +55,7 @@ export const useDataGridCell = <TData, TValue>({
|
|||||||
const inputRef = useRef<HTMLElement>(null)
|
const inputRef = useRef<HTMLElement>(null)
|
||||||
|
|
||||||
const handleOverlayMouseDown = useCallback(
|
const handleOverlayMouseDown = useCallback(
|
||||||
(e: MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
@@ -183,6 +158,10 @@ export const useDataGridCell = <TData, TValue>({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const event = new KeyboardEvent(e.type, e.nativeEvent)
|
const event = new KeyboardEvent(e.type, e.nativeEvent)
|
||||||
|
|
||||||
inputRef.current.focus()
|
inputRef.current.focus()
|
||||||
@@ -203,7 +182,7 @@ export const useDataGridCell = <TData, TValue>({
|
|||||||
}, [anchor, coords])
|
}, [anchor, coords])
|
||||||
|
|
||||||
const fieldWithoutOverlay = useMemo(() => {
|
const fieldWithoutOverlay = useMemo(() => {
|
||||||
return type === "boolean" || type === "select"
|
return type === "boolean"
|
||||||
}, [type])
|
}, [type])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -214,6 +193,7 @@ export const useDataGridCell = <TData, TValue>({
|
|||||||
|
|
||||||
const renderProps: DataGridCellRenderProps = {
|
const renderProps: DataGridCellRenderProps = {
|
||||||
container: {
|
container: {
|
||||||
|
field,
|
||||||
isAnchor,
|
isAnchor,
|
||||||
isSelected: getIsCellSelected(coords),
|
isSelected: getIsCellSelected(coords),
|
||||||
isDragSelected: getIsCellDragSelected(coords),
|
isDragSelected: getIsCellDragSelected(coords),
|
||||||
@@ -225,7 +205,7 @@ export const useDataGridCell = <TData, TValue>({
|
|||||||
type === "boolean" ? handleBooleanInnerMouseDown : undefined,
|
type === "boolean" ? handleBooleanInnerMouseDown : undefined,
|
||||||
onKeyDown: handleContainerKeyDown,
|
onKeyDown: handleContainerKeyDown,
|
||||||
onFocus: getWrapperFocusHandler(coords),
|
onFocus: getWrapperFocusHandler(coords),
|
||||||
"data-container-id": id,
|
...innerAttributes,
|
||||||
},
|
},
|
||||||
overlayProps: {
|
overlayProps: {
|
||||||
onMouseDown: handleOverlayMouseDown,
|
onMouseDown: handleOverlayMouseDown,
|
||||||
@@ -236,31 +216,15 @@ export const useDataGridCell = <TData, TValue>({
|
|||||||
onBlur: handleInputBlur,
|
onBlur: handleInputBlur,
|
||||||
onFocus: handleInputFocus,
|
onFocus: handleInputFocus,
|
||||||
onChange: getInputChangeHandler(field),
|
onChange: getInputChangeHandler(field),
|
||||||
"data-row": coords.row,
|
...inputAttributes,
|
||||||
"data-col": coords.col,
|
|
||||||
"data-cell-id": id,
|
|
||||||
"data-field": field,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
|
field,
|
||||||
register,
|
register,
|
||||||
control,
|
control,
|
||||||
renderProps,
|
renderProps,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useGridQueryTool = (
|
|
||||||
containerRef: React.RefObject<HTMLElement>
|
|
||||||
) => {
|
|
||||||
const queryToolRef = useRef<GridQueryTool | null>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (containerRef.current) {
|
|
||||||
queryToolRef.current = new GridQueryTool(containerRef.current)
|
|
||||||
}
|
|
||||||
}, [containerRef])
|
|
||||||
|
|
||||||
return queryToolRef.current
|
|
||||||
}
|
|
||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
import { useCallback } from "react"
|
||||||
|
import { FieldValues, Path, PathValue } from "react-hook-form"
|
||||||
|
|
||||||
|
import { DataGridBulkUpdateCommand, DataGridMatrix } from "../models"
|
||||||
|
import { DataGridCoordinates } from "../types"
|
||||||
|
|
||||||
|
type UseDataGridClipboardEventsOptions<
|
||||||
|
TData,
|
||||||
|
TFieldValues extends FieldValues
|
||||||
|
> = {
|
||||||
|
matrix: DataGridMatrix<TData, TFieldValues>
|
||||||
|
isEditing: boolean
|
||||||
|
anchor: DataGridCoordinates | null
|
||||||
|
rangeEnd: DataGridCoordinates | null
|
||||||
|
getSelectionValues: (
|
||||||
|
fields: string[]
|
||||||
|
) => PathValue<TFieldValues, Path<TFieldValues>>[]
|
||||||
|
setSelectionValues: (
|
||||||
|
fields: string[],
|
||||||
|
values: PathValue<TFieldValues, Path<TFieldValues>>[]
|
||||||
|
) => void
|
||||||
|
execute: (command: DataGridBulkUpdateCommand) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDataGridClipboardEvents = <
|
||||||
|
TData,
|
||||||
|
TFieldValues extends FieldValues
|
||||||
|
>({
|
||||||
|
matrix,
|
||||||
|
anchor,
|
||||||
|
rangeEnd,
|
||||||
|
isEditing,
|
||||||
|
getSelectionValues,
|
||||||
|
setSelectionValues,
|
||||||
|
execute,
|
||||||
|
}: UseDataGridClipboardEventsOptions<TData, TFieldValues>) => {
|
||||||
|
const handleCopyEvent = useCallback(
|
||||||
|
(e: ClipboardEvent) => {
|
||||||
|
if (isEditing || !anchor || !rangeEnd) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const fields = matrix.getFieldsInSelection(anchor, rangeEnd)
|
||||||
|
const values = getSelectionValues(fields)
|
||||||
|
|
||||||
|
const text = values.map((value) => `${value}` ?? "").join("\t")
|
||||||
|
|
||||||
|
e.clipboardData?.setData("text/plain", text)
|
||||||
|
},
|
||||||
|
[isEditing, anchor, rangeEnd, matrix, getSelectionValues]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handlePasteEvent = useCallback(
|
||||||
|
(e: ClipboardEvent) => {
|
||||||
|
if (isEditing || !anchor || !rangeEnd) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const text = e.clipboardData?.getData("text/plain")
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = text.split("\t")
|
||||||
|
|
||||||
|
const fields = matrix.getFieldsInSelection(anchor, rangeEnd)
|
||||||
|
const prev = getSelectionValues(fields)
|
||||||
|
|
||||||
|
const command = new DataGridBulkUpdateCommand({
|
||||||
|
fields,
|
||||||
|
next,
|
||||||
|
prev,
|
||||||
|
setter: setSelectionValues,
|
||||||
|
})
|
||||||
|
|
||||||
|
execute(command)
|
||||||
|
},
|
||||||
|
[
|
||||||
|
isEditing,
|
||||||
|
anchor,
|
||||||
|
rangeEnd,
|
||||||
|
matrix,
|
||||||
|
getSelectionValues,
|
||||||
|
setSelectionValues,
|
||||||
|
execute,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleCopyEvent,
|
||||||
|
handlePasteEvent,
|
||||||
|
}
|
||||||
|
}
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
import type { Column, Table } from "@tanstack/react-table"
|
||||||
|
import { useCallback } from "react"
|
||||||
|
import type { FieldValues } from "react-hook-form"
|
||||||
|
|
||||||
|
import { DataGridMatrix } from "../models"
|
||||||
|
import { GridColumnOption } from "../types"
|
||||||
|
|
||||||
|
export function useDataGridColumnVisibility<
|
||||||
|
TData,
|
||||||
|
TFieldValues extends FieldValues
|
||||||
|
>(grid: Table<TData>, matrix: DataGridMatrix<TData, TFieldValues>) {
|
||||||
|
const columns = grid.getAllLeafColumns()
|
||||||
|
|
||||||
|
const columnOptions: GridColumnOption[] = columns.map((column) => ({
|
||||||
|
id: column.id,
|
||||||
|
name: getColumnName(column),
|
||||||
|
checked: column.getIsVisible(),
|
||||||
|
disabled: !column.getCanHide(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const handleToggleColumn = useCallback(
|
||||||
|
(index: number) => (value: boolean) => {
|
||||||
|
const column = columns[index]
|
||||||
|
|
||||||
|
if (!column.getCanHide()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
matrix.toggleColumn(index, value)
|
||||||
|
column.toggleVisibility(value)
|
||||||
|
},
|
||||||
|
[columns, matrix]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleResetColumns = useCallback(() => {
|
||||||
|
grid.setColumnVisibility({})
|
||||||
|
}, [grid])
|
||||||
|
|
||||||
|
const optionCount = columnOptions.filter((c) => !c.disabled).length
|
||||||
|
const isDisabled = optionCount === 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
columnOptions,
|
||||||
|
handleToggleColumn,
|
||||||
|
handleResetColumns,
|
||||||
|
isDisabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColumnName<TData>(column: Column<TData, unknown>): string {
|
||||||
|
const id = column.columnDef.id
|
||||||
|
const enableHiding = column.columnDef.enableHiding
|
||||||
|
const meta = column?.columnDef.meta as { name?: string } | undefined
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
throw new Error(
|
||||||
|
"Column is missing an id, which is a required field. Please provide an id for the column."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === "development" && !meta?.name && enableHiding) {
|
||||||
|
console.warn(
|
||||||
|
`Column "${id}" does not have a name. You should add a name to the column definition. Falling back to the column id.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return meta?.name || id
|
||||||
|
}
|
||||||
+133
@@ -0,0 +1,133 @@
|
|||||||
|
import { Table, VisibilityState } from "@tanstack/react-table"
|
||||||
|
import { useCallback, useMemo, useState } from "react"
|
||||||
|
import { FieldError, FieldErrors, FieldValues } from "react-hook-form"
|
||||||
|
|
||||||
|
import { DataGridMatrix } from "../models"
|
||||||
|
import { VisibilitySnapshot } from "../types"
|
||||||
|
|
||||||
|
export const useDataGridErrorHighlighting = <
|
||||||
|
TData,
|
||||||
|
TFieldValues extends FieldValues
|
||||||
|
>(
|
||||||
|
matrix: DataGridMatrix<TData, TFieldValues>,
|
||||||
|
grid: Table<TData>,
|
||||||
|
errors: FieldErrors<TFieldValues>
|
||||||
|
) => {
|
||||||
|
const [isHighlighted, setIsHighlighted] = useState<boolean>(false)
|
||||||
|
const [visibilitySnapshot, setVisibilitySnapshot] =
|
||||||
|
useState<VisibilitySnapshot | null>(null)
|
||||||
|
|
||||||
|
const { flatRows } = grid.getRowModel()
|
||||||
|
const flatColumns = grid.getAllFlatColumns()
|
||||||
|
|
||||||
|
const errorPaths = findErrorPaths(errors)
|
||||||
|
const errorCount = errorPaths.length
|
||||||
|
|
||||||
|
const { rowsWithErrors, columnsWithErrors } = useMemo(() => {
|
||||||
|
const rowsWithErrors = new Set<number>()
|
||||||
|
const columnsWithErrors = new Set<number>()
|
||||||
|
|
||||||
|
errorPaths.forEach((errorPath) => {
|
||||||
|
const rowIndex = matrix.rowAccessors.findIndex(
|
||||||
|
(accessor) =>
|
||||||
|
accessor &&
|
||||||
|
(errorPath === accessor || errorPath.startsWith(`${accessor}.`))
|
||||||
|
)
|
||||||
|
if (rowIndex !== -1) {
|
||||||
|
rowsWithErrors.add(rowIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnIndex = matrix.columnAccessors.findIndex(
|
||||||
|
(accessor) =>
|
||||||
|
accessor &&
|
||||||
|
(errorPath === accessor || errorPath.endsWith(`.${accessor}`))
|
||||||
|
)
|
||||||
|
if (columnIndex !== -1) {
|
||||||
|
columnsWithErrors.add(columnIndex)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return { rowsWithErrors, columnsWithErrors }
|
||||||
|
}, [errorPaths, matrix.rowAccessors, matrix.columnAccessors])
|
||||||
|
|
||||||
|
const toggleErrorHighlighting = useCallback(
|
||||||
|
(
|
||||||
|
currentRowVisibility: VisibilityState,
|
||||||
|
currentColumnVisibility: VisibilityState,
|
||||||
|
setRowVisibility: (visibility: VisibilityState) => void,
|
||||||
|
setColumnVisibility: (visibility: VisibilityState) => void
|
||||||
|
) => {
|
||||||
|
if (isHighlighted) {
|
||||||
|
// Clear error highlights
|
||||||
|
if (visibilitySnapshot) {
|
||||||
|
setRowVisibility(visibilitySnapshot.rows)
|
||||||
|
setColumnVisibility(visibilitySnapshot.columns)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Highlight errors
|
||||||
|
setVisibilitySnapshot({
|
||||||
|
rows: { ...currentRowVisibility },
|
||||||
|
columns: { ...currentColumnVisibility },
|
||||||
|
})
|
||||||
|
|
||||||
|
const rowsToHide = flatRows
|
||||||
|
.map((_, index) => {
|
||||||
|
return !rowsWithErrors.has(index) ? index : undefined
|
||||||
|
})
|
||||||
|
.filter((index): index is number => index !== undefined)
|
||||||
|
|
||||||
|
const columnsToHide = flatColumns
|
||||||
|
.map((column, index) => {
|
||||||
|
return !columnsWithErrors.has(index) && index !== 0
|
||||||
|
? column.id
|
||||||
|
: undefined
|
||||||
|
})
|
||||||
|
.filter((id): id is string => id !== undefined)
|
||||||
|
|
||||||
|
setRowVisibility(
|
||||||
|
rowsToHide.reduce((acc, row) => ({ ...acc, [row]: false }), {})
|
||||||
|
)
|
||||||
|
|
||||||
|
setColumnVisibility(
|
||||||
|
columnsToHide.reduce(
|
||||||
|
(acc, column) => ({ ...acc, [column]: false }),
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsHighlighted((prev) => !prev)
|
||||||
|
},
|
||||||
|
[
|
||||||
|
isHighlighted,
|
||||||
|
visibilitySnapshot,
|
||||||
|
flatRows,
|
||||||
|
flatColumns,
|
||||||
|
rowsWithErrors,
|
||||||
|
columnsWithErrors,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
errorCount,
|
||||||
|
isHighlighted,
|
||||||
|
toggleErrorHighlighting,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findErrorPaths(
|
||||||
|
obj: FieldErrors | FieldError,
|
||||||
|
path: string[] = []
|
||||||
|
): string[] {
|
||||||
|
if (typeof obj !== "object" || obj === null) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("message" in obj && "type" in obj) {
|
||||||
|
return [path.join(".")]
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(obj).flatMap(([key, value]) =>
|
||||||
|
findErrorPaths(value, [...path, key])
|
||||||
|
)
|
||||||
|
}
|
||||||
+130
@@ -0,0 +1,130 @@
|
|||||||
|
import { useCallback } from "react"
|
||||||
|
import { FieldValues, Path, PathValue, UseFormReturn } from "react-hook-form"
|
||||||
|
|
||||||
|
import { DataGridMatrix } from "../models"
|
||||||
|
import { DataGridColumnType, DataGridCoordinates } from "../types"
|
||||||
|
|
||||||
|
type UseDataGridFormHandlersOptions<TData, TFieldValues extends FieldValues> = {
|
||||||
|
matrix: DataGridMatrix<TData, TFieldValues>
|
||||||
|
form: UseFormReturn<TFieldValues>
|
||||||
|
anchor: DataGridCoordinates | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDataGridFormHandlers = <
|
||||||
|
TData,
|
||||||
|
TFieldValues extends FieldValues
|
||||||
|
>({
|
||||||
|
matrix,
|
||||||
|
form,
|
||||||
|
anchor,
|
||||||
|
}: UseDataGridFormHandlersOptions<TData, TFieldValues>) => {
|
||||||
|
const { getValues, setValue } = form
|
||||||
|
|
||||||
|
const getSelectionValues = useCallback(
|
||||||
|
(fields: string[]): PathValue<TFieldValues, Path<TFieldValues>>[] => {
|
||||||
|
if (!fields.length) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields.map((field) => {
|
||||||
|
return getValues(field as Path<TFieldValues>)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[getValues]
|
||||||
|
)
|
||||||
|
|
||||||
|
const setSelectionValues = useCallback(
|
||||||
|
async (fields: string[], values: string[]) => {
|
||||||
|
if (!fields.length || !anchor) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = matrix.getCellType(anchor)
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const convertedValues = convertArrayToPrimitive(values, type)
|
||||||
|
|
||||||
|
fields.forEach((field, index) => {
|
||||||
|
if (!field) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueIndex = index % values.length
|
||||||
|
const value = convertedValues[valueIndex] as PathValue<
|
||||||
|
TFieldValues,
|
||||||
|
Path<TFieldValues>
|
||||||
|
>
|
||||||
|
|
||||||
|
setValue(field as Path<TFieldValues>, value, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[matrix, anchor, setValue]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
getSelectionValues,
|
||||||
|
setSelectionValues,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertToNumber(value: string | number): number {
|
||||||
|
if (typeof value === "number") {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const converted = Number(value)
|
||||||
|
|
||||||
|
if (isNaN(converted)) {
|
||||||
|
throw new Error(`String "${value}" cannot be converted to number.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return converted
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertToBoolean(value: string | boolean): boolean {
|
||||||
|
if (typeof value === "boolean") {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "undefined" || value === null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const lowerValue = value.toLowerCase()
|
||||||
|
|
||||||
|
if (lowerValue === "true" || lowerValue === "false") {
|
||||||
|
return lowerValue === "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`String "${value}" cannot be converted to boolean.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function covertToString(value: any): string {
|
||||||
|
if (typeof value === "undefined" || value === null) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertArrayToPrimitive(
|
||||||
|
values: any[],
|
||||||
|
type: DataGridColumnType
|
||||||
|
): any[] {
|
||||||
|
switch (type) {
|
||||||
|
case "number":
|
||||||
|
return values.map(convertToNumber)
|
||||||
|
case "boolean":
|
||||||
|
return values.map(convertToBoolean)
|
||||||
|
case "text":
|
||||||
|
return values.map(covertToString)
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported target type "${type}".`)
|
||||||
|
}
|
||||||
|
}
|
||||||
+537
@@ -0,0 +1,537 @@
|
|||||||
|
import { useCallback } from "react"
|
||||||
|
import type {
|
||||||
|
FieldValues,
|
||||||
|
Path,
|
||||||
|
PathValue,
|
||||||
|
UseFormGetValues,
|
||||||
|
UseFormSetValue,
|
||||||
|
} from "react-hook-form"
|
||||||
|
import {
|
||||||
|
DataGridBulkUpdateCommand,
|
||||||
|
DataGridMatrix,
|
||||||
|
DataGridQueryTool,
|
||||||
|
DataGridUpdateCommand,
|
||||||
|
} from "../models"
|
||||||
|
import { DataGridCoordinates } from "../types"
|
||||||
|
|
||||||
|
type UseDataGridKeydownEventOptions<TData, TFieldValues extends FieldValues> = {
|
||||||
|
matrix: DataGridMatrix<TData, TFieldValues>
|
||||||
|
anchor: DataGridCoordinates | null
|
||||||
|
rangeEnd: DataGridCoordinates | null
|
||||||
|
isEditing: boolean
|
||||||
|
scrollToCoordinates: (
|
||||||
|
coords: DataGridCoordinates,
|
||||||
|
direction: "horizontal" | "vertical" | "both"
|
||||||
|
) => void
|
||||||
|
setSingleRange: (coordinates: DataGridCoordinates | null) => void
|
||||||
|
setRangeEnd: (coordinates: DataGridCoordinates | null) => void
|
||||||
|
onEditingChangeHandler: (value: boolean) => void
|
||||||
|
getValues: UseFormGetValues<TFieldValues>
|
||||||
|
setValue: UseFormSetValue<TFieldValues>
|
||||||
|
execute: (command: DataGridUpdateCommand | DataGridBulkUpdateCommand) => void
|
||||||
|
undo: () => void
|
||||||
|
redo: () => void
|
||||||
|
queryTool: DataGridQueryTool | null
|
||||||
|
getSelectionValues: (
|
||||||
|
fields: string[]
|
||||||
|
) => PathValue<TFieldValues, Path<TFieldValues>>[]
|
||||||
|
setSelectionValues: (fields: string[], values: string[]) => void
|
||||||
|
restoreSnapshot: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ARROW_KEYS = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"]
|
||||||
|
const VERTICAL_KEYS = ["ArrowUp", "ArrowDown"]
|
||||||
|
|
||||||
|
export const useDataGridKeydownEvent = <
|
||||||
|
TData,
|
||||||
|
TFieldValues extends FieldValues
|
||||||
|
>({
|
||||||
|
matrix,
|
||||||
|
anchor,
|
||||||
|
rangeEnd,
|
||||||
|
isEditing,
|
||||||
|
scrollToCoordinates,
|
||||||
|
setSingleRange,
|
||||||
|
setRangeEnd,
|
||||||
|
onEditingChangeHandler,
|
||||||
|
getValues,
|
||||||
|
setValue,
|
||||||
|
execute,
|
||||||
|
undo,
|
||||||
|
redo,
|
||||||
|
queryTool,
|
||||||
|
getSelectionValues,
|
||||||
|
setSelectionValues,
|
||||||
|
restoreSnapshot,
|
||||||
|
}: UseDataGridKeydownEventOptions<TData, TFieldValues>) => {
|
||||||
|
const handleKeyboardNavigation = useCallback(
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
if (!anchor) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = matrix.getCellType(anchor)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the user is currently editing a cell, we don't want to
|
||||||
|
* handle the keyboard navigation.
|
||||||
|
*
|
||||||
|
* If the cell is of type boolean, we don't want to ignore the
|
||||||
|
* keyboard navigation, as we want to allow the user to navigate
|
||||||
|
* away from the cell directly, as you cannot "enter" a boolean cell.
|
||||||
|
*/
|
||||||
|
if (isEditing && type !== "boolean") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const direction = VERTICAL_KEYS.includes(e.key)
|
||||||
|
? "vertical"
|
||||||
|
: "horizontal"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the user performs a horizontal navigation, we want to
|
||||||
|
* use the anchor as the basis for the navigation.
|
||||||
|
*
|
||||||
|
* If the user performs a vertical navigation, the bases depends
|
||||||
|
* on the type of interaction. If the user is holding shift, we want
|
||||||
|
* to use the rangeEnd as the basis. If the user is not holding shift,
|
||||||
|
* we want to use the anchor as the basis.
|
||||||
|
*/
|
||||||
|
const basis =
|
||||||
|
direction === "horizontal" ? anchor : e.shiftKey ? rangeEnd : anchor
|
||||||
|
|
||||||
|
const updater =
|
||||||
|
direction === "horizontal"
|
||||||
|
? setSingleRange
|
||||||
|
: e.shiftKey
|
||||||
|
? setRangeEnd
|
||||||
|
: setSingleRange
|
||||||
|
|
||||||
|
if (!basis) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { row, col } = basis
|
||||||
|
|
||||||
|
const handleNavigation = (coords: DataGridCoordinates) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
scrollToCoordinates(coords, direction)
|
||||||
|
updater(coords)
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = matrix.getValidMovement(
|
||||||
|
row,
|
||||||
|
col,
|
||||||
|
e.key,
|
||||||
|
e.metaKey || e.ctrlKey
|
||||||
|
)
|
||||||
|
|
||||||
|
handleNavigation(next)
|
||||||
|
},
|
||||||
|
[
|
||||||
|
isEditing,
|
||||||
|
anchor,
|
||||||
|
rangeEnd,
|
||||||
|
scrollToCoordinates,
|
||||||
|
setSingleRange,
|
||||||
|
setRangeEnd,
|
||||||
|
matrix,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleUndo = useCallback(
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (e.shiftKey) {
|
||||||
|
redo()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
undo()
|
||||||
|
},
|
||||||
|
[redo, undo]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleSpaceKeyBoolean = useCallback(
|
||||||
|
(anchor: DataGridCoordinates) => {
|
||||||
|
const end = rangeEnd ?? anchor
|
||||||
|
|
||||||
|
const fields = matrix.getFieldsInSelection(anchor, end)
|
||||||
|
|
||||||
|
const prev = getSelectionValues(fields) as boolean[]
|
||||||
|
|
||||||
|
const allChecked = prev.every((value) => value === true)
|
||||||
|
const next = Array.from({ length: prev.length }, () => !allChecked)
|
||||||
|
|
||||||
|
const command = new DataGridBulkUpdateCommand({
|
||||||
|
fields,
|
||||||
|
next,
|
||||||
|
prev,
|
||||||
|
setter: setSelectionValues,
|
||||||
|
})
|
||||||
|
|
||||||
|
execute(command)
|
||||||
|
},
|
||||||
|
[rangeEnd, matrix, getSelectionValues, setSelectionValues, execute]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleSpaceKeyTextOrNumber = useCallback(
|
||||||
|
(anchor: DataGridCoordinates) => {
|
||||||
|
const field = matrix.getCellField(anchor)
|
||||||
|
const input = queryTool?.getInput(anchor)
|
||||||
|
|
||||||
|
if (!field || !input) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = getValues(field as Path<TFieldValues>)
|
||||||
|
const next = ""
|
||||||
|
|
||||||
|
const command = new DataGridUpdateCommand({
|
||||||
|
next,
|
||||||
|
prev: current,
|
||||||
|
setter: (value) => {
|
||||||
|
setValue(field as Path<TFieldValues>, value, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
execute(command)
|
||||||
|
|
||||||
|
input.focus()
|
||||||
|
},
|
||||||
|
[matrix, queryTool, getValues, execute, setValue]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleSpaceKey = useCallback(
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
if (!anchor || isEditing) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const type = matrix.getCellType(anchor)
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "boolean":
|
||||||
|
handleSpaceKeyBoolean(anchor)
|
||||||
|
break
|
||||||
|
case "number":
|
||||||
|
case "text":
|
||||||
|
handleSpaceKeyTextOrNumber(anchor)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
anchor,
|
||||||
|
isEditing,
|
||||||
|
matrix,
|
||||||
|
handleSpaceKeyBoolean,
|
||||||
|
handleSpaceKeyTextOrNumber,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleMoveOnEnter = useCallback(
|
||||||
|
(e: KeyboardEvent, anchor: DataGridCoordinates) => {
|
||||||
|
const direction = e.shiftKey ? "ArrowUp" : "ArrowDown"
|
||||||
|
|
||||||
|
const pos = matrix.getValidMovement(
|
||||||
|
anchor.row,
|
||||||
|
anchor.col,
|
||||||
|
direction,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
if (anchor.row !== pos.row || anchor.col !== pos.col) {
|
||||||
|
setSingleRange(pos)
|
||||||
|
scrollToCoordinates(pos, "vertical")
|
||||||
|
} else {
|
||||||
|
// If the the user is at the last cell, we want to focus the container of the cell.
|
||||||
|
const container = queryTool?.getContainer(anchor)
|
||||||
|
|
||||||
|
container?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
onEditingChangeHandler(false)
|
||||||
|
},
|
||||||
|
[
|
||||||
|
queryTool,
|
||||||
|
matrix,
|
||||||
|
scrollToCoordinates,
|
||||||
|
setSingleRange,
|
||||||
|
onEditingChangeHandler,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleEditOnEnter = useCallback(
|
||||||
|
(anchor: DataGridCoordinates) => {
|
||||||
|
const input = queryTool?.getInput(anchor)
|
||||||
|
|
||||||
|
if (!input) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
input.focus()
|
||||||
|
onEditingChangeHandler(true)
|
||||||
|
},
|
||||||
|
[queryTool, onEditingChangeHandler]
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the enter key for text and number cells.
|
||||||
|
*
|
||||||
|
* The behavior is as follows:
|
||||||
|
* - If the cell is currently not being edited, start editing the cell.
|
||||||
|
* - If the cell is currently being edited, move to the next cell.
|
||||||
|
*/
|
||||||
|
const handleEnterKeyTextOrNumber = useCallback(
|
||||||
|
(e: KeyboardEvent, anchor: DataGridCoordinates) => {
|
||||||
|
if (isEditing) {
|
||||||
|
handleMoveOnEnter(e, anchor)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEditOnEnter(anchor)
|
||||||
|
},
|
||||||
|
[handleMoveOnEnter, handleEditOnEnter, isEditing]
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the enter key for boolean cells.
|
||||||
|
*
|
||||||
|
* The behavior is as follows:
|
||||||
|
* - If the cell is currently undefined, set it to true.
|
||||||
|
* - If the cell is currently a boolean, invert the value.
|
||||||
|
* - After the value has been set, move to the next cell.
|
||||||
|
*/
|
||||||
|
const handleEnterKeyBoolean = useCallback(
|
||||||
|
(e: KeyboardEvent, anchor: DataGridCoordinates) => {
|
||||||
|
const field = matrix.getCellField(anchor)
|
||||||
|
|
||||||
|
if (!field) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = getValues(field as Path<TFieldValues>)
|
||||||
|
let next: boolean
|
||||||
|
|
||||||
|
if (typeof current === "boolean") {
|
||||||
|
next = !current
|
||||||
|
} else {
|
||||||
|
next = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const command = new DataGridUpdateCommand({
|
||||||
|
next,
|
||||||
|
prev: current,
|
||||||
|
setter: (value) => {
|
||||||
|
setValue(field as Path<TFieldValues>, value, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
execute(command)
|
||||||
|
handleMoveOnEnter(e, anchor)
|
||||||
|
},
|
||||||
|
[execute, getValues, handleMoveOnEnter, matrix, setValue]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleEnterKey = useCallback(
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
if (!anchor) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const type = matrix.getCellType(anchor)
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "text":
|
||||||
|
case "number":
|
||||||
|
handleEnterKeyTextOrNumber(e, anchor)
|
||||||
|
break
|
||||||
|
case "boolean": {
|
||||||
|
handleEnterKeyBoolean(e, anchor)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[anchor, matrix, handleEnterKeyTextOrNumber, handleEnterKeyBoolean]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleDeleteKeyTextOrNumber = useCallback(
|
||||||
|
(anchor: DataGridCoordinates, rangeEnd: DataGridCoordinates) => {
|
||||||
|
const fields = matrix.getFieldsInSelection(anchor, rangeEnd)
|
||||||
|
const prev = getSelectionValues(fields)
|
||||||
|
const next = Array.from({ length: prev.length }, () => "")
|
||||||
|
|
||||||
|
const command = new DataGridBulkUpdateCommand({
|
||||||
|
fields,
|
||||||
|
next,
|
||||||
|
prev,
|
||||||
|
setter: setSelectionValues,
|
||||||
|
})
|
||||||
|
|
||||||
|
execute(command)
|
||||||
|
},
|
||||||
|
[matrix, getSelectionValues, setSelectionValues, execute]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleDeleteKeyBoolean = useCallback(
|
||||||
|
(anchor: DataGridCoordinates, rangeEnd: DataGridCoordinates) => {
|
||||||
|
const fields = matrix.getFieldsInSelection(anchor, rangeEnd)
|
||||||
|
const prev = getSelectionValues(fields)
|
||||||
|
const next = Array.from({ length: prev.length }, () => false)
|
||||||
|
|
||||||
|
const command = new DataGridBulkUpdateCommand({
|
||||||
|
fields,
|
||||||
|
next,
|
||||||
|
prev,
|
||||||
|
setter: setSelectionValues,
|
||||||
|
})
|
||||||
|
|
||||||
|
execute(command)
|
||||||
|
},
|
||||||
|
[execute, getSelectionValues, matrix, setSelectionValues]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleDeleteKey = useCallback(
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
if (!anchor || !rangeEnd || isEditing) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const type = matrix.getCellType(anchor)
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "text":
|
||||||
|
case "number":
|
||||||
|
handleDeleteKeyTextOrNumber(anchor, rangeEnd)
|
||||||
|
break
|
||||||
|
case "boolean":
|
||||||
|
handleDeleteKeyBoolean(anchor, rangeEnd)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
anchor,
|
||||||
|
rangeEnd,
|
||||||
|
isEditing,
|
||||||
|
matrix,
|
||||||
|
handleDeleteKeyTextOrNumber,
|
||||||
|
handleDeleteKeyBoolean,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleEscapeKey = useCallback(
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
if (!anchor || !isEditing) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
// try to restore the previous value
|
||||||
|
restoreSnapshot()
|
||||||
|
|
||||||
|
// Restore focus to the container element
|
||||||
|
const container = queryTool?.getContainer(anchor)
|
||||||
|
container?.focus()
|
||||||
|
},
|
||||||
|
[queryTool, isEditing, anchor, restoreSnapshot]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleTabKey = useCallback(
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
if (!anchor || isEditing) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const direction = e.shiftKey ? "ArrowLeft" : "ArrowRight"
|
||||||
|
|
||||||
|
const next = matrix.getValidMovement(
|
||||||
|
anchor.row,
|
||||||
|
anchor.col,
|
||||||
|
direction,
|
||||||
|
e.metaKey || e.ctrlKey
|
||||||
|
)
|
||||||
|
|
||||||
|
setSingleRange(next)
|
||||||
|
scrollToCoordinates(next, "horizontal")
|
||||||
|
},
|
||||||
|
[anchor, isEditing, scrollToCoordinates, setSingleRange, matrix]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleKeyDownEvent = useCallback(
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
if (ARROW_KEYS.includes(e.key)) {
|
||||||
|
handleKeyboardNavigation(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === "z" && (e.metaKey || e.ctrlKey)) {
|
||||||
|
handleUndo(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === " ") {
|
||||||
|
handleSpaceKey(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === "Delete" || e.key === "Backspace") {
|
||||||
|
handleDeleteKey(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
handleEnterKey(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
handleEscapeKey(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === "Tab") {
|
||||||
|
handleTabKey(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
handleEscapeKey,
|
||||||
|
handleKeyboardNavigation,
|
||||||
|
handleUndo,
|
||||||
|
handleSpaceKey,
|
||||||
|
handleEnterKey,
|
||||||
|
handleDeleteKey,
|
||||||
|
handleTabKey,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleKeyDownEvent,
|
||||||
|
}
|
||||||
|
}
|
||||||
+96
@@ -0,0 +1,96 @@
|
|||||||
|
import { useCallback } from "react"
|
||||||
|
import { FieldValues, Path, PathValue } from "react-hook-form"
|
||||||
|
import { DataGridBulkUpdateCommand, DataGridMatrix } from "../models"
|
||||||
|
import { DataGridCoordinates } from "../types"
|
||||||
|
|
||||||
|
type UseDataGridMouseUpEventOptions<TData, TFieldValues extends FieldValues> = {
|
||||||
|
matrix: DataGridMatrix<TData, TFieldValues>
|
||||||
|
anchor: DataGridCoordinates | null
|
||||||
|
dragEnd: DataGridCoordinates | null
|
||||||
|
setDragEnd: (coords: DataGridCoordinates | null) => void
|
||||||
|
setRangeEnd: (coords: DataGridCoordinates | null) => void
|
||||||
|
setIsSelecting: (isSelecting: boolean) => void
|
||||||
|
setIsDragging: (isDragging: boolean) => void
|
||||||
|
getSelectionValues: (
|
||||||
|
fields: string[]
|
||||||
|
) => PathValue<TFieldValues, Path<TFieldValues>>[]
|
||||||
|
setSelectionValues: (
|
||||||
|
fields: string[],
|
||||||
|
values: PathValue<TFieldValues, Path<TFieldValues>>[]
|
||||||
|
) => void
|
||||||
|
execute: (command: DataGridBulkUpdateCommand) => void
|
||||||
|
isDragging: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDataGridMouseUpEvent = <
|
||||||
|
TData,
|
||||||
|
TFieldValues extends FieldValues
|
||||||
|
>({
|
||||||
|
matrix,
|
||||||
|
anchor,
|
||||||
|
dragEnd,
|
||||||
|
setDragEnd,
|
||||||
|
isDragging,
|
||||||
|
setIsDragging,
|
||||||
|
setRangeEnd,
|
||||||
|
setIsSelecting,
|
||||||
|
getSelectionValues,
|
||||||
|
setSelectionValues,
|
||||||
|
execute,
|
||||||
|
}: UseDataGridMouseUpEventOptions<TData, TFieldValues>) => {
|
||||||
|
const handleDragEnd = useCallback(() => {
|
||||||
|
if (!isDragging) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!anchor || !dragEnd) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const dragSelection = matrix.getFieldsInSelection(anchor, dragEnd)
|
||||||
|
const anchorField = matrix.getCellField(anchor)
|
||||||
|
|
||||||
|
if (!anchorField || !dragSelection.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const anchorValue = getSelectionValues([anchorField])
|
||||||
|
const fields = dragSelection.filter((field) => field !== anchorField)
|
||||||
|
|
||||||
|
const prev = getSelectionValues(fields)
|
||||||
|
const next = Array.from({ length: prev.length }, () => anchorValue[0])
|
||||||
|
|
||||||
|
const command = new DataGridBulkUpdateCommand({
|
||||||
|
fields,
|
||||||
|
prev,
|
||||||
|
next,
|
||||||
|
setter: setSelectionValues,
|
||||||
|
})
|
||||||
|
|
||||||
|
execute(command)
|
||||||
|
|
||||||
|
setIsDragging(false)
|
||||||
|
setDragEnd(null)
|
||||||
|
|
||||||
|
setRangeEnd(dragEnd)
|
||||||
|
}, [
|
||||||
|
isDragging,
|
||||||
|
anchor,
|
||||||
|
dragEnd,
|
||||||
|
matrix,
|
||||||
|
getSelectionValues,
|
||||||
|
setSelectionValues,
|
||||||
|
execute,
|
||||||
|
setIsDragging,
|
||||||
|
setDragEnd,
|
||||||
|
setRangeEnd,
|
||||||
|
])
|
||||||
|
|
||||||
|
const handleMouseUpEvent = useCallback(() => {
|
||||||
|
handleDragEnd()
|
||||||
|
setIsSelecting(false)
|
||||||
|
}, [handleDragEnd, setIsSelecting])
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleMouseUpEvent,
|
||||||
|
}
|
||||||
|
}
|
||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
import { Column, Row, VisibilityState } from "@tanstack/react-table"
|
||||||
|
import { ScrollToOptions, Virtualizer } from "@tanstack/react-virtual"
|
||||||
|
import { Dispatch, SetStateAction, useCallback } from "react"
|
||||||
|
import { FieldValues } from "react-hook-form"
|
||||||
|
import { DataGridMatrix, DataGridQueryTool } from "../models"
|
||||||
|
import { DataGridCoordinates } from "../types"
|
||||||
|
|
||||||
|
type UseDataGridNavigationOptions<TData, TFieldValues extends FieldValues> = {
|
||||||
|
matrix: DataGridMatrix<TData, TFieldValues>
|
||||||
|
anchor: DataGridCoordinates | null
|
||||||
|
visibleRows: Row<TData>[]
|
||||||
|
visibleColumns: Column<TData, unknown>[]
|
||||||
|
rowVirtualizer: Virtualizer<HTMLDivElement, Element>
|
||||||
|
columnVirtualizer: Virtualizer<HTMLDivElement, Element>
|
||||||
|
setColumnVisibility: Dispatch<SetStateAction<VisibilityState>>
|
||||||
|
flatColumns: Column<TData, unknown>[]
|
||||||
|
queryTool: DataGridQueryTool | null
|
||||||
|
setSingleRange: (coords: DataGridCoordinates | null) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDataGridNavigation = <TData, TFieldValues extends FieldValues>({
|
||||||
|
matrix,
|
||||||
|
anchor,
|
||||||
|
visibleColumns,
|
||||||
|
visibleRows,
|
||||||
|
columnVirtualizer,
|
||||||
|
rowVirtualizer,
|
||||||
|
setColumnVisibility,
|
||||||
|
flatColumns,
|
||||||
|
queryTool,
|
||||||
|
setSingleRange,
|
||||||
|
}: UseDataGridNavigationOptions<TData, TFieldValues>) => {
|
||||||
|
const scrollToCoordinates = useCallback(
|
||||||
|
(coords: DataGridCoordinates, direction: "horizontal" | "vertical" | "both") => {
|
||||||
|
if (!anchor) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { row, col } = coords
|
||||||
|
const { row: anchorRow, col: anchorCol } = anchor
|
||||||
|
|
||||||
|
const rowDirection = row >= anchorRow ? "down" : "up"
|
||||||
|
const colDirection = col >= anchorCol ? "right" : "left"
|
||||||
|
|
||||||
|
let toRow = rowDirection === "down" ? row + 1 : row - 1
|
||||||
|
if (visibleRows[toRow] === undefined) {
|
||||||
|
toRow = row
|
||||||
|
}
|
||||||
|
|
||||||
|
let toCol = colDirection === "right" ? col + 1 : col - 1
|
||||||
|
if (visibleColumns[toCol] === undefined) {
|
||||||
|
toCol = col
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollOptions: ScrollToOptions = { align: "auto", behavior: "auto" }
|
||||||
|
|
||||||
|
if (direction === "horizontal" || direction === "both") {
|
||||||
|
columnVirtualizer.scrollToIndex(toCol, scrollOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction === "vertical" || direction === "both") {
|
||||||
|
rowVirtualizer.scrollToIndex(toRow, scrollOptions)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[anchor, columnVirtualizer, visibleRows, rowVirtualizer, visibleColumns]
|
||||||
|
)
|
||||||
|
|
||||||
|
const navigateToField = useCallback(
|
||||||
|
(field: string) => {
|
||||||
|
const coords = matrix.getCoordinatesByField(field)
|
||||||
|
|
||||||
|
if (!coords) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const column = flatColumns[coords.col]
|
||||||
|
|
||||||
|
// Ensure that the column is visible
|
||||||
|
setColumnVisibility((prev) => {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[column.id]: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
scrollToCoordinates(coords, "both")
|
||||||
|
setSingleRange(coords)
|
||||||
|
})
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const input = queryTool?.getInput(coords)
|
||||||
|
|
||||||
|
if (input) {
|
||||||
|
input.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[
|
||||||
|
matrix,
|
||||||
|
flatColumns,
|
||||||
|
setColumnVisibility,
|
||||||
|
scrollToCoordinates,
|
||||||
|
setSingleRange,
|
||||||
|
queryTool,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
scrollToCoordinates,
|
||||||
|
navigateToField,
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
import { RefObject, useEffect, useRef } from "react"
|
||||||
|
|
||||||
|
import { DataGridQueryTool } from "../models"
|
||||||
|
|
||||||
|
export const useDataGridQueryTool = (containerRef: RefObject<HTMLElement>) => {
|
||||||
|
const queryToolRef = useRef<DataGridQueryTool | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (containerRef.current) {
|
||||||
|
queryToolRef.current = new DataGridQueryTool(containerRef.current)
|
||||||
|
}
|
||||||
|
}, [containerRef])
|
||||||
|
|
||||||
|
return queryToolRef.current
|
||||||
|
}
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
export * from "./data-grid"
|
export * from "./data-grid"
|
||||||
export * from "./data-grid-column-helpers"
|
export * from "./helpers"
|
||||||
export { createDataGridHelper } from "./utils"
|
|
||||||
@@ -1,295 +0,0 @@
|
|||||||
import { Command } from "../../hooks/use-command-history"
|
|
||||||
import { CellCoords, CellType } from "./types"
|
|
||||||
import { generateCellId } from "./utils"
|
|
||||||
|
|
||||||
export class Matrix {
|
|
||||||
private cells: ({ field: string; type: CellType } | null)[][]
|
|
||||||
|
|
||||||
constructor(rows: number, cols: number) {
|
|
||||||
this.cells = Array.from({ length: rows }, () => Array(cols).fill(null))
|
|
||||||
}
|
|
||||||
|
|
||||||
getFirstNavigableCell(): CellCoords | null {
|
|
||||||
for (let row = 0; row < this.cells.length; row++) {
|
|
||||||
for (let col = 0; col < this.cells[0].length; col++) {
|
|
||||||
if (this.cells[row][col] !== null) {
|
|
||||||
return { row, col }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register a navigable cell with a unique key
|
|
||||||
registerField(row: number, col: number, field: string, type: CellType) {
|
|
||||||
if (this._isValidPosition(row, col)) {
|
|
||||||
this.cells[row][col] = {
|
|
||||||
field,
|
|
||||||
type,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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]?.field as string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return keys
|
|
||||||
}
|
|
||||||
|
|
||||||
getCellField(cell: CellCoords): string | null {
|
|
||||||
if (this._isValidPosition(cell.row, cell.col)) {
|
|
||||||
return this.cells[cell.row][cell.col]?.field || null
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
getCellType(cell: CellCoords): CellType | null {
|
|
||||||
if (this._isValidPosition(cell.row, cell.col)) {
|
|
||||||
return this.cells[cell.row][cell.col]?.type || null
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
getIsCellSelected(
|
|
||||||
cell: CellCoords | null,
|
|
||||||
start: CellCoords | null,
|
|
||||||
end: CellCoords | null
|
|
||||||
): boolean {
|
|
||||||
if (!cell || !start || !end) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (start.col !== end.col) {
|
|
||||||
throw new Error("Selection must be in the same column")
|
|
||||||
}
|
|
||||||
|
|
||||||
const startRow = Math.min(start.row, end.row)
|
|
||||||
const endRow = Math.max(start.row, end.row)
|
|
||||||
const col = start.col
|
|
||||||
|
|
||||||
return cell.col === col && cell.row >= startRow && cell.row <= endRow
|
|
||||||
}
|
|
||||||
|
|
||||||
getValidMovement(
|
|
||||||
row: number,
|
|
||||||
col: number,
|
|
||||||
direction: string,
|
|
||||||
metaKey: boolean = false
|
|
||||||
): CellCoords {
|
|
||||||
const [dRow, dCol] = this._getDirectionDeltas(direction)
|
|
||||||
|
|
||||||
if (metaKey) {
|
|
||||||
return this._getLastValidCellInDirection(row, col, dRow, dCol)
|
|
||||||
} else {
|
|
||||||
let newRow = row + dRow
|
|
||||||
let newCol = col + dCol
|
|
||||||
|
|
||||||
if (
|
|
||||||
newRow < 0 ||
|
|
||||||
newRow >= this.cells.length ||
|
|
||||||
newCol < 0 ||
|
|
||||||
newCol >= this.cells[0].length
|
|
||||||
) {
|
|
||||||
return { row, col }
|
|
||||||
}
|
|
||||||
|
|
||||||
while (
|
|
||||||
this._isValidPosition(newRow, newCol) &&
|
|
||||||
this.cells[newRow][newCol] === null
|
|
||||||
) {
|
|
||||||
newRow += dRow
|
|
||||||
newCol += dCol
|
|
||||||
|
|
||||||
if (
|
|
||||||
newRow < 0 ||
|
|
||||||
newRow >= this.cells.length ||
|
|
||||||
newCol < 0 ||
|
|
||||||
newCol >= this.cells[0].length
|
|
||||||
) {
|
|
||||||
return { row, col }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._isValidPosition(newRow, newCol)
|
|
||||||
? { row: newRow, col: newCol }
|
|
||||||
: { row, col }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _isValidPosition(row: number, col: number): boolean {
|
|
||||||
return (
|
|
||||||
row >= 0 &&
|
|
||||||
row < this.cells.length &&
|
|
||||||
col >= 0 &&
|
|
||||||
col < this.cells[0].length
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getDirectionDeltas(direction: string): [number, number] {
|
|
||||||
switch (direction) {
|
|
||||||
case "ArrowUp":
|
|
||||||
return [-1, 0]
|
|
||||||
case "ArrowDown":
|
|
||||||
return [1, 0]
|
|
||||||
case "ArrowLeft":
|
|
||||||
return [0, -1]
|
|
||||||
case "ArrowRight":
|
|
||||||
return [0, 1]
|
|
||||||
default:
|
|
||||||
return [0, 0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getLastValidCellInDirection(
|
|
||||||
row: number,
|
|
||||||
col: number,
|
|
||||||
dRow: number,
|
|
||||||
dCol: number
|
|
||||||
): CellCoords {
|
|
||||||
let newRow = row
|
|
||||||
let newCol = col
|
|
||||||
let lastValidRow = row
|
|
||||||
let lastValidCol = col
|
|
||||||
|
|
||||||
while (this._isValidPosition(newRow + dRow, newCol + dCol)) {
|
|
||||||
newRow += dRow
|
|
||||||
newCol += dCol
|
|
||||||
if (this.cells[newRow][newCol] !== null) {
|
|
||||||
lastValidRow = newRow
|
|
||||||
lastValidCol = newCol
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
row: lastValidRow,
|
|
||||||
col: lastValidCol,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GridQueryTool {
|
|
||||||
private container: HTMLElement | null
|
|
||||||
|
|
||||||
constructor(container: HTMLElement | null) {
|
|
||||||
this.container = container
|
|
||||||
}
|
|
||||||
|
|
||||||
getInput(cell: CellCoords) {
|
|
||||||
const id = this._getCellId(cell)
|
|
||||||
|
|
||||||
const input = this.container?.querySelector(`[data-cell-id="${id}"]`)
|
|
||||||
|
|
||||||
if (!input) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return input as HTMLElement
|
|
||||||
}
|
|
||||||
|
|
||||||
getContainer(cell: CellCoords) {
|
|
||||||
const id = this._getCellId(cell)
|
|
||||||
|
|
||||||
const container = this.container?.querySelector(
|
|
||||||
`[data-container-id="${id}"]`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!container) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return container as HTMLElement
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getCellId(cell: CellCoords): string {
|
|
||||||
return generateCellId(cell)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BulkUpdateCommandArgs = {
|
|
||||||
fields: string[]
|
|
||||||
next: any[]
|
|
||||||
prev: any[]
|
|
||||||
setter: (fields: string[], values: any[]) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export class BulkUpdateCommand implements Command {
|
|
||||||
private _fields: string[]
|
|
||||||
|
|
||||||
private _prev: any[]
|
|
||||||
private _next: any[]
|
|
||||||
|
|
||||||
private _setter: (fields: string[], any: string[]) => void
|
|
||||||
|
|
||||||
constructor({ fields, prev, next, setter }: BulkUpdateCommandArgs) {
|
|
||||||
this._fields = fields
|
|
||||||
this._prev = prev
|
|
||||||
this._next = next
|
|
||||||
this._setter = setter
|
|
||||||
}
|
|
||||||
|
|
||||||
execute(): void {
|
|
||||||
this._setter(this._fields, this._next)
|
|
||||||
}
|
|
||||||
undo(): void {
|
|
||||||
this._setter(this._fields, this._prev)
|
|
||||||
}
|
|
||||||
redo(): void {
|
|
||||||
this.execute()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type UpdateCommandArgs = {
|
|
||||||
prev: any
|
|
||||||
next: any
|
|
||||||
setter: (value: any) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UpdateCommand implements Command {
|
|
||||||
private _prev: any
|
|
||||||
private _next: any
|
|
||||||
|
|
||||||
private _setter: (value: any) => void
|
|
||||||
|
|
||||||
constructor({ prev, next, setter }: UpdateCommandArgs) {
|
|
||||||
this._prev = prev
|
|
||||||
this._next = next
|
|
||||||
|
|
||||||
this._setter = setter
|
|
||||||
}
|
|
||||||
|
|
||||||
execute(): void {
|
|
||||||
this._setter(this._next)
|
|
||||||
}
|
|
||||||
|
|
||||||
undo(): void {
|
|
||||||
this._setter(this._prev)
|
|
||||||
}
|
|
||||||
|
|
||||||
redo(): void {
|
|
||||||
this.execute()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
import { Command } from "../../../hooks/use-command-history"
|
||||||
|
|
||||||
|
export type DataGridBulkUpdateCommandArgs = {
|
||||||
|
fields: string[]
|
||||||
|
next: any[]
|
||||||
|
prev: any[]
|
||||||
|
setter: (fields: string[], values: any[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DataGridBulkUpdateCommand implements Command {
|
||||||
|
private _fields: string[]
|
||||||
|
|
||||||
|
private _prev: any[]
|
||||||
|
private _next: any[]
|
||||||
|
|
||||||
|
private _setter: (fields: string[], any: string[]) => void
|
||||||
|
|
||||||
|
constructor({ fields, prev, next, setter }: DataGridBulkUpdateCommandArgs) {
|
||||||
|
this._fields = fields
|
||||||
|
this._prev = prev
|
||||||
|
this._next = next
|
||||||
|
this._setter = setter
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(): void {
|
||||||
|
this._setter(this._fields, this._next)
|
||||||
|
}
|
||||||
|
undo(): void {
|
||||||
|
this._setter(this._fields, this._prev)
|
||||||
|
}
|
||||||
|
redo(): void {
|
||||||
|
this.execute()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,388 @@
|
|||||||
|
import { ColumnDef, Row } from "@tanstack/react-table"
|
||||||
|
import { FieldValues } from "react-hook-form"
|
||||||
|
import { DataGridColumnType, DataGridCoordinates, Grid, GridCell, InternalColumnMeta } from "../types"
|
||||||
|
|
||||||
|
export class DataGridMatrix<TData, TFieldValues extends FieldValues> {
|
||||||
|
private cells: Grid<TFieldValues>
|
||||||
|
public rowAccessors: (string | null)[] = []
|
||||||
|
public columnAccessors: (string | null)[] = []
|
||||||
|
|
||||||
|
constructor(data: Row<TData>[], columns: ColumnDef<TData>[]) {
|
||||||
|
this.cells = this._populateCells(data, columns)
|
||||||
|
|
||||||
|
this.rowAccessors = this._computeRowAccessors()
|
||||||
|
this.columnAccessors = this._computeColumnAccessors()
|
||||||
|
}
|
||||||
|
|
||||||
|
private _computeRowAccessors(): (string | null)[] {
|
||||||
|
return this.cells.map((_, rowIndex) => this.getRowAccessor(rowIndex))
|
||||||
|
}
|
||||||
|
|
||||||
|
private _computeColumnAccessors(): (string | null)[] {
|
||||||
|
if (this.cells.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.cells[0].map((_, colIndex) => this.getColumnAccessor(colIndex))
|
||||||
|
}
|
||||||
|
|
||||||
|
getFirstNavigableCell(): DataGridCoordinates | null {
|
||||||
|
for (let row = 0; row < this.cells.length; row++) {
|
||||||
|
for (let col = 0; col < this.cells[0].length; col++) {
|
||||||
|
if (this.cells[row][col] !== null) {
|
||||||
|
return { row, col }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
getFieldsInRow(row: number): string[] {
|
||||||
|
const keys: string[] = []
|
||||||
|
|
||||||
|
if (row < 0 || row >= this.cells.length) {
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cells[row].forEach((cell) => {
|
||||||
|
if (cell !== null) {
|
||||||
|
keys.push(cell.field)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
getFieldsInSelection(
|
||||||
|
start: DataGridCoordinates | null,
|
||||||
|
end: DataGridCoordinates | 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]?.field as string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
getCellField(cell: DataGridCoordinates): string | null {
|
||||||
|
if (this._isValidPosition(cell.row, cell.col)) {
|
||||||
|
return this.cells[cell.row][cell.col]?.field || null
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
getCellType(cell: DataGridCoordinates): DataGridColumnType | null {
|
||||||
|
if (this._isValidPosition(cell.row, cell.col)) {
|
||||||
|
return this.cells[cell.row][cell.col]?.type || null
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
getIsCellSelected(
|
||||||
|
cell: DataGridCoordinates | null,
|
||||||
|
start: DataGridCoordinates | null,
|
||||||
|
end: DataGridCoordinates | null
|
||||||
|
): boolean {
|
||||||
|
if (!cell || !start || !end) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start.col !== end.col) {
|
||||||
|
throw new Error("Selection must be in the same column")
|
||||||
|
}
|
||||||
|
|
||||||
|
const startRow = Math.min(start.row, end.row)
|
||||||
|
const endRow = Math.max(start.row, end.row)
|
||||||
|
const col = start.col
|
||||||
|
|
||||||
|
return cell.col === col && cell.row >= startRow && cell.row <= endRow
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleColumn(col: number, enabled: boolean) {
|
||||||
|
if (col < 0 || col >= this.cells[0].length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cells.forEach((row, index) => {
|
||||||
|
const cell = row[col]
|
||||||
|
|
||||||
|
if (cell) {
|
||||||
|
this.cells[index][col] = {
|
||||||
|
...cell,
|
||||||
|
enabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleRow(row: number, enabled: boolean) {
|
||||||
|
if (row < 0 || row >= this.cells.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cells[row].forEach((cell, index) => {
|
||||||
|
if (cell) {
|
||||||
|
this.cells[row][index] = {
|
||||||
|
...cell,
|
||||||
|
enabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getCoordinatesByField(field: string): DataGridCoordinates | null {
|
||||||
|
if (this.rowAccessors.length === 1) {
|
||||||
|
const col = this.columnAccessors.indexOf(field)
|
||||||
|
|
||||||
|
if (col === -1) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return { row: 0, col }
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let row = 0; row < this.rowAccessors.length; row++) {
|
||||||
|
const rowAccessor = this.rowAccessors[row]
|
||||||
|
|
||||||
|
if (rowAccessor === null) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!field.startsWith(rowAccessor)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let column = 0; column < this.columnAccessors.length; column++) {
|
||||||
|
const columnAccessor = this.columnAccessors[column]
|
||||||
|
|
||||||
|
if (columnAccessor === null) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullFieldPath = `${rowAccessor}.${columnAccessor}`
|
||||||
|
|
||||||
|
if (fullFieldPath === field) {
|
||||||
|
return { row, col: column }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
getRowAccessor(row: number): string | null {
|
||||||
|
if (row < 0 || row >= this.cells.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const cells = this.cells[row]
|
||||||
|
|
||||||
|
const nonNullFields = cells
|
||||||
|
.filter((cell): cell is GridCell<TFieldValues> => cell !== null)
|
||||||
|
.map((cell) => cell.field.split("."))
|
||||||
|
|
||||||
|
if (nonNullFields.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
let commonParts = nonNullFields[0]
|
||||||
|
|
||||||
|
for (const segments of nonNullFields) {
|
||||||
|
commonParts = commonParts.filter(
|
||||||
|
(part, index) => segments[index] === part
|
||||||
|
)
|
||||||
|
|
||||||
|
if (commonParts.length === 0) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessor = commonParts.join(".")
|
||||||
|
|
||||||
|
if (!accessor) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessor
|
||||||
|
}
|
||||||
|
|
||||||
|
public getColumnAccessor(column: number): string | null {
|
||||||
|
if (column < 0 || column >= this.cells[0].length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the unique part of the field name for each row in the specified column
|
||||||
|
const uniqueParts = this.cells
|
||||||
|
.map((row, rowIndex) => {
|
||||||
|
const cell = row[column]
|
||||||
|
if (!cell) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the row accessor for the current row
|
||||||
|
const rowAccessor = this.getRowAccessor(rowIndex)
|
||||||
|
|
||||||
|
// Remove the row accessor part from the field name
|
||||||
|
if (rowAccessor && cell.field.startsWith(rowAccessor + ".")) {
|
||||||
|
return cell.field.slice(rowAccessor.length + 1) // Extract the part after the row accessor
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
.filter((part) => part !== null) // Filter out null values
|
||||||
|
|
||||||
|
if (uniqueParts.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure all unique parts are the same (this should be true for well-formed data)
|
||||||
|
const firstPart = uniqueParts[0]
|
||||||
|
const isConsistent = uniqueParts.every((part) => part === firstPart)
|
||||||
|
|
||||||
|
return isConsistent ? firstPart : null
|
||||||
|
}
|
||||||
|
|
||||||
|
getValidMovement(
|
||||||
|
row: number,
|
||||||
|
col: number,
|
||||||
|
direction: string,
|
||||||
|
metaKey: boolean = false
|
||||||
|
): DataGridCoordinates {
|
||||||
|
const [dRow, dCol] = this._getDirectionDeltas(direction)
|
||||||
|
|
||||||
|
if (metaKey) {
|
||||||
|
return this._getLastValidCellInDirection(row, col, dRow, dCol)
|
||||||
|
} else {
|
||||||
|
let newRow = row + dRow
|
||||||
|
let newCol = col + dCol
|
||||||
|
|
||||||
|
while (this._isValidPosition(newRow, newCol)) {
|
||||||
|
if (
|
||||||
|
this.cells[newRow][newCol] !== null &&
|
||||||
|
this.cells[newRow][newCol]?.enabled !== false
|
||||||
|
) {
|
||||||
|
return { row: newRow, col: newCol }
|
||||||
|
}
|
||||||
|
newRow += dRow
|
||||||
|
newCol += dCol
|
||||||
|
}
|
||||||
|
|
||||||
|
return { row, col }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _isValidPosition(
|
||||||
|
row: number,
|
||||||
|
col: number,
|
||||||
|
cells?: Grid<TFieldValues>
|
||||||
|
): boolean {
|
||||||
|
if (!cells) {
|
||||||
|
cells = this.cells
|
||||||
|
}
|
||||||
|
|
||||||
|
return row >= 0 && row < cells.length && col >= 0 && col < 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
|
||||||
|
): DataGridCoordinates {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _populateCells(rows: Row<TData>[], columns: ColumnDef<TData>[]) {
|
||||||
|
const cells = Array.from({ length: rows.length }, () =>
|
||||||
|
Array(columns.length).fill(null)
|
||||||
|
) as Grid<TFieldValues>
|
||||||
|
|
||||||
|
rows.forEach((row, rowIndex) => {
|
||||||
|
columns.forEach((column, colIndex) => {
|
||||||
|
if (!this._isValidPosition(rowIndex, colIndex, cells)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
name: _,
|
||||||
|
field,
|
||||||
|
type,
|
||||||
|
...rest
|
||||||
|
} = column.meta as InternalColumnMeta<TData, TFieldValues>
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
row,
|
||||||
|
column: {
|
||||||
|
...column,
|
||||||
|
meta: rest,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldValue = field ? field(context) : null
|
||||||
|
|
||||||
|
if (!fieldValue || !type) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cells[rowIndex][colIndex] = {
|
||||||
|
field: fieldValue,
|
||||||
|
type,
|
||||||
|
enabled: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return cells
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { DataGridCoordinates } from "../types"
|
||||||
|
import { generateCellId } from "../utils"
|
||||||
|
|
||||||
|
export class DataGridQueryTool {
|
||||||
|
private container: HTMLElement | null
|
||||||
|
|
||||||
|
constructor(container: HTMLElement | null) {
|
||||||
|
this.container = container
|
||||||
|
}
|
||||||
|
|
||||||
|
getInput(cell: DataGridCoordinates) {
|
||||||
|
const id = this._getCellId(cell)
|
||||||
|
|
||||||
|
const input = this.container?.querySelector(`[data-cell-id="${id}"]`)
|
||||||
|
|
||||||
|
if (!input) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return input as HTMLElement
|
||||||
|
}
|
||||||
|
|
||||||
|
getInputByField(field: string) {
|
||||||
|
const input = this.container?.querySelector(`[data-field="${field}"]`)
|
||||||
|
|
||||||
|
if (!input) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return input as HTMLElement
|
||||||
|
}
|
||||||
|
|
||||||
|
getCoordinatesByField(field: string): DataGridCoordinates | null {
|
||||||
|
const cell = this.container?.querySelector(
|
||||||
|
`[data-field="${field}"][data-cell-id]`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!cell) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const cellId = cell.getAttribute("data-cell-id")
|
||||||
|
|
||||||
|
if (!cellId) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const [row, col] = cellId.split(":").map((n) => parseInt(n, 10))
|
||||||
|
|
||||||
|
if (isNaN(row) || isNaN(col)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return { row, col }
|
||||||
|
}
|
||||||
|
|
||||||
|
getContainer(cell: DataGridCoordinates) {
|
||||||
|
const id = this._getCellId(cell)
|
||||||
|
|
||||||
|
const container = this.container?.querySelector(
|
||||||
|
`[data-container-id="${id}"]`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!container) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return container as HTMLElement
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getCellId(cell: DataGridCoordinates): string {
|
||||||
|
return generateCellId(cell)
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
import { Command } from "../../../hooks/use-command-history"
|
||||||
|
|
||||||
|
export type DataGridUpdateCommandArgs = {
|
||||||
|
prev: any
|
||||||
|
next: any
|
||||||
|
setter: (value: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DataGridUpdateCommand implements Command {
|
||||||
|
private _prev: any
|
||||||
|
private _next: any
|
||||||
|
|
||||||
|
private _setter: (value: any) => void
|
||||||
|
|
||||||
|
constructor({ prev, next, setter }: DataGridUpdateCommandArgs) {
|
||||||
|
this._prev = prev
|
||||||
|
this._next = next
|
||||||
|
|
||||||
|
this._setter = setter
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(): void {
|
||||||
|
this._setter(this._next)
|
||||||
|
}
|
||||||
|
|
||||||
|
undo(): void {
|
||||||
|
this._setter(this._prev)
|
||||||
|
}
|
||||||
|
|
||||||
|
redo(): void {
|
||||||
|
this.execute()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export * from "./data-grid-bulk-update-command"
|
||||||
|
export * from "./data-grid-matrix"
|
||||||
|
export * from "./data-grid-query-tool"
|
||||||
|
export * from "./data-grid-update-command"
|
||||||
|
|
||||||
@@ -1,21 +1,27 @@
|
|||||||
import { CellContext } from "@tanstack/react-table"
|
import {
|
||||||
|
CellContext,
|
||||||
|
ColumnDef,
|
||||||
|
ColumnMeta,
|
||||||
|
Row,
|
||||||
|
VisibilityState,
|
||||||
|
} from "@tanstack/react-table"
|
||||||
import React, { PropsWithChildren, ReactNode, RefObject } from "react"
|
import React, { PropsWithChildren, ReactNode, RefObject } from "react"
|
||||||
import { FieldValues, Path, PathValue } from "react-hook-form"
|
import {
|
||||||
|
FieldErrors,
|
||||||
|
FieldPath,
|
||||||
|
FieldValues,
|
||||||
|
Path,
|
||||||
|
PathValue,
|
||||||
|
} from "react-hook-form"
|
||||||
|
|
||||||
export type CellType = "text" | "number" | "select" | "boolean"
|
export type DataGridColumnType = "text" | "number" | "boolean"
|
||||||
|
|
||||||
export type CellCoords = {
|
export type DataGridCoordinates = {
|
||||||
row: number
|
row: number
|
||||||
col: number
|
col: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetCellHandlerProps = {
|
|
||||||
coords: CellCoords
|
|
||||||
readonly: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DataGridCellProps<TData = unknown, TValue = any> {
|
export interface DataGridCellProps<TData = unknown, TValue = any> {
|
||||||
field: string
|
|
||||||
context: CellContext<TData, TValue>
|
context: CellContext<TData, TValue>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,11 +37,28 @@ export interface DataGridCellContext<TData = unknown, TValue = any>
|
|||||||
rowIndex: number
|
rowIndex: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DataGridRowError = {
|
||||||
|
message: string
|
||||||
|
to: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DataGridErrorRenderProps<TFieldValues extends FieldValues> = {
|
||||||
|
errors: FieldErrors<TFieldValues>
|
||||||
|
rowErrors: DataGridRowError[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface DataGridCellRenderProps {
|
export interface DataGridCellRenderProps {
|
||||||
container: DataGridCellContainerProps
|
container: DataGridCellContainerProps
|
||||||
input: InputProps
|
input: InputProps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InputAttributes = {
|
||||||
|
"data-row": number
|
||||||
|
"data-col": number
|
||||||
|
"data-cell-id": string
|
||||||
|
"data-field": string
|
||||||
|
}
|
||||||
|
|
||||||
export interface InputProps {
|
export interface InputProps {
|
||||||
ref: RefObject<HTMLElement>
|
ref: RefObject<HTMLElement>
|
||||||
onBlur: () => void
|
onBlur: () => void
|
||||||
@@ -47,6 +70,10 @@ export interface InputProps {
|
|||||||
"data-field": string
|
"data-field": string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InnerAttributes = {
|
||||||
|
"data-container-id": string
|
||||||
|
}
|
||||||
|
|
||||||
interface InnerProps {
|
interface InnerProps {
|
||||||
ref: RefObject<HTMLDivElement>
|
ref: RefObject<HTMLDivElement>
|
||||||
onMouseOver: ((e: React.MouseEvent<HTMLElement>) => void) | undefined
|
onMouseOver: ((e: React.MouseEvent<HTMLElement>) => void) | undefined
|
||||||
@@ -61,6 +88,7 @@ interface OverlayProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface DataGridCellContainerProps extends PropsWithChildren<{}> {
|
export interface DataGridCellContainerProps extends PropsWithChildren<{}> {
|
||||||
|
field: string
|
||||||
innerProps: InnerProps
|
innerProps: InnerProps
|
||||||
overlayProps: OverlayProps
|
overlayProps: OverlayProps
|
||||||
isAnchor: boolean
|
isAnchor: boolean
|
||||||
@@ -70,9 +98,64 @@ export interface DataGridCellContainerProps extends PropsWithChildren<{}> {
|
|||||||
showOverlay: boolean
|
showOverlay: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DataGridColumnType = "string" | "number" | "boolean"
|
export type DataGridCellSnapshot<
|
||||||
|
TFieldValues extends FieldValues = FieldValues
|
||||||
export type CellSnapshot<TFieldValues extends FieldValues = FieldValues> = {
|
> = {
|
||||||
field: string
|
field: string
|
||||||
value: PathValue<TFieldValues, Path<TFieldValues>>
|
value: PathValue<TFieldValues, Path<TFieldValues>>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FieldContext<TData> = {
|
||||||
|
row: Row<TData>
|
||||||
|
column: ColumnDef<TData>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FieldFunction<TData, TFieldValues extends FieldValues> = (
|
||||||
|
context: FieldContext<TData>
|
||||||
|
) => FieldPath<TFieldValues> | null
|
||||||
|
|
||||||
|
export type InternalColumnMeta<TData, TFieldValues extends FieldValues> = {
|
||||||
|
name: string
|
||||||
|
field?: FieldFunction<TData, TFieldValues>
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
field: FieldFunction<TData, TFieldValues>
|
||||||
|
type: DataGridColumnType
|
||||||
|
}
|
||||||
|
| { field?: null | undefined; type?: never }
|
||||||
|
) &
|
||||||
|
ColumnMeta<TData, any>
|
||||||
|
|
||||||
|
export type GridCell<TFieldValues extends FieldValues> = {
|
||||||
|
field: FieldPath<TFieldValues>
|
||||||
|
type: DataGridColumnType
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Grid<TFieldValues extends FieldValues> =
|
||||||
|
(GridCell<TFieldValues> | null)[][]
|
||||||
|
|
||||||
|
export type CellMetadata = {
|
||||||
|
id: string
|
||||||
|
field: string
|
||||||
|
type: DataGridColumnType
|
||||||
|
inputAttributes: InputAttributes
|
||||||
|
innerAttributes: InnerAttributes
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CellErrorMetadata = {
|
||||||
|
field: string | null
|
||||||
|
accessor: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VisibilitySnapshot = {
|
||||||
|
rows: VisibilityState
|
||||||
|
columns: VisibilityState
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GridColumnOption = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
checked: boolean
|
||||||
|
disabled: boolean
|
||||||
|
}
|
||||||
@@ -1,229 +1,22 @@
|
|||||||
import {
|
import { DataGridCoordinates } from "./types"
|
||||||
CellContext,
|
|
||||||
Column,
|
|
||||||
ColumnDefTemplate,
|
|
||||||
HeaderContext,
|
|
||||||
createColumnHelper,
|
|
||||||
} from "@tanstack/react-table"
|
|
||||||
import { CellCoords, CellType, DataGridColumnType } from "./types"
|
|
||||||
|
|
||||||
export function generateCellId(coords: CellCoords) {
|
export function generateCellId(coords: DataGridCoordinates) {
|
||||||
return `${coords.row}:${coords.col}`
|
return `${coords.row}:${coords.col}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseCellId(cellId: string): CellCoords {
|
|
||||||
const [row, col] = cellId.split(":").map(Number)
|
|
||||||
|
|
||||||
if (isNaN(row) || isNaN(col)) {
|
|
||||||
throw new Error(`Invalid cell id: ${cellId}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return { row, col }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a cell is equal to a set of coords
|
* Check if a cell is equal to a set of coords
|
||||||
* @param cell - The cell to compare
|
* @param cell - The cell to compare
|
||||||
* @param coords - The coords to compare
|
* @param coords - The coords to compare
|
||||||
* @returns Whether the cell is equal to the coords
|
* @returns Whether the cell is equal to the coords
|
||||||
*/
|
*/
|
||||||
export function isCellMatch(cell: CellCoords, coords?: CellCoords | null) {
|
export function isCellMatch(
|
||||||
|
cell: DataGridCoordinates,
|
||||||
|
coords?: DataGridCoordinates | null
|
||||||
|
) {
|
||||||
if (!coords) {
|
if (!coords) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return cell.row === coords.row && cell.col === coords.col
|
return cell.row === coords.row && cell.col === coords.col
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the range of cells between two points.
|
|
||||||
* @param start - The start point
|
|
||||||
* @param end - The end point
|
|
||||||
* @returns A map of cell keys for the range
|
|
||||||
*/
|
|
||||||
export const getRange = (
|
|
||||||
start: CellCoords,
|
|
||||||
end: CellCoords
|
|
||||||
): Record<string, boolean> => {
|
|
||||||
const range: Record<string, boolean> = {}
|
|
||||||
|
|
||||||
const minX = Math.min(start.col, end.col)
|
|
||||||
const maxX = Math.max(start.col, end.col)
|
|
||||||
|
|
||||||
const minY = Math.min(start.row, end.row)
|
|
||||||
const maxY = Math.max(start.row, end.row)
|
|
||||||
|
|
||||||
for (let x = minX; x <= maxX; x++) {
|
|
||||||
for (let y = minY; y <= maxY; y++) {
|
|
||||||
range[
|
|
||||||
generateCellId({
|
|
||||||
row: y,
|
|
||||||
col: x,
|
|
||||||
})
|
|
||||||
] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return range
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getFieldsInRange(
|
|
||||||
range: Record<string, boolean>,
|
|
||||||
container: HTMLElement | null
|
|
||||||
): (string | null)[] {
|
|
||||||
container = container || document.body
|
|
||||||
|
|
||||||
if (!container) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const ids = Object.keys(range)
|
|
||||||
|
|
||||||
if (!ids.length) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const fields = ids.map((id) => {
|
|
||||||
const cell = container.querySelector(`[data-cell-id="${id}"][data-field]`)
|
|
||||||
|
|
||||||
if (!cell) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return cell.getAttribute("data-field")
|
|
||||||
})
|
|
||||||
|
|
||||||
return fields
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertToNumber(value: string | number): number {
|
|
||||||
if (typeof value === "number") {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
const converted = Number(value)
|
|
||||||
|
|
||||||
if (isNaN(converted)) {
|
|
||||||
throw new Error(`String "${value}" cannot be converted to number.`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return converted
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertToBoolean(value: string | boolean): boolean {
|
|
||||||
if (typeof value === "boolean") {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === "undefined" || value === null) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const lowerValue = value.toLowerCase()
|
|
||||||
|
|
||||||
if (lowerValue === "true" || lowerValue === "false") {
|
|
||||||
return lowerValue === "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`String "${value}" cannot be converted to boolean.`)
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertToString(value: string): string {
|
|
||||||
return String(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function convertArrayToPrimitive(values: any[], type: CellType): any[] {
|
|
||||||
switch (type) {
|
|
||||||
case "number":
|
|
||||||
return values.map(convertToNumber)
|
|
||||||
case "boolean":
|
|
||||||
return values.map(convertToBoolean)
|
|
||||||
case "text":
|
|
||||||
case "select":
|
|
||||||
return values.map(convertToString)
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported target type "${type}".`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type DataGridHelperColumnsProps<TData> = {
|
|
||||||
/**
|
|
||||||
* The id of the column.
|
|
||||||
*/
|
|
||||||
id: string
|
|
||||||
/**
|
|
||||||
* The name of the column, shown in the column visibility menu.
|
|
||||||
*/
|
|
||||||
name?: string
|
|
||||||
/**
|
|
||||||
* The header template for the column.
|
|
||||||
*/
|
|
||||||
header: ColumnDefTemplate<HeaderContext<TData, unknown>> | undefined
|
|
||||||
/**
|
|
||||||
* The cell template for the column.
|
|
||||||
*/
|
|
||||||
cell: ColumnDefTemplate<CellContext<TData, unknown>> | undefined
|
|
||||||
/**
|
|
||||||
* Whether the column cannot be hidden by the user.
|
|
||||||
*
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
disableHiding?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createDataGridHelper<TData>() {
|
|
||||||
const columnHelper = createColumnHelper<TData>()
|
|
||||||
|
|
||||||
return {
|
|
||||||
column: ({
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
header,
|
|
||||||
cell,
|
|
||||||
disableHiding = false,
|
|
||||||
}: DataGridHelperColumnsProps<TData>) =>
|
|
||||||
columnHelper.display({
|
|
||||||
id,
|
|
||||||
header,
|
|
||||||
cell,
|
|
||||||
enableHiding: !disableHiding,
|
|
||||||
meta: {
|
|
||||||
name,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getColumnName(column: Column<any, any>): string {
|
|
||||||
const id = column.columnDef.id
|
|
||||||
const meta = column?.columnDef.meta as { name?: string } | undefined
|
|
||||||
|
|
||||||
if (!id) {
|
|
||||||
throw new Error(
|
|
||||||
"Column is missing an id, which is a required field. Please provide an id for the column."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "development" && !meta?.name) {
|
|
||||||
console.warn(
|
|
||||||
`Column "${id}" does not have a name. You should add a name to the column definition. Falling back to the column id.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return meta?.name || id
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getColumnType(
|
|
||||||
cell: CellCoords,
|
|
||||||
columns: Column<any, any>[]
|
|
||||||
): DataGridColumnType {
|
|
||||||
const { col } = cell
|
|
||||||
|
|
||||||
const column = columns[col]
|
|
||||||
|
|
||||||
const meta = column?.columnDef.meta as
|
|
||||||
| { type?: DataGridColumnType }
|
|
||||||
| undefined
|
|
||||||
|
|
||||||
return meta?.type || "string"
|
|
||||||
}
|
|
||||||
@@ -3,9 +3,9 @@ import * as Dialog from "@radix-ui/react-dialog"
|
|||||||
import { SidebarLeft, TriangleRightMini, XMark } from "@medusajs/icons"
|
import { SidebarLeft, TriangleRightMini, XMark } from "@medusajs/icons"
|
||||||
import { IconButton, clx } from "@medusajs/ui"
|
import { IconButton, clx } from "@medusajs/ui"
|
||||||
import { PropsWithChildren } from "react"
|
import { PropsWithChildren } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
import { Link, Outlet, UIMatch, useMatches } from "react-router-dom"
|
import { Link, Outlet, UIMatch, useMatches } from "react-router-dom"
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { KeybindProvider } from "../../../providers/keybind-provider"
|
import { KeybindProvider } from "../../../providers/keybind-provider"
|
||||||
import { useGlobalShortcuts } from "../../../providers/keybind-provider/hooks"
|
import { useGlobalShortcuts } from "../../../providers/keybind-provider/hooks"
|
||||||
import { useSidebar } from "../../../providers/sidebar-provider"
|
import { useSidebar } from "../../../providers/sidebar-provider"
|
||||||
|
|||||||
@@ -218,14 +218,37 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dataGrid": {
|
"dataGrid": {
|
||||||
"editColumns": "Edit columns",
|
"columns": {
|
||||||
|
"view": "View",
|
||||||
|
"resetToDefault": "Reset to default",
|
||||||
|
"disabled": "Changing which columns are visible is disabled."
|
||||||
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"label": "Shortcuts",
|
"label": "Shortcuts",
|
||||||
"commands": {
|
"commands": {
|
||||||
"undo": "Undo",
|
"undo": "Undo",
|
||||||
"redo": "Redo",
|
"redo": "Redo",
|
||||||
"edit": "Edit the current cell"
|
"copy": "Copy",
|
||||||
|
"paste": "Paste",
|
||||||
|
"edit": "Edit",
|
||||||
|
"delete": "Delete",
|
||||||
|
"clear": "Clear",
|
||||||
|
"moveUp": "Move up",
|
||||||
|
"moveDown": "Move down",
|
||||||
|
"moveLeft": "Move left",
|
||||||
|
"moveRight": "Move right",
|
||||||
|
"moveTop": "Move to top",
|
||||||
|
"moveBottom": "Move to bottom",
|
||||||
|
"selectDown": "Select down",
|
||||||
|
"selectUp": "Select up",
|
||||||
|
"selectColumnDown": "Select column down",
|
||||||
|
"selectColumnUp": "Select column up"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"fixError": "Fix error",
|
||||||
|
"count_one": "{{count}} error",
|
||||||
|
"count_other": "{{count}} errors"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
@@ -322,7 +345,8 @@
|
|||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"variants": "Please select at least one variant.",
|
"variants": "Please select at least one variant.",
|
||||||
"options": "Please create at least one option."
|
"options": "Please create at least one option.",
|
||||||
|
"uniqueSku": "SKU must be unique."
|
||||||
},
|
},
|
||||||
"inventory": {
|
"inventory": {
|
||||||
"heading": "Inventory kits",
|
"heading": "Inventory kits",
|
||||||
@@ -672,13 +696,14 @@
|
|||||||
"deleteWarning": "You are about to delete an inventory item. This action cannot be undone.",
|
"deleteWarning": "You are about to delete an inventory item. This action cannot be undone.",
|
||||||
"editItemDetails": "Edit item details",
|
"editItemDetails": "Edit item details",
|
||||||
"create": {
|
"create": {
|
||||||
"title": "Add inventory item",
|
"title": "Create Inventory Item",
|
||||||
"details": "Details",
|
"details": "Details",
|
||||||
"availability": "Availability",
|
"availability": "Availability",
|
||||||
"locations": "Locations",
|
"locations": "Locations",
|
||||||
"attributes": "Attributes",
|
"attributes": "Attributes",
|
||||||
"requiresShipping": "Requires shipping",
|
"requiresShipping": "Requires shipping",
|
||||||
"requiresShippingHint": "Does the inventory item require shipping?"
|
"requiresShippingHint": "Does the inventory item require shipping?",
|
||||||
|
"successToast": "Inventory item was successfully created."
|
||||||
},
|
},
|
||||||
"reservation": {
|
"reservation": {
|
||||||
"header": "Reservation of {{itemName}}",
|
"header": "Reservation of {{itemName}}",
|
||||||
@@ -2531,7 +2556,6 @@
|
|||||||
"inStock": "In stock",
|
"inStock": "In stock",
|
||||||
"location": "Location",
|
"location": "Location",
|
||||||
"quantity": "Quantity",
|
"quantity": "Quantity",
|
||||||
"qty": "Qty",
|
|
||||||
"variant": "Variant",
|
"variant": "Variant",
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
"parent": "Parent",
|
"parent": "Parent",
|
||||||
|
|||||||
@@ -30,15 +30,17 @@ export function transformNullableFormData<
|
|||||||
}, {} as K extends true ? Nullable<T> : Optional<T>)
|
}, {} as K extends true ? Nullable<T> : Optional<T>)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function transformNullableFormNumber(
|
export function transformNullableFormNumber<K extends boolean = true>(
|
||||||
value?: string | number,
|
value?: string | number,
|
||||||
nullify = true
|
nullify: K = true as K
|
||||||
) {
|
): K extends true ? number | null : number | undefined {
|
||||||
if (
|
if (
|
||||||
typeof value === "undefined" ||
|
typeof value === "undefined" ||
|
||||||
(typeof value === "string" && value.trim() === "")
|
(typeof value === "string" && value.trim() === "")
|
||||||
) {
|
) {
|
||||||
return nullify ? null : undefined
|
return (nullify ? null : undefined) as K extends true
|
||||||
|
? number | null
|
||||||
|
: number | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
|
|||||||
+14
-15
@@ -8,17 +8,17 @@ import {
|
|||||||
createDataGridHelper,
|
createDataGridHelper,
|
||||||
} from "../../../../../components/data-grid"
|
} from "../../../../../components/data-grid"
|
||||||
import { useRouteModal } from "../../../../../components/modals"
|
import { useRouteModal } from "../../../../../components/modals"
|
||||||
import { useStockLocations } from "../../../../../hooks/api/stock-locations"
|
|
||||||
import { CreateInventoryItemSchema } from "./schema"
|
import { CreateInventoryItemSchema } from "./schema"
|
||||||
|
|
||||||
type InventoryAvailabilityFormProps = {
|
type InventoryAvailabilityFormProps = {
|
||||||
form: UseFormReturn<CreateInventoryItemSchema>
|
form: UseFormReturn<CreateInventoryItemSchema>
|
||||||
|
locations: HttpTypes.AdminStockLocation[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InventoryAvailabilityForm = ({
|
export const InventoryAvailabilityForm = ({
|
||||||
form,
|
form,
|
||||||
|
locations,
|
||||||
}: InventoryAvailabilityFormProps) => {
|
}: InventoryAvailabilityFormProps) => {
|
||||||
const { isPending, stock_locations = [] } = useStockLocations({ limit: 999 })
|
|
||||||
const { setCloseOnEscape } = useRouteModal()
|
const { setCloseOnEscape } = useRouteModal()
|
||||||
|
|
||||||
const columns = useColumns()
|
const columns = useColumns()
|
||||||
@@ -26,9 +26,8 @@ export const InventoryAvailabilityForm = ({
|
|||||||
return (
|
return (
|
||||||
<div className="size-full">
|
<div className="size-full">
|
||||||
<DataGrid
|
<DataGrid
|
||||||
isLoading={isPending}
|
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={stock_locations}
|
data={locations}
|
||||||
state={form}
|
state={form}
|
||||||
onEditingChange={(editing) => setCloseOnEscape(!editing)}
|
onEditingChange={(editing) => setCloseOnEscape(!editing)}
|
||||||
/>
|
/>
|
||||||
@@ -36,7 +35,10 @@ export const InventoryAvailabilityForm = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnHelper = createDataGridHelper<HttpTypes.AdminStockLocation>()
|
const columnHelper = createDataGridHelper<
|
||||||
|
HttpTypes.AdminStockLocation,
|
||||||
|
CreateInventoryItemSchema
|
||||||
|
>()
|
||||||
|
|
||||||
const useColumns = () => {
|
const useColumns = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -50,9 +52,11 @@ const useColumns = () => {
|
|||||||
<span className="truncate">{t("locations.domain")}</span>
|
<span className="truncate">{t("locations.domain")}</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
cell: ({ row }) => {
|
cell: (context) => {
|
||||||
return (
|
return (
|
||||||
<DataGrid.ReadonlyCell>{row.original.name}</DataGrid.ReadonlyCell>
|
<DataGrid.ReadonlyCell context={context}>
|
||||||
|
{context.row.original.name}
|
||||||
|
</DataGrid.ReadonlyCell>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
disableHiding: true,
|
disableHiding: true,
|
||||||
@@ -61,15 +65,10 @@ const useColumns = () => {
|
|||||||
id: "in-stock",
|
id: "in-stock",
|
||||||
name: t("fields.inStock"),
|
name: t("fields.inStock"),
|
||||||
header: t("fields.inStock"),
|
header: t("fields.inStock"),
|
||||||
|
field: (context) => `locations.${context.row.original.id}`,
|
||||||
|
type: "number",
|
||||||
cell: (context) => {
|
cell: (context) => {
|
||||||
return (
|
return <DataGrid.NumberCell placeholder="0" context={context} />
|
||||||
<DataGrid.NumberCell
|
|
||||||
min={0}
|
|
||||||
placeholder="0"
|
|
||||||
context={context}
|
|
||||||
field={`locations.${context.row.original.id}`}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
disableHiding: true,
|
disableHiding: true,
|
||||||
}),
|
}),
|
||||||
|
|||||||
+18
-5
@@ -13,6 +13,7 @@ import { useCallback, useEffect, useState } from "react"
|
|||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import { Divider } from "../../../../../components/common/divider"
|
import { Divider } from "../../../../../components/common/divider"
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
import { SwitchBox } from "../../../../../components/common/switch-box"
|
import { SwitchBox } from "../../../../../components/common/switch-box"
|
||||||
@@ -28,6 +29,7 @@ import {
|
|||||||
import { sdk } from "../../../../../lib/client"
|
import { sdk } from "../../../../../lib/client"
|
||||||
import {
|
import {
|
||||||
transformNullableFormData,
|
transformNullableFormData,
|
||||||
|
transformNullableFormNumber,
|
||||||
transformNullableFormNumbers,
|
transformNullableFormNumbers,
|
||||||
} from "../../../../../lib/form-helpers"
|
} from "../../../../../lib/form-helpers"
|
||||||
import { queryClient } from "../../../../../lib/query-client"
|
import { queryClient } from "../../../../../lib/query-client"
|
||||||
@@ -43,7 +45,11 @@ type StepStatus = {
|
|||||||
[key in Tab]: ProgressStatus
|
[key in Tab]: ProgressStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InventoryCreateForm() {
|
type InventoryCreateFormProps = {
|
||||||
|
locations: HttpTypes.AdminStockLocation[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InventoryCreateForm({ locations }: InventoryCreateFormProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { handleSuccess } = useRouteModal()
|
const { handleSuccess } = useRouteModal()
|
||||||
const [tab, setTab] = useState<Tab>(Tab.DETAILS)
|
const [tab, setTab] = useState<Tab>(Tab.DETAILS)
|
||||||
@@ -63,6 +69,9 @@ export function InventoryCreateForm() {
|
|||||||
description: "",
|
description: "",
|
||||||
requires_shipping: true,
|
requires_shipping: true,
|
||||||
thumbnail: "",
|
thumbnail: "",
|
||||||
|
locations: Object.fromEntries(
|
||||||
|
locations.map((location) => [location.id, ""])
|
||||||
|
),
|
||||||
},
|
},
|
||||||
resolver: zodResolver(CreateInventoryItemSchema),
|
resolver: zodResolver(CreateInventoryItemSchema),
|
||||||
})
|
})
|
||||||
@@ -108,7 +117,10 @@ export function InventoryCreateForm() {
|
|||||||
.filter(([_, quantiy]) => !!quantiy)
|
.filter(([_, quantiy]) => !!quantiy)
|
||||||
.map(([location_id, stocked_quantity]) => ({
|
.map(([location_id, stocked_quantity]) => ({
|
||||||
location_id,
|
location_id,
|
||||||
stocked_quantity,
|
stocked_quantity: transformNullableFormNumber(
|
||||||
|
stocked_quantity,
|
||||||
|
false
|
||||||
|
),
|
||||||
})),
|
})),
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
@@ -124,6 +136,7 @@ export function InventoryCreateForm() {
|
|||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
handleSuccess()
|
handleSuccess()
|
||||||
|
toast.success(t("inventory.create.successToast"))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -220,13 +233,13 @@ export function InventoryCreateForm() {
|
|||||||
|
|
||||||
<RouteFocusModal.Body
|
<RouteFocusModal.Body
|
||||||
className={clx(
|
className={clx(
|
||||||
"flex h-full w-full flex-col items-center divide-y overflow-hidden px-3",
|
"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
|
<ProgressTabs.Content
|
||||||
value={Tab.DETAILS}
|
value={Tab.DETAILS}
|
||||||
className="h-full w-full overflow-auto"
|
className="h-full w-full overflow-auto px-3"
|
||||||
>
|
>
|
||||||
<div className="mx-auto flex w-full max-w-[720px] flex-col gap-y-8 px-px py-16">
|
<div className="mx-auto flex w-full max-w-[720px] flex-col gap-y-8 px-px py-16">
|
||||||
<div className="flex flex-col gap-y-8">
|
<div className="flex flex-col gap-y-8">
|
||||||
@@ -470,7 +483,7 @@ export function InventoryCreateForm() {
|
|||||||
value={Tab.AVAILABILITY}
|
value={Tab.AVAILABILITY}
|
||||||
className="size-full"
|
className="size-full"
|
||||||
>
|
>
|
||||||
<InventoryAvailabilityForm form={form} />
|
<InventoryAvailabilityForm form={form} locations={locations} />
|
||||||
</ProgressTabs.Content>
|
</ProgressTabs.Content>
|
||||||
</RouteFocusModal.Body>
|
</RouteFocusModal.Body>
|
||||||
<RouteFocusModal.Footer>
|
<RouteFocusModal.Footer>
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ export const CreateInventoryItemSchema = z.object({
|
|||||||
material: z.string().optional(),
|
material: z.string().optional(),
|
||||||
requires_shipping: z.boolean().optional(),
|
requires_shipping: z.boolean().optional(),
|
||||||
thumbnail: z.string().optional(),
|
thumbnail: z.string().optional(),
|
||||||
locations: z.record(z.string(), z.number().optional()).optional(),
|
locations: z.record(z.string(), optionalInt).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export type CreateInventoryItemSchema = z.infer<
|
export type CreateInventoryItemSchema = z.infer<
|
||||||
|
|||||||
+12
-1
@@ -1,10 +1,21 @@
|
|||||||
import { RouteFocusModal } from "../../../components/modals"
|
import { RouteFocusModal } from "../../../components/modals"
|
||||||
|
import { useStockLocations } from "../../../hooks/api"
|
||||||
import { InventoryCreateForm } from "./components/inventory-create-form"
|
import { InventoryCreateForm } from "./components/inventory-create-form"
|
||||||
|
|
||||||
export function InventoryCreate() {
|
export function InventoryCreate() {
|
||||||
|
const { isPending, stock_locations, isError, error } = useStockLocations({
|
||||||
|
limit: 9999,
|
||||||
|
fields: "id,name",
|
||||||
|
})
|
||||||
|
const ready = !isPending && !!stock_locations
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteFocusModal>
|
<RouteFocusModal>
|
||||||
<InventoryCreateForm />
|
{ready && <InventoryCreateForm locations={stock_locations} />}
|
||||||
</RouteFocusModal>
|
</RouteFocusModal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import { HttpTypes } from "@medusajs/types"
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { createDataGridPriceColumns } from "../../../../components/data-grid/data-grid-column-helpers/create-data-grid-price-columns"
|
import { createDataGridPriceColumns } from "../../../../components/data-grid/helpers/create-data-grid-price-columns"
|
||||||
|
|
||||||
export const useShippingOptionPriceColumns = ({
|
export const useShippingOptionPriceColumns = ({
|
||||||
currencies = [],
|
currencies = [],
|
||||||
|
|||||||
+22
-11
@@ -4,13 +4,17 @@ 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 { DataGrid } from "../../../../components/data-grid"
|
import {
|
||||||
import { createDataGridPriceColumns } from "../../../../components/data-grid/data-grid-column-helpers/create-data-grid-price-columns"
|
createDataGridHelper,
|
||||||
import { createDataGridHelper } from "../../../../components/data-grid/utils"
|
DataGrid,
|
||||||
|
} from "../../../../components/data-grid"
|
||||||
|
import { createDataGridPriceColumns } from "../../../../components/data-grid/helpers/create-data-grid-price-columns"
|
||||||
|
import { PricingCreateSchemaType } from "../../price-list-create/components/price-list-create-form/schema"
|
||||||
import { isProductRow } from "../utils"
|
import { isProductRow } from "../utils"
|
||||||
|
|
||||||
const columnHelper = createDataGridHelper<
|
const columnHelper = createDataGridHelper<
|
||||||
HttpTypes.AdminProduct | HttpTypes.AdminProductVariant
|
HttpTypes.AdminProduct | HttpTypes.AdminProductVariant,
|
||||||
|
PricingCreateSchemaType
|
||||||
>()
|
>()
|
||||||
|
|
||||||
export const usePriceListGridColumns = ({
|
export const usePriceListGridColumns = ({
|
||||||
@@ -31,11 +35,11 @@ export const usePriceListGridColumns = ({
|
|||||||
columnHelper.column({
|
columnHelper.column({
|
||||||
id: t("fields.title"),
|
id: t("fields.title"),
|
||||||
header: t("fields.title"),
|
header: t("fields.title"),
|
||||||
cell: ({ row }) => {
|
cell: (context) => {
|
||||||
const entity = row.original
|
const entity = context.row.original
|
||||||
if (isProductRow(entity)) {
|
if (isProductRow(entity)) {
|
||||||
return (
|
return (
|
||||||
<DataGrid.ReadonlyCell>
|
<DataGrid.ReadonlyCell context={context}>
|
||||||
<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">
|
||||||
<Thumbnail src={entity.thumbnail} />
|
<Thumbnail src={entity.thumbnail} />
|
||||||
<span className="truncate">{entity.title}</span>
|
<span className="truncate">{entity.title}</span>
|
||||||
@@ -45,7 +49,7 @@ export const usePriceListGridColumns = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataGrid.ReadonlyCell>
|
<DataGrid.ReadonlyCell context={context}>
|
||||||
<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 className="truncate">{entity.title}</span>
|
<span className="truncate">{entity.title}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,7 +59,8 @@ export const usePriceListGridColumns = ({
|
|||||||
disableHiding: true,
|
disableHiding: true,
|
||||||
}),
|
}),
|
||||||
...createDataGridPriceColumns<
|
...createDataGridPriceColumns<
|
||||||
HttpTypes.AdminProduct | HttpTypes.AdminProductVariant
|
HttpTypes.AdminProduct | HttpTypes.AdminProductVariant,
|
||||||
|
PricingCreateSchemaType
|
||||||
>({
|
>({
|
||||||
currencies: currencies.map((c) => c.currency_code),
|
currencies: currencies.map((c) => c.currency_code),
|
||||||
regions,
|
regions,
|
||||||
@@ -65,10 +70,16 @@ export const usePriceListGridColumns = ({
|
|||||||
return isProductRow(entity)
|
return isProductRow(entity)
|
||||||
},
|
},
|
||||||
getFieldName: (context, value) => {
|
getFieldName: (context, value) => {
|
||||||
const entity = context.row.original as any
|
const entity = context.row.original
|
||||||
if (context.column.id.startsWith("currency_prices")) {
|
|
||||||
|
if (isProductRow(entity)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.column.id?.startsWith("currency_prices")) {
|
||||||
return `products.${entity.product_id}.variants.${entity.id}.currency_prices.${value}.amount`
|
return `products.${entity.product_id}.variants.${entity.id}.currency_prices.${value}.amount`
|
||||||
}
|
}
|
||||||
|
|
||||||
return `products.${entity.product_id}.variants.${entity.id}.region_prices.${value}.amount`
|
return `products.${entity.product_id}.variants.${entity.id}.region_prices.${value}.amount`
|
||||||
},
|
},
|
||||||
t,
|
t,
|
||||||
|
|||||||
+1
@@ -78,6 +78,7 @@ export const PriceListPricesForm = ({
|
|||||||
return (
|
return (
|
||||||
<div className="flex size-full flex-col divide-y overflow-hidden">
|
<div className="flex size-full flex-col divide-y overflow-hidden">
|
||||||
<DataGrid
|
<DataGrid
|
||||||
|
isLoading={isLoading}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={products}
|
data={products}
|
||||||
getSubRows={(row) => {
|
getSubRows={(row) => {
|
||||||
|
|||||||
+4
-7
@@ -89,13 +89,10 @@ export const PriceListPricesAddForm = ({
|
|||||||
) => {
|
) => {
|
||||||
form.clearErrors(fields)
|
form.clearErrors(fields)
|
||||||
|
|
||||||
const values = fields.reduce(
|
const values = fields.reduce((acc, key) => {
|
||||||
(acc, key) => {
|
acc[key] = form.getValues(key)
|
||||||
acc[key] = form.getValues(key)
|
return acc
|
||||||
return acc
|
}, {} as Record<string, unknown>)
|
||||||
},
|
|
||||||
{} as Record<string, unknown>
|
|
||||||
)
|
|
||||||
|
|
||||||
const validationResult = schema.safeParse(values)
|
const validationResult = schema.safeParse(values)
|
||||||
|
|
||||||
|
|||||||
+1
@@ -79,6 +79,7 @@ export const PriceListPricesAddPricesForm = ({
|
|||||||
return (
|
return (
|
||||||
<div className="flex size-full flex-col divide-y overflow-hidden">
|
<div className="flex size-full flex-col divide-y overflow-hidden">
|
||||||
<DataGrid
|
<DataGrid
|
||||||
|
isLoading={isLoading}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={products}
|
data={products}
|
||||||
getSubRows={(row) => {
|
getSubRows={(row) => {
|
||||||
|
|||||||
@@ -46,7 +46,10 @@ export const VariantPricingForm = ({ form }: VariantPricingFormProps) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnHelper = createDataGridHelper<HttpTypes.AdminProductVariant>()
|
const columnHelper = createDataGridHelper<
|
||||||
|
HttpTypes.AdminProductVariant,
|
||||||
|
ProductCreateSchemaType
|
||||||
|
>()
|
||||||
|
|
||||||
const useVariantPriceGridColumns = ({
|
const useVariantPriceGridColumns = ({
|
||||||
currencies = [],
|
currencies = [],
|
||||||
@@ -64,10 +67,10 @@ const useVariantPriceGridColumns = ({
|
|||||||
columnHelper.column({
|
columnHelper.column({
|
||||||
id: t("fields.title"),
|
id: t("fields.title"),
|
||||||
header: t("fields.title"),
|
header: t("fields.title"),
|
||||||
cell: ({ row }) => {
|
cell: (context) => {
|
||||||
const entity = row.original
|
const entity = context.row.original
|
||||||
return (
|
return (
|
||||||
<DataGrid.ReadonlyCell>
|
<DataGrid.ReadonlyCell context={context}>
|
||||||
<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 className="truncate">{entity.title}</span>
|
<span className="truncate">{entity.title}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -76,12 +79,15 @@ const useVariantPriceGridColumns = ({
|
|||||||
},
|
},
|
||||||
disableHiding: true,
|
disableHiding: true,
|
||||||
}),
|
}),
|
||||||
...createDataGridPriceColumns<HttpTypes.AdminProductVariant>({
|
...createDataGridPriceColumns<
|
||||||
|
HttpTypes.AdminProductVariant,
|
||||||
|
ProductCreateSchemaType
|
||||||
|
>({
|
||||||
currencies: currencies.map((c) => c.currency_code),
|
currencies: currencies.map((c) => c.currency_code),
|
||||||
regions,
|
regions,
|
||||||
pricePreferences,
|
pricePreferences,
|
||||||
getFieldName: (context, value) => {
|
getFieldName: (context, value) => {
|
||||||
if (context.column.id.startsWith("currency_prices")) {
|
if (context.column.id?.startsWith("currency_prices")) {
|
||||||
return `variants.${context.row.index}.prices.${value}`
|
return `variants.${context.row.index}.prices.${value}`
|
||||||
}
|
}
|
||||||
return `variants.${context.row.index}.prices.${value}`
|
return `variants.${context.row.index}.prices.${value}`
|
||||||
|
|||||||
+33
-34
@@ -3,9 +3,11 @@ 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 { DataGrid } from "../../../../../components/data-grid"
|
import {
|
||||||
import { createDataGridPriceColumns } from "../../../../../components/data-grid/data-grid-column-helpers/create-data-grid-price-columns"
|
createDataGridHelper,
|
||||||
import { createDataGridHelper } from "../../../../../components/data-grid/utils"
|
createDataGridPriceColumns,
|
||||||
|
DataGrid,
|
||||||
|
} from "../../../../../components/data-grid"
|
||||||
import { useRouteModal } from "../../../../../components/modals"
|
import { useRouteModal } from "../../../../../components/modals"
|
||||||
import { usePricePreferences } from "../../../../../hooks/api/price-preferences"
|
import { usePricePreferences } from "../../../../../hooks/api/price-preferences"
|
||||||
import { useRegions } from "../../../../../hooks/api/regions"
|
import { useRegions } from "../../../../../hooks/api/regions"
|
||||||
@@ -111,7 +113,10 @@ export const ProductCreateVariantsForm = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnHelper = createDataGridHelper<ProductCreateVariantSchema>()
|
const columnHelper = createDataGridHelper<
|
||||||
|
ProductCreateVariantSchema,
|
||||||
|
ProductCreateSchemaType
|
||||||
|
>()
|
||||||
|
|
||||||
const useColumns = ({
|
const useColumns = ({
|
||||||
options,
|
options,
|
||||||
@@ -137,10 +142,12 @@ const useColumns = ({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
cell: ({ row }) => {
|
cell: (context) => {
|
||||||
return (
|
return (
|
||||||
<DataGrid.ReadonlyCell>
|
<DataGrid.ReadonlyCell context={context}>
|
||||||
{options.map((o) => row.original.options[o.title]).join(" / ")}
|
{options
|
||||||
|
.map((o) => context.row.original.options[o.title])
|
||||||
|
.join(" / ")}
|
||||||
</DataGrid.ReadonlyCell>
|
</DataGrid.ReadonlyCell>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -150,52 +157,40 @@ const useColumns = ({
|
|||||||
id: "title",
|
id: "title",
|
||||||
name: t("fields.title"),
|
name: t("fields.title"),
|
||||||
header: t("fields.title"),
|
header: t("fields.title"),
|
||||||
|
field: (context) => `variants.${context.row.index}.title`,
|
||||||
|
type: "text",
|
||||||
cell: (context) => {
|
cell: (context) => {
|
||||||
return (
|
return <DataGrid.TextCell context={context} />
|
||||||
<DataGrid.TextCell
|
|
||||||
context={context}
|
|
||||||
field={`variants.${context.row.index}.title`}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
columnHelper.column({
|
columnHelper.column({
|
||||||
id: "sku",
|
id: "sku",
|
||||||
name: t("fields.sku"),
|
name: t("fields.sku"),
|
||||||
header: t("fields.sku"),
|
header: t("fields.sku"),
|
||||||
|
field: (context) => `variants.${context.row.index}.sku`,
|
||||||
|
type: "text",
|
||||||
cell: (context) => {
|
cell: (context) => {
|
||||||
return (
|
return <DataGrid.TextCell context={context} />
|
||||||
<DataGrid.TextCell
|
|
||||||
context={context}
|
|
||||||
field={`variants.${context.row.index}.sku`}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
columnHelper.column({
|
columnHelper.column({
|
||||||
id: "manage_inventory",
|
id: "manage_inventory",
|
||||||
name: t("fields.managedInventory"),
|
name: t("fields.managedInventory"),
|
||||||
header: t("fields.managedInventory"),
|
header: t("fields.managedInventory"),
|
||||||
|
field: (context) => `variants.${context.row.index}.manage_inventory`,
|
||||||
|
type: "boolean",
|
||||||
cell: (context) => {
|
cell: (context) => {
|
||||||
return (
|
return <DataGrid.BooleanCell context={context} />
|
||||||
<DataGrid.BooleanCell
|
|
||||||
context={context}
|
|
||||||
field={`variants.${context.row.index}.manage_inventory`}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
columnHelper.column({
|
columnHelper.column({
|
||||||
id: "allow_backorder",
|
id: "allow_backorder",
|
||||||
name: t("fields.allowBackorder"),
|
name: t("fields.allowBackorder"),
|
||||||
header: t("fields.allowBackorder"),
|
header: t("fields.allowBackorder"),
|
||||||
|
field: (context) => `variants.${context.row.index}.allow_backorder`,
|
||||||
|
type: "boolean",
|
||||||
cell: (context) => {
|
cell: (context) => {
|
||||||
return (
|
return <DataGrid.BooleanCell context={context} />
|
||||||
<DataGrid.BooleanCell
|
|
||||||
context={context}
|
|
||||||
field={`variants.${context.row.index}.allow_backorder`}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -203,23 +198,27 @@ const useColumns = ({
|
|||||||
id: "inventory_kit",
|
id: "inventory_kit",
|
||||||
name: t("fields.inventoryKit"),
|
name: t("fields.inventoryKit"),
|
||||||
header: t("fields.inventoryKit"),
|
header: t("fields.inventoryKit"),
|
||||||
|
field: (context) => `variants.${context.row.index}.inventory_kit`,
|
||||||
|
type: "boolean",
|
||||||
cell: (context) => {
|
cell: (context) => {
|
||||||
return (
|
return (
|
||||||
<DataGrid.BooleanCell
|
<DataGrid.BooleanCell
|
||||||
context={context}
|
context={context}
|
||||||
field={`variants.${context.row.index}.inventory_kit`}
|
|
||||||
disabled={!context.row.original.manage_inventory}
|
disabled={!context.row.original.manage_inventory}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
...createDataGridPriceColumns<ProductCreateVariantSchema>({
|
...createDataGridPriceColumns<
|
||||||
|
ProductCreateVariantSchema,
|
||||||
|
ProductCreateSchemaType
|
||||||
|
>({
|
||||||
currencies,
|
currencies,
|
||||||
regions,
|
regions,
|
||||||
pricePreferences,
|
pricePreferences,
|
||||||
getFieldName: (context, value) => {
|
getFieldName: (context, value) => {
|
||||||
if (context.column.id.startsWith("currency_prices")) {
|
if (context.column.id?.startsWith("currency_prices")) {
|
||||||
return `variants.${context.row.index}.prices.${value}`
|
return `variants.${context.row.index}.prices.${value}`
|
||||||
}
|
}
|
||||||
return `variants.${context.row.index}.prices.${value}`
|
return `variants.${context.row.index}.prices.${value}`
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
import { i18n } from "../../../components/utilities/i18n/i18n.tsx"
|
||||||
import { optionalInt } from "../../../lib/validation.ts"
|
import { optionalInt } from "../../../lib/validation.ts"
|
||||||
import { decorateVariantsWithDefaultValues } from "./utils.ts"
|
import { decorateVariantsWithDefaultValues } from "./utils.ts"
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ const ProductCreateVariantSchema = z.object({
|
|||||||
inventory_kit: z.boolean().optional(),
|
inventory_kit: z.boolean().optional(),
|
||||||
options: z.record(z.string(), z.string()),
|
options: z.record(z.string(), z.string()),
|
||||||
variant_rank: z.number(),
|
variant_rank: z.number(),
|
||||||
prices: z.record(z.string(), z.string().optional()).optional(),
|
prices: z.record(z.string(), optionalInt).optional(),
|
||||||
inventory: z
|
inventory: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -95,6 +96,22 @@ export const ProductCreateSchema = z
|
|||||||
message: "invalid_length",
|
message: "invalid_length",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const skus = new Set<string>()
|
||||||
|
|
||||||
|
data.variants.forEach((v, index) => {
|
||||||
|
if (v.sku) {
|
||||||
|
if (skus.has(v.sku)) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: [`variants.${index}.sku`],
|
||||||
|
message: i18n.t("products.create.errors.uniqueSku"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
skus.add(v.sku)
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
export const EditProductMediaSchema = z.object({
|
export const EditProductMediaSchema = z.object({
|
||||||
|
|||||||
-6
@@ -95,12 +95,6 @@ export const CreateRegionForm = ({
|
|||||||
is_tax_inclusive: values.is_tax_inclusive,
|
is_tax_inclusive: values.is_tax_inclusive,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onError: (e) => {
|
|
||||||
toast.error(t("general.error"), {
|
|
||||||
description: e.message,
|
|
||||||
dismissLabel: t("actions.close"),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onSuccess: ({ region }) => {
|
onSuccess: ({ region }) => {
|
||||||
toast.success(t("regions.toast.create"))
|
toast.success(t("regions.toast.create"))
|
||||||
handleSuccess(`../${region.id}`)
|
handleSuccess(`../${region.id}`)
|
||||||
|
|||||||
-6
@@ -64,12 +64,6 @@ export const EditRegionForm = ({
|
|||||||
is_tax_inclusive: values.is_tax_inclusive,
|
is_tax_inclusive: values.is_tax_inclusive,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onError: (e) => {
|
|
||||||
toast.error(t("general.error"), {
|
|
||||||
description: e.message,
|
|
||||||
dismissLabel: t("actions.close"),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(t("regions.toast.edit"))
|
toast.success(t("regions.toast.edit"))
|
||||||
handleSuccess()
|
handleSuccess()
|
||||||
|
|||||||
@@ -3566,6 +3566,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@hookform/error-message@npm:^2.0.1":
|
||||||
|
version: 2.0.1
|
||||||
|
resolution: "@hookform/error-message@npm:2.0.1"
|
||||||
|
peerDependencies:
|
||||||
|
react: ">=16.8.0"
|
||||||
|
react-dom: ">=16.8.0"
|
||||||
|
react-hook-form: ^7.0.0
|
||||||
|
checksum: 6b608bcdbd797ddb7c6cfc8c42b6bbac40066181a0c582b1f1a342bfa65fa7e8329cdb8e869a76e33988cd46fe8623d521ea597231b9d33e1f0ba3288e36c58e
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@hookform/resolvers@npm:3.4.2":
|
"@hookform/resolvers@npm:3.4.2":
|
||||||
version: 3.4.2
|
version: 3.4.2
|
||||||
resolution: "@hookform/resolvers@npm:3.4.2"
|
resolution: "@hookform/resolvers@npm:3.4.2"
|
||||||
@@ -4519,6 +4530,7 @@ __metadata:
|
|||||||
"@ariakit/react": ^0.4.1
|
"@ariakit/react": ^0.4.1
|
||||||
"@dnd-kit/core": ^6.1.0
|
"@dnd-kit/core": ^6.1.0
|
||||||
"@dnd-kit/sortable": ^8.0.0
|
"@dnd-kit/sortable": ^8.0.0
|
||||||
|
"@hookform/error-message": ^2.0.1
|
||||||
"@hookform/resolvers": 3.4.2
|
"@hookform/resolvers": 3.4.2
|
||||||
"@medusajs/admin-shared": ^0.0.1
|
"@medusajs/admin-shared": ^0.0.1
|
||||||
"@medusajs/admin-vite-plugin": 0.0.1
|
"@medusajs/admin-vite-plugin": 0.0.1
|
||||||
@@ -4529,7 +4541,7 @@ __metadata:
|
|||||||
"@medusajs/ui-preset": 1.1.3
|
"@medusajs/ui-preset": 1.1.3
|
||||||
"@radix-ui/react-collapsible": 1.1.0
|
"@radix-ui/react-collapsible": 1.1.0
|
||||||
"@tanstack/react-query": ^5.28.14
|
"@tanstack/react-query": ^5.28.14
|
||||||
"@tanstack/react-table": 8.10.7
|
"@tanstack/react-table": 8.20.5
|
||||||
"@tanstack/react-virtual": ^3.8.3
|
"@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
|
||||||
@@ -4554,6 +4566,7 @@ __metadata:
|
|||||||
react-country-flag: ^3.1.0
|
react-country-flag: ^3.1.0
|
||||||
react-currency-input-field: ^3.6.11
|
react-currency-input-field: ^3.6.11
|
||||||
react-dom: ^18.2.0
|
react-dom: ^18.2.0
|
||||||
|
react-helmet-async: ^2.0.5
|
||||||
react-hook-form: 7.49.1
|
react-hook-form: 7.49.1
|
||||||
react-i18next: 13.5.0
|
react-i18next: 13.5.0
|
||||||
react-jwt: ^1.2.0
|
react-jwt: ^1.2.0
|
||||||
@@ -11511,15 +11524,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@tanstack/react-table@npm:8.10.7":
|
"@tanstack/react-table@npm:8.20.5":
|
||||||
version: 8.10.7
|
version: 8.20.5
|
||||||
resolution: "@tanstack/react-table@npm:8.10.7"
|
resolution: "@tanstack/react-table@npm:8.20.5"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@tanstack/table-core": 8.10.7
|
"@tanstack/table-core": 8.20.5
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ">=16"
|
react: ">=16.8"
|
||||||
react-dom: ">=16"
|
react-dom: ">=16.8"
|
||||||
checksum: 2ddf6f90b06e7af069f16dbe5d0dc8c8afab3de88c25e33f6c297beaf2507c2e46fee1f746f7977d48bad2114909bba0016026fc2b6a85bcaee472cdafdc7ffd
|
checksum: 574fa62fc6868a3b1113dbd043323f8b73aeb60555609caa164d5137a14636d4502784a961191afde2ec46f33f8c2bbfc4561d27a701c3d084e899a632dda3c8
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -11535,10 +11548,10 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@tanstack/table-core@npm:8.10.7":
|
"@tanstack/table-core@npm:8.20.5":
|
||||||
version: 8.10.7
|
version: 8.20.5
|
||||||
resolution: "@tanstack/table-core@npm:8.10.7"
|
resolution: "@tanstack/table-core@npm:8.20.5"
|
||||||
checksum: 3f671484319094443bb2db86356f408d4246e22bebd7ad444edc919fef131899384c3a27261c5ee01fb18887bc9157c5a0d9db3e32aae940ce5416f6e58b038b
|
checksum: 3c27b5debd61b6bd9bfbb40bfc7c5d5af90873ae1a566b20e3bf2d2f4f2e9a78061c081aacc5259a00e256f8df506ec250eb5472f5c01ff04baf9918b554982b
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -26088,6 +26101,26 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"react-fast-compare@npm:^3.2.2":
|
||||||
|
version: 3.2.2
|
||||||
|
resolution: "react-fast-compare@npm:3.2.2"
|
||||||
|
checksum: 0bbd2f3eb41ab2ff7380daaa55105db698d965c396df73e6874831dbafec8c4b5b08ba36ff09df01526caa3c61595247e3269558c284e37646241cba2b90a367
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"react-helmet-async@npm:^2.0.5":
|
||||||
|
version: 2.0.5
|
||||||
|
resolution: "react-helmet-async@npm:2.0.5"
|
||||||
|
dependencies:
|
||||||
|
invariant: ^2.2.4
|
||||||
|
react-fast-compare: ^3.2.2
|
||||||
|
shallowequal: ^1.1.0
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.6.0 || ^17.0.0 || ^18.0.0
|
||||||
|
checksum: f390ea8bf13c2681850e5f8eb5b73d8613f407c245a5fd23e9db9b2cc14a3700dd1ce992d3966632886d1d613083294c2aeee009193f49dfa7d145d9f13ea2b0
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"react-hook-form@npm:7.49.1":
|
"react-hook-form@npm:7.49.1":
|
||||||
version: 7.49.1
|
version: 7.49.1
|
||||||
resolution: "react-hook-form@npm:7.49.1"
|
resolution: "react-hook-form@npm:7.49.1"
|
||||||
@@ -27742,7 +27775,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"shallowequal@npm:1.1.0":
|
"shallowequal@npm:1.1.0, shallowequal@npm:^1.1.0":
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
resolution: "shallowequal@npm:1.1.0"
|
resolution: "shallowequal@npm:1.1.0"
|
||||||
checksum: b926efb51cd0f47aa9bc061add788a4a650550bbe50647962113a4579b60af2abe7b62f9b02314acc6f97151d4cf87033a2b15fc20852fae306d1a095215396c
|
checksum: b926efb51cd0f47aa9bc061add788a4a650550bbe50647962113a4579b60af2abe7b62f9b02314acc6f97151d4cf87033a2b15fc20852fae306d1a095215396c
|
||||||
|
|||||||
Reference in new issue
Block a user