@cosmicdrift/kumiko-renderer 0.388.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 (161) hide show
  1. package/dist/app/base-path-context.d.ts +9 -0
  2. package/dist/app/base-path-context.js +13 -0
  3. package/dist/app/base-path.d.ts +3 -0
  4. package/dist/app/base-path.js +27 -0
  5. package/dist/app/config-edit-body.d.ts +1 -1
  6. package/dist/app/config-edit-body.js +15 -6
  7. package/dist/app/config-edit-without-secret-fields.d.ts +2 -0
  8. package/dist/app/config-edit-without-secret-fields.js +28 -0
  9. package/dist/app/entity-list-facet-specs.d.ts +3 -0
  10. package/dist/app/entity-list-facet-specs.js +65 -0
  11. package/dist/app/entity-list-screen.js +74 -206
  12. package/dist/app/kumiko-screen.js +6 -14
  13. package/dist/app/list-bulk-actions.d.ts +18 -0
  14. package/dist/app/list-bulk-actions.js +54 -0
  15. package/dist/app/locked-screen-header.d.ts +9 -0
  16. package/dist/app/locked-screen-header.js +33 -0
  17. package/dist/app/projection-list-body.js +51 -46
  18. package/dist/app/row-actions.d.ts +3 -0
  19. package/dist/app/row-actions.js +10 -5
  20. package/dist/app/screen-shared.js +9 -2
  21. package/dist/app/use-entity-range-facets.d.ts +24 -0
  22. package/dist/app/use-entity-range-facets.js +64 -0
  23. package/dist/app/use-projection-date-range.d.ts +12 -0
  24. package/dist/app/use-projection-date-range.js +21 -0
  25. package/dist/app/use-screen-list-rows.d.ts +37 -0
  26. package/dist/app/use-screen-list-rows.js +104 -0
  27. package/dist/app/use-screen-list-table.d.ts +24 -0
  28. package/dist/app/use-screen-list-table.js +46 -0
  29. package/dist/components/list-group-page-bridges.d.ts +3 -0
  30. package/dist/components/list-group-page-bridges.js +31 -0
  31. package/dist/components/list-state-store.d.ts +16 -0
  32. package/dist/components/list-state-store.js +31 -0
  33. package/dist/components/related-list-section.js +37 -89
  34. package/dist/components/render-field.js +6 -5
  35. package/dist/components/render-list-toolbar-action.d.ts +27 -0
  36. package/dist/components/render-list-toolbar-action.js +40 -0
  37. package/dist/components/render-list.d.ts +13 -29
  38. package/dist/components/render-list.js +42 -50
  39. package/dist/current-user.d.ts +2 -0
  40. package/dist/data-table-aggregate-types.d.ts +47 -0
  41. package/dist/data-table-aggregate-types.js +1 -0
  42. package/dist/data-table-bulk-types.d.ts +27 -0
  43. package/dist/data-table-bulk-types.js +1 -0
  44. package/dist/data-table-facet-types.d.ts +36 -0
  45. package/dist/data-table-facet-types.js +1 -0
  46. package/dist/data-table-view-types.d.ts +73 -0
  47. package/dist/data-table-view-types.js +1 -0
  48. package/dist/headless/view-model/edit.js +3 -1
  49. package/dist/headless/view-model/types.d.ts +4 -2
  50. package/dist/hooks/list-column-layout.d.ts +19 -0
  51. package/dist/hooks/list-column-layout.js +36 -0
  52. package/dist/hooks/list-group-pages.d.ts +26 -0
  53. package/dist/hooks/list-group-pages.js +37 -0
  54. package/dist/hooks/list-reset.d.ts +8 -0
  55. package/dist/hooks/list-reset.js +15 -0
  56. package/dist/hooks/list-server-groups.d.ts +39 -0
  57. package/dist/hooks/list-server-groups.js +84 -0
  58. package/dist/hooks/list-state-schema.d.ts +28 -0
  59. package/dist/hooks/list-state-schema.js +76 -0
  60. package/dist/hooks/use-bulk-run.d.ts +41 -0
  61. package/dist/hooks/use-bulk-run.js +65 -0
  62. package/dist/hooks/use-group-load-state.d.ts +11 -0
  63. package/dist/hooks/use-group-load-state.js +31 -0
  64. package/dist/hooks/use-list-aggregates.d.ts +38 -0
  65. package/dist/hooks/use-list-aggregates.js +107 -0
  66. package/dist/hooks/use-list-table.d.ts +38 -0
  67. package/dist/hooks/use-list-table.js +91 -0
  68. package/dist/hooks/use-list-view-state.d.ts +43 -0
  69. package/dist/hooks/use-list-view-state.js +161 -0
  70. package/dist/hooks/use-related-list-rows.d.ts +27 -0
  71. package/dist/hooks/use-related-list-rows.js +81 -0
  72. package/dist/hooks/use-related-list-state.d.ts +15 -0
  73. package/dist/hooks/use-related-list-state.js +31 -0
  74. package/dist/hooks/use-remembered-list-params.d.ts +13 -0
  75. package/dist/hooks/use-remembered-list-params.js +53 -0
  76. package/dist/hooks/use-remembered-local-list-state.d.ts +26 -0
  77. package/dist/hooks/use-remembered-local-list-state.js +63 -0
  78. package/dist/i18n-defaults.js +53 -0
  79. package/dist/index.d.ts +3 -1
  80. package/dist/index.js +2 -0
  81. package/dist/primitives.d.ts +11 -47
  82. package/dist/web/app/create-app.d.ts +5 -0
  83. package/dist/web/app/create-app.js +7 -4
  84. package/dist/web/app/history-leave-guard.d.ts +6 -0
  85. package/dist/web/app/history-leave-guard.js +115 -0
  86. package/dist/web/app/list-state-store-boot.d.ts +6 -0
  87. package/dist/web/app/list-state-store-boot.js +15 -0
  88. package/dist/web/app/nav.js +7 -54
  89. package/dist/web/app/session-list-state-store.d.ts +15 -0
  90. package/dist/web/app/session-list-state-store.js +117 -0
  91. package/dist/web/index.d.ts +1 -0
  92. package/dist/web/index.js +1 -0
  93. package/dist/web/lib/export-xlsx-request.d.ts +11 -0
  94. package/dist/web/lib/export-xlsx-request.js +46 -0
  95. package/dist/web/primitives/data-table/actions-column.d.ts +16 -0
  96. package/dist/web/primitives/data-table/actions-column.js +34 -0
  97. package/dist/web/primitives/data-table/bulk-selection-bar.d.ts +20 -0
  98. package/dist/web/primitives/data-table/bulk-selection-bar.js +37 -0
  99. package/dist/web/primitives/data-table/card-sort-select.d.ts +14 -0
  100. package/dist/web/primitives/data-table/card-sort-select.js +40 -0
  101. package/dist/web/primitives/data-table/column-layout-mode.d.ts +5 -0
  102. package/dist/web/primitives/data-table/column-layout-mode.js +10 -0
  103. package/dist/web/primitives/data-table/column-picker-section.d.ts +7 -0
  104. package/dist/web/primitives/data-table/column-picker-section.js +35 -0
  105. package/dist/web/primitives/data-table/column-resize-handle.d.ts +14 -0
  106. package/dist/web/primitives/data-table/column-resize-handle.js +46 -0
  107. package/dist/web/primitives/data-table/density.d.ts +14 -0
  108. package/dist/web/primitives/data-table/density.js +20 -0
  109. package/dist/web/primitives/data-table/export-button.d.ts +5 -0
  110. package/dist/web/primitives/data-table/export-button.js +37 -0
  111. package/dist/web/primitives/data-table/facet-cluster.d.ts +8 -0
  112. package/dist/web/primitives/data-table/facet-cluster.js +37 -0
  113. package/dist/web/primitives/data-table/group-band.d.ts +28 -0
  114. package/dist/web/primitives/data-table/group-band.js +53 -0
  115. package/dist/web/primitives/data-table/measure-value.d.ts +10 -0
  116. package/dist/web/primitives/data-table/measure-value.js +28 -0
  117. package/dist/web/primitives/data-table/pin-section.d.ts +8 -0
  118. package/dist/web/primitives/data-table/pin-section.js +20 -0
  119. package/dist/web/primitives/data-table/reset-all-button.d.ts +4 -0
  120. package/dist/web/primitives/data-table/reset-all-button.js +8 -0
  121. package/dist/web/primitives/data-table/server-group-header.d.ts +11 -0
  122. package/dist/web/primitives/data-table/server-group-header.js +51 -0
  123. package/dist/web/primitives/data-table/sortable-header.d.ts +17 -0
  124. package/dist/web/primitives/data-table/sortable-header.js +41 -0
  125. package/dist/web/primitives/data-table/table-chrome.d.ts +13 -0
  126. package/dist/web/primitives/data-table/table-chrome.js +30 -0
  127. package/dist/web/primitives/data-table/table-footer.d.ts +7 -0
  128. package/dist/web/primitives/data-table/table-footer.js +20 -0
  129. package/dist/web/primitives/data-table/toolbar-end-cluster.d.ts +10 -0
  130. package/dist/web/primitives/data-table/toolbar-end-cluster.js +13 -0
  131. package/dist/web/primitives/data-table/totals-footer.d.ts +24 -0
  132. package/dist/web/primitives/data-table/totals-footer.js +48 -0
  133. package/dist/web/primitives/data-table/use-bulk-selection.d.ts +21 -0
  134. package/dist/web/primitives/data-table/use-bulk-selection.js +66 -0
  135. package/dist/web/primitives/data-table/use-column-pins.d.ts +32 -0
  136. package/dist/web/primitives/data-table/use-column-pins.js +117 -0
  137. package/dist/web/primitives/data-table/use-column-sizing.d.ts +22 -0
  138. package/dist/web/primitives/data-table/use-column-sizing.js +44 -0
  139. package/dist/web/primitives/data-table/use-measured-column-widths.d.ts +15 -0
  140. package/dist/web/primitives/data-table/use-measured-column-widths.js +83 -0
  141. package/dist/web/primitives/data-table/use-row-keyboard-navigation.d.ts +22 -0
  142. package/dist/web/primitives/data-table/use-row-keyboard-navigation.js +65 -0
  143. package/dist/web/primitives/data-table/use-row-selection.d.ts +14 -0
  144. package/dist/web/primitives/data-table/use-row-selection.js +48 -0
  145. package/dist/web/primitives/data-table/use-view-columns.d.ts +6 -0
  146. package/dist/web/primitives/data-table/use-view-columns.js +17 -0
  147. package/dist/web/primitives/data-table/view-menu.d.ts +8 -0
  148. package/dist/web/primitives/data-table/view-menu.js +45 -0
  149. package/dist/web/primitives/data-table-cell.d.ts +0 -22
  150. package/dist/web/primitives/data-table-cell.js +1 -58
  151. package/dist/web/primitives/data-table.d.ts +1 -1
  152. package/dist/web/primitives/data-table.js +122 -144
  153. package/dist/web/primitives/file-value-list.d.ts +1 -1
  154. package/dist/web/primitives/file-value-list.js +5 -5
  155. package/dist/web/primitives/number-range-facet.d.ts +7 -0
  156. package/dist/web/primitives/number-range-facet.js +33 -0
  157. package/dist/web/widgets/bounded-load-cache.d.ts +5 -1
  158. package/dist/web/widgets/bounded-load-cache.js +48 -26
  159. package/dist/web/widgets/pdf-preview.js +9 -4
  160. package/package.json +5 -5
  161. package/src/changes.json +109 -0
