feat(dashboard): BulkEditor Boolean cell behaviour (#8418)
* progress * progress * add special Space behaviour for boolean cells * prevent shift clicking setting rangeEnd outside of anchor column
This commit is contained in:
1 parent
b78e286224
commit
b2250ed7b1
12 files changed
+593
-390
No files matched your search
@@ -36,7 +36,6 @@
|
|||||||
"@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",
|
||||||
"@radix-ui/react-hover-card": "1.1.1",
|
|
||||||
"@tanstack/react-query": "^5.28.14",
|
"@tanstack/react-query": "^5.28.14",
|
||||||
"@tanstack/react-table": "8.10.7",
|
"@tanstack/react-table": "8.10.7",
|
||||||
"@tanstack/react-virtual": "^3.8.3",
|
"@tanstack/react-virtual": "^3.8.3",
|
||||||
@@ -50,6 +49,7 @@
|
|||||||
"i18next-http-backend": "2.4.2",
|
"i18next-http-backend": "2.4.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"match-sorter": "^6.3.4",
|
"match-sorter": "^6.3.4",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
"qs": "^6.12.0",
|
"qs": "^6.12.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-country-flag": "^3.1.0",
|
"react-country-flag": "^3.1.0",
|
||||||
@@ -59,7 +59,6 @@
|
|||||||
"react-i18next": "13.5.0",
|
"react-i18next": "13.5.0",
|
||||||
"react-jwt": "^1.2.0",
|
"react-jwt": "^1.2.0",
|
||||||
"react-nestable": "^3.0.2",
|
"react-nestable": "^3.0.2",
|
||||||
"react-resizable-panels": "^2.0.16",
|
|
||||||
"react-router-dom": "6.20.1",
|
"react-router-dom": "6.20.1",
|
||||||
"zod": "3.22.4"
|
"zod": "3.22.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import { FocusEvent, MouseEvent, createContext } from "react"
|
import { FocusEvent, MouseEvent, createContext } from "react"
|
||||||
import { Control, FieldValues, Path, UseFormRegister } from "react-hook-form"
|
import { Control, FieldValues, Path, UseFormRegister } from "react-hook-form"
|
||||||
import { CellCoords } from "./types"
|
import { CellCoords, CellType } from "./types"
|
||||||
|
|
||||||
type DataGridContextType<TForm extends FieldValues> = {
|
type DataGridContextType<TForm extends FieldValues> = {
|
||||||
// Grid state
|
// Grid state
|
||||||
anchor: CellCoords | null
|
anchor: CellCoords | null
|
||||||
selection: Record<string, boolean>
|
trapActive: boolean
|
||||||
dragSelection: Record<string, boolean>
|
|
||||||
// Cell handlers
|
// Cell handlers
|
||||||
registerCell: (coords: CellCoords, key: string) => void
|
registerCell: (coords: CellCoords, field: string, type: CellType) => void
|
||||||
getIsCellSelected: (coords: CellCoords) => boolean
|
getIsCellSelected: (coords: CellCoords) => boolean
|
||||||
getIsCellDragSelected: (coords: CellCoords) => boolean
|
getIsCellDragSelected: (coords: CellCoords) => boolean
|
||||||
// Grid handlers
|
// Grid handlers
|
||||||
setIsEditing: (value: boolean) => void
|
setIsEditing: (value: boolean) => void
|
||||||
setIsSelecting: (value: boolean) => void
|
setIsSelecting: (value: boolean) => void
|
||||||
setRangeEnd: (coords: CellCoords) => void
|
setRangeEnd: (coords: CellCoords) => void
|
||||||
|
setSingleRange: (coords: CellCoords) => void
|
||||||
// Form state and handlers
|
// Form state and handlers
|
||||||
register: UseFormRegister<TForm>
|
register: UseFormRegister<TForm>
|
||||||
control: Control<TForm>
|
control: Control<TForm>
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@ export const DataGridBooleanCell = <TData, TValue = any>({
|
|||||||
const { control, renderProps } = useDataGridCell({
|
const { control, renderProps } = useDataGridCell({
|
||||||
field,
|
field,
|
||||||
context,
|
context,
|
||||||
type: "select",
|
type: "boolean",
|
||||||
})
|
})
|
||||||
|
|
||||||
const { container, input } = renderProps
|
const { container, input } = renderProps
|
||||||
|
|||||||
+328
-213
@@ -11,11 +11,16 @@ import {
|
|||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
useReactTable,
|
useReactTable,
|
||||||
} from "@tanstack/react-table"
|
} from "@tanstack/react-table"
|
||||||
import { VirtualItem, useVirtualizer } from "@tanstack/react-virtual"
|
import {
|
||||||
|
ScrollToOptions,
|
||||||
|
VirtualItem,
|
||||||
|
useVirtualizer,
|
||||||
|
} from "@tanstack/react-virtual"
|
||||||
import FocusTrap from "focus-trap-react"
|
import FocusTrap from "focus-trap-react"
|
||||||
import {
|
import {
|
||||||
FocusEvent,
|
FocusEvent,
|
||||||
MouseEvent,
|
MouseEvent,
|
||||||
|
ReactNode,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
@@ -26,14 +31,13 @@ import { FieldValues, Path, PathValue, UseFormReturn } from "react-hook-form"
|
|||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { useCommandHistory } from "../../../hooks/use-command-history"
|
import { useCommandHistory } from "../../../hooks/use-command-history"
|
||||||
import { DataGridContext } from "../context"
|
import { DataGridContext } from "../context"
|
||||||
|
import { useGridQueryTool } from "../hooks"
|
||||||
import { BulkUpdateCommand, Matrix, UpdateCommand } from "../models"
|
import { BulkUpdateCommand, Matrix, UpdateCommand } from "../models"
|
||||||
import { CellCoords } from "../types"
|
import { CellCoords, CellType } from "../types"
|
||||||
import {
|
import {
|
||||||
convertArrayToPrimitive,
|
convertArrayToPrimitive,
|
||||||
generateCellId,
|
generateCellId,
|
||||||
getColumnName,
|
getColumnName,
|
||||||
getColumnType,
|
|
||||||
getRange,
|
|
||||||
isCellMatch,
|
isCellMatch,
|
||||||
} from "../utils"
|
} from "../utils"
|
||||||
|
|
||||||
@@ -77,17 +81,12 @@ export const DataGridRoot = <
|
|||||||
const { redo, undo, execute } = useCommandHistory()
|
const { redo, undo, execute } = useCommandHistory()
|
||||||
const { register, control, getValues, setValue } = state
|
const { register, control, getValues, setValue } = state
|
||||||
|
|
||||||
const [active, setActive] = useState(true)
|
const [trapActive, setTrapActive] = useState(false)
|
||||||
|
|
||||||
const [anchor, setAnchor] = useState<CellCoords | null>(null)
|
const [anchor, setAnchor] = useState<CellCoords | null>(null)
|
||||||
const [rangeEnd, setRangeEnd] = useState<CellCoords | null>(null)
|
const [rangeEnd, setRangeEnd] = useState<CellCoords | null>(null)
|
||||||
const [dragEnd, setDragEnd] = useState<CellCoords | null>(null)
|
const [dragEnd, setDragEnd] = useState<CellCoords | null>(null)
|
||||||
|
|
||||||
const [selection, setSelection] = useState<Record<string, boolean>>({})
|
|
||||||
const [dragSelection, setDragSelection] = useState<Record<string, boolean>>(
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
|
|
||||||
const [isSelecting, setIsSelecting] = useState(false)
|
const [isSelecting, setIsSelecting] = useState(false)
|
||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
|
|
||||||
@@ -144,7 +143,7 @@ export const DataGridRoot = <
|
|||||||
toRender.add(rangeEnd.row)
|
toRender.add(rangeEnd.row)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(toRender)
|
return Array.from(toRender).sort((a, b) => a - b)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -176,7 +175,7 @@ export const DataGridRoot = <
|
|||||||
toRender.add(rangeEnd.col)
|
toRender.add(rangeEnd.col)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(toRender)
|
return Array.from(toRender).sort((a, b) => a - b)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -193,24 +192,38 @@ export const DataGridRoot = <
|
|||||||
}
|
}
|
||||||
|
|
||||||
const scrollToCell = useCallback(
|
const scrollToCell = useCallback(
|
||||||
(coords: CellCoords, direction: "horizontal" | "vertical") => {
|
(coords: CellCoords, direction: "horizontal" | "vertical" | "both") => {
|
||||||
const { row, col } = coords
|
if (!anchor) {
|
||||||
|
return
|
||||||
if (direction === "horizontal") {
|
|
||||||
columnVirtualizer.scrollToIndex(col, {
|
|
||||||
align: "auto",
|
|
||||||
behavior: "auto",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (direction === "vertical") {
|
const { row, col } = coords
|
||||||
rowVirtualizer.scrollToIndex(row, {
|
const { row: anchorRow, col: anchorCol } = anchor
|
||||||
align: "auto",
|
|
||||||
behavior: "auto",
|
const rowDirection = row >= anchorRow ? "down" : "up"
|
||||||
})
|
const colDirection = col >= anchorCol ? "right" : "left"
|
||||||
|
|
||||||
|
let toRow = rowDirection === "down" ? row + 1 : row - 1
|
||||||
|
if (flatRows[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)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[columnVirtualizer, rowVirtualizer]
|
[anchor, columnVirtualizer, flatRows, rowVirtualizer, visibleColumns]
|
||||||
)
|
)
|
||||||
|
|
||||||
const matrix = useMemo(
|
const matrix = useMemo(
|
||||||
@@ -218,63 +231,28 @@ export const DataGridRoot = <
|
|||||||
[flatRows, visibleColumns]
|
[flatRows, visibleColumns]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const queryTool = useGridQueryTool(containerRef)
|
||||||
|
|
||||||
const registerCell = useCallback(
|
const registerCell = useCallback(
|
||||||
(coords: CellCoords, key: string) => {
|
(coords: CellCoords, field: string, type: CellType) => {
|
||||||
matrix.registerField(coords.row, coords.col, key)
|
matrix.registerField(coords.row, coords.col, field, type)
|
||||||
},
|
},
|
||||||
[matrix]
|
[matrix]
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
const setSingleRange = useCallback((coordinates: CellCoords | null) => {
|
||||||
* Clears the start and end of current range.
|
|
||||||
*/
|
|
||||||
const clearRange = useCallback(
|
|
||||||
(point?: CellCoords | null) => {
|
|
||||||
const keys = Object.keys(selection)
|
|
||||||
const anchorKey = anchor ? generateCellId(anchor) : null
|
|
||||||
const newKey = point ? generateCellId(point) : null
|
|
||||||
|
|
||||||
const isAnchorOnlySelected = keys.length === 1 && anchorKey === keys[0]
|
|
||||||
const isAnchorNewPoint = anchorKey && newKey && anchorKey === newKey
|
|
||||||
|
|
||||||
const shouldIgnoreAnchor = isAnchorOnlySelected && isAnchorNewPoint
|
|
||||||
|
|
||||||
if (!shouldIgnoreAnchor) {
|
|
||||||
setAnchor(null)
|
|
||||||
setSelection({})
|
|
||||||
setRangeEnd(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
setDragSelection({})
|
|
||||||
},
|
|
||||||
[anchor, selection]
|
|
||||||
)
|
|
||||||
|
|
||||||
const setSingleRange = useCallback(
|
|
||||||
(coordinates: CellCoords | null) => {
|
|
||||||
clearRange(coordinates)
|
|
||||||
|
|
||||||
setAnchor(coordinates)
|
setAnchor(coordinates)
|
||||||
setRangeEnd(coordinates)
|
setRangeEnd(coordinates)
|
||||||
},
|
}, [])
|
||||||
[clearRange]
|
|
||||||
)
|
|
||||||
|
|
||||||
const getSelectionValues = useCallback(
|
const getSelectionValues = useCallback(
|
||||||
(fields: string[]): string[] => {
|
(fields: string[]): PathValue<TFieldValues, Path<TFieldValues>>[] => {
|
||||||
if (!fields.length) {
|
if (!fields.length) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
return fields.map((field) => {
|
return fields.map((field) => {
|
||||||
if (!field) {
|
return getValues(field as Path<TFieldValues>)
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = getValues(field as Path<TFieldValues>)
|
|
||||||
|
|
||||||
// Return the value as a string
|
|
||||||
return `${value}`
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[getValues]
|
[getValues]
|
||||||
@@ -308,7 +286,12 @@ export const DataGridRoot = <
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const type = getColumnType(anchor, visibleColumns)
|
const type = matrix.getCellType(anchor)
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const convertedValues = convertArrayToPrimitive(values, type)
|
const convertedValues = convertArrayToPrimitive(values, type)
|
||||||
|
|
||||||
fields.forEach((field, index) => {
|
fields.forEach((field, index) => {
|
||||||
@@ -322,10 +305,13 @@ export const DataGridRoot = <
|
|||||||
Path<TFieldValues>
|
Path<TFieldValues>
|
||||||
>
|
>
|
||||||
|
|
||||||
setValue(field as Path<TFieldValues>, value)
|
setValue(field as Path<TFieldValues>, value, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[anchor, setValue, visibleColumns]
|
[matrix, anchor, setValue]
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -338,11 +324,21 @@ export const DataGridRoot = <
|
|||||||
*/
|
*/
|
||||||
const handleKeyboardNavigation = useCallback(
|
const handleKeyboardNavigation = useCallback(
|
||||||
(e: KeyboardEvent) => {
|
(e: KeyboardEvent) => {
|
||||||
|
if (!anchor) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = matrix.getCellType(anchor)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the user is currently editing a cell, we don't want to
|
* If the user is currently editing a cell, we don't want to
|
||||||
* handle the keyboard navigation.
|
* 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) {
|
if (isEditing && type !== "boolean") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,37 +404,40 @@ export const DataGridRoot = <
|
|||||||
[redo, undo]
|
[redo, undo]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleSpaceKey = useCallback(
|
const handleSpaceKeyBoolean = useCallback(
|
||||||
(e: KeyboardEvent) => {
|
(anchor: CellCoords) => {
|
||||||
if (!anchor || isEditing) {
|
const end = rangeEnd ?? anchor
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault()
|
const fields = matrix.getFieldsInSelection(anchor, end)
|
||||||
|
|
||||||
const id = generateCellId(anchor)
|
const prev = getSelectionValues(fields) as boolean[]
|
||||||
const container = containerRef.current
|
|
||||||
|
|
||||||
if (!container) {
|
const allChecked = prev.every((value) => value === true)
|
||||||
return
|
const next = Array.from({ length: prev.length }, () => !allChecked)
|
||||||
}
|
|
||||||
|
|
||||||
const input = container.querySelector(
|
const command = new BulkUpdateCommand({
|
||||||
`[data-cell-id="${id}"]`
|
fields,
|
||||||
) as HTMLElement
|
next,
|
||||||
|
prev,
|
||||||
|
setter: setSelectionValues,
|
||||||
|
})
|
||||||
|
|
||||||
if (!input) {
|
execute(command)
|
||||||
return
|
},
|
||||||
}
|
[rangeEnd, matrix, getSelectionValues, setSelectionValues, execute]
|
||||||
|
)
|
||||||
|
|
||||||
const field = input.getAttribute("data-field")
|
const handleSpaceKeyTextOrNumber = useCallback(
|
||||||
|
(anchor: CellCoords) => {
|
||||||
|
const field = matrix.getCellField(anchor)
|
||||||
|
const input = queryTool?.getInput(anchor)
|
||||||
|
|
||||||
if (!field) {
|
if (!field || !input) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const current = getValues(field as Path<TFieldValues>)
|
const current = getValues(field as Path<TFieldValues>)
|
||||||
const next = "" as PathValue<TFieldValues, Path<TFieldValues>>
|
const next = ""
|
||||||
|
|
||||||
const command = new UpdateCommand({
|
const command = new UpdateCommand({
|
||||||
next,
|
next,
|
||||||
@@ -452,13 +451,48 @@ export const DataGridRoot = <
|
|||||||
})
|
})
|
||||||
|
|
||||||
execute(command)
|
execute(command)
|
||||||
|
|
||||||
input.focus()
|
input.focus()
|
||||||
},
|
},
|
||||||
[anchor, isEditing, setValue, getValues, execute]
|
[matrix, queryTool, getValues, execute, setValue]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleEnterEditMode = useCallback(
|
const handleSpaceKey = useCallback(
|
||||||
(e: KeyboardEvent, anchor: { row: number; col: number }) => {
|
(e: KeyboardEvent) => {
|
||||||
|
if (!anchor || isEditing) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const type = matrix.getCellType(anchor)
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "boolean":
|
||||||
|
handleSpaceKeyBoolean(anchor)
|
||||||
|
break
|
||||||
|
case "select":
|
||||||
|
case "number":
|
||||||
|
case "text":
|
||||||
|
handleSpaceKeyTextOrNumber(anchor)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
anchor,
|
||||||
|
isEditing,
|
||||||
|
matrix,
|
||||||
|
handleSpaceKeyBoolean,
|
||||||
|
handleSpaceKeyTextOrNumber,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleMoveOnEnter = useCallback(
|
||||||
|
(e: KeyboardEvent, anchor: CellCoords) => {
|
||||||
const direction = e.shiftKey ? "ArrowUp" : "ArrowDown"
|
const direction = e.shiftKey ? "ArrowUp" : "ArrowDown"
|
||||||
const pos = matrix.getValidMovement(
|
const pos = matrix.getValidMovement(
|
||||||
anchor.row,
|
anchor.row,
|
||||||
@@ -472,67 +506,117 @@ export const DataGridRoot = <
|
|||||||
scrollToCell(pos, "vertical")
|
scrollToCell(pos, "vertical")
|
||||||
} else {
|
} else {
|
||||||
// If the the user is at the last cell, we want to focus the container of the cell.
|
// If the the user is at the last cell, we want to focus the container of the cell.
|
||||||
const id = generateCellId(anchor)
|
const container = queryTool?.getContainer(anchor)
|
||||||
const container = containerRef.current
|
|
||||||
|
|
||||||
const cellContainer = container?.querySelector(
|
container?.focus()
|
||||||
`[data-container-id="${id}"]`
|
|
||||||
) as HTMLElement | null
|
|
||||||
|
|
||||||
cellContainer?.focus()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onEditingChangeHandler(false)
|
onEditingChangeHandler(false)
|
||||||
},
|
},
|
||||||
[matrix, scrollToCell, setSingleRange, onEditingChangeHandler]
|
[queryTool, matrix, scrollToCell, setSingleRange, onEditingChangeHandler]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleEnterNonEditMode = useCallback(
|
const handleEditOnEnter = useCallback(
|
||||||
(anchor: { row: number; col: number }) => {
|
(anchor: CellCoords) => {
|
||||||
const id = generateCellId(anchor)
|
const input = queryTool?.getInput(anchor)
|
||||||
const container = containerRef.current
|
|
||||||
if (!container) {
|
if (!input) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const input = container.querySelector(
|
|
||||||
`[data-cell-id="${id}"]`
|
|
||||||
) as HTMLElement
|
|
||||||
const field = input?.getAttribute("data-field")
|
|
||||||
|
|
||||||
if (input && field) {
|
|
||||||
input.focus()
|
input.focus()
|
||||||
onEditingChangeHandler(true)
|
onEditingChangeHandler(true)
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[onEditingChangeHandler]
|
[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: CellCoords) => {
|
||||||
|
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: CellCoords) => {
|
||||||
|
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 UpdateCommand({
|
||||||
|
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(
|
const handleEnterKey = useCallback(
|
||||||
(e: KeyboardEvent) => {
|
(e: KeyboardEvent) => {
|
||||||
if (!anchor || !containerRef.current) {
|
if (!anchor) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
if (isEditing) {
|
const type = matrix.getCellType(anchor)
|
||||||
handleEnterEditMode(e, anchor)
|
|
||||||
} else {
|
switch (type) {
|
||||||
handleEnterNonEditMode(anchor)
|
case "text":
|
||||||
|
case "number":
|
||||||
|
handleEnterKeyTextOrNumber(e, anchor)
|
||||||
|
break
|
||||||
|
case "boolean": {
|
||||||
|
handleEnterKeyBoolean(e, anchor)
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[anchor, isEditing, handleEnterEditMode, handleEnterNonEditMode]
|
[anchor, matrix, handleEnterKeyTextOrNumber, handleEnterKeyBoolean]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleDeleteKey = useCallback(
|
const handleDeleteKeyTextOrNumber = useCallback(
|
||||||
(e: KeyboardEvent) => {
|
(anchor: CellCoords, rangeEnd: CellCoords) => {
|
||||||
if (!anchor || !rangeEnd || isEditing) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
const fields = matrix.getFieldsInSelection(anchor, rangeEnd)
|
const fields = matrix.getFieldsInSelection(anchor, rangeEnd)
|
||||||
const prev = getSelectionValues(fields)
|
const prev = getSelectionValues(fields)
|
||||||
const next = Array.from({ length: prev.length }, () => "")
|
const next = Array.from({ length: prev.length }, () => "")
|
||||||
@@ -546,14 +630,58 @@ export const DataGridRoot = <
|
|||||||
|
|
||||||
execute(command)
|
execute(command)
|
||||||
},
|
},
|
||||||
|
[matrix, getSelectionValues, setSelectionValues, execute]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleDeleteKeyBoolean = useCallback(
|
||||||
|
(anchor: CellCoords, rangeEnd: CellCoords) => {
|
||||||
|
const fields = matrix.getFieldsInSelection(anchor, rangeEnd)
|
||||||
|
const prev = getSelectionValues(fields)
|
||||||
|
const next = Array.from({ length: prev.length }, () => false)
|
||||||
|
|
||||||
|
const command = new BulkUpdateCommand({
|
||||||
|
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,
|
anchor,
|
||||||
rangeEnd,
|
rangeEnd,
|
||||||
isEditing,
|
isEditing,
|
||||||
matrix,
|
matrix,
|
||||||
getSelectionValues,
|
handleDeleteKeyTextOrNumber,
|
||||||
setSelectionValues,
|
handleDeleteKeyBoolean,
|
||||||
execute,
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -567,17 +695,10 @@ export const DataGridRoot = <
|
|||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
// Restore focus to the container element
|
// Restore focus to the container element
|
||||||
const anchorContainer = containerRef.current?.querySelector(
|
const container = queryTool?.getContainer(anchor)
|
||||||
`[data-container-id="${generateCellId(anchor)}"]`
|
container?.focus()
|
||||||
) as HTMLElement | null
|
|
||||||
|
|
||||||
if (!anchorContainer) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
anchorContainer.focus()
|
|
||||||
},
|
},
|
||||||
[isEditing, anchor]
|
[queryTool, isEditing, anchor]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleTabKey = useCallback(
|
const handleTabKey = useCallback(
|
||||||
@@ -611,6 +732,7 @@ export const DataGridRoot = <
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (e.key === "z" && (e.metaKey || e.ctrlKey)) {
|
if (e.key === "z" && (e.metaKey || e.ctrlKey)) {
|
||||||
|
console.log("Undo/Redo")
|
||||||
handleUndo(e)
|
handleUndo(e)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -660,7 +782,7 @@ export const DataGridRoot = <
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const dragSelection = matrix.getFieldsInSelection(anchor, dragEnd)
|
const dragSelection = matrix.getFieldsInSelection(anchor, dragEnd)
|
||||||
const anchorField = matrix.getCellKey(anchor)
|
const anchorField = matrix.getCellField(anchor)
|
||||||
|
|
||||||
if (!anchorField || !dragSelection.length) {
|
if (!anchorField || !dragSelection.length) {
|
||||||
return
|
return
|
||||||
@@ -711,7 +833,7 @@ export const DataGridRoot = <
|
|||||||
const fields = matrix.getFieldsInSelection(anchor, rangeEnd)
|
const fields = matrix.getFieldsInSelection(anchor, rangeEnd)
|
||||||
const values = getSelectionValues(fields)
|
const values = getSelectionValues(fields)
|
||||||
|
|
||||||
const text = values.map((value) => value ?? "").join("\t")
|
const text = values.map((value) => `${value}` ?? "").join("\t")
|
||||||
|
|
||||||
e.clipboardData?.setData("text/plain", text)
|
e.clipboardData?.setData("text/plain", text)
|
||||||
},
|
},
|
||||||
@@ -760,7 +882,11 @@ export const DataGridRoot = <
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = containerRef.current
|
const container = containerRef.current
|
||||||
|
|
||||||
if (!container || !container.contains(document.activeElement) || !active) {
|
if (
|
||||||
|
!container ||
|
||||||
|
!container.contains(document.activeElement) ||
|
||||||
|
!trapActive
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -779,7 +905,7 @@ export const DataGridRoot = <
|
|||||||
window.removeEventListener("paste", handlePasteEvent)
|
window.removeEventListener("paste", handlePasteEvent)
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
active,
|
trapActive,
|
||||||
handleKeyDownEvent,
|
handleKeyDownEvent,
|
||||||
handleMouseUpEvent,
|
handleMouseUpEvent,
|
||||||
handleCopyEvent,
|
handleCopyEvent,
|
||||||
@@ -807,11 +933,11 @@ export const DataGridRoot = <
|
|||||||
}
|
}
|
||||||
|
|
||||||
setIsSelecting(true)
|
setIsSelecting(true)
|
||||||
clearRange(coords)
|
|
||||||
setAnchor(coords)
|
setSingleRange(coords)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[clearRange]
|
[setSingleRange]
|
||||||
)
|
)
|
||||||
|
|
||||||
const getWrapperMouseOverHandler = useCallback(
|
const getWrapperMouseOverHandler = useCallback(
|
||||||
@@ -864,47 +990,6 @@ export const DataGridRoot = <
|
|||||||
|
|
||||||
/** Effects */
|
/** Effects */
|
||||||
|
|
||||||
/**
|
|
||||||
* If anchor and rangeEnd are set, then select all cells between them.
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if (!anchor || !rangeEnd) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const range = getRange(anchor, rangeEnd)
|
|
||||||
|
|
||||||
setSelection(range)
|
|
||||||
}, [anchor, rangeEnd])
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If anchor and dragEnd are set, then select all cells between them.
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if (!anchor || !dragEnd) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const range = getRange(anchor, dragEnd)
|
|
||||||
|
|
||||||
setDragSelection(range)
|
|
||||||
}, [anchor, dragEnd])
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Auto corrective effect for ensuring that the anchor is always
|
|
||||||
* part of the selected cells.
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if (!anchor) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelection((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[generateCellId(anchor)]: true,
|
|
||||||
}))
|
|
||||||
}, [anchor])
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auto corrective effect for ensuring we always
|
* Auto corrective effect for ensuring we always
|
||||||
* have a range end.
|
* have a range end.
|
||||||
@@ -935,10 +1020,10 @@ export const DataGridRoot = <
|
|||||||
() => ({
|
() => ({
|
||||||
anchor,
|
anchor,
|
||||||
control,
|
control,
|
||||||
selection,
|
trapActive,
|
||||||
dragSelection,
|
|
||||||
setIsSelecting,
|
setIsSelecting,
|
||||||
setIsEditing: onEditingChangeHandler,
|
setIsEditing: onEditingChangeHandler,
|
||||||
|
setSingleRange,
|
||||||
setRangeEnd,
|
setRangeEnd,
|
||||||
getWrapperFocusHandler,
|
getWrapperFocusHandler,
|
||||||
getInputChangeHandler,
|
getInputChangeHandler,
|
||||||
@@ -952,10 +1037,10 @@ export const DataGridRoot = <
|
|||||||
[
|
[
|
||||||
anchor,
|
anchor,
|
||||||
control,
|
control,
|
||||||
selection,
|
trapActive,
|
||||||
dragSelection,
|
|
||||||
setIsSelecting,
|
setIsSelecting,
|
||||||
onEditingChangeHandler,
|
onEditingChangeHandler,
|
||||||
|
setSingleRange,
|
||||||
setRangeEnd,
|
setRangeEnd,
|
||||||
getWrapperFocusHandler,
|
getWrapperFocusHandler,
|
||||||
getInputChangeHandler,
|
getInputChangeHandler,
|
||||||
@@ -973,7 +1058,7 @@ export const DataGridRoot = <
|
|||||||
<div className="bg-ui-bg-subtle flex size-full flex-col">
|
<div className="bg-ui-bg-subtle flex size-full flex-col">
|
||||||
<DataGridHeader grid={grid} />
|
<DataGridHeader grid={grid} />
|
||||||
<FocusTrap
|
<FocusTrap
|
||||||
active={active}
|
active={trapActive}
|
||||||
focusTrapOptions={{
|
focusTrapOptions={{
|
||||||
initialFocus: () => {
|
initialFocus: () => {
|
||||||
if (!anchor) {
|
if (!anchor) {
|
||||||
@@ -998,8 +1083,8 @@ export const DataGridRoot = <
|
|||||||
|
|
||||||
return anchorContainer ?? undefined
|
return anchorContainer ?? undefined
|
||||||
},
|
},
|
||||||
onActivate: () => setActive(true),
|
onActivate: () => setTrapActive(true),
|
||||||
onDeactivate: () => setActive(false),
|
onDeactivate: () => setTrapActive(false),
|
||||||
fallbackFocus: () => {
|
fallbackFocus: () => {
|
||||||
if (!anchor) {
|
if (!anchor) {
|
||||||
const coords = matrix.getFirstNavigableCell()
|
const coords = matrix.getFirstNavigableCell()
|
||||||
@@ -1037,11 +1122,12 @@ export const DataGridRoot = <
|
|||||||
escapeDeactivates: false,
|
escapeDeactivates: false,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="size-full overflow-hidden outline-none" tabIndex={-1}>
|
<div className="size-full overflow-hidden">
|
||||||
<div tabIndex={0} className="outline-none focus:ring-2" />
|
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="relative h-full select-none overflow-auto"
|
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="grid" className="text-ui-fg-subtle grid">
|
||||||
<div
|
<div
|
||||||
@@ -1055,16 +1141,30 @@ export const DataGridRoot = <
|
|||||||
className="flex h-10 w-full"
|
className="flex h-10 w-full"
|
||||||
>
|
>
|
||||||
{virtualPaddingLeft ? (
|
{virtualPaddingLeft ? (
|
||||||
// Empty columns to fill the virtual padding
|
|
||||||
<div
|
<div
|
||||||
role="presentation"
|
role="presentation"
|
||||||
style={{ display: "flex", width: virtualPaddingLeft }}
|
style={{ display: "flex", width: virtualPaddingLeft }}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{virtualColumns.map((vc) => {
|
{virtualColumns.reduce((acc, vc, index, array) => {
|
||||||
const header = headerGroup.headers[vc.index]
|
const header = headerGroup.headers[vc.index]
|
||||||
|
const previousVC = array[index - 1]
|
||||||
|
|
||||||
return (
|
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
|
<div
|
||||||
key={header.id}
|
key={header.id}
|
||||||
role="columnheader"
|
role="columnheader"
|
||||||
@@ -1082,9 +1182,10 @@ export const DataGridRoot = <
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
|
||||||
|
return acc
|
||||||
|
}, [] as ReactNode[])}
|
||||||
{virtualPaddingRight ? (
|
{virtualPaddingRight ? (
|
||||||
// Empty columns to fill the virtual padding
|
|
||||||
<div
|
<div
|
||||||
role="presentation"
|
role="presentation"
|
||||||
style={{
|
style={{
|
||||||
@@ -1259,19 +1360,32 @@ const DataGridRow = <TData,>({
|
|||||||
className="bg-ui-bg-subtle txt-compact-small absolute flex h-10 w-full"
|
className="bg-ui-bg-subtle txt-compact-small absolute flex h-10 w-full"
|
||||||
>
|
>
|
||||||
{virtualPaddingLeft ? (
|
{virtualPaddingLeft ? (
|
||||||
// Empty column to fill the virtual padding
|
|
||||||
<div
|
<div
|
||||||
role="presentation"
|
role="presentation"
|
||||||
style={{ display: "flex", width: virtualPaddingLeft }}
|
style={{ display: "flex", width: virtualPaddingLeft }}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{virtualColumns.map((vc) => {
|
{virtualColumns.reduce((acc, vc, index, array) => {
|
||||||
const cell = visibleCells[vc.index]
|
const cell = visibleCells[vc.index]
|
||||||
const column = cell.column
|
const column = cell.column
|
||||||
|
|
||||||
const columnIndex = visibleColumns.findIndex((c) => c.id === column.id)
|
const columnIndex = visibleColumns.findIndex((c) => c.id === column.id)
|
||||||
|
const previousVC = array[index - 1]
|
||||||
|
|
||||||
return (
|
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
|
<DataGridCell
|
||||||
key={cell.id}
|
key={cell.id}
|
||||||
cell={cell}
|
cell={cell}
|
||||||
@@ -1281,9 +1395,10 @@ const DataGridRow = <TData,>({
|
|||||||
onDragToFillStart={onDragToFillStart}
|
onDragToFillStart={onDragToFillStart}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
|
||||||
|
return acc
|
||||||
|
}, [] as ReactNode[])}
|
||||||
{virtualPaddingRight ? (
|
{virtualPaddingRight ? (
|
||||||
// Empty column to fill the virtual padding
|
|
||||||
<div
|
<div
|
||||||
role="presentation"
|
role="presentation"
|
||||||
style={{ display: "flex", width: virtualPaddingRight }}
|
style={{ display: "flex", width: virtualPaddingRight }}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
} from "react"
|
} from "react"
|
||||||
import { DataGridContext } from "./context"
|
import { DataGridContext } from "./context"
|
||||||
|
import { GridQueryTool } from "./models"
|
||||||
import {
|
import {
|
||||||
CellCoords,
|
CellCoords,
|
||||||
DataGridCellContext,
|
DataGridCellContext,
|
||||||
@@ -31,7 +32,7 @@ const useDataGridContext = () => {
|
|||||||
type UseDataGridCellProps<TData, TValue> = {
|
type UseDataGridCellProps<TData, TValue> = {
|
||||||
field: string
|
field: string
|
||||||
context: CellContext<TData, TValue>
|
context: CellContext<TData, TValue>
|
||||||
type: "text" | "number" | "select"
|
type: "text" | "number" | "select" | "boolean"
|
||||||
}
|
}
|
||||||
|
|
||||||
const textCharacterRegex = /^.$/u
|
const textCharacterRegex = /^.$/u
|
||||||
@@ -57,20 +58,21 @@ export const useDataGridCell = <TData, TValue>({
|
|||||||
register,
|
register,
|
||||||
control,
|
control,
|
||||||
anchor,
|
anchor,
|
||||||
selection,
|
|
||||||
dragSelection,
|
|
||||||
setIsEditing,
|
setIsEditing,
|
||||||
|
setSingleRange,
|
||||||
setIsSelecting,
|
setIsSelecting,
|
||||||
setRangeEnd,
|
setRangeEnd,
|
||||||
getWrapperFocusHandler,
|
getWrapperFocusHandler,
|
||||||
getWrapperMouseOverHandler,
|
getWrapperMouseOverHandler,
|
||||||
getInputChangeHandler,
|
getInputChangeHandler,
|
||||||
|
getIsCellSelected,
|
||||||
|
getIsCellDragSelected,
|
||||||
registerCell,
|
registerCell,
|
||||||
} = useDataGridContext()
|
} = useDataGridContext()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
registerCell(coords, field)
|
registerCell(coords, field, type)
|
||||||
}, [coords, field, registerCell])
|
}, [coords, field, type, registerCell])
|
||||||
|
|
||||||
const [showOverlay, setShowOverlay] = useState(true)
|
const [showOverlay, setShowOverlay] = useState(true)
|
||||||
|
|
||||||
@@ -92,17 +94,46 @@ export const useDataGridCell = <TData, TValue>({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e.shiftKey) {
|
||||||
|
// Only allow setting the rangeEnd if the column matches the anchor column.
|
||||||
|
// If not we let the function continue and treat the click as if the shift key was not pressed.
|
||||||
|
if (coords.col === anchor?.col) {
|
||||||
|
setRangeEnd(coords)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (containerRef.current) {
|
||||||
|
setSingleRange(coords)
|
||||||
|
setIsSelecting(true)
|
||||||
|
containerRef.current.focus()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[coords, anchor, setRangeEnd, setSingleRange, setIsSelecting]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleBooleanInnerMouseDown = useCallback(
|
||||||
|
(e: React.MouseEvent<HTMLElement>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
if (e.detail === 2) {
|
||||||
|
inputRef.current?.focus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
setRangeEnd(coords)
|
setRangeEnd(coords)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (containerRef.current) {
|
if (containerRef.current) {
|
||||||
|
setSingleRange(coords)
|
||||||
setIsSelecting(true)
|
setIsSelecting(true)
|
||||||
containerRef.current.focus()
|
containerRef.current.focus()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setIsSelecting, setRangeEnd, coords]
|
[setIsSelecting, setSingleRange, setRangeEnd, coords]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleInputBlur = useCallback(() => {
|
const handleInputBlur = useCallback(() => {
|
||||||
@@ -171,13 +202,9 @@ export const useDataGridCell = <TData, TValue>({
|
|||||||
return anchor ? isCellMatch(coords, anchor) : false
|
return anchor ? isCellMatch(coords, anchor) : false
|
||||||
}, [anchor, coords])
|
}, [anchor, coords])
|
||||||
|
|
||||||
const isSelected = useMemo(() => {
|
const fieldWithoutOverlay = useMemo(() => {
|
||||||
return selection[id] || false
|
return type === "boolean" || type === "select"
|
||||||
}, [selection, id])
|
}, [type])
|
||||||
|
|
||||||
const isDragSelected = useMemo(() => {
|
|
||||||
return dragSelection[id] || false
|
|
||||||
}, [dragSelection, id])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isAnchor && !containerRef.current?.contains(document.activeElement)) {
|
if (isAnchor && !containerRef.current?.contains(document.activeElement)) {
|
||||||
@@ -188,12 +215,14 @@ export const useDataGridCell = <TData, TValue>({
|
|||||||
const renderProps: DataGridCellRenderProps = {
|
const renderProps: DataGridCellRenderProps = {
|
||||||
container: {
|
container: {
|
||||||
isAnchor,
|
isAnchor,
|
||||||
isSelected,
|
isSelected: getIsCellSelected(coords),
|
||||||
isDragSelected,
|
isDragSelected: getIsCellDragSelected(coords),
|
||||||
showOverlay,
|
showOverlay: fieldWithoutOverlay ? false : showOverlay,
|
||||||
innerProps: {
|
innerProps: {
|
||||||
ref: containerRef,
|
ref: containerRef,
|
||||||
onMouseOver: getWrapperMouseOverHandler(coords),
|
onMouseOver: getWrapperMouseOverHandler(coords),
|
||||||
|
onMouseDown:
|
||||||
|
type === "boolean" ? handleBooleanInnerMouseDown : undefined,
|
||||||
onKeyDown: handleContainerKeyDown,
|
onKeyDown: handleContainerKeyDown,
|
||||||
onFocus: getWrapperFocusHandler(coords),
|
onFocus: getWrapperFocusHandler(coords),
|
||||||
"data-container-id": id,
|
"data-container-id": id,
|
||||||
@@ -221,3 +250,17 @@ export const useDataGridCell = <TData, TValue>({
|
|||||||
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
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Command } from "../../hooks/use-command-history"
|
import { Command } from "../../hooks/use-command-history"
|
||||||
import { CellCoords } from "./types"
|
import { CellCoords, CellType } from "./types"
|
||||||
|
import { generateCellId } from "./utils"
|
||||||
|
|
||||||
export class Matrix {
|
export class Matrix {
|
||||||
private cells: (string | null)[][]
|
private cells: ({ field: string; type: CellType } | null)[][]
|
||||||
|
|
||||||
constructor(rows: number, cols: number) {
|
constructor(rows: number, cols: number) {
|
||||||
this.cells = Array.from({ length: rows }, () => Array(cols).fill(null))
|
this.cells = Array.from({ length: rows }, () => Array(cols).fill(null))
|
||||||
@@ -21,9 +22,12 @@ export class Matrix {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Register a navigable cell with a unique key
|
// Register a navigable cell with a unique key
|
||||||
registerField(row: number, col: number, key: string) {
|
registerField(row: number, col: number, field: string, type: CellType) {
|
||||||
if (this._isValidPosition(row, col)) {
|
if (this._isValidPosition(row, col)) {
|
||||||
this.cells[row][col] = key
|
this.cells[row][col] = {
|
||||||
|
field,
|
||||||
|
type,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,16 +51,24 @@ export class Matrix {
|
|||||||
|
|
||||||
for (let row = startRow; row <= endRow; row++) {
|
for (let row = startRow; row <= endRow; row++) {
|
||||||
if (this._isValidPosition(row, col) && this.cells[row][col] !== null) {
|
if (this._isValidPosition(row, col) && this.cells[row][col] !== null) {
|
||||||
keys.push(this.cells[row][col] as string)
|
keys.push(this.cells[row][col]?.field as string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
getCellKey(cell: CellCoords): string | null {
|
getCellField(cell: CellCoords): string | null {
|
||||||
if (this._isValidPosition(cell.row, cell.col)) {
|
if (this._isValidPosition(cell.row, cell.col)) {
|
||||||
return this.cells[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
|
return null
|
||||||
@@ -179,20 +191,58 @@ export class Matrix {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = {
|
export type BulkUpdateCommandArgs = {
|
||||||
fields: string[]
|
fields: string[]
|
||||||
next: string[]
|
next: any[]
|
||||||
prev: string[]
|
prev: any[]
|
||||||
setter: (fields: string[], values: string[]) => void
|
setter: (fields: string[], values: any[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BulkUpdateCommand implements Command {
|
export class BulkUpdateCommand implements Command {
|
||||||
private _fields: string[]
|
private _fields: string[]
|
||||||
|
|
||||||
private _prev: string[]
|
private _prev: any[]
|
||||||
private _next: string[]
|
private _next: any[]
|
||||||
|
|
||||||
private _setter: (string: string[], values: string[]) => void
|
private _setter: (fields: string[], any: string[]) => void
|
||||||
|
|
||||||
constructor({ fields, prev, next, setter }: BulkUpdateCommandArgs) {
|
constructor({ fields, prev, next, setter }: BulkUpdateCommandArgs) {
|
||||||
this._fields = fields
|
this._fields = fields
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { CellContext } from "@tanstack/react-table"
|
import { CellContext } from "@tanstack/react-table"
|
||||||
import React, { PropsWithChildren, ReactNode, RefObject } from "react"
|
import React, { PropsWithChildren, ReactNode, RefObject } from "react"
|
||||||
|
|
||||||
|
export type CellType = "text" | "number" | "select" | "boolean"
|
||||||
|
|
||||||
export type CellCoords = {
|
export type CellCoords = {
|
||||||
row: number
|
row: number
|
||||||
col: number
|
col: number
|
||||||
@@ -47,6 +49,7 @@ export interface InputProps {
|
|||||||
interface InnerProps {
|
interface InnerProps {
|
||||||
ref: RefObject<HTMLDivElement>
|
ref: RefObject<HTMLDivElement>
|
||||||
onMouseOver: ((e: React.MouseEvent<HTMLElement>) => void) | undefined
|
onMouseOver: ((e: React.MouseEvent<HTMLElement>) => void) | undefined
|
||||||
|
onMouseDown: ((e: React.MouseEvent<HTMLElement>) => void) | undefined
|
||||||
onKeyDown: (e: React.KeyboardEvent<HTMLDivElement>) => void
|
onKeyDown: (e: React.KeyboardEvent<HTMLDivElement>) => void
|
||||||
onFocus: (e: React.FocusEvent<HTMLElement>) => void
|
onFocus: (e: React.FocusEvent<HTMLElement>) => void
|
||||||
"data-container-id": string
|
"data-container-id": string
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
HeaderContext,
|
HeaderContext,
|
||||||
createColumnHelper,
|
createColumnHelper,
|
||||||
} from "@tanstack/react-table"
|
} from "@tanstack/react-table"
|
||||||
import { CellCoords, DataGridColumnType } from "./types"
|
import { CellCoords, CellType, DataGridColumnType } from "./types"
|
||||||
|
|
||||||
export function generateCellId(coords: CellCoords) {
|
export function generateCellId(coords: CellCoords) {
|
||||||
return `${coords.row}:${coords.col}`
|
return `${coords.row}:${coords.col}`
|
||||||
@@ -96,33 +96,54 @@ export function getFieldsInRange(
|
|||||||
return fields
|
return fields
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertArrayToPrimitive<
|
function convertToNumber(value: string | number): number {
|
||||||
T extends "boolean" | "number" | "string",
|
if (typeof value === "number") {
|
||||||
>(values: string[], type: T) {
|
return value
|
||||||
const convertedValues: any[] = []
|
}
|
||||||
|
|
||||||
for (const value of values) {
|
|
||||||
if (type === "number") {
|
|
||||||
const converted = Number(value)
|
const converted = Number(value)
|
||||||
|
|
||||||
if (isNaN(converted)) {
|
if (isNaN(converted)) {
|
||||||
throw new Error(`String "${value}" cannot be converted to number.`)
|
throw new Error(`String "${value}" cannot be converted to number.`)
|
||||||
}
|
}
|
||||||
convertedValues.push(converted)
|
|
||||||
} else if (type === "boolean") {
|
return converted
|
||||||
const lowerValue = value.toLowerCase()
|
}
|
||||||
if (lowerValue === "true" || lowerValue === "false") {
|
|
||||||
convertedValues.push(lowerValue === "true")
|
function convertToBoolean(value: string | boolean): boolean {
|
||||||
} else {
|
if (typeof value === "boolean") {
|
||||||
throw new Error(`String "${value}" cannot be converted to boolean.`)
|
return value
|
||||||
}
|
|
||||||
} else if (type === "string") {
|
|
||||||
convertedValues.push(String(value))
|
|
||||||
} else {
|
|
||||||
throw new Error(`Unsupported target type "${type}".`)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return convertedValues
|
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> = {
|
type DataGridHelperColumnsProps<TData> = {
|
||||||
@@ -142,31 +163,6 @@ type DataGridHelperColumnsProps<TData> = {
|
|||||||
* The cell template for the column.
|
* The cell template for the column.
|
||||||
*/
|
*/
|
||||||
cell: ColumnDefTemplate<CellContext<TData, unknown>> | undefined
|
cell: ColumnDefTemplate<CellContext<TData, unknown>> | undefined
|
||||||
/**
|
|
||||||
* The type of the column. This is used to for parsing the value of cells
|
|
||||||
* in the column in commands like copy and paste.
|
|
||||||
*/
|
|
||||||
type?: DataGridColumnType
|
|
||||||
/**
|
|
||||||
* Whether to only validate that the value can be converted to the desired
|
|
||||||
* type, but pass through the raw value to the form.
|
|
||||||
*
|
|
||||||
* An example of this might be a column with a type of "number" but the
|
|
||||||
* field is a string. This allows the commands to validate that the value
|
|
||||||
* can be converted to the desired type, but still pass through the raw
|
|
||||||
* value to the form.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* columnHelper.column({
|
|
||||||
* id: "price",
|
|
||||||
* // ...
|
|
||||||
* type: "number",
|
|
||||||
* asString: true,
|
|
||||||
* })
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
asString?: boolean
|
|
||||||
/**
|
/**
|
||||||
* Whether the column cannot be hidden by the user.
|
* Whether the column cannot be hidden by the user.
|
||||||
*
|
*
|
||||||
@@ -184,8 +180,6 @@ export function createDataGridHelper<TData>() {
|
|||||||
name,
|
name,
|
||||||
header,
|
header,
|
||||||
cell,
|
cell,
|
||||||
type = "string",
|
|
||||||
asString,
|
|
||||||
disableHiding = false,
|
disableHiding = false,
|
||||||
}: DataGridHelperColumnsProps<TData>) =>
|
}: DataGridHelperColumnsProps<TData>) =>
|
||||||
columnHelper.display({
|
columnHelper.display({
|
||||||
@@ -194,8 +188,6 @@ export function createDataGridHelper<TData>() {
|
|||||||
cell,
|
cell,
|
||||||
enableHiding: !disableHiding,
|
enableHiding: !disableHiding,
|
||||||
meta: {
|
meta: {
|
||||||
type,
|
|
||||||
asString,
|
|
||||||
name,
|
name,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
Whitespace-only changes.
+46
-11
@@ -2,6 +2,7 @@ import { HttpTypes } from "@medusajs/types"
|
|||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
import { UseFormReturn, useWatch } from "react-hook-form"
|
import { UseFormReturn, useWatch } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
import { DataGridBooleanCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-boolean-cell"
|
import { DataGridBooleanCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-boolean-cell"
|
||||||
import { DataGridReadOnlyCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-readonly-cell"
|
import { DataGridReadOnlyCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-readonly-cell"
|
||||||
import { DataGridTextCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-text-cell"
|
import { DataGridTextCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-text-cell"
|
||||||
@@ -12,6 +13,10 @@ 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"
|
||||||
import { useStore } from "../../../../../hooks/api/store"
|
import { useStore } from "../../../../../hooks/api/store"
|
||||||
|
import {
|
||||||
|
ProductCreateOptionSchema,
|
||||||
|
ProductCreateVariantSchema,
|
||||||
|
} from "../../constants"
|
||||||
import { ProductCreateSchemaType } from "../../types"
|
import { ProductCreateSchemaType } from "../../types"
|
||||||
|
|
||||||
type ProductCreateVariantsFormProps = {
|
type ProductCreateVariantsFormProps = {
|
||||||
@@ -21,11 +26,26 @@ type ProductCreateVariantsFormProps = {
|
|||||||
export const ProductCreateVariantsForm = ({
|
export const ProductCreateVariantsForm = ({
|
||||||
form,
|
form,
|
||||||
}: ProductCreateVariantsFormProps) => {
|
}: ProductCreateVariantsFormProps) => {
|
||||||
const { regions } = useRegions({ limit: 9999 })
|
const {
|
||||||
|
regions,
|
||||||
|
isPending: isRegionsPending,
|
||||||
|
isError: isRegionError,
|
||||||
|
error: regionError,
|
||||||
|
} = useRegions({ limit: 9999 })
|
||||||
|
|
||||||
const { store, isPending, isError, error } = useStore()
|
const {
|
||||||
|
store,
|
||||||
|
isPending: isStorePending,
|
||||||
|
isError: isStoreError,
|
||||||
|
error: storeError,
|
||||||
|
} = useStore()
|
||||||
|
|
||||||
const { price_preferences: pricePreferences } = usePricePreferences({})
|
const {
|
||||||
|
price_preferences,
|
||||||
|
isPending: isPricePreferencesPending,
|
||||||
|
isError: isPricePreferencesError,
|
||||||
|
error: pricePreferencesError,
|
||||||
|
} = usePricePreferences({})
|
||||||
|
|
||||||
const { setCloseOnEscape } = useRouteModal()
|
const { setCloseOnEscape } = useRouteModal()
|
||||||
|
|
||||||
@@ -53,7 +73,7 @@ export const ProductCreateVariantsForm = ({
|
|||||||
options,
|
options,
|
||||||
currencies: currencyCodes,
|
currencies: currencyCodes,
|
||||||
regions,
|
regions,
|
||||||
pricePreferences,
|
pricePreferences: price_preferences,
|
||||||
})
|
})
|
||||||
|
|
||||||
const variantData = useMemo(
|
const variantData = useMemo(
|
||||||
@@ -61,13 +81,29 @@ export const ProductCreateVariantsForm = ({
|
|||||||
[variants]
|
[variants]
|
||||||
)
|
)
|
||||||
|
|
||||||
if (isError) {
|
const isPending =
|
||||||
throw error
|
isRegionsPending ||
|
||||||
|
isStorePending ||
|
||||||
|
isPricePreferencesPending ||
|
||||||
|
!store ||
|
||||||
|
!regions ||
|
||||||
|
!price_preferences
|
||||||
|
|
||||||
|
if (isRegionError) {
|
||||||
|
throw regionError
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isStoreError) {
|
||||||
|
throw storeError
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPricePreferencesError) {
|
||||||
|
throw pricePreferencesError
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex size-full flex-col divide-y overflow-hidden">
|
<div className="flex size-full flex-col divide-y overflow-hidden">
|
||||||
{isPending && !store ? (
|
{isPending ? (
|
||||||
<div>Loading...</div>
|
<div>Loading...</div>
|
||||||
) : (
|
) : (
|
||||||
<DataGridRoot
|
<DataGridRoot
|
||||||
@@ -81,7 +117,7 @@ export const ProductCreateVariantsForm = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnHelper = createDataGridHelper<HttpTypes.AdminProductVariant>()
|
const columnHelper = createDataGridHelper<ProductCreateVariantSchema>()
|
||||||
|
|
||||||
const useColumns = ({
|
const useColumns = ({
|
||||||
options,
|
options,
|
||||||
@@ -89,7 +125,7 @@ const useColumns = ({
|
|||||||
regions = [],
|
regions = [],
|
||||||
pricePreferences = [],
|
pricePreferences = [],
|
||||||
}: {
|
}: {
|
||||||
options: any // CreateProductOptionSchemaType[]
|
options: ProductCreateOptionSchema[]
|
||||||
currencies?: string[]
|
currencies?: string[]
|
||||||
regions?: HttpTypes.AdminRegion[]
|
regions?: HttpTypes.AdminRegion[]
|
||||||
pricePreferences?: HttpTypes.AdminPricePreference[]
|
pricePreferences?: HttpTypes.AdminPricePreference[]
|
||||||
@@ -142,7 +178,6 @@ const useColumns = ({
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
columnHelper.column({
|
columnHelper.column({
|
||||||
id: "manage_inventory",
|
id: "manage_inventory",
|
||||||
name: t("fields.managedInventory"),
|
name: t("fields.managedInventory"),
|
||||||
@@ -188,7 +223,7 @@ const useColumns = ({
|
|||||||
type: "boolean",
|
type: "boolean",
|
||||||
}),
|
}),
|
||||||
|
|
||||||
...getPriceColumns<HttpTypes.AdminProductVariant>({
|
...getPriceColumns<ProductCreateVariantSchema>({
|
||||||
currencies,
|
currencies,
|
||||||
regions,
|
regions,
|
||||||
pricePreferences,
|
pricePreferences,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { decorateVariantsWithDefaultValues } from "./utils.ts"
|
|
||||||
import { optionalInt } from "../../../lib/validation.ts"
|
import { optionalInt } from "../../../lib/validation.ts"
|
||||||
|
import { decorateVariantsWithDefaultValues } from "./utils.ts"
|
||||||
|
|
||||||
export const MediaSchema = z.object({
|
export const MediaSchema = z.object({
|
||||||
id: z.string().optional(),
|
id: z.string().optional(),
|
||||||
@@ -9,45 +9,7 @@ export const MediaSchema = z.object({
|
|||||||
file: z.any().nullable(), // File
|
file: z.any().nullable(), // File
|
||||||
})
|
})
|
||||||
|
|
||||||
export const ProductCreateSchema = z
|
const ProductCreateVariantSchema = z.object({
|
||||||
.object({
|
|
||||||
title: z.string().min(1),
|
|
||||||
subtitle: z.string().optional(),
|
|
||||||
handle: z.string().optional(),
|
|
||||||
description: z.string().optional(),
|
|
||||||
discountable: z.boolean(),
|
|
||||||
type_id: z.string().optional(),
|
|
||||||
collection_id: z.string().optional(),
|
|
||||||
categories: z.array(z.string()),
|
|
||||||
tags: z.array(z.string()).optional(),
|
|
||||||
sales_channels: z
|
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
id: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.optional(),
|
|
||||||
origin_country: z.string().optional(),
|
|
||||||
material: z.string().optional(),
|
|
||||||
width: z.string().optional(),
|
|
||||||
length: z.string().optional(),
|
|
||||||
height: z.string().optional(),
|
|
||||||
weight: z.string().optional(),
|
|
||||||
mid_code: z.string().optional(),
|
|
||||||
hs_code: z.string().optional(),
|
|
||||||
options: z
|
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
title: z.string().min(1),
|
|
||||||
values: z.array(z.string()).min(1),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.min(1),
|
|
||||||
enable_variants: z.boolean(),
|
|
||||||
variants: z
|
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
should_create: z.boolean(),
|
should_create: z.boolean(),
|
||||||
is_default: z.boolean().optional(),
|
is_default: z.boolean().optional(),
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
@@ -78,9 +40,51 @@ export const ProductCreateSchema = z
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type ProductCreateVariantSchema = z.infer<
|
||||||
|
typeof ProductCreateVariantSchema
|
||||||
|
>
|
||||||
|
|
||||||
|
const ProductCreateOptionSchema = z.object({
|
||||||
|
title: z.string(),
|
||||||
|
values: z.array(z.string()).min(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type ProductCreateOptionSchema = z.infer<
|
||||||
|
typeof ProductCreateOptionSchema
|
||||||
|
>
|
||||||
|
|
||||||
|
export const ProductCreateSchema = z
|
||||||
|
.object({
|
||||||
|
title: z.string().min(1),
|
||||||
|
subtitle: z.string().optional(),
|
||||||
|
handle: z.string().optional(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
discountable: z.boolean(),
|
||||||
|
type_id: z.string().optional(),
|
||||||
|
collection_id: z.string().optional(),
|
||||||
|
categories: z.array(z.string()),
|
||||||
|
tags: z.array(z.string()).optional(),
|
||||||
|
sales_channels: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.min(1),
|
.optional(),
|
||||||
|
origin_country: z.string().optional(),
|
||||||
|
material: z.string().optional(),
|
||||||
|
width: z.string().optional(),
|
||||||
|
length: z.string().optional(),
|
||||||
|
height: z.string().optional(),
|
||||||
|
weight: z.string().optional(),
|
||||||
|
mid_code: z.string().optional(),
|
||||||
|
hs_code: z.string().optional(),
|
||||||
|
options: z.array(ProductCreateOptionSchema).min(1),
|
||||||
|
enable_variants: z.boolean(),
|
||||||
|
variants: z.array(ProductCreateVariantSchema).min(1),
|
||||||
media: z.array(MediaSchema).optional(),
|
media: z.array(MediaSchema).optional(),
|
||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
|
|||||||
@@ -4480,7 +4480,6 @@ __metadata:
|
|||||||
"@medusajs/ui": 3.0.0
|
"@medusajs/ui": 3.0.0
|
||||||
"@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
|
||||||
"@radix-ui/react-hover-card": 1.1.1
|
|
||||||
"@tanstack/react-query": ^5.28.14
|
"@tanstack/react-query": ^5.28.14
|
||||||
"@tanstack/react-table": 8.10.7
|
"@tanstack/react-table": 8.10.7
|
||||||
"@tanstack/react-virtual": ^3.8.3
|
"@tanstack/react-virtual": ^3.8.3
|
||||||
@@ -4501,6 +4500,7 @@ __metadata:
|
|||||||
match-sorter: ^6.3.4
|
match-sorter: ^6.3.4
|
||||||
postcss: ^8.4.33
|
postcss: ^8.4.33
|
||||||
prettier: ^3.1.1
|
prettier: ^3.1.1
|
||||||
|
prop-types: ^15.8.1
|
||||||
qs: ^6.12.0
|
qs: ^6.12.0
|
||||||
react: ^18.2.0
|
react: ^18.2.0
|
||||||
react-country-flag: ^3.1.0
|
react-country-flag: ^3.1.0
|
||||||
@@ -4510,7 +4510,6 @@ __metadata:
|
|||||||
react-i18next: 13.5.0
|
react-i18next: 13.5.0
|
||||||
react-jwt: ^1.2.0
|
react-jwt: ^1.2.0
|
||||||
react-nestable: ^3.0.2
|
react-nestable: ^3.0.2
|
||||||
react-resizable-panels: ^2.0.16
|
|
||||||
react-router-dom: 6.20.1
|
react-router-dom: 6.20.1
|
||||||
tailwindcss: ^3.4.1
|
tailwindcss: ^3.4.1
|
||||||
tsup: ^8.0.2
|
tsup: ^8.0.2
|
||||||
@@ -6572,33 +6571,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@radix-ui/react-hover-card@npm:1.1.1":
|
|
||||||
version: 1.1.1
|
|
||||||
resolution: "@radix-ui/react-hover-card@npm:1.1.1"
|
|
||||||
dependencies:
|
|
||||||
"@radix-ui/primitive": 1.1.0
|
|
||||||
"@radix-ui/react-compose-refs": 1.1.0
|
|
||||||
"@radix-ui/react-context": 1.1.0
|
|
||||||
"@radix-ui/react-dismissable-layer": 1.1.0
|
|
||||||
"@radix-ui/react-popper": 1.2.0
|
|
||||||
"@radix-ui/react-portal": 1.1.1
|
|
||||||
"@radix-ui/react-presence": 1.1.0
|
|
||||||
"@radix-ui/react-primitive": 2.0.0
|
|
||||||
"@radix-ui/react-use-controllable-state": 1.1.0
|
|
||||||
peerDependencies:
|
|
||||||
"@types/react": "*"
|
|
||||||
"@types/react-dom": "*"
|
|
||||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
peerDependenciesMeta:
|
|
||||||
"@types/react":
|
|
||||||
optional: true
|
|
||||||
"@types/react-dom":
|
|
||||||
optional: true
|
|
||||||
checksum: d27f89258caec7660113f6bdfcffdd8999c8ac25761eb099b467cdb6c2cdea14c2953a100130e212cfb2621f9718b6291e292be915280698e0cfd4c1b3ed29af
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@radix-ui/react-id@npm:1.0.0":
|
"@radix-ui/react-id@npm:1.0.0":
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
resolution: "@radix-ui/react-id@npm:1.0.0"
|
resolution: "@radix-ui/react-id@npm:1.0.0"
|
||||||
@@ -26316,16 +26288,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"react-resizable-panels@npm:^2.0.16":
|
|
||||||
version: 2.0.19
|
|
||||||
resolution: "react-resizable-panels@npm:2.0.19"
|
|
||||||
peerDependencies:
|
|
||||||
react: ^16.14.0 || ^17.0.0 || ^18.0.0
|
|
||||||
react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0
|
|
||||||
checksum: eb9cb511aec917895dba842cb933c9885ea510f752b4f3b8c358bf33be8b7b6bf2fc4a81db7a16977e6b09f614a14c6652f15232ff03bce68a8845dcf179abf7
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"react-router-dom@npm:6.20.1":
|
"react-router-dom@npm:6.20.1":
|
||||||
version: 6.20.1
|
version: 6.20.1
|
||||||
resolution: "react-router-dom@npm:6.20.1"
|
resolution: "react-router-dom@npm:6.20.1"
|
||||||
|
|||||||
Reference in new issue
Block a user