@cosmicdrift/kumiko-renderer 0.389.0 → 0.390.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/projection-detail-body.js +59 -394
- package/dist/app/projection-detail-header-model.d.ts +11 -0
- package/dist/app/projection-detail-header-model.js +33 -0
- package/dist/app/projection-detail-header-region.d.ts +28 -0
- package/dist/app/projection-detail-header-region.js +81 -0
- package/dist/app/projection-detail-header.d.ts +29 -0
- package/dist/app/projection-detail-header.js +84 -0
- package/dist/app/projection-detail-navigation.d.ts +13 -0
- package/dist/app/projection-detail-navigation.js +91 -0
- package/dist/app/projection-detail-states.d.ts +22 -0
- package/dist/app/projection-detail-states.js +26 -0
- package/dist/app/projection-drawer-host.d.ts +12 -0
- package/dist/app/projection-drawer-host.js +10 -0
- package/dist/app/projection-list-body.d.ts +1 -1
- package/dist/app/projection-list-body.js +69 -189
- package/dist/app/projection-list-render-props.d.ts +56 -0
- package/dist/app/projection-list-render-props.js +47 -0
- package/dist/app/projection-list-render.d.ts +26 -0
- package/dist/app/projection-list-render.js +20 -0
- package/dist/app/projection-list-row-click.d.ts +12 -0
- package/dist/app/projection-list-row-click.js +14 -0
- package/dist/app/use-projection-detail-actions.d.ts +35 -0
- package/dist/app/use-projection-detail-actions.js +156 -0
- package/dist/app/use-projection-detail-data.d.ts +19 -0
- package/dist/app/use-projection-detail-data.js +47 -0
- package/dist/app/use-projection-detail-layout.d.ts +17 -0
- package/dist/app/use-projection-detail-layout.js +23 -0
- package/dist/app/use-projection-list-actions.d.ts +31 -0
- package/dist/app/use-projection-list-actions.js +73 -0
- package/dist/app/use-projection-list-data.d.ts +20 -0
- package/dist/app/use-projection-list-data.js +26 -0
- package/dist/app/use-projection-list-definitions.d.ts +11 -0
- package/dist/app/use-projection-list-definitions.js +20 -0
- package/dist/app/use-projection-list-query.d.ts +28 -0
- package/dist/app/use-projection-list-query.js +83 -0
- package/dist/components/render-edit-banners.d.ts +12 -0
- package/dist/components/render-edit-banners.js +90 -0
- package/dist/components/render-edit-draft-save-helpers.d.ts +19 -0
- package/dist/components/render-edit-draft-save-helpers.js +42 -0
- package/dist/components/render-edit-field-grid.d.ts +4 -0
- package/dist/components/render-edit-field-grid.js +40 -0
- package/dist/components/render-edit-fields-section.d.ts +4 -0
- package/dist/components/render-edit-fields-section.js +76 -0
- package/dist/components/render-edit-form-actions.d.ts +4 -0
- package/dist/components/render-edit-form-actions.js +66 -0
- package/dist/components/render-edit-header-actions.d.ts +11 -0
- package/dist/components/render-edit-header-actions.js +120 -0
- package/dist/components/render-edit-layout-flags.d.ts +29 -0
- package/dist/components/render-edit-layout-flags.js +106 -0
- package/dist/components/render-edit-layout.d.ts +4 -0
- package/dist/components/render-edit-layout.js +79 -0
- package/dist/components/render-edit-render-model.d.ts +63 -0
- package/dist/components/render-edit-render-model.js +14 -0
- package/dist/components/render-edit-section-kinds.d.ts +4 -0
- package/dist/components/render-edit-section-kinds.js +115 -0
- package/dist/components/render-edit-submit-result.d.ts +8 -0
- package/dist/components/render-edit-submit-result.js +77 -0
- package/dist/components/render-edit-submit.d.ts +7 -0
- package/dist/components/render-edit-submit.js +113 -0
- package/dist/components/render-edit-titles.d.ts +17 -0
- package/dist/components/render-edit-titles.js +99 -0
- package/dist/components/render-edit-wizard-navigation.d.ts +11 -0
- package/dist/components/render-edit-wizard-navigation.js +115 -0
- package/dist/components/render-edit.d.ts +2 -2
- package/dist/components/render-edit.js +63 -1128
- package/dist/components/render-field-choice-inputs.d.ts +7 -0
- package/dist/components/render-field-choice-inputs.js +52 -0
- package/dist/components/render-field-control.d.ts +12 -0
- package/dist/components/render-field-control.js +93 -0
- package/dist/components/render-field-display-text.d.ts +4 -0
- package/dist/components/render-field-display-text.js +48 -0
- package/dist/components/render-field-fill-action.d.ts +13 -0
- package/dist/components/render-field-fill-action.js +63 -0
- package/dist/components/render-field-input-shared.d.ts +39 -0
- package/dist/components/render-field-input-shared.js +20 -0
- package/dist/components/render-field-input.d.ts +3 -0
- package/dist/components/render-field-input.js +34 -0
- package/dist/components/render-field-meta.d.ts +6 -0
- package/dist/components/render-field-meta.js +51 -0
- package/dist/components/render-field-read-only.d.ts +11 -0
- package/dist/components/render-field-read-only.js +75 -0
- package/dist/components/render-field-reference.d.ts +15 -0
- package/dist/components/render-field-reference.js +103 -0
- package/dist/components/render-field-scalar-inputs.d.ts +10 -0
- package/dist/components/render-field-scalar-inputs.js +35 -0
- package/dist/components/render-field-text-inputs.d.ts +3 -0
- package/dist/components/render-field-text-inputs.js +34 -0
- package/dist/components/render-field-values.d.ts +18 -0
- package/dist/components/render-field-values.js +111 -0
- package/dist/components/render-field-write-only.d.ts +9 -0
- package/dist/components/render-field-write-only.js +29 -0
- package/dist/components/render-field.d.ts +2 -2
- package/dist/components/render-field.js +39 -670
- package/dist/components/render-list-header-slot.d.ts +5 -0
- package/dist/components/render-list-header-slot.js +30 -0
- package/dist/components/render-list-reference-bridge.d.ts +3 -0
- package/dist/components/render-list-reference-bridge.js +25 -0
- package/dist/components/render-list-reference-columns.d.ts +25 -0
- package/dist/components/render-list-reference-columns.js +137 -0
- package/dist/components/render-list-table-props.d.ts +52 -0
- package/dist/components/render-list-table-props.js +53 -0
- package/dist/components/render-list-toolbar-model.d.ts +42 -0
- package/dist/components/render-list-toolbar-model.js +82 -0
- package/dist/components/render-list-toolbar.d.ts +38 -0
- package/dist/components/render-list-toolbar.js +40 -0
- package/dist/components/render-list-types.d.ts +113 -0
- package/dist/components/render-list-types.js +1 -0
- package/dist/components/render-list.d.ts +4 -113
- package/dist/components/render-list.js +91 -307
- package/dist/components/use-reference-input-state.d.ts +20 -0
- package/dist/components/use-reference-input-state.js +85 -0
- package/dist/components/use-render-edit-controlled-sync.d.ts +3 -0
- package/dist/components/use-render-edit-controlled-sync.js +26 -0
- package/dist/components/use-render-edit-draft-save.d.ts +29 -0
- package/dist/components/use-render-edit-draft-save.js +141 -0
- package/dist/components/use-render-edit-form.d.ts +10 -0
- package/dist/components/use-render-edit-form.js +101 -0
- package/dist/components/use-render-edit-sections.d.ts +15 -0
- package/dist/components/use-render-edit-sections.js +100 -0
- package/dist/components/use-render-edit-state.d.ts +65 -0
- package/dist/components/use-render-edit-state.js +158 -0
- package/dist/components/use-render-edit-steps.d.ts +13 -0
- package/dist/components/use-render-edit-steps.js +52 -0
- package/dist/components/use-render-list-columns.d.ts +18 -0
- package/dist/components/use-render-list-columns.js +41 -0
- package/dist/components/use-render-list-search.d.ts +4 -0
- package/dist/components/use-render-list-search.js +50 -0
- package/dist/primitives.d.ts +1 -1
- package/dist/web/app/dashboard-body.d.ts +1 -1
- package/dist/web/app/dashboard-body.js +42 -246
- package/dist/web/app/dashboard-header.d.ts +34 -0
- package/dist/web/app/dashboard-header.js +58 -0
- package/dist/web/app/dashboard-screen-controls.d.ts +11 -0
- package/dist/web/app/dashboard-screen-controls.js +42 -0
- package/dist/web/app/dashboard-screen-tiles.d.ts +8 -0
- package/dist/web/app/dashboard-screen-tiles.js +42 -0
- package/dist/web/app/dashboard-visibility.d.ts +42 -0
- package/dist/web/app/dashboard-visibility.js +102 -0
- package/dist/web/app/use-dashboard-screen-state.d.ts +35 -0
- package/dist/web/app/use-dashboard-screen-state.js +69 -0
- package/dist/web/primitives/data-table/actions-column.js +6 -5
- package/dist/web/primitives/data-table/card-body.d.ts +6 -0
- package/dist/web/primitives/data-table/card-body.js +32 -0
- package/dist/web/primitives/data-table/card-row.d.ts +5 -0
- package/dist/web/primitives/data-table/card-row.js +31 -0
- package/dist/web/primitives/data-table/card-view.d.ts +3 -0
- package/dist/web/primitives/data-table/card-view.js +13 -0
- package/dist/web/primitives/data-table/column-resize-handle.js +3 -1
- package/dist/web/primitives/data-table/data-cell-props.d.ts +7 -0
- package/dist/web/primitives/data-table/data-cell-props.js +8 -0
- package/dist/web/primitives/data-table/row-render-parts.d.ts +8 -0
- package/dist/web/primitives/data-table/row-render-parts.js +35 -0
- package/dist/web/primitives/data-table/table-body.d.ts +3 -0
- package/dist/web/primitives/data-table/table-body.js +32 -0
- package/dist/web/primitives/data-table/table-content.d.ts +3 -0
- package/dist/web/primitives/data-table/table-content.js +41 -0
- package/dist/web/primitives/data-table/table-data-cell.d.ts +4 -0
- package/dist/web/primitives/data-table/table-data-cell.js +25 -0
- package/dist/web/primitives/data-table/table-header-row.d.ts +3 -0
- package/dist/web/primitives/data-table/table-header-row.js +21 -0
- package/dist/web/primitives/data-table/table-view.d.ts +3 -0
- package/dist/web/primitives/data-table/table-view.js +25 -0
- package/dist/web/primitives/data-table/toolbar.d.ts +6 -0
- package/dist/web/primitives/data-table/toolbar.js +53 -0
- package/dist/web/primitives/data-table/use-data-table-state.d.ts +44 -0
- package/dist/web/primitives/data-table/use-data-table-state.js +123 -0
- package/dist/web/primitives/data-table.d.ts +3 -4
- package/dist/web/primitives/data-table.js +32 -252
- package/dist/web/primitives/file-value-list.js +1 -1
- package/dist/web/primitives/form-footer-parts.d.ts +25 -0
- package/dist/web/primitives/form-footer-parts.js +55 -0
- package/dist/web/primitives/form.js +33 -43
- package/dist/web/primitives/input-choice-kinds.d.ts +4 -0
- package/dist/web/primitives/input-choice-kinds.js +67 -0
- package/dist/web/primitives/input-file-kinds.d.ts +3 -0
- package/dist/web/primitives/input-file-kinds.js +6 -0
- package/dist/web/primitives/input-number-kinds.d.ts +4 -0
- package/dist/web/primitives/input-number-kinds.js +16 -0
- package/dist/web/primitives/input-optional-props.d.ts +20 -0
- package/dist/web/primitives/input-optional-props.js +24 -0
- package/dist/web/primitives/input-select-presentation.d.ts +13 -0
- package/dist/web/primitives/input-select-presentation.js +19 -0
- package/dist/web/primitives/input-shared.d.ts +22 -0
- package/dist/web/primitives/input-shared.js +10 -0
- package/dist/web/primitives/input-temporal-kinds.d.ts +5 -0
- package/dist/web/primitives/input-temporal-kinds.js +10 -0
- package/dist/web/primitives/input-text-kinds.d.ts +7 -0
- package/dist/web/primitives/input-text-kinds.js +33 -0
- package/dist/web/primitives/input.js +35 -161
- package/dist/web/widgets/chart-data.d.ts +35 -0
- package/dist/web/widgets/chart-data.js +37 -0
- package/dist/web/widgets/chart-markers.d.ts +26 -0
- package/dist/web/widgets/chart-markers.js +64 -0
- package/dist/web/widgets/chart-parts.d.ts +20 -0
- package/dist/web/widgets/chart-parts.js +15 -0
- package/dist/web/widgets/charts.d.ts +5 -78
- package/dist/web/widgets/charts.js +7 -485
- package/dist/web/widgets/drawer-panel-class.d.ts +3 -0
- package/dist/web/widgets/drawer-panel-class.js +40 -0
- package/dist/web/widgets/drawer-resize-controls.d.ts +11 -0
- package/dist/web/widgets/drawer-resize-controls.js +12 -0
- package/dist/web/widgets/drawer-sizing.d.ts +19 -0
- package/dist/web/widgets/drawer-sizing.js +60 -0
- package/dist/web/widgets/drawer.js +17 -116
- package/dist/web/widgets/stacked-area-brush.d.ts +13 -0
- package/dist/web/widgets/stacked-area-brush.js +91 -0
- package/dist/web/widgets/stacked-area-chart.d.ts +6 -0
- package/dist/web/widgets/stacked-area-chart.js +45 -0
- package/dist/web/widgets/stacked-area-chrome.d.ts +7 -0
- package/dist/web/widgets/stacked-area-chrome.js +77 -0
- package/dist/web/widgets/stacked-area-geometry.d.ts +54 -0
- package/dist/web/widgets/stacked-area-geometry.js +83 -0
- package/dist/web/widgets/stacked-area-model.d.ts +11 -0
- package/dist/web/widgets/stacked-area-model.js +1 -0
- package/dist/web/widgets/stacked-area-plot.d.ts +3 -0
- package/dist/web/widgets/stacked-area-plot.js +51 -0
- package/dist/web/widgets/stacked-area-props.d.ts +83 -0
- package/dist/web/widgets/stacked-area-props.js +16 -0
- package/dist/web/widgets/stacked-area-svg.d.ts +3 -0
- package/dist/web/widgets/stacked-area-svg.js +38 -0
- package/dist/web/widgets/stacked-area-tooltip.d.ts +3 -0
- package/dist/web/widgets/stacked-area-tooltip.js +86 -0
- package/dist/web/widgets/use-drawer-resize.d.ts +22 -0
- package/dist/web/widgets/use-drawer-resize.js +52 -0
- package/dist/web/widgets/use-stacked-area-tooltip.d.ts +9 -0
- package/dist/web/widgets/use-stacked-area-tooltip.js +34 -0
- package/dist/web/widgets/use-stacked-area-window.d.ts +20 -0
- package/dist/web/widgets/use-stacked-area-window.js +76 -0
- package/package.json +9 -9
- package/src/changes.json +54 -0
|
@@ -1,132 +1,24 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
import { resolveActionIcon } from "@cosmicdrift/kumiko-types/action-icon";
|
|
4
|
-
import { Fragment, useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { useState } from "react";
|
|
5
3
|
import { RenderEdit } from "../components/render-edit.js";
|
|
6
|
-
import { resolveRecordTitle } from "../components/render-edit-helpers.js";
|
|
7
4
|
import { useOptionalDispatcher } from "../context/dispatcher-context.js";
|
|
8
5
|
import { useUserRoles } from "../context/user-roles-context.js";
|
|
9
|
-
import { rendererWarn } from "../dev-console.js";
|
|
10
|
-
import { isSafeHref } from "../headless/index.js";
|
|
11
|
-
import { useQuery } from "../hooks/use-query.js";
|
|
12
6
|
import { useTranslation } from "../i18n.js";
|
|
13
|
-
import { requestLeave } from "../leave-guard.js";
|
|
14
7
|
import { usePageHeaderCompact, usePageHeaderSlotAvailable } from "../page-header-slot.js";
|
|
15
|
-
import {
|
|
8
|
+
import { usePrimitives } from "../primitives.js";
|
|
16
9
|
import { screenFillsHeight } from "../screen-fills-height.js";
|
|
17
|
-
import {
|
|
10
|
+
import { useDrawerAction } from "./action-form-body.js";
|
|
18
11
|
import { useAppFeatures } from "./app-features-context.js";
|
|
19
12
|
import { useClientActionRuntime } from "./client-actions.js";
|
|
20
|
-
import { ErrorReference } from "./error-reference.js";
|
|
21
|
-
import { HeaderActionsBar } from "./header-actions-bar.js";
|
|
22
13
|
import { useNav } from "./nav.js";
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
// Reuses runProjectionRowNavigate (row-actions.ts) instead of re-deriving
|
|
32
|
-
// navigate execution — a metric click has the same entity/screen/params
|
|
33
|
-
// shape as a RowActionNavigate, minus the fields (id, label, style) that
|
|
34
|
-
// function never reads.
|
|
35
|
-
function runMetricNavigate(nav, navigate, record, host) {
|
|
36
|
-
// tab alone (no screen/entity) stays on the current record and just
|
|
37
|
-
// activates that tab — no route change, so runProjectionRowNavigate
|
|
38
|
-
// (which always navigates) doesn't apply here.
|
|
39
|
-
if (navigate.screen === undefined && navigate.entity === undefined) {
|
|
40
|
-
const { tab } = navigate;
|
|
41
|
-
if (tab !== undefined)
|
|
42
|
-
requestLeave(() => nav.setSearchParams({ tab }));
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
const base = { kind: "navigate", id: "metric-navigate", label: "" };
|
|
46
|
-
const action = navigate.entity !== undefined
|
|
47
|
-
? {
|
|
48
|
-
...base,
|
|
49
|
-
entity: navigate.entity,
|
|
50
|
-
...(navigate.entityId !== undefined && { entityId: navigate.entityId }),
|
|
51
|
-
...(navigate.params !== undefined && { params: navigate.params }),
|
|
52
|
-
...(navigate.tab !== undefined && { tab: navigate.tab }),
|
|
53
|
-
}
|
|
54
|
-
: navigate.screen !== undefined
|
|
55
|
-
? {
|
|
56
|
-
...base,
|
|
57
|
-
screen: navigate.screen,
|
|
58
|
-
...(navigate.entityId !== undefined && { entityId: navigate.entityId }),
|
|
59
|
-
...(navigate.params !== undefined && { params: navigate.params }),
|
|
60
|
-
...(navigate.tab !== undefined && { tab: navigate.tab }),
|
|
61
|
-
}
|
|
62
|
-
: undefined;
|
|
63
|
-
if (action === undefined)
|
|
64
|
-
return;
|
|
65
|
-
runProjectionRowNavigate(nav, action, { id: "", values: record }, host);
|
|
66
|
-
}
|
|
67
|
-
// Absolute http(s) check for RecordHeaderSpec.subtitleHref — deliberately
|
|
68
|
-
// stricter than isSafeHref (which also allows relative paths and mailto:),
|
|
69
|
-
// since a relative/mailto value here should render as plain text, not a
|
|
70
|
-
// "_blank" external link.
|
|
71
|
-
function isAbsoluteHttpUrl(value) {
|
|
72
|
-
return /^https?:\/\//i.test(value) && isSafeHref(value);
|
|
73
|
-
}
|
|
74
|
-
// hrefFor throws for an entity without a detailFor screen; outside
|
|
75
|
-
// createKumikoApp the feature list is empty, so navigateTargetAllows can't
|
|
76
|
-
// rule that out beforehand. The NavApi owns target resolution, so asking it
|
|
77
|
-
// and falling back to plain text covers both cases.
|
|
78
|
-
function hrefForNavigate(nav, navigate, record) {
|
|
79
|
-
const entityId = navigate.entityId !== undefined ? String(record[navigate.entityId] ?? "") : undefined;
|
|
80
|
-
try {
|
|
81
|
-
if (navigate.entity !== undefined) {
|
|
82
|
-
return entityId === undefined || entityId === ""
|
|
83
|
-
? undefined
|
|
84
|
-
: nav.hrefFor({ entity: navigate.entity, id: entityId });
|
|
85
|
-
}
|
|
86
|
-
if (navigate.screen !== undefined) {
|
|
87
|
-
return nav.hrefFor({
|
|
88
|
-
screenId: navigate.screen,
|
|
89
|
-
...(entityId !== undefined && entityId !== "" && { entityId }),
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
return undefined;
|
|
95
|
-
}
|
|
96
|
-
return undefined;
|
|
97
|
-
}
|
|
98
|
-
function buildSubtitleParts(parts, record, nav, appFeatures, userRoles, host) {
|
|
99
|
-
return parts.flatMap((part) => {
|
|
100
|
-
const field = typeof part === "string" ? part : part.field;
|
|
101
|
-
const text = String(record[field] ?? "");
|
|
102
|
-
if (text === "")
|
|
103
|
-
return [];
|
|
104
|
-
const navigate = typeof part === "string" ? undefined : part.navigate;
|
|
105
|
-
if (navigate === undefined || !navigateTargetAllows(navigate, appFeatures, userRoles)) {
|
|
106
|
-
return [{ field, text }];
|
|
107
|
-
}
|
|
108
|
-
const href = hrefForNavigate(nav, navigate, record);
|
|
109
|
-
if (href === undefined)
|
|
110
|
-
return [{ field, text }];
|
|
111
|
-
return [{ field, text, href, onPress: () => runMetricNavigate(nav, navigate, record, host) }];
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
function headerStatusTone(header, record) {
|
|
115
|
-
if (header.status === undefined)
|
|
116
|
-
return undefined;
|
|
117
|
-
const value = String(record[header.statusToneField ?? header.status] ?? "");
|
|
118
|
-
const declared = header.statusTones !== undefined && Object.hasOwn(header.statusTones, value)
|
|
119
|
-
? header.statusTones[value]
|
|
120
|
-
: undefined;
|
|
121
|
-
const declaredTone = declared !== undefined ? statusToneForOptionTone(declared) : undefined;
|
|
122
|
-
return declaredTone ?? statusToneForValue(value);
|
|
123
|
-
}
|
|
124
|
-
function resolveSubtitleHref(header, record) {
|
|
125
|
-
if (header?.subtitleHref === undefined)
|
|
126
|
-
return undefined;
|
|
127
|
-
const value = record[header.subtitleHref];
|
|
128
|
-
return typeof value === "string" && isAbsoluteHttpUrl(value) ? value : undefined;
|
|
129
|
-
}
|
|
14
|
+
import { deriveDetailRegionPieces, renderDetailHeaderContent, } from "./projection-detail-header-region.js";
|
|
15
|
+
import { renderDetailQueryError, renderMissingIdBanner, renderRecordNotFound, } from "./projection-detail-states.js";
|
|
16
|
+
import { renderProjectionDrawerHost } from "./projection-drawer-host.js";
|
|
17
|
+
import { QueryLoadingBanner } from "./query-state-banners.js";
|
|
18
|
+
import { useReturnHost, useReturnTarget } from "./return-to.js";
|
|
19
|
+
import { useProjectionDetailActions } from "./use-projection-detail-actions.js";
|
|
20
|
+
import { useProjectionDetailData } from "./use-projection-detail-data.js";
|
|
21
|
+
import { useProjectionDetailLayout } from "./use-projection-detail-layout.js";
|
|
130
22
|
// Projection detail body: read-only single-row inspector over an explicit
|
|
131
23
|
// query instead of an entity (see projection-detail-shim.ts for the debt
|
|
132
24
|
// doc). Fetches itself via `screen.query` + `idParam` (like
|
|
@@ -139,67 +31,23 @@ function resolveSubtitleHref(header, record) {
|
|
|
139
31
|
// customSubmit; the no-op makes that path harmless instead of leaving it to
|
|
140
32
|
// chance.
|
|
141
33
|
export function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
142
|
-
const
|
|
34
|
+
const primitives = usePrimitives();
|
|
143
35
|
const t = useTranslation();
|
|
144
36
|
const pageHeaderSlotAvailable = usePageHeaderSlotAvailable();
|
|
145
37
|
const pageHeaderCompactContext = usePageHeaderCompact();
|
|
146
38
|
const effectiveTranslate = translate ?? t;
|
|
147
39
|
const nav = useNav();
|
|
148
|
-
const idParam = screen.idParam ?? "id";
|
|
149
40
|
// Singleton screens ignore whatever id the path happens to carry — the
|
|
150
41
|
// server picks the row from session/context, so forwarding a path id
|
|
151
42
|
// (even a stray/spoofed one) into the query or into extension sections'
|
|
152
43
|
// entityId fallback (resolveExtensionEntityId in render-edit.tsx) would
|
|
153
44
|
// let a client-controlled value leak where none is meant to reach.
|
|
154
45
|
const effectiveEntityId = screen.singleton === true ? undefined : entityId;
|
|
155
|
-
const
|
|
156
|
-
const activeSection = useMemo(() => {
|
|
157
|
-
if (!isTabsMode || Tabs === undefined)
|
|
158
|
-
return undefined;
|
|
159
|
-
const tabParam = nav.searchParams["tab"];
|
|
160
|
-
return (screen.layout.sections.find((section) => section.id === tabParam) ?? screen.layout.sections[0]);
|
|
161
|
-
}, [isTabsMode, Tabs, screen.layout.sections, nav.searchParams]);
|
|
162
|
-
const hasTabs = isTabsMode && Tabs !== undefined && activeSection !== undefined;
|
|
163
|
-
// Tabs is an optional Core-Primitive: without it the screen falls back to
|
|
164
|
-
// the stacked all-sections layout instead of silently truncating to section 1.
|
|
165
|
-
useEffect(() => {
|
|
166
|
-
if (isTabsMode && Tabs === undefined) {
|
|
167
|
-
rendererWarn(`[kumiko] screen "${screen.id}" uses layout.mode: "tabs", but no <Tabs> primitive is registered — the tab strip will not render. createKumikoApp() from kumiko-renderer-web wires it automatically.`);
|
|
168
|
-
}
|
|
169
|
-
}, [isTabsMode, Tabs, screen.id]);
|
|
170
|
-
const entity = useMemo(() => synthesizeProjectionDetailEntity(screen.layout), [screen.layout]);
|
|
171
|
-
const detailScreen = useMemo(() => {
|
|
172
|
-
const source = activeSection !== undefined
|
|
173
|
-
? { ...screen, layout: { ...screen.layout, sections: [activeSection] } }
|
|
174
|
-
: screen;
|
|
175
|
-
return synthesizeProjectionDetailScreen(source);
|
|
176
|
-
}, [screen, activeSection]);
|
|
177
|
-
const detailQuery = useQuery(screen.query, effectiveEntityId !== undefined ? { [idParam]: effectiveEntityId } : {});
|
|
178
|
-
// A writeForm section's handler creates a new record (see EditWriteFormSection's
|
|
179
|
-
// doc) — both the outer record (header/metrics/other sections) and a sibling
|
|
180
|
-
// relatedList section's own independently-fetched rows must reflect it. A
|
|
181
|
-
// query refetch alone only refreshes `record` below; bumping this as
|
|
182
|
-
// <RenderEdit>'s refreshToken makes RelatedListSection's own useQuery
|
|
183
|
-
// re-run too (it has no `live` subscription of its own). Refetch instead of
|
|
184
|
-
// remount (a `key`) so unsaved input in the other writeForm sections and
|
|
185
|
-
// the related lists' local state survive.
|
|
186
|
-
const [reloadNonce, setReloadNonce] = useState(0);
|
|
187
|
-
const reloadDetail = useCallback(async () => {
|
|
188
|
-
await refetchAfterWrite(detailQuery.refetch);
|
|
189
|
-
setReloadNonce((n) => n + 1);
|
|
190
|
-
}, [detailQuery.refetch]);
|
|
46
|
+
const { activeSection, hasTabs, entity, detailScreen, detailQuery, reloadNonce, reloadDetail } = useProjectionDetailData({ screen, Tabs: primitives.Tabs, nav, effectiveEntityId });
|
|
191
47
|
// Header actions render directly via RenderEditActionButton
|
|
192
48
|
// headerActions moved out of RenderEdit's footer — this component owns
|
|
193
49
|
// the failure-surfacing state RenderEdit used to own for them.
|
|
194
50
|
const [actionError, setActionError] = useState(null);
|
|
195
|
-
// Default edit action: resolved cross-feature over ALL mounted
|
|
196
|
-
// features, not just this feature's own schema — detailFor itself is
|
|
197
|
-
// resolved cross-feature by the boot-validator (detail-screens.ts), and
|
|
198
|
-
// the motivating case is a projectionDetail whose query belongs to one
|
|
199
|
-
// feature while the entity's entityEdit screen lives in another. Unlike
|
|
200
|
-
// useNavigateToCreateFor this must NOT filter on allowCreate/singleton —
|
|
201
|
-
// those gate create-targets, we're resolving an update-target by a
|
|
202
|
-
// known id.
|
|
203
51
|
const appFeatures = useAppFeatures();
|
|
204
52
|
const userRoles = useUserRoles();
|
|
205
53
|
const dispatcher = useOptionalDispatcher();
|
|
@@ -208,257 +56,74 @@ export function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
208
56
|
const returnTargetParam = useReturnTarget(screen.id);
|
|
209
57
|
// A singleton has no record route to come back to — same as entityEdit.
|
|
210
58
|
const returnTarget = screen.singleton === true ? undefined : returnTargetParam;
|
|
211
|
-
const
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
// Never return onto the just-deleted record.
|
|
215
|
-
const target = deletedShownRecord && returnTarget?.entityId === effectiveEntityId
|
|
216
|
-
? undefined
|
|
217
|
-
: returnTarget;
|
|
218
|
-
if (action.redirect !== undefined) {
|
|
219
|
-
navigateAfterRedirect({
|
|
220
|
-
nav,
|
|
221
|
-
schema,
|
|
222
|
-
appFeatures,
|
|
223
|
-
returnTarget: target,
|
|
224
|
-
redirect: action.redirect,
|
|
225
|
-
resultData,
|
|
226
|
-
record,
|
|
227
|
-
});
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
navigateToReturnOr(nav, target, () => {
|
|
231
|
-
const listScreen = screen.listScreenId ??
|
|
232
|
-
(screen.detailFor !== undefined
|
|
233
|
-
? findEntityListScreen(schema, screen.detailFor)?.id
|
|
234
|
-
: undefined);
|
|
235
|
-
if (listScreen !== undefined)
|
|
236
|
-
nav.navigate({ screenId: lastSegment(listScreen) });
|
|
237
|
-
else
|
|
238
|
-
void detailQuery.refetch();
|
|
239
|
-
});
|
|
240
|
-
}, [
|
|
241
|
-
detailQuery.data,
|
|
242
|
-
detailQuery.refetch,
|
|
243
|
-
nav,
|
|
59
|
+
const drawer = useDrawerAction(schema);
|
|
60
|
+
const { openDrawer } = drawer;
|
|
61
|
+
const { buildSectionActions, headerActions } = useProjectionDetailActions({
|
|
244
62
|
schema,
|
|
245
|
-
|
|
246
|
-
returnTarget,
|
|
247
|
-
effectiveEntityId,
|
|
248
|
-
screen.listScreenId,
|
|
249
|
-
screen.detailFor,
|
|
250
|
-
]);
|
|
251
|
-
const editScreen = useMemo(() => {
|
|
252
|
-
const detailFor = screen.detailFor;
|
|
253
|
-
if (detailFor === undefined)
|
|
254
|
-
return undefined;
|
|
255
|
-
return findEditScreenFor(detailFor, appFeatures, userRoles);
|
|
256
|
-
}, [appFeatures, screen.detailFor, userRoles]);
|
|
257
|
-
const defaultEditAction = useMemo(() => {
|
|
258
|
-
if (editScreen === undefined)
|
|
259
|
-
return undefined;
|
|
260
|
-
// editScreen.id is registry-qualified ("feature:screen:contact-edit");
|
|
261
|
-
// nav.navigate expects the short form (see useNavigateToCreateFor above).
|
|
262
|
-
const targetScreenId = lastSegment(editScreen.id);
|
|
263
|
-
return {
|
|
264
|
-
id: "edit",
|
|
265
|
-
label: effectiveTranslate("kumiko.actions.edit"),
|
|
266
|
-
icon: resolveActionIcon("edit"),
|
|
267
|
-
onPress: () => {
|
|
268
|
-
const target = {
|
|
269
|
-
screenId: targetScreenId,
|
|
270
|
-
...(effectiveEntityId !== undefined && { entityId: effectiveEntityId }),
|
|
271
|
-
};
|
|
272
|
-
navigateWithReturnTo(nav, target, host);
|
|
273
|
-
},
|
|
274
|
-
};
|
|
275
|
-
}, [editScreen, effectiveTranslate, nav, effectiveEntityId, host]);
|
|
276
|
-
// a navigate-screen action targeting an entityEdit screen for the
|
|
277
|
-
// SAME entity this projectionDetail shows (screen.detailFor) defaults its
|
|
278
|
-
// entityId to the shown record's own id — without this fallback the target
|
|
279
|
-
// opens an empty create-form instead of the shown record, silently.
|
|
280
|
-
// Searched cross-feature, consistent with editScreen above.
|
|
281
|
-
const sameEntityScreenId = useCallback((targetScreen) => {
|
|
282
|
-
const record = detailQuery.data ?? {};
|
|
283
|
-
const targetIsEntityEditSameEntity = screen.detailFor !== undefined &&
|
|
284
|
-
appFeatures.some((feature) => feature.screens.some((s) => s.type === "entityEdit" &&
|
|
285
|
-
s.entity === screen.detailFor &&
|
|
286
|
-
lastSegment(s.id) === targetScreen));
|
|
287
|
-
return targetIsEntityEditSameEntity ? String(record["id"] ?? "") : undefined;
|
|
288
|
-
}, [detailQuery.data, screen.detailFor, appFeatures]);
|
|
289
|
-
const buildSectionActions = useCallback((actions) => buildRecordActions({
|
|
290
|
-
actions,
|
|
291
|
-
record: detailQuery.data ?? {},
|
|
292
|
-
translate: effectiveTranslate,
|
|
293
|
-
nav,
|
|
294
|
-
host,
|
|
295
|
-
dispatcher,
|
|
296
|
-
openDrawer,
|
|
297
|
-
onWriteSuccess: reloadDetail,
|
|
298
|
-
onRecordLeft: handleRecordLeft,
|
|
299
|
-
sameEntityScreenId,
|
|
300
|
-
clientActions,
|
|
301
|
-
}), [
|
|
302
|
-
detailQuery.data,
|
|
303
|
-
reloadDetail,
|
|
63
|
+
screen,
|
|
304
64
|
effectiveTranslate,
|
|
65
|
+
effectiveEntityId,
|
|
305
66
|
nav,
|
|
306
67
|
host,
|
|
307
68
|
dispatcher,
|
|
308
69
|
clientActions,
|
|
70
|
+
appFeatures,
|
|
71
|
+
userRoles,
|
|
72
|
+
returnTarget,
|
|
309
73
|
openDrawer,
|
|
310
|
-
|
|
311
|
-
sameEntityScreenId,
|
|
312
|
-
]);
|
|
313
|
-
const headerActions = useMemo(() => {
|
|
314
|
-
const declaredHasEdit = screen.actions?.some((a) => a.id === "edit") === true;
|
|
315
|
-
const resolved = buildRecordActions({
|
|
316
|
-
actions: screen.actions ?? [],
|
|
317
|
-
record: detailQuery.data ?? {},
|
|
318
|
-
translate: effectiveTranslate,
|
|
319
|
-
nav,
|
|
320
|
-
host,
|
|
321
|
-
dispatcher,
|
|
322
|
-
openDrawer,
|
|
323
|
-
onWriteSuccess: reloadDetail,
|
|
324
|
-
onRecordLeft: handleRecordLeft,
|
|
325
|
-
sameEntityScreenId,
|
|
326
|
-
clientActions,
|
|
327
|
-
});
|
|
328
|
-
if (defaultEditAction === undefined || declaredHasEdit)
|
|
329
|
-
return resolved;
|
|
330
|
-
return [defaultEditAction, ...(resolved ?? [])];
|
|
331
|
-
}, [
|
|
332
|
-
screen.actions,
|
|
333
|
-
defaultEditAction,
|
|
334
|
-
effectiveTranslate,
|
|
335
|
-
nav,
|
|
336
|
-
host,
|
|
337
|
-
dispatcher,
|
|
338
|
-
clientActions,
|
|
339
|
-
detailQuery.data,
|
|
74
|
+
detailQuery,
|
|
340
75
|
reloadDetail,
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
sameEntityScreenId,
|
|
344
|
-
]);
|
|
345
|
-
const hasHeader = screen.header !== undefined;
|
|
346
|
-
const hasMetrics = screen.metrics !== undefined && screen.metrics.length > 0;
|
|
347
|
-
// ?? [] rather than threading `headerActions !== undefined` through every
|
|
348
|
-
// use below — an empty array is a safe no-op for .map/.length.
|
|
349
|
-
const headerActionsList = headerActions ?? [];
|
|
350
|
-
const hasHeaderActions = headerActionsList.length > 0;
|
|
351
|
-
const hasHeaderCard = hasHeader || hasMetrics || hasHeaderActions;
|
|
352
|
-
// A header card (metric band, status, actions) needs more room than the
|
|
353
|
-
// 640px form column; screens with only a record title keep the narrow one.
|
|
354
|
-
const widensScreenFormColumn = hasHeaderCard &&
|
|
355
|
-
!hasTabs &&
|
|
356
|
-
screenFillsHeight(screen) &&
|
|
357
|
-
detailScreen.layout.width === undefined;
|
|
358
|
-
const renderedDetailScreen = useMemo(() => widensScreenFormColumn
|
|
359
|
-
? { ...detailScreen, layout: { ...detailScreen.layout, width: HEADER_CARD_DEFAULT_WIDTH } }
|
|
360
|
-
: detailScreen, [detailScreen, widensScreenFormColumn]);
|
|
76
|
+
});
|
|
77
|
+
const { hasMetrics, headerActionsList, hasHeaderActions, hasHeaderCard, renderedDetailScreen } = useProjectionDetailLayout({ screen, headerActions, hasTabs, detailScreen });
|
|
361
78
|
if (effectiveEntityId === undefined && screen.singleton !== true) {
|
|
362
|
-
return (
|
|
79
|
+
return renderMissingIdBanner(primitives, screen);
|
|
363
80
|
}
|
|
364
81
|
if (detailQuery.loading && detailQuery.data === null) {
|
|
365
82
|
return _jsx(QueryLoadingBanner, {});
|
|
366
83
|
}
|
|
367
84
|
if (detailQuery.error && detailQuery.data === null) {
|
|
368
|
-
return (
|
|
85
|
+
return renderDetailQueryError(detailQuery.error, effectiveTranslate, detailQuery.refetch);
|
|
369
86
|
}
|
|
370
87
|
// A failed refetch keeps the last good record on screen.
|
|
371
|
-
const refetchErrorBanner = detailQuery.error
|
|
88
|
+
const refetchErrorBanner = detailQuery.error
|
|
89
|
+
? renderDetailQueryError(detailQuery.error, effectiveTranslate, detailQuery.refetch)
|
|
90
|
+
: null;
|
|
372
91
|
const record = detailQuery.data;
|
|
373
92
|
if (!record) {
|
|
374
|
-
|
|
375
|
-
(screen.detailFor !== undefined
|
|
376
|
-
? findEntityListScreen(schema, screen.detailFor)?.id
|
|
377
|
-
: undefined);
|
|
378
|
-
return (_jsx(RecordNotFoundState, { translate: effectiveTranslate, ...((returnTarget !== undefined || backListScreen !== undefined) && {
|
|
379
|
-
onBack: () => navigateToReturnOr(nav, returnTarget, () => {
|
|
380
|
-
if (backListScreen !== undefined) {
|
|
381
|
-
nav.navigate({ screenId: lastSegment(backListScreen) });
|
|
382
|
-
}
|
|
383
|
-
}),
|
|
384
|
-
}) }));
|
|
93
|
+
return renderRecordNotFound({ schema, screen, nav, returnTarget, effectiveTranslate });
|
|
385
94
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
// top right) instead of rendering as an unframed block above it.
|
|
409
|
-
const metricItems = screen.metrics?.map((metric) => {
|
|
410
|
-
const field = metric.field;
|
|
411
|
-
const label = effectiveTranslate(metric.label);
|
|
412
|
-
const value = String(record[field] ?? "");
|
|
413
|
-
const testId = `kumiko-screen-projection-detail-metric-${field}`;
|
|
414
|
-
const navigate = metric.navigate;
|
|
415
|
-
const onPress = navigate !== undefined && navigateTargetAllows(navigate, appFeatures, userRoles)
|
|
416
|
-
? () => runMetricNavigate(nav, navigate, record, host)
|
|
417
|
-
: undefined;
|
|
418
|
-
return Metric !== undefined ? (_jsx(Metric, { label: label, value: value, testId: testId, ...(onPress !== undefined && { onPress }) }, field)) : (_jsxs(GridCell, { children: [_jsx(Text, { variant: "small", testId: `${testId}-label`, children: label }), _jsx(Text, { testId: `${testId}-value`, children: value })] }, field));
|
|
419
|
-
});
|
|
420
|
-
// Metrics are a dl band when the primitive exists; the old Grid stays as the fallback.
|
|
421
|
-
const metricsBlock = (subtitle, lead) => MetricBand !== undefined ? (_jsx(MetricBand, { testId: "kumiko-screen-projection-detail-metrics", subtitle: subtitle, ...(lead !== undefined && { lead }), children: metricItems })) : (_jsx(Grid, { columns: screen.metrics?.length ?? 1, testId: "kumiko-screen-projection-detail-metrics", children: metricItems }));
|
|
422
|
-
const usesPageHeaderSlot = pageHeaderSlotAvailable && PageHeader !== undefined;
|
|
423
|
-
const pageHeaderCompact = pageHeaderCompactContext && usesPageHeaderSlot;
|
|
424
|
-
// The phone shell header clips a long status mid-word, so on phones the badge
|
|
425
|
-
// sits in the body instead of the shell header slot.
|
|
426
|
-
const statusBadge = header?.status !== undefined && StatusBadge !== undefined ? (_jsx(StatusBadge, { value: String(record[header.status] ?? ""), tone: headerStatusTone(header, record), testId: "kumiko-screen-projection-detail-status" })) : undefined;
|
|
427
|
-
// Lives in the band's subtitle row so it shares the metrics' horizontal inset.
|
|
428
|
-
const bandSubtitle = pageHeaderCompact && statusBadge !== undefined ? (_jsxs(_Fragment, { children: [_jsx(Grid, { columns: "auto", testId: "kumiko-screen-projection-detail-status-row", children: statusBadge }), headerSubtitleSlot] })) : (headerSubtitleSlot);
|
|
429
|
-
const actionErrorBanner = actionError !== null && (_jsxs(Banner, { variant: "error", testId: "render-edit-action-error", children: [actionError.message, _jsx(ErrorReference, { requestId: actionError.requestId })] }));
|
|
430
|
-
const renderHeaderContent = (headerSlot) => (_jsxs(_Fragment, { children: [usesPageHeaderSlot ? (_jsxs(_Fragment, { children: [_jsx(PageHeader, { ...(header !== undefined && { title: String(record[header.title] ?? "") }), ...(header === undefined && recordTitle !== undefined && { title: recordTitle }), ...(!pageHeaderCompact && statusBadge !== undefined && { status: statusBadge }), ...(hasHeaderActions && {
|
|
431
|
-
actions: (_jsx(Grid, { columns: "auto", testId: "kumiko-screen-projection-detail-actions", children: _jsx(HeaderActionsBar, { actions: headerActionsList, Button: Button, Dialog: Dialog, ActionOverflowMenu: ActionOverflowMenu, onError: setActionError, collapseAfterPrimary: true, compact: pageHeaderCompact, PageHeader: PageHeader, ...(screen.primaryAction !== undefined && {
|
|
432
|
-
preferredPrimaryId: screen.primaryAction,
|
|
433
|
-
}) }) })),
|
|
434
|
-
}) }), MetricBand === undefined && bandSubtitle, MetricBand === undefined && headerSlot, (hasMetrics || bandSubtitle !== undefined || headerSlot !== undefined) &&
|
|
435
|
-
(MetricBand !== undefined || hasMetrics) &&
|
|
436
|
-
metricsBlock(bandSubtitle, MetricBand !== undefined ? headerSlot : undefined), actionErrorBanner] })) : ((hasHeaderCard || headerSlot !== undefined || recordTitle !== undefined) && (_jsxs(Card, { slots: {
|
|
437
|
-
...(headerTitleSlot !== undefined && { title: headerTitleSlot }),
|
|
438
|
-
...(headerTitleSlot === undefined &&
|
|
439
|
-
recordTitle !== undefined && { title: recordTitle }),
|
|
440
|
-
...(headerSubtitleSlot !== undefined && { subtitle: headerSubtitleSlot }),
|
|
441
|
-
...(headerSlot !== undefined && { headerContent: headerSlot }),
|
|
442
|
-
...(hasHeaderActions && { headerActions: headerActionsContent }),
|
|
443
|
-
}, children: [hasMetrics && metricsBlock(), actionErrorBanner] }))), hasTabs && activeSection !== undefined && (_jsx(Tabs, { testId: "kumiko-screen-projection-detail-tabs", items: screen.layout.sections.map((section) => {
|
|
444
|
-
const countField = isWriteFormEditSection(section) ? undefined : section.countField;
|
|
445
|
-
const countValue = countField !== undefined ? record[countField] : undefined;
|
|
446
|
-
const count = typeof countValue === "number" && Number.isFinite(countValue)
|
|
447
|
-
? countValue
|
|
448
|
-
: undefined;
|
|
449
|
-
return {
|
|
450
|
-
id: section.id ?? "",
|
|
451
|
-
label: effectiveTranslate(section.title ?? section.id ?? ""),
|
|
452
|
-
...(count !== undefined && { count }),
|
|
453
|
-
};
|
|
454
|
-
}), activeId: activeSection.id ?? "", onSelect: (id) => requestLeave(() => nav.setSearchParams({ tab: id })) }))] }));
|
|
95
|
+
const regionModel = {
|
|
96
|
+
screen,
|
|
97
|
+
record,
|
|
98
|
+
primitives,
|
|
99
|
+
effectiveTranslate,
|
|
100
|
+
nav,
|
|
101
|
+
appFeatures,
|
|
102
|
+
userRoles,
|
|
103
|
+
host,
|
|
104
|
+
headerActionsList,
|
|
105
|
+
setActionError,
|
|
106
|
+
actionError,
|
|
107
|
+
pageHeaderSlotAvailable,
|
|
108
|
+
pageHeaderCompactContext,
|
|
109
|
+
hasHeaderActions,
|
|
110
|
+
hasMetrics,
|
|
111
|
+
hasHeaderCard,
|
|
112
|
+
hasTabs,
|
|
113
|
+
activeSection,
|
|
114
|
+
};
|
|
115
|
+
const regionPieces = deriveDetailRegionPieces(regionModel);
|
|
116
|
+
const renderHeaderContent = (headerSlot) => renderDetailHeaderContent(regionModel, regionPieces, headerSlot);
|
|
455
117
|
return (_jsxs(_Fragment, { children: [refetchErrorBanner, _jsx(RenderEdit, { refreshToken: reloadNonce, screen: renderedDetailScreen, entity: entity, featureName: schema.featureName, initial: record, entityId: effectiveEntityId, customSubmit: async () => ({ isSuccess: true, validationBlocked: false, data: undefined }), onReload: reloadDetail, onRelatedListDrawerAction: openDrawer, ...(translate !== undefined && { translate }), ...(hasTabs && { hideSectionTitles: true }), ...(screenFillsHeight(screen) && { fillScreenHeight: true }), ...((hasHeaderCard ||
|
|
456
118
|
hasTabs ||
|
|
457
119
|
screen.slots?.header !== undefined ||
|
|
458
120
|
screen.recordTitleField !== undefined) && {
|
|
459
121
|
headerRegion: renderHeaderContent,
|
|
460
|
-
}), buildSectionActions: buildSectionActions, valueDisplay: screen.valueDisplay ?? "text" }, entityId),
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
122
|
+
}), buildSectionActions: buildSectionActions, valueDisplay: screen.valueDisplay ?? "text" }, entityId), renderProjectionDrawerHost({
|
|
123
|
+
schema,
|
|
124
|
+
translate,
|
|
125
|
+
userRoles,
|
|
126
|
+
drawer,
|
|
127
|
+
refreshRows: reloadDetail,
|
|
128
|
+
})] }));
|
|
464
129
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ProjectionDetailScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import type { Translate } from "../headless/index.ts";
|
|
3
|
+
import { type StatusTone } from "../primitives.tsx";
|
|
4
|
+
export declare function headerStatusTone(header: NonNullable<ProjectionDetailScreenDefinition["header"]>, record: Readonly<Record<string, unknown>>): StatusTone | undefined;
|
|
5
|
+
export declare function resolveSubtitleHref(header: ProjectionDetailScreenDefinition["header"], record: Readonly<Record<string, unknown>>): string | undefined;
|
|
6
|
+
export type DetailTabItem = {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly label: string;
|
|
9
|
+
readonly count?: number;
|
|
10
|
+
};
|
|
11
|
+
export declare function buildDetailTabItems(sections: ProjectionDetailScreenDefinition["layout"]["sections"], record: Readonly<Record<string, unknown>>, translate: Translate): DetailTabItem[];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { isWriteFormEditSection } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import { statusToneForOptionTone, statusToneForValue } from "../primitives.js";
|
|
3
|
+
import { isAbsoluteHttpUrl } from "./projection-detail-navigation.js";
|
|
4
|
+
export function headerStatusTone(header, record) {
|
|
5
|
+
// skip: a header without status has no tone
|
|
6
|
+
if (header.status === undefined)
|
|
7
|
+
return undefined;
|
|
8
|
+
const value = String(record[header.statusToneField ?? header.status] ?? "");
|
|
9
|
+
const declared = header.statusTones !== undefined && Object.hasOwn(header.statusTones, value)
|
|
10
|
+
? header.statusTones[value]
|
|
11
|
+
: undefined;
|
|
12
|
+
const declaredTone = declared !== undefined ? statusToneForOptionTone(declared) : undefined;
|
|
13
|
+
return declaredTone ?? statusToneForValue(value);
|
|
14
|
+
}
|
|
15
|
+
export function resolveSubtitleHref(header, record) {
|
|
16
|
+
// skip: no subtitleHref declared
|
|
17
|
+
if (header?.subtitleHref === undefined)
|
|
18
|
+
return undefined;
|
|
19
|
+
const value = record[header.subtitleHref];
|
|
20
|
+
return typeof value === "string" && isAbsoluteHttpUrl(value) ? value : undefined;
|
|
21
|
+
}
|
|
22
|
+
export function buildDetailTabItems(sections, record, translate) {
|
|
23
|
+
return sections.map((section) => {
|
|
24
|
+
const countField = isWriteFormEditSection(section) ? undefined : section.countField;
|
|
25
|
+
const countValue = countField !== undefined ? record[countField] : undefined;
|
|
26
|
+
const count = typeof countValue === "number" && Number.isFinite(countValue) ? countValue : undefined;
|
|
27
|
+
return {
|
|
28
|
+
id: section.id ?? "",
|
|
29
|
+
label: translate(section.title ?? section.id ?? ""),
|
|
30
|
+
...(count !== undefined && { count }),
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ProjectionDetailScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import type { ErrorNotice } from "./error-reference.tsx";
|
|
4
|
+
import { type DetailHeaderModel } from "./projection-detail-header.tsx";
|
|
5
|
+
export type DetailRegionModel = DetailHeaderModel & {
|
|
6
|
+
readonly actionError: ErrorNotice | null;
|
|
7
|
+
readonly pageHeaderSlotAvailable: boolean;
|
|
8
|
+
readonly pageHeaderCompactContext: boolean;
|
|
9
|
+
readonly hasHeaderActions: boolean;
|
|
10
|
+
readonly hasMetrics: boolean;
|
|
11
|
+
readonly hasHeaderCard: boolean;
|
|
12
|
+
readonly hasTabs: boolean;
|
|
13
|
+
readonly activeSection: ProjectionDetailScreenDefinition["layout"]["sections"][number] | undefined;
|
|
14
|
+
};
|
|
15
|
+
export type DetailRegionPieces = {
|
|
16
|
+
readonly PageHeader: DetailHeaderModel["primitives"]["PageHeader"];
|
|
17
|
+
readonly pageHeaderCompact: boolean;
|
|
18
|
+
readonly recordTitle: string | undefined;
|
|
19
|
+
readonly headerTitleSlot: ReactNode;
|
|
20
|
+
readonly headerSubtitleSlot: ReactNode;
|
|
21
|
+
readonly headerActionsContent: ReactNode;
|
|
22
|
+
readonly statusBadge: ReactNode;
|
|
23
|
+
readonly bandSubtitle: ReactNode;
|
|
24
|
+
readonly metricItems: ReactNode[] | undefined;
|
|
25
|
+
readonly actionErrorBanner: ReactNode;
|
|
26
|
+
};
|
|
27
|
+
export declare function deriveDetailRegionPieces(model: DetailRegionModel): DetailRegionPieces;
|
|
28
|
+
export declare function renderDetailHeaderContent(model: DetailRegionModel, pieces: DetailRegionPieces, headerSlot: ReactNode | undefined): ReactNode;
|