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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FormStore, type AnyFormStore, type FormStatus } from "../../Helpers/FormStore.ts";
|
|
2
2
|
import type { DatePreferences } from "../../Interfaces/DateInterfaces.ts";
|
|
3
|
-
import type
|
|
3
|
+
import { type FormProps, type FormValues } from "../../Interfaces/FormInterfaces.ts";
|
|
4
|
+
import { formInputLabel } from "../Inputs/ComputedInput.tsx";
|
|
4
5
|
/**
|
|
5
6
|
* A form store for the page to hold: pass it to a form as `form`, then read, watch, set, and submit through it.
|
|
6
7
|
*
|
|
@@ -19,7 +20,7 @@ export declare function useFormStatus(form: AnyFormStore): FormStatus;
|
|
|
19
20
|
/** The form's own store, or the one given. */
|
|
20
21
|
export declare function useFormStore<TValues extends FormValues>(given: FormStore<TValues> | undefined): FormStore<TValues>;
|
|
21
22
|
/** An input's name in the form's messages: its `fieldName`, its label when it is text, or its name. */
|
|
22
|
-
export
|
|
23
|
+
export { formInputLabel };
|
|
23
24
|
/** The names a list shows: all of them up to one more than `most` ("and 1 more" says less than the name), else the first `most`. */
|
|
24
25
|
export declare function namesToShow<T>(names: readonly T[], most?: number): {
|
|
25
26
|
shown: readonly T[];
|
|
@@ -41,9 +42,9 @@ export declare function formatFormTime(ms: number, preferences: DatePreferences)
|
|
|
41
42
|
* @example
|
|
42
43
|
* <Form
|
|
43
44
|
* inputs={[
|
|
44
|
-
* { type:
|
|
45
|
-
* { type:
|
|
46
|
-
* { type:
|
|
45
|
+
* { type: EInputTypes.Text, name: "fullName", label: "Full name", required: true, span: 6 },
|
|
46
|
+
* { type: EInputTypes.Email, name: "email", label: "Email", span: 6 },
|
|
47
|
+
* { type: EInputTypes.Dropdown, name: "role", label: "Role", options: roles, span: 4 },
|
|
47
48
|
* ]}
|
|
48
49
|
* defaultValue={employee}
|
|
49
50
|
* onValueChange={values => saveFilters(values)}
|
|
@@ -8,9 +8,13 @@ import { useInputDesign } from "../../Helpers/UserPreferences.js";
|
|
|
8
8
|
import { formatDate, toIsoDate, usesDevanagari } from "../../Helpers/DateHelpers.js";
|
|
9
9
|
import { useDatePreferences } from "../../Helpers/DatePreferences.js";
|
|
10
10
|
import { FormStore } from "../../Helpers/FormStore.js";
|
|
11
|
-
import {
|
|
11
|
+
import { withInputConditions } from "../../Helpers/InputConditions.js";
|
|
12
|
+
import { formNarrowingNames, narrowInputParamsBy } from "../../Helpers/InputNarrowing.js";
|
|
13
|
+
import { isBooleanInput, isRowsInput } from "../../Helpers/InputParamsHelpers.js";
|
|
12
14
|
import { formatTime } from "../../Helpers/TimeHelpers.js";
|
|
15
|
+
import { EInputTypes } from "../../Interfaces/FormInterfaces.js";
|
|
13
16
|
import { Button } from "../Documents/Button.js";
|
|
17
|
+
import { ComputedInput, formInputLabel } from "../Inputs/ComputedInput.js";
|
|
14
18
|
import { isFirstInvalid } from "../Inputs/InputField.js";
|
|
15
19
|
import { renderInput } from "../Inputs/InputRenderer.js";
|
|
16
20
|
import { firstFocusable } from "../Inputs/RowsInput.js";
|
|
@@ -20,11 +24,21 @@ import { firstFocusable } from "../Inputs/RowsInput.js";
|
|
|
20
24
|
* its own: SubmitForm checks on submit, and a plain <form> around a Form blocks its submit while any input, or the form's
|
|
21
25
|
* own rule, has a problem.
|
|
22
26
|
*/
|
|
23
|
-
/** Inputs without a box: they line up with the boxes beside them. */
|
|
24
|
-
const CHOICE_TYPES = new Set(["checkbox", "switch", "radio"]);
|
|
25
27
|
/** What takes focus in an input's cell: a control to type in or pick, before any button of its toolbar. */
|
|
26
28
|
const TYPING_TARGETS = 'input:not([type="hidden"]):not(.nx-visually-hidden):not(:disabled), textarea:not(.nx-visually-hidden):not(:disabled), select:not(:disabled), [contenteditable="true"]';
|
|
27
29
|
const INVALID_CONTROLS = ":is(input, textarea, select):invalid";
|
|
30
|
+
const NO_INPUTS = [];
|
|
31
|
+
const NO_SUBSCRIBE = () => () => { };
|
|
32
|
+
const NO_VERSION = () => 0;
|
|
33
|
+
/** This browser's local storage, or nothing where it cannot be reached (storage switched off). */
|
|
34
|
+
function browserStorage() {
|
|
35
|
+
try {
|
|
36
|
+
return window.localStorage;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
28
42
|
/* Hooks */
|
|
29
43
|
/**
|
|
30
44
|
* A form store for the page to hold: pass it to a form as `form`, then read, watch, set, and submit through it.
|
|
@@ -61,11 +75,7 @@ export function useFormStore(given) {
|
|
|
61
75
|
}
|
|
62
76
|
/* Text */
|
|
63
77
|
/** An input's name in the form's messages: its `fieldName`, its label when it is text, or its name. */
|
|
64
|
-
export
|
|
65
|
-
if (!params)
|
|
66
|
-
return name;
|
|
67
|
-
return inputFieldName(params) ?? (typeof params.label === "string" ? params.label : params.name);
|
|
68
|
-
}
|
|
78
|
+
export { formInputLabel };
|
|
69
79
|
/** The names a list shows: all of them up to one more than `most` ("and 1 more" says less than the name), else the first `most`. */
|
|
70
80
|
export function namesToShow(names, most = 3) {
|
|
71
81
|
return names.length <= most + 1 ? { shown: names, more: 0 } : { shown: names.slice(0, most), more: names.length - most };
|
|
@@ -90,13 +100,48 @@ export function formatFormTime(ms, preferences) {
|
|
|
90
100
|
return time;
|
|
91
101
|
return `${formatDate(date, preferences)}, ${time}`;
|
|
92
102
|
}
|
|
103
|
+
/* Cells */
|
|
104
|
+
/**
|
|
105
|
+
* What the page passed as the form's `computeWith`, for the inputs worked out from it (`compute`): one object for the
|
|
106
|
+
* form's life, so the cells' context stays the same and only the worked-out inputs draw again when it changes.
|
|
107
|
+
*/
|
|
108
|
+
class ComputeSource {
|
|
109
|
+
listeners = new Set();
|
|
110
|
+
version = 0;
|
|
111
|
+
value;
|
|
112
|
+
constructor(value) {
|
|
113
|
+
this.value = value;
|
|
114
|
+
}
|
|
115
|
+
set(value) {
|
|
116
|
+
if (Object.is(value, this.value))
|
|
117
|
+
return;
|
|
118
|
+
this.value = value;
|
|
119
|
+
this.version++;
|
|
120
|
+
for (const listener of [...this.listeners])
|
|
121
|
+
listener();
|
|
122
|
+
}
|
|
123
|
+
subscribe = (listener) => {
|
|
124
|
+
this.listeners.add(listener);
|
|
125
|
+
return () => {
|
|
126
|
+
this.listeners.delete(listener);
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
getVersion = () => this.version;
|
|
130
|
+
}
|
|
93
131
|
const spanOf = (params) => Math.max(1, Math.min(12, Math.round(params.span ?? 12)));
|
|
94
132
|
/** A checkbox, switch, or radio that shares a row with labeled boxes: it lines up with their boxes, below their labels. */
|
|
95
|
-
const besideBoxes = (params, span, design) =>
|
|
133
|
+
const besideBoxes = (params, span, design) => isBooleanInput(params) && span < 12 && (design ?? "standard") === "standard";
|
|
96
134
|
/** The input. It starts from the store's value and reports each change; a new stamp mounts it again with a new value. */
|
|
97
|
-
function FieldInput({ store, params, stamp, error, context }) {
|
|
135
|
+
function FieldInput({ store, params, stamp, error, context, cell, heldFocus }) {
|
|
98
136
|
const [initial] = useState(() => store.get(params.name));
|
|
99
|
-
|
|
137
|
+
// Runs before the input leaves the page (a new stamp: a value from code, a load, a remembered value a picker gave
|
|
138
|
+
// late): a focus inside it is noted, or it would fall to the page with nothing said.
|
|
139
|
+
useLayoutEffect(() => () => {
|
|
140
|
+
const active = typeof document === "undefined" ? null : document.activeElement;
|
|
141
|
+
if (active && cell.current?.contains(active))
|
|
142
|
+
heldFocus.current = true;
|
|
143
|
+
}, [cell, heldFocus]);
|
|
144
|
+
const onValueChange = useCallback((value, item) => store.input(params.name, value, stamp, item), [store, params.name, stamp]);
|
|
100
145
|
const own = params;
|
|
101
146
|
const overrides = {
|
|
102
147
|
id: `${context.idPrefix}-${params.name}`,
|
|
@@ -108,13 +153,51 @@ function FieldInput({ store, params, stamp, error, context }) {
|
|
|
108
153
|
};
|
|
109
154
|
if (context.design && !own.design)
|
|
110
155
|
overrides.design = context.design;
|
|
111
|
-
if (context.scopeNames && params.type ===
|
|
156
|
+
if (context.scopeNames && params.type === EInputTypes.RadioGroup)
|
|
112
157
|
overrides.name = `${context.idPrefix}:${params.name}`;
|
|
113
158
|
// Only a message from the form replaces the input's own.
|
|
114
159
|
if (error !== undefined)
|
|
115
160
|
overrides.error = error;
|
|
116
|
-
|
|
161
|
+
// A picker asked whether it still offers the remembered value (`remember`): its spinner shows until it answers.
|
|
162
|
+
if (store.isRemembering(params.name))
|
|
163
|
+
overrides.loading = true;
|
|
164
|
+
// Options another input narrows (`narrowedBy`): read with its value as it is now; the cell draws again when it changes.
|
|
165
|
+
// Copied afresh on every draw, never kept: the copy reads the params' getters, and one that `dependsOn` follows
|
|
166
|
+
// (readOnly, hint, label, options; span is read by the cell) must answer for the values of this draw. The box keeps its loaded pages by `loadKey`.
|
|
167
|
+
const shown = params.narrowedBy ? narrowInputParamsBy(params, narrowing => ({ value: store.get(narrowing.name), label: store.inputParams(narrowing.name)?.label })) : params;
|
|
168
|
+
// Rows whose inputs follow this form's values (`narrowedBy` `from: "form"`): handed them as they are now; the rows input
|
|
169
|
+
// draws again when one changes (it follows them as `dependsOn` would).
|
|
170
|
+
const rowInputs = isRowsInput(params) ? (params.inputs ?? []) : NO_INPUTS;
|
|
171
|
+
if (rowInputs.length) {
|
|
172
|
+
const names = formNarrowingNames(rowInputs);
|
|
173
|
+
if (names.length)
|
|
174
|
+
overrides.formValues = Object.fromEntries(names.map(name => [name, { value: store.get(name), label: store.inputParams(name)?.label }]));
|
|
175
|
+
}
|
|
176
|
+
// Rows with inputs worked out from others (`compute`) get the form's `computeWith`, unless their params name their own;
|
|
177
|
+
// the rows input draws again when the page passes a new one.
|
|
178
|
+
const handsCompute = rowInputs.some(input => input.compute) && !("computeWith" in params);
|
|
179
|
+
const source = context.computeWith;
|
|
180
|
+
useSyncExternalStore(handsCompute ? source.subscribe : NO_SUBSCRIBE, handsCompute ? source.getVersion : NO_VERSION, handsCompute ? source.getVersion : NO_VERSION);
|
|
181
|
+
if (handsCompute)
|
|
182
|
+
overrides.computeWith = source.value;
|
|
183
|
+
return renderInput(shown, overrides);
|
|
117
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* An input worked out from others (`compute`) in a form: `ComputedInput` reading the form's values and its `computeWith`
|
|
187
|
+
* (it alone draws again when the page passes a new one), and writing a typed-in figure's answer as the person's change.
|
|
188
|
+
*/
|
|
189
|
+
function ComputedField({ store, params, context }) {
|
|
190
|
+
const source = context.computeWith;
|
|
191
|
+
useSyncExternalStore(source.subscribe, source.getVersion, source.getVersion);
|
|
192
|
+
const { idPrefix } = context;
|
|
193
|
+
const read = useCallback(() => store.getValue(), [store]);
|
|
194
|
+
const write = useCallback((values) => void store.setFromPerson(values), [store]);
|
|
195
|
+
const inputParams = useCallback((name) => store.inputParams(name), [store]);
|
|
196
|
+
const sourceId = useCallback((name) => `${idPrefix}-${name}`, [idPrefix]);
|
|
197
|
+
return (_jsx(ComputedInput, { params: params, id: `${idPrefix}-${params.name}`, read: read, withValue: source.value, write: write, inputParams: inputParams, sourceId: sourceId, size: context.size, design: context.design, loading: context.loading, readOnly: context.readOnly, disabled: context.disabled }));
|
|
198
|
+
}
|
|
199
|
+
/** The control an input mounted again takes the focus back in: its box, a list's combobox, a choice, an editor. */
|
|
200
|
+
const REFOCUSABLE = '[role="combobox"]:not([disabled]), input:not([type="hidden"]):not([disabled]):not([tabindex="-1"]), textarea:not([disabled]), select:not([disabled]), [contenteditable="true"], button:not([disabled]):not([tabindex="-1"])';
|
|
118
201
|
/** One input's place on the grid. It renders again only when its input must: a message from the form, or a value from code. */
|
|
119
202
|
const FormCell = memo(function FormCell({ store, params, context }) {
|
|
120
203
|
const { name } = params;
|
|
@@ -122,6 +205,23 @@ const FormCell = memo(function FormCell({ store, params, context }) {
|
|
|
122
205
|
const version = () => store.fieldVersion(name);
|
|
123
206
|
useSyncExternalStore(subscribe, version, version);
|
|
124
207
|
const stamp = store.stamp(name);
|
|
208
|
+
const cellRef = useRef(null);
|
|
209
|
+
const heldFocus = useRef(false);
|
|
210
|
+
// An input mounted again while it had the focus gets it back in its new control (the checked choice of a group
|
|
211
|
+
// first): a person in the box stays there, and a screen reader says the box again with the value it now holds.
|
|
212
|
+
useLayoutEffect(() => {
|
|
213
|
+
if (!heldFocus.current)
|
|
214
|
+
return;
|
|
215
|
+
heldFocus.current = false;
|
|
216
|
+
const cell = cellRef.current;
|
|
217
|
+
const active = document.activeElement;
|
|
218
|
+
if (!cell || (active && active !== document.body && cell.contains(active)))
|
|
219
|
+
return;
|
|
220
|
+
const target = cell.querySelector("input:checked:not([disabled])") ?? cell.querySelector(REFOCUSABLE);
|
|
221
|
+
target?.focus({ preventScroll: true });
|
|
222
|
+
}, [stamp]);
|
|
223
|
+
// Read on every draw: a `span` getter, or `spanWhen`, answers for the values as they are now. The input below keeps
|
|
224
|
+
// its key, so a new width never mounts it again: what is typed, the caret and the focus stay.
|
|
125
225
|
const span = spanOf(params);
|
|
126
226
|
// The design the inputs draw in, the person's choice included: choice boxes sit beside others only in `standard`.
|
|
127
227
|
const design = useInputDesign(context.design);
|
|
@@ -134,7 +234,7 @@ const FormCell = memo(function FormCell({ store, params, context }) {
|
|
|
134
234
|
return;
|
|
135
235
|
store.touch(name);
|
|
136
236
|
}
|
|
137
|
-
return (_jsx("div", { className: cx("nx-form__cell",
|
|
237
|
+
return (_jsx("div", { className: cx("nx-form__cell", isBooleanInput(params) && "nx-form__cell--choice"), style: { "--nx-span": span }, "data-span": span, "data-beside": besideBoxes(params, span, design) || undefined, "data-nx-field": name, onBlur: onBlur, ref: cellRef, children: params.compute ? (_jsx(ComputedField, { store: store, params: params, context: context })) : (_jsx(FieldInput, { store: store, params: params, stamp: stamp, error: store.shownError(name), context: context, cell: cellRef, heldFocus: heldFocus }, stamp)) }));
|
|
138
238
|
});
|
|
139
239
|
/** The note over a form whose unsaved changes came back from a draft. */
|
|
140
240
|
function DraftNotice({ store, restored, inputs, preferences }) {
|
|
@@ -155,18 +255,24 @@ function DraftNotice({ store, restored, inputs, preferences }) {
|
|
|
155
255
|
* @example
|
|
156
256
|
* <Form
|
|
157
257
|
* inputs={[
|
|
158
|
-
* { type:
|
|
159
|
-
* { type:
|
|
160
|
-
* { type:
|
|
258
|
+
* { type: EInputTypes.Text, name: "fullName", label: "Full name", required: true, span: 6 },
|
|
259
|
+
* { type: EInputTypes.Email, name: "email", label: "Email", span: 6 },
|
|
260
|
+
* { type: EInputTypes.Dropdown, name: "role", label: "Role", options: roles, span: 4 },
|
|
161
261
|
* ]}
|
|
162
262
|
* defaultValue={employee}
|
|
163
263
|
* onValueChange={values => saveFilters(values)}
|
|
164
264
|
* />
|
|
165
265
|
*/
|
|
166
266
|
export function Form(props) {
|
|
167
|
-
const {
|
|
267
|
+
const { design, size = "md", readOnly = false, disabled = false, loading = false, draftKey, id, className, style, ref } = props;
|
|
168
268
|
const store = useFormStore(props.form);
|
|
169
269
|
const anyStore = store;
|
|
270
|
+
// What the page passes for the worked-out inputs (`compute`): they alone draw again when it changes.
|
|
271
|
+
const [computeWith] = useState(() => new ComputeSource(props.computeWith));
|
|
272
|
+
useLayoutEffect(() => computeWith.set(props.computeWith), [computeWith, props.computeWith]);
|
|
273
|
+
// An input with `showWhen` gets a live `hidden`, one with `readOnlyWhen` a live `readOnly`, that reads this store; it
|
|
274
|
+
// follows the inputs they name (`dependsOn`).
|
|
275
|
+
const inputs = useMemo(() => withInputConditions(props.inputs, name => anyStore.get(name)), [props.inputs, anyStore]);
|
|
170
276
|
const preferences = useDatePreferences();
|
|
171
277
|
const idPrefix = useId();
|
|
172
278
|
const rootRef = useRef(null);
|
|
@@ -190,6 +296,13 @@ export function Form(props) {
|
|
|
190
296
|
rulesRef.current = rulesKey;
|
|
191
297
|
store.configure(options, !previous || rulesKey.some((item, index) => !Object.is(item, previous[index])));
|
|
192
298
|
});
|
|
299
|
+
// Remembered inputs (`remember`) read and keep their values in this browser: given once drawn - after the store has
|
|
300
|
+
// these inputs (a store a CRUD loaded first takes them in the effect above) - so the first draw is the same on a
|
|
301
|
+
// server and in the browser, and they open on what is kept before the first paint.
|
|
302
|
+
useLayoutEffect(() => {
|
|
303
|
+
if (inputs.some(params => params.remember))
|
|
304
|
+
store.attachMemory(browserStorage());
|
|
305
|
+
}, [store, inputs]);
|
|
193
306
|
useLayoutEffect(() => store.setLoading(loading), [store, loading]);
|
|
194
307
|
useImperativeHandle(ref, () => store, [store]);
|
|
195
308
|
// Radio groups need names of their own when no <form> keeps them apart.
|
|
@@ -208,7 +321,7 @@ export function Form(props) {
|
|
|
208
321
|
}
|
|
209
322
|
}
|
|
210
323
|
const fromValues = new Set(store.problems());
|
|
211
|
-
const names =
|
|
324
|
+
const names = anyStore.inputs.filter(params => !params.hidden && (invalid.has(params.name) || fromValues.has(params.name))).map(params => params.name);
|
|
212
325
|
return { names, invalid, fromValues };
|
|
213
326
|
}
|
|
214
327
|
function focusInput(name) {
|
|
@@ -253,7 +366,7 @@ export function Form(props) {
|
|
|
253
366
|
const proxy = proxyRef.current;
|
|
254
367
|
if (!proxy)
|
|
255
368
|
return;
|
|
256
|
-
const first =
|
|
369
|
+
const first = anyStore.inputs.find(params => !params.hidden && store.formRuleError(params.name));
|
|
257
370
|
proxy.setCustomValidity((first ? store.formRuleError(first.name) : store.formRuleMessage) ?? "");
|
|
258
371
|
};
|
|
259
372
|
const recount = () => {
|
|
@@ -311,7 +424,7 @@ export function Form(props) {
|
|
|
311
424
|
}
|
|
312
425
|
// While the values load, the real inputs show, disabled: the form has its final shape from the first frame, so a
|
|
313
426
|
// popup around it never jumps when the record lands. The store refuses anything typed until then.
|
|
314
|
-
const context = useMemo(() => ({ idPrefix, size, design, readOnly, disabled: disabled || loading, scopeNames: !inForm }), [idPrefix, size, design, readOnly, disabled, loading, inForm]);
|
|
427
|
+
const context = useMemo(() => ({ idPrefix, size, design, readOnly, disabled: disabled || loading, loading, scopeNames: !inForm, computeWith }), [idPrefix, size, design, readOnly, disabled, loading, inForm, computeWith]);
|
|
315
428
|
// With FormButtons on the page, the form's message is in their status line.
|
|
316
429
|
const ownMessage = status.buttons === 0 ? status.message : null;
|
|
317
430
|
return (_jsxs("div", { ref: rootRef, id: id, className: cx("nx-form", `nx-form--${size}`, className), style: style, "data-nx-form": "", "aria-busy": loading || undefined, children: [status.restored && !loading && _jsx(DraftNotice, { store: anyStore, restored: status.restored, inputs: inputs, preferences: preferences }), _jsx("div", { className: "nx-form__grid", children: inputs.filter(params => params.dependsOn?.length || !params.hidden).map((params, index, visible) => (_jsxs(Fragment, { children: [params.section && (index === 0 || visible[index - 1]?.section !== params.section) && _jsx("h3", { className: "nx-form__section", children: params.section }), _jsx(FormCell, { store: anyStore, params: params, context: context })] }, params.name))) }), loading && (_jsx("p", { className: "nx-visually-hidden", role: "status", children: "Loading\u2026" })), _jsx("input", { ref: proxyRef, className: "nx-visually-hidden", tabIndex: -1, "aria-hidden": "true", disabled: locked, onInvalid: onProxyInvalid }), ownMessage && (_jsxs("p", { className: "nx-form__message nx-field__message nx-field__message--error", role: "alert", children: [_jsx(InlineIcon, { icon: iconAlertCircle }), _jsx("span", { children: ownMessage })] }))] }));
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { AnyFormStore } from "../../Helpers/FormStore.ts";
|
|
2
|
+
/** One form a guard watches: its store, and whether it has changes that leaving would lose now. */
|
|
3
|
+
export interface GuardedForm {
|
|
4
|
+
form: AnyFormStore;
|
|
5
|
+
changed: () => boolean;
|
|
6
|
+
}
|
|
7
|
+
/** The forms in a place, as they come and go. */
|
|
8
|
+
export declare class FormLeaveGuard {
|
|
9
|
+
private readonly forms;
|
|
10
|
+
/** Watches a form until the function it answers is called (the form leaves). */
|
|
11
|
+
watch(entry: GuardedForm): () => void;
|
|
12
|
+
/** The first form with changes that leaving would lose, or `undefined`. */
|
|
13
|
+
changed(): AnyFormStore | undefined;
|
|
14
|
+
}
|
|
15
|
+
/** The guard of the place a form is drawn in; `null` outside one. */
|
|
16
|
+
export declare const FormLeaveGuardContext: import("react").Context<FormLeaveGuard | null>;
|
|
17
|
+
/**
|
|
18
|
+
* The page's own forms: the ones a navigation inside the app takes away with the page. A `SubmitForm` (and so an
|
|
19
|
+
* `ApiForm`) that asks before the page closes joins it too, unless it is drawn in a `PopupHost` popup, which stays open
|
|
20
|
+
* through a navigation. A link inside the app and `confirmPageLeave()` ask "Discard your changes?" while one has changes.
|
|
21
|
+
*/
|
|
22
|
+
export declare class PageLeaveGuard extends FormLeaveGuard {
|
|
23
|
+
private allowedUntil;
|
|
24
|
+
private unloading;
|
|
25
|
+
/** Lets the page's forms go without asking for a moment: a navigation the person confirmed, until it has started. */
|
|
26
|
+
allow(ms?: number): void;
|
|
27
|
+
/**
|
|
28
|
+
* Lets every form go, the page's and the popups', until the page itself goes (`pagehide`): a full-page move the person
|
|
29
|
+
* confirmed (a sign-out, `location.assign`), however long its own call takes first. `end()` takes it back.
|
|
30
|
+
*/
|
|
31
|
+
allowUnload(): void;
|
|
32
|
+
/** Asks again: a confirmed move that did not happen (its call failed, the page stays). */
|
|
33
|
+
end(): void;
|
|
34
|
+
/** Whether a confirmed navigation is under way, so neither a link nor a page form's close question asks again. */
|
|
35
|
+
get allowed(): boolean;
|
|
36
|
+
/** Whether a confirmed full-page move is under way: only then a popup form's close question is not asked either. */
|
|
37
|
+
get unloadAllowed(): boolean;
|
|
38
|
+
}
|
|
39
|
+
/** The one page guard of this browser tab. */
|
|
40
|
+
export declare const pageLeaveGuard: PageLeaveGuard;
|
|
41
|
+
/** Every form a full-page load takes away: the page's and the ones in popups (`confirmPageLeave({ unload: true })`). */
|
|
42
|
+
export declare const unloadLeaveGuard: FormLeaveGuard;
|
|
43
|
+
/** The page guard a form joins: the tab's, or `null` inside a place that outlives a navigation (a `PopupHost` popup). */
|
|
44
|
+
export declare const PageLeaveGuardContext: import("react").Context<PageLeaveGuard | null>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext } from "react";
|
|
3
|
+
/** The forms in a place, as they come and go. */
|
|
4
|
+
export class FormLeaveGuard {
|
|
5
|
+
forms = new Set();
|
|
6
|
+
/** Watches a form until the function it answers is called (the form leaves). */
|
|
7
|
+
watch(entry) {
|
|
8
|
+
this.forms.add(entry);
|
|
9
|
+
return () => {
|
|
10
|
+
this.forms.delete(entry);
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/** The first form with changes that leaving would lose, or `undefined`. */
|
|
14
|
+
changed() {
|
|
15
|
+
for (const entry of this.forms)
|
|
16
|
+
if (entry.changed())
|
|
17
|
+
return entry.form;
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** The guard of the place a form is drawn in; `null` outside one. */
|
|
22
|
+
export const FormLeaveGuardContext = createContext(null);
|
|
23
|
+
/**
|
|
24
|
+
* The page's own forms: the ones a navigation inside the app takes away with the page. A `SubmitForm` (and so an
|
|
25
|
+
* `ApiForm`) that asks before the page closes joins it too, unless it is drawn in a `PopupHost` popup, which stays open
|
|
26
|
+
* through a navigation. A link inside the app and `confirmPageLeave()` ask "Discard your changes?" while one has changes.
|
|
27
|
+
*/
|
|
28
|
+
export class PageLeaveGuard extends FormLeaveGuard {
|
|
29
|
+
allowedUntil = 0;
|
|
30
|
+
unloading = false;
|
|
31
|
+
/** Lets the page's forms go without asking for a moment: a navigation the person confirmed, until it has started. */
|
|
32
|
+
allow(ms = 1500) {
|
|
33
|
+
this.allowedUntil = Date.now() + ms;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Lets every form go, the page's and the popups', until the page itself goes (`pagehide`): a full-page move the person
|
|
37
|
+
* confirmed (a sign-out, `location.assign`), however long its own call takes first. `end()` takes it back.
|
|
38
|
+
*/
|
|
39
|
+
allowUnload() {
|
|
40
|
+
if (this.unloading)
|
|
41
|
+
return;
|
|
42
|
+
this.unloading = true;
|
|
43
|
+
if (typeof window !== "undefined")
|
|
44
|
+
window.addEventListener("pagehide", () => this.end(), { once: true });
|
|
45
|
+
}
|
|
46
|
+
/** Asks again: a confirmed move that did not happen (its call failed, the page stays). */
|
|
47
|
+
end() {
|
|
48
|
+
this.unloading = false;
|
|
49
|
+
this.allowedUntil = 0;
|
|
50
|
+
}
|
|
51
|
+
/** Whether a confirmed navigation is under way, so neither a link nor a page form's close question asks again. */
|
|
52
|
+
get allowed() {
|
|
53
|
+
return this.unloading || Date.now() < this.allowedUntil;
|
|
54
|
+
}
|
|
55
|
+
/** Whether a confirmed full-page move is under way: only then a popup form's close question is not asked either. */
|
|
56
|
+
get unloadAllowed() {
|
|
57
|
+
return this.unloading;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** The one page guard of this browser tab. */
|
|
61
|
+
export const pageLeaveGuard = new PageLeaveGuard();
|
|
62
|
+
/** Every form a full-page load takes away: the page's and the ones in popups (`confirmPageLeave({ unload: true })`). */
|
|
63
|
+
export const unloadLeaveGuard = new FormLeaveGuard();
|
|
64
|
+
/** The page guard a form joins: the tab's, or `null` inside a place that outlives a navigation (a `PopupHost` popup). */
|
|
65
|
+
export const PageLeaveGuardContext = createContext(pageLeaveGuard);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { FormConfirmQuestion } from "../../Helpers/FormConfirm.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Asks a settled question (`formConfirmQuestion`): a question box with what will be saved drawn as a record's details,
|
|
4
|
+
* Confirm holding the focus, a press outside it changing nothing. Resolves whether to go on; an abort answers no.
|
|
5
|
+
*/
|
|
6
|
+
export declare function askFormQuestion(question: FormConfirmQuestion, signal?: AbortSignal): Promise<boolean>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { messageBox } from "../Layouts/MessageBox.js";
|
|
4
|
+
import { CrudDetails } from "./CrudParts.js";
|
|
5
|
+
/*
|
|
6
|
+
* The question asked before a save, drawn once for every place that asks it: a form's `confirm` (SubmitForm) and a row
|
|
7
|
+
* form's, including a row action that is its question alone (CrudRowForms.tsx). Not exported from the package: a page
|
|
8
|
+
* asks through those params.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Asks a settled question (`formConfirmQuestion`): a question box with what will be saved drawn as a record's details,
|
|
12
|
+
* Confirm holding the focus, a press outside it changing nothing. Resolves whether to go on; an abort answers no.
|
|
13
|
+
*/
|
|
14
|
+
export function askFormQuestion(question, signal) {
|
|
15
|
+
const details = question.details;
|
|
16
|
+
return messageBox.confirm({
|
|
17
|
+
tone: "question",
|
|
18
|
+
closable: true,
|
|
19
|
+
closeOnBackdrop: false,
|
|
20
|
+
signal,
|
|
21
|
+
size: question.size,
|
|
22
|
+
title: question.title,
|
|
23
|
+
message: question.message,
|
|
24
|
+
details: details ? _jsx(CrudDetails, { row: details.row, columns: details.columns, currency: details.currency }) : null,
|
|
25
|
+
confirmLabel: question.confirmLabel,
|
|
26
|
+
cancelLabel: question.cancelLabel,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type RecordWriteRequest } from "../../Helpers/RecordCards.ts";
|
|
3
|
+
import type { ApiResult, ApiTarget } from "../../Interfaces/ApiInterfaces.ts";
|
|
4
|
+
import type { ApiRequest, InputParams } from "../../Interfaces/FormInterfaces.ts";
|
|
5
|
+
import type { RecordConfirmParams, RecordContext, RecordFlowParams } from "../../Interfaces/RecordInterfaces.ts";
|
|
6
|
+
import { type PopupHandle } from "../Layouts/PopupHost.tsx";
|
|
7
|
+
/** What a popup works with: the card's data as held now, and the page's ways to write it and to end the session. */
|
|
8
|
+
export interface RecordFlowHost<TData = any, TItem = any> {
|
|
9
|
+
/** The card's data and the item, as held now. */
|
|
10
|
+
context: () => RecordContext<TData, TItem>;
|
|
11
|
+
/** Draws the popup again when the page's data changes. */
|
|
12
|
+
subscribe: (listener: () => void) => () => void;
|
|
13
|
+
snapshot: () => unknown;
|
|
14
|
+
/** Writes an answer into the card's data, as `writes` says. */
|
|
15
|
+
write: (request: Pick<RecordWriteRequest, "writes" | "result" | "values" | "fallback">) => Promise<void>;
|
|
16
|
+
/** A refusal that ended the session: the page shows its words and closes its popups. Answers whether it was one. */
|
|
17
|
+
ended: (answer: ApiResult) => boolean;
|
|
18
|
+
/** Keeps the popup, so an ended session closes it, and the focus comes home after it. */
|
|
19
|
+
track: (popup: PopupHandle) => void;
|
|
20
|
+
}
|
|
21
|
+
export interface RecordFlowRequest<TData = any, TItem = any> {
|
|
22
|
+
flow: RecordFlowParams<TData, TItem>;
|
|
23
|
+
host: RecordFlowHost<TData, TItem>;
|
|
24
|
+
/** One popup per key: opened again, it comes back as it was. */
|
|
25
|
+
key: string;
|
|
26
|
+
/** The popup's title and line when the flow names neither. */
|
|
27
|
+
title: ReactNode;
|
|
28
|
+
description?: ReactNode;
|
|
29
|
+
/**
|
|
30
|
+
* `edit`: a card's Edit - its inputs when the flow names none, opened on the data, its values merged over the data as
|
|
31
|
+
* held now when sent and written back when the answer is no record. `add`: the values alone. `item`: opened on the item,
|
|
32
|
+
* its values merged over it.
|
|
33
|
+
*/
|
|
34
|
+
kind: "edit" | "add" | "item";
|
|
35
|
+
inputs?: readonly InputParams[];
|
|
36
|
+
}
|
|
37
|
+
/** Where a change goes, sent through the app's `api` without its toasts: an endpoint, a URL (POST), a request, or a function. */
|
|
38
|
+
export declare function sendRecordCall(submit: ApiTarget | ApiRequest | ((body: unknown) => Promise<unknown>), body: unknown): Promise<ApiResult>;
|
|
39
|
+
/**
|
|
40
|
+
* Opens a record page's popup: one per key, brought back as it was when it is open. Each step is a form (none of its
|
|
41
|
+
* inputs is required: a step can be its sentence and its button); its answer is written into the card's data, then the
|
|
42
|
+
* step's `next` shows, or the popup closes and `done` runs. Closing with changes asks first, unless the step says there
|
|
43
|
+
* is nothing worth keeping.
|
|
44
|
+
*/
|
|
45
|
+
export declare function openRecordFlow<TData, TItem>(request: RecordFlowRequest<TData, TItem>): PopupHandle;
|
|
46
|
+
/**
|
|
47
|
+
* A change that needs no form: the question first ("Remove ram@example.com?"), then the call, its answer written into
|
|
48
|
+
* the card's data. `busy` marks the item while it runs. A refusal is said in a toast, in the server's words.
|
|
49
|
+
*/
|
|
50
|
+
export declare function runRecordConfirm<TData, TItem>(confirm: RecordConfirmParams<TData, TItem>, host: RecordFlowHost<TData, TItem>, busy: (on: boolean) => void): Promise<void>;
|