@cosmicdrift/kumiko-renderer 0.387.0 → 0.389.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 (210) hide show
  1. package/dist/app/action-form-body.js +10 -2
  2. package/dist/app/action-form-support.d.ts +1 -0
  3. package/dist/app/action-form-support.js +1 -1
  4. package/dist/app/base-path-context.d.ts +9 -0
  5. package/dist/app/base-path-context.js +13 -0
  6. package/dist/app/base-path.d.ts +3 -0
  7. package/dist/app/base-path.js +27 -0
  8. package/dist/app/config-edit-body.d.ts +1 -1
  9. package/dist/app/config-edit-body.js +19 -6
  10. package/dist/app/config-edit-without-secret-fields.d.ts +2 -0
  11. package/dist/app/config-edit-without-secret-fields.js +28 -0
  12. package/dist/app/entity-edit-prefill.d.ts +11 -0
  13. package/dist/app/entity-edit-prefill.js +9 -4
  14. package/dist/app/entity-list-facet-specs.d.ts +3 -0
  15. package/dist/app/entity-list-facet-specs.js +65 -0
  16. package/dist/app/entity-list-screen.js +74 -206
  17. package/dist/app/extension-sections.d.ts +5 -3
  18. package/dist/app/form-schema.js +1 -1
  19. package/dist/app/header-actions-bar.js +1 -1
  20. package/dist/app/kumiko-screen.js +6 -14
  21. package/dist/app/list-bulk-actions.d.ts +18 -0
  22. package/dist/app/list-bulk-actions.js +54 -0
  23. package/dist/app/locked-screen-header.d.ts +9 -0
  24. package/dist/app/locked-screen-header.js +33 -0
  25. package/dist/app/projection-list-body.js +51 -46
  26. package/dist/app/row-actions.d.ts +3 -0
  27. package/dist/app/row-actions.js +10 -5
  28. package/dist/app/screen-shared.js +9 -2
  29. package/dist/app/use-entity-range-facets.d.ts +24 -0
  30. package/dist/app/use-entity-range-facets.js +64 -0
  31. package/dist/app/use-projection-date-range.d.ts +12 -0
  32. package/dist/app/use-projection-date-range.js +21 -0
  33. package/dist/app/use-screen-list-rows.d.ts +37 -0
  34. package/dist/app/use-screen-list-rows.js +104 -0
  35. package/dist/app/use-screen-list-table.d.ts +24 -0
  36. package/dist/app/use-screen-list-table.js +46 -0
  37. package/dist/components/edit-mounts.d.ts +2 -2
  38. package/dist/components/embedded-list-field.js +1 -1
  39. package/dist/components/list-group-page-bridges.d.ts +3 -0
  40. package/dist/components/list-group-page-bridges.js +31 -0
  41. package/dist/components/list-state-store.d.ts +16 -0
  42. package/dist/components/list-state-store.js +31 -0
  43. package/dist/components/related-list-section.js +51 -90
  44. package/dist/components/render-edit-action-button.d.ts +1 -3
  45. package/dist/components/render-edit-action-button.js +2 -2
  46. package/dist/components/render-edit.js +11 -8
  47. package/dist/components/render-field.js +11 -7
  48. package/dist/components/render-list-toolbar-action.d.ts +27 -0
  49. package/dist/components/render-list-toolbar-action.js +40 -0
  50. package/dist/components/render-list.d.ts +13 -29
  51. package/dist/components/render-list.js +72 -67
  52. package/dist/current-user.d.ts +2 -0
  53. package/dist/data-table-aggregate-types.d.ts +47 -0
  54. package/dist/data-table-aggregate-types.js +1 -0
  55. package/dist/data-table-bulk-types.d.ts +27 -0
  56. package/dist/data-table-bulk-types.js +1 -0
  57. package/dist/data-table-facet-types.d.ts +36 -0
  58. package/dist/data-table-facet-types.js +1 -0
  59. package/dist/data-table-view-types.d.ts +73 -0
  60. package/dist/data-table-view-types.js +1 -0
  61. package/dist/flow-diagram/render.js +9 -1
  62. package/dist/headless/form/form-controller.js +8 -0
  63. package/dist/headless/form/types.d.ts +1 -0
  64. package/dist/headless/format/index.js +89 -64
  65. package/dist/headless/view-model/edit.js +5 -1
  66. package/dist/headless/view-model/list.js +184 -149
  67. package/dist/headless/view-model/types.d.ts +7 -2
  68. package/dist/hooks/list-column-layout.d.ts +19 -0
  69. package/dist/hooks/list-column-layout.js +36 -0
  70. package/dist/hooks/list-group-pages.d.ts +26 -0
  71. package/dist/hooks/list-group-pages.js +37 -0
  72. package/dist/hooks/list-reset.d.ts +8 -0
  73. package/dist/hooks/list-reset.js +15 -0
  74. package/dist/hooks/list-server-groups.d.ts +39 -0
  75. package/dist/hooks/list-server-groups.js +84 -0
  76. package/dist/hooks/list-state-schema.d.ts +28 -0
  77. package/dist/hooks/list-state-schema.js +76 -0
  78. package/dist/hooks/live-refetch-scheduler.js +4 -0
  79. package/dist/hooks/use-bulk-run.d.ts +41 -0
  80. package/dist/hooks/use-bulk-run.js +65 -0
  81. package/dist/hooks/use-group-load-state.d.ts +11 -0
  82. package/dist/hooks/use-group-load-state.js +31 -0
  83. package/dist/hooks/use-list-aggregates.d.ts +38 -0
  84. package/dist/hooks/use-list-aggregates.js +107 -0
  85. package/dist/hooks/use-list-table.d.ts +38 -0
  86. package/dist/hooks/use-list-table.js +91 -0
  87. package/dist/hooks/use-list-view-state.d.ts +43 -0
  88. package/dist/hooks/use-list-view-state.js +161 -0
  89. package/dist/hooks/use-reference-lookup.d.ts +9 -1
  90. package/dist/hooks/use-reference-lookup.js +17 -3
  91. package/dist/hooks/use-related-list-rows.d.ts +27 -0
  92. package/dist/hooks/use-related-list-rows.js +81 -0
  93. package/dist/hooks/use-related-list-state.d.ts +15 -0
  94. package/dist/hooks/use-related-list-state.js +31 -0
  95. package/dist/hooks/use-remembered-list-params.d.ts +13 -0
  96. package/dist/hooks/use-remembered-list-params.js +53 -0
  97. package/dist/hooks/use-remembered-local-list-state.d.ts +26 -0
  98. package/dist/hooks/use-remembered-local-list-state.js +63 -0
  99. package/dist/i18n-defaults.js +59 -2
  100. package/dist/index.d.ts +4 -1
  101. package/dist/index.js +3 -0
  102. package/dist/primitives.d.ts +30 -47
  103. package/dist/web/app/create-app.d.ts +5 -0
  104. package/dist/web/app/create-app.js +15 -7
  105. package/dist/web/app/dashboard-body.js +17 -1
  106. package/dist/web/app/dashboard-panels.js +12 -2
  107. package/dist/web/app/history-leave-guard.d.ts +6 -0
  108. package/dist/web/app/history-leave-guard.js +115 -0
  109. package/dist/web/app/list-state-store-boot.d.ts +6 -0
  110. package/dist/web/app/list-state-store-boot.js +15 -0
  111. package/dist/web/app/nav.js +7 -54
  112. package/dist/web/app/session-list-state-store.d.ts +15 -0
  113. package/dist/web/app/session-list-state-store.js +117 -0
  114. package/dist/web/index.d.ts +2 -1
  115. package/dist/web/index.js +2 -1
  116. package/dist/web/layout/shell-header.js +2 -1
  117. package/dist/web/layout/uniform-bar-controls.d.ts +1 -0
  118. package/dist/web/layout/uniform-bar-controls.js +4 -0
  119. package/dist/web/lib/export-xlsx-request.d.ts +11 -0
  120. package/dist/web/lib/export-xlsx-request.js +46 -0
  121. package/dist/web/lib/file-ref-download.js +4 -1
  122. package/dist/web/primitives/action-menu.js +1 -1
  123. package/dist/web/primitives/code-input.d.ts +6 -0
  124. package/dist/web/primitives/code-input.js +31 -0
  125. package/dist/web/primitives/data-table/actions-column.d.ts +16 -0
  126. package/dist/web/primitives/data-table/actions-column.js +34 -0
  127. package/dist/web/primitives/data-table/bulk-selection-bar.d.ts +20 -0
  128. package/dist/web/primitives/data-table/bulk-selection-bar.js +37 -0
  129. package/dist/web/primitives/data-table/card-sort-select.d.ts +14 -0
  130. package/dist/web/primitives/data-table/card-sort-select.js +40 -0
  131. package/dist/web/primitives/data-table/column-layout-mode.d.ts +5 -0
  132. package/dist/web/primitives/data-table/column-layout-mode.js +10 -0
  133. package/dist/web/primitives/data-table/column-picker-section.d.ts +7 -0
  134. package/dist/web/primitives/data-table/column-picker-section.js +35 -0
  135. package/dist/web/primitives/data-table/column-resize-handle.d.ts +14 -0
  136. package/dist/web/primitives/data-table/column-resize-handle.js +46 -0
  137. package/dist/web/primitives/data-table/density.d.ts +14 -0
  138. package/dist/web/primitives/data-table/density.js +20 -0
  139. package/dist/web/primitives/data-table/export-button.d.ts +5 -0
  140. package/dist/web/primitives/data-table/export-button.js +37 -0
  141. package/dist/web/primitives/data-table/facet-cluster.d.ts +8 -0
  142. package/dist/web/primitives/data-table/facet-cluster.js +37 -0
  143. package/dist/web/primitives/data-table/group-band.d.ts +28 -0
  144. package/dist/web/primitives/data-table/group-band.js +53 -0
  145. package/dist/web/primitives/data-table/measure-value.d.ts +10 -0
  146. package/dist/web/primitives/data-table/measure-value.js +28 -0
  147. package/dist/web/primitives/data-table/pin-section.d.ts +8 -0
  148. package/dist/web/primitives/data-table/pin-section.js +20 -0
  149. package/dist/web/primitives/data-table/reset-all-button.d.ts +4 -0
  150. package/dist/web/primitives/data-table/reset-all-button.js +8 -0
  151. package/dist/web/primitives/data-table/server-group-header.d.ts +11 -0
  152. package/dist/web/primitives/data-table/server-group-header.js +51 -0
  153. package/dist/web/primitives/data-table/sortable-header.d.ts +17 -0
  154. package/dist/web/primitives/data-table/sortable-header.js +41 -0
  155. package/dist/web/primitives/data-table/table-chrome.d.ts +13 -0
  156. package/dist/web/primitives/data-table/table-chrome.js +30 -0
  157. package/dist/web/primitives/data-table/table-footer.d.ts +7 -0
  158. package/dist/web/primitives/data-table/table-footer.js +20 -0
  159. package/dist/web/primitives/data-table/toolbar-end-cluster.d.ts +10 -0
  160. package/dist/web/primitives/data-table/toolbar-end-cluster.js +13 -0
  161. package/dist/web/primitives/data-table/totals-footer.d.ts +24 -0
  162. package/dist/web/primitives/data-table/totals-footer.js +48 -0
  163. package/dist/web/primitives/data-table/use-bulk-selection.d.ts +21 -0
  164. package/dist/web/primitives/data-table/use-bulk-selection.js +66 -0
  165. package/dist/web/primitives/data-table/use-column-pins.d.ts +32 -0
  166. package/dist/web/primitives/data-table/use-column-pins.js +117 -0
  167. package/dist/web/primitives/data-table/use-column-sizing.d.ts +22 -0
  168. package/dist/web/primitives/data-table/use-column-sizing.js +44 -0
  169. package/dist/web/primitives/data-table/use-measured-column-widths.d.ts +15 -0
  170. package/dist/web/primitives/data-table/use-measured-column-widths.js +83 -0
  171. package/dist/web/primitives/data-table/use-row-keyboard-navigation.d.ts +22 -0
  172. package/dist/web/primitives/data-table/use-row-keyboard-navigation.js +65 -0
  173. package/dist/web/primitives/data-table/use-row-selection.d.ts +14 -0
  174. package/dist/web/primitives/data-table/use-row-selection.js +48 -0
  175. package/dist/web/primitives/data-table/use-view-columns.d.ts +6 -0
  176. package/dist/web/primitives/data-table/use-view-columns.js +17 -0
  177. package/dist/web/primitives/data-table/view-menu.d.ts +8 -0
  178. package/dist/web/primitives/data-table/view-menu.js +45 -0
  179. package/dist/web/primitives/data-table-cell.d.ts +0 -22
  180. package/dist/web/primitives/data-table-cell.js +1 -58
  181. package/dist/web/primitives/data-table.d.ts +1 -1
  182. package/dist/web/primitives/data-table.js +125 -146
  183. package/dist/web/primitives/facet-filter.js +1 -1
  184. package/dist/web/primitives/file-thumbnail.d.ts +3 -0
  185. package/dist/web/primitives/file-thumbnail.js +5 -0
  186. package/dist/web/primitives/file-value-list.d.ts +1 -1
  187. package/dist/web/primitives/file-value-list.js +5 -5
  188. package/dist/web/primitives/input.js +6 -1
  189. package/dist/web/primitives/layout-elements.js +6 -4
  190. package/dist/web/primitives/number-range-facet.d.ts +7 -0
  191. package/dist/web/primitives/number-range-facet.js +33 -0
  192. package/dist/web/primitives/page-header.js +3 -1
  193. package/dist/web/primitives/row-actions.js +3 -3
  194. package/dist/web/primitives/use-focus-on-mount.d.ts +2 -0
  195. package/dist/web/primitives/use-focus-on-mount.js +9 -0
  196. package/dist/web/widgets/bounded-load-cache.d.ts +5 -1
  197. package/dist/web/widgets/bounded-load-cache.js +49 -21
  198. package/dist/web/widgets/choice-cards.d.ts +17 -0
  199. package/dist/web/widgets/choice-cards.js +12 -0
  200. package/dist/web/widgets/collapsible-section.js +1 -1
  201. package/dist/web/widgets/detail-list.d.ts +5 -1
  202. package/dist/web/widgets/detail-list.js +5 -1
  203. package/dist/web/widgets/index.d.ts +1 -0
  204. package/dist/web/widgets/index.js +1 -0
  205. package/dist/web/widgets/pdf-preview.js +10 -5
  206. package/dist/web/widgets/plan-card.d.ts +2 -0
  207. package/dist/web/widgets/plan-card.js +1 -1
  208. package/dist/web/widgets/upload-zone.js +13 -5
  209. package/package.json +5 -5
  210. package/src/changes.json +158 -0
