@ai-matrx/records-ui 0.68.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 +27 -0
- package/dist/index.cjs +118 -6
- 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 +118 -6
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
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
|
+
|
|
3
30
|
## 0.68.0
|
|
4
31
|
|
|
5
32
|
**Two things a screen was still doing behind a person's back.** (lane TAILS-6, found while
|
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
|
] });
|
|
@@ -5413,7 +5524,8 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
|
|
|
5413
5524
|
value: value === "" ? null : value,
|
|
5414
5525
|
onChange: (next) => write({ value: next === null || next === void 0 ? "" : String(next) }),
|
|
5415
5526
|
id: "condition-value-record",
|
|
5416
|
-
className: "min-w-[8rem]"
|
|
5527
|
+
className: "min-w-[8rem]",
|
|
5528
|
+
allowCreate: false
|
|
5417
5529
|
}
|
|
5418
5530
|
)
|
|
5419
5531
|
) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|