@declarion/react 0.1.83 → 0.1.85

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,18 +1,18 @@
1
- import { Gt as e, Ut as t, Wt as n, at as r, n as i, t as a } from "./useUserLocale-CSS6fwi8.js";
1
+ import { Gt as e, Kt as t, Wt as n, n as r, ot as i, t as a } from "./useUserLocale-B3bnZHMZ.js";
2
2
  import { useCallback as o, useMemo as s, useRef as c } from "react";
3
3
  import { jsx as l, jsxs as u } from "react/jsx-runtime";
4
4
  import { DndContext as d, PointerSensor as f, closestCenter as p, useSensor as m, useSensors as h } from "@dnd-kit/core";
5
5
  import { SortableContext as g, useSortable as _, verticalListSortingStrategy as v } from "@dnd-kit/sortable";
6
6
  import { CSS as y } from "@dnd-kit/utilities";
7
7
  //#region src/components/detail-layout/DraggableChildrenTable.tsx
8
- function b({ config: t, childEntity: i, schema: a, rows: _, onChange: y, validationErrors: b, refs: S }) {
9
- let C = c(null), w = t.position_field, T = t.columns, E = h(m(f, { activationConstraint: { distance: 5 } })), D = new Set(Array.isArray(t.foreign_key) ? t.foreign_key : [t.foreign_key]), O = T.filter((e) => {
8
+ function b({ config: n, childEntity: r, schema: a, rows: _, onChange: y, validationErrors: b, refs: S }) {
9
+ let C = c(null), w = n.position_field, T = n.columns, E = h(m(f, { activationConstraint: { distance: 5 } })), D = new Set(Array.isArray(n.foreign_key) ? n.foreign_key : [n.foreign_key]), O = T.filter((e) => {
10
10
  if (D.has(e) || e === w) return !1;
11
- let t = i.fields.get(e);
11
+ let t = r.fields.get(e);
12
12
  return !(!t || t.auto || t.primary);
13
13
  }), k = new Set(T.filter((e) => {
14
14
  if (e === w) return !0;
15
- let t = i.fields.get(e);
15
+ let t = r.fields.get(e);
16
16
  return t && (t.auto || t.primary);
17
17
  })), A = T.filter((e) => e !== w), j = s(() => _.filter((e) => !e._deleted).map((e) => e._key), [_]), M = o((e, t, n) => {
18
18
  y(_.map((r) => r._key === e ? {
@@ -24,22 +24,22 @@ function b({ config: t, childEntity: i, schema: a, rows: _, onChange: y, validat
24
24
  let e = _.filter((e) => !e._deleted).reduce((e, t) => {
25
25
  let n = typeof t[w] == "number" ? t[w] : -1;
26
26
  return Math.max(e, n);
27
- }, -1), n = {
27
+ }, -1), t = {
28
28
  _key: crypto.randomUUID(),
29
29
  _dirty: !0,
30
30
  [w]: e + 1
31
- }, r = t.quick_add?.defaults ?? {};
31
+ }, i = n.quick_add?.defaults ?? {};
32
32
  for (let e of O) {
33
- let t = i.fields.get(e);
34
- t && (r[e] === void 0 ? t.default === void 0 ? t.type === "bool" && (n[e] = !1) : n[e] = t.default : n[e] = r[e]);
33
+ let n = r.fields.get(e);
34
+ n && (i[e] === void 0 ? n.default === void 0 ? n.type === "bool" && (t[e] = !1) : t[e] = n.default : t[e] = i[e]);
35
35
  }
36
- y([..._, n]);
36
+ y([..._, t]);
37
37
  }, [
38
38
  _,
39
39
  y,
40
40
  O,
41
- i,
42
- t.quick_add?.defaults,
41
+ r,
42
+ n.quick_add?.defaults,
43
43
  w
44
44
  ]), P = o((e) => {
45
45
  y(_.map((t) => t._key === e ? {
@@ -131,10 +131,10 @@ function b({ config: t, childEntity: i, schema: a, rows: _, onChange: y, validat
131
131
  padding: "0 4px"
132
132
  } }),
133
133
  A.map((e) => {
134
- let t = i.fields.get(e);
134
+ let t = r.fields.get(e);
135
135
  return /* @__PURE__ */ l("th", {
136
136
  style: R,
137
- children: t ? r(t, e, a.entities) : e
137
+ children: t ? i(t, e, a.entities) : e
138
138
  }, e);
139
139
  }),
140
140
  /* @__PURE__ */ l("th", { style: {
@@ -149,7 +149,7 @@ function b({ config: t, childEntity: i, schema: a, rows: _, onChange: y, validat
149
149
  columns: A,
150
150
  editableColumns: O,
151
151
  displayOnlyColumns: k,
152
- childEntity: i,
152
+ childEntity: r,
153
153
  validationErrors: b,
154
154
  refs: S,
155
155
  onCellChange: M,
@@ -161,18 +161,18 @@ function b({ config: t, childEntity: i, schema: a, rows: _, onChange: y, validat
161
161
  })
162
162
  }), /* @__PURE__ */ l("div", {
163
163
  style: { marginTop: 8 },
164
- children: /* @__PURE__ */ l(n, {
164
+ children: /* @__PURE__ */ l(e, {
165
165
  kind: "ghost",
166
166
  size: "xs",
167
- icon: /* @__PURE__ */ l(e.plus, { size: 12 }),
167
+ icon: /* @__PURE__ */ l(t.plus, { size: 12 }),
168
168
  onClick: N,
169
169
  children: "Add row"
170
170
  })
171
171
  })] });
172
172
  }
173
- function x({ row: n, columns: r, editableColumns: o, displayOnlyColumns: s, childEntity: c, validationErrors: d, refs: f, onCellChange: p, onDelete: m, onUndoDelete: h }) {
174
- let g = a(), v = !!n._deleted, b = d?.[n._key], { attributes: x, listeners: S, setNodeRef: C, transform: w, transition: T, isDragging: E } = _({
175
- id: n._key,
173
+ function x({ row: e, columns: i, editableColumns: o, displayOnlyColumns: s, childEntity: c, validationErrors: d, refs: f, onCellChange: p, onDelete: m, onUndoDelete: h }) {
174
+ let g = a(), v = !!e._deleted, b = d?.[e._key], { attributes: x, listeners: S, setNodeRef: C, transform: w, transition: T, isDragging: E } = _({
175
+ id: e._key,
176
176
  disabled: v
177
177
  }), D = {
178
178
  transform: y.Translate.toString(w),
@@ -215,44 +215,44 @@ function x({ row: n, columns: r, editableColumns: o, displayOnlyColumns: s, chil
215
215
  },
216
216
  ...x,
217
217
  ...S,
218
- children: /* @__PURE__ */ l(e.grid, { size: 14 })
218
+ children: /* @__PURE__ */ l(t.grid, { size: 14 })
219
219
  })
220
220
  }),
221
- r.map((e) => {
222
- let t = c.fields.get(e), r = b?.includes(e), a = s.has(e), u = o.includes(e);
221
+ i.map((t) => {
222
+ let n = c.fields.get(t), i = b?.includes(t), a = s.has(t), u = o.includes(t);
223
223
  return v || a || !u ? /* @__PURE__ */ l("td", {
224
224
  style: {
225
225
  ...O,
226
226
  textDecoration: v ? "line-through" : "none"
227
227
  },
228
- children: t ? i({
229
- field: t,
230
- fieldName: e,
231
- value: n[e],
228
+ children: n ? r({
229
+ field: n,
230
+ fieldName: t,
231
+ value: e[t],
232
232
  mode: "display",
233
- record: n,
233
+ record: e,
234
234
  refs: f,
235
235
  locale: g
236
- }) : String(n[e] ?? "")
237
- }, e) : /* @__PURE__ */ l("td", {
236
+ }) : String(e[t] ?? "")
237
+ }, t) : /* @__PURE__ */ l("td", {
238
238
  style: {
239
239
  ...k,
240
- outline: r ? "1px solid var(--danger)" : "none",
240
+ outline: i ? "1px solid var(--danger)" : "none",
241
241
  outlineOffset: -1,
242
- borderRadius: r ? 4 : 0
242
+ borderRadius: i ? 4 : 0
243
243
  },
244
- title: r ? `${e} is required` : void 0,
245
- children: t ? i({
246
- field: t,
247
- fieldName: e,
248
- value: n[e],
244
+ title: i ? `${t} is required` : void 0,
245
+ children: n ? r({
246
+ field: n,
247
+ fieldName: t,
248
+ value: e[t],
249
249
  mode: "edit",
250
- onChange: (t) => p(n._key, e, t),
251
- record: n,
250
+ onChange: (n) => p(e._key, t, n),
251
+ record: e,
252
252
  refs: f,
253
253
  locale: g
254
- }) : String(n[e] ?? "")
255
- }, e);
254
+ }) : String(e[t] ?? "")
255
+ }, t);
256
256
  }),
257
257
  /* @__PURE__ */ l("td", {
258
258
  style: {
@@ -260,15 +260,15 @@ function x({ row: n, columns: r, editableColumns: o, displayOnlyColumns: s, chil
260
260
  borderBottom: "1px solid var(--divider)",
261
261
  verticalAlign: "middle"
262
262
  },
263
- children: v ? /* @__PURE__ */ l(t, {
263
+ children: v ? /* @__PURE__ */ l(n, {
264
264
  size: 24,
265
- icon: /* @__PURE__ */ l(e.refresh, { size: 12 }),
266
- onClick: () => h(n._key),
265
+ icon: /* @__PURE__ */ l(t.refresh, { size: 12 }),
266
+ onClick: () => h(e._key),
267
267
  title: "Undo delete"
268
- }) : /* @__PURE__ */ l(t, {
268
+ }) : /* @__PURE__ */ l(n, {
269
269
  size: 24,
270
- icon: /* @__PURE__ */ l(e.trash, { size: 12 }),
271
- onClick: () => m(n._key),
270
+ icon: /* @__PURE__ */ l(t.trash, { size: 12 }),
271
+ onClick: () => m(e._key),
272
272
  title: "Delete row"
273
273
  })
274
274
  })
@@ -278,4 +278,4 @@ function x({ row: n, columns: r, editableColumns: o, displayOnlyColumns: s, chil
278
278
  //#endregion
279
279
  export { b as DraggableChildrenTable };
280
280
 
281
- //# sourceMappingURL=DraggableChildrenTable-XKJWMb1S.js.map
281
+ //# sourceMappingURL=DraggableChildrenTable-CVdCUPGE.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"DraggableChildrenTable-XKJWMb1S.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 { 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 const thStyle: React.CSSProperties = {\n height: 30,\n padding: \"0 12px\",\n textAlign: \"left\",\n fontWeight: 600,\n fontSize: 11,\n letterSpacing: \"0.04em\",\n color: \"var(--text-3)\",\n textTransform: \"uppercase\",\n borderBottom: \"1px solid var(--divider)\",\n };\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={{ width: \"100%\", fontSize: 12.5, borderCollapse: \"separate\", borderSpacing: 0 }}\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: React.CSSProperties = {\n padding: \"6px 12px\",\n borderBottom: \"1px solid var(--divider)\",\n verticalAlign: \"middle\",\n color: \"var(--text-2)\",\n };\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":";;;;;;;AAoCA,SAAgB,EAAuB,EACrC,WACA,gBACA,WACA,SACA,aACA,qBACA,WAC8B;CAC9B,IAAM,IAAW,EAAyB,KAAK,EACzC,IAAgB,EAAO,gBACvB,IAAU,EAAO,SAGjB,IAAU,EACd,EAAU,GAAe,EAAE,sBAAsB,EAAE,UAAU,GAAG,EAAE,CAAC,CACpE,EAGK,IAAQ,IAAI,IAAI,MAAM,QAAQ,EAAO,YAAY,GAAG,EAAO,cAAc,CAAC,EAAO,YAAY,CAAC,EAC9F,IAAkB,EAAQ,QAAQ,MAAQ;AAE9C,MADI,EAAM,IAAI,EAAI,IACd,MAAQ,EAAe,QAAO;EAClC,IAAM,IAAQ,EAAY,OAAO,IAAI,EAAI;AAGzC,SADA,EADI,CAAC,KACD,EAAM,QAAQ,EAAM;GAExB,EAGI,IAAqB,IAAI,IAC7B,EAAQ,QAAQ,MAAQ;AACtB,MAAI,MAAQ,EAAe,QAAO;EAClC,IAAM,IAAQ,EAAY,OAAO,IAAI,EAAI;AACzC,SAAO,MAAU,EAAM,QAAQ,EAAM;GACrC,CACH,EAGK,IAAiB,EAAQ,QAAQ,MAAQ,MAAQ,EAAc,EAG/D,IAAS,QACP,EAAK,QAAQ,MAAM,CAAC,EAAE,SAAS,CAAC,KAAK,MAAM,EAAE,KAAK,EACxD,CAAC,EAAK,CACP,EAEK,IAAmB,GACtB,GAAgB,GAAmB,MAAmB;AAMrD,IALgB,EAAK,KAAK,MACxB,EAAI,SAAS,IACT;GAAE,GAAG;IAAM,IAAY;GAAO,QAAQ;GAAM,GAC5C,EACL,CACgB;IAEnB,CAAC,GAAM,EAAS,CACjB,EAEK,IAAe,QAAkB;EACrC,IAAM,IAAc,EACjB,QAAQ,MAAM,CAAC,EAAE,SAAS,CAC1B,QAAQ,GAAK,MAAM;GAClB,IAAM,IAAM,OAAO,EAAE,MAAmB,WAAY,EAAE,KAA4B;AAClF,UAAO,KAAK,IAAI,GAAK,EAAI;KACxB,GAAG,EAEF,IAA0B;GAC9B,MAAM,OAAO,YAAY;GACzB,QAAQ;IACP,IAAgB,IAAc;GAChC,EAEK,IAAgB,EAAO,WAAW,YAAY,EAAE;AACtD,OAAK,IAAM,KAAO,GAAiB;GACjC,IAAM,IAAQ,EAAY,OAAO,IAAI,EAAI;AACpC,SACD,EAAc,OAAS,KAAA,IAEhB,EAAM,YAAY,KAAA,IAElB,EAAM,SAAS,WACxB,EAAO,KAAO,MAFd,EAAO,KAAO,EAAM,UAFpB,EAAO,KAAO,EAAc;;AAOhC,IAAS,CAAC,GAAG,GAAM,EAAO,CAAC;IAC1B;EAAC;EAAM;EAAU;EAAiB;EAAa,EAAO,WAAW;EAAU;EAAc,CAAC,EAEvF,IAAkB,GACrB,MAAmB;AAIlB,IAHgB,EAAK,KAAK,MACxB,EAAI,SAAS,IAAS;GAAE,GAAG;GAAK,UAAU;GAAM,GAAG,EACpD,CACgB;IAEnB,CAAC,GAAM,EAAS,CACjB,EAEK,IAAmB,GACtB,MAAmB;AAIlB,IAHgB,EAAK,KAAK,MACxB,EAAI,SAAS,IAAS;GAAE,GAAG;GAAK,UAAU;GAAO,GAAG,EACrD,CACgB;IAEnB,CAAC,GAAM,EAAS,CACjB,EAEK,IAAgB,GACnB,MAAwB;EACvB,IAAM,EAAE,WAAQ,YAAS;AACzB,MAAI,CAAC,KAAQ,EAAO,OAAO,EAAK,GAAI;EAGpC,IAAM,IAAa,EAAK,QAAQ,MAAM,CAAC,EAAE,SAAS,EAC5C,IAAU,EAAK,QAAQ,MAAM,EAAE,SAAS,EAExC,IAAW,EAAW,WAAW,MAAM,EAAE,SAAS,OAAO,EAAO,GAAG,CAAC,EACpE,IAAW,EAAW,WAAW,MAAM,EAAE,SAAS,OAAO,EAAK,GAAG,CAAC;AACxE,MAAI,MAAa,MAAM,MAAa,GAAI;EAGxC,IAAM,IAAY,CAAC,GAAG,EAAW,EAC3B,CAAC,KAAS,EAAU,OAAO,GAAU,EAAE;AAW7C,EAVA,EAAU,OAAO,GAAU,GAAG,EAAM,EAUpC,EAAS,CAAC,GAPY,EAAU,KAAK,GAAK,OAAW;GACnD,GAAG;IACF,IAAgB;GACjB,QAAQ,EAAI,OAAmB,IAAe,EAAI,SAAX;GACxC,EAAE,EAGyB,GAAG,EAAQ,CAAC;IAE1C;EAAC;EAAM;EAAU;EAAc,CAChC,EAEK,IAAgB,GACnB,MAA6C;EAC5C,IAAM,IAAS,EAAE,QACX,IAAO,EAAO,QAAQ,KAAK;AACjC,MAAI,CAAC,EAAM;EACX,IAAM,IAAM,EAAK,QAAQ,KAAK;AAC9B,MAAI,CAAC,EAAK;EACV,IAAM,IAAQ,EAAI,QAAQ,QAAQ;AAClC,MAAI,CAAC,EAAO;EAEZ,IAAM,IAAY,MAAM,KAAK,EAAI,MAAM,CAAC,QAAQ,EAA6B,EACvE,IAAW,MAAM,KAAK,EAAM,KAAK,CAAC,QAAQ,EAA2B;AAE3E,MAAI,EAAE,QAAQ,OAAO;GACnB,IAAM,IAAY,EAAE,WAAW,KAAK,GAC9B,IAAU,MAAM,KAAK,EAAM,KAAK,EAClC,IAAU,GACV,IAAW,IAAY;AAE3B,UAAO,KAAW,KAAK,IAAU,EAAQ,QACvC,KAAI,KAAY,KAAK,IAAW,EAAQ,GAAS,MAAM,QAAQ;IAE7D,IAAM,IADS,EAAQ,GAAS,MAAM,GACjB,cAAc,sCAAsC;AACzE,QAAI,GAAO;AAET,KADA,EAAE,gBAAgB,EAClB,EAAM,OAAO;AACb;;AAEF,SAAY;SAGZ,CADA,KAAW,GACX,IAAW,IAAY,IAAI,KAAK,EAAQ,IAAU,MAAM,UAAU,KAAK;aAGlE,EAAE,QAAQ,SAAS;AAC5B,KAAE,gBAAgB;GAClB,IAAM,IAAU,MAAM,KAAK,EAAM,KAAK;AACtC,OAAI,IAAW,EAAQ,SAAS,GAAG;IAEjC,IAAM,IADS,EAAQ,IAAW,GAAG,MAAM,IACrB,cAAc,sCAAsC;AAC1E,IAAI,KAAO,EAAM,OAAO;SAGxB,CADA,GAAc,EACd,4BAA4B;IAC1B,IAAM,IAAU,EAAM,iBAAiB,KAAK,EACtC,IAAU,EAAQ,EAAQ,SAAS;AACzC,QAAI,GAAS;KACX,IAAM,IAAa,EAAQ,cAAc,sCAAsC;AAC/E,KAAI,KAAY,EAAW,OAAO;;KAEpC;SAEK,EAAE,QAAQ,YAClB,EAAuB,QAAQ;IAGpC,CAAC,EAAa,CACf,EAEK,IAA+B;EACnC,QAAQ;EACR,SAAS;EACT,WAAW;EACX,YAAY;EACZ,UAAU;EACV,eAAe;EACf,OAAO;EACP,eAAe;EACf,cAAc;EACf;AAED,QACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,OAAD;EAAK,OAAO,EAAE,WAAW,QAAQ;YAC/B,kBAAC,GAAD;GAAqB;GAAS,oBAAoB;GAAe,WAAW;aAC1E,kBAAC,SAAD;IACE,KAAK;IACL,OAAO;KAAE,OAAO;KAAQ,UAAU;KAAM,gBAAgB;KAAY,eAAe;KAAG;IACtF,WAAW;cAHb,CAKE,kBAAC,SAAD,EAAA,UACE,kBAAC,MAAD,EAAA,UAAA;KACE,kBAAC,MAAD,EAAI,OAAO;MAAE,GAAG;MAAS,OAAO;MAAI,SAAS;MAAS,EAAI,CAAA;KACzD,EAAe,KAAK,MAAQ;MAC3B,IAAM,IAAQ,EAAY,OAAO,IAAI,EAAI;AACzC,aACE,kBAAC,MAAD;OAAc,OAAO;iBAClB,IAAQ,EAAiB,GAAO,GAAK,EAAO,SAAS,GAAG;OACtD,EAFI,EAEJ;OAEP;KACF,kBAAC,MAAD,EAAI,OAAO;MAAE,GAAG;MAAS,OAAO;MAAI,EAAI,CAAA;KACrC,EAAA,CAAA,EACC,CAAA,EACR,kBAAC,GAAD;KAAiB,OAAO;KAAQ,UAAU;eACxC,kBAAC,SAAD,EAAA,UACG,EAAK,KAAK,MACT,kBAAC,GAAD;MAEO;MACL,SAAS;MACQ;MACG;MACP;MACK;MACZ;MACN,cAAc;MACd,UAAU;MACV,cAAc;MACd,EAXK,EAAI,KAWT,CACF,EACI,CAAA;KACQ,CAAA,CACZ;;GACG,CAAA;EACT,CAAA,EACN,kBAAC,OAAD;EAAK,OAAO,EAAE,WAAW,GAAG;YAC1B,kBAAC,GAAD;GAAK,MAAK;GAAQ,MAAK;GAAK,MAAM,kBAAC,EAAK,MAAN,EAAW,MAAM,IAAM,CAAA;GAAE,SAAS;aAAc;GAE5E,CAAA;EACF,CAAA,CACF,EAAA,CAAA;;AAkBV,SAAS,EAAY,EACnB,QACA,YACA,oBACA,uBACA,gBACA,qBACA,SACA,iBACA,aACA,mBACmB;CACnB,IAAM,IAAa,GAAe,EAC5B,IAAY,CAAC,CAAC,EAAI,UAClB,IAAY,IAAmB,EAAI,OAEnC,EACJ,eACA,cACA,eACA,cACA,eACA,kBACE,EAAY;EAAE,IAAI,EAAI;EAAM,UAAU;EAAW,CAAC,EAEhD,IAAQ;EACZ,WAAW,EAAI,UAAU,SAAS,EAAU;EAC5C;EACA,QAAQ,IAAa,KAAK,KAAA;EAC1B,SAAS,IAAa,KAAM,KAAA;EAC7B,EAEK,IAAiC;EACrC,SAAS;EACT,cAAc;EACd,eAAe;EACf,OAAO;EACR,EACK,IAAqC;EACzC,SAAS;EACT,cAAc;EACd,eAAe;EAChB;AAED,QACE,kBAAC,MAAD;EAAI,KAAK;EAAY,OAAO;GAAE,GAAG;GAAO,SAAS,IAAY,KAAM,GAAO,WAAW;GAAG;YAAxF;GACE,kBAAC,MAAD;IAAI,OAAO;KAAE,SAAS;KAAW,cAAc;KAA4B,OAAO;KAAI,eAAe;KAAU;cAC5G,CAAC,KACA,kBAAC,UAAD;KACE,MAAK;KACL,OAAO;MACL,QAAQ;MACR,SAAS;MACT,OAAO;MACP,YAAY;MACZ,QAAQ;MACR,SAAS;MACV;KACD,GAAI;KACJ,GAAI;eAEJ,kBAAC,EAAK,MAAN,EAAW,MAAM,IAAM,CAAA;KAChB,CAAA;IAER,CAAA;GACJ,EAAQ,KAAK,MAAQ;IACpB,IAAM,IAAQ,EAAY,OAAO,IAAI,EAAI,EACnC,IAAW,GAAW,SAAS,EAAI,EACnC,IAAgB,EAAmB,IAAI,EAAI,EAC3C,IAAa,EAAgB,SAAS,EAAI;AA0BhD,WAxBI,KAAa,KAAiB,CAAC,IAE/B,kBAAC,MAAD;KAEE,OAAO;MACL,GAAG;MACH,gBAAgB,IAAY,iBAAiB;MAC9C;eAEA,IACG,EAAY;MACV;MACA,WAAW;MACX,OAAO,EAAI;MACX,MAAM;MACN,QAAQ;MACR;MACF,QAAQ;MACP,CAAC,GACF,OAAO,EAAI,MAAQ,GAAG;KACvB,EAjBE,EAiBF,GAKP,kBAAC,MAAD;KAEE,OAAO;MACL,GAAG;MACH,SAAS,IAAW,4BAA4B;MAChD,eAAe;MACf,cAAc,IAAW,IAAI;MAC9B;KACD,OAAO,IAAW,GAAG,EAAI,gBAAgB,KAAA;eAExC,IACG,EAAY;MACV;MACA,WAAW;MACX,OAAO,EAAI;MACX,MAAM;MACN,WAAW,MAAQ,EAAa,EAAI,MAAM,GAAK,EAAI;MACnD,QAAQ;MACR;MACF,QAAQ;MACP,CAAC,GACF,OAAO,EAAI,MAAQ,GAAG;KACvB,EArBE,EAqBF;KAEP;GACF,kBAAC,MAAD;IAAI,OAAO;KAAE,SAAS;KAAW,cAAc;KAA4B,eAAe;KAAU;cACjG,IACC,kBAAC,GAAD;KACE,MAAM;KACN,MAAM,kBAAC,EAAK,SAAN,EAAc,MAAM,IAAM,CAAA;KAChC,eAAe,EAAa,EAAI,KAAK;KACrC,OAAM;KACN,CAAA,GAEF,kBAAC,GAAD;KACE,MAAM;KACN,MAAM,kBAAC,EAAK,OAAN,EAAY,MAAM,IAAM,CAAA;KAC9B,eAAe,EAAS,EAAI,KAAK;KACjC,OAAM;KACN,CAAA;IAED,CAAA;GACF"}
1
+ {"version":3,"file":"DraggableChildrenTable-CVdCUPGE.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 { 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 const thStyle: React.CSSProperties = {\n height: 30,\n padding: \"0 12px\",\n textAlign: \"left\",\n fontWeight: 600,\n fontSize: 11,\n letterSpacing: \"0.04em\",\n color: \"var(--text-3)\",\n textTransform: \"uppercase\",\n borderBottom: \"1px solid var(--divider)\",\n };\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={{ width: \"100%\", fontSize: 12.5, borderCollapse: \"separate\", borderSpacing: 0 }}\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: React.CSSProperties = {\n padding: \"6px 12px\",\n borderBottom: \"1px solid var(--divider)\",\n verticalAlign: \"middle\",\n color: \"var(--text-2)\",\n };\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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"}
@@ -3,7 +3,15 @@ import { type WidgetContext } from "../../../components/shared/ResponsiveActionB
3
3
  import type { Action } from "../../../types/schema";
4
4
  export interface ListHeaderProps {
5
5
  title: string;
6
+ /** Total matching rows, from the separate count query. Undefined while
7
+ * the count is in flight or after it failed. */
6
8
  total: number | undefined;
9
+ /** Strategy used to produce `total` ("exact" | "estimated"). Drives
10
+ * display precision. Undefined when no count has resolved. */
11
+ countMode?: "exact" | "estimated";
12
+ /** True while the count query is in flight - render a loading affordance
13
+ * in the count slot instead of a number. */
14
+ countPending?: boolean;
7
15
  loaded: number;
8
16
  toolbarActions: {
9
17
  code: string;
@@ -20,4 +28,4 @@ export interface ListHeaderProps {
20
28
  /** Dispatch surface threaded into ResponsiveActionBar (plan §8 Task 10). */
21
29
  widgetContext: WidgetContext;
22
30
  }
23
- export declare function ListHeader({ title, total, loaded, toolbarActions, screenActionLayout, entityActionLayout, selectedIds, data, rowKey, onActionSuccess, widgetContext, }: ListHeaderProps): import("react/jsx-runtime").JSX.Element;
31
+ export declare function ListHeader({ title, total, countMode, countPending, loaded, toolbarActions, screenActionLayout, entityActionLayout, selectedIds, data, rowKey, onActionSuccess, widgetContext, }: ListHeaderProps): import("react/jsx-runtime").JSX.Element;
@@ -1,2 +1,26 @@
1
- import type { ListParams } from "../types/api";
2
- export declare function useEntityData(entity: string | undefined, params: ListParams): import("@tanstack/react-query").UseQueryResult<import("../types/api").ListResponse<Record<string, unknown>>, Error>;
1
+ import type { ListParams, ListResponse } from "../types/api";
2
+ /**
3
+ * useEntityData fetches one page-window of entity rows.
4
+ *
5
+ * Decoupled-count contract (docs/plans/2026-05-18-declarative-row-count.md):
6
+ * this hook issues the WINDOW request only - `count: "window"`, so the
7
+ * server returns the page of rows and never blocks on a COUNT(*). The total
8
+ * is fetched separately by `useEntityCount`; the table renders rows the
9
+ * moment this query resolves, independent of count latency.
10
+ */
11
+ export declare function useEntityData(entity: string | undefined, params: ListParams): import("@tanstack/react-query").UseQueryResult<ListResponse<Record<string, unknown>>, Error>;
12
+ /**
13
+ * useEntityCount fetches the total row count for a list view in a SEPARATE
14
+ * request from the window (`count: "only"` - the server skips the data
15
+ * query entirely and returns just `meta.total` + `meta.count_mode`).
16
+ *
17
+ * The count is page-independent: `page` and `sort` are stripped from the
18
+ * params so a page change or a re-sort does not refetch the count - only a
19
+ * filter / search / screen / include_deleted change invalidates it.
20
+ *
21
+ * The server picks exact vs estimated per the entity's declared `count`
22
+ * strategy; the resolved `meta.count_mode` tells the caller which precision
23
+ * to render. A count failure does NOT break the table: callers treat a
24
+ * failed/absent count as "unknown" and fall back to the loaded row count.
25
+ */
26
+ export declare function useEntityCount(entity: string | undefined, params: ListParams): import("@tanstack/react-query").UseQueryResult<ListResponse<Record<string, unknown>>, Error>;
@@ -14,7 +14,7 @@ export { useSchema } from "./hooks/useSchema";
14
14
  export { useEntity } from "./hooks/useEntity";
15
15
  export { useScreen } from "./hooks/useScreen";
16
16
  export { useAuth } from "./hooks/useAuth";
17
- export { useEntityData } from "./hooks/useEntityData";
17
+ export { useEntityData, useEntityCount } from "./hooks/useEntityData";
18
18
  export { useAllEntityData } from "./hooks/useAllEntityData";
19
19
  export { useEntityItem } from "./hooks/useEntityItem";
20
20
  export { useCreateEntity, useUpdateEntity, useDeleteEntity } from "./hooks/useMutations";
@@ -103,6 +103,7 @@ export { Breadcrumbs } from "./components/shared/Breadcrumbs";
103
103
  export type { BreadcrumbsProps, Crumb } from "./components/shared/Breadcrumbs";
104
104
  export type { FilterNode, FilterOp } from "./lib/filters";
105
105
  export { parseFiltersFromURL, setFiltersInURL, hasOrGroups, countActiveFilters, OP_LABELS, NO_VALUE_OPS, } from "./lib/filters";
106
+ export { formatCount, formatExactCount, formatEstimatedCount } from "./lib/format-count";
106
107
  export type { MassEditTarget, MassEditTargetKind } from "./lib/mass-edit";
107
108
  export { buildMassEditTargets, parseTargetFilter } from "./lib/mass-edit";
108
109
  export { PropertyGridEditor, detectCommonValue, MULTIPLE_VALUES } from "./components/mass-edit/PropertyGridEditor";