nexus-shared 3.2.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1564 -0
- package/README.md +1705 -1123
- package/dist/Client.Index.d.ts +14 -0
- package/dist/Client.Index.js +14 -0
- package/dist/Components/Documents/DocumentFrame.d.ts +50 -0
- package/dist/Components/Documents/DocumentFrame.js +83 -0
- package/dist/Components/Documents/Menu.d.ts +4 -0
- package/dist/Components/Documents/Menu.js +40 -24
- package/dist/Components/Documents/PrintDocument.d.ts +95 -0
- package/dist/Components/Documents/PrintDocument.js +62 -0
- package/dist/Components/Documents/Tabs.d.ts +59 -0
- package/dist/Components/Documents/Tabs.js +70 -0
- package/dist/Components/Forms/ApiForm.js +19 -2
- package/dist/Components/Forms/Crud.d.ts +2 -2
- package/dist/Components/Forms/Crud.js +576 -243
- package/dist/Components/Forms/CrudActions.d.ts +136 -0
- package/dist/Components/Forms/CrudActions.js +184 -0
- package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
- package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
- package/dist/Components/Forms/CrudParts.d.ts +30 -2
- package/dist/Components/Forms/CrudParts.js +55 -5
- package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
- package/dist/Components/Forms/CrudRowForms.js +114 -0
- package/dist/Components/Forms/CrudScopes.d.ts +28 -0
- package/dist/Components/Forms/CrudScopes.js +76 -0
- package/dist/Components/Forms/CrudTree.d.ts +90 -0
- package/dist/Components/Forms/CrudTree.js +363 -0
- package/dist/Components/Forms/CrudWorkspace.d.ts +63 -0
- package/dist/Components/Forms/CrudWorkspace.js +169 -0
- package/dist/Components/Forms/ExcelImport.js +3 -3
- package/dist/Components/Forms/Form.d.ts +4 -4
- package/dist/Components/Forms/Form.js +123 -17
- package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
- package/dist/Components/Forms/FormLeaveGuard.js +65 -0
- package/dist/Components/Forms/RecordFlows.d.ts +50 -0
- package/dist/Components/Forms/RecordFlows.js +282 -0
- package/dist/Components/Forms/SubmitForm.d.ts +37 -2
- package/dist/Components/Forms/SubmitForm.js +195 -10
- package/dist/Components/Inputs/CheckBox.js +1 -1
- package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
- package/dist/Components/Inputs/ChoiceField.js +28 -2
- package/dist/Components/Inputs/Dropdown.d.ts +13 -0
- package/dist/Components/Inputs/Dropdown.js +5 -5
- package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
- package/dist/Components/Inputs/DropdownParts.js +16 -4
- package/dist/Components/Inputs/Field.d.ts +7 -1
- package/dist/Components/Inputs/Field.js +2 -2
- package/dist/Components/Inputs/GroupForm.d.ts +6 -6
- package/dist/Components/Inputs/GroupForm.js +10 -11
- package/dist/Components/Inputs/InputRenderer.d.ts +9 -6
- package/dist/Components/Inputs/InputRenderer.js +37 -32
- package/dist/Components/Inputs/PictureInput.d.ts +41 -0
- package/dist/Components/Inputs/PictureInput.js +70 -0
- package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
- package/dist/Components/Inputs/RadioGroup.js +8 -3
- package/dist/Components/Inputs/RowsInput.d.ts +1 -1
- package/dist/Components/Inputs/RowsInput.js +18 -3
- package/dist/Components/Inputs/TabularForm.d.ts +5 -5
- package/dist/Components/Inputs/TabularForm.js +29 -29
- package/dist/Components/Inputs/TextArea.d.ts +6 -0
- package/dist/Components/Inputs/TextArea.js +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 +53 -2
- package/dist/Components/Inputs/TreeView.js +147 -13
- package/dist/Components/Layouts/MessageBox.d.ts +15 -2
- package/dist/Components/Layouts/MessageBox.js +56 -7
- package/dist/Components/Layouts/Popup.d.ts +13 -6
- package/dist/Components/Layouts/Popup.js +37 -9
- package/dist/Components/Layouts/PopupDock.d.ts +3 -1
- package/dist/Components/Layouts/PopupDock.js +2 -1
- package/dist/Components/Layouts/PopupHost.js +5 -4
- package/dist/Components/Viewers/DataTable.js +150 -48
- package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
- package/dist/Components/Viewers/DataTableFilters.js +17 -2
- package/dist/Components/Viewers/DataTableParts.d.ts +21 -5
- package/dist/Components/Viewers/DataTableParts.js +26 -14
- package/dist/Components/Viewers/OwnerFiles.d.ts +175 -0
- package/dist/Components/Viewers/OwnerFiles.js +589 -0
- package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
- package/dist/Components/Viewers/QrCodeImage.js +15 -0
- package/dist/Components/Viewers/RecordCards.d.ts +16 -0
- package/dist/Components/Viewers/RecordCards.js +509 -0
- package/dist/Helpers/BusyHooks.d.ts +6 -0
- package/dist/Helpers/BusyHooks.js +39 -0
- package/dist/Helpers/CrudBackend.d.ts +20 -2
- package/dist/Helpers/CrudBackend.js +46 -2
- package/dist/Helpers/CrudColumns.d.ts +53 -6
- package/dist/Helpers/CrudColumns.js +98 -7
- package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
- package/dist/Helpers/CrudDetailsTabs.js +126 -0
- package/dist/Helpers/CrudHelpers.d.ts +81 -2
- package/dist/Helpers/CrudHelpers.js +195 -0
- package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
- package/dist/Helpers/CrudNarrowPick.js +157 -0
- package/dist/Helpers/CrudRowForms.d.ts +15 -0
- package/dist/Helpers/CrudRowForms.js +43 -0
- package/dist/Helpers/CrudScopes.d.ts +32 -0
- package/dist/Helpers/CrudScopes.js +79 -0
- package/dist/Helpers/DropdownHelpers.d.ts +3 -1
- package/dist/Helpers/DropdownHelpers.js +6 -2
- package/dist/Helpers/ExcelHelpers.d.ts +1 -1
- package/dist/Helpers/ExcelHelpers.js +12 -11
- package/dist/Helpers/FileAdapter.d.ts +78 -0
- package/dist/Helpers/FileAdapter.js +102 -0
- package/dist/Helpers/FilePrimaries.d.ts +48 -0
- package/dist/Helpers/FilePrimaries.js +138 -0
- package/dist/Helpers/FormConfirm.d.ts +32 -0
- package/dist/Helpers/FormConfirm.js +40 -0
- package/dist/Helpers/FormStore.d.ts +90 -5
- package/dist/Helpers/FormStore.js +318 -22
- package/dist/Helpers/InputConditions.d.ts +50 -0
- package/dist/Helpers/InputConditions.js +168 -0
- package/dist/Helpers/InputMemory.d.ts +106 -0
- package/dist/Helpers/InputMemory.js +385 -0
- package/dist/Helpers/InputNarrowing.d.ts +18 -0
- package/dist/Helpers/InputNarrowing.js +67 -0
- package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
- package/dist/Helpers/InputParamsHelpers.js +157 -74
- package/dist/Helpers/NexusDriveAdapter.d.ts +55 -0
- package/dist/Helpers/NexusDriveAdapter.js +207 -0
- package/dist/Helpers/NumberHelpers.d.ts +95 -0
- package/dist/Helpers/NumberHelpers.js +126 -0
- package/dist/Helpers/Permissions.js +2 -1
- package/dist/Helpers/QrCode.d.ts +26 -0
- package/dist/Helpers/QrCode.js +463 -0
- package/dist/Helpers/RecordCards.d.ts +205 -0
- package/dist/Helpers/RecordCards.js +378 -0
- package/dist/Helpers/RequestCache.d.ts +14 -2
- package/dist/Helpers/RequestCache.js +22 -3
- package/dist/Helpers/RowsStore.d.ts +37 -2
- package/dist/Helpers/RowsStore.js +128 -10
- package/dist/Helpers/ServerPreview.d.ts +69 -0
- package/dist/Helpers/ServerPreview.js +99 -0
- package/dist/Helpers/TableColumns.js +4 -3
- package/dist/Helpers/TableExport.d.ts +7 -1
- package/dist/Helpers/TableExport.js +9 -3
- package/dist/Helpers/TableFilters.d.ts +1 -1
- package/dist/Helpers/TableFilters.js +24 -23
- package/dist/Helpers/TableHelpers.d.ts +23 -2
- package/dist/Helpers/TableHelpers.js +51 -10
- package/dist/Helpers/TableJoins.d.ts +13 -0
- package/dist/Helpers/TableJoins.js +34 -0
- package/dist/Helpers/TableStore.d.ts +21 -1
- package/dist/Helpers/TableStore.js +64 -7
- package/dist/Helpers/TreeStore.d.ts +81 -0
- package/dist/Helpers/TreeStore.js +365 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +465 -5
- package/dist/Interfaces/FileInterfaces.d.ts +162 -0
- package/dist/Interfaces/FileInterfaces.js +4 -0
- package/dist/Interfaces/FormInterfaces.d.ts +391 -30
- package/dist/Interfaces/FormInterfaces.js +79 -1
- package/dist/Interfaces/InputInterfaces.d.ts +15 -3
- package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
- package/dist/Interfaces/RecordInterfaces.js +1 -0
- package/dist/Interfaces/TableInterfaces.d.ts +74 -1
- 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 +37 -5
- package/dist/Services/JoinSources.js +90 -14
- package/dist/Services/ModuleImport.d.ts +7 -0
- package/dist/Services/ModuleImport.js +22 -0
- package/dist/Shared.Index.d.ts +16 -0
- package/dist/Shared.Index.js +16 -0
- package/package.json +7 -2
- package/src/Styles/Nexus.Avatar.css +6 -5
- package/src/Styles/Nexus.Button.css +10 -4
- package/src/Styles/Nexus.Choice.css +16 -0
- package/src/Styles/Nexus.Crud.css +383 -9
- package/src/Styles/Nexus.Dropdown.css +8 -0
- package/src/Styles/Nexus.Files.css +410 -0
- package/src/Styles/Nexus.Form.css +5 -2
- package/src/Styles/Nexus.Index.css +4 -0
- package/src/Styles/Nexus.Input.css +42 -0
- package/src/Styles/Nexus.Menu.css +10 -1
- package/src/Styles/Nexus.Popup.css +77 -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 +67 -5
- package/src/Styles/Nexus.Themes.css +12 -6
- package/src/Styles/Nexus.Tokens.css +8 -3
- package/src/Styles/Nexus.Tree.css +31 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EInputTypes } from "../Interfaces/FormInterfaces.js";
|
|
1
2
|
import { ExcelDataType, ExcelRowKind } from "../Interfaces/ExcelInterfaces.js";
|
|
2
3
|
import { toIsoDate } from "./DateHelpers.js";
|
|
3
4
|
import { toIsoTime } from "./TimeHelpers.js";
|
|
@@ -111,28 +112,28 @@ export function excelColumnInput(column) {
|
|
|
111
112
|
...(column.maximum === null || column.maximum === undefined ? {} : { max: column.maximum }),
|
|
112
113
|
};
|
|
113
114
|
if (column.isReference)
|
|
114
|
-
return { ...shared, type:
|
|
115
|
+
return { ...shared, type: EInputTypes.Text, readOnly: true, readOnlyMessage: excelText("referenceLocked") };
|
|
115
116
|
if (column.isReadOnly)
|
|
116
|
-
return { ...shared, type:
|
|
117
|
+
return { ...shared, type: EInputTypes.Text, readOnly: true, readOnlyMessage: excelText("generated") };
|
|
117
118
|
switch (column.dataType) {
|
|
118
119
|
case ExcelDataType.Note:
|
|
119
|
-
return { ...shared, type:
|
|
120
|
+
return { ...shared, type: EInputTypes.TextArea, rows: 2, required, ...lengths };
|
|
120
121
|
case ExcelDataType.WholeNumber:
|
|
121
|
-
return { ...shared, type:
|
|
122
|
+
return { ...shared, type: EInputTypes.Number, required, decimals: 0, ...limits };
|
|
122
123
|
case ExcelDataType.Decimal:
|
|
123
|
-
return { ...shared, type:
|
|
124
|
+
return { ...shared, type: EInputTypes.Number, required, ...limits };
|
|
124
125
|
case ExcelDataType.Amount:
|
|
125
|
-
return { ...shared, type:
|
|
126
|
+
return { ...shared, type: EInputTypes.Number, required, decimals: 2, fixedDecimals: true, grouping: true, ...limits };
|
|
126
127
|
case ExcelDataType.Date:
|
|
127
|
-
return { ...shared, type:
|
|
128
|
+
return { ...shared, type: EInputTypes.Date, required };
|
|
128
129
|
case ExcelDataType.DateTime:
|
|
129
|
-
return { ...shared, type:
|
|
130
|
+
return { ...shared, type: EInputTypes.Text, required, pattern: MOMENT_PATTERN, placeholder: "2026-09-29 14:30" };
|
|
130
131
|
case ExcelDataType.Time:
|
|
131
|
-
return { ...shared, type:
|
|
132
|
+
return { ...shared, type: EInputTypes.Time, required };
|
|
132
133
|
case ExcelDataType.YesNo:
|
|
133
|
-
return { ...shared, type:
|
|
134
|
+
return { ...shared, type: EInputTypes.Dropdown, required, options: YES_NO, searchable: false };
|
|
134
135
|
default:
|
|
135
|
-
return { ...shared, type:
|
|
136
|
+
return { ...shared, type: EInputTypes.Text, required, ...lengths };
|
|
136
137
|
}
|
|
137
138
|
}
|
|
138
139
|
/** Every column of a sheet as the inputs of one editor row, in the order they read. */
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { ApiModuleValue, ApiResult } from "../Interfaces/ApiInterfaces.ts";
|
|
2
|
+
import type { FileCallOptions, FileCategoryInfo, FileCopy, FileOwner, FileUpload, OwnerFiles, PictureType, StoredFile } from "../Interfaces/FileInterfaces.ts";
|
|
3
|
+
export declare abstract class FileAdapter {
|
|
4
|
+
/** The owner's files and what their category takes, `canWrite` included. */
|
|
5
|
+
abstract list(owner: FileOwner<ApiModuleValue>, options?: FileCallOptions): Promise<ApiResult<OwnerFiles>>;
|
|
6
|
+
/**
|
|
7
|
+
* Each owner's picture, in the order asked (null where an owner has none): a page's rows in one call. At most
|
|
8
|
+
* `maxOwners` at a time; `primaryPictures` splits a longer list.
|
|
9
|
+
*/
|
|
10
|
+
abstract primaries(module: ApiModuleValue, category: string, ownerIds: readonly string[], options?: FileCallOptions): Promise<ApiResult<(StoredFile | null)[]>>;
|
|
11
|
+
/** Stores one file for the owner and answers it as stored. */
|
|
12
|
+
abstract upload(owner: FileOwner<ApiModuleValue>, upload: FileUpload, options?: FileCallOptions): Promise<ApiResult<StoredFile>>;
|
|
13
|
+
/** Makes one of the owner's files its picture, or none (`null`). */
|
|
14
|
+
abstract setPrimary(owner: FileOwner<ApiModuleValue>, fileId: string | null, options?: FileCallOptions): Promise<ApiResult<unknown>>;
|
|
15
|
+
/** A new name for one of the owner's files; the backend keeps the extension of what the file really is. */
|
|
16
|
+
abstract rename(owner: FileOwner<ApiModuleValue>, fileId: string, name: string, options?: FileCallOptions): Promise<ApiResult<unknown>>;
|
|
17
|
+
/** Deletes one of the owner's files. */
|
|
18
|
+
abstract remove(owner: FileOwner<ApiModuleValue>, fileId: string, options?: FileCallOptions): Promise<ApiResult<unknown>>;
|
|
19
|
+
/** The address an `<img>`, a link or the viewer opens the file by. */
|
|
20
|
+
abstract fileUrl(file: StoredFile, copy: FileCopy): string;
|
|
21
|
+
/**
|
|
22
|
+
* The address of the image editor module (`nexus-image-editor`, an ES module) as this backend serves it, versioned,
|
|
23
|
+
* so `loadImageEditor` can import it. Rejects with an `Error` in plain words when it is not there.
|
|
24
|
+
*/
|
|
25
|
+
abstract editorModuleUrl(signal?: AbortSignal): Promise<string>;
|
|
26
|
+
/** The most owners one `primaries` call takes. */
|
|
27
|
+
get maxOwners(): number;
|
|
28
|
+
}
|
|
29
|
+
/** One owner's key, for caches and listeners: the same module, category and id are the same owner. */
|
|
30
|
+
export declare const fileOwnerKey: (owner: FileOwner<ApiModuleValue>) => string;
|
|
31
|
+
/** Whether a category takes anything that is not a picture: its files are a document list, not a gallery. */
|
|
32
|
+
export declare const takesDocuments: (category: Pick<FileCategoryInfo, "types">) => boolean;
|
|
33
|
+
/** What a file input offers for a category: its MIME types. */
|
|
34
|
+
export declare const acceptOf: (category: Pick<FileCategoryInfo, "types">) => string;
|
|
35
|
+
/** A file's kind in a word, for a list: "JPEG", "PDF". */
|
|
36
|
+
export declare function fileKindName(contentType: string): string;
|
|
37
|
+
/** "JPEG, PNG or WebP" for a category's types, as a sentence names them. */
|
|
38
|
+
export declare function fileKindsSentence(types: readonly string[]): string;
|
|
39
|
+
/** A size in words: "5 MB". */
|
|
40
|
+
export declare const fileSizeText: (bytes: number) => string;
|
|
41
|
+
/**
|
|
42
|
+
* What a stored picture may be when the backend does not say (Drive's own defaults): its longest side in pixels, the
|
|
43
|
+
* highest quality in percent, and the most bytes. A company that hosts the site on its own machine raises Drive's
|
|
44
|
+
* settings, and the browser follows what Drive's List answers.
|
|
45
|
+
*/
|
|
46
|
+
export declare const PICTURE_LIMITS: {
|
|
47
|
+
readonly maxSide: 1920;
|
|
48
|
+
readonly quality: 80;
|
|
49
|
+
readonly maxBytes: number;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* A category's limits for a stored picture: what the backend said, else `PICTURE_LIMITS`; the bytes never above the
|
|
53
|
+
* category's own `maxBytes`, the quality at most 100.
|
|
54
|
+
*/
|
|
55
|
+
export declare function pictureLimits(category: Pick<FileCategoryInfo, "maxBytes"> & Partial<Pick<FileCategoryInfo, "pictureMaxSide" | "pictureQuality" | "pictureMaxBytes">>): {
|
|
56
|
+
maxSide: number;
|
|
57
|
+
quality: number;
|
|
58
|
+
maxBytes: number;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Why a chosen file would be refused, before it is sent: a kind the category does not take, or larger than it takes -
|
|
62
|
+
* for a picture, the category's picture limit (`pictureLimits`). The backend checks the bytes again; this only spares
|
|
63
|
+
* the person an upload that cannot succeed. Null when it fits.
|
|
64
|
+
*/
|
|
65
|
+
export declare function fileRefusal(category: Pick<FileCategoryInfo, "types" | "maxBytes"> & Partial<Pick<FileCategoryInfo, "pictureMaxSide" | "pictureQuality" | "pictureMaxBytes">>, file: Pick<Blob, "type" | "size">, name: string): string | null;
|
|
66
|
+
/**
|
|
67
|
+
* A picture's kind read from its first bytes - JPEG, PNG or WebP - for a file the browser could not type (none, or
|
|
68
|
+
* `application/octet-stream`: a phone's share, a file dragged from another app, a name with no extension), so it still
|
|
69
|
+
* goes through the editor. Null for anything
|
|
70
|
+
* else, which is left to the backend (it reads the bytes too).
|
|
71
|
+
*/
|
|
72
|
+
export declare function sniffPictureType(file: Blob): Promise<PictureType | null>;
|
|
73
|
+
/** A chosen file with its kind: the browser's, else a picture's read from its bytes (`sniffPictureType`), else as it was. */
|
|
74
|
+
export declare function typedFile(file: File): Promise<File>;
|
|
75
|
+
/** How many more files the owner keeps: never below 0. */
|
|
76
|
+
export declare const roomLeft: (files: OwnerFiles) => number;
|
|
77
|
+
/** The name a picture made by the editor is kept by: the chosen file's name with the saved kind's extension. */
|
|
78
|
+
export declare function pictureName(name: string | null | undefined, type: string, fallback?: string): string;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { isPictureType } from "../Interfaces/FileInterfaces.js";
|
|
2
|
+
import { chatFileSize } from "./ChatHelpers.js";
|
|
3
|
+
/*
|
|
4
|
+
* Where an owner's files are kept. `FileAdapter` is the class an app implements for its own file backend;
|
|
5
|
+
* `NexusDriveAdapter` (nexus-shared/client) is the Nexus Drive and runs when nothing is registered - the same
|
|
6
|
+
* arrangement as `ApiModules` / `NexusApiModules` and `ExcelBackend` / `NexusExcelBackend`. The picture input, the
|
|
7
|
+
* gallery, the document list and the picture column call nothing else, so a page that declares pictures never names
|
|
8
|
+
* a backend.
|
|
9
|
+
*
|
|
10
|
+
* Every call answers an `ApiResult` and never throws for a refused or failed request, like `api`. None shows a toast:
|
|
11
|
+
* the panel that asked says what happened, once.
|
|
12
|
+
*/
|
|
13
|
+
export class FileAdapter {
|
|
14
|
+
/** The most owners one `primaries` call takes. */
|
|
15
|
+
get maxOwners() {
|
|
16
|
+
return 200;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** One owner's key, for caches and listeners: the same module, category and id are the same owner. */
|
|
20
|
+
export const fileOwnerKey = (owner) => `${String(owner.module)}|${owner.category}|${owner.ownerId}`;
|
|
21
|
+
/** Whether a category takes anything that is not a picture: its files are a document list, not a gallery. */
|
|
22
|
+
export const takesDocuments = (category) => category.types.some(type => !isPictureType(type));
|
|
23
|
+
/** What a file input offers for a category: its MIME types. */
|
|
24
|
+
export const acceptOf = (category) => category.types.join(",");
|
|
25
|
+
/** A file's kind in a word, for a list: "JPEG", "PDF". */
|
|
26
|
+
export function fileKindName(contentType) {
|
|
27
|
+
const kind = contentType.split("/")[1] ?? contentType;
|
|
28
|
+
return kind === "jpeg" ? "JPEG" : kind.toUpperCase();
|
|
29
|
+
}
|
|
30
|
+
/** "JPEG, PNG or WebP" for a category's types, as a sentence names them. */
|
|
31
|
+
export function fileKindsSentence(types) {
|
|
32
|
+
const names = types.map(type => (type === "image/webp" ? "WebP" : fileKindName(type)));
|
|
33
|
+
if (names.length <= 1)
|
|
34
|
+
return names[0] ?? "";
|
|
35
|
+
return `${names.slice(0, -1).join(", ")} or ${names[names.length - 1]}`;
|
|
36
|
+
}
|
|
37
|
+
/** A size in words: "5 MB". */
|
|
38
|
+
export const fileSizeText = (bytes) => chatFileSize(bytes);
|
|
39
|
+
/**
|
|
40
|
+
* What a stored picture may be when the backend does not say (Drive's own defaults): its longest side in pixels, the
|
|
41
|
+
* highest quality in percent, and the most bytes. A company that hosts the site on its own machine raises Drive's
|
|
42
|
+
* settings, and the browser follows what Drive's List answers.
|
|
43
|
+
*/
|
|
44
|
+
export const PICTURE_LIMITS = { maxSide: 1920, quality: 80, maxBytes: 300 * 1024 };
|
|
45
|
+
/**
|
|
46
|
+
* A category's limits for a stored picture: what the backend said, else `PICTURE_LIMITS`; the bytes never above the
|
|
47
|
+
* category's own `maxBytes`, the quality at most 100.
|
|
48
|
+
*/
|
|
49
|
+
export function pictureLimits(category) {
|
|
50
|
+
const positive = (value) => (typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : null);
|
|
51
|
+
const bytes = positive(category.pictureMaxBytes) ?? PICTURE_LIMITS.maxBytes;
|
|
52
|
+
return {
|
|
53
|
+
maxSide: positive(category.pictureMaxSide) ?? PICTURE_LIMITS.maxSide,
|
|
54
|
+
quality: Math.min(100, positive(category.pictureQuality) ?? PICTURE_LIMITS.quality),
|
|
55
|
+
maxBytes: category.maxBytes > 0 ? Math.min(bytes, category.maxBytes) : bytes,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Why a chosen file would be refused, before it is sent: a kind the category does not take, or larger than it takes -
|
|
60
|
+
* for a picture, the category's picture limit (`pictureLimits`). The backend checks the bytes again; this only spares
|
|
61
|
+
* the person an upload that cannot succeed. Null when it fits.
|
|
62
|
+
*/
|
|
63
|
+
export function fileRefusal(category, file, name) {
|
|
64
|
+
if (file.type && !category.types.includes(file.type))
|
|
65
|
+
return `"${name}" is not a ${fileKindsSentence(category.types)} file.`;
|
|
66
|
+
const limit = isPictureType(file.type) ? pictureLimits(category).maxBytes : category.maxBytes;
|
|
67
|
+
if (limit > 0 && file.size > limit)
|
|
68
|
+
return `"${name}" is larger than the ${fileSizeText(limit)} this takes.`;
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A picture's kind read from its first bytes - JPEG, PNG or WebP - for a file the browser could not type (none, or
|
|
73
|
+
* `application/octet-stream`: a phone's share, a file dragged from another app, a name with no extension), so it still
|
|
74
|
+
* goes through the editor. Null for anything
|
|
75
|
+
* else, which is left to the backend (it reads the bytes too).
|
|
76
|
+
*/
|
|
77
|
+
export async function sniffPictureType(file) {
|
|
78
|
+
const head = new Uint8Array(await file.slice(0, 12).arrayBuffer());
|
|
79
|
+
const text = (from, to) => String.fromCharCode(...head.subarray(from, to));
|
|
80
|
+
if (head.length >= 3 && head[0] === 0xff && head[1] === 0xd8 && head[2] === 0xff)
|
|
81
|
+
return "image/jpeg";
|
|
82
|
+
if (head.length >= 8 && [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a].every((byte, at) => head[at] === byte))
|
|
83
|
+
return "image/png";
|
|
84
|
+
if (head.length >= 12 && text(0, 4) === "RIFF" && text(8, 12) === "WEBP")
|
|
85
|
+
return "image/webp";
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
/** A chosen file with its kind: the browser's, else a picture's read from its bytes (`sniffPictureType`), else as it was. */
|
|
89
|
+
export async function typedFile(file) {
|
|
90
|
+
if (file.type && file.type !== "application/octet-stream")
|
|
91
|
+
return file;
|
|
92
|
+
const type = await sniffPictureType(file);
|
|
93
|
+
return type ? new File([file], file.name, { type, lastModified: file.lastModified }) : file;
|
|
94
|
+
}
|
|
95
|
+
/** How many more files the owner keeps: never below 0. */
|
|
96
|
+
export const roomLeft = (files) => Math.max(0, files.category.maxCount - files.files.length);
|
|
97
|
+
/** The name a picture made by the editor is kept by: the chosen file's name with the saved kind's extension. */
|
|
98
|
+
export function pictureName(name, type, fallback = "Picture") {
|
|
99
|
+
const base = (name ?? "").replace(/\.[A-Za-z][A-Za-z0-9]{0,3}$/, "").trim() || fallback;
|
|
100
|
+
const extension = type === "image/png" ? "png" : type === "image/webp" ? "webp" : "jpg";
|
|
101
|
+
return `${base}.${extension}`;
|
|
102
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ApiModuleValue, ApiResult } from "../Interfaces/ApiInterfaces.ts";
|
|
2
|
+
import type { FileOwner, StoredFile } from "../Interfaces/FileInterfaces.ts";
|
|
3
|
+
/** Reads several owners' pictures, in the order asked: `FileAdapter.primaries`. */
|
|
4
|
+
export type PrimariesReader = (module: ApiModuleValue, category: string, ownerIds: readonly string[]) => Promise<ApiResult<(StoredFile | null)[]>>;
|
|
5
|
+
export interface PrimaryPicturesOptions {
|
|
6
|
+
/** The most owners one call takes, or a function asked when a batch is sent. Default 200, the Nexus Drive's limit. */
|
|
7
|
+
batchSize?: number | (() => number);
|
|
8
|
+
/** How long asks are gathered before they are sent, in milliseconds. Default 0: the asks of one render. */
|
|
9
|
+
delay?: number;
|
|
10
|
+
/** How long a refused or failed read is kept as "no picture" before an ask reads again, in milliseconds. Default 30000. */
|
|
11
|
+
retryAfter?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare class PrimaryPictures {
|
|
14
|
+
private readonly read;
|
|
15
|
+
private readonly batchSize;
|
|
16
|
+
private readonly delay;
|
|
17
|
+
private readonly retryAfter;
|
|
18
|
+
/** Answers kept: a file, or null for "no picture". A key that is missing has not been read. */
|
|
19
|
+
private readonly known;
|
|
20
|
+
/** Reads under way, so two thumbnails of one owner ask once. */
|
|
21
|
+
private readonly pending;
|
|
22
|
+
/** Asks gathered per module and category, not sent yet. */
|
|
23
|
+
private readonly queue;
|
|
24
|
+
private readonly listeners;
|
|
25
|
+
/** Owners whose last read failed, with the time an ask may read again. Their `known` answer is null until then. */
|
|
26
|
+
private readonly failedUntil;
|
|
27
|
+
/** Bumped by `clear`, so an answer that comes back after it is not kept. */
|
|
28
|
+
private generation;
|
|
29
|
+
private timer;
|
|
30
|
+
constructor(read: PrimariesReader, options?: PrimaryPicturesOptions);
|
|
31
|
+
/** The owner's picture as kept: a file, `null` for none, `undefined` when it has not been read. */
|
|
32
|
+
peek(owner: FileOwner<ApiModuleValue>): StoredFile | null | undefined;
|
|
33
|
+
/** The owner's picture, read with the other asks of this moment unless it is kept already. */
|
|
34
|
+
get(owner: FileOwner<ApiModuleValue>): Promise<StoredFile | null>;
|
|
35
|
+
/** Writes in what a change on this page made the owner's picture, and tells its thumbnails. */
|
|
36
|
+
set(owner: FileOwner<ApiModuleValue>, file: StoredFile | null): void;
|
|
37
|
+
/** Forgets the owner's picture, so the next ask reads it again. */
|
|
38
|
+
forget(owner: FileOwner<ApiModuleValue>): void;
|
|
39
|
+
/** Forgets every kept picture: after a sign-out, or a reload of the whole page's data. */
|
|
40
|
+
clear(): void;
|
|
41
|
+
/** Calls `listener` whenever the owner's kept picture changes. Answers the function that stops it. */
|
|
42
|
+
subscribe(owner: FileOwner<ApiModuleValue>, listener: () => void): () => void;
|
|
43
|
+
/** Whether the owner's last read failed and its wait is over: the next ask reads again. */
|
|
44
|
+
private mayRetry;
|
|
45
|
+
private notify;
|
|
46
|
+
private flush;
|
|
47
|
+
private send;
|
|
48
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { fileOwnerKey } from "./FileAdapter.js";
|
|
2
|
+
export class PrimaryPictures {
|
|
3
|
+
read;
|
|
4
|
+
batchSize;
|
|
5
|
+
delay;
|
|
6
|
+
retryAfter;
|
|
7
|
+
/** Answers kept: a file, or null for "no picture". A key that is missing has not been read. */
|
|
8
|
+
known = new Map();
|
|
9
|
+
/** Reads under way, so two thumbnails of one owner ask once. */
|
|
10
|
+
pending = new Map();
|
|
11
|
+
/** Asks gathered per module and category, not sent yet. */
|
|
12
|
+
queue = new Map();
|
|
13
|
+
listeners = new Map();
|
|
14
|
+
/** Owners whose last read failed, with the time an ask may read again. Their `known` answer is null until then. */
|
|
15
|
+
failedUntil = new Map();
|
|
16
|
+
/** Bumped by `clear`, so an answer that comes back after it is not kept. */
|
|
17
|
+
generation = 0;
|
|
18
|
+
timer = null;
|
|
19
|
+
constructor(read, options = {}) {
|
|
20
|
+
this.read = read;
|
|
21
|
+
const size = options.batchSize ?? 200;
|
|
22
|
+
this.batchSize = () => Math.max(1, typeof size === "function" ? size() : size);
|
|
23
|
+
this.delay = Math.max(0, options.delay ?? 0);
|
|
24
|
+
this.retryAfter = Math.max(0, options.retryAfter ?? 30_000);
|
|
25
|
+
}
|
|
26
|
+
/** The owner's picture as kept: a file, `null` for none, `undefined` when it has not been read. */
|
|
27
|
+
peek(owner) {
|
|
28
|
+
const key = fileOwnerKey(owner);
|
|
29
|
+
if (this.mayRetry(key))
|
|
30
|
+
return undefined;
|
|
31
|
+
return this.known.get(key);
|
|
32
|
+
}
|
|
33
|
+
/** The owner's picture, read with the other asks of this moment unless it is kept already. */
|
|
34
|
+
get(owner) {
|
|
35
|
+
const key = fileOwnerKey(owner);
|
|
36
|
+
if (this.mayRetry(key)) {
|
|
37
|
+
this.failedUntil.delete(key);
|
|
38
|
+
this.known.delete(key);
|
|
39
|
+
}
|
|
40
|
+
if (this.known.has(key))
|
|
41
|
+
return Promise.resolve(this.known.get(key) ?? null);
|
|
42
|
+
const pending = this.pending.get(key);
|
|
43
|
+
if (pending)
|
|
44
|
+
return pending;
|
|
45
|
+
const waiting = { owner, resolve: () => undefined, generation: this.generation };
|
|
46
|
+
const asked = new Promise(resolve => {
|
|
47
|
+
waiting.resolve = resolve;
|
|
48
|
+
});
|
|
49
|
+
waiting.asked = asked;
|
|
50
|
+
const group = `${String(owner.module)}|${owner.category}`;
|
|
51
|
+
const list = this.queue.get(group) ?? [];
|
|
52
|
+
list.push(waiting);
|
|
53
|
+
this.queue.set(group, list);
|
|
54
|
+
this.timer ??= setTimeout(() => this.flush(), this.delay);
|
|
55
|
+
this.pending.set(key, asked);
|
|
56
|
+
return asked;
|
|
57
|
+
}
|
|
58
|
+
/** Writes in what a change on this page made the owner's picture, and tells its thumbnails. */
|
|
59
|
+
set(owner, file) {
|
|
60
|
+
const key = fileOwnerKey(owner);
|
|
61
|
+
this.failedUntil.delete(key);
|
|
62
|
+
this.known.set(key, file);
|
|
63
|
+
this.notify(key);
|
|
64
|
+
}
|
|
65
|
+
/** Forgets the owner's picture, so the next ask reads it again. */
|
|
66
|
+
forget(owner) {
|
|
67
|
+
const key = fileOwnerKey(owner);
|
|
68
|
+
this.failedUntil.delete(key);
|
|
69
|
+
this.known.delete(key);
|
|
70
|
+
this.notify(key);
|
|
71
|
+
}
|
|
72
|
+
/** Forgets every kept picture: after a sign-out, or a reload of the whole page's data. */
|
|
73
|
+
clear() {
|
|
74
|
+
this.generation++;
|
|
75
|
+
this.known.clear();
|
|
76
|
+
this.failedUntil.clear();
|
|
77
|
+
// Reads still under way were asked for the person before; a `get` after this asks again.
|
|
78
|
+
this.pending.clear();
|
|
79
|
+
for (const key of this.listeners.keys())
|
|
80
|
+
this.notify(key);
|
|
81
|
+
}
|
|
82
|
+
/** Calls `listener` whenever the owner's kept picture changes. Answers the function that stops it. */
|
|
83
|
+
subscribe(owner, listener) {
|
|
84
|
+
const key = fileOwnerKey(owner);
|
|
85
|
+
const set = this.listeners.get(key) ?? new Set();
|
|
86
|
+
set.add(listener);
|
|
87
|
+
this.listeners.set(key, set);
|
|
88
|
+
return () => {
|
|
89
|
+
set.delete(listener);
|
|
90
|
+
if (set.size === 0)
|
|
91
|
+
this.listeners.delete(key);
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** Whether the owner's last read failed and its wait is over: the next ask reads again. */
|
|
95
|
+
mayRetry(key) {
|
|
96
|
+
const until = this.failedUntil.get(key);
|
|
97
|
+
return until !== undefined && Date.now() >= until;
|
|
98
|
+
}
|
|
99
|
+
notify(key) {
|
|
100
|
+
for (const listener of [...(this.listeners.get(key) ?? [])])
|
|
101
|
+
listener();
|
|
102
|
+
}
|
|
103
|
+
flush() {
|
|
104
|
+
this.timer = null;
|
|
105
|
+
const groups = [...this.queue.values()];
|
|
106
|
+
this.queue.clear();
|
|
107
|
+
for (const waiting of groups) {
|
|
108
|
+
const size = this.batchSize();
|
|
109
|
+
for (let start = 0; start < waiting.length; start += size)
|
|
110
|
+
void this.send(waiting.slice(start, start + size));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async send(batch) {
|
|
114
|
+
const first = batch[0].owner;
|
|
115
|
+
const answer = await this.read(first.module, first.category, batch.map(item => item.owner.ownerId)).catch(() => ({ ok: false }));
|
|
116
|
+
batch.forEach((item, index) => {
|
|
117
|
+
const key = fileOwnerKey(item.owner);
|
|
118
|
+
if (this.pending.get(key) === item.asked)
|
|
119
|
+
this.pending.delete(key);
|
|
120
|
+
const current = item.generation === this.generation;
|
|
121
|
+
if (!answer.ok) {
|
|
122
|
+
// Kept as no picture for a while, so the rows that draw again do not each ask again; then an ask reads again.
|
|
123
|
+
if (current && !this.known.has(key)) {
|
|
124
|
+
this.known.set(key, null);
|
|
125
|
+
this.failedUntil.set(key, Date.now() + this.retryAfter);
|
|
126
|
+
this.notify(key);
|
|
127
|
+
}
|
|
128
|
+
return item.resolve(null);
|
|
129
|
+
}
|
|
130
|
+
const file = answer.result?.[index] ?? null;
|
|
131
|
+
if (current && !this.known.has(key)) {
|
|
132
|
+
this.known.set(key, file);
|
|
133
|
+
this.notify(key);
|
|
134
|
+
}
|
|
135
|
+
item.resolve(current ? (this.known.get(key) ?? file) : file);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { PopupSize } from "../Components/Layouts/Popup.tsx";
|
|
3
|
+
import type { FormConfirmDetails, FormConfirmParams, FormValues } from "../Interfaces/FormInterfaces.ts";
|
|
4
|
+
import type { TableColumn, TableRow } from "../Interfaces/TableInterfaces.ts";
|
|
5
|
+
/** A run of neighbouring columns with one `group` (`group` null for columns with none): one section of a record's details. */
|
|
6
|
+
export interface CrudDetailsRun<TRow extends TableRow = TableRow> {
|
|
7
|
+
group: string | null;
|
|
8
|
+
columns: TableColumn<TRow>[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A record's fields in sections: each run of neighbouring columns that name the same `group` is one section under that
|
|
12
|
+
* heading, and a run of columns with no group is a section with none. A group named again further on starts a new run,
|
|
13
|
+
* as a table's band does. An empty group name counts as none.
|
|
14
|
+
*/
|
|
15
|
+
export declare function crudDetailsRuns<TRow extends TableRow>(columns: readonly TableColumn<TRow>[]): CrudDetailsRun<TRow>[];
|
|
16
|
+
/** Whether the form asks before this save: a `confirm` is given, and its `when` holds for the values (or it has none). */
|
|
17
|
+
export declare function formConfirmAsks<TValues extends FormValues>(confirm: FormConfirmParams<TValues> | null | undefined, values: TValues): boolean;
|
|
18
|
+
/** The question a form asks before a save, with every text settled for the values and the body to be sent. */
|
|
19
|
+
export interface FormConfirmQuestion {
|
|
20
|
+
title: ReactNode;
|
|
21
|
+
message: ReactNode;
|
|
22
|
+
details: FormConfirmDetails | null;
|
|
23
|
+
confirmLabel: ReactNode;
|
|
24
|
+
cancelLabel: ReactNode;
|
|
25
|
+
size: PopupSize;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The question for these values and this body: each text worked out from them, the details drawn when given (a null
|
|
29
|
+
* answer draws none), "Confirm and save" and "Back to the form" unless given, and the width `lg` with details (two
|
|
30
|
+
* fields to a line) or `sm` without, unless given. A rule that throws throws here, before anything is shown.
|
|
31
|
+
*/
|
|
32
|
+
export declare function formConfirmQuestion<TValues extends FormValues>(confirm: FormConfirmParams<TValues>, values: TValues, body: unknown): FormConfirmQuestion;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A record's fields in sections: each run of neighbouring columns that name the same `group` is one section under that
|
|
3
|
+
* heading, and a run of columns with no group is a section with none. A group named again further on starts a new run,
|
|
4
|
+
* as a table's band does. An empty group name counts as none.
|
|
5
|
+
*/
|
|
6
|
+
export function crudDetailsRuns(columns) {
|
|
7
|
+
const runs = [];
|
|
8
|
+
for (const column of columns) {
|
|
9
|
+
const group = column.group || null;
|
|
10
|
+
const last = runs[runs.length - 1];
|
|
11
|
+
if (last && last.group === group)
|
|
12
|
+
last.columns.push(column);
|
|
13
|
+
else
|
|
14
|
+
runs.push({ group, columns: [column] });
|
|
15
|
+
}
|
|
16
|
+
return runs;
|
|
17
|
+
}
|
|
18
|
+
/** Whether the form asks before this save: a `confirm` is given, and its `when` holds for the values (or it has none). */
|
|
19
|
+
export function formConfirmAsks(confirm, values) {
|
|
20
|
+
if (!confirm)
|
|
21
|
+
return false;
|
|
22
|
+
return confirm.when ? Boolean(confirm.when(values)) : true;
|
|
23
|
+
}
|
|
24
|
+
const settle = (text, values, body) => typeof text === "function" ? text(values, body) : text;
|
|
25
|
+
/**
|
|
26
|
+
* The question for these values and this body: each text worked out from them, the details drawn when given (a null
|
|
27
|
+
* answer draws none), "Confirm and save" and "Back to the form" unless given, and the width `lg` with details (two
|
|
28
|
+
* fields to a line) or `sm` without, unless given. A rule that throws throws here, before anything is shown.
|
|
29
|
+
*/
|
|
30
|
+
export function formConfirmQuestion(confirm, values, body) {
|
|
31
|
+
const details = confirm.details?.(values, body) ?? null;
|
|
32
|
+
return {
|
|
33
|
+
title: settle(confirm.title, values, body),
|
|
34
|
+
message: confirm.message === undefined ? null : settle(confirm.message, values, body),
|
|
35
|
+
details,
|
|
36
|
+
confirmLabel: confirm.confirmLabel === undefined ? "Confirm and save" : settle(confirm.confirmLabel, values, body),
|
|
37
|
+
cancelLabel: confirm.cancelLabel ?? "Back to the form",
|
|
38
|
+
size: confirm.size ?? (details ? "lg" : "sm"),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FormErrors, FormRuleResult, FormValues, InputParams, InputValidationContext } from "../Interfaces/FormInterfaces.ts";
|
|
2
|
+
import { type InputMemoryStorage } from "./InputMemory.ts";
|
|
2
3
|
export interface FormStoreOptions<TValues extends FormValues = FormValues> {
|
|
3
4
|
inputs: readonly InputParams[];
|
|
4
5
|
validate?(values: TValues): FormRuleResult;
|
|
@@ -58,6 +59,21 @@ export declare class FormStore<TValues extends FormValues = FormValues> {
|
|
|
58
59
|
private inputByName;
|
|
59
60
|
private values;
|
|
60
61
|
private initial;
|
|
62
|
+
/** The values the last load was given, as given: an input they name with a value is the form's own, never remembered. */
|
|
63
|
+
private given;
|
|
64
|
+
/** Where remembered inputs (`remember`) are read and kept; set by the form that draws this store, in the browser. */
|
|
65
|
+
private memory;
|
|
66
|
+
/** Pickers asked whether they still offer a remembered value; the next opening (a load, a reset) or a submit stops it. */
|
|
67
|
+
private memoryRound;
|
|
68
|
+
/** The inputs whose picker is being asked: drawn loading, and a change to one (the person's pick, code) ends its ask. */
|
|
69
|
+
private asking;
|
|
70
|
+
/** The values memory opened inputs on: an opening that finds nothing (a reset after the choice went) takes its own back. */
|
|
71
|
+
private fromMemory;
|
|
72
|
+
/**
|
|
73
|
+
* The values a load (or a reset, a draft) is working out, while it works them out: what `get` answers then, so an
|
|
74
|
+
* input's live `hidden` (`showWhen`) is judged on the values the form opens with, never on the ones it had before.
|
|
75
|
+
*/
|
|
76
|
+
private reading;
|
|
61
77
|
private isStarted;
|
|
62
78
|
private ruleErrors;
|
|
63
79
|
/** Rows inputs are checked when asked: 10,000 rows on every keystroke would be slow, and the rows check themselves. */
|
|
@@ -87,6 +103,8 @@ export declare class FormStore<TValues extends FormValues = FormValues> {
|
|
|
87
103
|
private draftDropped;
|
|
88
104
|
/** An input reported a value while the values loaded and was refused: it is drawn again from the store when loading ends. */
|
|
89
105
|
private refusedWhileLoading;
|
|
106
|
+
/** What a choice input reported beside the value the user picked last (`pickedItem`): its item, kept with that value. */
|
|
107
|
+
private picked;
|
|
90
108
|
/** A load during a render tells nobody; `announce` does, once the form has mounted. */
|
|
91
109
|
private quiet;
|
|
92
110
|
private unannounced;
|
|
@@ -100,6 +118,8 @@ export declare class FormStore<TValues extends FormValues = FormValues> {
|
|
|
100
118
|
/** Every value, plus the fields the record came with. The same object until something changes; do not change it. */
|
|
101
119
|
getValue(): TValues;
|
|
102
120
|
get(name: string): unknown;
|
|
121
|
+
/** The params of the input of this name, as the form was given them; undefined for a name no input has. */
|
|
122
|
+
inputParams(name: string): InputParams | undefined;
|
|
103
123
|
/** The values the form opened with, or saved last. */
|
|
104
124
|
getInitial(): TValues;
|
|
105
125
|
/** Only the values that differ from the ones the form opened with or saved last. */
|
|
@@ -107,11 +127,23 @@ export declare class FormStore<TValues extends FormValues = FormValues> {
|
|
|
107
127
|
/** The inputs whose values changed, in form order. */
|
|
108
128
|
changedInputs(): string[];
|
|
109
129
|
isChanged(): boolean;
|
|
130
|
+
/**
|
|
131
|
+
* Whether the changes are worth asking about before they are dropped (Cancel, closing a popup, leaving the page): a
|
|
132
|
+
* changed value of any input but a password or a one-time code (`keepsTyping`). A form where only those were typed is
|
|
133
|
+
* changed, and Save and its line say so, but nothing asks before it goes.
|
|
134
|
+
*/
|
|
135
|
+
isWorthKeeping(): boolean;
|
|
110
136
|
getStatus: () => FormStatus;
|
|
111
137
|
/** The message an input shows for the form: a server's, the form's rule once the input was left or a submit ran, or its own rule after a submit its input missed. */
|
|
112
138
|
shownError(name: string): string | undefined;
|
|
113
139
|
/** The shown message, with the form's rule messages given, such as the ones before a change. */
|
|
114
140
|
private shownWith;
|
|
141
|
+
/**
|
|
142
|
+
* The item a choice input reported with the value it holds now, when the user picked that value: a dropdown's option
|
|
143
|
+
* (its options, with `multiple`). `undefined` for a value loaded, set from code, or changed since - so what it answers
|
|
144
|
+
* always goes with the value. A CRUD names what a save joined from it (`crudJoinedNames`).
|
|
145
|
+
*/
|
|
146
|
+
pickedItem(name: string): unknown;
|
|
115
147
|
/** The input's own rule message for its value, whether it shows or not. */
|
|
116
148
|
ruleError(name: string): string | null;
|
|
117
149
|
/** The form's own rule message for an input, whether it shows or not. */
|
|
@@ -152,17 +184,21 @@ export declare class FormStore<TValues extends FormValues = FormValues> {
|
|
|
152
184
|
/** Tells listeners about a quiet load. */
|
|
153
185
|
announce(): void;
|
|
154
186
|
private loadValues;
|
|
155
|
-
/**
|
|
187
|
+
/**
|
|
188
|
+
* Brings back the values the form opened with or saved last, and clears every message. The draft goes too. A
|
|
189
|
+
* remembered input opens on the choice kept now, so a form reset after a save starts on what was just saved.
|
|
190
|
+
*/
|
|
156
191
|
reset(): void;
|
|
157
192
|
/** Sets a value from code. Its input shows it at once, and it counts as a change. */
|
|
158
193
|
set(name: string, value: unknown): void;
|
|
159
194
|
/** Sets several values from code, as one change. */
|
|
160
195
|
setValues(values: Partial<TValues>): void;
|
|
161
196
|
/**
|
|
162
|
-
* A value the user typed or picked, from the input rendered with `stamp
|
|
163
|
-
*
|
|
197
|
+
* A value the user typed or picked, from the input rendered with `stamp`, with what a choice input reported beside it
|
|
198
|
+
* (`item`: a dropdown's option, kept for `pickedItem`). An input that was replaced since (by a value from code, a load,
|
|
199
|
+
* or a reset) can still report its last value as it leaves; that value is ignored.
|
|
164
200
|
*/
|
|
165
|
-
input(name: string, value: unknown, stamp: string): void;
|
|
201
|
+
input(name: string, value: unknown, stamp: string, item?: unknown): void;
|
|
166
202
|
/** Marks an input as left by the user: the form's rule messages on it show from now on. */
|
|
167
203
|
touch(name: string): void;
|
|
168
204
|
/**
|
|
@@ -199,6 +235,28 @@ export declare class FormStore<TValues extends FormValues = FormValues> {
|
|
|
199
235
|
* `extra` (from the server, such as a new id) is set on top.
|
|
200
236
|
*/
|
|
201
237
|
commit(saved: TValues, extra?: Partial<TValues>): void;
|
|
238
|
+
/**
|
|
239
|
+
* @internal Whether the input's picker is being asked whether it still offers a remembered value: the input draws
|
|
240
|
+
* loading until it answers, the person picks, or the form opens again.
|
|
241
|
+
*/
|
|
242
|
+
isRemembering(name: string): boolean;
|
|
243
|
+
/**
|
|
244
|
+
* @internal The browser storage remembered inputs read and keep (`InputLayoutParams.remember`), set by the form that
|
|
245
|
+
* draws this store once it is in the browser. Inputs the values left without one open on it at once (as the values
|
|
246
|
+
* the form opened with), a picker once it answers that it still offers the value; a form drawn on a server opened
|
|
247
|
+
* without it, so nothing differs while it hydrates.
|
|
248
|
+
*/
|
|
249
|
+
attachMemory(storage: InputMemoryStorage | null): void;
|
|
250
|
+
/**
|
|
251
|
+
* Inputs that follow ones memory opened (`dependsOn`: a `compute.from`, `showWhen`, `spanWhen`, `labelWhen`, or a
|
|
252
|
+
* `narrowedBy`) draw again with their params read afresh, as after a change: a cell redraws only on its own bump.
|
|
253
|
+
*/
|
|
254
|
+
private bumpFollowers;
|
|
255
|
+
/**
|
|
256
|
+
* @internal A save: each remembered input keeps the value saved (an empty one forgets), for the next form that asks -
|
|
257
|
+
* under the saved values' `per` value, and only where its `when` holds on them.
|
|
258
|
+
*/
|
|
259
|
+
rememberSaved(values: TValues): void;
|
|
202
260
|
/** @internal A save after which the form went back to its opening values (`resetOnSuccess`). */
|
|
203
261
|
markSaved(): void;
|
|
204
262
|
/** @internal While the values load, nothing typed is taken (`input`), and a submit waits (`SubmitForm`). */
|
|
@@ -210,10 +268,37 @@ export declare class FormStore<TValues extends FormValues = FormValues> {
|
|
|
210
268
|
private indexInputs;
|
|
211
269
|
/** Each input's value in the type the input reports; a missing one is the input's default. Other fields stay as they came. */
|
|
212
270
|
private normalize;
|
|
271
|
+
/** Runs `work` with `get` answering from these values (a load's, a reset's, a draft's), and gives back its answer. */
|
|
272
|
+
private readingFrom;
|
|
273
|
+
/**
|
|
274
|
+
* The values with each remembered input the given values leave without one opened on the company's preferred value
|
|
275
|
+
* or the person's last choice (`rememberedOpeningNow`), its `when` and `per` read among these values; without storage
|
|
276
|
+
* (on a server, before the form is drawn) only a preferred value applies. A kept choice no longer offered is forgotten.
|
|
277
|
+
* A picker that must be asked first opens nothing now: it is asked, and its answer opens the input later
|
|
278
|
+
* (`askPickers`) - the round any earlier opening started stops here. An input memory opened before (a reset after a
|
|
279
|
+
* save, the storage arriving) that now opens on nothing takes the value the given values give it back.
|
|
280
|
+
*/
|
|
281
|
+
private openRemembered;
|
|
282
|
+
/** Stops asking pickers about remembered values: their answers no longer open anything, and their inputs stop loading. */
|
|
283
|
+
private stopAskingPickers;
|
|
284
|
+
/**
|
|
285
|
+
* Asks each picker whether it still offers the remembered value (`findRememberedOpening`), the input drawn loading
|
|
286
|
+
* meanwhile. An answer forgets a choice no longer offered, and opens an offered one (`openAsked`) only while the input
|
|
287
|
+
* is still asked about - no pick, no value from code since - and the values it was judged on (`rememberDependencies`:
|
|
288
|
+
* the group that narrows it, the one it is kept per, its `when`) are still the form's: a group changed meanwhile opens
|
|
289
|
+
* nothing.
|
|
290
|
+
*/
|
|
291
|
+
private askPickers;
|
|
292
|
+
/**
|
|
293
|
+
* A picker answered for a remembered value: the input opens on it, as the value the form opened with (not a change) -
|
|
294
|
+
* only while it is as the form opened it and still on the form. One the person typed in, or a draft gave back, keeps
|
|
295
|
+
* what it has.
|
|
296
|
+
*/
|
|
297
|
+
private openAsked;
|
|
213
298
|
/** Inputs that were not there, or were hidden, get their value in the input's type, as a load gives it. */
|
|
214
299
|
private adoptNewInputs;
|
|
215
300
|
private change;
|
|
216
|
-
/** The inputs whose `dependsOn` names any of `names`. */
|
|
301
|
+
/** The inputs whose `dependsOn` (or `narrowedBy`) names any of `names`. */
|
|
217
302
|
private followersOf;
|
|
218
303
|
private checkRule;
|
|
219
304
|
private checkAllRules;
|