@cosmicdrift/kumiko-renderer 0.393.0 → 0.394.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/entity-list-screen.js +4 -2
- package/dist/app/kumiko-screen.js +4 -36
- package/dist/app/projection-list-body.js +1 -1
- package/dist/app/qn.d.ts +1 -6
- package/dist/app/qn.js +1 -12
- package/dist/app/screen-visibility-gate.d.ts +19 -0
- package/dist/app/screen-visibility-gate.js +60 -0
- package/dist/app/use-embedded-screen.d.ts +2 -0
- package/dist/app/use-embedded-screen.js +1 -1
- package/dist/app/use-gated-toolbar-actions.d.ts +7 -0
- package/dist/app/use-gated-toolbar-actions.js +51 -0
- package/dist/app/use-projection-list-actions.d.ts +1 -0
- package/dist/app/use-projection-list-actions.js +11 -2
- package/dist/app/visible-when.d.ts +5 -1
- package/dist/app/visible-when.js +11 -0
- package/dist/data-table-view-types.d.ts +4 -2
- package/dist/hooks/list-column-layout.d.ts +3 -1
- package/dist/hooks/list-column-layout.js +20 -7
- package/dist/hooks/use-list-view-state.d.ts +1 -1
- package/dist/hooks/use-list-view-state.js +3 -2
- package/dist/i18n-defaults.js +2 -0
- package/dist/web/app/dashboard-body.js +1 -1
- package/dist/web/app/dashboard-screen-tiles.d.ts +2 -1
- package/dist/web/app/dashboard-screen-tiles.js +9 -7
- package/dist/web/app/dashboard-shared.d.ts +2 -0
- package/dist/web/app/use-dashboard-screen-state.js +6 -1
- package/dist/web/layout/shell-breadcrumb.d.ts +4 -1
- package/dist/web/layout/shell-breadcrumb.js +18 -3
- package/dist/web/layout/shell-header.js +4 -2
- package/dist/web/primitives/action-menu.js +2 -2
- package/dist/web/primitives/calendar-popover.js +1 -1
- package/dist/web/primitives/combobox.js +1 -1
- package/dist/web/primitives/data-table/table-data-cell.js +27 -1
- package/dist/web/primitives/data-table/table-header-row.js +1 -1
- package/dist/web/primitives/data-table/table-view.js +2 -2
- package/dist/web/primitives/data-table/use-column-sizing.d.ts +4 -2
- package/dist/web/primitives/data-table/use-column-sizing.js +34 -11
- package/dist/web/primitives/data-table/view-menu.js +1 -1
- package/dist/web/primitives/data-table-cell.js +1 -1
- package/dist/web/primitives/dialog.js +1 -1
- package/dist/web/primitives/dropdown-menu.d.ts +1 -1
- package/dist/web/primitives/dropdown-menu.js +2 -2
- package/dist/web/primitives/lightbox.js +1 -1
- package/dist/web/primitives/modal-shell.js +1 -1
- package/dist/web/primitives/modal.js +1 -1
- package/dist/web/primitives/narrow-pinned-footer.js +1 -1
- package/dist/web/primitives/toast.js +2 -2
- package/dist/web/sse/live-events.js +0 -7
- package/dist/web/widgets/floating-panel-geometry.d.ts +32 -0
- package/dist/web/widgets/floating-panel-geometry.js +71 -0
- package/dist/web/widgets/floating-panel.d.ts +2 -6
- package/dist/web/widgets/floating-panel.js +11 -55
- package/dist/web/widgets/infinity-list.js +16 -7
- package/dist/web/widgets/plan-card.d.ts +2 -0
- package/dist/web/widgets/plan-card.js +5 -1
- package/package.json +6 -15
- package/src/changes.json +42 -0
|
@@ -31,6 +31,7 @@ import { buildClientActionRowAction, buildDefaultEditRowAction, buildDrawerRowAc
|
|
|
31
31
|
import { findEditScreenFor } from "./screen-access.js";
|
|
32
32
|
import { buildListQueryPayload, entityQueryCommand, isEntityEditScreenOfEntity, useNavigateToCreateFor, } from "./screen-shared.js";
|
|
33
33
|
import { useEntityRangeFacets } from "./use-entity-range-facets.js";
|
|
34
|
+
import { useGatedToolbarActions } from "./use-gated-toolbar-actions.js";
|
|
34
35
|
import { listPager, useScreenListRows } from "./use-screen-list-rows.js";
|
|
35
36
|
import { useScreenListTable } from "./use-screen-list-table.js";
|
|
36
37
|
export function EntityListScreen({ schema, screen, translate, onRowClick, }) {
|
|
@@ -299,7 +300,7 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
299
300
|
});
|
|
300
301
|
// ToolbarActions: Schema → Resolved-Form (analog rowActions), shared with
|
|
301
302
|
// projectionList and relatedList sections via buildProjectionToolbarActions.
|
|
302
|
-
const
|
|
303
|
+
const resolvedToolbarActions = useMemo(() => buildProjectionToolbarActions({
|
|
303
304
|
toolbarActions: screen.toolbarActions,
|
|
304
305
|
translate: effectiveTranslate,
|
|
305
306
|
dispatcher,
|
|
@@ -319,6 +320,7 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
319
320
|
appFeatures,
|
|
320
321
|
userRoles,
|
|
321
322
|
]);
|
|
323
|
+
const { actions: toolbarActions, gateQueries: toolbarGateQueries } = useGatedToolbarActions(screen.toolbarActions, resolvedToolbarActions);
|
|
322
324
|
const emptyContent = useListEmptyContent(screen.emptyState, effectiveTranslate, {
|
|
323
325
|
nav,
|
|
324
326
|
host,
|
|
@@ -407,7 +409,7 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
407
409
|
expandedRowIds,
|
|
408
410
|
onToggleRowExpanded: toggleRowExpanded,
|
|
409
411
|
renderExpandedRow,
|
|
410
|
-
}) }), _jsx(DrawerHost, { schema: schema, drawerAction: drawerAction, drawerScreen: drawerScreen, ...(drawerInitialValues !== undefined && { drawerInitialValues }), userRoles: userRoles, ...(translate !== undefined && { translate }), onClose: closeDrawer, onSuccess: () => {
|
|
412
|
+
}) }), toolbarGateQueries, _jsx(DrawerHost, { schema: schema, drawerAction: drawerAction, drawerScreen: drawerScreen, ...(drawerInitialValues !== undefined && { drawerInitialValues }), userRoles: userRoles, ...(translate !== undefined && { translate }), onClose: closeDrawer, onSuccess: () => {
|
|
411
413
|
closeDrawer();
|
|
412
414
|
invalidateExpansions();
|
|
413
415
|
void refreshRowsAfterWrite();
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { useMemo } from "react";
|
|
3
3
|
import { useUserRoles } from "../context/user-roles-context.js";
|
|
4
|
-
import { useQuery } from "../hooks/use-query.js";
|
|
5
|
-
import { useTranslation } from "../i18n.js";
|
|
6
4
|
import { PageHeaderSlotAvailableProvider } from "../page-header-slot.js";
|
|
7
5
|
import { usePrimitives } from "../primitives.js";
|
|
8
6
|
import { ActionFormBody } from "./action-form-body.js";
|
|
@@ -16,14 +14,13 @@ import { ProjectionDetailBody } from "./projection-detail-body.js";
|
|
|
16
14
|
import { ProjectionListBody } from "./projection-list-body.js";
|
|
17
15
|
import { lastSegment } from "./qn.js";
|
|
18
16
|
import { qualifyScreenId } from "./qualify-screen-id.js";
|
|
19
|
-
import { QueryLoadingBanner } from "./query-state-banners.js";
|
|
20
17
|
import { ReturnHostProvider, useReturnHost } from "./return-to.js";
|
|
21
18
|
import { screenAccessAllows } from "./screen-access.js";
|
|
22
19
|
import { ScreenAccessDeniedBanner, ScreenMissingState } from "./screen-shared.js";
|
|
20
|
+
import { ScreenNotice, ScreenVisibilityGate } from "./screen-visibility-gate.js";
|
|
23
21
|
import { SecretMintBody } from "./secret-mint-body.js";
|
|
24
22
|
import { SecretsEditBody } from "./secrets-edit-body.js";
|
|
25
23
|
import { useEmbeddedScreen } from "./use-embedded-screen.js";
|
|
26
|
-
import { evalVisibleWhenAfterShown } from "./visible-when.js";
|
|
27
24
|
export { qualifyScreenId };
|
|
28
25
|
/** Mirrors qualifyScreenId for nav QNs (registry form `<feature>:nav:<short-id>`).
|
|
29
26
|
* Callers building QNs (e.g. the WorkspaceShell resolver) should use this instead of string-concat, so a QN-schema change only touches one place. */
|
|
@@ -55,7 +52,7 @@ export function KumikoScreen({ schema, qn, translate, entityId, onRowClick, onCo
|
|
|
55
52
|
onCopyLink,
|
|
56
53
|
});
|
|
57
54
|
// Gate every entry (incl. direct URL): the body element is only mounted once the condition holds.
|
|
58
|
-
const body = screen.visibleWhen !== undefined ? (_jsx(ScreenVisibilityGate, { entityId: entityId,
|
|
55
|
+
const body = screen.visibleWhen !== undefined ? (_jsx(ScreenVisibilityGate, { entityId: entityId, visibleWhen: screen.visibleWhen, fallback: screen.fallback, translate: translate, renderFallbackScreen: (target, fallbackEntityId) => (_jsx(FallbackScreen, { featureName: schema.featureName, fallback: target, translate: translate, ...(fallbackEntityId !== undefined && { entityId: fallbackEntityId }) })), children: _jsx(StandInChainBoundary, { children: screenBody }) }, entityId)) : (_jsx(StandInChainBoundary, { children: screenBody }));
|
|
59
56
|
// An embedded screen (e.g. a dashboard panel) keeps its parent's host and
|
|
60
57
|
// must not portal its page header into the shell that hosts the outer screen.
|
|
61
58
|
if (outerHost !== undefined) {
|
|
@@ -63,41 +60,12 @@ export function KumikoScreen({ schema, qn, translate, entityId, onRowClick, onCo
|
|
|
63
60
|
}
|
|
64
61
|
return _jsx(ReturnHostProvider, { value: ownHost, children: body });
|
|
65
62
|
}
|
|
66
|
-
function ScreenVisibilityGate({ entityId, featureName, visibleWhen, fallback, translate, children, }) {
|
|
67
|
-
// An idParam gate without a route id has nothing to ask: treated as hidden, never queried.
|
|
68
|
-
const missingRouteId = visibleWhen.idParam !== undefined && entityId === undefined;
|
|
69
|
-
const result = useQuery(visibleWhen.query, visibleWhen.idParam !== undefined && entityId !== undefined
|
|
70
|
-
? { [visibleWhen.idParam]: entityId }
|
|
71
|
-
: {}, { live: true, enabled: !missingRouteId });
|
|
72
|
-
const wasShown = useRef(false);
|
|
73
|
-
const verdict = missingRouteId
|
|
74
|
-
? "hidden"
|
|
75
|
-
: evalVisibleWhenAfterShown(visibleWhen, result, wasShown.current);
|
|
76
|
-
useEffect(() => {
|
|
77
|
-
if (verdict === "visible")
|
|
78
|
-
wasShown.current = true;
|
|
79
|
-
}, [verdict]);
|
|
80
|
-
if (verdict === "visible")
|
|
81
|
-
return children;
|
|
82
|
-
if (verdict === "loading") {
|
|
83
|
-
return _jsx(QueryLoadingBanner, {});
|
|
84
|
-
}
|
|
85
|
-
if (fallback !== undefined) {
|
|
86
|
-
return (_jsx(FallbackScreen, { featureName: featureName, fallback: fallback, translate: translate, ...(visibleWhen.idParam !== undefined && entityId !== undefined && { entityId }) }));
|
|
87
|
-
}
|
|
88
|
-
return (_jsxs(_Fragment, { children: [_jsx(LockedHeaderScope, {}), _jsx(ScreenUnavailableBanner, {})] }));
|
|
89
|
-
}
|
|
90
|
-
function ScreenUnavailableBanner() {
|
|
91
|
-
const { Banner } = usePrimitives();
|
|
92
|
-
const t = useTranslation();
|
|
93
|
-
return (_jsx(Banner, { padded: true, variant: "info", testId: "kumiko-screen-unavailable", children: t("kumiko.screen.unavailable") }));
|
|
94
|
-
}
|
|
95
63
|
function FallbackScreen({ featureName, fallback, translate, entityId, }) {
|
|
96
64
|
const target = useEmbeddedScreen(featureName, fallback);
|
|
97
65
|
// Also undefined when the user lacks access to the fallback screen, which is
|
|
98
66
|
// not a framework error.
|
|
99
67
|
if (target === undefined) {
|
|
100
|
-
return (_jsxs(_Fragment, { children: [_jsx(LockedHeaderScope, {}), _jsx(
|
|
68
|
+
return (_jsxs(_Fragment, { children: [_jsx(LockedHeaderScope, {}), _jsx(ScreenNotice, {})] }));
|
|
101
69
|
}
|
|
102
70
|
return (_jsx(LockedHeaderScope, { standInScreenId: lastSegment(target.qn), children: _jsx(KumikoScreen, { schema: target.schema, qn: target.qn, translate: translate, ...(entityId !== undefined && { entityId }) }) }));
|
|
103
71
|
}
|
|
@@ -108,7 +108,7 @@ export function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
|
|
|
108
108
|
limit: query.limit,
|
|
109
109
|
urlState: query.urlState,
|
|
110
110
|
});
|
|
111
|
-
return (_jsxs(_Fragment, { children: [refetchErrorBanner, _jsx(ListGroupPageBridges, { ...data.listTable.aggregates.groupPages }), _jsx(ReferenceFacetBridges, { specs: query.facetSpecs, onOptions: query.handleProjectionFacetOptions }), renderProjectionList({
|
|
111
|
+
return (_jsxs(_Fragment, { children: [refetchErrorBanner, actions.toolbarGateQueries, _jsx(ListGroupPageBridges, { ...data.listTable.aggregates.groupPages }), _jsx(ReferenceFacetBridges, { specs: query.facetSpecs, onOptions: query.handleProjectionFacetOptions }), renderProjectionList({
|
|
112
112
|
schema,
|
|
113
113
|
screen,
|
|
114
114
|
translate,
|
package/dist/app/qn.d.ts
CHANGED
|
@@ -1,7 +1,2 @@
|
|
|
1
1
|
export declare function lastSegment(qn: string): string;
|
|
2
|
-
|
|
3
|
-
* Client-safe copy of framework `toKebab` (engine/qualified-name.ts).
|
|
4
|
-
* Must stay in sync — do NOT import from `@cosmicdrift/kumiko-framework/engine`
|
|
5
|
-
* here: that barrel pulls server deps into the browser bundle.
|
|
6
|
-
*/
|
|
7
|
-
export declare function toKebab(input: string): string;
|
|
2
|
+
export { toKebab } from "@cosmicdrift/kumiko-types/kebab-case";
|
package/dist/app/qn.js
CHANGED
|
@@ -18,15 +18,4 @@ export function lastSegment(qn) {
|
|
|
18
18
|
const idx = qn.lastIndexOf(":");
|
|
19
19
|
return idx < 0 ? qn : qn.slice(idx + 1);
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
* Client-safe copy of framework `toKebab` (engine/qualified-name.ts).
|
|
23
|
-
* Must stay in sync — do NOT import from `@cosmicdrift/kumiko-framework/engine`
|
|
24
|
-
* here: that barrel pulls server deps into the browser bundle.
|
|
25
|
-
*/
|
|
26
|
-
export function toKebab(input) {
|
|
27
|
-
return input
|
|
28
|
-
.replace(/\./g, "-")
|
|
29
|
-
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
|
|
30
|
-
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
31
|
-
.toLowerCase();
|
|
32
|
-
}
|
|
21
|
+
export { toKebab } from "@cosmicdrift/kumiko-types/kebab-case";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ScreenFallback, ScreenVisibility } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
import type { Translate } from "../headless/index.ts";
|
|
4
|
+
type NoticeKey = "kumiko.screen.unavailable" | "kumiko.screen.forbidden" | "kumiko.screen.notFound";
|
|
5
|
+
export declare function ScreenNotice({ messageKey, }: {
|
|
6
|
+
readonly messageKey?: NoticeKey;
|
|
7
|
+
}): ReactNode;
|
|
8
|
+
export declare function LockedNotice({ messageKey }: {
|
|
9
|
+
readonly messageKey?: NoticeKey;
|
|
10
|
+
}): ReactNode;
|
|
11
|
+
export declare function ScreenVisibilityGate({ entityId, visibleWhen, fallback, translate, renderFallbackScreen, children, }: {
|
|
12
|
+
readonly entityId: string | undefined;
|
|
13
|
+
readonly visibleWhen: ScreenVisibility;
|
|
14
|
+
readonly fallback: ScreenFallback | undefined;
|
|
15
|
+
readonly translate?: Translate;
|
|
16
|
+
readonly renderFallbackScreen: (target: string, entityId: string | undefined) => ReactNode;
|
|
17
|
+
readonly children: ReactNode;
|
|
18
|
+
}): ReactNode;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { fallbackTargetFor } from "@cosmicdrift/kumiko-types/screen";
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
import { useQuery } from "../hooks/use-query.js";
|
|
5
|
+
import { useTranslation } from "../i18n.js";
|
|
6
|
+
import { usePrimitives } from "../primitives.js";
|
|
7
|
+
import { LockedHeaderScope } from "./locked-screen-header.js";
|
|
8
|
+
import { QueryErrorBanner, QueryLoadingBanner } from "./query-state-banners.js";
|
|
9
|
+
import { evalVisibleWhenAfterShown, screenFallbackReason, } from "./visible-when.js";
|
|
10
|
+
export function ScreenNotice({ messageKey = "kumiko.screen.unavailable", }) {
|
|
11
|
+
const { Banner } = usePrimitives();
|
|
12
|
+
const t = useTranslation();
|
|
13
|
+
return (_jsx(Banner, { padded: true, variant: "info", testId: "kumiko-screen-unavailable", children: t(messageKey) }));
|
|
14
|
+
}
|
|
15
|
+
export function LockedNotice({ messageKey }) {
|
|
16
|
+
return (_jsxs(_Fragment, { children: [_jsx(LockedHeaderScope, {}), _jsx(ScreenNotice, { ...(messageKey !== undefined && { messageKey }) })] }));
|
|
17
|
+
}
|
|
18
|
+
// Only the map form gets reason-specific defaults; a plain string or no fallback keeps the
|
|
19
|
+
// single "unavailable" notice. `error` renders a retry banner instead, so its entry is a
|
|
20
|
+
// safety net for an error verdict that carries no error object.
|
|
21
|
+
const DEFAULT_NOTICE_KEY = {
|
|
22
|
+
hidden: "kumiko.screen.unavailable",
|
|
23
|
+
forbidden: "kumiko.screen.forbidden",
|
|
24
|
+
notFound: "kumiko.screen.notFound",
|
|
25
|
+
error: "kumiko.screen.unavailable",
|
|
26
|
+
};
|
|
27
|
+
export function ScreenVisibilityGate({ entityId, visibleWhen, fallback, translate, renderFallbackScreen, children, }) {
|
|
28
|
+
// An idParam gate without a route id has nothing to ask: treated as hidden, never queried.
|
|
29
|
+
const missingRouteId = visibleWhen.idParam !== undefined && entityId === undefined;
|
|
30
|
+
const result = useQuery(visibleWhen.query, visibleWhen.idParam !== undefined && entityId !== undefined
|
|
31
|
+
? { [visibleWhen.idParam]: entityId }
|
|
32
|
+
: {}, { live: true, enabled: !missingRouteId });
|
|
33
|
+
const wasShown = useRef(false);
|
|
34
|
+
const verdict = missingRouteId
|
|
35
|
+
? "hidden"
|
|
36
|
+
: evalVisibleWhenAfterShown(visibleWhen, result, wasShown.current);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (verdict === "visible")
|
|
39
|
+
wasShown.current = true;
|
|
40
|
+
}, [verdict]);
|
|
41
|
+
if (verdict === "visible")
|
|
42
|
+
return children;
|
|
43
|
+
if (verdict === "loading") {
|
|
44
|
+
return _jsx(QueryLoadingBanner, {});
|
|
45
|
+
}
|
|
46
|
+
return (_jsx(ClosedGate, { verdict: verdict, result: result, fallback: fallback, translate: translate, renderFallbackScreen: (target) => renderFallbackScreen(target, visibleWhen.idParam !== undefined && entityId !== undefined ? entityId : undefined) }));
|
|
47
|
+
}
|
|
48
|
+
function ClosedGate({ verdict, result, fallback, translate, renderFallbackScreen, }) {
|
|
49
|
+
const t = useTranslation();
|
|
50
|
+
const reason = screenFallbackReason(verdict, result.error);
|
|
51
|
+
const target = fallback === undefined ? undefined : fallbackTargetFor(fallback, reason);
|
|
52
|
+
if (target !== undefined)
|
|
53
|
+
return renderFallbackScreen(target);
|
|
54
|
+
if (typeof fallback !== "object")
|
|
55
|
+
return _jsx(LockedNotice, {});
|
|
56
|
+
if (reason === "error" && result.error) {
|
|
57
|
+
return (_jsxs(_Fragment, { children: [_jsx(LockedHeaderScope, {}), _jsx(QueryErrorBanner, { error: result.error, translate: translate ?? t, onRetry: result.refetch })] }));
|
|
58
|
+
}
|
|
59
|
+
return _jsx(LockedNotice, { messageKey: DEFAULT_NOTICE_KEY[reason] });
|
|
60
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type { ScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
1
2
|
import type { FeatureSchema } from "./feature-schema.ts";
|
|
2
3
|
export type EmbeddedScreenTarget = {
|
|
3
4
|
readonly schema: FeatureSchema;
|
|
4
5
|
readonly qn: string;
|
|
6
|
+
readonly screenType: ScreenDefinition["type"];
|
|
5
7
|
};
|
|
6
8
|
export declare function useEmbeddedScreen(hostFeatureName: string, screen: string): EmbeddedScreenTarget | undefined;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ToolbarAction } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
import type { ToolbarActionButton } from "../components/render-list-toolbar-action.tsx";
|
|
4
|
+
export declare function useGatedToolbarActions(toolbarActions: readonly ToolbarAction[] | undefined, resolved: readonly ToolbarActionButton[] | undefined): {
|
|
5
|
+
readonly actions: readonly ToolbarActionButton[] | undefined;
|
|
6
|
+
readonly gateQueries: ReactNode;
|
|
7
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { useQuery } from "../hooks/use-query.js";
|
|
4
|
+
import { evalVisibleWhen } from "./visible-when.js";
|
|
5
|
+
// Same query name, same answer: buttons gating on one query share a single live request.
|
|
6
|
+
function GateQuery({ query, onResult, onForget, }) {
|
|
7
|
+
const { data, error, loading, refetch } = useQuery(query, {}, { live: true });
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
onResult(query, { data, error, loading, refetch });
|
|
10
|
+
}, [query, data, error, loading, refetch, onResult]);
|
|
11
|
+
useEffect(() => () => onForget(query), [query, onForget]);
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
function gatesById(toolbarActions) {
|
|
15
|
+
const gates = new Map();
|
|
16
|
+
for (const action of toolbarActions ?? []) {
|
|
17
|
+
if (action.visibleWhen !== undefined)
|
|
18
|
+
gates.set(action.id, action.visibleWhen);
|
|
19
|
+
}
|
|
20
|
+
return gates;
|
|
21
|
+
}
|
|
22
|
+
// Fail-closed: a gated button shows only after its query answered with a match; while the
|
|
23
|
+
// query loads or after it failed it stays hidden.
|
|
24
|
+
export function useGatedToolbarActions(toolbarActions, resolved) {
|
|
25
|
+
const [results, setResults] = useState(new Map());
|
|
26
|
+
const report = useCallback((query, result) => {
|
|
27
|
+
setResults((prev) => new Map(prev).set(query, result));
|
|
28
|
+
}, []);
|
|
29
|
+
const forget = useCallback((query) => {
|
|
30
|
+
setResults((prev) => {
|
|
31
|
+
if (!prev.has(query))
|
|
32
|
+
return prev;
|
|
33
|
+
const next = new Map(prev);
|
|
34
|
+
next.delete(query);
|
|
35
|
+
return next;
|
|
36
|
+
});
|
|
37
|
+
}, []);
|
|
38
|
+
const gates = useMemo(() => gatesById(toolbarActions), [toolbarActions]);
|
|
39
|
+
const distinctQueries = useMemo(() => [...new Set([...gates.values()].map((gate) => gate.query))], [gates]);
|
|
40
|
+
const actions = useMemo(() => {
|
|
41
|
+
if (gates.size === 0)
|
|
42
|
+
return resolved;
|
|
43
|
+
const visible = resolved?.filter((action) => {
|
|
44
|
+
const gate = gates.get(action.id);
|
|
45
|
+
return gate === undefined || evalVisibleWhen(gate, results.get(gate.query)) === "visible";
|
|
46
|
+
});
|
|
47
|
+
return visible !== undefined && visible.length > 0 ? visible : undefined;
|
|
48
|
+
}, [gates, resolved, results]);
|
|
49
|
+
const gateQueries = distinctQueries.map((query) => (_jsx(GateQuery, { query: query, onResult: report, onForget: forget }, query)));
|
|
50
|
+
return { actions, gateQueries };
|
|
51
|
+
}
|
|
@@ -25,6 +25,7 @@ export declare function useProjectionListActions(input: ProjectionListActionsInp
|
|
|
25
25
|
runNavigate: (action: RowActionNavigate, row: ListRowViewModel) => void;
|
|
26
26
|
rowActions: readonly import("../primitives.tsx").DataTableRowAction[] | undefined;
|
|
27
27
|
toolbarActions: readonly import("../components/render-list-toolbar-action.tsx").ToolbarActionButton[] | undefined;
|
|
28
|
+
toolbarGateQueries: import("react").ReactNode;
|
|
28
29
|
bulk: import("../data-table-bulk-types.ts").DataTableBulk | undefined;
|
|
29
30
|
emptyContent: import("../components/list-empty-state.tsx").ListEmptyStateContent | undefined;
|
|
30
31
|
};
|
|
@@ -2,6 +2,7 @@ import { useCallback, useMemo } from "react";
|
|
|
2
2
|
import { useListBulk } from "./list-bulk-actions.js";
|
|
3
3
|
import { useListEmptyContent } from "./list-empty-content.js";
|
|
4
4
|
import { buildDefaultEditRowAction, buildProjectionRowActions, buildProjectionToolbarActions, runProjectionRowNavigate, } from "./row-actions.js";
|
|
5
|
+
import { useGatedToolbarActions } from "./use-gated-toolbar-actions.js";
|
|
5
6
|
export function useProjectionListActions(input) {
|
|
6
7
|
const { screen, effectiveTranslate, dispatcher, clientActions, nav, host, refreshRows, openDrawer, appFeatures, userRoles, defaultEditScreen, } = input;
|
|
7
8
|
// Entity targets: see EntityListBody.runNavigate for why the resolution happens
|
|
@@ -35,7 +36,7 @@ export function useProjectionListActions(input) {
|
|
|
35
36
|
defaultEditRowAction,
|
|
36
37
|
host,
|
|
37
38
|
]);
|
|
38
|
-
const
|
|
39
|
+
const resolvedToolbarActions = useMemo(() => buildProjectionToolbarActions({
|
|
39
40
|
toolbarActions: screen.toolbarActions,
|
|
40
41
|
translate: effectiveTranslate,
|
|
41
42
|
dispatcher,
|
|
@@ -55,6 +56,7 @@ export function useProjectionListActions(input) {
|
|
|
55
56
|
appFeatures,
|
|
56
57
|
userRoles,
|
|
57
58
|
]);
|
|
59
|
+
const { actions: toolbarActions, gateQueries: toolbarGateQueries } = useGatedToolbarActions(screen.toolbarActions, resolvedToolbarActions);
|
|
58
60
|
const bulk = useListBulk({
|
|
59
61
|
bulkActions: screen.bulkActions,
|
|
60
62
|
rowActions: screen.rowActions,
|
|
@@ -69,5 +71,12 @@ export function useProjectionListActions(input) {
|
|
|
69
71
|
openDrawer,
|
|
70
72
|
onWriteSuccess: refreshRows,
|
|
71
73
|
});
|
|
72
|
-
return {
|
|
74
|
+
return {
|
|
75
|
+
runNavigate,
|
|
76
|
+
rowActions,
|
|
77
|
+
toolbarActions,
|
|
78
|
+
toolbarGateQueries,
|
|
79
|
+
bulk,
|
|
80
|
+
emptyContent,
|
|
81
|
+
};
|
|
73
82
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import type { DashboardPanelVisibility } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
1
|
+
import type { DashboardPanelVisibility, ScreenFallbackReason } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
2
|
import type { UseQueryResult } from "../hooks/use-query.ts";
|
|
3
3
|
export type VisibleWhenVerdict = "loading" | "visible" | "hidden" | "error";
|
|
4
4
|
export declare function evalVisibleWhen(visibleWhen: DashboardPanelVisibility, result: UseQueryResult<Readonly<Record<string, unknown>>> | undefined): VisibleWhenVerdict;
|
|
5
5
|
export declare function evalVisibleWhenAfterShown(visibleWhen: DashboardPanelVisibility, result: UseQueryResult<Readonly<Record<string, unknown>>> | undefined, wasShown: boolean): VisibleWhenVerdict;
|
|
6
|
+
export type FailedVerdict = Exclude<VisibleWhenVerdict, "visible" | "loading">;
|
|
7
|
+
export declare function screenFallbackReason(verdict: FailedVerdict, error: {
|
|
8
|
+
readonly httpStatus: number;
|
|
9
|
+
} | null): ScreenFallbackReason;
|
package/dist/app/visible-when.js
CHANGED
|
@@ -16,3 +16,14 @@ export function evalVisibleWhenAfterShown(visibleWhen, result, wasShown) {
|
|
|
16
16
|
const lastGoodDataMatches = result?.data?.[visibleWhen.field] === visibleWhen.eq;
|
|
17
17
|
return verdict === "error" && wasShown && lastGoodDataMatches ? "visible" : verdict;
|
|
18
18
|
}
|
|
19
|
+
// Splits a closed gate by cause so a screen's `fallback` map can answer "not allowed" and
|
|
20
|
+
// "gone" differently from "condition unmet" and "something broke".
|
|
21
|
+
export function screenFallbackReason(verdict, error) {
|
|
22
|
+
if (verdict === "hidden")
|
|
23
|
+
return "hidden";
|
|
24
|
+
if (error?.httpStatus === 403)
|
|
25
|
+
return "forbidden";
|
|
26
|
+
if (error?.httpStatus === 404)
|
|
27
|
+
return "notFound";
|
|
28
|
+
return "error";
|
|
29
|
+
}
|
|
@@ -21,11 +21,13 @@ export type DataTableViewOptions = {
|
|
|
21
21
|
/** Present: pinning is on and the view menu has a pin section. Unpinned columns have no entry. */
|
|
22
22
|
readonly pins?: DataTableColumnPins;
|
|
23
23
|
readonly onPinsChange?: (next: DataTableColumnPins) => void;
|
|
24
|
-
/** Present: resizing is on
|
|
24
|
+
/** Present: resizing is on. Only columns with a start width or a user-chosen width have an entry (px);
|
|
25
|
+
* while none of the visible columns has one, the table keeps the auto layout and sizes by content. */
|
|
25
26
|
readonly widths?: DataTableColumnWidths;
|
|
26
27
|
/** Present together with `widths`; every column has an entry (px). */
|
|
27
28
|
readonly minWidths?: DataTableColumnWidths;
|
|
28
|
-
|
|
29
|
+
/** `others` holds the displayed widths of the columns that had no entry yet, so they keep their size once the layout turns fixed. */
|
|
30
|
+
readonly onWidthChange?: (field: string, width: number, others?: DataTableColumnWidths) => void;
|
|
29
31
|
readonly keyboardNavigation?: boolean;
|
|
30
32
|
/** Present: the view menu has a "group by" section. */
|
|
31
33
|
readonly groupBy?: DataTableGroupByChoice;
|
|
@@ -12,8 +12,10 @@ type LayoutSchemaColumn = {
|
|
|
12
12
|
export declare function defaultPins(schema: readonly LayoutSchemaColumn[]): DataTableColumnPins;
|
|
13
13
|
export declare function minWidthsOf(schema: readonly LayoutSchemaColumn[]): DataTableColumnWidths;
|
|
14
14
|
export declare function clampWidth(width: number, min: number): number;
|
|
15
|
+
/** Only columns with a schema `width`: the others are sized by their content until the user resizes. */
|
|
15
16
|
export declare function defaultWidths(schema: readonly LayoutSchemaColumn[], minWidths: DataTableColumnWidths): DataTableColumnWidths;
|
|
16
|
-
/** A saved width wins per field;
|
|
17
|
+
/** A saved width wins per field; fields the schema no longer has are dropped. */
|
|
17
18
|
export declare function mergeSavedWidths(defaults: DataTableColumnWidths, saved: Readonly<Record<string, number>>, minWidths: DataTableColumnWidths): DataTableColumnWidths;
|
|
19
|
+
export declare function clampWidths(widths: DataTableColumnWidths, minWidths: DataTableColumnWidths): DataTableColumnWidths;
|
|
18
20
|
export declare function sameRecord<T>(a: Readonly<Record<string, T>>, b: Readonly<Record<string, T>>): boolean;
|
|
19
21
|
export {};
|
|
@@ -16,20 +16,33 @@ export function minWidthsOf(schema) {
|
|
|
16
16
|
export function clampWidth(width, min) {
|
|
17
17
|
return Math.min(MAX_COLUMN_WIDTH_PX, Math.max(min, Math.round(width)));
|
|
18
18
|
}
|
|
19
|
+
/** Only columns with a schema `width`: the others are sized by their content until the user resizes. */
|
|
19
20
|
export function defaultWidths(schema, minWidths) {
|
|
20
|
-
return Object.fromEntries(schema.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
return Object.fromEntries(schema.flatMap((column) => column.width === undefined
|
|
22
|
+
? []
|
|
23
|
+
: [
|
|
24
|
+
[
|
|
25
|
+
column.field,
|
|
26
|
+
clampWidth(column.width, minWidths[column.field] ?? DEFAULT_MIN_COLUMN_WIDTH_PX),
|
|
27
|
+
],
|
|
28
|
+
]));
|
|
24
29
|
}
|
|
25
|
-
/** A saved width wins per field;
|
|
30
|
+
/** A saved width wins per field; fields the schema no longer has are dropped. */
|
|
26
31
|
export function mergeSavedWidths(defaults, saved, minWidths) {
|
|
27
|
-
return Object.fromEntries(Object.
|
|
32
|
+
return Object.fromEntries(Object.keys(minWidths).flatMap((field) => {
|
|
28
33
|
const savedWidth = saved[field];
|
|
29
34
|
const min = minWidths[field] ?? DEFAULT_MIN_COLUMN_WIDTH_PX;
|
|
30
|
-
|
|
35
|
+
if (savedWidth !== undefined)
|
|
36
|
+
return [[field, clampWidth(savedWidth, min)]];
|
|
37
|
+
const fallback = defaults[field];
|
|
38
|
+
return fallback === undefined ? [] : [[field, fallback]];
|
|
31
39
|
}));
|
|
32
40
|
}
|
|
41
|
+
export function clampWidths(widths, minWidths) {
|
|
42
|
+
return Object.fromEntries(Object.entries(widths).flatMap(([field, width]) => field in minWidths
|
|
43
|
+
? [[field, clampWidth(width, minWidths[field] ?? DEFAULT_MIN_COLUMN_WIDTH_PX)]]
|
|
44
|
+
: []));
|
|
45
|
+
}
|
|
33
46
|
export function sameRecord(a, b) {
|
|
34
47
|
const keys = Object.keys(a);
|
|
35
48
|
return keys.length === Object.keys(b).length && keys.every((key) => a[key] === b[key]);
|
|
@@ -21,7 +21,7 @@ export type ListViewState = {
|
|
|
21
21
|
/** Undefined unless `table.resizing` is on. */
|
|
22
22
|
readonly widths: DataTableColumnWidths | undefined;
|
|
23
23
|
readonly minWidths: DataTableColumnWidths | undefined;
|
|
24
|
-
readonly setWidth: (field: string, width: number) => void;
|
|
24
|
+
readonly setWidth: (field: string, width: number, others?: DataTableColumnWidths) => void;
|
|
25
25
|
/** Active group field, "" = not grouped. */
|
|
26
26
|
readonly groupBy: string;
|
|
27
27
|
/** Undefined unless `table.grouping.userChoice` is on. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { normalizeListColumn } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
2
|
import { useCallback, useMemo, useState } from "react";
|
|
3
|
-
import { clampWidth, DEFAULT_MIN_COLUMN_WIDTH_PX, defaultPins, defaultWidths, mergeSavedWidths, minWidthsOf, sameRecord, } from "./list-column-layout.js";
|
|
3
|
+
import { clampWidth, clampWidths, DEFAULT_MIN_COLUMN_WIDTH_PX, defaultPins, defaultWidths, mergeSavedWidths, minWidthsOf, sameRecord, } from "./list-column-layout.js";
|
|
4
4
|
import { sanitizeStoredListState } from "./list-state-schema.js";
|
|
5
5
|
export function listViewSchemaColumns(columns) {
|
|
6
6
|
return columns.map((column) => {
|
|
@@ -124,9 +124,10 @@ export function useListViewState(args) {
|
|
|
124
124
|
}, [commit, view]);
|
|
125
125
|
const setDensity = useCallback((next) => commit({ ...view, density: next }), [commit, view]);
|
|
126
126
|
const setPins = useCallback((next) => commit({ ...view, pins: next }), [commit, view]);
|
|
127
|
-
const setWidth = useCallback((field, width) => commit({
|
|
127
|
+
const setWidth = useCallback((field, width, others = {}) => commit({
|
|
128
128
|
...view,
|
|
129
129
|
widths: {
|
|
130
|
+
...clampWidths(others, minWidths),
|
|
130
131
|
...view.widths,
|
|
131
132
|
[field]: clampWidth(width, minWidths[field] ?? DEFAULT_MIN_COLUMN_WIDTH_PX),
|
|
132
133
|
},
|
package/dist/i18n-defaults.js
CHANGED
|
@@ -191,6 +191,8 @@ export const kumikoDefaultTranslations = {
|
|
|
191
191
|
"kumiko.dashboard.progress.unlimited": "unlimited",
|
|
192
192
|
"kumiko.dashboard.record.missing": "Open this page from a record to see its dashboard.",
|
|
193
193
|
"kumiko.dashboard.time-range": "Time range",
|
|
194
|
+
"kumiko.screen.forbidden": "You do not have access to this page.",
|
|
195
|
+
"kumiko.screen.notFound": "This page could not be found.",
|
|
194
196
|
"kumiko.screen.unavailable": "This page is not available right now.",
|
|
195
197
|
"kumiko.dashboard.brush-end": "Window end",
|
|
196
198
|
"kumiko.dashboard.brush-start": "Window start",
|
|
@@ -48,7 +48,7 @@ function PanelBody({ panel, label, screenId, screenParams, translate, }) {
|
|
|
48
48
|
}
|
|
49
49
|
function renderDashboardPanel(panel, featureName, screenId, screenParams, translate) {
|
|
50
50
|
if (panel.kind === "screen") {
|
|
51
|
-
return (_jsx(ScreenPanelTile, { panel: panel, featureName: featureName, translate: translate }, panel.id));
|
|
51
|
+
return (_jsx(ScreenPanelTile, { panel: panel, featureName: featureName, recordId: screenParams.recordId, translate: translate }, panel.id));
|
|
52
52
|
}
|
|
53
53
|
const label = panel.kind === "custom" || panel.label === undefined ? undefined : translate(panel.label);
|
|
54
54
|
return (_jsx(GatedGridCell, { panel: panel, screenParams: screenParams, label: label ?? panel.id, className: panelSpanClassName(panel), children: _jsx(PanelBody, { panel: panel, label: label, screenId: screenId, screenParams: screenParams, translate: translate }) }, panel.id));
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { DashboardScreenPanel } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
2
|
import { type ReactNode } from "react";
|
|
3
3
|
import type { Translate } from "../../headless/index.ts";
|
|
4
|
-
export declare function ScreenPanelTile({ panel, featureName, translate, }: {
|
|
4
|
+
export declare function ScreenPanelTile({ panel, featureName, recordId, translate, }: {
|
|
5
5
|
readonly panel: DashboardScreenPanel;
|
|
6
6
|
readonly featureName: string;
|
|
7
|
+
readonly recordId: string | undefined;
|
|
7
8
|
readonly translate: Translate;
|
|
8
9
|
}): ReactNode;
|
|
@@ -6,19 +6,21 @@ import { EmbeddedFormProvider } from "../primitives/index.js";
|
|
|
6
6
|
import { SectionCard } from "../widgets/section-card.js";
|
|
7
7
|
import { PanelError, WIDE_PANEL } from "./dashboard-shared.js";
|
|
8
8
|
import { NO_GATE_PAYLOAD, VisibilityQueryContext, visibilityGate, } from "./dashboard-visibility.js";
|
|
9
|
-
function EmbeddedScreenPanelTile({ panel, target, translate, }) {
|
|
10
|
-
|
|
9
|
+
function EmbeddedScreenPanelTile({ recordId, panel, target, translate, }) {
|
|
10
|
+
// Only a record screen takes the dashboard's bound record; other screens have no id to read.
|
|
11
|
+
const entityId = target.screenType === "projectionDetail" ? recordId : undefined;
|
|
12
|
+
const screenBody = (_jsx(KumikoScreen, { schema: target.schema, qn: target.qn, translate: translate, ...(entityId !== undefined && { entityId }) }));
|
|
11
13
|
const embedded = panel.chromeless === true ? (_jsx(EmbeddedFormProvider, { children: _jsx(EmbeddedScreenProvider, { children: screenBody }) })) : (screenBody);
|
|
12
14
|
return (_jsx("div", { className: cn("min-w-0", WIDE_PANEL), "data-testid": `dashboard-panel-${panel.id}`, children: panel.label !== undefined && panel.chromeless !== true ? (_jsx(SectionCard, { title: translate(panel.label), children: embedded })) : (_jsxs(_Fragment, { children: [panel.label !== undefined && (_jsx("h3", { className: "mb-3 text-sm font-semibold text-foreground", children: translate(panel.label) })), embedded] })) }));
|
|
13
15
|
}
|
|
14
16
|
// Own component so the target lookup (a hook) only runs for panels that actually fall back.
|
|
15
|
-
function FallbackScreenPanelTile({ panel, fallback, featureName, translate, }) {
|
|
17
|
+
function FallbackScreenPanelTile({ panel, fallback, featureName, recordId, translate, }) {
|
|
16
18
|
const target = useEmbeddedScreen(featureName, fallback);
|
|
17
19
|
if (target === undefined)
|
|
18
20
|
return null;
|
|
19
|
-
return _jsx(EmbeddedScreenPanelTile, { panel: panel, target: target, translate: translate });
|
|
21
|
+
return (_jsx(EmbeddedScreenPanelTile, { panel: panel, target: target, recordId: recordId, translate: translate }));
|
|
20
22
|
}
|
|
21
|
-
export function ScreenPanelTile({ panel, featureName, translate, }) {
|
|
23
|
+
export function ScreenPanelTile({ panel, featureName, recordId, translate, }) {
|
|
22
24
|
const visibleWhen = panel.visibleWhen;
|
|
23
25
|
const visibility = useContext(VisibilityQueryContext).get(visibleWhen !== undefined ? visibilityGate(visibleWhen.query, NO_GATE_PAYLOAD).key : "");
|
|
24
26
|
const target = useEmbeddedScreen(featureName, panel.screen);
|
|
@@ -27,7 +29,7 @@ export function ScreenPanelTile({ panel, featureName, translate, }) {
|
|
|
27
29
|
if (verdict === "loading")
|
|
28
30
|
return null;
|
|
29
31
|
if (verdict !== "visible" && panel.fallback !== undefined) {
|
|
30
|
-
return (_jsx(FallbackScreenPanelTile, { panel: panel, fallback: panel.fallback, featureName: featureName, translate: translate }));
|
|
32
|
+
return (_jsx(FallbackScreenPanelTile, { panel: panel, fallback: panel.fallback, featureName: featureName, recordId: recordId, translate: translate }));
|
|
31
33
|
}
|
|
32
34
|
if (target === undefined)
|
|
33
35
|
return null;
|
|
@@ -38,5 +40,5 @@ export function ScreenPanelTile({ panel, featureName, translate, }) {
|
|
|
38
40
|
}
|
|
39
41
|
if (verdict !== "visible")
|
|
40
42
|
return null;
|
|
41
|
-
return _jsx(EmbeddedScreenPanelTile, { panel: panel, target: target, translate: translate });
|
|
43
|
+
return (_jsx(EmbeddedScreenPanelTile, { panel: panel, target: target, recordId: recordId, translate: translate }));
|
|
42
44
|
}
|
|
@@ -11,6 +11,8 @@ export type ScreenParams = {
|
|
|
11
11
|
readonly rangeParams: Readonly<Record<string, unknown>>;
|
|
12
12
|
/** `{ [payloadKey]: recordId }` of a record-bound dashboard, else empty. */
|
|
13
13
|
readonly recordParams: Readonly<Record<string, unknown>>;
|
|
14
|
+
/** Id read from `record.param`; set whenever the dashboard renders its panels. */
|
|
15
|
+
readonly recordId?: string;
|
|
14
16
|
};
|
|
15
17
|
export declare function panelPayload(panel: DashboardPanelQueryOptions, screenParams: ScreenParams): Readonly<Record<string, unknown>>;
|
|
16
18
|
type RefetchQuery = () => Promise<void>;
|
|
@@ -12,7 +12,12 @@ export function useDashboardScreenParams(screen, translate, nav) {
|
|
|
12
12
|
const recordQueryName = recordBinding?.query;
|
|
13
13
|
const { params: filterParams, picker } = useFilterParams(screen, !recordMissing);
|
|
14
14
|
const { params: rangeParams, control: rangeControl } = useTimeRange(screen, translate);
|
|
15
|
-
const screenParams = {
|
|
15
|
+
const screenParams = {
|
|
16
|
+
filterParams,
|
|
17
|
+
rangeParams,
|
|
18
|
+
recordParams,
|
|
19
|
+
...(recordId !== "" && { recordId }),
|
|
20
|
+
};
|
|
16
21
|
const { loadedAtMs, reportLoaded } = useLoadedAt(screenParams);
|
|
17
22
|
return {
|
|
18
23
|
recordBinding,
|
|
@@ -5,9 +5,12 @@ export type BreadcrumbCrumb = {
|
|
|
5
5
|
readonly screenId?: string;
|
|
6
6
|
};
|
|
7
7
|
export declare function screenTitleKey(screenShortId: string): string;
|
|
8
|
+
export declare function screenCreateTitleKey(screenShortId: string): string;
|
|
8
9
|
/** The short id of {screenId}'s parent list screen, resolved via the same
|
|
9
10
|
* logic as `resolveDetailBreadcrumb` — `undefined` when nothing resolves
|
|
10
11
|
* (unknown screen, or no explicit/heuristic parent). */
|
|
11
12
|
export declare function resolveParentScreenId(screens: readonly ScreenDefinition[], screenId: string): string | undefined;
|
|
12
|
-
export declare function resolveDetailBreadcrumb(screens: readonly ScreenDefinition[], detailScreenId: string, t: (key: string) => string
|
|
13
|
+
export declare function resolveDetailBreadcrumb(screens: readonly ScreenDefinition[], detailScreenId: string, t: (key: string) => string, options?: {
|
|
14
|
+
readonly creating?: boolean;
|
|
15
|
+
}): readonly BreadcrumbCrumb[] | undefined;
|
|
13
16
|
export declare function activeNavLabel(nodes: readonly NavNode[], screenId: string, t: (key: string) => string): string | undefined;
|