@ai-matrx/records-ui 0.52.0 → 0.56.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 +126 -0
- package/dist/index.cjs +156 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -1
- package/dist/index.d.ts +19 -1
- package/dist/index.js +156 -22
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,131 @@
|
|
|
1
1
|
# Changelog — @ai-matrx/records-ui
|
|
2
2
|
|
|
3
|
+
## 0.56.0
|
|
4
|
+
|
|
5
|
+
**A condition the builder cannot draw is said in words, not blanked.**
|
|
6
|
+
|
|
7
|
+
Found by a screenshot, which is what the screenshots were for. The Birchwood
|
|
8
|
+
Avenue renovation's own rule demands "a second quote from a DIFFERENT
|
|
9
|
+
contractor" — a `sibling_count`, which the one-clause builder cannot draw — and
|
|
10
|
+
the editor rendered it as **"— pick a column —"**. That is the screen saying
|
|
11
|
+
there is no condition when there is one, beside a live Save button, on a rule
|
|
12
|
+
that is turning people's work away right now.
|
|
13
|
+
|
|
14
|
+
It now reads: *"it cannot get here unless **how many others there are for the
|
|
15
|
+
same Room from a different Contractor** — this part of the rule is not something
|
|
16
|
+
this builder can draw yet, so it is shown as it reads"*, with a **Replace it**
|
|
17
|
+
button. Replacing is a thing a person DID, never a thing that happened to them
|
|
18
|
+
because a control looked empty — and until they press it, the clause they cannot
|
|
19
|
+
draw is still the one that saves.
|
|
20
|
+
|
|
21
|
+
`conditionIsSimple` and `conditionInWords` are exported, so every screen that
|
|
22
|
+
mounts the builder gets the same honesty rather than each deciding for itself.
|
|
23
|
+
|
|
24
|
+
## 0.55.0
|
|
25
|
+
|
|
26
|
+
**Three things the bulk paste got wrong on its first real fifty rows.**
|
|
27
|
+
|
|
28
|
+
Proved headlessly on **Rincon Plumbing Co's September billing run** — fifty real
|
|
29
|
+
invoices, `INV-5100`…`INV-5149`, a `$1,250.50` and a `(285.00)` credit, two
|
|
30
|
+
cells deliberately wrong. The paste itself worked end to end: the preview
|
|
31
|
+
appeared with the store untouched, the money arrived as `1250.5` rather than the
|
|
32
|
+
sheet's string, the bracketed credit as `-285`, and the outcome read *"All 198
|
|
33
|
+
values are in the table — read back out of it, not just accepted"*, confirmed by
|
|
34
|
+
an independent read as the same identity. These are the three things that were
|
|
35
|
+
wrong with it anyway.
|
|
36
|
+
|
|
37
|
+
* **Every refusal said "Row 0" and named a raw column key.** `validateCell` and
|
|
38
|
+
the planner ask the same question, and for a cell on a row that already exists
|
|
39
|
+
the primitive asks first — so the refusal arrived already decided, with a
|
|
40
|
+
column id and no line, and the planner's correct "Row 12, Amount Due" could
|
|
41
|
+
only ever fire for an overflow row. The refused cells are folded back into the
|
|
42
|
+
block with their own values (design-system 0.31.0 keeps them) and the planner
|
|
43
|
+
judges the whole thing: one place deciding, one set of words, a line number and
|
|
44
|
+
the column's own name on every refusal. With fifty rows on a clipboard that is
|
|
45
|
+
the difference between a fixable cell and a shrug.
|
|
46
|
+
* **"This will change 28 Invoicess and create 22 new Invoicess."** The sentence
|
|
47
|
+
appended an "s" to `label_singular`, which for that table is already
|
|
48
|
+
"Invoices". A plural is a word a table knows (`label_plural`), never a rule
|
|
49
|
+
applied to another word.
|
|
50
|
+
* **Every pasted date drew one day early**, systematically, across all fifty.
|
|
51
|
+
`2026-09-01` parsed as an INSTANT is midnight UTC, and a Ventura grid renders
|
|
52
|
+
that as 31 August. A date field holds a DAY — the day the invoice was issued is
|
|
53
|
+
the same day in Ventura and in Berlin — so a pasted day is kept as that day,
|
|
54
|
+
and only a field that actually declares a time becomes an instant. A
|
|
55
|
+
spreadsheet's own format ("9/1/2026") keeps the day the reader can see, not the
|
|
56
|
+
UTC one.
|
|
57
|
+
|
|
58
|
+
**And the demo harness could not stand up in a fresh organization.** All four
|
|
59
|
+
`ensure*Field` helpers updated the Table's `fields` list BEFORE inserting the
|
|
60
|
+
Field definition, which `custom._claimed_column_guard` refuses outright — *"Records
|
|
61
|
+
UI Demo says it has a column called \"signed_by\", and there is no such field."*
|
|
62
|
+
The harness only worked in organizations provisioned before that guard landed.
|
|
63
|
+
The order is the fix; the guard stays.
|
|
64
|
+
|
|
65
|
+
Needs `@ai-matrx/design-system` 0.31.0.
|
|
66
|
+
|
|
67
|
+
## 0.54.0
|
|
68
|
+
|
|
69
|
+
**The grid shows a column the panel beside it just added, without a reload.**
|
|
70
|
+
|
|
71
|
+
Measured on production 2026-09-21, Ridgeline Physical Therapy's patients table:
|
|
72
|
+
"Add field" succeeded, the panel confirmed the column with its own read-back and
|
|
73
|
+
closed — and the grid did not show it until the page was reloaded. The panel was
|
|
74
|
+
telling the truth; the grid next to it was drawing a table that no longer
|
|
75
|
+
existed.
|
|
76
|
+
|
|
77
|
+
The cause is not in this package's screens, and that is the point: the grid and
|
|
78
|
+
the field panel are siblings, each calling `useFields(tableId)`, each holding
|
|
79
|
+
its own copy, with nothing joining them. Fixing the pair that surfaced it would
|
|
80
|
+
have left the same staleness between every other pair — the rail and the grid,
|
|
81
|
+
the filter and the form builder. So the signal is `@ai-matrx/records` 0.39.0's
|
|
82
|
+
`tableShapeChanged`, which every shape-changing door announces and every reader
|
|
83
|
+
subscribes to, and this package simply inherits it.
|
|
84
|
+
|
|
85
|
+
`src/shape-changed.test.tsx` holds the five clauses at the pair a person
|
|
86
|
+
actually sees, against the real client through the real doors: a column added in
|
|
87
|
+
the panel appears in the grid beside it; every screen on that table re-reads,
|
|
88
|
+
not one; a screen on a DIFFERENT table is left alone (a signal that woke the
|
|
89
|
+
whole app would be a storm with a sensible name on it); the tick carries no data
|
|
90
|
+
and the listener re-reads through the door; and a retired column disappears the
|
|
91
|
+
same way. Four of the five are RED on 0.53.0's bytes.
|
|
92
|
+
|
|
93
|
+
Needs `@ai-matrx/records` 0.39.0.
|
|
94
|
+
|
|
95
|
+
## 0.53.0
|
|
96
|
+
|
|
97
|
+
**Every card on every board said "Loading…", forever.**
|
|
98
|
+
|
|
99
|
+
Birchwood Avenue Renovation is a whole-house remodel run out of a spare bedroom.
|
|
100
|
+
Her quotes board, grouped by Quote stage, had the counts exactly right — Accepted
|
|
101
|
+
6, Expired 1, Pending 6, Rejected 1, No value 1, off her real quotes — and all
|
|
102
|
+
fifteen cards under those headings read "Loading…" and never resolved. No error,
|
|
103
|
+
no refusal, no spinner that ever stopped. She could count her quotes and could
|
|
104
|
+
not open one.
|
|
105
|
+
|
|
106
|
+
The cause was one missing argument. `scalarText` draws "Loading…" for an id it
|
|
107
|
+
has no resolver for, and `Card` called it with no resolver at all, so a title
|
|
108
|
+
Field of type `list` or `relation` could never resolve on any card, ever — and
|
|
109
|
+
Birchwood's title field is `room`, a relation to her rooms table. `Kanban` asks
|
|
110
|
+
the resolver for the GROUPING field, which is precisely why the headings read
|
|
111
|
+
"Accepted" while the cards beneath them read "Loading…": two halves of one board,
|
|
112
|
+
one asking and one not.
|
|
113
|
+
|
|
114
|
+
`Card` is the card of the **kanban, the calendar and the gallery**, so one
|
|
115
|
+
argument broke three layouts and one fix closes all three. The component that
|
|
116
|
+
draws the value now declares the Field it is about to draw, which is also what
|
|
117
|
+
makes a future surface unable to reintroduce this by forgetting; `want` is keyed
|
|
118
|
+
by Field id, so two hundred cards still ask once.
|
|
119
|
+
|
|
120
|
+
**And a card that cannot load now says why.** Declaring the Field is what lets
|
|
121
|
+
the resolver SETTLE it. Once settled, an id it did not return is chased one at a
|
|
122
|
+
time and comes back either as the record's own name or as the store's own
|
|
123
|
+
sentence for a record this reader may not open
|
|
124
|
+
(`platform.relation_withheld_label()`). "Loading…" is now only ever true.
|
|
125
|
+
|
|
126
|
+
`src/card-names-what-it-draws.test.tsx` — three clauses, two shown RED on
|
|
127
|
+
0.52.0's bytes ("Loading…") and green on these.
|
|
128
|
+
|
|
3
129
|
## 0.52.0
|
|
4
130
|
|
|
5
131
|
**Four things this package could make and no screen could reach: a form, a
|
package/dist/index.cjs
CHANGED
|
@@ -94,6 +94,7 @@ __export(src_exports, {
|
|
|
94
94
|
MyChecklistSteps: () => MyChecklistSteps,
|
|
95
95
|
NEEDS_A_SENTENCE: () => NEEDS_A_SENTENCE,
|
|
96
96
|
NOT_ANSWERED_YET: () => NOT_ANSWERED_YET,
|
|
97
|
+
NOT_DRAWN_HERE: () => NOT_DRAWN_HERE,
|
|
97
98
|
NO_AGENT_PORT: () => NO_AGENT_PORT,
|
|
98
99
|
NO_CHAT_REASON: () => NO_CHAT_REASON,
|
|
99
100
|
NO_COLUMNS_WHY: () => NO_COLUMNS_WHY,
|
|
@@ -162,6 +163,8 @@ __export(src_exports, {
|
|
|
162
163
|
chooseSurface: () => chooseSurface,
|
|
163
164
|
colorFromTheValue: () => colorFromTheValue,
|
|
164
165
|
columnForField: () => columnForField,
|
|
166
|
+
conditionInWords: () => conditionInWords,
|
|
167
|
+
conditionIsSimple: () => conditionIsSimple,
|
|
165
168
|
conditionValue: () => conditionValue,
|
|
166
169
|
controlFor: () => controlFor,
|
|
167
170
|
currencyCodeFor: () => currencyCodeFor,
|
|
@@ -2728,7 +2731,7 @@ function EnrichPanel({ tableId, fieldId, className }) {
|
|
|
2728
2731
|
var import_react17 = require("react");
|
|
2729
2732
|
var import_design_system8 = require("@ai-matrx/design-system");
|
|
2730
2733
|
var import_jsx_runtime11 = require("react/jsx-runtime");
|
|
2731
|
-
function PastePreview({ plan, noun, onConfirm, onCancel }) {
|
|
2734
|
+
function PastePreview({ plan, noun, nounPlural, onConfirm, onCancel }) {
|
|
2732
2735
|
const [busy, setBusy] = (0, import_react17.useState)(false);
|
|
2733
2736
|
const [outcome, setOutcome] = (0, import_react17.useState)(null);
|
|
2734
2737
|
const nothingToDo = plan.cells === 0;
|
|
@@ -2748,7 +2751,7 @@ function PastePreview({ plan, noun, onConfirm, onCancel }) {
|
|
|
2748
2751
|
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_design_system8.Separator, {}),
|
|
2749
2752
|
outcome === null ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
|
|
2750
2753
|
nothingToDo ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "text-sm", children: "Nothing in what you pasted can be written to this table. Every cell is listed below with the reason." }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("p", { className: "text-sm", children: [
|
|
2751
|
-
sentence(plan, noun),
|
|
2754
|
+
sentence(plan, noun, nounPlural),
|
|
2752
2755
|
" The values below are exactly what will be stored."
|
|
2753
2756
|
] }),
|
|
2754
2757
|
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(PlanTable, { plan }),
|
|
@@ -2788,7 +2791,7 @@ function PastePreview({ plan, noun, onConfirm, onCancel }) {
|
|
|
2788
2791
|
setBusy(true);
|
|
2789
2792
|
void onConfirm().then(setOutcome).finally(() => setBusy(false));
|
|
2790
2793
|
},
|
|
2791
|
-
children: busy ? "Writing\u2026" : confirmLabel(plan, noun)
|
|
2794
|
+
children: busy ? "Writing\u2026" : confirmLabel(plan, noun, nounPlural)
|
|
2792
2795
|
}
|
|
2793
2796
|
)
|
|
2794
2797
|
] })
|
|
@@ -2843,19 +2846,19 @@ function plural(n, one, many) {
|
|
|
2843
2846
|
function summarize(plan) {
|
|
2844
2847
|
return `${plural(plan.cells, "value", "values")} across ${plural(plan.rows.length, "row", "rows")}`;
|
|
2845
2848
|
}
|
|
2846
|
-
function sentence(plan, noun) {
|
|
2849
|
+
function sentence(plan, noun, nounPlural) {
|
|
2847
2850
|
const parts = [];
|
|
2848
|
-
if (plan.updates > 0) parts.push(`change ${plural(plan.updates, noun,
|
|
2849
|
-
if (plan.creates > 0) parts.push(`create ${plural(plan.creates, `new ${noun}`, `new ${
|
|
2851
|
+
if (plan.updates > 0) parts.push(`change ${plural(plan.updates, noun, nounPlural)}`);
|
|
2852
|
+
if (plan.creates > 0) parts.push(`create ${plural(plan.creates, `new ${noun}`, `new ${nounPlural}`)}`);
|
|
2850
2853
|
const what = parts.length === 2 ? `${parts[0]} and ${parts[1]}` : parts[0] ?? "write nothing";
|
|
2851
2854
|
return `This will ${what}.`;
|
|
2852
2855
|
}
|
|
2853
|
-
function confirmLabel(plan, noun) {
|
|
2856
|
+
function confirmLabel(plan, noun, nounPlural) {
|
|
2854
2857
|
if (plan.creates > 0 && plan.updates === 0) {
|
|
2855
|
-
return `Create ${plural(plan.creates, noun,
|
|
2858
|
+
return `Create ${plural(plan.creates, noun, nounPlural)}`;
|
|
2856
2859
|
}
|
|
2857
2860
|
if (plan.creates > 0) return `Write ${plural(plan.rows.length, "row", "rows")}`;
|
|
2858
|
-
return `Update ${plural(plan.updates, noun,
|
|
2861
|
+
return `Update ${plural(plan.updates, noun, nounPlural)}`;
|
|
2859
2862
|
}
|
|
2860
2863
|
|
|
2861
2864
|
// src/pasteValues.ts
|
|
@@ -2890,13 +2893,21 @@ function valueFromPastedText(field, raw) {
|
|
|
2890
2893
|
return { value: negative ? -n : n };
|
|
2891
2894
|
}
|
|
2892
2895
|
case "datetime": {
|
|
2896
|
+
const wantsTime = String(field.config?.["kind"] ?? "date") === "datetime";
|
|
2897
|
+
const plainDay = /^\s*(\d{4})-(\d{2})-(\d{2})\s*$/.exec(raw);
|
|
2898
|
+
if (!wantsTime && plainDay) return { value: `${plainDay[1]}-${plainDay[2]}-${plainDay[3]}` };
|
|
2893
2899
|
const at = Date.parse(text);
|
|
2894
2900
|
if (Number.isNaN(at)) {
|
|
2895
2901
|
return {
|
|
2896
2902
|
refusal: `${label} holds a date, and \u201C${raw}\u201D is not one this can read. Paste it as 2026-09-21, or as your spreadsheet's own date format.`
|
|
2897
2903
|
};
|
|
2898
2904
|
}
|
|
2899
|
-
|
|
2905
|
+
const parsed = new Date(at);
|
|
2906
|
+
if (wantsTime) return { value: parsed.toISOString() };
|
|
2907
|
+
const y = parsed.getFullYear();
|
|
2908
|
+
const m = String(parsed.getMonth() + 1).padStart(2, "0");
|
|
2909
|
+
const d = String(parsed.getDate()).padStart(2, "0");
|
|
2910
|
+
return { value: `${y}-${m}-${d}` };
|
|
2900
2911
|
}
|
|
2901
2912
|
case "json": {
|
|
2902
2913
|
try {
|
|
@@ -3087,42 +3098,55 @@ function Grid({
|
|
|
3087
3098
|
const fieldList = fields.data ?? [];
|
|
3088
3099
|
const byRecord = /* @__PURE__ */ new Map();
|
|
3089
3100
|
const order = [];
|
|
3090
|
-
|
|
3091
|
-
let held = byRecord.get(
|
|
3101
|
+
const take = (rowId, columnId, value) => {
|
|
3102
|
+
let held = byRecord.get(rowId);
|
|
3092
3103
|
if (held === void 0) {
|
|
3093
3104
|
held = {};
|
|
3094
|
-
byRecord.set(
|
|
3095
|
-
order.push(
|
|
3105
|
+
byRecord.set(rowId, held);
|
|
3106
|
+
order.push(rowId);
|
|
3096
3107
|
}
|
|
3097
|
-
held[
|
|
3108
|
+
held[columnId] = value === null || value === void 0 ? "" : String(value);
|
|
3109
|
+
};
|
|
3110
|
+
for (const patch of sheetPlan.accepted) take(patch.rowId, patch.columnId, patch.value);
|
|
3111
|
+
for (const refused of sheetPlan.refusals) {
|
|
3112
|
+
if (refused.rowId === null) continue;
|
|
3113
|
+
take(refused.rowId, refused.columnId, refused.value);
|
|
3098
3114
|
}
|
|
3115
|
+
const onScreen = rows.map((r) => r.id).filter((id) => byRecord.has(id));
|
|
3116
|
+
const touched = [...onScreen, ...order.filter((id) => !onScreen.includes(id))];
|
|
3099
3117
|
const columnKeys = Array.from(
|
|
3100
3118
|
/* @__PURE__ */ new Set([
|
|
3101
3119
|
...sheetPlan.accepted.map((p) => p.columnId),
|
|
3120
|
+
...sheetPlan.refusals.map((r) => r.columnId),
|
|
3102
3121
|
...sheetPlan.overflow.flatMap((row) => Object.keys(row))
|
|
3103
3122
|
])
|
|
3104
3123
|
);
|
|
3105
3124
|
const built = planPastedBlock({
|
|
3106
3125
|
fields: fieldList,
|
|
3107
3126
|
columnKeys,
|
|
3108
|
-
onExisting:
|
|
3127
|
+
onExisting: touched.map((recordId) => ({
|
|
3109
3128
|
recordId,
|
|
3110
3129
|
raw: columnKeys.map((key) => byRecord.get(recordId)?.[key])
|
|
3111
3130
|
})),
|
|
3112
3131
|
onNew: sheetPlan.overflow.map((row) => columnKeys.map((key) => row[key])),
|
|
3113
3132
|
pastRightEdge: sheetPlan.pastRightEdge
|
|
3114
3133
|
});
|
|
3134
|
+
const notWritable = /* @__PURE__ */ new Map();
|
|
3115
3135
|
for (const refused of sheetPlan.refusals) {
|
|
3136
|
+
if (built.refusals.some((r) => r.column === columnLabel(fieldList, refused.columnId))) continue;
|
|
3137
|
+
notWritable.set(refused.columnId, refused.reason);
|
|
3138
|
+
}
|
|
3139
|
+
for (const [columnId, why] of notWritable) {
|
|
3116
3140
|
built.refusals.push({
|
|
3117
3141
|
fromLine: 0,
|
|
3118
|
-
column:
|
|
3142
|
+
column: columnLabel(fieldList, columnId),
|
|
3119
3143
|
raw: "",
|
|
3120
|
-
why
|
|
3144
|
+
why
|
|
3121
3145
|
});
|
|
3122
3146
|
}
|
|
3123
3147
|
setPastePlan({ plan: built });
|
|
3124
3148
|
},
|
|
3125
|
-
[fields.data]
|
|
3149
|
+
[fields.data, rows]
|
|
3126
3150
|
);
|
|
3127
3151
|
const applyPaste = (0, import_react18.useCallback)(async () => {
|
|
3128
3152
|
const held = pastePlan;
|
|
@@ -3433,6 +3457,7 @@ function Grid({
|
|
|
3433
3457
|
{
|
|
3434
3458
|
plan: pastePlan.plan,
|
|
3435
3459
|
noun: table.data?.label_singular ?? "record",
|
|
3460
|
+
nounPlural: table.data?.label_plural ?? `${table.data?.label_singular ?? "record"}s`,
|
|
3436
3461
|
onConfirm: applyPaste,
|
|
3437
3462
|
onCancel: () => setPastePlan(null)
|
|
3438
3463
|
}
|
|
@@ -3450,6 +3475,10 @@ function Grid({
|
|
|
3450
3475
|
] })
|
|
3451
3476
|
] }) });
|
|
3452
3477
|
}
|
|
3478
|
+
function columnLabel(fields, columnId) {
|
|
3479
|
+
const field = fields.find((f) => f.key === columnId);
|
|
3480
|
+
return field ? fieldName(field) : humanize(columnId);
|
|
3481
|
+
}
|
|
3453
3482
|
function AddFieldButton({ onClick }) {
|
|
3454
3483
|
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
3455
3484
|
import_design_system9.Button,
|
|
@@ -4788,6 +4817,60 @@ var CONDITION_OPS = [
|
|
|
4788
4817
|
{ op: "gt", label: "is more than" },
|
|
4789
4818
|
{ op: "lt", label: "is less than" }
|
|
4790
4819
|
];
|
|
4820
|
+
function conditionIsSimple(expr) {
|
|
4821
|
+
if (expr === null || expr === void 0) return true;
|
|
4822
|
+
const node = expr;
|
|
4823
|
+
if (typeof node.op !== "string" || !CONDITION_OPS.some((c) => c.op === node.op)) return false;
|
|
4824
|
+
if (!Array.isArray(node.args)) return false;
|
|
4825
|
+
const first = node.args[0];
|
|
4826
|
+
if (!first || typeof first.field !== "string") return false;
|
|
4827
|
+
if (node.op === "present") return node.args.length === 1;
|
|
4828
|
+
const second = node.args[1];
|
|
4829
|
+
return node.args.length === 2 && !!second && "const" in second;
|
|
4830
|
+
}
|
|
4831
|
+
function conditionInWords(expr, fields) {
|
|
4832
|
+
const name = (id) => {
|
|
4833
|
+
const f = fields.find((x) => x.id === id);
|
|
4834
|
+
return f ? f.label || f.key : "a column";
|
|
4835
|
+
};
|
|
4836
|
+
const said = {
|
|
4837
|
+
eq: "is",
|
|
4838
|
+
ne: "is not",
|
|
4839
|
+
gt: "is more than",
|
|
4840
|
+
lt: "is less than",
|
|
4841
|
+
gte: "is at least",
|
|
4842
|
+
lte: "is at most",
|
|
4843
|
+
and: "and",
|
|
4844
|
+
or: "or",
|
|
4845
|
+
matches: "matches"
|
|
4846
|
+
};
|
|
4847
|
+
const walk = (node) => {
|
|
4848
|
+
if (node === null || node === void 0) return "nothing";
|
|
4849
|
+
const n = node;
|
|
4850
|
+
if ("const" in n) return JSON.stringify(n["const"]);
|
|
4851
|
+
if (typeof n["field"] === "string" && !("op" in n)) return name(n["field"]);
|
|
4852
|
+
const op = typeof n["op"] === "string" ? n["op"] : "";
|
|
4853
|
+
if (op === "previous" && typeof n["field"] === "string") return `what ${name(n["field"])} used to say`;
|
|
4854
|
+
if (op === "sibling_count") {
|
|
4855
|
+
const same = n["same"] ?? [];
|
|
4856
|
+
const differs = n["differs"] ?? [];
|
|
4857
|
+
const sameWords = same.length > 0 ? ` for the same ${same.map(name).join(" and ")}` : "";
|
|
4858
|
+
const diffWords = differs.length > 0 ? ` from a different ${differs.map(name).join(" and ")}` : "";
|
|
4859
|
+
return `how many others there are${sameWords}${diffWords}`;
|
|
4860
|
+
}
|
|
4861
|
+
if (op === "stage_count") return "how many are in that column already";
|
|
4862
|
+
if (op === "actor_at_least") return `whoever is moving it is at least ${String(n["level"] ?? "somebody")}`;
|
|
4863
|
+
const args = Array.isArray(n["args"]) ? n["args"] : [];
|
|
4864
|
+
if (op === "not") return `not ${walk(args[0])}`;
|
|
4865
|
+
if (op === "present") return `${walk(args[0])} has been answered`;
|
|
4866
|
+
const word = said[op] ?? op;
|
|
4867
|
+
if (op === "and" || op === "or") return args.map(walk).join(` ${word} `);
|
|
4868
|
+
if (args.length === 2) return `${walk(args[0])} ${word} ${walk(args[1])}`;
|
|
4869
|
+
if (args.length === 1) return `${walk(args[0])} ${word}`;
|
|
4870
|
+
return op;
|
|
4871
|
+
};
|
|
4872
|
+
return walk(expr);
|
|
4873
|
+
}
|
|
4791
4874
|
function conditionValue(typed) {
|
|
4792
4875
|
const trimmed = typed.trim();
|
|
4793
4876
|
if (trimmed === "") return typed;
|
|
@@ -4859,6 +4942,7 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
|
|
|
4859
4942
|
var import_jsx_runtime18 = require("react/jsx-runtime");
|
|
4860
4943
|
var NO_RULES_YET = "Nothing has to be true to get here yet.";
|
|
4861
4944
|
var NEEDS_A_SENTENCE = "Write the sentence somebody reads when this stops them, the way you would say it out loud.";
|
|
4945
|
+
var NOT_DRAWN_HERE = "This part of the rule is not something this builder can draw yet";
|
|
4862
4946
|
var ON_FAIL = [
|
|
4863
4947
|
{ value: "refuse", label: "Turn it away", note: "The card stays where it is and the person is told why." },
|
|
4864
4948
|
{
|
|
@@ -5039,7 +5123,7 @@ function StageRulesSection({ tableId, stage, className }) {
|
|
|
5039
5123
|
}
|
|
5040
5124
|
),
|
|
5041
5125
|
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
5042
|
-
|
|
5126
|
+
Clause,
|
|
5043
5127
|
{
|
|
5044
5128
|
lead: "When",
|
|
5045
5129
|
emptyLabel: "every card",
|
|
@@ -5049,7 +5133,7 @@ function StageRulesSection({ tableId, stage, className }) {
|
|
|
5049
5133
|
}
|
|
5050
5134
|
),
|
|
5051
5135
|
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
5052
|
-
|
|
5136
|
+
Clause,
|
|
5053
5137
|
{
|
|
5054
5138
|
lead: "it cannot get here unless",
|
|
5055
5139
|
emptyLabel: "\u2014 pick a column \u2014",
|
|
@@ -5093,6 +5177,50 @@ function StageRulesSection({ tableId, stage, className }) {
|
|
|
5093
5177
|
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_design_system15.Separator, {})
|
|
5094
5178
|
] });
|
|
5095
5179
|
}
|
|
5180
|
+
function Clause({
|
|
5181
|
+
lead,
|
|
5182
|
+
emptyLabel,
|
|
5183
|
+
expr,
|
|
5184
|
+
fields,
|
|
5185
|
+
onChange
|
|
5186
|
+
}) {
|
|
5187
|
+
const [replacing, setReplacing] = (0, import_react29.useState)(false);
|
|
5188
|
+
const drawable = conditionIsSimple(expr) || replacing;
|
|
5189
|
+
if (drawable) {
|
|
5190
|
+
return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
5191
|
+
ConditionRow,
|
|
5192
|
+
{
|
|
5193
|
+
lead,
|
|
5194
|
+
emptyLabel,
|
|
5195
|
+
expr: replacing && !conditionIsSimple(expr) ? null : expr,
|
|
5196
|
+
fields,
|
|
5197
|
+
onChange
|
|
5198
|
+
}
|
|
5199
|
+
);
|
|
5200
|
+
}
|
|
5201
|
+
return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex flex-wrap items-center gap-1 text-xs", "data-testid": "stage-rule-clause-in-words", children: [
|
|
5202
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "text-muted-foreground", children: lead }),
|
|
5203
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "font-medium", children: conditionInWords(expr, fields) }),
|
|
5204
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { className: "text-muted-foreground", children: [
|
|
5205
|
+
"\u2014 ",
|
|
5206
|
+
NOT_DRAWN_HERE,
|
|
5207
|
+
", so it is shown as it reads."
|
|
5208
|
+
] }),
|
|
5209
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
5210
|
+
import_design_system15.Button,
|
|
5211
|
+
{
|
|
5212
|
+
size: "sm",
|
|
5213
|
+
variant: "ghost",
|
|
5214
|
+
className: "h-6",
|
|
5215
|
+
onClick: () => {
|
|
5216
|
+
setReplacing(true);
|
|
5217
|
+
onChange(null);
|
|
5218
|
+
},
|
|
5219
|
+
children: "Replace it"
|
|
5220
|
+
}
|
|
5221
|
+
)
|
|
5222
|
+
] });
|
|
5223
|
+
}
|
|
5096
5224
|
|
|
5097
5225
|
// src/TableSettings.tsx
|
|
5098
5226
|
var import_react31 = require("react");
|
|
@@ -6404,6 +6532,7 @@ function Card({
|
|
|
6404
6532
|
|
|
6405
6533
|
// src/ViewSwitcher.tsx
|
|
6406
6534
|
var import_jsx_runtime25 = require("react/jsx-runtime");
|
|
6535
|
+
var NO_FIELDS = [];
|
|
6407
6536
|
function useViewRecords(view, pageSize = 200) {
|
|
6408
6537
|
const client = (0, import_react46.useRecordsClient)();
|
|
6409
6538
|
const table = (0, import_react46.useRecords)(view.ruleId ? null : view.subject, { pageSize });
|
|
@@ -6773,7 +6902,12 @@ function Card2({
|
|
|
6773
6902
|
onOpenRecord
|
|
6774
6903
|
}) {
|
|
6775
6904
|
const host = useRecordsUi();
|
|
6776
|
-
const
|
|
6905
|
+
const wanted = (0, import_react45.useMemo)(
|
|
6906
|
+
() => field && pointsAtRecords(field) ? [field] : NO_FIELDS,
|
|
6907
|
+
[field]
|
|
6908
|
+
);
|
|
6909
|
+
const labels = useRecordLabels(wanted);
|
|
6910
|
+
const label = field ? scalarText(field, (record.document ?? {})[field.key], labels) : rowName(record, null, "Untitled");
|
|
6777
6911
|
return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
|
|
6778
6912
|
"button",
|
|
6779
6913
|
{
|