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,4 +1,5 @@
|
|
|
1
1
|
import { CRUD_MARK_BITS, hasMark, withMark } from "./CrudHelpers.js";
|
|
2
|
+
import { nowUtcDateTime } from "./DateTimeHelpers.js";
|
|
2
3
|
const NO_AUDIT_WORDS = { commands: {}, directions: {}, parts: {} };
|
|
3
4
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
5
|
export class CrudBackend {
|
|
@@ -117,13 +118,26 @@ export class CrudBackend {
|
|
|
117
118
|
}
|
|
118
119
|
/**
|
|
119
120
|
* The key an answer gives in place of the record: `Create` answering the new record's id. `null` when the answer
|
|
120
|
-
* is the record itself, or says nothing. With a key the new record goes straight into the list,
|
|
121
|
-
* a
|
|
121
|
+
* is the record itself, or says nothing. With a key the new record goes straight into the list, finished by `savedRow`;
|
|
122
|
+
* only a record whose list still cannot be told is read back on its own through `details` - never the whole list again.
|
|
122
123
|
*/
|
|
123
124
|
answeredKey(result) {
|
|
124
125
|
void result;
|
|
125
126
|
return null;
|
|
126
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* A record a save wrote - what the form sent, under what the answer said - with the fields only the server keeps
|
|
130
|
+
* filled in as this backend writes them, so nothing has to be read back: a status, the times, the marks. `before` is
|
|
131
|
+
* the record as the page holds it now (an edit; `undefined` for a new one): an edit's form carries its own copy of those
|
|
132
|
+
* fields from when it opened, which a flag or a mark made since has left behind. Default: the record as it is - and a
|
|
133
|
+
* record whose list still cannot be told from it (`listMembership`) is read back once through `details`, where there is
|
|
134
|
+
* one.
|
|
135
|
+
*/
|
|
136
|
+
savedRow(action, record, before) {
|
|
137
|
+
void action;
|
|
138
|
+
void before;
|
|
139
|
+
return record;
|
|
140
|
+
}
|
|
127
141
|
/**
|
|
128
142
|
* Whether a record belongs in a list, read from the record itself. It is what lets one answer be written into the
|
|
129
143
|
* lists that are **not** on screen: a flagged record joins the flagged list, a trashed one leaves the active list
|
|
@@ -328,6 +342,36 @@ export class NexusCrudBackend extends CrudBackend {
|
|
|
328
342
|
answeredKey(result) {
|
|
329
343
|
return typeof result === "string" && result !== "" ? result : null;
|
|
330
344
|
}
|
|
345
|
+
/**
|
|
346
|
+
* What a Nexus save leaves to the server, written as the server writes it, so nothing is read back (the owner,
|
|
347
|
+
* 2026-10-10: "the creationTime, recordStatus we will provide fake or just right now"). A new record lacking them is
|
|
348
|
+
* Active (every Create writes `NEXUS_ACTIVE_STATUS`), has no marks, was added now and never changed; what its answer
|
|
349
|
+
* said stays. An Update writes none of the status, the marks and the time added, so an edited record has the ones the
|
|
350
|
+
* page holds (`before`) - over its form's older copies - and was changed now. Times are UTC ISO strings, as a date-time
|
|
351
|
+
* column reads them.
|
|
352
|
+
*/
|
|
353
|
+
savedRow(action, record, before) {
|
|
354
|
+
const row = { ...record };
|
|
355
|
+
const now = nowUtcDateTime();
|
|
356
|
+
const fill = (field, value) => {
|
|
357
|
+
if (value !== undefined && (row[field] === undefined || row[field] === null))
|
|
358
|
+
row[field] = value;
|
|
359
|
+
};
|
|
360
|
+
if (action === "create") {
|
|
361
|
+
fill("recordStatus", NEXUS_ACTIVE_STATUS);
|
|
362
|
+
fill("bookmarkTypes", 0);
|
|
363
|
+
fill("creationTime", now);
|
|
364
|
+
fill("modificationTime", null);
|
|
365
|
+
return row;
|
|
366
|
+
}
|
|
367
|
+
const held = (before ?? {});
|
|
368
|
+
for (const field of ["recordStatus", "bookmarkTypes", "creationTime"]) {
|
|
369
|
+
if (held[field] !== undefined)
|
|
370
|
+
row[field] = held[field];
|
|
371
|
+
}
|
|
372
|
+
row.modificationTime = now;
|
|
373
|
+
return row;
|
|
374
|
+
}
|
|
331
375
|
/**
|
|
332
376
|
* The bands the owner settled on: under 40 is live, 25–29 of that is flagged, 40–59 is the trash, and 60 and up is
|
|
333
377
|
* deleted. A row without a `recordStatus` says nothing, and the lists it cannot answer for load again.
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import type { ApiEndpoint } from "../Interfaces/ApiInterfaces.ts";
|
|
2
|
-
import type
|
|
2
|
+
import { type InputParams } from "../Interfaces/FormInterfaces.ts";
|
|
3
3
|
import type { TableColumn, TableJoin, TableRow } from "../Interfaces/TableInterfaces.ts";
|
|
4
4
|
import { type CrudBackend } from "./CrudBackend.ts";
|
|
5
|
+
import { type PriceLadderOptions } from "./NumberHelpers.ts";
|
|
5
6
|
/** The longest description a record keeps: the owner's rule for every model (the column holds 4000). */
|
|
6
7
|
export declare const CRUD_DESCRIPTION_MAX = 1000;
|
|
8
|
+
/**
|
|
9
|
+
* The fields a record keeps its price's figures in (`crudInputs.netAmount`, `crudColumns.amounts`). Defaults: `price`,
|
|
10
|
+
* `discountPercentage` and `vatPercentage`, and no quantity (one of it).
|
|
11
|
+
*/
|
|
12
|
+
export type PriceKeys = {
|
|
13
|
+
price?: string;
|
|
14
|
+
quantity?: string;
|
|
15
|
+
discount?: string;
|
|
16
|
+
vat?: string;
|
|
17
|
+
};
|
|
7
18
|
export declare const crudColumns: {
|
|
8
|
-
/** The record's status in words ("Active", "Flagged", "Trashed"), from the backend's own numbers; blank
|
|
19
|
+
/** The record's status in words ("Active", "Flagged", "Trashed"), from the backend's own numbers; blank for a record that says none. */
|
|
9
20
|
status<TRow extends TableRow>(changes?: Partial<TableColumn<TRow>>, backend?: CrudBackend): TableColumn<TRow>;
|
|
10
21
|
/** The description, as Markdown: text in the table, formatted in the details. */
|
|
11
22
|
description<TRow extends TableRow>(changes?: Partial<TableColumn<TRow>>): TableColumn<TRow>;
|
|
@@ -16,11 +27,14 @@ export declare const crudColumns: {
|
|
|
16
27
|
/**
|
|
17
28
|
* Another record's name the **database** joined into the row: the row carries the name (`key`) and that record's own
|
|
18
29
|
* status (`status`), and the cell shows the name with a short tag when the record is no longer available ("Trashed",
|
|
19
|
-
* "Deleted": `CrudBackend.unavailableTag`). One call, nothing looked up.
|
|
30
|
+
* "Deleted": `CrudBackend.unavailableTag`). One call, nothing looked up. Give it `from` - the field holding that
|
|
31
|
+
* record's key, an input of the form: a save answers what was sent, not the names the database joins, so a CRUD names
|
|
32
|
+
* it again from the record picked for the key, with no call (`crudJoinedNames`). Without `from` a CRUD reads a new
|
|
33
|
+
* record back once (`details`) to name it.
|
|
20
34
|
*
|
|
21
|
-
* @example crudColumns.dbJoin({ key: "regionName", label: "Region", status: "regionRecordStatus" })
|
|
35
|
+
* @example crudColumns.dbJoin({ key: "regionName", label: "Region", status: "regionRecordStatus", from: "regionId" })
|
|
22
36
|
*/
|
|
23
|
-
dbJoin<TRow extends TableRow>({ status, ...column }: DbJoinColumn<TRow>, backend?: CrudBackend): TableColumn<TRow>;
|
|
37
|
+
dbJoin<TRow extends TableRow>({ status, from, ...column }: DbJoinColumn<TRow>, backend?: CrudBackend): TableColumn<TRow>;
|
|
24
38
|
/**
|
|
25
39
|
* Another record's name the **table** joins: the row carries only that record's key (`from`), and the table names it
|
|
26
40
|
* from the other controller's `findListing` - every key the rows hold in one call, kept for every table and form on the
|
|
@@ -30,10 +44,33 @@ export declare const crudColumns: {
|
|
|
30
44
|
* @example crudColumns.clientJoin(SUB_REGIONS, { key: "subRegionName", label: "Sub-region", from: "subRegionId" })
|
|
31
45
|
*/
|
|
32
46
|
clientJoin<TRow extends TableRow>(api: JoinController, { from, field, ...column }: ClientJoinColumn<TRow>, backend?: CrudBackend): TableColumn<TRow>;
|
|
47
|
+
/**
|
|
48
|
+
* A price's figures under one band, "Amount details" (2026-06-20's): **Price** (money), **Discount** and **VAT**
|
|
49
|
+
* (percents), and the **Calculated amount** - the price less the discount, plus VAT on the rest, worked out from the
|
|
50
|
+
* row's own figures by the server's rule (`priceAmounts`), so a row a save just answered shows it at once. A page
|
|
51
|
+
* spreads them where its price columns go: `...crudColumns.amounts()`. The Calculated amount's key is `netAmount`.
|
|
52
|
+
*
|
|
53
|
+
* @example const COLUMNS = [crudColumns.dbJoin({ ... }), ...crudColumns.amounts(), crudColumns.status()];
|
|
54
|
+
*/
|
|
55
|
+
amounts<TRow extends TableRow>(keys?: PriceKeys, changes?: {
|
|
56
|
+
group?: string;
|
|
57
|
+
}): TableColumn<TRow>[];
|
|
58
|
+
/**
|
|
59
|
+
* A receipt's money ladder as columns for a record's details (`CrudDetails`), in one section (`group`, default
|
|
60
|
+
* "Money"): Rate, Amount, Discount (5%), VAT (13%) - each left out when it is none - and the Calculated amount, money
|
|
61
|
+
* with 2 places, each read from the row's own field (`priceLadder`, on the server's `CounterAmounts` names unless
|
|
62
|
+
* `keys` says otherwise). Pass the person's number locale, so the percentages are written as they read numbers.
|
|
63
|
+
*
|
|
64
|
+
* @example ...crudColumns.priceLadder(line, { locale: useNumberLocale() })
|
|
65
|
+
*/
|
|
66
|
+
priceLadder<TRow extends TableRow>(row: Readonly<Record<string, unknown>>, options?: PriceLadderOptions & {
|
|
67
|
+
group?: string;
|
|
68
|
+
}): TableColumn<TRow>[];
|
|
33
69
|
};
|
|
34
|
-
/** What `crudColumns.dbJoin` takes: a column,
|
|
70
|
+
/** What `crudColumns.dbJoin` takes: a column, the row's field that holds the joined record's status, and the one with its key. */
|
|
35
71
|
export type DbJoinColumn<TRow extends TableRow> = Partial<TableColumn<TRow>> & Pick<TableColumn<TRow>, "key" | "label"> & {
|
|
36
72
|
status: string;
|
|
73
|
+
from?: string;
|
|
37
74
|
};
|
|
38
75
|
/** What `crudColumns.clientJoin` takes: a column, the row's field that holds the other record's key, and its own field. */
|
|
39
76
|
export type ClientJoinColumn<TRow extends TableRow> = Partial<TableColumn<TRow>> & Pick<TableColumn<TRow>, "key" | "label"> & Pick<TableJoin, "from" | "field">;
|
|
@@ -48,4 +85,14 @@ export declare const crudInputs: {
|
|
|
48
85
|
* in a form, such as `span`.
|
|
49
86
|
*/
|
|
50
87
|
description(changes?: Partial<InputParams>): InputParams;
|
|
88
|
+
/**
|
|
89
|
+
* The **Calculated amount** in a price's form: a box after Price, Discount and VAT that follows them as they are typed
|
|
90
|
+
* (`compute`), by the server's rule (`priceAmounts`) - never typed, never sent, never a change. Blank while the price
|
|
91
|
+
* is empty, so a cleared price never reads 0.00. Span 3, so Price | Discount | VAT | Calculated amount fill a row (two
|
|
92
|
+
* pairs on a narrow form). `changes` may replace any key, `compute` included: a counter's box shows the server's
|
|
93
|
+
* preview instead (`compute: { from: [], value: (_values, line) => line?.netAmount }` with the form's `computeWith`).
|
|
94
|
+
*
|
|
95
|
+
* @example INPUTS = [..., price, discountPercentage, vatPercentage, crudInputs.netAmount()]
|
|
96
|
+
*/
|
|
97
|
+
netAmount(keys?: PriceKeys, changes?: Partial<InputParams>): InputParams;
|
|
51
98
|
};
|
|
@@ -1,15 +1,50 @@
|
|
|
1
|
+
import { EInputTypes } from "../Interfaces/FormInterfaces.js";
|
|
1
2
|
import { getCrudBackend } from "./CrudBackend.js";
|
|
3
|
+
import { priceAmounts, priceLadder } from "./NumberHelpers.js";
|
|
2
4
|
import { readPath } from "./TableHelpers.js";
|
|
3
5
|
/*
|
|
4
6
|
* The columns and inputs every Nexus record has, so a page names them instead of writing them again: its status in
|
|
5
7
|
* words, its Markdown description, and when it was added and changed - and the two ways a page shows another record's
|
|
6
8
|
* name, joined by the database (`dbJoin`) or by the table (`clientJoin`). Each takes changes over its defaults, as a
|
|
7
|
-
* column or an input of the page's own would: `crudColumns.added({ hidden: false })`.
|
|
9
|
+
* column or an input of the page's own would: `crudColumns.added({ hidden: false })`. A record with a price has its
|
|
10
|
+
* money the same way: its figures and the Calculated amount (`amounts`, `netAmount`), and a receipt's ladder
|
|
11
|
+
* (`priceLadder`), each in one wording for every page.
|
|
8
12
|
*/
|
|
9
13
|
/** The longest description a record keeps: the owner's rule for every model (the column holds 4000). */
|
|
10
14
|
export const CRUD_DESCRIPTION_MAX = 1000;
|
|
15
|
+
/** The words of the worked-out figure, wherever it is a label: the owner's "calculate value", 2026-06-20's "Calculated Amount". */
|
|
16
|
+
const NET_AMOUNT_LABEL = "Calculated amount";
|
|
17
|
+
/** The price's fields, with the defaults. */
|
|
18
|
+
function priceKeysOf(keys = {}) {
|
|
19
|
+
return { price: keys.price ?? "price", quantity: keys.quantity, discount: keys.discount ?? "discountPercentage", vat: keys.vat ?? "vatPercentage" };
|
|
20
|
+
}
|
|
21
|
+
/** A figure among a record's (or a form's) values: a number, or the text of one; anything else is missing. */
|
|
22
|
+
function figureAt(values, key) {
|
|
23
|
+
if (key === undefined)
|
|
24
|
+
return null;
|
|
25
|
+
const value = readPath(values, key);
|
|
26
|
+
if (typeof value === "number")
|
|
27
|
+
return Number.isFinite(value) ? value : null;
|
|
28
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
29
|
+
const number = Number(value);
|
|
30
|
+
return Number.isFinite(number) ? number : null;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
/** The Calculated amount of a record's (or a form's) values by the server's rule (`priceAmounts`); null while the price is empty. */
|
|
35
|
+
function netAmountAt(values, keys) {
|
|
36
|
+
const price = readPath(values, keys.price);
|
|
37
|
+
if (price === null || price === undefined || price === "")
|
|
38
|
+
return null;
|
|
39
|
+
return priceAmounts({
|
|
40
|
+
price: figureAt(values, keys.price),
|
|
41
|
+
quantity: keys.quantity === undefined ? 1 : figureAt(values, keys.quantity),
|
|
42
|
+
discountPercentage: figureAt(values, keys.discount),
|
|
43
|
+
vatPercentage: figureAt(values, keys.vat),
|
|
44
|
+
}).netAmount;
|
|
45
|
+
}
|
|
11
46
|
export const crudColumns = {
|
|
12
|
-
/** The record's status in words ("Active", "Flagged", "Trashed"), from the backend's own numbers; blank
|
|
47
|
+
/** The record's status in words ("Active", "Flagged", "Trashed"), from the backend's own numbers; blank for a record that says none. */
|
|
13
48
|
status(changes = {}, backend) {
|
|
14
49
|
return {
|
|
15
50
|
key: "recordStatus",
|
|
@@ -34,12 +69,15 @@ export const crudColumns = {
|
|
|
34
69
|
/**
|
|
35
70
|
* Another record's name the **database** joined into the row: the row carries the name (`key`) and that record's own
|
|
36
71
|
* status (`status`), and the cell shows the name with a short tag when the record is no longer available ("Trashed",
|
|
37
|
-
* "Deleted": `CrudBackend.unavailableTag`). One call, nothing looked up.
|
|
72
|
+
* "Deleted": `CrudBackend.unavailableTag`). One call, nothing looked up. Give it `from` - the field holding that
|
|
73
|
+
* record's key, an input of the form: a save answers what was sent, not the names the database joins, so a CRUD names
|
|
74
|
+
* it again from the record picked for the key, with no call (`crudJoinedNames`). Without `from` a CRUD reads a new
|
|
75
|
+
* record back once (`details`) to name it.
|
|
38
76
|
*
|
|
39
|
-
* @example crudColumns.dbJoin({ key: "regionName", label: "Region", status: "regionRecordStatus" })
|
|
77
|
+
* @example crudColumns.dbJoin({ key: "regionName", label: "Region", status: "regionRecordStatus", from: "regionId" })
|
|
40
78
|
*/
|
|
41
|
-
dbJoin({ status, ...column }, backend) {
|
|
42
|
-
return { tag: row => (backend ?? getCrudBackend()).unavailableTag(readPath(row, status)), ...column };
|
|
79
|
+
dbJoin({ status, from, ...column }, backend) {
|
|
80
|
+
return { tag: row => (backend ?? getCrudBackend()).unavailableTag(readPath(row, status)), ...column, dbJoin: from ? { from, status } : { status } };
|
|
43
81
|
},
|
|
44
82
|
/**
|
|
45
83
|
* Another record's name the **table** joins: the row carries only that record's key (`from`), and the table names it
|
|
@@ -56,6 +94,36 @@ export const crudColumns = {
|
|
|
56
94
|
...column,
|
|
57
95
|
};
|
|
58
96
|
},
|
|
97
|
+
/**
|
|
98
|
+
* A price's figures under one band, "Amount details" (2026-06-20's): **Price** (money), **Discount** and **VAT**
|
|
99
|
+
* (percents), and the **Calculated amount** - the price less the discount, plus VAT on the rest, worked out from the
|
|
100
|
+
* row's own figures by the server's rule (`priceAmounts`), so a row a save just answered shows it at once. A page
|
|
101
|
+
* spreads them where its price columns go: `...crudColumns.amounts()`. The Calculated amount's key is `netAmount`.
|
|
102
|
+
*
|
|
103
|
+
* @example const COLUMNS = [crudColumns.dbJoin({ ... }), ...crudColumns.amounts(), crudColumns.status()];
|
|
104
|
+
*/
|
|
105
|
+
amounts(keys, changes = {}) {
|
|
106
|
+
const k = priceKeysOf(keys);
|
|
107
|
+
const group = changes.group ?? "Amount details";
|
|
108
|
+
return [
|
|
109
|
+
{ key: k.price, label: "Price", format: "money", decimals: 2, width: "8rem", group },
|
|
110
|
+
{ key: k.discount, label: "Discount", format: "percent", decimals: 2, width: "7rem", group },
|
|
111
|
+
{ key: k.vat, label: "VAT", format: "percent", decimals: 2, width: "6rem", group },
|
|
112
|
+
{ key: "netAmount", label: NET_AMOUNT_LABEL, format: "money", decimals: 2, width: "9rem", group, value: row => netAmountAt(row, k) },
|
|
113
|
+
];
|
|
114
|
+
},
|
|
115
|
+
/**
|
|
116
|
+
* A receipt's money ladder as columns for a record's details (`CrudDetails`), in one section (`group`, default
|
|
117
|
+
* "Money"): Rate, Amount, Discount (5%), VAT (13%) - each left out when it is none - and the Calculated amount, money
|
|
118
|
+
* with 2 places, each read from the row's own field (`priceLadder`, on the server's `CounterAmounts` names unless
|
|
119
|
+
* `keys` says otherwise). Pass the person's number locale, so the percentages are written as they read numbers.
|
|
120
|
+
*
|
|
121
|
+
* @example ...crudColumns.priceLadder(line, { locale: useNumberLocale() })
|
|
122
|
+
*/
|
|
123
|
+
priceLadder(row, options = {}) {
|
|
124
|
+
const { group = "Money", ...ladder } = options;
|
|
125
|
+
return priceLadder(row, ladder).map(line => ({ key: line.key, label: line.label, format: "money", decimals: 2, group }));
|
|
126
|
+
},
|
|
59
127
|
};
|
|
60
128
|
export const crudInputs = {
|
|
61
129
|
/**
|
|
@@ -64,6 +132,29 @@ export const crudInputs = {
|
|
|
64
132
|
* in a form, such as `span`.
|
|
65
133
|
*/
|
|
66
134
|
description(changes = {}) {
|
|
67
|
-
return { type:
|
|
135
|
+
return { type: EInputTypes.Markdown, name: "description", label: "Description", maxLength: CRUD_DESCRIPTION_MAX, height: 160, hint: "Anything worth knowing about it. Markdown.", span: 12, ...changes };
|
|
136
|
+
},
|
|
137
|
+
/**
|
|
138
|
+
* The **Calculated amount** in a price's form: a box after Price, Discount and VAT that follows them as they are typed
|
|
139
|
+
* (`compute`), by the server's rule (`priceAmounts`) - never typed, never sent, never a change. Blank while the price
|
|
140
|
+
* is empty, so a cleared price never reads 0.00. Span 3, so Price | Discount | VAT | Calculated amount fill a row (two
|
|
141
|
+
* pairs on a narrow form). `changes` may replace any key, `compute` included: a counter's box shows the server's
|
|
142
|
+
* preview instead (`compute: { from: [], value: (_values, line) => line?.netAmount }` with the form's `computeWith`).
|
|
143
|
+
*
|
|
144
|
+
* @example INPUTS = [..., price, discountPercentage, vatPercentage, crudInputs.netAmount()]
|
|
145
|
+
*/
|
|
146
|
+
netAmount(keys, changes = {}) {
|
|
147
|
+
const k = priceKeysOf(keys);
|
|
148
|
+
const from = [k.price, k.quantity, k.discount, k.vat].filter((key) => key !== undefined);
|
|
149
|
+
return {
|
|
150
|
+
type: EInputTypes.Number,
|
|
151
|
+
name: "netAmount",
|
|
152
|
+
label: NET_AMOUNT_LABEL,
|
|
153
|
+
decimals: 2,
|
|
154
|
+
span: 3,
|
|
155
|
+
hint: "Price less the discount, plus VAT on the rest.",
|
|
156
|
+
compute: { from, value: values => netAmountAt(values, k) },
|
|
157
|
+
...changes,
|
|
158
|
+
};
|
|
68
159
|
},
|
|
69
160
|
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ApiEndpoint } from "../Interfaces/ApiInterfaces.ts";
|
|
2
|
+
import type { CrudDetailsPicture, CrudDetailsProfile, CrudDetailsTab } from "../Interfaces/CrudInterfaces.ts";
|
|
3
|
+
import type { FormValues } from "../Interfaces/FormInterfaces.ts";
|
|
4
|
+
import type { TableColumn } from "../Interfaces/TableInterfaces.ts";
|
|
5
|
+
/** The built-in tab: the record's fields, with the details' `aside` beside them. */
|
|
6
|
+
export declare const CRUD_DETAILS_TAB = "details";
|
|
7
|
+
/** A tab as offered: its label settled and its badge worked out for the record. */
|
|
8
|
+
export interface OfferedCrudTab<TRow extends FormValues = FormValues> extends CrudDetailsTab<TRow> {
|
|
9
|
+
label: string;
|
|
10
|
+
/** Whether it is the built-in fields (a `details` entry without a `render`). */
|
|
11
|
+
builtIn: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The tabs offered for a record, in the page's order: the built-in `details` first unless the page lists it, each tab
|
|
15
|
+
* whose `endpoint` the person may not call left out (`can`, the rule every action follows), and each the page hides
|
|
16
|
+
* for this record. A tab id listed twice is offered once, where it is first listed.
|
|
17
|
+
*/
|
|
18
|
+
export declare function offeredCrudTabs<TRow extends FormValues>(tabs: readonly CrudDetailsTab<TRow>[] | undefined, row: TRow, can: (endpoint: ApiEndpoint) => boolean): OfferedCrudTab<TRow>[];
|
|
19
|
+
/** The tab a popup opens on: the one asked for, else the one kept, while offered; otherwise the first. */
|
|
20
|
+
export declare function openingCrudTab(offered: readonly {
|
|
21
|
+
id: string;
|
|
22
|
+
}[], ...wanted: (string | null | undefined)[]): string | undefined;
|
|
23
|
+
/** Where this browser keeps the last tab: `nx-crud-tab:{key}:{viewer}`; nothing without a viewer. */
|
|
24
|
+
export declare function crudTabStorageKey(remember: {
|
|
25
|
+
key: string;
|
|
26
|
+
viewer?: string | null;
|
|
27
|
+
} | undefined): string | null;
|
|
28
|
+
/** The kept tab's id, or null. */
|
|
29
|
+
export declare function readCrudTab(storage: Pick<Storage, "getItem"> | null, key: string | null): string | null;
|
|
30
|
+
/** Keeps the tab's id; a browser that will not keep it (storage off, full) is left as it is. */
|
|
31
|
+
export declare function writeCrudTab(storage: Pick<Storage, "setItem"> | null, key: string | null, tab: string): void;
|
|
32
|
+
/** Whether a profile is given as its parts (a `title`), rather than drawn by the page. */
|
|
33
|
+
export declare function isCrudProfile<TRow extends FormValues = FormValues>(value: unknown): value is CrudDetailsProfile<TRow>;
|
|
34
|
+
/** How a details popup's full record (its details endpoint) is coming along: being read, read, or not read (it failed). */
|
|
35
|
+
export type CrudRecordRead = "loading" | "done" | "failed";
|
|
36
|
+
/**
|
|
37
|
+
* Whether a field waits on the full record: it is still being read (or its read failed) and the row has no value under
|
|
38
|
+
* the field's key at all - the list left it out. A column that draws or works out its own value (`render`, `value`)
|
|
39
|
+
* never waits: only it knows what it reads. The Details tab and the profile draw such a field blank while it is read,
|
|
40
|
+
* and "Not loaded" when the read failed.
|
|
41
|
+
*/
|
|
42
|
+
export declare function isAwaitedCrudField<TRow extends FormValues>(column: TableColumn<TRow>, row: TRow, read: CrudRecordRead): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* The fields a record's profile shows (`CrudDetailsProfile.fields`), in the page's order: a column given whole, or the
|
|
45
|
+
* details' column of the key given (a key no column has is left out); a key named twice is shown once, where first
|
|
46
|
+
* named; and a field with no value for the record - its own `value`, a join's or the field's: null, undefined, blank
|
|
47
|
+
* text, an empty list, NaN - is left out. `false` and `0` are values. A field the row lacks while the full record is
|
|
48
|
+
* read, or after its read failed (`isAwaitedCrudField`), is kept: the profile holds its line, then says "Not loaded".
|
|
49
|
+
*/
|
|
50
|
+
export declare function crudProfileFields<TRow extends FormValues>(fields: readonly (TableColumn<TRow> | string)[] | undefined, columns: readonly TableColumn<TRow>[], row: TRow, read?: CrudRecordRead): TableColumn<TRow>[];
|
|
51
|
+
/**
|
|
52
|
+
* The details' columns less every key a profile's `fields` names, so the Details tab never shows a field the profile
|
|
53
|
+
* does. A key the profile leaves out for having no value stays out of the Details too: it has nothing to show there. A
|
|
54
|
+
* field still being read, or whose read failed, is in the profile (`crudProfileFields`), held or "Not loaded".
|
|
55
|
+
*/
|
|
56
|
+
export declare function crudDetailsWithoutProfile<TRow extends FormValues>(columns: readonly TableColumn<TRow>[], fields: readonly (TableColumn<TRow> | string)[] | undefined): readonly TableColumn<TRow>[];
|
|
57
|
+
/** Whether a profile's picture is given by the owner's Drive params, rather than drawn by the page. */
|
|
58
|
+
export declare function isCrudPicture(value: unknown): value is CrudDetailsPicture;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { isEmptyValue } from "./InputConditions.js";
|
|
2
|
+
import { columnValue, readPath } from "./TableHelpers.js";
|
|
3
|
+
/*
|
|
4
|
+
* The rules of a details popup's tabs (`CrudDetailsOptions.tabs`), apart from drawing them, so they are tested without
|
|
5
|
+
* a browser: which tabs a person is offered for a record, which one opens, and where this browser keeps the last one;
|
|
6
|
+
* and which fields its profile shows (`profile.fields`), which the Details tab then leaves out.
|
|
7
|
+
*/
|
|
8
|
+
/** The built-in tab: the record's fields, with the details' `aside` beside them. */
|
|
9
|
+
export const CRUD_DETAILS_TAB = "details";
|
|
10
|
+
/**
|
|
11
|
+
* The tabs offered for a record, in the page's order: the built-in `details` first unless the page lists it, each tab
|
|
12
|
+
* whose `endpoint` the person may not call left out (`can`, the rule every action follows), and each the page hides
|
|
13
|
+
* for this record. A tab id listed twice is offered once, where it is first listed.
|
|
14
|
+
*/
|
|
15
|
+
export function offeredCrudTabs(tabs, row, can) {
|
|
16
|
+
const listed = tabs ?? [];
|
|
17
|
+
const all = listed.some(tab => tab.id === CRUD_DETAILS_TAB) ? listed : [{ id: CRUD_DETAILS_TAB }, ...listed];
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
const offered = [];
|
|
20
|
+
for (const tab of all) {
|
|
21
|
+
if (seen.has(tab.id))
|
|
22
|
+
continue;
|
|
23
|
+
seen.add(tab.id);
|
|
24
|
+
if (tab.endpoint && !can(tab.endpoint))
|
|
25
|
+
continue;
|
|
26
|
+
if (tab.hidden?.(row))
|
|
27
|
+
continue;
|
|
28
|
+
const builtIn = tab.id === CRUD_DETAILS_TAB && !tab.render;
|
|
29
|
+
offered.push({ ...tab, label: tab.label ?? (tab.id === CRUD_DETAILS_TAB ? "Details" : tab.id), builtIn });
|
|
30
|
+
}
|
|
31
|
+
return offered;
|
|
32
|
+
}
|
|
33
|
+
/** The tab a popup opens on: the one asked for, else the one kept, while offered; otherwise the first. */
|
|
34
|
+
export function openingCrudTab(offered, ...wanted) {
|
|
35
|
+
for (const id of wanted)
|
|
36
|
+
if (id && offered.some(tab => tab.id === id))
|
|
37
|
+
return id;
|
|
38
|
+
return offered[0]?.id;
|
|
39
|
+
}
|
|
40
|
+
/** Where this browser keeps the last tab: `nx-crud-tab:{key}:{viewer}`; nothing without a viewer. */
|
|
41
|
+
export function crudTabStorageKey(remember) {
|
|
42
|
+
const viewer = remember?.viewer?.trim();
|
|
43
|
+
return remember?.key && viewer ? `nx-crud-tab:${remember.key}:${viewer}` : null;
|
|
44
|
+
}
|
|
45
|
+
/** The kept tab's id, or null. */
|
|
46
|
+
export function readCrudTab(storage, key) {
|
|
47
|
+
if (!storage || !key)
|
|
48
|
+
return null;
|
|
49
|
+
try {
|
|
50
|
+
const kept = storage.getItem(key);
|
|
51
|
+
return kept ? kept : null;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Keeps the tab's id; a browser that will not keep it (storage off, full) is left as it is. */
|
|
58
|
+
export function writeCrudTab(storage, key, tab) {
|
|
59
|
+
if (!storage || !key)
|
|
60
|
+
return;
|
|
61
|
+
try {
|
|
62
|
+
storage.setItem(key, tab);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Not kept: the popup opens on the first tab next time.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Whether a profile is given as its parts (a `title`), rather than drawn by the page. */
|
|
69
|
+
export function isCrudProfile(value) {
|
|
70
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && "title" in value && !("$$typeof" in value);
|
|
71
|
+
}
|
|
72
|
+
/** The key a profile's field names: a column's own, or the key given. */
|
|
73
|
+
const profileFieldKey = (field) => (typeof field === "string" ? field : field.key);
|
|
74
|
+
/**
|
|
75
|
+
* Whether a field waits on the full record: it is still being read (or its read failed) and the row has no value under
|
|
76
|
+
* the field's key at all - the list left it out. A column that draws or works out its own value (`render`, `value`)
|
|
77
|
+
* never waits: only it knows what it reads. The Details tab and the profile draw such a field blank while it is read,
|
|
78
|
+
* and "Not loaded" when the read failed.
|
|
79
|
+
*/
|
|
80
|
+
export function isAwaitedCrudField(column, row, read) {
|
|
81
|
+
return read !== "done" && !column.render && !column.value && readPath(row, column.key) === undefined;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The fields a record's profile shows (`CrudDetailsProfile.fields`), in the page's order: a column given whole, or the
|
|
85
|
+
* details' column of the key given (a key no column has is left out); a key named twice is shown once, where first
|
|
86
|
+
* named; and a field with no value for the record - its own `value`, a join's or the field's: null, undefined, blank
|
|
87
|
+
* text, an empty list, NaN - is left out. `false` and `0` are values. A field the row lacks while the full record is
|
|
88
|
+
* read, or after its read failed (`isAwaitedCrudField`), is kept: the profile holds its line, then says "Not loaded".
|
|
89
|
+
*/
|
|
90
|
+
export function crudProfileFields(fields, columns, row, read = "done") {
|
|
91
|
+
const seen = new Set();
|
|
92
|
+
const shown = [];
|
|
93
|
+
for (const field of fields ?? []) {
|
|
94
|
+
const key = profileFieldKey(field);
|
|
95
|
+
if (seen.has(key))
|
|
96
|
+
continue;
|
|
97
|
+
seen.add(key);
|
|
98
|
+
const column = typeof field === "string" ? columns.find(one => one.key === field) : field;
|
|
99
|
+
if (!column)
|
|
100
|
+
continue;
|
|
101
|
+
if (isAwaitedCrudField(column, row, read)) {
|
|
102
|
+
shown.push(column);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const value = columnValue(column, row);
|
|
106
|
+
if (isEmptyValue(value) || (typeof value === "number" && Number.isNaN(value)))
|
|
107
|
+
continue;
|
|
108
|
+
shown.push(column);
|
|
109
|
+
}
|
|
110
|
+
return shown;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The details' columns less every key a profile's `fields` names, so the Details tab never shows a field the profile
|
|
114
|
+
* does. A key the profile leaves out for having no value stays out of the Details too: it has nothing to show there. A
|
|
115
|
+
* field still being read, or whose read failed, is in the profile (`crudProfileFields`), held or "Not loaded".
|
|
116
|
+
*/
|
|
117
|
+
export function crudDetailsWithoutProfile(columns, fields) {
|
|
118
|
+
if (!fields?.length)
|
|
119
|
+
return columns;
|
|
120
|
+
const taken = new Set(fields.map(profileFieldKey));
|
|
121
|
+
return columns.filter(column => !taken.has(column.key));
|
|
122
|
+
}
|
|
123
|
+
/** Whether a profile's picture is given by the owner's Drive params, rather than drawn by the page. */
|
|
124
|
+
export function isCrudPicture(value) {
|
|
125
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && "category" in value && "ownerId" in value && !("$$typeof" in value);
|
|
126
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { ApiEndpoint } from "../Interfaces/ApiInterfaces.ts";
|
|
2
|
-
import type { CrudActionName, CrudEndpoints, CrudMark, CrudMassActionName, CrudMode, CrudRowActionName } from "../Interfaces/CrudInterfaces.ts";
|
|
2
|
+
import type { CrudActionName, CrudChange, CrudEndpoints, CrudMark, CrudMassActionName, CrudMode, CrudNarrow, CrudRowActionName, CrudTreePlacement } from "../Interfaces/CrudInterfaces.ts";
|
|
3
|
+
import type { FormValues } from "../Interfaces/FormInterfaces.ts";
|
|
4
|
+
import type { CrudBackend } from "./CrudBackend.ts";
|
|
5
|
+
import type { TreeItemChange } from "./TreeStore.ts";
|
|
3
6
|
import type { MessageActionName } from "../Interfaces/MessageInterfaces.ts";
|
|
4
|
-
import type { TablePage, TablePaging, TableRow } from "../Interfaces/TableInterfaces.ts";
|
|
7
|
+
import type { TableColumn, TablePage, TablePaging, TableRow } from "../Interfaces/TableInterfaces.ts";
|
|
5
8
|
/** Every list, in the order the CRUD shows them. */
|
|
6
9
|
export declare const CRUD_MODES: readonly CrudMode[];
|
|
7
10
|
/** The built-in actions on one record, per list, in their menu order. A record's history is on every list. */
|
|
@@ -40,6 +43,82 @@ export declare function massActionAllowed(action: CrudMassActionName, api: CrudE
|
|
|
40
43
|
* the list it is in. `markOn` is the record's new flag.
|
|
41
44
|
*/
|
|
42
45
|
export declare function leavesList(action: CrudRowActionName | CrudMassActionName, mode: CrudMode, markOn?: boolean): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* A CRUD change as changes to a tree that shows the same records (`CrudTreePlacement`): a record in the active list is
|
|
48
|
+
* put under its parent - added, changed or moved - and one out of it (in the trash, deleted) leaves the tree. Where it
|
|
49
|
+
* was comes from `before`: under its parent while it was active, out of the tree when it was not or is new, and not
|
|
50
|
+
* known otherwise (the tree then looks for it). A record without a status counts as active. Pins and favourites are a
|
|
51
|
+
* user's own and change no tree.
|
|
52
|
+
*/
|
|
53
|
+
export declare function crudTreeChanges<TRow extends FormValues, TItem extends object>(change: CrudChange<TRow>, place: CrudTreePlacement<TRow, TItem>, backend: CrudBackend): TreeItemChange<TItem>[];
|
|
54
|
+
/**
|
|
55
|
+
* The records whose child count a change moved, among `rows` (a list's rows), through a tree placement: what the tree's
|
|
56
|
+
* parents follow (`crudTreeChanges`) written into the parents' own rows. A record added or back under a parent counts one
|
|
57
|
+
* more there; one moved counts one fewer where it was and one more where it went; one trashed or deleted one fewer. Each
|
|
58
|
+
* parent row comes back with what `recordOf` reads from its item with the count moved written over it (so `recordOf`
|
|
59
|
+
* must carry the item's count back into the record); a row whose count did not move, or a count that is not a number, is
|
|
60
|
+
* left out.
|
|
61
|
+
*/
|
|
62
|
+
export declare function crudParentCounts<TRow extends FormValues, TItem extends object>(treeChanges: readonly TreeItemChange<TItem>[], rows: readonly TRow[], place: CrudTreePlacement<TRow, TItem>): TRow[];
|
|
63
|
+
/**
|
|
64
|
+
* A saved record with the child count a tree placement keeps for it (`rules.hasChildrenKey`): a new record's own, as
|
|
65
|
+
* `itemOf` gives it (none yet), and an edited one's from the row the page holds (`held`) - its form's copy is as old as
|
|
66
|
+
* the form, and a child added since counts. Written over the record through `recordOf`, which carries an item's count
|
|
67
|
+
* back; a placement without a count leaves the record as it is.
|
|
68
|
+
*/
|
|
69
|
+
export declare function crudSavedCount<TRow extends FormValues, TItem extends object>(record: TRow, held: TRow | undefined, place: CrudTreePlacement<TRow, TItem>): TRow;
|
|
70
|
+
/** What a save knows of the record a joined key names (`crudJoinedNames`): its name, and its status when it says. */
|
|
71
|
+
export interface CrudJoinedRecord {
|
|
72
|
+
name: unknown;
|
|
73
|
+
status?: unknown;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The names the database joins into a record (`crudColumns.dbJoin` columns with `from`), written again after a save that
|
|
77
|
+
* answered only what was sent - with no call. For each such column: a key the save kept as `before` had it keeps
|
|
78
|
+
* `before`'s name and status; an emptied key empties both; a new or changed key takes the name and the status of the
|
|
79
|
+
* record `find(from, key)` gives - the one picked in the form, or one the page holds. Where it gives none, a new record
|
|
80
|
+
* keeps the name its save answered, if any, and an edited one drops the old record's name and status rather than show
|
|
81
|
+
* them for the new key: either way the record is then read back once (`crudJoinsMissing`). Names and statuses are written
|
|
82
|
+
* as top-level fields of the record.
|
|
83
|
+
*/
|
|
84
|
+
export declare function crudJoinedNames<TRow extends TableRow>(record: TRow, before: TRow | undefined, columns: readonly TableColumn<TRow>[], find: (from: string, key: string) => CrudJoinedRecord | null | undefined): TRow;
|
|
85
|
+
/**
|
|
86
|
+
* Whether a saved record still lacks a name the database joins into it, which is the one case a CRUD reads it back
|
|
87
|
+
* (`details`, once): a `crudColumns.dbJoin` column whose name the record does not hold while its key is set - no record
|
|
88
|
+
* was known for a new key (`crudJoinedNames`) - or one that names no key field (`from`) on a new record, whose save
|
|
89
|
+
* answered no name. An edited record keeps the name it had on a column without `from`: whether its key changed cannot
|
|
90
|
+
* be told.
|
|
91
|
+
*/
|
|
92
|
+
export declare function crudJoinsMissing<TRow extends TableRow>(record: TRow, before: TRow | undefined, columns: readonly TableColumn<TRow>[]): boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Where an Add or Edit form with fixed fields stands, as text for its popup's key and its draft's: the fixed fields'
|
|
95
|
+
* names and values, in name order (`"@countryId=7&parentId="`), or "" when nothing is fixed. So a form opened inside one
|
|
96
|
+
* parent never comes back inside another, a fixed form never stands in for a free one, and a draft written before the
|
|
97
|
+
* record moved is not brought back over its new place.
|
|
98
|
+
*/
|
|
99
|
+
export declare function crudFormPlace(fixed: Readonly<Record<string, unknown>> | null | undefined): string;
|
|
100
|
+
/** What a narrowed list shows and fixes, read once from its `narrow`. */
|
|
101
|
+
export interface CrudNarrowing {
|
|
102
|
+
/** The field of the list's records that names the picked record. */
|
|
103
|
+
key: string;
|
|
104
|
+
/** The picked record's value as text, which a record's field must equal; `null` while none is picked: the list is empty. */
|
|
105
|
+
value: string | null;
|
|
106
|
+
/** The same value as the picked record holds it (a number stays a number): what a new record and a server's filter get. */
|
|
107
|
+
raw: unknown;
|
|
108
|
+
/** The picked record's name: its `labelKey` field, else the value. */
|
|
109
|
+
label: string;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* A `Crud` `narrow` read once: the field, the picked record's value (its `valueKey` field, else its `key` field) and its
|
|
113
|
+
* name (its `labelKey` field, else the value). `null` without a narrowing; a value of `null` while nothing is picked, or
|
|
114
|
+
* the picked record holds no value.
|
|
115
|
+
*/
|
|
116
|
+
export declare function crudNarrowing(narrow: CrudNarrow | null | undefined): CrudNarrowing | null;
|
|
117
|
+
/**
|
|
118
|
+
* Whether a record belongs in a narrowed list: its field holds the picked record's value (compared as text, so 7 and "7"
|
|
119
|
+
* are one). Every record does without a narrowing, and none while nothing is picked.
|
|
120
|
+
*/
|
|
121
|
+
export declare function crudInNarrowing(row: TableRow, narrowing: CrudNarrowing | null): boolean;
|
|
43
122
|
/** The message builder's action for a built-in action: "Guest recovered.", "Unpin 3 guests?" */
|
|
44
123
|
export declare function crudMessageAction(action: CrudRowActionName | CrudMassActionName, markOn?: boolean): MessageActionName;
|
|
45
124
|
/** What every CRUD follows, whatever module it shows. `configureCrud` changes it once, for the whole app. */
|