@ai-matrx/records-ui 0.62.0 → 0.63.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 +71 -0
- package/dist/index.cjs +47 -63
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +47 -63
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,76 @@
|
|
|
1
1
|
# Changelog — @ai-matrx/records-ui
|
|
2
2
|
|
|
3
|
+
## 0.63.0
|
|
4
|
+
|
|
5
|
+
**A relation cell finally SHOWS what its column was configured to show, and the
|
|
6
|
+
six hand-rolled copies of "what is this id called" are gone — all six, by moving
|
|
7
|
+
the code, none by allowlisting.** (lane RELATION-DISPLAY-2.)
|
|
8
|
+
|
|
9
|
+
* **The display spec reached the store in 0.62.0 and no screen could read it.**
|
|
10
|
+
REL-DISP gave a relation column a `display` — which of the target's columns it
|
|
11
|
+
shows, in what order, joined by what — and shipped `ReferenceBuilder` to
|
|
12
|
+
configure it. But `labels.tsx`, which every grid, board, gallery, calendar and
|
|
13
|
+
peek cell reads through, resolved names ITSELF: 200 rows of the target Table
|
|
14
|
+
plus `tableList()` for its `title_field`. So a person could set Rincon
|
|
15
|
+
Plumbing's `jobs.customer` to read "first name + last name", press Save, and
|
|
16
|
+
the board would go on drawing "Maria Chen" for both of the Maria Chens. The
|
|
17
|
+
builder wrote a setting that nothing displayed.
|
|
18
|
+
`labels.tsx` now fetches NOTHING up front and asks `relation_words_many` for
|
|
19
|
+
the ids the page actually draws — one call per Field per tick, the store's one
|
|
20
|
+
resolver, this column's own spec, the visibility ladder asked BEFORE the name
|
|
21
|
+
is read, and no 200-row prefetch ceiling. Its own comment recorded the batch
|
|
22
|
+
door as missing ("there is still no batch door on the store"); REL-DISP built
|
|
23
|
+
it, so the per-id `record_read` lane is gone entirely.
|
|
24
|
+
* **`RelationPicker` offers the same words the cell shows.** It had the third
|
|
25
|
+
copy of the derivation, which is why a column configured to read "Maria Chen —
|
|
26
|
+
Rincon" still asked a person to choose between two identical "Maria Chen"s.
|
|
27
|
+
One `relation_words_many` now covers the offered rows AND the already-picked
|
|
28
|
+
ids, so what you pick from and what you read back can never disagree.
|
|
29
|
+
* **`recordNameIn` / `rowNameIn` — the one naming call for a record whose Table
|
|
30
|
+
you already hold.** `Grid` (the Talk action), `Peek` (the peek title),
|
|
31
|
+
`PortalsPanel`'s client picker and the host test bench were each writing
|
|
32
|
+
`recordName(row.document, table.title_field)` out by hand. That is not the
|
|
33
|
+
store's question — there is no id to resolve — but it is the same question
|
|
34
|
+
answered five times in four files, which is how a Table titled by `cname` read
|
|
35
|
+
"Untitled" on one screen and correctly on the next. It lives once now.
|
|
36
|
+
NOT a second resolver: hold an id, and the answer is still the store's.
|
|
37
|
+
* **`check:one-reference-builder` goes from a baseline of SIX to ZERO.** The
|
|
38
|
+
guard shipped saying out loud that allowlisting the six "would be the same lie
|
|
39
|
+
with extra steps". None was allowlisted. Proven red on a planted copy at
|
|
40
|
+
baseline 0, then green.
|
|
41
|
+
|
|
42
|
+
**Consumer action:** none for a host that only mounts these components — the
|
|
43
|
+
change is behind them. A host resolving a relation label itself should delete
|
|
44
|
+
that code and call `client.relationWordsMany`; one holding a Table and a
|
|
45
|
+
document should call `recordNameIn` / `rowNameIn`.
|
|
46
|
+
|
|
47
|
+
## 0.62.0
|
|
48
|
+
|
|
49
|
+
**THE ONE REFERENCE BUILDER.** (lane RELATION-DISPLAY.)
|
|
50
|
+
|
|
51
|
+
*Written after the fact by lane RELATION-DISPLAY-2 — 0.62.0 published with no
|
|
52
|
+
entry here, and a released version with no changelog row is a version nobody
|
|
53
|
+
downstream can read. Sourced from that lane's own progress note, not invented.*
|
|
54
|
+
|
|
55
|
+
* **`ReferenceBuilder` + `previewWords`** — the one place a reference to another
|
|
56
|
+
table is built. It asks, in the order a person thinks of it: which table →
|
|
57
|
+
what a record of it is called TODAY (its own reference column, SHOWN rather
|
|
58
|
+
than hidden) → only if they want it, which columns instead, in order, with a
|
|
59
|
+
separator → a preview on that table's REAL first rows. `FieldEditor`'s private
|
|
60
|
+
`TableToPointAt` is deleted and this stands in its place.
|
|
61
|
+
* **A relation is asked its questions when EDITING too.** That branch skipped
|
|
62
|
+
every existing column that was not a choice list, so there was nowhere to say
|
|
63
|
+
which columns a link should show — the one thing about an existing relation a
|
|
64
|
+
person most wants to change. The target stays locked there, because the panel
|
|
65
|
+
sends no `relation_target` in a patch and a picker that changed it would be a
|
|
66
|
+
control that silently does nothing.
|
|
67
|
+
* **`looksLikeId` and `WITHHELD_RECORD_LABEL` are exported at last** — they were
|
|
68
|
+
defined and unreachable, so a host could not agree with the package about what
|
|
69
|
+
a name is.
|
|
70
|
+
|
|
71
|
+
**Consumer action:** a surface that lets a person choose a column's target table
|
|
72
|
+
mounts `ReferenceBuilder` instead of building its own dropdown.
|
|
73
|
+
|
|
3
74
|
## 0.61.0
|
|
4
75
|
|
|
5
76
|
**A link to a board opens a board, and a cell that is still resolving says what
|
package/dist/index.cjs
CHANGED
|
@@ -223,12 +223,14 @@ __export(src_exports, {
|
|
|
223
223
|
previewWords: () => previewWords,
|
|
224
224
|
publiclyAnswerable: () => publiclyAnswerable,
|
|
225
225
|
recordName: () => recordName,
|
|
226
|
+
recordNameIn: () => recordNameIn,
|
|
226
227
|
recordsDataSource: () => recordsDataSource,
|
|
227
228
|
refusalForAPerson: () => refusalForAPerson,
|
|
228
229
|
refusalLineForAPerson: () => refusalLineForAPerson,
|
|
229
230
|
renderValue: () => renderValue,
|
|
230
231
|
revokeConsequence: () => revokeConsequence,
|
|
231
232
|
rowName: () => rowName,
|
|
233
|
+
rowNameIn: () => rowNameIn,
|
|
232
234
|
scalarText: () => scalarText,
|
|
233
235
|
shareUnavailableReason: () => shareUnavailableReason,
|
|
234
236
|
specFromBlock: () => specFromBlock,
|
|
@@ -541,6 +543,12 @@ function looksLikeId(value) {
|
|
|
541
543
|
function rowName(row, titleKey, fallback = "Untitled") {
|
|
542
544
|
return recordName(row?.document, titleKey, fallback);
|
|
543
545
|
}
|
|
546
|
+
function recordNameIn(table, document2, fallback = "Untitled") {
|
|
547
|
+
return recordName(document2, table?.title_field ?? null, fallback);
|
|
548
|
+
}
|
|
549
|
+
function rowNameIn(table, row, fallback = "Untitled") {
|
|
550
|
+
return recordName(row?.document, table?.title_field ?? null, fallback);
|
|
551
|
+
}
|
|
544
552
|
var WITHHELD_RECORD_LABEL = "A record you have not been given access to";
|
|
545
553
|
|
|
546
554
|
// src/ReferenceBuilder.tsx
|
|
@@ -886,8 +894,7 @@ var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
|
886
894
|
function RelationPicker({ field, value, onChange, disabled, id, className }) {
|
|
887
895
|
const client = (0, import_react6.useRecordsClient)();
|
|
888
896
|
const [rows, setRows] = (0, import_react5.useState)(null);
|
|
889
|
-
const [
|
|
890
|
-
const [elsewhere, setElsewhere] = (0, import_react5.useState)({});
|
|
897
|
+
const [words, setWords] = (0, import_react5.useState)({});
|
|
891
898
|
const [refusal, setRefusal] = (0, import_react5.useState)(null);
|
|
892
899
|
const [search, setSearch] = (0, import_react5.useState)("");
|
|
893
900
|
const [open, setOpen] = (0, import_react5.useState)(false);
|
|
@@ -904,47 +911,37 @@ function RelationPicker({ field, value, onChange, disabled, id, className }) {
|
|
|
904
911
|
if (result.ok) setRows(result.data.rows);
|
|
905
912
|
else setRefusal(refusalLineForAPerson(result.error));
|
|
906
913
|
});
|
|
907
|
-
void client.tableList().then((result) => {
|
|
908
|
-
if (cancelled || !result.ok) return;
|
|
909
|
-
setTitleKey(result.data.find((table) => table.id === target)?.title_field ?? null);
|
|
910
|
-
});
|
|
911
914
|
return () => {
|
|
912
915
|
cancelled = true;
|
|
913
916
|
};
|
|
914
917
|
}, [client, field.relation_target]);
|
|
915
918
|
(0, import_react5.useEffect)(() => {
|
|
916
919
|
if (!rows) return;
|
|
917
|
-
const
|
|
918
|
-
|
|
919
|
-
|
|
920
|
+
const wanted = [.../* @__PURE__ */ new Set([...rows.map((row) => row.id), ...picked])].filter(
|
|
921
|
+
(id2) => words[id2] === void 0
|
|
922
|
+
);
|
|
923
|
+
if (wanted.length === 0) return;
|
|
920
924
|
let cancelled = false;
|
|
921
|
-
void
|
|
922
|
-
missing.map(async (id2) => {
|
|
923
|
-
const result = await client.recordRead({ record_id: id2 });
|
|
924
|
-
return [id2, result.ok ? rowName({ document: result.data.document }, titleKey) : WITHHELD_RECORD_LABEL];
|
|
925
|
-
})
|
|
926
|
-
).then((pairs) => {
|
|
925
|
+
void client.relationWordsMany({ field_id: field.id, record_ids: wanted }).then((result) => {
|
|
927
926
|
if (cancelled) return;
|
|
928
|
-
|
|
927
|
+
const learned = {};
|
|
928
|
+
for (const id2 of wanted) {
|
|
929
|
+
const said = result.ok ? result.data[id2] : void 0;
|
|
930
|
+
learned[id2] = typeof said === "string" && said !== "" ? said : WITHHELD_RECORD_LABEL;
|
|
931
|
+
}
|
|
932
|
+
setWords((current) => ({ ...current, ...learned }));
|
|
929
933
|
});
|
|
930
934
|
return () => {
|
|
931
935
|
cancelled = true;
|
|
932
936
|
};
|
|
933
|
-
}, [client, rows, picked,
|
|
937
|
+
}, [client, field.id, rows, picked, words]);
|
|
934
938
|
if (!field.relation_target) {
|
|
935
939
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "text-xs text-muted-foreground", children: [
|
|
936
940
|
fieldName(field),
|
|
937
941
|
" points at records, but it does not say which Table yet, so there is nothing to pick from. A table admin sets its target in the field editor."
|
|
938
942
|
] });
|
|
939
943
|
}
|
|
940
|
-
const
|
|
941
|
-
const titleOf = (recordId) => {
|
|
942
|
-
const row = byId.get(recordId);
|
|
943
|
-
if (row) return rowName(row, titleKey, "Untitled");
|
|
944
|
-
const away = elsewhere[recordId];
|
|
945
|
-
if (away !== void 0) return away;
|
|
946
|
-
return "Loading\u2026";
|
|
947
|
-
};
|
|
944
|
+
const titleOf = (recordId) => words[recordId] ?? "Loading\u2026";
|
|
948
945
|
const full = field.multi && field.relation_max !== null && picked.length >= field.relation_max;
|
|
949
946
|
const toggle = (recordId) => {
|
|
950
947
|
if (!field.multi) {
|
|
@@ -1688,28 +1685,12 @@ function RecordLabelProvider({ children }) {
|
|
|
1688
1685
|
stopWaitClock(field.id);
|
|
1689
1686
|
return;
|
|
1690
1687
|
}
|
|
1691
|
-
|
|
1692
|
-
if (!target) {
|
|
1688
|
+
if (!field.relation_target) {
|
|
1693
1689
|
setSettled((current) => ({ ...current, [field.id]: true }));
|
|
1694
1690
|
stopWaitClock(field.id);
|
|
1695
1691
|
return;
|
|
1696
1692
|
}
|
|
1697
|
-
const [rows, tables] = await Promise.all([
|
|
1698
|
-
client.list({ table_id: target, limit: 200 }),
|
|
1699
|
-
client.tableList()
|
|
1700
|
-
]);
|
|
1701
|
-
const titleKey = tables.ok ? tables.data.find((table) => table.id === target)?.title_field ?? null : null;
|
|
1702
|
-
if (rows.ok) {
|
|
1703
|
-
setNames((current) => {
|
|
1704
|
-
const next = { ...current };
|
|
1705
|
-
for (const row of rows.data.rows) {
|
|
1706
|
-
next[row.id] = recordName(row.document, titleKey, "Untitled");
|
|
1707
|
-
}
|
|
1708
|
-
return next;
|
|
1709
|
-
});
|
|
1710
|
-
}
|
|
1711
1693
|
setSettled((current) => ({ ...current, [field.id]: true }));
|
|
1712
|
-
stopWaitClock(field.id);
|
|
1713
1694
|
})();
|
|
1714
1695
|
},
|
|
1715
1696
|
[client, startWaitClock, stopWaitClock]
|
|
@@ -1718,27 +1699,26 @@ function RecordLabelProvider({ children }) {
|
|
|
1718
1699
|
const flushing = (0, import_react13.useRef)(null);
|
|
1719
1700
|
const flush = (0, import_react13.useCallback)(() => {
|
|
1720
1701
|
flushing.current = null;
|
|
1721
|
-
const batch = [...queued.current.
|
|
1702
|
+
const batch = [...queued.current.values()];
|
|
1722
1703
|
queued.current.clear();
|
|
1723
1704
|
if (batch.length === 0) return;
|
|
1724
1705
|
void (async () => {
|
|
1725
1706
|
const learned = {};
|
|
1726
|
-
const LANES = 6;
|
|
1727
|
-
let at = 0;
|
|
1728
1707
|
await Promise.all(
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1708
|
+
batch.map(async ({ field, ids }) => {
|
|
1709
|
+
const wanted = [...ids];
|
|
1710
|
+
const result = await client.relationWordsMany({
|
|
1711
|
+
field_id: field.id,
|
|
1712
|
+
record_ids: wanted
|
|
1713
|
+
});
|
|
1714
|
+
for (const id of wanted) {
|
|
1715
|
+
const words = result.ok ? result.data[id] : void 0;
|
|
1716
|
+
learned[id] = typeof words === "string" && words !== "" ? words : WITHHELD_RECORD_LABEL;
|
|
1737
1717
|
}
|
|
1738
1718
|
})
|
|
1739
1719
|
);
|
|
1740
1720
|
setNames((current) => ({ ...current, ...learned }));
|
|
1741
|
-
for (const
|
|
1721
|
+
for (const { field } of batch) stopWaitClock(field.id);
|
|
1742
1722
|
})();
|
|
1743
1723
|
}, [client, stopWaitClock]);
|
|
1744
1724
|
const chase = (0, import_react13.useCallback)(
|
|
@@ -1746,7 +1726,9 @@ function RecordLabelProvider({ children }) {
|
|
|
1746
1726
|
const key = `${field.id}:${id}`;
|
|
1747
1727
|
if (chased.current.has(key)) return;
|
|
1748
1728
|
chased.current.add(key);
|
|
1749
|
-
queued.current.
|
|
1729
|
+
const pending = queued.current.get(field.id);
|
|
1730
|
+
if (pending) pending.ids.add(id);
|
|
1731
|
+
else queued.current.set(field.id, { field, ids: /* @__PURE__ */ new Set([id]) });
|
|
1750
1732
|
startWaitClock(field, null);
|
|
1751
1733
|
if (flushing.current === null) flushing.current = setTimeout(flush, 0);
|
|
1752
1734
|
},
|
|
@@ -3868,9 +3850,11 @@ function Grid({
|
|
|
3868
3850
|
onClick: () => host.talkToRecord?.({
|
|
3869
3851
|
tableId,
|
|
3870
3852
|
recordId: row.id,
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3853
|
+
// ONE NAMING CALL (`names.ts`), not this file's own join
|
|
3854
|
+
// of a Table's title column to a document.
|
|
3855
|
+
title: recordNameIn(
|
|
3856
|
+
table.data,
|
|
3857
|
+
row.document ?? {}
|
|
3874
3858
|
)
|
|
3875
3859
|
}),
|
|
3876
3860
|
children: "Talk"
|
|
@@ -6528,7 +6512,7 @@ function Peek({ tableId, recordId, onClose, className }) {
|
|
|
6528
6512
|
if (record.error) return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(RefusalNotice, { error: record.error, className });
|
|
6529
6513
|
if (fields.error) return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(RefusalNotice, { error: fields.error, className });
|
|
6530
6514
|
const document2 = record.data?.document;
|
|
6531
|
-
const title = document2 ?
|
|
6515
|
+
const title = document2 ? recordNameIn(table.data, document2) : "";
|
|
6532
6516
|
const href = host.hrefForRecord && table.data ? host.hrefForRecord({ table: table.data, recordId }) : void 0;
|
|
6533
6517
|
return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: (0, import_design_system21.cn)("flex min-w-0 flex-col gap-2 text-xs", className), children: [
|
|
6534
6518
|
/* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
@@ -10198,7 +10182,7 @@ function Preview({
|
|
|
10198
10182
|
function Invite({ card, onInvited }) {
|
|
10199
10183
|
const client = (0, import_react69.useRecordsClient)();
|
|
10200
10184
|
const [rows, setRows] = (0, import_react68.useState)(null);
|
|
10201
|
-
const [
|
|
10185
|
+
const [clientTable, setClientTable] = (0, import_react68.useState)(null);
|
|
10202
10186
|
const [search, setSearch] = (0, import_react68.useState)("");
|
|
10203
10187
|
const [picked, setPicked] = (0, import_react68.useState)(null);
|
|
10204
10188
|
const [email, setEmail] = (0, import_react68.useState)("");
|
|
@@ -10217,17 +10201,17 @@ function Invite({ card, onInvited }) {
|
|
|
10217
10201
|
});
|
|
10218
10202
|
void client.tableList().then((answered) => {
|
|
10219
10203
|
if (cancelled || !answered.ok) return;
|
|
10220
|
-
|
|
10204
|
+
setClientTable(answered.data.find((t) => t.id === card.client_table_id) ?? null);
|
|
10221
10205
|
});
|
|
10222
10206
|
return () => {
|
|
10223
10207
|
cancelled = true;
|
|
10224
10208
|
};
|
|
10225
10209
|
}, [client, card.client_table_id]);
|
|
10226
10210
|
const options = (0, import_react68.useMemo)(() => {
|
|
10227
|
-
const all = (rows ?? []).map((row) => ({ id: row.id, name:
|
|
10211
|
+
const all = (rows ?? []).map((row) => ({ id: row.id, name: rowNameIn(clientTable, row) }));
|
|
10228
10212
|
const needle = search.trim().toLowerCase();
|
|
10229
10213
|
return needle === "" ? all.slice(0, 25) : all.filter((o) => o.name.toLowerCase().includes(needle)).slice(0, 25);
|
|
10230
|
-
}, [rows,
|
|
10214
|
+
}, [rows, clientTable, search]);
|
|
10231
10215
|
const send = (0, import_react68.useCallback)(async () => {
|
|
10232
10216
|
if (!picked) return;
|
|
10233
10217
|
setBusy(true);
|