nexus-shared 3.1.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 +2268 -446
- package/LICENSE +21 -21
- package/README.md +1705 -1084
- package/dist/Client.Index.d.ts +17 -0
- package/dist/Client.Index.js +17 -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 +871 -377
- package/dist/Components/Forms/CrudActions.d.ts +136 -0
- package/dist/Components/Forms/CrudActions.js +184 -0
- package/dist/Components/Forms/CrudHistory.d.ts +66 -0
- package/dist/Components/Forms/CrudHistory.js +140 -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 +56 -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 +125 -16
- 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/CodeBox.d.ts +69 -0
- package/dist/Components/Inputs/CodeBox.js +316 -0
- package/dist/Components/Inputs/DatePicker.js +4 -1
- package/dist/Components/Inputs/DateRangePicker.js +4 -1
- package/dist/Components/Inputs/DateTimePicker.js +4 -1
- package/dist/Components/Inputs/Dropdown.d.ts +13 -0
- package/dist/Components/Inputs/Dropdown.js +8 -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 +39 -31
- package/dist/Components/Inputs/NumberBox.js +4 -2
- 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 +14 -2
- package/dist/Components/Inputs/TextBox.js +4 -1
- package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
- package/dist/Components/Inputs/TextBoxTypes.js +17 -0
- package/dist/Components/Inputs/TimePicker.js +4 -1
- package/dist/Components/Inputs/TreeView.d.ts +107 -6
- package/dist/Components/Inputs/TreeView.js +513 -30
- 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/Layouts/ThemePicker.d.ts +3 -2
- package/dist/Components/Layouts/ThemePicker.js +16 -4
- package/dist/Components/Viewers/DataTable.js +205 -61
- package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
- package/dist/Components/Viewers/DataTableFilters.js +17 -2
- package/dist/Components/Viewers/DataTableParts.d.ts +46 -12
- package/dist/Components/Viewers/DataTableParts.js +62 -21
- 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/ApiModules.d.ts +3 -34
- package/dist/Helpers/ApiModules.js +3 -3
- package/dist/Helpers/ApiRoutes.d.ts +68 -26
- package/dist/Helpers/ApiRoutes.js +31 -27
- package/dist/Helpers/ArrayHelpers.d.ts +0 -0
- package/dist/Helpers/ArrayHelpers.js +2 -0
- package/dist/Helpers/BusyHooks.d.ts +6 -0
- package/dist/Helpers/BusyHooks.js +39 -0
- package/dist/Helpers/CodeHelpers.d.ts +97 -0
- package/dist/Helpers/CodeHelpers.js +332 -0
- package/dist/Helpers/CrudBackend.d.ts +114 -6
- package/dist/Helpers/CrudBackend.js +214 -11
- package/dist/Helpers/CrudColumns.d.ts +98 -0
- package/dist/Helpers/CrudColumns.js +160 -0
- package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
- package/dist/Helpers/CrudDetailsTabs.js +126 -0
- package/dist/Helpers/CrudHelpers.d.ts +130 -6
- package/dist/Helpers/CrudHelpers.js +304 -12
- 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 +162 -71
- package/dist/Helpers/MessageBuilder.js +2 -0
- package/dist/Helpers/NexusDriveAdapter.d.ts +55 -0
- package/dist/Helpers/NexusDriveAdapter.js +207 -0
- package/dist/Helpers/NexusHelpers.d.ts +0 -0
- package/dist/Helpers/NexusHelpers.js +1 -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 +47 -0
- package/dist/Helpers/RequestCache.js +132 -0
- 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 +21 -15
- package/dist/Helpers/TableFilters.d.ts +1 -1
- package/dist/Helpers/TableFilters.js +24 -23
- package/dist/Helpers/TableHelpers.d.ts +24 -3
- package/dist/Helpers/TableHelpers.js +58 -12
- package/dist/Helpers/TableJoins.d.ts +83 -0
- package/dist/Helpers/TableJoins.js +284 -0
- package/dist/Helpers/TableStore.d.ts +26 -1
- package/dist/Helpers/TableStore.js +73 -7
- package/dist/Helpers/TreeStore.d.ts +306 -11
- package/dist/Helpers/TreeStore.js +1048 -90
- package/dist/Helpers/UserPreferences.d.ts +8 -0
- package/dist/Helpers/UserPreferences.js +17 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +512 -14
- package/dist/Interfaces/FileInterfaces.d.ts +162 -0
- package/dist/Interfaces/FileInterfaces.js +4 -0
- package/dist/Interfaces/FormInterfaces.d.ts +393 -29
- package/dist/Interfaces/FormInterfaces.js +79 -1
- package/dist/Interfaces/InputInterfaces.d.ts +39 -3
- package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
- package/dist/Interfaces/PreferenceInterfaces.d.ts +8 -0
- package/dist/Interfaces/PreferenceInterfaces.js +2 -5
- package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
- package/dist/Interfaces/RecordInterfaces.js +1 -0
- package/dist/Interfaces/TableInterfaces.d.ts +160 -5
- package/dist/Services/ApiProxy.d.ts +36 -1
- package/dist/Services/ApiProxy.js +182 -52
- package/dist/Services/ApiProxyRules.d.ts +203 -0
- package/dist/Services/ApiProxyRules.js +426 -0
- 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 +80 -0
- package/dist/Services/JoinSources.js +150 -0
- package/dist/Services/ModuleImport.d.ts +7 -0
- package/dist/Services/ModuleImport.js +22 -0
- package/dist/Services/ServerApi.d.ts +13 -2
- package/dist/Services/ServerApi.js +8 -3
- package/dist/Shared.Index.d.ts +20 -0
- package/dist/Shared.Index.js +20 -0
- package/package.json +92 -85
- package/src/Styles/Nexus.Avatar.css +80 -79
- package/src/Styles/Nexus.Base.css +117 -117
- package/src/Styles/Nexus.Button.css +186 -180
- package/src/Styles/Nexus.Calendar.css +273 -273
- package/src/Styles/Nexus.Chat.css +1443 -1443
- package/src/Styles/Nexus.Choice.css +359 -343
- package/src/Styles/Nexus.Crud.css +542 -168
- package/src/Styles/Nexus.DateTime.css +81 -81
- package/src/Styles/Nexus.Dropdown.css +733 -725
- package/src/Styles/Nexus.Editor.css +295 -295
- package/src/Styles/Nexus.Excel.css +389 -389
- package/src/Styles/Nexus.Files.css +410 -0
- package/src/Styles/Nexus.Form.css +282 -279
- package/src/Styles/Nexus.Index.css +41 -37
- package/src/Styles/Nexus.Input.css +238 -4
- package/src/Styles/Nexus.Markdown.css +237 -237
- package/src/Styles/Nexus.Menu.css +246 -237
- package/src/Styles/Nexus.Popup.css +800 -724
- 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.Rows.css +637 -637
- package/src/Styles/Nexus.Slider.css +276 -276
- package/src/Styles/Nexus.Tab.Buttons.css +497 -432
- package/src/Styles/Nexus.Table.css +98 -35
- package/src/Styles/Nexus.Theme.Picker.css +59 -3
- package/src/Styles/Nexus.Themes.css +200 -194
- package/src/Styles/Nexus.Time.css +144 -144
- package/src/Styles/Nexus.Toast.css +263 -263
- package/src/Styles/Nexus.Tokens.css +94 -89
- package/src/Styles/Nexus.Tree.css +530 -431
|
@@ -11,7 +11,7 @@ export declare const DEFAULT_MAX_SELECTION = 500;
|
|
|
11
11
|
export declare function readPath(row: unknown, path: string): unknown;
|
|
12
12
|
/** A row's key as text, so 5 and "5" are the same row. */
|
|
13
13
|
export declare function rowKeyText<TRow extends TableRow>(row: TRow, rowKey: string | ((row: TRow) => TableKey)): string;
|
|
14
|
-
/** A column's value in a row. */
|
|
14
|
+
/** A column's value in a row: its own `value`, the record a `join` looked up, or the field. */
|
|
15
15
|
export declare function columnValue<TRow extends TableRow>(column: TableColumn<TRow>, row: TRow): unknown;
|
|
16
16
|
/** The option a value stands for, if the column has options. Values compare as text, so 2 matches "2". */
|
|
17
17
|
export declare function columnOption(column: TableColumn<never> | TableColumn, value: unknown): TableColumnOption | undefined;
|
|
@@ -26,12 +26,17 @@ export interface CellTextOptions {
|
|
|
26
26
|
timeZone?: string;
|
|
27
27
|
/** How numbers are written: a locale, or the person's settings (`numberLocale`, `useNumberLocale`). Default `en-US`. */
|
|
28
28
|
locale?: string;
|
|
29
|
+
/** The currency of a `money` column without its own (`DataTableProps.currency`): "Rs.". */
|
|
30
|
+
currency?: string | null;
|
|
29
31
|
}
|
|
30
32
|
/**
|
|
31
33
|
* 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
|
|
32
|
-
* user's settings, "Yes" or "No", an option's label. Empty values give "".
|
|
34
|
+
* user's settings, "Yes" or "No", an option's label. Empty values give "". `row` is the record, for a currency the
|
|
35
|
+
* column reads from it (`currency: row => row.currencyText`).
|
|
33
36
|
*/
|
|
34
|
-
export declare function formatCellText<TRow extends TableRow>(column: TableColumn<TRow>, value: unknown, options?: CellTextOptions): string;
|
|
37
|
+
export declare function formatCellText<TRow extends TableRow>(column: TableColumn<TRow>, value: unknown, options?: CellTextOptions, row?: TRow): string;
|
|
38
|
+
/** A money column's currency: its own (a text, or read from the row), else the table's; "" for none. */
|
|
39
|
+
export declare function columnCurrency<TRow extends TableRow>(column: TableColumn<TRow>, options?: CellTextOptions, row?: TRow): string;
|
|
35
40
|
/** Text the search looks in: the column's `text`, an option's label, or the value as text (dates as ISO text). */
|
|
36
41
|
export declare function searchTextOf<TRow extends TableRow>(column: TableColumn<TRow>, row: TRow): string;
|
|
37
42
|
/** Compares two values of a column for sorting: numbers as numbers, text as a person reads it ("Room 9" before "Room 10"). */
|
|
@@ -95,7 +100,23 @@ export interface ViewColumnsOptions extends ColumnRights {
|
|
|
95
100
|
widths?: Readonly<Record<string, number>>;
|
|
96
101
|
/** Pixels from the pinned side, measured from the columns before it. */
|
|
97
102
|
offsets?: Readonly<Record<string, number>>;
|
|
103
|
+
/** Pinned columns that scroll for now, the table being too narrow for them (`releasedPins`): their `pin` is null. */
|
|
104
|
+
released?: ReadonlySet<string>;
|
|
98
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* The most of a table's width on screen that the columns staying in place may take: the numbers, the checkboxes and the
|
|
108
|
+
* actions, which always stay, and the pinned columns. Past it the pinned ones give way (`releasedPins`), so a phone
|
|
109
|
+
* keeps at least 40% of the table for the columns that scroll.
|
|
110
|
+
*/
|
|
111
|
+
export declare const PINNED_SHARE = 0.6;
|
|
112
|
+
/**
|
|
113
|
+
* Which pinned columns scroll for now, the table being narrow: the pinned ones stay from the outer edge in - the left
|
|
114
|
+
* side first - while they and the columns that always stay (`frame`: the numbers and checkboxes, the actions; their
|
|
115
|
+
* width in pixels) take at most `share` of `view`, the width on screen; every one past that scrolls with the rest,
|
|
116
|
+
* and so do the ones inside it on the same side. `left` and `right`: each side's pinned columns with their widths,
|
|
117
|
+
* from the outer edge in. A table not laid out (`view` 0, in a closed tab) releases none.
|
|
118
|
+
*/
|
|
119
|
+
export declare function releasedPins(view: number, frame: number, left: readonly (readonly [string, number])[], right: readonly (readonly [string, number])[], share?: number): Set<string>;
|
|
99
120
|
/**
|
|
100
121
|
* The columns as the table draws them: in the user's order, with the pinned ones moved to their side (keeping their
|
|
101
122
|
* order among themselves), each with its width, its distance from that edge, and what the user may do with it.
|
|
@@ -3,6 +3,7 @@ import { formatDateTime } from "./DateTimeHelpers.js";
|
|
|
3
3
|
import { foldSearchText, searchWords } from "./DropdownHelpers.js";
|
|
4
4
|
import { formatNumber } from "./NumberHelpers.js";
|
|
5
5
|
import { filterMatcher } from "./TableFilters.js";
|
|
6
|
+
import { joinedValue } from "./TableJoins.js";
|
|
6
7
|
import { formatTime } from "./TimeHelpers.js";
|
|
7
8
|
/*
|
|
8
9
|
* Data table helpers, with no React: reading a column's value, its text, searching, sorting, and paging rows, and the
|
|
@@ -33,9 +34,13 @@ export function rowKeyText(row, rowKey) {
|
|
|
33
34
|
const key = typeof rowKey === "function" ? rowKey(row) : readPath(row, rowKey);
|
|
34
35
|
return key === undefined || key === null ? "" : String(key);
|
|
35
36
|
}
|
|
36
|
-
/** A column's value in a row. */
|
|
37
|
+
/** A column's value in a row: its own `value`, the record a `join` looked up, or the field. */
|
|
37
38
|
export function columnValue(column, row) {
|
|
38
|
-
|
|
39
|
+
if (column.value)
|
|
40
|
+
return column.value(row);
|
|
41
|
+
if (column.join)
|
|
42
|
+
return joinedValue(column.join, column.key, row);
|
|
43
|
+
return readPath(row, column.key);
|
|
39
44
|
}
|
|
40
45
|
/** The option a value stands for, if the column has options. Values compare as text, so 2 matches "2". */
|
|
41
46
|
export function columnOption(column, value) {
|
|
@@ -67,9 +72,10 @@ function toNumber(value) {
|
|
|
67
72
|
}
|
|
68
73
|
/**
|
|
69
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
|
|
70
|
-
* 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`).
|
|
71
77
|
*/
|
|
72
|
-
export function formatCellText(column, value, options = {}) {
|
|
78
|
+
export function formatCellText(column, value, options = {}, row) {
|
|
73
79
|
const option = columnOption(column, value);
|
|
74
80
|
if (option)
|
|
75
81
|
return option.label;
|
|
@@ -85,7 +91,8 @@ export function formatCellText(column, value, options = {}) {
|
|
|
85
91
|
return String(value);
|
|
86
92
|
const decimals = column.decimals ?? (column.format === "money" ? 2 : undefined);
|
|
87
93
|
const text = formatNumber(number, { decimals, fixedDecimals: column.format === "money" || column.decimals !== undefined, grouping: true, locale: options.locale });
|
|
88
|
-
|
|
94
|
+
const currency = column.format === "money" ? columnCurrency(column, options, row) : "";
|
|
95
|
+
return `${currency ? currency + " " : ""}${prefix}${text}${column.format === "percent" && !suffix ? "%" : suffix}`;
|
|
89
96
|
}
|
|
90
97
|
case "date":
|
|
91
98
|
return formatDate(value, options.preferences) || String(value);
|
|
@@ -99,6 +106,11 @@ export function formatCellText(column, value, options = {}) {
|
|
|
99
106
|
return `${prefix}${typeof value === "object" ? JSON.stringify(value) : String(value)}${suffix}`;
|
|
100
107
|
}
|
|
101
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
|
+
}
|
|
102
114
|
/** Text the search looks in: the column's `text`, an option's label, or the value as text (dates as ISO text). */
|
|
103
115
|
export function searchTextOf(column, row) {
|
|
104
116
|
if (column.text)
|
|
@@ -251,18 +263,52 @@ export function moveColumnKey(keys, key, beforeKey) {
|
|
|
251
263
|
rest.splice(at, 0, key);
|
|
252
264
|
return rest;
|
|
253
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
|
+
}
|
|
254
298
|
/**
|
|
255
299
|
* The columns as the table draws them: in the user's order, with the pinned ones moved to their side (keeping their
|
|
256
300
|
* order among themselves), each with its width, its distance from that edge, and what the user may do with it.
|
|
257
301
|
*/
|
|
258
302
|
export function viewColumns(columns, options = {}) {
|
|
259
|
-
const { order, pinned = {}, widths = {}, offsets = {}, reorderable = true, resizable = true } = options;
|
|
303
|
+
const { order, pinned = {}, widths = {}, offsets = {}, released, reorderable = true, resizable = true } = options;
|
|
260
304
|
const groups = { left: [], middle: [], right: [] };
|
|
261
305
|
for (const column of orderColumns(columns, order)) {
|
|
262
|
-
const
|
|
263
|
-
groups[
|
|
306
|
+
const side = pinned[column.key] ?? column.pinned ?? null;
|
|
307
|
+
groups[side === "left" ? "left" : side === "right" ? "right" : "middle"].push({
|
|
264
308
|
column,
|
|
265
|
-
|
|
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,
|
|
266
312
|
offset: offsets[column.key] ?? 0,
|
|
267
313
|
edge: false,
|
|
268
314
|
resizable: resizable && column.resizable !== false,
|
|
@@ -270,11 +316,11 @@ export function viewColumns(columns, options = {}) {
|
|
|
270
316
|
width: widths[column.key],
|
|
271
317
|
});
|
|
272
318
|
}
|
|
273
|
-
// The shadow that says columns scroll under a pinned side belongs to its last column.
|
|
274
|
-
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);
|
|
275
321
|
if (last)
|
|
276
322
|
last.edge = true;
|
|
277
|
-
const first = groups.right
|
|
323
|
+
const first = groups.right.find(view => view.pin);
|
|
278
324
|
if (first)
|
|
279
325
|
first.edge = true;
|
|
280
326
|
return [...groups.left, ...groups.middle, ...groups.right];
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ApiEndpoint } from "../Interfaces/ApiInterfaces.ts";
|
|
2
|
+
import type { TableJoin, TableJoinSource, TableJoinedRecord, TableRow } from "../Interfaces/TableInterfaces.ts";
|
|
3
|
+
/** How long an answer is kept before the next request asks again: the CRUD's own cache lifetime. */
|
|
4
|
+
export declare const JOIN_LIFETIME: number;
|
|
5
|
+
/** What is known of one key: its record, that its source has none, that it is on its way, or that the call failed. */
|
|
6
|
+
export type JoinedState = {
|
|
7
|
+
status: "ready";
|
|
8
|
+
record: TableJoinedRecord;
|
|
9
|
+
} | {
|
|
10
|
+
status: "missing";
|
|
11
|
+
} | {
|
|
12
|
+
status: "loading";
|
|
13
|
+
} | {
|
|
14
|
+
status: "failed";
|
|
15
|
+
};
|
|
16
|
+
export declare class TableJoins {
|
|
17
|
+
private sources;
|
|
18
|
+
private lists;
|
|
19
|
+
private listeners;
|
|
20
|
+
private version;
|
|
21
|
+
/** Called after every answer, and after answers are marked old. For `useSyncExternalStore`. */
|
|
22
|
+
subscribe: (listener: () => void) => (() => void);
|
|
23
|
+
/** A number that changes with every answer, so a table knows to draw again. */
|
|
24
|
+
getVersion: () => number;
|
|
25
|
+
private notify;
|
|
26
|
+
private cacheOf;
|
|
27
|
+
/**
|
|
28
|
+
* What is known of `key` in the source called `name`, for drawing: its record - an aged one too, until the new one is
|
|
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
|
+
*/
|
|
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;
|
|
38
|
+
/**
|
|
39
|
+
* Asks for every key the cache cannot answer fresh. The keys asked for in one moment - by every table and input on
|
|
40
|
+
* the page - go in one call per source, after the moment is over; a key already on its way is not asked again.
|
|
41
|
+
*/
|
|
42
|
+
request(source: TableJoinSource, keys: Iterable<string>): void;
|
|
43
|
+
private queue;
|
|
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;
|
|
50
|
+
/**
|
|
51
|
+
* The records behind these keys, once they are in: what a picker shows for the values a form already holds. The
|
|
52
|
+
* calls and the answers are the ones the tables share, so a form opened from a table costs nothing more.
|
|
53
|
+
*/
|
|
54
|
+
records(source: TableJoinSource, keys: Iterable<string>): Promise<TableJoinedRecord[]>;
|
|
55
|
+
/**
|
|
56
|
+
* A whole list kept under `name` - a picker's options - loaded once per lifetime; a second ask while it loads waits for
|
|
57
|
+
* the same call. A list that failed is not kept, so the next ask tries again.
|
|
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;
|
|
61
|
+
list(name: string, load: (signal: AbortSignal) => Promise<readonly TableJoinedRecord[]>, lifetime?: number): Promise<readonly TableJoinedRecord[]>;
|
|
62
|
+
/**
|
|
63
|
+
* Marks answers as old - a source's, some of its keys, or every one - after the records behind them may have changed:
|
|
64
|
+
* a save, a move to the trash, a Reload. They are still drawn, and asked for again at the next request.
|
|
65
|
+
*/
|
|
66
|
+
expire(name?: string, keys?: Iterable<string>): void;
|
|
67
|
+
/** Drops a kept list (or every one), so the next ask loads it again. */
|
|
68
|
+
expireList(name?: string): void;
|
|
69
|
+
/** Forgets everything. For tests. */
|
|
70
|
+
clear(): void;
|
|
71
|
+
}
|
|
72
|
+
/** The page's one cache of looked-up records: every table and form shares it. */
|
|
73
|
+
export declare const tableJoins: TableJoins;
|
|
74
|
+
/** The name a source is kept under: its own, or an endpoint's module, method and path. */
|
|
75
|
+
export declare function joinSourceName(source: ApiEndpoint | TableJoinSource): string;
|
|
76
|
+
/** The key a row holds for a join, as text; null when it holds none. */
|
|
77
|
+
export declare function joinKey(join: TableJoin, row: TableRow): string | null;
|
|
78
|
+
/** What is known of the record a row's join names; null when the row names none. */
|
|
79
|
+
export declare function joinedState(join: TableJoin, row: TableRow): JoinedState | null;
|
|
80
|
+
/** A join column's value: the looked-up record's field, or undefined until it is in. */
|
|
81
|
+
export declare function joinedValue(join: TableJoin, columnKey: string, row: TableRow): unknown;
|
|
82
|
+
/** The keys rows hold for every join among these columns, by source: what one request per source asks for. */
|
|
83
|
+
export declare function joinKeys(joins: readonly TableJoin[], rows: readonly TableRow[]): Map<ApiEndpoint | TableJoinSource, Set<string>>;
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { isApiEndpoint } from "./ApiRoutes.js";
|
|
2
|
+
// A cycle, and a harmless one: TableHelpers reads a join column through this module, and only calls are made across it.
|
|
3
|
+
import { readPath } from "./TableHelpers.js";
|
|
4
|
+
/*
|
|
5
|
+
* The records a page looks up by key, kept once for every table and form on it: a country's sub-region, named from the
|
|
6
|
+
* sub-regions' FindListing. It is the client side of a join - the database answers the row with the key alone, and the
|
|
7
|
+
* page names it - and these are its rules (the owner's caching rules, Project-Skills/README.RULES.md):
|
|
8
|
+
*
|
|
9
|
+
* - one call per source for every key wanted in the same moment, however many tables and inputs ask;
|
|
10
|
+
* - a call already out is waited for, never made twice;
|
|
11
|
+
* - an answer is kept for a lifetime, and one that has aged is still drawn while the new one is asked for, so a
|
|
12
|
+
* refresh never blanks a cell;
|
|
13
|
+
* - a key the source does not answer is remembered as missing ("Not found"), and a call that failed is not made
|
|
14
|
+
* again for a short while, so a backend that is down is not asked once per render.
|
|
15
|
+
*
|
|
16
|
+
* Nothing here knows about React or the API: a source loads, and whoever draws subscribes. Kept in memory, for the life
|
|
17
|
+
* of the page.
|
|
18
|
+
*/
|
|
19
|
+
/** How long an answer is kept before the next request asks again: the CRUD's own cache lifetime. */
|
|
20
|
+
export const JOIN_LIFETIME = 5 * 60 * 1000;
|
|
21
|
+
/** How long a key whose call failed waits before it is asked again. */
|
|
22
|
+
const RETRY_AFTER = 30 * 1000;
|
|
23
|
+
/** The most keys one call carries; more are sent in turn. A Nexus mass action takes 500, and so does a FindListing. */
|
|
24
|
+
const KEYS_PER_CALL = 500;
|
|
25
|
+
const LOADING = { status: "loading" };
|
|
26
|
+
const MISSING = { status: "missing" };
|
|
27
|
+
const FAILED = { status: "failed" };
|
|
28
|
+
export class TableJoins {
|
|
29
|
+
sources = new Map();
|
|
30
|
+
lists = new Map();
|
|
31
|
+
listeners = new Set();
|
|
32
|
+
version = 0;
|
|
33
|
+
/** Called after every answer, and after answers are marked old. For `useSyncExternalStore`. */
|
|
34
|
+
subscribe = (listener) => {
|
|
35
|
+
this.listeners.add(listener);
|
|
36
|
+
return () => {
|
|
37
|
+
this.listeners.delete(listener);
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
/** A number that changes with every answer, so a table knows to draw again. */
|
|
41
|
+
getVersion = () => this.version;
|
|
42
|
+
notify() {
|
|
43
|
+
this.version++;
|
|
44
|
+
for (const listener of this.listeners)
|
|
45
|
+
listener();
|
|
46
|
+
}
|
|
47
|
+
cacheOf(name) {
|
|
48
|
+
let cache = this.sources.get(name);
|
|
49
|
+
if (!cache) {
|
|
50
|
+
cache = { entries: new Map(), pending: new Map(), failedAt: new Map(), queued: null };
|
|
51
|
+
this.sources.set(name, cache);
|
|
52
|
+
}
|
|
53
|
+
return cache;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* What is known of `key` in the source called `name`, for drawing: its record - an aged one too, until the new one is
|
|
57
|
+
* 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.
|
|
58
|
+
*/
|
|
59
|
+
peek(name, key) {
|
|
60
|
+
const cache = this.sources.get(name);
|
|
61
|
+
const entry = cache?.entries.get(key);
|
|
62
|
+
if (entry)
|
|
63
|
+
return entry.record ? { status: "ready", record: entry.record } : MISSING;
|
|
64
|
+
if (cache?.failedAt.has(key) && !cache.pending.has(key))
|
|
65
|
+
return FAILED;
|
|
66
|
+
return LOADING;
|
|
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
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Asks for every key the cache cannot answer fresh. The keys asked for in one moment - by every table and input on
|
|
79
|
+
* the page - go in one call per source, after the moment is over; a key already on its way is not asked again.
|
|
80
|
+
*/
|
|
81
|
+
request(source, keys) {
|
|
82
|
+
const cache = this.cacheOf(source.name);
|
|
83
|
+
const lifetime = source.lifetime ?? JOIN_LIFETIME;
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
for (const key of keys) {
|
|
86
|
+
if (!key || cache.pending.has(key))
|
|
87
|
+
continue;
|
|
88
|
+
const entry = cache.entries.get(key);
|
|
89
|
+
if (entry && now - entry.at < lifetime)
|
|
90
|
+
continue;
|
|
91
|
+
const failed = cache.failedAt.get(key);
|
|
92
|
+
if (!entry && failed !== undefined && now - failed < RETRY_AFTER)
|
|
93
|
+
continue;
|
|
94
|
+
this.queue(source, cache, key);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
queue(source, cache, key) {
|
|
98
|
+
let batch = cache.queued;
|
|
99
|
+
if (!batch) {
|
|
100
|
+
let finish = () => { };
|
|
101
|
+
const done = new Promise(resolve => (finish = resolve));
|
|
102
|
+
const queued = { keys: new Set(), done, finish };
|
|
103
|
+
cache.queued = batch = queued;
|
|
104
|
+
queueMicrotask(() => void this.send(source, cache, queued));
|
|
105
|
+
}
|
|
106
|
+
batch.keys.add(key);
|
|
107
|
+
cache.pending.set(key, batch.done);
|
|
108
|
+
}
|
|
109
|
+
async send(source, cache, batch) {
|
|
110
|
+
if (cache.queued === batch)
|
|
111
|
+
cache.queued = null;
|
|
112
|
+
const keys = [...batch.keys];
|
|
113
|
+
const keyField = source.key ?? "id";
|
|
114
|
+
let answered = 0;
|
|
115
|
+
try {
|
|
116
|
+
for (let start = 0; start < keys.length; start += KEYS_PER_CALL) {
|
|
117
|
+
const part = keys.slice(start, start + KEYS_PER_CALL);
|
|
118
|
+
const records = await source.load(part, new AbortController().signal);
|
|
119
|
+
const found = new Map();
|
|
120
|
+
for (const record of records) {
|
|
121
|
+
const key = record[keyField];
|
|
122
|
+
if (key !== undefined && key !== null)
|
|
123
|
+
found.set(String(key), record);
|
|
124
|
+
}
|
|
125
|
+
const at = Date.now();
|
|
126
|
+
for (const key of part) {
|
|
127
|
+
cache.entries.set(key, { record: found.get(key) ?? null, at });
|
|
128
|
+
cache.failedAt.delete(key);
|
|
129
|
+
}
|
|
130
|
+
answered += part.length;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// A key with an older answer keeps it; one with none is "could not be read", and waits before it is asked again.
|
|
135
|
+
const at = Date.now();
|
|
136
|
+
for (const key of keys.slice(answered))
|
|
137
|
+
if (!cache.entries.has(key))
|
|
138
|
+
cache.failedAt.set(key, at);
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
for (const key of keys)
|
|
142
|
+
if (cache.pending.get(key) === batch.done)
|
|
143
|
+
cache.pending.delete(key);
|
|
144
|
+
batch.finish();
|
|
145
|
+
this.notify();
|
|
146
|
+
}
|
|
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
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The records behind these keys, once they are in: what a picker shows for the values a form already holds. The
|
|
170
|
+
* calls and the answers are the ones the tables share, so a form opened from a table costs nothing more.
|
|
171
|
+
*/
|
|
172
|
+
async records(source, keys) {
|
|
173
|
+
const wanted = [...new Set([...keys].filter(Boolean))];
|
|
174
|
+
this.request(source, wanted);
|
|
175
|
+
const cache = this.cacheOf(source.name);
|
|
176
|
+
await Promise.all(wanted.map(key => cache.pending.get(key)).filter(Boolean));
|
|
177
|
+
const found = [];
|
|
178
|
+
for (const key of wanted) {
|
|
179
|
+
const record = cache.entries.get(key)?.record;
|
|
180
|
+
if (record)
|
|
181
|
+
found.push(record);
|
|
182
|
+
}
|
|
183
|
+
return found;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* A whole list kept under `name` - a picker's options - loaded once per lifetime; a second ask while it loads waits for
|
|
187
|
+
* the same call. A list that failed is not kept, so the next ask tries again.
|
|
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
|
+
}
|
|
194
|
+
list(name, load, lifetime = JOIN_LIFETIME) {
|
|
195
|
+
const kept = this.lists.get(name);
|
|
196
|
+
if (kept?.pending)
|
|
197
|
+
return kept.pending;
|
|
198
|
+
if (kept?.rows && Date.now() - kept.at < lifetime)
|
|
199
|
+
return Promise.resolve(kept.rows);
|
|
200
|
+
const pending = load(new AbortController().signal).then(rows => {
|
|
201
|
+
this.lists.set(name, { at: Date.now(), rows });
|
|
202
|
+
return rows;
|
|
203
|
+
}, (error) => {
|
|
204
|
+
this.lists.delete(name);
|
|
205
|
+
throw error;
|
|
206
|
+
});
|
|
207
|
+
this.lists.set(name, { ...kept, at: kept?.at ?? 0, pending });
|
|
208
|
+
return pending;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Marks answers as old - a source's, some of its keys, or every one - after the records behind them may have changed:
|
|
212
|
+
* a save, a move to the trash, a Reload. They are still drawn, and asked for again at the next request.
|
|
213
|
+
*/
|
|
214
|
+
expire(name, keys) {
|
|
215
|
+
const caches = name === undefined ? [...this.sources.values()] : [this.sources.get(name)].filter((cache) => Boolean(cache));
|
|
216
|
+
for (const cache of caches) {
|
|
217
|
+
if (keys) {
|
|
218
|
+
for (const key of keys) {
|
|
219
|
+
const entry = cache.entries.get(key);
|
|
220
|
+
if (entry)
|
|
221
|
+
entry.at = 0;
|
|
222
|
+
cache.failedAt.delete(key);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
for (const entry of cache.entries.values())
|
|
227
|
+
entry.at = 0;
|
|
228
|
+
cache.failedAt.clear();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (caches.length)
|
|
232
|
+
this.notify();
|
|
233
|
+
}
|
|
234
|
+
/** Drops a kept list (or every one), so the next ask loads it again. */
|
|
235
|
+
expireList(name) {
|
|
236
|
+
if (name === undefined)
|
|
237
|
+
this.lists.clear();
|
|
238
|
+
else
|
|
239
|
+
this.lists.delete(name);
|
|
240
|
+
}
|
|
241
|
+
/** Forgets everything. For tests. */
|
|
242
|
+
clear() {
|
|
243
|
+
this.sources.clear();
|
|
244
|
+
this.lists.clear();
|
|
245
|
+
this.notify();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/** The page's one cache of looked-up records: every table and form shares it. */
|
|
249
|
+
export const tableJoins = new TableJoins();
|
|
250
|
+
/** The name a source is kept under: its own, or an endpoint's module, method and path. */
|
|
251
|
+
export function joinSourceName(source) {
|
|
252
|
+
return isApiEndpoint(source) ? `${String(source.module)} ${source.method} ${source.path}` : source.name;
|
|
253
|
+
}
|
|
254
|
+
/** The key a row holds for a join, as text; null when it holds none. */
|
|
255
|
+
export function joinKey(join, row) {
|
|
256
|
+
const key = readPath(row, join.from);
|
|
257
|
+
return key === undefined || key === null || key === "" ? null : String(key);
|
|
258
|
+
}
|
|
259
|
+
/** What is known of the record a row's join names; null when the row names none. */
|
|
260
|
+
export function joinedState(join, row) {
|
|
261
|
+
const key = joinKey(join, row);
|
|
262
|
+
return key === null ? null : tableJoins.peek(joinSourceName(join.source), key);
|
|
263
|
+
}
|
|
264
|
+
/** A join column's value: the looked-up record's field, or undefined until it is in. */
|
|
265
|
+
export function joinedValue(join, columnKey, row) {
|
|
266
|
+
const state = joinedState(join, row);
|
|
267
|
+
return state?.status === "ready" ? readPath(state.record, join.field ?? columnKey) : undefined;
|
|
268
|
+
}
|
|
269
|
+
/** The keys rows hold for every join among these columns, by source: what one request per source asks for. */
|
|
270
|
+
export function joinKeys(joins, rows) {
|
|
271
|
+
const bySource = new Map();
|
|
272
|
+
for (const join of joins) {
|
|
273
|
+
const name = joinSourceName(join.source);
|
|
274
|
+
let entry = bySource.get(name);
|
|
275
|
+
if (!entry)
|
|
276
|
+
bySource.set(name, (entry = { source: join.source, keys: new Set() }));
|
|
277
|
+
for (const row of rows) {
|
|
278
|
+
const key = joinKey(join, row);
|
|
279
|
+
if (key !== null)
|
|
280
|
+
entry.keys.add(key);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return new Map([...bySource.values()].map(entry => [entry.source, entry.keys]));
|
|
284
|
+
}
|
|
@@ -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;
|
|
@@ -83,6 +90,11 @@ export declare class TableStore<TRow extends TableRow = TableRow> {
|
|
|
83
90
|
private fail;
|
|
84
91
|
/** Searches, filters, sorts, and pages every row (client tables), then tells the table. */
|
|
85
92
|
private view;
|
|
93
|
+
/**
|
|
94
|
+
* Some columns' values changed while the rows did not: the names a `join` column reads came in. A client table reads
|
|
95
|
+
* its search text again and searches, filters, sorts and pages anew; a server table draws again. Nothing loads.
|
|
96
|
+
*/
|
|
97
|
+
refreshValues(): void;
|
|
86
98
|
/** Changes the query: a client table shows it at once, a server table loads it. */
|
|
87
99
|
private query;
|
|
88
100
|
setSearch(search: string): void;
|
|
@@ -121,6 +133,19 @@ export declare class TableStore<TRow extends TableRow = TableRow> {
|
|
|
121
133
|
private refreshSelected;
|
|
122
134
|
/** A client table forgets selected rows that are gone from its rows. */
|
|
123
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;
|
|
124
149
|
setBusy(keys: readonly TableKey[], on: boolean): void;
|
|
125
150
|
flash(keys: readonly TableKey[]): void;
|
|
126
151
|
setHidden(key: string, hidden: boolean): void;
|