@declarion/react 0.2.15 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { $t as e, Qt as t, Zt as n, a as r, ft as i, i as a, n as o, r as s, t as c } from "./dataTableStyles-DQdH4Ilw.js";
1
+ import { $t as e, Qt as t, Zt as n, a as r, ft as i, i as a, n as o, r as s, t as c } from "./dataTableStyles-D3BsVlSi.js";
2
2
  import { useCallback as l, useMemo as u, useRef as d } from "react";
3
3
  import { jsx as f, jsxs as p } from "react/jsx-runtime";
4
4
  import { DndContext as m, PointerSensor as h, closestCenter as g, useSensor as _, useSensors as v } from "@dnd-kit/core";
@@ -258,4 +258,4 @@ function w({ row: t, columns: i, editableColumns: o, displayOnlyColumns: s, chil
258
258
  //#endregion
259
259
  export { C as DraggableChildrenTable };
260
260
 
261
- //# sourceMappingURL=DraggableChildrenTable-B4JD8tAe.js.map
261
+ //# sourceMappingURL=DraggableChildrenTable-C5Mzy-o2.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"DraggableChildrenTable-B4JD8tAe.js","names":[],"sources":["../src/components/detail-layout/DraggableChildrenTable.tsx"],"sourcesContent":["import { useCallback, useMemo, useRef } from \"react\";\nimport {\n DndContext,\n closestCenter,\n PointerSensor,\n useSensor,\n useSensors,\n type DragEndEvent,\n} from \"@dnd-kit/core\";\nimport {\n SortableContext,\n verticalListSortingStrategy,\n useSortable,\n} from \"@dnd-kit/sortable\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport { Btn, Icon, IconBtn } from \"@/components/primitives\";\nimport {\n dataTableStyle,\n dataTableHeadCellStyle,\n dataTableCellStyle,\n} from \"@/components/primitives/dataTableStyles\";\nimport { fieldDisplayName } from \"@/types/schema\";\nimport type { Entity, Schema } from \"@/types/schema\";\nimport { renderField } from \"@/components/fields\";\nimport type { RefsMap } from \"@/components/fields\";\nimport { useUserLocale } from \"@/hooks/useUserLocale\";\nimport type { PendingChildRow } from \"./LayoutRenderer\";\nimport type { ResolvedChildConfig } from \"@/lib/child-config\";\n\ninterface DraggableChildrenTableProps {\n config: ResolvedChildConfig;\n childEntity: Entity;\n schema: Schema;\n rows: PendingChildRow[];\n onChange: (rows: PendingChildRow[]) => void;\n validationErrors?: Record<string, string[]>;\n refs?: RefsMap;\n}\n\n// DraggableChildrenTable renders child rows with drag handles for reordering.\n// Used when position_field is configured on a child relation.\nexport function DraggableChildrenTable({\n config,\n childEntity,\n schema,\n rows,\n onChange,\n validationErrors,\n refs,\n}: DraggableChildrenTableProps) {\n const tableRef = useRef<HTMLTableElement>(null);\n const positionField = config.position_field!;\n const columns = config.columns;\n\n // distance: 5 separates click from drag.\n const sensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),\n );\n\n // Editable columns: exclude FK, auto, primary, and position_field.\n const fkSet = new Set(Array.isArray(config.foreign_key) ? config.foreign_key : [config.foreign_key]);\n const editableColumns = columns.filter((col) => {\n if (fkSet.has(col)) return false;\n if (col === positionField) return false;\n const field = childEntity.fields.get(col);\n if (!field) return false;\n if (field.auto || field.primary) return false;\n return true;\n });\n\n // Display-only columns (auto/primary but visible in column list, or position_field).\n const displayOnlyColumns = new Set(\n columns.filter((col) => {\n if (col === positionField) return true;\n const field = childEntity.fields.get(col);\n return field && (field.auto || field.primary);\n }),\n );\n\n // Visible columns: exclude position_field from display since it's managed by drag.\n const visibleColumns = columns.filter((col) => col !== positionField);\n\n // Row IDs for SortableContext - only non-deleted rows participate in drag.\n const rowIds = useMemo(\n () => rows.filter((r) => !r._deleted).map((r) => r._key),\n [rows],\n );\n\n const handleCellChange = useCallback(\n (rowKey: string, fieldName: string, value: unknown) => {\n const updated = rows.map((row) =>\n row._key === rowKey\n ? { ...row, [fieldName]: value, _dirty: true }\n : row,\n );\n onChange(updated);\n },\n [rows, onChange],\n );\n\n const handleAddRow = useCallback(() => {\n const maxPosition = rows\n .filter((r) => !r._deleted)\n .reduce((max, r) => {\n const pos = typeof r[positionField] === \"number\" ? (r[positionField] as number) : -1;\n return Math.max(max, pos);\n }, -1);\n\n const newRow: PendingChildRow = {\n _key: crypto.randomUUID(),\n _dirty: true,\n [positionField]: maxPosition + 1,\n };\n // Pre-fill from quick_add.defaults first, then field-level defaults.\n const quickDefaults = config.quick_add?.defaults ?? {};\n for (const col of editableColumns) {\n const field = childEntity.fields.get(col);\n if (!field) continue;\n if (quickDefaults[col] !== undefined) {\n newRow[col] = quickDefaults[col];\n } else if (field.default !== undefined) {\n newRow[col] = field.default;\n } else if (field.type === \"bool\") {\n newRow[col] = false;\n }\n }\n onChange([...rows, newRow]);\n }, [rows, onChange, editableColumns, childEntity, config.quick_add?.defaults, positionField]);\n\n const handleDeleteRow = useCallback(\n (rowKey: string) => {\n const updated = rows.map((row) =>\n row._key === rowKey ? { ...row, _deleted: true } : row,\n );\n onChange(updated);\n },\n [rows, onChange],\n );\n\n const handleUndoDelete = useCallback(\n (rowKey: string) => {\n const updated = rows.map((row) =>\n row._key === rowKey ? { ...row, _deleted: false } : row,\n );\n onChange(updated);\n },\n [rows, onChange],\n );\n\n const handleDragEnd = useCallback(\n (event: DragEndEvent) => {\n const { active, over } = event;\n if (!over || active.id === over.id) return;\n\n // Reorder non-deleted rows.\n const nonDeleted = rows.filter((r) => !r._deleted);\n const deleted = rows.filter((r) => r._deleted);\n\n const oldIndex = nonDeleted.findIndex((r) => r._key === String(active.id));\n const newIndex = nonDeleted.findIndex((r) => r._key === String(over.id));\n if (oldIndex === -1 || newIndex === -1) return;\n\n // Move the row.\n const reordered = [...nonDeleted];\n const [moved] = reordered.splice(oldIndex, 1);\n reordered.splice(newIndex, 0, moved);\n\n // Re-assign sequential position values and mark dirty only if position changed.\n const withPositions = reordered.map((row, index) => ({\n ...row,\n [positionField]: index,\n _dirty: row[positionField] !== index ? true : row._dirty,\n }));\n\n // Append deleted rows at the end (they'll be excluded from payload).\n onChange([...withPositions, ...deleted]);\n },\n [rows, onChange, positionField],\n );\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent<HTMLTableElement>) => {\n const target = e.target as HTMLElement;\n const cell = target.closest(\"td\");\n if (!cell) return;\n const row = cell.closest(\"tr\");\n if (!row) return;\n const tbody = row.closest(\"tbody\");\n if (!tbody) return;\n\n const cellIndex = Array.from(row.cells).indexOf(cell as HTMLTableCellElement);\n const rowIndex = Array.from(tbody.rows).indexOf(row as HTMLTableRowElement);\n\n if (e.key === \"Tab\") {\n const direction = e.shiftKey ? -1 : 1;\n const allRows = Array.from(tbody.rows);\n let nextRow = rowIndex;\n let nextCell = cellIndex + direction;\n\n while (nextRow >= 0 && nextRow < allRows.length) {\n if (nextCell >= 0 && nextCell < allRows[nextRow].cells.length) {\n const nextTd = allRows[nextRow].cells[nextCell];\n const input = nextTd.querySelector(\"input, select, textarea, [tabindex]\") as HTMLElement | null;\n if (input) {\n e.preventDefault();\n input.focus();\n return;\n }\n nextCell += direction;\n } else {\n nextRow += direction;\n nextCell = direction > 0 ? 0 : (allRows[nextRow]?.cells.length ?? 1) - 1;\n }\n }\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n const allRows = Array.from(tbody.rows);\n if (rowIndex < allRows.length - 1) {\n const nextTd = allRows[rowIndex + 1].cells[cellIndex];\n const input = nextTd?.querySelector(\"input, select, textarea, [tabindex]\") as HTMLElement | null;\n if (input) input.focus();\n } else {\n handleAddRow();\n requestAnimationFrame(() => {\n const newRows = tbody.querySelectorAll(\"tr\");\n const lastRow = newRows[newRows.length - 1];\n if (lastRow) {\n const firstInput = lastRow.querySelector(\"input, select, textarea, [tabindex]\") as HTMLElement | null;\n if (firstInput) firstInput.focus();\n }\n });\n }\n } else if (e.key === \"Escape\") {\n (target as HTMLElement).blur?.();\n }\n },\n [handleAddRow],\n );\n\n // Shared table chrome - see primitives/dataTableStyles.ts.\n const thStyle = dataTableHeadCellStyle;\n\n return (\n <div>\n <div style={{ overflowX: \"auto\" }}>\n <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>\n <table\n ref={tableRef}\n style={dataTableStyle}\n onKeyDown={handleKeyDown}\n >\n <thead>\n <tr>\n <th style={{ ...thStyle, width: 32, padding: \"0 4px\" }} />\n {visibleColumns.map((col) => {\n const field = childEntity.fields.get(col);\n return (\n <th key={col} style={thStyle}>\n {field ? fieldDisplayName(field, col, schema.entities) : col}\n </th>\n );\n })}\n <th style={{ ...thStyle, width: 40 }} />\n </tr>\n </thead>\n <SortableContext items={rowIds} strategy={verticalListSortingStrategy}>\n <tbody>\n {rows.map((row) => (\n <SortableRow\n key={row._key}\n row={row}\n columns={visibleColumns}\n editableColumns={editableColumns}\n displayOnlyColumns={displayOnlyColumns}\n childEntity={childEntity}\n validationErrors={validationErrors}\n refs={refs}\n onCellChange={handleCellChange}\n onDelete={handleDeleteRow}\n onUndoDelete={handleUndoDelete}\n />\n ))}\n </tbody>\n </SortableContext>\n </table>\n </DndContext>\n </div>\n <div style={{ marginTop: 8 }}>\n <Btn kind=\"ghost\" size=\"xs\" icon={<Icon.plus size={12} />} onClick={handleAddRow}>\n Add row\n </Btn>\n </div>\n </div>\n );\n}\n\n// SortableRow wraps a table row with drag-and-drop support.\ninterface SortableRowProps {\n row: PendingChildRow;\n columns: string[];\n editableColumns: string[];\n displayOnlyColumns: Set<string>;\n childEntity: Entity;\n validationErrors?: Record<string, string[]>;\n refs?: RefsMap;\n onCellChange: (rowKey: string, fieldName: string, value: unknown) => void;\n onDelete: (rowKey: string) => void;\n onUndoDelete: (rowKey: string) => void;\n}\n\nfunction SortableRow({\n row,\n columns,\n editableColumns,\n displayOnlyColumns,\n childEntity,\n validationErrors,\n refs,\n onCellChange,\n onDelete,\n onUndoDelete,\n}: SortableRowProps) {\n const userLocale = useUserLocale();\n const isDeleted = !!row._deleted;\n const rowErrors = validationErrors?.[row._key];\n\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: row._key, disabled: isDeleted });\n\n const style = {\n transform: CSS.Translate.toString(transform),\n transition,\n zIndex: isDragging ? 10 : undefined,\n opacity: isDragging ? 0.5 : undefined,\n };\n\n const cellStyle = dataTableCellStyle;\n const editCellStyle: React.CSSProperties = {\n padding: \"4px 4px\",\n borderBottom: \"1px solid var(--divider)\",\n verticalAlign: \"middle\",\n };\n\n return (\n <tr ref={setNodeRef} style={{ ...style, opacity: isDeleted ? 0.5 : style?.opacity ?? 1 }}>\n <td style={{ padding: \"4px 4px\", borderBottom: \"1px solid var(--divider)\", width: 32, verticalAlign: \"middle\" }}>\n {!isDeleted && (\n <button\n type=\"button\"\n style={{\n cursor: \"grab\",\n padding: 4,\n color: \"var(--text-3)\",\n background: \"transparent\",\n border: 0,\n display: \"inline-flex\",\n }}\n {...attributes}\n {...listeners}\n >\n <Icon.grid size={14} />\n </button>\n )}\n </td>\n {columns.map((col) => {\n const field = childEntity.fields.get(col);\n const hasError = rowErrors?.includes(col);\n const isDisplayOnly = displayOnlyColumns.has(col);\n const isEditable = editableColumns.includes(col);\n\n if (isDeleted || isDisplayOnly || !isEditable) {\n return (\n <td\n key={col}\n style={{\n ...cellStyle,\n textDecoration: isDeleted ? \"line-through\" : \"none\",\n }}\n >\n {field\n ? renderField({\n field,\n fieldName: col,\n value: row[col],\n mode: \"display\",\n record: row as Record<string, unknown>,\n refs,\n locale: userLocale,\n })\n : String(row[col] ?? \"\")}\n </td>\n );\n }\n\n return (\n <td\n key={col}\n style={{\n ...editCellStyle,\n outline: hasError ? \"1px solid var(--danger)\" : \"none\",\n outlineOffset: -1,\n borderRadius: hasError ? 4 : 0,\n }}\n title={hasError ? `${col} is required` : undefined}\n >\n {field\n ? renderField({\n field,\n fieldName: col,\n value: row[col],\n mode: \"edit\",\n onChange: (val) => onCellChange(row._key, col, val),\n record: row as Record<string, unknown>,\n refs,\n locale: userLocale,\n })\n : String(row[col] ?? \"\")}\n </td>\n );\n })}\n <td style={{ padding: \"4px 8px\", borderBottom: \"1px solid var(--divider)\", verticalAlign: \"middle\" }}>\n {isDeleted ? (\n <IconBtn\n size={24}\n icon={<Icon.refresh size={12} />}\n onClick={() => onUndoDelete(row._key)}\n title=\"Undo delete\"\n />\n ) : (\n <IconBtn\n size={24}\n icon={<Icon.trash size={12} />}\n onClick={() => onDelete(row._key)}\n title=\"Delete row\"\n />\n )}\n </td>\n </tr>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"DraggableChildrenTable-C5Mzy-o2.js","names":[],"sources":["../src/components/detail-layout/DraggableChildrenTable.tsx"],"sourcesContent":["import { useCallback, useMemo, useRef } from \"react\";\nimport {\n DndContext,\n closestCenter,\n PointerSensor,\n useSensor,\n useSensors,\n type DragEndEvent,\n} from \"@dnd-kit/core\";\nimport {\n SortableContext,\n verticalListSortingStrategy,\n useSortable,\n} from \"@dnd-kit/sortable\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport { Btn, Icon, IconBtn } from \"@/components/primitives\";\nimport {\n dataTableStyle,\n dataTableHeadCellStyle,\n dataTableCellStyle,\n} from \"@/components/primitives/dataTableStyles\";\nimport { fieldDisplayName } from \"@/types/schema\";\nimport type { Entity, Schema } from \"@/types/schema\";\nimport { renderField } from \"@/components/fields\";\nimport type { RefsMap } from \"@/components/fields\";\nimport { useUserLocale } from \"@/hooks/useUserLocale\";\nimport type { PendingChildRow } from \"./LayoutRenderer\";\nimport type { ResolvedChildConfig } from \"@/lib/child-config\";\n\ninterface DraggableChildrenTableProps {\n config: ResolvedChildConfig;\n childEntity: Entity;\n schema: Schema;\n rows: PendingChildRow[];\n onChange: (rows: PendingChildRow[]) => void;\n validationErrors?: Record<string, string[]>;\n refs?: RefsMap;\n}\n\n// DraggableChildrenTable renders child rows with drag handles for reordering.\n// Used when position_field is configured on a child relation.\nexport function DraggableChildrenTable({\n config,\n childEntity,\n schema,\n rows,\n onChange,\n validationErrors,\n refs,\n}: DraggableChildrenTableProps) {\n const tableRef = useRef<HTMLTableElement>(null);\n const positionField = config.position_field!;\n const columns = config.columns;\n\n // distance: 5 separates click from drag.\n const sensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),\n );\n\n // Editable columns: exclude FK, auto, primary, and position_field.\n const fkSet = new Set(Array.isArray(config.foreign_key) ? config.foreign_key : [config.foreign_key]);\n const editableColumns = columns.filter((col) => {\n if (fkSet.has(col)) return false;\n if (col === positionField) return false;\n const field = childEntity.fields.get(col);\n if (!field) return false;\n if (field.auto || field.primary) return false;\n return true;\n });\n\n // Display-only columns (auto/primary but visible in column list, or position_field).\n const displayOnlyColumns = new Set(\n columns.filter((col) => {\n if (col === positionField) return true;\n const field = childEntity.fields.get(col);\n return field && (field.auto || field.primary);\n }),\n );\n\n // Visible columns: exclude position_field from display since it's managed by drag.\n const visibleColumns = columns.filter((col) => col !== positionField);\n\n // Row IDs for SortableContext - only non-deleted rows participate in drag.\n const rowIds = useMemo(\n () => rows.filter((r) => !r._deleted).map((r) => r._key),\n [rows],\n );\n\n const handleCellChange = useCallback(\n (rowKey: string, fieldName: string, value: unknown) => {\n const updated = rows.map((row) =>\n row._key === rowKey\n ? { ...row, [fieldName]: value, _dirty: true }\n : row,\n );\n onChange(updated);\n },\n [rows, onChange],\n );\n\n const handleAddRow = useCallback(() => {\n const maxPosition = rows\n .filter((r) => !r._deleted)\n .reduce((max, r) => {\n const pos = typeof r[positionField] === \"number\" ? (r[positionField] as number) : -1;\n return Math.max(max, pos);\n }, -1);\n\n const newRow: PendingChildRow = {\n _key: crypto.randomUUID(),\n _dirty: true,\n [positionField]: maxPosition + 1,\n };\n // Pre-fill from quick_add.defaults first, then field-level defaults.\n const quickDefaults = config.quick_add?.defaults ?? {};\n for (const col of editableColumns) {\n const field = childEntity.fields.get(col);\n if (!field) continue;\n if (quickDefaults[col] !== undefined) {\n newRow[col] = quickDefaults[col];\n } else if (field.default !== undefined) {\n newRow[col] = field.default;\n } else if (field.type === \"bool\") {\n newRow[col] = false;\n }\n }\n onChange([...rows, newRow]);\n }, [rows, onChange, editableColumns, childEntity, config.quick_add?.defaults, positionField]);\n\n const handleDeleteRow = useCallback(\n (rowKey: string) => {\n const updated = rows.map((row) =>\n row._key === rowKey ? { ...row, _deleted: true } : row,\n );\n onChange(updated);\n },\n [rows, onChange],\n );\n\n const handleUndoDelete = useCallback(\n (rowKey: string) => {\n const updated = rows.map((row) =>\n row._key === rowKey ? { ...row, _deleted: false } : row,\n );\n onChange(updated);\n },\n [rows, onChange],\n );\n\n const handleDragEnd = useCallback(\n (event: DragEndEvent) => {\n const { active, over } = event;\n if (!over || active.id === over.id) return;\n\n // Reorder non-deleted rows.\n const nonDeleted = rows.filter((r) => !r._deleted);\n const deleted = rows.filter((r) => r._deleted);\n\n const oldIndex = nonDeleted.findIndex((r) => r._key === String(active.id));\n const newIndex = nonDeleted.findIndex((r) => r._key === String(over.id));\n if (oldIndex === -1 || newIndex === -1) return;\n\n // Move the row.\n const reordered = [...nonDeleted];\n const [moved] = reordered.splice(oldIndex, 1);\n reordered.splice(newIndex, 0, moved);\n\n // Re-assign sequential position values and mark dirty only if position changed.\n const withPositions = reordered.map((row, index) => ({\n ...row,\n [positionField]: index,\n _dirty: row[positionField] !== index ? true : row._dirty,\n }));\n\n // Append deleted rows at the end (they'll be excluded from payload).\n onChange([...withPositions, ...deleted]);\n },\n [rows, onChange, positionField],\n );\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent<HTMLTableElement>) => {\n const target = e.target as HTMLElement;\n const cell = target.closest(\"td\");\n if (!cell) return;\n const row = cell.closest(\"tr\");\n if (!row) return;\n const tbody = row.closest(\"tbody\");\n if (!tbody) return;\n\n const cellIndex = Array.from(row.cells).indexOf(cell as HTMLTableCellElement);\n const rowIndex = Array.from(tbody.rows).indexOf(row as HTMLTableRowElement);\n\n if (e.key === \"Tab\") {\n const direction = e.shiftKey ? -1 : 1;\n const allRows = Array.from(tbody.rows);\n let nextRow = rowIndex;\n let nextCell = cellIndex + direction;\n\n while (nextRow >= 0 && nextRow < allRows.length) {\n if (nextCell >= 0 && nextCell < allRows[nextRow].cells.length) {\n const nextTd = allRows[nextRow].cells[nextCell];\n const input = nextTd.querySelector(\"input, select, textarea, [tabindex]\") as HTMLElement | null;\n if (input) {\n e.preventDefault();\n input.focus();\n return;\n }\n nextCell += direction;\n } else {\n nextRow += direction;\n nextCell = direction > 0 ? 0 : (allRows[nextRow]?.cells.length ?? 1) - 1;\n }\n }\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n const allRows = Array.from(tbody.rows);\n if (rowIndex < allRows.length - 1) {\n const nextTd = allRows[rowIndex + 1].cells[cellIndex];\n const input = nextTd?.querySelector(\"input, select, textarea, [tabindex]\") as HTMLElement | null;\n if (input) input.focus();\n } else {\n handleAddRow();\n requestAnimationFrame(() => {\n const newRows = tbody.querySelectorAll(\"tr\");\n const lastRow = newRows[newRows.length - 1];\n if (lastRow) {\n const firstInput = lastRow.querySelector(\"input, select, textarea, [tabindex]\") as HTMLElement | null;\n if (firstInput) firstInput.focus();\n }\n });\n }\n } else if (e.key === \"Escape\") {\n (target as HTMLElement).blur?.();\n }\n },\n [handleAddRow],\n );\n\n // Shared table chrome - see primitives/dataTableStyles.ts.\n const thStyle = dataTableHeadCellStyle;\n\n return (\n <div>\n <div style={{ overflowX: \"auto\" }}>\n <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>\n <table\n ref={tableRef}\n style={dataTableStyle}\n onKeyDown={handleKeyDown}\n >\n <thead>\n <tr>\n <th style={{ ...thStyle, width: 32, padding: \"0 4px\" }} />\n {visibleColumns.map((col) => {\n const field = childEntity.fields.get(col);\n return (\n <th key={col} style={thStyle}>\n {field ? fieldDisplayName(field, col, schema.entities) : col}\n </th>\n );\n })}\n <th style={{ ...thStyle, width: 40 }} />\n </tr>\n </thead>\n <SortableContext items={rowIds} strategy={verticalListSortingStrategy}>\n <tbody>\n {rows.map((row) => (\n <SortableRow\n key={row._key}\n row={row}\n columns={visibleColumns}\n editableColumns={editableColumns}\n displayOnlyColumns={displayOnlyColumns}\n childEntity={childEntity}\n validationErrors={validationErrors}\n refs={refs}\n onCellChange={handleCellChange}\n onDelete={handleDeleteRow}\n onUndoDelete={handleUndoDelete}\n />\n ))}\n </tbody>\n </SortableContext>\n </table>\n </DndContext>\n </div>\n <div style={{ marginTop: 8 }}>\n <Btn kind=\"ghost\" size=\"xs\" icon={<Icon.plus size={12} />} onClick={handleAddRow}>\n Add row\n </Btn>\n </div>\n </div>\n );\n}\n\n// SortableRow wraps a table row with drag-and-drop support.\ninterface SortableRowProps {\n row: PendingChildRow;\n columns: string[];\n editableColumns: string[];\n displayOnlyColumns: Set<string>;\n childEntity: Entity;\n validationErrors?: Record<string, string[]>;\n refs?: RefsMap;\n onCellChange: (rowKey: string, fieldName: string, value: unknown) => void;\n onDelete: (rowKey: string) => void;\n onUndoDelete: (rowKey: string) => void;\n}\n\nfunction SortableRow({\n row,\n columns,\n editableColumns,\n displayOnlyColumns,\n childEntity,\n validationErrors,\n refs,\n onCellChange,\n onDelete,\n onUndoDelete,\n}: SortableRowProps) {\n const userLocale = useUserLocale();\n const isDeleted = !!row._deleted;\n const rowErrors = validationErrors?.[row._key];\n\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: row._key, disabled: isDeleted });\n\n const style = {\n transform: CSS.Translate.toString(transform),\n transition,\n zIndex: isDragging ? 10 : undefined,\n opacity: isDragging ? 0.5 : undefined,\n };\n\n const cellStyle = dataTableCellStyle;\n const editCellStyle: React.CSSProperties = {\n padding: \"4px 4px\",\n borderBottom: \"1px solid var(--divider)\",\n verticalAlign: \"middle\",\n };\n\n return (\n <tr ref={setNodeRef} style={{ ...style, opacity: isDeleted ? 0.5 : style?.opacity ?? 1 }}>\n <td style={{ padding: \"4px 4px\", borderBottom: \"1px solid var(--divider)\", width: 32, verticalAlign: \"middle\" }}>\n {!isDeleted && (\n <button\n type=\"button\"\n style={{\n cursor: \"grab\",\n padding: 4,\n color: \"var(--text-3)\",\n background: \"transparent\",\n border: 0,\n display: \"inline-flex\",\n }}\n {...attributes}\n {...listeners}\n >\n <Icon.grid size={14} />\n </button>\n )}\n </td>\n {columns.map((col) => {\n const field = childEntity.fields.get(col);\n const hasError = rowErrors?.includes(col);\n const isDisplayOnly = displayOnlyColumns.has(col);\n const isEditable = editableColumns.includes(col);\n\n if (isDeleted || isDisplayOnly || !isEditable) {\n return (\n <td\n key={col}\n style={{\n ...cellStyle,\n textDecoration: isDeleted ? \"line-through\" : \"none\",\n }}\n >\n {field\n ? renderField({\n field,\n fieldName: col,\n value: row[col],\n mode: \"display\",\n record: row as Record<string, unknown>,\n refs,\n locale: userLocale,\n })\n : String(row[col] ?? \"\")}\n </td>\n );\n }\n\n return (\n <td\n key={col}\n style={{\n ...editCellStyle,\n outline: hasError ? \"1px solid var(--danger)\" : \"none\",\n outlineOffset: -1,\n borderRadius: hasError ? 4 : 0,\n }}\n title={hasError ? `${col} is required` : undefined}\n >\n {field\n ? renderField({\n field,\n fieldName: col,\n value: row[col],\n mode: \"edit\",\n onChange: (val) => onCellChange(row._key, col, val),\n record: row as Record<string, unknown>,\n refs,\n locale: userLocale,\n })\n : String(row[col] ?? \"\")}\n </td>\n );\n })}\n <td style={{ padding: \"4px 8px\", borderBottom: \"1px solid var(--divider)\", verticalAlign: \"middle\" }}>\n {isDeleted ? (\n <IconBtn\n size={24}\n icon={<Icon.refresh size={12} />}\n onClick={() => onUndoDelete(row._key)}\n title=\"Undo delete\"\n />\n ) : (\n <IconBtn\n size={24}\n icon={<Icon.trash size={12} />}\n onClick={() => onDelete(row._key)}\n title=\"Delete row\"\n />\n )}\n </td>\n </tr>\n );\n}\n"],"mappings":"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"}
@@ -23,10 +23,11 @@ export interface ExportResponse {
23
23
  export declare function exportEntities(entity: string, body: ExportRequest): Promise<ExportResponse>;
24
24
  /**
25
25
  * Action request body is flat: top-level keys are handler params. The only
26
- * reserved control key is `_ids` (array of object IDs for single/batch
27
- * scope). The server resolves entity from the action's registration; the
28
- * URL code is fully qualified ("entity.action" for entity-scoped,
29
- * "action" for global).
26
+ * reserved control key is `object_ids` (array of object IDs for
27
+ * single/batch scope). The server resolves entity from the action's
28
+ * registration; the URL code is fully qualified ("entity.action" for
29
+ * entity-scoped, "action" for global). Legacy `_ids` is rejected 400 by
30
+ * the server (parseActionBody) — there is no compat shim.
30
31
  */
31
32
  export type ActionExecuteRequest = Record<string, unknown>;
32
33
  export interface ActionExecuteResponse {
@@ -67,8 +68,14 @@ export declare class ActionBatchError extends Error {
67
68
  }
68
69
  /**
69
70
  * Build a flat action body from handler params + optional object IDs.
70
- * Promotes `ids` to the reserved `_ids` key. Throws if a caller passes a
71
- * param literally named `_ids` (collision with the reserved control key).
71
+ * Promotes `ids` to the reserved `object_ids` key. Throws if a caller
72
+ * passes a param literally named `object_ids` (collision with the
73
+ * reserved control key) or the now-rejected legacy `_ids` key.
74
+ *
75
+ * Plan 2026-06-01-write-api-final: the wire-level reserved key is
76
+ * `object_ids` (server-side parseActionBody decodes it into
77
+ * ctx.ObjectIDs). The legacy `_ids` envelope was removed in Task 5;
78
+ * passing it triggers a 400 from the server.
72
79
  */
73
80
  export declare function buildActionBody(params: Record<string, unknown> | undefined, ids?: string[] | undefined): ActionExecuteRequest;
74
81
  /**
@@ -38,29 +38,88 @@ export declare class EntityClient<T extends Record<string, unknown> = Record<str
38
38
  }>;
39
39
  /** Create a single record. Wraps in array, returns unwrapped result. */
40
40
  create(item: Partial<T> & Record<string, unknown>): Promise<T & EnrichedRow & RowHelpers>;
41
- /** Create multiple records in one request. */
41
+ /**
42
+ * Create multiple records in one transactional batch.
43
+ *
44
+ * Plan 2026-06-01-write-api-final §1.2 + Non-Obvious Knowledge:
45
+ * `data.create` is one-row-per-dispatch on the new envelope; N independent
46
+ * inserts compose through the platform batch primitive (`system.batch`,
47
+ * atomic READ COMMITTED tx). Each child op targets `<entity>.__create`
48
+ * with a FLAT `{entity, fields}` body inside `BatchOp.params`.
49
+ *
50
+ * Returns rows in input order. The batch fails atomically: any per-op
51
+ * error surfaces as a thrown Error with the first failing op's code +
52
+ * message; `committed: false` (4xx/200-soft-fail) means no row was
53
+ * written. The HTTP transport itself failing (5xx, network) keeps
54
+ * propagating as ApiError unchanged.
55
+ */
42
56
  createMany(items: (Partial<T> & Record<string, unknown>)[]): Promise<(T & EnrichedRow & RowHelpers)[]>;
43
57
  /** Update a single record. Wraps in array, returns unwrapped result. */
44
58
  update(id: string, data: Partial<T> & Record<string, unknown>): Promise<T & EnrichedRow & RowHelpers>;
45
- /** Update multiple records in one request. Each item must include its id. */
59
+ /**
60
+ * Update multiple records in one request. Each item must include its id.
61
+ *
62
+ * Plan 2026-06-01-write-api-final §1.2 + Non-Obvious Knowledge:
63
+ * `data.update` is one-fields-map-per-dispatch on the new envelope
64
+ * ({object_ids, entity, fields}); per-row variation composes through
65
+ * `system.batch` (atomic READ COMMITTED tx) — one __update child op per
66
+ * input item, mirroring createMany / upsertMany. The batch is atomic:
67
+ * any per-op failure rolls back all rows. The first per-op error
68
+ * surfaces as a thrown Error.
69
+ *
70
+ * Returns rows in input order.
71
+ */
46
72
  updateMany(items: (Partial<T> & Record<string, unknown> & {
47
73
  id: string;
48
74
  })[]): Promise<(T & EnrichedRow & RowHelpers)[]>;
49
75
  /** Delete a single record by ID. */
50
76
  delete(id: string): Promise<void>;
51
- /** Delete multiple records in one request. Returns the count of deleted records. */
77
+ /**
78
+ * Delete multiple records in one request. Returns the count of deleted
79
+ * records.
80
+ *
81
+ * Plan 2026-06-01-write-api-final §1.2: dispatches
82
+ * /api/actions/<entity>.__delete with a FLAT envelope
83
+ * ({object_ids, entity}). The reserved `object_ids` key lands directly
84
+ * in ctx.ObjectIDs; the dispatcher derives items from those + the
85
+ * entity's PK fields. NO legacy `_ids` on the wire (rejected 400 by
86
+ * parseActionBody as of Task 5).
87
+ */
52
88
  deleteMany(ids: string[]): Promise<{
53
89
  count: number;
54
90
  }>;
55
91
  /** Restore a single soft-deleted record by ID. */
56
92
  restore(id: string): Promise<void>;
57
- /** Restore multiple soft-deleted records. Returns the count of restored records. */
93
+ /**
94
+ * Restore multiple soft-deleted records. Returns the count of restored
95
+ * records.
96
+ *
97
+ * Plan 2026-06-01-write-api-final §1.2: routes through the platform-
98
+ * default `__restore` action. The legacy CRUD route
99
+ * POST /api/data/{entity}/restore is retired (returns 410). The FLAT
100
+ * envelope ({object_ids, entity}) lands directly in DataRestoreParams
101
+ * via JSON tags; no `params:` wrapper on the wire.
102
+ */
58
103
  restoreMany(ids: string[]): Promise<{
59
104
  count: number;
60
105
  }>;
61
106
  /** Upsert a single record. Wraps in array, returns unwrapped result. */
62
107
  upsert(item: Partial<T> & Record<string, unknown>, uniqueFields: string[]): Promise<T & EnrichedRow & RowHelpers>;
63
- /** Upsert multiple records. Existing matches (by unique_fields) are updated. */
108
+ /**
109
+ * Upsert multiple records. Existing matches (by uniqueFields) are
110
+ * updated; absent rows are inserted.
111
+ *
112
+ * Plan 2026-06-01-write-api-final §1.2 + OQ-1: `data.upsert` is one-row-
113
+ * per-dispatch on the new envelope (per-row uniqueness matching), so N
114
+ * rows compose through `system.batch`. Each child op targets
115
+ * `<entity>.__upsert` with FLAT `{entity, fields, unique_by}` params.
116
+ * The handler returns `DataUpsertResult.rows[]` where each
117
+ * DataUpsertRowResult carries `{pk, action, row}`; the row payload is
118
+ * extracted to preserve the prior API shape (array of enriched rows).
119
+ *
120
+ * The batch is atomic: any per-op failure rolls back all rows. The
121
+ * first per-op error surfaces as a thrown Error.
122
+ */
64
123
  upsertMany(items: (Partial<T> & Record<string, unknown>)[], uniqueFields: string[]): Promise<(T & EnrichedRow & RowHelpers)[]>;
65
124
  }
66
125
  /**
@@ -13,9 +13,13 @@ export declare function fetchPublicParams(): Promise<PublicParamsResponse>;
13
13
  */
14
14
  export declare function fetchParam(code: string): Promise<ParamLookup>;
15
15
  /**
16
- * Persist a parameter override at user scope. Writes via the existing
17
- * subresource $params channel on the user entity (PATCH /api/data/user).
18
- * Returns the updated row envelope.
16
+ * Persist a parameter override at user scope. Writes via the platform-default
17
+ * user.__update action endpoint (POST /api/actions/user.__update) with the
18
+ * $params subresource block inside `fields`. Per plan 2026-06-01-write-api-final
19
+ * §1.2 the wire envelope is FLAT: `object_ids` is the only reserved key, and
20
+ * `entity` + `fields` sit at the top level (decoded into DataUpdateParams via
21
+ * JSON tags). subresource.ExtractSubResources routes the $params array to
22
+ * declarion.object_params at write time.
19
23
  *
20
24
  * Plan G5/T23 contract: ParameterDef.scopes gates the write server-side;
21
25
  * a 400 will surface back to the caller if the scope is not allowed.