@ai-matrx/records-ui 0.93.3 → 0.93.6
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.
- package/CHANGELOG.md +28 -0
- package/dist/index.cjs +491 -241
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +68 -2
- package/dist/index.d.ts +68 -2
- package/dist/index.js +893 -632
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -54,6 +54,7 @@ __export(src_exports, {
|
|
|
54
54
|
CHECKLIST_REQUIREMENT_LABEL: () => CHECKLIST_REQUIREMENT_LABEL,
|
|
55
55
|
CONDITION_OPS: () => CONDITION_OPS,
|
|
56
56
|
CONTEXT_POLICY_LABEL: () => CONTEXT_POLICY_LABEL,
|
|
57
|
+
CUSTOM_FIELD_ADDABLE_TYPES: () => ADDABLE_TYPES,
|
|
57
58
|
CaptureRun: () => CaptureRun,
|
|
58
59
|
CaptureSheet: () => CaptureSheet,
|
|
59
60
|
ChartBlock: () => ChartBlock,
|
|
@@ -246,6 +247,7 @@ __export(src_exports, {
|
|
|
246
247
|
conditionValue: () => conditionValue,
|
|
247
248
|
controlFor: () => controlFor,
|
|
248
249
|
currencyCodeFor: () => currencyCodeFor,
|
|
250
|
+
customFieldRequestProblems: () => customFieldRequestProblems,
|
|
249
251
|
dashboardDeclareArgs: () => dashboardDeclareArgs,
|
|
250
252
|
dashboardFromSummary: () => dashboardFromSummary,
|
|
251
253
|
dateFormatFor: () => dateFormatFor,
|
|
@@ -7978,6 +7980,10 @@ function useMergedGridControls(args) {
|
|
|
7978
7980
|
(columnKey) => enabled && fieldOf2(columnKey) ? headerMenuItems(columnKey, ctx) : void 0,
|
|
7979
7981
|
[enabled, ctx, fieldOf2]
|
|
7980
7982
|
);
|
|
7983
|
+
const columnsControl = (0, import_react40.useMemo)(
|
|
7984
|
+
() => enabled ? { order, hidden, onChange: keepColumns } : void 0,
|
|
7985
|
+
[enabled, order, hidden, keepColumns]
|
|
7986
|
+
);
|
|
7981
7987
|
const close = () => setDialog(null);
|
|
7982
7988
|
const dialogs = !enabled ? null : /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_jsx_runtime28.Fragment, { children: [
|
|
7983
7989
|
colours.dialogs,
|
|
@@ -8109,7 +8115,7 @@ function useMergedGridControls(args) {
|
|
|
8109
8115
|
headerMenuFor,
|
|
8110
8116
|
filterFor,
|
|
8111
8117
|
contextMenu,
|
|
8112
|
-
columns:
|
|
8118
|
+
columns: columnsControl,
|
|
8113
8119
|
dialogs,
|
|
8114
8120
|
addColumn: () => setDialog({ kind: "insert", anchor: null, side: "right" }),
|
|
8115
8121
|
sheetTools: enabled ? /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_jsx_runtime28.Fragment, { children: [
|
|
@@ -9639,7 +9645,43 @@ function useGridUndo(args) {
|
|
|
9639
9645
|
[push, undo, redo, held, busy, notice, dismiss]
|
|
9640
9646
|
);
|
|
9641
9647
|
}
|
|
9642
|
-
function
|
|
9648
|
+
function useUndoSource(undo) {
|
|
9649
|
+
const box = (0, import_react52.useRef)({
|
|
9650
|
+
undo,
|
|
9651
|
+
face: { back: undo.depth.back, forward: undo.depth.forward, busy: undo.busy },
|
|
9652
|
+
listeners: /* @__PURE__ */ new Set()
|
|
9653
|
+
});
|
|
9654
|
+
box.current.undo = undo;
|
|
9655
|
+
const source = (0, import_react52.useMemo)(
|
|
9656
|
+
() => ({
|
|
9657
|
+
face: () => box.current.face,
|
|
9658
|
+
subscribe: (listener) => {
|
|
9659
|
+
box.current.listeners.add(listener);
|
|
9660
|
+
return () => {
|
|
9661
|
+
box.current.listeners.delete(listener);
|
|
9662
|
+
};
|
|
9663
|
+
},
|
|
9664
|
+
undo: () => void box.current.undo.undo(),
|
|
9665
|
+
redo: () => void box.current.undo.redo()
|
|
9666
|
+
}),
|
|
9667
|
+
[]
|
|
9668
|
+
);
|
|
9669
|
+
const { back, forward } = undo.depth;
|
|
9670
|
+
const busy = undo.busy;
|
|
9671
|
+
(0, import_react52.useLayoutEffect)(() => {
|
|
9672
|
+
const held = box.current.face;
|
|
9673
|
+
if (held.back === back && held.forward === forward && held.busy === busy) return;
|
|
9674
|
+
box.current.face = { back, forward, busy };
|
|
9675
|
+
for (const listener of box.current.listeners) listener();
|
|
9676
|
+
}, [back, forward, busy]);
|
|
9677
|
+
return source;
|
|
9678
|
+
}
|
|
9679
|
+
function UndoRedoLive({ source }) {
|
|
9680
|
+
const face = (0, import_react52.useSyncExternalStore)(source.subscribe, source.face, source.face);
|
|
9681
|
+
return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(UndoRedoPair, { face, onUndo: source.undo, onRedo: source.redo });
|
|
9682
|
+
}
|
|
9683
|
+
function UndoRedoPair({ face, onUndo, onRedo }) {
|
|
9684
|
+
const undo = { depth: { back: face.back, forward: face.forward }, busy: face.busy };
|
|
9643
9685
|
if (undo.depth.back === 0 && undo.depth.forward === 0) return null;
|
|
9644
9686
|
return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(import_jsx_runtime37.Fragment, { children: [
|
|
9645
9687
|
/* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
|
|
@@ -9651,7 +9693,7 @@ function UndoRedoButtons({ undo }) {
|
|
|
9651
9693
|
title: undo.depth.back > 0 ? `Undo (Cmd-Z) \xB7 ${undo.depth.back} ${undo.depth.back === 1 ? "step" : "steps"} back` : "Nothing to undo",
|
|
9652
9694
|
"aria-label": `Undo, ${undo.depth.back} ${undo.depth.back === 1 ? "step" : "steps"}`,
|
|
9653
9695
|
disabled: undo.busy || undo.depth.back === 0,
|
|
9654
|
-
onClick:
|
|
9696
|
+
onClick: onUndo,
|
|
9655
9697
|
children: [
|
|
9656
9698
|
"Undo ",
|
|
9657
9699
|
/* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "ml-1 tabular-nums text-muted-foreground", "data-records-undo-count": "", children: undo.depth.back })
|
|
@@ -9667,7 +9709,7 @@ function UndoRedoButtons({ undo }) {
|
|
|
9667
9709
|
title: undo.depth.forward > 0 ? `Redo (Shift-Cmd-Z) \xB7 ${undo.depth.forward} ${undo.depth.forward === 1 ? "step" : "steps"} forward` : "Nothing to redo",
|
|
9668
9710
|
"aria-label": `Redo, ${undo.depth.forward} ${undo.depth.forward === 1 ? "step" : "steps"}`,
|
|
9669
9711
|
disabled: undo.busy || undo.depth.forward === 0,
|
|
9670
|
-
onClick:
|
|
9712
|
+
onClick: onRedo,
|
|
9671
9713
|
children: [
|
|
9672
9714
|
"Redo ",
|
|
9673
9715
|
/* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "ml-1 tabular-nums text-muted-foreground", "data-records-redo-count": "", children: undo.depth.forward })
|
|
@@ -11013,6 +11055,7 @@ function Grid({
|
|
|
11013
11055
|
);
|
|
11014
11056
|
const rowVersion = (0, import_react60.useCallback)((row) => editingNow.current.rowSignature(row.id), []);
|
|
11015
11057
|
const undo = useGridUndo({ tableId, reload: records.reload, forget: editing.forget });
|
|
11058
|
+
const undoSource = useUndoSource(undo);
|
|
11016
11059
|
pushWritten.current = (write) => {
|
|
11017
11060
|
const field2 = (fields.data ?? []).find((f) => f.key === write.key);
|
|
11018
11061
|
const name = changedRecordName(table.data, rows.find((r) => r.id === write.rowId)?.document ?? {}, write.key, write.after);
|
|
@@ -11564,9 +11607,14 @@ function Grid({
|
|
|
11564
11607
|
);
|
|
11565
11608
|
const footerNow = (0, import_react60.useRef)({
|
|
11566
11609
|
summaryCell: () => null,
|
|
11567
|
-
addLine: () => null
|
|
11610
|
+
addLine: () => null,
|
|
11611
|
+
choose: () => void 0,
|
|
11612
|
+
add: () => void 0
|
|
11568
11613
|
});
|
|
11569
11614
|
footerNow.current = {
|
|
11615
|
+
// The footer is memoised on what it shows (`footerRows` below), so its buttons act through here.
|
|
11616
|
+
choose: chooseSummary,
|
|
11617
|
+
add: () => void addRowHere(),
|
|
11570
11618
|
summaryCell: (columnId) => {
|
|
11571
11619
|
const field2 = ordered.find((f) => f.key === columnId);
|
|
11572
11620
|
if (!field2) return null;
|
|
@@ -11579,11 +11627,11 @@ function Grid({
|
|
|
11579
11627
|
value: summaries.values[columnId],
|
|
11580
11628
|
rowCount: summaries.rowCount,
|
|
11581
11629
|
summaries,
|
|
11582
|
-
onChoose: (op2) =>
|
|
11630
|
+
onChoose: (op2) => footerNow.current.choose(columnId, op2)
|
|
11583
11631
|
}
|
|
11584
11632
|
);
|
|
11585
11633
|
},
|
|
11586
|
-
addLine: () => /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(AddRowLine, { onAdd: () =>
|
|
11634
|
+
addLine: () => /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(AddRowLine, { onAdd: () => footerNow.current.add(), busy: added.busy })
|
|
11587
11635
|
};
|
|
11588
11636
|
const addColumnNow = (0, import_react60.useRef)(controls.addColumn);
|
|
11589
11637
|
addColumnNow.current = controls.addColumn;
|
|
@@ -11698,13 +11746,24 @@ function Grid({
|
|
|
11698
11746
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- `hasTalk`/`hasAgent` stand for the host ports the JSX reads; handlers run through `rowActionsNow`.
|
|
11699
11747
|
[canWrite, mayWriteRow, tableActions.runnable, tableActions.agent, runner.busy, hasTalk, hasAgent, editable, rights.write]
|
|
11700
11748
|
);
|
|
11701
|
-
const
|
|
11749
|
+
const headerHeld = (0, import_react60.useRef)(/* @__PURE__ */ new Map());
|
|
11750
|
+
const menuLatest = (0, import_react60.useRef)(/* @__PURE__ */ new Map());
|
|
11751
|
+
const addColumnDef = (0, import_react60.useMemo)(() => addColumnColumn(() => addColumnNow.current()), []);
|
|
11752
|
+
const drawnColumns = (0, import_react60.useMemo)(
|
|
11702
11753
|
() => {
|
|
11703
|
-
const
|
|
11704
|
-
|
|
11754
|
+
const filterFor = controls.filterFor;
|
|
11755
|
+
const drawn = withHeaderMenus(
|
|
11756
|
+
bodyColumns,
|
|
11757
|
+
(key) => holdHeaderMenu(headerHeld.current, menuLatest.current, key, controls.headerMenuFor(key)),
|
|
11758
|
+
(key) => holdByContent(headerHeld.current, `filter:${key}`, filterFor(key))
|
|
11759
|
+
);
|
|
11760
|
+
return merged && rights.structure && editable ? [...drawn, addColumnDef] : drawn;
|
|
11705
11761
|
},
|
|
11706
|
-
[bodyColumns, controls.headerMenuFor, controls.filterFor, merged, rights.structure, editable]
|
|
11762
|
+
[bodyColumns, controls.headerMenuFor, controls.filterFor, merged, rights.structure, editable, addColumnDef]
|
|
11707
11763
|
);
|
|
11764
|
+
const columnsHeld = (0, import_react60.useRef)(null);
|
|
11765
|
+
const columns = columnsHeld.current !== null && sameColumns(columnsHeld.current, drawnColumns) ? columnsHeld.current : drawnColumns;
|
|
11766
|
+
columnsHeld.current = columns;
|
|
11708
11767
|
const [localColumns, setLocalColumns] = (0, import_react60.useState)(
|
|
11709
11768
|
null
|
|
11710
11769
|
);
|
|
@@ -11777,6 +11836,274 @@ function Grid({
|
|
|
11777
11836
|
} : {}
|
|
11778
11837
|
};
|
|
11779
11838
|
}, [presentation, onPresentationChange, writeLook, merged, phone]);
|
|
11839
|
+
const queryTotal = records.data?.total ?? rows.length;
|
|
11840
|
+
const tableQuery = (0, import_react60.useMemo)(
|
|
11841
|
+
() => merged ? {
|
|
11842
|
+
// THE STORE ANSWERS THE QUERY (tranche 6k): `data` is already the page.
|
|
11843
|
+
mode: "controlled",
|
|
11844
|
+
state: query,
|
|
11845
|
+
totalItems: queryTotal,
|
|
11846
|
+
onStateChange: setQuery,
|
|
11847
|
+
sourceProcessing: { search: "source", columnFilters: "source", sort: "source" }
|
|
11848
|
+
} : { mode: "controlled-local", state: query, onStateChange: setQuery },
|
|
11849
|
+
[merged, query, queryTotal]
|
|
11850
|
+
);
|
|
11851
|
+
const showAddLine = mayAddRow && rows.length > 0;
|
|
11852
|
+
const showSummaryLine = rows.length > 0 && (!merged || Object.keys(chosenSummaries).length > 0);
|
|
11853
|
+
const footerFace = JSON.stringify([
|
|
11854
|
+
showAddLine ? added.busy : null,
|
|
11855
|
+
showSummaryLine ? [chosenSummaries, summaries.values, summaries.rowCount, summaries.loading] : null
|
|
11856
|
+
]);
|
|
11857
|
+
const footerRows = (0, import_react60.useMemo)(
|
|
11858
|
+
() => [...showAddLine ? [ADD_ROW] : [], ...showSummaryLine ? [SUMMARY_ROW] : []],
|
|
11859
|
+
// `footerFace`, `ordered` and the summaries' operations are what the footer's cells draw.
|
|
11860
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
11861
|
+
[showAddLine, showSummaryLine, footerFace, ordered, summaries.operations, summaries.askOperations]
|
|
11862
|
+
);
|
|
11863
|
+
const coverageTotal = typeof records.data?.total === "number" ? records.data.total : null;
|
|
11864
|
+
const tableCoverage = (0, import_react60.useMemo)(
|
|
11865
|
+
() => ({
|
|
11866
|
+
answeredBy: merged ? "source" : "client",
|
|
11867
|
+
...merged && coverageTotal !== null ? { matched: coverageTotal } : {},
|
|
11868
|
+
noun: "record",
|
|
11869
|
+
...coverageTotal !== null ? { total: coverageTotal } : {}
|
|
11870
|
+
}),
|
|
11871
|
+
[merged, coverageTotal]
|
|
11872
|
+
);
|
|
11873
|
+
const sheetNow = (0, import_react60.useRef)({ merged, writeRange, editing, fields: fields.data ?? [], mayWriteRow, notify: host.notify });
|
|
11874
|
+
sheetNow.current = { merged, writeRange, editing, fields: fields.data ?? [], mayWriteRow, notify: host.notify };
|
|
11875
|
+
const hasNotify = Boolean(host.notify);
|
|
11876
|
+
const readOnlyReason = !editable || !rights.known || rights.write ? "Editing is off here, so this table can be copied but not changed." : rights.why("write");
|
|
11877
|
+
const spreadsheetConfig = (0, import_react60.useMemo)(
|
|
11878
|
+
() => canWrite && editable ? {
|
|
11879
|
+
enabled: true,
|
|
11880
|
+
writableColumnIds: pasteableKeys,
|
|
11881
|
+
onPatch: (patches, intent) => {
|
|
11882
|
+
const now = sheetNow.current;
|
|
11883
|
+
if (now.merged) {
|
|
11884
|
+
void now.writeRange(patches, intent);
|
|
11885
|
+
return;
|
|
11886
|
+
}
|
|
11887
|
+
for (const patch of patches) {
|
|
11888
|
+
now.editing.setCell(patch.rowId, patch.columnId, patch.value);
|
|
11889
|
+
}
|
|
11890
|
+
},
|
|
11891
|
+
...merged ? { undoDepth: 0, onSelectionChange: followSelection, controllerRef: sheetController } : {},
|
|
11892
|
+
columnRefusal: (columnId) => {
|
|
11893
|
+
const field2 = sheetNow.current.fields.find((f) => f.key === columnId);
|
|
11894
|
+
if (!field2) return `This table has no column called ${columnId}.`;
|
|
11895
|
+
return `${fieldName(field2)} cannot take a pasted value \u2014 ${fieldTypeLabel(field2)} is worked out or chosen, not typed.`;
|
|
11896
|
+
},
|
|
11897
|
+
validateCell: ({ columnId, value }) => {
|
|
11898
|
+
const field2 = sheetNow.current.fields.find((f) => f.key === columnId);
|
|
11899
|
+
if (!field2) return `This table has no column called ${columnId}.`;
|
|
11900
|
+
const judged = valueFromPastedText(field2, value === null ? "" : String(value));
|
|
11901
|
+
return "refusal" in judged ? judged.refusal : void 0;
|
|
11902
|
+
},
|
|
11903
|
+
onPastePlan: receivePastePlan,
|
|
11904
|
+
/*
|
|
11905
|
+
* TYPE-TO-EDIT (design-system 0.43 `onEditRequest`): the cell a person is on
|
|
11906
|
+
* opens ITS OWN editor — the Field's — holding what they typed (a letter, an
|
|
11907
|
+
* IME composition, an Option/AltGr character, dictation). A worked-out column
|
|
11908
|
+
* or a row this person may not write answers false, so nothing is swallowed.
|
|
11909
|
+
*/
|
|
11910
|
+
onEditRequest: (address, seed) => {
|
|
11911
|
+
const now = sheetNow.current;
|
|
11912
|
+
const field2 = now.fields.find((f) => f.key === address.columnId);
|
|
11913
|
+
if (!field2 || !fieldIsEditable(field2) || !now.mayWriteRow(address.rowId)) return false;
|
|
11914
|
+
if (seed !== null && seed !== "" && typingOpens(field2) === "none") return false;
|
|
11915
|
+
now.editing.begin({ rowId: address.rowId, key: field2.key }, seed);
|
|
11916
|
+
return true;
|
|
11917
|
+
},
|
|
11918
|
+
...hasNotify ? { notify: (message) => sheetNow.current.notify?.success(message) } : {}
|
|
11919
|
+
} : {
|
|
11920
|
+
/*
|
|
11921
|
+
* RANGE SELECTION AND COPY FOR A READER (merge step 3, 0.42 `readOnly`). A person
|
|
11922
|
+
* who may not change this table may still drag out a range, select all and press
|
|
11923
|
+
* Cmd-C for a real Excel/Sheets block — the UX walk graded range selection BROKEN
|
|
11924
|
+
* on a table whose writes were refused. Every write gesture (cut, paste, clear,
|
|
11925
|
+
* fill, undo) says why it cannot, in the store's own words.
|
|
11926
|
+
*/
|
|
11927
|
+
enabled: true,
|
|
11928
|
+
readOnly: true,
|
|
11929
|
+
...merged ? { onSelectionChange: followSelection, controllerRef: sheetController } : {},
|
|
11930
|
+
readOnlyReason
|
|
11931
|
+
},
|
|
11932
|
+
[canWrite, editable, pasteableKeys, merged, followSelection, receivePastePlan, hasNotify, readOnlyReason]
|
|
11933
|
+
);
|
|
11934
|
+
const toolbarFields = fields.data;
|
|
11935
|
+
const hasSeveralRows = rows.length > 1;
|
|
11936
|
+
const editingBusy = editing.busy;
|
|
11937
|
+
const addRecord = editing.addRecord;
|
|
11938
|
+
const phoneSheetTools = merged && phone ? controls.sheetTools : null;
|
|
11939
|
+
const tableToolbar = (0, import_react60.useMemo)(
|
|
11940
|
+
() => ({
|
|
11941
|
+
search: true,
|
|
11942
|
+
searchPlaceholder: "Search records",
|
|
11943
|
+
/*
|
|
11944
|
+
* A PHONE (tranche 6o): search and the one Tools button sit side by side at the START of
|
|
11945
|
+
* the row, so Tools is never scrolled out of reach; the search is the same query.
|
|
11946
|
+
*/
|
|
11947
|
+
...merged && phone ? {
|
|
11948
|
+
customSearch: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("div", { className: "flex min-w-0 flex-1 items-center gap-1.5", "data-records-grid-phone-search": "", children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
|
|
11949
|
+
"input",
|
|
11950
|
+
{
|
|
11951
|
+
type: "search",
|
|
11952
|
+
value: query.search,
|
|
11953
|
+
placeholder: "Search",
|
|
11954
|
+
"aria-label": "Search records",
|
|
11955
|
+
onChange: (event) => {
|
|
11956
|
+
const search = event.target.value;
|
|
11957
|
+
setQuery((held) => ({ ...held, search, page: 1 }));
|
|
11958
|
+
},
|
|
11959
|
+
className: "h-8 w-full min-w-0 rounded-md border border-input bg-background px-2 text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
11960
|
+
style: { fontSize: "16px" }
|
|
11961
|
+
}
|
|
11962
|
+
) })
|
|
11963
|
+
} : {},
|
|
11964
|
+
/* ONE ROW (lane TABLE-PAGE-CHROME): on a table page the grid's search, columns and
|
|
11965
|
+
sort are drawn INTO the page's toolbar row, never a second bar under it. */
|
|
11966
|
+
...toolbarSlot !== void 0 ? { portalInto: toolbarSlot, singleRow: true } : {},
|
|
11967
|
+
// SCR-2 / SCR-4: PRESENT for an admin, ABSENT for everyone else.
|
|
11968
|
+
// There is no third state: this package ships no disabled variant of
|
|
11969
|
+
// these controls, so a member cannot be shown a dead one by mistake.
|
|
11970
|
+
/*
|
|
11971
|
+
* WHERE THE GRID'S CONTROLS GO (lane C, review 2026-09-26 D6). Inline when the row has
|
|
11972
|
+
* room; behind the row's "…" once the page row is folded that far (Undo / Redo and New
|
|
11973
|
+
* record stay beside it — the primary action is never folded); on a phone EVERY control
|
|
11974
|
+
* — the table's own (clear, copy/export, group, columns), these, the search, the sort and
|
|
11975
|
+
* the page's filter, layout and share — in ONE "Tools" sheet. Never dropped.
|
|
11976
|
+
*/
|
|
11977
|
+
actions: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(import_jsx_runtime42.Fragment, { children: [
|
|
11978
|
+
manual ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
|
|
11979
|
+
SortStateControl,
|
|
11980
|
+
{
|
|
11981
|
+
...handOrderSortState({
|
|
11982
|
+
sortName: columnSort ? columnLabel(toolbarFields ?? [], columnSort.id) : null,
|
|
11983
|
+
direction: columnSort?.direction ?? null,
|
|
11984
|
+
onBack: () => setQuery((held) => ({ ...held, sort: null, page: 1 })),
|
|
11985
|
+
...viewOrder?.onUseSort && columnSort ? {
|
|
11986
|
+
onUseSort: () => {
|
|
11987
|
+
const sort = columnSort;
|
|
11988
|
+
const chosen = { field: sort.id, direction: sort.direction };
|
|
11989
|
+
setHeldOrder({ order: "sorted", sorts: [chosen] });
|
|
11990
|
+
viewOrder.onUseSort?.(chosen);
|
|
11991
|
+
host.notify?.success(
|
|
11992
|
+
`Sorted by ${columnLabel(toolbarFields ?? [], sort.id)} ${sortArrow(sort.direction)}. ${MANUAL_SORT.replacedToast}`
|
|
11993
|
+
);
|
|
11994
|
+
}
|
|
11995
|
+
} : {}
|
|
11996
|
+
})
|
|
11997
|
+
}
|
|
11998
|
+
) : null,
|
|
11999
|
+
viewId && viewOrder?.onPlaced && hasSeveralRows ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_design_system35.Button, { size: "sm", variant: "ghost", onClick: () => setReordering(true), "data-records-reorder-open": "", children: "Reorder" }) : null,
|
|
12000
|
+
!merged && Boolean(editable) && rights.write ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_design_system35.Button, { size: "sm", disabled: editingBusy, onClick: () => void addRecord(), children: editingBusy ? "Adding\u2026" : "New record" }) : null,
|
|
12001
|
+
rights.structure && onAddField ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(AddFieldButton, { onClick: onAddField }) : null,
|
|
12002
|
+
/* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
|
|
12003
|
+
GridLayoutMenu,
|
|
12004
|
+
{
|
|
12005
|
+
look,
|
|
12006
|
+
firstColumn: sortedKeys[0] ? {
|
|
12007
|
+
key: sortedKeys[0],
|
|
12008
|
+
name: (() => {
|
|
12009
|
+
const first = (toolbarFields ?? []).find((f) => f.key === sortedKeys[0]);
|
|
12010
|
+
return first ? fieldName(first) : humanize(sortedKeys[0]);
|
|
12011
|
+
})()
|
|
12012
|
+
} : null,
|
|
12013
|
+
onChange: changeLayout,
|
|
12014
|
+
kept: Boolean(onPresentationChange)
|
|
12015
|
+
}
|
|
12016
|
+
)
|
|
12017
|
+
] }),
|
|
12018
|
+
...merged ? {
|
|
12019
|
+
overflow: {
|
|
12020
|
+
mode: phone ? "sheet" : toolbarFold2 >= 3 ? "menu" : "inline",
|
|
12021
|
+
pinned: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(import_jsx_runtime42.Fragment, { children: [
|
|
12022
|
+
/* @__PURE__ */ (0, import_jsx_runtime42.jsx)(UndoRedoLive, { source: undoSource }),
|
|
12023
|
+
Boolean(editable) && rights.write ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_design_system35.Button, { size: "sm", className: "shrink-0", disabled: added.busy, onClick: () => addRowRef.current(), children: added.busy ? "Adding\u2026" : "New record" }) : null
|
|
12024
|
+
] }),
|
|
12025
|
+
...phone ? {
|
|
12026
|
+
sheetTop: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "flex flex-col gap-1.5 pb-1", "data-records-grid-sheet-top": "", "data-matrx-table-tools-keep-open": "", children: [
|
|
12027
|
+
/* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
|
|
12028
|
+
"input",
|
|
12029
|
+
{
|
|
12030
|
+
type: "search",
|
|
12031
|
+
value: query.search,
|
|
12032
|
+
placeholder: "Search",
|
|
12033
|
+
"aria-label": "Search records",
|
|
12034
|
+
onChange: (event) => {
|
|
12035
|
+
const search = event.target.value;
|
|
12036
|
+
setQuery((held) => ({ ...held, search, page: 1 }));
|
|
12037
|
+
},
|
|
12038
|
+
className: "h-10 w-full min-w-0 rounded-md border border-input bg-background px-2 text-foreground placeholder:text-muted-foreground",
|
|
12039
|
+
style: { fontSize: "16px" }
|
|
12040
|
+
}
|
|
12041
|
+
),
|
|
12042
|
+
/* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("label", { className: "flex items-center gap-2 text-xs text-muted-foreground", "data-records-grid-sheet-sort": "", children: [
|
|
12043
|
+
/* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "shrink-0", children: "Sort by" }),
|
|
12044
|
+
/* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
|
|
12045
|
+
"select",
|
|
12046
|
+
{
|
|
12047
|
+
"aria-label": "Sort by",
|
|
12048
|
+
value: query.sort ? `${query.sort.id}:${query.sort.direction}` : "",
|
|
12049
|
+
onChange: (event) => {
|
|
12050
|
+
const [id, direction] = event.target.value.split(":");
|
|
12051
|
+
setQuery((held) => ({
|
|
12052
|
+
...held,
|
|
12053
|
+
page: 1,
|
|
12054
|
+
sort: id ? { id, direction: direction === "desc" ? "desc" : "asc" } : null
|
|
12055
|
+
}));
|
|
12056
|
+
},
|
|
12057
|
+
className: "h-10 min-w-0 flex-1 rounded-md border border-input bg-background px-2 text-foreground",
|
|
12058
|
+
style: { fontSize: "16px" },
|
|
12059
|
+
children: [
|
|
12060
|
+
/* @__PURE__ */ (0, import_jsx_runtime42.jsx)("option", { value: "", children: "No sort" }),
|
|
12061
|
+
ordered.flatMap((field2) => [
|
|
12062
|
+
/* @__PURE__ */ (0, import_jsx_runtime42.jsx)("option", { value: `${field2.key}:asc`, children: `${fieldName(field2)} \u2191` }, `${field2.key}:asc`),
|
|
12063
|
+
/* @__PURE__ */ (0, import_jsx_runtime42.jsx)("option", { value: `${field2.key}:desc`, children: `${fieldName(field2)} \u2193` }, `${field2.key}:desc`)
|
|
12064
|
+
])
|
|
12065
|
+
]
|
|
12066
|
+
}
|
|
12067
|
+
)
|
|
12068
|
+
] }),
|
|
12069
|
+
phoneSheetTools
|
|
12070
|
+
] }),
|
|
12071
|
+
...sheetExtras ? { sheetExtras } : {}
|
|
12072
|
+
} : {}
|
|
12073
|
+
}
|
|
12074
|
+
} : {}
|
|
12075
|
+
}),
|
|
12076
|
+
[
|
|
12077
|
+
merged,
|
|
12078
|
+
phone,
|
|
12079
|
+
query.search,
|
|
12080
|
+
query.sort,
|
|
12081
|
+
toolbarSlot,
|
|
12082
|
+
manual,
|
|
12083
|
+
columnSort,
|
|
12084
|
+
toolbarFields,
|
|
12085
|
+
viewOrder,
|
|
12086
|
+
host,
|
|
12087
|
+
viewId,
|
|
12088
|
+
hasSeveralRows,
|
|
12089
|
+
editable,
|
|
12090
|
+
rights.write,
|
|
12091
|
+
rights.structure,
|
|
12092
|
+
editingBusy,
|
|
12093
|
+
addRecord,
|
|
12094
|
+
onAddField,
|
|
12095
|
+
look,
|
|
12096
|
+
sortedKeys,
|
|
12097
|
+
changeLayout,
|
|
12098
|
+
onPresentationChange,
|
|
12099
|
+
toolbarFold2,
|
|
12100
|
+
undoSource,
|
|
12101
|
+
added.busy,
|
|
12102
|
+
ordered,
|
|
12103
|
+
phoneSheetTools,
|
|
12104
|
+
sheetExtras
|
|
12105
|
+
]
|
|
12106
|
+
);
|
|
11780
12107
|
const labelOfField = (0, import_react60.useCallback)(
|
|
11781
12108
|
(key) => {
|
|
11782
12109
|
const held = (fields.data ?? []).find((f) => f.key === key);
|
|
@@ -11945,25 +12272,12 @@ function Grid({
|
|
|
11945
12272
|
viewTabs: false,
|
|
11946
12273
|
stickyHeader: true,
|
|
11947
12274
|
pageSize,
|
|
11948
|
-
query:
|
|
11949
|
-
// THE STORE ANSWERS THE QUERY (tranche 6k): `data` is already the page.
|
|
11950
|
-
mode: "controlled",
|
|
11951
|
-
state: query,
|
|
11952
|
-
totalItems: records.data?.total ?? rows.length,
|
|
11953
|
-
onStateChange: setQuery,
|
|
11954
|
-
sourceProcessing: { search: "source", columnFilters: "source", sort: "source" }
|
|
11955
|
-
} : { mode: "controlled-local", state: query, onStateChange: setQuery },
|
|
12275
|
+
query: tableQuery,
|
|
11956
12276
|
tableClassName: (0, import_design_system35.cn)(
|
|
11957
12277
|
!look.wrap && "[&_tbody_td]:max-w-[28rem] [&_tbody_td]:overflow-hidden [&_tbody_td]:text-ellipsis [&_tbody_td]:whitespace-nowrap! [&_tbody_td_*]:whitespace-nowrap!",
|
|
11958
12278
|
look.fit === "scroll" && "sm:[&>table]:w-max! sm:[&>table]:max-w-none! sm:[&>table]:min-w-full!"
|
|
11959
12279
|
),
|
|
11960
|
-
|
|
11961
|
-
const footerRows = [
|
|
11962
|
-
...mayAddRow && rows.length > 0 ? [ADD_ROW] : [],
|
|
11963
|
-
...rows.length > 0 && (!merged || Object.keys(chosenSummaries).length > 0) ? [SUMMARY_ROW] : []
|
|
11964
|
-
];
|
|
11965
|
-
return footerRows.length > 0 ? { footerRows } : {};
|
|
11966
|
-
})(),
|
|
12280
|
+
footerRows,
|
|
11967
12281
|
...copyConfig ? { copy: copyConfig } : {},
|
|
11968
12282
|
...offerSelection ? {
|
|
11969
12283
|
selection: {
|
|
@@ -11997,75 +12311,15 @@ function Grid({
|
|
|
11997
12311
|
] })
|
|
11998
12312
|
}
|
|
11999
12313
|
} : {},
|
|
12000
|
-
detail:
|
|
12001
|
-
window:
|
|
12314
|
+
detail: DETAIL_OFF,
|
|
12315
|
+
window: WINDOW_OFF,
|
|
12002
12316
|
...controls.contextMenu ? { contextMenu: controls.contextMenu } : {},
|
|
12003
|
-
|
|
12004
|
-
spreadsheet: {
|
|
12005
|
-
enabled: true,
|
|
12006
|
-
writableColumnIds: pasteableKeys,
|
|
12007
|
-
onPatch: (patches, intent) => {
|
|
12008
|
-
if (merged) {
|
|
12009
|
-
void writeRange(patches, intent);
|
|
12010
|
-
return;
|
|
12011
|
-
}
|
|
12012
|
-
for (const patch of patches) {
|
|
12013
|
-
editing.setCell(patch.rowId, patch.columnId, patch.value);
|
|
12014
|
-
}
|
|
12015
|
-
},
|
|
12016
|
-
...merged ? { undoDepth: 0, onSelectionChange: followSelection, controllerRef: sheetController } : {},
|
|
12017
|
-
columnRefusal: (columnId) => {
|
|
12018
|
-
const field2 = (fields.data ?? []).find((f) => f.key === columnId);
|
|
12019
|
-
if (!field2) return `This table has no column called ${columnId}.`;
|
|
12020
|
-
return `${fieldName(field2)} cannot take a pasted value \u2014 ${fieldTypeLabel(field2)} is worked out or chosen, not typed.`;
|
|
12021
|
-
},
|
|
12022
|
-
validateCell: ({ columnId, value }) => {
|
|
12023
|
-
const field2 = (fields.data ?? []).find((f) => f.key === columnId);
|
|
12024
|
-
if (!field2) return `This table has no column called ${columnId}.`;
|
|
12025
|
-
const judged = valueFromPastedText(field2, value === null ? "" : String(value));
|
|
12026
|
-
return "refusal" in judged ? judged.refusal : void 0;
|
|
12027
|
-
},
|
|
12028
|
-
onPastePlan: receivePastePlan,
|
|
12029
|
-
/*
|
|
12030
|
-
* TYPE-TO-EDIT (design-system 0.43 `onEditRequest`): the cell a person is on
|
|
12031
|
-
* opens ITS OWN editor — the Field's — holding what they typed (a letter, an
|
|
12032
|
-
* IME composition, an Option/AltGr character, dictation). A worked-out column
|
|
12033
|
-
* or a row this person may not write answers false, so nothing is swallowed.
|
|
12034
|
-
*/
|
|
12035
|
-
onEditRequest: (address, seed) => {
|
|
12036
|
-
const field2 = (fields.data ?? []).find((f) => f.key === address.columnId);
|
|
12037
|
-
if (!field2 || !fieldIsEditable(field2) || !mayWriteRow(address.rowId)) return false;
|
|
12038
|
-
if (seed !== null && seed !== "" && typingOpens(field2) === "none") return false;
|
|
12039
|
-
editing.begin({ rowId: address.rowId, key: field2.key }, seed);
|
|
12040
|
-
return true;
|
|
12041
|
-
},
|
|
12042
|
-
...host.notify ? { notify: (message) => host.notify?.success(message) } : {}
|
|
12043
|
-
}
|
|
12044
|
-
} : {
|
|
12045
|
-
/*
|
|
12046
|
-
* RANGE SELECTION AND COPY FOR A READER (merge step 3, 0.42 `readOnly`). A person
|
|
12047
|
-
* who may not change this table may still drag out a range, select all and press
|
|
12048
|
-
* Cmd-C for a real Excel/Sheets block — the UX walk graded range selection BROKEN
|
|
12049
|
-
* on a table whose writes were refused. Every write gesture (cut, paste, clear,
|
|
12050
|
-
* fill, undo) says why it cannot, in the store's own words.
|
|
12051
|
-
*/
|
|
12052
|
-
spreadsheet: {
|
|
12053
|
-
enabled: true,
|
|
12054
|
-
readOnly: true,
|
|
12055
|
-
...merged ? { onSelectionChange: followSelection, controllerRef: sheetController } : {},
|
|
12056
|
-
readOnlyReason: !editable || !rights.known || rights.write ? "Editing is off here, so this table can be copied but not changed." : rights.why("write")
|
|
12057
|
-
}
|
|
12058
|
-
},
|
|
12317
|
+
spreadsheet: spreadsheetConfig,
|
|
12059
12318
|
...groupingConfig ? { grouping: groupingConfig } : {},
|
|
12060
12319
|
columnState,
|
|
12061
12320
|
...look.rowHeight ? { rowHeight: look.rowHeight } : {},
|
|
12062
|
-
virtualize: fills ?
|
|
12063
|
-
coverage:
|
|
12064
|
-
answeredBy: merged ? "source" : "client",
|
|
12065
|
-
...merged && typeof records.data?.total === "number" ? { matched: records.data.total } : {},
|
|
12066
|
-
noun: "record",
|
|
12067
|
-
...typeof records.data?.total === "number" ? { total: records.data.total } : {}
|
|
12068
|
-
},
|
|
12321
|
+
virtualize: fills ? VIRTUALIZE_FILLED : true,
|
|
12322
|
+
coverage: tableCoverage,
|
|
12069
12323
|
...tableStyleProp ? { tableStyle: tableStyleProp } : {},
|
|
12070
12324
|
...onOpenRecord ? { onRowOpen: (row) => onOpenRecord(row.id) } : {},
|
|
12071
12325
|
...offersRowActions ? { rowActions } : {},
|
|
@@ -12077,142 +12331,7 @@ function Grid({
|
|
|
12077
12331
|
...mayAddRow ? { addFirst: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(AddFirstRowButton, { onAdd: () => void addRowHere(), busy: added.busy }) } : {},
|
|
12078
12332
|
emptyDescription: rights.write ? "Add the first one and it appears here." : rights.known ? `Nothing has been written to this table. ${rights.why("write")}` : "Nothing has been written to this table."
|
|
12079
12333
|
}),
|
|
12080
|
-
toolbar:
|
|
12081
|
-
search: true,
|
|
12082
|
-
searchPlaceholder: "Search records",
|
|
12083
|
-
/*
|
|
12084
|
-
* A PHONE (tranche 6o): search and the one Tools button sit side by side at the START of
|
|
12085
|
-
* the row, so Tools is never scrolled out of reach; the search is the same query.
|
|
12086
|
-
*/
|
|
12087
|
-
...merged && phone ? {
|
|
12088
|
-
customSearch: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("div", { className: "flex min-w-0 flex-1 items-center gap-1.5", "data-records-grid-phone-search": "", children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
|
|
12089
|
-
"input",
|
|
12090
|
-
{
|
|
12091
|
-
type: "search",
|
|
12092
|
-
value: query.search,
|
|
12093
|
-
placeholder: "Search",
|
|
12094
|
-
"aria-label": "Search records",
|
|
12095
|
-
onChange: (event) => {
|
|
12096
|
-
const search = event.target.value;
|
|
12097
|
-
setQuery((held) => ({ ...held, search, page: 1 }));
|
|
12098
|
-
},
|
|
12099
|
-
className: "h-8 w-full min-w-0 rounded-md border border-input bg-background px-2 text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
12100
|
-
style: { fontSize: "16px" }
|
|
12101
|
-
}
|
|
12102
|
-
) })
|
|
12103
|
-
} : {},
|
|
12104
|
-
/* ONE ROW (lane TABLE-PAGE-CHROME): on a table page the grid's search, columns and
|
|
12105
|
-
sort are drawn INTO the page's toolbar row, never a second bar under it. */
|
|
12106
|
-
...toolbarSlot !== void 0 ? { portalInto: toolbarSlot, singleRow: true } : {},
|
|
12107
|
-
// SCR-2 / SCR-4: PRESENT for an admin, ABSENT for everyone else.
|
|
12108
|
-
// There is no third state: this package ships no disabled variant of
|
|
12109
|
-
// these controls, so a member cannot be shown a dead one by mistake.
|
|
12110
|
-
/*
|
|
12111
|
-
* WHERE THE GRID'S CONTROLS GO (lane C, review 2026-09-26 D6). Inline when the row has
|
|
12112
|
-
* room; behind the row's "…" once the page row is folded that far (Undo / Redo and New
|
|
12113
|
-
* record stay beside it — the primary action is never folded); on a phone EVERY control
|
|
12114
|
-
* — the table's own (clear, copy/export, group, columns), these, the search, the sort and
|
|
12115
|
-
* the page's filter, layout and share — in ONE "Tools" sheet. Never dropped.
|
|
12116
|
-
*/
|
|
12117
|
-
actions: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(import_jsx_runtime42.Fragment, { children: [
|
|
12118
|
-
manual ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
|
|
12119
|
-
SortStateControl,
|
|
12120
|
-
{
|
|
12121
|
-
...handOrderSortState({
|
|
12122
|
-
sortName: columnSort ? columnLabel(fields.data ?? [], columnSort.id) : null,
|
|
12123
|
-
direction: columnSort?.direction ?? null,
|
|
12124
|
-
onBack: () => setQuery((held) => ({ ...held, sort: null, page: 1 })),
|
|
12125
|
-
...viewOrder?.onUseSort && columnSort ? {
|
|
12126
|
-
onUseSort: () => {
|
|
12127
|
-
const sort = columnSort;
|
|
12128
|
-
const chosen = { field: sort.id, direction: sort.direction };
|
|
12129
|
-
setHeldOrder({ order: "sorted", sorts: [chosen] });
|
|
12130
|
-
viewOrder.onUseSort?.(chosen);
|
|
12131
|
-
host.notify?.success(
|
|
12132
|
-
`Sorted by ${columnLabel(fields.data ?? [], sort.id)} ${sortArrow(sort.direction)}. ${MANUAL_SORT.replacedToast}`
|
|
12133
|
-
);
|
|
12134
|
-
}
|
|
12135
|
-
} : {}
|
|
12136
|
-
})
|
|
12137
|
-
}
|
|
12138
|
-
) : null,
|
|
12139
|
-
viewId && viewOrder?.onPlaced && rows.length > 1 ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_design_system35.Button, { size: "sm", variant: "ghost", onClick: () => setReordering(true), "data-records-reorder-open": "", children: "Reorder" }) : null,
|
|
12140
|
-
!merged && Boolean(editable) && rights.write ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_design_system35.Button, { size: "sm", disabled: editing.busy, onClick: () => void editing.addRecord(), children: editing.busy ? "Adding\u2026" : "New record" }) : null,
|
|
12141
|
-
rights.structure && onAddField ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(AddFieldButton, { onClick: onAddField }) : null,
|
|
12142
|
-
/* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
|
|
12143
|
-
GridLayoutMenu,
|
|
12144
|
-
{
|
|
12145
|
-
look,
|
|
12146
|
-
firstColumn: sortedKeys[0] ? {
|
|
12147
|
-
key: sortedKeys[0],
|
|
12148
|
-
name: (() => {
|
|
12149
|
-
const first = (fields.data ?? []).find((f) => f.key === sortedKeys[0]);
|
|
12150
|
-
return first ? fieldName(first) : humanize(sortedKeys[0]);
|
|
12151
|
-
})()
|
|
12152
|
-
} : null,
|
|
12153
|
-
onChange: changeLayout,
|
|
12154
|
-
kept: Boolean(onPresentationChange)
|
|
12155
|
-
}
|
|
12156
|
-
)
|
|
12157
|
-
] }),
|
|
12158
|
-
...merged ? {
|
|
12159
|
-
overflow: {
|
|
12160
|
-
mode: phone ? "sheet" : toolbarFold2 >= 3 ? "menu" : "inline",
|
|
12161
|
-
pinned: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(import_jsx_runtime42.Fragment, { children: [
|
|
12162
|
-
/* @__PURE__ */ (0, import_jsx_runtime42.jsx)(UndoRedoButtons, { undo }),
|
|
12163
|
-
Boolean(editable) && rights.write ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_design_system35.Button, { size: "sm", className: "shrink-0", disabled: added.busy, onClick: () => void addRowHere(), children: added.busy ? "Adding\u2026" : "New record" }) : null
|
|
12164
|
-
] }),
|
|
12165
|
-
...phone ? {
|
|
12166
|
-
sheetTop: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "flex flex-col gap-1.5 pb-1", "data-records-grid-sheet-top": "", "data-matrx-table-tools-keep-open": "", children: [
|
|
12167
|
-
/* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
|
|
12168
|
-
"input",
|
|
12169
|
-
{
|
|
12170
|
-
type: "search",
|
|
12171
|
-
value: query.search,
|
|
12172
|
-
placeholder: "Search",
|
|
12173
|
-
"aria-label": "Search records",
|
|
12174
|
-
onChange: (event) => {
|
|
12175
|
-
const search = event.target.value;
|
|
12176
|
-
setQuery((held) => ({ ...held, search, page: 1 }));
|
|
12177
|
-
},
|
|
12178
|
-
className: "h-10 w-full min-w-0 rounded-md border border-input bg-background px-2 text-foreground placeholder:text-muted-foreground",
|
|
12179
|
-
style: { fontSize: "16px" }
|
|
12180
|
-
}
|
|
12181
|
-
),
|
|
12182
|
-
/* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("label", { className: "flex items-center gap-2 text-xs text-muted-foreground", "data-records-grid-sheet-sort": "", children: [
|
|
12183
|
-
/* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "shrink-0", children: "Sort by" }),
|
|
12184
|
-
/* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
|
|
12185
|
-
"select",
|
|
12186
|
-
{
|
|
12187
|
-
"aria-label": "Sort by",
|
|
12188
|
-
value: query.sort ? `${query.sort.id}:${query.sort.direction}` : "",
|
|
12189
|
-
onChange: (event) => {
|
|
12190
|
-
const [id, direction] = event.target.value.split(":");
|
|
12191
|
-
setQuery((held) => ({
|
|
12192
|
-
...held,
|
|
12193
|
-
page: 1,
|
|
12194
|
-
sort: id ? { id, direction: direction === "desc" ? "desc" : "asc" } : null
|
|
12195
|
-
}));
|
|
12196
|
-
},
|
|
12197
|
-
className: "h-10 min-w-0 flex-1 rounded-md border border-input bg-background px-2 text-foreground",
|
|
12198
|
-
style: { fontSize: "16px" },
|
|
12199
|
-
children: [
|
|
12200
|
-
/* @__PURE__ */ (0, import_jsx_runtime42.jsx)("option", { value: "", children: "No sort" }),
|
|
12201
|
-
ordered.flatMap((field2) => [
|
|
12202
|
-
/* @__PURE__ */ (0, import_jsx_runtime42.jsx)("option", { value: `${field2.key}:asc`, children: `${fieldName(field2)} \u2191` }, `${field2.key}:asc`),
|
|
12203
|
-
/* @__PURE__ */ (0, import_jsx_runtime42.jsx)("option", { value: `${field2.key}:desc`, children: `${fieldName(field2)} \u2193` }, `${field2.key}:desc`)
|
|
12204
|
-
])
|
|
12205
|
-
]
|
|
12206
|
-
}
|
|
12207
|
-
)
|
|
12208
|
-
] }),
|
|
12209
|
-
controls.sheetTools
|
|
12210
|
-
] }),
|
|
12211
|
-
...sheetExtras ? { sheetExtras } : {}
|
|
12212
|
-
} : {}
|
|
12213
|
-
}
|
|
12214
|
-
} : {}
|
|
12215
|
-
}
|
|
12334
|
+
toolbar: tableToolbar
|
|
12216
12335
|
},
|
|
12217
12336
|
`records-grid-${tableId}:${openingKey}`
|
|
12218
12337
|
)
|
|
@@ -12314,6 +12433,58 @@ function Grid({
|
|
|
12314
12433
|
}
|
|
12315
12434
|
) });
|
|
12316
12435
|
}
|
|
12436
|
+
var DETAIL_OFF = { enabled: false };
|
|
12437
|
+
var WINDOW_OFF = { enabled: false };
|
|
12438
|
+
var VIRTUALIZE_FILLED = { threshold: 40 };
|
|
12439
|
+
function menuFace(items) {
|
|
12440
|
+
return JSON.stringify(items, (_key, value) => typeof value === "function" ? "\u0192" : value);
|
|
12441
|
+
}
|
|
12442
|
+
function menuThroughLatest(items, at, latest) {
|
|
12443
|
+
return items.map((item) => {
|
|
12444
|
+
const path = `${at}/${item.id}`;
|
|
12445
|
+
return {
|
|
12446
|
+
...item,
|
|
12447
|
+
...item.onSelect ? { onSelect: () => latest.get(path)?.() } : {},
|
|
12448
|
+
...item.submenu ? { submenu: menuThroughLatest(item.submenu, path, latest) } : {}
|
|
12449
|
+
};
|
|
12450
|
+
});
|
|
12451
|
+
}
|
|
12452
|
+
function bindLatest(items, at, latest) {
|
|
12453
|
+
for (const item of items) {
|
|
12454
|
+
const path = `${at}/${item.id}`;
|
|
12455
|
+
if (item.onSelect) latest.set(path, item.onSelect);
|
|
12456
|
+
if (item.submenu) bindLatest(item.submenu, path, latest);
|
|
12457
|
+
}
|
|
12458
|
+
}
|
|
12459
|
+
function holdHeaderMenu(held, latest, key, items) {
|
|
12460
|
+
if (!items) return items;
|
|
12461
|
+
const at = `menu:${key}`;
|
|
12462
|
+
bindLatest(items, at, latest);
|
|
12463
|
+
const sig = menuFace(items);
|
|
12464
|
+
const was = held.get(at);
|
|
12465
|
+
if (was && was.sig === sig) return was.value;
|
|
12466
|
+
const value = menuThroughLatest(items, at, latest);
|
|
12467
|
+
held.set(at, { sig, value });
|
|
12468
|
+
return value;
|
|
12469
|
+
}
|
|
12470
|
+
function holdByContent(held, at, value) {
|
|
12471
|
+
if (value === void 0) return value;
|
|
12472
|
+
const sig = JSON.stringify(value);
|
|
12473
|
+
const was = held.get(at);
|
|
12474
|
+
if (was && was.sig === sig) return was.value;
|
|
12475
|
+
held.set(at, { sig, value });
|
|
12476
|
+
return value;
|
|
12477
|
+
}
|
|
12478
|
+
function sameColumns(a, b) {
|
|
12479
|
+
if (a === b) return true;
|
|
12480
|
+
if (a.length !== b.length) return false;
|
|
12481
|
+
return a.every((column, i) => {
|
|
12482
|
+
const other = b[i];
|
|
12483
|
+
if (column === other) return true;
|
|
12484
|
+
const keys = Object.keys(column);
|
|
12485
|
+
return keys.length === Object.keys(other).length && keys.every((key) => Object.is(column[key], other[key]));
|
|
12486
|
+
});
|
|
12487
|
+
}
|
|
12317
12488
|
function withHeaderMenus(columns, menuFor, filterFor) {
|
|
12318
12489
|
return columns.map((column) => {
|
|
12319
12490
|
const items = menuFor(String(column.id));
|
|
@@ -13178,6 +13349,32 @@ var ADDABLE_TYPES = ADDABLE_TYPE_IDS.map((value) => ({
|
|
|
13178
13349
|
value,
|
|
13179
13350
|
label: fieldTypeChoice(value)?.label ?? value
|
|
13180
13351
|
}));
|
|
13352
|
+
var ADDABLE_TYPE_SET = new Set(ADDABLE_TYPE_IDS);
|
|
13353
|
+
function customFieldRequestProblems(requests, existingLabels) {
|
|
13354
|
+
const problems = [];
|
|
13355
|
+
const taken = new Set(existingLabels.map((l) => l.trim().toLowerCase()));
|
|
13356
|
+
const asked = /* @__PURE__ */ new Set();
|
|
13357
|
+
if (requests.length === 0) problems.push("Name at least one field to add.");
|
|
13358
|
+
requests.forEach((request, index) => {
|
|
13359
|
+
const at = requests.length > 1 ? `Field ${index + 1}: ` : "";
|
|
13360
|
+
const label = typeof request?.label === "string" ? request.label.trim() : "";
|
|
13361
|
+
if (!label) {
|
|
13362
|
+
problems.push(`${at}a field needs a name.`);
|
|
13363
|
+
return;
|
|
13364
|
+
}
|
|
13365
|
+
const type = request.type ?? "text";
|
|
13366
|
+
if (!ADDABLE_TYPE_SET.has(type)) {
|
|
13367
|
+
problems.push(
|
|
13368
|
+
`${at}"${label}" cannot hold "${String(type)}". Choose one of: ${ADDABLE_TYPE_IDS.join(", ")}.`
|
|
13369
|
+
);
|
|
13370
|
+
}
|
|
13371
|
+
const key = label.toLowerCase();
|
|
13372
|
+
if (taken.has(key)) problems.push(`${at}there is already a field named "${label}".`);
|
|
13373
|
+
else if (asked.has(key)) problems.push(`${at}"${label}" is asked for twice.`);
|
|
13374
|
+
asked.add(key);
|
|
13375
|
+
});
|
|
13376
|
+
return problems;
|
|
13377
|
+
}
|
|
13181
13378
|
function asField(f) {
|
|
13182
13379
|
return {
|
|
13183
13380
|
id: f.id,
|
|
@@ -13198,8 +13395,10 @@ function CustomFieldsSection({
|
|
|
13198
13395
|
entityToken,
|
|
13199
13396
|
recordId,
|
|
13200
13397
|
title = "Custom fields",
|
|
13201
|
-
className
|
|
13398
|
+
className,
|
|
13399
|
+
agentDoor
|
|
13202
13400
|
}) {
|
|
13401
|
+
const client = (0, import_react68.useRecordsClient)();
|
|
13203
13402
|
const read = (0, import_react68.useEntityRecord)(entityToken, recordId);
|
|
13204
13403
|
const rights = (0, import_react68.useEntityFieldRights)(entityToken);
|
|
13205
13404
|
const mutation = (0, import_react68.useEntityFieldMutation)();
|
|
@@ -13210,14 +13409,70 @@ function CustomFieldsSection({
|
|
|
13210
13409
|
const [draft, setDraft] = (0, import_react67.useState)({});
|
|
13211
13410
|
const fields = (0, import_react67.useMemo)(() => read.data?.fields ?? [], [read.data]);
|
|
13212
13411
|
const mayAdd = rights.data?.may_declare === true;
|
|
13412
|
+
const live = (0, import_react67.useRef)({ fields, rights: rights.data, loading: rights.loading, reload: read.reload });
|
|
13413
|
+
(0, import_react67.useEffect)(() => {
|
|
13414
|
+
live.current = { fields, rights: rights.data, loading: rights.loading, reload: read.reload };
|
|
13415
|
+
});
|
|
13416
|
+
(0, import_react67.useEffect)(() => {
|
|
13417
|
+
if (!agentDoor) return;
|
|
13418
|
+
const state = () => {
|
|
13419
|
+
const now = live.current;
|
|
13420
|
+
return {
|
|
13421
|
+
entityToken,
|
|
13422
|
+
recordId,
|
|
13423
|
+
entityLabel: now.rights?.label ?? null,
|
|
13424
|
+
mayAdd: now.rights ? now.rights.may_declare === true : now.loading ? null : false,
|
|
13425
|
+
refusal: now.rights && !now.rights.may_declare ? now.rights.reason ?? null : null,
|
|
13426
|
+
fields: now.fields.map((f) => ({ key: f.key, label: f.label, type: f.type })),
|
|
13427
|
+
types: ADDABLE_TYPES
|
|
13428
|
+
};
|
|
13429
|
+
};
|
|
13430
|
+
const check = (requests) => {
|
|
13431
|
+
const now = state();
|
|
13432
|
+
if (now.mayAdd === false) {
|
|
13433
|
+
return [now.refusal ?? "This person may not add a field here."];
|
|
13434
|
+
}
|
|
13435
|
+
if (now.mayAdd === null) return ["The section is still reading who may add a field. Try again in a moment."];
|
|
13436
|
+
return customFieldRequestProblems(requests, now.fields.map((f) => f.label));
|
|
13437
|
+
};
|
|
13438
|
+
const addField = async (request) => {
|
|
13439
|
+
const problems = check([request]);
|
|
13440
|
+
if (problems.length > 0) return { ok: false, message: problems.join(" ") };
|
|
13441
|
+
const label = request.label.trim();
|
|
13442
|
+
const type = request.type ?? "text";
|
|
13443
|
+
const declared2 = await client.entityFieldDeclare({ token: entityToken, spec: { label, type } });
|
|
13444
|
+
if (!declared2.ok) {
|
|
13445
|
+
const hint = hintForAPerson(declared2.error.hint);
|
|
13446
|
+
return { ok: false, message: hint ? `${declared2.error.message} ${hint}` : declared2.error.message };
|
|
13447
|
+
}
|
|
13448
|
+
const back = await client.entityRecordRead({ token: entityToken, record_id: recordId });
|
|
13449
|
+
live.current.reload();
|
|
13450
|
+
if (!back.ok || !(back.data?.fields ?? []).some((f) => f.id === declared2.data)) {
|
|
13451
|
+
return {
|
|
13452
|
+
ok: false,
|
|
13453
|
+
message: `The store took "${label}" but this record does not show it yet. Nothing was lost \u2014 read the page again before adding it a second time.`
|
|
13454
|
+
};
|
|
13455
|
+
}
|
|
13456
|
+
return { ok: true, field_id: declared2.data, label, type };
|
|
13457
|
+
};
|
|
13458
|
+
return agentDoor({ entityToken, recordId, state, check, addField });
|
|
13459
|
+
}, [agentDoor, client, entityToken, recordId]);
|
|
13213
13460
|
if (!read.loading && !read.error && fields.length === 0 && !mayAdd) return null;
|
|
13214
13461
|
if (read.error) return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(RefusalLine, { error: read.error, className });
|
|
13215
13462
|
const values = read.data?.custom ?? {};
|
|
13216
13463
|
const written = read.data?.custom_written ?? {};
|
|
13464
|
+
const empty = fields.length === 0;
|
|
13465
|
+
if (empty && !adding) {
|
|
13466
|
+
return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("section", { className: (0, import_design_system38.cn)("flex min-w-0 items-center gap-2 text-xs", className), children: [
|
|
13467
|
+
/* @__PURE__ */ (0, import_jsx_runtime45.jsx)("h3", { className: "text-sm font-medium", children: title }),
|
|
13468
|
+
!read.loading ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "text-muted-foreground", children: "None yet" }) : null,
|
|
13469
|
+
mayAdd ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_design_system38.Button, { size: "sm", variant: "ghost", className: "ml-auto", onClick: () => setAdding(true), children: "Add field" }) : null
|
|
13470
|
+
] });
|
|
13471
|
+
}
|
|
13217
13472
|
return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("section", { className: (0, import_design_system38.cn)("flex min-w-0 flex-col gap-2 text-xs", className), children: [
|
|
13218
13473
|
/* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
13219
13474
|
/* @__PURE__ */ (0, import_jsx_runtime45.jsx)("h3", { className: "text-sm font-medium", children: title }),
|
|
13220
|
-
/* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "text-muted-foreground", children: fields.length }),
|
|
13475
|
+
!empty ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "text-muted-foreground", children: fields.length }) : null,
|
|
13221
13476
|
fields.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
|
|
13222
13477
|
import_design_system38.Button,
|
|
13223
13478
|
{
|
|
@@ -13286,9 +13541,9 @@ function CustomFieldsSection({
|
|
|
13286
13541
|
/* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("label", { className: "flex flex-col gap-1", children: [
|
|
13287
13542
|
/* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "text-muted-foreground", children: "Name" }),
|
|
13288
13543
|
/* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
|
|
13289
|
-
|
|
13544
|
+
import_design_system38.BasicInput,
|
|
13290
13545
|
{
|
|
13291
|
-
className: "h-8
|
|
13546
|
+
className: "h-8",
|
|
13292
13547
|
value: newLabel,
|
|
13293
13548
|
onChange: (e) => setNewLabel(e.target.value),
|
|
13294
13549
|
placeholder: "Account tier",
|
|
@@ -13296,17 +13551,12 @@ function CustomFieldsSection({
|
|
|
13296
13551
|
}
|
|
13297
13552
|
)
|
|
13298
13553
|
] }),
|
|
13299
|
-
/* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("
|
|
13554
|
+
/* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "flex flex-col gap-1", children: [
|
|
13300
13555
|
/* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "text-muted-foreground", children: "Holds" }),
|
|
13301
|
-
/* @__PURE__ */ (0, import_jsx_runtime45.
|
|
13302
|
-
"
|
|
13303
|
-
{
|
|
13304
|
-
|
|
13305
|
-
value: newType,
|
|
13306
|
-
onChange: (e) => setNewType(e.target.value),
|
|
13307
|
-
children: ADDABLE_TYPES.map((t) => /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("option", { value: t.value, children: t.label }, t.value))
|
|
13308
|
-
}
|
|
13309
|
-
)
|
|
13556
|
+
/* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(import_design_system38.Select, { value: newType, onValueChange: setNewType, children: [
|
|
13557
|
+
/* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_design_system38.SelectTrigger, { size: "sm", "aria-label": "What this field holds", children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_design_system38.SelectValue, {}) }),
|
|
13558
|
+
/* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_design_system38.SelectContent, { children: ADDABLE_TYPES.map((t) => /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_design_system38.SelectItem, { value: t.value, children: t.label }, t.value)) })
|
|
13559
|
+
] })
|
|
13310
13560
|
] }),
|
|
13311
13561
|
/* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_design_system38.Button, { size: "sm", type: "submit", disabled: mutation.saving || newLabel.trim() === "", children: mutation.saving ? "Adding\u2026" : "Add" })
|
|
13312
13562
|
]
|