@ai-matrx/records-ui 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/dist/index.cjs +80 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +44 -2
- package/dist/index.d.ts +44 -2
- package/dist/index.js +96 -34
- package/dist/index.js.map +1 -1
- package/package.json +8 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog — @ai-matrx/records-ui
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
What the first headless walk of 0.6.0 through a real browser found, in the same
|
|
6
|
+
hour — every one of them on a table a person had just made.
|
|
7
|
+
|
|
8
|
+
- **A new table takes an empty record.** `DEFAULT_FIELDS` made `title`
|
|
9
|
+
REQUIRED, so "New record" — which writes an empty record and opens its first
|
|
10
|
+
cell — was refused (`REC-51: Title is required`) on every table the app makes.
|
|
11
|
+
A title is what makes a record a chip (REC-2), not a promise it can never be
|
|
12
|
+
written without; an organization that wants it demanded says so in the field
|
|
13
|
+
editor and the store then enforces it for everyone.
|
|
14
|
+
- **`TableSettings` can delete the table.** SCR-3 claims rename, reorder,
|
|
15
|
+
retype and delete; the panel only ever deleted a FIELD, so a table made in the
|
|
16
|
+
product could not be got rid of from anywhere in it. It is the same soft
|
|
17
|
+
delete every record gets, through the same door, and the consequence is named
|
|
18
|
+
before the second press. `onDeleted` lets the host leave the page, and
|
|
19
|
+
`TablePage` passes its `onLeave`.
|
|
20
|
+
- **Nobody conflicts with themselves any more.** Three separate paths wrote a
|
|
21
|
+
record twice against one version and were — correctly — refused `PT409` by the
|
|
22
|
+
store, so the screen showed a person a conflict with nobody but themselves:
|
|
23
|
+
Tab fired the cell's key handler AND its blur handler; two cells of one record
|
|
24
|
+
were written in parallel; and `useRecordVersion` never moved after a save, so
|
|
25
|
+
switching a saved view's layout twice, or saving a form twice, conflicted with
|
|
26
|
+
the save before it. One commit per opening, one write at a time per record,
|
|
27
|
+
and every successful write hands its new version back.
|
|
28
|
+
- **The demo harness renders like the product.** It imported the design
|
|
29
|
+
system's structural sheet and nothing else, so every page was unstyled HTML in
|
|
30
|
+
a real browser while `pnpm test` was green. It now runs Tailwind over this
|
|
31
|
+
package's own source, and the address bar takes `?demo=` and `?table=` so a
|
|
32
|
+
walk can make a throwaway table, work in it and delete it again.
|
|
33
|
+
|
|
34
|
+
### Consumer action
|
|
35
|
+
|
|
36
|
+
**`useRecordVersion` now answers `{ version, note }` rather than a number.** A
|
|
37
|
+
screen that writes the same record more than once must hand each successful
|
|
38
|
+
write's new version back through `note(...)`, or the next write will be refused
|
|
39
|
+
as a conflict with the person who just made the last one. Nothing else changed:
|
|
40
|
+
`version` is the same value the hook used to return, and `null` still means "no
|
|
41
|
+
version was read", which is the store's ordinary last-writer-wins write.
|
|
42
|
+
|
|
43
|
+
A host that shows `TableSettings` on a page about one table should pass
|
|
44
|
+
`onDeleted`, or the person is left on a page about a table that is gone.
|
|
45
|
+
|
|
3
46
|
## 0.6.0
|
|
4
47
|
|
|
5
48
|
Editing where the value is shown, and four other things an independent walk of
|
package/dist/index.cjs
CHANGED
|
@@ -937,6 +937,7 @@ function useGridEditing({ tableId, fields, rows, reload, canWrite }) {
|
|
|
937
937
|
const [rowError, setRowError] = (0, import_react8.useState)(null);
|
|
938
938
|
const [busy, setBusy] = (0, import_react8.useState)(false);
|
|
939
939
|
const versions = (0, import_react8.useRef)(/* @__PURE__ */ new Map());
|
|
940
|
+
const queues = (0, import_react8.useRef)(/* @__PURE__ */ new Map());
|
|
940
941
|
const editable = (0, import_react8.useMemo)(() => fields.filter(fieldIsEditable), [fields]);
|
|
941
942
|
const order = (0, import_react8.useMemo)(() => editable.map((f) => f.key), [editable]);
|
|
942
943
|
const rowIds = (0, import_react8.useMemo)(() => rows.map((r) => r.id), [rows]);
|
|
@@ -970,7 +971,7 @@ function useGridEditing({ tableId, fields, rows, reload, canWrite }) {
|
|
|
970
971
|
[canWrite, client]
|
|
971
972
|
);
|
|
972
973
|
const cancel = (0, import_react8.useCallback)(() => setEditing(null), []);
|
|
973
|
-
const
|
|
974
|
+
const writeCellNow = (0, import_react8.useCallback)(
|
|
974
975
|
async (rowId, key, value2) => {
|
|
975
976
|
const id = cellKey(rowId, key);
|
|
976
977
|
setOptimistic((held) => ({ ...held, [id]: value2 }));
|
|
@@ -1014,6 +1015,22 @@ function useGridEditing({ tableId, fields, rows, reload, canWrite }) {
|
|
|
1014
1015
|
},
|
|
1015
1016
|
[client, reload]
|
|
1016
1017
|
);
|
|
1018
|
+
const writeCell = (0, import_react8.useCallback)(
|
|
1019
|
+
(rowId, key, value2) => {
|
|
1020
|
+
const behind = queues.current.get(rowId) ?? Promise.resolve();
|
|
1021
|
+
const next = behind.then(() => writeCellNow(rowId, key, value2));
|
|
1022
|
+
queues.current.set(
|
|
1023
|
+
rowId,
|
|
1024
|
+
next.then(
|
|
1025
|
+
() => void 0,
|
|
1026
|
+
() => void 0
|
|
1027
|
+
)
|
|
1028
|
+
);
|
|
1029
|
+
return next;
|
|
1030
|
+
},
|
|
1031
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1032
|
+
[client, reload]
|
|
1033
|
+
);
|
|
1017
1034
|
const commit = (0, import_react8.useCallback)(
|
|
1018
1035
|
(value2, move = null) => {
|
|
1019
1036
|
const open = editing;
|
|
@@ -1222,10 +1239,16 @@ function OpenCell({
|
|
|
1222
1239
|
const [draft, setDraft] = (0, import_react8.useState)(value2);
|
|
1223
1240
|
const multiline = editorKindFor(field) === "long_text";
|
|
1224
1241
|
const blurCommits = closesOnBlur(field);
|
|
1242
|
+
const done = (0, import_react8.useRef)(false);
|
|
1243
|
+
const once = (run) => {
|
|
1244
|
+
if (done.current) return;
|
|
1245
|
+
done.current = true;
|
|
1246
|
+
run();
|
|
1247
|
+
};
|
|
1225
1248
|
const blur = blurCommits ? {
|
|
1226
1249
|
onBlur: (event) => {
|
|
1227
1250
|
if (event.currentTarget.contains(event.relatedTarget)) return;
|
|
1228
|
-
onCommit(draft, null);
|
|
1251
|
+
once(() => onCommit(draft, null));
|
|
1229
1252
|
}
|
|
1230
1253
|
} : {};
|
|
1231
1254
|
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
@@ -1242,27 +1265,27 @@ function OpenCell({
|
|
|
1242
1265
|
if (event.key === "Escape") {
|
|
1243
1266
|
event.preventDefault();
|
|
1244
1267
|
event.stopPropagation();
|
|
1245
|
-
onCancel
|
|
1268
|
+
once(onCancel);
|
|
1246
1269
|
return;
|
|
1247
1270
|
}
|
|
1248
1271
|
if (event.key === "Tab") {
|
|
1249
1272
|
event.preventDefault();
|
|
1250
1273
|
event.stopPropagation();
|
|
1251
|
-
onCommit(draft, event.shiftKey ? "previous" : "next");
|
|
1274
|
+
once(() => onCommit(draft, event.shiftKey ? "previous" : "next"));
|
|
1252
1275
|
return;
|
|
1253
1276
|
}
|
|
1254
1277
|
if (event.key === "Enter" && (!multiline || event.metaKey || event.ctrlKey)) {
|
|
1255
1278
|
event.preventDefault();
|
|
1256
1279
|
event.stopPropagation();
|
|
1257
|
-
onCommit(draft, null);
|
|
1280
|
+
once(() => onCommit(draft, null));
|
|
1258
1281
|
}
|
|
1259
1282
|
},
|
|
1260
1283
|
...blur,
|
|
1261
1284
|
children: [
|
|
1262
1285
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(FieldControl, { field, value: draft, onChange: setDraft, id: `cell-${field.id}` }),
|
|
1263
1286
|
blurCommits ? /* @__PURE__ */ (0, import_jsx_runtime7.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_runtime7.jsxs)("span", { className: "flex items-center gap-1", children: [
|
|
1264
|
-
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_design_system6.Button, { size: "sm", onClick: () => onCommit(draft, null), children: "Save" }),
|
|
1265
|
-
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_design_system6.Button, { size: "sm", variant: "ghost", onClick: onCancel, children: "Cancel" })
|
|
1287
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_design_system6.Button, { size: "sm", onClick: () => once(() => onCommit(draft, null)), children: "Save" }),
|
|
1288
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_design_system6.Button, { size: "sm", variant: "ghost", onClick: () => once(onCancel), children: "Cancel" })
|
|
1266
1289
|
] })
|
|
1267
1290
|
]
|
|
1268
1291
|
}
|
|
@@ -1872,7 +1895,7 @@ function useRecordVersion(recordId) {
|
|
|
1872
1895
|
cancelled = true;
|
|
1873
1896
|
};
|
|
1874
1897
|
}, [client, recordId]);
|
|
1875
|
-
return version;
|
|
1898
|
+
return { version, note: setVersion };
|
|
1876
1899
|
}
|
|
1877
1900
|
|
|
1878
1901
|
// src/RecordForm.tsx
|
|
@@ -1891,7 +1914,8 @@ function RecordForm({
|
|
|
1891
1914
|
const mutation = (0, import_react20.useRecordMutation)();
|
|
1892
1915
|
const [draft, setDraft] = (0, import_react19.useState)({});
|
|
1893
1916
|
const [touched, setTouched] = (0, import_react19.useState)(false);
|
|
1894
|
-
const
|
|
1917
|
+
const loaded = useRecordVersion(recordId ?? null);
|
|
1918
|
+
const loadedVersion = loaded.version;
|
|
1895
1919
|
(0, import_react19.useEffect)(() => {
|
|
1896
1920
|
const document3 = existing.data?.document;
|
|
1897
1921
|
if (document3) setDraft({ ...document3 });
|
|
@@ -1920,6 +1944,7 @@ function RecordForm({
|
|
|
1920
1944
|
...version === null ? {} : { expectedVersion: version }
|
|
1921
1945
|
});
|
|
1922
1946
|
if (moved !== null) {
|
|
1947
|
+
loaded.note(moved);
|
|
1923
1948
|
host.notify?.success("Saved");
|
|
1924
1949
|
existing.reload();
|
|
1925
1950
|
onSaved?.(recordId);
|
|
@@ -2277,6 +2302,7 @@ function TableSettings({
|
|
|
2277
2302
|
proposals,
|
|
2278
2303
|
onAcceptProposal,
|
|
2279
2304
|
onRejectProposal,
|
|
2305
|
+
onDeleted,
|
|
2280
2306
|
className
|
|
2281
2307
|
}) {
|
|
2282
2308
|
const table = (0, import_react26.useTable)(tableId);
|
|
@@ -2284,6 +2310,7 @@ function TableSettings({
|
|
|
2284
2310
|
const rights = useTableRights(table.data);
|
|
2285
2311
|
const mutation = (0, import_react26.useRecordMutation)();
|
|
2286
2312
|
const [editing, setEditing] = (0, import_react25.useState)(null);
|
|
2313
|
+
const [askingToDelete, setAskingToDelete] = (0, import_react25.useState)(false);
|
|
2287
2314
|
if (table.error) return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(RefusalNotice, { error: table.error, className });
|
|
2288
2315
|
if (fields.error) return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(RefusalNotice, { error: fields.error, className });
|
|
2289
2316
|
if (!rights.admin) return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: (0, import_design_system13.cn)("text-xs text-muted-foreground", className), children: rights.reason });
|
|
@@ -2300,6 +2327,11 @@ function TableSettings({
|
|
|
2300
2327
|
await mutation.remove({ record_id: field.id });
|
|
2301
2328
|
fields.reload();
|
|
2302
2329
|
};
|
|
2330
|
+
const deleteTable = async () => {
|
|
2331
|
+
const gone = await mutation.remove({ record_id: tableId });
|
|
2332
|
+
setAskingToDelete(false);
|
|
2333
|
+
if (gone !== null) onDeleted?.();
|
|
2334
|
+
};
|
|
2303
2335
|
if (editing) {
|
|
2304
2336
|
return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
2305
2337
|
FieldEditor,
|
|
@@ -2375,7 +2407,28 @@ function TableSettings({
|
|
|
2375
2407
|
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_design_system13.Badge, { variant: "secondary", className: "text-[10px] font-normal", children: proposal.proposed_by }),
|
|
2376
2408
|
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_design_system13.Button, { size: "sm", variant: "outline", onClick: () => onAcceptProposal?.(proposal), children: "Accept" }),
|
|
2377
2409
|
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_design_system13.Button, { size: "sm", variant: "ghost", onClick: () => onRejectProposal?.(proposal), children: "Reject" })
|
|
2378
|
-
] }, proposal.id)) })
|
|
2410
|
+
] }, proposal.id)) }),
|
|
2411
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_design_system13.Separator, {}),
|
|
2412
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "flex flex-col gap-1", children: [
|
|
2413
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "font-medium", children: "This table" }),
|
|
2414
|
+
askingToDelete ? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
|
|
2415
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("p", { className: "text-muted-foreground", children: [
|
|
2416
|
+
"Deleting ",
|
|
2417
|
+
table.data ? tableName(table.data) : "this table",
|
|
2418
|
+
" takes it out of everyone's list, with its ",
|
|
2419
|
+
rows.length,
|
|
2420
|
+
" field",
|
|
2421
|
+
rows.length === 1 ? "" : "s",
|
|
2422
|
+
" and every record in it.",
|
|
2423
|
+
table.data?.retention_days ? ` The store keeps them for ${table.data.retention_days} days, so it can still be restored.` : " The store keeps them, so it can still be restored."
|
|
2424
|
+
] }),
|
|
2425
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "flex gap-1", children: [
|
|
2426
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_design_system13.Button, { size: "sm", variant: "destructive", disabled: mutation.saving, onClick: () => void deleteTable(), children: mutation.saving ? "Deleting\u2026" : "Delete this table" }),
|
|
2427
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_design_system13.Button, { size: "sm", variant: "ghost", onClick: () => setAskingToDelete(false), children: "Keep it" })
|
|
2428
|
+
] })
|
|
2429
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_design_system13.Button, { size: "sm", variant: "outline", onClick: () => setAskingToDelete(true), children: "Delete this table" }) }),
|
|
2430
|
+
mutation.error ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(RefusalNotice, { error: mutation.error }) : null
|
|
2431
|
+
] })
|
|
2379
2432
|
] });
|
|
2380
2433
|
}
|
|
2381
2434
|
|
|
@@ -3877,7 +3930,7 @@ function FormBuilder({ tableId, seed, activeFormId, onActiveForm, className }) {
|
|
|
3877
3930
|
const [draft, setDraft] = (0, import_react46.useState)(null);
|
|
3878
3931
|
const [saving, setSaving] = (0, import_react46.useState)(false);
|
|
3879
3932
|
const [saved, setSaved] = (0, import_react46.useState)(null);
|
|
3880
|
-
const
|
|
3933
|
+
const loaded = useRecordVersion(activeId);
|
|
3881
3934
|
const formTableId = home.tableId;
|
|
3882
3935
|
const load = (0, import_react46.useCallback)(async () => {
|
|
3883
3936
|
if (!formTableId) return;
|
|
@@ -3941,13 +3994,14 @@ function FormBuilder({ tableId, seed, activeFormId, onActiveForm, className }) {
|
|
|
3941
3994
|
record_id: draft.id,
|
|
3942
3995
|
patch: document2,
|
|
3943
3996
|
// The form record's own version: two people editing one form are TOLD.
|
|
3944
|
-
...
|
|
3997
|
+
...loaded.version === null ? {} : { expectedVersion: loaded.version }
|
|
3945
3998
|
});
|
|
3946
3999
|
setSaving(false);
|
|
3947
4000
|
if (!written.ok) {
|
|
3948
4001
|
setError(written.error);
|
|
3949
4002
|
return;
|
|
3950
4003
|
}
|
|
4004
|
+
loaded.note(written.data);
|
|
3951
4005
|
setSaved("Saved.");
|
|
3952
4006
|
await load();
|
|
3953
4007
|
}
|
|
@@ -5050,7 +5104,7 @@ function DashboardCanvas({ tableId, seed, activeDashboardId, className }) {
|
|
|
5050
5104
|
const [boards, setBoards] = (0, import_react59.useState)(null);
|
|
5051
5105
|
const [error, setError] = (0, import_react59.useState)(null);
|
|
5052
5106
|
const [activeId, setActiveId] = (0, import_react59.useState)(activeDashboardId ?? null);
|
|
5053
|
-
const
|
|
5107
|
+
const loaded = useRecordVersion(activeId);
|
|
5054
5108
|
const boardTableId = home.tableId;
|
|
5055
5109
|
const load = (0, import_react59.useCallback)(async () => {
|
|
5056
5110
|
if (!boardTableId) return;
|
|
@@ -5096,12 +5150,13 @@ function DashboardCanvas({ tableId, seed, activeDashboardId, className }) {
|
|
|
5096
5150
|
record_id: board2.id,
|
|
5097
5151
|
patch: dashboardDocument({ ...board2, blocks }),
|
|
5098
5152
|
// The dashboard record's own version: two tabs editing one board are TOLD.
|
|
5099
|
-
...
|
|
5153
|
+
...loaded.version === null ? {} : { expectedVersion: loaded.version }
|
|
5100
5154
|
});
|
|
5101
5155
|
if (!updated.ok) {
|
|
5102
5156
|
setError(updated.error);
|
|
5103
5157
|
return;
|
|
5104
5158
|
}
|
|
5159
|
+
loaded.note(updated.data);
|
|
5105
5160
|
await load();
|
|
5106
5161
|
}
|
|
5107
5162
|
async function create() {
|
|
@@ -6556,7 +6611,7 @@ function tokenFor(name) {
|
|
|
6556
6611
|
return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 48) || "table";
|
|
6557
6612
|
}
|
|
6558
6613
|
var DEFAULT_FIELDS = [
|
|
6559
|
-
{ key: "title", label: "Title", type: "text", sort: 10, required:
|
|
6614
|
+
{ key: "title", label: "Title", type: "text", sort: 10, required: false }
|
|
6560
6615
|
];
|
|
6561
6616
|
async function declareTable(client, spec) {
|
|
6562
6617
|
const fields = spec.fields && spec.fields.length > 0 ? spec.fields : DEFAULT_FIELDS;
|
|
@@ -6782,11 +6837,12 @@ function TablePage({
|
|
|
6782
6837
|
setView({ ...current, ...patch });
|
|
6783
6838
|
const document2 = viewPatchDocument(patch);
|
|
6784
6839
|
if (Object.keys(document2).length === 0) return;
|
|
6785
|
-
await client.recordUpdate({
|
|
6840
|
+
const written = await client.recordUpdate({
|
|
6786
6841
|
record_id: current.id,
|
|
6787
6842
|
patch: document2,
|
|
6788
|
-
...viewVersion === null ? {} : { expectedVersion: viewVersion }
|
|
6843
|
+
...viewVersion.version === null ? {} : { expectedVersion: viewVersion.version }
|
|
6789
6844
|
});
|
|
6845
|
+
if (written.ok) viewVersion.note(written.data);
|
|
6790
6846
|
},
|
|
6791
6847
|
[client, view, viewVersion]
|
|
6792
6848
|
);
|
|
@@ -6839,7 +6895,13 @@ function TablePage({
|
|
|
6839
6895
|
] })
|
|
6840
6896
|
] }),
|
|
6841
6897
|
rail === "none" ? null : /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("aside", { className: "w-[26rem] shrink-0 overflow-y-auto rounded-md border p-3", children: [
|
|
6842
|
-
rail === "settings" ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
|
|
6898
|
+
rail === "settings" ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
|
|
6899
|
+
TableSettings,
|
|
6900
|
+
{
|
|
6901
|
+
tableId,
|
|
6902
|
+
...onLeave ? { onDeleted: onLeave } : {}
|
|
6903
|
+
}
|
|
6904
|
+
) : null,
|
|
6843
6905
|
rail === "inbox" ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
|
|
6844
6906
|
ActionInbox,
|
|
6845
6907
|
{
|