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.
Files changed (163) hide show
  1. package/CHANGELOG.md +795 -0
  2. package/README.md +220 -14
  3. package/dist/Client.Index.d.ts +13 -0
  4. package/dist/Client.Index.js +13 -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/TabButtons.d.ts +6 -0
  22. package/dist/Components/Documents/TabButtons.js +5 -4
  23. package/dist/Components/Domains/DomainCard.d.ts +98 -0
  24. package/dist/Components/Domains/DomainCard.js +182 -0
  25. package/dist/Components/Forms/ApiForm.js +5 -1
  26. package/dist/Components/Forms/Crud.js +72 -24
  27. package/dist/Components/Forms/CrudActions.d.ts +5 -4
  28. package/dist/Components/Forms/CrudActions.js +42 -12
  29. package/dist/Components/Forms/CrudHistory.d.ts +6 -1
  30. package/dist/Components/Forms/CrudHistory.js +14 -6
  31. package/dist/Components/Forms/CrudParts.d.ts +11 -4
  32. package/dist/Components/Forms/CrudParts.js +76 -10
  33. package/dist/Components/Forms/CrudRowForms.js +65 -5
  34. package/dist/Components/Forms/CrudTree.js +7 -7
  35. package/dist/Components/Forms/CrudWorkspace.d.ts +15 -5
  36. package/dist/Components/Forms/CrudWorkspace.js +19 -11
  37. package/dist/Components/Forms/Form.d.ts +3 -2
  38. package/dist/Components/Forms/Form.js +33 -26
  39. package/dist/Components/Forms/FormQuestion.d.ts +6 -0
  40. package/dist/Components/Forms/FormQuestion.js +28 -0
  41. package/dist/Components/Forms/SubmitForm.js +2 -15
  42. package/dist/Components/Inputs/ComputedInput.d.ts +51 -0
  43. package/dist/Components/Inputs/ComputedInput.js +179 -0
  44. package/dist/Components/Inputs/Dropdown.js +4 -2
  45. package/dist/Components/Inputs/GroupForm.js +3 -2
  46. package/dist/Components/Inputs/InputRenderer.d.ts +5 -1
  47. package/dist/Components/Inputs/MarkdownEditor.d.ts +6 -0
  48. package/dist/Components/Inputs/MarkdownEditor.js +32 -6
  49. package/dist/Components/Inputs/PictureInput.js +8 -3
  50. package/dist/Components/Inputs/RowsInput.d.ts +14 -1
  51. package/dist/Components/Inputs/RowsInput.js +78 -5
  52. package/dist/Components/Inputs/TabularForm.js +16 -3
  53. package/dist/Components/Inputs/TreeView.d.ts +24 -1
  54. package/dist/Components/Inputs/TreeView.js +84 -7
  55. package/dist/Components/Layouts/Popup.d.ts +22 -2
  56. package/dist/Components/Layouts/Popup.js +108 -6
  57. package/dist/Components/Messages/MailClient.d.ts +50 -0
  58. package/dist/Components/Messages/MailClient.js +410 -0
  59. package/dist/Components/Messages/MailColumns.d.ts +21 -0
  60. package/dist/Components/Messages/MailColumns.js +92 -0
  61. package/dist/Components/Messages/MailCompose.d.ts +46 -0
  62. package/dist/Components/Messages/MailCompose.js +154 -0
  63. package/dist/Components/Messages/MailReader.d.ts +43 -0
  64. package/dist/Components/Messages/MailReader.js +207 -0
  65. package/dist/Components/Messages/MailThread.d.ts +71 -0
  66. package/dist/Components/Messages/MailThread.js +185 -0
  67. package/dist/Components/Messages/MessageBell.d.ts +41 -0
  68. package/dist/Components/Messages/MessageBell.js +76 -0
  69. package/dist/Components/Viewers/DataTable.js +7 -5
  70. package/dist/Components/Viewers/DataTableColumns.d.ts +2 -2
  71. package/dist/Components/Viewers/DataTableColumns.js +2 -1
  72. package/dist/Components/Viewers/DataTableParts.d.ts +3 -1
  73. package/dist/Components/Viewers/DataTableParts.js +2 -2
  74. package/dist/Components/Viewers/OwnerFiles.d.ts +57 -5
  75. package/dist/Components/Viewers/OwnerFiles.js +142 -31
  76. package/dist/Helpers/ApiModules.js +4 -0
  77. package/dist/Helpers/AuditEntries.d.ts +50 -0
  78. package/dist/Helpers/AuditEntries.js +96 -0
  79. package/dist/Helpers/ChatBackend.d.ts +8 -1
  80. package/dist/Helpers/ChatBackend.js +18 -0
  81. package/dist/Helpers/ChatHelpers.d.ts +2 -0
  82. package/dist/Helpers/ChatHelpers.js +4 -1
  83. package/dist/Helpers/CrudColumns.d.ts +44 -6
  84. package/dist/Helpers/CrudColumns.js +63 -13
  85. package/dist/Helpers/CrudHelpers.d.ts +36 -1
  86. package/dist/Helpers/CrudHelpers.js +61 -0
  87. package/dist/Helpers/CrudPickerPlan.d.ts +58 -0
  88. package/dist/Helpers/CrudPickerPlan.js +46 -0
  89. package/dist/Helpers/CrudRowForms.d.ts +18 -1
  90. package/dist/Helpers/CrudRowForms.js +35 -2
  91. package/dist/Helpers/DashboardHelpers.d.ts +115 -0
  92. package/dist/Helpers/DashboardHelpers.js +305 -0
  93. package/dist/Helpers/DomainCards.d.ts +140 -0
  94. package/dist/Helpers/DomainCards.js +370 -0
  95. package/dist/Helpers/FileAdapter.d.ts +82 -2
  96. package/dist/Helpers/FileAdapter.js +72 -1
  97. package/dist/Helpers/FormStore.d.ts +11 -1
  98. package/dist/Helpers/FormStore.js +66 -19
  99. package/dist/Helpers/InputConditions.d.ts +6 -0
  100. package/dist/Helpers/InputConditions.js +24 -0
  101. package/dist/Helpers/InputMemory.js +40 -30
  102. package/dist/Helpers/InputNarrowing.d.ts +38 -6
  103. package/dist/Helpers/InputNarrowing.js +102 -21
  104. package/dist/Helpers/MailClientHelpers.d.ts +96 -0
  105. package/dist/Helpers/MailClientHelpers.js +211 -0
  106. package/dist/Helpers/MessageBackend.d.ts +152 -0
  107. package/dist/Helpers/MessageBackend.js +329 -0
  108. package/dist/Helpers/MessageHelpers.d.ts +160 -0
  109. package/dist/Helpers/MessageHelpers.js +573 -0
  110. package/dist/Helpers/MessageUnread.d.ts +95 -0
  111. package/dist/Helpers/MessageUnread.js +245 -0
  112. package/dist/Helpers/NexusDriveAdapter.d.ts +17 -1
  113. package/dist/Helpers/NexusDriveAdapter.js +32 -3
  114. package/dist/Helpers/NumberHelpers.d.ts +61 -0
  115. package/dist/Helpers/NumberHelpers.js +124 -8
  116. package/dist/Helpers/PagedPicker.d.ts +42 -0
  117. package/dist/Helpers/PagedPicker.js +85 -0
  118. package/dist/Helpers/RowsStore.d.ts +24 -0
  119. package/dist/Helpers/RowsStore.js +79 -10
  120. package/dist/Helpers/TableHelpers.d.ts +15 -2
  121. package/dist/Helpers/TableHelpers.js +28 -1
  122. package/dist/Helpers/TableStore.d.ts +7 -3
  123. package/dist/Helpers/TableStore.js +9 -4
  124. package/dist/Helpers/ToolbarFit.d.ts +23 -0
  125. package/dist/Helpers/ToolbarFit.js +30 -0
  126. package/dist/Helpers/TreeStore.d.ts +75 -6
  127. package/dist/Helpers/TreeStore.js +182 -18
  128. package/dist/Interfaces/ApiInterfaces.d.ts +10 -1
  129. package/dist/Interfaces/ApiInterfaces.js +10 -1
  130. package/dist/Interfaces/ChatInterfaces.d.ts +22 -0
  131. package/dist/Interfaces/CrudInterfaces.d.ts +78 -5
  132. package/dist/Interfaces/DashboardInterfaces.d.ts +123 -0
  133. package/dist/Interfaces/DashboardInterfaces.js +1 -0
  134. package/dist/Interfaces/DomainInterfaces.d.ts +145 -0
  135. package/dist/Interfaces/DomainInterfaces.js +1 -0
  136. package/dist/Interfaces/FileInterfaces.d.ts +21 -0
  137. package/dist/Interfaces/FileInterfaces.js +2 -0
  138. package/dist/Interfaces/FormInterfaces.d.ts +78 -11
  139. package/dist/Interfaces/InputInterfaces.d.ts +12 -5
  140. package/dist/Interfaces/MailInterfaces.d.ts +192 -0
  141. package/dist/Interfaces/MailInterfaces.js +1 -0
  142. package/dist/Interfaces/TableInterfaces.d.ts +28 -2
  143. package/dist/Services/JoinSources.d.ts +47 -6
  144. package/dist/Services/JoinSources.js +68 -33
  145. package/dist/Services/MessageApi.d.ts +18 -0
  146. package/dist/Services/MessageApi.js +64 -0
  147. package/dist/Shared.Index.d.ts +12 -0
  148. package/dist/Shared.Index.js +12 -0
  149. package/package.json +7 -1
  150. package/src/Styles/Nexus.Button.css +29 -0
  151. package/src/Styles/Nexus.Chat.css +114 -22
  152. package/src/Styles/Nexus.Crud.css +119 -28
  153. package/src/Styles/Nexus.Dashboard.css +763 -0
  154. package/src/Styles/Nexus.Domain.css +384 -0
  155. package/src/Styles/Nexus.Editor.css +20 -0
  156. package/src/Styles/Nexus.Files.css +27 -0
  157. package/src/Styles/Nexus.Index.css +3 -0
  158. package/src/Styles/Nexus.Input.css +31 -0
  159. package/src/Styles/Nexus.Mail.css +1062 -0
  160. package/src/Styles/Nexus.Popup.css +58 -0
  161. package/src/Styles/Nexus.Table.css +1458 -1449
  162. package/src/Styles/Nexus.Theme.Picker.css +1 -0
  163. package/src/Styles/Nexus.Tree.css +10 -0
