@ai-matrx/records-ui 0.41.0 → 0.43.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 +67 -0
- package/dist/index.cjs +50 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +50 -10
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,72 @@
|
|
|
1
1
|
# Changelog — @ai-matrx/records-ui
|
|
2
2
|
|
|
3
|
+
## 0.43.0
|
|
4
|
+
|
|
5
|
+
**Five headings carrying the store's real counts, over five empty columns.**
|
|
6
|
+
|
|
7
|
+
Measured in the real app (lane SCREENS-2, 2026-09-20) on a dental practice's
|
|
8
|
+
treatment plans, immediately after 0.42.0 made the board reachable at all: it
|
|
9
|
+
drew *Proposed 4*, *Insurance review 3*, *Scheduled 2 / 2* — and every column was
|
|
10
|
+
empty. That is the worst thing a screen can do. A heading that says four over a
|
|
11
|
+
column that shows none, with nothing anywhere saying the two disagree.
|
|
12
|
+
|
|
13
|
+
**A real disagreement about what a cell holds.** `PipelineStage.key` is documented
|
|
14
|
+
as "what a cell holds" and `custom.pipeline_board` keys its columns that way
|
|
15
|
+
(`stage_key: "proposed"`). `custom.read_records` answers the LABEL in the cell and
|
|
16
|
+
puts the key BESIDE it:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{"stage": "Proposed", "_choices": {"stage": {"key": "proposed", "label": "Proposed"}}}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
so `cardsByStage.get(stage.key)` matched nothing, for every row, on every pipeline.
|
|
23
|
+
|
|
24
|
+
`stageOfCard` is the one seam that decides it and it asks the ROW's own resolved
|
|
25
|
+
choice first, never the wording. Two fallbacks follow, because a cell may
|
|
26
|
+
legitimately hold the key (an agent wrote it) or only the label (an import did),
|
|
27
|
+
and neither is a reason to lose a card.
|
|
28
|
+
|
|
29
|
+
**And a card with nowhere to go is now SAID.** A retired stage, or a value an
|
|
30
|
+
import wrote that is not one of the board's choices, used to vanish — a board
|
|
31
|
+
quietly one card short is the same lie as the heading that disagreed with its
|
|
32
|
+
column. The board names them and says how to move one.
|
|
33
|
+
|
|
34
|
+
`src/pipeline-cards.test.tsx` — five clauses, all red on 0.42.0's bytes.
|
|
35
|
+
|
|
36
|
+
**Consumer action:** none.
|
|
37
|
+
|
|
38
|
+
## 0.42.0
|
|
39
|
+
|
|
40
|
+
**A pipeline table opened as "pick a column", and the board it already had was
|
|
41
|
+
unreachable.**
|
|
42
|
+
|
|
43
|
+
Measured in the real app (lane SCREENS-2, 2026-09-20) against a real pipeline —
|
|
44
|
+
five stages, a moves rule, a requires rule and a WIP limit of 2.
|
|
45
|
+
`custom.pipeline_read` answered `is_pipeline: true` with `stage_field: "stage"`,
|
|
46
|
+
every rule beside it, and the kanban drew
|
|
47
|
+
|
|
48
|
+
> A kanban board needs a Field to make its columns from — pick one and the board
|
|
49
|
+
> appears.
|
|
50
|
+
|
|
51
|
+
Two things stacked. `Board`'s "no grouping field" gate sat ABOVE its pipeline
|
|
52
|
+
branch, and `useIsPipeline` answered `stage.key === groupField` — so a table was
|
|
53
|
+
only a pipeline once somebody had ALREADY guessed which column to group by. The
|
|
54
|
+
store knew the answer the whole time and the screen asked the person for it. A
|
|
55
|
+
board nobody can reach without knowing the answer in advance is not a board.
|
|
56
|
+
|
|
57
|
+
The stage column is now the TABLE's own answer (`custom.table_stage_field`), used
|
|
58
|
+
both to know the table is a pipeline and to know which column its columns come
|
|
59
|
+
from. A view that names its own grouping column still wins — grouping a pipeline
|
|
60
|
+
by its owner is a real thing to want, and it draws the plain board of that column
|
|
61
|
+
and says so. While the table has not answered, NOTHING is claimed in either
|
|
62
|
+
direction: the "pick one" sentence would be a question about to answer itself.
|
|
63
|
+
|
|
64
|
+
`src/pipeline-reach.test.tsx` is at the one seam that decides it
|
|
65
|
+
(`boardColumnChoice`, exported for exactly that reason) and all four clauses fail
|
|
66
|
+
on 0.41.0's bytes.
|
|
67
|
+
|
|
68
|
+
**Consumer action:** none. `ViewSwitcher`'s props are unchanged.
|
|
69
|
+
|
|
3
70
|
## 0.41.0
|
|
4
71
|
|
|
5
72
|
**The last two product screens that could not work at all, rewritten against the
|
package/dist/index.cjs
CHANGED
|
@@ -5029,6 +5029,27 @@ var import_react38 = require("react");
|
|
|
5029
5029
|
var import_react39 = require("@ai-matrx/records/react");
|
|
5030
5030
|
var import_design_system18 = require("@ai-matrx/design-system");
|
|
5031
5031
|
var import_jsx_runtime20 = require("react/jsx-runtime");
|
|
5032
|
+
var UNPLACED = "\0unplaced";
|
|
5033
|
+
function stageOfCard(row, stageKey, stages) {
|
|
5034
|
+
const document2 = row.document ?? {};
|
|
5035
|
+
const choices = document2["_choices"];
|
|
5036
|
+
if (choices && typeof choices === "object") {
|
|
5037
|
+
const chosen = choices[stageKey];
|
|
5038
|
+
if (chosen && typeof chosen === "object") {
|
|
5039
|
+
const key = chosen["key"];
|
|
5040
|
+
if (typeof key === "string" && key !== "") {
|
|
5041
|
+
return stages.some((s) => s.key === key) ? key : null;
|
|
5042
|
+
}
|
|
5043
|
+
}
|
|
5044
|
+
}
|
|
5045
|
+
const raw = document2[stageKey];
|
|
5046
|
+
if (raw === null || raw === void 0 || raw === "") return null;
|
|
5047
|
+
const said = String(raw).trim();
|
|
5048
|
+
const byKey = stages.find((s) => s.key === said);
|
|
5049
|
+
if (byKey) return byKey.key;
|
|
5050
|
+
const byLabel = stages.find((s) => s.label.toLowerCase() === said.toLowerCase());
|
|
5051
|
+
return byLabel ? byLabel.key : null;
|
|
5052
|
+
}
|
|
5032
5053
|
function PipelineBoard({
|
|
5033
5054
|
tableId,
|
|
5034
5055
|
rows,
|
|
@@ -5078,12 +5099,12 @@ function PipelineBoard({
|
|
|
5078
5099
|
const map = /* @__PURE__ */ new Map();
|
|
5079
5100
|
if (!stageKey) return map;
|
|
5080
5101
|
for (const row of rows) {
|
|
5081
|
-
const
|
|
5082
|
-
|
|
5083
|
-
map.set(key, [...map.get(key) ?? [], row]);
|
|
5102
|
+
const key = stageOfCard(row, stageKey, definition?.stages ?? []);
|
|
5103
|
+
map.set(key ?? UNPLACED, [...map.get(key ?? UNPLACED) ?? [], row]);
|
|
5084
5104
|
}
|
|
5085
5105
|
return map;
|
|
5086
|
-
}, [rows, stageKey]);
|
|
5106
|
+
}, [rows, stageKey, definition?.stages]);
|
|
5107
|
+
const unplaced = cardsByStage.get(UNPLACED) ?? [];
|
|
5087
5108
|
const all = fields.data ?? [];
|
|
5088
5109
|
const titleField = (0, import_react38.useMemo)(
|
|
5089
5110
|
() => all.find((f) => f.key !== stageKey) ?? all[0],
|
|
@@ -5230,7 +5251,14 @@ function PipelineBoard({
|
|
|
5230
5251
|
},
|
|
5231
5252
|
stage.key
|
|
5232
5253
|
);
|
|
5233
|
-
}) })
|
|
5254
|
+
}) }),
|
|
5255
|
+
unplaced.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("p", { className: "text-xs text-muted-foreground", "data-testid": "pipeline-unplaced", children: [
|
|
5256
|
+
unplaced.length === 1 ? "One record is" : `${unplaced.length} records are`,
|
|
5257
|
+
" not in any of these columns \u2014 ",
|
|
5258
|
+
unplaced.slice(0, 3).map((r) => rowName(r, titleField?.key ?? null, "Untitled")).join(", "),
|
|
5259
|
+
unplaced.length > 3 ? ` and ${unplaced.length - 3} more` : "",
|
|
5260
|
+
". Their stage is not one this pipeline offers, so nothing here can draw them; open one from the grid to move it."
|
|
5261
|
+
] }) : null
|
|
5234
5262
|
] });
|
|
5235
5263
|
}
|
|
5236
5264
|
function formatTotal(total) {
|
|
@@ -5439,7 +5467,7 @@ function offerableFields(all, want) {
|
|
|
5439
5467
|
return kind === "datetime" || kind === "date";
|
|
5440
5468
|
});
|
|
5441
5469
|
}
|
|
5442
|
-
function
|
|
5470
|
+
function useStageField(tableId) {
|
|
5443
5471
|
const client = (0, import_react41.useRecordsClient)();
|
|
5444
5472
|
const [stage, setStage] = (0, import_react40.useState)({
|
|
5445
5473
|
asked: false,
|
|
@@ -5456,8 +5484,17 @@ function useIsPipeline(tableId, groupField) {
|
|
|
5456
5484
|
cancelled = true;
|
|
5457
5485
|
};
|
|
5458
5486
|
}, [client, tableId]);
|
|
5459
|
-
|
|
5460
|
-
|
|
5487
|
+
return stage;
|
|
5488
|
+
}
|
|
5489
|
+
function boardColumnChoice(fields, chosenKey, stage) {
|
|
5490
|
+
const has = (key) => key != null && fields.some((f) => f.key === key) ? key : null;
|
|
5491
|
+
const chosen = has(chosenKey);
|
|
5492
|
+
if (chosen) {
|
|
5493
|
+
return { groupKey: chosen, isPipeline: stage.asked ? stage.key === chosen : null, stillAsking: false };
|
|
5494
|
+
}
|
|
5495
|
+
if (!stage.asked) return { groupKey: null, isPipeline: null, stillAsking: true };
|
|
5496
|
+
const fromTable = has(stage.key);
|
|
5497
|
+
return { groupKey: fromTable, isPipeline: fromTable != null, stillAsking: false };
|
|
5461
5498
|
}
|
|
5462
5499
|
function Board({
|
|
5463
5500
|
view,
|
|
@@ -5469,7 +5506,7 @@ function Board({
|
|
|
5469
5506
|
}) {
|
|
5470
5507
|
const fields = (0, import_react41.useFields)(view.subject);
|
|
5471
5508
|
const records = useViewRecords(view, pageSize);
|
|
5472
|
-
const
|
|
5509
|
+
const stage = useStageField(view.subject);
|
|
5473
5510
|
if (fields.error) return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(RefusalNotice, { error: fields.error });
|
|
5474
5511
|
if (records.error) return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(RefusalNotice, { error: records.error });
|
|
5475
5512
|
if (fields.loading || records.loading) return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_design_system19.Skeleton, { className: "h-40 w-full" });
|
|
@@ -5477,7 +5514,9 @@ function Board({
|
|
|
5477
5514
|
const byKey = (key) => all.find((f) => f.key === key);
|
|
5478
5515
|
const titleField = byKey(view.groupField) ? all.find((f) => f.key !== view.groupField) ?? all[0] : all[0];
|
|
5479
5516
|
if (view.layout === "kanban") {
|
|
5480
|
-
const
|
|
5517
|
+
const choice = boardColumnChoice(all, view.groupField, stage);
|
|
5518
|
+
const groupField = byKey(choice.groupKey);
|
|
5519
|
+
const isPipeline = choice.isPipeline;
|
|
5481
5520
|
const picker = /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
5482
5521
|
FieldPicker,
|
|
5483
5522
|
{
|
|
@@ -5489,6 +5528,7 @@ function Board({
|
|
|
5489
5528
|
onPick: (key) => onChange({ groupField: key })
|
|
5490
5529
|
}
|
|
5491
5530
|
);
|
|
5531
|
+
if (choice.stillAsking) return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_design_system19.Skeleton, { className: "h-40 w-full" });
|
|
5492
5532
|
if (!groupField) return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Needs, { layout: "kanban", picker });
|
|
5493
5533
|
return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex min-h-0 flex-col gap-2", children: [
|
|
5494
5534
|
picker,
|