nexus-shared 3.2.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2359 -0
- package/README.md +1911 -1123
- package/dist/Client.Index.d.ts +27 -0
- package/dist/Client.Index.js +27 -0
- package/dist/Components/Chats/ChatComposer.d.ts +7 -1
- package/dist/Components/Chats/ChatComposer.js +9 -6
- package/dist/Components/Chats/ChatParts.d.ts +15 -2
- package/dist/Components/Chats/ChatParts.js +15 -4
- package/dist/Components/Chats/ChatPopup.d.ts +46 -0
- package/dist/Components/Chats/ChatPopup.js +34 -0
- package/dist/Components/Chats/ChatThread.d.ts +9 -1
- package/dist/Components/Chats/ChatThread.js +54 -6
- package/dist/Components/Dashboards/DashboardBoard.d.ts +62 -0
- package/dist/Components/Dashboards/DashboardBoard.js +594 -0
- package/dist/Components/Dashboards/DashboardWidgets.d.ts +155 -0
- package/dist/Components/Dashboards/DashboardWidgets.js +159 -0
- package/dist/Components/Dashboards/WidgetFrame.d.ts +51 -0
- package/dist/Components/Dashboards/WidgetFrame.js +45 -0
- package/dist/Components/Documents/Button.d.ts +6 -1
- package/dist/Components/Documents/Button.js +2 -2
- package/dist/Components/Documents/DocumentFrame.d.ts +50 -0
- package/dist/Components/Documents/DocumentFrame.js +83 -0
- package/dist/Components/Documents/Menu.d.ts +4 -0
- package/dist/Components/Documents/Menu.js +40 -24
- package/dist/Components/Documents/PrintDocument.d.ts +95 -0
- package/dist/Components/Documents/PrintDocument.js +62 -0
- package/dist/Components/Documents/TabButtons.d.ts +6 -0
- package/dist/Components/Documents/TabButtons.js +5 -4
- package/dist/Components/Documents/Tabs.d.ts +59 -0
- package/dist/Components/Documents/Tabs.js +70 -0
- package/dist/Components/Domains/DomainCard.d.ts +98 -0
- package/dist/Components/Domains/DomainCard.js +182 -0
- package/dist/Components/Forms/ApiForm.js +24 -3
- package/dist/Components/Forms/Crud.d.ts +2 -2
- package/dist/Components/Forms/Crud.js +628 -247
- package/dist/Components/Forms/CrudActions.d.ts +137 -0
- package/dist/Components/Forms/CrudActions.js +214 -0
- package/dist/Components/Forms/CrudHistory.d.ts +6 -1
- package/dist/Components/Forms/CrudHistory.js +14 -6
- package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
- package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
- package/dist/Components/Forms/CrudParts.d.ts +37 -2
- package/dist/Components/Forms/CrudParts.js +123 -7
- package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
- package/dist/Components/Forms/CrudRowForms.js +174 -0
- package/dist/Components/Forms/CrudScopes.d.ts +28 -0
- package/dist/Components/Forms/CrudScopes.js +76 -0
- package/dist/Components/Forms/CrudTree.d.ts +90 -0
- package/dist/Components/Forms/CrudTree.js +363 -0
- package/dist/Components/Forms/CrudWorkspace.d.ts +73 -0
- package/dist/Components/Forms/CrudWorkspace.js +177 -0
- package/dist/Components/Forms/ExcelImport.js +3 -3
- package/dist/Components/Forms/Form.d.ts +6 -5
- package/dist/Components/Forms/Form.js +134 -21
- package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
- package/dist/Components/Forms/FormLeaveGuard.js +65 -0
- package/dist/Components/Forms/FormQuestion.d.ts +6 -0
- package/dist/Components/Forms/FormQuestion.js +28 -0
- package/dist/Components/Forms/RecordFlows.d.ts +50 -0
- package/dist/Components/Forms/RecordFlows.js +282 -0
- package/dist/Components/Forms/SubmitForm.d.ts +37 -2
- package/dist/Components/Forms/SubmitForm.js +182 -10
- package/dist/Components/Inputs/CheckBox.js +1 -1
- package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
- package/dist/Components/Inputs/ChoiceField.js +28 -2
- package/dist/Components/Inputs/ComputedInput.d.ts +51 -0
- package/dist/Components/Inputs/ComputedInput.js +179 -0
- package/dist/Components/Inputs/Dropdown.d.ts +13 -0
- package/dist/Components/Inputs/Dropdown.js +8 -6
- package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
- package/dist/Components/Inputs/DropdownParts.js +16 -4
- package/dist/Components/Inputs/Field.d.ts +7 -1
- package/dist/Components/Inputs/Field.js +2 -2
- package/dist/Components/Inputs/GroupForm.d.ts +6 -6
- package/dist/Components/Inputs/GroupForm.js +12 -12
- package/dist/Components/Inputs/InputRenderer.d.ts +13 -6
- package/dist/Components/Inputs/InputRenderer.js +37 -32
- package/dist/Components/Inputs/MarkdownEditor.d.ts +6 -0
- package/dist/Components/Inputs/MarkdownEditor.js +32 -6
- package/dist/Components/Inputs/PictureInput.d.ts +41 -0
- package/dist/Components/Inputs/PictureInput.js +75 -0
- package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
- package/dist/Components/Inputs/RadioGroup.js +8 -3
- package/dist/Components/Inputs/RowsInput.d.ts +15 -2
- package/dist/Components/Inputs/RowsInput.js +96 -8
- package/dist/Components/Inputs/TabularForm.d.ts +5 -5
- package/dist/Components/Inputs/TabularForm.js +45 -32
- package/dist/Components/Inputs/TextArea.d.ts +6 -0
- package/dist/Components/Inputs/TextArea.js +11 -2
- package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
- package/dist/Components/Inputs/TextBoxTypes.js +17 -0
- package/dist/Components/Inputs/TreeView.d.ts +76 -2
- package/dist/Components/Inputs/TreeView.js +227 -16
- package/dist/Components/Layouts/MessageBox.d.ts +15 -2
- package/dist/Components/Layouts/MessageBox.js +56 -7
- package/dist/Components/Layouts/Popup.d.ts +34 -7
- package/dist/Components/Layouts/Popup.js +143 -13
- package/dist/Components/Layouts/PopupDock.d.ts +3 -1
- package/dist/Components/Layouts/PopupDock.js +2 -1
- package/dist/Components/Layouts/PopupHost.js +5 -4
- package/dist/Components/Messages/MailClient.d.ts +50 -0
- package/dist/Components/Messages/MailClient.js +410 -0
- package/dist/Components/Messages/MailColumns.d.ts +21 -0
- package/dist/Components/Messages/MailColumns.js +92 -0
- package/dist/Components/Messages/MailCompose.d.ts +46 -0
- package/dist/Components/Messages/MailCompose.js +154 -0
- package/dist/Components/Messages/MailReader.d.ts +43 -0
- package/dist/Components/Messages/MailReader.js +207 -0
- package/dist/Components/Messages/MailThread.d.ts +71 -0
- package/dist/Components/Messages/MailThread.js +185 -0
- package/dist/Components/Messages/MessageBell.d.ts +41 -0
- package/dist/Components/Messages/MessageBell.js +76 -0
- package/dist/Components/Viewers/DataTable.js +153 -49
- package/dist/Components/Viewers/DataTableColumns.d.ts +2 -2
- package/dist/Components/Viewers/DataTableColumns.js +2 -1
- package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
- package/dist/Components/Viewers/DataTableFilters.js +17 -2
- package/dist/Components/Viewers/DataTableParts.d.ts +23 -5
- package/dist/Components/Viewers/DataTableParts.js +26 -14
- package/dist/Components/Viewers/OwnerFiles.d.ts +227 -0
- package/dist/Components/Viewers/OwnerFiles.js +700 -0
- package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
- package/dist/Components/Viewers/QrCodeImage.js +15 -0
- package/dist/Components/Viewers/RecordCards.d.ts +16 -0
- package/dist/Components/Viewers/RecordCards.js +509 -0
- package/dist/Helpers/ApiModules.js +4 -0
- package/dist/Helpers/AuditEntries.d.ts +50 -0
- package/dist/Helpers/AuditEntries.js +96 -0
- package/dist/Helpers/BusyHooks.d.ts +6 -0
- package/dist/Helpers/BusyHooks.js +39 -0
- package/dist/Helpers/ChatBackend.d.ts +8 -1
- package/dist/Helpers/ChatBackend.js +18 -0
- package/dist/Helpers/ChatHelpers.d.ts +2 -0
- package/dist/Helpers/ChatHelpers.js +4 -1
- package/dist/Helpers/CrudBackend.d.ts +20 -2
- package/dist/Helpers/CrudBackend.js +46 -2
- package/dist/Helpers/CrudColumns.d.ts +91 -6
- package/dist/Helpers/CrudColumns.js +148 -7
- package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
- package/dist/Helpers/CrudDetailsTabs.js +126 -0
- package/dist/Helpers/CrudHelpers.d.ts +116 -2
- package/dist/Helpers/CrudHelpers.js +256 -0
- package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
- package/dist/Helpers/CrudNarrowPick.js +157 -0
- package/dist/Helpers/CrudPickerPlan.d.ts +58 -0
- package/dist/Helpers/CrudPickerPlan.js +46 -0
- package/dist/Helpers/CrudRowForms.d.ts +32 -0
- package/dist/Helpers/CrudRowForms.js +76 -0
- package/dist/Helpers/CrudScopes.d.ts +32 -0
- package/dist/Helpers/CrudScopes.js +79 -0
- package/dist/Helpers/DashboardHelpers.d.ts +115 -0
- package/dist/Helpers/DashboardHelpers.js +305 -0
- package/dist/Helpers/DomainCards.d.ts +140 -0
- package/dist/Helpers/DomainCards.js +370 -0
- package/dist/Helpers/DropdownHelpers.d.ts +3 -1
- package/dist/Helpers/DropdownHelpers.js +6 -2
- package/dist/Helpers/ExcelHelpers.d.ts +1 -1
- package/dist/Helpers/ExcelHelpers.js +12 -11
- package/dist/Helpers/FileAdapter.d.ts +158 -0
- package/dist/Helpers/FileAdapter.js +173 -0
- package/dist/Helpers/FilePrimaries.d.ts +48 -0
- package/dist/Helpers/FilePrimaries.js +138 -0
- package/dist/Helpers/FormConfirm.d.ts +32 -0
- package/dist/Helpers/FormConfirm.js +40 -0
- package/dist/Helpers/FormStore.d.ts +100 -5
- package/dist/Helpers/FormStore.js +368 -25
- package/dist/Helpers/InputConditions.d.ts +56 -0
- package/dist/Helpers/InputConditions.js +192 -0
- package/dist/Helpers/InputMemory.d.ts +106 -0
- package/dist/Helpers/InputMemory.js +395 -0
- package/dist/Helpers/InputNarrowing.d.ts +50 -0
- package/dist/Helpers/InputNarrowing.js +148 -0
- package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
- package/dist/Helpers/InputParamsHelpers.js +157 -74
- package/dist/Helpers/MailClientHelpers.d.ts +96 -0
- package/dist/Helpers/MailClientHelpers.js +211 -0
- package/dist/Helpers/MessageBackend.d.ts +152 -0
- package/dist/Helpers/MessageBackend.js +329 -0
- package/dist/Helpers/MessageHelpers.d.ts +160 -0
- package/dist/Helpers/MessageHelpers.js +573 -0
- package/dist/Helpers/MessageUnread.d.ts +95 -0
- package/dist/Helpers/MessageUnread.js +245 -0
- package/dist/Helpers/NexusDriveAdapter.d.ts +71 -0
- package/dist/Helpers/NexusDriveAdapter.js +236 -0
- package/dist/Helpers/NumberHelpers.d.ts +156 -0
- package/dist/Helpers/NumberHelpers.js +242 -0
- package/dist/Helpers/PagedPicker.d.ts +42 -0
- package/dist/Helpers/PagedPicker.js +85 -0
- package/dist/Helpers/Permissions.js +2 -1
- package/dist/Helpers/QrCode.d.ts +26 -0
- package/dist/Helpers/QrCode.js +463 -0
- package/dist/Helpers/RecordCards.d.ts +205 -0
- package/dist/Helpers/RecordCards.js +378 -0
- package/dist/Helpers/RequestCache.d.ts +14 -2
- package/dist/Helpers/RequestCache.js +22 -3
- package/dist/Helpers/RowsStore.d.ts +61 -2
- package/dist/Helpers/RowsStore.js +203 -16
- package/dist/Helpers/ServerPreview.d.ts +69 -0
- package/dist/Helpers/ServerPreview.js +99 -0
- package/dist/Helpers/TableColumns.js +4 -3
- package/dist/Helpers/TableExport.d.ts +7 -1
- package/dist/Helpers/TableExport.js +9 -3
- package/dist/Helpers/TableFilters.d.ts +1 -1
- package/dist/Helpers/TableFilters.js +24 -23
- package/dist/Helpers/TableHelpers.d.ts +38 -4
- package/dist/Helpers/TableHelpers.js +78 -10
- package/dist/Helpers/TableJoins.d.ts +13 -0
- package/dist/Helpers/TableJoins.js +34 -0
- package/dist/Helpers/TableStore.d.ts +28 -4
- package/dist/Helpers/TableStore.js +73 -11
- package/dist/Helpers/ToolbarFit.d.ts +23 -0
- package/dist/Helpers/ToolbarFit.js +30 -0
- package/dist/Helpers/TreeStore.d.ts +156 -6
- package/dist/Helpers/TreeStore.js +547 -18
- package/dist/Interfaces/ApiInterfaces.d.ts +10 -1
- package/dist/Interfaces/ApiInterfaces.js +10 -1
- package/dist/Interfaces/ChatInterfaces.d.ts +22 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +539 -6
- package/dist/Interfaces/DashboardInterfaces.d.ts +123 -0
- package/dist/Interfaces/DashboardInterfaces.js +1 -0
- package/dist/Interfaces/DomainInterfaces.d.ts +145 -0
- package/dist/Interfaces/DomainInterfaces.js +1 -0
- package/dist/Interfaces/FileInterfaces.d.ts +183 -0
- package/dist/Interfaces/FileInterfaces.js +6 -0
- package/dist/Interfaces/FormInterfaces.d.ts +460 -32
- package/dist/Interfaces/FormInterfaces.js +79 -1
- package/dist/Interfaces/InputInterfaces.d.ts +22 -3
- package/dist/Interfaces/MailInterfaces.d.ts +192 -0
- package/dist/Interfaces/MailInterfaces.js +1 -0
- package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
- package/dist/Interfaces/RecordInterfaces.js +1 -0
- package/dist/Interfaces/TableInterfaces.d.ts +102 -3
- package/dist/Services/ExcelApi.js +5 -0
- package/dist/Services/FileApi.d.ts +13 -0
- package/dist/Services/FileApi.js +29 -0
- package/dist/Services/ImageEditorLoader.d.ts +14 -0
- package/dist/Services/ImageEditorLoader.js +51 -0
- package/dist/Services/JoinSources.d.ts +79 -6
- package/dist/Services/JoinSources.js +132 -21
- package/dist/Services/MessageApi.d.ts +18 -0
- package/dist/Services/MessageApi.js +64 -0
- package/dist/Services/ModuleImport.d.ts +7 -0
- package/dist/Services/ModuleImport.js +22 -0
- package/dist/Shared.Index.d.ts +28 -0
- package/dist/Shared.Index.js +28 -0
- package/package.json +13 -2
- package/src/Styles/Nexus.Avatar.css +6 -5
- package/src/Styles/Nexus.Button.css +39 -4
- package/src/Styles/Nexus.Chat.css +114 -22
- package/src/Styles/Nexus.Choice.css +16 -0
- package/src/Styles/Nexus.Crud.css +486 -21
- package/src/Styles/Nexus.Dashboard.css +763 -0
- package/src/Styles/Nexus.Domain.css +384 -0
- package/src/Styles/Nexus.Dropdown.css +8 -0
- package/src/Styles/Nexus.Editor.css +20 -0
- package/src/Styles/Nexus.Files.css +437 -0
- package/src/Styles/Nexus.Form.css +5 -2
- package/src/Styles/Nexus.Index.css +7 -0
- package/src/Styles/Nexus.Input.css +73 -0
- package/src/Styles/Nexus.Mail.css +1062 -0
- package/src/Styles/Nexus.Menu.css +10 -1
- package/src/Styles/Nexus.Popup.css +135 -1
- package/src/Styles/Nexus.Print.css +288 -0
- package/src/Styles/Nexus.QrCode.css +12 -0
- package/src/Styles/Nexus.Record.css +430 -0
- package/src/Styles/Nexus.Tab.Buttons.css +87 -22
- package/src/Styles/Nexus.Table.css +1458 -1387
- package/src/Styles/Nexus.Theme.Picker.css +1 -0
- package/src/Styles/Nexus.Themes.css +12 -6
- package/src/Styles/Nexus.Tokens.css +8 -3
- package/src/Styles/Nexus.Tree.css +41 -1
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { ApiModuleValue, ApiResult } from "../Interfaces/ApiInterfaces.ts";
|
|
2
|
+
import type { FileCallOptions, FileCategoryInfo, FileChange, 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
|
+
/**
|
|
18
|
+
* Deletes one of the owner's files. In a category that holds removals for a review (`FileCategoryInfo.holdsRemovals`)
|
|
19
|
+
* the file stays: the answer is the file as it is now, `pendingRemoval: true` (at least `{ pendingRemoval: true }`),
|
|
20
|
+
* which `removalHeld` reads.
|
|
21
|
+
*/
|
|
22
|
+
abstract remove(owner: FileOwner<ApiModuleValue>, fileId: string, options?: FileCallOptions): Promise<ApiResult<unknown>>;
|
|
23
|
+
/**
|
|
24
|
+
* Keeps a file whose removal waits for a review (`StoredFile.pendingRemoval`): the removal is cancelled and the file
|
|
25
|
+
* answered as it is now (null when the backend answers nothing). A backend without held removals leaves it out, and no
|
|
26
|
+
* Keep is offered.
|
|
27
|
+
*/
|
|
28
|
+
keep?(owner: FileOwner<ApiModuleValue>, fileId: string, options?: FileCallOptions): Promise<ApiResult<StoredFile | null>>;
|
|
29
|
+
/** The address an `<img>`, a link or the viewer opens the file by. */
|
|
30
|
+
abstract fileUrl(file: StoredFile, copy: FileCopy): string;
|
|
31
|
+
/**
|
|
32
|
+
* The address of the image editor module (`nexus-image-editor`, an ES module) as this backend serves it, versioned,
|
|
33
|
+
* so `loadImageEditor` can import it. Rejects with an `Error` in plain words when it is not there.
|
|
34
|
+
*/
|
|
35
|
+
abstract editorModuleUrl(signal?: AbortSignal): Promise<string>;
|
|
36
|
+
/** The most owners one `primaries` call takes. */
|
|
37
|
+
get maxOwners(): number;
|
|
38
|
+
/**
|
|
39
|
+
* The changes still allowed when a refused change says the owner's files may now be changed only some ways - a
|
|
40
|
+
* receipt posted since the page was read, whose documents are only added to - else `null` (any other refusal). The
|
|
41
|
+
* panel then offers only those for that owner, under the refusal's own words. Default: no refusal says so.
|
|
42
|
+
*/
|
|
43
|
+
changesLeft(answer: Pick<ApiResult, "messageCode" | "errorCode" | "status">): readonly FileChange[] | null;
|
|
44
|
+
}
|
|
45
|
+
/** Whether a Delete's answer says the file stays until a review (`FileAdapter.remove` in a category holding removals). */
|
|
46
|
+
export declare const removalHeld: (result: unknown) => boolean;
|
|
47
|
+
/** The words on a file whose removal waits for a review. */
|
|
48
|
+
export declare const PENDING_REMOVAL_TAG = "Removal waiting for review";
|
|
49
|
+
/** Who reviews a held removal when a page does not say (`OwnerFilesParams.removalReviewer`). */
|
|
50
|
+
export declare const REMOVAL_REVIEWER = "Nexus support";
|
|
51
|
+
/** "The picture stays until Nexus support approves its removal.": what a Delete held for a review says once sent. */
|
|
52
|
+
export declare const removalHeldSentence: (noun: string, reviewer?: string | null) => string;
|
|
53
|
+
/**
|
|
54
|
+
* "If the public sees this picture, it stays until Nexus support approves its removal.": what a Delete asks in a category
|
|
55
|
+
* that holds removals, where only a file the public sees (approved) is held and the answer says which.
|
|
56
|
+
*/
|
|
57
|
+
export declare const removalHeldQuestion: (noun: string, reviewer?: string | null) => string;
|
|
58
|
+
/** Whether a change is allowed: every change when no list is given, else the ones it names. */
|
|
59
|
+
export declare const allowsFileChange: (changes: readonly FileChange[] | null | undefined, change: FileChange) => boolean;
|
|
60
|
+
/**
|
|
61
|
+
* The changes allowed when both the page and a refusal narrowed them: those both allow. `null` from both: every change.
|
|
62
|
+
*/
|
|
63
|
+
export declare function narrowFileChanges(page: readonly FileChange[] | null | undefined, learned: readonly FileChange[] | null | undefined): readonly FileChange[] | null;
|
|
64
|
+
/**
|
|
65
|
+
* What a list says under its files while some changes are left out: the page's reason when its own `changes` leave them
|
|
66
|
+
* out, else the words of the refusal that narrowed them (`FileAdapter.changesLeft`); nothing while every change is allowed.
|
|
67
|
+
*/
|
|
68
|
+
export declare function fileChangesNote(page: {
|
|
69
|
+
changes?: readonly FileChange[] | null;
|
|
70
|
+
reason?: string | null;
|
|
71
|
+
}, learned: {
|
|
72
|
+
changes: readonly FileChange[];
|
|
73
|
+
reason: string | null;
|
|
74
|
+
} | null | undefined): string | null;
|
|
75
|
+
/** What an owner's files offer a person: each button a gallery, a document list or the picture input may show. */
|
|
76
|
+
export interface OwnerFileActions {
|
|
77
|
+
/** Upload (or Replace, in a category that keeps one file and has one: that also deletes the one kept). */
|
|
78
|
+
upload: boolean;
|
|
79
|
+
/** Rename a document. */
|
|
80
|
+
rename: boolean;
|
|
81
|
+
/** Delete a file. */
|
|
82
|
+
remove: boolean;
|
|
83
|
+
/** Make this file the owner's picture (the star); never on the one that already is. */
|
|
84
|
+
makePrimary: boolean;
|
|
85
|
+
/** Show no picture, the pictures kept (Remove, beside Upload). */
|
|
86
|
+
clearPrimary: boolean;
|
|
87
|
+
/** Edit a picture: saved as a new one - the owner's picture when this one was, in place of it in a one-file category. */
|
|
88
|
+
edit: boolean;
|
|
89
|
+
/** Keep a file whose removal waits for a review: the removal cancelled. Offered in place of Delete. */
|
|
90
|
+
keep: boolean;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The buttons offered for an owner's files, and for one file when given: none without `canWrite` (the backend's, a page's
|
|
94
|
+
* `readOnly` or `lockReason`), and each only when the changes allowed hold every change it makes - Replace and Edit in a
|
|
95
|
+
* one-file category delete the file kept; Edit of the owner's picture makes the new one the picture.
|
|
96
|
+
*
|
|
97
|
+
* Held removals (`holdsRemovals`): a file whose removal waits for a review offers only Keep (`keeps`: the backend can), never
|
|
98
|
+
* Rename, Delete, Make main or Edit; and a one-file category offers no Replace or in-place Edit, since its Delete frees no
|
|
99
|
+
* room until the review.
|
|
100
|
+
*/
|
|
101
|
+
export declare function ownerFileActions({ canWrite, changes, maxCount, kept, holdsRemovals, keeps }: {
|
|
102
|
+
canWrite: boolean;
|
|
103
|
+
changes?: readonly FileChange[] | null;
|
|
104
|
+
maxCount: number;
|
|
105
|
+
kept: number;
|
|
106
|
+
holdsRemovals?: boolean;
|
|
107
|
+
keeps?: boolean;
|
|
108
|
+
}, file?: Pick<StoredFile, "isPrimary" | "pendingRemoval"> | null): OwnerFileActions;
|
|
109
|
+
/** One owner's key, for caches and listeners: the same module, category and id are the same owner. */
|
|
110
|
+
export declare const fileOwnerKey: (owner: FileOwner<ApiModuleValue>) => string;
|
|
111
|
+
/** Whether a category takes anything that is not a picture: its files are a document list, not a gallery. */
|
|
112
|
+
export declare const takesDocuments: (category: Pick<FileCategoryInfo, "types">) => boolean;
|
|
113
|
+
/** What a file input offers for a category: its MIME types. */
|
|
114
|
+
export declare const acceptOf: (category: Pick<FileCategoryInfo, "types">) => string;
|
|
115
|
+
/** A file's kind in a word, for a list: "JPEG", "PDF". */
|
|
116
|
+
export declare function fileKindName(contentType: string): string;
|
|
117
|
+
/** "JPEG, PNG or WebP" for a category's types, as a sentence names them. */
|
|
118
|
+
export declare function fileKindsSentence(types: readonly string[]): string;
|
|
119
|
+
/** A size in words: "5 MB". */
|
|
120
|
+
export declare const fileSizeText: (bytes: number) => string;
|
|
121
|
+
/**
|
|
122
|
+
* What a stored picture may be when the backend does not say (Drive's own defaults): its longest side in pixels, the
|
|
123
|
+
* highest quality in percent, and the most bytes. A company that hosts the site on its own machine raises Drive's
|
|
124
|
+
* settings, and the browser follows what Drive's List answers.
|
|
125
|
+
*/
|
|
126
|
+
export declare const PICTURE_LIMITS: {
|
|
127
|
+
readonly maxSide: 1920;
|
|
128
|
+
readonly quality: 80;
|
|
129
|
+
readonly maxBytes: number;
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* A category's limits for a stored picture: what the backend said, else `PICTURE_LIMITS`; the bytes never above the
|
|
133
|
+
* category's own `maxBytes`, the quality at most 100.
|
|
134
|
+
*/
|
|
135
|
+
export declare function pictureLimits(category: Pick<FileCategoryInfo, "maxBytes"> & Partial<Pick<FileCategoryInfo, "pictureMaxSide" | "pictureQuality" | "pictureMaxBytes">>): {
|
|
136
|
+
maxSide: number;
|
|
137
|
+
quality: number;
|
|
138
|
+
maxBytes: number;
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Why a chosen file would be refused, before it is sent: a kind the category does not take, or larger than it takes -
|
|
142
|
+
* for a picture, the category's picture limit (`pictureLimits`). The backend checks the bytes again; this only spares
|
|
143
|
+
* the person an upload that cannot succeed. Null when it fits.
|
|
144
|
+
*/
|
|
145
|
+
export declare function fileRefusal(category: Pick<FileCategoryInfo, "types" | "maxBytes"> & Partial<Pick<FileCategoryInfo, "pictureMaxSide" | "pictureQuality" | "pictureMaxBytes">>, file: Pick<Blob, "type" | "size">, name: string): string | null;
|
|
146
|
+
/**
|
|
147
|
+
* A picture's kind read from its first bytes - JPEG, PNG or WebP - for a file the browser could not type (none, or
|
|
148
|
+
* `application/octet-stream`: a phone's share, a file dragged from another app, a name with no extension), so it still
|
|
149
|
+
* goes through the editor. Null for anything
|
|
150
|
+
* else, which is left to the backend (it reads the bytes too).
|
|
151
|
+
*/
|
|
152
|
+
export declare function sniffPictureType(file: Blob): Promise<PictureType | null>;
|
|
153
|
+
/** A chosen file with its kind: the browser's, else a picture's read from its bytes (`sniffPictureType`), else as it was. */
|
|
154
|
+
export declare function typedFile(file: File): Promise<File>;
|
|
155
|
+
/** How many more files the owner keeps: never below 0. */
|
|
156
|
+
export declare const roomLeft: (files: OwnerFiles) => number;
|
|
157
|
+
/** The name a picture made by the editor is kept by: the chosen file's name with the saved kind's extension. */
|
|
158
|
+
export declare function pictureName(name: string | null | undefined, type: string, fallback?: string): string;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { FILE_CHANGES, 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
|
+
* The changes still allowed when a refused change says the owner's files may now be changed only some ways - a
|
|
20
|
+
* receipt posted since the page was read, whose documents are only added to - else `null` (any other refusal). The
|
|
21
|
+
* panel then offers only those for that owner, under the refusal's own words. Default: no refusal says so.
|
|
22
|
+
*/
|
|
23
|
+
changesLeft(answer) {
|
|
24
|
+
void answer;
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Whether a Delete's answer says the file stays until a review (`FileAdapter.remove` in a category holding removals). */
|
|
29
|
+
export const removalHeld = (result) => typeof result === "object" && result !== null && result.pendingRemoval === true;
|
|
30
|
+
/** The words on a file whose removal waits for a review. */
|
|
31
|
+
export const PENDING_REMOVAL_TAG = "Removal waiting for review";
|
|
32
|
+
/** Who reviews a held removal when a page does not say (`OwnerFilesParams.removalReviewer`). */
|
|
33
|
+
export const REMOVAL_REVIEWER = "Nexus support";
|
|
34
|
+
/** "The picture stays until Nexus support approves its removal.": what a Delete held for a review says once sent. */
|
|
35
|
+
export const removalHeldSentence = (noun, reviewer) => `The ${noun} stays until ${reviewer?.trim() || REMOVAL_REVIEWER} approves its removal.`;
|
|
36
|
+
/**
|
|
37
|
+
* "If the public sees this picture, it stays until Nexus support approves its removal.": what a Delete asks in a category
|
|
38
|
+
* that holds removals, where only a file the public sees (approved) is held and the answer says which.
|
|
39
|
+
*/
|
|
40
|
+
export const removalHeldQuestion = (noun, reviewer) => `If the public sees this ${noun}, it stays until ${reviewer?.trim() || REMOVAL_REVIEWER} approves its removal.`;
|
|
41
|
+
/** Whether a change is allowed: every change when no list is given, else the ones it names. */
|
|
42
|
+
export const allowsFileChange = (changes, change) => !changes || changes.includes(change);
|
|
43
|
+
/**
|
|
44
|
+
* The changes allowed when both the page and a refusal narrowed them: those both allow. `null` from both: every change.
|
|
45
|
+
*/
|
|
46
|
+
export function narrowFileChanges(page, learned) {
|
|
47
|
+
if (!page)
|
|
48
|
+
return learned ?? null;
|
|
49
|
+
if (!learned)
|
|
50
|
+
return page;
|
|
51
|
+
return page.filter(change => learned.includes(change));
|
|
52
|
+
}
|
|
53
|
+
/** Whether a list of changes leaves any out (none given leaves nothing out). */
|
|
54
|
+
const leavesOut = (changes) => Boolean(changes) && FILE_CHANGES.some(change => !changes.includes(change));
|
|
55
|
+
/**
|
|
56
|
+
* What a list says under its files while some changes are left out: the page's reason when its own `changes` leave them
|
|
57
|
+
* out, else the words of the refusal that narrowed them (`FileAdapter.changesLeft`); nothing while every change is allowed.
|
|
58
|
+
*/
|
|
59
|
+
export function fileChangesNote(page, learned) {
|
|
60
|
+
if (!leavesOut(narrowFileChanges(page.changes, learned?.changes)))
|
|
61
|
+
return null;
|
|
62
|
+
const pageReason = leavesOut(page.changes) ? page.reason?.trim() : undefined;
|
|
63
|
+
return pageReason || learned?.reason?.trim() || null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The buttons offered for an owner's files, and for one file when given: none without `canWrite` (the backend's, a page's
|
|
67
|
+
* `readOnly` or `lockReason`), and each only when the changes allowed hold every change it makes - Replace and Edit in a
|
|
68
|
+
* one-file category delete the file kept; Edit of the owner's picture makes the new one the picture.
|
|
69
|
+
*
|
|
70
|
+
* Held removals (`holdsRemovals`): a file whose removal waits for a review offers only Keep (`keeps`: the backend can), never
|
|
71
|
+
* Rename, Delete, Make main or Edit; and a one-file category offers no Replace or in-place Edit, since its Delete frees no
|
|
72
|
+
* room until the review.
|
|
73
|
+
*/
|
|
74
|
+
export function ownerFileActions({ canWrite, changes, maxCount, kept, holdsRemovals = false, keeps = false }, file) {
|
|
75
|
+
const can = (change) => canWrite && allowsFileChange(changes, change);
|
|
76
|
+
const single = maxCount === 1;
|
|
77
|
+
const pending = file?.pendingRemoval === true;
|
|
78
|
+
// Replace and an in-place Edit delete the file kept first: held, it would still take the one place.
|
|
79
|
+
const replaces = can("remove") && !holdsRemovals;
|
|
80
|
+
return {
|
|
81
|
+
upload: can("add") && (!single || kept === 0 || replaces),
|
|
82
|
+
rename: can("rename") && !pending,
|
|
83
|
+
remove: can("remove") && !pending,
|
|
84
|
+
makePrimary: can("primary") && Boolean(file) && !file.isPrimary && !pending,
|
|
85
|
+
clearPrimary: can("primary"),
|
|
86
|
+
edit: can("add") && !pending && (single ? replaces : !file?.isPrimary || can("primary")),
|
|
87
|
+
keep: pending && keeps && can("remove"),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** One owner's key, for caches and listeners: the same module, category and id are the same owner. */
|
|
91
|
+
export const fileOwnerKey = (owner) => `${String(owner.module)}|${owner.category}|${owner.ownerId}`;
|
|
92
|
+
/** Whether a category takes anything that is not a picture: its files are a document list, not a gallery. */
|
|
93
|
+
export const takesDocuments = (category) => category.types.some(type => !isPictureType(type));
|
|
94
|
+
/** What a file input offers for a category: its MIME types. */
|
|
95
|
+
export const acceptOf = (category) => category.types.join(",");
|
|
96
|
+
/** A file's kind in a word, for a list: "JPEG", "PDF". */
|
|
97
|
+
export function fileKindName(contentType) {
|
|
98
|
+
const kind = contentType.split("/")[1] ?? contentType;
|
|
99
|
+
return kind === "jpeg" ? "JPEG" : kind.toUpperCase();
|
|
100
|
+
}
|
|
101
|
+
/** "JPEG, PNG or WebP" for a category's types, as a sentence names them. */
|
|
102
|
+
export function fileKindsSentence(types) {
|
|
103
|
+
const names = types.map(type => (type === "image/webp" ? "WebP" : fileKindName(type)));
|
|
104
|
+
if (names.length <= 1)
|
|
105
|
+
return names[0] ?? "";
|
|
106
|
+
return `${names.slice(0, -1).join(", ")} or ${names[names.length - 1]}`;
|
|
107
|
+
}
|
|
108
|
+
/** A size in words: "5 MB". */
|
|
109
|
+
export const fileSizeText = (bytes) => chatFileSize(bytes);
|
|
110
|
+
/**
|
|
111
|
+
* What a stored picture may be when the backend does not say (Drive's own defaults): its longest side in pixels, the
|
|
112
|
+
* highest quality in percent, and the most bytes. A company that hosts the site on its own machine raises Drive's
|
|
113
|
+
* settings, and the browser follows what Drive's List answers.
|
|
114
|
+
*/
|
|
115
|
+
export const PICTURE_LIMITS = { maxSide: 1920, quality: 80, maxBytes: 300 * 1024 };
|
|
116
|
+
/**
|
|
117
|
+
* A category's limits for a stored picture: what the backend said, else `PICTURE_LIMITS`; the bytes never above the
|
|
118
|
+
* category's own `maxBytes`, the quality at most 100.
|
|
119
|
+
*/
|
|
120
|
+
export function pictureLimits(category) {
|
|
121
|
+
const positive = (value) => (typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : null);
|
|
122
|
+
const bytes = positive(category.pictureMaxBytes) ?? PICTURE_LIMITS.maxBytes;
|
|
123
|
+
return {
|
|
124
|
+
maxSide: positive(category.pictureMaxSide) ?? PICTURE_LIMITS.maxSide,
|
|
125
|
+
quality: Math.min(100, positive(category.pictureQuality) ?? PICTURE_LIMITS.quality),
|
|
126
|
+
maxBytes: category.maxBytes > 0 ? Math.min(bytes, category.maxBytes) : bytes,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Why a chosen file would be refused, before it is sent: a kind the category does not take, or larger than it takes -
|
|
131
|
+
* for a picture, the category's picture limit (`pictureLimits`). The backend checks the bytes again; this only spares
|
|
132
|
+
* the person an upload that cannot succeed. Null when it fits.
|
|
133
|
+
*/
|
|
134
|
+
export function fileRefusal(category, file, name) {
|
|
135
|
+
if (file.type && !category.types.includes(file.type))
|
|
136
|
+
return `"${name}" is not a ${fileKindsSentence(category.types)} file.`;
|
|
137
|
+
const limit = isPictureType(file.type) ? pictureLimits(category).maxBytes : category.maxBytes;
|
|
138
|
+
if (limit > 0 && file.size > limit)
|
|
139
|
+
return `"${name}" is larger than the ${fileSizeText(limit)} this takes.`;
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* A picture's kind read from its first bytes - JPEG, PNG or WebP - for a file the browser could not type (none, or
|
|
144
|
+
* `application/octet-stream`: a phone's share, a file dragged from another app, a name with no extension), so it still
|
|
145
|
+
* goes through the editor. Null for anything
|
|
146
|
+
* else, which is left to the backend (it reads the bytes too).
|
|
147
|
+
*/
|
|
148
|
+
export async function sniffPictureType(file) {
|
|
149
|
+
const head = new Uint8Array(await file.slice(0, 12).arrayBuffer());
|
|
150
|
+
const text = (from, to) => String.fromCharCode(...head.subarray(from, to));
|
|
151
|
+
if (head.length >= 3 && head[0] === 0xff && head[1] === 0xd8 && head[2] === 0xff)
|
|
152
|
+
return "image/jpeg";
|
|
153
|
+
if (head.length >= 8 && [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a].every((byte, at) => head[at] === byte))
|
|
154
|
+
return "image/png";
|
|
155
|
+
if (head.length >= 12 && text(0, 4) === "RIFF" && text(8, 12) === "WEBP")
|
|
156
|
+
return "image/webp";
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
/** A chosen file with its kind: the browser's, else a picture's read from its bytes (`sniffPictureType`), else as it was. */
|
|
160
|
+
export async function typedFile(file) {
|
|
161
|
+
if (file.type && file.type !== "application/octet-stream")
|
|
162
|
+
return file;
|
|
163
|
+
const type = await sniffPictureType(file);
|
|
164
|
+
return type ? new File([file], file.name, { type, lastModified: file.lastModified }) : file;
|
|
165
|
+
}
|
|
166
|
+
/** How many more files the owner keeps: never below 0. */
|
|
167
|
+
export const roomLeft = (files) => Math.max(0, files.category.maxCount - files.files.length);
|
|
168
|
+
/** The name a picture made by the editor is kept by: the chosen file's name with the saved kind's extension. */
|
|
169
|
+
export function pictureName(name, type, fallback = "Picture") {
|
|
170
|
+
const base = (name ?? "").replace(/\.[A-Za-z][A-Za-z0-9]{0,3}$/, "").trim() || fallback;
|
|
171
|
+
const extension = type === "image/png" ? "png" : type === "image/webp" ? "webp" : "jpg";
|
|
172
|
+
return `${base}.${extension}`;
|
|
173
|
+
}
|
|
@@ -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
|
+
}
|