@ai-matrx/records-ui 0.67.0 → 0.69.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 +49 -0
- package/dist/index.cjs +136 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +136 -9
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# Changelog — @ai-matrx/records-ui
|
|
2
2
|
|
|
3
|
+
## 0.69.0
|
|
4
|
+
|
|
5
|
+
**She can make the one that is missing, from where she is looking for it.** (lane TAILS-7.)
|
|
6
|
+
|
|
7
|
+
* **The relation picker offers to create.** Rincon Plumbing's office opens a job for a customer
|
|
8
|
+
who has never been written down, and the picker's last word was *"Nothing in that table
|
|
9
|
+
matches."* — a dead end with nowhere to go: leave the job, find the Customers table, add a
|
|
10
|
+
row, come back, re-open the picker, search again. It now ends the list the way Airtable and
|
|
11
|
+
Notion do: type a name that matches nothing and the control reads **Create "Delfina
|
|
12
|
+
Arreola"**, one press; type nothing and it reads **New customer** and asks for a name. The
|
|
13
|
+
new record is written through `record_write` into the TARGET TABLE'S OWN `title_field` —
|
|
14
|
+
never a guessed key — and it comes back named by the store (`relation_words_many`), so a
|
|
15
|
+
column configured to show two of the target's columns shows both on the new chip too.
|
|
16
|
+
* **It is ONE control, so every relation surface inherited it at once**: the grid cell
|
|
17
|
+
(`gridEditing` → `FieldControl`), the generated record form, the record rail's edit mode and
|
|
18
|
+
the public form runner all draw `RelationPicker`. Nothing was forked per surface.
|
|
19
|
+
* **Except the one place creating would be wrong.** `Condition` — the filter builder — passes
|
|
20
|
+
`allowCreate={false}`: choosing a value to LOOK by is not a reason to put a customer in the
|
|
21
|
+
book. The guard asserts that absence, so a later "make it consistent" cannot spread it there
|
|
22
|
+
silently.
|
|
23
|
+
* **Absent or honest, never dead.** A target Table with no `title_field` cannot name a new
|
|
24
|
+
record, so the create says exactly that and names who fixes it, instead of writing a nameless
|
|
25
|
+
row. A refused write shows the store's own refusal in a person's words.
|
|
26
|
+
* Guard: `src/a-relation-can-make-the-one-that-is-missing.test.tsx`, in the blocking gate.
|
|
27
|
+
Shown RED against the pre-change picker — 4 of 5 clauses failing, the fifth being the filter
|
|
28
|
+
builder's absence, which must pass in both states — then GREEN. Package gate 251/251.
|
|
29
|
+
|
|
30
|
+
## 0.68.0
|
|
31
|
+
|
|
32
|
+
**Two things a screen was still doing behind a person's back.** (lane TAILS-6, found while
|
|
33
|
+
proving 0.66.0 on the live seat.)
|
|
34
|
+
|
|
35
|
+
* **The engineer's line was being READ ALOUD.** `sr-only` does not hide a line — it hides it
|
|
36
|
+
from people with EYES and reads it to everybody else. So the one person who could not skim
|
|
37
|
+
past *"@ai-matrx/records never reached the store from checklistTemplates: TypeError: Failed
|
|
38
|
+
to fetch"* was the blind owner of the business, who had it announced in full underneath
|
|
39
|
+
"We could not reach your data". `RefusalNotice` now marks that line `aria-hidden`; it stays
|
|
40
|
+
in the DOM with the SQLSTATE on it, and on the notice's `title`, for whoever debugs it.
|
|
41
|
+
The guard asserts on what a screen reader would SAY, not on what the DOM contains.
|
|
42
|
+
* **The Group by picker contradicted the board beside it.** `offerableFields` answers what is
|
|
43
|
+
a SENSIBLE column to group by — Choice columns — and the select listed only those. A view,
|
|
44
|
+
or a `?group=` link from a dashboard number, can name a column outside that set: the board
|
|
45
|
+
grouped by it happily and the select fell back to its blank "Choose a field". Watched on
|
|
46
|
+
Rincon Plumbing Co's Jobs board with six columns headed RPC-1000, RPC-1001 … over a picker
|
|
47
|
+
saying nothing was chosen. The chosen column is now always in the list, named, and marked
|
|
48
|
+
"an unusual column to use here".
|
|
49
|
+
|
|
50
|
+
**Consumer action:** none.
|
|
51
|
+
|
|
3
52
|
## 0.67.0
|
|
4
53
|
|
|
5
54
|
**The office can copy a waiting client's own link, from the panel where it invited them.**
|
package/dist/index.cjs
CHANGED
|
@@ -919,22 +919,28 @@ 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
|
-
function RelationPicker({ field, value, onChange, disabled, id, className }) {
|
|
922
|
+
function RelationPicker({ field, value, onChange, disabled, id, className, allowCreate = true }) {
|
|
923
923
|
const client = (0, import_react6.useRecordsClient)();
|
|
924
924
|
const [rows, setRows] = (0, import_react5.useState)(null);
|
|
925
925
|
const [words, setWords] = (0, import_react5.useState)({});
|
|
926
926
|
const [refusal, setRefusal] = (0, import_react5.useState)(null);
|
|
927
927
|
const [search, setSearch] = (0, import_react5.useState)("");
|
|
928
928
|
const [open, setOpen] = (0, import_react5.useState)(false);
|
|
929
|
+
const [target, setTarget] = (0, import_react5.useState)(null);
|
|
930
|
+
const [targetRefusal, setTargetRefusal] = (0, import_react5.useState)(null);
|
|
931
|
+
const [creating, setCreating] = (0, import_react5.useState)(false);
|
|
932
|
+
const [newTitle, setNewTitle] = (0, import_react5.useState)("");
|
|
933
|
+
const [busy, setBusy] = (0, import_react5.useState)(false);
|
|
934
|
+
const [createRefusal, setCreateRefusal] = (0, import_react5.useState)(null);
|
|
929
935
|
const picked = (0, import_react5.useMemo)(
|
|
930
936
|
() => Array.isArray(value) ? value.map(String) : value === null || value === void 0 ? [] : [String(value)],
|
|
931
937
|
[value]
|
|
932
938
|
);
|
|
933
939
|
(0, import_react5.useEffect)(() => {
|
|
934
|
-
const
|
|
935
|
-
if (!
|
|
940
|
+
const target2 = field.relation_target;
|
|
941
|
+
if (!target2) return;
|
|
936
942
|
let cancelled = false;
|
|
937
|
-
void client.list({ table_id:
|
|
943
|
+
void client.list({ table_id: target2, limit: 200 }).then((result) => {
|
|
938
944
|
if (cancelled) return;
|
|
939
945
|
if (result.ok) setRows(result.data.rows);
|
|
940
946
|
else setRefusal(refusalLineForAPerson(result.error));
|
|
@@ -963,6 +969,28 @@ function RelationPicker({ field, value, onChange, disabled, id, className }) {
|
|
|
963
969
|
cancelled = true;
|
|
964
970
|
};
|
|
965
971
|
}, [client, field.id, rows, picked, words]);
|
|
972
|
+
(0, import_react5.useEffect)(() => {
|
|
973
|
+
if (!open || !allowCreate || target || targetRefusal) return;
|
|
974
|
+
const id2 = field.relation_target;
|
|
975
|
+
if (!id2) return;
|
|
976
|
+
let cancelled = false;
|
|
977
|
+
void client.tableList().then((result) => {
|
|
978
|
+
if (cancelled) return;
|
|
979
|
+
if (!result.ok) {
|
|
980
|
+
setTargetRefusal(refusalLineForAPerson(result.error));
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
const found = result.data.find((t) => t.id === id2) ?? null;
|
|
984
|
+
if (found) setTarget(found);
|
|
985
|
+
else
|
|
986
|
+
setTargetRefusal(
|
|
987
|
+
"That Table is not one you can open, so a new record cannot be added to it from here."
|
|
988
|
+
);
|
|
989
|
+
});
|
|
990
|
+
return () => {
|
|
991
|
+
cancelled = true;
|
|
992
|
+
};
|
|
993
|
+
}, [client, open, allowCreate, target, targetRefusal, field.relation_target]);
|
|
966
994
|
if (!field.relation_target) {
|
|
967
995
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "text-xs text-muted-foreground", children: [
|
|
968
996
|
fieldName(field),
|
|
@@ -982,6 +1010,39 @@ function RelationPicker({ field, value, onChange, disabled, id, className }) {
|
|
|
982
1010
|
else if (!full) next.add(recordId);
|
|
983
1011
|
onChange(next.size === 0 ? null : [...next]);
|
|
984
1012
|
};
|
|
1013
|
+
const targetWord = (target?.label_singular ?? "").trim() || (target?.name ?? "").trim() || "record";
|
|
1014
|
+
const create = async (rawTitle) => {
|
|
1015
|
+
const title = rawTitle.trim();
|
|
1016
|
+
const tableId = field.relation_target;
|
|
1017
|
+
if (!tableId || title === "" || busy) return;
|
|
1018
|
+
if (!target) {
|
|
1019
|
+
setCreateRefusal("Still reading that Table. Try again in a moment.");
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
const titleField = target.title_field;
|
|
1023
|
+
if (!titleField) {
|
|
1024
|
+
setCreateRefusal(
|
|
1025
|
+
`${target.name} has no column that names a record yet, so a new one would have nothing to show here. A table admin picks its title column in that table's settings.`
|
|
1026
|
+
);
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
setBusy(true);
|
|
1030
|
+
setCreateRefusal(null);
|
|
1031
|
+
const written = await client.recordWrite({ table_id: tableId, data: { [titleField]: title } });
|
|
1032
|
+
if (!written.ok) {
|
|
1033
|
+
setBusy(false);
|
|
1034
|
+
setCreateRefusal(refusalLineForAPerson(written.error));
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
const createdId = String(written.data);
|
|
1038
|
+
const refreshed = await client.list({ table_id: tableId, limit: 200 });
|
|
1039
|
+
if (refreshed.ok) setRows(refreshed.data.rows);
|
|
1040
|
+
setBusy(false);
|
|
1041
|
+
setCreating(false);
|
|
1042
|
+
setNewTitle("");
|
|
1043
|
+
setSearch("");
|
|
1044
|
+
toggle(createdId);
|
|
1045
|
+
};
|
|
985
1046
|
const visible = (rows ?? []).filter(
|
|
986
1047
|
(row) => search.trim() === "" ? true : titleOf(row.id).toLowerCase().includes(search.trim().toLowerCase())
|
|
987
1048
|
);
|
|
@@ -1028,7 +1089,57 @@ function RelationPicker({ field, value, onChange, disabled, id, className }) {
|
|
|
1028
1089
|
}
|
|
1029
1090
|
) }, row.id)) }),
|
|
1030
1091
|
rows && visible.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "p-2 text-xs text-muted-foreground", children: "Nothing in that table matches." }) : null
|
|
1031
|
-
] })
|
|
1092
|
+
] }),
|
|
1093
|
+
allowCreate && !full ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mt-1 border-t pt-1", children: [
|
|
1094
|
+
targetRefusal ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "p-2 text-xs text-destructive", children: targetRefusal }) : !target ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "p-2 text-xs text-muted-foreground", children: "Reading that table\u2026" }) : creating ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-1 p-1", children: [
|
|
1095
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1096
|
+
import_design_system2.BasicInput,
|
|
1097
|
+
{
|
|
1098
|
+
autoFocus: true,
|
|
1099
|
+
value: newTitle,
|
|
1100
|
+
onChange: (e) => setNewTitle(e.target.value),
|
|
1101
|
+
onKeyDown: (e) => {
|
|
1102
|
+
if (e.key === "Enter") {
|
|
1103
|
+
e.preventDefault();
|
|
1104
|
+
void create(newTitle);
|
|
1105
|
+
}
|
|
1106
|
+
},
|
|
1107
|
+
placeholder: `New ${targetWord}`,
|
|
1108
|
+
className: "h-7 flex-1 text-xs",
|
|
1109
|
+
"aria-label": `Name the new ${targetWord}`
|
|
1110
|
+
}
|
|
1111
|
+
),
|
|
1112
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1113
|
+
import_design_system2.Button,
|
|
1114
|
+
{
|
|
1115
|
+
type: "button",
|
|
1116
|
+
size: "sm",
|
|
1117
|
+
variant: "default",
|
|
1118
|
+
disabled: busy || newTitle.trim() === "",
|
|
1119
|
+
onClick: () => void create(newTitle),
|
|
1120
|
+
children: busy ? "Creating\u2026" : "Create"
|
|
1121
|
+
}
|
|
1122
|
+
)
|
|
1123
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1124
|
+
import_design_system2.Button,
|
|
1125
|
+
{
|
|
1126
|
+
type: "button",
|
|
1127
|
+
size: "sm",
|
|
1128
|
+
variant: "ghost",
|
|
1129
|
+
className: "w-full justify-start text-xs",
|
|
1130
|
+
disabled: busy,
|
|
1131
|
+
onClick: () => {
|
|
1132
|
+
if (search.trim() !== "") void create(search);
|
|
1133
|
+
else {
|
|
1134
|
+
setNewTitle("");
|
|
1135
|
+
setCreating(true);
|
|
1136
|
+
}
|
|
1137
|
+
},
|
|
1138
|
+
children: search.trim() !== "" ? `Create \u201C${search.trim()}\u201D` : `New ${targetWord}`
|
|
1139
|
+
}
|
|
1140
|
+
),
|
|
1141
|
+
createRefusal ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "p-2 text-xs text-destructive", children: createRefusal }) : null
|
|
1142
|
+
] }) : null
|
|
1032
1143
|
] })
|
|
1033
1144
|
] })
|
|
1034
1145
|
] });
|
|
@@ -1492,7 +1603,16 @@ function RefusalNotice({
|
|
|
1492
1603
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_design_system5.AlertDescription, { className: "space-y-1 text-xs", children: [
|
|
1493
1604
|
plain.sentence.replace(/\.$/, "") === plain.title ? null : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { children: plain.sentence }),
|
|
1494
1605
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "opacity-80", children: plain.remedy }),
|
|
1495
|
-
plain.forEngineers ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1606
|
+
plain.forEngineers ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1607
|
+
"p",
|
|
1608
|
+
{
|
|
1609
|
+
className: "sr-only",
|
|
1610
|
+
"aria-hidden": "true",
|
|
1611
|
+
"data-for-engineers": "",
|
|
1612
|
+
...error.sqlstate ? { "data-sqlstate": error.sqlstate } : {},
|
|
1613
|
+
children: plain.forEngineers
|
|
1614
|
+
}
|
|
1615
|
+
) : null,
|
|
1496
1616
|
actions
|
|
1497
1617
|
] })
|
|
1498
1618
|
]
|
|
@@ -5404,7 +5524,8 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
|
|
|
5404
5524
|
value: value === "" ? null : value,
|
|
5405
5525
|
onChange: (next) => write({ value: next === null || next === void 0 ? "" : String(next) }),
|
|
5406
5526
|
id: "condition-value-record",
|
|
5407
|
-
className: "min-w-[8rem]"
|
|
5527
|
+
className: "min-w-[8rem]",
|
|
5528
|
+
allowCreate: false
|
|
5408
5529
|
}
|
|
5409
5530
|
)
|
|
5410
5531
|
) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
@@ -7429,7 +7550,9 @@ function FieldPicker({
|
|
|
7429
7550
|
}) {
|
|
7430
7551
|
const want = LAYOUT_FIELD_KIND[layout];
|
|
7431
7552
|
const offerable = want ? offerableFields(all, want) : [];
|
|
7432
|
-
|
|
7553
|
+
const chosenField = chosen ? all.find((field) => field.key === chosen) : void 0;
|
|
7554
|
+
const unusual = chosenField && !offerable.some((field) => field.key === chosenField.key) ? chosenField : void 0;
|
|
7555
|
+
if (offerable.length === 0 && !unusual) {
|
|
7433
7556
|
return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("p", { className: "text-xs text-muted-foreground", children: LAYOUT_NO_FIELD[layout] });
|
|
7434
7557
|
}
|
|
7435
7558
|
return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex items-center gap-1.5 text-xs", children: [
|
|
@@ -7444,7 +7567,11 @@ function FieldPicker({
|
|
|
7444
7567
|
onChange: (event) => onPick(event.target.value === "" ? null : event.target.value),
|
|
7445
7568
|
children: [
|
|
7446
7569
|
/* @__PURE__ */ (0, import_jsx_runtime26.jsx)("option", { value: "", children: "Choose a field" }),
|
|
7447
|
-
offerable.map((field) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("option", { value: field.key, children: fieldName(field) }, field.key))
|
|
7570
|
+
offerable.map((field) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("option", { value: field.key, children: fieldName(field) }, field.key)),
|
|
7571
|
+
unusual ? /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("option", { value: unusual.key, children: [
|
|
7572
|
+
fieldName(unusual),
|
|
7573
|
+
" \u2014 an unusual column to use here"
|
|
7574
|
+
] }, unusual.key) : null
|
|
7448
7575
|
]
|
|
7449
7576
|
}
|
|
7450
7577
|
),
|