nexus-shared 4.0.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 +795 -0
- package/README.md +220 -14
- package/dist/Client.Index.d.ts +13 -0
- package/dist/Client.Index.js +13 -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/TabButtons.d.ts +6 -0
- package/dist/Components/Documents/TabButtons.js +5 -4
- package/dist/Components/Domains/DomainCard.d.ts +98 -0
- package/dist/Components/Domains/DomainCard.js +182 -0
- package/dist/Components/Forms/ApiForm.js +5 -1
- package/dist/Components/Forms/Crud.js +72 -24
- package/dist/Components/Forms/CrudActions.d.ts +5 -4
- package/dist/Components/Forms/CrudActions.js +42 -12
- package/dist/Components/Forms/CrudHistory.d.ts +6 -1
- package/dist/Components/Forms/CrudHistory.js +14 -6
- package/dist/Components/Forms/CrudParts.d.ts +11 -4
- package/dist/Components/Forms/CrudParts.js +76 -10
- package/dist/Components/Forms/CrudRowForms.js +65 -5
- package/dist/Components/Forms/CrudTree.js +7 -7
- package/dist/Components/Forms/CrudWorkspace.d.ts +15 -5
- package/dist/Components/Forms/CrudWorkspace.js +19 -11
- package/dist/Components/Forms/Form.d.ts +3 -2
- package/dist/Components/Forms/Form.js +33 -26
- package/dist/Components/Forms/FormQuestion.d.ts +6 -0
- package/dist/Components/Forms/FormQuestion.js +28 -0
- package/dist/Components/Forms/SubmitForm.js +2 -15
- package/dist/Components/Inputs/ComputedInput.d.ts +51 -0
- package/dist/Components/Inputs/ComputedInput.js +179 -0
- package/dist/Components/Inputs/Dropdown.js +4 -2
- package/dist/Components/Inputs/GroupForm.js +3 -2
- package/dist/Components/Inputs/InputRenderer.d.ts +5 -1
- package/dist/Components/Inputs/MarkdownEditor.d.ts +6 -0
- package/dist/Components/Inputs/MarkdownEditor.js +32 -6
- package/dist/Components/Inputs/PictureInput.js +8 -3
- package/dist/Components/Inputs/RowsInput.d.ts +14 -1
- package/dist/Components/Inputs/RowsInput.js +78 -5
- package/dist/Components/Inputs/TabularForm.js +16 -3
- package/dist/Components/Inputs/TreeView.d.ts +24 -1
- package/dist/Components/Inputs/TreeView.js +84 -7
- package/dist/Components/Layouts/Popup.d.ts +22 -2
- package/dist/Components/Layouts/Popup.js +108 -6
- 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 +7 -5
- package/dist/Components/Viewers/DataTableColumns.d.ts +2 -2
- package/dist/Components/Viewers/DataTableColumns.js +2 -1
- package/dist/Components/Viewers/DataTableParts.d.ts +3 -1
- package/dist/Components/Viewers/DataTableParts.js +2 -2
- package/dist/Components/Viewers/OwnerFiles.d.ts +57 -5
- package/dist/Components/Viewers/OwnerFiles.js +142 -31
- 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/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/CrudColumns.d.ts +44 -6
- package/dist/Helpers/CrudColumns.js +63 -13
- package/dist/Helpers/CrudHelpers.d.ts +36 -1
- package/dist/Helpers/CrudHelpers.js +61 -0
- package/dist/Helpers/CrudPickerPlan.d.ts +58 -0
- package/dist/Helpers/CrudPickerPlan.js +46 -0
- package/dist/Helpers/CrudRowForms.d.ts +18 -1
- package/dist/Helpers/CrudRowForms.js +35 -2
- 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/FileAdapter.d.ts +82 -2
- package/dist/Helpers/FileAdapter.js +72 -1
- package/dist/Helpers/FormStore.d.ts +11 -1
- package/dist/Helpers/FormStore.js +66 -19
- package/dist/Helpers/InputConditions.d.ts +6 -0
- package/dist/Helpers/InputConditions.js +24 -0
- package/dist/Helpers/InputMemory.js +40 -30
- package/dist/Helpers/InputNarrowing.d.ts +38 -6
- package/dist/Helpers/InputNarrowing.js +102 -21
- 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 +17 -1
- package/dist/Helpers/NexusDriveAdapter.js +32 -3
- package/dist/Helpers/NumberHelpers.d.ts +61 -0
- package/dist/Helpers/NumberHelpers.js +124 -8
- package/dist/Helpers/PagedPicker.d.ts +42 -0
- package/dist/Helpers/PagedPicker.js +85 -0
- package/dist/Helpers/RowsStore.d.ts +24 -0
- package/dist/Helpers/RowsStore.js +79 -10
- package/dist/Helpers/TableHelpers.d.ts +15 -2
- package/dist/Helpers/TableHelpers.js +28 -1
- package/dist/Helpers/TableStore.d.ts +7 -3
- package/dist/Helpers/TableStore.js +9 -4
- package/dist/Helpers/ToolbarFit.d.ts +23 -0
- package/dist/Helpers/ToolbarFit.js +30 -0
- package/dist/Helpers/TreeStore.d.ts +75 -6
- package/dist/Helpers/TreeStore.js +182 -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 +78 -5
- 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 +21 -0
- package/dist/Interfaces/FileInterfaces.js +2 -0
- package/dist/Interfaces/FormInterfaces.d.ts +78 -11
- package/dist/Interfaces/InputInterfaces.d.ts +12 -5
- package/dist/Interfaces/MailInterfaces.d.ts +192 -0
- package/dist/Interfaces/MailInterfaces.js +1 -0
- package/dist/Interfaces/TableInterfaces.d.ts +28 -2
- package/dist/Services/JoinSources.d.ts +47 -6
- package/dist/Services/JoinSources.js +68 -33
- package/dist/Services/MessageApi.d.ts +18 -0
- package/dist/Services/MessageApi.js +64 -0
- package/dist/Shared.Index.d.ts +12 -0
- package/dist/Shared.Index.js +12 -0
- package/package.json +7 -1
- package/src/Styles/Nexus.Button.css +29 -0
- package/src/Styles/Nexus.Chat.css +114 -22
- package/src/Styles/Nexus.Crud.css +119 -28
- package/src/Styles/Nexus.Dashboard.css +763 -0
- package/src/Styles/Nexus.Domain.css +384 -0
- package/src/Styles/Nexus.Editor.css +20 -0
- package/src/Styles/Nexus.Files.css +27 -0
- package/src/Styles/Nexus.Index.css +3 -0
- package/src/Styles/Nexus.Input.css +31 -0
- package/src/Styles/Nexus.Mail.css +1062 -0
- package/src/Styles/Nexus.Popup.css +58 -0
- package/src/Styles/Nexus.Table.css +1458 -1449
- package/src/Styles/Nexus.Theme.Picker.css +1 -0
- package/src/Styles/Nexus.Tree.css +10 -0
|
@@ -77,6 +77,8 @@ export interface ChatRun {
|
|
|
77
77
|
mine: boolean;
|
|
78
78
|
/** Written by the system: a centered note, with no avatar and no side. */
|
|
79
79
|
system: boolean;
|
|
80
|
+
/** Notifications (`kind: "notification"`): event lines, with the author's name and no side. */
|
|
81
|
+
notification: boolean;
|
|
80
82
|
/** The "New messages" line goes before this run. */
|
|
81
83
|
startsUnread: boolean;
|
|
82
84
|
messages: readonly ChatMessage[];
|
|
@@ -225,10 +225,12 @@ export function groupChatMessages(messages, options) {
|
|
|
225
225
|
}
|
|
226
226
|
const at = Date.parse(message.createdAt);
|
|
227
227
|
const system = message.isSystem === true;
|
|
228
|
+
const notification = !system && message.kind === "notification";
|
|
228
229
|
const unread = unreadFrom !== null && sameChatId(message.id, unreadFrom);
|
|
229
230
|
const joins = run !== null &&
|
|
230
231
|
!unread &&
|
|
231
232
|
run.system === system &&
|
|
233
|
+
run.notification === notification &&
|
|
232
234
|
sameChatId(run.authorId, message.authorId) &&
|
|
233
235
|
Number.isFinite(at) &&
|
|
234
236
|
Number.isFinite(run.last) &&
|
|
@@ -242,8 +244,9 @@ export function groupChatMessages(messages, options) {
|
|
|
242
244
|
authorId: message.authorId,
|
|
243
245
|
authorName: message.authorName?.trim() || (system ? "" : CHAT_UNKNOWN_AUTHOR),
|
|
244
246
|
authorAvatar: message.authorAvatar ?? null,
|
|
245
|
-
mine: !system && sameChatId(message.authorId, userId),
|
|
247
|
+
mine: !system && !notification && sameChatId(message.authorId, userId),
|
|
246
248
|
system,
|
|
249
|
+
notification,
|
|
247
250
|
startsUnread: unread,
|
|
248
251
|
messages: [message],
|
|
249
252
|
last: Number.isFinite(at) ? at : 0,
|
|
@@ -6,14 +6,16 @@ import { type PriceLadderOptions } from "./NumberHelpers.ts";
|
|
|
6
6
|
/** The longest description a record keeps: the owner's rule for every model (the column holds 4000). */
|
|
7
7
|
export declare const CRUD_DESCRIPTION_MAX = 1000;
|
|
8
8
|
/**
|
|
9
|
-
* The fields a record keeps its price's figures in (`crudInputs.netAmount`, `
|
|
10
|
-
* `discountPercentage` and `vatPercentage`,
|
|
9
|
+
* The fields a record keeps its price's figures in (`crudInputs.netAmount`, `crudInputs.lineAmount`,
|
|
10
|
+
* `crudColumns.amounts`). Defaults: `price`, `discountPercentage` and `vatPercentage`, no quantity (one of it), and no
|
|
11
|
+
* `other`: an amount added whole after VAT, such as a line's carriage (`otherCostAmount`), rounded to 2 places.
|
|
11
12
|
*/
|
|
12
13
|
export type PriceKeys = {
|
|
13
14
|
price?: string;
|
|
14
15
|
quantity?: string;
|
|
15
16
|
discount?: string;
|
|
16
17
|
vat?: string;
|
|
18
|
+
other?: string;
|
|
17
19
|
};
|
|
18
20
|
export declare const crudColumns: {
|
|
19
21
|
/** The record's status in words ("Active", "Flagged", "Trashed"), from the backend's own numbers; blank for a record that says none. */
|
|
@@ -74,6 +76,21 @@ export type DbJoinColumn<TRow extends TableRow> = Partial<TableColumn<TRow>> & P
|
|
|
74
76
|
};
|
|
75
77
|
/** What `crudColumns.clientJoin` takes: a column, the row's field that holds the other record's key, and its own field. */
|
|
76
78
|
export type ClientJoinColumn<TRow extends TableRow> = Partial<TableColumn<TRow>> & Pick<TableColumn<TRow>, "key" | "label"> & Pick<TableJoin, "from" | "field">;
|
|
79
|
+
/** How `crudInputs.netAmount` works: whether it can be typed in, the price's places, and whether discount and VAT count. */
|
|
80
|
+
export interface NetAmountOptions {
|
|
81
|
+
/** The figure can be typed in, setting the price that comes to it (`priceFromNet`). Default `true`. */
|
|
82
|
+
editable?: boolean;
|
|
83
|
+
/** The places the price input keeps (its `decimals`), so the price set is one it can hold. Default 2. */
|
|
84
|
+
priceDecimals?: number;
|
|
85
|
+
/**
|
|
86
|
+
* Whether the discount and VAT count, asked with the form's (or the rows') `computeWith` and the values: a receipt's
|
|
87
|
+
* lines are discounted and taxed only for a purchase, not a donation. `false` counts both as 0, in the figure and when
|
|
88
|
+
* it is typed in. Default: they always count.
|
|
89
|
+
*
|
|
90
|
+
* @example crudInputs.lineAmount({}, {}, { taxed: (receipt: { purchase: boolean } | undefined) => receipt?.purchase !== false })
|
|
91
|
+
*/
|
|
92
|
+
taxed?: (withValue: any, values: Readonly<Record<string, unknown>>) => boolean;
|
|
93
|
+
}
|
|
77
94
|
/** The controller a client join names its records from: any with a `findListing` (`MODULES.controller(...)`). */
|
|
78
95
|
export interface JoinController {
|
|
79
96
|
findListing: ApiEndpoint;
|
|
@@ -87,12 +104,33 @@ export declare const crudInputs: {
|
|
|
87
104
|
description(changes?: Partial<InputParams>): InputParams;
|
|
88
105
|
/**
|
|
89
106
|
* 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
|
|
107
|
+
* (`compute`), by the server's rule (`priceAmounts`) - never sent and never a change of its own. Blank while the price
|
|
91
108
|
* 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).
|
|
93
|
-
*
|
|
109
|
+
* pairs on a narrow form).
|
|
110
|
+
*
|
|
111
|
+
* **It can be typed in** (2026-06-20's, the owner's 2026-11-10 ask): a person who knows the final figure (2,500.00) and
|
|
112
|
+
* the discount and VAT types it, and on leaving the box the price is set to the one that comes to it exactly by the
|
|
113
|
+
* server's rule (`priceFromNet`: 2,458.21 at 10 % and 13 %), or, when no price does, to the nearest, and the line under
|
|
114
|
+
* the box says so. The price is set as if the person typed it, so its rules, the unsaved-changes state and Save see it.
|
|
115
|
+
* `options.editable: false` keeps it read-only; `options.priceDecimals` is the price input's places (default 2).
|
|
116
|
+
*
|
|
117
|
+
* `changes` may replace any key, `compute` included: a counter's box shows the server's preview instead
|
|
118
|
+
* (`compute: { from: [], value: (_values, line) => line?.netAmount }` with the form's `computeWith`), which is read-only,
|
|
119
|
+
* since that `compute` has no `solve`.
|
|
94
120
|
*
|
|
95
121
|
* @example INPUTS = [..., price, discountPercentage, vatPercentage, crudInputs.netAmount()]
|
|
122
|
+
* @example crudInputs.netAmount({ price: "unitPrice", quantity: "quantity" }, { span: 4 }, { priceDecimals: 4 })
|
|
123
|
+
*/
|
|
124
|
+
netAmount(keys?: PriceKeys, changes?: Partial<InputParams>, options?: NetAmountOptions): InputParams;
|
|
125
|
+
/**
|
|
126
|
+
* The **Calculated amount** of a line in a table of lines (a receipt's, an invoice's): `netAmount` on the line's own
|
|
127
|
+
* figures - the rate (`unitPrice`) times the `quantity`, less the discount, plus VAT, plus any `other` amount - worked
|
|
128
|
+
* out on each row as it is typed, sized as a table column (`width`, no growing). **It can be typed in**: a line's total
|
|
129
|
+
* typed in sets that line's rate to the one that comes to it exactly by the server's rule (`priceFromNet`, per row), or
|
|
130
|
+
* the nearest, and the line under the table says which. The rows hold no value under its name, so they never send it.
|
|
131
|
+
* `keys`, `changes` and `options` as `netAmount`'s.
|
|
132
|
+
*
|
|
133
|
+
* @example inputs: [item, quantity, unitPrice, discountPercentage, vatPercentage, otherCostAmount, crudInputs.lineAmount({ other: "otherCostAmount" })]
|
|
96
134
|
*/
|
|
97
|
-
|
|
135
|
+
lineAmount(keys?: PriceKeys, changes?: Partial<InputParams>, options?: NetAmountOptions): InputParams;
|
|
98
136
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EInputTypes } from "../Interfaces/FormInterfaces.js";
|
|
2
2
|
import { getCrudBackend } from "./CrudBackend.js";
|
|
3
|
-
import { priceAmounts, priceLadder } from "./NumberHelpers.js";
|
|
3
|
+
import { priceAmounts, priceFromNet, priceLadder, roundDecimal } from "./NumberHelpers.js";
|
|
4
4
|
import { readPath } from "./TableHelpers.js";
|
|
5
5
|
/*
|
|
6
6
|
* The columns and inputs every Nexus record has, so a page names them instead of writing them again: its status in
|
|
@@ -16,7 +16,7 @@ export const CRUD_DESCRIPTION_MAX = 1000;
|
|
|
16
16
|
const NET_AMOUNT_LABEL = "Calculated amount";
|
|
17
17
|
/** The price's fields, with the defaults. */
|
|
18
18
|
function priceKeysOf(keys = {}) {
|
|
19
|
-
return { price: keys.price ?? "price", quantity: keys.quantity, discount: keys.discount ?? "discountPercentage", vat: keys.vat ?? "vatPercentage" };
|
|
19
|
+
return { price: keys.price ?? "price", quantity: keys.quantity, discount: keys.discount ?? "discountPercentage", vat: keys.vat ?? "vatPercentage", other: keys.other };
|
|
20
20
|
}
|
|
21
21
|
/** A figure among a record's (or a form's) values: a number, or the text of one; anything else is missing. */
|
|
22
22
|
function figureAt(values, key) {
|
|
@@ -31,17 +31,23 @@ function figureAt(values, key) {
|
|
|
31
31
|
}
|
|
32
32
|
return null;
|
|
33
33
|
}
|
|
34
|
+
/** The amount a record adds whole after VAT (`PriceKeys.other`), at 2 places; 0 when it has none. */
|
|
35
|
+
function otherAt(values, keys) {
|
|
36
|
+
const other = figureAt(values, keys.other);
|
|
37
|
+
return other === null ? 0 : roundDecimal(other, 2);
|
|
38
|
+
}
|
|
34
39
|
/** 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) {
|
|
40
|
+
function netAmountAt(values, keys, taxed = true) {
|
|
36
41
|
const price = readPath(values, keys.price);
|
|
37
42
|
if (price === null || price === undefined || price === "")
|
|
38
43
|
return null;
|
|
39
|
-
|
|
44
|
+
const net = priceAmounts({
|
|
40
45
|
price: figureAt(values, keys.price),
|
|
41
46
|
quantity: keys.quantity === undefined ? 1 : figureAt(values, keys.quantity),
|
|
42
|
-
discountPercentage: figureAt(values, keys.discount),
|
|
43
|
-
vatPercentage: figureAt(values, keys.vat),
|
|
47
|
+
discountPercentage: taxed ? figureAt(values, keys.discount) : 0,
|
|
48
|
+
vatPercentage: taxed ? figureAt(values, keys.vat) : 0,
|
|
44
49
|
}).netAmount;
|
|
50
|
+
return keys.other === undefined ? net : roundDecimal(net + otherAt(values, keys), 2);
|
|
45
51
|
}
|
|
46
52
|
export const crudColumns = {
|
|
47
53
|
/** The record's status in words ("Active", "Flagged", "Trashed"), from the backend's own numbers; blank for a record that says none. */
|
|
@@ -136,25 +142,69 @@ export const crudInputs = {
|
|
|
136
142
|
},
|
|
137
143
|
/**
|
|
138
144
|
* 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
|
|
145
|
+
* (`compute`), by the server's rule (`priceAmounts`) - never sent and never a change of its own. Blank while the price
|
|
140
146
|
* 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).
|
|
142
|
-
*
|
|
147
|
+
* pairs on a narrow form).
|
|
148
|
+
*
|
|
149
|
+
* **It can be typed in** (2026-06-20's, the owner's 2026-11-10 ask): a person who knows the final figure (2,500.00) and
|
|
150
|
+
* the discount and VAT types it, and on leaving the box the price is set to the one that comes to it exactly by the
|
|
151
|
+
* server's rule (`priceFromNet`: 2,458.21 at 10 % and 13 %), or, when no price does, to the nearest, and the line under
|
|
152
|
+
* the box says so. The price is set as if the person typed it, so its rules, the unsaved-changes state and Save see it.
|
|
153
|
+
* `options.editable: false` keeps it read-only; `options.priceDecimals` is the price input's places (default 2).
|
|
154
|
+
*
|
|
155
|
+
* `changes` may replace any key, `compute` included: a counter's box shows the server's preview instead
|
|
156
|
+
* (`compute: { from: [], value: (_values, line) => line?.netAmount }` with the form's `computeWith`), which is read-only,
|
|
157
|
+
* since that `compute` has no `solve`.
|
|
143
158
|
*
|
|
144
159
|
* @example INPUTS = [..., price, discountPercentage, vatPercentage, crudInputs.netAmount()]
|
|
160
|
+
* @example crudInputs.netAmount({ price: "unitPrice", quantity: "quantity" }, { span: 4 }, { priceDecimals: 4 })
|
|
145
161
|
*/
|
|
146
|
-
netAmount(keys, changes = {}) {
|
|
162
|
+
netAmount(keys, changes = {}, options = {}) {
|
|
147
163
|
const k = priceKeysOf(keys);
|
|
148
|
-
const
|
|
164
|
+
const { editable = true, priceDecimals = 2, taxed } = options;
|
|
165
|
+
const from = [k.price, k.quantity, k.discount, k.vat, k.other].filter((key) => key !== undefined);
|
|
166
|
+
const counts = (values, withValue) => (taxed ? taxed(withValue, values) : true);
|
|
167
|
+
const value = (values, withValue) => netAmountAt(values, k, counts(values, withValue));
|
|
168
|
+
const solve = (typed, values, withValue) => {
|
|
169
|
+
const both = counts(values, withValue);
|
|
170
|
+
// An amount added whole after VAT is no part of the price: the price comes to what is left of the figure.
|
|
171
|
+
const other = otherAt(values, k);
|
|
172
|
+
if (typed < other)
|
|
173
|
+
return "The amount cannot be less than the other costs.";
|
|
174
|
+
const found = priceFromNet(other ? roundDecimal(typed - other, 2) : typed, {
|
|
175
|
+
quantity: k.quantity === undefined ? 1 : figureAt(values, k.quantity),
|
|
176
|
+
discountPercentage: both ? figureAt(values, k.discount) : 0,
|
|
177
|
+
vatPercentage: both ? figureAt(values, k.vat) : 0,
|
|
178
|
+
decimals: priceDecimals,
|
|
179
|
+
});
|
|
180
|
+
return found.ok ? { values: { [k.price]: found.price } } : found.message;
|
|
181
|
+
};
|
|
149
182
|
return {
|
|
150
183
|
type: EInputTypes.Number,
|
|
151
184
|
name: "netAmount",
|
|
152
185
|
label: NET_AMOUNT_LABEL,
|
|
153
186
|
decimals: 2,
|
|
154
187
|
span: 3,
|
|
155
|
-
hint: "Price less the discount, plus VAT on the rest.",
|
|
156
|
-
compute: { from, value:
|
|
188
|
+
hint: editable ? "Price less the discount, plus VAT. Type a total to set the price." : "Price less the discount, plus VAT on the rest.",
|
|
189
|
+
compute: editable ? { from, value, solve } : { from, value },
|
|
157
190
|
...changes,
|
|
158
191
|
};
|
|
159
192
|
},
|
|
193
|
+
/**
|
|
194
|
+
* The **Calculated amount** of a line in a table of lines (a receipt's, an invoice's): `netAmount` on the line's own
|
|
195
|
+
* figures - the rate (`unitPrice`) times the `quantity`, less the discount, plus VAT, plus any `other` amount - worked
|
|
196
|
+
* out on each row as it is typed, sized as a table column (`width`, no growing). **It can be typed in**: a line's total
|
|
197
|
+
* typed in sets that line's rate to the one that comes to it exactly by the server's rule (`priceFromNet`, per row), or
|
|
198
|
+
* the nearest, and the line under the table says which. The rows hold no value under its name, so they never send it.
|
|
199
|
+
* `keys`, `changes` and `options` as `netAmount`'s.
|
|
200
|
+
*
|
|
201
|
+
* @example inputs: [item, quantity, unitPrice, discountPercentage, vatPercentage, otherCostAmount, crudInputs.lineAmount({ other: "otherCostAmount" })]
|
|
202
|
+
*/
|
|
203
|
+
lineAmount(keys = {}, changes = {}, options = {}) {
|
|
204
|
+
const k = { price: "unitPrice", quantity: "quantity", ...keys };
|
|
205
|
+
const editable = options.editable ?? true;
|
|
206
|
+
const plus = k.other === undefined ? "plus VAT" : "plus VAT and other costs";
|
|
207
|
+
const hint = editable ? `Rate times quantity, less the discount, ${plus}. Type a total to set the rate.` : `Rate times quantity, less the discount, ${plus}.`;
|
|
208
|
+
return crudInputs.netAmount(k, { width: "9rem", grow: 0, hint, ...changes }, options);
|
|
209
|
+
},
|
|
160
210
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ApiEndpoint } from "../Interfaces/ApiInterfaces.ts";
|
|
2
2
|
import type { CrudActionName, CrudChange, CrudEndpoints, CrudMark, CrudMassActionName, CrudMode, CrudNarrow, CrudRowActionName, CrudTreePlacement } from "../Interfaces/CrudInterfaces.ts";
|
|
3
|
-
import type { FormValues } from "../Interfaces/FormInterfaces.ts";
|
|
3
|
+
import type { FormValues, InputParams } from "../Interfaces/FormInterfaces.ts";
|
|
4
4
|
import type { CrudBackend } from "./CrudBackend.ts";
|
|
5
5
|
import type { TreeItemChange } from "./TreeStore.ts";
|
|
6
6
|
import type { MessageActionName } from "../Interfaces/MessageInterfaces.ts";
|
|
@@ -90,6 +90,41 @@ export declare function crudJoinedNames<TRow extends TableRow>(record: TRow, bef
|
|
|
90
90
|
* be told.
|
|
91
91
|
*/
|
|
92
92
|
export declare function crudJoinsMissing<TRow extends TableRow>(record: TRow, before: TRow | undefined, columns: readonly TableColumn<TRow>[]): boolean;
|
|
93
|
+
/** What a CRUD form's save sent, which the record it leaves is built from (`crudSavedRecord`). */
|
|
94
|
+
export interface CrudSent {
|
|
95
|
+
/** The form's values as they were sent: what its `transform` was given. */
|
|
96
|
+
values: FormValues;
|
|
97
|
+
/** What was sent: the `transform`'s answer, or the values themselves. */
|
|
98
|
+
body: unknown;
|
|
99
|
+
/** The inputs `showWhen` hid when the form was saved (`crudHiddenOnSave`). */
|
|
100
|
+
hidden: readonly string[];
|
|
101
|
+
}
|
|
102
|
+
/** Two answers with the same fields and the same values in each (`Object.is`), or the same value; lists compared item by item. */
|
|
103
|
+
export declare function sameShallow(a: unknown, b: unknown): boolean;
|
|
104
|
+
/**
|
|
105
|
+
* A Crud form's `computeWith` (`CrudFormOptions.computeWith`) for the values as they are now: `previous` itself when the
|
|
106
|
+
* rule answers the same fields and values (so the form redraws nothing), the new answer otherwise. A rule that throws
|
|
107
|
+
* keeps `previous` and says why in the console. No rule: `undefined`.
|
|
108
|
+
*/
|
|
109
|
+
export declare function crudComputeWith<TRow extends FormValues>(rule: ((values: TRow, context: {
|
|
110
|
+
action: "create" | "edit";
|
|
111
|
+
row: TRow | null;
|
|
112
|
+
}) => unknown) | undefined, values: TRow, context: {
|
|
113
|
+
action: "create" | "edit";
|
|
114
|
+
row: TRow | null;
|
|
115
|
+
}, previous: unknown): unknown;
|
|
116
|
+
/** The inputs `showWhen` hides for these values: each one that holds a value of the form's (not a picture, nor one worked out from others). */
|
|
117
|
+
export declare function crudHiddenOnSave(inputs: readonly InputParams[], values: FormValues): string[];
|
|
118
|
+
/**
|
|
119
|
+
* The record a save leaves, as the server now holds it, with no call: the row the form was opened on (none for a new
|
|
120
|
+
* record), then what was **sent** - the body `transform` made, when it is a record, over the form's values, so a field it
|
|
121
|
+
* sends as null is empty on the row and one it reshapes shows as sent - then the answer. An input `showWhen` hid when the
|
|
122
|
+
* form was saved is one the form says does not apply, so the row holds it only as the server does: as the answer gives
|
|
123
|
+
* it, when the answer is a record (a field it leaves out has none, as a Nexus answer leaves out its nulls), else as the
|
|
124
|
+
* body gives it (a field the body leaves out has none). Never the form's own copy: that copy kept a hidden value on the
|
|
125
|
+
* row after a save, though the server held none, until the list was read again.
|
|
126
|
+
*/
|
|
127
|
+
export declare function crudSavedRecord<TRow extends FormValues>(row: TRow | null | undefined, sent: CrudSent, answered: Partial<TRow> | null): TRow;
|
|
93
128
|
/**
|
|
94
129
|
* 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
130
|
* names and values, in name order (`"@countryId=7&parentId="`), or "" when nothing is fixed. So a form opened inside one
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { inputShown, isComputedInput } from "./InputConditions.js";
|
|
2
|
+
import { holdsValue } from "./InputParamsHelpers.js";
|
|
1
3
|
import { readPath } from "./TableHelpers.js";
|
|
2
4
|
/*
|
|
3
5
|
* The rules of the CRUD component, with no React: which lists and actions a controller offers, which actions each list
|
|
@@ -283,6 +285,65 @@ export function crudJoinsMissing(record, before, columns) {
|
|
|
283
285
|
}
|
|
284
286
|
return false;
|
|
285
287
|
}
|
|
288
|
+
/** Two answers with the same fields and the same values in each (`Object.is`), or the same value; lists compared item by item. */
|
|
289
|
+
export function sameShallow(a, b) {
|
|
290
|
+
if (Object.is(a, b))
|
|
291
|
+
return true;
|
|
292
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
|
|
293
|
+
return false;
|
|
294
|
+
if (Array.isArray(a) !== Array.isArray(b))
|
|
295
|
+
return false;
|
|
296
|
+
const aKeys = Object.keys(a);
|
|
297
|
+
const bKeys = Object.keys(b);
|
|
298
|
+
if (aKeys.length !== bKeys.length)
|
|
299
|
+
return false;
|
|
300
|
+
return aKeys.every(key => Object.prototype.hasOwnProperty.call(b, key) && Object.is(a[key], b[key]));
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* A Crud form's `computeWith` (`CrudFormOptions.computeWith`) for the values as they are now: `previous` itself when the
|
|
304
|
+
* rule answers the same fields and values (so the form redraws nothing), the new answer otherwise. A rule that throws
|
|
305
|
+
* keeps `previous` and says why in the console. No rule: `undefined`.
|
|
306
|
+
*/
|
|
307
|
+
export function crudComputeWith(rule, values, context, previous) {
|
|
308
|
+
if (!rule)
|
|
309
|
+
return undefined;
|
|
310
|
+
try {
|
|
311
|
+
const next = rule(values, context);
|
|
312
|
+
return sameShallow(previous, next) ? previous : next;
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
console.error("A Crud form's computeWith failed; its last answer is kept.", error);
|
|
316
|
+
return previous;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/** A record of fields, as a JSON body is: not a list, a `FormData` or anything else with a prototype of its own. */
|
|
320
|
+
const isPlainRecord = (value) => {
|
|
321
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
322
|
+
return false;
|
|
323
|
+
const prototype = Object.getPrototypeOf(value);
|
|
324
|
+
return prototype === Object.prototype || prototype === null;
|
|
325
|
+
};
|
|
326
|
+
/** The inputs `showWhen` hides for these values: each one that holds a value of the form's (not a picture, nor one worked out from others). */
|
|
327
|
+
export function crudHiddenOnSave(inputs, values) {
|
|
328
|
+
return inputs.filter(params => params.showWhen && holdsValue(params) && !isComputedInput(params) && !inputShown(params.showWhen, name => values[name])).map(params => params.name);
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* The record a save leaves, as the server now holds it, with no call: the row the form was opened on (none for a new
|
|
332
|
+
* record), then what was **sent** - the body `transform` made, when it is a record, over the form's values, so a field it
|
|
333
|
+
* sends as null is empty on the row and one it reshapes shows as sent - then the answer. An input `showWhen` hid when the
|
|
334
|
+
* form was saved is one the form says does not apply, so the row holds it only as the server does: as the answer gives
|
|
335
|
+
* it, when the answer is a record (a field it leaves out has none, as a Nexus answer leaves out its nulls), else as the
|
|
336
|
+
* body gives it (a field the body leaves out has none). Never the form's own copy: that copy kept a hidden value on the
|
|
337
|
+
* row after a save, though the server held none, until the list was read again.
|
|
338
|
+
*/
|
|
339
|
+
export function crudSavedRecord(row, sent, answered) {
|
|
340
|
+
const body = isPlainRecord(sent.body) ? sent.body : null;
|
|
341
|
+
const record = { ...row, ...sent.values, ...body, ...answered };
|
|
342
|
+
const holder = answered ?? body ?? sent.values;
|
|
343
|
+
for (const name of sent.hidden)
|
|
344
|
+
record[name] = holder[name] ?? null;
|
|
345
|
+
return record;
|
|
346
|
+
}
|
|
286
347
|
/**
|
|
287
348
|
* Where an Add or Edit form with fixed fields stands, as text for its popup's key and its draft's: the fixed fields'
|
|
288
349
|
* names and values, in name order (`"@countryId=7&parentId="`), or "" when nothing is fixed. So a form opened inside one
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ApiEndpoint } from "../Interfaces/ApiInterfaces.ts";
|
|
2
|
+
import type { DropdownNarrow } from "../Interfaces/InputInterfaces.ts";
|
|
3
|
+
import type { TableJoinSource, TableJoinedRecord } from "../Interfaces/TableInterfaces.ts";
|
|
4
|
+
import type { CrudBackend } from "./CrudBackend.ts";
|
|
5
|
+
/** The controller's doors a picker may use: its picker's list and its lookup by keys, either of which it may lack. */
|
|
6
|
+
export interface CrudPickerDoors {
|
|
7
|
+
activeListing?: ApiEndpoint;
|
|
8
|
+
findListing?: ApiEndpoint;
|
|
9
|
+
}
|
|
10
|
+
/** The options that decide the plan (`CrudPickerOptions`). */
|
|
11
|
+
export interface CrudPickerPlanOptions {
|
|
12
|
+
list?: ApiEndpoint;
|
|
13
|
+
valueKey?: string;
|
|
14
|
+
find?: ApiEndpoint | TableJoinSource;
|
|
15
|
+
usable?: (record: TableJoinedRecord) => boolean;
|
|
16
|
+
}
|
|
17
|
+
/** Where a picker's options come from and which of them it offers. */
|
|
18
|
+
export interface CrudPickerPlan {
|
|
19
|
+
/** The listing read once and kept for the page: `list`, else the controller's `activeListing`. */
|
|
20
|
+
list: ApiEndpoint;
|
|
21
|
+
/**
|
|
22
|
+
* Where a value the list does not offer is looked up, and the field its records are filed by: the picker's `find`, else
|
|
23
|
+
* the controller's `findListing` while the values are the records' keys, else nowhere (`null`).
|
|
24
|
+
*/
|
|
25
|
+
lookup: {
|
|
26
|
+
source: ApiEndpoint | TableJoinSource;
|
|
27
|
+
key: string;
|
|
28
|
+
} | null;
|
|
29
|
+
/** Whether a record is offered, and shown untagged when a form holds it: `usable`, else the backend's `isPickable`. */
|
|
30
|
+
usable: (record: TableJoinedRecord) => boolean;
|
|
31
|
+
}
|
|
32
|
+
/** The names a listing's path takes (`StockBalance/Units/{storeId}/{itemId}`: storeId and itemId). */
|
|
33
|
+
export declare function pathValueNames(list: Pick<ApiEndpoint, "path">): string[];
|
|
34
|
+
/** How a picker asks for its options narrowed by several values at once (`crudPickerNarrowAsk`). */
|
|
35
|
+
export interface CrudPickerNarrowAsk {
|
|
36
|
+
/** The values that go into the listing's path. */
|
|
37
|
+
params: Record<string, string>;
|
|
38
|
+
/** The values that go as query parameters. */
|
|
39
|
+
query: Record<string, string>;
|
|
40
|
+
/** The narrowings the browser applies to what the server answers, by the records' own fields. */
|
|
41
|
+
filters: DropdownNarrow[];
|
|
42
|
+
/** Where the answer is kept: the listing's name with the values the server was asked for. */
|
|
43
|
+
name: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* How a picker asks for its options narrowed by these values: each in the listing's path when the path names it
|
|
47
|
+
* (`{storeId}`), as a query parameter when `narrowQuery` names it, else filtered in the browser by the records' own field
|
|
48
|
+
* (`orUnset` there too). `null` when a value the path needs is empty: there is nothing to ask for.
|
|
49
|
+
*/
|
|
50
|
+
export declare function crudPickerNarrowAsk(list: Pick<ApiEndpoint, "path">, listName: string, narrows: readonly DropdownNarrow[], narrowQuery?: string | readonly string[]): CrudPickerNarrowAsk | null;
|
|
51
|
+
/** Whether a record fits a narrowing the browser applies: its field holds the value, or is empty with `orUnset`. */
|
|
52
|
+
export declare function fitsNarrow(record: Readonly<Record<string, unknown>>, { key, value, orUnset }: DropdownNarrow): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* The plan for a picker over this controller. A controller with no `activeListing` needs a `list`; one with no
|
|
55
|
+
* `findListing` looks nothing up (a value its list does not offer is named from what the form holds, or not found),
|
|
56
|
+
* unless the picker has its own `find`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function crudPickerPlan(controller: CrudPickerDoors, { list, valueKey, find, usable }: CrudPickerPlanOptions, backend: CrudBackend): CrudPickerPlan;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** The names a listing's path takes (`StockBalance/Units/{storeId}/{itemId}`: storeId and itemId). */
|
|
2
|
+
export function pathValueNames(list) {
|
|
3
|
+
return [...list.path.matchAll(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g)].map(match => match[1]);
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* How a picker asks for its options narrowed by these values: each in the listing's path when the path names it
|
|
7
|
+
* (`{storeId}`), as a query parameter when `narrowQuery` names it, else filtered in the browser by the records' own field
|
|
8
|
+
* (`orUnset` there too). `null` when a value the path needs is empty: there is nothing to ask for.
|
|
9
|
+
*/
|
|
10
|
+
export function crudPickerNarrowAsk(list, listName, narrows, narrowQuery) {
|
|
11
|
+
const inPath = new Set(pathValueNames(list));
|
|
12
|
+
const asQuery = new Set(typeof narrowQuery === "string" ? [narrowQuery] : (narrowQuery ?? []));
|
|
13
|
+
const params = {};
|
|
14
|
+
const query = {};
|
|
15
|
+
const filters = [];
|
|
16
|
+
for (const narrow of narrows) {
|
|
17
|
+
if (inPath.has(narrow.key)) {
|
|
18
|
+
if (narrow.value === null)
|
|
19
|
+
return null;
|
|
20
|
+
params[narrow.key] = String(narrow.value);
|
|
21
|
+
}
|
|
22
|
+
else if (asQuery.has(narrow.key) && narrow.value !== null && !narrow.orUnset)
|
|
23
|
+
query[narrow.key] = String(narrow.value);
|
|
24
|
+
else
|
|
25
|
+
filters.push(narrow);
|
|
26
|
+
}
|
|
27
|
+
const asked = [...Object.entries(params), ...Object.entries(query)].sort(([a], [b]) => a.localeCompare(b));
|
|
28
|
+
return { params, query, filters, name: asked.length ? `${listName}?${asked.map(([key, value]) => `${key}=${value}`).join("&")}` : listName };
|
|
29
|
+
}
|
|
30
|
+
/** Whether a record fits a narrowing the browser applies: its field holds the value, or is empty with `orUnset`. */
|
|
31
|
+
export function fitsNarrow(record, { key, value, orUnset }) {
|
|
32
|
+
const field = record[key];
|
|
33
|
+
return (value !== null && String(field) === String(value)) || (orUnset === true && (field === null || field === undefined || field === ""));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The plan for a picker over this controller. A controller with no `activeListing` needs a `list`; one with no
|
|
37
|
+
* `findListing` looks nothing up (a value its list does not offer is named from what the form holds, or not found),
|
|
38
|
+
* unless the picker has its own `find`.
|
|
39
|
+
*/
|
|
40
|
+
export function crudPickerPlan(controller, { list, valueKey = "id", find, usable }, backend) {
|
|
41
|
+
const listing = list ?? controller.activeListing;
|
|
42
|
+
if (!listing)
|
|
43
|
+
throw new Error("crudPicker: the controller has no activeListing; name the listing to read with `list`.");
|
|
44
|
+
const lookup = find ? { source: find, key: valueKey } : valueKey === "id" && controller.findListing ? { source: controller.findListing, key: "id" } : null;
|
|
45
|
+
return { list: listing, lookup, usable: usable ?? (record => backend.isPickable(record)) };
|
|
46
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ApiEndpoint } from "../Interfaces/ApiInterfaces.ts";
|
|
2
2
|
import type { CrudMode, CrudRowForm } from "../Interfaces/CrudInterfaces.ts";
|
|
3
|
-
import type { FormValues } from "../Interfaces/FormInterfaces.ts";
|
|
3
|
+
import type { FormConfirmParams, FormValues } from "../Interfaces/FormInterfaces.ts";
|
|
4
|
+
import { type FormConfirmQuestion } from "./FormConfirm.ts";
|
|
4
5
|
/**
|
|
5
6
|
* Whether a row form is offered on a record: on its lists (default the lists that offer Edit), to a person who may call
|
|
6
7
|
* its `submit` and, when it is an endpoint, its `load`, and by the page's own `when`.
|
|
@@ -13,3 +14,19 @@ export declare function crudRowFormOffered<TRow extends FormValues>(form: CrudRo
|
|
|
13
14
|
* while the popup was minimized) stays.
|
|
14
15
|
*/
|
|
15
16
|
export declare function crudRowFormWrites<TRow extends FormValues, TValues extends FormValues>(writes: CrudRowForm<TRow, TValues>["writes"], result: unknown, row: TRow, values: TValues): Partial<TRow> | null;
|
|
17
|
+
/**
|
|
18
|
+
* Whether a row form is its question alone: it gives no inputs. Choosing it asks (its `confirm`, else "{label} {name}?")
|
|
19
|
+
* and sends on Confirm, with no popup form.
|
|
20
|
+
*/
|
|
21
|
+
export declare function crudRowFormAsksOnly<TRow extends FormValues, TValues extends FormValues>(form: CrudRowForm<TRow, TValues>): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* The question a row form asks for this record: its `confirm` as given, or made from the record and what `load` read;
|
|
24
|
+
* for a question alone that gives none, "{label} {name}?" (`name` is the record's, as its row is named). `null`: a form
|
|
25
|
+
* that saves without asking.
|
|
26
|
+
*/
|
|
27
|
+
export declare function crudRowFormConfirm<TRow extends FormValues, TValues extends FormValues>(form: CrudRowForm<TRow, TValues>, row: TRow, loaded: TValues | null, name: string): FormConfirmParams<TValues> | null;
|
|
28
|
+
/**
|
|
29
|
+
* The question settled for these values and this body (`formConfirmQuestion`). A question alone has no form to go back
|
|
30
|
+
* to: its buttons are the action's own word (`submitLabel`, else `label`) and "Cancel", unless the page names them.
|
|
31
|
+
*/
|
|
32
|
+
export declare function crudRowFormQuestion<TRow extends FormValues, TValues extends FormValues>(form: CrudRowForm<TRow, TValues>, confirm: FormConfirmParams<TValues>, values: TValues, body: unknown): FormConfirmQuestion;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { CRUD_ROW_ACTIONS } from "./CrudHelpers.js";
|
|
2
2
|
import { isApiEndpoint } from "./ApiRoutes.js";
|
|
3
|
+
import { formConfirmQuestion } from "./FormConfirm.js";
|
|
3
4
|
/*
|
|
4
5
|
* The pure half of a row action that opens a form of its own for the record (`Crud` `rowForms`,
|
|
5
|
-
* Components/Forms/CrudRowForms.tsx): where it is offered, and what of a save's answer is
|
|
6
|
-
* Pure, so Node tests run it.
|
|
6
|
+
* Components/Forms/CrudRowForms.tsx): where it is offered, what it asks before saving, and what of a save's answer is
|
|
7
|
+
* written back into the record. Pure, so Node tests run it.
|
|
7
8
|
*/
|
|
8
9
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
10
|
/**
|
|
@@ -41,3 +42,35 @@ export function crudRowFormWrites(writes, result, row, values) {
|
|
|
41
42
|
patch[field] = result[field];
|
|
42
43
|
return Object.keys(patch).length ? patch : null;
|
|
43
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Whether a row form is its question alone: it gives no inputs. Choosing it asks (its `confirm`, else "{label} {name}?")
|
|
47
|
+
* and sends on Confirm, with no popup form.
|
|
48
|
+
*/
|
|
49
|
+
export function crudRowFormAsksOnly(form) {
|
|
50
|
+
return form.inputs === undefined;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The question a row form asks for this record: its `confirm` as given, or made from the record and what `load` read;
|
|
54
|
+
* for a question alone that gives none, "{label} {name}?" (`name` is the record's, as its row is named). `null`: a form
|
|
55
|
+
* that saves without asking.
|
|
56
|
+
*/
|
|
57
|
+
export function crudRowFormConfirm(form, row, loaded, name) {
|
|
58
|
+
const given = typeof form.confirm === "function" ? form.confirm(row, loaded) : form.confirm;
|
|
59
|
+
if (given)
|
|
60
|
+
return given;
|
|
61
|
+
return crudRowFormAsksOnly(form) ? { title: `${form.label} ${name}?` } : null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The question settled for these values and this body (`formConfirmQuestion`). A question alone has no form to go back
|
|
65
|
+
* to: its buttons are the action's own word (`submitLabel`, else `label`) and "Cancel", unless the page names them.
|
|
66
|
+
*/
|
|
67
|
+
export function crudRowFormQuestion(form, confirm, values, body) {
|
|
68
|
+
const question = formConfirmQuestion(confirm, values, body);
|
|
69
|
+
if (!crudRowFormAsksOnly(form))
|
|
70
|
+
return question;
|
|
71
|
+
return {
|
|
72
|
+
...question,
|
|
73
|
+
confirmLabel: confirm.confirmLabel === undefined ? (form.submitLabel ?? form.label) : question.confirmLabel,
|
|
74
|
+
cancelLabel: confirm.cancelLabel ?? "Cancel",
|
|
75
|
+
};
|
|
76
|
+
}
|