@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,16 +1,16 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { evalFieldCondition
|
|
2
|
+
import { evalFieldCondition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
3
3
|
import { resolveActionIcon } from "@cosmicdrift/kumiko-types/action-icon";
|
|
4
4
|
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
5
|
-
import { useCallback, useEffect, useMemo,
|
|
5
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
6
|
+
import { ListGroupPageBridges } from "../components/list-group-page-bridges.js";
|
|
6
7
|
import { RenderList } from "../components/render-list.js";
|
|
7
8
|
import { useOptionalDispatcher } from "../context/dispatcher-context.js";
|
|
8
9
|
import { useIsEmbeddedScreen } from "../context/embedded-screen-context.js";
|
|
9
10
|
import { useUserRoles } from "../context/user-roles-context.js";
|
|
10
11
|
import { rendererWarn } from "../dev-console.js";
|
|
11
|
-
import { computeRelatedListSectionViewModel
|
|
12
|
+
import { computeRelatedListSectionViewModel } from "../headless/index.js";
|
|
12
13
|
import { useListUrlState } from "../hooks/use-list-url-state.js";
|
|
13
|
-
import { useQuery } from "../hooks/use-query.js";
|
|
14
14
|
import { useTranslation } from "../i18n.js";
|
|
15
15
|
import { usePrimitives } from "../primitives.js";
|
|
16
16
|
import { screenFillsHeight } from "../screen-fills-height.js";
|
|
@@ -18,73 +18,21 @@ import { DrawerHost, useDrawerAction } from "./action-form-body.js";
|
|
|
18
18
|
import { useAppFeatures } from "./app-features-context.js";
|
|
19
19
|
import { useClientActionRuntime } from "./client-actions.js";
|
|
20
20
|
import { EntityListExpandedRow } from "./entity-list-expanded-row.js";
|
|
21
|
+
import { resolveEntityFacetSpecs } from "./entity-list-facet-specs.js";
|
|
21
22
|
import { FacetCountBridges } from "./facet-count-bridge.js";
|
|
23
|
+
import { useListBulk } from "./list-bulk-actions.js";
|
|
22
24
|
import { isDefaultFilterActive, isListNarrowed, useListEmptyContent, } from "./list-empty-content.js";
|
|
23
25
|
import { buildEntityFilterFacets, buildFacetCountRequests, buildFilterPayload, mergeReferenceFacetOptions, normalizeDefaultFilters, } from "./list-facets.js";
|
|
24
26
|
import { useNav } from "./nav.js";
|
|
25
27
|
import { QueryErrorBanner, QueryLoadingBanner } from "./query-state-banners.js";
|
|
26
28
|
import { ReferenceFacetBridges } from "./reference-facet-bridge.js";
|
|
27
29
|
import { navigateWithReturnTo, useReturnHost } from "./return-to.js";
|
|
28
|
-
import { buildClientActionRowAction, buildDefaultEditRowAction, buildDrawerRowAction, buildProjectionToolbarActions,
|
|
30
|
+
import { buildClientActionRowAction, buildDefaultEditRowAction, buildDrawerRowAction, buildProjectionToolbarActions, buildWriteHandlerRowAction, findRowClickAction, navigateActionSearchParams, runDrawerRowAction, } from "./row-actions.js";
|
|
29
31
|
import { findEditScreenFor } from "./screen-access.js";
|
|
30
|
-
import { buildListQueryPayload, entityQueryCommand, isEntityEditScreenOfEntity,
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
function resolveEntityFacetSpecs(fields, featureName, entityName, translate) {
|
|
35
|
-
const out = [];
|
|
36
|
-
for (const [field, rawDef] of Object.entries(fields)) {
|
|
37
|
-
// entity.fields is weakly typed at the renderer layer (Record<string,
|
|
38
|
-
// unknown>, deserialized from the schema); boundary cast like buildInitialValues.
|
|
39
|
-
const def = rawDef;
|
|
40
|
-
if (def.filterable !== true)
|
|
41
|
-
continue;
|
|
42
|
-
const label = translate(fieldLabelKey(featureName, entityName, field));
|
|
43
|
-
if (def.type === "select" && Array.isArray(def.options)) {
|
|
44
|
-
out.push({
|
|
45
|
-
field,
|
|
46
|
-
type: "select",
|
|
47
|
-
label,
|
|
48
|
-
options: def.options.map((value) => ({
|
|
49
|
-
value,
|
|
50
|
-
label: translate(fieldOptionLabelKey(featureName, entityName, field, value)),
|
|
51
|
-
})),
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
else if (def.type === "boolean") {
|
|
55
|
-
out.push({
|
|
56
|
-
field,
|
|
57
|
-
type: "boolean",
|
|
58
|
-
label,
|
|
59
|
-
options: [
|
|
60
|
-
{
|
|
61
|
-
value: "true",
|
|
62
|
-
label: translate(fieldOptionLabelKey(featureName, entityName, field, "true")),
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
value: "false",
|
|
66
|
-
label: translate(fieldOptionLabelKey(featureName, entityName, field, "false")),
|
|
67
|
-
},
|
|
68
|
-
],
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
else if (def.type === "reference" && def.entity !== undefined) {
|
|
72
|
-
const target = parseRefTarget(def.entity, featureName);
|
|
73
|
-
out.push({
|
|
74
|
-
field,
|
|
75
|
-
type: "reference",
|
|
76
|
-
label,
|
|
77
|
-
options: [],
|
|
78
|
-
reference: {
|
|
79
|
-
refEntity: target.entityName,
|
|
80
|
-
refFeature: target.featureName,
|
|
81
|
-
labelField: def.labelField ?? "id",
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return out;
|
|
87
|
-
}
|
|
32
|
+
import { buildListQueryPayload, entityQueryCommand, isEntityEditScreenOfEntity, useNavigateToCreateFor, } from "./screen-shared.js";
|
|
33
|
+
import { useEntityRangeFacets } from "./use-entity-range-facets.js";
|
|
34
|
+
import { listPager, useScreenListRows } from "./use-screen-list-rows.js";
|
|
35
|
+
import { useScreenListTable } from "./use-screen-list-table.js";
|
|
88
36
|
export function EntityListScreen({ schema, screen, translate, onRowClick, }) {
|
|
89
37
|
const { Banner, Text } = usePrimitives();
|
|
90
38
|
const entity = schema.entities[screen.entity];
|
|
@@ -120,29 +68,17 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
120
68
|
const paginationMode = screen.pagination ?? "pages";
|
|
121
69
|
const usePager = paginationMode === "pages";
|
|
122
70
|
const useInfinite = paginationMode === "infinite";
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// Reset rows + cursor on a sort/q change, before the next useQuery so the
|
|
135
|
-
// reload starts with cursor=undefined.
|
|
136
|
-
useEffect(() => {
|
|
137
|
-
if (!useInfinite)
|
|
138
|
-
return;
|
|
139
|
-
if (sortQRef.current === sortQKey)
|
|
140
|
-
return;
|
|
141
|
-
sortQRef.current = sortQKey;
|
|
142
|
-
setAccumulated([]);
|
|
143
|
-
setCursor(undefined);
|
|
144
|
-
setHasMore(true);
|
|
145
|
-
}, [useInfinite, sortQKey]);
|
|
71
|
+
// Hook order: ALL hooks before the early return for loading/error,
|
|
72
|
+
// otherwise the hook slots collide between renders.
|
|
73
|
+
const t = useTranslation();
|
|
74
|
+
const effectiveTranslate = translate ?? t;
|
|
75
|
+
const range = useEntityRangeFacets({
|
|
76
|
+
fields: entity.fields,
|
|
77
|
+
screen,
|
|
78
|
+
featureName,
|
|
79
|
+
translate: effectiveTranslate,
|
|
80
|
+
urlState,
|
|
81
|
+
});
|
|
146
82
|
// User-selected faceted filters from the URL state -> payload.filters.
|
|
147
83
|
// typeOf reads the field type straight from entity.fields (not gated on
|
|
148
84
|
// filterable, deliberately permissive; see the buildFilterPayload doc).
|
|
@@ -162,7 +98,7 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
162
98
|
usePager,
|
|
163
99
|
page: urlState.page,
|
|
164
100
|
useInfinite,
|
|
165
|
-
cursor,
|
|
101
|
+
cursor: undefined,
|
|
166
102
|
});
|
|
167
103
|
// Screen filter (tier 2.7c), declared by the author on the schema and
|
|
168
104
|
// independent of the user's q search. Several buckets of the same entity
|
|
@@ -171,9 +107,9 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
171
107
|
if (screen.filter !== undefined) {
|
|
172
108
|
payload["filter"] = screen.filter;
|
|
173
109
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
110
|
+
const filters = [...filterPayload, ...range.rangeFilters];
|
|
111
|
+
if (filters.length > 0)
|
|
112
|
+
payload["filters"] = filters;
|
|
177
113
|
return payload;
|
|
178
114
|
}, [
|
|
179
115
|
limit,
|
|
@@ -181,82 +117,51 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
181
117
|
effectiveSort,
|
|
182
118
|
screen.filter,
|
|
183
119
|
filterPayload,
|
|
120
|
+
range.rangeFilters,
|
|
184
121
|
usePager,
|
|
185
122
|
urlState.page,
|
|
186
123
|
useInfinite,
|
|
187
|
-
cursor,
|
|
188
124
|
]);
|
|
189
|
-
const rowsQuery = useQuery(queryType, queryPayload, { live: true });
|
|
190
125
|
// Facet counts come from their own queries, which a write leaves untouched.
|
|
191
126
|
const [facetCountRefreshNonce, setFacetCountRefreshNonce] = useState(0);
|
|
192
127
|
const refreshFacetCounts = useCallback(() => setFacetCountRefreshNonce((n) => n + 1), []);
|
|
193
|
-
const refreshRowsAfterWrite = useCallback(async () => {
|
|
194
|
-
refreshFacetCounts();
|
|
195
|
-
if (useInfinite) {
|
|
196
|
-
setAccumulated([]);
|
|
197
|
-
setCursor(undefined);
|
|
198
|
-
setHasMore(true);
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
await refetchAfterWrite(rowsQuery.refetch);
|
|
202
|
-
}, [useInfinite, rowsQuery.refetch, refreshFacetCounts]);
|
|
203
|
-
// Infinite scroll: on every successful result append the rows and derive
|
|
204
|
-
// hasMore from nextCursor. Live updates (postgres NOTIFY) and initial loads
|
|
205
|
-
// both pass through here; the useEffect dependency array pins the dedup to
|
|
206
|
-
// the last processed data pointer.
|
|
207
|
-
const lastDataRef = useRef(null);
|
|
208
|
-
useEffect(() => {
|
|
209
|
-
if (!useInfinite)
|
|
210
|
-
return;
|
|
211
|
-
const data = rowsQuery.data;
|
|
212
|
-
if (data === null)
|
|
213
|
-
return;
|
|
214
|
-
if (data === lastDataRef.current)
|
|
215
|
-
return;
|
|
216
|
-
lastDataRef.current = data;
|
|
217
|
-
setAccumulated((prev) => {
|
|
218
|
-
// When cursor is undefined this is the first page after a sort/q change:
|
|
219
|
-
// replace everything instead of appending. Otherwise dedupe on id in case
|
|
220
|
-
// live updates bring in an entry on the next page that was already appended.
|
|
221
|
-
if (cursor === undefined)
|
|
222
|
-
return data.rows;
|
|
223
|
-
const seen = new Set(prev.map((r) => r["id"]));
|
|
224
|
-
const fresh = data.rows.filter((r) => !seen.has(r["id"]));
|
|
225
|
-
return [...prev, ...fresh];
|
|
226
|
-
});
|
|
227
|
-
setHasMore(data.nextCursor !== null);
|
|
228
|
-
}, [rowsQuery.data, useInfinite, cursor]);
|
|
229
|
-
const loadMore = useCallback(() => {
|
|
230
|
-
if (!useInfinite)
|
|
231
|
-
return;
|
|
232
|
-
if (rowsQuery.loading)
|
|
233
|
-
return;
|
|
234
|
-
const data = rowsQuery.data;
|
|
235
|
-
if (data?.nextCursor === undefined || data.nextCursor === null)
|
|
236
|
-
return;
|
|
237
|
-
setCursor(data.nextCursor);
|
|
238
|
-
}, [useInfinite, rowsQuery.loading, rowsQuery.data]);
|
|
239
|
-
// RowActions: schema form (handler QN + i18n key) -> resolved form
|
|
240
|
-
// (dispatcher call + translated strings). dispatcher.write knows the user
|
|
241
|
-
// internally (JWT cookie). The schema may contain raw strings as well as
|
|
242
|
-
// i18n keys; translate() returns the key when the bundle does not know it
|
|
243
|
-
// (convention everywhere in the renderer).
|
|
244
|
-
// Hook order: ALL hooks before the early return for loading/error,
|
|
245
|
-
// otherwise the hook slots collide between renders.
|
|
246
|
-
const t = useTranslation();
|
|
247
|
-
const effectiveTranslate = translate ?? t;
|
|
248
128
|
// Faceted filter: one dropdown per filterable select/boolean/reference field.
|
|
249
129
|
// Labels + select option labels use the same i18n convention as the column
|
|
250
130
|
// headers (fieldLabelKey / :option:<value>); reference fields load their
|
|
251
131
|
// options asynchronously through ReferenceFacetBridges.
|
|
252
132
|
const entityFacetSpecs = useMemo(() => resolveEntityFacetSpecs(entity.fields, featureName, screen.entity, effectiveTranslate), [entity.fields, featureName, screen.entity, effectiveTranslate]);
|
|
133
|
+
const facetFilterFields = useMemo(() => [...entityFacetSpecs.map((spec) => spec.field), ...range.rangeFields], [entityFacetSpecs, range.rangeFields]);
|
|
134
|
+
const listTable = useScreenListTable({
|
|
135
|
+
screen,
|
|
136
|
+
featureName,
|
|
137
|
+
payload: queryPayload,
|
|
138
|
+
filterFields: facetFilterFields,
|
|
139
|
+
queryType,
|
|
140
|
+
pageSize: limit,
|
|
141
|
+
paging: useInfinite ? "cursor" : "offset",
|
|
142
|
+
translate: effectiveTranslate,
|
|
143
|
+
});
|
|
144
|
+
const listRows = useScreenListRows({
|
|
145
|
+
queryType,
|
|
146
|
+
payload: queryPayload,
|
|
147
|
+
infinite: useInfinite,
|
|
148
|
+
aggregates: listTable.aggregates,
|
|
149
|
+
});
|
|
150
|
+
const { refreshAfterWrite: refreshListAfterWrite } = listRows;
|
|
151
|
+
const refreshRowsAfterWrite = useCallback(async () => {
|
|
152
|
+
refreshFacetCounts();
|
|
153
|
+
await refreshListAfterWrite();
|
|
154
|
+
}, [refreshFacetCounts, refreshListAfterWrite]);
|
|
253
155
|
const [entityReferenceFacetOptions, setEntityReferenceFacetOptions] = useState({});
|
|
254
156
|
const handleEntityFacetOptions = useCallback((field, options) => setEntityReferenceFacetOptions((prev) => prev[field] === options ? prev : { ...prev, [field]: options }), []);
|
|
255
157
|
const [facetCounts, setFacetCounts] = useState({});
|
|
256
158
|
const handleFacetCount = useCallback((key, count) => setFacetCounts((prev) => (prev[key] === count ? prev : { ...prev, [key]: count })), []);
|
|
257
159
|
const filterFacets = useMemo(() => buildEntityFilterFacets(mergeReferenceFacetOptions(entityFacetSpecs, entityReferenceFacetOptions), screen.facets, effectiveTranslate, facetCounts), [entityFacetSpecs, entityReferenceFacetOptions, screen.facets, effectiveTranslate, facetCounts]);
|
|
258
160
|
const facetCountQueries = useMemo(() => buildFacetCountRequests(entityFacetSpecs, screen.facets, effectiveTranslate).map((request) => {
|
|
259
|
-
const filters =
|
|
161
|
+
const filters = [
|
|
162
|
+
...buildFilterPayload({ ...urlState.filters, [request.field]: request.values }, (field) => entity.fields[field]?.type),
|
|
163
|
+
...range.rangeFilters,
|
|
164
|
+
];
|
|
260
165
|
return {
|
|
261
166
|
key: request.key,
|
|
262
167
|
payload: {
|
|
@@ -275,6 +180,7 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
275
180
|
urlState.filters,
|
|
276
181
|
urlState.q,
|
|
277
182
|
entity.fields,
|
|
183
|
+
range.rangeFilters,
|
|
278
184
|
]);
|
|
279
185
|
// Soft dispatcher: in tests that mount without a DispatcherProvider,
|
|
280
186
|
// rowActions stays undefined instead of crashing. Real apps have the
|
|
@@ -371,35 +277,7 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
371
277
|
}
|
|
372
278
|
if (dispatcher === undefined)
|
|
373
279
|
return null;
|
|
374
|
-
|
|
375
|
-
const writeActionVisible = writeAction.visible;
|
|
376
|
-
return {
|
|
377
|
-
id: writeAction.id,
|
|
378
|
-
label: effectiveTranslate(writeAction.label),
|
|
379
|
-
style: writeAction.style,
|
|
380
|
-
icon: resolveActionIcon(writeAction.id, writeAction.icon),
|
|
381
|
-
confirm: writeAction.confirm !== undefined ? effectiveTranslate(writeAction.confirm) : undefined,
|
|
382
|
-
confirmLabel: writeAction.confirmLabel !== undefined
|
|
383
|
-
? effectiveTranslate(writeAction.confirmLabel)
|
|
384
|
-
: undefined,
|
|
385
|
-
onTrigger: async (row) => {
|
|
386
|
-
const buildPayload = writeAction.payload;
|
|
387
|
-
const payload = buildPayload !== undefined
|
|
388
|
-
? evalRowExtractor(buildPayload, row.values)
|
|
389
|
-
: { id: row.values["id"] };
|
|
390
|
-
const result = await dispatcher.write(writeAction.handler, payload);
|
|
391
|
-
// write() does not throw: a failure result MUST become an error here,
|
|
392
|
-
// otherwise the confirm dialog closes silently and the user sees "nothing
|
|
393
|
-
// happened" (prod bug 2026-06-07).
|
|
394
|
-
if (!result.isSuccess) {
|
|
395
|
-
throw new WriteFailedError(result.error, dispatcherErrorText(result.error, effectiveTranslate));
|
|
396
|
-
}
|
|
397
|
-
await refreshRowsAfterWrite();
|
|
398
|
-
},
|
|
399
|
-
isVisible: writeActionVisible !== undefined
|
|
400
|
-
? (row) => evalFieldCondition(writeActionVisible, row.values)
|
|
401
|
-
: undefined,
|
|
402
|
-
};
|
|
280
|
+
return buildWriteHandlerRowAction(action, effectiveTranslate, refreshRowsAfterWrite, dispatcher);
|
|
403
281
|
})
|
|
404
282
|
.filter((a) => a !== null);
|
|
405
283
|
}, [
|
|
@@ -412,6 +290,13 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
412
290
|
refreshRowsAfterWrite,
|
|
413
291
|
openDrawer,
|
|
414
292
|
]);
|
|
293
|
+
const bulk = useListBulk({
|
|
294
|
+
bulkActions: screen.bulkActions,
|
|
295
|
+
rowActions: screen.rowActions,
|
|
296
|
+
translate: effectiveTranslate,
|
|
297
|
+
dispatcher,
|
|
298
|
+
reload: refreshRowsAfterWrite,
|
|
299
|
+
});
|
|
415
300
|
// ToolbarActions: Schema → Resolved-Form (analog rowActions), shared with
|
|
416
301
|
// projectionList and relatedList sections via buildProjectionToolbarActions.
|
|
417
302
|
const toolbarActions = useMemo(() => buildProjectionToolbarActions({
|
|
@@ -471,14 +356,14 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
471
356
|
refreshRowsAfterWrite,
|
|
472
357
|
invalidateExpansions,
|
|
473
358
|
]);
|
|
474
|
-
if (
|
|
359
|
+
if (listRows.firstLoad) {
|
|
475
360
|
return _jsx(QueryLoadingBanner, {});
|
|
476
361
|
}
|
|
477
|
-
if (
|
|
478
|
-
return (_jsx(QueryErrorBanner, { error:
|
|
362
|
+
if (listRows.error && !listRows.hasData) {
|
|
363
|
+
return (_jsx(QueryErrorBanner, { error: listRows.error, translate: effectiveTranslate, onRetry: listRows.retry }));
|
|
479
364
|
}
|
|
480
365
|
// A failed refetch keeps the last good data on screen.
|
|
481
|
-
const refetchErrorBanner =
|
|
366
|
+
const refetchErrorBanner = listRows.error ? (_jsx(QueryErrorBanner, { error: listRows.error, translate: effectiveTranslate, onRetry: listRows.retry })) : null;
|
|
482
367
|
// RenderList's onRowClick is a 1-arg callback; KumikoScreen's
|
|
483
368
|
// 2-arg shape (row, entityName) is the public surface — thread the
|
|
484
369
|
// screen's entity through here so callers don't have to re-derive
|
|
@@ -507,41 +392,24 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
507
392
|
// Assemble pager props only for pagination="pages". The server total
|
|
508
393
|
// arrives async; until it is there RenderList renders the table without a
|
|
509
394
|
// pager (the pager has its own total>0 guard).
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
? {
|
|
513
|
-
page: urlState.page,
|
|
514
|
-
limit,
|
|
515
|
-
total,
|
|
516
|
-
onPageChange: urlState.setPage,
|
|
517
|
-
pageSizeOptions: pageSizeOptionsFor(limit),
|
|
518
|
-
onPageSizeChange: urlState.setPageSize,
|
|
519
|
-
}
|
|
520
|
-
: undefined;
|
|
521
|
-
// In infinite mode rows come from accumulated (collected across several
|
|
522
|
-
// useQuery calls), with pages/false from the current result.
|
|
523
|
-
const renderRows = useInfinite ? accumulated : (rowsQuery.data?.rows ?? []);
|
|
524
|
-
return (_jsxs(_Fragment, { children: [refetchErrorBanner, _jsx(ReferenceFacetBridges, { specs: entityFacetSpecs, onOptions: handleEntityFacetOptions }), _jsx(FacetCountBridges, { queryType: queryType, queries: facetCountQueries, onCount: handleFacetCount, refreshNonce: facetCountRefreshNonce }), _jsx(RenderList, { screen: screen, entity: entity, rows: renderRows, featureName: featureName, searchable: searchable, narrowed: isListNarrowed(urlState.q, urlState.filters, defaultFilters), defaultFilterActive: isDefaultFilterActive(urlState.q, urlState.filters, defaultFilters), ...(screen.description !== undefined && {
|
|
395
|
+
const pager = listPager({ usePager, page: listRows.page, limit, urlState });
|
|
396
|
+
return (_jsxs(_Fragment, { children: [refetchErrorBanner, _jsx(ListGroupPageBridges, { ...listTable.aggregates.groupPages }), _jsx(ReferenceFacetBridges, { specs: entityFacetSpecs, onOptions: handleEntityFacetOptions }), _jsx(FacetCountBridges, { queryType: queryType, queries: facetCountQueries, onCount: handleFacetCount, refreshNonce: facetCountRefreshNonce }), _jsx(RenderList, { screen: screen, entity: entity, rows: listRows.rows, featureName: featureName, searchable: searchable, narrowed: isListNarrowed(urlState.q, urlState.filters, defaultFilters), defaultFilterActive: isDefaultFilterActive(urlState.q, urlState.filters, defaultFilters), ...(screen.description !== undefined && {
|
|
525
397
|
description: effectiveTranslate(screen.description),
|
|
526
398
|
}), ...(emptyContent !== undefined && { emptyContent }), searchValue: urlState.q, onSearchChange: urlState.setQ, sort: effectiveSort, onSortChange: urlState.setSort, ...(isEmbeddedScreen
|
|
527
399
|
? { screenPadding: false, chromeless: true }
|
|
528
|
-
: { screenPadding: true }), ...(screenFillsHeight(screen) && { scrollBody: true }), ...(pager !== undefined && { pager }), ...(rowActions !== undefined && { rowActions }), ...(screen.rowActionMode !== undefined && { rowActionMode: screen.rowActionMode }), ...(toolbarActions !== undefined && toolbarActions.length > 0 && { toolbarActions }), ...(
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
}), ...(onCreate !== undefined && { onCreate }), ...(translate !== undefined && { translate }), ...(wrappedOnRowClick !== undefined && { onRowClick: wrappedOnRowClick }), ...(filterFacets.length > 0 && {
|
|
533
|
-
filterFacets,
|
|
400
|
+
: { screenPadding: true }), ...(screenFillsHeight(screen) && { scrollBody: true }), ...(pager !== undefined && { pager }), ...(rowActions !== undefined && { rowActions }), ...(screen.rowActionMode !== undefined && { rowActionMode: screen.rowActionMode }), ...(bulk !== undefined && { bulk }), ...(toolbarActions !== undefined && toolbarActions.length > 0 && { toolbarActions }), ...listRows.infinite, ...(onCreate !== undefined && { onCreate }), ...(translate !== undefined && { translate }), ...(wrappedOnRowClick !== undefined && { onRowClick: wrappedOnRowClick }), ...(filterFacets.length > 0 && { filterFacets, onFilterChange: urlState.setFilter }), ...(range.rangeFacets.length > 0 && {
|
|
401
|
+
rangeFacets: range.rangeFacets,
|
|
402
|
+
onRangeChange: range.onRangeChange,
|
|
403
|
+
}), ...((filterFacets.length > 0 || range.rangeFacets.length > 0) && {
|
|
534
404
|
filterValues: urlState.filters,
|
|
535
|
-
onFilterChange: urlState.setFilter,
|
|
536
405
|
onFilterReset: urlState.clearFilters,
|
|
537
|
-
}), ...(expandableSection !== undefined && {
|
|
406
|
+
}), ...listTable.props, ...(expandableSection !== undefined && {
|
|
538
407
|
expandedRowIds,
|
|
539
408
|
onToggleRowExpanded: toggleRowExpanded,
|
|
540
409
|
renderExpandedRow,
|
|
541
410
|
}) }), _jsx(DrawerHost, { schema: schema, drawerAction: drawerAction, drawerScreen: drawerScreen, ...(drawerInitialValues !== undefined && { drawerInitialValues }), userRoles: userRoles, ...(translate !== undefined && { translate }), onClose: closeDrawer, onSuccess: () => {
|
|
542
411
|
closeDrawer();
|
|
543
412
|
invalidateExpansions();
|
|
544
|
-
|
|
545
|
-
void rowsQuery.refetch();
|
|
413
|
+
void refreshRowsAfterWrite();
|
|
546
414
|
} })] }));
|
|
547
415
|
}
|
|
@@ -43,10 +43,12 @@ export type ExtensionSectionProps = {
|
|
|
43
43
|
* `snapshot.errors` on the field instead of a collective message.
|
|
44
44
|
* Undefined outside entityEdit sections. */
|
|
45
45
|
readonly validate?: () => boolean;
|
|
46
|
-
/** Marks
|
|
47
|
-
*
|
|
46
|
+
/** Marks host form values as saved after the section wrote them itself, so
|
|
47
|
+
* a following navigation does not ask to discard. Pass the field names the
|
|
48
|
+
* section persisted: other host fields keep their unsaved state. Without
|
|
49
|
+
* `fields`, every current host value counts as saved.
|
|
48
50
|
* Undefined outside entityEdit sections and the entityEdit footer slot. */
|
|
49
|
-
readonly markSaved?: () => void;
|
|
51
|
+
readonly markSaved?: (fields?: readonly string[]) => void;
|
|
50
52
|
/** Whether the host form has unsaved changes. Only set in the entityEdit
|
|
51
53
|
* footer slot (`screen.slots.footer`); undefined in all other mounts. */
|
|
52
54
|
readonly hasUnsavedChanges?: boolean;
|
package/dist/app/form-schema.js
CHANGED
|
@@ -29,7 +29,7 @@ function isRequiredValueMissing(spec, field, record, unchangedFrom) {
|
|
|
29
29
|
// — they're fillable, so NO_WIDGET_FIELD_TYPES's "embedded"
|
|
30
30
|
// entry must not exempt them. A statically-`required: true` field
|
|
31
31
|
// with no bound widget is also caught at boot
|
|
32
|
-
// (validateNoWidgetRequiredField in boot-validator/screens.ts).
|
|
32
|
+
// (validateNoWidgetRequiredField in boot-validator/screens-forms.ts).
|
|
33
33
|
if (NO_WIDGET_FIELD_TYPES.includes(field.type) && !isEmbeddedListField(field))
|
|
34
34
|
return false;
|
|
35
35
|
if (isPresent(record[spec.field]))
|
|
@@ -18,7 +18,7 @@ export function HeaderActionsBar({ actions, Button, Dialog, ActionOverflowMenu,
|
|
|
18
18
|
if (compact && PageHeader !== undefined) {
|
|
19
19
|
const compactPrimary = compactHeaderPrimary(actions, preferredPrimaryId);
|
|
20
20
|
const overflowActions = actions.filter((a) => a !== compactPrimary?.action);
|
|
21
|
-
return (_jsxs(_Fragment, { children: [compactPrimary !== undefined && (_jsx(RenderEditActionButton, { action: compactPrimary.action, Button: Button, Dialog: Dialog, iconOnly: compactPrimary.iconOnly,
|
|
21
|
+
return (_jsxs(_Fragment, { children: [compactPrimary !== undefined && (_jsx(RenderEditActionButton, { action: compactPrimary.action, Button: Button, Dialog: Dialog, iconOnly: compactPrimary.iconOnly, onError: onError }, compactPrimary.action.id)), overflowActions.length > 0 && (_jsx(PageHeader, { overflowItems: overflowActions.map(toMenuItem) })), pendingActionDialog] }));
|
|
22
22
|
}
|
|
23
23
|
const maxPlainButtons = collapseAfterPrimary ? 1 : 2;
|
|
24
24
|
if (actions.length <= maxPlainButtons || ActionOverflowMenu === undefined) {
|
|
@@ -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
|
|
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(
|
|
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(
|
|
94
|
+
return (_jsxs(_Fragment, { children: [_jsx(LockedHeaderScope, {}), _jsx(ScreenUnavailableBanner, {})] }));
|
|
103
95
|
}
|
|
104
|
-
return (
|
|
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
|
+
}
|