@@ -12,6 +12,17 @@
12
12
  * itself: the view asks, and tells the store what came back (`beginLoad`, `receivePage`, `failLoad`).
13
13
  */
14
14
  import { RequestCache } from "./RequestCache.js";
15
+ /** Where a tree keeps the filter chosen in this browser: per `key` and `viewer`, nothing without a viewer. */
16
+ export function treeFilterStorageKey(remember) {
17
+ const viewer = remember?.viewer?.trim();
18
+ return remember?.key && viewer ? `nx-tree-filter:${remember.key}:${viewer}` : null;
19
+ }
20
+ /** The filter on: the one chosen, else the default, else the first; `undefined` without filters. An id no filter has is passed over. */
21
+ export function shownTreeFilter(filters, chosen, defaultId) {
22
+ if (!filters || filters.length === 0)
23
+ return undefined;
24
+ return filters.find(filter => filter.id === chosen) ?? filters.find(filter => filter.id === defaultId) ?? filters[0];
25
+ }
15
26
  /** Children's pages kept for every lazy tree on the page that names a `cacheKey`, for 5 minutes: see `RequestCache`. */
16
27
  export const treePages = new RequestCache();
17
28
  /** The start of every key a tree keeps its pages under, or one parent's pages (`null`: the top level). */
@@ -310,6 +321,11 @@ export class TreeStore {
310
321
  openNodes = 0;
311
322
  openCheckedNodes = 0;
312
323
  indexCache = null;
324
+ /** The leaves shown (`setFilter`); `null`: all. */
325
+ match = null;
326
+ /** Goes up whenever a node's counts are worked out again, so what is kept from them knows it is old. */
327
+ counted = 0;
328
+ shownRoots = null;
313
329
  constructor(items = [], options = {}, value) {
314
330
  this.valueKey = options.valueKey ?? TREE_DEFAULT_KEYS.value;
315
331
  this.parentKey = options.parentKey ?? TREE_DEFAULT_KEYS.parent;
@@ -322,8 +338,40 @@ export class TreeStore {
322
338
  this.isChecked = options.isItemChecked ?? defaultChecked;
323
339
  if (this.lazy && options.lazyRoots)
324
340
  this.rootPages = idlePages();
341
+ this.match = options.filter ?? null;
325
342
  this.setItems(items, value);
326
343
  }
344
+ /** Whether a filter is on (`setFilter`). */
345
+ get filtered() {
346
+ return this.match !== null;
347
+ }
348
+ /**
349
+ * Shows only the leaves `match` takes, and the parents that hold one (`null`: every item). Nothing is checked or
350
+ * unchecked by it, and the value is the same whatever filter is on. While one is on, a parent's box, its count, and a
351
+ * check of it - or of "select all" - work on the leaves shown only: checking a parent checks its shown leaves, and a
352
+ * leaf the filter hides keeps its check exactly as it was. Expanded items stay expanded.
353
+ */
354
+ setFilter(match) {
355
+ this.match = match;
356
+ this.recount(preOrder(this.roots));
357
+ this.rows = this.visibleRows(this.roots, null);
358
+ }
359
+ /** Children of a parent (`null`: the top level) that are rows of the filtered tree: all of them without a filter. */
360
+ shownChildCount(parent) {
361
+ if (parent)
362
+ return parent.shownChildren;
363
+ if (this.match === null)
364
+ return this.roots.length;
365
+ // Counted once per change of counts, not once per row drawn.
366
+ if (this.shownRoots?.counted !== this.counted) {
367
+ let count = 0;
368
+ for (const root of this.roots)
369
+ if (root.shown)
370
+ count++;
371
+ this.shownRoots = { counted: this.counted, count };
372
+ }
373
+ return this.shownRoots.count;
374
+ }
327
375
  /** Items in the tree, drafts included. */
328
376
  get size() {
329
377
  return this.byKey.size;
@@ -456,15 +504,22 @@ export class TreeStore {
456
504
  * unloaded items are not in the value - unless the value names it (`valueMode: "checked"`, where it stands for all).
457
505
  */
458
506
  state(node) {
507
+ return this.stateOver(node, node.shownChecked, node.shownLeaves);
508
+ }
509
+ /** The state of a parent over all of its leaves, whatever the filter shows. */
510
+ wholeState(node) {
511
+ return this.stateOver(node, node.checkedLeaves, node.leaves);
512
+ }
513
+ stateOver(node, checked, leaves) {
459
514
  if (!this.cascade || node.children.length === 0)
460
515
  return node.checked ? "checked" : "unchecked";
461
516
  if (node.partial)
462
- return this.pending.has(node.key) ? "checked" : node.checkedLeaves > 0 ? "mixed" : "unchecked";
463
- if (node.checkedLeaves === 0)
517
+ return this.pending.has(node.key) ? "checked" : checked > 0 ? "mixed" : "unchecked";
518
+ if (checked === 0)
464
519
  return "unchecked";
465
- return node.checkedLeaves === node.leaves ? "checked" : "mixed";
520
+ return checked === leaves ? "checked" : "mixed";
466
521
  }
467
- /** Checked boxes and all boxes: leaves with `cascade`, every item without it. Drafts count. */
522
+ /** Checked boxes and all boxes: leaves with `cascade`, every item without it. Drafts count. The filter does not narrow it. */
468
523
  counts() {
469
524
  if (!this.cascade)
470
525
  return { checked: this.checkedNodes, total: this.byKey.size };
@@ -476,6 +531,29 @@ export class TreeStore {
476
531
  }
477
532
  return { checked, total };
478
533
  }
534
+ /** `counts` over what the filter shows: the leaves shown with `cascade`, the rows of items without it. */
535
+ shownCounts() {
536
+ if (this.match === null)
537
+ return this.counts();
538
+ let checked = 0;
539
+ let total = 0;
540
+ if (this.cascade) {
541
+ for (const root of this.roots) {
542
+ checked += root.shownChecked;
543
+ total += root.shownLeaves;
544
+ }
545
+ }
546
+ else {
547
+ for (const node of this.byKey.values()) {
548
+ if (!node.shown)
549
+ continue;
550
+ total++;
551
+ if (node.checked)
552
+ checked++;
553
+ }
554
+ }
555
+ return { checked, total };
556
+ }
479
557
  /**
480
558
  * The checked values, in tree order, following `valueMode`. In a lazy tree, values whose items are not loaded yet
481
559
  * (or not settled) follow the others, so a saved value survives until its items come.
@@ -543,9 +621,12 @@ export class TreeStore {
543
621
  return false;
544
622
  if (!this.cascade)
545
623
  return this.setNodeChecked(node, !node.checked);
546
- return this.setChecked(node, node.openChecked < node.openLeaves);
624
+ return this.setChecked(node, node.shownOpenChecked < node.shownOpenLeaves);
547
625
  }
548
- /** Checks or unchecks a node; with `cascade`, every item under it that is not locked. Returns whether anything changed. */
626
+ /**
627
+ * Checks or unchecks a node; with `cascade`, every item under it that is not locked - and, while a filter is on, that
628
+ * the filter shows: the hidden ones keep their checks. Returns whether anything changed.
629
+ */
549
630
  setChecked(node, checked) {
550
631
  if (node.locked || node.status)
551
632
  return false;
@@ -559,7 +640,7 @@ export class TreeStore {
559
640
  this.dropClaimsAbove(node);
560
641
  return changed;
561
642
  }
562
- /** The state of a "select all" box, over the items that are not locked; `null` when every item is locked. */
643
+ /** The state of a "select all" box, over the items that are not locked (and shown); `null` when every one is locked. */
563
644
  allState() {
564
645
  let open = this.openNodes;
565
646
  let checked = this.openCheckedNodes;
@@ -567,8 +648,19 @@ export class TreeStore {
567
648
  open = 0;
568
649
  checked = 0;
569
650
  for (const root of this.roots) {
570
- open += root.openLeaves;
571
- checked += root.openChecked;
651
+ open += root.shownOpenLeaves;
652
+ checked += root.shownOpenChecked;
653
+ }
654
+ }
655
+ else if (this.match !== null) {
656
+ open = 0;
657
+ checked = 0;
658
+ for (const node of this.byKey.values()) {
659
+ if (!node.shown || node.locked)
660
+ continue;
661
+ open++;
662
+ if (node.checked)
663
+ checked++;
572
664
  }
573
665
  }
574
666
  if (open === 0)
@@ -581,9 +673,9 @@ export class TreeStore {
581
673
  return state !== null && this.checkAll(state !== "checked");
582
674
  }
583
675
  /**
584
- * Checks or unchecks every item that is not locked. Returns whether anything changed. In a lazy tree with `cascade`,
585
- * a node whose items are not all loaded is passed over and the loaded parts under it are changed; values whose
586
- * items are not loaded stay as they are.
676
+ * Checks or unchecks every item that is not locked, and that the filter shows. Returns whether anything changed. In a
677
+ * lazy tree with `cascade`, a node whose items are not all loaded is passed over and the loaded parts under it are
678
+ * changed; values whose items are not loaded stay as they are.
587
679
  */
588
680
  checkAll(checked) {
589
681
  let changed = false;
@@ -593,7 +685,7 @@ export class TreeStore {
593
685
  }
594
686
  else {
595
687
  for (const node of preOrder(this.roots))
596
- changed = (!node.locked && this.setNodeChecked(node, checked)) || changed;
688
+ changed = (!node.locked && node.shown && this.setNodeChecked(node, checked)) || changed;
597
689
  }
598
690
  return changed;
599
691
  }
@@ -602,6 +694,9 @@ export class TreeStore {
602
694
  isVisible(node) {
603
695
  if (node.status)
604
696
  return this.indexOf(node) >= 0;
697
+ // A node the filter hides is no row; a shown node's parents are shown too.
698
+ if (!node.shown)
699
+ return false;
605
700
  for (let parent = node.parent; parent; parent = parent.parent)
606
701
  if (!parent.expanded)
607
702
  return false;
@@ -888,7 +983,7 @@ export class TreeStore {
888
983
  const removed = preOrder([node]);
889
984
  // A parent that loses its last child becomes a leaf, checked when all of its items were.
890
985
  if (parent && this.cascade && siblings.length === 1)
891
- parent.checked = this.state(parent) === "checked";
986
+ parent.checked = this.wholeState(parent) === "checked";
892
987
  siblings.splice(index, 1);
893
988
  if (parent && parent.children.length === 0 && !this.canExpand(parent))
894
989
  parent.expanded = false;
@@ -1086,6 +1181,12 @@ export class TreeStore {
1086
1181
  draft: false,
1087
1182
  pages: this.lazy ? this.newPages(item) : undefined,
1088
1183
  partial: false,
1184
+ shown: true,
1185
+ shownChildren: 0,
1186
+ shownLeaves: 1,
1187
+ shownChecked: 0,
1188
+ shownOpenLeaves: 0,
1189
+ shownOpenChecked: 0,
1089
1190
  };
1090
1191
  }
1091
1192
  /** A lazy node's pages before anything is asked: none to ask for when its item says it has no children. */
@@ -1134,6 +1235,8 @@ export class TreeStore {
1134
1235
  }
1135
1236
  for (let index = path.length - 1; index >= 0; index--)
1136
1237
  this.aggregate(path[index]);
1238
+ if (this.match !== null && added.length > 0)
1239
+ shownPositions(siblings);
1137
1240
  this.count(path, 1);
1138
1241
  this.count(added, 1);
1139
1242
  return added;
@@ -1287,6 +1390,9 @@ export class TreeStore {
1287
1390
  stack.push(nodes[index]);
1288
1391
  while (stack.length > 0) {
1289
1392
  const node = stack.pop();
1393
+ // Hidden by the filter, with everything under it.
1394
+ if (!node.shown)
1395
+ continue;
1290
1396
  list.push(node);
1291
1397
  if (!node.expanded)
1292
1398
  continue;
@@ -1336,6 +1442,12 @@ export class TreeStore {
1336
1442
  openChecked: 0,
1337
1443
  draft: false,
1338
1444
  partial: false,
1445
+ shown: true,
1446
+ shownChildren: 0,
1447
+ shownLeaves: 0,
1448
+ shownChecked: 0,
1449
+ shownOpenLeaves: 0,
1450
+ shownOpenChecked: 0,
1339
1451
  };
1340
1452
  this.statusRows.set(parentKey, row);
1341
1453
  }
@@ -1371,6 +1483,12 @@ export class TreeStore {
1371
1483
  }
1372
1484
  for (let index = ordered.length - 1; index >= 0; index--)
1373
1485
  this.aggregate(ordered[index]);
1486
+ // Under a filter a row's place among its siblings (aria-posinset) counts the shown ones only.
1487
+ if (this.match !== null) {
1488
+ shownPositions(this.roots);
1489
+ for (const node of ordered)
1490
+ shownPositions(node.children);
1491
+ }
1374
1492
  this.checkedNodes = 0;
1375
1493
  this.openNodes = 0;
1376
1494
  this.openCheckedNodes = 0;
@@ -1381,6 +1499,7 @@ export class TreeStore {
1381
1499
  * does for every node, and what a page of children does for each node on its parent's path.
1382
1500
  */
1383
1501
  aggregate(node) {
1502
+ this.counted++;
1384
1503
  const pages = node.pages;
1385
1504
  const own = pages !== undefined && (pages.status !== "ready" || pages.hasMore);
1386
1505
  if (node.children.length === 0) {
@@ -1392,6 +1511,14 @@ export class TreeStore {
1392
1511
  node.checkedLeaves = node.checked ? 1 : 0;
1393
1512
  node.openLeaves = fixed ? 0 : 1;
1394
1513
  node.openChecked = fixed ? 0 : node.checkedLeaves;
1514
+ // A draft being named shows whatever the filter says: its name box must not vanish.
1515
+ const matched = this.match === null || node.draft || this.match(node.item);
1516
+ node.shown = matched || own;
1517
+ node.shownChildren = 0;
1518
+ node.shownLeaves = matched ? 1 : 0;
1519
+ node.shownChecked = matched ? node.checkedLeaves : 0;
1520
+ node.shownOpenLeaves = matched ? node.openLeaves : 0;
1521
+ node.shownOpenChecked = matched ? node.openChecked : 0;
1395
1522
  return;
1396
1523
  }
1397
1524
  let leaves = 0;
@@ -1399,7 +1526,13 @@ export class TreeStore {
1399
1526
  let openLeaves = 0;
1400
1527
  let openChecked = 0;
1401
1528
  let allLocked = true;
1529
+ let shownLocked = true;
1402
1530
  let partial = own;
1531
+ let shownChildren = 0;
1532
+ let shownLeaves = 0;
1533
+ let shownChecked = 0;
1534
+ let shownOpenLeaves = 0;
1535
+ let shownOpenChecked = 0;
1403
1536
  for (const child of node.children) {
1404
1537
  leaves += child.leaves;
1405
1538
  checked += child.checkedLeaves;
@@ -1407,14 +1540,29 @@ export class TreeStore {
1407
1540
  openChecked += child.openChecked;
1408
1541
  allLocked &&= child.locked;
1409
1542
  partial ||= child.partial;
1543
+ if (!child.shown)
1544
+ continue;
1545
+ shownChildren++;
1546
+ shownLeaves += child.shownLeaves;
1547
+ shownChecked += child.shownChecked;
1548
+ shownOpenLeaves += child.shownOpenLeaves;
1549
+ shownOpenChecked += child.shownOpenChecked;
1550
+ shownLocked &&= child.locked;
1410
1551
  }
1411
1552
  node.leaves = leaves;
1412
1553
  node.checkedLeaves = checked;
1413
1554
  node.openLeaves = openLeaves;
1414
1555
  node.openChecked = openChecked;
1415
1556
  node.partial = partial;
1416
- // A parent's box follows its items only with cascade; without it, a parent stays free.
1417
- node.locked = node.disabledPath || (this.cascade && allLocked);
1557
+ node.shown = this.match === null || shownChildren > 0 || own;
1558
+ node.shownChildren = shownChildren;
1559
+ node.shownLeaves = shownLeaves;
1560
+ node.shownChecked = shownChecked;
1561
+ node.shownOpenLeaves = shownOpenLeaves;
1562
+ node.shownOpenChecked = shownOpenChecked;
1563
+ // A parent's box follows its items only with cascade; without it, a parent stays free. Under a filter it follows the
1564
+ // items shown: a parent whose shown items are all locked has nothing a check could change.
1565
+ node.locked = node.disabledPath || (this.cascade && (this.match !== null && shownChildren > 0 ? shownLocked : allLocked));
1418
1566
  }
1419
1567
  /** Sets every open leaf under a node (the node itself for a leaf), updating counts up the path of each leaf that changes. */
1420
1568
  setLeaves(node, checked) {
@@ -1422,8 +1570,9 @@ export class TreeStore {
1422
1570
  const stack = [node];
1423
1571
  while (stack.length > 0) {
1424
1572
  const current = stack.pop();
1425
- // A subtree that is locked, or already all checked or all unchecked, has nothing to change.
1426
- if (current.locked || current.openChecked === (checked ? current.openLeaves : 0))
1573
+ // A subtree that is locked, or whose shown leaves are already all checked or all unchecked, has nothing to change:
1574
+ // a leaf the filter hides counts in no `shown` number, so it is never reached.
1575
+ if (current.locked || current.shownOpenChecked === (checked ? current.shownOpenLeaves : 0))
1427
1576
  continue;
1428
1577
  if (current.children.length > 0) {
1429
1578
  for (const child of current.children)
@@ -1435,6 +1584,8 @@ export class TreeStore {
1435
1584
  for (let path = current; path; path = path.parent) {
1436
1585
  path.checkedLeaves += step;
1437
1586
  path.openChecked += step;
1587
+ path.shownChecked += step;
1588
+ path.shownOpenChecked += step;
1438
1589
  }
1439
1590
  changed = true;
1440
1591
  }
@@ -1449,10 +1600,16 @@ export class TreeStore {
1449
1600
  if (!node.locked)
1450
1601
  this.openCheckedNodes += step;
1451
1602
  if (node.children.length === 0) {
1603
+ const shown = node.shownLeaves === 1;
1452
1604
  for (let path = node; path; path = path.parent) {
1453
1605
  path.checkedLeaves += step;
1454
1606
  if (!node.locked)
1455
1607
  path.openChecked += step;
1608
+ if (!shown)
1609
+ continue;
1610
+ path.shownChecked += step;
1611
+ if (!node.locked)
1612
+ path.shownOpenChecked += step;
1456
1613
  }
1457
1614
  }
1458
1615
  return true;
@@ -1517,6 +1674,13 @@ function valueMap(values) {
1517
1674
  }
1518
1675
  return map;
1519
1676
  }
1677
+ /** Numbers siblings among the shown ones (`position`), as a filtered tree's rows are read out. */
1678
+ function shownPositions(siblings) {
1679
+ let position = 0;
1680
+ for (const node of siblings)
1681
+ if (node.shown)
1682
+ node.position = position++;
1683
+ }
1520
1684
  /** Nodes in tree order: each node, then its children. No recursion, so even a chain 100,000 levels deep works. */
1521
1685
  export function preOrder(nodes) {
1522
1686
  const list = [];
@@ -8,7 +8,9 @@ export type ApiModuleValue = string | number;
8
8
  * **These numbers are the backend's, not this package's.** A permission is a bitmask of them, so a value that differs
9
9
  * from the backend by one bit does not fail loudly: it silently grants or denies the wrong module. `Chat` was added
10
10
  * at bit 6 in the backend on 2026-09-25 and every module after it moved up one bit; this list moved with it, which
11
- * changed the value of `Accounting` through `Portfolio`. See CHANGELOG.md.
11
+ * changed the value of `Accounting` through `Portfolio`. `Inventory` was added at bit 17 on 2026-11-09, after
12
+ * `Portfolio`: no value moved. `Report` (bit 8) was retired on 2026-11-11 and keeps its bit, which no module may take.
13
+ * A new module takes the next bit at the end, never one in between. See CHANGELOG.md.
12
14
  */
13
15
  export declare const NexusModule: {
14
16
  /** The app's own API routes (`/api/...`), not a backend module. */
@@ -21,6 +23,11 @@ export declare const NexusModule: {
21
23
  readonly Message: number;
22
24
  readonly Chat: number;
23
25
  readonly Accounting: number;
26
+ /**
27
+ * @deprecated Retired by the backend on 2026-11-11: its print layouts and print door are the Configuration module's
28
+ * now (`NexusModule.Configuration`). The value stays so that bit 8 is never given to another module, and old
29
+ * permission masks that hold it still read; no backend serves it, so offer it nowhere and call it never.
30
+ */
24
31
  readonly Report: number;
25
32
  readonly Requisition: number;
26
33
  readonly Tenant: number;
@@ -30,6 +37,8 @@ export declare const NexusModule: {
30
37
  readonly Pharmacy: number;
31
38
  readonly Staffing: number;
32
39
  readonly Portfolio: number;
40
+ /** Stores, items, stock received (Dakhila) and issued (Nikasha), demands, purchase orders, write-off. */
41
+ readonly Inventory: number;
33
42
  };
34
43
  export type NexusModule = (typeof NexusModule)[keyof typeof NexusModule];
35
44
  /**
@@ -5,7 +5,9 @@
5
5
  * **These numbers are the backend's, not this package's.** A permission is a bitmask of them, so a value that differs
6
6
  * from the backend by one bit does not fail loudly: it silently grants or denies the wrong module. `Chat` was added
7
7
  * at bit 6 in the backend on 2026-09-25 and every module after it moved up one bit; this list moved with it, which
8
- * changed the value of `Accounting` through `Portfolio`. See CHANGELOG.md.
8
+ * changed the value of `Accounting` through `Portfolio`. `Inventory` was added at bit 17 on 2026-11-09, after
9
+ * `Portfolio`: no value moved. `Report` (bit 8) was retired on 2026-11-11 and keeps its bit, which no module may take.
10
+ * A new module takes the next bit at the end, never one in between. See CHANGELOG.md.
9
11
  */
10
12
  export const NexusModule = {
11
13
  /** The app's own API routes (`/api/...`), not a backend module. */
@@ -18,6 +20,11 @@ export const NexusModule = {
18
20
  Message: 1 << 5,
19
21
  Chat: 1 << 6,
20
22
  Accounting: 1 << 7,
23
+ /**
24
+ * @deprecated Retired by the backend on 2026-11-11: its print layouts and print door are the Configuration module's
25
+ * now (`NexusModule.Configuration`). The value stays so that bit 8 is never given to another module, and old
26
+ * permission masks that hold it still read; no backend serves it, so offer it nowhere and call it never.
27
+ */
21
28
  Report: 1 << 8,
22
29
  Requisition: 1 << 9,
23
30
  Tenant: 1 << 10,
@@ -27,4 +34,6 @@ export const NexusModule = {
27
34
  Pharmacy: 1 << 14,
28
35
  Staffing: 1 << 15,
29
36
  Portfolio: 1 << 16,
37
+ /** Stores, items, stock received (Dakhila) and issued (Nikasha), demands, purchase orders, write-off. */
38
+ Inventory: 1 << 17,
30
39
  };
@@ -1,3 +1,5 @@
1
+ import type { ReactNode } from "react";
2
+ import type { DomainCardDefinition } from "./DomainInterfaces.ts";
1
3
  import type { DropdownPage, DropdownPageRequest } from "./InputInterfaces.ts";
2
4
  /** An id, as the backend hands it out: a number, or text (a GUID, an encrypted id). */
3
5
  export type ChatId = string | number;
@@ -45,9 +47,22 @@ export interface ChatDomainInfo<TDomain extends ChatDomainValue = AppChatDomain>
45
47
  key: string;
46
48
  /** Its name on screen: "Leave request". */
47
49
  label: string;
50
+ /**
51
+ * How a message about a record of this kind shows the record: read live from its module and drawn as a card
52
+ * (`DomainCard`; a mail thread draws it at its head). Without one, the message shows the kind and the record's words.
53
+ */
54
+ card?: DomainCardDefinition<any>;
55
+ /** Its icon where its kind is listed (a mail client's Types): `<Icon name="beach" />`. Default a clipboard. */
56
+ icon?: ReactNode;
48
57
  }
49
58
  /** `sent`: the server has it. `sending`: on its way. `failed`: it did not arrive, and the text is kept to send again. */
50
59
  export type ChatMessageStatus = "sent" | "sending" | "failed";
60
+ /**
61
+ * What a message is. `chat`: a line in a conversation, or a reply. `mail`: the message a mail thread opens with, which a
62
+ * person wrote. `notification`: what an app wrote about an event (approved, sent back), under the name of the person who
63
+ * acted - drawn as a quiet event line, not a bubble, and unread for everyone but its author.
64
+ */
65
+ export type ChatMessageKind = "chat" | "mail" | "notification";
51
66
  /** A file on a message. The chat shows and links it; uploading is the app's `ChatSource`. */
52
67
  export interface ChatAttachment {
53
68
  id?: ChatId;
@@ -81,6 +96,8 @@ export interface ChatMessage {
81
96
  attachments?: readonly ChatAttachment[];
82
97
  /** Written by the system, not a person ("Sita approved the request."): a centered note, with no avatar or side. */
83
98
  isSystem?: boolean;
99
+ /** What it is. Default `chat`. A `notification` is drawn as an event line: who acted, what happened, when. */
100
+ kind?: ChatMessageKind;
84
101
  /** Who it has reached. Two ticks once anyone has it, filled once anyone has read it. */
85
102
  deliveredTo?: readonly ChatId[];
86
103
  /** Who has read it. */
@@ -147,6 +164,11 @@ export interface ChatDraft {
147
164
  files?: readonly File[];
148
165
  /** This browser's id for the message, so the answer replaces the right one and Retry sends the same draft. */
149
166
  clientId: string;
167
+ /**
168
+ * What the line is. Default: the source's own, a chat line. A mail thread's Reply sends `mail` (a new mail in the
169
+ * thread, drawn as mail), its discussion `chat`.
170
+ */
171
+ kind?: ChatMessageKind;
150
172
  }
151
173
  /** Someone in a conversation, or someone who could be added to it. */
152
174
  export interface ChatParticipant {
@@ -113,12 +113,23 @@ export interface CrudFormOptions<TRow extends FormValues = FormValues> {
113
113
  aside?: ReactNode | ((context: CrudFormAsideContext<TRow>) => ReactNode);
114
114
  /** Values of a new record, over each input's default. */
115
115
  newValues?: Partial<TRow> | (() => Partial<TRow>);
116
+ /**
117
+ * Values of a new record read when the Add form opens, over `newValues`: who a new message goes to by default, a
118
+ * counter's usual account. The inputs show disabled until they land, and a draft of the form comes back over them.
119
+ * They are a help, not the record: when the read fails the form opens on `newValues` alone, and a toast says why.
120
+ * The values given to `create(values)` (a parent it is added under) still win.
121
+ */
122
+ createDefaults?: (signal: AbortSignal) => Promise<Partial<TRow> | null | undefined>;
116
123
  /**
117
124
  * What Edit opens with: the row as the table has it (`row`, the default), the record from the `details` endpoint
118
125
  * (`details`, for fields a list leaves out), or a function that loads it.
119
126
  */
120
127
  load?: "row" | "details" | ((row: TRow, signal: AbortSignal) => Promise<Partial<TRow> | null | undefined>);
121
- /** Shapes what is sent, e.g. to add a parent's id. Default: the values, with the fields the record came with. */
128
+ /**
129
+ * Shapes what is sent, e.g. to add a parent's id. Default: the values, with the fields the record came with. The row the
130
+ * save leaves is built from what this sends (`crudSavedRecord`): a field it sends as null is empty on the row, whatever
131
+ * the form still holds.
132
+ */
122
133
  transform?: (values: TRow, context: {
123
134
  action: "create" | "edit";
124
135
  row: TRow | null;
@@ -132,6 +143,21 @@ export interface CrudFormOptions<TRow extends FormValues = FormValues> {
132
143
  validate?: FormProps<TRow>["validate"];
133
144
  /** Derived values: return values to set after a change. */
134
145
  onFieldChange?: FormProps<TRow>["onFieldChange"];
146
+ /**
147
+ * What the inputs worked out from others (`compute`) are handed - the form's own and those of its Tabular and Group
148
+ * rows (`crudInputs.lineAmount`) - worked out from the form's values as they change: a goods receipt hands its lines
149
+ * `{ taxed: values.source === Purchase }`, so a donation's lines count no discount or VAT. It is read after every
150
+ * change and compared field by field with the last answer (a shallow compare), so a fresh object of the same figures
151
+ * redraws nothing; a different one redraws the worked-out cells alone, with nothing remounted and the focus kept. A
152
+ * rule that throws keeps the last answer and says why in the console.
153
+ *
154
+ * @example
155
+ * form={{ computeWith: values => ({ purchase: values.source !== DONATION }) }}
156
+ */
157
+ computeWith?: (values: TRow, context: {
158
+ action: "create" | "edit";
159
+ row: TRow | null;
160
+ }) => unknown;
135
161
  /** Label design of every input that does not set its own. */
136
162
  design?: InputDesign;
137
163
  /**
@@ -217,6 +243,33 @@ export interface CrudDetailsOptions<TRow extends FormValues = FormValues> {
217
243
  key: string;
218
244
  viewer?: string | null;
219
245
  };
246
+ /**
247
+ * The Details tab's sections (the runs of columns with one `group`) that change on their own: an Edit button at the
248
+ * end of the section's numbered heading opens a form for that part only - the Crud's own Edit form (`edit: "edit"`)
249
+ * or one of its `rowForms` (by its `id`) - and once it is saved the workspace reads the record again (`refresh()`), so
250
+ * the section shows what was saved. The button is there only where the form itself is offered (Edit: a list that
251
+ * offers Edit, the update endpoint allowed, no `rowActionLock` on it; a row form: `crudRowFormOffered` and no `lock`),
252
+ * and the focus comes back to it when the form closes. Only in a record's workspace (`tabs` or `profile`).
253
+ *
254
+ * @example
255
+ * sections: [
256
+ * { group: "KYC details", edit: "edit" },
257
+ * { group: "Contact details", edit: "contacts" }, // a rowForms entry's id
258
+ * { group: "Public profile", edit: "public-profile", label: "Edit", hidden: row => row.isLocked },
259
+ * ]
260
+ */
261
+ sections?: readonly CrudDetailsSection<TRow>[];
262
+ }
263
+ /** A section of a record's Details tab that changes on its own (`CrudDetailsOptions.sections`). */
264
+ export interface CrudDetailsSection<TRow extends FormValues = FormValues> {
265
+ /** The `group` its columns name. */
266
+ group: string;
267
+ /** What its button opens: `"edit"`, the Crud's own Edit form, or the `id` of one of the Crud's `rowForms`. */
268
+ edit?: "edit" | (string & {});
269
+ /** The button's word. Default "Edit". */
270
+ label?: string;
271
+ /** Leaves the button out for this record. */
272
+ hidden?: (row: TRow) => boolean;
220
273
  }
221
274
  /**
222
275
  * A button in the details popup's footer. One that names the endpoint it calls shows only to a user who may call
@@ -516,7 +569,7 @@ export interface CrudScopes<TRow extends FormValues = FormValues> {
516
569
  * beside its Edit. The page gives its inputs, where its values are read and where a save goes, and which of the answer's
517
570
  * fields change the record; the CRUD offers it, opens it (one popup per record, brought back if open), and writes those
518
571
  * fields into the record wherever it holds it - the table, every list kept, the pinned sections - with no list read
519
- * again.
572
+ * again. With no inputs it is a question alone (a pay run's Lock): it asks over the record's figures and sends.
520
573
  *
521
574
  * @example
522
575
  * {
@@ -526,6 +579,12 @@ export interface CrudScopes<TRow extends FormValues = FormValues> {
526
579
  * transform: (values, row) => ({ branchUserId: row.id, permissionKeys: values.permissionKeys ?? [], description: values.description }),
527
580
  * writes: ["assignedPermissionCount"],
528
581
  * }
582
+ * @example
583
+ * {
584
+ * id: "lock", label: "Lock", icon: <Icon name="lock" />, when: run => run.payRunState === PAY_RUN_DRAFT, submit: PAY_RUN_LOCK,
585
+ * transform: (_values, run) => ({ id: run.id, generatedTime: run.generatedTime }), writes: result => result as Partial<PayRun>,
586
+ * confirm: run => ({ title: `Lock ${run.runCode}?`, message: "It is made again first.", details: () => ({ row: run, columns: TOTALS }) }),
587
+ * }
529
588
  */
530
589
  export interface CrudRowForm<TRow extends FormValues = FormValues, TValues extends FormValues = FormValues> {
531
590
  /** Unique among the record's actions. */
@@ -548,8 +607,22 @@ export interface CrudRowForm<TRow extends FormValues = FormValues, TValues exten
548
607
  lock?: (row: TRow) => string | null | undefined;
549
608
  /** What the endpoints are called with in `{id}` (the record the form is about, by its token). Default the record's key. */
550
609
  target?: (row: TRow) => string | number;
551
- /** The form's inputs, or a function of the record and of what `load` read (`null` until it has, and without one); it may answer a promise. */
552
- inputs: readonly InputParams[] | ((row: TRow, loaded: TValues | null) => readonly InputParams[] | Promise<readonly InputParams[]>);
610
+ /**
611
+ * The form's inputs, or a function of the record and of what `load` read (`null` until it has, and without one); it may
612
+ * answer a promise. Without inputs the action is its question alone: no popup, it asks (`confirm`, else "{label}
613
+ * {the record's name}?") and saves on Confirm - the values sent are what `load` read, else the record as the list has
614
+ * it, through `transform`; a refusal shows as the API's message, and nothing is written.
615
+ */
616
+ inputs?: readonly InputParams[] | ((row: TRow, loaded: TValues | null) => readonly InputParams[] | Promise<readonly InputParams[]>);
617
+ /**
618
+ * Asks before it saves, showing what will be saved in full (as a form's `confirm`, `FormConfirmParams`): after every
619
+ * value has passed its checks, Confirm holding the focus, read out with its details, and a hasty second Enter never
620
+ * answering it. Params, or a function of the record and of what `load` read, for a question that names the record:
621
+ * `confirm: run => ({ title: "Lock " + run.runCode + "?", details: () => ({ row: run, columns: TOTALS }), confirmLabel: "Lock" })`.
622
+ * A form with inputs keeps "Confirm and save" and "Back to the form" unless told; a question alone defaults to its
623
+ * `submitLabel` (else its `label`) and "Cancel".
624
+ */
625
+ confirm?: FormConfirmParams<TValues> | ((row: TRow, loaded: TValues | null) => FormConfirmParams<TValues>);
553
626
  /**
554
627
  * Where the form's values are read: an endpoint called with the record's key (`{id}`, as the details are), or a
555
628
  * function. Until they are read the form waits, and a read that fails - an answer that is not ok, or no record - shows
@@ -582,7 +655,7 @@ export type CrudLockableAction = "edit" | "flag" | "trash" | "recover" | "delete
582
655
  /** A record's own locks, by action: the sentence that says why (`rowActionLock`). Nothing, or null, for an action that may run. */
583
656
  export type CrudActionLocks = Partial<Record<CrudLockableAction, string | null | undefined>>;
584
657
  /** Table settings a CRUD passes on to its `DataTable`. */
585
- export type CrudTableOptions<TRow extends FormValues = FormValues> = Pick<DataTableProps<TRow>, "pager" | "pageSize" | "pageSizes" | "defaultSort" | "numbered" | "maxHeight" | "fill" | "excel" | "size" | "searchPlaceholder" | "maxSelection" | "emptyMessage" | "columnsMenu" | "stateKey" | "currency" | "searchDelay" | "filterInputs" | "reorderable" | "resizable" | "pinnable" | "moreActions" | "exportable" | "printable" | "exportName" | "onImport" | "openOnClick">;
658
+ export type CrudTableOptions<TRow extends FormValues = FormValues> = Pick<DataTableProps<TRow>, "pager" | "pageSize" | "pageSizes" | "defaultSort" | "numbered" | "maxHeight" | "fill" | "excel" | "size" | "searchPlaceholder" | "maxSelection" | "emptyMessage" | "columnsMenu" | "stateKey" | "currency" | "searchDelay" | "filterInputs" | "reorderable" | "resizable" | "pinnable" | "moreActions" | "exportable" | "printable" | "exportName" | "onImport" | "openOnClick" | "rowStrong">;
586
659
  /** Props of `Crud`. */
587
660
  export interface CrudProps<TRow extends FormValues = FormValues> {
588
661
  /** The endpoints: `MODULES.controller(NexusModule.Reservation, "Guest", "guest")`, or an object of your own. */