@cosmicdrift/kumiko-renderer 0.327.0 → 0.328.1
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-shim.d.ts +14 -0
- package/{src/app/action-form-shim.ts → dist/app/action-form-shim.js} +26 -44
- package/dist/app/app-features-context.d.ts +8 -0
- package/dist/app/app-features-context.js +16 -0
- package/dist/app/column-renderers.d.ts +30 -0
- package/dist/app/column-renderers.js +34 -0
- package/dist/app/config-edit-shim.d.ts +11 -0
- package/{src/app/config-edit-shim.ts → dist/app/config-edit-shim.js} +12 -25
- package/dist/app/content-editors.d.ts +38 -0
- package/dist/app/content-editors.js +26 -0
- package/dist/app/content-preview.d.ts +11 -0
- package/dist/app/content-preview.js +30 -0
- package/dist/app/custom-screens.d.ts +11 -0
- package/dist/app/custom-screens.js +20 -0
- package/dist/app/dashboard-body.d.ts +16 -0
- package/dist/app/dashboard-body.js +11 -0
- package/dist/app/extension-form-submit.d.ts +32 -0
- package/dist/app/extension-form-submit.js +73 -0
- package/dist/app/extension-sections.d.ts +72 -0
- package/dist/app/extension-sections.js +34 -0
- package/dist/app/feature-schema.d.ts +4 -0
- package/{src/app/feature-schema.ts → dist/app/feature-schema.js} +12 -22
- package/dist/app/form-schema.d.ts +4 -0
- package/dist/app/form-schema.js +85 -0
- package/dist/app/kumiko-screen.d.ts +43 -0
- package/dist/app/kumiko-screen.js +2206 -0
- package/dist/app/layout-fields.d.ts +3 -0
- package/dist/app/layout-fields.js +20 -0
- package/dist/app/list-facets.d.ts +48 -0
- package/dist/app/list-facets.js +185 -0
- package/dist/app/nav.d.ts +61 -0
- package/dist/app/nav.js +139 -0
- package/dist/app/projection-detail-shim.d.ts +8 -0
- package/dist/app/projection-detail-shim.js +71 -0
- package/dist/app/projection-list-shim.d.ts +13 -0
- package/{src/app/projection-list-shim.ts → dist/app/projection-list-shim.js} +26 -38
- package/dist/app/qn.d.ts +7 -0
- package/{src/app/qn.ts → dist/app/qn.js} +9 -11
- package/dist/app/qualify-screen-id.d.ts +2 -0
- package/{src/app/qualify-screen-id.ts → dist/app/qualify-screen-id.js} +5 -6
- package/dist/app/reference-facet-bridge.d.ts +10 -0
- package/dist/app/reference-facet-bridge.js +22 -0
- package/dist/app/return-to.d.ts +31 -0
- package/dist/app/return-to.js +188 -0
- package/dist/app/row-actions.d.ts +77 -0
- package/dist/app/row-actions.js +416 -0
- package/dist/app/screen-access.d.ts +8 -0
- package/dist/app/screen-access.js +48 -0
- package/dist/app/secret-mint-body.d.ts +10 -0
- package/dist/app/secret-mint-body.js +140 -0
- package/dist/app/secrets-edit-body.d.ts +8 -0
- package/dist/app/secrets-edit-body.js +76 -0
- package/dist/app/use-embedded-screen.d.ts +6 -0
- package/dist/app/use-embedded-screen.js +19 -0
- package/dist/app/variable-chips.d.ts +7 -0
- package/dist/app/variable-chips.js +16 -0
- package/dist/app/write-failed-error.d.ts +6 -0
- package/dist/app/write-failed-error.js +21 -0
- package/dist/components/embedded-list-field.d.ts +10 -0
- package/dist/components/embedded-list-field.js +259 -0
- package/dist/components/grid-cell-for-field.d.ts +25 -0
- package/dist/components/grid-cell-for-field.js +31 -0
- package/dist/components/multi-select-checkboxes.d.ts +14 -0
- package/dist/components/multi-select-checkboxes.js +36 -0
- package/dist/components/reference-create-dialog.d.ts +13 -0
- package/dist/components/reference-create-dialog.js +46 -0
- package/dist/components/related-list-section.d.ts +25 -0
- package/dist/components/related-list-section.js +273 -0
- package/dist/components/render-edit-action-button.d.ts +22 -0
- package/dist/components/render-edit-action-button.js +43 -0
- package/dist/components/render-edit-logic.d.ts +6 -0
- package/dist/components/render-edit-logic.js +63 -0
- package/dist/components/render-edit-types.d.ts +209 -0
- package/dist/components/render-edit-types.js +1 -0
- package/dist/components/render-edit.d.ts +6 -0
- package/dist/components/render-edit.js +1169 -0
- package/dist/components/render-field.d.ts +38 -0
- package/dist/components/render-field.js +544 -0
- package/dist/components/render-list.d.ts +117 -0
- package/dist/components/render-list.js +232 -0
- package/dist/components/write-form-section.d.ts +17 -0
- package/dist/components/write-form-section.js +104 -0
- package/dist/context/dispatcher-context.d.ts +10 -0
- package/{src/context/dispatcher-context.tsx → dist/context/dispatcher-context.js} +16 -29
- package/dist/context/draft-storage-context.d.ts +12 -0
- package/dist/context/draft-storage-context.js +18 -0
- package/dist/context/user-roles-context.d.ts +7 -0
- package/{src/context/user-roles-context.tsx → dist/context/user-roles-context.js} +7 -15
- package/dist/format-when.d.ts +1 -0
- package/{src/format-when.ts → dist/format-when.js} +7 -7
- package/dist/hooks/reference-limits.d.ts +3 -0
- package/{src/hooks/reference-limits.ts → dist/hooks/reference-limits.js} +0 -1
- package/dist/hooks/use-ai-text.d.ts +52 -0
- package/dist/hooks/use-ai-text.js +90 -0
- package/dist/hooks/use-disclosure.d.ts +7 -0
- package/dist/hooks/use-disclosure.js +8 -0
- package/dist/hooks/use-form.d.ts +7 -0
- package/dist/hooks/use-form.js +36 -0
- package/dist/hooks/use-list-url-state.d.ts +43 -0
- package/dist/hooks/use-list-url-state.js +131 -0
- package/dist/hooks/use-mutation.d.ts +9 -0
- package/dist/hooks/use-mutation.js +37 -0
- package/dist/hooks/use-query.d.ts +14 -0
- package/dist/hooks/use-query.js +118 -0
- package/dist/hooks/use-reference-lookup.d.ts +14 -0
- package/dist/hooks/use-reference-lookup.js +63 -0
- package/dist/hooks/use-store.d.ts +3 -0
- package/dist/hooks/use-store.js +38 -0
- package/dist/hooks/use-stream-handler.d.ts +14 -0
- package/dist/hooks/use-stream-handler.js +93 -0
- package/dist/i18n-defaults.d.ts +2 -0
- package/dist/i18n-defaults.js +201 -0
- package/dist/i18n.d.ts +64 -0
- package/dist/i18n.js +166 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.js +55 -0
- package/dist/inside-drawer.d.ts +2 -0
- package/dist/inside-drawer.js +9 -0
- package/dist/page-header-slot.d.ts +2 -0
- package/dist/page-header-slot.js +9 -0
- package/dist/primitives.d.ts +1351 -0
- package/dist/primitives.js +73 -0
- package/dist/screen-fills-height.d.ts +3 -0
- package/dist/screen-fills-height.js +3 -0
- package/dist/share-links.d.ts +4 -0
- package/dist/share-links.js +11 -0
- package/dist/sort-by-accessor.d.ts +5 -0
- package/dist/sort-by-accessor.js +16 -0
- package/dist/sse/live-events.d.ts +23 -0
- package/dist/sse/live-events.js +28 -0
- package/dist/tokens.d.ts +83 -0
- package/dist/tokens.js +80 -0
- package/package.json +16 -9
- package/src/changes.json +52 -0
- package/src/__tests__/error-i18n-defaults.test.ts +0 -82
- package/src/__tests__/format-when.test.ts +0 -12
- package/src/__tests__/i18n.test.tsx +0 -283
- package/src/__tests__/merge-search-params-into-initial.test.ts +0 -525
- package/src/__tests__/qn.test.ts +0 -83
- package/src/__tests__/share-links.test.ts +0 -26
- package/src/__tests__/sort-by-accessor.test.ts +0 -48
- package/src/__tests__/stringify-nav-params.test.ts +0 -33
- package/src/__tests__/use-list-url-state.test.tsx +0 -250
- package/src/app/__tests__/action-form-field-labels.test.tsx +0 -92
- package/src/app/__tests__/action-form-shim.test.ts +0 -137
- package/src/app/__tests__/build-list-query-payload.test.ts +0 -107
- package/src/app/__tests__/config-edit-shim.test.ts +0 -57
- package/src/app/__tests__/content-editors.test.tsx +0 -128
- package/src/app/__tests__/content-preview.test.tsx +0 -184
- package/src/app/__tests__/entity-edit-create-search-params.test.tsx +0 -236
- package/src/app/__tests__/entity-edit-jsonb-embedded-submit.test.tsx +0 -239
- package/src/app/__tests__/entity-list-create-singleton.test.tsx +0 -128
- package/src/app/__tests__/entity-list-row-action-entity-target.test.tsx +0 -424
- package/src/app/__tests__/entity-list-row-action-icon.test.tsx +0 -248
- package/src/app/__tests__/entity-list-row-action-kumiko-actions-view.test.tsx +0 -162
- package/src/app/__tests__/entity-list-row-action-refetch.test.tsx +0 -401
- package/src/app/__tests__/entity-list-screen-padding.test.tsx +0 -111
- package/src/app/__tests__/feature-schema.test.ts +0 -40
- package/src/app/__tests__/form-schema.test.ts +0 -247
- package/src/app/__tests__/list-daterange-facet.test.ts +0 -165
- package/src/app/__tests__/list-filter-facets.test.tsx +0 -521
- package/src/app/__tests__/nav.test.ts +0 -198
- package/src/app/__tests__/projection-detail-actions.test.tsx +0 -719
- package/src/app/__tests__/projection-detail-reference-field.test.tsx +0 -165
- package/src/app/__tests__/projection-detail-shim.test.ts +0 -188
- package/src/app/__tests__/projection-detail-singleton.test.tsx +0 -191
- package/src/app/__tests__/projection-list-bad-shape.test.tsx +0 -152
- package/src/app/__tests__/projection-list-reference-column.test.tsx +0 -219
- package/src/app/__tests__/projection-list-search-sort.test.tsx +0 -212
- package/src/app/__tests__/qualified-names.test.ts +0 -37
- package/src/app/__tests__/return-to.test.ts +0 -396
- package/src/app/__tests__/row-actions-navigate-tab.test.ts +0 -223
- package/src/app/__tests__/row-default-edit-action.test.tsx +0 -324
- package/src/app/__tests__/screen-access-allows.test.ts +0 -34
- package/src/app/__tests__/secret-mint-body.test.tsx +0 -404
- package/src/app/__tests__/variable-chips.test.tsx +0 -70
- package/src/app/app-features-context.tsx +0 -25
- package/src/app/column-renderers.tsx +0 -68
- package/src/app/content-editors.tsx +0 -85
- package/src/app/content-preview.tsx +0 -67
- package/src/app/custom-screens.tsx +0 -29
- package/src/app/dashboard-body.tsx +0 -33
- package/src/app/extension-form-submit.tsx +0 -123
- package/src/app/extension-sections.tsx +0 -111
- package/src/app/form-schema.ts +0 -91
- package/src/app/kumiko-screen.tsx +0 -3614
- package/src/app/layout-fields.ts +0 -31
- package/src/app/list-facets.ts +0 -259
- package/src/app/nav.tsx +0 -258
- package/src/app/projection-detail-shim.ts +0 -85
- package/src/app/reference-facet-bridge.tsx +0 -58
- package/src/app/return-to.tsx +0 -234
- package/src/app/row-actions.ts +0 -639
- package/src/app/screen-access.ts +0 -63
- package/src/app/secret-mint-body.tsx +0 -259
- package/src/app/secrets-edit-body.tsx +0 -194
- package/src/app/use-embedded-screen.ts +0 -27
- package/src/app/variable-chips.tsx +0 -39
- package/src/app/write-failed-error.ts +0 -27
- package/src/components/__tests__/embedded-list-field.test.tsx +0 -531
- package/src/components/__tests__/multi-select-checkboxes.test.tsx +0 -162
- package/src/components/__tests__/related-list-section-empty-state.test.tsx +0 -143
- package/src/components/__tests__/related-list-section.test.tsx +0 -1669
- package/src/components/__tests__/render-edit-action-button.test.tsx +0 -205
- package/src/components/__tests__/render-edit-hidden-section.test.tsx +0 -128
- package/src/components/__tests__/render-edit-logic.test.ts +0 -314
- package/src/components/__tests__/render-edit-screen-description.test.tsx +0 -152
- package/src/components/__tests__/render-edit-submit-actions.test.tsx +0 -460
- package/src/components/__tests__/render-field-app-locale.test.tsx +0 -142
- package/src/components/__tests__/render-field-enum-option.test.tsx +0 -99
- package/src/components/__tests__/render-field-icon-derivation.test.tsx +0 -119
- package/src/components/__tests__/render-field-json-format.test.tsx +0 -164
- package/src/components/__tests__/render-field-money-embedded-list-consistency.test.tsx +0 -212
- package/src/components/__tests__/render-field-money-roundtrip.test.tsx +0 -217
- package/src/components/__tests__/render-field-number-grouping.test.tsx +0 -76
- package/src/components/__tests__/render-field-number-unit.test.tsx +0 -227
- package/src/components/__tests__/render-field-password-format.test.tsx +0 -101
- package/src/components/__tests__/render-field-select-display.test.tsx +0 -134
- package/src/components/__tests__/render-field-unit-format.test.tsx +0 -107
- package/src/components/__tests__/render-field-unsupported-types.test.tsx +0 -195
- package/src/components/__tests__/render-list-toolbar-icon-only.test.tsx +0 -146
- package/src/components/__tests__/write-form-section.test.tsx +0 -375
- package/src/components/embedded-list-field.tsx +0 -354
- package/src/components/grid-cell-for-field.tsx +0 -60
- package/src/components/multi-select-checkboxes.tsx +0 -93
- package/src/components/reference-create-dialog.tsx +0 -91
- package/src/components/related-list-section.tsx +0 -467
- package/src/components/render-edit-action-button.tsx +0 -106
- package/src/components/render-edit-logic.ts +0 -85
- package/src/components/render-edit-types.ts +0 -216
- package/src/components/render-edit.tsx +0 -1700
- package/src/components/render-field.tsx +0 -942
- package/src/components/render-list.tsx +0 -561
- package/src/components/write-form-section.tsx +0 -199
- package/src/context/draft-storage-context.tsx +0 -39
- package/src/hooks/__tests__/use-ai-text.test.tsx +0 -177
- package/src/hooks/__tests__/use-disclosure.test.tsx +0 -25
- package/src/hooks/__tests__/use-form.test.tsx +0 -65
- package/src/hooks/__tests__/use-mutation.test.tsx +0 -77
- package/src/hooks/__tests__/use-stream-handler.test.tsx +0 -107
- package/src/hooks/use-ai-text.ts +0 -172
- package/src/hooks/use-disclosure.ts +0 -20
- package/src/hooks/use-form.ts +0 -78
- package/src/hooks/use-list-url-state.ts +0 -171
- package/src/hooks/use-mutation.ts +0 -61
- package/src/hooks/use-query.ts +0 -132
- package/src/hooks/use-reference-lookup.ts +0 -76
- package/src/hooks/use-store.ts +0 -48
- package/src/hooks/use-stream-handler.ts +0 -134
- package/src/i18n-defaults.ts +0 -220
- package/src/i18n.tsx +0 -286
- package/src/index.ts +0 -272
- package/src/primitives.tsx +0 -1446
- package/src/share-links.ts +0 -12
- package/src/sort-by-accessor.ts +0 -20
- package/src/sse/live-events.tsx +0 -56
- package/src/tokens.tsx +0 -170
|
@@ -16,21 +16,19 @@
|
|
|
16
16
|
// nimmt den letzten ":"-getrennten Teil. Robust gegen Strings ohne
|
|
17
17
|
// ":" (returnt sie unverändert, damit App-Author-Code mit Short-Form-
|
|
18
18
|
// ids in eigenen Stellen weiter passt).
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return idx < 0 ? qn : qn.slice(idx + 1);
|
|
19
|
+
export function lastSegment(qn) {
|
|
20
|
+
const idx = qn.lastIndexOf(":");
|
|
21
|
+
return idx < 0 ? qn : qn.slice(idx + 1);
|
|
23
22
|
}
|
|
24
|
-
|
|
25
23
|
/**
|
|
26
24
|
* Client-safe copy of framework `toKebab` (engine/qualified-name.ts).
|
|
27
25
|
* Must stay in sync — do NOT import from `@cosmicdrift/kumiko-framework/engine`
|
|
28
26
|
* here: that barrel pulls server deps into the browser bundle.
|
|
29
27
|
*/
|
|
30
|
-
export function toKebab(input
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
export function toKebab(input) {
|
|
29
|
+
return input
|
|
30
|
+
.replace(/\./g, "-")
|
|
31
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
|
|
32
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
33
|
+
.toLowerCase();
|
|
36
34
|
}
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
// Dependency-free: kumiko-screen.tsx already imports useNav from nav.tsx, so
|
|
2
2
|
// this can't live in either without a cycle. Form must match the registry's
|
|
3
3
|
// qualification rule (packages/framework/src/engine/qualified-name.ts).
|
|
4
|
-
export function qualifyScreenId(featureName
|
|
5
|
-
|
|
4
|
+
export function qualifyScreenId(featureName, screenId) {
|
|
5
|
+
return `${featureName}:screen:${screenId}`;
|
|
6
6
|
}
|
|
7
|
-
|
|
8
7
|
// Inverse of qualifyScreenId. A short id ("task-list") has no feature
|
|
9
8
|
// segment and returns undefined — callers need that to tell "this redirect
|
|
10
9
|
// names a feature" apart from "this redirect is feature-less, resolve by
|
|
11
10
|
// short id alone" (fw#2485).
|
|
12
|
-
export function featureNameFromQualifiedScreenId(id
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
export function featureNameFromQualifiedScreenId(id) {
|
|
12
|
+
const parts = id.split(":");
|
|
13
|
+
return parts.length === 3 && parts[1] === "screen" ? parts[0] : undefined;
|
|
15
14
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { ResolvedFacetSpec } from "./list-facets.js";
|
|
3
|
+
export type ReferenceFacetOption = {
|
|
4
|
+
readonly value: string;
|
|
5
|
+
readonly label: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function ReferenceFacetBridges({ specs, onOptions, }: {
|
|
8
|
+
readonly specs: readonly ResolvedFacetSpec[];
|
|
9
|
+
readonly onOptions: (field: string, options: readonly ReferenceFacetOption[]) => void;
|
|
10
|
+
}): ReactNode;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// One bridge component per reference facet, mirroring ReferenceLookupBridge —
|
|
3
|
+
// facet count varies per screen, so the hook can't be called in a loop.
|
|
4
|
+
import { useEffect } from "react";
|
|
5
|
+
import { useReferenceLookup } from "../hooks/use-reference-lookup.js";
|
|
6
|
+
function ReferenceFacetOptionsBridge({ field, refEntity, refFeature, labelField, onOptions, }) {
|
|
7
|
+
const lookup = useReferenceLookup(refFeature, refEntity, labelField);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
if (lookup.loading)
|
|
10
|
+
return;
|
|
11
|
+
onOptions(field, [...lookup.map.entries()].map(([value, label]) => ({ value, label })));
|
|
12
|
+
}, [lookup.loading, lookup.map, field, onOptions]);
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
// One invisible bridge per "reference" facet spec, each publishing its
|
|
16
|
+
// resolved options back via `onOptions` once loaded.
|
|
17
|
+
export function ReferenceFacetBridges({ specs, onOptions, }) {
|
|
18
|
+
const referenceSpecs = specs.filter((spec) => spec.reference !== undefined);
|
|
19
|
+
if (referenceSpecs.length === 0)
|
|
20
|
+
return null;
|
|
21
|
+
return (_jsx(_Fragment, { children: referenceSpecs.map((spec) => (_jsx(ReferenceFacetOptionsBridge, { field: spec.field, refEntity: spec.reference?.refEntity ?? "", refFeature: spec.reference?.refFeature ?? "", labelField: spec.reference?.labelField ?? "id", onOptions: onOptions }, spec.field))) }));
|
|
22
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { FeatureSchema } from "./feature-schema.js";
|
|
3
|
+
import { type NavApi, type NavTarget, type ScreenTarget } from "./nav.js";
|
|
4
|
+
export declare const RETURN_TO_PARAM = "returnTo";
|
|
5
|
+
export type ReturnHost = {
|
|
6
|
+
readonly screenId: string;
|
|
7
|
+
readonly entityId?: string;
|
|
8
|
+
};
|
|
9
|
+
export type ReturnTo = {
|
|
10
|
+
readonly path: string;
|
|
11
|
+
readonly state: Readonly<Record<string, string>>;
|
|
12
|
+
};
|
|
13
|
+
/** Splits `<screenId>[/<entityId>][?<host-params>]`. One `URLSearchParams`
|
|
14
|
+
* pass per nesting level — a deeper level stays encoded inside a value and
|
|
15
|
+
* is only decoded when that level is reached. */
|
|
16
|
+
export declare function splitReturnTo(raw: string): ReturnTo;
|
|
17
|
+
export declare function formatReturnTo(host: ReturnHost, hostParams?: Readonly<Record<string, string>>): string;
|
|
18
|
+
export declare function returnToParams(host: ReturnHost | undefined, target: ScreenTarget, hostParams?: Readonly<Record<string, string>>): Readonly<Record<string, string>>;
|
|
19
|
+
export declare function navigateWithReturnTo(nav: NavApi, target: NavTarget, host: ReturnHost | undefined, params?: Readonly<Record<string, string | null>>): void;
|
|
20
|
+
/** Jumps back to an already-validated returnTo target and restores the host
|
|
21
|
+
* search params carried in the same value. The host's own returnTo travels
|
|
22
|
+
* with them, so a chain unwinds one level per jump. */
|
|
23
|
+
export declare function navigateToReturn(nav: NavApi, target: ScreenTarget): void;
|
|
24
|
+
export declare function navigateToReturnOr(nav: NavApi, target: ScreenTarget | undefined, fallback: () => void): void;
|
|
25
|
+
export declare function resolveReturnTarget(raw: string | undefined, ownScreenId: string, features: readonly FeatureSchema[], userRoles: readonly string[] | undefined): ScreenTarget | undefined;
|
|
26
|
+
export declare function ReturnHostProvider({ value, children, }: {
|
|
27
|
+
readonly value: ReturnHost;
|
|
28
|
+
readonly children: ReactNode;
|
|
29
|
+
}): ReactNode;
|
|
30
|
+
export declare function useReturnHost(): ReturnHost | undefined;
|
|
31
|
+
export declare function useReturnTarget(ownScreenId: string): ScreenTarget | undefined;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useMemo } from "react";
|
|
3
|
+
import { useUserRoles } from "../context/user-roles-context.js";
|
|
4
|
+
import { useAppFeatures } from "./app-features-context.js";
|
|
5
|
+
import { useNav, } from "./nav.js";
|
|
6
|
+
import { lastSegment } from "./qn.js";
|
|
7
|
+
import { screenAccessAllows } from "./screen-access.js";
|
|
8
|
+
// A search param, not nav/route state, so it survives a full page reload
|
|
9
|
+
// and a copied/shared URL.
|
|
10
|
+
export const RETURN_TO_PARAM = "returnTo";
|
|
11
|
+
// Screen levels one returnTo value may carry: the host itself plus its own
|
|
12
|
+
// nested chain. Beyond that the host's returnTo is left out of the snapshot.
|
|
13
|
+
const MAX_RETURN_DEPTH = 3;
|
|
14
|
+
// Serialized host snapshot longer than this is dropped whole — the bare
|
|
15
|
+
// target still resolves, only the restored state is lost.
|
|
16
|
+
const MAX_RETURN_STATE_LENGTH = 512;
|
|
17
|
+
const NO_RETURN_STATE = Object.freeze({});
|
|
18
|
+
/** Splits `<screenId>[/<entityId>][?<host-params>]`. One `URLSearchParams`
|
|
19
|
+
* pass per nesting level — a deeper level stays encoded inside a value and
|
|
20
|
+
* is only decoded when that level is reached. */
|
|
21
|
+
export function splitReturnTo(raw) {
|
|
22
|
+
const queryStart = raw.indexOf("?");
|
|
23
|
+
if (queryStart === -1)
|
|
24
|
+
return { path: raw, state: NO_RETURN_STATE };
|
|
25
|
+
return {
|
|
26
|
+
path: raw.slice(0, queryStart),
|
|
27
|
+
state: Object.fromEntries(new URLSearchParams(raw.slice(queryStart + 1))),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// Saturates at MAX_RETURN_DEPTH — callers only ask whether the cap is
|
|
31
|
+
// reached, and an unbounded walk would follow attacker-sized nesting.
|
|
32
|
+
function returnToDepth(raw) {
|
|
33
|
+
let depth = 1;
|
|
34
|
+
let current = raw;
|
|
35
|
+
while (depth < MAX_RETURN_DEPTH) {
|
|
36
|
+
const nested = splitReturnTo(current).state[RETURN_TO_PARAM];
|
|
37
|
+
if (nested === undefined)
|
|
38
|
+
return depth;
|
|
39
|
+
depth += 1;
|
|
40
|
+
current = nested;
|
|
41
|
+
}
|
|
42
|
+
return depth;
|
|
43
|
+
}
|
|
44
|
+
// Both caps degrade to the pre-snapshot value instead of failing, so a value
|
|
45
|
+
// that hits one still navigates.
|
|
46
|
+
function returnStateSnapshot(hostParams) {
|
|
47
|
+
if (hostParams === undefined)
|
|
48
|
+
return "";
|
|
49
|
+
const snapshot = new URLSearchParams();
|
|
50
|
+
for (const [key, value] of Object.entries(hostParams)) {
|
|
51
|
+
if (value === "")
|
|
52
|
+
continue;
|
|
53
|
+
if (key === RETURN_TO_PARAM && returnToDepth(value) >= MAX_RETURN_DEPTH)
|
|
54
|
+
continue;
|
|
55
|
+
snapshot.set(key, value);
|
|
56
|
+
}
|
|
57
|
+
const serialized = snapshot.toString();
|
|
58
|
+
return serialized.length > MAX_RETURN_STATE_LENGTH ? "" : serialized;
|
|
59
|
+
}
|
|
60
|
+
export function formatReturnTo(host, hostParams) {
|
|
61
|
+
const path = host.entityId !== undefined ? `${host.screenId}/${host.entityId}` : host.screenId;
|
|
62
|
+
const state = returnStateSnapshot(hostParams);
|
|
63
|
+
return state === "" ? path : `${path}?${state}`;
|
|
64
|
+
}
|
|
65
|
+
// {} when target IS host — pushPath no-ops on the same path, so the param
|
|
66
|
+
// would land on the current page instead of the one being left.
|
|
67
|
+
export function returnToParams(host, target, hostParams) {
|
|
68
|
+
if (host === undefined)
|
|
69
|
+
return {};
|
|
70
|
+
if (lastSegment(target.screenId) === host.screenId && target.entityId === host.entityId) {
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
return { [RETURN_TO_PARAM]: formatReturnTo(host, hostParams) };
|
|
74
|
+
}
|
|
75
|
+
// ObjectTarget only resolves to a path inside the NavApi impl (detailFor
|
|
76
|
+
// lookup across all features), so compare hrefs instead of the raw target.
|
|
77
|
+
function objectTargetIsHost(nav, target, host) {
|
|
78
|
+
const hostTarget = {
|
|
79
|
+
screenId: host.screenId,
|
|
80
|
+
...(host.entityId !== undefined && { entityId: host.entityId }),
|
|
81
|
+
};
|
|
82
|
+
return nav.hrefFor(target) === nav.hrefFor(hostTarget);
|
|
83
|
+
}
|
|
84
|
+
function returnParamsFor(nav, target, host) {
|
|
85
|
+
if ("screenId" in target)
|
|
86
|
+
return returnToParams(host, target, nav.searchParams);
|
|
87
|
+
if (host === undefined || objectTargetIsHost(nav, target, host))
|
|
88
|
+
return {};
|
|
89
|
+
return { [RETURN_TO_PARAM]: formatReturnTo(host, nav.searchParams) };
|
|
90
|
+
}
|
|
91
|
+
export function navigateWithReturnTo(nav, target, host, params) {
|
|
92
|
+
// Snapshot before navigating — nav.searchParams still describes the host.
|
|
93
|
+
const returnParams = returnParamsFor(nav, target, host);
|
|
94
|
+
nav.navigate(target);
|
|
95
|
+
const merged = {
|
|
96
|
+
...(params ?? {}),
|
|
97
|
+
...returnParams,
|
|
98
|
+
};
|
|
99
|
+
if (Object.keys(merged).length > 0) {
|
|
100
|
+
nav.setSearchParams(merged);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** Jumps back to an already-validated returnTo target and restores the host
|
|
104
|
+
* search params carried in the same value. The host's own returnTo travels
|
|
105
|
+
* with them, so a chain unwinds one level per jump. */
|
|
106
|
+
export function navigateToReturn(nav, target) {
|
|
107
|
+
const raw = nav.searchParams[RETURN_TO_PARAM];
|
|
108
|
+
const state = raw === undefined ? NO_RETURN_STATE : splitReturnTo(raw).state;
|
|
109
|
+
nav.navigate(target);
|
|
110
|
+
// The explicit null drops this screen's own returnTo on nav impls whose
|
|
111
|
+
// navigate keeps the query.
|
|
112
|
+
nav.setSearchParams({ ...state, [RETURN_TO_PARAM]: state[RETURN_TO_PARAM] ?? null });
|
|
113
|
+
}
|
|
114
|
+
export function navigateToReturnOr(nav, target, fallback) {
|
|
115
|
+
if (target === undefined) {
|
|
116
|
+
fallback();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
navigateToReturn(nav, target);
|
|
120
|
+
}
|
|
121
|
+
// "%" is left alone — route entityIds come raw from the path and can carry it.
|
|
122
|
+
const UNSAFE_ENTITY_ID_PATTERN = /[\\?#\s]/;
|
|
123
|
+
// Every failure branch returns undefined so a malformed or stale value falls
|
|
124
|
+
// back to the caller's own default instead of throwing.
|
|
125
|
+
export function resolveReturnTarget(raw, ownScreenId, features, userRoles) {
|
|
126
|
+
if (raw === undefined || raw === "")
|
|
127
|
+
return undefined;
|
|
128
|
+
const segments = splitReturnTo(raw).path.split("/");
|
|
129
|
+
if (segments.length !== 1 && segments.length !== 2)
|
|
130
|
+
return undefined;
|
|
131
|
+
if (segments.some((segment) => segment === ""))
|
|
132
|
+
return undefined;
|
|
133
|
+
const [screenId, entityId] = segments;
|
|
134
|
+
if (screenId === undefined || screenId.includes(":"))
|
|
135
|
+
return undefined;
|
|
136
|
+
if (screenId === lastSegment(ownScreenId))
|
|
137
|
+
return undefined;
|
|
138
|
+
let matched;
|
|
139
|
+
for (const feature of features) {
|
|
140
|
+
matched = feature.screens.find((s) => lastSegment(s.id) === screenId);
|
|
141
|
+
if (matched !== undefined)
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
if (matched === undefined)
|
|
145
|
+
return undefined;
|
|
146
|
+
if (!screenAccessAllows(matched.access, userRoles))
|
|
147
|
+
return undefined;
|
|
148
|
+
if (entityId !== undefined) {
|
|
149
|
+
if (entityId === "." || entityId === "..")
|
|
150
|
+
return undefined;
|
|
151
|
+
if (UNSAFE_ENTITY_ID_PATTERN.test(entityId))
|
|
152
|
+
return undefined;
|
|
153
|
+
let decodedEntityId;
|
|
154
|
+
try {
|
|
155
|
+
decodedEntityId = decodeURIComponent(entityId);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
if (decodedEntityId === "." || decodedEntityId === "..")
|
|
161
|
+
return undefined;
|
|
162
|
+
if (UNSAFE_ENTITY_ID_PATTERN.test(decodedEntityId) || decodedEntityId.includes("/")) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
if (matched.type !== "entityEdit" && matched.type !== "projectionDetail")
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
if (matched.type === "projectionDetail" && matched.singleton !== true && entityId === undefined) {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
return { screenId, ...(entityId !== undefined && { entityId }) };
|
|
172
|
+
}
|
|
173
|
+
const ReturnHostContext = createContext(undefined);
|
|
174
|
+
export function ReturnHostProvider({ value, children, }) {
|
|
175
|
+
return _jsx(ReturnHostContext.Provider, { value: value, children: children });
|
|
176
|
+
}
|
|
177
|
+
export function useReturnHost() {
|
|
178
|
+
return useContext(ReturnHostContext);
|
|
179
|
+
}
|
|
180
|
+
// Reads through useNav().searchParams (reactive), not a useState snapshot —
|
|
181
|
+
// a later setSearchParams call must not leave this stale.
|
|
182
|
+
export function useReturnTarget(ownScreenId) {
|
|
183
|
+
const nav = useNav();
|
|
184
|
+
const features = useAppFeatures();
|
|
185
|
+
const userRoles = useUserRoles();
|
|
186
|
+
const raw = nav.searchParams[RETURN_TO_PARAM];
|
|
187
|
+
return useMemo(() => resolveReturnTarget(raw, ownScreenId, features, userRoles), [raw, ownScreenId, features, userRoles]);
|
|
188
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { EntityEditScreenDefinition, RelatedListToolbarAction, RowAction, RowActionDrawer, RowActionNavigate, RowActionWriteHandler, RowFieldExtractor, ToolbarAction } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import type { Dispatcher, ListRowViewModel, Translate } from "@cosmicdrift/kumiko-headless";
|
|
3
|
+
import type { RenderEditAction } from "../components/render-edit-types.js";
|
|
4
|
+
import type { ToolbarActionButton } from "../components/render-list.js";
|
|
5
|
+
import type { DataTableRowAction } from "../primitives.js";
|
|
6
|
+
import type { NavApi } from "./nav.js";
|
|
7
|
+
import { type ReturnHost } from "./return-to.js";
|
|
8
|
+
export declare function buildDefaultEditRowAction(editScreen: EntityEditScreenDefinition | undefined, idColumn?: string): RowActionNavigate | undefined;
|
|
9
|
+
export declare function evalRowExtractor(extractor: RowFieldExtractor, row: Record<string, unknown>): Record<string, unknown>;
|
|
10
|
+
export declare function isWriteHandlerRowAction(action: RowAction): action is RowActionWriteHandler;
|
|
11
|
+
export declare function stringifyNavParams(params: Record<string, unknown>): Record<string, string | null>;
|
|
12
|
+
export declare function navigateActionSearchParams(action: RowActionNavigate, values: Readonly<Record<string, unknown>>): Record<string, string | null> | undefined;
|
|
13
|
+
export declare function refetchAfterWrite(refetch: () => Promise<unknown>): Promise<void>;
|
|
14
|
+
export declare function runProjectionRowNavigate(nav: NavApi, action: RowActionNavigate, row: ListRowViewModel, host: ReturnHost | undefined): void;
|
|
15
|
+
type OpenDrawer = (action: RowActionDrawer, initialValues: Readonly<Record<string, unknown>> | undefined) => void;
|
|
16
|
+
/** @internal test-only */
|
|
17
|
+
export declare function resetDrawerActionWarningsForTests(): void;
|
|
18
|
+
export declare function buildProjectionRowActions(options: {
|
|
19
|
+
readonly rowActions: readonly RowAction[] | undefined;
|
|
20
|
+
readonly translate: Translate;
|
|
21
|
+
readonly dispatcher: Dispatcher | undefined;
|
|
22
|
+
readonly nav: NavApi;
|
|
23
|
+
readonly refetch: () => Promise<unknown>;
|
|
24
|
+
/** Opens the drawer-kind action's target actionForm, prefilled from the
|
|
25
|
+
* clicked row. Omitted callers (e.g. RelatedListSection embedded directly
|
|
26
|
+
* without onOpenDrawer) drop drawer actions and get a dev warning —
|
|
27
|
+
* mirrors the `dispatcher === undefined` skip below for writeHandler. */
|
|
28
|
+
readonly openDrawer?: OpenDrawer;
|
|
29
|
+
/** Prepended unless a declared action already has id "edit" — declared wins. */
|
|
30
|
+
readonly defaultEditRowAction?: RowActionNavigate;
|
|
31
|
+
readonly host: ReturnHost | undefined;
|
|
32
|
+
}): readonly DataTableRowAction[] | undefined;
|
|
33
|
+
type OpenToolbarDrawer = (action: ToolbarAction & {
|
|
34
|
+
readonly kind: "drawer";
|
|
35
|
+
}) => void;
|
|
36
|
+
export declare function buildProjectionToolbarActions(options: {
|
|
37
|
+
readonly toolbarActions: readonly RelatedListToolbarAction[] | undefined;
|
|
38
|
+
readonly translate: Translate;
|
|
39
|
+
readonly dispatcher: Dispatcher | undefined;
|
|
40
|
+
readonly nav: NavApi;
|
|
41
|
+
readonly refetch: () => Promise<unknown>;
|
|
42
|
+
/** Omitted callers drop drawer-kind toolbar actions and get a dev warning,
|
|
43
|
+
* mirroring the rowActions behavior above. */
|
|
44
|
+
readonly openDrawer?: OpenToolbarDrawer;
|
|
45
|
+
/** Search params set on the target after a navigate-kind action, e.g. a
|
|
46
|
+
* relatedList's parent id for create-form prefill. */
|
|
47
|
+
readonly navigatePrefill?: Readonly<Record<string, unknown>>;
|
|
48
|
+
/** The relatedList's parent record — only a relatedList caller has one.
|
|
49
|
+
* Enables `visible`/`params` (RelatedListToolbarAction), evaluated
|
|
50
|
+
* against it exactly like header actions/RowAction.visible. Plain
|
|
51
|
+
* entityList/projectionList toolbars have no record and omit this. */
|
|
52
|
+
readonly record?: Readonly<Record<string, unknown>>;
|
|
53
|
+
readonly host: ReturnHost | undefined;
|
|
54
|
+
}): readonly ToolbarActionButton[] | undefined;
|
|
55
|
+
export declare function buildRecordActions(options: {
|
|
56
|
+
readonly actions: readonly RowAction[];
|
|
57
|
+
readonly record: Readonly<Record<string, unknown>>;
|
|
58
|
+
readonly translate: Translate;
|
|
59
|
+
readonly nav: NavApi;
|
|
60
|
+
readonly host: ReturnHost | undefined;
|
|
61
|
+
readonly dispatcher: Dispatcher | undefined;
|
|
62
|
+
readonly openDrawer: (action: RowActionDrawer, initialValues: Readonly<Record<string, unknown>> | undefined) => void;
|
|
63
|
+
/** Called after a writeHandler action succeeds (refetch/reload). */
|
|
64
|
+
readonly onWriteSuccess: () => void | Promise<void>;
|
|
65
|
+
/** Screen-target navigate default entityId when the action declares no
|
|
66
|
+
* explicit `entityId` field-source. A caller editing a fixed entity can
|
|
67
|
+
* pass its own `entityId` here directly (the record it already is). A
|
|
68
|
+
* caller whose shown record may differ from the navigate target's entity
|
|
69
|
+
* resolves this per-target via `sameEntityScreenId` instead. */
|
|
70
|
+
readonly defaultScreenTargetEntityId?: string;
|
|
71
|
+
/** Given a navigate action's target screen id, returns the record id to
|
|
72
|
+
* default to IF that screen edits the same entity the current record
|
|
73
|
+
* belongs to (cross-feature lookup) — undefined otherwise. Omitted by
|
|
74
|
+
* callers with no such cross-screen lookup. */
|
|
75
|
+
readonly sameEntityScreenId?: (targetScreen: string) => string | undefined;
|
|
76
|
+
}): readonly RenderEditAction[] | undefined;
|
|
77
|
+
export {};
|