@ai-matrx/records-ui 0.70.0 → 0.71.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,33 @@
1
1
  # Changelog — @ai-matrx/records-ui
2
2
 
3
+ ## 0.71.0
4
+
5
+ **A public form no longer takes the page down when it asks for a choice, a linked record or a
6
+ person.** (lane FIX-10A, from VERIFIER-10 finding F1.)
7
+
8
+ An agent built and published a job-request form for a plumbing company in forty seconds and
9
+ handed over its link. Opened by a signed-out stranger — the only person that link is for — it
10
+ answered **HTTP 500**, on production, with `useRecordsClient was called outside
11
+ <RecordsProvider>` in the console.
12
+
13
+ The public arm of `FormRunner` is deliberately the arm that mounts **no store client**: the
14
+ server resolved the Fields through `custom.form_public`, and the browser on that page holds no
15
+ key, knows no organization and may not read a row. `useOptionalRecordsClient` exists in
16
+ `@ai-matrx/records` for exactly that screen. But three of the editors `FieldControl` can draw
17
+ asked for the client with `useRecordsClient`, which throws by design — so **any** public form
18
+ carrying a drop-down, a relation or a person question died, whatever it was about.
19
+
20
+ * **`OptionControl`, `RelationPicker` and `PersonPicker` now take the optional client** and,
21
+ with none mounted, are **absent and honest** instead of fatal. A choice question takes the
22
+ answer as text and says why the list cannot be read on a page answered without an account; a
23
+ relation question says it cannot be answered here and to leave it blank; a person question
24
+ already had that sentence and now reaches it instead of throwing above it.
25
+ * **The guard** — `src/a-public-form-needs-no-store-client.test.tsx` — walks `FIELD_KINDS`, the
26
+ store's own closed list of the nineteen kinds a Field can be, and renders the public arm with
27
+ a question of every one of them and no provider of any sort above it. Red on the bytes before
28
+ this release (4 of 19), green on these. A twentieth kind whose editor reaches for the store
29
+ turns it red the day it is added, not the day a stranger opens a link.
30
+
3
31
  ## 0.70.0
4
32
 
5
33
  **The bar that said four now opens four.** (lane DRILL.)
package/dist/index.cjs CHANGED
@@ -919,8 +919,9 @@ var import_react5 = require("react");
919
919
  var import_react6 = require("@ai-matrx/records/react");
920
920
  var import_design_system2 = require("@ai-matrx/design-system");
921
921
  var import_jsx_runtime3 = require("react/jsx-runtime");
