nexus-shared 3.2.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/CHANGELOG.md +2359 -0
  2. package/README.md +1911 -1123
  3. package/dist/Client.Index.d.ts +27 -0
  4. package/dist/Client.Index.js +27 -0
  5. package/dist/Components/Chats/ChatComposer.d.ts +7 -1
  6. package/dist/Components/Chats/ChatComposer.js +9 -6
  7. package/dist/Components/Chats/ChatParts.d.ts +15 -2
  8. package/dist/Components/Chats/ChatParts.js +15 -4
  9. package/dist/Components/Chats/ChatPopup.d.ts +46 -0
  10. package/dist/Components/Chats/ChatPopup.js +34 -0
  11. package/dist/Components/Chats/ChatThread.d.ts +9 -1
  12. package/dist/Components/Chats/ChatThread.js +54 -6
  13. package/dist/Components/Dashboards/DashboardBoard.d.ts +62 -0
  14. package/dist/Components/Dashboards/DashboardBoard.js +594 -0
  15. package/dist/Components/Dashboards/DashboardWidgets.d.ts +155 -0
  16. package/dist/Components/Dashboards/DashboardWidgets.js +159 -0
  17. package/dist/Components/Dashboards/WidgetFrame.d.ts +51 -0
  18. package/dist/Components/Dashboards/WidgetFrame.js +45 -0
  19. package/dist/Components/Documents/Button.d.ts +6 -1
  20. package/dist/Components/Documents/Button.js +2 -2
  21. package/dist/Components/Documents/DocumentFrame.d.ts +50 -0
  22. package/dist/Components/Documents/DocumentFrame.js +83 -0
  23. package/dist/Components/Documents/Menu.d.ts +4 -0
  24. package/dist/Components/Documents/Menu.js +40 -24
  25. package/dist/Components/Documents/PrintDocument.d.ts +95 -0
  26. package/dist/Components/Documents/PrintDocument.js +62 -0
  27. package/dist/Components/Documents/TabButtons.d.ts +6 -0
  28. package/dist/Components/Documents/TabButtons.js +5 -4
  29. package/dist/Components/Documents/Tabs.d.ts +59 -0
  30. package/dist/Components/Documents/Tabs.js +70 -0
  31. package/dist/Components/Domains/DomainCard.d.ts +98 -0
  32. package/dist/Components/Domains/DomainCard.js +182 -0
  33. package/dist/Components/Forms/ApiForm.js +24 -3
  34. package/dist/Components/Forms/Crud.d.ts +2 -2
  35. package/dist/Components/Forms/Crud.js +628 -247
  36. package/dist/Components/Forms/CrudActions.d.ts +137 -0
  37. package/dist/Components/Forms/CrudActions.js +214 -0
  38. package/dist/Components/Forms/CrudHistory.d.ts +6 -1
  39. package/dist/Components/Forms/CrudHistory.js +14 -6
  40. package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
  41. package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
  42. package/dist/Components/Forms/CrudParts.d.ts +37 -2
  43. package/dist/Components/Forms/CrudParts.js +123 -7
  44. package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
  45. package/dist/Components/Forms/CrudRowForms.js +174 -0
  46. package/dist/Components/Forms/CrudScopes.d.ts +28 -0
  47. package/dist/Components/Forms/CrudScopes.js +76 -0
  48. package/dist/Components/Forms/CrudTree.d.ts +90 -0
  49. package/dist/Components/Forms/CrudTree.js +363 -0
  50. package/dist/Components/Forms/CrudWorkspace.d.ts +73 -0
  51. package/dist/Components/Forms/CrudWorkspace.js +177 -0
  52. package/dist/Components/Forms/ExcelImport.js +3 -3
  53. package/dist/Components/Forms/Form.d.ts +6 -5
  54. package/dist/Components/Forms/Form.js +134 -21
  55. package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
  56. package/dist/Components/Forms/FormLeaveGuard.js +65 -0
  57. package/dist/Components/Forms/FormQuestion.d.ts +6 -0
  58. package/dist/Components/Forms/FormQuestion.js +28 -0
  59. package/dist/Components/Forms/RecordFlows.d.ts +50 -0
  60. package/dist/Components/Forms/RecordFlows.js +282 -0
  61. package/dist/Components/Forms/SubmitForm.d.ts +37 -2
  62. package/dist/Components/Forms/SubmitForm.js +182 -10
  63. package/dist/Components/Inputs/CheckBox.js +1 -1
  64. package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
  65. package/dist/Components/Inputs/ChoiceField.js +28 -2
  66. package/dist/Components/Inputs/ComputedInput.d.ts +51 -0
  67. package/dist/Components/Inputs/ComputedInput.js +179 -0
  68. package/dist/Components/Inputs/Dropdown.d.ts +13 -0
  69. package/dist/Components/Inputs/Dropdown.js +8 -6
  70. package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
  71. package/dist/Components/Inputs/DropdownParts.js +16 -4
  72. package/dist/Components/Inputs/Field.d.ts +7 -1
  73. package/dist/Components/Inputs/Field.js +2 -2
  74. package/dist/Components/Inputs/GroupForm.d.ts +6 -6
  75. package/dist/Components/Inputs/GroupForm.js +12 -12
  76. package/dist/Components/Inputs/InputRenderer.d.ts +13 -6
  77. package/dist/Components/Inputs/InputRenderer.js +37 -32
  78. package/dist/Components/Inputs/MarkdownEditor.d.ts +6 -0
  79. package/dist/Components/Inputs/MarkdownEditor.js +32 -6
  80. package/dist/Components/Inputs/PictureInput.d.ts +41 -0
  81. package/dist/Components/Inputs/PictureInput.js +75 -0
  82. package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
  83. package/dist/Components/Inputs/RadioGroup.js +8 -3
  84. package/dist/Components/Inputs/RowsInput.d.ts +15 -2
  85. package/dist/Components/Inputs/RowsInput.js +96 -8
  86. package/dist/Components/Inputs/TabularForm.d.ts +5 -5
  87. package/dist/Components/Inputs/TabularForm.js +45 -32
  88. package/dist/Components/Inputs/TextArea.d.ts +6 -0
  89. package/dist/Components/Inputs/TextArea.js +11 -2
  90. package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
  91. package/dist/Components/Inputs/TextBoxTypes.js +17 -0
  92. package/dist/Components/Inputs/TreeView.d.ts +76 -2
  93. package/dist/Components/Inputs/TreeView.js +227 -16
  94. package/dist/Components/Layouts/MessageBox.d.ts +15 -2
  95. package/dist/Components/Layouts/MessageBox.js +56 -7
  96. package/dist/Components/Layouts/Popup.d.ts +34 -7
  97. package/dist/Components/Layouts/Popup.js +143 -13
  98. package/dist/Components/Layouts/PopupDock.d.ts +3 -1
  99. package/dist/Components/Layouts/PopupDock.js +2 -1
  100. package/dist/Components/Layouts/PopupHost.js +5 -4
  101. package/dist/Components/Messages/MailClient.d.ts +50 -0
  102. package/dist/Components/Messages/MailClient.js +410 -0
  103. package/dist/Components/Messages/MailColumns.d.ts +21 -0
  104. package/dist/Components/Messages/MailColumns.js +92 -0
  105. package/dist/Components/Messages/MailCompose.d.ts +46 -0
  106. package/dist/Components/Messages/MailCompose.js +154 -0
  107. package/dist/Components/Messages/MailReader.d.ts +43 -0
  108. package/dist/Components/Messages/MailReader.js +207 -0
  109. package/dist/Components/Messages/MailThread.d.ts +71 -0
  110. package/dist/Components/Messages/MailThread.js +185 -0
  111. package/dist/Components/Messages/MessageBell.d.ts +41 -0
  112. package/dist/Components/Messages/MessageBell.js +76 -0
  113. package/dist/Components/Viewers/DataTable.js +153 -49
  114. package/dist/Components/Viewers/DataTableColumns.d.ts +2 -2
  115. package/dist/Components/Viewers/DataTableColumns.js +2 -1
  116. package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
  117. package/dist/Components/Viewers/DataTableFilters.js +17 -2
  118. package/dist/Components/Viewers/DataTableParts.d.ts +23 -5
  119. package/dist/Components/Viewers/DataTableParts.js +26 -14
  120. package/dist/Components/Viewers/OwnerFiles.d.ts +227 -0
  121. package/dist/Components/Viewers/OwnerFiles.js +700 -0
  122. package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
  123. package/dist/Components/Viewers/QrCodeImage.js +15 -0
  124. package/dist/Components/Viewers/RecordCards.d.ts +16 -0
  125. package/dist/Components/Viewers/RecordCards.js +509 -0
  126. package/dist/Helpers/ApiModules.js +4 -0
  127. package/dist/Helpers/AuditEntries.d.ts +50 -0
  128. package/dist/Helpers/AuditEntries.js +96 -0
  129. package/dist/Helpers/BusyHooks.d.ts +6 -0
  130. package/dist/Helpers/BusyHooks.js +39 -0
  131. package/dist/Helpers/ChatBackend.d.ts +8 -1
  132. package/dist/Helpers/ChatBackend.js +18 -0
  133. package/dist/Helpers/ChatHelpers.d.ts +2 -0
  134. package/dist/Helpers/ChatHelpers.js +4 -1
  135. package/dist/Helpers/CrudBackend.d.ts +20 -2
  136. package/dist/Helpers/CrudBackend.js +46 -2
  137. package/dist/Helpers/CrudColumns.d.ts +91 -6
  138. package/dist/Helpers/CrudColumns.js +148 -7
  139. package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
  140. package/dist/Helpers/CrudDetailsTabs.js +126 -0
  141. package/dist/Helpers/CrudHelpers.d.ts +116 -2
  142. package/dist/Helpers/CrudHelpers.js +256 -0
  143. package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
  144. package/dist/Helpers/CrudNarrowPick.js +157 -0
  145. package/dist/Helpers/CrudPickerPlan.d.ts +58 -0
  146. package/dist/Helpers/CrudPickerPlan.js +46 -0
  147. package/dist/Helpers/CrudRowForms.d.ts +32 -0
  148. package/dist/Helpers/CrudRowForms.js +76 -0
  149. package/dist/Helpers/CrudScopes.d.ts +32 -0
  150. package/dist/Helpers/CrudScopes.js +79 -0
  151. package/dist/Helpers/DashboardHelpers.d.ts +115 -0
  152. package/dist/Helpers/DashboardHelpers.js +305 -0
  153. package/dist/Helpers/DomainCards.d.ts +140 -0
  154. package/dist/Helpers/DomainCards.js +370 -0
  155. package/dist/Helpers/DropdownHelpers.d.ts +3 -1
  156. package/dist/Helpers/DropdownHelpers.js +6 -2
  157. package/dist/Helpers/ExcelHelpers.d.ts +1 -1
  158. package/dist/Helpers/ExcelHelpers.js +12 -11
  159. package/dist/Helpers/FileAdapter.d.ts +158 -0
  160. package/dist/Helpers/FileAdapter.js +173 -0
  161. package/dist/Helpers/FilePrimaries.d.ts +48 -0
  162. package/dist/Helpers/FilePrimaries.js +138 -0
  163. package/dist/Helpers/FormConfirm.d.ts +32 -0
  164. package/dist/Helpers/FormConfirm.js +40 -0
  165. package/dist/Helpers/FormStore.d.ts +100 -5
  166. package/dist/Helpers/FormStore.js +368 -25
  167. package/dist/Helpers/InputConditions.d.ts +56 -0
  168. package/dist/Helpers/InputConditions.js +192 -0
  169. package/dist/Helpers/InputMemory.d.ts +106 -0
  170. package/dist/Helpers/InputMemory.js +395 -0
  171. package/dist/Helpers/InputNarrowing.d.ts +50 -0
  172. package/dist/Helpers/InputNarrowing.js +148 -0
  173. package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
  174. package/dist/Helpers/InputParamsHelpers.js +157 -74
  175. package/dist/Helpers/MailClientHelpers.d.ts +96 -0
  176. package/dist/Helpers/MailClientHelpers.js +211 -0
  177. package/dist/Helpers/MessageBackend.d.ts +152 -0
  178. package/dist/Helpers/MessageBackend.js +329 -0
  179. package/dist/Helpers/MessageHelpers.d.ts +160 -0
  180. package/dist/Helpers/MessageHelpers.js +573 -0
  181. package/dist/Helpers/MessageUnread.d.ts +95 -0
  182. package/dist/Helpers/MessageUnread.js +245 -0
  183. package/dist/Helpers/NexusDriveAdapter.d.ts +71 -0
  184. package/dist/Helpers/NexusDriveAdapter.js +236 -0
  185. package/dist/Helpers/NumberHelpers.d.ts +156 -0
  186. package/dist/Helpers/NumberHelpers.js +242 -0
  187. package/dist/Helpers/PagedPicker.d.ts +42 -0
  188. package/dist/Helpers/PagedPicker.js +85 -0
  189. package/dist/Helpers/Permissions.js +2 -1
  190. package/dist/Helpers/QrCode.d.ts +26 -0
  191. package/dist/Helpers/QrCode.js +463 -0
  192. package/dist/Helpers/RecordCards.d.ts +205 -0
  193. package/dist/Helpers/RecordCards.js +378 -0
  194. package/dist/Helpers/RequestCache.d.ts +14 -2
  195. package/dist/Helpers/RequestCache.js +22 -3
  196. package/dist/Helpers/RowsStore.d.ts +61 -2
  197. package/dist/Helpers/RowsStore.js +203 -16
  198. package/dist/Helpers/ServerPreview.d.ts +69 -0
  199. package/dist/Helpers/ServerPreview.js +99 -0
  200. package/dist/Helpers/TableColumns.js +4 -3
  201. package/dist/Helpers/TableExport.d.ts +7 -1
  202. package/dist/Helpers/TableExport.js +9 -3
  203. package/dist/Helpers/TableFilters.d.ts +1 -1
  204. package/dist/Helpers/TableFilters.js +24 -23
  205. package/dist/Helpers/TableHelpers.d.ts +38 -4
  206. package/dist/Helpers/TableHelpers.js +78 -10
  207. package/dist/Helpers/TableJoins.d.ts +13 -0
  208. package/dist/Helpers/TableJoins.js +34 -0
  209. package/dist/Helpers/TableStore.d.ts +28 -4
  210. package/dist/Helpers/TableStore.js +73 -11
  211. package/dist/Helpers/ToolbarFit.d.ts +23 -0
  212. package/dist/Helpers/ToolbarFit.js +30 -0
  213. package/dist/Helpers/TreeStore.d.ts +156 -6
  214. package/dist/Helpers/TreeStore.js +547 -18
  215. package/dist/Interfaces/ApiInterfaces.d.ts +10 -1
  216. package/dist/Interfaces/ApiInterfaces.js +10 -1
  217. package/dist/Interfaces/ChatInterfaces.d.ts +22 -0
  218. package/dist/Interfaces/CrudInterfaces.d.ts +539 -6
  219. package/dist/Interfaces/DashboardInterfaces.d.ts +123 -0
  220. package/dist/Interfaces/DashboardInterfaces.js +1 -0
  221. package/dist/Interfaces/DomainInterfaces.d.ts +145 -0
  222. package/dist/Interfaces/DomainInterfaces.js +1 -0
  223. package/dist/Interfaces/FileInterfaces.d.ts +183 -0
  224. package/dist/Interfaces/FileInterfaces.js +6 -0
  225. package/dist/Interfaces/FormInterfaces.d.ts +460 -32
  226. package/dist/Interfaces/FormInterfaces.js +79 -1
  227. package/dist/Interfaces/InputInterfaces.d.ts +22 -3
  228. package/dist/Interfaces/MailInterfaces.d.ts +192 -0
  229. package/dist/Interfaces/MailInterfaces.js +1 -0
  230. package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
  231. package/dist/Interfaces/RecordInterfaces.js +1 -0
  232. package/dist/Interfaces/TableInterfaces.d.ts +102 -3
  233. package/dist/Services/ExcelApi.js +5 -0
  234. package/dist/Services/FileApi.d.ts +13 -0
  235. package/dist/Services/FileApi.js +29 -0
  236. package/dist/Services/ImageEditorLoader.d.ts +14 -0
  237. package/dist/Services/ImageEditorLoader.js +51 -0
  238. package/dist/Services/JoinSources.d.ts +79 -6
  239. package/dist/Services/JoinSources.js +132 -21
  240. package/dist/Services/MessageApi.d.ts +18 -0
  241. package/dist/Services/MessageApi.js +64 -0
  242. package/dist/Services/ModuleImport.d.ts +7 -0
  243. package/dist/Services/ModuleImport.js +22 -0
  244. package/dist/Shared.Index.d.ts +28 -0
  245. package/dist/Shared.Index.js +28 -0
  246. package/package.json +13 -2
  247. package/src/Styles/Nexus.Avatar.css +6 -5
  248. package/src/Styles/Nexus.Button.css +39 -4
  249. package/src/Styles/Nexus.Chat.css +114 -22
  250. package/src/Styles/Nexus.Choice.css +16 -0
  251. package/src/Styles/Nexus.Crud.css +486 -21
  252. package/src/Styles/Nexus.Dashboard.css +763 -0
  253. package/src/Styles/Nexus.Domain.css +384 -0
  254. package/src/Styles/Nexus.Dropdown.css +8 -0
  255. package/src/Styles/Nexus.Editor.css +20 -0
  256. package/src/Styles/Nexus.Files.css +437 -0
  257. package/src/Styles/Nexus.Form.css +5 -2
  258. package/src/Styles/Nexus.Index.css +7 -0
  259. package/src/Styles/Nexus.Input.css +73 -0
  260. package/src/Styles/Nexus.Mail.css +1062 -0
  261. package/src/Styles/Nexus.Menu.css +10 -1
  262. package/src/Styles/Nexus.Popup.css +135 -1
  263. package/src/Styles/Nexus.Print.css +288 -0
  264. package/src/Styles/Nexus.QrCode.css +12 -0
  265. package/src/Styles/Nexus.Record.css +430 -0
  266. package/src/Styles/Nexus.Tab.Buttons.css +87 -22
  267. package/src/Styles/Nexus.Table.css +1458 -1387
  268. package/src/Styles/Nexus.Theme.Picker.css +1 -0
  269. package/src/Styles/Nexus.Themes.css +12 -6
  270. package/src/Styles/Nexus.Tokens.css +8 -3
  271. package/src/Styles/Nexus.Tree.css +41 -1