@@ -0,0 +1,36 @@
1
+ export const DEFAULT_COLUMN_WIDTH_PX = 160;
2
+ export const DEFAULT_MIN_COLUMN_WIDTH_PX = 64;
3
+ /** Upper bound so a tampered stored width cannot blow the table up. */
4
+ export const MAX_COLUMN_WIDTH_PX = 2000;
5
+ export function defaultPins(schema) {
6
+ const pins = {};
7
+ for (const column of schema) {
8
+ if (column.pinned !== undefined)
9
+ pins[column.field] = column.pinned;
10
+ }
11
+ return pins;
12
+ }
13
+ export function minWidthsOf(schema) {
14
+ return Object.fromEntries(schema.map((column) => [column.field, column.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH_PX]));
15
+ }
16
+ export function clampWidth(width, min) {
17
+ return Math.min(MAX_COLUMN_WIDTH_PX, Math.max(min, Math.round(width)));
18
+ }
19
+ export function defaultWidths(schema, minWidths) {
20
+ return Object.fromEntries(schema.map((column) => [
21
+ column.field,
22
+ clampWidth(column.width ?? DEFAULT_COLUMN_WIDTH_PX, minWidths[column.field] ?? DEFAULT_MIN_COLUMN_WIDTH_PX),
23
+ ]));
24
+ }
25
+ /** A saved width wins per field; a column the saved view has never seen keeps its default. */
26
+ export function mergeSavedWidths(defaults, saved, minWidths) {
27
+ return Object.fromEntries(Object.entries(defaults).map(([field, fallback]) => {
28
+ const savedWidth = saved[field];
29
+ const min = minWidths[field] ?? DEFAULT_MIN_COLUMN_WIDTH_PX;
30
+ return [field, savedWidth === undefined ? fallback : clampWidth(savedWidth, min)];
31
+ }));
32
+ }
33
+ export function sameRecord(a, b) {
34
+ const keys = Object.keys(a);
35
+ return keys.length === Object.keys(b).length && keys.every((key) => a[key] === b[key]);
36
+ }
@@ -0,0 +1,26 @@
1
+ import type { AggregateKey } from "@cosmicdrift/kumiko-types/aggregate-types";
2
+ /** How a group pages through its rows: offset for `pagination: "pages"`, cursor for `"infinite"`. */
3
+ export type ListGroupPaging = "offset" | "cursor";
4
+ export type ListGroupPageResult = {
5
+ readonly rows: readonly Readonly<Record<string, unknown>>[];
6
+ readonly nextCursor: string | null;
7
+ readonly loading: boolean;
8
+ };
9
+ export type ListGroupPageRequest = {
10
+ readonly pageKey: string;
11
+ readonly payload: Readonly<Record<string, unknown>>;
12
+ };
13
+ /** The list payload as the aggregate sees it: filters, search and screen filter, no paging. */
14
+ export declare function withoutListPaging(payload: Readonly<Record<string, unknown>>): Record<string, unknown>;
15
+ export declare function groupPageKey(groupKey: string, pageIndex: number): string;
16
+ export declare function groupPagePayload(args: {
17
+ readonly base: Readonly<Record<string, unknown>>;
18
+ readonly groupField: string;
19
+ readonly groupValue: AggregateKey;
20
+ readonly pageSize: number;
21
+ readonly pageIndex: number;
22
+ readonly paging: ListGroupPaging;
23
+ readonly cursor: string | undefined;
24
+ }): Record<string, unknown>;
25
+ /** Loaded pages of one group in order, up to the first page that has not answered yet. */
26
+ export declare function loadedGroupPages(results: ReadonlyMap<string, ListGroupPageResult>, groupKey: string, requestedPages: number): ListGroupPageResult[];
@@ -0,0 +1,37 @@
1
+ const PAGING_KEYS = ["limit", "offset", "cursor", "totalCount"];
2
+ /** The list payload as the aggregate sees it: filters, search and screen filter, no paging. */
3
+ export function withoutListPaging(payload) {
4
+ const kept = { ...payload };
5
+ for (const key of PAGING_KEYS)
6
+ delete kept[key];
7
+ return kept;
8
+ }
9
+ export function groupPageKey(groupKey, pageIndex) {
10
+ return `${groupKey}#${pageIndex}`;
11
+ }
12
+ function existingFilters(base) {
13
+ const filters = base["filters"];
14
+ return Array.isArray(filters) ? filters : [];
15
+ }
16
+ export function groupPagePayload(args) {
17
+ const { base, groupField, groupValue, pageSize, pageIndex, paging, cursor } = args;
18
+ const offset = pageIndex * pageSize;
19
+ return {
20
+ ...base,
21
+ filters: [...existingFilters(base), { field: groupField, op: "eq", value: groupValue }],
22
+ limit: pageSize,
23
+ ...(paging === "offset" && offset > 0 && { offset }),
24
+ ...(paging === "cursor" && cursor !== undefined && { cursor }),
25
+ };
26
+ }
27
+ /** Loaded pages of one group in order, up to the first page that has not answered yet. */
28
+ export function loadedGroupPages(results, groupKey, requestedPages) {
29
+ const pages = [];
30
+ for (let pageIndex = 0; pageIndex < requestedPages; pageIndex++) {
31
+ const page = results.get(groupPageKey(groupKey, pageIndex));
32
+ if (page === undefined)
33
+ break;
34
+ pages.push(page);
35
+ }
36
+ return pages;
37
+ }
@@ -0,0 +1,8 @@
1
+ type UrlParams = Readonly<Record<string, string | undefined>>;
2
+ /** Exact `${screenId}.` prefix: `orders` must not match `orders-archive.q`. */
3
+ export declare function listParamKeys(params: UrlParams, screenId: string): string[];
4
+ /** The page alone is navigation, not a deviation from the default view. */
5
+ export declare function isListStateAtDefault(params: UrlParams, screenId: string): boolean;
6
+ /** Nulling every list key lets defaultSort/defaultFilters apply again, and drops the `~` "no filter" markers. */
7
+ export declare function listResetUpdates(params: UrlParams, screenId: string): Record<string, null>;
8
+ export {};
@@ -0,0 +1,15 @@
1
+ const PAGE_SUFFIX = "page";
2
+ /** Exact `${screenId}.` prefix: `orders` must not match `orders-archive.q`. */
3
+ export function listParamKeys(params, screenId) {
4
+ const prefix = `${screenId}.`;
5
+ return Object.keys(params).filter((key) => key.startsWith(prefix));
6
+ }
7
+ /** The page alone is navigation, not a deviation from the default view. */
8
+ export function isListStateAtDefault(params, screenId) {
9
+ const pageKey = `${screenId}.${PAGE_SUFFIX}`;
10
+ return listParamKeys(params, screenId).every((key) => key === pageKey);
11
+ }
12
+ /** Nulling every list key lets defaultSort/defaultFilters apply again, and drops the `~` "no filter" markers. */
13
+ export function listResetUpdates(params, screenId) {
14
+ return Object.fromEntries(listParamKeys(params, screenId).map((key) => [key, null]));
15
+ }
@@ -0,0 +1,39 @@
1
+ import type { ListAggregateGroup } from "@cosmicdrift/kumiko-types/list-aggregate-types";
2
+ import { type ListTableGrouping } from "@cosmicdrift/kumiko-types/screen";
3
+ import type { DataTableMeasure, DataTableServerGroup } from "../data-table-aggregate-types.ts";
4
+ import type { Translate } from "../headless/index.ts";
5
+ import { type ListGroupPageRequest, type ListGroupPageResult, type ListGroupPaging } from "./list-group-pages.ts";
6
+ export type GroupLoadState = {
7
+ readonly resetKey: string;
8
+ readonly toggled: ReadonlySet<string>;
9
+ readonly visible: ReadonlySet<string>;
10
+ readonly requestedPages: ReadonlyMap<string, number>;
11
+ readonly results: ReadonlyMap<string, ListGroupPageResult>;
12
+ };
13
+ export type MeasureSpec = {
14
+ readonly field: string;
15
+ readonly kind: DataTableMeasure["kind"];
16
+ };
17
+ type Row = Readonly<Record<string, unknown>>;
18
+ export declare function freshLoadState(resetKey: string): GroupLoadState;
19
+ export declare function measuresOf(totals: readonly MeasureSpec[], group: ListAggregateGroup | undefined): DataTableMeasure[];
20
+ type ServerGroupsInput = {
21
+ readonly answer: readonly ListAggregateGroup[];
22
+ readonly groupField: string;
23
+ /** The declared grouping, only when the active field is the declared one. */
24
+ readonly declared: ListTableGrouping | undefined;
25
+ readonly state: GroupLoadState;
26
+ readonly base: Readonly<Record<string, unknown>>;
27
+ readonly pageSize: number;
28
+ readonly paging: ListGroupPaging;
29
+ readonly measures: readonly MeasureSpec[];
30
+ readonly translate: Translate;
31
+ };
32
+ export type ServerGroupsBuild = {
33
+ readonly groups: DataTableServerGroup[];
34
+ readonly rows: Row[];
35
+ readonly requests: ListGroupPageRequest[];
36
+ };
37
+ /** Header entries, loaded rows and the page requests of the open, visible groups. */
38
+ export declare function buildServerGroups(input: ServerGroupsInput): ServerGroupsBuild;
39
+ export {};
@@ -0,0 +1,84 @@
1
+ import { relatedListGroupHeaderLabel, relatedListGroupKey, } from "@cosmicdrift/kumiko-types/screen";
2
+ import { groupPageKey, groupPagePayload, loadedGroupPages, } from "./list-group-pages.js";
3
+ export function freshLoadState(resetKey) {
4
+ return {
5
+ resetKey,
6
+ toggled: new Set(),
7
+ visible: new Set(),
8
+ requestedPages: new Map(),
9
+ results: new Map(),
10
+ };
11
+ }
12
+ export function measuresOf(totals, group) {
13
+ return totals.map((total, index) => ({
14
+ field: total.field,
15
+ kind: total.kind,
16
+ value: group?.measures[index] ?? null,
17
+ }));
18
+ }
19
+ function startsCollapsed(declared, key) {
20
+ return (declared?.collapsedWhen !== undefined && key === relatedListGroupKey(declared.collapsedWhen));
21
+ }
22
+ function headerLabel(input, key, group) {
23
+ const serverLabel = group.label ?? undefined;
24
+ const labelKey = input.declared === undefined ? undefined : relatedListGroupHeaderLabel(input.declared, key);
25
+ if (labelKey === undefined)
26
+ return serverLabel;
27
+ return input.translate(labelKey, { count: group.count, value: serverLabel ?? key });
28
+ }
29
+ function pageRequests(input, group, key, pages, requestedPages) {
30
+ const requests = [];
31
+ for (let pageIndex = 0; pageIndex < requestedPages; pageIndex++) {
32
+ const cursor = pageIndex === 0 ? undefined : (pages[pageIndex - 1]?.nextCursor ?? undefined);
33
+ // skip: a cursor page waits for the page before it
34
+ if (input.paging === "cursor" && pageIndex > 0 && cursor === undefined)
35
+ break;
36
+ requests.push({
37
+ pageKey: groupPageKey(key, pageIndex),
38
+ payload: groupPagePayload({
39
+ base: input.base,
40
+ groupField: input.groupField,
41
+ groupValue: group.key,
42
+ pageSize: input.pageSize,
43
+ pageIndex,
44
+ paging: input.paging,
45
+ cursor,
46
+ }),
47
+ });
48
+ }
49
+ return requests;
50
+ }
51
+ function hasMoreRows(paging, loaded, count, lastPage) {
52
+ return loaded < count && (paging === "offset" || lastPage?.nextCursor !== null);
53
+ }
54
+ function buildGroup(input, group, into) {
55
+ const { state } = input;
56
+ const key = relatedListGroupKey(group.key);
57
+ const collapsed = startsCollapsed(input.declared, key) !== state.toggled.has(key);
58
+ const requestedPages = state.requestedPages.get(key) ?? 1;
59
+ const pages = collapsed ? [] : loadedGroupPages(state.results, key, requestedPages);
60
+ const loadsRows = !collapsed && state.visible.has(key);
61
+ if (loadsRows)
62
+ into.requests.push(...pageRequests(input, group, key, pages, requestedPages));
63
+ const groupRows = pages.flatMap((page) => page.rows);
64
+ into.rows.push(...groupRows);
65
+ const label = headerLabel(input, key, group);
66
+ into.groups.push({
67
+ key,
68
+ value: group.key,
69
+ ...(label !== undefined && { label }),
70
+ count: group.count,
71
+ measures: measuresOf(input.measures, group),
72
+ collapsed,
73
+ rowCount: groupRows.length,
74
+ hasMore: hasMoreRows(input.paging, groupRows.length, group.count, pages[pages.length - 1]),
75
+ loading: loadsRows && (pages.length < requestedPages || pages.some((page) => page.loading)),
76
+ });
77
+ }
78
+ /** Header entries, loaded rows and the page requests of the open, visible groups. */
79
+ export function buildServerGroups(input) {
80
+ const build = { groups: [], rows: [], requests: [] };
81
+ for (const group of input.answer)
82
+ buildGroup(input, group, build);
83
+ return build;
84
+ }
@@ -0,0 +1,28 @@
1
+ import { type ListColumnPin, type ListTableDensity } from "@cosmicdrift/kumiko-types/screen";
2
+ export type StoredListColumn = {
3
+ readonly field: string;
4
+ readonly visible: boolean;
5
+ };
6
+ /** Remembered state of one list. `params` holds the list's URL parameters
7
+ * without the screen-id prefix (`sort`, `dir`, `q`, `page`, `size`, `f.<field>`). */
8
+ export type StoredListState = {
9
+ readonly params?: Readonly<Record<string, string>>;
10
+ readonly view?: {
11
+ readonly columns?: readonly StoredListColumn[];
12
+ readonly density?: ListTableDensity;
13
+ readonly pins?: Readonly<Record<string, ListColumnPin>>;
14
+ readonly widths?: Readonly<Record<string, number>>;
15
+ /** Chosen group field, "" = the user turned grouping off. */
16
+ readonly groupBy?: string;
17
+ };
18
+ };
19
+ /** Storage boundary: anything that is not a well-formed state reads as "nothing remembered". */
20
+ export declare function parseStoredListState(raw: unknown): StoredListState | undefined;
21
+ export declare function parseStoredListStateJson(text: string): StoredListState | undefined;
22
+ export type KnownListFields = {
23
+ readonly columnFields: readonly string[];
24
+ readonly filterFields: readonly string[];
25
+ };
26
+ /** Drops what the screen no longer has (renamed field, removed column) so an old
27
+ * session never feeds dead keys into the URL or the column picker. */
28
+ export declare function sanitizeStoredListState(raw: unknown, known: KnownListFields): StoredListState | undefined;
@@ -0,0 +1,76 @@
1
+ import { LIST_COLUMN_PINS, LIST_TABLE_DENSITIES, } from "@cosmicdrift/kumiko-types/screen";
2
+ import * as z from "zod";
3
+ const storedListStateSchema = z.object({
4
+ params: z.record(z.string(), z.string()).optional(),
5
+ view: z
6
+ .object({
7
+ columns: z.array(z.object({ field: z.string(), visible: z.boolean() })).optional(),
8
+ density: z.enum(LIST_TABLE_DENSITIES).optional(),
9
+ pins: z.record(z.string(), z.enum(LIST_COLUMN_PINS)).optional(),
10
+ widths: z.record(z.string(), z.number().finite().positive()).optional(),
11
+ groupBy: z.string().optional(),
12
+ })
13
+ .optional(),
14
+ });
15
+ /** Storage boundary: anything that is not a well-formed state reads as "nothing remembered". */
16
+ export function parseStoredListState(raw) {
17
+ const parsed = storedListStateSchema.safeParse(raw);
18
+ return parsed.success ? parsed.data : undefined;
19
+ }
20
+ export function parseStoredListStateJson(text) {
21
+ try {
22
+ return parseStoredListState(JSON.parse(text));
23
+ }
24
+ catch {
25
+ return undefined;
26
+ }
27
+ }
28
+ const PLAIN_PARAM_SUFFIXES = new Set(["sort", "dir", "q", "page", "size"]);
29
+ const FILTER_PREFIX = "f.";
30
+ const RANGE_BOUND_SUFFIXES = [".from", ".to", ".min", ".max"];
31
+ function isKnownParamSuffix(suffix, filterFields) {
32
+ if (PLAIN_PARAM_SUFFIXES.has(suffix))
33
+ return true;
34
+ if (!suffix.startsWith(FILTER_PREFIX))
35
+ return false;
36
+ const filterKey = suffix.slice(FILTER_PREFIX.length);
37
+ if (filterFields.has(filterKey))
38
+ return true;
39
+ return RANGE_BOUND_SUFFIXES.some((bound) => filterKey.endsWith(bound) && filterFields.has(filterKey.slice(0, -bound.length)));
40
+ }
41
+ function onKnownFields(record, known) {
42
+ if (record === undefined)
43
+ return undefined;
44
+ return Object.fromEntries(Object.entries(record).filter(([field]) => known.has(field)));
45
+ }
46
+ /** Drops what the screen no longer has (renamed field, removed column) so an old
47
+ * session never feeds dead keys into the URL or the column picker. */
48
+ export function sanitizeStoredListState(raw, known) {
49
+ const state = parseStoredListState(raw);
50
+ if (state === undefined)
51
+ return undefined;
52
+ const filterFields = new Set(known.filterFields);
53
+ const columnFields = new Set(known.columnFields);
54
+ const params = state.params === undefined
55
+ ? undefined
56
+ : Object.fromEntries(Object.entries(state.params).filter(([suffix]) => isKnownParamSuffix(suffix, filterFields)));
57
+ const columns = state.view?.columns?.filter((column) => columnFields.has(column.field));
58
+ const density = state.view?.density;
59
+ const pins = onKnownFields(state.view?.pins, columnFields);
60
+ const widths = onKnownFields(state.view?.widths, columnFields);
61
+ const savedGroupBy = state.view?.groupBy;
62
+ const groupBy = savedGroupBy === "" || (savedGroupBy !== undefined && columnFields.has(savedGroupBy))
63
+ ? savedGroupBy
64
+ : undefined;
65
+ const view = {
66
+ ...(columns !== undefined && { columns }),
67
+ ...(density !== undefined && { density }),
68
+ ...(pins !== undefined && { pins }),
69
+ ...(widths !== undefined && { widths }),
70
+ ...(groupBy !== undefined && { groupBy }),
71
+ };
72
+ return {
73
+ ...(params !== undefined && { params }),
74
+ ...(state.view !== undefined && { view }),
75
+ };
76
+ }
@@ -12,6 +12,10 @@ export function createLiveRefetchScheduler(run, windowMs = LIVE_REFETCH_COALESCE
12
12
  try {
13
13
  await run();
14
14
  }
15
+ catch {
16
+ // The caller surfaces its own load errors in state; a failed run must
17
+ // neither become an unhandled rejection nor drop the queued follow-up.
18
+ }
15
19
  finally {
16
20
  inFlight = false;
17
21
  }
@@ -0,0 +1,41 @@
1
+ import type { DataTableBulkAction } from "../data-table-bulk-types.ts";
2
+ import type { ListRowViewModel } from "../headless/index.ts";
3
+ /** Parallel row writes of one run; more would crowd out the list's own requests. */
4
+ export declare const BULK_WRITE_CONCURRENCY = 4;
5
+ export type BulkRunResult = {
6
+ readonly succeededIds: readonly string[];
7
+ readonly failedIds: readonly string[];
8
+ readonly skippedIds: readonly string[];
9
+ /** Rows a cancel kept from starting. */
10
+ readonly notStartedIds: readonly string[];
11
+ readonly firstFailureMessage?: string;
12
+ };
13
+ type BulkRunAction = Pick<DataTableBulkAction, "appliesTo" | "writeRow">;
14
+ /** One write per row, at most `concurrency` at a time. Cancel stops new writes; running ones finish. */
15
+ export declare function runBulkWrites({ rows, action, isCancelled, onRowSettled, concurrency, }: {
16
+ readonly rows: readonly ListRowViewModel[];
17
+ readonly action: BulkRunAction;
18
+ readonly isCancelled: () => boolean;
19
+ readonly onRowSettled?: () => void;
20
+ readonly concurrency?: number;
21
+ }): Promise<BulkRunResult>;
22
+ export type BulkRunState = {
23
+ readonly phase: "idle";
24
+ } | {
25
+ readonly phase: "running";
26
+ readonly total: number;
27
+ readonly settled: number;
28
+ readonly cancelRequested: boolean;
29
+ } | {
30
+ readonly phase: "finished";
31
+ readonly result: BulkRunResult;
32
+ };
33
+ export type BulkRun = {
34
+ readonly state: BulkRunState;
35
+ readonly start: (action: BulkRunAction, rows: readonly ListRowViewModel[]) => Promise<BulkRunResult>;
36
+ readonly cancel: () => void;
37
+ readonly dismissResult: () => void;
38
+ };
39
+ /** Execution model of a bulk run: progress counter, cancel and the final tally. */
40
+ export declare function useBulkRun(): BulkRun;
41
+ export {};
@@ -0,0 +1,65 @@
1
+ import { useCallback, useRef, useState } from "react";
2
+ /** Parallel row writes of one run; more would crowd out the list's own requests. */
3
+ export const BULK_WRITE_CONCURRENCY = 4;
4
+ function failureOf(error) {
5
+ return { kind: "failed", message: error instanceof Error ? error.message : String(error) };
6
+ }
7
+ /** One write per row, at most `concurrency` at a time. Cancel stops new writes; running ones finish. */
8
+ export async function runBulkWrites({ rows, action, isCancelled, onRowSettled, concurrency = BULK_WRITE_CONCURRENCY, }) {
9
+ const applicable = rows.filter((row) => action.appliesTo(row));
10
+ const skippedIds = rows.filter((row) => !action.appliesTo(row)).map((row) => row.id);
11
+ const succeededIds = [];
12
+ const failedIds = [];
13
+ let firstFailureMessage;
14
+ let nextIndex = 0;
15
+ async function drainQueue() {
16
+ while (nextIndex < applicable.length && !isCancelled()) {
17
+ const row = applicable[nextIndex];
18
+ nextIndex += 1;
19
+ if (row === undefined)
20
+ continue;
21
+ const outcome = await action.writeRow(row).catch(failureOf);
22
+ if (outcome.kind === "succeeded") {
23
+ succeededIds.push(row.id);
24
+ }
25
+ else {
26
+ failedIds.push(row.id);
27
+ firstFailureMessage ??= outcome.message;
28
+ }
29
+ onRowSettled?.();
30
+ }
31
+ }
32
+ const workerCount = Math.min(concurrency, applicable.length);
33
+ await Promise.all(Array.from({ length: workerCount }, drainQueue));
34
+ return {
35
+ succeededIds,
36
+ failedIds,
37
+ skippedIds,
38
+ notStartedIds: applicable.slice(nextIndex).map((row) => row.id),
39
+ ...(firstFailureMessage !== undefined && { firstFailureMessage }),
40
+ };
41
+ }
42
+ /** Execution model of a bulk run: progress counter, cancel and the final tally. */
43
+ export function useBulkRun() {
44
+ const [state, setState] = useState({ phase: "idle" });
45
+ const cancelledRef = useRef(false);
46
+ const start = useCallback(async (action, rows) => {
47
+ cancelledRef.current = false;
48
+ const total = rows.filter((row) => action.appliesTo(row)).length;
49
+ setState({ phase: "running", total, settled: 0, cancelRequested: false });
50
+ const result = await runBulkWrites({
51
+ rows,
52
+ action,
53
+ isCancelled: () => cancelledRef.current,
54
+ onRowSettled: () => setState((prev) => prev.phase === "running" ? { ...prev, settled: prev.settled + 1 } : prev),
55
+ });
56
+ setState({ phase: "finished", result });
57
+ return result;
58
+ }, []);
59
+ const cancel = useCallback(() => {
60
+ cancelledRef.current = true;
61
+ setState((prev) => (prev.phase === "running" ? { ...prev, cancelRequested: true } : prev));
62
+ }, []);
63
+ const dismissResult = useCallback(() => setState({ phase: "idle" }), []);
64
+ return { state, start, cancel, dismissResult };
65
+ }
@@ -0,0 +1,11 @@
1
+ import type { ListGroupPageResult } from "./list-group-pages.ts";
2
+ import { type GroupLoadState } from "./list-server-groups.ts";
3
+ export type GroupLoadControls = {
4
+ readonly state: GroupLoadState;
5
+ readonly onToggle: (key: string) => void;
6
+ readonly onHeaderVisible: (key: string) => void;
7
+ readonly onLoadMore: (key: string) => void;
8
+ readonly onResult: (pageKey: string, result: ListGroupPageResult) => void;
9
+ };
10
+ /** Open/visible/paged state of the server groups; starts over when `resetKey` changes. */
11
+ export declare function useGroupLoadState(resetKey: string): GroupLoadControls;
@@ -0,0 +1,31 @@
1
+ import { useCallback, useState } from "react";
2
+ import { freshLoadState } from "./list-server-groups.js";
3
+ function withToggled(set, key) {
4
+ const next = new Set(set);
5
+ if (!next.delete(key))
6
+ next.add(key);
7
+ return next;
8
+ }
9
+ /** Open/visible/paged state of the server groups; starts over when `resetKey` changes. */
10
+ export function useGroupLoadState(resetKey) {
11
+ const [loadState, setLoadState] = useState(() => freshLoadState(resetKey));
12
+ const state = loadState.resetKey === resetKey ? loadState : freshLoadState(resetKey);
13
+ if (loadState.resetKey !== resetKey)
14
+ setLoadState(state);
15
+ const update = useCallback((change) => setLoadState((current) => (current.resetKey === resetKey ? change(current) : current)), [resetKey]);
16
+ const onToggle = useCallback((key) => update((current) => ({ ...current, toggled: withToggled(current.toggled, key) })), [update]);
17
+ const onHeaderVisible = useCallback((key) => update((current) => current.visible.has(key)
18
+ ? current
19
+ : { ...current, visible: new Set([...current.visible, key]) }), [update]);
20
+ const onLoadMore = useCallback((key) => update((current) => {
21
+ const requestedPages = new Map(current.requestedPages);
22
+ requestedPages.set(key, (current.requestedPages.get(key) ?? 1) + 1);
23
+ return { ...current, requestedPages };
24
+ }), [update]);
25
+ const onResult = useCallback((pageKey, result) => update((current) => {
26
+ const results = new Map(current.results);
27
+ results.set(pageKey, result);
28
+ return { ...current, results };
29
+ }), [update]);
30
+ return { state, onToggle, onHeaderVisible, onLoadMore, onResult };
31
+ }
@@ -0,0 +1,38 @@
1
+ import type { ListTableOptions } from "@cosmicdrift/kumiko-types/screen";
2
+ import type { DataTableListNotice, DataTableServerGroups, DataTableTotals } from "../data-table-aggregate-types.ts";
3
+ import type { DispatcherError, Translate } from "../headless/index.ts";
4
+ import { type ListGroupPageRequest, type ListGroupPageResult, type ListGroupPaging } from "./list-group-pages.ts";
5
+ type ListAggregatesArgs = {
6
+ readonly queryType: string;
7
+ /** The list payload as the ungrouped list would send it; paging keys are dropped. */
8
+ readonly payload: Readonly<Record<string, unknown>>;
9
+ readonly table: ListTableOptions | undefined;
10
+ readonly groupField: string | undefined;
11
+ readonly pageSize: number;
12
+ readonly paging: ListGroupPaging;
13
+ readonly translate: Translate;
14
+ };
15
+ export type ListGroupPagesProps = {
16
+ readonly queryType: string;
17
+ readonly requests: readonly ListGroupPageRequest[];
18
+ readonly onResult: (pageKey: string, result: ListGroupPageResult) => void;
19
+ readonly refreshNonce: number;
20
+ };
21
+ export type ListAggregates = {
22
+ readonly grouped: boolean;
23
+ /** Loaded rows of the open groups, in group order. */
24
+ readonly rows: readonly Readonly<Record<string, unknown>>[];
25
+ readonly serverGroups?: DataTableServerGroups;
26
+ readonly totals?: DataTableTotals;
27
+ readonly notice?: DataTableListNotice;
28
+ readonly groupPages: ListGroupPagesProps;
29
+ /** Reloads groups, totals and the loaded group pages (after a write). */
30
+ readonly refresh: () => void;
31
+ /** True until the first group answer is in. */
32
+ readonly loading: boolean;
33
+ readonly error: DispatcherError | null;
34
+ };
35
+ /** Server-side grouping and totals of a list: groups are the load unit, each open
36
+ * and visible group pages through its own rows; nothing is summed on the client. */
37
+ export declare function useListAggregates({ queryType, payload, table, groupField, pageSize, paging, translate, }: ListAggregatesArgs): ListAggregates;
38
+ export {};