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,96 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The words of a record's history entry (Components/Forms/CrudHistory.tsx) that are worked out rather than drawn, so Node
|
|
3
|
+
* tests run them, and the app's way of naming the people behind the entries' tokens. Only `setAuditActorNames` and its
|
|
4
|
+
* type are exported from the package.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* A token, short. It names nobody and only tells two apart, so its first characters are enough. A token is the same
|
|
8
|
+
* text for one id on every entry this reader is given (the id is encrypted for the reader, not at random).
|
|
9
|
+
*/
|
|
10
|
+
export const shortToken = (token) => (token.length > 8 ? `${token.slice(0, 6)}…` : token);
|
|
11
|
+
let actorResolver = null;
|
|
12
|
+
/** What the app answered, by token, for the page's life: a second history asks only for the people it has not seen. */
|
|
13
|
+
const actorNames = new Map();
|
|
14
|
+
/** Tokens the app answered nothing for: not asked again, so a history full of one stranger never asks on every page. */
|
|
15
|
+
const actorUnknown = new Set();
|
|
16
|
+
/**
|
|
17
|
+
* Sets, once for the app, how a record's history (`AuditHistory`) names the person behind an entry its answer names
|
|
18
|
+
* nobody for (no `creatorName`). The history collects the tokens of every page it shows (the first and each "Show
|
|
19
|
+
* more"), asks once per page for those it has had no answer for, and says "by Sita Rai (sita.rai)" for each one
|
|
20
|
+
* answered. A token is the same text for one person on every entry this reader is given, so the app can look it up (its
|
|
21
|
+
* own list of people, or a call to the server that made it). A call that fails keeps the line's "User 3fA9b2…" and is
|
|
22
|
+
* asked again by the next history; a token left out of the answer keeps it for the page's life. Answers are kept for the
|
|
23
|
+
* page's life; `null` stops asking and forgets them.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* setAuditActorNames(async (tokens, signal) => {
|
|
27
|
+
* const answer = await api.post<Record<string, string>>(USERS.names, tokens, { signal, toast: false });
|
|
28
|
+
* return answer.ok ? answer.result : {};
|
|
29
|
+
* });
|
|
30
|
+
*/
|
|
31
|
+
export function setAuditActorNames(resolver) {
|
|
32
|
+
actorResolver = resolver;
|
|
33
|
+
actorNames.clear();
|
|
34
|
+
actorUnknown.clear();
|
|
35
|
+
}
|
|
36
|
+
/** The name the app gave for a token, once it has given one. */
|
|
37
|
+
export const knownAuditActorName = (token) => (token ? actorNames.get(token) : undefined);
|
|
38
|
+
/**
|
|
39
|
+
* The tokens of these entries the app is still to be asked for: entries with no name in the answer, whose token has had
|
|
40
|
+
* no answer yet, each token once. Empty without a resolver.
|
|
41
|
+
*/
|
|
42
|
+
export function unnamedAuditActors(entries) {
|
|
43
|
+
if (!actorResolver)
|
|
44
|
+
return [];
|
|
45
|
+
const tokens = new Set();
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
const token = entry.creatorUserId;
|
|
48
|
+
if (!token || entry.creatorName?.trim() || actorNames.has(token) || actorUnknown.has(token))
|
|
49
|
+
continue;
|
|
50
|
+
tokens.add(token);
|
|
51
|
+
}
|
|
52
|
+
return [...tokens];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Asks the app, in one call, for the names of these entries' unnamed people (`unnamedAuditActors`) and keeps the answer.
|
|
56
|
+
* Resolves `true` when it learnt a name. A failed or aborted call keeps nothing, so a later history asks again.
|
|
57
|
+
*/
|
|
58
|
+
export async function resolveAuditActors(entries, signal) {
|
|
59
|
+
const resolver = actorResolver;
|
|
60
|
+
const tokens = unnamedAuditActors(entries);
|
|
61
|
+
if (!resolver || tokens.length === 0)
|
|
62
|
+
return false;
|
|
63
|
+
let answer;
|
|
64
|
+
try {
|
|
65
|
+
answer = await resolver(tokens, signal);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
// The history went away, or the app set another resolver meanwhile: this answer is not kept.
|
|
71
|
+
if (signal.aborted || resolver !== actorResolver)
|
|
72
|
+
return false;
|
|
73
|
+
let learnt = false;
|
|
74
|
+
for (const token of tokens) {
|
|
75
|
+
const given = answer?.[token];
|
|
76
|
+
const name = typeof given === "string" ? given.trim() : "";
|
|
77
|
+
if (name) {
|
|
78
|
+
actorNames.set(token, name);
|
|
79
|
+
learnt = true;
|
|
80
|
+
}
|
|
81
|
+
else
|
|
82
|
+
actorUnknown.add(token);
|
|
83
|
+
}
|
|
84
|
+
return learnt;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Who made a change, as the entry's line says it: "by Sita Rai" when the history's answer names them (`creatorName`) or
|
|
88
|
+
* the app has named their token (`setAuditActorNames`), else the start of their token ("User 3fA9b2…"), which still
|
|
89
|
+
* tells two people apart; `null` when it says neither.
|
|
90
|
+
*/
|
|
91
|
+
export function auditActorText(entry) {
|
|
92
|
+
const name = entry.creatorName?.trim() || knownAuditActorName(entry.creatorUserId);
|
|
93
|
+
if (name)
|
|
94
|
+
return `by ${name}`;
|
|
95
|
+
return entry.creatorUserId ? `User ${shortToken(entry.creatorUserId)}` : null;
|
|
96
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether to say that something is still under way: `true` only once `busy` has lasted 400ms, and then for at least
|
|
3
|
+
* 300ms, however soon `busy` ends. A status line that reads it says "Loading…" for a slow read and nothing for a quick
|
|
4
|
+
* one. A Crud's details footer and `DocumentFrame` use it.
|
|
5
|
+
*/
|
|
6
|
+
export declare function useSlowBusy(busy: boolean): boolean;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* A read that ends sooner than this says nothing: most backends answer sooner, and a word shown for a frame or two is
|
|
5
|
+
* the flicker this avoids. Longer than a popup's 160ms fade, so the word never lands on a half-faded popup.
|
|
6
|
+
*/
|
|
7
|
+
const SLOW_DELAY = 400;
|
|
8
|
+
/** Once the word shows, it stays at least this long, so a read that ends just after the delay does not blink it. */
|
|
9
|
+
const SLOW_HOLD = 300;
|
|
10
|
+
/**
|
|
11
|
+
* Whether to say that something is still under way: `true` only once `busy` has lasted 400ms, and then for at least
|
|
12
|
+
* 300ms, however soon `busy` ends. A status line that reads it says "Loading…" for a slow read and nothing for a quick
|
|
13
|
+
* one. A Crud's details footer and `DocumentFrame` use it.
|
|
14
|
+
*/
|
|
15
|
+
export function useSlowBusy(busy) {
|
|
16
|
+
const [said, setSaid] = useState(false);
|
|
17
|
+
const since = useRef(0);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (busy) {
|
|
20
|
+
if (said)
|
|
21
|
+
return;
|
|
22
|
+
const timer = window.setTimeout(() => {
|
|
23
|
+
since.current = performance.now();
|
|
24
|
+
setSaid(true);
|
|
25
|
+
}, SLOW_DELAY);
|
|
26
|
+
return () => window.clearTimeout(timer);
|
|
27
|
+
}
|
|
28
|
+
if (!said)
|
|
29
|
+
return;
|
|
30
|
+
const left = since.current + SLOW_HOLD - performance.now();
|
|
31
|
+
if (left <= 0) {
|
|
32
|
+
setSaid(false);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const timer = window.setTimeout(() => setSaid(false), left);
|
|
36
|
+
return () => window.clearTimeout(timer);
|
|
37
|
+
}, [busy, said]);
|
|
38
|
+
return said;
|
|
39
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ApiEndpoint, type ApiModuleValue } from "../Interfaces/ApiInterfaces.ts";
|
|
2
|
-
import type { AppChatDomain, ChatConversation, ChatDomainInfo, ChatDomainValue, ChatDraft, ChatHistory, ChatHistoryRequest, ChatId, ChatMessage, ChatParticipant, ChatParticipantPage, ChatParticipantQuery, ChatScope, ChatSummary, NewChatRequest } from "../Interfaces/ChatInterfaces.ts";
|
|
2
|
+
import type { AppChatDomain, ChatConversation, ChatDomainInfo, ChatDomainValue, ChatDraft, ChatHistory, ChatHistoryRequest, ChatId, ChatMessage, ChatMessageKind, ChatParticipant, ChatParticipantPage, ChatParticipantQuery, ChatScope, ChatSummary, NewChatRequest } from "../Interfaces/ChatInterfaces.ts";
|
|
3
3
|
/** The actions a chat needs from a backend. A backend that does not serve one takes that feature off the panel. */
|
|
4
4
|
export declare const CHAT_ACTIONS: {
|
|
5
5
|
/** The conversations a record holds, for this reader. */
|
|
@@ -106,6 +106,11 @@ export declare class NexusChatBackend<TDomain extends ChatDomainValue = AppChatD
|
|
|
106
106
|
protected toConversation(row: Row): ChatConversation;
|
|
107
107
|
/** One message row. The names are the ones a Nexus contract uses; override this alone for a backend that differs. */
|
|
108
108
|
protected toMessage(row: Row): ChatMessage;
|
|
109
|
+
/**
|
|
110
|
+
* What a message is, from the backend's `EChatMessageTypes` (0 a chat line, 1 the mail a thread opens with, 2 a
|
|
111
|
+
* notification) or a word. Anything else is a chat line.
|
|
112
|
+
*/
|
|
113
|
+
protected toKind(value: unknown): ChatMessageKind;
|
|
109
114
|
/** One set of counts. */
|
|
110
115
|
protected toSummary(row: Row): ChatSummary;
|
|
111
116
|
/** One participant row. */
|
|
@@ -125,6 +130,8 @@ export declare function getChatBackend(): ChatBackend<any>;
|
|
|
125
130
|
* { domain: EDomainTypes.Leave, key: "leave", label: "Leave request" },
|
|
126
131
|
* { domain: EDomainTypes.Procurement, key: "procurement", label: "Purchase order" },
|
|
127
132
|
* ]);
|
|
133
|
+
*
|
|
134
|
+
* A row's `card` shows a message's record as a card read live from its module (`domainCard`, `DomainCard`).
|
|
128
135
|
*/
|
|
129
136
|
export declare function defineChatDomains<TDomain extends ChatDomainValue>(list: readonly ChatDomainInfo<TDomain>[]): readonly ChatDomainInfo<TDomain>[];
|
|
130
137
|
/** The registered domains. Empty until an app calls `defineChatDomains`. */
|
|
@@ -254,10 +254,23 @@ export class NexusChatBackend extends ChatBackend {
|
|
|
254
254
|
})),
|
|
255
255
|
}),
|
|
256
256
|
...(row.isSystem === true && { isSystem: true }),
|
|
257
|
+
...((row.messageType ?? row.kind) !== undefined && { kind: this.toKind(row.messageType ?? row.kind) }),
|
|
257
258
|
...(Array.isArray(row.deliveredTo) && { deliveredTo: row.deliveredTo.map(id).filter((value) => value !== null) }),
|
|
258
259
|
...(Array.isArray(row.readBy) && { readBy: row.readBy.map(id).filter((value) => value !== null) }),
|
|
259
260
|
};
|
|
260
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* What a message is, from the backend's `EChatMessageTypes` (0 a chat line, 1 the mail a thread opens with, 2 a
|
|
264
|
+
* notification) or a word. Anything else is a chat line.
|
|
265
|
+
*/
|
|
266
|
+
toKind(value) {
|
|
267
|
+
const written = String(value).toLowerCase();
|
|
268
|
+
if (written === "1" || written === "mail")
|
|
269
|
+
return "mail";
|
|
270
|
+
if (written === "2" || written === "notification")
|
|
271
|
+
return "notification";
|
|
272
|
+
return "chat";
|
|
273
|
+
}
|
|
261
274
|
/** One set of counts. */
|
|
262
275
|
toSummary(row) {
|
|
263
276
|
const lastText = text(row.lastMessageText ?? row.lastMessage ?? row.lastBody);
|
|
@@ -309,6 +322,8 @@ let domains = [];
|
|
|
309
322
|
* { domain: EDomainTypes.Leave, key: "leave", label: "Leave request" },
|
|
310
323
|
* { domain: EDomainTypes.Procurement, key: "procurement", label: "Purchase order" },
|
|
311
324
|
* ]);
|
|
325
|
+
*
|
|
326
|
+
* A row's `card` shows a message's record as a card read live from its module (`domainCard`, `DomainCard`).
|
|
312
327
|
*/
|
|
313
328
|
export function defineChatDomains(list) {
|
|
314
329
|
const seen = new Set();
|
|
@@ -317,6 +332,9 @@ export function defineChatDomains(list) {
|
|
|
317
332
|
if (seen.has(key))
|
|
318
333
|
throw new Error(`Two chat domains have the value ${key}.`);
|
|
319
334
|
seen.add(key);
|
|
335
|
+
if (info.card && (typeof info.card.load !== "function" || typeof info.card.view !== "function")) {
|
|
336
|
+
throw new Error(`The chat domain ${key}'s card needs a load and a view.`);
|
|
337
|
+
}
|
|
320
338
|
}
|
|
321
339
|
domains = list;
|
|
322
340
|
return list;
|
|
@@ -77,6 +77,8 @@ export interface ChatRun {
|
|
|
77
77
|
mine: boolean;
|
|
78
78
|
/** Written by the system: a centered note, with no avatar and no side. */
|
|
79
79
|
system: boolean;
|
|
80
|
+
/** Notifications (`kind: "notification"`): event lines, with the author's name and no side. */
|
|
81
|
+
notification: boolean;
|
|
80
82
|
/** The "New messages" line goes before this run. */
|
|
81
83
|
startsUnread: boolean;
|
|
82
84
|
messages: readonly ChatMessage[];
|
|
@@ -225,10 +225,12 @@ export function groupChatMessages(messages, options) {
|
|
|
225
225
|
}
|
|
226
226
|
const at = Date.parse(message.createdAt);
|
|
227
227
|
const system = message.isSystem === true;
|
|
228
|
+
const notification = !system && message.kind === "notification";
|
|
228
229
|
const unread = unreadFrom !== null && sameChatId(message.id, unreadFrom);
|
|
229
230
|
const joins = run !== null &&
|
|
230
231
|
!unread &&
|
|
231
232
|
run.system === system &&
|
|
233
|
+
run.notification === notification &&
|
|
232
234
|
sameChatId(run.authorId, message.authorId) &&
|
|
233
235
|
Number.isFinite(at) &&
|
|
234
236
|
Number.isFinite(run.last) &&
|
|
@@ -242,8 +244,9 @@ export function groupChatMessages(messages, options) {
|
|
|
242
244
|
authorId: message.authorId,
|
|
243
245
|
authorName: message.authorName?.trim() || (system ? "" : CHAT_UNKNOWN_AUTHOR),
|
|
244
246
|
authorAvatar: message.authorAvatar ?? null,
|
|
245
|
-
mine: !system && sameChatId(message.authorId, userId),
|
|
247
|
+
mine: !system && !notification && sameChatId(message.authorId, userId),
|
|
246
248
|
system,
|
|
249
|
+
notification,
|
|
247
250
|
startsUnread: unread,
|
|
248
251
|
messages: [message],
|
|
249
252
|
last: Number.isFinite(at) ? at : 0,
|
|
@@ -105,10 +105,19 @@ export declare abstract class CrudBackend {
|
|
|
105
105
|
answeredRow<TRow extends TableRow>(result: unknown): Partial<TRow> | null;
|
|
106
106
|
/**
|
|
107
107
|
* The key an answer gives in place of the record: `Create` answering the new record's id. `null` when the answer
|
|
108
|
-
* is the record itself, or says nothing. With a key the new record goes straight into the list,
|
|
109
|
-
* a
|
|
108
|
+
* is the record itself, or says nothing. With a key the new record goes straight into the list, finished by `savedRow`;
|
|
109
|
+
* only a record whose list still cannot be told is read back on its own through `details` - never the whole list again.
|
|
110
110
|
*/
|
|
111
111
|
answeredKey(result: unknown): TableKey | null;
|
|
112
|
+
/**
|
|
113
|
+
* A record a save wrote - what the form sent, under what the answer said - with the fields only the server keeps
|
|
114
|
+
* filled in as this backend writes them, so nothing has to be read back: a status, the times, the marks. `before` is
|
|
115
|
+
* the record as the page holds it now (an edit; `undefined` for a new one): an edit's form carries its own copy of those
|
|
116
|
+
* fields from when it opened, which a flag or a mark made since has left behind. Default: the record as it is - and a
|
|
117
|
+
* record whose list still cannot be told from it (`listMembership`) is read back once through `details`, where there is
|
|
118
|
+
* one.
|
|
119
|
+
*/
|
|
120
|
+
savedRow<TRow extends TableRow>(action: "create" | "edit", record: TRow, before?: TRow): TRow;
|
|
112
121
|
/**
|
|
113
122
|
* Whether a record belongs in a list, read from the record itself. It is what lets one answer be written into the
|
|
114
123
|
* lists that are **not** on screen: a flagged record joins the flagged list, a trashed one leaves the active list
|
|
@@ -176,6 +185,15 @@ export declare class NexusCrudBackend extends CrudBackend {
|
|
|
176
185
|
answeredRow<TRow extends TableRow>(result: unknown): Partial<TRow> | null;
|
|
177
186
|
/** `Create` and `Update` answer the record's id: a token when ids are encrypted, the number as text when not. */
|
|
178
187
|
answeredKey(result: unknown): TableKey | null;
|
|
188
|
+
/**
|
|
189
|
+
* What a Nexus save leaves to the server, written as the server writes it, so nothing is read back (the owner,
|
|
190
|
+
* 2026-10-10: "the creationTime, recordStatus we will provide fake or just right now"). A new record lacking them is
|
|
191
|
+
* Active (every Create writes `NEXUS_ACTIVE_STATUS`), has no marks, was added now and never changed; what its answer
|
|
192
|
+
* said stays. An Update writes none of the status, the marks and the time added, so an edited record has the ones the
|
|
193
|
+
* page holds (`before`) - over its form's older copies - and was changed now. Times are UTC ISO strings, as a date-time
|
|
194
|
+
* column reads them.
|
|
195
|
+
*/
|
|
196
|
+
savedRow<TRow extends TableRow>(action: "create" | "edit", record: TRow, before?: TRow): TRow;
|
|
179
197
|
/**
|
|
180
198
|
* The bands the owner settled on: under 40 is live, 25–29 of that is flagged, 40–59 is the trash, and 60 and up is
|
|
181
199
|
* deleted. A row without a `recordStatus` says nothing, and the lists it cannot answer for load again.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CRUD_MARK_BITS, hasMark, withMark } from "./CrudHelpers.js";
|
|
2
|
+
import { nowUtcDateTime } from "./DateTimeHelpers.js";
|
|
2
3
|
const NO_AUDIT_WORDS = { commands: {}, directions: {}, parts: {} };
|
|
3
4
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
5
|
export class CrudBackend {
|
|
@@ -117,13 +118,26 @@ export class CrudBackend {
|
|
|
117
118
|
}
|
|
118
119
|
/**
|
|
119
120
|
* The key an answer gives in place of the record: `Create` answering the new record's id. `null` when the answer
|
|
120
|
-
* is the record itself, or says nothing. With a key the new record goes straight into the list,
|
|
121
|
-
* a
|
|
121
|
+
* is the record itself, or says nothing. With a key the new record goes straight into the list, finished by `savedRow`;
|
|
122
|
+
* only a record whose list still cannot be told is read back on its own through `details` - never the whole list again.
|
|
122
123
|
*/
|
|
123
124
|
answeredKey(result) {
|
|
124
125
|
void result;
|
|
125
126
|
return null;
|
|
126
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* A record a save wrote - what the form sent, under what the answer said - with the fields only the server keeps
|
|
130
|
+
* filled in as this backend writes them, so nothing has to be read back: a status, the times, the marks. `before` is
|
|
131
|
+
* the record as the page holds it now (an edit; `undefined` for a new one): an edit's form carries its own copy of those
|
|
132
|
+
* fields from when it opened, which a flag or a mark made since has left behind. Default: the record as it is - and a
|
|
133
|
+
* record whose list still cannot be told from it (`listMembership`) is read back once through `details`, where there is
|
|
134
|
+
* one.
|
|
135
|
+
*/
|
|
136
|
+
savedRow(action, record, before) {
|
|
137
|
+
void action;
|
|
138
|
+
void before;
|
|
139
|
+
return record;
|
|
140
|
+
}
|
|
127
141
|
/**
|
|
128
142
|
* Whether a record belongs in a list, read from the record itself. It is what lets one answer be written into the
|
|
129
143
|
* lists that are **not** on screen: a flagged record joins the flagged list, a trashed one leaves the active list
|
|
@@ -328,6 +342,36 @@ export class NexusCrudBackend extends CrudBackend {
|
|
|
328
342
|
answeredKey(result) {
|
|
329
343
|
return typeof result === "string" && result !== "" ? result : null;
|
|
330
344
|
}
|
|
345
|
+
/**
|
|
346
|
+
* What a Nexus save leaves to the server, written as the server writes it, so nothing is read back (the owner,
|
|
347
|
+
* 2026-10-10: "the creationTime, recordStatus we will provide fake or just right now"). A new record lacking them is
|
|
348
|
+
* Active (every Create writes `NEXUS_ACTIVE_STATUS`), has no marks, was added now and never changed; what its answer
|
|
349
|
+
* said stays. An Update writes none of the status, the marks and the time added, so an edited record has the ones the
|
|
350
|
+
* page holds (`before`) - over its form's older copies - and was changed now. Times are UTC ISO strings, as a date-time
|
|
351
|
+
* column reads them.
|
|
352
|
+
*/
|
|
353
|
+
savedRow(action, record, before) {
|
|
354
|
+
const row = { ...record };
|
|
355
|
+
const now = nowUtcDateTime();
|
|
356
|
+
const fill = (field, value) => {
|
|
357
|
+
if (value !== undefined && (row[field] === undefined || row[field] === null))
|
|
358
|
+
row[field] = value;
|
|
359
|
+
};
|
|
360
|
+
if (action === "create") {
|
|
361
|
+
fill("recordStatus", NEXUS_ACTIVE_STATUS);
|
|
362
|
+
fill("bookmarkTypes", 0);
|
|
363
|
+
fill("creationTime", now);
|
|
364
|
+
fill("modificationTime", null);
|
|
365
|
+
return row;
|
|
366
|
+
}
|
|
367
|
+
const held = (before ?? {});
|
|
368
|
+
for (const field of ["recordStatus", "bookmarkTypes", "creationTime"]) {
|
|
369
|
+
if (held[field] !== undefined)
|
|
370
|
+
row[field] = held[field];
|
|
371
|
+
}
|
|
372
|
+
row.modificationTime = now;
|
|
373
|
+
return row;
|
|
374
|
+
}
|
|
331
375
|
/**
|
|
332
376
|
* The bands the owner settled on: under 40 is live, 25–29 of that is flagged, 40–59 is the trash, and 60 and up is
|
|
333
377
|
* deleted. A row without a `recordStatus` says nothing, and the lists it cannot answer for load again.
|
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import type { ApiEndpoint } from "../Interfaces/ApiInterfaces.ts";
|
|
2
|
-
import type
|
|
2
|
+
import { type InputParams } from "../Interfaces/FormInterfaces.ts";
|
|
3
3
|
import type { TableColumn, TableJoin, TableRow } from "../Interfaces/TableInterfaces.ts";
|
|
4
4
|
import { type CrudBackend } from "./CrudBackend.ts";
|
|
5
|
+
import { type PriceLadderOptions } from "./NumberHelpers.ts";
|
|
5
6
|
/** The longest description a record keeps: the owner's rule for every model (the column holds 4000). */
|
|
6
7
|
export declare const CRUD_DESCRIPTION_MAX = 1000;
|
|
8
|
+
/**
|
|
9
|
+
* The fields a record keeps its price's figures in (`crudInputs.netAmount`, `crudInputs.lineAmount`,
|
|
10
|
+
* `crudColumns.amounts`). Defaults: `price`, `discountPercentage` and `vatPercentage`, no quantity (one of it), and no
|
|
11
|
+
* `other`: an amount added whole after VAT, such as a line's carriage (`otherCostAmount`), rounded to 2 places.
|
|
12
|
+
*/
|
|
13
|
+
export type PriceKeys = {
|
|
14
|
+
price?: string;
|
|
15
|
+
quantity?: string;
|
|
16
|
+
discount?: string;
|
|
17
|
+
vat?: string;
|
|
18
|
+
other?: string;
|
|
19
|
+
};
|
|
7
20
|
export declare const crudColumns: {
|
|
8
|
-
/** The record's status in words ("Active", "Flagged", "Trashed"), from the backend's own numbers; blank
|
|
21
|
+
/** The record's status in words ("Active", "Flagged", "Trashed"), from the backend's own numbers; blank for a record that says none. */
|
|
9
22
|
status<TRow extends TableRow>(changes?: Partial<TableColumn<TRow>>, backend?: CrudBackend): TableColumn<TRow>;
|
|
10
23
|
/** The description, as Markdown: text in the table, formatted in the details. */
|
|
11
24
|
description<TRow extends TableRow>(changes?: Partial<TableColumn<TRow>>): TableColumn<TRow>;
|
|
@@ -16,11 +29,14 @@ export declare const crudColumns: {
|
|
|
16
29
|
/**
|
|
17
30
|
* Another record's name the **database** joined into the row: the row carries the name (`key`) and that record's own
|
|
18
31
|
* status (`status`), and the cell shows the name with a short tag when the record is no longer available ("Trashed",
|
|
19
|
-
* "Deleted": `CrudBackend.unavailableTag`). One call, nothing looked up.
|
|
32
|
+
* "Deleted": `CrudBackend.unavailableTag`). One call, nothing looked up. Give it `from` - the field holding that
|
|
33
|
+
* record's key, an input of the form: a save answers what was sent, not the names the database joins, so a CRUD names
|
|
34
|
+
* it again from the record picked for the key, with no call (`crudJoinedNames`). Without `from` a CRUD reads a new
|
|
35
|
+
* record back once (`details`) to name it.
|
|
20
36
|
*
|
|
21
|
-
* @example crudColumns.dbJoin({ key: "regionName", label: "Region", status: "regionRecordStatus" })
|
|
37
|
+
* @example crudColumns.dbJoin({ key: "regionName", label: "Region", status: "regionRecordStatus", from: "regionId" })
|
|
22
38
|
*/
|
|
23
|
-
dbJoin<TRow extends TableRow>({ status, ...column }: DbJoinColumn<TRow>, backend?: CrudBackend): TableColumn<TRow>;
|
|
39
|
+
dbJoin<TRow extends TableRow>({ status, from, ...column }: DbJoinColumn<TRow>, backend?: CrudBackend): TableColumn<TRow>;
|
|
24
40
|
/**
|
|
25
41
|
* Another record's name the **table** joins: the row carries only that record's key (`from`), and the table names it
|
|
26
42
|
* from the other controller's `findListing` - every key the rows hold in one call, kept for every table and form on the
|
|
@@ -30,13 +46,51 @@ export declare const crudColumns: {
|
|
|
30
46
|
* @example crudColumns.clientJoin(SUB_REGIONS, { key: "subRegionName", label: "Sub-region", from: "subRegionId" })
|
|
31
47
|
*/
|
|
32
48
|
clientJoin<TRow extends TableRow>(api: JoinController, { from, field, ...column }: ClientJoinColumn<TRow>, backend?: CrudBackend): TableColumn<TRow>;
|
|
49
|
+
/**
|
|
50
|
+
* A price's figures under one band, "Amount details" (2026-06-20's): **Price** (money), **Discount** and **VAT**
|
|
51
|
+
* (percents), and the **Calculated amount** - the price less the discount, plus VAT on the rest, worked out from the
|
|
52
|
+
* row's own figures by the server's rule (`priceAmounts`), so a row a save just answered shows it at once. A page
|
|
53
|
+
* spreads them where its price columns go: `...crudColumns.amounts()`. The Calculated amount's key is `netAmount`.
|
|
54
|
+
*
|
|
55
|
+
* @example const COLUMNS = [crudColumns.dbJoin({ ... }), ...crudColumns.amounts(), crudColumns.status()];
|
|
56
|
+
*/
|
|
57
|
+
amounts<TRow extends TableRow>(keys?: PriceKeys, changes?: {
|
|
58
|
+
group?: string;
|
|
59
|
+
}): TableColumn<TRow>[];
|
|
60
|
+
/**
|
|
61
|
+
* A receipt's money ladder as columns for a record's details (`CrudDetails`), in one section (`group`, default
|
|
62
|
+
* "Money"): Rate, Amount, Discount (5%), VAT (13%) - each left out when it is none - and the Calculated amount, money
|
|
63
|
+
* with 2 places, each read from the row's own field (`priceLadder`, on the server's `CounterAmounts` names unless
|
|
64
|
+
* `keys` says otherwise). Pass the person's number locale, so the percentages are written as they read numbers.
|
|
65
|
+
*
|
|
66
|
+
* @example ...crudColumns.priceLadder(line, { locale: useNumberLocale() })
|
|
67
|
+
*/
|
|
68
|
+
priceLadder<TRow extends TableRow>(row: Readonly<Record<string, unknown>>, options?: PriceLadderOptions & {
|
|
69
|
+
group?: string;
|
|
70
|
+
}): TableColumn<TRow>[];
|
|
33
71
|
};
|
|
34
|
-
/** What `crudColumns.dbJoin` takes: a column,
|
|
72
|
+
/** What `crudColumns.dbJoin` takes: a column, the row's field that holds the joined record's status, and the one with its key. */
|
|
35
73
|
export type DbJoinColumn<TRow extends TableRow> = Partial<TableColumn<TRow>> & Pick<TableColumn<TRow>, "key" | "label"> & {
|
|
36
74
|
status: string;
|
|
75
|
+
from?: string;
|
|
37
76
|
};
|
|
38
77
|
/** What `crudColumns.clientJoin` takes: a column, the row's field that holds the other record's key, and its own field. */
|
|
39
78
|
export type ClientJoinColumn<TRow extends TableRow> = Partial<TableColumn<TRow>> & Pick<TableColumn<TRow>, "key" | "label"> & Pick<TableJoin, "from" | "field">;
|
|
79
|
+
/** How `crudInputs.netAmount` works: whether it can be typed in, the price's places, and whether discount and VAT count. */
|
|
80
|
+
export interface NetAmountOptions {
|
|
81
|
+
/** The figure can be typed in, setting the price that comes to it (`priceFromNet`). Default `true`. */
|
|
82
|
+
editable?: boolean;
|
|
83
|
+
/** The places the price input keeps (its `decimals`), so the price set is one it can hold. Default 2. */
|
|
84
|
+
priceDecimals?: number;
|
|
85
|
+
/**
|
|
86
|
+
* Whether the discount and VAT count, asked with the form's (or the rows') `computeWith` and the values: a receipt's
|
|
87
|
+
* lines are discounted and taxed only for a purchase, not a donation. `false` counts both as 0, in the figure and when
|
|
88
|
+
* it is typed in. Default: they always count.
|
|
89
|
+
*
|
|
90
|
+
* @example crudInputs.lineAmount({}, {}, { taxed: (receipt: { purchase: boolean } | undefined) => receipt?.purchase !== false })
|
|
91
|
+
*/
|
|
92
|
+
taxed?: (withValue: any, values: Readonly<Record<string, unknown>>) => boolean;
|
|
93
|
+
}
|
|
40
94
|
/** The controller a client join names its records from: any with a `findListing` (`MODULES.controller(...)`). */
|
|
41
95
|
export interface JoinController {
|
|
42
96
|
findListing: ApiEndpoint;
|
|
@@ -48,4 +102,35 @@ export declare const crudInputs: {
|
|
|
48
102
|
* in a form, such as `span`.
|
|
49
103
|
*/
|
|
50
104
|
description(changes?: Partial<InputParams>): InputParams;
|
|
105
|
+
/**
|
|
106
|
+
* The **Calculated amount** in a price's form: a box after Price, Discount and VAT that follows them as they are typed
|
|
107
|
+
* (`compute`), by the server's rule (`priceAmounts`) - never sent and never a change of its own. Blank while the price
|
|
108
|
+
* is empty, so a cleared price never reads 0.00. Span 3, so Price | Discount | VAT | Calculated amount fill a row (two
|
|
109
|
+
* pairs on a narrow form).
|
|
110
|
+
*
|
|
111
|
+
* **It can be typed in** (2026-06-20's, the owner's 2026-11-10 ask): a person who knows the final figure (2,500.00) and
|
|
112
|
+
* the discount and VAT types it, and on leaving the box the price is set to the one that comes to it exactly by the
|
|
113
|
+
* server's rule (`priceFromNet`: 2,458.21 at 10 % and 13 %), or, when no price does, to the nearest, and the line under
|
|
114
|
+
* the box says so. The price is set as if the person typed it, so its rules, the unsaved-changes state and Save see it.
|
|
115
|
+
* `options.editable: false` keeps it read-only; `options.priceDecimals` is the price input's places (default 2).
|
|
116
|
+
*
|
|
117
|
+
* `changes` may replace any key, `compute` included: a counter's box shows the server's preview instead
|
|
118
|
+
* (`compute: { from: [], value: (_values, line) => line?.netAmount }` with the form's `computeWith`), which is read-only,
|
|
119
|
+
* since that `compute` has no `solve`.
|
|
120
|
+
*
|
|
121
|
+
* @example INPUTS = [..., price, discountPercentage, vatPercentage, crudInputs.netAmount()]
|
|
122
|
+
* @example crudInputs.netAmount({ price: "unitPrice", quantity: "quantity" }, { span: 4 }, { priceDecimals: 4 })
|
|
123
|
+
*/
|
|
124
|
+
netAmount(keys?: PriceKeys, changes?: Partial<InputParams>, options?: NetAmountOptions): InputParams;
|
|
125
|
+
/**
|
|
126
|
+
* The **Calculated amount** of a line in a table of lines (a receipt's, an invoice's): `netAmount` on the line's own
|
|
127
|
+
* figures - the rate (`unitPrice`) times the `quantity`, less the discount, plus VAT, plus any `other` amount - worked
|
|
128
|
+
* out on each row as it is typed, sized as a table column (`width`, no growing). **It can be typed in**: a line's total
|
|
129
|
+
* typed in sets that line's rate to the one that comes to it exactly by the server's rule (`priceFromNet`, per row), or
|
|
130
|
+
* the nearest, and the line under the table says which. The rows hold no value under its name, so they never send it.
|
|
131
|
+
* `keys`, `changes` and `options` as `netAmount`'s.
|
|
132
|
+
*
|
|
133
|
+
* @example inputs: [item, quantity, unitPrice, discountPercentage, vatPercentage, otherCostAmount, crudInputs.lineAmount({ other: "otherCostAmount" })]
|
|
134
|
+
*/
|
|
135
|
+
lineAmount(keys?: PriceKeys, changes?: Partial<InputParams>, options?: NetAmountOptions): InputParams;
|
|
51
136
|
};
|