@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 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 [titleKey, setTitleKey] = (0, import_react5.useState)(null);
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 known = new Set(rows.map((row) => row.id));
918
- const missing = picked.filter((id2) => !known.has(id2) && elsewhere[id2] === void 0);
919
- if (missing.length === 0) return;
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 Promise.all(
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
- setElsewhere((current) => ({ ...current, ...Object.fromEntries(pairs) }));
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, elsewhere, titleKey]);
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 byId = new Map((rows ?? []).map((row) => [row.id, row]));
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
- const target = field.relation_target;
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.entries()];
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
- Array.from({ length: Math.min(LANES, batch.length) }, async () => {
1730
- for (; ; ) {
1731
- const next = batch[at];
1732
- at += 1;
1733
- if (!next) return;
1734
- const [id] = next;
1735
- const result = await client.recordRead({ record_id: id });
1736
- learned[id] = result.ok ? recordName(result.data.document, null, "Untitled") : WITHHELD_RECORD_LABEL;
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 [, field] of batch) stopWaitClock(field.id);
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.set(id, field);
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
- title: recordName(
3872
- row.document ?? {},
3873
- table.data?.title_field
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 ? recordName(document2, table.data?.title_field) : "";
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 [titleKey, setTitleKey] = (0, import_react68.useState)(null);
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
- setTitleKey(answered.data.find((t) => t.id === card.client_table_id)?.title_field ?? null);
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: rowName(row, titleKey) }));
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, titleKey, search]);
10214
+ }, [rows, clientTable, search]);
10231
10215
  const send = (0, import_react68.useCallback)(async () => {
10232
10216
  if (!picked) return;
10233
10217
  setBusy(true);