nexus-shared 4.0.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 +795 -0
- package/README.md +220 -14
- package/dist/Client.Index.d.ts +13 -0
- package/dist/Client.Index.js +13 -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/TabButtons.d.ts +6 -0
- package/dist/Components/Documents/TabButtons.js +5 -4
- package/dist/Components/Domains/DomainCard.d.ts +98 -0
- package/dist/Components/Domains/DomainCard.js +182 -0
- package/dist/Components/Forms/ApiForm.js +5 -1
- package/dist/Components/Forms/Crud.js +72 -24
- package/dist/Components/Forms/CrudActions.d.ts +5 -4
- package/dist/Components/Forms/CrudActions.js +42 -12
- package/dist/Components/Forms/CrudHistory.d.ts +6 -1
- package/dist/Components/Forms/CrudHistory.js +14 -6
- package/dist/Components/Forms/CrudParts.d.ts +11 -4
- package/dist/Components/Forms/CrudParts.js +76 -10
- package/dist/Components/Forms/CrudRowForms.js +65 -5
- package/dist/Components/Forms/CrudTree.js +7 -7
- package/dist/Components/Forms/CrudWorkspace.d.ts +15 -5
- package/dist/Components/Forms/CrudWorkspace.js +19 -11
- package/dist/Components/Forms/Form.d.ts +3 -2
- package/dist/Components/Forms/Form.js +33 -26
- package/dist/Components/Forms/FormQuestion.d.ts +6 -0
- package/dist/Components/Forms/FormQuestion.js +28 -0
- package/dist/Components/Forms/SubmitForm.js +2 -15
- package/dist/Components/Inputs/ComputedInput.d.ts +51 -0
- package/dist/Components/Inputs/ComputedInput.js +179 -0
- package/dist/Components/Inputs/Dropdown.js +4 -2
- package/dist/Components/Inputs/GroupForm.js +3 -2
- package/dist/Components/Inputs/InputRenderer.d.ts +5 -1
- package/dist/Components/Inputs/MarkdownEditor.d.ts +6 -0
- package/dist/Components/Inputs/MarkdownEditor.js +32 -6
- package/dist/Components/Inputs/PictureInput.js +8 -3
- package/dist/Components/Inputs/RowsInput.d.ts +14 -1
- package/dist/Components/Inputs/RowsInput.js +78 -5
- package/dist/Components/Inputs/TabularForm.js +16 -3
- package/dist/Components/Inputs/TreeView.d.ts +24 -1
- package/dist/Components/Inputs/TreeView.js +84 -7
- package/dist/Components/Layouts/Popup.d.ts +22 -2
- package/dist/Components/Layouts/Popup.js +108 -6
- 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 +7 -5
- package/dist/Components/Viewers/DataTableColumns.d.ts +2 -2
- package/dist/Components/Viewers/DataTableColumns.js +2 -1
- package/dist/Components/Viewers/DataTableParts.d.ts +3 -1
- package/dist/Components/Viewers/DataTableParts.js +2 -2
- package/dist/Components/Viewers/OwnerFiles.d.ts +57 -5
- package/dist/Components/Viewers/OwnerFiles.js +142 -31
- 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/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/CrudColumns.d.ts +44 -6
- package/dist/Helpers/CrudColumns.js +63 -13
- package/dist/Helpers/CrudHelpers.d.ts +36 -1
- package/dist/Helpers/CrudHelpers.js +61 -0
- package/dist/Helpers/CrudPickerPlan.d.ts +58 -0
- package/dist/Helpers/CrudPickerPlan.js +46 -0
- package/dist/Helpers/CrudRowForms.d.ts +18 -1
- package/dist/Helpers/CrudRowForms.js +35 -2
- 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/FileAdapter.d.ts +82 -2
- package/dist/Helpers/FileAdapter.js +72 -1
- package/dist/Helpers/FormStore.d.ts +11 -1
- package/dist/Helpers/FormStore.js +66 -19
- package/dist/Helpers/InputConditions.d.ts +6 -0
- package/dist/Helpers/InputConditions.js +24 -0
- package/dist/Helpers/InputMemory.js +40 -30
- package/dist/Helpers/InputNarrowing.d.ts +38 -6
- package/dist/Helpers/InputNarrowing.js +102 -21
- 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 +17 -1
- package/dist/Helpers/NexusDriveAdapter.js +32 -3
- package/dist/Helpers/NumberHelpers.d.ts +61 -0
- package/dist/Helpers/NumberHelpers.js +124 -8
- package/dist/Helpers/PagedPicker.d.ts +42 -0
- package/dist/Helpers/PagedPicker.js +85 -0
- package/dist/Helpers/RowsStore.d.ts +24 -0
- package/dist/Helpers/RowsStore.js +79 -10
- package/dist/Helpers/TableHelpers.d.ts +15 -2
- package/dist/Helpers/TableHelpers.js +28 -1
- package/dist/Helpers/TableStore.d.ts +7 -3
- package/dist/Helpers/TableStore.js +9 -4
- package/dist/Helpers/ToolbarFit.d.ts +23 -0
- package/dist/Helpers/ToolbarFit.js +30 -0
- package/dist/Helpers/TreeStore.d.ts +75 -6
- package/dist/Helpers/TreeStore.js +182 -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 +78 -5
- 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 +21 -0
- package/dist/Interfaces/FileInterfaces.js +2 -0
- package/dist/Interfaces/FormInterfaces.d.ts +78 -11
- package/dist/Interfaces/InputInterfaces.d.ts +12 -5
- package/dist/Interfaces/MailInterfaces.d.ts +192 -0
- package/dist/Interfaces/MailInterfaces.js +1 -0
- package/dist/Interfaces/TableInterfaces.d.ts +28 -2
- package/dist/Services/JoinSources.d.ts +47 -6
- package/dist/Services/JoinSources.js +68 -33
- package/dist/Services/MessageApi.d.ts +18 -0
- package/dist/Services/MessageApi.js +64 -0
- package/dist/Shared.Index.d.ts +12 -0
- package/dist/Shared.Index.js +12 -0
- package/package.json +7 -1
- package/src/Styles/Nexus.Button.css +29 -0
- package/src/Styles/Nexus.Chat.css +114 -22
- package/src/Styles/Nexus.Crud.css +119 -28
- package/src/Styles/Nexus.Dashboard.css +763 -0
- package/src/Styles/Nexus.Domain.css +384 -0
- package/src/Styles/Nexus.Editor.css +20 -0
- package/src/Styles/Nexus.Files.css +27 -0
- package/src/Styles/Nexus.Index.css +3 -0
- package/src/Styles/Nexus.Input.css +31 -0
- package/src/Styles/Nexus.Mail.css +1062 -0
- package/src/Styles/Nexus.Popup.css +58 -0
- package/src/Styles/Nexus.Table.css +1458 -1449
- package/src/Styles/Nexus.Theme.Picker.css +1 -0
- package/src/Styles/Nexus.Tree.css +10 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { ChatDomainInfo, ChatDomainValue, ChatId } from "../Interfaces/ChatInterfaces.ts";
|
|
2
|
+
import type { DatePreferences } from "../Interfaces/DateInterfaces.ts";
|
|
3
|
+
import type { DomainCardContext, DomainCardDefinition, DomainCardField, DomainCardParams, DomainCardShape, DomainCardSnapshot, DomainCardValueKind } from "../Interfaces/DomainInterfaces.ts";
|
|
4
|
+
/** How long a card's answer is kept for every card of the page that draws the same record, in milliseconds. */
|
|
5
|
+
export declare const DOMAIN_CARD_LIFETIME = 30000;
|
|
6
|
+
/**
|
|
7
|
+
* A kind's card, with `view` typed by what `load` answers.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* defineChatDomains([{ domain: EDomainTypes.LeaveRequest, key: "leave-request", label: "Leave request", card: domainCard({
|
|
11
|
+
* load: (id, signal) => api.get<LeaveRequestCard>(REQUEST_CARD.leave(id), { signal }),
|
|
12
|
+
* view: leave => ({ title: `${leave.leaveTypeName} · ${formatDays(leave.days)}`, code: leave.requestCode, ... }),
|
|
13
|
+
* }) }]);
|
|
14
|
+
*/
|
|
15
|
+
export declare function domainCard<TData>(definition: DomainCardDefinition<TData>): DomainCardDefinition<TData>;
|
|
16
|
+
/** A kind's registered row and its card, or `undefined` when the app gave it none. */
|
|
17
|
+
export declare function domainCardOf(domainType: ChatDomainValue): {
|
|
18
|
+
info: ChatDomainInfo<ChatDomainValue>;
|
|
19
|
+
card: DomainCardDefinition;
|
|
20
|
+
} | undefined;
|
|
21
|
+
/** The key a record's answer is kept under: its kind, its id, and who reads it (the same record reads otherwise for another). */
|
|
22
|
+
export declare function domainCardKey(domainType: ChatDomainValue, id: ChatId, viewer?: ChatId | null): string;
|
|
23
|
+
/** The start of every key of one record (every reader's), or of every record of a kind without an id. */
|
|
24
|
+
export declare function domainCardRecordPrefix(domainType: ChatDomainValue, id?: ChatId): string;
|
|
25
|
+
/** One wait for a read: its answer, and a way to stop waiting (the last to stop aborts it). */
|
|
26
|
+
export interface DomainCardReading<T> {
|
|
27
|
+
promise: Promise<T>;
|
|
28
|
+
leave(): void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Reads shared by every card on the page: one read of a record at a time, its answer kept for a lifetime, a failure not
|
|
32
|
+
* kept. A read nobody waits for any more is aborted.
|
|
33
|
+
*/
|
|
34
|
+
export declare class DomainCardReads {
|
|
35
|
+
private entries;
|
|
36
|
+
private now;
|
|
37
|
+
constructor(now?: () => number);
|
|
38
|
+
/** The answer kept under `key` while it is younger than `lifetime` and was read from a read started at or after `since`. */
|
|
39
|
+
peek<T>(key: string, lifetime?: number, since?: number): T | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* The answer under `key`: a read on its way that started at or after `since`, else a kept fresh one, else a new read by
|
|
42
|
+
* `load`. `since` is the moment the record was seen to change: what was read before it does not answer.
|
|
43
|
+
*/
|
|
44
|
+
read<T>(key: string, load: (signal: AbortSignal) => Promise<T>, options?: {
|
|
45
|
+
lifetime?: number;
|
|
46
|
+
since?: number;
|
|
47
|
+
}): DomainCardReading<T>;
|
|
48
|
+
private wait;
|
|
49
|
+
/**
|
|
50
|
+
* Drops what is kept or on its way under keys that start with `prefix` (every key when none); a read on its way still
|
|
51
|
+
* answers its waiters. `keptOnly` leaves the reads on their way, so cards reading again for the same change still share one.
|
|
52
|
+
*/
|
|
53
|
+
drop(prefix?: string, keptOnly?: boolean): number;
|
|
54
|
+
/** Answers kept and reads on their way. */
|
|
55
|
+
get size(): number;
|
|
56
|
+
}
|
|
57
|
+
/** The reads every card of the page shares. */
|
|
58
|
+
export declare const domainCardReads: DomainCardReads;
|
|
59
|
+
/** A read the page called off (the card closed): not a failure to show. */
|
|
60
|
+
export declare function isDomainCardAbort(error: unknown): boolean;
|
|
61
|
+
/** Whether a failure means the reader may not open the record, or it is not there for them: 403 and 404 alike. */
|
|
62
|
+
export declare function isDomainCardRefusal(error: unknown): boolean;
|
|
63
|
+
/** What a run reads and how. */
|
|
64
|
+
export interface DomainCardRunOptions<TData> {
|
|
65
|
+
domainType: ChatDomainValue;
|
|
66
|
+
id: ChatId;
|
|
67
|
+
/** Who reads: part of the key, and told to `load`. */
|
|
68
|
+
viewer?: ChatId | null;
|
|
69
|
+
load: (signal: AbortSignal) => Promise<TData>;
|
|
70
|
+
/** Default 30,000. */
|
|
71
|
+
lifetime?: number;
|
|
72
|
+
/** Default: the page's `domainCardReads`. */
|
|
73
|
+
reads?: DomainCardReads;
|
|
74
|
+
now?: () => number;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* One card's read: loading, then the record, a refusal or a failure; read again when the record changes (the card stays
|
|
78
|
+
* on screen meanwhile, and says so when the record turned out different); tried again on Retry. Subscribe to draw it.
|
|
79
|
+
*/
|
|
80
|
+
export declare class DomainCardRun<TData = unknown> {
|
|
81
|
+
readonly domainType: ChatDomainValue;
|
|
82
|
+
readonly id: ChatId;
|
|
83
|
+
readonly key: string;
|
|
84
|
+
private options;
|
|
85
|
+
private reads;
|
|
86
|
+
private now;
|
|
87
|
+
private snapshot;
|
|
88
|
+
private listeners;
|
|
89
|
+
private reading;
|
|
90
|
+
private round;
|
|
91
|
+
constructor(options: DomainCardRunOptions<TData>);
|
|
92
|
+
private get lifetime();
|
|
93
|
+
readonly subscribe: (listener: () => void) => (() => void);
|
|
94
|
+
readonly getSnapshot: () => DomainCardSnapshot<TData>;
|
|
95
|
+
private set;
|
|
96
|
+
/** The card is drawn: reads, unless it already shows a kept answer. */
|
|
97
|
+
start(): void;
|
|
98
|
+
/** The card is gone: stops waiting (the read is aborted when no other card waits for it). */
|
|
99
|
+
stop(): void;
|
|
100
|
+
/**
|
|
101
|
+
* The record changed: reads it again, from a read started now. The card on screen stays until the answer lands, and
|
|
102
|
+
* what other readers kept of the record is forgotten, since it changed for them too.
|
|
103
|
+
*/
|
|
104
|
+
reload(): void;
|
|
105
|
+
/** After a failure: reads once more. */
|
|
106
|
+
retry(): void;
|
|
107
|
+
private read;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* A record changed (the page approved it, a message said so): every card drawing it reads it again, and no answer kept
|
|
111
|
+
* from before is handed out. Without an id, every record of the kind.
|
|
112
|
+
*/
|
|
113
|
+
export declare function refreshDomainCards(domainType: ChatDomainValue, id?: ChatId, reads?: DomainCardReads): number;
|
|
114
|
+
/** The kind's name inside a sentence: "Leave request" reads "leave request"; "HR letter" keeps its capitals. */
|
|
115
|
+
export declare function domainCardNoun(kind?: string | null): string;
|
|
116
|
+
/** What a reader the record's module refused reads: the same for "not yours" and "not there", so a stranger learns nothing. */
|
|
117
|
+
export declare function domainCardRefusedText(kind?: string | null): string;
|
|
118
|
+
/** What a failed read says, beside Try again. */
|
|
119
|
+
export declare function domainCardFailedText(kind?: string | null): string;
|
|
120
|
+
/** What a card's values are written with: the reader's date settings and zone, number locale and currency. */
|
|
121
|
+
export interface DomainCardValueOptions {
|
|
122
|
+
preferences?: Partial<DatePreferences>;
|
|
123
|
+
timeZone?: string | null;
|
|
124
|
+
locale?: string;
|
|
125
|
+
currency?: string | null;
|
|
126
|
+
}
|
|
127
|
+
/** A value written by its kind, or "" when it is missing. A value its kind cannot read is written as it is. */
|
|
128
|
+
export declare function domainCardValueText(value: string | number | null | undefined, kind?: DomainCardValueKind, options?: DomainCardValueOptions): string;
|
|
129
|
+
/** The fields a card draws: the ones with a value, and the ones that say what stands in for a missing one. */
|
|
130
|
+
export declare function domainCardFields(params: Pick<DomainCardParams, "fields">): DomainCardField[];
|
|
131
|
+
/** What a placeholder holds when a kind gave nothing and none of its cards has loaded yet: a title, a state, four fields. */
|
|
132
|
+
export declare const DEFAULT_DOMAIN_CARD_SHAPE: DomainCardShape;
|
|
133
|
+
/** How many of each part a card drew. */
|
|
134
|
+
export declare function domainCardShape(params: DomainCardParams): DomainCardShape;
|
|
135
|
+
/** Keeps a kind's last drawn shape, so its next placeholder holds the same parts. */
|
|
136
|
+
export declare function rememberDomainCardShape(domainType: ChatDomainValue, shape: DomainCardShape): void;
|
|
137
|
+
/** The shape a kind's placeholder holds: its last card's, else what its card says, else the default. */
|
|
138
|
+
export declare function heldDomainCardShape(domainType: ChatDomainValue, card?: Pick<DomainCardDefinition, "held"> | null): DomainCardShape;
|
|
139
|
+
/** What `load`, `view` and `extra` are told about a record. */
|
|
140
|
+
export declare function domainCardContext(domainType: ChatDomainValue, id: ChatId, extra?: Omit<DomainCardContext, "domainType" | "id">): DomainCardContext;
|