@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
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
// One bridge component per reference facet, mirroring ReferenceLookupBridge —
|
|
2
|
-
// facet count varies per screen, so the hook can't be called in a loop.
|
|
3
|
-
import { type ReactNode, useEffect } from "react";
|
|
4
|
-
import { useReferenceLookup } from "../hooks/use-reference-lookup";
|
|
5
|
-
import type { ResolvedFacetSpec } from "./list-facets";
|
|
6
|
-
|
|
7
|
-
export type ReferenceFacetOption = { readonly value: string; readonly label: string };
|
|
8
|
-
|
|
9
|
-
function ReferenceFacetOptionsBridge({
|
|
10
|
-
field,
|
|
11
|
-
refEntity,
|
|
12
|
-
refFeature,
|
|
13
|
-
labelField,
|
|
14
|
-
onOptions,
|
|
15
|
-
}: {
|
|
16
|
-
readonly field: string;
|
|
17
|
-
readonly refEntity: string;
|
|
18
|
-
readonly refFeature: string;
|
|
19
|
-
readonly labelField: string;
|
|
20
|
-
readonly onOptions: (field: string, options: readonly ReferenceFacetOption[]) => void;
|
|
21
|
-
}): ReactNode {
|
|
22
|
-
const lookup = useReferenceLookup(refFeature, refEntity, labelField);
|
|
23
|
-
useEffect(() => {
|
|
24
|
-
if (lookup.loading) return;
|
|
25
|
-
onOptions(
|
|
26
|
-
field,
|
|
27
|
-
[...lookup.map.entries()].map(([value, label]) => ({ value, label })),
|
|
28
|
-
);
|
|
29
|
-
}, [lookup.loading, lookup.map, field, onOptions]);
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// One invisible bridge per "reference" facet spec, each publishing its
|
|
34
|
-
// resolved options back via `onOptions` once loaded.
|
|
35
|
-
export function ReferenceFacetBridges({
|
|
36
|
-
specs,
|
|
37
|
-
onOptions,
|
|
38
|
-
}: {
|
|
39
|
-
readonly specs: readonly ResolvedFacetSpec[];
|
|
40
|
-
readonly onOptions: (field: string, options: readonly ReferenceFacetOption[]) => void;
|
|
41
|
-
}): ReactNode {
|
|
42
|
-
const referenceSpecs = specs.filter((spec) => spec.reference !== undefined);
|
|
43
|
-
if (referenceSpecs.length === 0) return null;
|
|
44
|
-
return (
|
|
45
|
-
<>
|
|
46
|
-
{referenceSpecs.map((spec) => (
|
|
47
|
-
<ReferenceFacetOptionsBridge
|
|
48
|
-
key={spec.field}
|
|
49
|
-
field={spec.field}
|
|
50
|
-
refEntity={spec.reference?.refEntity ?? ""}
|
|
51
|
-
refFeature={spec.reference?.refFeature ?? ""}
|
|
52
|
-
labelField={spec.reference?.labelField ?? "id"}
|
|
53
|
-
onOptions={onOptions}
|
|
54
|
-
/>
|
|
55
|
-
))}
|
|
56
|
-
</>
|
|
57
|
-
);
|
|
58
|
-
}
|
package/src/app/return-to.tsx
DELETED
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
import type { ScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
-
import { createContext, type ReactNode, useContext, useMemo } from "react";
|
|
3
|
-
import { useUserRoles } from "../context/user-roles-context";
|
|
4
|
-
import { useAppFeatures } from "./app-features-context";
|
|
5
|
-
import type { FeatureSchema } from "./feature-schema";
|
|
6
|
-
import { type NavApi, type NavTarget, type ObjectTarget, type ScreenTarget, useNav } from "./nav";
|
|
7
|
-
import { lastSegment } from "./qn";
|
|
8
|
-
import { screenAccessAllows } from "./screen-access";
|
|
9
|
-
|
|
10
|
-
// A search param, not nav/route state, so it survives a full page reload
|
|
11
|
-
// and a copied/shared URL.
|
|
12
|
-
export const RETURN_TO_PARAM = "returnTo";
|
|
13
|
-
|
|
14
|
-
// Screen levels one returnTo value may carry: the host itself plus its own
|
|
15
|
-
// nested chain. Beyond that the host's returnTo is left out of the snapshot.
|
|
16
|
-
const MAX_RETURN_DEPTH = 3;
|
|
17
|
-
// Serialized host snapshot longer than this is dropped whole — the bare
|
|
18
|
-
// target still resolves, only the restored state is lost.
|
|
19
|
-
const MAX_RETURN_STATE_LENGTH = 512;
|
|
20
|
-
|
|
21
|
-
const NO_RETURN_STATE: Readonly<Record<string, string>> = Object.freeze({});
|
|
22
|
-
|
|
23
|
-
export type ReturnHost = {
|
|
24
|
-
readonly screenId: string;
|
|
25
|
-
readonly entityId?: string;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export type ReturnTo = {
|
|
29
|
-
readonly path: string;
|
|
30
|
-
readonly state: Readonly<Record<string, string>>;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
/** Splits `<screenId>[/<entityId>][?<host-params>]`. One `URLSearchParams`
|
|
34
|
-
* pass per nesting level — a deeper level stays encoded inside a value and
|
|
35
|
-
* is only decoded when that level is reached. */
|
|
36
|
-
export function splitReturnTo(raw: string): ReturnTo {
|
|
37
|
-
const queryStart = raw.indexOf("?");
|
|
38
|
-
if (queryStart === -1) return { path: raw, state: NO_RETURN_STATE };
|
|
39
|
-
return {
|
|
40
|
-
path: raw.slice(0, queryStart),
|
|
41
|
-
state: Object.fromEntries(new URLSearchParams(raw.slice(queryStart + 1))),
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Saturates at MAX_RETURN_DEPTH — callers only ask whether the cap is
|
|
46
|
-
// reached, and an unbounded walk would follow attacker-sized nesting.
|
|
47
|
-
function returnToDepth(raw: string): number {
|
|
48
|
-
let depth = 1;
|
|
49
|
-
let current = raw;
|
|
50
|
-
while (depth < MAX_RETURN_DEPTH) {
|
|
51
|
-
const nested = splitReturnTo(current).state[RETURN_TO_PARAM];
|
|
52
|
-
if (nested === undefined) return depth;
|
|
53
|
-
depth += 1;
|
|
54
|
-
current = nested;
|
|
55
|
-
}
|
|
56
|
-
return depth;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Both caps degrade to the pre-snapshot value instead of failing, so a value
|
|
60
|
-
// that hits one still navigates.
|
|
61
|
-
function returnStateSnapshot(hostParams: Readonly<Record<string, string>> | undefined): string {
|
|
62
|
-
if (hostParams === undefined) return "";
|
|
63
|
-
const snapshot = new URLSearchParams();
|
|
64
|
-
for (const [key, value] of Object.entries(hostParams)) {
|
|
65
|
-
if (value === "") continue;
|
|
66
|
-
if (key === RETURN_TO_PARAM && returnToDepth(value) >= MAX_RETURN_DEPTH) continue;
|
|
67
|
-
snapshot.set(key, value);
|
|
68
|
-
}
|
|
69
|
-
const serialized = snapshot.toString();
|
|
70
|
-
return serialized.length > MAX_RETURN_STATE_LENGTH ? "" : serialized;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function formatReturnTo(
|
|
74
|
-
host: ReturnHost,
|
|
75
|
-
hostParams?: Readonly<Record<string, string>>,
|
|
76
|
-
): string {
|
|
77
|
-
const path = host.entityId !== undefined ? `${host.screenId}/${host.entityId}` : host.screenId;
|
|
78
|
-
const state = returnStateSnapshot(hostParams);
|
|
79
|
-
return state === "" ? path : `${path}?${state}`;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// {} when target IS host — pushPath no-ops on the same path, so the param
|
|
83
|
-
// would land on the current page instead of the one being left.
|
|
84
|
-
export function returnToParams(
|
|
85
|
-
host: ReturnHost | undefined,
|
|
86
|
-
target: ScreenTarget,
|
|
87
|
-
hostParams?: Readonly<Record<string, string>>,
|
|
88
|
-
): Readonly<Record<string, string>> {
|
|
89
|
-
if (host === undefined) return {};
|
|
90
|
-
if (lastSegment(target.screenId) === host.screenId && target.entityId === host.entityId) {
|
|
91
|
-
return {};
|
|
92
|
-
}
|
|
93
|
-
return { [RETURN_TO_PARAM]: formatReturnTo(host, hostParams) };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ObjectTarget only resolves to a path inside the NavApi impl (detailFor
|
|
97
|
-
// lookup across all features), so compare hrefs instead of the raw target.
|
|
98
|
-
function objectTargetIsHost(nav: NavApi, target: ObjectTarget, host: ReturnHost): boolean {
|
|
99
|
-
const hostTarget: ScreenTarget = {
|
|
100
|
-
screenId: host.screenId,
|
|
101
|
-
...(host.entityId !== undefined && { entityId: host.entityId }),
|
|
102
|
-
};
|
|
103
|
-
return nav.hrefFor(target) === nav.hrefFor(hostTarget);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function returnParamsFor(
|
|
107
|
-
nav: NavApi,
|
|
108
|
-
target: NavTarget,
|
|
109
|
-
host: ReturnHost | undefined,
|
|
110
|
-
): Readonly<Record<string, string>> {
|
|
111
|
-
if ("screenId" in target) return returnToParams(host, target, nav.searchParams);
|
|
112
|
-
if (host === undefined || objectTargetIsHost(nav, target, host)) return {};
|
|
113
|
-
return { [RETURN_TO_PARAM]: formatReturnTo(host, nav.searchParams) };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function navigateWithReturnTo(
|
|
117
|
-
nav: NavApi,
|
|
118
|
-
target: NavTarget,
|
|
119
|
-
host: ReturnHost | undefined,
|
|
120
|
-
params?: Readonly<Record<string, string | null>>,
|
|
121
|
-
): void {
|
|
122
|
-
// Snapshot before navigating — nav.searchParams still describes the host.
|
|
123
|
-
const returnParams = returnParamsFor(nav, target, host);
|
|
124
|
-
nav.navigate(target);
|
|
125
|
-
const merged: Record<string, string | null> = {
|
|
126
|
-
...(params ?? {}),
|
|
127
|
-
...returnParams,
|
|
128
|
-
};
|
|
129
|
-
if (Object.keys(merged).length > 0) {
|
|
130
|
-
nav.setSearchParams(merged);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Jumps back to an already-validated returnTo target and restores the host
|
|
135
|
-
* search params carried in the same value. The host's own returnTo travels
|
|
136
|
-
* with them, so a chain unwinds one level per jump. */
|
|
137
|
-
export function navigateToReturn(nav: NavApi, target: ScreenTarget): void {
|
|
138
|
-
const raw = nav.searchParams[RETURN_TO_PARAM];
|
|
139
|
-
const state = raw === undefined ? NO_RETURN_STATE : splitReturnTo(raw).state;
|
|
140
|
-
nav.navigate(target);
|
|
141
|
-
// The explicit null drops this screen's own returnTo on nav impls whose
|
|
142
|
-
// navigate keeps the query.
|
|
143
|
-
nav.setSearchParams({ ...state, [RETURN_TO_PARAM]: state[RETURN_TO_PARAM] ?? null });
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export function navigateToReturnOr(
|
|
147
|
-
nav: NavApi,
|
|
148
|
-
target: ScreenTarget | undefined,
|
|
149
|
-
fallback: () => void,
|
|
150
|
-
): void {
|
|
151
|
-
if (target === undefined) {
|
|
152
|
-
fallback();
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
navigateToReturn(nav, target);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// "%" is left alone — route entityIds come raw from the path and can carry it.
|
|
159
|
-
const UNSAFE_ENTITY_ID_PATTERN = /[\\?#\s]/;
|
|
160
|
-
|
|
161
|
-
// Every failure branch returns undefined so a malformed or stale value falls
|
|
162
|
-
// back to the caller's own default instead of throwing.
|
|
163
|
-
export function resolveReturnTarget(
|
|
164
|
-
raw: string | undefined,
|
|
165
|
-
ownScreenId: string,
|
|
166
|
-
features: readonly FeatureSchema[],
|
|
167
|
-
userRoles: readonly string[] | undefined,
|
|
168
|
-
): ScreenTarget | undefined {
|
|
169
|
-
if (raw === undefined || raw === "") return undefined;
|
|
170
|
-
const segments = splitReturnTo(raw).path.split("/");
|
|
171
|
-
if (segments.length !== 1 && segments.length !== 2) return undefined;
|
|
172
|
-
if (segments.some((segment) => segment === "")) return undefined;
|
|
173
|
-
const [screenId, entityId] = segments;
|
|
174
|
-
if (screenId === undefined || screenId.includes(":")) return undefined;
|
|
175
|
-
if (screenId === lastSegment(ownScreenId)) return undefined;
|
|
176
|
-
|
|
177
|
-
let matched: ScreenDefinition | undefined;
|
|
178
|
-
for (const feature of features) {
|
|
179
|
-
matched = feature.screens.find((s) => lastSegment(s.id) === screenId);
|
|
180
|
-
if (matched !== undefined) break;
|
|
181
|
-
}
|
|
182
|
-
if (matched === undefined) return undefined;
|
|
183
|
-
if (!screenAccessAllows(matched.access, userRoles)) return undefined;
|
|
184
|
-
|
|
185
|
-
if (entityId !== undefined) {
|
|
186
|
-
if (entityId === "." || entityId === "..") return undefined;
|
|
187
|
-
if (UNSAFE_ENTITY_ID_PATTERN.test(entityId)) return undefined;
|
|
188
|
-
let decodedEntityId: string;
|
|
189
|
-
try {
|
|
190
|
-
decodedEntityId = decodeURIComponent(entityId);
|
|
191
|
-
} catch {
|
|
192
|
-
return undefined;
|
|
193
|
-
}
|
|
194
|
-
if (decodedEntityId === "." || decodedEntityId === "..") return undefined;
|
|
195
|
-
if (UNSAFE_ENTITY_ID_PATTERN.test(decodedEntityId) || decodedEntityId.includes("/")) {
|
|
196
|
-
return undefined;
|
|
197
|
-
}
|
|
198
|
-
if (matched.type !== "entityEdit" && matched.type !== "projectionDetail") return undefined;
|
|
199
|
-
}
|
|
200
|
-
if (matched.type === "projectionDetail" && matched.singleton !== true && entityId === undefined) {
|
|
201
|
-
return undefined;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return { screenId, ...(entityId !== undefined && { entityId }) };
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const ReturnHostContext = createContext<ReturnHost | undefined>(undefined);
|
|
208
|
-
|
|
209
|
-
export function ReturnHostProvider({
|
|
210
|
-
value,
|
|
211
|
-
children,
|
|
212
|
-
}: {
|
|
213
|
-
readonly value: ReturnHost;
|
|
214
|
-
readonly children: ReactNode;
|
|
215
|
-
}): ReactNode {
|
|
216
|
-
return <ReturnHostContext.Provider value={value}>{children}</ReturnHostContext.Provider>;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export function useReturnHost(): ReturnHost | undefined {
|
|
220
|
-
return useContext(ReturnHostContext);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Reads through useNav().searchParams (reactive), not a useState snapshot —
|
|
224
|
-
// a later setSearchParams call must not leave this stale.
|
|
225
|
-
export function useReturnTarget(ownScreenId: string): ScreenTarget | undefined {
|
|
226
|
-
const nav = useNav();
|
|
227
|
-
const features = useAppFeatures();
|
|
228
|
-
const userRoles = useUserRoles();
|
|
229
|
-
const raw = nav.searchParams[RETURN_TO_PARAM];
|
|
230
|
-
return useMemo(
|
|
231
|
-
() => resolveReturnTarget(raw, ownScreenId, features, userRoles),
|
|
232
|
-
[raw, ownScreenId, features, userRoles],
|
|
233
|
-
);
|
|
234
|
-
}
|