@cosmicdrift/kumiko-renderer 0.384.0 → 0.385.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.d.ts +50 -0
- package/dist/app/action-form-body.js +275 -0
- package/dist/app/client-actions.js +2 -2
- package/dist/app/config-edit-body.d.ts +9 -0
- package/dist/app/config-edit-body.js +340 -0
- package/dist/app/entity-edit-prefill.d.ts +27 -0
- package/dist/app/entity-edit-prefill.js +377 -0
- package/dist/app/entity-edit-screen.d.ts +11 -0
- package/dist/app/entity-edit-screen.js +395 -0
- package/dist/app/entity-list-screen.d.ts +10 -0
- package/dist/app/entity-list-screen.js +547 -0
- package/dist/app/kumiko-screen.d.ts +1 -36
- package/dist/app/kumiko-screen.js +16 -2855
- package/dist/app/projection-detail-body.d.ts +12 -0
- package/dist/app/projection-detail-body.js +464 -0
- package/dist/app/projection-detail-shim.d.ts +1 -1
- package/dist/app/projection-detail-shim.js +5 -1
- package/dist/app/projection-list-body.d.ts +10 -0
- package/dist/app/projection-list-body.js +245 -0
- package/dist/app/projection-list-shim.d.ts +2 -1
- package/dist/app/projection-list-shim.js +4 -0
- package/dist/app/reference-facet-bridge.js +2 -2
- package/dist/app/row-actions.js +3 -4
- package/dist/app/screen-shared.d.ts +72 -0
- package/dist/app/screen-shared.js +325 -0
- package/dist/app/secret-mint-body.js +3 -3
- package/dist/components/edit-mounts.d.ts +33 -0
- package/dist/components/edit-mounts.js +53 -0
- package/dist/components/error-boundary.js +2 -2
- package/dist/components/reference-create-dialog.js +1 -1
- package/dist/components/related-list-section.js +2 -2
- package/dist/components/render-edit-helpers.d.ts +29 -0
- package/dist/components/render-edit-helpers.js +121 -0
- package/dist/components/render-edit.d.ts +1 -4
- package/dist/components/render-edit.js +41 -443
- package/dist/components/render-field.js +3 -4
- package/dist/components/render-list.js +2 -2
- package/dist/components/use-wizard-form-draft.d.ts +51 -0
- package/dist/components/use-wizard-form-draft.js +291 -0
- package/dist/dev-console.d.ts +3 -0
- package/dist/dev-console.js +15 -0
- package/dist/headless/contracts/primitives.js +14 -17
- package/dist/headless/format/index.js +3 -3
- package/dist/headless/view-model/edit.d.ts +1 -1
- package/dist/headless/view-model/edit.js +9 -16
- package/dist/headless/view-model/list.d.ts +1 -1
- package/dist/headless/view-model/list.js +3 -8
- package/dist/index.d.ts +2 -1
- package/dist/index.js +5 -5
- package/dist/live/error-mapping.js +3 -4
- package/dist/web/app/create-app.js +5 -8
- package/dist/web/app/dashboard-body.js +4 -599
- package/dist/web/app/dashboard-panels.d.ts +53 -0
- package/dist/web/app/dashboard-panels.js +373 -0
- package/dist/web/app/dashboard-shared.d.ts +90 -0
- package/dist/web/app/dashboard-shared.js +171 -0
- package/dist/web/app/dashboard-text.d.ts +6 -0
- package/dist/web/app/dashboard-text.js +65 -0
- package/dist/web/layout/nav-tree.js +3 -4
- package/dist/web/layout/target-resolver-stub.js +2 -2
- package/dist/web/primitives/banner.d.ts +4 -0
- package/dist/web/primitives/banner.js +28 -0
- package/dist/web/primitives/button.d.ts +7 -0
- package/dist/web/primitives/button.js +48 -0
- package/dist/web/primitives/card-styles.d.ts +8 -0
- package/dist/web/primitives/card-styles.js +18 -0
- package/dist/web/primitives/card.d.ts +4 -0
- package/dist/web/primitives/card.js +41 -0
- package/dist/web/primitives/contexts.d.ts +22 -0
- package/dist/web/primitives/contexts.js +50 -0
- package/dist/web/primitives/data-table-cell.d.ts +76 -0
- package/dist/web/primitives/data-table-cell.js +403 -0
- package/dist/web/primitives/data-table.d.ts +4 -0
- package/dist/web/primitives/data-table.js +279 -0
- package/dist/web/primitives/default-form.d.ts +4 -0
- package/dist/web/primitives/default-form.js +79 -0
- package/dist/web/primitives/facet-filter.d.ts +14 -0
- package/dist/web/primitives/facet-filter.js +39 -0
- package/dist/web/primitives/field.d.ts +63 -0
- package/dist/web/primitives/field.js +181 -0
- package/dist/web/primitives/form.d.ts +53 -0
- package/dist/web/primitives/form.js +252 -0
- package/dist/web/primitives/index.d.ts +7 -30
- package/dist/web/primitives/index.js +27 -2006
- package/dist/web/primitives/input.d.ts +4 -0
- package/dist/web/primitives/input.js +166 -0
- package/dist/web/primitives/layout-elements.d.ts +14 -0
- package/dist/web/primitives/layout-elements.js +194 -0
- package/dist/web/primitives/modal.js +1 -1
- package/dist/web/primitives/pager.d.ts +22 -0
- package/dist/web/primitives/pager.js +89 -0
- package/dist/web/primitives/row-actions.d.ts +13 -0
- package/dist/web/primitives/row-actions.js +178 -0
- package/dist/web/primitives/secret-reveal.d.ts +4 -0
- package/dist/web/primitives/secret-reveal.js +67 -0
- package/package.json +5 -5
- package/src/changes.json +6 -0
|
@@ -1,588 +1,16 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
import { DASHBOARD_STAT_TONES } from "@cosmicdrift/kumiko-types/screen";
|
|
4
|
-
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
5
|
-
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
2
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from "react";
|
|
6
3
|
import { ErrorReference } from "../../app/error-reference.js";
|
|
7
|
-
import {
|
|
8
|
-
import { dispatcherErrorText, EmbeddedScreenProvider, evalVisibleWhen, extensionSectionName, HeaderActionsBar, KumikoScreen, lastSegment, useEmbeddedScreen, useExtensionSectionComponent, useLocale, useNav, usePageHeaderCompact, usePageHeaderSlotAvailable, usePrimitives, useQuery, useRecordActions, useTranslation, useUserRoles, } from "../../index.js";
|
|
4
|
+
import { dispatcherErrorText, EmbeddedScreenProvider, evalVisibleWhen, HeaderActionsBar, KumikoScreen, useEmbeddedScreen, useLocale, useNav, usePageHeaderCompact, usePageHeaderSlotAvailable, usePrimitives, useQuery, useRecordActions, useTranslation, } from "../../index.js";
|
|
9
5
|
import { cn } from "../lib/cn.js";
|
|
10
6
|
import { EmbeddedFormProvider } from "../primitives/index.js";
|
|
11
7
|
import { PageSection } from "../primitives/layout.js";
|
|
12
|
-
import { resolveSafeLocale } from "../primitives/locale-number.js";
|
|
13
|
-
import { formatMoney } from "../primitives/money-input.js";
|
|
14
|
-
import { Skeleton } from "../ui/skeleton.js";
|
|
15
|
-
import { initialWindowSelection, } from "../widgets/chart-window.js";
|
|
16
|
-
import { chartToneColor, SegmentBarChart, StackedAreaChart, StackedBarChart, TimeseriesChart, } from "../widgets/charts.js";
|
|
17
|
-
import { DashboardListTable } from "../widgets/dashboard-list.js";
|
|
18
|
-
import { FeedList } from "../widgets/feed-list.js";
|
|
19
8
|
import { ModeSwitch } from "../widgets/mode-switch.js";
|
|
20
|
-
import { ProgressList } from "../widgets/progress-list.js";
|
|
21
9
|
import { SectionCard } from "../widgets/section-card.js";
|
|
22
|
-
import { StatCard, StatStripCell } from "../widgets/stat.js";
|
|
23
|
-
import { EmptyState, LoadingState } from "../widgets/states.js";
|
|
24
10
|
import { StatusBadge } from "../widgets/status-badge.js";
|
|
11
|
+
import { ChartPanelBody, CustomPanelBody, FeedPanelBody, ListPanelBody, ProgressListPanelBody, StatGroupPanelBody, StatPanelBody, TextPanelBody, } from "./dashboard-panels.js";
|
|
25
12
|
import { resolveRefreshIntervalMs } from "./dashboard-refresh.js";
|
|
26
|
-
import {
|
|
27
|
-
const SKELETON_SEGMENT_WIDTHS = ["100%", "72%", "48%"];
|
|
28
|
-
const WIDE_PANEL = "sm:col-span-2 lg:col-span-4";
|
|
29
|
-
const HALF_PANEL = "sm:col-span-2 lg:col-span-2";
|
|
30
|
-
const CURRENCY_CODE_PATTERN = /^[A-Za-z]{3}$/;
|
|
31
|
-
const HOUR_MS = 3_600_000;
|
|
32
|
-
const DAY_MS = 24 * HOUR_MS;
|
|
33
|
-
const SKELETON_BAR_HEIGHTS = ["40%", "65%", "50%", "85%", "60%", "75%", "45%", "70%"];
|
|
34
|
-
function panelPayload(panel, screenParams) {
|
|
35
|
-
return {
|
|
36
|
-
...(panel.ignoreScreenFilter === true ? {} : screenParams.filterParams),
|
|
37
|
-
...screenParams.rangeParams,
|
|
38
|
-
...screenParams.recordParams,
|
|
39
|
-
...panel.params,
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
const DashboardQueryContext = createContext({
|
|
43
|
-
refreshIntervalMs: undefined,
|
|
44
|
-
register: () => () => { },
|
|
45
|
-
});
|
|
46
|
-
function useRefetchRegistry() {
|
|
47
|
-
const registered = useRef(new Set());
|
|
48
|
-
const register = useCallback((refetch) => {
|
|
49
|
-
registered.current.add(refetch);
|
|
50
|
-
return () => {
|
|
51
|
-
registered.current.delete(refetch);
|
|
52
|
-
};
|
|
53
|
-
}, []);
|
|
54
|
-
const refetchRegistered = useCallback(async () => {
|
|
55
|
-
await Promise.all([...registered.current].map((refetch) => refetch()));
|
|
56
|
-
}, []);
|
|
57
|
-
return { register, refetchRegistered };
|
|
58
|
-
}
|
|
59
|
-
/** Live query of a panel or gate that takes part in the dashboard's refresh:
|
|
60
|
-
* polls at the screen's interval and is refetched by `refetchAll` after an
|
|
61
|
-
* action. A disabled query registers nothing. */
|
|
62
|
-
function useDashboardQuery(type, payload, options = {}) {
|
|
63
|
-
const { refreshIntervalMs, register } = useContext(DashboardQueryContext);
|
|
64
|
-
const enabled = options.enabled ?? true;
|
|
65
|
-
const result = useQuery(type, payload, {
|
|
66
|
-
live: true,
|
|
67
|
-
enabled,
|
|
68
|
-
...(refreshIntervalMs !== undefined && { refetchIntervalMs: refreshIntervalMs }),
|
|
69
|
-
});
|
|
70
|
-
const { refetch } = result;
|
|
71
|
-
useEffect(() => (enabled ? register(refetch) : undefined), [enabled, register, refetch]);
|
|
72
|
-
return result;
|
|
73
|
-
}
|
|
74
|
-
function useDashboardFormats() {
|
|
75
|
-
const resolver = useLocale();
|
|
76
|
-
const locale = resolver.locale();
|
|
77
|
-
const timeZone = resolver.timeZone();
|
|
78
|
-
const formatRelative = useRelativeTimeFormatter(locale);
|
|
79
|
-
const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 1 });
|
|
80
|
-
// Without the significant-digit cap, values below the locale's first
|
|
81
|
-
// abbreviation step keep a decimal ("298.989,9 €" in de); lessPrecision
|
|
82
|
-
// picks whichever rule rounds more, so that becomes "299.000 €" while
|
|
83
|
-
// "1,5 Mio. €" keeps its one decimal.
|
|
84
|
-
const compactOptions = {
|
|
85
|
-
notation: "compact",
|
|
86
|
-
maximumFractionDigits: 1,
|
|
87
|
-
maximumSignificantDigits: 3,
|
|
88
|
-
roundingPriority: "lessPrecision",
|
|
89
|
-
};
|
|
90
|
-
const compactNumber = new Intl.NumberFormat(locale, compactOptions);
|
|
91
|
-
const percent = new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 0 });
|
|
92
|
-
const day = new Intl.DateTimeFormat(locale, { day: "2-digit", month: "2-digit", timeZone });
|
|
93
|
-
const month = new Intl.DateTimeFormat(locale, { month: "2-digit", year: "numeric", timeZone });
|
|
94
|
-
const hour = new Intl.DateTimeFormat(locale, { hour: "2-digit", minute: "2-digit", timeZone });
|
|
95
|
-
const dateTime = new Intl.DateTimeFormat(locale, {
|
|
96
|
-
dateStyle: "medium",
|
|
97
|
-
timeStyle: "short",
|
|
98
|
-
timeZone,
|
|
99
|
-
});
|
|
100
|
-
const medium = new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeZone });
|
|
101
|
-
const formatMinor = (amountMinor, currency, fractionDigits) => formatMoney(amountMinor, currency, locale, fractionDigits);
|
|
102
|
-
return {
|
|
103
|
-
timeZone,
|
|
104
|
-
formatMedium: (atMs) => medium.format(atMs),
|
|
105
|
-
formatMinor,
|
|
106
|
-
formatRelative,
|
|
107
|
-
formatPanelValue: (valueFormat) => valueFormat === undefined
|
|
108
|
-
? (value) => number.format(value)
|
|
109
|
-
: (value) => formatMinor(value, valueFormat.currency, valueFormat.fractionDigits),
|
|
110
|
-
formatPanelAxisValue: (valueFormat) => {
|
|
111
|
-
if (valueFormat === undefined || !CURRENCY_CODE_PATTERN.test(valueFormat.currency)) {
|
|
112
|
-
return (value) => compactNumber.format(value);
|
|
113
|
-
}
|
|
114
|
-
const compactMoney = new Intl.NumberFormat(resolveSafeLocale(locale), {
|
|
115
|
-
...compactOptions,
|
|
116
|
-
style: "currency",
|
|
117
|
-
currency: valueFormat.currency,
|
|
118
|
-
});
|
|
119
|
-
const decimals = currencyDecimals(valueFormat.currency);
|
|
120
|
-
return (value) => compactMoney.format(value / 10 ** decimals);
|
|
121
|
-
},
|
|
122
|
-
formatNumber: (value) => number.format(value),
|
|
123
|
-
formatPercent: (fraction) => percent.format(fraction),
|
|
124
|
-
formatDay: (atMs) => day.format(atMs),
|
|
125
|
-
formatMonth: (atMs) => month.format(atMs),
|
|
126
|
-
formatHour: (atMs) => hour.format(atMs),
|
|
127
|
-
formatDateTime: (atMs) => dateTime.format(atMs),
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
const MAX_TEXT_PARAM_DEPTH = 4;
|
|
131
|
-
function isRecord(value) {
|
|
132
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
133
|
-
}
|
|
134
|
-
function isDashboardI18nText(value) {
|
|
135
|
-
return isRecord(value) && typeof value["i18nKey"] === "string";
|
|
136
|
-
}
|
|
137
|
-
function isMoneyParam(value) {
|
|
138
|
-
return (isRecord(value) &&
|
|
139
|
-
value["kind"] === "money" &&
|
|
140
|
-
typeof value["amountMinor"] === "number" &&
|
|
141
|
-
typeof value["currency"] === "string");
|
|
142
|
-
}
|
|
143
|
-
function isDateParam(value) {
|
|
144
|
-
return isRecord(value) && value["kind"] === "date" && typeof value["atMs"] === "number";
|
|
145
|
-
}
|
|
146
|
-
function isRelativeTimeParam(value) {
|
|
147
|
-
return isRecord(value) && value["kind"] === "relativeTime" && typeof value["atMs"] === "number";
|
|
148
|
-
}
|
|
149
|
-
function isTextParam(value) {
|
|
150
|
-
return (typeof value === "string" ||
|
|
151
|
-
typeof value === "number" ||
|
|
152
|
-
isDashboardI18nText(value) ||
|
|
153
|
-
isMoneyParam(value) ||
|
|
154
|
-
isDateParam(value) ||
|
|
155
|
-
isRelativeTimeParam(value));
|
|
156
|
-
}
|
|
157
|
-
function resolveTextParam(param, translate, formats, depth) {
|
|
158
|
-
if (typeof param === "string" || typeof param === "number")
|
|
159
|
-
return param;
|
|
160
|
-
if (isMoneyParam(param))
|
|
161
|
-
return formats.formatMinor(param.amountMinor, param.currency);
|
|
162
|
-
if (isDateParam(param))
|
|
163
|
-
return formats.formatMedium(param.atMs);
|
|
164
|
-
if (isRelativeTimeParam(param))
|
|
165
|
-
return formats.formatRelative(param.atMs);
|
|
166
|
-
if (isDashboardI18nText(param)) {
|
|
167
|
-
// Too deep: show the key instead of recursing further.
|
|
168
|
-
return depth >= MAX_TEXT_PARAM_DEPTH
|
|
169
|
-
? param.i18nKey
|
|
170
|
-
: resolveI18nText(param, translate, formats, depth + 1);
|
|
171
|
-
}
|
|
172
|
-
return "";
|
|
173
|
-
}
|
|
174
|
-
function resolveI18nText(text, translate, formats, depth) {
|
|
175
|
-
if (text.i18nParams === undefined)
|
|
176
|
-
return translate(text.i18nKey);
|
|
177
|
-
const params = {};
|
|
178
|
-
for (const [name, param] of Object.entries(text.i18nParams)) {
|
|
179
|
-
if (isTextParam(param))
|
|
180
|
-
params[name] = resolveTextParam(param, translate, formats, depth);
|
|
181
|
-
}
|
|
182
|
-
return translate(text.i18nKey, params);
|
|
183
|
-
}
|
|
184
|
-
/** Plain strings pass through; DashboardI18nText is translated (nested params first).
|
|
185
|
-
* Anything else yields undefined so callers fall back to their own placeholder. */
|
|
186
|
-
function resolveDashboardText(value, translate, formats) {
|
|
187
|
-
if (typeof value === "string")
|
|
188
|
-
return value;
|
|
189
|
-
if (typeof value === "number")
|
|
190
|
-
return String(value);
|
|
191
|
-
if (isDashboardI18nText(value))
|
|
192
|
-
return resolveI18nText(value, translate, formats, 1);
|
|
193
|
-
return undefined;
|
|
194
|
-
}
|
|
195
|
-
function PanelShell({ testId, title, subtitle, action, children, }) {
|
|
196
|
-
return (_jsxs("section", { "data-testid": testId, className: "flex h-full flex-col rounded-lg border border-border bg-card text-card-foreground", children: [_jsxs("header", { className: "flex min-h-11 shrink-0 flex-wrap items-center justify-between gap-x-3 gap-y-2 border-b border-border-row px-4 py-2", children: [_jsxs("div", { className: "flex min-w-0 flex-col gap-0.5", children: [_jsx("h2", { className: "text-sm font-semibold break-words", children: title }), subtitle !== undefined && _jsx("p", { className: "text-xs text-muted-foreground", children: subtitle })] }), action !== undefined && _jsx("div", { className: "shrink-0", children: action })] }), _jsx("div", { className: "grow p-4", children: children })] }));
|
|
197
|
-
}
|
|
198
|
-
function PanelError({ label, error, onRetry, testId, }) {
|
|
199
|
-
const { Button } = usePrimitives();
|
|
200
|
-
const t = useTranslation();
|
|
201
|
-
return (_jsxs("div", { role: "alert", "data-testid": testId, className: "flex flex-col items-start gap-2 rounded-lg border border-dashed border-status-bad/40 px-4 py-6 text-sm", children: [_jsx("div", { className: "font-medium text-status-bad", children: t("kumiko.dashboard.panel.error.title", { label }) }), _jsx("div", { className: "text-muted-foreground", children: dispatcherErrorText(error, t) }), _jsx(Button, { variant: "secondary", onClick: onRetry, children: t("kumiko.dashboard.panel.error.retry") }), _jsx("div", { className: "text-xs text-muted-foreground", children: t("kumiko.dashboard.panel.error.isolated") })] }));
|
|
202
|
-
}
|
|
203
|
-
function PanelSkeleton({ shape }) {
|
|
204
|
-
const t = useTranslation();
|
|
205
|
-
if (shape === "rows")
|
|
206
|
-
return _jsx(LoadingState, { rows: 3 });
|
|
207
|
-
if (shape === "segments") {
|
|
208
|
-
return (_jsx("output", { "aria-label": t("kumiko.widget.loading"), className: "flex flex-col gap-4", children: SKELETON_SEGMENT_WIDTHS.map((width) => (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Skeleton, { className: "h-3 w-24" }), _jsx(Skeleton, { className: "h-2.5 rounded-full", style: { width } })] }, width))) }));
|
|
209
|
-
}
|
|
210
|
-
return (_jsx("output", { "aria-label": t("kumiko.widget.loading"), className: "flex h-40 items-end gap-1", children: SKELETON_BAR_HEIGHTS.map((height) => (_jsx(Skeleton, { className: "flex-1", style: { height } }, height))) }));
|
|
211
|
-
}
|
|
212
|
-
function QueryPanel({ panel, label, subtitle, action, screenParams, translate, skeleton, isEmpty, children, }) {
|
|
213
|
-
const formats = useDashboardFormats();
|
|
214
|
-
const { data, error, loading, refetch } = useDashboardQuery(panel.query, panelPayload(panel, screenParams));
|
|
215
|
-
useReportPanelLoaded(data);
|
|
216
|
-
let body;
|
|
217
|
-
if (loading && data === null) {
|
|
218
|
-
body = _jsx(PanelSkeleton, { shape: skeleton });
|
|
219
|
-
}
|
|
220
|
-
else if (error !== null) {
|
|
221
|
-
body = _jsx(PanelError, { label: label, error: error, onRetry: () => void refetch() });
|
|
222
|
-
}
|
|
223
|
-
else if (data === null || isEmpty(data)) {
|
|
224
|
-
body = (_jsx(EmptyState, { title: translate(panel.emptyLabel ?? "kumiko.list.no-entries"), description: panel.emptyHint !== undefined ? translate(panel.emptyHint) : undefined }));
|
|
225
|
-
}
|
|
226
|
-
else {
|
|
227
|
-
body = children(data, formats);
|
|
228
|
-
}
|
|
229
|
-
return (_jsx(PanelShell, { testId: `dashboard-panel-${panel.id}`, title: label, subtitle: subtitle, action: action, children: body }));
|
|
230
|
-
}
|
|
231
|
-
function isSparkPoint(value) {
|
|
232
|
-
return (typeof value === "object" &&
|
|
233
|
-
value !== null &&
|
|
234
|
-
"value" in value &&
|
|
235
|
-
(typeof value.value === "number" || value.value === null));
|
|
236
|
-
}
|
|
237
|
-
function readSparkline(panel, record) {
|
|
238
|
-
if (panel.sparklineField === undefined)
|
|
239
|
-
return undefined;
|
|
240
|
-
const raw = record[panel.sparklineField];
|
|
241
|
-
if (!Array.isArray(raw))
|
|
242
|
-
return undefined;
|
|
243
|
-
return raw.filter(isSparkPoint).flatMap((p) => (p.value === null ? [] : [p.value]));
|
|
244
|
-
}
|
|
245
|
-
function readTone(raw) {
|
|
246
|
-
return isOneOf(DASHBOARD_STAT_TONES, raw) ? raw : undefined;
|
|
247
|
-
}
|
|
248
|
-
function fallbackText(raw, placeholder) {
|
|
249
|
-
if (raw === undefined || raw === null || isRecord(raw))
|
|
250
|
-
return placeholder;
|
|
251
|
-
return String(raw);
|
|
252
|
-
}
|
|
253
|
-
function StatPanelBody({ panel, label, screenId, screenParams, translate, variant, }) {
|
|
254
|
-
const formats = useDashboardFormats();
|
|
255
|
-
// Resolved HERE (not in a separate always-rendered child) so `icon` on
|
|
256
|
-
// <StatCard> is `undefined` — not a React element that renders empty —
|
|
257
|
-
// when the icon name isn't registered. StatCard gates its accent chip on
|
|
258
|
-
// `icon !== undefined`, so a resolved-but-hidden element used to leave a
|
|
259
|
-
// stray accent-colored chip next to the label.
|
|
260
|
-
const iconName = panel.icon !== undefined ? extensionSectionName(panel.icon) : undefined;
|
|
261
|
-
const Icon = useExtensionSectionComponent(iconName);
|
|
262
|
-
useEffect(() => {
|
|
263
|
-
if (panel.icon !== undefined && iconName !== undefined && Icon === undefined) {
|
|
264
|
-
// biome-ignore lint/suspicious/noConsole: dev-warning für Setup-Fehler
|
|
265
|
-
console.warn(`[kumiko] Dashboard stat-panel "${panel.id}" on screen "${screenId}" references icon ` +
|
|
266
|
-
`"${iconName}", which is not registered in clientFeatures.extensionSectionComponents.`);
|
|
267
|
-
}
|
|
268
|
-
}, [panel.icon, panel.id, iconName, Icon, screenId]);
|
|
269
|
-
const { data, error, loading, refetch } = useDashboardQuery(panel.query, panelPayload(panel, screenParams));
|
|
270
|
-
useReportPanelLoaded(data);
|
|
271
|
-
if (loading && data === null)
|
|
272
|
-
return _jsx(LoadingState, { rows: 2 });
|
|
273
|
-
if (error !== null) {
|
|
274
|
-
return _jsx(PanelError, { label: label, error: error, onRetry: () => void refetch() });
|
|
275
|
-
}
|
|
276
|
-
const record = data ?? {};
|
|
277
|
-
const rawValue = record[panel.valueField];
|
|
278
|
-
const tone = readTone(panel.toneField !== undefined ? record[panel.toneField] : undefined) ??
|
|
279
|
-
// A fixed tone must not paint an all-clear "0" as an alarm.
|
|
280
|
-
(rawValue === 0 ? undefined : panel.tone);
|
|
281
|
-
const sub = panel.subField !== undefined ? record[panel.subField] : undefined;
|
|
282
|
-
const delta = readDelta(panel, record);
|
|
283
|
-
const valueFormat = panel.valueFormat;
|
|
284
|
-
let value;
|
|
285
|
-
if (typeof rawValue !== "number") {
|
|
286
|
-
value =
|
|
287
|
-
resolveDashboardText(rawValue, translate, formats) ??
|
|
288
|
-
fallbackText(rawValue, EMPTY_VALUE_PLACEHOLDER);
|
|
289
|
-
}
|
|
290
|
-
else if (valueFormat?.kind === "relativeTime") {
|
|
291
|
-
value = formats.formatRelative(rawValue);
|
|
292
|
-
}
|
|
293
|
-
else {
|
|
294
|
-
value = formats.formatPanelValue(valueFormat)(rawValue);
|
|
295
|
-
}
|
|
296
|
-
const spark = readSparkline(panel, record);
|
|
297
|
-
const testId = `dashboard-panel-${panel.id}`;
|
|
298
|
-
const subText = resolveDashboardText(sub, translate, formats) ?? fallbackText(sub, undefined);
|
|
299
|
-
const icon = Icon !== undefined ? (_jsx(Icon, { entityName: screenId, entityId: null, screenId: screenId, filterParams: screenParams.filterParams })) : undefined;
|
|
300
|
-
if (variant === "strip") {
|
|
301
|
-
return (_jsx(StatStripCell, { icon: icon, label: label, value: value, tone: tone ?? "default", ...(Icon !== undefined && { accentColor: panel.accentColor }), ...(subText !== undefined && { sub: subText }), ...(delta !== undefined && { delta }), ...(spark !== undefined && { spark }), testId: testId }));
|
|
302
|
-
}
|
|
303
|
-
return (_jsx(StatCard, { icon: icon, label: label, value: value, tone: tone ?? "default", ...(Icon !== undefined && { accentColor: panel.accentColor }), ...(subText !== undefined && { sub: subText }), ...(delta !== undefined && { delta }), ...(spark !== undefined && { spark }), testId: testId }));
|
|
304
|
-
}
|
|
305
|
-
function readDelta(panel, record) {
|
|
306
|
-
if (panel.deltaField === undefined || panel.deltaDirectionField === undefined)
|
|
307
|
-
return undefined;
|
|
308
|
-
const value = record[panel.deltaField];
|
|
309
|
-
const direction = record[panel.deltaDirectionField];
|
|
310
|
-
if (value === undefined || value === null)
|
|
311
|
-
return undefined;
|
|
312
|
-
if (direction !== "up" && direction !== "down")
|
|
313
|
-
return undefined;
|
|
314
|
-
const tone = readTone(panel.deltaToneField !== undefined ? record[panel.deltaToneField] : undefined);
|
|
315
|
-
return { value: String(value), direction, ...(tone !== undefined && { tone }) };
|
|
316
|
-
}
|
|
317
|
-
function StatGroupPanelBody({ panel, label, screenId, screenParams, translate, }) {
|
|
318
|
-
const testId = `dashboard-panel-${panel.id}`;
|
|
319
|
-
if (label === undefined) {
|
|
320
|
-
return (_jsx("div", { "data-testid": testId, className: "grid grid-cols-2 gap-y-2 rounded-lg border border-border bg-card py-1 lg:auto-cols-fr lg:grid-flow-col lg:divide-x lg:divide-border-row", children: panel.stats.map((stat) => (_jsx(StatPanelBody, { panel: stat, label: translate(stat.label), screenId: screenId, screenParams: screenParams, translate: translate, variant: "strip" }, stat.id))) }));
|
|
321
|
-
}
|
|
322
|
-
return (_jsx(SectionCard, { title: label, ...(panel.subtitle !== undefined && { subtitle: translate(panel.subtitle) }), testId: testId, children: _jsx("section", { className: "grid grid-cols-[repeat(auto-fit,minmax(min(12rem,100%),1fr))] gap-3", children: panel.stats.map((stat) => (_jsx(StatPanelBody, { panel: stat, label: translate(stat.label), screenId: screenId, screenParams: screenParams, translate: translate, variant: "card" }, stat.id))) }) }));
|
|
323
|
-
}
|
|
324
|
-
function markerKindColor(kind) {
|
|
325
|
-
if (kind === undefined)
|
|
326
|
-
return undefined;
|
|
327
|
-
if (kind.color !== undefined)
|
|
328
|
-
return kind.color;
|
|
329
|
-
return kind.tone !== undefined ? chartToneColor(kind.tone) : undefined;
|
|
330
|
-
}
|
|
331
|
-
function resolveMarkers(markers, translate, formats, markerKinds) {
|
|
332
|
-
return markers?.map((marker) => {
|
|
333
|
-
const color = typeof marker.kind === "string" ? markerKindColor(markerKinds?.[marker.kind]) : undefined;
|
|
334
|
-
const legendKey = typeof marker.kind === "string" ? markerKinds?.[marker.kind]?.label : undefined;
|
|
335
|
-
const legendLabel = legendKey !== undefined ? translate(legendKey) : undefined;
|
|
336
|
-
return {
|
|
337
|
-
atMs: marker.atMs,
|
|
338
|
-
label: resolveDashboardText(marker.label, translate, formats) ?? "",
|
|
339
|
-
...(color !== undefined && { color }),
|
|
340
|
-
...(legendLabel !== undefined && { legendLabel }),
|
|
341
|
-
};
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
// A plain bar chart ("changes per day") ships `points` without `series`.
|
|
345
|
-
const SINGLE_SERIES_KEY = "value";
|
|
346
|
-
function chartSeries(data, panelLabel, translate) {
|
|
347
|
-
if (data.series !== undefined && data.series.length > 0) {
|
|
348
|
-
return data.series.map((s) => ({ ...s, label: translate(s.label) }));
|
|
349
|
-
}
|
|
350
|
-
if (data.points !== undefined && data.points.length > 0) {
|
|
351
|
-
return [{ key: SINGLE_SERIES_KEY, label: panelLabel, points: data.points }];
|
|
352
|
-
}
|
|
353
|
-
return [];
|
|
354
|
-
}
|
|
355
|
-
function distinctBucketCount(series) {
|
|
356
|
-
return new Set(series.flatMap((s) => s.points.map((p) => p.atMs))).size;
|
|
357
|
-
}
|
|
358
|
-
function isChartEmpty(chart, data, series) {
|
|
359
|
-
if (chart === "segment-bars")
|
|
360
|
-
return (data.rows ?? []).length === 0;
|
|
361
|
-
if (chart === "stacked-bars")
|
|
362
|
-
return distinctBucketCount(series) === 0;
|
|
363
|
-
if (chart === "stacked-area")
|
|
364
|
-
return distinctBucketCount(series) < 2;
|
|
365
|
-
const measured = (data.points ?? []).filter((p) => p.value !== null);
|
|
366
|
-
return measured.length < 2;
|
|
367
|
-
}
|
|
368
|
-
function windowOf(data, series) {
|
|
369
|
-
const times = series.flatMap((s) => s.points.map((p) => p.atMs));
|
|
370
|
-
const startMs = data.windowStartMs ?? (times.length > 0 ? Math.min(...times) : 0);
|
|
371
|
-
const endMs = data.windowEndMs ?? (times.length > 0 ? Math.max(...times) : 1);
|
|
372
|
-
return { startMs, endMs };
|
|
373
|
-
}
|
|
374
|
-
function ChartPanelBody({ panel, label, screenParams, translate, }) {
|
|
375
|
-
const t = useTranslation();
|
|
376
|
-
const todayLabel = t("kumiko.dashboard.today");
|
|
377
|
-
const tones = panel.seriesTones;
|
|
378
|
-
const colors = panel.seriesColors;
|
|
379
|
-
const testId = `dashboard-chart-${panel.id}`;
|
|
380
|
-
const ranges = panel.chart === "stacked-area" && panel.ranges !== undefined
|
|
381
|
-
? {
|
|
382
|
-
default: panel.ranges.default,
|
|
383
|
-
options: panel.ranges.options.map((option) => ({
|
|
384
|
-
...option,
|
|
385
|
-
label: translate(option.label),
|
|
386
|
-
})),
|
|
387
|
-
}
|
|
388
|
-
: undefined;
|
|
389
|
-
const [windowSelection, setWindowSelection] = useState(() => initialWindowSelection(ranges, panel.chart === "stacked-area" ? panel.initialWindow : undefined));
|
|
390
|
-
const rangeSwitch = ranges !== undefined ? (_jsx(ModeSwitch, { value: windowSelection?.kind === "range" ? windowSelection.value : null, options: ranges.options.map((option) => ({
|
|
391
|
-
value: option.value,
|
|
392
|
-
label: option.label,
|
|
393
|
-
})), onChange: (value) => setWindowSelection({ kind: "range", value }), ariaLabel: label, variant: "pill", testId: `dashboard-chart-range-${panel.id}` })) : undefined;
|
|
394
|
-
return (_jsx(QueryPanel, { panel: panel, label: label, subtitle: panel.subtitle !== undefined ? translate(panel.subtitle) : undefined, action: rangeSwitch, screenParams: screenParams, translate: translate, skeleton: panel.chart === "segment-bars" ? "segments" : "bars", isEmpty: (data) => isChartEmpty(panel.chart, data, chartSeries(data, label, translate)), children: (data, formats) => {
|
|
395
|
-
const series = chartSeries(data, label, translate);
|
|
396
|
-
const { startMs, endMs } = windowOf(data, series);
|
|
397
|
-
const markers = resolveMarkers(data.markers, translate, formats, panel.markerKinds);
|
|
398
|
-
const formatValue = formats.formatPanelValue(panel.valueFormat);
|
|
399
|
-
if (panel.chart === "segment-bars") {
|
|
400
|
-
return (_jsx(SegmentBarChart, { rows: (data.rows ?? []).map((row) => ({
|
|
401
|
-
...row,
|
|
402
|
-
label: translate(row.label),
|
|
403
|
-
segments: row.segments.map((seg) => ({ ...seg, label: translate(seg.label) })),
|
|
404
|
-
})), tones: tones, colors: colors, ariaLabel: label, formatValue: formatValue, testId: testId }));
|
|
405
|
-
}
|
|
406
|
-
if (panel.chart === "stacked-bars") {
|
|
407
|
-
const times = series.flatMap((s) => s.points.map((p) => p.atMs));
|
|
408
|
-
const spacing = Math.min(...[...new Set(times)]
|
|
409
|
-
.sort((a, b) => a - b)
|
|
410
|
-
.flatMap((atMs, i, all) => {
|
|
411
|
-
const prev = all[i - 1];
|
|
412
|
-
return prev === undefined ? [] : [atMs - prev];
|
|
413
|
-
}), DAY_MS);
|
|
414
|
-
return (_jsx(StackedBarChart, { series: series, windowEndMs: endMs, tones: tones, colors: colors, ariaLabel: label, todayLabel: todayLabel, formatBucketLabel: spacing < DAY_MS - HOUR_MS ? formats.formatHour : formats.formatDay, formatValue: formatValue, formatAxisValue: formats.formatPanelAxisValue(panel.valueFormat), testId: testId }));
|
|
415
|
-
}
|
|
416
|
-
if (panel.chart === "stacked-area") {
|
|
417
|
-
const lines = (data.lines ?? []).map((line) => ({
|
|
418
|
-
...line,
|
|
419
|
-
label: translate(line.label),
|
|
420
|
-
}));
|
|
421
|
-
const todayMs = data.todayMs ?? undefined;
|
|
422
|
-
const formatDateByFormat = (atMs, format) => format === "month" ? formats.formatMonth(atMs) : formats.formatDay(atMs);
|
|
423
|
-
return (_jsx(StackedAreaChart, { series: series, lines: lines, windowStartMs: startMs, windowEndMs: endMs, ...(todayMs !== undefined && { todayMs }), tones: tones, colors: colors, markers: markers, ...(ranges !== undefined && { ranges }), brush: panel.brush === true, windowSelection: windowSelection, onWindowSelectionChange: setWindowSelection, brushLabels: {
|
|
424
|
-
start: t("kumiko.dashboard.brush-start"),
|
|
425
|
-
end: t("kumiko.dashboard.brush-end"),
|
|
426
|
-
}, ariaLabel: label, todayLabel: todayLabel, formatBucketLabel: formatDateByFormat, formatValue: formatValue, formatAxisValue: formats.formatPanelAxisValue(panel.valueFormat), markerLabels: panel.markerLabels === true, formatMarkerTime: formatDateByFormat, timeZone: formats.timeZone, ...(panel.dateFormat !== undefined && { dateFormat: panel.dateFormat }), ...(panel.markerLegend !== undefined && { markerLegend: panel.markerLegend }), scrollable: panel.scrollable === true, showLegendTotals: panel.legendTotals !== false, emptyContent: _jsx(EmptyState, { title: translate(panel.emptyLabel ?? "kumiko.list.no-entries") }), testId: testId }));
|
|
427
|
-
}
|
|
428
|
-
return (_jsx(TimeseriesChart, { points: data.points ?? [], windowStartMs: startMs, windowEndMs: endMs, markers: markers, formatMarkerTime: formats.formatDay, ...((panel.valueFormat !== undefined || panel.unit !== undefined) && {
|
|
429
|
-
yAxis: {
|
|
430
|
-
ticks: 3,
|
|
431
|
-
format: formats.formatPanelAxisValue(panel.valueFormat),
|
|
432
|
-
...(panel.unit !== undefined && { unit: translate(panel.unit) }),
|
|
433
|
-
},
|
|
434
|
-
}), ariaLabel: label, testId: testId }));
|
|
435
|
-
} }));
|
|
436
|
-
}
|
|
437
|
-
function ListPanelBody({ panel, label, screenParams, translate, }) {
|
|
438
|
-
const { formatPercent, formatNumber, formatDateTime } = useDashboardFormats();
|
|
439
|
-
const columns = panel.columns.map((c) => {
|
|
440
|
-
const normalized = normalizeListColumn(c);
|
|
441
|
-
return {
|
|
442
|
-
field: normalized.field,
|
|
443
|
-
label: translate(normalized.label ?? normalized.field),
|
|
444
|
-
...(normalized.display !== undefined && { display: normalized.display }),
|
|
445
|
-
...(normalized.badgeToneField !== undefined && {
|
|
446
|
-
badgeToneField: normalized.badgeToneField,
|
|
447
|
-
}),
|
|
448
|
-
};
|
|
449
|
-
});
|
|
450
|
-
return (_jsx(QueryPanel, { panel: panel, label: label, screenParams: screenParams, translate: translate, skeleton: "rows", isEmpty: (data) => (data.rows ?? []).length === 0, children: (data) => (_jsx(DashboardListTable, { columns: columns, rows: data.rows ?? [], formatPercent: formatPercent, formatNumber: formatNumber, formatDateTime: formatDateTime, translate: translate })) }));
|
|
451
|
-
}
|
|
452
|
-
function FeedPanelBody({ panel, label, screenParams, translate, }) {
|
|
453
|
-
return (_jsx(QueryPanel, { panel: panel, label: label, screenParams: screenParams, translate: translate, skeleton: "rows", isEmpty: (data) => (data.rows ?? []).length === 0, children: (data, formats) => {
|
|
454
|
-
const rows = (data.rows ?? []).map((row, i) => {
|
|
455
|
-
const trailing = resolveDashboardText(row.trailing, translate, formats);
|
|
456
|
-
return {
|
|
457
|
-
id: String(i),
|
|
458
|
-
primary: resolveDashboardText(row.primary, translate, formats) ?? EMPTY_VALUE_PLACEHOLDER,
|
|
459
|
-
...(trailing !== undefined && { trailing }),
|
|
460
|
-
};
|
|
461
|
-
});
|
|
462
|
-
return _jsx(FeedList, { rows: rows });
|
|
463
|
-
} }));
|
|
464
|
-
}
|
|
465
|
-
function isFiniteNumber(value) {
|
|
466
|
-
return typeof value === "number" && Number.isFinite(value);
|
|
467
|
-
}
|
|
468
|
-
function isProgressTone(value) {
|
|
469
|
-
return value === "default" || value === "danger";
|
|
470
|
-
}
|
|
471
|
-
function progressUsageOf(row) {
|
|
472
|
-
const { used, limit } = row;
|
|
473
|
-
if (!isFiniteNumber(used))
|
|
474
|
-
return undefined;
|
|
475
|
-
if (limit === null)
|
|
476
|
-
return { used, limit: null };
|
|
477
|
-
return isFiniteNumber(limit) ? { used, limit } : undefined;
|
|
478
|
-
}
|
|
479
|
-
function toProgressListRow(row, index, translate, formats) {
|
|
480
|
-
const sub = resolveDashboardText(row["sub"], translate, formats);
|
|
481
|
-
const usage = progressUsageOf(row);
|
|
482
|
-
const explicitTone = isProgressTone(row["tone"]) ? row["tone"] : undefined;
|
|
483
|
-
const common = {
|
|
484
|
-
id: String(index),
|
|
485
|
-
label: resolveDashboardText(row["label"], translate, formats) ?? EMPTY_VALUE_PLACEHOLDER,
|
|
486
|
-
...(sub !== undefined && { sub }),
|
|
487
|
-
};
|
|
488
|
-
if (usage === undefined) {
|
|
489
|
-
return {
|
|
490
|
-
...common,
|
|
491
|
-
value: resolveDashboardText(row["value"], translate, formats) ?? EMPTY_VALUE_PLACEHOLDER,
|
|
492
|
-
fraction: isFiniteNumber(row["fraction"]) ? row["fraction"] : 0,
|
|
493
|
-
...(explicitTone !== undefined && { tone: explicitTone }),
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
|
-
const { used, limit } = usage;
|
|
497
|
-
const usedText = formats.formatNumber(used);
|
|
498
|
-
const limitText = limit === null ? translate("kumiko.dashboard.progress.unlimited") : formats.formatNumber(limit);
|
|
499
|
-
const exhausted = limit !== null && used >= limit;
|
|
500
|
-
const tone = explicitTone ?? (exhausted ? "danger" : undefined);
|
|
501
|
-
let fraction = null;
|
|
502
|
-
if (limit !== null)
|
|
503
|
-
fraction = limit === 0 ? 1 : used / limit;
|
|
504
|
-
return {
|
|
505
|
-
...common,
|
|
506
|
-
value: `${usedText} / ${limitText}`,
|
|
507
|
-
fraction,
|
|
508
|
-
...(tone !== undefined && { tone }),
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
function ProgressListPanelBody({ panel, label, screenParams, translate, }) {
|
|
512
|
-
return (_jsx(QueryPanel, { panel: panel, label: label, screenParams: screenParams, translate: translate, skeleton: "rows", isEmpty: (data) => (data.rows ?? []).length === 0, children: (data, formats) => {
|
|
513
|
-
const rows = (data.rows ?? []).map((row, i) => toProgressListRow(row, i, translate, formats));
|
|
514
|
-
return _jsx(ProgressList, { rows: rows });
|
|
515
|
-
} }));
|
|
516
|
-
}
|
|
517
|
-
function TextBlockView({ block, translate, }) {
|
|
518
|
-
switch (block.kind) {
|
|
519
|
-
case "heading":
|
|
520
|
-
return (_jsx("h3", { "data-block": "heading", className: "text-sm font-semibold text-foreground", children: translate(block.text) }));
|
|
521
|
-
case "paragraph":
|
|
522
|
-
return (_jsx("p", { "data-block": "paragraph", className: block.emphasis === "strong"
|
|
523
|
-
? "text-sm font-medium text-foreground"
|
|
524
|
-
: "text-sm text-muted-foreground", children: translate(block.text) }));
|
|
525
|
-
case "list":
|
|
526
|
-
return (_jsx("ul", { "data-block": "list", className: "list-disc space-y-1 pl-5 text-sm text-muted-foreground", children: block.items.map((item) => (_jsx("li", { children: translate(item) }, item))) }));
|
|
527
|
-
case "code":
|
|
528
|
-
return (_jsx("pre", { "data-block": "code", className: "overflow-x-auto rounded-md bg-muted p-3 text-xs", children: _jsx("code", { children: block.code }) }));
|
|
529
|
-
case "hint":
|
|
530
|
-
return (_jsx("p", { "data-block": "hint", className: "text-xs text-muted-foreground", children: translate(block.text) }));
|
|
531
|
-
case "link":
|
|
532
|
-
return _jsx(TextBlockLink, { block: block, translate: translate });
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
function TextBlockLink({ block, translate, }) {
|
|
536
|
-
const { Link } = usePrimitives();
|
|
537
|
-
const nav = useNav();
|
|
538
|
-
const userRoles = useUserRoles();
|
|
539
|
-
if (block.roles !== undefined && !block.roles.some((role) => userRoles?.includes(role))) {
|
|
540
|
-
return null;
|
|
541
|
-
}
|
|
542
|
-
const target = { screenId: lastSegment(block.screen) };
|
|
543
|
-
return (_jsx(Link, { href: nav.hrefFor(target), onPress: () => nav.navigate(target), testId: `dashboard-text-link-${target.screenId}`, children: translate(block.text) }));
|
|
544
|
-
}
|
|
545
|
-
function textBlockKey(block) {
|
|
546
|
-
switch (block.kind) {
|
|
547
|
-
case "list":
|
|
548
|
-
return `list:${block.items.join("|")}`;
|
|
549
|
-
case "code":
|
|
550
|
-
return `code:${block.code}`;
|
|
551
|
-
default:
|
|
552
|
-
return `${block.kind}:${block.text}`;
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
function TextPanelBody({ panel, label, translate, }) {
|
|
556
|
-
const blocks = (_jsx("div", { className: "flex flex-col gap-3", children: panel.blocks.map((block) => (_jsx(TextBlockView, { block: block, translate: translate }, textBlockKey(block)))) }));
|
|
557
|
-
const testId = `dashboard-panel-${panel.id}`;
|
|
558
|
-
if (label !== undefined) {
|
|
559
|
-
return (_jsx(PanelShell, { testId: testId, title: label, children: blocks }));
|
|
560
|
-
}
|
|
561
|
-
return (_jsx("section", { "data-testid": testId, className: "h-full rounded-lg border border-border bg-card p-4 text-card-foreground", children: blocks }));
|
|
562
|
-
}
|
|
563
|
-
// Löst panel.component über dieselbe extensionSectionComponents-Registry auf
|
|
564
|
-
// wie entityEdit-Extension-Sections und List-Header-Slots — bleibt an seiner
|
|
565
|
-
// Array-Position statt in einen separaten Slot zu wandern (siehe Datei-Kopf-
|
|
566
|
-
// Kommentar zur Registry). Rendert nichts + dev-Warnung bei unregistriertem
|
|
567
|
-
// Namen, analog zu ListHeaderSlotMount in render-list.tsx.
|
|
568
|
-
function CustomPanelBody({ panel, screenId, filterParams, }) {
|
|
569
|
-
const name = extensionSectionName(panel.component);
|
|
570
|
-
const Component = useExtensionSectionComponent(name);
|
|
571
|
-
useEffect(() => {
|
|
572
|
-
if (name !== undefined && Component === undefined) {
|
|
573
|
-
// biome-ignore lint/suspicious/noConsole: dev-warning für Setup-Fehler
|
|
574
|
-
console.warn(`[kumiko] Dashboard custom-panel "${panel.id}" on screen "${screenId}" references component ` +
|
|
575
|
-
`"${name}", which is not registered in clientFeatures.extensionSectionComponents — the panel renders nothing.`);
|
|
576
|
-
}
|
|
577
|
-
}, [name, Component, panel.id, screenId]);
|
|
578
|
-
if (Component === undefined)
|
|
579
|
-
return null;
|
|
580
|
-
// Dashboard-Panels haben keine Entity — entityName trägt die screen.id,
|
|
581
|
-
// damit ExtensionSectionProps nicht extra für diesen einen Mount-Ort
|
|
582
|
-
// aufgeweicht werden muss (das bräche jede bestehende registrierte
|
|
583
|
-
// Section, die entityName als garantiert gesetzten string erwartet).
|
|
584
|
-
return (_jsx(Component, { entityName: screenId, entityId: null, screenId: screenId, filterParams: filterParams }));
|
|
585
|
-
}
|
|
13
|
+
import { DashboardQueryContext, HALF_PANEL, PanelError, PanelLoadedContext, useDashboardFormats, useDashboardQuery, useLoadedAt, useRefetchRegistry, WIDE_PANEL, } from "./dashboard-shared.js";
|
|
586
14
|
// Value lives in the URL under `filter.id` (like useListUrlState's
|
|
587
15
|
// `<screenId>.page`, same replaceState) so a `navigate` rowAction with
|
|
588
16
|
// `params` can deep-link onto this filter.
|
|
@@ -770,29 +198,6 @@ function ScreenPanelTile({ panel, featureName, translate, }) {
|
|
|
770
198
|
return null;
|
|
771
199
|
return _jsx(EmbeddedScreenPanelTile, { panel: panel, target: target, translate: translate });
|
|
772
200
|
}
|
|
773
|
-
// Starts at render time and restarts whenever the merged query params change;
|
|
774
|
-
// panels additionally report each successful (re)load, so live refetches and
|
|
775
|
-
// retries keep the stamp current.
|
|
776
|
-
function useLoadedAt(screenParams) {
|
|
777
|
-
const paramsKey = JSON.stringify([
|
|
778
|
-
screenParams.filterParams,
|
|
779
|
-
screenParams.rangeParams,
|
|
780
|
-
screenParams.recordParams,
|
|
781
|
-
]);
|
|
782
|
-
const [loaded, setLoaded] = useState(() => ({ paramsKey, atMs: Date.now() }));
|
|
783
|
-
if (loaded.paramsKey !== paramsKey)
|
|
784
|
-
setLoaded({ paramsKey, atMs: Date.now() });
|
|
785
|
-
const reportLoaded = useCallback((atMs) => setLoaded((prev) => (prev.atMs >= atMs ? prev : { ...prev, atMs })), []);
|
|
786
|
-
return { loadedAtMs: loaded.atMs, reportLoaded };
|
|
787
|
-
}
|
|
788
|
-
const PanelLoadedContext = createContext(() => { });
|
|
789
|
-
function useReportPanelLoaded(data) {
|
|
790
|
-
const reportLoaded = useContext(PanelLoadedContext);
|
|
791
|
-
useEffect(() => {
|
|
792
|
-
if (data !== null)
|
|
793
|
-
reportLoaded(Date.now());
|
|
794
|
-
}, [data, reportLoaded]);
|
|
795
|
-
}
|
|
796
201
|
const NO_RECORD_ROW = {};
|
|
797
202
|
function recordTitleText(row, field) {
|
|
798
203
|
if (row === null || field === undefined)
|