@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
@@ -3,7 +3,7 @@ import { useEffect, useMemo, useRef } from "react";
3
3
  import { useUserRoles } from "../context/user-roles-context.js";
4
4
  import { useQuery } from "../hooks/use-query.js";
5
5
  import { useTranslation } from "../i18n.js";
6
- import { PageHeaderSlotAvailableProvider, usePageHeaderSlotAvailable, } from "../page-header-slot.js";
6
+ import { PageHeaderSlotAvailableProvider } from "../page-header-slot.js";
7
7
  import { usePrimitives } from "../primitives.js";
8
8
  import { ActionFormBody } from "./action-form-body.js";
9
9
  import { ConfigEditBody } from "./config-edit-body.js";
@@ -11,6 +11,7 @@ import { useCustomScreenComponent } from "./custom-screens.js";
11
11
  import { useDashboardBody } from "./dashboard-body.js";
12
12
  import { EntityEditScreen } from "./entity-edit-screen.js";
13
13
  import { EntityListScreen } from "./entity-list-screen.js";
14
+ import { LockedHeaderScope, StandInChainBoundary } from "./locked-screen-header.js";
14
15
  import { ProjectionDetailBody } from "./projection-detail-body.js";
15
16
  import { ProjectionListBody } from "./projection-list-body.js";
16
17
  import { lastSegment } from "./qn.js";
@@ -54,7 +55,7 @@ export function KumikoScreen({ schema, qn, translate, entityId, onRowClick, onCo
54
55
  onCopyLink,
55
56
  });
56
57
  // Gate every entry (incl. direct URL): the body element is only mounted once the condition holds.
57
- const body = screen.visibleWhen !== undefined ? (_jsx(ScreenVisibilityGate, { featureName: schema.featureName, visibleWhen: screen.visibleWhen, fallback: screen.fallback, translate: translate, children: screenBody })) : (screenBody);
58
+ const body = screen.visibleWhen !== undefined ? (_jsx(ScreenVisibilityGate, { featureName: schema.featureName, visibleWhen: screen.visibleWhen, fallback: screen.fallback, translate: translate, children: _jsx(StandInChainBoundary, { children: screenBody }) })) : (_jsx(StandInChainBoundary, { children: screenBody }));
58
59
  // An embedded screen (e.g. a dashboard panel) keeps its parent's host and
59
60
  // must not portal its page header into the shell that hosts the outer screen.
