@cosmicdrift/kumiko-renderer 0.387.0 → 0.389.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/action-form-body.js +10 -2
- package/dist/app/action-form-support.d.ts +1 -0
- package/dist/app/action-form-support.js +1 -1
- package/dist/app/base-path-context.d.ts +9 -0
- package/dist/app/base-path-context.js +13 -0
- package/dist/app/base-path.d.ts +3 -0
- package/dist/app/base-path.js +27 -0
- package/dist/app/config-edit-body.d.ts +1 -1
- package/dist/app/config-edit-body.js +19 -6
- package/dist/app/config-edit-without-secret-fields.d.ts +2 -0
- package/dist/app/config-edit-without-secret-fields.js +28 -0
- package/dist/app/entity-edit-prefill.d.ts +11 -0
- package/dist/app/entity-edit-prefill.js +9 -4
- package/dist/app/entity-list-facet-specs.d.ts +3 -0
- package/dist/app/entity-list-facet-specs.js +65 -0
- package/dist/app/entity-list-screen.js +74 -206
- package/dist/app/extension-sections.d.ts +5 -3
- package/dist/app/form-schema.js +1 -1
- package/dist/app/header-actions-bar.js +1 -1
- package/dist/app/kumiko-screen.js +6 -14
- package/dist/app/list-bulk-actions.d.ts +18 -0
- package/dist/app/list-bulk-actions.js +54 -0
- package/dist/app/locked-screen-header.d.ts +9 -0
- package/dist/app/locked-screen-header.js +33 -0
- package/dist/app/projection-list-body.js +51 -46
- package/dist/app/row-actions.d.ts +3 -0
- package/dist/app/row-actions.js +10 -5
- package/dist/app/screen-shared.js +9 -2
- package/dist/app/use-entity-range-facets.d.ts +24 -0
- package/dist/app/use-entity-range-facets.js +64 -0
- package/dist/app/use-projection-date-range.d.ts +12 -0
- package/dist/app/use-projection-date-range.js +21 -0
- package/dist/app/use-screen-list-rows.d.ts +37 -0
- package/dist/app/use-screen-list-rows.js +104 -0
- package/dist/app/use-screen-list-table.d.ts +24 -0
- package/dist/app/use-screen-list-table.js +46 -0
- package/dist/components/edit-mounts.d.ts +2 -2
- package/dist/components/embedded-list-field.js +1 -1
- package/dist/components/list-group-page-bridges.d.ts +3 -0
- package/dist/components/list-group-page-bridges.js +31 -0
- package/dist/components/list-state-store.d.ts +16 -0
- package/dist/components/list-state-store.js +31 -0
- package/dist/components/related-list-section.js +51 -90
- package/dist/components/render-edit-action-button.d.ts +1 -3
- package/dist/components/render-edit-action-button.js +2 -2
- package/dist/components/render-edit.js +11 -8
- package/dist/components/render-field.js +11 -7
- package/dist/components/render-list-toolbar-action.d.ts +27 -0
- package/dist/components/render-list-toolbar-action.js +40 -0
- package/dist/components/render-list.d.ts +13 -29
- package/dist/components/render-list.js +72 -67
- package/dist/current-user.d.ts +2 -0
- package/dist/data-table-aggregate-types.d.ts +47 -0
- package/dist/data-table-aggregate-types.js +1 -0
- package/dist/data-table-bulk-types.d.ts +27 -0
- package/dist/data-table-bulk-types.js +1 -0
- package/dist/data-table-facet-types.d.ts +36 -0
- package/dist/data-table-facet-types.js +1 -0
- package/dist/data-table-view-types.d.ts +73 -0
- package/dist/data-table-view-types.js +1 -0
- package/dist/flow-diagram/render.js +9 -1
- package/dist/headless/form/form-controller.js +8 -0
- package/dist/headless/form/types.d.ts +1 -0
- package/dist/headless/format/index.js +89 -64
- package/dist/headless/view-model/edit.js +5 -1
- package/dist/headless/view-model/list.js +184 -149
- package/dist/headless/view-model/types.d.ts +7 -2
- package/dist/hooks/list-column-layout.d.ts +19 -0
- package/dist/hooks/list-column-layout.js +36 -0
- package/dist/hooks/list-group-pages.d.ts +26 -0
- package/dist/hooks/list-group-pages.js +37 -0
- package/dist/hooks/list-reset.d.ts +8 -0
- package/dist/hooks/list-reset.js +15 -0
- package/dist/hooks/list-server-groups.d.ts +39 -0
- package/dist/hooks/list-server-groups.js +84 -0
- package/dist/hooks/list-state-schema.d.ts +28 -0
- package/dist/hooks/list-state-schema.js +76 -0
- package/dist/hooks/live-refetch-scheduler.js +4 -0
- package/dist/hooks/use-bulk-run.d.ts +41 -0
- package/dist/hooks/use-bulk-run.js +65 -0
- package/dist/hooks/use-group-load-state.d.ts +11 -0
- package/dist/hooks/use-group-load-state.js +31 -0
- package/dist/hooks/use-list-aggregates.d.ts +38 -0
- package/dist/hooks/use-list-aggregates.js +107 -0
- package/dist/hooks/use-list-table.d.ts +38 -0
- package/dist/hooks/use-list-table.js +91 -0
- package/dist/hooks/use-list-view-state.d.ts +43 -0
- package/dist/hooks/use-list-view-state.js +161 -0
- package/dist/hooks/use-reference-lookup.d.ts +9 -1
- package/dist/hooks/use-reference-lookup.js +17 -3
- package/dist/hooks/use-related-list-rows.d.ts +27 -0
- package/dist/hooks/use-related-list-rows.js +81 -0
- package/dist/hooks/use-related-list-state.d.ts +15 -0
- package/dist/hooks/use-related-list-state.js +31 -0
- package/dist/hooks/use-remembered-list-params.d.ts +13 -0
- package/dist/hooks/use-remembered-list-params.js +53 -0
- package/dist/hooks/use-remembered-local-list-state.d.ts +26 -0
- package/dist/hooks/use-remembered-local-list-state.js +63 -0
- package/dist/i18n-defaults.js +59 -2
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -0
- package/dist/primitives.d.ts +30 -47
- package/dist/web/app/create-app.d.ts +5 -0
- package/dist/web/app/create-app.js +15 -7
- package/dist/web/app/dashboard-body.js +17 -1
- package/dist/web/app/dashboard-panels.js +12 -2
- package/dist/web/app/history-leave-guard.d.ts +6 -0
- package/dist/web/app/history-leave-guard.js +115 -0
- package/dist/web/app/list-state-store-boot.d.ts +6 -0
- package/dist/web/app/list-state-store-boot.js +15 -0
- package/dist/web/app/nav.js +7 -54
- package/dist/web/app/session-list-state-store.d.ts +15 -0
- package/dist/web/app/session-list-state-store.js +117 -0
- package/dist/web/index.d.ts +2 -1
- package/dist/web/index.js +2 -1
- package/dist/web/layout/shell-header.js +2 -1
- package/dist/web/layout/uniform-bar-controls.d.ts +1 -0
- package/dist/web/layout/uniform-bar-controls.js +4 -0
- package/dist/web/lib/export-xlsx-request.d.ts +11 -0
- package/dist/web/lib/export-xlsx-request.js +46 -0
- package/dist/web/lib/file-ref-download.js +4 -1
- package/dist/web/primitives/action-menu.js +1 -1
- package/dist/web/primitives/code-input.d.ts +6 -0
- package/dist/web/primitives/code-input.js +31 -0
- package/dist/web/primitives/data-table/actions-column.d.ts +16 -0
- package/dist/web/primitives/data-table/actions-column.js +34 -0
- package/dist/web/primitives/data-table/bulk-selection-bar.d.ts +20 -0
- package/dist/web/primitives/data-table/bulk-selection-bar.js +37 -0
- package/dist/web/primitives/data-table/card-sort-select.d.ts +14 -0
- package/dist/web/primitives/data-table/card-sort-select.js +40 -0
- package/dist/web/primitives/data-table/column-layout-mode.d.ts +5 -0
- package/dist/web/primitives/data-table/column-layout-mode.js +10 -0
- package/dist/web/primitives/data-table/column-picker-section.d.ts +7 -0
- package/dist/web/primitives/data-table/column-picker-section.js +35 -0
- package/dist/web/primitives/data-table/column-resize-handle.d.ts +14 -0
- package/dist/web/primitives/data-table/column-resize-handle.js +46 -0
- package/dist/web/primitives/data-table/density.d.ts +14 -0
- package/dist/web/primitives/data-table/density.js +20 -0
- package/dist/web/primitives/data-table/export-button.d.ts +5 -0
- package/dist/web/primitives/data-table/export-button.js +37 -0
- package/dist/web/primitives/data-table/facet-cluster.d.ts +8 -0
- package/dist/web/primitives/data-table/facet-cluster.js +37 -0
- package/dist/web/primitives/data-table/group-band.d.ts +28 -0
- package/dist/web/primitives/data-table/group-band.js +53 -0
- package/dist/web/primitives/data-table/measure-value.d.ts +10 -0
- package/dist/web/primitives/data-table/measure-value.js +28 -0
- package/dist/web/primitives/data-table/pin-section.d.ts +8 -0
- package/dist/web/primitives/data-table/pin-section.js +20 -0
- package/dist/web/primitives/data-table/reset-all-button.d.ts +4 -0
- package/dist/web/primitives/data-table/reset-all-button.js +8 -0
- package/dist/web/primitives/data-table/server-group-header.d.ts +11 -0
- package/dist/web/primitives/data-table/server-group-header.js +51 -0
- package/dist/web/primitives/data-table/sortable-header.d.ts +17 -0
- package/dist/web/primitives/data-table/sortable-header.js +41 -0
- package/dist/web/primitives/data-table/table-chrome.d.ts +13 -0
- package/dist/web/primitives/data-table/table-chrome.js +30 -0
- package/dist/web/primitives/data-table/table-footer.d.ts +7 -0
- package/dist/web/primitives/data-table/table-footer.js +20 -0
- package/dist/web/primitives/data-table/toolbar-end-cluster.d.ts +10 -0
- package/dist/web/primitives/data-table/toolbar-end-cluster.js +13 -0
- package/dist/web/primitives/data-table/totals-footer.d.ts +24 -0
- package/dist/web/primitives/data-table/totals-footer.js +48 -0
- package/dist/web/primitives/data-table/use-bulk-selection.d.ts +21 -0
- package/dist/web/primitives/data-table/use-bulk-selection.js +66 -0
- package/dist/web/primitives/data-table/use-column-pins.d.ts +32 -0
- package/dist/web/primitives/data-table/use-column-pins.js +117 -0
- package/dist/web/primitives/data-table/use-column-sizing.d.ts +22 -0
- package/dist/web/primitives/data-table/use-column-sizing.js +44 -0
- package/dist/web/primitives/data-table/use-measured-column-widths.d.ts +15 -0
- package/dist/web/primitives/data-table/use-measured-column-widths.js +83 -0
- package/dist/web/primitives/data-table/use-row-keyboard-navigation.d.ts +22 -0
- package/dist/web/primitives/data-table/use-row-keyboard-navigation.js +65 -0
- package/dist/web/primitives/data-table/use-row-selection.d.ts +14 -0
- package/dist/web/primitives/data-table/use-row-selection.js +48 -0
- package/dist/web/primitives/data-table/use-view-columns.d.ts +6 -0
- package/dist/web/primitives/data-table/use-view-columns.js +17 -0
- package/dist/web/primitives/data-table/view-menu.d.ts +8 -0
- package/dist/web/primitives/data-table/view-menu.js +45 -0
- package/dist/web/primitives/data-table-cell.d.ts +0 -22
- package/dist/web/primitives/data-table-cell.js +1 -58
- package/dist/web/primitives/data-table.d.ts +1 -1
- package/dist/web/primitives/data-table.js +125 -146
- package/dist/web/primitives/facet-filter.js +1 -1
- package/dist/web/primitives/file-thumbnail.d.ts +3 -0
- package/dist/web/primitives/file-thumbnail.js +5 -0
- package/dist/web/primitives/file-value-list.d.ts +1 -1
- package/dist/web/primitives/file-value-list.js +5 -5
- package/dist/web/primitives/input.js +6 -1
- package/dist/web/primitives/layout-elements.js +6 -4
- package/dist/web/primitives/number-range-facet.d.ts +7 -0
- package/dist/web/primitives/number-range-facet.js +33 -0
- package/dist/web/primitives/page-header.js +3 -1
- package/dist/web/primitives/row-actions.js +3 -3
- package/dist/web/primitives/use-focus-on-mount.d.ts +2 -0
- package/dist/web/primitives/use-focus-on-mount.js +9 -0
- package/dist/web/widgets/bounded-load-cache.d.ts +5 -1
- package/dist/web/widgets/bounded-load-cache.js +49 -21
- package/dist/web/widgets/choice-cards.d.ts +17 -0
- package/dist/web/widgets/choice-cards.js +12 -0
- package/dist/web/widgets/collapsible-section.js +1 -1
- package/dist/web/widgets/detail-list.d.ts +5 -1
- package/dist/web/widgets/detail-list.js +5 -1
- package/dist/web/widgets/index.d.ts +1 -0
- package/dist/web/widgets/index.js +1 -0
- package/dist/web/widgets/pdf-preview.js +10 -5
- package/dist/web/widgets/plan-card.d.ts +2 -0
- package/dist/web/widgets/plan-card.js +1 -1
- package/dist/web/widgets/upload-zone.js +13 -5
- package/package.json +5 -5
- package/src/changes.json +158 -0
|
@@ -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 {
|
|
9
|
-
import {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
205
|
+
onWriteSuccess: refreshRows,
|
|
191
206
|
});
|
|
192
|
-
if (
|
|
207
|
+
if (listRows.firstLoad) {
|
|
193
208
|
return _jsx(QueryLoadingBanner, {});
|
|
194
209
|
}
|
|
195
|
-
if (
|
|
196
|
-
return (_jsx(QueryErrorBanner, { error:
|
|
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 =
|
|
200
|
-
//
|
|
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 (
|
|
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
|
|
218
|
-
|
|
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
|
|
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;
|
package/dist/app/row-actions.js
CHANGED
|
@@ -153,7 +153,14 @@ function mergeDefaultEditAction(rowActions, defaultEditRowAction) {
|
|
|
153
153
|
? [defaultEditRowAction, ...(rowActions ?? [])]
|
|
154
154
|
: (rowActions ?? []);
|
|
155
155
|
}
|
|
156
|
-
|
|
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
|
|
170
|
-
|
|
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
|
|
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
|
-
|
|
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 {};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useQuery } from "../hooks/use-query.js";
|
|
3
|
+
import { refetchAfterWrite } from "./row-actions.js";
|
|
4
|
+
import { pageSizeOptionsFor } from "./screen-shared.js";
|
|
5
|
+
function freshInfiniteState(resetKey) {
|
|
6
|
+
return { resetKey, rows: [], cursor: undefined, hasMore: true };
|
|
7
|
+
}
|
|
8
|
+
// Live updates can bring an entry onto the next page that was already appended.
|
|
9
|
+
function appendUnseen(previous, next) {
|
|
10
|
+
const seen = new Set(previous.map((row) => row["id"]));
|
|
11
|
+
return [...previous, ...next.filter((row) => !seen.has(row["id"]))];
|
|
12
|
+
}
|
|
13
|
+
/** Rows of a URL-backed list screen: one page, accumulated infinite pages, or the
|
|
14
|
+
* loaded rows of the open server groups. `payload` carries no cursor. */
|
|
15
|
+
export function useScreenListRows({ queryType, payload, infinite, aggregates, }) {
|
|
16
|
+
const { grouped, refresh: refreshAggregates } = aggregates;
|
|
17
|
+
const resetKey = JSON.stringify(payload);
|
|
18
|
+
const [infiniteState, setInfiniteState] = useState(() => freshInfiniteState(resetKey));
|
|
19
|
+
const state = infiniteState.resetKey === resetKey ? infiniteState : freshInfiniteState(resetKey);
|
|
20
|
+
if (infiniteState.resetKey !== resetKey)
|
|
21
|
+
setInfiniteState(state);
|
|
22
|
+
const cursor = infinite ? state.cursor : undefined;
|
|
23
|
+
const rowsPayload = useMemo(() => (cursor === undefined ? payload : { ...payload, cursor }), [payload, cursor]);
|
|
24
|
+
const rowsQuery = useQuery(queryType, rowsPayload, { live: true, enabled: !grouped });
|
|
25
|
+
const { data, loading, refetch } = rowsQuery;
|
|
26
|
+
const lastDataRef = useRef(null);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
// skip: only a new answer of an infinite list is appended
|
|
29
|
+
if (!infinite || data === null || data === lastDataRef.current)
|
|
30
|
+
return;
|
|
31
|
+
lastDataRef.current = data;
|
|
32
|
+
setInfiniteState((current) => current.resetKey !== resetKey
|
|
33
|
+
? current
|
|
34
|
+
: {
|
|
35
|
+
...current,
|
|
36
|
+
rows: current.cursor === undefined ? data.rows : appendUnseen(current.rows, data.rows),
|
|
37
|
+
hasMore: data.nextCursor !== null,
|
|
38
|
+
});
|
|
39
|
+
}, [data, infinite, resetKey]);
|
|
40
|
+
const onReachEnd = useCallback(() => {
|
|
41
|
+
// skip: the next page waits for the current answer
|
|
42
|
+
if (loading)
|
|
43
|
+
return;
|
|
44
|
+
const nextCursor = data?.nextCursor;
|
|
45
|
+
// skip: no cursor means the last page is loaded
|
|
46
|
+
if (nextCursor === undefined || nextCursor === null)
|
|
47
|
+
return;
|
|
48
|
+
setInfiniteState((current) => current.resetKey === resetKey ? { ...current, cursor: nextCursor } : current);
|
|
49
|
+
}, [loading, data, resetKey]);
|
|
50
|
+
const refreshAfterWrite = useCallback(async () => {
|
|
51
|
+
refreshAggregates();
|
|
52
|
+
// skip: grouped rows reload through the aggregates refresh
|
|
53
|
+
if (grouped)
|
|
54
|
+
return;
|
|
55
|
+
if (infinite && cursor !== undefined) {
|
|
56
|
+
setInfiniteState(freshInfiniteState(resetKey));
|
|
57
|
+
// skip: starting over refetches the first page
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
await refetchAfterWrite(refetch);
|
|
61
|
+
}, [refreshAggregates, grouped, infinite, cursor, resetKey, refetch]);
|
|
62
|
+
const retry = useCallback(async () => {
|
|
63
|
+
if (grouped)
|
|
64
|
+
refreshAggregates();
|
|
65
|
+
else
|
|
66
|
+
await refetch();
|
|
67
|
+
}, [grouped, refreshAggregates, refetch]);
|
|
68
|
+
if (grouped) {
|
|
69
|
+
return {
|
|
70
|
+
rows: aggregates.rows,
|
|
71
|
+
page: null,
|
|
72
|
+
firstLoad: aggregates.loading,
|
|
73
|
+
error: aggregates.error,
|
|
74
|
+
hasData: aggregates.serverGroups !== undefined,
|
|
75
|
+
retry,
|
|
76
|
+
refreshAfterWrite,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
rows: infinite ? state.rows : (data?.rows ?? []),
|
|
81
|
+
page: data,
|
|
82
|
+
firstLoad: loading && data === null,
|
|
83
|
+
error: rowsQuery.error,
|
|
84
|
+
hasData: data !== null,
|
|
85
|
+
retry,
|
|
86
|
+
...(infinite && { infinite: { onReachEnd, loadingMore: loading, hasMore: state.hasMore } }),
|
|
87
|
+
refreshAfterWrite,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** Pager props for `pagination: "pages"`; none until the server total arrives. */
|
|
91
|
+
export function listPager(args) {
|
|
92
|
+
const { usePager, page, limit, urlState } = args;
|
|
93
|
+
const total = page?.total;
|
|
94
|
+
if (!usePager || total === undefined)
|
|
95
|
+
return undefined;
|
|
96
|
+
return {
|
|
97
|
+
page: urlState.page,
|
|
98
|
+
limit,
|
|
99
|
+
total,
|
|
100
|
+
onPageChange: urlState.setPage,
|
|
101
|
+
pageSizeOptions: pageSizeOptionsFor(limit),
|
|
102
|
+
onPageSizeChange: urlState.setPageSize,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { EntityListScreenDefinition, ProjectionListScreenDefinition } from "@cosmicdrift/kumiko-types/screen";
|
|
2
|
+
import type { Translate } from "../headless/index.ts";
|
|
3
|
+
import type { ListGroupPaging } from "../hooks/list-group-pages.ts";
|
|
4
|
+
import { type ListAggregates } from "../hooks/use-list-aggregates.ts";
|
|
5
|
+
import { type ListTableProps } from "../hooks/use-list-table.ts";
|
|
6
|
+
import type { DataTableTableBlockProps } from "../primitives.tsx";
|
|
7
|
+
type ScreenListTableArgs = {
|
|
8
|
+
readonly screen: EntityListScreenDefinition | ProjectionListScreenDefinition;
|
|
9
|
+
readonly featureName: string;
|
|
10
|
+
/** The list query payload as sent; paging keys are stripped for the export. */
|
|
11
|
+
readonly payload: Readonly<Record<string, unknown>>;
|
|
12
|
+
readonly filterFields: readonly string[];
|
|
13
|
+
readonly queryType: string;
|
|
14
|
+
readonly pageSize: number;
|
|
15
|
+
readonly paging: ListGroupPaging;
|
|
16
|
+
readonly translate: Translate;
|
|
17
|
+
};
|
|
18
|
+
export type ScreenListTable = {
|
|
19
|
+
readonly props: ListTableProps & Pick<DataTableTableBlockProps, "serverGroups" | "totals" | "notice">;
|
|
20
|
+
readonly aggregates: ListAggregates;
|
|
21
|
+
};
|
|
22
|
+
/** Table-block wiring of a URL-backed list screen (entityList, projectionList). */
|
|
23
|
+
export declare function useScreenListTable({ screen, featureName, payload, filterFields, queryType, pageSize, paging, translate, }: ScreenListTableArgs): ScreenListTable;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useListStateStore } from "../components/list-state-store.js";
|
|
3
|
+
import { useListAggregates } from "../hooks/use-list-aggregates.js";
|
|
4
|
+
import { useListTable } from "../hooks/use-list-table.js";
|
|
5
|
+
import { listViewSchemaColumns } from "../hooks/use-list-view-state.js";
|
|
6
|
+
import { useRememberedListParams } from "../hooks/use-remembered-list-params.js";
|
|
7
|
+
/** Table-block wiring of a URL-backed list screen (entityList, projectionList). */
|
|
8
|
+
export function useScreenListTable({ screen, featureName, payload, filterFields, queryType, pageSize, paging, translate, }) {
|
|
9
|
+
const { id: screenId, table, columns } = screen;
|
|
10
|
+
const store = useListStateStore();
|
|
11
|
+
const schemaColumns = useMemo(() => listViewSchemaColumns(columns), [columns]);
|
|
12
|
+
const known = useMemo(() => ({ columnFields: schemaColumns.map((column) => column.field), filterFields }), [schemaColumns, filterFields]);
|
|
13
|
+
useRememberedListParams({
|
|
14
|
+
screenId,
|
|
15
|
+
enabled: table?.rememberState === true,
|
|
16
|
+
store,
|
|
17
|
+
known,
|
|
18
|
+
});
|
|
19
|
+
const exportSource = useMemo(() => ({ screen: `${featureName}:screen:${screenId}`, screenShortId: screenId, payload }), [featureName, screenId, payload]);
|
|
20
|
+
const { props, groupField } = useListTable({
|
|
21
|
+
storeKey: screenId,
|
|
22
|
+
table,
|
|
23
|
+
schemaColumns,
|
|
24
|
+
url: { screenId },
|
|
25
|
+
exportSource,
|
|
26
|
+
});
|
|
27
|
+
const aggregates = useListAggregates({
|
|
28
|
+
queryType,
|
|
29
|
+
payload,
|
|
30
|
+
table,
|
|
31
|
+
groupField,
|
|
32
|
+
pageSize,
|
|
33
|
+
paging,
|
|
34
|
+
translate,
|
|
35
|
+
});
|
|
36
|
+
const { serverGroups, totals, notice } = aggregates;
|
|
37
|
+
return {
|
|
38
|
+
props: {
|
|
39
|
+
...props,
|
|
40
|
+
...(serverGroups !== undefined && { serverGroups }),
|
|
41
|
+
...(totals !== undefined && { totals }),
|
|
42
|
+
...(notice !== undefined && { notice }),
|
|
43
|
+
},
|
|
44
|
+
aggregates,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -10,7 +10,7 @@ export declare function ExtensionSectionMount({ section, entityName: hostEntityN
|
|
|
10
10
|
readonly values?: Readonly<Record<string, unknown>>;
|
|
11
11
|
readonly patch?: (partial: Readonly<Record<string, unknown>>) => void;
|
|
12
12
|
readonly validate?: () => boolean;
|
|
13
|
-
readonly markSaved?: () => void;
|
|
13
|
+
readonly markSaved?: (fields?: readonly string[]) => void;
|
|
14
14
|
readonly hideTitle?: boolean;
|
|
15
15
|
/** section.actions, already resolved into buttons by the caller. */
|
|
16
16
|
readonly actions?: ReactNode;
|
|
@@ -24,7 +24,7 @@ export declare function EditSlotMount({ slot, slotName, screenId, entityName, en
|
|
|
24
24
|
readonly entityName: string;
|
|
25
25
|
readonly entityId: string | null;
|
|
26
26
|
readonly values: Readonly<Record<string, unknown>>;
|
|
27
|
-
readonly markSaved?: () => void;
|
|
27
|
+
readonly markSaved?: (fields?: readonly string[]) => void;
|
|
28
28
|
readonly hasUnsavedChanges?: boolean;
|
|
29
29
|
readonly wizardStep?: {
|
|
30
30
|
readonly index: number;
|
|
@@ -169,7 +169,7 @@ function EmbeddedListFieldInner({ field, id, onChange, allIssues, featureName, }
|
|
|
169
169
|
};
|
|
170
170
|
}
|
|
171
171
|
return {
|
|
172
|
-
field: entry.from.join("+")
|
|
172
|
+
field: `${entry.from.join("+")}:${entry.label}`,
|
|
173
173
|
label: t(entry.label),
|
|
174
174
|
value: entry.from.reduce((sum, name) => sum + sumEmbeddedListColumn(rows, name), 0),
|
|
175
175
|
money: entry.from.every((name) => cells.find((c) => c.field === name)?.type === "money"),
|