@ai-matrx/records-ui 0.28.0 → 0.30.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 +71 -0
- package/dist/index.cjs +144 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -3
- package/dist/index.d.ts +66 -3
- package/dist/index.js +144 -4
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,74 @@
|
|
|
1
1
|
# Changelog — @ai-matrx/records-ui
|
|
2
2
|
|
|
3
|
+
## 0.30.0
|
|
4
|
+
|
|
5
|
+
**The notification editor stops asking for the notifier's own reader.**
|
|
6
|
+
|
|
7
|
+
`NotifyRuleEditor` listed subscriptions with `client.aggSubscriptions()` and read
|
|
8
|
+
its cadences from `client.aggSubscriptionCadences()`. Both are `custom.agg_*` —
|
|
9
|
+
the NOTIFIER's internals — and neither holds a client grant, so a signed-in
|
|
10
|
+
person opened this panel on *"permission denied for function agg_subscriptions"*
|
|
11
|
+
and the cadence picker silently fell back to a word typed into the file. Lane
|
|
12
|
+
TEST-BENCH photographed that refusal on the try-everything page on 2026-09-20.
|
|
13
|
+
|
|
14
|
+
It now asks the person's own doors: `client.subscriptions({ table_id })`, which
|
|
15
|
+
answers what is addressed to them plus — only where they hold admin on this
|
|
16
|
+
Table — anyone's over it, narrowed to Tables they can already open; and
|
|
17
|
+
`client.subscriptionCadences()`, which delegates to the notifier's one array.
|
|
18
|
+
|
|
19
|
+
Two consequences, both visible: the list is now scoped to the Table the editor is
|
|
20
|
+
about (it used to answer the whole organization), and a **muted** subscription
|
|
21
|
+
appears with the sentence *"Switched off — it tells nobody until somebody
|
|
22
|
+
switches it back on."* The notifier's reader excluded muted rows, so switching one
|
|
23
|
+
off used to make it vanish from this list and read as gone rather than as off.
|
|
24
|
+
|
|
25
|
+
**Consumer action:** requires `@ai-matrx/records` 0.17.0 or above.
|
|
26
|
+
|
|
27
|
+
## 0.29.0
|
|
28
|
+
|
|
29
|
+
**A saved view can now say how the table is SECTIONED, how wide its columns are,
|
|
30
|
+
and how tall its rows are — and the grid windows itself and stops implying it
|
|
31
|
+
counted everything.**
|
|
32
|
+
|
|
33
|
+
The shared data table gained grouping, controlled column widths, virtualization
|
|
34
|
+
and an honest count in design-system 0.23.0. This is the store's half, and it
|
|
35
|
+
rides the document a view already has (`records_ui_view.presentation`): no new
|
|
36
|
+
column, no new table, and an agent sets any of it with the same record write it
|
|
37
|
+
already uses for colors and formats.
|
|
38
|
+
|
|
39
|
+
- **Sections by a Field.** `presentation.grouping` names a Field key, with
|
|
40
|
+
optional per-Field subtotals, a section order and the sections to keep shut.
|
|
41
|
+
Grouped by Status, the table draws one collapsible header per status with a
|
|
42
|
+
count of records on it — and a view that names a Field but no subtotals gets a
|
|
43
|
+
COUNT, never a total nobody asked for.
|
|
44
|
+
- **Remembered column widths.** `presentation.widths` is sparse and keyed by
|
|
45
|
+
Field key, so a view saved before widths existed still reads. Drag a column
|
|
46
|
+
wider and the width is kept.
|
|
47
|
+
- **A row height.** `presentation.rowHeight`, clamped to 24–96px. A view asking
|
|
48
|
+
for anything outside that is dropped rather than obeyed, because a saved look
|
|
49
|
+
must not be able to make a table unreadable.
|
|
50
|
+
- **The grid windows itself.** Virtualization is on for every record table; the
|
|
51
|
+
primitive's own threshold decides when it engages, so short tables are
|
|
52
|
+
untouched and a very long one scrolls.
|
|
53
|
+
- **The count says what it covers.** The search and filters in the grid's
|
|
54
|
+
toolbar run in the browser over the page the read door returned, so the table
|
|
55
|
+
now states that in plain English instead of letting the loaded count pose as
|
|
56
|
+
the whole table — and says the total is unknown when the door cannot give one.
|
|
57
|
+
- **The person's own changes are saved back onto the view.** Picking a column to
|
|
58
|
+
group by, shutting a section and resizing a column travel `Grid`'s new
|
|
59
|
+
`onPresentationChange` into the door every other view change already uses, so
|
|
60
|
+
`TablePage` writes them onto the view record with the version it read. A host
|
|
61
|
+
that binds no door still gets all three for the visit.
|
|
62
|
+
|
|
63
|
+
### Consumer action
|
|
64
|
+
|
|
65
|
+
- Nothing to change. Every new field is optional and a view that carries none of
|
|
66
|
+
them renders exactly as it did.
|
|
67
|
+
- A host that mounts `Grid` directly and wants these remembered binds
|
|
68
|
+
`onPresentationChange` and saves the look onto its view record;
|
|
69
|
+
`ViewSwitcher` and `TablePage` already do.
|
|
70
|
+
- Needs `@ai-matrx/design-system` 0.23.0 or above.
|
|
71
|
+
|
|
3
72
|
## 0.28.0
|
|
4
73
|
|
|
5
74
|
**The publish canary installs every peer this package declares.** It named
|
|
@@ -12,6 +81,8 @@ installed in a pass of their own so each brings its own peers (`react-is` is
|
|
|
12
81
|
`recharts`'). 0.27.0 was tagged and never published; its tag was left where it
|
|
13
82
|
is.
|
|
14
83
|
|
|
84
|
+
## 0.27.0
|
|
85
|
+
|
|
15
86
|
**A first-time admin could not make a table, and the gate could not see it.**
|
|
16
87
|
|
|
17
88
|
Creating one showed a red refusal and made a table anyway; that table opened with
|
package/dist/index.cjs
CHANGED
|
@@ -81,6 +81,8 @@ __export(src_exports, {
|
|
|
81
81
|
LAYOUT_NO_FIELD: () => LAYOUT_NO_FIELD,
|
|
82
82
|
LEVEL_WORD: () => LEVEL_WORD,
|
|
83
83
|
MACHINE_IDENTITY: () => MACHINE_IDENTITY,
|
|
84
|
+
MAX_ROW_HEIGHT: () => MAX_ROW_HEIGHT,
|
|
85
|
+
MIN_ROW_HEIGHT: () => MIN_ROW_HEIGHT,
|
|
84
86
|
NOT_ANSWERED_YET: () => NOT_ANSWERED_YET,
|
|
85
87
|
NO_CHAT_REASON: () => NO_CHAT_REASON,
|
|
86
88
|
NO_COLUMNS_WHY: () => NO_COLUMNS_WHY,
|
|
@@ -1874,6 +1876,27 @@ var import_data_table3 = require("@ai-matrx/design-system/data-table");
|
|
|
1874
1876
|
var import_data_table2 = require("@ai-matrx/design-system/data-table");
|
|
1875
1877
|
var EMPTY_PRESENTATION = {};
|
|
1876
1878
|
var FROZEN_COLUMN_WIDTH = 200;
|
|
1879
|
+
var MIN_ROW_HEIGHT = 24;
|
|
1880
|
+
var MAX_ROW_HEIGHT = 96;
|
|
1881
|
+
var AGGREGATE_KINDS = [
|
|
1882
|
+
"count",
|
|
1883
|
+
"sum",
|
|
1884
|
+
"average",
|
|
1885
|
+
"min",
|
|
1886
|
+
"max",
|
|
1887
|
+
"empty",
|
|
1888
|
+
"filled",
|
|
1889
|
+
"unique",
|
|
1890
|
+
"checked",
|
|
1891
|
+
"unchecked",
|
|
1892
|
+
"percentFilled"
|
|
1893
|
+
];
|
|
1894
|
+
var GROUP_ORDERS = [
|
|
1895
|
+
"value-asc",
|
|
1896
|
+
"value-desc",
|
|
1897
|
+
"count-desc",
|
|
1898
|
+
"first-seen"
|
|
1899
|
+
];
|
|
1877
1900
|
function isRecord(v) {
|
|
1878
1901
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1879
1902
|
}
|
|
@@ -1910,10 +1933,51 @@ function parseGridPresentation(raw) {
|
|
|
1910
1933
|
const frozen = value["frozen"].filter((k) => typeof k === "string");
|
|
1911
1934
|
if (frozen.length > 0) out.frozen = frozen;
|
|
1912
1935
|
}
|
|
1936
|
+
const grouping = parseGrouping(value["grouping"]);
|
|
1937
|
+
if (grouping) out.grouping = grouping;
|
|
1938
|
+
if (isRecord(value["widths"])) {
|
|
1939
|
+
const widths = {};
|
|
1940
|
+
for (const [key, raw2] of Object.entries(value["widths"])) {
|
|
1941
|
+
if (typeof raw2 !== "number" || !Number.isFinite(raw2) || raw2 <= 0) continue;
|
|
1942
|
+
widths[key] = Math.round(raw2);
|
|
1943
|
+
}
|
|
1944
|
+
if (Object.keys(widths).length > 0) out.widths = widths;
|
|
1945
|
+
}
|
|
1946
|
+
const rowHeight = value["rowHeight"];
|
|
1947
|
+
if (typeof rowHeight === "number" && Number.isFinite(rowHeight) && rowHeight >= MIN_ROW_HEIGHT && rowHeight <= MAX_ROW_HEIGHT) {
|
|
1948
|
+
out.rowHeight = Math.round(rowHeight);
|
|
1949
|
+
}
|
|
1950
|
+
return out;
|
|
1951
|
+
}
|
|
1952
|
+
function parseGrouping(raw) {
|
|
1953
|
+
if (!isRecord(raw)) return void 0;
|
|
1954
|
+
const field = raw["field"];
|
|
1955
|
+
if (typeof field !== "string" || field.trim() === "") return void 0;
|
|
1956
|
+
const out = { field };
|
|
1957
|
+
if (isRecord(raw["aggregates"])) {
|
|
1958
|
+
const aggregates = {};
|
|
1959
|
+
for (const [key, spec] of Object.entries(raw["aggregates"])) {
|
|
1960
|
+
if (!isRecord(spec)) continue;
|
|
1961
|
+
const kind = spec["kind"];
|
|
1962
|
+
if (typeof kind !== "string") continue;
|
|
1963
|
+
if (!AGGREGATE_KINDS.includes(kind)) continue;
|
|
1964
|
+
const label = spec["label"];
|
|
1965
|
+
aggregates[key] = typeof label === "string" ? { kind, label } : { kind };
|
|
1966
|
+
}
|
|
1967
|
+
if (Object.keys(aggregates).length > 0) out.aggregates = aggregates;
|
|
1968
|
+
}
|
|
1969
|
+
const order = raw["order"];
|
|
1970
|
+
if (typeof order === "string" && GROUP_ORDERS.includes(order)) {
|
|
1971
|
+
out.order = order;
|
|
1972
|
+
}
|
|
1973
|
+
if (Array.isArray(raw["collapsed"])) {
|
|
1974
|
+
const collapsed = raw["collapsed"].filter((k) => typeof k === "string");
|
|
1975
|
+
if (collapsed.length > 0) out.collapsed = collapsed;
|
|
1976
|
+
}
|
|
1913
1977
|
return out;
|
|
1914
1978
|
}
|
|
1915
1979
|
function presentationIsEmpty(p) {
|
|
1916
|
-
return (p.style === void 0 || (0, import_data_table2.tableStyleIsEmpty)(p.style)) && Object.keys(p.formats ?? {}).length === 0 && (p.frozen?.length ?? 0) === 0;
|
|
1980
|
+
return (p.style === void 0 || (0, import_data_table2.tableStyleIsEmpty)(p.style)) && Object.keys(p.formats ?? {}).length === 0 && (p.frozen?.length ?? 0) === 0 && p.grouping === void 0 && Object.keys(p.widths ?? {}).length === 0 && p.rowHeight === void 0;
|
|
1917
1981
|
}
|
|
1918
1982
|
function presentationDocument(p) {
|
|
1919
1983
|
return presentationIsEmpty(p) ? "" : JSON.stringify(p);
|
|
@@ -1984,6 +2048,7 @@ function Grid({
|
|
|
1984
2048
|
toolbarActions,
|
|
1985
2049
|
editable = true,
|
|
1986
2050
|
presentation = EMPTY_PRESENTATION,
|
|
2051
|
+
onPresentationChange,
|
|
1987
2052
|
className
|
|
1988
2053
|
}) {
|
|
1989
2054
|
const host = useRecordsUi();
|
|
@@ -2035,6 +2100,67 @@ function Grid({
|
|
|
2035
2100
|
),
|
|
2036
2101
|
[ordered, canWrite, editing, mayWriteRow, whyNotRow, presentation.formats, frozenKeys]
|
|
2037
2102
|
);
|
|
2103
|
+
const columnIds = (0, import_react13.useMemo)(() => ordered.map((field) => field.key), [ordered]);
|
|
2104
|
+
const [localColumns, setLocalColumns] = (0, import_react13.useState)(
|
|
2105
|
+
null
|
|
2106
|
+
);
|
|
2107
|
+
const [localWidths, setLocalWidths] = (0, import_react13.useState)(null);
|
|
2108
|
+
const writeLook = (0, import_react13.useCallback)(
|
|
2109
|
+
(next) => onPresentationChange?.(next),
|
|
2110
|
+
[onPresentationChange]
|
|
2111
|
+
);
|
|
2112
|
+
const widths = localWidths ?? presentation.widths;
|
|
2113
|
+
const columnState = (0, import_react13.useMemo)(
|
|
2114
|
+
() => ({
|
|
2115
|
+
order: localColumns?.order ?? columnIds,
|
|
2116
|
+
hidden: localColumns?.hidden ?? [],
|
|
2117
|
+
onChange: (next) => setLocalColumns(next),
|
|
2118
|
+
...widths ? { widths } : {},
|
|
2119
|
+
// A drag that changed nothing would be the silent half of a control that
|
|
2120
|
+
// looks alive, so the width always lands somewhere — the session at
|
|
2121
|
+
// least, the view record when a door is bound.
|
|
2122
|
+
onWidthsChange: (next) => {
|
|
2123
|
+
setLocalWidths(next);
|
|
2124
|
+
writeLook({ ...presentation, widths: next });
|
|
2125
|
+
}
|
|
2126
|
+
}),
|
|
2127
|
+
[columnIds, localColumns, widths, presentation, writeLook]
|
|
2128
|
+
);
|
|
2129
|
+
const groupingConfig = (0, import_react13.useMemo)(() => {
|
|
2130
|
+
const view = presentation.grouping;
|
|
2131
|
+
const field = view?.field ?? null;
|
|
2132
|
+
if (field === null && !onPresentationChange) return null;
|
|
2133
|
+
const aggregates = view && Object.keys(view.aggregates ?? {}).length > 0 ? view.aggregates : field ? { [field]: { kind: "count" } } : void 0;
|
|
2134
|
+
return {
|
|
2135
|
+
columnId: field,
|
|
2136
|
+
rowNoun: "record",
|
|
2137
|
+
...aggregates ? { aggregates } : {},
|
|
2138
|
+
...view?.order ? { order: view.order } : {},
|
|
2139
|
+
...view?.collapsed ? { collapsed: view.collapsed } : {},
|
|
2140
|
+
...onPresentationChange ? {
|
|
2141
|
+
onColumnIdChange: (columnId) => {
|
|
2142
|
+
if (columnId === null) {
|
|
2143
|
+
const { grouping: _dropped, ...rest } = presentation;
|
|
2144
|
+
writeLook(rest);
|
|
2145
|
+
return;
|
|
2146
|
+
}
|
|
2147
|
+
writeLook({
|
|
2148
|
+
...presentation,
|
|
2149
|
+
grouping: {
|
|
2150
|
+
field: columnId,
|
|
2151
|
+
...presentation.grouping?.aggregates ? { aggregates: presentation.grouping.aggregates } : {},
|
|
2152
|
+
...presentation.grouping?.order ? { order: presentation.grouping.order } : {}
|
|
2153
|
+
}
|
|
2154
|
+
});
|
|
2155
|
+
},
|
|
2156
|
+
onCollapsedChange: (collapsed) => {
|
|
2157
|
+
const current = presentation.grouping;
|
|
2158
|
+
if (!current) return;
|
|
2159
|
+
writeLook({ ...presentation, grouping: { ...current, collapsed } });
|
|
2160
|
+
}
|
|
2161
|
+
} : {}
|
|
2162
|
+
};
|
|
2163
|
+
}, [presentation, onPresentationChange, writeLook]);
|
|
2038
2164
|
if (table.error) return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(RefusalNotice, { error: table.error, className });
|
|
2039
2165
|
if (fields.error) return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(RefusalNotice, { error: fields.error, className });
|
|
2040
2166
|
const nothingToShowYet = table.loading || fields.loading || !rights.known || records.loading && rows.length === 0;
|
|
@@ -2098,6 +2224,15 @@ function Grid({
|
|
|
2098
2224
|
pageSize,
|
|
2099
2225
|
detail: { enabled: false },
|
|
2100
2226
|
window: { enabled: false },
|
|
2227
|
+
...groupingConfig ? { grouping: groupingConfig } : {},
|
|
2228
|
+
columnState,
|
|
2229
|
+
...presentation.rowHeight ? { rowHeight: presentation.rowHeight } : {},
|
|
2230
|
+
virtualize: true,
|
|
2231
|
+
coverage: {
|
|
2232
|
+
answeredBy: "client",
|
|
2233
|
+
noun: "record",
|
|
2234
|
+
...typeof records.data?.total === "number" ? { total: records.data.total } : {}
|
|
2235
|
+
},
|
|
2101
2236
|
...presentation.style ? {
|
|
2102
2237
|
tableStyle: {
|
|
2103
2238
|
style: presentation.style,
|
|
@@ -3990,6 +4125,7 @@ function ViewSwitcher({
|
|
|
3990
4125
|
tableId: view.subject,
|
|
3991
4126
|
pageSize,
|
|
3992
4127
|
...local.presentation ?? view.presentation ? { presentation: local.presentation ?? view.presentation } : {},
|
|
4128
|
+
...onViewChange ? { onPresentationChange: (next) => change({ presentation: next }) } : {},
|
|
3993
4129
|
...onOpenRecord ? { onOpenRecord } : {},
|
|
3994
4130
|
...onNewRecordForm ? { onNewRecordForm } : {}
|
|
3995
4131
|
}
|
|
@@ -6833,10 +6969,13 @@ function NotifyRuleEditor({ tableId, seed, className }) {
|
|
|
6833
6969
|
const [busy, setBusy] = (0, import_react64.useState)(false);
|
|
6834
6970
|
const load = (0, import_react64.useCallback)(async () => {
|
|
6835
6971
|
const [held, offered] = await Promise.all([
|
|
6836
|
-
|
|
6972
|
+
// THE PERSON'S OWN DOOR, not the notifier's. It answers what is addressed
|
|
6973
|
+
// to them plus — only where they hold admin on this Table — anyone's over
|
|
6974
|
+
// it, and it is narrowed to Tables they can already open.
|
|
6975
|
+
client.subscriptions({ table_id: tableId }),
|
|
6837
6976
|
// The cadences are ASKED of the store, never a list typed into a picker
|
|
6838
6977
|
// here that could drift from what the digest runner understands.
|
|
6839
|
-
client.
|
|
6978
|
+
client.subscriptionCadences()
|
|
6840
6979
|
]);
|
|
6841
6980
|
if (!held.ok) {
|
|
6842
6981
|
setError(held.error);
|
|
@@ -6847,7 +6986,7 @@ function NotifyRuleEditor({ tableId, seed, className }) {
|
|
|
6847
6986
|
if (offered.ok) setCadences(offered.data);
|
|
6848
6987
|
if (host.savedViews) setViews(await host.savedViews());
|
|
6849
6988
|
else setViews(null);
|
|
6850
|
-
}, [client, host]);
|
|
6989
|
+
}, [client, host, tableId]);
|
|
6851
6990
|
(0, import_react64.useEffect)(() => {
|
|
6852
6991
|
void load();
|
|
6853
6992
|
}, [load]);
|
|
@@ -6937,6 +7076,7 @@ function NotifyRuleEditor({ tableId, seed, className }) {
|
|
|
6937
7076
|
held.cadence === "digest" && held.schedule ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "text-muted-foreground", children: held.schedule }) : null,
|
|
6938
7077
|
held.recipient_user_id === null ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "text-destructive", children: "This one has nobody to tell, so it fires at nobody." }) : null,
|
|
6939
7078
|
held.saved_view_id === null ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "text-destructive", children: "This one names no view, so the store never admits a record to it." }) : null,
|
|
7079
|
+
held.muted ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "text-muted-foreground", children: "Switched off \u2014 it tells nobody until somebody switches it back on." }) : null,
|
|
6940
7080
|
rights.write ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
|
|
6941
7081
|
import_design_system30.Button,
|
|
6942
7082
|
{
|