@@ -1,15 +1,56 @@
1
+ import { EInputTypes } from "../Interfaces/FormInterfaces.js";
1
2
  import { getCrudBackend } from "./CrudBackend.js";
3
+ import { priceAmounts, priceFromNet, priceLadder, roundDecimal } 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", other: keys.other };
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 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
+ }
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. */
40
+ function netAmountAt(values, keys, taxed = true) {
41
+ const price = readPath(values, keys.price);
42
+ if (price === null || price === undefined || price === "")
43
+ return null;
44
+ const net = priceAmounts({
45
+ price: figureAt(values, keys.price),
46
+ quantity: keys.quantity === undefined ? 1 : figureAt(values, keys.quantity),
47
+ discountPercentage: taxed ? figureAt(values, keys.discount) : 0,
48
+ vatPercentage: taxed ? figureAt(values, keys.vat) : 0,
49
+ }).netAmount;
50
+ return keys.other === undefined ? net : roundDecimal(net + otherAt(values, keys), 2);
51
+ }
11
52
  export const crudColumns = {
12
- /** The record's status in words ("Active", "Flagged", "Trashed"), from the backend's own numbers; blank until a new record is read back. */
53
+ /** The record's status in words ("Active", "Flagged", "Trashed"), from the backend's own numbers; blank for a record that says none. */
13
54
  status(changes = {}, backend) {
14
55
  return {
15
56
  key: "recordStatus",
@@ -34,12 +75,15 @@ export const crudColumns = {
34
75
  /**
35
76
  * Another record's name the **database** joined into the row: the row carries the name (`key`) and that record's own
36
77
  * 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.
78
+ * "Deleted": `CrudBackend.unavailableTag`). One call, nothing looked up. Give it `from` - the field holding that
79
+ * record's key, an input of the form: a save answers what was sent, not the names the database joins, so a CRUD names
80
+ * it again from the record picked for the key, with no call (`crudJoinedNames`). Without `from` a CRUD reads a new
81
+ * record back once (`details`) to name it.
38
82
  *
39
- * @example crudColumns.dbJoin({ key: "regionName", label: "Region", status: "regionRecordStatus" })
83
+ * @example crudColumns.dbJoin({ key: "regionName", label: "Region", status: "regionRecordStatus", from: "regionId" })
40
84
  */
41
- dbJoin({ status, ...column }, backend) {
42
- return { tag: row => (backend ?? getCrudBackend()).unavailableTag(readPath(row, status)), ...column };
85
+ dbJoin({ status, from, ...column }, backend) {
86
+ return { tag: row => (backend ?? getCrudBackend()).unavailableTag(readPath(row, status)), ...column, dbJoin: from ? { from, status } : { status } };
43
87
  },
44
88
  /**
45
89
  * Another record's name the **table** joins: the row carries only that record's key (`from`), and the table names it
@@ -56,6 +100,36 @@ export const crudColumns = {
56
100
  ...column,
57
101
  };
58
102
  },
103
+ /**
104
+ * A price's figures under one band, "Amount details" (2026-06-20's): **Price** (money), **Discount** and **VAT**
105
+ * (percents), and the **Calculated amount** - the price less the discount, plus VAT on the rest, worked out from the
106
+ * row's own figures by the server's rule (`priceAmounts`), so a row a save just answered shows it at once. A page
107
+ * spreads them where its price columns go: `...crudColumns.amounts()`. The Calculated amount's key is `netAmount`.
108
+ *
109
+ * @example const COLUMNS = [crudColumns.dbJoin({ ... }), ...crudColumns.amounts(), crudColumns.status()];
110
+ */
111
+ amounts(keys, changes = {}) {
112
+ const k = priceKeysOf(keys);
113
+ const group = changes.group ?? "Amount details";
114
+ return [
115
+ { key: k.price, label: "Price", format: "money", decimals: 2, width: "8rem", group },
116
+ { key: k.discount, label: "Discount", format: "percent", decimals: 2, width: "7rem", group },
117
+ { key: k.vat, label: "VAT", format: "percent", decimals: 2, width: "6rem", group },
118
+ { key: "netAmount", label: NET_AMOUNT_LABEL, format: "money", decimals: 2, width: "9rem", group, value: row => netAmountAt(row, k) },
119
+ ];
120
+ },
121
+ /**
122
+ * A receipt's money ladder as columns for a record's details (`CrudDetails`), in one section (`group`, default
123
+ * "Money"): Rate, Amount, Discount (5%), VAT (13%) - each left out when it is none - and the Calculated amount, money
124
+ * with 2 places, each read from the row's own field (`priceLadder`, on the server's `CounterAmounts` names unless
125
+ * `keys` says otherwise). Pass the person's number locale, so the percentages are written as they read numbers.
126
+ *
127
+ * @example ...crudColumns.priceLadder(line, { locale: useNumberLocale() })
128
+ */
129
+ priceLadder(row, options = {}) {
130
+ const { group = "Money", ...ladder } = options;
131
+ return priceLadder(row, ladder).map(line => ({ key: line.key, label: line.label, format: "money", decimals: 2, group }));
132
+ },
59
133
  };
60
134
  export const crudInputs = {
61
135
  /**
@@ -64,6 +138,73 @@ export const crudInputs = {
64
138
  * in a form, such as `span`.
65
139
  */
66
140
  description(changes = {}) {
67
- return { type: "markdown", name: "description", label: "Description", maxLength: CRUD_DESCRIPTION_MAX, height: 160, hint: "Anything worth knowing about it. Markdown.", span: 12, ...changes };
141
+ return { type: EInputTypes.Markdown, name: "description", label: "Description", maxLength: CRUD_DESCRIPTION_MAX, height: 160, hint: "Anything worth knowing about it. Markdown.", span: 12, ...changes };
142
+ },
143
+ /**
144
+ * The **Calculated amount** in a price's form: a box after Price, Discount and VAT that follows them as they are typed
145
+ * (`compute`), by the server's rule (`priceAmounts`) - never sent and never a change of its own. Blank while the price
146
+ * is empty, so a cleared price never reads 0.00. Span 3, so Price | Discount | VAT | Calculated amount fill a row (two
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`.
158
+ *
159
+ * @example INPUTS = [..., price, discountPercentage, vatPercentage, crudInputs.netAmount()]
160
+ * @example crudInputs.netAmount({ price: "unitPrice", quantity: "quantity" }, { span: 4 }, { priceDecimals: 4 })
161
+ */
162
+ netAmount(keys, changes = {}, options = {}) {
163
+ const k = priceKeysOf(keys);
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
+ };
182
+ return {
183
+ type: EInputTypes.Number,
184
+ name: "netAmount",
185
+ label: NET_AMOUNT_LABEL,
186
+ decimals: 2,
187
+ span: 3,
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 },
190
+ ...changes,
191
+ };
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);
68
209
  },
69
210
  };
@@ -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, InputParams } 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,117 @@ 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
+ /** 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;
128
+ /**
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'
130
+ * names and values, in name order (`"@countryId=7&parentId="`), or "" when nothing is fixed. So a form opened inside one
131
+ * parent never comes back inside another, a fixed form never stands in for a free one, and a draft written before the
132
+ * record moved is not brought back over its new place.
133
+ */
134
+ export declare function crudFormPlace(fixed: Readonly<Record<string, unknown>> | null | undefined): string;
135
+ /** What a narrowed list shows and fixes, read once from its `narrow`. */
136
+ export interface CrudNarrowing {
137
+ /** The field of the list's records that names the picked record. */
138
+ key: string;
139
+ /** The picked record's value as text, which a record's field must equal; `null` while none is picked: the list is empty. */
140
+ value: string | null;
141
+ /** The same value as the picked record holds it (a number stays a number): what a new record and a server's filter get. */
142
+ raw: unknown;
143
+ /** The picked record's name: its `labelKey` field, else the value. */
144
+ label: string;
145
+ }
146
+ /**
147
+ * A `Crud` `narrow` read once: the field, the picked record's value (its `valueKey` field, else its `key` field) and its
148
+ * name (its `labelKey` field, else the value). `null` without a narrowing; a value of `null` while nothing is picked, or
149
+ * the picked record holds no value.
150
+ */
151
+ export declare function crudNarrowing(narrow: CrudNarrow | null | undefined): CrudNarrowing | null;
152
+ /**
153
+ * Whether a record belongs in a narrowed list: its field holds the picked record's value (compared as text, so 7 and "7"
154
+ * are one). Every record does without a narrowing, and none while nothing is picked.
155
+ */
156
+ export declare function crudInNarrowing(row: TableRow, narrowing: CrudNarrowing | null): boolean;
43
157
  /** The message builder's action for a built-in action: "Guest recovered.", "Unpin 3 guests?" */
44
158
  export declare function crudMessageAction(action: CrudRowActionName | CrudMassActionName, markOn?: boolean): MessageActionName;
45
159
  /** What every CRUD follows, whatever module it shows. `configureCrud` changes it once, for the whole app. */