@@ -1,9 +1,11 @@
1
- import { type EntityDefinition, type EntityListScreenDefinition, type IconKey } from "@cosmicdrift/kumiko-framework/ui-types";
1
+ import { type EntityDefinition, type EntityListScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
2
2
  import { type ReactNode } from "react";
3
3
  import type { ListRowViewModel, Translate } from "../headless/index.ts";
4
4
  import type { ListSort } from "../hooks/use-list-url-state.ts";
5
- import { type DataTableDateRangeFacet, type DataTableFacet, type DataTableProps, type DataTableRowAction, type DataTableRowActionMode, type DataTableRowGrouping } from "../primitives.tsx";
5
+ import { type DataTableDateRangeFacet, type DataTableFacet, type DataTableProps, type DataTableRowAction, type DataTableRowActionMode, type DataTableTableBlockProps } from "../primitives.tsx";
6
6
  import { type ListEmptyStateContent } from "./list-empty-state.tsx";
7
+ import { type ToolbarActionButton } from "./render-list-toolbar-action.tsx";
8
+ export type { ToolbarActionButton };
7
9
  export type RenderListProps = {
8
10
  readonly screen: EntityListScreenDefinition;
9
11
  readonly entity: Pick<EntityDefinition, "fields" | "derivedFields">;
@@ -67,12 +69,11 @@ export type RenderListProps = {
67
69
  * EntityListScreenDefinition.rowActions: handler-QN becomes a dispatcher
68
70
  * call, and i18n keys become translated strings. */
69
71
  readonly rowActions?: readonly DataTableRowAction[];
70
- /** How the row-action column renders (see `DataTableProps.rowActionMode`).
71
- * KumikoScreen derives it from the actions themselves; without it the
72
- * DataTable falls back to its adaptive default. */
72
+ /** How the row-action column renders (see `DataTableProps.rowActionMode`). KumikoScreen
73
+ * derives it from the actions; without it the DataTable uses its adaptive default. */
73
74
  readonly rowActionMode?: DataTableRowActionMode;
74
- /** Forwarded to `DataTableProps.rowGrouping` / `rowTone`. */
75
- readonly rowGrouping?: DataTableRowGrouping;
75
+ /** Forwarded to `DataTableProps.bulk` / `rowTone`. */
76
+ readonly bulk?: DataTableProps["bulk"];
76
77
  readonly rowTone?: DataTableProps["rowTone"];
77
78
  /** Toolbar actions in the list header in resolved form. KumikoScreen
78
79
  * builds them from EntityListScreenDefinition.toolbarActions:
@@ -93,13 +94,11 @@ export type RenderListProps = {
93
94
  readonly dateRangeFacets?: readonly DataTableDateRangeFacet[];
94
95
  /** One bound changed (set URL state + refetch). */
95
96
  readonly onDateRangeChange?: (field: string, bound: "from" | "to", value: string) => void;
96
- /** Forwarded to `DataTableProps.chromeless` — drops the table's own card
97
- * frame for a host that already provides one (relatedList in a tabs-mode
98
- * section). Default false. */
97
+ /** Forwarded to `DataTableProps.chromeless` — drops the table's own card frame for a
98
+ * host that already provides one (relatedList in a tabs-mode section). Default false. */
99
99
  readonly chromeless?: boolean;
100
- /** Forwarded to `DataTableProps.scrollBody` — bounds the table height and
101
- * scrolls rows internally instead of growing the page (relatedList in a
102
- * tabs-mode section). Default false. */
100
+ /** Forwarded to `DataTableProps.scrollBody` — bounds the table height and scrolls rows
101
+ * internally instead of growing the page (relatedList in a tabs-mode section). Default false. */
103
102
  readonly scrollBody?: boolean;
104
103
  /** Forwarded to `DataTableProps.screenPadding` — `true`: the table carries
105
104
  * the shared screen padding because it is the screen body (entityList/
@@ -112,20 +111,5 @@ export type RenderListProps = {
112
111
  readonly expandedRowIds?: ReadonlySet<string>;
113
112
  readonly onToggleRowExpanded?: (rowId: string) => void;
114
113
  readonly renderExpandedRow?: (row: ListRowViewModel) => ReactNode;
115
- };
116
- export type ToolbarActionButton = {
117
- readonly id: string;
118
- readonly label: string;
119
- readonly style?: "primary" | "secondary" | "danger";
120
- readonly confirm?: string;
121
- readonly confirmLabel?: string;
122
- /** Overrides the default "danger implies a confirm dialog" rule. Schema-driven
123
- * navigate/drawer actions set it to false: the colour marks the action as
124
- * destructive, but the target form is the confirmation. */
125
- readonly confirmRequired?: boolean;
126
- readonly onTrigger: () => Promise<void> | void;
127
- /** Id-derived default icon (ACTION_ICON_BY_ID in kumiko-screen.tsx) —
128
- * ToolbarAction has no author-declared icon field, unlike RowAction. */
129
- readonly icon?: IconKey;
130
- };
114
+ } & DataTableTableBlockProps;
131
115
  export declare function RenderList(props: RenderListProps): ReactNode;
@@ -11,6 +11,7 @@ import { usePageHeaderCompact, usePageHeaderSlotAvailable } from "../page-header
11
11
  import { shouldRenderActionsIconOnly, usePrimitives, } from "../primitives.js";
12
12
  import { ListEmptyStateBody } from "./list-empty-state.js";
13
13
  import { needsActionConfirm } from "./render-edit-action-button.js";
14
+ import { ToolbarActionView } from "./render-list-toolbar-action.js";
14
15
  const SEARCH_DEBOUNCE_MS = 300;
15
16
  // Icon-less actions are promoted too; on phones the call site falls back to a
16
17
  // "plus" icon, on desktop the label carries the action.
@@ -18,7 +19,7 @@ function isHeaderPromotableToolbarAction(action) {
18
19
  return action.style === "primary" && !needsActionConfirm(action);
19
20
  }
20
21
  export function RenderList(props) {
21
- const { screen, entity, rows, featureName, translate: translateProp, onRowClick, emptyState, onCreate, createLabel, searchable = false, searchPlaceholder, description, emptyContent, narrowed = false, defaultFilterActive: defaultFilterActiveProp = false, itemNounKey, searchValue, onSearchChange, sort, onSortChange, pager, onReachEnd, loadingMore, hasMore, rowActions, rowActionMode, rowGrouping, rowTone, toolbarActions, filterFacets, filterValues, onFilterChange, onFilterReset, dateRangeFacets, onDateRangeChange, chromeless, scrollBody, screenPadding, expandedRowIds, onToggleRowExpanded, renderExpandedRow, } = props;
22
+ const { screen, entity, rows, featureName, translate: translateProp, onRowClick, emptyState, onCreate, createLabel, searchable = false, searchPlaceholder, description, emptyContent, narrowed = false, defaultFilterActive: defaultFilterActiveProp = false, itemNounKey, searchValue, onSearchChange, sort, onSortChange, pager, onReachEnd, loadingMore, hasMore, rowActions, rowActionMode, bulk, rowTone, toolbarActions, filterFacets, filterValues, onFilterChange, onFilterReset, dateRangeFacets, onDateRangeChange, chromeless, scrollBody, screenPadding, expandedRowIds, onToggleRowExpanded, renderExpandedRow, ...tableBlockProps } = props;
22
23
  // As in RenderEdit, fall back to the i18next context for translation;
23
24
  // otherwise column headers would display raw i18n keys.
24
25
  const t = useTranslation();
@@ -27,6 +28,10 @@ export function RenderList(props) {
27
28
  const pageHeaderSlotAvailable = usePageHeaderSlotAvailable();
28
29
  const pageHeaderCompact = usePageHeaderCompact();
29
30
  const createInPageHeader = onCreate !== undefined && PageHeader !== undefined && pageHeaderSlotAvailable;
31
+ // Promoted actions that moved into the phone header's overflow menu have no button of
32
+ // their own, so their busy and error state lives here.
33
+ const [menuActionBusyId, setMenuActionBusyId] = useState(null);
34
+ const [menuActionError, setMenuActionError] = useState(null);
30
35
  // Local search buffer + debounce. External changes (browser back,
31
36
  // cross-component reset) are mirrored back; typing only fires
32
37
  // onSearchChange after 300ms without further keystrokes.
@@ -214,16 +219,41 @@ export function RenderList(props) {
214
219
  const hasHeaderSlot = screen.slots?.header !== undefined;
215
220
  const toolbarIconOnly = hasToolbarActions && shouldRenderActionsIconOnly(toolbarActions);
216
221
  const createButton = onCreate !== undefined && createInPageHeader && pageHeaderCompact ? (_jsx(Button, { variant: "primary", icon: "plus", size: "icon", ariaLabel: effectiveCreateLabel, title: effectiveCreateLabel, onClick: onCreate, testId: "render-list-create" })) : onCreate !== undefined ? (_jsx(Button, { variant: "primary", icon: "plus", onClick: onCreate, testId: "render-list-create", className: "px-3.5", children: effectiveCreateLabel })) : undefined;
217
- // Without onCreate (e.g. a navigate action as the list's only CTA) the primary
218
- // toolbar action takes the page-header slot like a "+ Neu" button would: an icon
219
- // on phones, labelled on desktop, so projection lists look like entity lists.
220
- const promotedToolbarAction = onCreate === undefined && PageHeader !== undefined && pageHeaderSlotAvailable
221
- ? toolbarActions?.find(isHeaderPromotableToolbarAction)
222
- : undefined;
223
- const promotedActionButton = promotedToolbarAction !== undefined ? (_jsx(ToolbarActionView, { action: pageHeaderCompact
224
- ? { ...promotedToolbarAction, icon: promotedToolbarAction.icon ?? "plus" }
225
- : promotedToolbarAction, iconOnly: pageHeaderCompact, Button: Button, Dialog: Dialog, Banner: Banner })) : undefined;
226
- const toolbarActionsInToolbar = toolbarActions?.filter((a) => a !== promotedToolbarAction) ?? [];
222
+ // Every primary toolbar action takes the page-header slot, left of the create
223
+ // button (which stays the rightmost primary action): labelled on desktop, so
224
+ // projection lists look like entity lists. On phones an icon button, or, next to
225
+ // the create button, an entry in the header's overflow menu: the header has no
226
+ // room for a labelled action beside it.
227
+ const promotedToolbarActions = PageHeader !== undefined && pageHeaderSlotAvailable
228
+ ? (toolbarActions?.filter(isHeaderPromotableToolbarAction) ?? [])
229
+ : [];
230
+ const promotedInOverflowMenu = pageHeaderCompact && createInPageHeader && promotedToolbarActions.length > 0;
231
+ const promotedMenuItems = promotedInOverflowMenu
232
+ ? promotedToolbarActions.map((a) => ({
233
+ id: a.id,
234
+ label: a.label,
235
+ ...(a.icon !== undefined && { icon: a.icon }),
236
+ ...(menuActionBusyId !== null && { disabled: true }),
237
+ testId: `render-list-toolbar-action-${a.id}`,
238
+ onSelect: () => {
239
+ setMenuActionBusyId(a.id);
240
+ setMenuActionError(null);
241
+ void (async () => {
242
+ try {
243
+ await a.onTrigger();
244
+ }
245
+ catch (e) {
246
+ setMenuActionError(e instanceof Error ? e.message : String(e));
247
+ }
248
+ finally {
249
+ setMenuActionBusyId(null);
250
+ }
251
+ })();
252
+ },
253
+ }))
254
+ : [];
255
+ const headerActions = promotedToolbarActions.length > 0 && !promotedInOverflowMenu ? (_jsxs(_Fragment, { children: [promotedToolbarActions.map((a) => (_jsx(ToolbarActionView, { action: pageHeaderCompact ? { ...a, icon: a.icon ?? "plus" } : a, iconOnly: pageHeaderCompact, Button: Button, Dialog: Dialog, Banner: Banner }, a.id))), createInPageHeader && createButton] })) : createInPageHeader ? (createButton) : undefined;
256
+ const toolbarActionsInToolbar = toolbarActions?.filter((a) => !promotedToolbarActions.includes(a)) ?? [];
227
257
  const hasToolbarActionsInToolbar = toolbarActionsInToolbar.length > 0;
228
258
  const toolbarEnd = hasHeaderSlot ||
229
259
  hasToolbarActionsInToolbar ||
@@ -242,8 +272,7 @@ export function RenderList(props) {
242
272
  const titleKey = `screen:${screen.id}.title`;
243
273
  const resolvedTitle = translate(titleKey);
244
274
  const toolbarTitle = resolvedTitle === titleKey ? screen.id : resolvedTitle;
245
- // ListSort = DataTableSort (aliased in use-list-url-state), so no cast is needed.
246
- return (_jsxs(_Fragment, { children: [createInPageHeader && PageHeader !== undefined && _jsx(PageHeader, { actions: createButton }), promotedActionButton !== undefined && PageHeader !== undefined && (_jsx(PageHeader, { actions: promotedActionButton })), referenceColumns.map((rc) => (_jsx(ReferenceLookupBridge, { field: rc.field, refEntity: rc.refEntity, labelField: rc.labelField, featureName: rc.refFeature, onMap: handleLookupMap }, rc.field))), _jsx(DataTable, { columns: enrichedVm.columns, rows: enrichedVm.rows, toolbarTitle: toolbarTitle, ...(onRowClick !== undefined && { onRowClick }), ...(composedEmptyState !== undefined && { emptyState: composedEmptyState }), ...(toolbarStart !== undefined && { toolbarStart }), ...(description !== undefined && { toolbarDescription: description }), ...(toolbarEnd !== undefined && { toolbarEnd }), ...(sort !== undefined && { sort }), ...(onSortChange !== undefined && { onSortChange }), ...(pager !== undefined && { pager }), ...(onReachEnd !== undefined && { onReachEnd }), ...(loadingMore !== undefined && { loadingMore }), ...(hasMore !== undefined && { hasMore }), ...(itemNoun !== undefined && { itemNoun }), ...(rowActions !== undefined && { rowActions }), ...(rowActionMode !== undefined && { rowActionMode }), ...(rowGrouping !== undefined && { rowGrouping }), ...(rowTone !== undefined && { rowTone }), ...(filterFacets !== undefined && { filterFacets }), ...(filterValues !== undefined && { filterValues }), ...(onFilterChange !== undefined && { onFilterChange }), ...(onFilterReset !== undefined && { onFilterReset }), ...(dateRangeFacets !== undefined && { dateRangeFacets }), ...(onDateRangeChange !== undefined && { onDateRangeChange }), ...(chromeless !== undefined && { chromeless }), ...(scrollBody !== undefined && { scrollBody }), ...(screenPadding !== undefined && { screenPadding }), ...(expandedRowIds !== undefined && { expandedRowIds }), ...(onToggleRowExpanded !== undefined && { onToggleRowExpanded }), ...(renderExpandedRow !== undefined && { renderExpandedRow }), testId: "render-list-table" })] }));
275
+ return (_jsxs(_Fragment, { children: [headerActions !== undefined && PageHeader !== undefined && (_jsx(PageHeader, { actions: headerActions, ...(promotedInOverflowMenu && { overflowItems: promotedMenuItems }) })), menuActionError !== null && (_jsx(Banner, { variant: "error", testId: "render-list-toolbar-action-error", children: menuActionError })), referenceColumns.map((rc) => (_jsx(ReferenceLookupBridge, { field: rc.field, refEntity: rc.refEntity, labelField: rc.labelField, featureName: rc.refFeature, onMap: handleLookupMap }, rc.field))), _jsx(DataTable, { columns: enrichedVm.columns, rows: enrichedVm.rows, toolbarTitle: toolbarTitle, ...(onRowClick !== undefined && { onRowClick }), ...(composedEmptyState !== undefined && { emptyState: composedEmptyState }), ...(toolbarStart !== undefined && { toolbarStart }), ...(description !== undefined && { toolbarDescription: description }), ...(toolbarEnd !== undefined && { toolbarEnd }), ...(sort !== undefined && { sort }), ...(onSortChange !== undefined && { onSortChange }), ...(pager !== undefined && { pager }), ...(onReachEnd !== undefined && { onReachEnd }), ...(loadingMore !== undefined && { loadingMore }), ...(hasMore !== undefined && { hasMore }), ...(itemNoun !== undefined && { itemNoun }), ...(rowActions !== undefined && { rowActions }), ...(rowActionMode !== undefined && { rowActionMode }), ...(bulk !== undefined && { bulk }), ...(rowTone !== undefined && { rowTone }), ...(filterFacets !== undefined && { filterFacets }), ...(filterValues !== undefined && { filterValues }), ...(onFilterChange !== undefined && { onFilterChange }), ...(onFilterReset !== undefined && { onFilterReset }), ...(dateRangeFacets !== undefined && { dateRangeFacets }), ...(onDateRangeChange !== undefined && { onDateRangeChange }), ...(chromeless !== undefined && { chromeless }), ...(scrollBody !== undefined && { scrollBody }), ...(screenPadding !== undefined && { screenPadding }), ...(expandedRowIds !== undefined && { expandedRowIds }), ...(onToggleRowExpanded !== undefined && { onToggleRowExpanded }), ...(renderExpandedRow !== undefined && { renderExpandedRow }), ...tableBlockProps, testId: "render-list-table" })] }));
247
276
  }
248
277
  // Header slot in the list toolbar (toolbarEnd): `screen.slots.header` is
249
278
  // a PlatformComponent (`{ react: { __component: "X" } }`) resolved through
@@ -290,43 +319,6 @@ function ReferenceLookupBridge({ field, refEntity, labelField, featureName, onMa
290
319
  }, [field, lookup.map, lookup.complete, resolved, onMap]);
291
320
  return null;
292
321
  }
293
- // ToolbarActionView renders one button per toolbar action, with a confirm
294
- // dialog when required. It follows the inline RowAction pattern but has no
295
- // row context. The busy state covers async onTrigger; the dialog opens before
296
- // the trigger when confirmation is required for confirm/danger actions.
297
- function ToolbarActionView({ action, iconOnly = false, Button, Dialog, Banner, }) {
298
- const [busy, setBusy] = useState(false);
299
- const [confirmOpen, setConfirmOpen] = useState(false);
300
- // Toolbar actions do not have access to the toast primitive, unlike
301
- // rowActions routed through the DataTable primitive, so an inline error
302
- // banner is used to surface failures. Without it, a swallowed failure result
303
- // would look to the user as though nothing happened.
304
- const [errorText, setErrorText] = useState(null);
305
- const trigger = async () => {
306
- setBusy(true);
307
- setErrorText(null);
308
- try {
309
- await action.onTrigger();
310
- }
311
- catch (e) {
312
- setErrorText(e instanceof Error ? e.message : String(e));
313
- }
314
- finally {
315
- setBusy(false);
316
- }
317
- };
318
- const variant = action.style ?? "secondary";
319
- const needsConfirm = needsActionConfirm(action);
320
- const showIconOnly = iconOnly && action.icon !== undefined;
321
- return (_jsxs(_Fragment, { children: [_jsx(Button, { variant: variant, loading: busy, ...(action.icon !== undefined && { icon: action.icon }), size: showIconOnly ? "icon" : "md", ...(showIconOnly && { ariaLabel: action.label, title: action.label }), onClick: () => {
322
- if (needsConfirm) {
323
- setConfirmOpen(true);
324
- }
325
- else {
326
- void trigger();
327
- }
328
- }, testId: `render-list-toolbar-action-${action.id}`, children: showIconOnly ? null : action.label }), _jsx(Dialog, { open: confirmOpen, onOpenChange: setConfirmOpen, title: action.label, ...(action.confirm !== undefined && { description: action.confirm }), confirmLabel: action.confirmLabel ?? action.label, ...(action.style === "danger" && { variant: "danger" }), onConfirm: trigger, testId: `render-list-toolbar-action-${action.id}-dialog` }), errorText !== null && (_jsx(Banner, { variant: "error", testId: `render-list-toolbar-action-${action.id}-error`, children: errorText }))] }));
329
- }
330
322
  function isPlainRecord(value) {
331
323
  return typeof value === "object" && value !== null && !Array.isArray(value);
332
324
  }
@@ -7,6 +7,8 @@ export type CurrentUser = {
7
7
  readonly displayName: string;
8
8
  /** Effective IANA zone of the user (override, else tenant). */
9
9
  readonly timeZone?: string;
10
+ /** Active tenant; scopes per-user browser state such as remembered list views. */
11
+ readonly tenantId?: string;
10
12
  };
11
13
  export declare function CurrentUserProvider({ user, children, }: {
12
14
  readonly user: CurrentUser | undefined;
@@ -0,0 +1,47 @@
1
+ import type { AggregateKey } from "@cosmicdrift/kumiko-types/aggregate-types";
2
+ import type { ListAggregateMeasureKind, ListAggregateValue } from "@cosmicdrift/kumiko-types/list-aggregate-types";
3
+ /** One server-computed measure; a money measure holds one minor-unit value per currency. */
4
+ export type DataTableMeasure = {
5
+ readonly field: string;
6
+ readonly kind: ListAggregateMeasureKind;
7
+ readonly value: ListAggregateValue;
8
+ };
9
+ export type DataTableServerGroup = {
10
+ /** `relatedListGroupKey` of the group value; stable across reloads. */
11
+ readonly key: string;
12
+ /** Raw group value, rendered through the group column when `label` is absent. */
13
+ readonly value: AggregateKey;
14
+ readonly label?: string;
15
+ /** Rows of the whole group on the server, not the loaded ones. */
16
+ readonly count: number;
17
+ readonly measures: readonly DataTableMeasure[];
18
+ readonly collapsed: boolean;
19
+ /** How many of the table's `rows` belong to this group; rows arrive in group order. */
20
+ readonly rowCount: number;
21
+ readonly hasMore: boolean;
22
+ readonly loading: boolean;
23
+ };
24
+ /** Server-side grouping: groups are the load unit, each loads its own rows. */
25
+ export type DataTableServerGroups = {
26
+ readonly field: string;
27
+ readonly groups: readonly DataTableServerGroup[];
28
+ readonly totalCount: number;
29
+ readonly onToggle: (key: string) => void;
30
+ readonly onLoadMore: (key: string) => void;
31
+ readonly onHeaderVisible: (key: string) => void;
32
+ };
33
+ export type DataTableTotals = {
34
+ readonly measures: readonly DataTableMeasure[];
35
+ };
36
+ /** `truncated`: more groups than the server answers. `searchTruncated`: the search hit its cap. */
37
+ export type DataTableListNotice = "truncated" | "searchTruncated";
38
+ export type DataTableRangeBound = "min" | "max";
39
+ /** "From/to" inputs on a number field. Values are the typed strings, "" = open bound. */
40
+ export type DataTableRangeFacet = {
41
+ readonly field: string;
42
+ readonly label: string;
43
+ readonly min: string;
44
+ readonly max: string;
45
+ /** Shown after the inputs, e.g. the money field's currency. */
46
+ readonly unit?: string;
47
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ import type { IconKey } from "@cosmicdrift/kumiko-types/nav-icon";
2
+ import type { ListRowViewModel } from "./headless/index.ts";
3
+ export type BulkRowOutcome = {
4
+ readonly kind: "succeeded";
5
+ } | {
6
+ readonly kind: "failed";
7
+ readonly message: string;
8
+ };
9
+ export type DataTableBulkAction = {
10
+ readonly id: string;
11
+ /** Translated label of the underlying row action. */
12
+ readonly label: string;
13
+ readonly style?: "primary" | "secondary" | "danger";
14
+ readonly icon?: IconKey;
15
+ /** Present: one confirm dialog for the whole selection, with this text for `count` rows. */
16
+ readonly confirmText?: (count: number) => string;
17
+ readonly confirmLabel?: string;
18
+ /** False: the row action's `visible` hides it for this row; the run skips the row. */
19
+ readonly appliesTo: (row: ListRowViewModel) => boolean;
20
+ /** Exactly one server write for one row. Never throws for a rejected write. */
21
+ readonly writeRow: (row: ListRowViewModel) => Promise<BulkRowOutcome>;
22
+ };
23
+ export type DataTableBulk = {
24
+ readonly actions: readonly DataTableBulkAction[];
25
+ /** Reloads the list once after a run. */
26
+ readonly reload: () => Promise<void>;
27
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
1
+ export type DataTableFacet = {
2
+ /** Entity-Feldname (= URL-State-Key + payload.filters[].field). */
3
+ readonly field: string;
4
+ /** Translated Button-Label ("Status", "Active"). */
5
+ readonly label: string;
6
+ /** Selectable values with display labels. `value` is the raw filter value
7
+ * (a select option or `"true"`/`"false"`), transported as a string. */
8
+ readonly options: readonly {
9
+ readonly value: string;
10
+ readonly label: string;
11
+ }[];
12
+ /** Present: render the facet as a row of single-choice toggle chips instead
13
+ * of a dropdown. A chip is pressed while the selection equals its values. */
14
+ readonly chips?: readonly DataTableFacetChip[];
15
+ /** Chips only: show each chip's `count` next to its label. */
16
+ readonly showCounts?: boolean;
17
+ /** Chips only: hide chips whose `count` is 0 (unless pressed). */
18
+ readonly hideEmpty?: boolean;
19
+ };
20
+ export type DataTableFacetChip = {
21
+ readonly id: string;
22
+ /** Translated chip label. */
23
+ readonly label: string;
24
+ /** Selection a click sets; `[]` clears the facet. */
25
+ readonly values: readonly string[];
26
+ /** Matching row count, undefined while unknown. */
27
+ readonly count?: number;
28
+ };
29
+ export type DataTableDateRangeFacet = {
30
+ /** The facet's namespace (its date field) — a key, not a filter value. */
31
+ readonly field: string;
32
+ /** Translated label ("When"). */
33
+ readonly label: string;
34
+ readonly from: string;
35
+ readonly to: string;
36
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,73 @@
1
+ import type { ListColumnPin, ListTableDensity } from "@cosmicdrift/kumiko-types/screen";
2
+ import type { DataTableListNotice, DataTableRangeBound, DataTableRangeFacet, DataTableServerGroups, DataTableTotals } from "./data-table-aggregate-types.ts";
3
+ export type DataTableDensity = ListTableDensity;
4
+ export type DataTableColumnPin = ListColumnPin;
5
+ export type DataTableColumnPins = Readonly<Record<string, DataTableColumnPin>>;
6
+ export type DataTableColumnWidths = Readonly<Record<string, number>>;
7
+ /** One column in the view menu. The label comes from the DataTable's own
8
+ * column of the same `field`. */
9
+ export type DataTableViewColumn = {
10
+ readonly field: string;
11
+ readonly visible: boolean;
12
+ };
13
+ export type DataTableViewOptions = {
14
+ /** Present: the view menu has a column section (visibility + order). The
15
+ * order lists every column; hidden ones stay in the list. */
16
+ readonly columns?: readonly DataTableViewColumn[];
17
+ readonly onColumnsChange?: (next: readonly DataTableViewColumn[]) => void;
18
+ readonly density: DataTableDensity;
19
+ /** Present: the view menu has a density section. */
20
+ readonly onDensityChange?: (next: DataTableDensity) => void;
21
+ /** Present: pinning is on and the view menu has a pin section. Unpinned columns have no entry. */
22
+ readonly pins?: DataTableColumnPins;
23
+ readonly onPinsChange?: (next: DataTableColumnPins) => void;
24
+ /** Present: resizing is on; the table switches to table-layout fixed. Every column has an entry (px). */
25
+ readonly widths?: DataTableColumnWidths;
26
+ /** Present together with `widths`; every column has an entry (px). */
27
+ readonly minWidths?: DataTableColumnWidths;
28
+ readonly onWidthChange?: (field: string, width: number) => void;
29
+ readonly keyboardNavigation?: boolean;
30
+ /** Present: the view menu has a "group by" section. */
31
+ readonly groupBy?: DataTableGroupByChoice;
32
+ /** False once columns, order or density differ from the screen default. */
33
+ readonly viewIsDefault: boolean;
34
+ readonly onViewReset: () => void;
35
+ };
36
+ export type DataTableGroupByChoice = {
37
+ /** Active group field, "" = not grouped. */
38
+ readonly value: string;
39
+ readonly fields: readonly string[];
40
+ readonly onChange: (next: string) => void;
41
+ };
42
+ export type DataTableResetAll = {
43
+ /** True while something deviates from the screen default. */
44
+ readonly active: boolean;
45
+ readonly onReset: () => void;
46
+ };
47
+ /** What the export button sends to POST /api/export/xlsx (xlsx is the only format). */
48
+ export type DataTableExport = {
49
+ /** Screen QN `<feature>:screen:<id>`. */
50
+ readonly screen: string;
51
+ readonly screenShortId: string;
52
+ /** The list payload without paging keys; the server reads the whole filtered list. */
53
+ readonly payload: Readonly<Record<string, unknown>>;
54
+ /** Visible column ids in display order. */
55
+ readonly columns: readonly string[];
56
+ };
57
+ /** The DataTable props a screen's `table` block feeds. */
58
+ export type DataTableTableBlock = {
59
+ /** View menu (column picker, density); absent = no menu. */
60
+ readonly view?: DataTableViewOptions;
61
+ /** Replaces the facet reset with one that also covers sort, search and page size. */
62
+ readonly resetAll?: DataTableResetAll;
63
+ /** Present: the toolbar has an export button (web only). */
64
+ readonly exportList?: DataTableExport;
65
+ /** Present: the rows are grouped on the server; replaces pager and infinite scroll. */
66
+ readonly serverGroups?: DataTableServerGroups;
67
+ /** Present: a footer row with the server-computed totals of the filtered list. */
68
+ readonly totals?: DataTableTotals;
69
+ /** Present: the toolbar says the answer is incomplete and asks to narrow the list. */
70
+ readonly notice?: DataTableListNotice;
71
+ readonly rangeFacets?: readonly DataTableRangeFacet[];
72
+ readonly onRangeChange?: (field: string, bound: DataTableRangeBound, value: string) => void;
73
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -56,6 +56,7 @@ function wasEmptiedByWriteExclusion(declaredFieldSpecs, remainingFields) {
56
56
  export function computeRelatedListSectionViewModel(sectionSpec, translate) {
57
57
  return {
58
58
  kind: "relatedList",
59
+ ...(sectionSpec.id !== undefined && { id: sectionSpec.id }),
59
60
  title: translate(sectionSpec.title),
60
61
  query: sectionSpec.query,
61
62
  ...(sectionSpec.parentParam !== undefined && { parentParam: sectionSpec.parentParam }),
@@ -67,13 +68,14 @@ export function computeRelatedListSectionViewModel(sectionSpec, translate) {
67
68
  }),
68
69
  ...(sectionSpec.itemNoun !== undefined && { itemNoun: sectionSpec.itemNoun }),
69
70
  ...(sectionSpec.pageSize !== undefined && { pageSize: sectionSpec.pageSize }),
71
+ ...(sectionSpec.table !== undefined && { table: sectionSpec.table }),
70
72
  ...(sectionSpec.defaultSort !== undefined && { defaultSort: sectionSpec.defaultSort }),
71
73
  ...(sectionSpec.searchable !== undefined && { searchable: sectionSpec.searchable }),
72
74
  ...(sectionSpec.facets !== undefined && { facets: sectionSpec.facets }),
73
75
  ...(sectionSpec.rowClick !== undefined && { rowClick: sectionSpec.rowClick }),
74
76
  ...(sectionSpec.rowActions !== undefined && { rowActions: sectionSpec.rowActions }),
75
77
  ...(sectionSpec.rowActionMode !== undefined && { rowActionMode: sectionSpec.rowActionMode }),
76
- ...(sectionSpec.groupBy !== undefined && { groupBy: sectionSpec.groupBy }),
78
+ ...(sectionSpec.bulkActions !== undefined && { bulkActions: sectionSpec.bulkActions }),
77
79
  ...(sectionSpec.rowTone !== undefined && { rowTone: sectionSpec.rowTone }),
78
80
  ...(sectionSpec.toolbarActions !== undefined && { toolbarActions: sectionSpec.toolbarActions }),
79
81
  ...(sectionSpec.actions !== undefined && { actions: sectionSpec.actions }),
@@ -1,4 +1,4 @@
1
- import type { EditFieldSpec, EditSectionSpec, FieldAffix, FieldFillAction, FieldIconKey, FieldRenderer, IconKey, ListColumnSpec, ListFacetSpec, ListSortSpec, OptionsQueryPayload, PlatformComponent, RelatedListGroupBy, RelatedListRowTone, RelatedListToolbarAction, RowAction, RowActionMode, ScreenSlots, SelectOptionTone } from "@cosmicdrift/kumiko-framework/ui-types";
1
+ import type { EditFieldSpec, EditSectionSpec, FieldAffix, FieldFillAction, FieldIconKey, FieldRenderer, IconKey, ListBulkAction, ListColumnSpec, ListFacetSpec, ListSortSpec, ListTableOptions, OptionsQueryPayload, PlatformComponent, RelatedListRowTone, RelatedListToolbarAction, RowAction, RowActionMode, ScreenSlots, SelectOptionTone } from "@cosmicdrift/kumiko-framework/ui-types";
2
2
  export type RuntimeRenderer = (value: unknown, row?: Readonly<Record<string, unknown>>) => string;
3
3
  /** Where a cell's text links to. `href` comes from NavApi.hrefFor (new-tab and
4
4
  * copy-link work); `onPress` is the in-app navigation for a plain click. */
@@ -333,6 +333,7 @@ export type EditExtensionSectionViewModel = {
333
333
  };
334
334
  export type EditRelatedListSectionViewModel = {
335
335
  readonly kind: "relatedList";
336
+ readonly id?: string;
336
337
  readonly title: string;
337
338
  readonly query: string;
338
339
  readonly parentParam?: string;
@@ -345,6 +346,7 @@ export type EditRelatedListSectionViewModel = {
345
346
  /** Untranslated plural-forms key, resolved with the row count by the list footer. */
346
347
  readonly itemNoun?: string;
347
348
  readonly pageSize?: number;
349
+ readonly table?: ListTableOptions;
348
350
  readonly defaultSort?: ListSortSpec;
349
351
  readonly searchable?: boolean;
350
352
  readonly facets?: readonly ListFacetSpec[];
@@ -354,7 +356,7 @@ export type EditRelatedListSectionViewModel = {
354
356
  };
355
357
  readonly rowActions?: readonly RowAction[];
356
358
  readonly rowActionMode?: RowActionMode;
357
- readonly groupBy?: RelatedListGroupBy;
359
+ readonly bulkActions?: readonly ListBulkAction[];
358
360
  readonly rowTone?: RelatedListRowTone;
359
361
  readonly toolbarActions?: readonly RelatedListToolbarAction[];
360
362
  /** From `EditRelatedListSection.actions` — rendered in the
@@ -0,0 +1,19 @@
1
+ import type { DataTableColumnPin, DataTableColumnPins, DataTableColumnWidths } from "../data-table-view-types.ts";
2
+ export declare const DEFAULT_COLUMN_WIDTH_PX = 160;
3
+ export declare const DEFAULT_MIN_COLUMN_WIDTH_PX = 64;
4
+ /** Upper bound so a tampered stored width cannot blow the table up. */
5
+ export declare const MAX_COLUMN_WIDTH_PX = 2000;
6
+ type LayoutSchemaColumn = {
7
+ readonly field: string;
8
+ readonly pinned?: DataTableColumnPin;
9
+ readonly width?: number;
10
+ readonly minWidth?: number;
11
+ };
12
+ export declare function defaultPins(schema: readonly LayoutSchemaColumn[]): DataTableColumnPins;
13
+ export declare function minWidthsOf(schema: readonly LayoutSchemaColumn[]): DataTableColumnWidths;
14
+ export declare function clampWidth(width: number, min: number): number;
15
+ export declare function defaultWidths(schema: readonly LayoutSchemaColumn[], minWidths: DataTableColumnWidths): DataTableColumnWidths;
16
+ /** A saved width wins per field; a column the saved view has never seen keeps its default. */
17
+ export declare function mergeSavedWidths(defaults: DataTableColumnWidths, saved: Readonly<Record<string, number>>, minWidths: DataTableColumnWidths): DataTableColumnWidths;
18
+ export declare function sameRecord<T>(a: Readonly<Record<string, T>>, b: Readonly<Record<string, T>>): boolean;
19
+ export {};
@@ -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
+ }