nexus-shared 3.2.0 → 4.0.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 +1564 -0
- package/README.md +1705 -1123
- package/dist/Client.Index.d.ts +14 -0
- package/dist/Client.Index.js +14 -0
- package/dist/Components/Documents/DocumentFrame.d.ts +50 -0
- package/dist/Components/Documents/DocumentFrame.js +83 -0
- package/dist/Components/Documents/Menu.d.ts +4 -0
- package/dist/Components/Documents/Menu.js +40 -24
- package/dist/Components/Documents/PrintDocument.d.ts +95 -0
- package/dist/Components/Documents/PrintDocument.js +62 -0
- package/dist/Components/Documents/Tabs.d.ts +59 -0
- package/dist/Components/Documents/Tabs.js +70 -0
- package/dist/Components/Forms/ApiForm.js +19 -2
- package/dist/Components/Forms/Crud.d.ts +2 -2
- package/dist/Components/Forms/Crud.js +576 -243
- package/dist/Components/Forms/CrudActions.d.ts +136 -0
- package/dist/Components/Forms/CrudActions.js +184 -0
- package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
- package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
- package/dist/Components/Forms/CrudParts.d.ts +30 -2
- package/dist/Components/Forms/CrudParts.js +55 -5
- package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
- package/dist/Components/Forms/CrudRowForms.js +114 -0
- package/dist/Components/Forms/CrudScopes.d.ts +28 -0
- package/dist/Components/Forms/CrudScopes.js +76 -0
- package/dist/Components/Forms/CrudTree.d.ts +90 -0
- package/dist/Components/Forms/CrudTree.js +363 -0
- package/dist/Components/Forms/CrudWorkspace.d.ts +63 -0
- package/dist/Components/Forms/CrudWorkspace.js +169 -0
- package/dist/Components/Forms/ExcelImport.js +3 -3
- package/dist/Components/Forms/Form.d.ts +4 -4
- package/dist/Components/Forms/Form.js +123 -17
- package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
- package/dist/Components/Forms/FormLeaveGuard.js +65 -0
- package/dist/Components/Forms/RecordFlows.d.ts +50 -0
- package/dist/Components/Forms/RecordFlows.js +282 -0
- package/dist/Components/Forms/SubmitForm.d.ts +37 -2
- package/dist/Components/Forms/SubmitForm.js +195 -10
- package/dist/Components/Inputs/CheckBox.js +1 -1
- package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
- package/dist/Components/Inputs/ChoiceField.js +28 -2
- package/dist/Components/Inputs/Dropdown.d.ts +13 -0
- package/dist/Components/Inputs/Dropdown.js +5 -5
- package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
- package/dist/Components/Inputs/DropdownParts.js +16 -4
- package/dist/Components/Inputs/Field.d.ts +7 -1
- package/dist/Components/Inputs/Field.js +2 -2
- package/dist/Components/Inputs/GroupForm.d.ts +6 -6
- package/dist/Components/Inputs/GroupForm.js +10 -11
- package/dist/Components/Inputs/InputRenderer.d.ts +9 -6
- package/dist/Components/Inputs/InputRenderer.js +37 -32
- package/dist/Components/Inputs/PictureInput.d.ts +41 -0
- package/dist/Components/Inputs/PictureInput.js +70 -0
- package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
- package/dist/Components/Inputs/RadioGroup.js +8 -3
- package/dist/Components/Inputs/RowsInput.d.ts +1 -1
- package/dist/Components/Inputs/RowsInput.js +18 -3
- package/dist/Components/Inputs/TabularForm.d.ts +5 -5
- package/dist/Components/Inputs/TabularForm.js +29 -29
- package/dist/Components/Inputs/TextArea.d.ts +6 -0
- package/dist/Components/Inputs/TextArea.js +11 -2
- package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
- package/dist/Components/Inputs/TextBoxTypes.js +17 -0
- package/dist/Components/Inputs/TreeView.d.ts +53 -2
- package/dist/Components/Inputs/TreeView.js +147 -13
- package/dist/Components/Layouts/MessageBox.d.ts +15 -2
- package/dist/Components/Layouts/MessageBox.js +56 -7
- package/dist/Components/Layouts/Popup.d.ts +13 -6
- package/dist/Components/Layouts/Popup.js +37 -9
- package/dist/Components/Layouts/PopupDock.d.ts +3 -1
- package/dist/Components/Layouts/PopupDock.js +2 -1
- package/dist/Components/Layouts/PopupHost.js +5 -4
- package/dist/Components/Viewers/DataTable.js +150 -48
- package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
- package/dist/Components/Viewers/DataTableFilters.js +17 -2
- package/dist/Components/Viewers/DataTableParts.d.ts +21 -5
- package/dist/Components/Viewers/DataTableParts.js +26 -14
- package/dist/Components/Viewers/OwnerFiles.d.ts +175 -0
- package/dist/Components/Viewers/OwnerFiles.js +589 -0
- package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
- package/dist/Components/Viewers/QrCodeImage.js +15 -0
- package/dist/Components/Viewers/RecordCards.d.ts +16 -0
- package/dist/Components/Viewers/RecordCards.js +509 -0
- package/dist/Helpers/BusyHooks.d.ts +6 -0
- package/dist/Helpers/BusyHooks.js +39 -0
- package/dist/Helpers/CrudBackend.d.ts +20 -2
- package/dist/Helpers/CrudBackend.js +46 -2
- package/dist/Helpers/CrudColumns.d.ts +53 -6
- package/dist/Helpers/CrudColumns.js +98 -7
- package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
- package/dist/Helpers/CrudDetailsTabs.js +126 -0
- package/dist/Helpers/CrudHelpers.d.ts +81 -2
- package/dist/Helpers/CrudHelpers.js +195 -0
- package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
- package/dist/Helpers/CrudNarrowPick.js +157 -0
- package/dist/Helpers/CrudRowForms.d.ts +15 -0
- package/dist/Helpers/CrudRowForms.js +43 -0
- package/dist/Helpers/CrudScopes.d.ts +32 -0
- package/dist/Helpers/CrudScopes.js +79 -0
- package/dist/Helpers/DropdownHelpers.d.ts +3 -1
- package/dist/Helpers/DropdownHelpers.js +6 -2
- package/dist/Helpers/ExcelHelpers.d.ts +1 -1
- package/dist/Helpers/ExcelHelpers.js +12 -11
- package/dist/Helpers/FileAdapter.d.ts +78 -0
- package/dist/Helpers/FileAdapter.js +102 -0
- package/dist/Helpers/FilePrimaries.d.ts +48 -0
- package/dist/Helpers/FilePrimaries.js +138 -0
- package/dist/Helpers/FormConfirm.d.ts +32 -0
- package/dist/Helpers/FormConfirm.js +40 -0
- package/dist/Helpers/FormStore.d.ts +90 -5
- package/dist/Helpers/FormStore.js +318 -22
- package/dist/Helpers/InputConditions.d.ts +50 -0
- package/dist/Helpers/InputConditions.js +168 -0
- package/dist/Helpers/InputMemory.d.ts +106 -0
- package/dist/Helpers/InputMemory.js +385 -0
- package/dist/Helpers/InputNarrowing.d.ts +18 -0
- package/dist/Helpers/InputNarrowing.js +67 -0
- package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
- package/dist/Helpers/InputParamsHelpers.js +157 -74
- package/dist/Helpers/NexusDriveAdapter.d.ts +55 -0
- package/dist/Helpers/NexusDriveAdapter.js +207 -0
- package/dist/Helpers/NumberHelpers.d.ts +95 -0
- package/dist/Helpers/NumberHelpers.js +126 -0
- package/dist/Helpers/Permissions.js +2 -1
- package/dist/Helpers/QrCode.d.ts +26 -0
- package/dist/Helpers/QrCode.js +463 -0
- package/dist/Helpers/RecordCards.d.ts +205 -0
- package/dist/Helpers/RecordCards.js +378 -0
- package/dist/Helpers/RequestCache.d.ts +14 -2
- package/dist/Helpers/RequestCache.js +22 -3
- package/dist/Helpers/RowsStore.d.ts +37 -2
- package/dist/Helpers/RowsStore.js +128 -10
- package/dist/Helpers/ServerPreview.d.ts +69 -0
- package/dist/Helpers/ServerPreview.js +99 -0
- package/dist/Helpers/TableColumns.js +4 -3
- package/dist/Helpers/TableExport.d.ts +7 -1
- package/dist/Helpers/TableExport.js +9 -3
- package/dist/Helpers/TableFilters.d.ts +1 -1
- package/dist/Helpers/TableFilters.js +24 -23
- package/dist/Helpers/TableHelpers.d.ts +23 -2
- package/dist/Helpers/TableHelpers.js +51 -10
- package/dist/Helpers/TableJoins.d.ts +13 -0
- package/dist/Helpers/TableJoins.js +34 -0
- package/dist/Helpers/TableStore.d.ts +21 -1
- package/dist/Helpers/TableStore.js +64 -7
- package/dist/Helpers/TreeStore.d.ts +81 -0
- package/dist/Helpers/TreeStore.js +365 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +465 -5
- package/dist/Interfaces/FileInterfaces.d.ts +162 -0
- package/dist/Interfaces/FileInterfaces.js +4 -0
- package/dist/Interfaces/FormInterfaces.d.ts +391 -30
- package/dist/Interfaces/FormInterfaces.js +79 -1
- package/dist/Interfaces/InputInterfaces.d.ts +15 -3
- package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
- package/dist/Interfaces/RecordInterfaces.js +1 -0
- package/dist/Interfaces/TableInterfaces.d.ts +74 -1
- package/dist/Services/ExcelApi.js +5 -0
- package/dist/Services/FileApi.d.ts +13 -0
- package/dist/Services/FileApi.js +29 -0
- package/dist/Services/ImageEditorLoader.d.ts +14 -0
- package/dist/Services/ImageEditorLoader.js +51 -0
- package/dist/Services/JoinSources.d.ts +37 -5
- package/dist/Services/JoinSources.js +90 -14
- package/dist/Services/ModuleImport.d.ts +7 -0
- package/dist/Services/ModuleImport.js +22 -0
- package/dist/Shared.Index.d.ts +16 -0
- package/dist/Shared.Index.js +16 -0
- package/package.json +7 -2
- package/src/Styles/Nexus.Avatar.css +6 -5
- package/src/Styles/Nexus.Button.css +10 -4
- package/src/Styles/Nexus.Choice.css +16 -0
- package/src/Styles/Nexus.Crud.css +383 -9
- package/src/Styles/Nexus.Dropdown.css +8 -0
- package/src/Styles/Nexus.Files.css +410 -0
- package/src/Styles/Nexus.Form.css +5 -2
- package/src/Styles/Nexus.Index.css +4 -0
- package/src/Styles/Nexus.Input.css +42 -0
- package/src/Styles/Nexus.Menu.css +10 -1
- package/src/Styles/Nexus.Popup.css +77 -1
- package/src/Styles/Nexus.Print.css +288 -0
- package/src/Styles/Nexus.QrCode.css +12 -0
- package/src/Styles/Nexus.Record.css +430 -0
- package/src/Styles/Nexus.Tab.Buttons.css +87 -22
- package/src/Styles/Nexus.Table.css +67 -5
- package/src/Styles/Nexus.Themes.css +12 -6
- package/src/Styles/Nexus.Tokens.css +8 -3
- package/src/Styles/Nexus.Tree.css +31 -1
|
@@ -72,9 +72,10 @@ function toNumber(value) {
|
|
|
72
72
|
}
|
|
73
73
|
/**
|
|
74
74
|
* A value as the cell writes it, in the column's format: "1,250.00", "Rs. 4,500.00", "12.5%", the date and time in the
|
|
75
|
-
* user's settings, "Yes" or "No", an option's label. Empty values give "".
|
|
75
|
+
* user's settings, "Yes" or "No", an option's label. Empty values give "". `row` is the record, for a currency the
|
|
76
|
+
* column reads from it (`currency: row => row.currencyText`).
|
|
76
77
|
*/
|
|
77
|
-
export function formatCellText(column, value, options = {}) {
|
|
78
|
+
export function formatCellText(column, value, options = {}, row) {
|
|
78
79
|
const option = columnOption(column, value);
|
|
79
80
|
if (option)
|
|
80
81
|
return option.label;
|
|
@@ -90,7 +91,8 @@ export function formatCellText(column, value, options = {}) {
|
|
|
90
91
|
return String(value);
|
|
91
92
|
const decimals = column.decimals ?? (column.format === "money" ? 2 : undefined);
|
|
92
93
|
const text = formatNumber(number, { decimals, fixedDecimals: column.format === "money" || column.decimals !== undefined, grouping: true, locale: options.locale });
|
|
93
|
-
|
|
94
|
+
const currency = column.format === "money" ? columnCurrency(column, options, row) : "";
|
|
95
|
+
return `${currency ? currency + " " : ""}${prefix}${text}${column.format === "percent" && !suffix ? "%" : suffix}`;
|
|
94
96
|
}
|
|
95
97
|
case "date":
|
|
96
98
|
return formatDate(value, options.preferences) || String(value);
|
|
@@ -104,6 +106,11 @@ export function formatCellText(column, value, options = {}) {
|
|
|
104
106
|
return `${prefix}${typeof value === "object" ? JSON.stringify(value) : String(value)}${suffix}`;
|
|
105
107
|
}
|
|
106
108
|
}
|
|
109
|
+
/** A money column's currency: its own (a text, or read from the row), else the table's; "" for none. */
|
|
110
|
+
export function columnCurrency(column, options = {}, row) {
|
|
111
|
+
const own = typeof column.currency === "function" ? (row ? column.currency(row) : undefined) : column.currency;
|
|
112
|
+
return (own ?? options.currency ?? "").trim();
|
|
113
|
+
}
|
|
107
114
|
/** Text the search looks in: the column's `text`, an option's label, or the value as text (dates as ISO text). */
|
|
108
115
|
export function searchTextOf(column, row) {
|
|
109
116
|
if (column.text)
|
|
@@ -256,18 +263,52 @@ export function moveColumnKey(keys, key, beforeKey) {
|
|
|
256
263
|
rest.splice(at, 0, key);
|
|
257
264
|
return rest;
|
|
258
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* The most of a table's width on screen that the columns staying in place may take: the numbers, the checkboxes and the
|
|
268
|
+
* actions, which always stay, and the pinned columns. Past it the pinned ones give way (`releasedPins`), so a phone
|
|
269
|
+
* keeps at least 40% of the table for the columns that scroll.
|
|
270
|
+
*/
|
|
271
|
+
export const PINNED_SHARE = 0.6;
|
|
272
|
+
/**
|
|
273
|
+
* Which pinned columns scroll for now, the table being narrow: the pinned ones stay from the outer edge in - the left
|
|
274
|
+
* side first - while they and the columns that always stay (`frame`: the numbers and checkboxes, the actions; their
|
|
275
|
+
* width in pixels) take at most `share` of `view`, the width on screen; every one past that scrolls with the rest,
|
|
276
|
+
* and so do the ones inside it on the same side. `left` and `right`: each side's pinned columns with their widths,
|
|
277
|
+
* from the outer edge in. A table not laid out (`view` 0, in a closed tab) releases none.
|
|
278
|
+
*/
|
|
279
|
+
export function releasedPins(view, frame, left, right, share = PINNED_SHARE) {
|
|
280
|
+
const released = new Set();
|
|
281
|
+
if (!(view > 0))
|
|
282
|
+
return released;
|
|
283
|
+
const room = view * share;
|
|
284
|
+
let used = frame;
|
|
285
|
+
for (const side of [left, right]) {
|
|
286
|
+
let open = true;
|
|
287
|
+
for (const [key, width] of side) {
|
|
288
|
+
if (open && used + width <= room + 0.5)
|
|
289
|
+
used += width;
|
|
290
|
+
else {
|
|
291
|
+
open = false;
|
|
292
|
+
released.add(key);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return released;
|
|
297
|
+
}
|
|
259
298
|
/**
|
|
260
299
|
* The columns as the table draws them: in the user's order, with the pinned ones moved to their side (keeping their
|
|
261
300
|
* order among themselves), each with its width, its distance from that edge, and what the user may do with it.
|
|
262
301
|
*/
|
|
263
302
|
export function viewColumns(columns, options = {}) {
|
|
264
|
-
const { order, pinned = {}, widths = {}, offsets = {}, reorderable = true, resizable = true } = options;
|
|
303
|
+
const { order, pinned = {}, widths = {}, offsets = {}, released, reorderable = true, resizable = true } = options;
|
|
265
304
|
const groups = { left: [], middle: [], right: [] };
|
|
266
305
|
for (const column of orderColumns(columns, order)) {
|
|
267
|
-
const
|
|
268
|
-
groups[
|
|
306
|
+
const side = pinned[column.key] ?? column.pinned ?? null;
|
|
307
|
+
groups[side === "left" ? "left" : side === "right" ? "right" : "middle"].push({
|
|
269
308
|
column,
|
|
270
|
-
|
|
309
|
+
// A pinned column the table is too narrow for keeps its side and scrolls with the rest.
|
|
310
|
+
pin: side && !released?.has(column.key) ? side : null,
|
|
311
|
+
side,
|
|
271
312
|
offset: offsets[column.key] ?? 0,
|
|
272
313
|
edge: false,
|
|
273
314
|
resizable: resizable && column.resizable !== false,
|
|
@@ -275,11 +316,11 @@ export function viewColumns(columns, options = {}) {
|
|
|
275
316
|
width: widths[column.key],
|
|
276
317
|
});
|
|
277
318
|
}
|
|
278
|
-
// The shadow that says columns scroll under a pinned side belongs to its last column.
|
|
279
|
-
const last = groups.left
|
|
319
|
+
// The shadow that says columns scroll under a pinned side belongs to its last column that stays.
|
|
320
|
+
const last = groups.left.filter(view => view.pin).at(-1);
|
|
280
321
|
if (last)
|
|
281
322
|
last.edge = true;
|
|
282
|
-
const first = groups.right
|
|
323
|
+
const first = groups.right.find(view => view.pin);
|
|
283
324
|
if (first)
|
|
284
325
|
first.edge = true;
|
|
285
326
|
return [...groups.left, ...groups.middle, ...groups.right];
|
|
@@ -29,6 +29,12 @@ export declare class TableJoins {
|
|
|
29
29
|
* in - or that it is missing, on its way, or could not be read. A key nobody has asked for yet reads as on its way.
|
|
30
30
|
*/
|
|
31
31
|
peek(name: string, key: string): JoinedState;
|
|
32
|
+
/**
|
|
33
|
+
* The record kept for `key` while its answer is fresh - within `lifetime`, and not marked old since (`expire`: a save
|
|
34
|
+
* there, a Reload) - at once and with no call; `undefined` otherwise. What names a value from a form's first draw
|
|
35
|
+
* without standing in for a record that may have changed since.
|
|
36
|
+
*/
|
|
37
|
+
peekFresh(name: string, key: string, lifetime?: number): TableJoinedRecord | undefined;
|
|
32
38
|
/**
|
|
33
39
|
* Asks for every key the cache cannot answer fresh. The keys asked for in one moment - by every table and input on
|
|
34
40
|
* the page - go in one call per source, after the moment is over; a key already on its way is not asked again.
|
|
@@ -36,6 +42,11 @@ export declare class TableJoins {
|
|
|
36
42
|
request(source: TableJoinSource, keys: Iterable<string>): void;
|
|
37
43
|
private queue;
|
|
38
44
|
private send;
|
|
45
|
+
/**
|
|
46
|
+
* Records the page already holds, kept as if a lookup of `source` had just answered them: a form opened on a record
|
|
47
|
+
* whose parent the page shows - a tree's row - names that parent with no call.
|
|
48
|
+
*/
|
|
49
|
+
remember(source: TableJoinSource, records: readonly TableJoinedRecord[]): void;
|
|
39
50
|
/**
|
|
40
51
|
* The records behind these keys, once they are in: what a picker shows for the values a form already holds. The
|
|
41
52
|
* calls and the answers are the ones the tables share, so a form opened from a table costs nothing more.
|
|
@@ -45,6 +56,8 @@ export declare class TableJoins {
|
|
|
45
56
|
* A whole list kept under `name` - a picker's options - loaded once per lifetime; a second ask while it loads waits for
|
|
46
57
|
* the same call. A list that failed is not kept, so the next ask tries again.
|
|
47
58
|
*/
|
|
59
|
+
/** The list kept under `name` while it is fresh, without loading it; `undefined` when there is none yet, or it aged. */
|
|
60
|
+
peekList(name: string, lifetime?: number): readonly TableJoinedRecord[] | undefined;
|
|
48
61
|
list(name: string, load: (signal: AbortSignal) => Promise<readonly TableJoinedRecord[]>, lifetime?: number): Promise<readonly TableJoinedRecord[]>;
|
|
49
62
|
/**
|
|
50
63
|
* Marks answers as old - a source's, some of its keys, or every one - after the records behind them may have changed:
|
|
@@ -65,6 +65,15 @@ export class TableJoins {
|
|
|
65
65
|
return FAILED;
|
|
66
66
|
return LOADING;
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* The record kept for `key` while its answer is fresh - within `lifetime`, and not marked old since (`expire`: a save
|
|
70
|
+
* there, a Reload) - at once and with no call; `undefined` otherwise. What names a value from a form's first draw
|
|
71
|
+
* without standing in for a record that may have changed since.
|
|
72
|
+
*/
|
|
73
|
+
peekFresh(name, key, lifetime = JOIN_LIFETIME) {
|
|
74
|
+
const entry = this.sources.get(name)?.entries.get(key);
|
|
75
|
+
return entry?.record && Date.now() - entry.at < lifetime ? entry.record : undefined;
|
|
76
|
+
}
|
|
68
77
|
/**
|
|
69
78
|
* Asks for every key the cache cannot answer fresh. The keys asked for in one moment - by every table and input on
|
|
70
79
|
* the page - go in one call per source, after the moment is over; a key already on its way is not asked again.
|
|
@@ -136,6 +145,26 @@ export class TableJoins {
|
|
|
136
145
|
this.notify();
|
|
137
146
|
}
|
|
138
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Records the page already holds, kept as if a lookup of `source` had just answered them: a form opened on a record
|
|
150
|
+
* whose parent the page shows - a tree's row - names that parent with no call.
|
|
151
|
+
*/
|
|
152
|
+
remember(source, records) {
|
|
153
|
+
const cache = this.cacheOf(source.name);
|
|
154
|
+
const keyField = source.key ?? "id";
|
|
155
|
+
const at = Date.now();
|
|
156
|
+
let changed = false;
|
|
157
|
+
for (const record of records) {
|
|
158
|
+
const key = record[keyField];
|
|
159
|
+
if (key === undefined || key === null || key === "")
|
|
160
|
+
continue;
|
|
161
|
+
cache.entries.set(String(key), { record, at });
|
|
162
|
+
cache.failedAt.delete(String(key));
|
|
163
|
+
changed = true;
|
|
164
|
+
}
|
|
165
|
+
if (changed)
|
|
166
|
+
this.notify();
|
|
167
|
+
}
|
|
139
168
|
/**
|
|
140
169
|
* The records behind these keys, once they are in: what a picker shows for the values a form already holds. The
|
|
141
170
|
* calls and the answers are the ones the tables share, so a form opened from a table costs nothing more.
|
|
@@ -157,6 +186,11 @@ export class TableJoins {
|
|
|
157
186
|
* A whole list kept under `name` - a picker's options - loaded once per lifetime; a second ask while it loads waits for
|
|
158
187
|
* the same call. A list that failed is not kept, so the next ask tries again.
|
|
159
188
|
*/
|
|
189
|
+
/** The list kept under `name` while it is fresh, without loading it; `undefined` when there is none yet, or it aged. */
|
|
190
|
+
peekList(name, lifetime = JOIN_LIFETIME) {
|
|
191
|
+
const kept = this.lists.get(name);
|
|
192
|
+
return kept?.rows && Date.now() - kept.at < lifetime ? kept.rows : undefined;
|
|
193
|
+
}
|
|
160
194
|
list(name, load, lifetime = JOIN_LIFETIME) {
|
|
161
195
|
const kept = this.lists.get(name);
|
|
162
196
|
if (kept?.pending)
|
|
@@ -10,6 +10,8 @@ export interface TableStoreOptions<TRow extends TableRow = TableRow> {
|
|
|
10
10
|
paging: TablePaging;
|
|
11
11
|
maxSelection?: number;
|
|
12
12
|
onSelectionChange?: (rows: TRow[]) => void;
|
|
13
|
+
/** After the picked row changes: another row, its newest copy, or none (`DataTable` `onPick`). */
|
|
14
|
+
onPickChange?: (row: TRow | null) => void;
|
|
13
15
|
/** The advanced filter's inputs, as the table prepared them (`asFilterInput`). */
|
|
14
16
|
filterInputs?: readonly InputParams[];
|
|
15
17
|
}
|
|
@@ -63,7 +65,10 @@ export declare class TableStore<TRow extends TableRow = TableRow> {
|
|
|
63
65
|
/** Takes the component's props. Tells nobody: the component calls it while rendering. */
|
|
64
66
|
configure(options: TableStoreOptions<TRow>): void;
|
|
65
67
|
private set;
|
|
66
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Puts back what was saved on this device (page size, sort, columns): before the first load, telling nobody, or, with
|
|
70
|
+
* `notify`, over a table already shown (another key's settings), its rows shown again in them on the first page.
|
|
71
|
+
*/
|
|
67
72
|
restore(saved: {
|
|
68
73
|
pageSize?: number;
|
|
69
74
|
sort?: TableSort | null;
|
|
@@ -71,6 +76,8 @@ export declare class TableStore<TRow extends TableRow = TableRow> {
|
|
|
71
76
|
order?: readonly string[];
|
|
72
77
|
widths?: Readonly<Record<string, number>>;
|
|
73
78
|
pinned?: Readonly<Record<string, TablePinSide>>;
|
|
79
|
+
}, options?: {
|
|
80
|
+
notify?: boolean;
|
|
74
81
|
}): void;
|
|
75
82
|
/** Shows these rows: every row there is. The table searches, sorts, and pages them. */
|
|
76
83
|
setRows(rows: readonly TRow[]): void;
|
|
@@ -126,6 +133,19 @@ export declare class TableStore<TRow extends TableRow = TableRow> {
|
|
|
126
133
|
private refreshSelected;
|
|
127
134
|
/** A client table forgets selected rows that are gone from its rows. */
|
|
128
135
|
private dropMissingSelection;
|
|
136
|
+
/**
|
|
137
|
+
* Picks a row (`DataTable` `onPick`), or none: the one row whose own records the page shows beside the table. It
|
|
138
|
+
* follows the record from then on - its newest copy after an upsert, none once it is removed or gone after a load -
|
|
139
|
+
* and every change is told to `onPickChange`.
|
|
140
|
+
*/
|
|
141
|
+
pick(row: TRow | null): void;
|
|
142
|
+
/**
|
|
143
|
+
* The picked row as these rows hold it: its newest copy, or none when they are every row there is (`whole`, a client
|
|
144
|
+
* table) and it is not among them. A server page without it keeps it, since it may be on another page.
|
|
145
|
+
*/
|
|
146
|
+
private followPicked;
|
|
147
|
+
/** Tells the page when a change left another copy of the picked row, or none. */
|
|
148
|
+
private pickedChanged;
|
|
129
149
|
setBusy(keys: readonly TableKey[], on: boolean): void;
|
|
130
150
|
flash(keys: readonly TableKey[]): void;
|
|
131
151
|
setHidden(key: string, hidden: boolean): void;
|
|
@@ -19,6 +19,7 @@ function initialState(pageSize) {
|
|
|
19
19
|
matches: 0,
|
|
20
20
|
query: { search: "", sort: null, page: 1, pageSize },
|
|
21
21
|
selected: EMPTY_MAP,
|
|
22
|
+
picked: null,
|
|
22
23
|
busy: EMPTY_SET,
|
|
23
24
|
flashed: EMPTY_SET,
|
|
24
25
|
hidden: EMPTY_SET,
|
|
@@ -103,13 +104,18 @@ export class TableStore {
|
|
|
103
104
|
if (changes.selected && changes.selected !== previous.selected)
|
|
104
105
|
this.options.onSelectionChange?.(this.getSelected());
|
|
105
106
|
}
|
|
106
|
-
/**
|
|
107
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Puts back what was saved on this device (page size, sort, columns): before the first load, telling nobody, or, with
|
|
109
|
+
* `notify`, over a table already shown (another key's settings), its rows shown again in them on the first page.
|
|
110
|
+
*/
|
|
111
|
+
restore(saved, options = {}) {
|
|
108
112
|
const query = { ...this.state.query };
|
|
109
113
|
if (saved.pageSize && saved.pageSize > 0)
|
|
110
114
|
query.pageSize = saved.pageSize;
|
|
111
115
|
if (saved.sort !== undefined)
|
|
112
116
|
query.sort = saved.sort;
|
|
117
|
+
if (options.notify)
|
|
118
|
+
query.page = 1;
|
|
113
119
|
this.state = {
|
|
114
120
|
...this.state,
|
|
115
121
|
query,
|
|
@@ -118,6 +124,8 @@ export class TableStore {
|
|
|
118
124
|
widths: saved.widths ?? this.state.widths,
|
|
119
125
|
pinned: saved.pinned ?? this.state.pinned,
|
|
120
126
|
};
|
|
127
|
+
if (options.notify)
|
|
128
|
+
this.view();
|
|
121
129
|
}
|
|
122
130
|
/* Rows given by the page */
|
|
123
131
|
/** Shows these rows: every row there is. The table searches, sorts, and pages them. */
|
|
@@ -127,7 +135,9 @@ export class TableStore {
|
|
|
127
135
|
this.given = rows;
|
|
128
136
|
this.all = [...rows];
|
|
129
137
|
this.dropMissingSelection();
|
|
130
|
-
|
|
138
|
+
const picked = this.state.picked;
|
|
139
|
+
this.view({ status: "ready", error: null, refreshing: false, picked: this.followPicked(this.all, true) });
|
|
140
|
+
this.pickedChanged(picked);
|
|
131
141
|
}
|
|
132
142
|
/* Loading */
|
|
133
143
|
/**
|
|
@@ -151,6 +161,9 @@ export class TableStore {
|
|
|
151
161
|
this.state = { ...this.state, query: { ...this.state.query, search: "", page: 1 } };
|
|
152
162
|
if (this.state.selected.size)
|
|
153
163
|
this.set({ selected: EMPTY_MAP });
|
|
164
|
+
// Another list: the row picked in the one before is not in it.
|
|
165
|
+
if (this.state.picked)
|
|
166
|
+
this.pick(null);
|
|
154
167
|
}
|
|
155
168
|
const keepRows = quiet ?? (this.state.status === "ready" && !reset);
|
|
156
169
|
const query = this.state.query;
|
|
@@ -197,6 +210,7 @@ export class TableStore {
|
|
|
197
210
|
void this.reload({ quiet: true });
|
|
198
211
|
return;
|
|
199
212
|
}
|
|
213
|
+
const picked = this.state.picked;
|
|
200
214
|
this.set({
|
|
201
215
|
status: "ready",
|
|
202
216
|
refreshing: false,
|
|
@@ -207,13 +221,17 @@ export class TableStore {
|
|
|
207
221
|
offset: (query.page - 1) * query.pageSize,
|
|
208
222
|
query,
|
|
209
223
|
selected: this.refreshSelected(page.rows),
|
|
224
|
+
picked: this.followPicked(page.rows, false),
|
|
210
225
|
});
|
|
226
|
+
this.pickedChanged(picked);
|
|
211
227
|
return;
|
|
212
228
|
}
|
|
213
229
|
this.all = page.rows;
|
|
214
230
|
this.given = null;
|
|
215
231
|
this.dropMissingSelection();
|
|
216
|
-
|
|
232
|
+
const picked = this.state.picked;
|
|
233
|
+
this.view({ status: "ready", refreshing: false, error: null, picked: this.followPicked(this.all, true) });
|
|
234
|
+
this.pickedChanged(picked);
|
|
217
235
|
}
|
|
218
236
|
fail(error, seq) {
|
|
219
237
|
if (seq !== this.seq)
|
|
@@ -301,6 +319,9 @@ export class TableStore {
|
|
|
301
319
|
*/
|
|
302
320
|
upsert(row, { at = "start", flash = true } = {}) {
|
|
303
321
|
const key = this.keyOf(row);
|
|
322
|
+
// The picked record's new copy is the one picked now.
|
|
323
|
+
const before = this.state.picked;
|
|
324
|
+
const picked = before && key && this.keyOf(before) === key ? row : before;
|
|
304
325
|
if (this.options.paging === "server") {
|
|
305
326
|
const index = this.state.rows.findIndex(candidate => this.keyOf(candidate) === key);
|
|
306
327
|
const rows = [...this.state.rows];
|
|
@@ -315,7 +336,7 @@ export class TableStore {
|
|
|
315
336
|
total++;
|
|
316
337
|
matches++;
|
|
317
338
|
}
|
|
318
|
-
this.set({ rows, total, matches, selected: this.replaceSelected(key, row), status: "ready" });
|
|
339
|
+
this.set({ rows, total, matches, selected: this.replaceSelected(key, row), picked, status: "ready" });
|
|
319
340
|
}
|
|
320
341
|
else {
|
|
321
342
|
const index = this.all.findIndex(candidate => this.keyOf(candidate) === key);
|
|
@@ -326,8 +347,9 @@ export class TableStore {
|
|
|
326
347
|
else
|
|
327
348
|
this.all.push(row);
|
|
328
349
|
this.all = [...this.all];
|
|
329
|
-
this.view({ selected: this.replaceSelected(key, row), status: "ready" });
|
|
350
|
+
this.view({ selected: this.replaceSelected(key, row), picked, status: "ready" });
|
|
330
351
|
}
|
|
352
|
+
this.pickedChanged(before);
|
|
331
353
|
if (flash && key)
|
|
332
354
|
this.flash([key]);
|
|
333
355
|
}
|
|
@@ -355,6 +377,9 @@ export class TableStore {
|
|
|
355
377
|
for (const key of gone)
|
|
356
378
|
selected.delete(key);
|
|
357
379
|
const busy = new Set([...this.state.busy].filter(key => !gone.has(key)));
|
|
380
|
+
// A picked record that left the list is no longer picked.
|
|
381
|
+
const before = this.state.picked;
|
|
382
|
+
const picked = before && gone.has(this.keyOf(before)) ? null : before;
|
|
358
383
|
if (this.options.paging === "server") {
|
|
359
384
|
const rows = this.state.rows.filter(row => !gone.has(this.keyOf(row)));
|
|
360
385
|
const removed = this.state.rows.length - rows.length;
|
|
@@ -363,15 +388,18 @@ export class TableStore {
|
|
|
363
388
|
total: Math.max(0, this.state.total - removed),
|
|
364
389
|
matches: Math.max(0, this.state.matches - removed),
|
|
365
390
|
selected: selected.size === this.state.selected.size ? this.state.selected : selected,
|
|
391
|
+
picked,
|
|
366
392
|
busy,
|
|
367
393
|
});
|
|
394
|
+
this.pickedChanged(before);
|
|
368
395
|
// A short wait lets several removals (a mass action's rows) share one load.
|
|
369
396
|
clearTimeout(this.reloadTimer);
|
|
370
397
|
this.reloadTimer = setTimeout(() => void this.reload({ quiet: true }), 60);
|
|
371
398
|
return;
|
|
372
399
|
}
|
|
373
400
|
this.all = this.all.filter(row => !gone.has(this.keyOf(row)));
|
|
374
|
-
this.view({ selected: selected.size === this.state.selected.size ? this.state.selected : selected, busy });
|
|
401
|
+
this.view({ selected: selected.size === this.state.selected.size ? this.state.selected : selected, picked, busy });
|
|
402
|
+
this.pickedChanged(before);
|
|
375
403
|
}
|
|
376
404
|
/* Selection */
|
|
377
405
|
/** Selects or unselects rows, up to `maxSelection`. Returns how many rows it could not select. */
|
|
@@ -440,6 +468,35 @@ export class TableStore {
|
|
|
440
468
|
this.state = { ...this.state, selected: selected.size ? selected : EMPTY_MAP };
|
|
441
469
|
this.options.onSelectionChange?.(this.getSelected());
|
|
442
470
|
}
|
|
471
|
+
/* The picked row */
|
|
472
|
+
/**
|
|
473
|
+
* Picks a row (`DataTable` `onPick`), or none: the one row whose own records the page shows beside the table. It
|
|
474
|
+
* follows the record from then on - its newest copy after an upsert, none once it is removed or gone after a load -
|
|
475
|
+
* and every change is told to `onPickChange`.
|
|
476
|
+
*/
|
|
477
|
+
pick(row) {
|
|
478
|
+
const next = row !== null && this.keyOf(row) !== "" ? row : null;
|
|
479
|
+
if (next === this.state.picked)
|
|
480
|
+
return;
|
|
481
|
+
this.set({ picked: next });
|
|
482
|
+
this.options.onPickChange?.(next);
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* The picked row as these rows hold it: its newest copy, or none when they are every row there is (`whole`, a client
|
|
486
|
+
* table) and it is not among them. A server page without it keeps it, since it may be on another page.
|
|
487
|
+
*/
|
|
488
|
+
followPicked(rows, whole) {
|
|
489
|
+
const picked = this.state.picked;
|
|
490
|
+
if (!picked)
|
|
491
|
+
return null;
|
|
492
|
+
const key = this.keyOf(picked);
|
|
493
|
+
return rows.find(row => this.keyOf(row) === key) ?? (whole ? null : picked);
|
|
494
|
+
}
|
|
495
|
+
/** Tells the page when a change left another copy of the picked row, or none. */
|
|
496
|
+
pickedChanged(before) {
|
|
497
|
+
if (this.state.picked !== before)
|
|
498
|
+
this.options.onPickChange?.(this.state.picked);
|
|
499
|
+
}
|
|
443
500
|
/* Rows with an action running, and rows to highlight */
|
|
444
501
|
setBusy(keys, on) {
|
|
445
502
|
const busy = new Set(this.state.busy);
|
|
@@ -78,6 +78,40 @@ export interface TreeChildrenPage<TItem extends TreeItem = TreeItem> {
|
|
|
78
78
|
/** Whether another page follows. Default: `total` says so, or the page was full. */
|
|
79
79
|
hasMore?: boolean;
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* A change to one item of a tree's source made somewhere else - a form saved, a record moved to the trash - written into
|
|
83
|
+
* the pages kept for the tree (`writeTreeChanges`) and into every tree on screen that shows them, so nothing is read
|
|
84
|
+
* from the server again for it.
|
|
85
|
+
*/
|
|
86
|
+
export type TreeItemChange<TItem extends TreeItem = TreeItem> = {
|
|
87
|
+
/** Added, changed, or moved: the item as it is now, under the item whose value is `parent` (`null`: the top level). */
|
|
88
|
+
type: "put";
|
|
89
|
+
item: TItem;
|
|
90
|
+
parent: TreeValue | null;
|
|
91
|
+
/**
|
|
92
|
+
* Where it was: its parent's value (`null`: the top level), or `false` when it was not in the tree at all - just
|
|
93
|
+
* added, or back from the trash. Left out when not known: a tree that holds the item knows, and the kept pages are
|
|
94
|
+
* searched; an item found nowhere then changes no count.
|
|
95
|
+
*/
|
|
96
|
+
before?: TreeValue | null | false;
|
|
97
|
+
} | {
|
|
98
|
+
/** Out of the tree - moved to the trash, deleted: the item's value, and its parent's when known. */
|
|
99
|
+
type: "remove";
|
|
100
|
+
value: TreeValue;
|
|
101
|
+
parent?: TreeValue | null;
|
|
102
|
+
};
|
|
103
|
+
/** How a tree reads its items, for changes written from outside it: the same keys the tree is given. */
|
|
104
|
+
export interface TreeChangeRules<TItem extends TreeItem = TreeItem> {
|
|
105
|
+
/** The item field that holds its value: the tree's `valueKey`. Default `id`. */
|
|
106
|
+
valueKey?: string;
|
|
107
|
+
/** The tree's `hasChildrenKey`: a parent's count there goes up and down with its children, and its chevron with it. */
|
|
108
|
+
hasChildrenKey?: string;
|
|
109
|
+
/**
|
|
110
|
+
* The order of siblings, as the server sorts them, so an item added or changed goes where a fresh read would put it.
|
|
111
|
+
* Without it an added item goes after the children loaded, and a changed one stays where it was.
|
|
112
|
+
*/
|
|
113
|
+
compare?: (a: TItem, b: TItem) => number;
|
|
114
|
+
}
|
|
81
115
|
/** Children's pages kept for every lazy tree on the page that names a `cacheKey`, for 5 minutes: see `RequestCache`. */
|
|
82
116
|
export declare const treePages: RequestCache<TreeChildrenPage<object>>;
|
|
83
117
|
/** The start of every key a tree keeps its pages under, or one parent's pages (`null`: the top level). */
|
|
@@ -90,6 +124,24 @@ export declare function treePageKey(cacheKey: string, parent: TreeValue | null,
|
|
|
90
124
|
* its `refresh()` loads that again.
|
|
91
125
|
*/
|
|
92
126
|
export declare function expireTreePages(cacheKey: string, parent?: TreeValue | null | readonly (TreeValue | null)[]): number;
|
|
127
|
+
/**
|
|
128
|
+
* Writes changes made elsewhere into the pages kept for a tree's source, and into every tree on screen that keeps its
|
|
129
|
+
* pages under `cacheKey` - so a record saved in a form, moved, or sent to the trash shows in the tree at once, and
|
|
130
|
+
* leaving the page and coming back reads nothing again for it. Items go where the parent's children are known (see
|
|
131
|
+
* `TreeStore.placeItem`); a parent whose children were never loaded only counts one more or one fewer, and its first page
|
|
132
|
+
* comes from the server when it is opened, as it would have anyway.
|
|
133
|
+
*/
|
|
134
|
+
export declare function writeTreeChanges<TItem extends TreeItem>(cacheKey: string, changes: readonly TreeItemChange<TItem>[], rules?: TreeChangeRules<TItem>): void;
|
|
135
|
+
type TreeChangeListener = (changes: readonly TreeItemChange[], rules: TreeChangeRules) => void;
|
|
136
|
+
/** Hears the changes written for `cacheKey`: what a tree on screen does to write them into itself. Returns the way out. */
|
|
137
|
+
export declare function subscribeTreeChanges(cacheKey: string, listener: TreeChangeListener): () => void;
|
|
138
|
+
/**
|
|
139
|
+
* The kept pages under `cacheKey` with the changes written in (what `writeTreeChanges` does to `treePages`). A parent's
|
|
140
|
+
* pages are read as one list from its first child, the changes applied to it, and the list written back in pages of the
|
|
141
|
+
* same size, each with the parent's total - so a tree reading them again, page by page, asks the server for exactly the
|
|
142
|
+
* children it does not have. A page still on its way for a changed parent is dropped: its answer may predate the change.
|
|
143
|
+
*/
|
|
144
|
+
export declare function patchTreePages<TItem extends TreeItem>(cache: RequestCache<TreeChildrenPage>, cacheKey: string, changes: readonly TreeItemChange<TItem>[], rules?: TreeChangeRules<TItem>): void;
|
|
93
145
|
/** A has-children field read as the tree reads it: a boolean, or a count above 0; `undefined` when it says nothing. */
|
|
94
146
|
export declare function treeHasChildren(value: unknown): boolean | undefined;
|
|
95
147
|
/** The key of the status row under a parent (`null`: the top level), as `TreeNode.key` holds it. */
|
|
@@ -342,6 +394,34 @@ export declare class TreeStore<TItem extends TreeItem = TreeItem> {
|
|
|
342
394
|
}): void;
|
|
343
395
|
/** Removes a node and everything under it. Returns the removed nodes in tree order. */
|
|
344
396
|
remove(node: TreeNode<TItem>): TreeNode<TItem>[];
|
|
397
|
+
/**
|
|
398
|
+
* Writes an item where a change made elsewhere left it (see `TreeItemChange`): in place under the same parent (moved
|
|
399
|
+
* among its siblings by `compare`), under its new parent when it moved - with what is loaded under it - and in for the
|
|
400
|
+
* first time when it is new or back. Only where the parent's children are known: given, loaded, or known to be none.
|
|
401
|
+
* Under a parent never opened it waits for that parent's first page, and one that sorts after the loaded children while
|
|
402
|
+
* more are to come waits for its page. A lazy parent's `loaded` and `total` follow, so its next page skips the right
|
|
403
|
+
* number, and so does the count in `hasChildrenKey` on the parent's own item, which its chevron goes by. Returns the
|
|
404
|
+
* item's node where it is now, or `undefined` when this tree does not show it.
|
|
405
|
+
*/
|
|
406
|
+
placeItem(item: TItem, parentValue: TreeValue | null, options?: {
|
|
407
|
+
before?: TreeValue | null | false;
|
|
408
|
+
compare?: (a: TItem, b: TItem) => number;
|
|
409
|
+
}): TreeNode<TItem> | undefined;
|
|
410
|
+
/**
|
|
411
|
+
* Takes out an item a change made elsewhere took out of the tree - moved to the trash, deleted - with everything under
|
|
412
|
+
* it. Its parent counts one fewer (its pages' `loaded` and `total`, the count on its item) and loses its chevron when
|
|
413
|
+
* none are left. An item not loaded here still lowers its parent's counts when `parentValue` names the parent.
|
|
414
|
+
* Returns the nodes taken out.
|
|
415
|
+
*/
|
|
416
|
+
removeItem(value: TreeValue, parentValue?: TreeValue | null): TreeNode<TItem>[];
|
|
417
|
+
/** Whether a parent's children are known here: given in the items, loaded (all or the first pages), or known to be none. */
|
|
418
|
+
private listsChildren;
|
|
419
|
+
/**
|
|
420
|
+
* One child more or fewer under a parent (`null`: the top level): its pages' total, and the count its own item
|
|
421
|
+
* carries in `hasChildrenKey` - a new copy of the item. A parent never opened reads its children from the new count,
|
|
422
|
+
* so one that has none left loses its chevron; one whose children were loaded knows already.
|
|
423
|
+
*/
|
|
424
|
+
private countChild;
|
|
345
425
|
private createNode;
|
|
346
426
|
/** A lazy node's pages before anything is asked: none to ask for when its item says it has no children. */
|
|
347
427
|
private newPages;
|
|
@@ -400,3 +480,4 @@ export declare class TreeStore<TItem extends TreeItem = TreeItem> {
|
|
|
400
480
|
}
|
|
401
481
|
/** Nodes in tree order: each node, then its children. No recursion, so even a chain 100,000 levels deep works. */
|
|
402
482
|
export declare function preOrder<TItem extends TreeItem>(nodes: readonly TreeNode<TItem>[]): TreeNode<TItem>[];
|
|
483
|
+
export {};
|