@ai-matrx/records-ui 0.9.0 → 0.10.3
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 +89 -0
- package/dist/index.cjs +166 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -31
- package/dist/index.d.ts +66 -31
- package/dist/index.js +227 -104
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,94 @@
|
|
|
1
1
|
# Changelog — @ai-matrx/records-ui
|
|
2
2
|
|
|
3
|
+
## 0.10.3
|
|
4
|
+
|
|
5
|
+
Reissues the unpublished 0.10.2 candidate. Its live-store release gate exposed
|
|
6
|
+
a race in the field-add smoke harness: it unmounted each panel immediately
|
|
7
|
+
after clicking Save, before the asynchronous declaration had landed. The
|
|
8
|
+
helper now waits until the real store reports the exact new Field before
|
|
9
|
+
proceeding. Package behavior is unchanged.
|
|
10
|
+
|
|
11
|
+
### Consumer action
|
|
12
|
+
|
|
13
|
+
None.
|
|
14
|
+
|
|
15
|
+
## 0.10.2
|
|
16
|
+
|
|
17
|
+
Reissues the unpublished 0.10.1 candidate after its live-store release gate
|
|
18
|
+
revealed that the demo selector read `document.title` while the actual
|
|
19
|
+
`custom.record` row exposes the record payload as `data.title`. The smoke test
|
|
20
|
+
now selects Acme Industrial from the real row shape; package behavior is unchanged.
|
|
21
|
+
|
|
22
|
+
### Consumer action
|
|
23
|
+
|
|
24
|
+
None.
|
|
25
|
+
|
|
26
|
+
## 0.10.1
|
|
27
|
+
|
|
28
|
+
Reissues the unpublished 0.10.0 package after its live-store smoke test selected
|
|
29
|
+
the oldest demo record while asserting the identity of Acme Industrial. The
|
|
30
|
+
test now selects that record by title, so re-seeding cannot substitute another
|
|
31
|
+
record. Package behavior is unchanged from the 0.10.0 candidate.
|
|
32
|
+
|
|
33
|
+
### Consumer action
|
|
34
|
+
|
|
35
|
+
None.
|
|
36
|
+
|
|
37
|
+
## 0.10.0
|
|
38
|
+
|
|
39
|
+
**The three things the 19 September verdict (fifth pass) said a person hits the
|
|
40
|
+
moment they touch a table.**
|
|
41
|
+
|
|
42
|
+
**A double-click opens the typed editor, on every cell.** The verdict: "only the
|
|
43
|
+
Title column can be opened by double-clicking; double-clicking any other cell
|
|
44
|
+
opens a developer inspector — a floating window showing the record's raw JSON,
|
|
45
|
+
its machine keys, a truncated id, and the headings DOCUMENT, LEVEL and HIDDEN."
|
|
46
|
+
Two things did that. The data table ships a side panel AND a floating window,
|
|
47
|
+
both falling back to `DataRowInspector`; the grid now turns both OFF, because
|
|
48
|
+
this package has a record panel of its own (`Peek`, through `onRowOpen`) and a
|
|
49
|
+
second one beside it is the parallel layer the platform forbids. And a cell now
|
|
50
|
+
FILLS its cell and answers `dblclick` itself — the second press used to land on
|
|
51
|
+
bare table cell after the first had swapped the DOM under the pointer, and a
|
|
52
|
+
bare cell is a row click, which is exactly why a wide column worked and every
|
|
53
|
+
narrow one did not.
|
|
54
|
+
|
|
55
|
+
**Tab commits the typed value before it moves.** The verdict: "when we tabbed
|
|
56
|
+
along a row, the first five values saved and the last four were silently
|
|
57
|
+
dropped." The draft used to live in the open CELL, and this screen re-renders
|
|
58
|
+
that cell for reasons that have nothing to do with the person typing — every
|
|
59
|
+
accepted write calls `reload()`, which rebuilds the rows, the columns and the
|
|
60
|
+
cells. A cell that remounted mid-edit came back initialised from the stored
|
|
61
|
+
value, so the commit wrote what was already there and the screen correctly
|
|
62
|
+
treated it as "nothing changed". The draft now lives in the editing session,
|
|
63
|
+
beside the address of the open cell and the value it started from; `commit(move)`
|
|
64
|
+
takes no value at all, because there is only one place the typed value can be
|
|
65
|
+
read from. The editor is focused once per opening rather than on every render.
|
|
66
|
+
|
|
67
|
+
**"Total of linked records" can be added, and a refusal is a sentence.** The
|
|
68
|
+
panel now offers EVERY relation column — including the ones that hold a single
|
|
69
|
+
record, marked "holds one" — because hiding "Crew lead" from the list taught a
|
|
70
|
+
person nothing. `whatIsMissing` asks, in the panel's own words, what has not been
|
|
71
|
+
answered yet ("Crew lead points at one record at a time, so there is nothing to
|
|
72
|
+
total. Tick 'Can hold more than one' on Crew lead, or use Borrowed value to read
|
|
73
|
+
its one value."), the sentence sits under the button, and the panel never sends a
|
|
74
|
+
declaration it already knows the store must refuse.
|
|
75
|
+
|
|
76
|
+
**A hint written for an engineer never reaches a person.** A store refusal
|
|
77
|
+
carries a message for the person and a hint for whoever has to fix the caller
|
|
78
|
+
("FLD-11: … config.via names a relation Field of this same table, by its key").
|
|
79
|
+
This package printed both. `hintForAPerson` now filters the hint BY SHAPE — a
|
|
80
|
+
contract row id, a schema-qualified database object, a path inside a document,
|
|
81
|
+
a snake_case identifier or a SQLSTATE — and what it filters joins the SQLSTATE on
|
|
82
|
+
the notice's title, for an engineer. A hint in ordinary words is still shown
|
|
83
|
+
verbatim.
|
|
84
|
+
|
|
85
|
+
New exports: `whatIsMissing`, `hintForAPerson`, `hintIsMachineIdentity`.
|
|
86
|
+
|
|
87
|
+
### Consumer action
|
|
88
|
+
|
|
89
|
+
None. `GridEditing.commit` now takes `(move)` instead of `(value, move)` and
|
|
90
|
+
carries `draft` / `type`; nothing outside this package calls it.
|
|
91
|
+
|
|
3
92
|
## 0.9.0
|
|
4
93
|
|
|
5
94
|
**Sharing reaches the record and table screens (SCR-N-5).** `ShareControl` puts a
|
package/dist/index.cjs
CHANGED
|
@@ -134,6 +134,8 @@ __export(src_exports, {
|
|
|
134
134
|
formDocument: () => formDocument,
|
|
135
135
|
formFromRecord: () => formFromRecord,
|
|
136
136
|
groupLabel: () => groupLabel,
|
|
137
|
+
hintForAPerson: () => hintForAPerson,
|
|
138
|
+
hintIsMachineIdentity: () => hintIsMachineIdentity,
|
|
137
139
|
humanize: () => humanize,
|
|
138
140
|
idsOf: () => idsOf,
|
|
139
141
|
isId: () => isId,
|
|
@@ -166,7 +168,8 @@ __export(src_exports, {
|
|
|
166
168
|
useViewRecords: () => useViewRecords,
|
|
167
169
|
viewDocument: () => viewDocument,
|
|
168
170
|
viewFromRecord: () => viewFromRecord,
|
|
169
|
-
viewPatchDocument: () => viewPatchDocument
|
|
171
|
+
viewPatchDocument: () => viewPatchDocument,
|
|
172
|
+
whatIsMissing: () => whatIsMissing
|
|
170
173
|
});
|
|
171
174
|
module.exports = __toCommonJS(src_exports);
|
|
172
175
|
|
|
@@ -873,27 +876,49 @@ var TITLE = {
|
|
|
873
876
|
store_not_served: "The store is not being served",
|
|
874
877
|
internal: "The store said something this package does not recognise"
|
|
875
878
|
};
|
|
879
|
+
var MACHINE_IDENTITY = [
|
|
880
|
+
/\b[A-Z]{2,4}-\d{1,3}\b/,
|
|
881
|
+
// FLD-11, REC-29, AGT-7
|
|
882
|
+
/\b(?:custom|platform|iam|history|public)\.[a-z_]+/,
|
|
883
|
+
// a database object
|
|
884
|
+
/\b(?:config|data|document|spec)\.[a-z_]+/,
|
|
885
|
+
// a path inside a document
|
|
886
|
+
/\b[a-z]+(?:_[a-z]+)+\b/,
|
|
887
|
+
// any snake_case identifier
|
|
888
|
+
// A SQLSTATE: five characters of digits and capitals, at least one a digit —
|
|
889
|
+
// so `23514` and `PT409` match and an emphatic `NEVER` does not.
|
|
890
|
+
/\b(?=[0-9A-Z]{5}\b)(?=[0-9A-Z]*\d)[0-9A-Z]{5}\b/
|
|
891
|
+
];
|
|
892
|
+
function hintIsMachineIdentity(hint) {
|
|
893
|
+
return MACHINE_IDENTITY.some((shape) => shape.test(hint));
|
|
894
|
+
}
|
|
895
|
+
function hintForAPerson(hint) {
|
|
896
|
+
const said = (hint ?? "").trim();
|
|
897
|
+
if (said === "") return null;
|
|
898
|
+
return hintIsMachineIdentity(said) ? null : said;
|
|
899
|
+
}
|
|
876
900
|
function RefusalNotice({
|
|
877
901
|
error,
|
|
878
902
|
className,
|
|
879
903
|
actions
|
|
880
904
|
}) {
|
|
905
|
+
const hint = hintForAPerson(error.hint);
|
|
906
|
+
const forEngineers = [
|
|
907
|
+
error.sqlstate ? `The store's code for this was ${error.sqlstate}.` : null,
|
|
908
|
+
hint === null && error.hint ? error.hint : null
|
|
909
|
+
].filter(Boolean).join(" ");
|
|
881
910
|
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
882
911
|
import_design_system4.Alert,
|
|
883
912
|
{
|
|
884
913
|
variant: "destructive",
|
|
885
914
|
className: (0, import_design_system4.cn)("text-xs", className),
|
|
886
|
-
...
|
|
915
|
+
...forEngineers ? { title: forEngineers } : {},
|
|
887
916
|
children: [
|
|
888
917
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_design_system4.AlertTitle, { className: "text-xs font-medium", children: TITLE[error.code] ?? "The store refused this" }),
|
|
889
918
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_design_system4.AlertDescription, { className: "space-y-1 text-xs", children: [
|
|
890
919
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { children: error.message }),
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
"The store's code for this was ",
|
|
894
|
-
error.sqlstate,
|
|
895
|
-
"."
|
|
896
|
-
] }) : null,
|
|
920
|
+
hint ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "opacity-80", children: hint }) : null,
|
|
921
|
+
forEngineers ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "sr-only", ...error.sqlstate ? { "data-sqlstate": error.sqlstate } : {}, children: forEngineers }) : null,
|
|
897
922
|
actions
|
|
898
923
|
] })
|
|
899
924
|
]
|
|
@@ -901,7 +926,14 @@ function RefusalNotice({
|
|
|
901
926
|
);
|
|
902
927
|
}
|
|
903
928
|
function RefusalLine({ error, className }) {
|
|
904
|
-
|
|
929
|
+
const hint = hintForAPerson(error.hint);
|
|
930
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { className: (0, import_design_system4.cn)("text-xs text-destructive", className), title: error.hint ?? void 0, children: [
|
|
931
|
+
error.message,
|
|
932
|
+
hint ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "opacity-80", children: [
|
|
933
|
+
" ",
|
|
934
|
+
hint
|
|
935
|
+
] }) : null
|
|
936
|
+
] });
|
|
905
937
|
}
|
|
906
938
|
|
|
907
939
|
// src/values.tsx
|
|
@@ -1081,6 +1113,8 @@ async function versionOf(client, cache, recordId) {
|
|
|
1081
1113
|
function useGridEditing({ tableId, fields, rows, reload, canWrite }) {
|
|
1082
1114
|
const client = (0, import_react11.useRecordsClient)();
|
|
1083
1115
|
const [editing, setEditing] = (0, import_react10.useState)(null);
|
|
1116
|
+
const [draft, setDraft] = (0, import_react10.useState)(null);
|
|
1117
|
+
const startedFrom = (0, import_react10.useRef)(null);
|
|
1084
1118
|
const [optimistic, setOptimistic] = (0, import_react10.useState)({});
|
|
1085
1119
|
const [saving, setSaving] = (0, import_react10.useState)({});
|
|
1086
1120
|
const [refusals, setRefusals] = (0, import_react10.useState)({});
|
|
@@ -1112,15 +1146,31 @@ function useGridEditing({ tableId, fields, rows, reload, canWrite }) {
|
|
|
1112
1146
|
(rowId, key) => refusals[cellKey(rowId, key)] ?? null,
|
|
1113
1147
|
[refusals]
|
|
1114
1148
|
);
|
|
1149
|
+
const shownValue = (0, import_react10.useCallback)(
|
|
1150
|
+
(rowId, key) => {
|
|
1151
|
+
const held = optimistic[cellKey(rowId, key)];
|
|
1152
|
+
if (held !== void 0) return held;
|
|
1153
|
+
return rows.find((r) => r.id === rowId)?.document?.[key];
|
|
1154
|
+
},
|
|
1155
|
+
[optimistic, rows]
|
|
1156
|
+
);
|
|
1115
1157
|
const begin = (0, import_react10.useCallback)(
|
|
1116
1158
|
(address) => {
|
|
1117
1159
|
if (!canWrite) return;
|
|
1160
|
+
if (editing && editing.rowId === address.rowId && editing.key === address.key) return;
|
|
1161
|
+
const current = shownValue(address.rowId, address.key);
|
|
1162
|
+
startedFrom.current = current;
|
|
1163
|
+
setDraft(current);
|
|
1118
1164
|
setEditing(address);
|
|
1119
1165
|
void versionOf(client, versions.current, address.rowId);
|
|
1120
1166
|
},
|
|
1121
|
-
[canWrite, client]
|
|
1167
|
+
[canWrite, client, editing, shownValue]
|
|
1122
1168
|
);
|
|
1123
|
-
const cancel = (0, import_react10.useCallback)(() =>
|
|
1169
|
+
const cancel = (0, import_react10.useCallback)(() => {
|
|
1170
|
+
setEditing(null);
|
|
1171
|
+
setDraft(null);
|
|
1172
|
+
startedFrom.current = null;
|
|
1173
|
+
}, []);
|
|
1124
1174
|
const writeCellNow = (0, import_react10.useCallback)(
|
|
1125
1175
|
async (rowId, key, value2) => {
|
|
1126
1176
|
const id = cellKey(rowId, key);
|
|
@@ -1182,21 +1232,31 @@ function useGridEditing({ tableId, fields, rows, reload, canWrite }) {
|
|
|
1182
1232
|
[client, reload]
|
|
1183
1233
|
);
|
|
1184
1234
|
const commit = (0, import_react10.useCallback)(
|
|
1185
|
-
(
|
|
1235
|
+
(move = null) => {
|
|
1186
1236
|
const open = editing;
|
|
1187
1237
|
if (!open) return;
|
|
1188
|
-
const
|
|
1189
|
-
|
|
1190
|
-
if (!unchanged) void writeCell(open.rowId, open.key, value2);
|
|
1238
|
+
const value2 = draft;
|
|
1239
|
+
if (!sameValue(startedFrom.current, value2)) void writeCell(open.rowId, open.key, value2);
|
|
1191
1240
|
if (!move) {
|
|
1192
1241
|
setEditing(null);
|
|
1242
|
+
setDraft(null);
|
|
1243
|
+
startedFrom.current = null;
|
|
1193
1244
|
return;
|
|
1194
1245
|
}
|
|
1195
1246
|
const next = step(open, order, rowIds, move);
|
|
1247
|
+
if (!next) {
|
|
1248
|
+
setEditing(null);
|
|
1249
|
+
setDraft(null);
|
|
1250
|
+
startedFrom.current = null;
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
const starting = shownValue(next.rowId, next.key);
|
|
1254
|
+
startedFrom.current = starting;
|
|
1255
|
+
setDraft(starting);
|
|
1196
1256
|
setEditing(next);
|
|
1197
|
-
|
|
1257
|
+
void versionOf(client, versions.current, next.rowId);
|
|
1198
1258
|
},
|
|
1199
|
-
[editing,
|
|
1259
|
+
[editing, draft, writeCell, order, rowIds, client, shownValue]
|
|
1200
1260
|
);
|
|
1201
1261
|
const retry = (0, import_react10.useCallback)(
|
|
1202
1262
|
(rowId, key) => {
|
|
@@ -1251,12 +1311,18 @@ function useGridEditing({ tableId, fields, rows, reload, canWrite }) {
|
|
|
1251
1311
|
}
|
|
1252
1312
|
reload();
|
|
1253
1313
|
const first = order[0];
|
|
1254
|
-
if (first)
|
|
1314
|
+
if (first) {
|
|
1315
|
+
startedFrom.current = void 0;
|
|
1316
|
+
setDraft(void 0);
|
|
1317
|
+
setEditing({ rowId: written.data, key: first });
|
|
1318
|
+
}
|
|
1255
1319
|
return null;
|
|
1256
1320
|
}, [client, tableId, reload, order]);
|
|
1257
1321
|
return {
|
|
1258
1322
|
valueFor,
|
|
1259
1323
|
editing,
|
|
1324
|
+
draft,
|
|
1325
|
+
type: setDraft,
|
|
1260
1326
|
stateOf,
|
|
1261
1327
|
refusalOf,
|
|
1262
1328
|
begin,
|
|
@@ -1319,27 +1385,40 @@ function GridCell({
|
|
|
1319
1385
|
OpenCell,
|
|
1320
1386
|
{
|
|
1321
1387
|
field,
|
|
1322
|
-
|
|
1323
|
-
|
|
1388
|
+
draft: editing.draft,
|
|
1389
|
+
onType: editing.type,
|
|
1390
|
+
onCommit: (move) => editing.commit(move),
|
|
1324
1391
|
onCancel: editing.cancel
|
|
1325
1392
|
}
|
|
1326
1393
|
);
|
|
1327
1394
|
}
|
|
1328
|
-
|
|
1395
|
+
const openThisCell = (event) => {
|
|
1396
|
+
event.preventDefault();
|
|
1397
|
+
event.stopPropagation();
|
|
1398
|
+
editing.begin({ rowId: row.id, key: field.key });
|
|
1399
|
+
};
|
|
1400
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { "data-matrx-cell-control": "", className: "flex w-full min-w-0 flex-col gap-0.5", children: [
|
|
1329
1401
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
1330
1402
|
"span",
|
|
1331
1403
|
{
|
|
1332
1404
|
role: editable ? "button" : void 0,
|
|
1333
1405
|
tabIndex: editable ? 0 : void 0,
|
|
1334
1406
|
"aria-label": editable ? `Edit ${fieldName(field)}` : void 0,
|
|
1335
|
-
title: editable ? `${fieldName(field)} \u2014 press Enter to edit` :
|
|
1407
|
+
title: editable ? `${fieldName(field)} \u2014 click or press Enter to edit` : `${fieldName(field)} is worked out by the system, so it cannot be typed in`,
|
|
1336
1408
|
className: (0, import_design_system6.cn)(
|
|
1337
|
-
|
|
1409
|
+
// `w-full` so the whole cell is the control. A gap beside it is bare
|
|
1410
|
+
// table cell, and a click there is a row click.
|
|
1411
|
+
"block w-full min-w-0 truncate rounded px-1 py-0.5",
|
|
1338
1412
|
editable ? "cursor-text hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" : "",
|
|
1339
1413
|
state === "saving" ? "opacity-60" : "",
|
|
1340
1414
|
state === "refused" ? "ring-1 ring-destructive" : ""
|
|
1341
1415
|
),
|
|
1342
|
-
onClick: editable ?
|
|
1416
|
+
onClick: editable ? openThisCell : void 0,
|
|
1417
|
+
onDoubleClick: (event) => {
|
|
1418
|
+
event.preventDefault();
|
|
1419
|
+
event.stopPropagation();
|
|
1420
|
+
if (editable) editing.begin({ rowId: row.id, key: field.key });
|
|
1421
|
+
},
|
|
1343
1422
|
onKeyDown: editable ? (event) => {
|
|
1344
1423
|
if (event.key === "Enter" || event.key === "F2") {
|
|
1345
1424
|
event.preventDefault();
|
|
@@ -1382,11 +1461,11 @@ function closesOnBlur(field) {
|
|
|
1382
1461
|
}
|
|
1383
1462
|
function OpenCell({
|
|
1384
1463
|
field,
|
|
1385
|
-
|
|
1464
|
+
draft,
|
|
1465
|
+
onType,
|
|
1386
1466
|
onCommit,
|
|
1387
1467
|
onCancel
|
|
1388
1468
|
}) {
|
|
1389
|
-
const [draft, setDraft] = (0, import_react10.useState)(value2);
|
|
1390
1469
|
const multiline = editorKindFor(field) === "long_text";
|
|
1391
1470
|
const blurCommits = closesOnBlur(field);
|
|
1392
1471
|
const done = (0, import_react10.useRef)(false);
|
|
@@ -1395,22 +1474,24 @@ function OpenCell({
|
|
|
1395
1474
|
done.current = true;
|
|
1396
1475
|
run();
|
|
1397
1476
|
};
|
|
1477
|
+
const host = (0, import_react10.useRef)(null);
|
|
1478
|
+
(0, import_react10.useEffect)(() => {
|
|
1479
|
+
const control = host.current?.querySelector("input, textarea, [role='combobox'], button");
|
|
1480
|
+
if (control && document.activeElement !== control) control.focus();
|
|
1481
|
+
}, [field.id]);
|
|
1398
1482
|
const blur = blurCommits ? {
|
|
1399
1483
|
onBlur: (event) => {
|
|
1400
1484
|
if (event.currentTarget.contains(event.relatedTarget)) return;
|
|
1401
|
-
once(() => onCommit(
|
|
1485
|
+
once(() => onCommit(null));
|
|
1402
1486
|
}
|
|
1403
1487
|
} : {};
|
|
1404
1488
|
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|
|
1405
1489
|
"span",
|
|
1406
1490
|
{
|
|
1491
|
+
ref: host,
|
|
1407
1492
|
"data-matrx-cell-control": "",
|
|
1408
|
-
className: "flex min-w-[10rem] flex-col gap-1",
|
|
1409
|
-
|
|
1410
|
-
if (!node) return;
|
|
1411
|
-
const control = node.querySelector("input, textarea, [role='combobox'], button");
|
|
1412
|
-
if (control && document.activeElement !== control) control.focus();
|
|
1413
|
-
},
|
|
1493
|
+
className: "flex w-full min-w-[10rem] flex-col gap-1",
|
|
1494
|
+
onDoubleClick: (event) => event.stopPropagation(),
|
|
1414
1495
|
onKeyDown: (event) => {
|
|
1415
1496
|
if (event.key === "Escape") {
|
|
1416
1497
|
event.preventDefault();
|
|
@@ -1421,20 +1502,20 @@ function OpenCell({
|
|
|
1421
1502
|
if (event.key === "Tab") {
|
|
1422
1503
|
event.preventDefault();
|
|
1423
1504
|
event.stopPropagation();
|
|
1424
|
-
once(() => onCommit(
|
|
1505
|
+
once(() => onCommit(event.shiftKey ? "previous" : "next"));
|
|
1425
1506
|
return;
|
|
1426
1507
|
}
|
|
1427
1508
|
if (event.key === "Enter" && (!multiline || event.metaKey || event.ctrlKey)) {
|
|
1428
1509
|
event.preventDefault();
|
|
1429
1510
|
event.stopPropagation();
|
|
1430
|
-
once(() => onCommit(
|
|
1511
|
+
once(() => onCommit(null));
|
|
1431
1512
|
}
|
|
1432
1513
|
},
|
|
1433
1514
|
...blur,
|
|
1434
1515
|
children: [
|
|
1435
|
-
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(FieldControl, { field, value: draft, onChange:
|
|
1516
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(FieldControl, { field, value: draft, onChange: onType, id: `cell-${field.id}` }),
|
|
1436
1517
|
blurCommits ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "text-[10px] text-muted-foreground", children: multiline ? "\u2318\u23CE saves \xB7 Esc cancels \xB7 Tab next" : "\u23CE saves \xB7 Esc cancels \xB7 Tab next" }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "flex items-center gap-1", children: [
|
|
1437
|
-
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_design_system6.Button, { size: "sm", onClick: () => once(() => onCommit(
|
|
1518
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_design_system6.Button, { size: "sm", onClick: () => once(() => onCommit(null)), children: "Save" }),
|
|
1438
1519
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_design_system6.Button, { size: "sm", variant: "ghost", onClick: () => once(onCancel), children: "Cancel" })
|
|
1439
1520
|
] })
|
|
1440
1521
|
]
|
|
@@ -1523,6 +1604,8 @@ function Grid({
|
|
|
1523
1604
|
tableId: `records-grid-${tableId}`,
|
|
1524
1605
|
stickyHeader: true,
|
|
1525
1606
|
pageSize,
|
|
1607
|
+
detail: { enabled: false },
|
|
1608
|
+
window: { enabled: false },
|
|
1526
1609
|
...onOpenRecord ? { onRowOpen: (row) => onOpenRecord(row.id) } : {},
|
|
1527
1610
|
...canWrite ? {
|
|
1528
1611
|
rowActions: (row) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
@@ -2326,7 +2409,6 @@ function FieldEditor({ tableId, field, onSaved, onCancel, onRemoved, className }
|
|
|
2326
2409
|
[fields.data, field?.id]
|
|
2327
2410
|
);
|
|
2328
2411
|
const relations = (0, import_react25.useMemo)(() => others.filter((f) => f.type === "relation"), [others]);
|
|
2329
|
-
const manyRelations = (0, import_react25.useMemo)(() => relations.filter((f) => f.multi), [relations]);
|
|
2330
2412
|
const choice = fieldTypeChoice(type);
|
|
2331
2413
|
const editing = Boolean(field);
|
|
2332
2414
|
const derivedKey = field?.key ?? keyFor(label);
|
|
@@ -2334,7 +2416,9 @@ function FieldEditor({ tableId, field, onSaved, onCancel, onRemoved, className }
|
|
|
2334
2416
|
return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: (0, import_design_system12.cn)("text-xs text-muted-foreground", className), children: rights.reason });
|
|
2335
2417
|
}
|
|
2336
2418
|
const options = optionText.split("\n").map((word) => word.trim()).filter((word) => word !== "");
|
|
2419
|
+
const missing = editing ? null : whatIsMissing({ type, label, options, via, pick, agg, of, relations, formulaFields });
|
|
2337
2420
|
const save = async () => {
|
|
2421
|
+
if (missing) return;
|
|
2338
2422
|
if (editing && field) {
|
|
2339
2423
|
const patch = {
|
|
2340
2424
|
label,
|
|
@@ -2440,7 +2524,6 @@ function FieldEditor({ tableId, field, onSaved, onCancel, onRemoved, className }
|
|
|
2440
2524
|
formulaFields,
|
|
2441
2525
|
setFormulaFields,
|
|
2442
2526
|
relations,
|
|
2443
|
-
manyRelations,
|
|
2444
2527
|
others,
|
|
2445
2528
|
existingOptionsTable: Boolean(field?.options_table_id ?? field?.config?.["options_table_id"])
|
|
2446
2529
|
}
|
|
@@ -2555,8 +2638,17 @@ function FieldEditor({ tableId, field, onSaved, onCancel, onRemoved, className }
|
|
|
2555
2638
|
] }, `${rule.kind}-${index}`))
|
|
2556
2639
|
] }),
|
|
2557
2640
|
mutation.error ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(RefusalNotice, { error: mutation.error }) : null,
|
|
2641
|
+
missing ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "text-[11px] text-destructive", children: missing }) : null,
|
|
2558
2642
|
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
2559
|
-
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
2643
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
2644
|
+
import_design_system12.Button,
|
|
2645
|
+
{
|
|
2646
|
+
size: "sm",
|
|
2647
|
+
disabled: mutation.saving || missing !== null,
|
|
2648
|
+
onClick: () => void save(),
|
|
2649
|
+
children: mutation.saving ? "Saving\u2026" : field ? "Save field" : "Add field"
|
|
2650
|
+
}
|
|
2651
|
+
),
|
|
2560
2652
|
onCancel ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_design_system12.Button, { size: "sm", variant: "ghost", onClick: onCancel, children: "Cancel" }) : null,
|
|
2561
2653
|
field ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "ml-auto flex items-center gap-2", children: askingToRemove ? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
|
|
2562
2654
|
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("span", { className: "text-destructive", children: [
|
|
@@ -2570,6 +2662,34 @@ function FieldEditor({ tableId, field, onSaved, onCancel, onRemoved, className }
|
|
|
2570
2662
|
] })
|
|
2571
2663
|
] });
|
|
2572
2664
|
}
|
|
2665
|
+
function whatIsMissing(state) {
|
|
2666
|
+
const { type, label, options, via, pick, agg, of, relations, formulaFields } = state;
|
|
2667
|
+
if (label.trim() === "") return "Give the column a name.";
|
|
2668
|
+
if (type === "select" || type === "multi_select") {
|
|
2669
|
+
if (options.length === 0) return "Type the choices this column offers, one per line.";
|
|
2670
|
+
return null;
|
|
2671
|
+
}
|
|
2672
|
+
if (type === "lookup" || type === "rollup") {
|
|
2673
|
+
if (relations.length === 0) {
|
|
2674
|
+
return type === "rollup" ? "A total is worked out across the records a column points at, and this table has no column that points at records yet. Add a Person or File column that can hold more than one, then come back." : "A borrowed value is read from a record a column points at, and this table has no column that points at another record yet. Add a Person or File column, then come back.";
|
|
2675
|
+
}
|
|
2676
|
+
const chosen = relations.find((relation) => relation.key === via);
|
|
2677
|
+
if (!chosen) {
|
|
2678
|
+
return type === "rollup" ? "Choose the column whose records this one adds up." : "Choose the column whose record this one reads from.";
|
|
2679
|
+
}
|
|
2680
|
+
if (type === "rollup" && !chosen.multi) {
|
|
2681
|
+
return `${fieldName(chosen)} points at one record at a time, so there is nothing to total. Tick "Can hold more than one" on ${fieldName(chosen)}, or use Borrowed value to read its one value.`;
|
|
2682
|
+
}
|
|
2683
|
+
if (type === "lookup" && pick.trim() === "") {
|
|
2684
|
+
return `Say which value to read from the record ${fieldName(chosen)} points at.`;
|
|
2685
|
+
}
|
|
2686
|
+
if (type === "rollup" && agg !== "count" && of.trim() === "") {
|
|
2687
|
+
return "Say which value of those records to work out.";
|
|
2688
|
+
}
|
|
2689
|
+
return null;
|
|
2690
|
+
}
|
|
2691
|
+
return null;
|
|
2692
|
+
}
|
|
2573
2693
|
function multiApplies(type) {
|
|
2574
2694
|
return type === "member" || type === "attachment" || type === "text";
|
|
2575
2695
|
}
|
|
@@ -2635,16 +2755,19 @@ function TypeQuestions(props) {
|
|
|
2635
2755
|
] });
|
|
2636
2756
|
}
|
|
2637
2757
|
if (type === "lookup" || type === "rollup") {
|
|
2638
|
-
const usable =
|
|
2758
|
+
const usable = props.relations;
|
|
2639
2759
|
if (usable.length === 0) {
|
|
2640
|
-
return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "text-[11px] text-muted-foreground", children: type === "rollup" ? "This kind of column works something out from records
|
|
2760
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "text-[11px] text-muted-foreground", children: type === "rollup" ? "This kind of column works something out from the records another column points at, and this table has no column that points at records yet. Add a Person or File column that can hold more than one first." : "This kind of column reads a value from a record another column points at, and this table has no column that points at another record yet. Add a Person or File column first." });
|
|
2641
2761
|
}
|
|
2642
2762
|
return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "grid gap-3 sm:grid-cols-2", children: [
|
|
2643
2763
|
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "flex flex-col gap-1", children: [
|
|
2644
2764
|
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_design_system12.Label, { className: "text-xs", children: type === "rollup" ? "Across the records in" : "Read it from" }),
|
|
2645
2765
|
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_design_system12.Select, { value: props.via, onValueChange: props.setVia, children: [
|
|
2646
2766
|
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_design_system12.SelectTrigger, { size: "sm", "aria-label": type === "rollup" ? "Across the records in" : "Read it from", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_design_system12.SelectValue, { placeholder: "Choose a column" }) }),
|
|
2647
|
-
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_design_system12.SelectContent, { children: usable.map((f) => /* @__PURE__ */ (0, import_jsx_runtime14.
|
|
2767
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_design_system12.SelectContent, { children: usable.map((f) => /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_design_system12.SelectItem, { value: f.key, children: [
|
|
2768
|
+
fieldName(f),
|
|
2769
|
+
type === "rollup" && !f.multi ? " \u2014 holds one" : ""
|
|
2770
|
+
] }, f.id)) })
|
|
2648
2771
|
] })
|
|
2649
2772
|
] }),
|
|
2650
2773
|
type === "lookup" ? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("label", { className: "flex flex-col gap-1", children: [
|