60
61
  if (outerHost !== undefined) {
@@ -78,16 +79,7 @@ function ScreenVisibilityGate({ featureName, visibleWhen, fallback, translate, c
78
79
  if (fallback !== undefined) {
79
80
  return _jsx(FallbackScreen, { featureName: featureName, fallback: fallback, translate: translate });
80
81
  }
81
- return (_jsxs(_Fragment, { children: [_jsx(LockedScreenHeader, {}), _jsx(ScreenUnavailableBanner, {})] }));
82
- }
83
- // The route still names the locked screen; its breadcrumb must not label what stands in for it.
84
- // With a stand-in the header is labelled with that screen instead, none if it has no title.
85
- function LockedScreenHeader({ standInScreenId }) {
86
- const { PageHeader } = usePrimitives();
87
- const pageHeaderSlotAvailable = usePageHeaderSlotAvailable();
88
- if (PageHeader === undefined || !pageHeaderSlotAvailable)
89
- return null;
90
- return standInScreenId !== undefined ? (_jsx(PageHeader, { hideBreadcrumb: true, breadcrumbScreenId: standInScreenId })) : (_jsx(PageHeader, { hideBreadcrumb: true }));
82
+ return (_jsxs(_Fragment, { children: [_jsx(LockedHeaderScope, {}), _jsx(ScreenUnavailableBanner, {})] }));
91
83
  }
92
84
  function ScreenUnavailableBanner() {
93
85
  const { Banner } = usePrimitives();
@@ -99,9 +91,9 @@ function FallbackScreen({ featureName, fallback, translate, }) {
99
91
  // Also undefined when the user lacks access to the fallback screen, which is
100
92
  // not a framework error.
101
93
  if (target === undefined) {
102
- return (_jsxs(_Fragment, { children: [_jsx(LockedScreenHeader, {}), _jsx(ScreenUnavailableBanner, {})] }));
94
+ return (_jsxs(_Fragment, { children: [_jsx(LockedHeaderScope, {}), _jsx(ScreenUnavailableBanner, {})] }));
103
95
  }
104
- return (_jsxs(_Fragment, { children: [_jsx(LockedScreenHeader, { standInScreenId: lastSegment(target.qn) }), _jsx(KumikoScreen, { schema: target.schema, qn: target.qn, translate: translate })] }));
96
+ return (_jsx(LockedHeaderScope, { standInScreenId: lastSegment(target.qn), children: _jsx(KumikoScreen, { schema: target.schema, qn: target.qn, translate: translate }) }));
105
97
  }
106
98
  function renderScreenBody({ schema, screen, translate, entityId, onRowClick, onCopyLink, }) {
107
99
  switch (screen.type) {
@@ -0,0 +1,18 @@
1
+ import type { ListBulkAction, RowAction } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import type { DataTableBulk, DataTableBulkAction } from "../data-table-bulk-types.ts";
3
+ import type { Dispatcher, Translate } from "../headless/index.ts";
4
+ /** Resolves each bulk action against its writeHandler row action; one that resolves to nothing is dropped. */
5
+ export declare function buildListBulkActions(options: {
6
+ readonly bulkActions: readonly ListBulkAction[];
7
+ readonly rowActions: readonly RowAction[] | undefined;
8
+ readonly translate: Translate;
9
+ readonly dispatcher: Dispatcher;
10
+ }): readonly DataTableBulkAction[];
11
+ /** DataTable `bulk` prop of a list screen; undefined without bulk actions or dispatcher. */
12
+ export declare function useListBulk(options: {
13
+ readonly bulkActions: readonly ListBulkAction[] | undefined;
14
+ readonly rowActions: readonly RowAction[] | undefined;
15
+ readonly translate: Translate;
16
+ readonly dispatcher: Dispatcher | undefined;
17
+ readonly reload: () => Promise<unknown>;
18
+ }): DataTableBulk | undefined;
@@ -0,0 +1,54 @@
1
+ import { evalFieldCondition } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import { resolveActionIcon } from "@cosmicdrift/kumiko-types/action-icon";
3
+ import { useMemo } from "react";
4
+ import { isWriteHandlerRowAction, refetchAfterWrite, writeRowAction } from "./row-actions.js";
5
+ import { dispatcherErrorText } from "./write-failed-error.js";
6
+ function asksForConfirm(bulk, rowAction) {
7
+ return (bulk.confirm !== undefined || rowAction.confirm !== undefined || rowAction.style === "danger");
8
+ }
9
+ function buildBulkAction(bulk, rowAction, translate, dispatcher) {
10
+ const label = translate(rowAction.label);
11
+ const visible = rowAction.visible;
12
+ const confirmKey = bulk.confirm ?? "kumiko.list.bulk.confirm";
13
+ return {
14
+ id: bulk.id,
15
+ label,
16
+ ...(rowAction.style !== undefined && { style: rowAction.style }),
17
+ icon: resolveActionIcon(rowAction.id, rowAction.icon),
18
+ ...(asksForConfirm(bulk, rowAction) && {
19
+ confirmText: (count) => translate(confirmKey, { count, action: label }),
20
+ }),
21
+ ...(rowAction.confirmLabel !== undefined && {
22
+ confirmLabel: translate(rowAction.confirmLabel),
23
+ }),
24
+ appliesTo: (row) => visible === undefined || evalFieldCondition(visible, row.values),
25
+ writeRow: async (row) => {
26
+ const result = await writeRowAction(rowAction, row, dispatcher);
27
+ return result.isSuccess
28
+ ? { kind: "succeeded" }
29
+ : { kind: "failed", message: dispatcherErrorText(result.error, translate) };
30
+ },
31
+ };
32
+ }
33
+ /** Resolves each bulk action against its writeHandler row action; one that resolves to nothing is dropped. */
34
+ export function buildListBulkActions(options) {
35
+ const { bulkActions, rowActions, translate, dispatcher } = options;
36
+ return bulkActions.flatMap((bulk) => {
37
+ const rowAction = rowActions?.find((action) => action.id === bulk.rowAction);
38
+ if (rowAction === undefined || !isWriteHandlerRowAction(rowAction))
39
+ return [];
40
+ return [buildBulkAction(bulk, rowAction, translate, dispatcher)];
41
+ });
42
+ }
43
+ /** DataTable `bulk` prop of a list screen; undefined without bulk actions or dispatcher. */
44
+ export function useListBulk(options) {
45
+ const { bulkActions, rowActions, translate, dispatcher, reload } = options;
46
+ return useMemo(() => {
47
+ if (bulkActions === undefined || dispatcher === undefined)
48
+ return undefined;
49
+ const actions = buildListBulkActions({ bulkActions, rowActions, translate, dispatcher });
50
+ if (actions.length === 0)
51
+ return undefined;
52
+ return { actions, reload: () => refetchAfterWrite(reload) };
53
+ }, [bulkActions, rowActions, translate, dispatcher, reload]);
54
+ }
@@ -0,0 +1,9 @@
1
+ import { type ReactNode } from "react";
2
+ /** Screen bodies (and anything embedded in them) are not part of a fallback chain. */
3
+ export declare function StandInChainBoundary({ children }: {
4
+ readonly children: ReactNode;
5
+ }): ReactNode;
6
+ export declare function LockedHeaderScope({ standInScreenId, children, }: {
7
+ readonly standInScreenId?: string;
8
+ readonly children?: ReactNode;
9
+ }): ReactNode;
@@ -0,0 +1,33 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createContext, useContext, useEffect, useState } from "react";
3
+ import { usePageHeaderSlotAvailable } from "../page-header-slot.js";
4
+ import { usePrimitives } from "../primitives.js";
5
+ // The route still names the locked screen; its breadcrumb must not label what stands in for it.
6
+ // With a stand-in the header is labelled with that screen instead, none if it has no title.
7
+ function LockedScreenHeader({ standInScreenId }) {
8
+ const { PageHeader } = usePrimitives();
9
+ const pageHeaderSlotAvailable = usePageHeaderSlotAvailable();
10
+ if (PageHeader === undefined || !pageHeaderSlotAvailable)
11
+ return null;
12
+ return standInScreenId !== undefined ? (_jsx(PageHeader, { hideBreadcrumb: true, breadcrumbScreenId: standInScreenId })) : (_jsx(PageHeader, { hideBreadcrumb: true }));
13
+ }
14
+ // A stand-in can itself be locked and fall back again. The header slot is a single value that
15
+ // every mounted PageHeader overwrites and clears on unmount, so only the outermost scope mounts
16
+ // a header; nested scopes report the screen that finally renders up to it.
17
+ const ReportStandInContext = createContext(undefined);
18
+ /** Screen bodies (and anything embedded in them) are not part of a fallback chain. */
19
+ export function StandInChainBoundary({ children }) {
20
+ return (_jsx(ReportStandInContext.Provider, { value: undefined, children: children }));
21
+ }
22
+ export function LockedHeaderScope({ standInScreenId, children, }) {
23
+ const reportToOuter = useContext(ReportStandInContext);
24
+ const [inner, setInner] = useState(undefined);
25
+ const effectiveScreenId = inner !== undefined ? inner.screenId : standInScreenId;
26
+ useEffect(() => {
27
+ if (reportToOuter === undefined)
28
+ return;
29
+ reportToOuter({ screenId: effectiveScreenId });
30
+ return () => reportToOuter(undefined);
31
+ }, [reportToOuter, effectiveScreenId]);
32
+ return (_jsxs(_Fragment, { children: [reportToOuter === undefined && (_jsx(LockedScreenHeader, { ...(effectiveScreenId !== undefined && { standInScreenId: effectiveScreenId }) })), _jsx(ReportStandInContext.Provider, { value: setInner, children: children })] }));
33
+ }
@@ -1,19 +1,20 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useMemo, useState } from "react";
3
+ import { ListGroupPageBridges } from "../components/list-group-page-bridges.js";
3
4
  import { RenderList } from "../components/render-list.js";
4
5
  import { useOptionalDispatcher } from "../context/dispatcher-context.js";
5
6
  import { useIsEmbeddedScreen } from "../context/embedded-screen-context.js";
6
7
  import { useUserRoles } from "../context/user-roles-context.js";
7
8
  import { useListUrlState } from "../hooks/use-list-url-state.js";
8
- import { useQuery } from "../hooks/use-query.js";
9
- import { useOptionalTimeZone, useTranslation } from "../i18n.js";
10
- import { usePrimitives, } from "../primitives.js";
9
+ import { useTranslation } from "../i18n.js";
10
+ import { usePrimitives } from "../primitives.js";
11
11
  import { screenFillsHeight } from "../screen-fills-height.js";
12
12
  import { DrawerHost, useDrawerAction } from "./action-form-body.js";
13
13
  import { useAppFeatures } from "./app-features-context.js";
14
14
  import { useClientActionRuntime } from "./client-actions.js";
15
+ import { useListBulk } from "./list-bulk-actions.js";
15
16
  import { isListNarrowed, useListEmptyContent } from "./list-empty-content.js";
16
- import { buildDateRangePayload, buildFilterFacets, buildFilterPayload, clampDateRange, mergeReferenceFacetOptions, readDateRange, resolveDateRangeFacets, resolveProjectionFacetSpecs, } from "./list-facets.js";
17
+ import { buildFilterFacets, buildFilterPayload, mergeReferenceFacetOptions, resolveProjectionFacetSpecs, } from "./list-facets.js";
17
18
  import { useNav } from "./nav.js";
18
19
  import { synthesizeProjectionEntity, synthesizeProjectionScreen } from "./projection-list-shim.js";
19
20
  import { QueryErrorBanner, QueryLoadingBanner } from "./query-state-banners.js";
@@ -21,7 +22,10 @@ import { ReferenceFacetBridges } from "./reference-facet-bridge.js";
21
22
  import { useReturnHost } from "./return-to.js";
22
23
  import { buildDefaultEditRowAction, buildProjectionRowActions, buildProjectionToolbarActions, findRowClickAction, runDrawerRowAction, runProjectionRowNavigate, } from "./row-actions.js";
23
24
  import { findEditScreenFor } from "./screen-access.js";
24
- import { buildListQueryPayload, pageSizeOptionsFor } from "./screen-shared.js";
25
+ import { buildListQueryPayload } from "./screen-shared.js";
26
+ import { useProjectionDateRange } from "./use-projection-date-range.js";
27
+ import { listPager, useScreenListRows } from "./use-screen-list-rows.js";
28
+ import { useScreenListTable } from "./use-screen-list-table.js";
25
29
  // ---- projection-list ----
26
30
  // Like entityList, but the list query comes DIRECTLY from `screen.query`
27
31
  // (instead of being derived from an entity) — cross-feature capable.
@@ -82,12 +86,7 @@ export function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
82
86
  const handleProjectionFacetOptions = useCallback((field, options) => setProjectionReferenceFacetOptions((prev) => prev[field] === options ? prev : { ...prev, [field]: options }), []);
83
87
  const resolvedFacetSpecs = useMemo(() => mergeReferenceFacetOptions(facetSpecs, projectionReferenceFacetOptions), [facetSpecs, projectionReferenceFacetOptions]);
84
88
  const filterPayload = useMemo(() => buildFilterPayload(urlState.filters, (field) => facetSpecs.find((spec) => spec.field === field)?.type), [urlState.filters, facetSpecs]);
85
- // dateRange facets bypass `filters` entirely — their two bounds
86
- // become the top-level query params the facet declares, resolved from a
87
- // calendar date to an instant in the viewer's zone.
88
- const timeZone = useOptionalTimeZone();
89
- const dateRangeSpecs = useMemo(() => resolveDateRangeFacets(screen.facets, effectiveTranslate), [screen.facets, effectiveTranslate]);
90
- const dateRangePayload = useMemo(() => buildDateRangePayload(dateRangeSpecs, urlState.filters, timeZone), [dateRangeSpecs, urlState.filters, timeZone]);
89
+ const { dateRangePayload, dateRangeFacets, handleDateRangeChange } = useProjectionDateRange(screen.facets, effectiveTranslate, urlState);
91
90
  const queryPayload = useMemo(() => {
92
91
  const payload = buildListQueryPayload({
93
92
  limit,
@@ -121,16 +120,25 @@ export function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
121
120
  filterPayload,
122
121
  dateRangePayload,
123
122
  ]);
124
- const rowsQuery = useQuery(screen.query, queryPayload, { live: true });
123
+ const facetFilterFields = useMemo(() => (screen.facets ?? []).map((facet) => facet.field), [screen.facets]);
124
+ const listTable = useScreenListTable({
125
+ screen,
126
+ featureName: schema.featureName,
127
+ payload: queryPayload,
128
+ filterFields: facetFilterFields,
129
+ queryType: screen.query,
130
+ pageSize: limit,
131
+ paging: usePager ? "offset" : "cursor",
132
+ translate: effectiveTranslate,
133
+ });
134
+ const listRows = useScreenListRows({
135
+ queryType: screen.query,
136
+ payload: queryPayload,
137
+ infinite: false,
138
+ aggregates: listTable.aggregates,
139
+ });
140
+ const refreshRows = listRows.refreshAfterWrite;
125
141
  const filterFacets = useMemo(() => buildFilterFacets(resolvedFacetSpecs), [resolvedFacetSpecs]);
126
- const dateRangeFacets = useMemo(() => dateRangeSpecs.map((spec) => ({
127
- field: spec.field,
128
- label: spec.label,
129
- ...readDateRange(urlState.filters, spec.field),
130
- })), [dateRangeSpecs, urlState.filters]);
131
- const handleDateRangeChange = useCallback((field, bound, value) => {
132
- urlState.setDateRange(field, clampDateRange(readDateRange(urlState.filters, field), bound, value));
133
- }, [urlState.setDateRange, urlState.filters]);
134
142
  // Entity targets: see EntityListBody.runNavigate for why the resolution happens
135
143
  // in the NavApi impl. Unlike there: NO row["id"]
136
144
  // fallback — projectionList rows come from an arbitrary query projection
@@ -146,7 +154,7 @@ export function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
146
154
  translate: effectiveTranslate,
147
155
  dispatcher,
148
156
  nav,
149
- refetch: rowsQuery.refetch,
157
+ refetch: refreshRows,
150
158
  openDrawer,
151
159
  defaultEditRowAction,
152
160
  host,
@@ -157,7 +165,7 @@ export function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
157
165
  dispatcher,
158
166
  clientActions,
159
167
  nav,
160
- rowsQuery.refetch,
168
+ refreshRows,
161
169
  openDrawer,
162
170
  defaultEditRowAction,
163
171
  host,
@@ -167,7 +175,7 @@ export function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
167
175
  translate: effectiveTranslate,
168
176
  dispatcher,
169
177
  nav,
170
- refetch: rowsQuery.refetch,
178
+ refetch: refreshRows,
171
179
  openDrawer,
172
180
  host,
173
181
  access: { appFeatures, userRoles },
@@ -177,30 +185,37 @@ export function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
177
185
  nav,
178
186
  host,
179
187
  dispatcher,
180
- rowsQuery.refetch,
188
+ refreshRows,
181
189
  openDrawer,
182
190
  appFeatures,
183
191
  userRoles,
184
192
  ]);
193
+ const bulk = useListBulk({
194
+ bulkActions: screen.bulkActions,
195
+ rowActions: screen.rowActions,
196
+ translate: effectiveTranslate,
197
+ dispatcher,
198
+ reload: refreshRows,
199
+ });
185
200
  const emptyContent = useListEmptyContent(screen.emptyState, effectiveTranslate, {
186
201
  nav,
187
202
  host,
188
203
  dispatcher,
189
204
  openDrawer,
190
- onWriteSuccess: rowsQuery.refetch,
205
+ onWriteSuccess: refreshRows,
191
206
  });
192
- if (rowsQuery.loading && rowsQuery.data === null) {
207
+ if (listRows.firstLoad) {
193
208
  return _jsx(QueryLoadingBanner, {});
194
209
  }
195
- if (rowsQuery.error && rowsQuery.data === null) {
196
- return (_jsx(QueryErrorBanner, { error: rowsQuery.error, translate: effectiveTranslate, onRetry: rowsQuery.refetch }));
210
+ if (listRows.error && !listRows.hasData) {
211
+ return (_jsx(QueryErrorBanner, { error: listRows.error, translate: effectiveTranslate, onRetry: listRows.retry }));
197
212
  }
198
213
  // A failed refetch keeps the last good data on screen.
199
- const refetchErrorBanner = rowsQuery.error ? (_jsx(QueryErrorBanner, { error: rowsQuery.error, translate: effectiveTranslate, onRetry: rowsQuery.refetch })) : null;
200
- // rowsQuery.data is typed as PagedRows but the query handler is free to
214
+ const refetchErrorBanner = listRows.error ? (_jsx(QueryErrorBanner, { error: listRows.error, translate: effectiveTranslate, onRetry: listRows.retry })) : null;
215
+ // listRows.page is typed as PagedRows but the query handler is free to
201
216
  // return anything at runtime (a bare array silently rendered an empty
202
217
  // table instead of surfacing the mismatch), so the shape is checked.
203
- if (rowsQuery.data !== null && !Array.isArray(rowsQuery.data.rows)) {
218
+ if (listRows.page !== null && !Array.isArray(listRows.page.rows)) {
204
219
  return (_jsxs(Banner, { padded: true, variant: "error", testId: "kumiko-screen-projection-list-bad-shape", children: ["Screen ", _jsx(Text, { variant: "code", children: screen.id }), " query", " ", _jsx(Text, { variant: "code", children: screen.query }), " did not return a PagedRows envelope; a projectionList query must return ", _jsx(Text, { variant: "code", children: "{ rows, nextCursor }" }), "."] }));
205
220
  }
206
221
  const rowClickAction = findRowClickAction(screen.rowActions);
@@ -214,32 +229,22 @@ export function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
214
229
  // Same pager-construction as EntityListBody: no Pager UI until the
215
230
  // server-provided `total` arrives (guards pagination="pages" without
216
231
  // totalCount support).
217
- const total = rowsQuery.data?.total;
218
- const pager = usePager && total !== undefined
219
- ? {
220
- page: urlState.page,
221
- limit,
222
- total,
223
- onPageChange: urlState.setPage,
224
- pageSizeOptions: pageSizeOptionsFor(limit),
225
- onPageSizeChange: urlState.setPageSize,
226
- }
227
- : undefined;
228
- return (_jsxs(_Fragment, { children: [refetchErrorBanner, _jsx(ReferenceFacetBridges, { specs: facetSpecs, onOptions: handleProjectionFacetOptions }), _jsx(RenderList, { screen: listScreen, entity: entity, rows: rowsQuery.data?.rows ?? [], featureName: schema.featureName, searchable: searchable, narrowed: isListNarrowed(activeSearch, urlState.filters), ...(screen.description !== undefined && {
232
+ const pager = listPager({ usePager, page: listRows.page, limit, urlState });
233
+ return (_jsxs(_Fragment, { children: [refetchErrorBanner, _jsx(ListGroupPageBridges, { ...listTable.aggregates.groupPages }), _jsx(ReferenceFacetBridges, { specs: facetSpecs, onOptions: handleProjectionFacetOptions }), _jsx(RenderList, { screen: listScreen, entity: entity, rows: listRows.rows, featureName: schema.featureName, searchable: searchable, narrowed: isListNarrowed(activeSearch, urlState.filters), ...(screen.description !== undefined && {
229
234
  description: effectiveTranslate(screen.description),
230
235
  }), ...(emptyContent !== undefined && { emptyContent }), searchValue: urlState.q, onSearchChange: urlState.setQ, sort: activeSort, onSortChange: urlState.setSort, ...(isEmbeddedScreen
231
236
  ? { screenPadding: false, chromeless: true }
232
- : { screenPadding: true }), ...(screenFillsHeight(listScreen) && { scrollBody: true }), ...(pager !== undefined && { pager }), ...(rowActions !== undefined && { rowActions }), ...(screen.rowActionMode !== undefined && { rowActionMode: screen.rowActionMode }), ...(toolbarActions !== undefined && { toolbarActions }), ...(translate !== undefined && { translate }), ...(wrappedOnRowClick !== undefined && { onRowClick: wrappedOnRowClick }), ...(filterFacets.length > 0 && {
237
+ : { screenPadding: true }), ...(screenFillsHeight(listScreen) && { scrollBody: true }), ...(pager !== undefined && { pager }), ...(rowActions !== undefined && { rowActions }), ...(screen.rowActionMode !== undefined && { rowActionMode: screen.rowActionMode }), ...(bulk !== undefined && { bulk }), ...(toolbarActions !== undefined && { toolbarActions }), ...(translate !== undefined && { translate }), ...(wrappedOnRowClick !== undefined && { onRowClick: wrappedOnRowClick }), ...(filterFacets.length > 0 && {
233
238
  filterFacets,
234
239
  onFilterChange: urlState.setFilter,
235
240
  }), ...(dateRangeFacets.length > 0 && {
236
241
  dateRangeFacets,
237
242
  onDateRangeChange: handleDateRangeChange,
238
- }), ...((filterFacets.length > 0 || dateRangeFacets.length > 0) && {
243
+ }), ...listTable.props, ...((filterFacets.length > 0 || dateRangeFacets.length > 0) && {
239
244
  filterValues: urlState.filters,
240
245
  onFilterReset: urlState.clearFilters,
241
246
  }) }), _jsx(DrawerHost, { schema: schema, drawerAction: drawerAction, drawerScreen: drawerScreen, ...(drawerInitialValues !== undefined && { drawerInitialValues }), userRoles: userRoles, ...(translate !== undefined && { translate }), onClose: closeDrawer, onSuccess: () => {
242
247
  closeDrawer();
243
- void rowsQuery.refetch();
248
+ void refreshRows();
244
249
  } })] }));
245
250
  }
@@ -21,6 +21,9 @@ export declare function runDrawerRowAction(action: RowActionDrawer, openDrawer:
21
21
  * validator allows at most one per list. */
22
22
  export declare function findRowClickAction(rowActions: readonly RowAction[] | undefined): RowActionNavigate | RowActionDrawer | undefined;
23
23
  export declare function buildDrawerRowAction(action: RowActionDrawer, translate: Translate, openDrawer: OpenDrawer): DataTableRowAction;
24
+ /** The one server write of a writeHandler row action for one row. */
25
+ export declare function writeRowAction(action: RowActionWriteHandler, row: ListRowViewModel, dispatcher: Dispatcher): ReturnType<Dispatcher["write"]>;
26
+ export declare function buildWriteHandlerRowAction(action: RowActionWriteHandler, translate: Translate, refetch: () => Promise<unknown>, dispatcher: Dispatcher): DataTableRowAction;
24
27
  export declare function buildClientActionRowAction(action: RowActionClientAction, translate: Translate, runtime: ClientActionRuntime | undefined, refetch: () => Promise<unknown>): DataTableRowAction;
25
28
  export declare function buildProjectionRowActions(options: {
26
29
  readonly rowActions: readonly RowAction[] | undefined;
@@ -153,7 +153,14 @@ function mergeDefaultEditAction(rowActions, defaultEditRowAction) {
153
153
  ? [defaultEditRowAction, ...(rowActions ?? [])]
154
154
  : (rowActions ?? []);
155
155
  }
156
- function buildWriteHandlerRowAction(action, translate, refetch, dispatcher) {
156
+ /** The one server write of a writeHandler row action for one row. */
157
+ export function writeRowAction(action, row, dispatcher) {
158
+ const payload = action.payload !== undefined
159
+ ? evalRowExtractor(action.payload, row.values)
160
+ : { id: row.values["id"] };
161
+ return dispatcher.write(action.handler, payload);
162
+ }
163
+ export function buildWriteHandlerRowAction(action, translate, refetch, dispatcher) {
157
164
  const writeVisible = action.visible;
158
165
  return {
159
166
  id: action.id,
@@ -166,10 +173,8 @@ function buildWriteHandlerRowAction(action, translate, refetch, dispatcher) {
166
173
  confirmLabel: translate(action.confirmLabel),
167
174
  }),
168
175
  onTrigger: async (row) => {
169
- const payload = action.payload !== undefined
170
- ? evalRowExtractor(action.payload, row.values)
171
- : { id: row.values["id"] };
172
- const result = await dispatcher.write(action.handler, payload);
176
+ const result = await writeRowAction(action, row, dispatcher);
177
+ // write() does not throw: an unthrown failure would close the confirm dialog silently.
173
178
  if (!result.isSuccess) {
174
179
  throw new WriteFailedError(result.error, dispatcherErrorText(result.error, translate));
175
180
  }
@@ -130,7 +130,7 @@ export function formatSummaryParams(prefill, locale) {
130
130
  }
131
131
  // Placeholders of `successMessage`: formatted submitted values, with a
132
132
  // reference field resolved to the chosen record's label. A lookup that fails or
133
- // misses the row (the lookup list is capped) falls back to the raw id.
133
+ // misses the row falls back to the raw id.
134
134
  export async function resolveSuccessMessageParams(screen, featureName, values, dispatcher, locale) {
135
135
  const params = formatSummaryParams(values, locale);
136
136
  await Promise.all(Object.entries(screen.fields).map(async ([name, def]) => {
@@ -139,7 +139,14 @@ export async function resolveSuccessMessageParams(screen, featureName, values, d
139
139
  return;
140
140
  const target = parseRefTarget(def.entity, featureName);
141
141
  const { queryQn, labelKey } = referenceLookupSource(target.featureName, target.entityName, def.labelField ?? "id");
142
- const lookup = await dispatcher.query(queryQn, { limit: REFERENCE_LIST_LOOKUP_LIMIT });
142
+ // Ask for exactly the chosen record so it cannot fall beyond the lookup cap: `ids` is the
143
+ // by-id input of directory sources, `filters` that of entity list handlers. A source that
144
+ // knows neither returns its capped list and the scan below still finds the row if present.
145
+ const lookup = await dispatcher.query(queryQn, {
146
+ limit: REFERENCE_LIST_LOOKUP_LIMIT,
147
+ ids: [raw],
148
+ filters: [{ field: "id", op: "in", value: [raw] }],
149
+ });
143
150
  if (!lookup.isSuccess)
144
151
  return;
145
152
  const row = lookup.data.rows.find((candidate) => candidate["id"] === raw);
@@ -0,0 +1,24 @@
1
+ import type { EntityListScreenDefinition } from "@cosmicdrift/kumiko-types/screen";
2
+ import type { DataTableRangeBound, DataTableRangeFacet } from "../data-table-aggregate-types.ts";
3
+ import { type Translate } from "../headless/index.ts";
4
+ import type { ListUrlStateApi } from "../hooks/use-list-url-state.ts";
5
+ export type RangeFilter = {
6
+ readonly field: string;
7
+ readonly op: "gte" | "lte";
8
+ readonly value: number;
9
+ };
10
+ export type EntityRangeFacets = {
11
+ readonly rangeFacets: readonly DataTableRangeFacet[];
12
+ readonly rangeFilters: readonly RangeFilter[];
13
+ /** Facet keys the remembered list state may keep (`<field>`, bounds hang off it). */
14
+ readonly rangeFields: readonly string[];
15
+ readonly onRangeChange: (field: string, bound: DataTableRangeBound, value: string) => void;
16
+ };
17
+ /** `display: "range"` facets of an entityList: from/to inputs sent as `gte`/`lte`. */
18
+ export declare function useEntityRangeFacets({ fields, screen, featureName, translate, urlState, }: {
19
+ readonly fields: Readonly<Record<string, unknown>>;
20
+ readonly screen: EntityListScreenDefinition;
21
+ readonly featureName: string;
22
+ readonly translate: Translate;
23
+ readonly urlState: Pick<ListUrlStateApi, "filters" | "setFilter">;
24
+ }): EntityRangeFacets;
@@ -0,0 +1,64 @@
1
+ import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
2
+ import { useCallback, useMemo } from "react";
3
+ import { fieldLabelKey } from "../headless/index.js";
4
+ const RANGE_FIELD_TYPES = new Set(["number", "decimal", "money", "bigInt"]);
5
+ // Money is stored and filtered in minor units; the user types the major amount.
6
+ const MONEY_MINOR_UNITS = 100;
7
+ // The bounds live in the facet URL namespace (`<screenId>.f.<field>.min`), so
8
+ // page reset and clearFilters cover them; buildFilterPayload skips the dotted key.
9
+ function rangeUrlField(field, bound) {
10
+ return `${field}.${bound}`;
11
+ }
12
+ function literalCurrencyOf(def) {
13
+ const currency = propertyOf(def, "currency");
14
+ const code = propertyOf(currency, "code");
15
+ return propertyOf(currency, "kind") === "literal" && typeof code === "string" ? code : undefined;
16
+ }
17
+ function resolveRangeSpecs(fields, facets, featureName, entityName, translate) {
18
+ return Object.entries(facets ?? {}).flatMap(([field, config]) => {
19
+ if (config === false || config.display !== "range")
20
+ return [];
21
+ const def = fields[field];
22
+ const type = propertyOf(def, "type");
23
+ if (typeof type !== "string" || !RANGE_FIELD_TYPES.has(type))
24
+ return [];
25
+ const unit = type === "money" ? literalCurrencyOf(def) : undefined;
26
+ return [
27
+ {
28
+ field,
29
+ label: translate(fieldLabelKey(featureName, entityName, field)),
30
+ money: type === "money",
31
+ ...(unit !== undefined && { unit }),
32
+ },
33
+ ];
34
+ });
35
+ }
36
+ function boundOf(urlFilters, field, bound) {
37
+ return urlFilters[rangeUrlField(field, bound)]?.[0] ?? "";
38
+ }
39
+ function rangeFiltersOf(specs, urlFilters) {
40
+ return specs.flatMap((spec) => ["min", "max"].flatMap((bound) => {
41
+ const typed = boundOf(urlFilters, spec.field, bound);
42
+ const amount = Number(typed);
43
+ if (typed === "" || !Number.isFinite(amount))
44
+ return [];
45
+ const value = spec.money ? Math.round(amount * MONEY_MINOR_UNITS) : amount;
46
+ return [{ field: spec.field, op: bound === "min" ? "gte" : "lte", value }];
47
+ }));
48
+ }
49
+ /** `display: "range"` facets of an entityList: from/to inputs sent as `gte`/`lte`. */
50
+ export function useEntityRangeFacets({ fields, screen, featureName, translate, urlState, }) {
51
+ const { filters: urlFilters, setFilter } = urlState;
52
+ const specs = useMemo(() => resolveRangeSpecs(fields, screen.facets, featureName, screen.entity, translate), [fields, screen.facets, featureName, screen.entity, translate]);
53
+ const rangeFacets = useMemo(() => specs.map((spec) => ({
54
+ field: spec.field,
55
+ label: spec.label,
56
+ min: boundOf(urlFilters, spec.field, "min"),
57
+ max: boundOf(urlFilters, spec.field, "max"),
58
+ ...(spec.unit !== undefined && { unit: spec.unit }),
59
+ })), [specs, urlFilters]);
60
+ const rangeFilters = useMemo(() => rangeFiltersOf(specs, urlFilters), [specs, urlFilters]);
61
+ const rangeFields = useMemo(() => specs.map((spec) => spec.field), [specs]);
62
+ const onRangeChange = useCallback((field, bound, value) => setFilter(rangeUrlField(field, bound), value === "" ? [] : [value]), [setFilter]);
63
+ return { rangeFacets, rangeFilters, rangeFields, onRangeChange };
64
+ }
@@ -0,0 +1,12 @@
1
+ import type { ProjectionListScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import type { Translate } from "../headless/index.ts";
3
+ import type { ListUrlStateApi } from "../hooks/use-list-url-state.ts";
4
+ import type { DataTableDateRangeFacet } from "../primitives.tsx";
5
+ /** dateRange facets bypass `filters` entirely: their two bounds become the
6
+ * top-level query params the facet declares, resolved from a calendar date
7
+ * to an instant in the viewer's zone. */
8
+ export declare function useProjectionDateRange(facets: ProjectionListScreenDefinition["facets"], translate: Translate, urlState: ListUrlStateApi): {
9
+ dateRangePayload: Record<string, string>;
10
+ dateRangeFacets: DataTableDateRangeFacet[];
11
+ handleDateRangeChange: (field: string, bound: "from" | "to", value: string) => void;
12
+ };
@@ -0,0 +1,21 @@
1
+ import { useCallback, useMemo } from "react";
2
+ import { useOptionalTimeZone } from "../i18n.js";
3
+ import { buildDateRangePayload, clampDateRange, readDateRange, resolveDateRangeFacets, } from "./list-facets.js";
4
+ /** dateRange facets bypass `filters` entirely: their two bounds become the
5
+ * top-level query params the facet declares, resolved from a calendar date
6
+ * to an instant in the viewer's zone. */
7
+ export function useProjectionDateRange(facets, translate, urlState) {
8
+ const timeZone = useOptionalTimeZone();
9
+ const dateRangeSpecs = useMemo(() => resolveDateRangeFacets(facets, translate), [facets, translate]);
10
+ const dateRangePayload = useMemo(() => buildDateRangePayload(dateRangeSpecs, urlState.filters, timeZone), [dateRangeSpecs, urlState.filters, timeZone]);
11
+ const dateRangeFacets = useMemo(() => dateRangeSpecs.map((spec) => ({
12
+ field: spec.field,
13
+ label: spec.label,
14
+ ...readDateRange(urlState.filters, spec.field),
15
+ })), [dateRangeSpecs, urlState.filters]);
16
+ const { setDateRange, filters } = urlState;
17
+ const handleDateRangeChange = useCallback((field, bound, value) => {
18
+ setDateRange(field, clampDateRange(readDateRange(filters, field), bound, value));
19
+ }, [setDateRange, filters]);
20
+ return { dateRangePayload, dateRangeFacets, handleDateRangeChange };
21
+ }
@@ -0,0 +1,37 @@
1
+ import type { DispatcherError } from "../headless/index.ts";
2
+ import type { ListAggregates } from "../hooks/use-list-aggregates.ts";
3
+ import type { ListUrlStateApi } from "../hooks/use-list-url-state.ts";
4
+ import type { DataTableProps } from "../primitives.tsx";
5
+ import { type PagedRows } from "./screen-shared.tsx";
6
+ type Row = Readonly<Record<string, unknown>>;
7
+ export type ScreenListRows = {
8
+ readonly rows: readonly Row[];
9
+ /** The ungrouped page answer; null while grouped or before the first answer. */
10
+ readonly page: PagedRows | null;
11
+ readonly firstLoad: boolean;
12
+ readonly error: DispatcherError | null;
13
+ readonly hasData: boolean;
14
+ readonly retry: () => Promise<void>;
15
+ readonly infinite?: {
16
+ readonly onReachEnd: () => void;
17
+ readonly loadingMore: boolean;
18
+ readonly hasMore: boolean;
19
+ };
20
+ readonly refreshAfterWrite: () => Promise<void>;
21
+ };
22
+ /** Rows of a URL-backed list screen: one page, accumulated infinite pages, or the
23
+ * loaded rows of the open server groups. `payload` carries no cursor. */
24
+ export declare function useScreenListRows({ queryType, payload, infinite, aggregates, }: {
25
+ readonly queryType: string;
26
+ readonly payload: Readonly<Record<string, unknown>>;
27
+ readonly infinite: boolean;
28
+ readonly aggregates: ListAggregates;
29
+ }): ScreenListRows;
30
+ /** Pager props for `pagination: "pages"`; none until the server total arrives. */
31
+ export declare function listPager(args: {
32
+ readonly usePager: boolean;
33
+ readonly page: PagedRows | null;
34
+ readonly limit: number;
35
+ readonly urlState: Pick<ListUrlStateApi, "page" | "setPage" | "setPageSize">;
36
+ }): DataTableProps["pager"];
37
+ export {};