922
+ var NO_STORE_ON_THIS_SCREEN_REASON = "cannot be answered on this page: it points at a record in somebody's own data, and a page answered without an account is never allowed to search that. Leave it blank and whoever receives this will fill it in.";
922
923
  function RelationPicker({ field, value, onChange, disabled, id, className, allowCreate = true }) {
923
- const client = (0, import_react6.useRecordsClient)();
924
+ const client = (0, import_react6.useOptionalRecordsClient)();
924
925
  const [rows, setRows] = (0, import_react5.useState)(null);
925
926
  const [words, setWords] = (0, import_react5.useState)({});
926
927
  const [refusal, setRefusal] = (0, import_react5.useState)(null);
@@ -938,7 +939,7 @@ function RelationPicker({ field, value, onChange, disabled, id, className, allow
938
939
  );
939
940
  (0, import_react5.useEffect)(() => {
940
941
  const target2 = field.relation_target;
941
- if (!target2) return;
942
+ if (!target2 || !client) return;
942
943
  let cancelled = false;
943
944
  void client.list({ table_id: target2, limit: 200 }).then((result) => {
944
945
  if (cancelled) return;
@@ -950,7 +951,7 @@ function RelationPicker({ field, value, onChange, disabled, id, className, allow
950
951
  };
951
952
  }, [client, field.relation_target]);
952
953
  (0, import_react5.useEffect)(() => {
953
- if (!rows) return;
954
+ if (!rows || !client) return;
954
955
  const wanted = [.../* @__PURE__ */ new Set([...rows.map((row) => row.id), ...picked])].filter(
955
956
  (id2) => words[id2] === void 0
956
957
  );
@@ -970,7 +971,7 @@ function RelationPicker({ field, value, onChange, disabled, id, className, allow
970
971
  };
971
972
  }, [client, field.id, rows, picked, words]);
972
973
  (0, import_react5.useEffect)(() => {
973
- if (!open || !allowCreate || target || targetRefusal) return;
974
+ if (!open || !allowCreate || target || targetRefusal || !client) return;
974
975
  const id2 = field.relation_target;
975
976
  if (!id2) return;
976
977
  let cancelled = false;
@@ -991,6 +992,13 @@ function RelationPicker({ field, value, onChange, disabled, id, className, allow
991
992
  cancelled = true;
992
993
  };
993
994
  }, [client, open, allowCreate, target, targetRefusal, field.relation_target]);
995
+ if (!client) {
996
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { id, className: (0, import_design_system2.cn)("text-xs text-muted-foreground", className), children: [
997
+ fieldName(field),
998
+ " ",
999
+ NO_STORE_ON_THIS_SCREEN_REASON
1000
+ ] });
1001
+ }
994
1002
  if (!field.relation_target) {
995
1003
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "text-xs text-muted-foreground", children: [
996
1004
  fieldName(field),
@@ -1321,10 +1329,11 @@ function JsonControl({ field, value, onChange, disabled, id }) {
1321
1329
  ] });
1322
1330
  }
1323
1331
  function OptionControl({ field, value, onChange, disabled, id }) {
1324
- const client = (0, import_react8.useRecordsClient)();
1332
+ const client = (0, import_react8.useOptionalRecordsClient)();
1325
1333
  const [options, setOptions] = (0, import_react7.useState)(null);
1326
1334
  const [refusal, setRefusal] = (0, import_react7.useState)(null);
1327
1335
  (0, import_react7.useEffect)(() => {
1336
+ if (!client) return;
1328
1337
  let cancelled = false;
1329
1338
  void client.fieldOptions({ field_id: field.id }).then((result) => {
1330
1339
  if (cancelled) return;
@@ -1335,6 +1344,23 @@ function OptionControl({ field, value, onChange, disabled, id }) {
1335
1344
  cancelled = true;
1336
1345
  };
1337
1346
  }, [client, field.id]);
1347
+ if (!client) {
1348
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-col gap-1", children: [
1349
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1350
+ import_design_system3.BasicInput,
1351
+ {
1352
+ id,
1353
+ value: value === null || value === void 0 ? "" : String(value),
1354
+ disabled,
1355
+ onChange: (e) => onChange(e.target.value === "" ? null : e.target.value)
1356
+ }
1357
+ ),
1358
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { className: "text-xs text-muted-foreground", children: [
1359
+ fieldName(field),
1360
+ " is normally picked from a list. This page is answered without an account, so the list cannot be read here \u2014 type your answer and whoever receives it will match it up."
1361
+ ] })
1362
+ ] });
1363
+ }
1338
1364
  if (refusal) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { id, className: "text-xs text-destructive", children: refusal });
1339
1365
  if (!options) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { id, className: "text-xs text-muted-foreground", children: "Reading this field's choices\u2026" });
1340
1366
  if (options.length === 0) {
@@ -1419,7 +1445,7 @@ async function personRecordForMember(client, personTableId, people, member) {
1419
1445
  return written;
1420
1446
  }
1421
1447
  function PersonPicker({ field, value, onChange, disabled, id, className }) {
1422
- const client = (0, import_react10.useRecordsClient)();
1448
+ const client = (0, import_react10.useOptionalRecordsClient)();
1423
1449
  const host = useRecordsUi();
1424
1450
  const [members, setMembers] = (0, import_react9.useState)(null);
1425
1451
  const [people, setPeople] = (0, import_react9.useState)([]);
@@ -1434,7 +1460,7 @@ function PersonPicker({ field, value, onChange, disabled, id, className }) {
1434
1460
  [value]
1435
1461
  );
1436
1462
  const load = (0, import_react9.useCallback)(async () => {
1437
- if (!membersPort) return;
1463
+ if (!membersPort || !client) return;
1438
1464
  const [roster, records] = await Promise.all([
1439
1465
  membersPort(),
1440
1466
  personTable ? client.list({ table_id: personTable, limit: 500 }) : Promise.resolve(null)
@@ -1451,7 +1477,7 @@ function PersonPicker({ field, value, onChange, disabled, id, className }) {
1451
1477
  void load();
1452
1478
  }, [open, load]);
1453
1479
  (0, import_react9.useEffect)(() => {
1454
- if (picked.length === 0 || !personTable || people.length > 0) return;
1480
+ if (picked.length === 0 || !personTable || people.length > 0 || !client) return;
1455
1481
  let cancelled = false;
1456
1482
  void client.list({ table_id: personTable, limit: 500 }).then((result) => {
1457
1483
  if (cancelled || !result.ok) return;
@@ -1461,7 +1487,7 @@ function PersonPicker({ field, value, onChange, disabled, id, className }) {
1461
1487
  cancelled = true;
1462
1488
  };
1463
1489
  }, [client, personTable, picked.length, people.length]);
1464
- if (!membersPort) {
1490
+ if (!membersPort || !client) {
1465
1491
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: (0, import_design_system4.cn)("text-xs text-muted-foreground", className), children: NO_MEMBERS_REASON });
1466
1492
  }
1467
1493
  if (!personTable) {