@cosmicdrift/kumiko-renderer 0.393.0 → 0.395.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/content-preview.js +1 -9
- 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/secrets-edit-body.js +5 -4
- 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/components/form-screen-layout.d.ts +2 -1
- package/dist/components/form-screen-layout.js +2 -2
- package/dist/components/render-edit-banners.js +1 -1
- package/dist/components/render-edit-layout-flags.d.ts +2 -1
- package/dist/components/render-edit-layout-flags.js +3 -2
- package/dist/components/render-edit-logic.d.ts +2 -1
- package/dist/components/render-edit-logic.js +14 -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/client-plugin.d.ts +1 -1
- 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/index.d.ts +1 -1
- package/dist/web/index.js +1 -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/lib/upload-file-ref.d.ts +4 -1
- package/dist/web/lib/upload-file-ref.js +15 -3
- 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/file-upload.js +5 -2
- package/dist/web/primitives/files-dropzone.js +8 -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 +27 -14
- package/dist/web/widgets/plan-card.d.ts +2 -0
- package/dist/web/widgets/plan-card.js +5 -1
- package/package.json +6 -19
- package/src/changes.json +54 -0
- package/dist/web/app/rich-content-editor.d.ts +0 -3
- package/dist/web/app/rich-content-editor.js +0 -15
- package/dist/web/app/tiptap-editor.d.ts +0 -3
- package/dist/web/app/tiptap-editor.js +0 -120
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useId } from "react";
|
|
3
|
-
import { escapeHtmlAttr } from "../headless/index.js";
|
|
4
3
|
import { useContentEditor } from "./content-editors.js";
|
|
5
4
|
const VARIABLE_PATTERN = /\{\{\s*(\w+)\s*\}\}/g;
|
|
6
5
|
const noop = () => { };
|
|
@@ -12,15 +11,8 @@ export function substituteVariables(content, variables) {
|
|
|
12
11
|
export function ContentPreview({ content, variables, contentFormat, }) {
|
|
13
12
|
const Editor = useContentEditor(contentFormat);
|
|
14
13
|
const id = useId();
|
|
15
|
-
// "rich" content is HTML (see ContentCollectionDefinition.contentFormat) —
|
|
16
|
-
// an example value substituted in raw could break the markup (`<`) or
|
|
17
|
-
// render as an unescaped entity (`&`). "plain"/"markdown" content is text,
|
|
18
|
-
// no escaping wanted there.
|
|
19
|
-
const safeVariables = contentFormat === "rich"
|
|
20
|
-
? Object.fromEntries(Object.entries(variables).map(([name, value]) => [name, escapeHtmlAttr(value)]))
|
|
21
|
-
: variables;
|
|
22
14
|
// "document" keeps `{{x}}` as-is: substituting it into a button/link href would make the
|
|
23
15
|
// schema drop that node; variable chips stay visible.
|
|
24
|
-
const value = contentFormat === "document" ? content : substituteVariables(content,
|
|
16
|
+
const value = contentFormat === "document" ? content : substituteVariables(content, variables);
|
|
25
17
|
return _jsx(Editor, { id: id, value: value, onChange: noop, variables: [], readOnly: true });
|
|
26
18
|
}
|
|
@@ -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,5 +1,6 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useMemo, useState } from "react";
|
|
3
|
+
import { FormScreenLayout } from "../components/form-screen-layout.js";
|
|
3
4
|
import { useDispatcher } from "../context/dispatcher-context.js";
|
|
4
5
|
import { useUserRoles } from "../context/user-roles-context.js";
|
|
5
6
|
import { useQuery } from "../hooks/use-query.js";
|
|
@@ -20,7 +21,7 @@ function groupSectionsByTitle(sections) {
|
|
|
20
21
|
// unlike ConfigEditBody there is no server value to pre-fill a draft with —
|
|
21
22
|
// every input starts at "" and stays that way unless the user types into it.
|
|
22
23
|
export function SecretsEditBody({ screen, translate }) {
|
|
23
|
-
const { Banner, Dialog,
|
|
24
|
+
const { Banner, Dialog, Section, Field, Input, Button, Text, Grid } = usePrimitives();
|
|
24
25
|
const t = useTranslation();
|
|
25
26
|
const effectiveTranslate = translate ?? t;
|
|
26
27
|
const dispatcher = useDispatcher();
|
|
@@ -111,10 +112,10 @@ export function SecretsEditBody({ screen, translate }) {
|
|
|
111
112
|
}))
|
|
112
113
|
.filter((group) => group.fields.length > 0);
|
|
113
114
|
const unsavedCount = Object.values(drafts).filter((value) => value.trim() !== "").length;
|
|
114
|
-
return (_jsxs(_Fragment, { children: [_jsxs(
|
|
115
|
+
return (_jsxs(_Fragment, { children: [_jsxs(FormScreenLayout, { onSubmit: () => {
|
|
115
116
|
void handleSubmit();
|
|
116
|
-
}, testId: "secrets-edit-form", width: "full",
|
|
117
|
-
|
|
117
|
+
}, testId: "secrets-edit-form", width: "full", unsavedCount: unsavedCount, ...(screen.description !== undefined && {
|
|
118
|
+
description: effectiveTranslate(screen.description),
|
|
118
119
|
}), actions: _jsxs(_Fragment, { children: [unsavedCount > 0 && (_jsx(Button, { type: "button", variant: "ghost", disabled: submitting, onClick: () => setDrafts({}), testId: "secrets-edit-discard", [STICKY_PRIMARY_ACTION_PROP]: true, children: effectiveTranslate("kumiko.form.discard") })), _jsx(Button, { type: "submit", variant: "primary", loading: submitting, disabled: submitting || unsavedCount === 0, testId: "secrets-edit-submit", children: effectiveTranslate("kumiko.form.saveChanges") })] }), children: [submitError !== null && (_jsx(Banner, { variant: "error", testId: "secrets-edit-error", children: submitError })), sectionGroups.map((section) => (_jsx(Section, { layout: "settings-list", title: effectiveTranslate(section.title), subtitle: effectiveTranslate("config.secrets.description"), children: _jsx(Grid, { columns: 1, list: true, children: section.fields.map((fieldId) => {
|
|
119
120
|
const qualified = screen.secretKeys[fieldId];
|
|
120
121
|
if (qualified === undefined)
|
|
@@ -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
|
+
}
|
|
@@ -7,7 +7,8 @@ export type FormScreenLayoutProps = {
|
|
|
7
7
|
readonly secondaryActions?: ReactNode;
|
|
8
8
|
readonly onSubmit?: () => void;
|
|
9
9
|
readonly width?: FormWidth;
|
|
10
|
+
readonly unsavedCount?: number;
|
|
10
11
|
readonly testId?: string;
|
|
11
12
|
};
|
|
12
13
|
/** For custom screens: the standard form screen layout; fill it with `Section` (title, icon, subtitle). */
|
|
13
|
-
export declare function FormScreenLayout({ children, description, actions, secondaryActions, onSubmit, width, testId, }: FormScreenLayoutProps): ReactNode;
|
|
14
|
+
export declare function FormScreenLayout({ children, description, actions, secondaryActions, onSubmit, width, unsavedCount, testId, }: FormScreenLayoutProps): ReactNode;
|
|
@@ -7,7 +7,7 @@ const FORM_SCREEN_LAYOUT_PROPS = {
|
|
|
7
7
|
};
|
|
8
8
|
const NOOP = () => { };
|
|
9
9
|
/** For custom screens: the standard form screen layout; fill it with `Section` (title, icon, subtitle). */
|
|
10
|
-
export function FormScreenLayout({ children, description, actions, secondaryActions, onSubmit = NOOP, width, testId, }) {
|
|
10
|
+
export function FormScreenLayout({ children, description, actions, secondaryActions, onSubmit = NOOP, width, unsavedCount, testId, }) {
|
|
11
11
|
const { Form } = usePrimitives();
|
|
12
|
-
return (_jsx(Form, { onSubmit: onSubmit, ...FORM_SCREEN_LAYOUT_PROPS, ...(description !== undefined && { subtitle: description }), ...(actions !== undefined && { actions }), ...(secondaryActions !== undefined && { secondaryActions }), ...(width !== undefined && { width }), ...(testId !== undefined && { testId }), children: children }));
|
|
12
|
+
return (_jsx(Form, { onSubmit: onSubmit, ...FORM_SCREEN_LAYOUT_PROPS, ...(description !== undefined && { subtitle: description }), ...(actions !== undefined && { actions }), ...(secondaryActions !== undefined && { secondaryActions }), ...(width !== undefined && { width }), ...(unsavedCount !== undefined && { unsavedCount }), ...(testId !== undefined && { testId }), children: children }));
|
|
13
13
|
}
|
|
@@ -52,7 +52,7 @@ export function renderFormErrorBanner({ engine: { props, state }, layout, }) {
|
|
|
52
52
|
onReload();
|
|
53
53
|
state.setFormError(null);
|
|
54
54
|
state.setExtraDeleteIssues([]);
|
|
55
|
-
}, testId: "render-edit-form-error-reload", children: translate("kumiko.actions.reload") })) : undefined, children: [_jsx(Text, { testId: "render-edit-form-error-key", children: translate(formError.i18nKey, formError.i18nParams) }), layout.rootIssues.map((issue) => (_jsx(Text, { testId: "render-edit-form-error-root", children: translate(issue.i18nKey, issue.params) }, `${issue.path}:${issue.i18nKey}`))), state.extraDeleteIssues.map((issue) => (_jsx(Text, { testId: "render-edit-form-error-extra", children: translate(issue.i18nKey, issue.params) }, `${issue.path}:${issue.i18nKey}`)))] })));
|
|
55
|
+
}, testId: "render-edit-form-error-reload", children: translate("kumiko.actions.reload") })) : undefined, children: [_jsx(Text, { testId: "render-edit-form-error-key", children: translate(formError.i18nKey, formError.i18nParams) }), layout.rootIssues.map((issue) => (_jsx(Text, { testId: "render-edit-form-error-root", children: translate(issue.i18nKey, issue.params) }, `${issue.path}:${issue.i18nKey}`))), layout.unplacedIssues.map((issue) => (_jsx(Text, { testId: "render-edit-form-error-unplaced", children: translate(issue.i18nKey, issue.params) }, `${issue.path}:${issue.i18nKey}`))), state.extraDeleteIssues.map((issue) => (_jsx(Text, { testId: "render-edit-form-error-extra", children: translate(issue.i18nKey, issue.params) }, `${issue.path}:${issue.i18nKey}`)))] })));
|
|
56
56
|
}
|
|
57
57
|
export function renderRootIssueBanners({ engine: { state }, layout, }) {
|
|
58
58
|
const { Banner, Text } = state.primitives;
|
|
@@ -3,6 +3,7 @@ import type { FieldIssue, FormValues } from "../headless/index.ts";
|
|
|
3
3
|
import type { RenderEditEngine } from "./render-edit-render-model.ts";
|
|
4
4
|
export type LayoutFlags = {
|
|
5
5
|
readonly rootIssues: readonly FieldIssue[];
|
|
6
|
+
readonly unplacedIssues: readonly FieldIssue[];
|
|
6
7
|
readonly isScreenForm: boolean;
|
|
7
8
|
readonly titleInShell: boolean;
|
|
8
9
|
readonly menuPageHeaderActions: boolean;
|
|
@@ -25,5 +26,5 @@ export type FooterFlags = {
|
|
|
25
26
|
readonly hasUnsavedInput: boolean;
|
|
26
27
|
};
|
|
27
28
|
export type FooterSlotComponent = ReturnType<typeof useExtensionSectionComponent>;
|
|
28
|
-
export declare function deriveLayoutFlags<TValues extends FormValues, TCtx>({ props, state, form, }: RenderEditEngine<TValues, TCtx>): LayoutFlags;
|
|
29
|
+
export declare function deriveLayoutFlags<TValues extends FormValues, TCtx>({ props, state, form, sections, }: RenderEditEngine<TValues, TCtx>): LayoutFlags;
|
|
29
30
|
export declare function deriveFooterFlags<TValues extends FormValues, TCtx>(engine: RenderEditEngine<TValues, TCtx>, layout: LayoutFlags, footerSlotComponent: FooterSlotComponent): FooterFlags;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PROJECTION_DETAIL_ENTITY } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
2
|
import { shouldRenderActionsIconOnly } from "../primitives.js";
|
|
3
|
-
import { ROOT_ISSUE_PATH, resolveExtensionEntityId } from "./render-edit-logic.js";
|
|
3
|
+
import { issuesWithoutRenderedField, ROOT_ISSUE_PATH, resolveExtensionEntityId, } from "./render-edit-logic.js";
|
|
4
4
|
function hasSecondaryFormActions(props, menuPageHeaderActions, cancelInHeaderMenu) {
|
|
5
5
|
const { onDelete, onCopyLink, actions, menuActions, onCancel } = props;
|
|
6
6
|
const hasRecordActions = !menuPageHeaderActions && (onDelete !== undefined || onCopyLink !== undefined);
|
|
@@ -9,7 +9,7 @@ function hasSecondaryFormActions(props, menuPageHeaderActions, cancelInHeaderMen
|
|
|
9
9
|
const hasCancel = onCancel !== undefined && !cancelInHeaderMenu;
|
|
10
10
|
return hasRecordActions || hasCustomActions || hasMenuActions || hasCancel;
|
|
11
11
|
}
|
|
12
|
-
export function deriveLayoutFlags({ props, state, form, }) {
|
|
12
|
+
export function deriveLayoutFlags({ props, state, form, sections, }) {
|
|
13
13
|
const { fillScreenHeight, hideSectionTitles, headerRegion, actions, onCancel } = props;
|
|
14
14
|
const { PageHeader, ActionOverflowMenu } = state.primitives;
|
|
15
15
|
// Screen forms (entityEdit/actionForm filling the shell height) drop the card;
|
|
@@ -24,6 +24,7 @@ export function deriveLayoutFlags({ props, state, form, }) {
|
|
|
24
24
|
const cancelInHeaderMenu = menuPageHeaderActions && state.isWizard && onCancel !== undefined;
|
|
25
25
|
return {
|
|
26
26
|
rootIssues,
|
|
27
|
+
unplacedIssues: issuesWithoutRenderedField(form.snapshot.errors, sections.filteredSections, new Set(Object.keys(props.formDerivation?.derive ?? {}))),
|
|
27
28
|
isScreenForm,
|
|
28
29
|
titleInShell,
|
|
29
30
|
menuPageHeaderActions,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type { DispatcherError, EditFieldViewModel, EditSectionViewModel, SubmitResult } from "../headless/index.ts";
|
|
1
|
+
import type { DispatcherError, EditFieldViewModel, EditSectionViewModel, FieldIssue, SubmitResult } from "../headless/index.ts";
|
|
2
2
|
export declare function hasEditableSection(sections: readonly EditSectionViewModel[]): boolean;
|
|
3
3
|
export declare function resolveExtensionEntityId(entityIdProp: string | null | undefined, vmId: string | null): string | null;
|
|
4
4
|
export declare function shouldNotifyCaller(result: SubmitResult<unknown>, extensionsPersisted: boolean): boolean;
|
|
5
5
|
export declare function issueRootField(path: string): string;
|
|
6
6
|
export declare function hasIssueWithoutRenderedField(issuePaths: readonly string[], sections: readonly EditSectionViewModel[], derivedTargets?: ReadonlySet<string>): boolean;
|
|
7
|
+
export declare function issuesWithoutRenderedField(errors: Readonly<Record<string, readonly FieldIssue[]>>, sections: readonly EditSectionViewModel[], derivedTargets?: ReadonlySet<string>): readonly FieldIssue[];
|
|
7
8
|
export declare function hasIssueWithoutVisibleField(issuePaths: readonly string[], fields: readonly EditFieldViewModel[]): boolean;
|
|
8
9
|
export declare function findFirstErroringSectionIndex(sections: readonly EditSectionViewModel[], issuePaths: readonly string[]): number | undefined;
|
|
9
10
|
export declare function filterEditSections(sections: readonly EditSectionViewModel[], fieldsFilter: readonly string[] | undefined): readonly EditSectionViewModel[];
|
|
@@ -42,6 +42,20 @@ export function hasIssueWithoutRenderedField(issuePaths, sections, derivedTarget
|
|
|
42
42
|
const renderedFields = sections.flatMap((s) => s.kind === "fields" && s.visible ? s.fields : []);
|
|
43
43
|
return hasIssueWithoutVisibleField(paths, renderedFields);
|
|
44
44
|
}
|
|
45
|
+
// The server issues the banner has to carry itself because no rendered field shows them
|
|
46
|
+
// (an extension section's value such as a page body, `version`, a hidden field).
|
|
47
|
+
export function issuesWithoutRenderedField(errors, sections, derivedTargets = new Set()) {
|
|
48
|
+
const visibleFields = new Set(sections
|
|
49
|
+
.flatMap((s) => (s.kind === "fields" && s.visible ? s.fields : []))
|
|
50
|
+
.filter((f) => f.visible)
|
|
51
|
+
.map((f) => f.field));
|
|
52
|
+
return Object.entries(errors)
|
|
53
|
+
.filter(([path]) => {
|
|
54
|
+
const root = issueRootField(path);
|
|
55
|
+
return path !== ROOT_ISSUE_PATH && !derivedTargets.has(root) && !visibleFields.has(root);
|
|
56
|
+
})
|
|
57
|
+
.flatMap(([, issues]) => issues);
|
|
58
|
+
}
|
|
45
59
|
export function hasIssueWithoutVisibleField(issuePaths, fields) {
|
|
46
60
|
if (issuePaths.length === 0)
|
|
47
61
|
return true;
|
|
@@ -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 {};
|