nexus-shared 3.2.0 → 4.1.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 +2359 -0
- package/README.md +1911 -1123
- package/dist/Client.Index.d.ts +27 -0
- package/dist/Client.Index.js +27 -0
- package/dist/Components/Chats/ChatComposer.d.ts +7 -1
- package/dist/Components/Chats/ChatComposer.js +9 -6
- package/dist/Components/Chats/ChatParts.d.ts +15 -2
- package/dist/Components/Chats/ChatParts.js +15 -4
- package/dist/Components/Chats/ChatPopup.d.ts +46 -0
- package/dist/Components/Chats/ChatPopup.js +34 -0
- package/dist/Components/Chats/ChatThread.d.ts +9 -1
- package/dist/Components/Chats/ChatThread.js +54 -6
- package/dist/Components/Dashboards/DashboardBoard.d.ts +62 -0
- package/dist/Components/Dashboards/DashboardBoard.js +594 -0
- package/dist/Components/Dashboards/DashboardWidgets.d.ts +155 -0
- package/dist/Components/Dashboards/DashboardWidgets.js +159 -0
- package/dist/Components/Dashboards/WidgetFrame.d.ts +51 -0
- package/dist/Components/Dashboards/WidgetFrame.js +45 -0
- package/dist/Components/Documents/Button.d.ts +6 -1
- package/dist/Components/Documents/Button.js +2 -2
- 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/TabButtons.d.ts +6 -0
- package/dist/Components/Documents/TabButtons.js +5 -4
- package/dist/Components/Documents/Tabs.d.ts +59 -0
- package/dist/Components/Documents/Tabs.js +70 -0
- package/dist/Components/Domains/DomainCard.d.ts +98 -0
- package/dist/Components/Domains/DomainCard.js +182 -0
- package/dist/Components/Forms/ApiForm.js +24 -3
- package/dist/Components/Forms/Crud.d.ts +2 -2
- package/dist/Components/Forms/Crud.js +628 -247
- package/dist/Components/Forms/CrudActions.d.ts +137 -0
- package/dist/Components/Forms/CrudActions.js +214 -0
- package/dist/Components/Forms/CrudHistory.d.ts +6 -1
- package/dist/Components/Forms/CrudHistory.js +14 -6
- package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
- package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
- package/dist/Components/Forms/CrudParts.d.ts +37 -2
- package/dist/Components/Forms/CrudParts.js +123 -7
- package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
- package/dist/Components/Forms/CrudRowForms.js +174 -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 +73 -0
- package/dist/Components/Forms/CrudWorkspace.js +177 -0
- package/dist/Components/Forms/ExcelImport.js +3 -3
- package/dist/Components/Forms/Form.d.ts +6 -5
- package/dist/Components/Forms/Form.js +134 -21
- package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
- package/dist/Components/Forms/FormLeaveGuard.js +65 -0
- package/dist/Components/Forms/FormQuestion.d.ts +6 -0
- package/dist/Components/Forms/FormQuestion.js +28 -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 +182 -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/ComputedInput.d.ts +51 -0
- package/dist/Components/Inputs/ComputedInput.js +179 -0
- package/dist/Components/Inputs/Dropdown.d.ts +13 -0
- package/dist/Components/Inputs/Dropdown.js +8 -6
- 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 +12 -12
- package/dist/Components/Inputs/InputRenderer.d.ts +13 -6
- package/dist/Components/Inputs/InputRenderer.js +37 -32
- package/dist/Components/Inputs/MarkdownEditor.d.ts +6 -0
- package/dist/Components/Inputs/MarkdownEditor.js +32 -6
- package/dist/Components/Inputs/PictureInput.d.ts +41 -0
- package/dist/Components/Inputs/PictureInput.js +75 -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 +15 -2
- package/dist/Components/Inputs/RowsInput.js +96 -8
- package/dist/Components/Inputs/TabularForm.d.ts +5 -5
- package/dist/Components/Inputs/TabularForm.js +45 -32
- 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 +76 -2
- package/dist/Components/Inputs/TreeView.js +227 -16
- package/dist/Components/Layouts/MessageBox.d.ts +15 -2
- package/dist/Components/Layouts/MessageBox.js +56 -7
- package/dist/Components/Layouts/Popup.d.ts +34 -7
- package/dist/Components/Layouts/Popup.js +143 -13
- 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/Messages/MailClient.d.ts +50 -0
- package/dist/Components/Messages/MailClient.js +410 -0
- package/dist/Components/Messages/MailColumns.d.ts +21 -0
- package/dist/Components/Messages/MailColumns.js +92 -0
- package/dist/Components/Messages/MailCompose.d.ts +46 -0
- package/dist/Components/Messages/MailCompose.js +154 -0
- package/dist/Components/Messages/MailReader.d.ts +43 -0
- package/dist/Components/Messages/MailReader.js +207 -0
- package/dist/Components/Messages/MailThread.d.ts +71 -0
- package/dist/Components/Messages/MailThread.js +185 -0
- package/dist/Components/Messages/MessageBell.d.ts +41 -0
- package/dist/Components/Messages/MessageBell.js +76 -0
- package/dist/Components/Viewers/DataTable.js +153 -49
- package/dist/Components/Viewers/DataTableColumns.d.ts +2 -2
- package/dist/Components/Viewers/DataTableColumns.js +2 -1
- package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
- package/dist/Components/Viewers/DataTableFilters.js +17 -2
- package/dist/Components/Viewers/DataTableParts.d.ts +23 -5
- package/dist/Components/Viewers/DataTableParts.js +26 -14
- package/dist/Components/Viewers/OwnerFiles.d.ts +227 -0
- package/dist/Components/Viewers/OwnerFiles.js +700 -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.js +4 -0
- package/dist/Helpers/AuditEntries.d.ts +50 -0
- package/dist/Helpers/AuditEntries.js +96 -0
- package/dist/Helpers/BusyHooks.d.ts +6 -0
- package/dist/Helpers/BusyHooks.js +39 -0
- package/dist/Helpers/ChatBackend.d.ts +8 -1
- package/dist/Helpers/ChatBackend.js +18 -0
- package/dist/Helpers/ChatHelpers.d.ts +2 -0
- package/dist/Helpers/ChatHelpers.js +4 -1
- package/dist/Helpers/CrudBackend.d.ts +20 -2
- package/dist/Helpers/CrudBackend.js +46 -2
- package/dist/Helpers/CrudColumns.d.ts +91 -6
- package/dist/Helpers/CrudColumns.js +148 -7
- package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
- package/dist/Helpers/CrudDetailsTabs.js +126 -0
- package/dist/Helpers/CrudHelpers.d.ts +116 -2
- package/dist/Helpers/CrudHelpers.js +256 -0
- package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
- package/dist/Helpers/CrudNarrowPick.js +157 -0
- package/dist/Helpers/CrudPickerPlan.d.ts +58 -0
- package/dist/Helpers/CrudPickerPlan.js +46 -0
- package/dist/Helpers/CrudRowForms.d.ts +32 -0
- package/dist/Helpers/CrudRowForms.js +76 -0
- package/dist/Helpers/CrudScopes.d.ts +32 -0
- package/dist/Helpers/CrudScopes.js +79 -0
- package/dist/Helpers/DashboardHelpers.d.ts +115 -0
- package/dist/Helpers/DashboardHelpers.js +305 -0
- package/dist/Helpers/DomainCards.d.ts +140 -0
- package/dist/Helpers/DomainCards.js +370 -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 +158 -0
- package/dist/Helpers/FileAdapter.js +173 -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 +100 -5
- package/dist/Helpers/FormStore.js +368 -25
- package/dist/Helpers/InputConditions.d.ts +56 -0
- package/dist/Helpers/InputConditions.js +192 -0
- package/dist/Helpers/InputMemory.d.ts +106 -0
- package/dist/Helpers/InputMemory.js +395 -0
- package/dist/Helpers/InputNarrowing.d.ts +50 -0
- package/dist/Helpers/InputNarrowing.js +148 -0
- package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
- package/dist/Helpers/InputParamsHelpers.js +157 -74
- package/dist/Helpers/MailClientHelpers.d.ts +96 -0
- package/dist/Helpers/MailClientHelpers.js +211 -0
- package/dist/Helpers/MessageBackend.d.ts +152 -0
- package/dist/Helpers/MessageBackend.js +329 -0
- package/dist/Helpers/MessageHelpers.d.ts +160 -0
- package/dist/Helpers/MessageHelpers.js +573 -0
- package/dist/Helpers/MessageUnread.d.ts +95 -0
- package/dist/Helpers/MessageUnread.js +245 -0
- package/dist/Helpers/NexusDriveAdapter.d.ts +71 -0
- package/dist/Helpers/NexusDriveAdapter.js +236 -0
- package/dist/Helpers/NumberHelpers.d.ts +156 -0
- package/dist/Helpers/NumberHelpers.js +242 -0
- package/dist/Helpers/PagedPicker.d.ts +42 -0
- package/dist/Helpers/PagedPicker.js +85 -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 +61 -2
- package/dist/Helpers/RowsStore.js +203 -16
- 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 +38 -4
- package/dist/Helpers/TableHelpers.js +78 -10
- package/dist/Helpers/TableJoins.d.ts +13 -0
- package/dist/Helpers/TableJoins.js +34 -0
- package/dist/Helpers/TableStore.d.ts +28 -4
- package/dist/Helpers/TableStore.js +73 -11
- package/dist/Helpers/ToolbarFit.d.ts +23 -0
- package/dist/Helpers/ToolbarFit.js +30 -0
- package/dist/Helpers/TreeStore.d.ts +156 -6
- package/dist/Helpers/TreeStore.js +547 -18
- package/dist/Interfaces/ApiInterfaces.d.ts +10 -1
- package/dist/Interfaces/ApiInterfaces.js +10 -1
- package/dist/Interfaces/ChatInterfaces.d.ts +22 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +539 -6
- package/dist/Interfaces/DashboardInterfaces.d.ts +123 -0
- package/dist/Interfaces/DashboardInterfaces.js +1 -0
- package/dist/Interfaces/DomainInterfaces.d.ts +145 -0
- package/dist/Interfaces/DomainInterfaces.js +1 -0
- package/dist/Interfaces/FileInterfaces.d.ts +183 -0
- package/dist/Interfaces/FileInterfaces.js +6 -0
- package/dist/Interfaces/FormInterfaces.d.ts +460 -32
- package/dist/Interfaces/FormInterfaces.js +79 -1
- package/dist/Interfaces/InputInterfaces.d.ts +22 -3
- package/dist/Interfaces/MailInterfaces.d.ts +192 -0
- package/dist/Interfaces/MailInterfaces.js +1 -0
- package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
- package/dist/Interfaces/RecordInterfaces.js +1 -0
- package/dist/Interfaces/TableInterfaces.d.ts +102 -3
- 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 +79 -6
- package/dist/Services/JoinSources.js +132 -21
- package/dist/Services/MessageApi.d.ts +18 -0
- package/dist/Services/MessageApi.js +64 -0
- package/dist/Services/ModuleImport.d.ts +7 -0
- package/dist/Services/ModuleImport.js +22 -0
- package/dist/Shared.Index.d.ts +28 -0
- package/dist/Shared.Index.js +28 -0
- package/package.json +13 -2
- package/src/Styles/Nexus.Avatar.css +6 -5
- package/src/Styles/Nexus.Button.css +39 -4
- package/src/Styles/Nexus.Chat.css +114 -22
- package/src/Styles/Nexus.Choice.css +16 -0
- package/src/Styles/Nexus.Crud.css +486 -21
- package/src/Styles/Nexus.Dashboard.css +763 -0
- package/src/Styles/Nexus.Domain.css +384 -0
- package/src/Styles/Nexus.Dropdown.css +8 -0
- package/src/Styles/Nexus.Editor.css +20 -0
- package/src/Styles/Nexus.Files.css +437 -0
- package/src/Styles/Nexus.Form.css +5 -2
- package/src/Styles/Nexus.Index.css +7 -0
- package/src/Styles/Nexus.Input.css +73 -0
- package/src/Styles/Nexus.Mail.css +1062 -0
- package/src/Styles/Nexus.Menu.css +10 -1
- package/src/Styles/Nexus.Popup.css +135 -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 +1458 -1387
- package/src/Styles/Nexus.Theme.Picker.css +1 -0
- package/src/Styles/Nexus.Themes.css +12 -6
- package/src/Styles/Nexus.Tokens.css +8 -3
- package/src/Styles/Nexus.Tree.css +41 -1
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { CRUD_ROW_ACTIONS } from "./CrudHelpers.js";
|
|
2
|
+
import { isApiEndpoint } from "./ApiRoutes.js";
|
|
3
|
+
import { formConfirmQuestion } from "./FormConfirm.js";
|
|
4
|
+
/*
|
|
5
|
+
* The pure half of a row action that opens a form of its own for the record (`Crud` `rowForms`,
|
|
6
|
+
* Components/Forms/CrudRowForms.tsx): where it is offered, what it asks before saving, and what of a save's answer is
|
|
7
|
+
* written back into the record. Pure, so Node tests run it.
|
|
8
|
+
*/
|
|
9
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
|
+
/**
|
|
11
|
+
* Whether a row form is offered on a record: on its lists (default the lists that offer Edit), to a person who may call
|
|
12
|
+
* its `submit` and, when it is an endpoint, its `load`, and by the page's own `when`.
|
|
13
|
+
*/
|
|
14
|
+
export function crudRowFormOffered(form, row, mode, can) {
|
|
15
|
+
const modes = form.modes ?? Object.keys(CRUD_ROW_ACTIONS).filter(list => CRUD_ROW_ACTIONS[list].includes("edit"));
|
|
16
|
+
if (!modes.includes(mode))
|
|
17
|
+
return false;
|
|
18
|
+
if (!can(form.submit))
|
|
19
|
+
return false;
|
|
20
|
+
if (isApiEndpoint(form.load) && !can(form.load))
|
|
21
|
+
return false;
|
|
22
|
+
return form.when ? form.when(row, mode) : true;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* What a save changes on the record, as `writes` says: the answer's fields it names (each one the answer carries; an
|
|
26
|
+
* answer that is not a record changes nothing), or what its function answers. Never the record as the form was opened
|
|
27
|
+
* on: the CRUD merges this over the record as it holds it when the answer lands, so a change made meanwhile (an Edit
|
|
28
|
+
* while the popup was minimized) stays.
|
|
29
|
+
*/
|
|
30
|
+
export function crudRowFormWrites(writes, result, row, values) {
|
|
31
|
+
if (!writes)
|
|
32
|
+
return null;
|
|
33
|
+
if (typeof writes === "function") {
|
|
34
|
+
const patch = writes(result, row, values);
|
|
35
|
+
return patch && Object.keys(patch).length ? patch : null;
|
|
36
|
+
}
|
|
37
|
+
if (!isRecord(result))
|
|
38
|
+
return null;
|
|
39
|
+
const patch = {};
|
|
40
|
+
for (const field of writes)
|
|
41
|
+
if (result[field] !== undefined)
|
|
42
|
+
patch[field] = result[field];
|
|
43
|
+
return Object.keys(patch).length ? patch : null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Whether a row form is its question alone: it gives no inputs. Choosing it asks (its `confirm`, else "{label} {name}?")
|
|
47
|
+
* and sends on Confirm, with no popup form.
|
|
48
|
+
*/
|
|
49
|
+
export function crudRowFormAsksOnly(form) {
|
|
50
|
+
return form.inputs === undefined;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The question a row form asks for this record: its `confirm` as given, or made from the record and what `load` read;
|
|
54
|
+
* for a question alone that gives none, "{label} {name}?" (`name` is the record's, as its row is named). `null`: a form
|
|
55
|
+
* that saves without asking.
|
|
56
|
+
*/
|
|
57
|
+
export function crudRowFormConfirm(form, row, loaded, name) {
|
|
58
|
+
const given = typeof form.confirm === "function" ? form.confirm(row, loaded) : form.confirm;
|
|
59
|
+
if (given)
|
|
60
|
+
return given;
|
|
61
|
+
return crudRowFormAsksOnly(form) ? { title: `${form.label} ${name}?` } : null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The question settled for these values and this body (`formConfirmQuestion`). A question alone has no form to go back
|
|
65
|
+
* to: its buttons are the action's own word (`submitLabel`, else `label`) and "Cancel", unless the page names them.
|
|
66
|
+
*/
|
|
67
|
+
export function crudRowFormQuestion(form, confirm, values, body) {
|
|
68
|
+
const question = formConfirmQuestion(confirm, values, body);
|
|
69
|
+
if (!crudRowFormAsksOnly(form))
|
|
70
|
+
return question;
|
|
71
|
+
return {
|
|
72
|
+
...question,
|
|
73
|
+
confirmLabel: confirm.confirmLabel === undefined ? (form.submitLabel ?? form.label) : question.confirmLabel,
|
|
74
|
+
cancelLabel: confirm.cancelLabel ?? "Cancel",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { CrudProps, CrudScope, CrudScopes } from "../Interfaces/CrudInterfaces.ts";
|
|
2
|
+
import type { FormValues } from "../Interfaces/FormInterfaces.ts";
|
|
3
|
+
/**
|
|
4
|
+
* The scopes offered now: those whose `when` (the page's own rule) allows them and whose lists the person may call
|
|
5
|
+
* (`callable`, the CRUD's permission test on the scope's endpoints). None while the permissions are not known.
|
|
6
|
+
*/
|
|
7
|
+
export declare function offeredCrudScopes<TRow extends FormValues>(scopes: CrudScopes<TRow> | undefined, callable: (scope: CrudScope<TRow>) => boolean): CrudScope<TRow>[];
|
|
8
|
+
/** The scope shown: the one chosen while it is offered, else the CRUD's own list (`null`). */
|
|
9
|
+
export declare function shownCrudScope<TRow extends FormValues>(offered: readonly CrudScope<TRow>[], chosen: string | null): CrudScope<TRow> | null;
|
|
10
|
+
/**
|
|
11
|
+
* The CRUD's props on a scope: its endpoints, and its columns, inputs, forms, table settings and name where it gives its
|
|
12
|
+
* own (`form` and `table` over the CRUD's, field by field). The CRUD's own list is the props as they are given.
|
|
13
|
+
*/
|
|
14
|
+
export declare function crudScopeProps<TRow extends FormValues>(given: CrudProps<TRow>, scope: CrudScope<TRow> | null): CrudProps<TRow>;
|
|
15
|
+
/**
|
|
16
|
+
* Where a scope keeps its table's settings: the scope's own `table.stateKey`, else the CRUD's key and the scope's id, so
|
|
17
|
+
* each list keeps the columns shown, their order and widths, its sort and its page size apart (a Branch column hidden
|
|
18
|
+
* on one branch's list and shown on every branch's). The CRUD's own list keeps the CRUD's key.
|
|
19
|
+
*/
|
|
20
|
+
export declare function crudScopeStateKey<TRow extends FormValues>(ownKey: string, scope: CrudScope<TRow> | null): string;
|
|
21
|
+
/** Where this browser keeps the choice: `nx-crud-scope:{key}:{viewer}`; nothing without a viewer. */
|
|
22
|
+
export declare function crudScopeStorageKey(remember: {
|
|
23
|
+
key: string;
|
|
24
|
+
viewer?: string | null;
|
|
25
|
+
} | undefined): string | null;
|
|
26
|
+
/**
|
|
27
|
+
* The scope kept: its id, `null` for the CRUD's own list (kept as nothing), `undefined` when nothing could be read (no
|
|
28
|
+
* storage, no key). A kept scope the CRUD no longer offers stays kept and opens the CRUD's own list until it is offered.
|
|
29
|
+
*/
|
|
30
|
+
export declare function readCrudScope(storage: Pick<Storage, "getItem"> | null, key: string | null): string | null | undefined;
|
|
31
|
+
/** Keeps the choice; the CRUD's own list is kept as nothing. A browser that keeps nothing opens the CRUD's own list. */
|
|
32
|
+
export declare function writeCrudScope(storage: Pick<Storage, "setItem" | "removeItem"> | null, key: string | null, scope: string | null): void;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The pure half of a CRUD whose list can be widened (`Crud` `scopes`, Components/Forms/CrudScopes.tsx): which lists are
|
|
3
|
+
* offered, which one shows, the props it gives the CRUD, and where this browser keeps the choice. Pure, so Node tests
|
|
4
|
+
* run it.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* The scopes offered now: those whose `when` (the page's own rule) allows them and whose lists the person may call
|
|
8
|
+
* (`callable`, the CRUD's permission test on the scope's endpoints). None while the permissions are not known.
|
|
9
|
+
*/
|
|
10
|
+
export function offeredCrudScopes(scopes, callable) {
|
|
11
|
+
if (!scopes)
|
|
12
|
+
return [];
|
|
13
|
+
return scopes.options.filter(scope => (scope.when ? scope.when() : true) && callable(scope));
|
|
14
|
+
}
|
|
15
|
+
/** The scope shown: the one chosen while it is offered, else the CRUD's own list (`null`). */
|
|
16
|
+
export function shownCrudScope(offered, chosen) {
|
|
17
|
+
return chosen === null ? null : (offered.find(scope => scope.id === chosen) ?? null);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The CRUD's props on a scope: its endpoints, and its columns, inputs, forms, table settings and name where it gives its
|
|
21
|
+
* own (`form` and `table` over the CRUD's, field by field). The CRUD's own list is the props as they are given.
|
|
22
|
+
*/
|
|
23
|
+
export function crudScopeProps(given, scope) {
|
|
24
|
+
if (!scope)
|
|
25
|
+
return given;
|
|
26
|
+
return {
|
|
27
|
+
...given,
|
|
28
|
+
api: scope.api,
|
|
29
|
+
columns: scope.columns ?? given.columns,
|
|
30
|
+
inputs: scope.inputs ?? given.inputs,
|
|
31
|
+
form: scope.form ? { ...given.form, ...scope.form } : given.form,
|
|
32
|
+
table: scope.table ? { ...given.table, ...scope.table } : given.table,
|
|
33
|
+
"aria-label": scope["aria-label"] ?? given["aria-label"],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Where a scope keeps its table's settings: the scope's own `table.stateKey`, else the CRUD's key and the scope's id, so
|
|
38
|
+
* each list keeps the columns shown, their order and widths, its sort and its page size apart (a Branch column hidden
|
|
39
|
+
* on one branch's list and shown on every branch's). The CRUD's own list keeps the CRUD's key.
|
|
40
|
+
*/
|
|
41
|
+
export function crudScopeStateKey(ownKey, scope) {
|
|
42
|
+
if (!scope)
|
|
43
|
+
return ownKey;
|
|
44
|
+
return scope.table?.stateKey ?? `${ownKey}:${scope.id}`;
|
|
45
|
+
}
|
|
46
|
+
/** Where this browser keeps the choice: `nx-crud-scope:{key}:{viewer}`; nothing without a viewer. */
|
|
47
|
+
export function crudScopeStorageKey(remember) {
|
|
48
|
+
const viewer = remember?.viewer?.trim();
|
|
49
|
+
return remember?.key && viewer ? `nx-crud-scope:${remember.key}:${viewer}` : null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The scope kept: its id, `null` for the CRUD's own list (kept as nothing), `undefined` when nothing could be read (no
|
|
53
|
+
* storage, no key). A kept scope the CRUD no longer offers stays kept and opens the CRUD's own list until it is offered.
|
|
54
|
+
*/
|
|
55
|
+
export function readCrudScope(storage, key) {
|
|
56
|
+
if (!storage || !key)
|
|
57
|
+
return undefined;
|
|
58
|
+
try {
|
|
59
|
+
const kept = storage.getItem(key);
|
|
60
|
+
return kept ? kept : null;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Keeps the choice; the CRUD's own list is kept as nothing. A browser that keeps nothing opens the CRUD's own list. */
|
|
67
|
+
export function writeCrudScope(storage, key, scope) {
|
|
68
|
+
if (!storage || !key)
|
|
69
|
+
return;
|
|
70
|
+
try {
|
|
71
|
+
if (scope)
|
|
72
|
+
storage.setItem(key, scope);
|
|
73
|
+
else
|
|
74
|
+
storage.removeItem(key);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Not kept: the CRUD opens its own list next time.
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { DashboardLayout, DashboardLayoutItem, WidgetContext, WidgetDefinition, WidgetModuleLoader, WidgetSize } from "../Interfaces/DashboardInterfaces.ts";
|
|
2
|
+
/** Each size's span on the 12-column grid, and its name in the size menu. */
|
|
3
|
+
export declare const WIDGET_SPANS: Readonly<Record<WidgetSize, {
|
|
4
|
+
readonly columns: number;
|
|
5
|
+
readonly rows: number;
|
|
6
|
+
readonly label: string;
|
|
7
|
+
}>>;
|
|
8
|
+
/** The sizes in the order a size menu lists them. */
|
|
9
|
+
export declare const WIDGET_SIZES: readonly WidgetSize[];
|
|
10
|
+
/** "Medium (6 × 2)". */
|
|
11
|
+
export declare const widgetSizeLabel: (size: WidgetSize) => string;
|
|
12
|
+
/** Whether a widget may go on a place's board: an `anywhere` widget everywhere, an `own` one on its module's board only. */
|
|
13
|
+
export declare function widgetAllowedIn(widget: Pick<WidgetDefinition, "placement" | "module">, placeModule: string | null | undefined): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The widgets a place's board may draw now, by code: allowed in the place and seen by the person (`canSee`: the module
|
|
16
|
+
* held, the key held). Duplicate codes keep the first.
|
|
17
|
+
*/
|
|
18
|
+
export declare function drawableWidgets(widgets: readonly WidgetDefinition<any>[], placeModule: string | null | undefined, canSee?: (widget: WidgetDefinition<any>) => boolean): Map<string, WidgetDefinition<any>>;
|
|
19
|
+
/**
|
|
20
|
+
* A layout as the board keeps it: each code once (the first), each size one its widget takes (else its first). A code
|
|
21
|
+
* the board does not know now (a widget hidden, removed from the app, or not allowed here) is kept as it was, so it
|
|
22
|
+
* comes back if it can be drawn again.
|
|
23
|
+
*/
|
|
24
|
+
export declare function normaliseLayout(layout: readonly DashboardLayoutItem[] | null | undefined, widgets: readonly WidgetDefinition<any>[]): DashboardLayoutItem[];
|
|
25
|
+
/** The items of a layout the board draws now, in order. */
|
|
26
|
+
export declare function drawnItems(layout: DashboardLayout, drawable: ReadonlyMap<string, unknown>): DashboardLayoutItem[];
|
|
27
|
+
/**
|
|
28
|
+
* A layout to save after an edit of the drawn items: the edited items in their new order, and every item that was not
|
|
29
|
+
* drawn (hidden now) back in its place - after the drawn item it followed, or at the start. One whose drawn neighbour
|
|
30
|
+
* was removed follows the drawn item before that one. Nothing hidden is lost by a save.
|
|
31
|
+
*/
|
|
32
|
+
export declare function mergeHidden(full: DashboardLayout, edited: DashboardLayout, drawable: ReadonlyMap<string, unknown>): DashboardLayoutItem[];
|
|
33
|
+
/** The list with the item at `from` moved to `to` (both clamped). */
|
|
34
|
+
export declare function moveItem<T>(list: readonly T[], from: number, to: number): T[];
|
|
35
|
+
/** Whether two layouts are the same widgets, sizes and order. */
|
|
36
|
+
export declare function sameLayout(a: DashboardLayout | null | undefined, b: DashboardLayout | null | undefined): boolean;
|
|
37
|
+
/** A widget's box on the screen, as the keyboard and the drag read it. */
|
|
38
|
+
export interface WidgetRect {
|
|
39
|
+
left: number;
|
|
40
|
+
top: number;
|
|
41
|
+
width: number;
|
|
42
|
+
height: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The widget an arrow key up or down moves to: in the nearest row above (or below) the one at `index`, the one whose
|
|
46
|
+
* middle is nearest across. `null` when there is no row that way. Rows are told apart by their tops.
|
|
47
|
+
*/
|
|
48
|
+
export declare function neighbourInColumn(rects: readonly WidgetRect[], index: number, direction: "up" | "down"): number | null;
|
|
49
|
+
/** The widget under a point, leaving out `skip` (the one being dragged). `null` over a gap. */
|
|
50
|
+
export declare function widgetAtPoint(rects: readonly WidgetRect[], x: number, y: number, skip: number): number | null;
|
|
51
|
+
/** A module's widgets in the Add list. */
|
|
52
|
+
export interface WidgetGroup {
|
|
53
|
+
module: string;
|
|
54
|
+
widgets: {
|
|
55
|
+
widget: WidgetDefinition<any>;
|
|
56
|
+
onBoard: boolean;
|
|
57
|
+
}[];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The Add list: every widget the place may draw now, grouped by module - the place's own module first, the rest in the
|
|
61
|
+
* order the app declared them - each marked when it is on the board already.
|
|
62
|
+
*/
|
|
63
|
+
export declare function addableWidgets(drawable: ReadonlyMap<string, WidgetDefinition<any>>, layout: DashboardLayout, placeModule: string | null | undefined): WidgetGroup[];
|
|
64
|
+
/** Whether a widget's data is nothing to show: its own `isEmpty`, else `null`, `undefined` or an empty list. */
|
|
65
|
+
export declare function widgetDataEmpty(widget: Pick<WidgetDefinition<any>, "isEmpty">, data: unknown): boolean;
|
|
66
|
+
/** Whether a widget reads data at all (`load` or `batch`). */
|
|
67
|
+
export declare const widgetNeedsData: (widget: Pick<WidgetDefinition<any>, "load" | "batch">) => boolean;
|
|
68
|
+
/** What `createWidgetBatcher` answers. */
|
|
69
|
+
export interface WidgetBatcher {
|
|
70
|
+
/**
|
|
71
|
+
* The widget's data: from the cache while fresh (unless `force`), from a call already on its way, else asked. Batched
|
|
72
|
+
* widgets asked in the same task go to their module together, one call per module.
|
|
73
|
+
*/
|
|
74
|
+
request(widget: WidgetDefinition<any>, context: WidgetContext, options?: {
|
|
75
|
+
force?: boolean;
|
|
76
|
+
}): Promise<unknown>;
|
|
77
|
+
/** The widget's data as kept, and when it came. */
|
|
78
|
+
cached(code: string): {
|
|
79
|
+
data: unknown;
|
|
80
|
+
at: number;
|
|
81
|
+
} | undefined;
|
|
82
|
+
/** Keeps data read elsewhere (with the page, on the server) as if it had been asked now. */
|
|
83
|
+
prime(code: string, data: unknown): void;
|
|
84
|
+
/** Whether the widget's kept data is still fresh (its `refresh` seconds; no `refresh`: the page's life). */
|
|
85
|
+
isFresh(widget: Pick<WidgetDefinition<any>, "code" | "refresh">): boolean;
|
|
86
|
+
/** Forgets the kept data (of these codes, or all), so the next request asks again. */
|
|
87
|
+
clear(codes?: readonly string[]): void;
|
|
88
|
+
/** How many calls went out: each module call and each own `load` count one. */
|
|
89
|
+
readonly calls: number;
|
|
90
|
+
}
|
|
91
|
+
export interface WidgetBatcherOptions {
|
|
92
|
+
/** The clock, for tests. Default `Date.now`. */
|
|
93
|
+
now?: () => number;
|
|
94
|
+
/** When the gathered requests go out. Default: the next task (`setTimeout(0)`), so one draw's requests go together. */
|
|
95
|
+
defer?: (flush: () => void) => void;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* One cache and one queue for a board's widgets: every batched widget asked while one draw runs goes to its module in
|
|
99
|
+
* one call (`loadModule(module, codes)`), each answer is kept for the page's life (or the widget's `refresh` seconds),
|
|
100
|
+
* and a widget asked twice while its call is on its way waits for that call.
|
|
101
|
+
*/
|
|
102
|
+
export declare function createWidgetBatcher(loadModule: WidgetModuleLoader | undefined, options?: WidgetBatcherOptions): WidgetBatcher;
|
|
103
|
+
/** A typed mark on a calendar widget's day: a holiday, a leave, an event, or a kind of the app's own. */
|
|
104
|
+
export interface CalendarMark {
|
|
105
|
+
/** The first day, an ISO date. */
|
|
106
|
+
date: string;
|
|
107
|
+
/** The last day of a mark over several days (a leave), an ISO date; none for one day. */
|
|
108
|
+
until?: string;
|
|
109
|
+
/** `holiday`, `leave`, `event`, or the app's own kind (give it in `kinds`). */
|
|
110
|
+
kind: string;
|
|
111
|
+
/** What it is: "Dashain", "Sita Rai - annual leave". */
|
|
112
|
+
label: string;
|
|
113
|
+
}
|
|
114
|
+
/** Each day's marks, by ISO date: a mark over several days is on each of them. A mark with a broken date is left out. */
|
|
115
|
+
export declare function marksByDay(marks: readonly CalendarMark[]): Map<string, CalendarMark[]>;
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* A dashboard board's rules, kept apart from React so they can be tested alone: the size presets, which widgets a place
|
|
3
|
+
* may draw, a layout kept whole through edits (the widgets a person cannot see now keep their places), moving in the
|
|
4
|
+
* list by keyboard and by pointer, the Add list, and the batcher that asks each module once per draw.
|
|
5
|
+
*/
|
|
6
|
+
import { addDays, isIsoDate } from "./DateHelpers.js";
|
|
7
|
+
/** Each size's span on the 12-column grid, and its name in the size menu. */
|
|
8
|
+
export const WIDGET_SPANS = {
|
|
9
|
+
S: { columns: 3, rows: 1, label: "Small" },
|
|
10
|
+
M: { columns: 6, rows: 2, label: "Medium" },
|
|
11
|
+
L: { columns: 6, rows: 4, label: "Large" },
|
|
12
|
+
W: { columns: 12, rows: 2, label: "Wide" },
|
|
13
|
+
T: { columns: 12, rows: 4, label: "Tall" },
|
|
14
|
+
};
|
|
15
|
+
/** The sizes in the order a size menu lists them. */
|
|
16
|
+
export const WIDGET_SIZES = ["S", "M", "L", "W", "T"];
|
|
17
|
+
/** "Medium (6 × 2)". */
|
|
18
|
+
export const widgetSizeLabel = (size) => `${WIDGET_SPANS[size].label} (${WIDGET_SPANS[size].columns} × ${WIDGET_SPANS[size].rows})`;
|
|
19
|
+
/** Whether a widget may go on a place's board: an `anywhere` widget everywhere, an `own` one on its module's board only. */
|
|
20
|
+
export function widgetAllowedIn(widget, placeModule) {
|
|
21
|
+
return widget.placement === "anywhere" || (placeModule !== null && placeModule !== undefined && widget.module === placeModule);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The widgets a place's board may draw now, by code: allowed in the place and seen by the person (`canSee`: the module
|
|
25
|
+
* held, the key held). Duplicate codes keep the first.
|
|
26
|
+
*/
|
|
27
|
+
export function drawableWidgets(widgets, placeModule, canSee) {
|
|
28
|
+
const drawable = new Map();
|
|
29
|
+
for (const widget of widgets) {
|
|
30
|
+
if (drawable.has(widget.code) || widget.sizes.length === 0)
|
|
31
|
+
continue;
|
|
32
|
+
if (!widgetAllowedIn(widget, placeModule))
|
|
33
|
+
continue;
|
|
34
|
+
if (canSee && !canSee(widget))
|
|
35
|
+
continue;
|
|
36
|
+
drawable.set(widget.code, widget);
|
|
37
|
+
}
|
|
38
|
+
return drawable;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A layout as the board keeps it: each code once (the first), each size one its widget takes (else its first). A code
|
|
42
|
+
* the board does not know now (a widget hidden, removed from the app, or not allowed here) is kept as it was, so it
|
|
43
|
+
* comes back if it can be drawn again.
|
|
44
|
+
*/
|
|
45
|
+
export function normaliseLayout(layout, widgets) {
|
|
46
|
+
const byCode = new Map(widgets.map(widget => [widget.code, widget]));
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
const items = [];
|
|
49
|
+
for (const item of layout ?? []) {
|
|
50
|
+
if (!item || typeof item.code !== "string" || seen.has(item.code))
|
|
51
|
+
continue;
|
|
52
|
+
seen.add(item.code);
|
|
53
|
+
const widget = byCode.get(item.code);
|
|
54
|
+
const size = widget && !widget.sizes.includes(item.size) ? widget.sizes[0] : item.size;
|
|
55
|
+
items.push({ code: item.code, size });
|
|
56
|
+
}
|
|
57
|
+
return items;
|
|
58
|
+
}
|
|
59
|
+
/** The items of a layout the board draws now, in order. */
|
|
60
|
+
export function drawnItems(layout, drawable) {
|
|
61
|
+
return layout.filter(item => drawable.has(item.code));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A layout to save after an edit of the drawn items: the edited items in their new order, and every item that was not
|
|
65
|
+
* drawn (hidden now) back in its place - after the drawn item it followed, or at the start. One whose drawn neighbour
|
|
66
|
+
* was removed follows the drawn item before that one. Nothing hidden is lost by a save.
|
|
67
|
+
*/
|
|
68
|
+
export function mergeHidden(full, edited, drawable) {
|
|
69
|
+
const editedCodes = new Set(edited.map(item => item.code));
|
|
70
|
+
// Each hidden item and the drawn items before it, nearest first: it follows the nearest that is still there.
|
|
71
|
+
const anchored = new Map();
|
|
72
|
+
const before = [];
|
|
73
|
+
for (const item of full) {
|
|
74
|
+
if (drawable.has(item.code)) {
|
|
75
|
+
before.push(item.code);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (editedCodes.has(item.code))
|
|
79
|
+
continue;
|
|
80
|
+
let anchor = null;
|
|
81
|
+
for (let index = before.length - 1; index >= 0; index--)
|
|
82
|
+
if (editedCodes.has(before[index])) {
|
|
83
|
+
anchor = before[index];
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
anchored.set(anchor, [...(anchored.get(anchor) ?? []), item]);
|
|
87
|
+
}
|
|
88
|
+
const result = [...(anchored.get(null) ?? [])];
|
|
89
|
+
for (const item of edited) {
|
|
90
|
+
result.push(item);
|
|
91
|
+
result.push(...(anchored.get(item.code) ?? []));
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
/** The list with the item at `from` moved to `to` (both clamped). */
|
|
96
|
+
export function moveItem(list, from, to) {
|
|
97
|
+
const result = [...list];
|
|
98
|
+
if (from < 0 || from >= result.length)
|
|
99
|
+
return result;
|
|
100
|
+
const target = Math.max(0, Math.min(result.length - 1, to));
|
|
101
|
+
const [item] = result.splice(from, 1);
|
|
102
|
+
result.splice(target, 0, item);
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
/** Whether two layouts are the same widgets, sizes and order. */
|
|
106
|
+
export function sameLayout(a, b) {
|
|
107
|
+
const left = a ?? [];
|
|
108
|
+
const right = b ?? [];
|
|
109
|
+
return left.length === right.length && left.every((item, index) => item.code === right[index].code && item.size === right[index].size);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The widget an arrow key up or down moves to: in the nearest row above (or below) the one at `index`, the one whose
|
|
113
|
+
* middle is nearest across. `null` when there is no row that way. Rows are told apart by their tops.
|
|
114
|
+
*/
|
|
115
|
+
export function neighbourInColumn(rects, index, direction) {
|
|
116
|
+
const from = rects[index];
|
|
117
|
+
if (!from)
|
|
118
|
+
return null;
|
|
119
|
+
const middle = from.left + from.width / 2;
|
|
120
|
+
let rowTop = null;
|
|
121
|
+
for (const rect of rects) {
|
|
122
|
+
const wanted = direction === "up" ? rect.top < from.top - 1 : rect.top > from.top + 1;
|
|
123
|
+
if (!wanted)
|
|
124
|
+
continue;
|
|
125
|
+
if (rowTop === null || (direction === "up" ? rect.top > rowTop : rect.top < rowTop))
|
|
126
|
+
rowTop = rect.top;
|
|
127
|
+
}
|
|
128
|
+
if (rowTop === null)
|
|
129
|
+
return null;
|
|
130
|
+
let best = null;
|
|
131
|
+
let bestDistance = Infinity;
|
|
132
|
+
rects.forEach((rect, at) => {
|
|
133
|
+
if (Math.abs(rect.top - rowTop) > 1)
|
|
134
|
+
return;
|
|
135
|
+
const distance = Math.abs(rect.left + rect.width / 2 - middle);
|
|
136
|
+
if (distance < bestDistance) {
|
|
137
|
+
bestDistance = distance;
|
|
138
|
+
best = at;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
return best;
|
|
142
|
+
}
|
|
143
|
+
/** The widget under a point, leaving out `skip` (the one being dragged). `null` over a gap. */
|
|
144
|
+
export function widgetAtPoint(rects, x, y, skip) {
|
|
145
|
+
for (let index = 0; index < rects.length; index++) {
|
|
146
|
+
if (index === skip)
|
|
147
|
+
continue;
|
|
148
|
+
const rect = rects[index];
|
|
149
|
+
if (x >= rect.left && x < rect.left + rect.width && y >= rect.top && y < rect.top + rect.height)
|
|
150
|
+
return index;
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The Add list: every widget the place may draw now, grouped by module - the place's own module first, the rest in the
|
|
156
|
+
* order the app declared them - each marked when it is on the board already.
|
|
157
|
+
*/
|
|
158
|
+
export function addableWidgets(drawable, layout, placeModule) {
|
|
159
|
+
const onBoard = new Set(layout.map(item => item.code));
|
|
160
|
+
const groups = new Map();
|
|
161
|
+
if (placeModule)
|
|
162
|
+
groups.set(placeModule, { module: placeModule, widgets: [] });
|
|
163
|
+
for (const widget of drawable.values()) {
|
|
164
|
+
const group = groups.get(widget.module) ?? {
|
|
165
|
+
module: widget.module,
|
|
166
|
+
widgets: [],
|
|
167
|
+
};
|
|
168
|
+
group.widgets.push({ widget, onBoard: onBoard.has(widget.code) });
|
|
169
|
+
groups.set(widget.module, group);
|
|
170
|
+
}
|
|
171
|
+
return [...groups.values()].filter(group => group.widgets.length > 0);
|
|
172
|
+
}
|
|
173
|
+
/** Whether a widget's data is nothing to show: its own `isEmpty`, else `null`, `undefined` or an empty list. */
|
|
174
|
+
export function widgetDataEmpty(widget, data) {
|
|
175
|
+
if (widget.isEmpty)
|
|
176
|
+
return widget.isEmpty(data);
|
|
177
|
+
return data === null || data === undefined || (Array.isArray(data) && data.length === 0);
|
|
178
|
+
}
|
|
179
|
+
/** Whether a widget reads data at all (`load` or `batch`). */
|
|
180
|
+
export const widgetNeedsData = (widget) => Boolean(widget.load || widget.batch);
|
|
181
|
+
/**
|
|
182
|
+
* One cache and one queue for a board's widgets: every batched widget asked while one draw runs goes to its module in
|
|
183
|
+
* one call (`loadModule(module, codes)`), each answer is kept for the page's life (or the widget's `refresh` seconds),
|
|
184
|
+
* and a widget asked twice while its call is on its way waits for that call.
|
|
185
|
+
*/
|
|
186
|
+
export function createWidgetBatcher(loadModule, options = {}) {
|
|
187
|
+
const now = options.now ?? Date.now;
|
|
188
|
+
const defer = options.defer ?? (flush => void setTimeout(flush, 0));
|
|
189
|
+
const cache = new Map();
|
|
190
|
+
const inFlight = new Map();
|
|
191
|
+
let pending = new Map();
|
|
192
|
+
let scheduled = false;
|
|
193
|
+
let calls = 0;
|
|
194
|
+
function keep(code, data) {
|
|
195
|
+
cache.set(code, { data, at: now() });
|
|
196
|
+
}
|
|
197
|
+
function flush() {
|
|
198
|
+
scheduled = false;
|
|
199
|
+
const batches = pending;
|
|
200
|
+
pending = new Map();
|
|
201
|
+
for (const [module, waiting] of batches) {
|
|
202
|
+
const codes = [...new Set(waiting.flatMap(one => one.widget.batch?.codes ?? [one.widget.code]))];
|
|
203
|
+
calls++;
|
|
204
|
+
let call;
|
|
205
|
+
try {
|
|
206
|
+
call = loadModule ? Promise.resolve(loadModule(module, codes)) : Promise.reject(new Error(`No loadModule for ${module}.`));
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
call = Promise.reject(error);
|
|
210
|
+
}
|
|
211
|
+
call.then(answer => {
|
|
212
|
+
for (const one of waiting) {
|
|
213
|
+
const own = one.widget.batch?.codes ?? [one.widget.code];
|
|
214
|
+
const parts = {};
|
|
215
|
+
for (const code of own)
|
|
216
|
+
if (answer && Object.prototype.hasOwnProperty.call(answer, code))
|
|
217
|
+
parts[code] = answer[code];
|
|
218
|
+
let data;
|
|
219
|
+
try {
|
|
220
|
+
data = one.widget.batch?.pick ? one.widget.batch.pick(parts) : own.length === 1 ? parts[own[0]] : parts;
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
one.reject(error);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
keep(one.widget.code, data);
|
|
227
|
+
one.resolve(data);
|
|
228
|
+
}
|
|
229
|
+
}, error => waiting.forEach(one => one.reject(error)));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const batcher = {
|
|
233
|
+
request(widget, context, requestOptions = {}) {
|
|
234
|
+
if (!requestOptions.force && batcher.isFresh(widget))
|
|
235
|
+
return Promise.resolve(cache.get(widget.code).data);
|
|
236
|
+
const running = inFlight.get(widget.code);
|
|
237
|
+
if (running)
|
|
238
|
+
return running;
|
|
239
|
+
let promise;
|
|
240
|
+
if (widget.batch) {
|
|
241
|
+
promise = new Promise((resolve, reject) => {
|
|
242
|
+
const module = widget.batch.module;
|
|
243
|
+
pending.set(module, [...(pending.get(module) ?? []), { widget, resolve, reject }]);
|
|
244
|
+
if (!scheduled) {
|
|
245
|
+
scheduled = true;
|
|
246
|
+
defer(flush);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
else if (widget.load) {
|
|
251
|
+
calls++;
|
|
252
|
+
promise = Promise.resolve()
|
|
253
|
+
.then(() => widget.load(context))
|
|
254
|
+
.then(data => {
|
|
255
|
+
keep(widget.code, data);
|
|
256
|
+
return data;
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
keep(widget.code, undefined);
|
|
261
|
+
return Promise.resolve(undefined);
|
|
262
|
+
}
|
|
263
|
+
inFlight.set(widget.code, promise);
|
|
264
|
+
const done = () => {
|
|
265
|
+
if (inFlight.get(widget.code) === promise)
|
|
266
|
+
inFlight.delete(widget.code);
|
|
267
|
+
};
|
|
268
|
+
promise.then(done, done);
|
|
269
|
+
return promise;
|
|
270
|
+
},
|
|
271
|
+
cached: code => cache.get(code),
|
|
272
|
+
prime: (code, data) => keep(code, data),
|
|
273
|
+
isFresh(widget) {
|
|
274
|
+
const kept = cache.get(widget.code);
|
|
275
|
+
if (!kept)
|
|
276
|
+
return false;
|
|
277
|
+
return widget.refresh === undefined || widget.refresh <= 0 || now() - kept.at < widget.refresh * 1000;
|
|
278
|
+
},
|
|
279
|
+
clear(codes) {
|
|
280
|
+
if (!codes)
|
|
281
|
+
cache.clear();
|
|
282
|
+
else
|
|
283
|
+
codes.forEach(code => cache.delete(code));
|
|
284
|
+
},
|
|
285
|
+
get calls() {
|
|
286
|
+
return calls;
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
return batcher;
|
|
290
|
+
}
|
|
291
|
+
/** At most this many days of one mark are drawn, so a broken range never fills a year of cells. */
|
|
292
|
+
const MAX_MARK_DAYS = 366;
|
|
293
|
+
/** Each day's marks, by ISO date: a mark over several days is on each of them. A mark with a broken date is left out. */
|
|
294
|
+
export function marksByDay(marks) {
|
|
295
|
+
const days = new Map();
|
|
296
|
+
for (const mark of marks) {
|
|
297
|
+
if (!isIsoDate(mark.date))
|
|
298
|
+
continue;
|
|
299
|
+
const last = mark.until && isIsoDate(mark.until) && mark.until > mark.date ? mark.until : mark.date;
|
|
300
|
+
let day = mark.date;
|
|
301
|
+
for (let count = 0; day <= last && count < MAX_MARK_DAYS; count++, day = addDays(day, 1))
|
|
302
|
+
days.set(day, [...(days.get(day) ?? []), mark]);
|
|
303
|
+
}
|
|
304
|
+
return days;
|
|
305
|
+
}
|