@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
package/src/app/layout-fields.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
EditFieldSpec,
|
|
3
|
-
EntityEditScreenDefinition,
|
|
4
|
-
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
5
|
-
import {
|
|
6
|
-
isFieldsEditSection,
|
|
7
|
-
normalizeEditField,
|
|
8
|
-
sectionFieldSpecs,
|
|
9
|
-
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
10
|
-
|
|
11
|
-
// Normalized field specs actually rendered by the screen's layout, extension
|
|
12
|
-
// (and relatedList) sections skipped. Both this and `layoutFieldNames` key
|
|
13
|
-
// off "rendered by the layout" for the same reason: a field the user never
|
|
14
|
-
// sees gets no chance to review/correct a value nor to fix a presence error
|
|
15
|
-
// (search-param merge, #1708; presence schema in form-schema.ts).
|
|
16
|
-
export function layoutEditFields(
|
|
17
|
-
screen: EntityEditScreenDefinition,
|
|
18
|
-
): readonly Exclude<EditFieldSpec, string>[] {
|
|
19
|
-
const specs: Exclude<EditFieldSpec, string>[] = [];
|
|
20
|
-
for (const section of screen.layout.sections) {
|
|
21
|
-
if (!isFieldsEditSection(section)) continue;
|
|
22
|
-
for (const spec of sectionFieldSpecs(section)) {
|
|
23
|
-
specs.push(normalizeEditField(spec));
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return specs;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function layoutFieldNames(screen: EntityEditScreenDefinition): ReadonlySet<string> {
|
|
30
|
-
return new Set(layoutEditFields(screen).map((spec) => spec.field));
|
|
31
|
-
}
|
package/src/app/list-facets.ts
DELETED
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
// Split from kumiko-screen.tsx: related-list-section.tsx renders through
|
|
2
|
-
// kumiko-screen.tsx, so importing this back from there would be a require cycle.
|
|
3
|
-
|
|
4
|
-
import type { ListFacetSpec } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
5
|
-
import { parseRefTarget } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
6
|
-
import type { Translate } from "@cosmicdrift/kumiko-headless";
|
|
7
|
-
import { Temporal } from "temporal-polyfill";
|
|
8
|
-
import type { DataTableFacet } from "../primitives";
|
|
9
|
-
|
|
10
|
-
// One resolved facet, independent of where the type info came from — an
|
|
11
|
-
// entity field (entityList) or an explicit ListFacetSpec (projectionList,
|
|
12
|
-
// relatedList). Shared by buildFilterFacets/buildFilterPayload so all list
|
|
13
|
-
// screens build filters and facet UI through one code path.
|
|
14
|
-
export type ResolvedFacetSpec = {
|
|
15
|
-
readonly field: string;
|
|
16
|
-
readonly type: "select" | "boolean" | "reference";
|
|
17
|
-
readonly label: string;
|
|
18
|
-
/** Empty for a "reference" facet until `ReferenceFacetBridges` resolves it
|
|
19
|
-
* at render time, unlike select/boolean's author-declared options. */
|
|
20
|
-
readonly options: readonly { readonly value: string; readonly label: string }[];
|
|
21
|
-
/** Present only for a "reference" facet — the resolved lookup target
|
|
22
|
-
* `ReferenceFacetBridges` needs to fetch its options. */
|
|
23
|
-
readonly reference?: {
|
|
24
|
-
readonly refEntity: string;
|
|
25
|
-
readonly refFeature: string;
|
|
26
|
-
readonly labelField: string;
|
|
27
|
-
};
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
// Fills in a "reference" spec's initially-empty `options` from the
|
|
31
|
-
// field-keyed options ReferenceFacetBridges resolved, shared by every caller.
|
|
32
|
-
export function mergeReferenceFacetOptions(
|
|
33
|
-
specs: readonly ResolvedFacetSpec[],
|
|
34
|
-
optionsByField: Readonly<
|
|
35
|
-
Record<string, readonly { readonly value: string; readonly label: string }[]>
|
|
36
|
-
>,
|
|
37
|
-
): ResolvedFacetSpec[] {
|
|
38
|
-
return specs.map((spec) =>
|
|
39
|
-
spec.reference !== undefined
|
|
40
|
-
? { ...spec, options: optionsByField[spec.field] ?? spec.options }
|
|
41
|
-
: spec,
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function buildFilterFacets(specs: readonly ResolvedFacetSpec[]): DataTableFacet[] {
|
|
46
|
-
return specs.map((spec) => ({ field: spec.field, label: spec.label, options: spec.options }));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// User-selected faceted filters from URL-state → payload.filters. Boolean
|
|
50
|
-
// fields coerce "true"/"false" strings to real booleans (DB column is
|
|
51
|
-
// boolean); everything else stays string[] under op:"in" (multi-select
|
|
52
|
-
// semantics). `typeOf` resolves a field to its known type string —
|
|
53
|
-
// undefined means "unknown field", so it's dropped (typo-safe: a stale/
|
|
54
|
-
// hand-crafted URL param for an undeclared field never reaches the
|
|
55
|
-
// server). Deliberately NOT gated on the field being a *facet* — entityList
|
|
56
|
-
// passes through any field present in entity.fields, matching its
|
|
57
|
-
// pre-fw#2224 behavior; only the boolean-coercion branch cares about type.
|
|
58
|
-
|
|
59
|
-
function isFacetI18nKey(label: string): boolean {
|
|
60
|
-
return !/\s/.test(label) && (label.includes(".") || label.includes(":"));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function buildFilterPayload(
|
|
64
|
-
urlFilters: Readonly<Record<string, readonly string[]>>,
|
|
65
|
-
typeOf: (field: string) => string | undefined,
|
|
66
|
-
): { field: string; op: "in"; value: unknown }[] {
|
|
67
|
-
const out: { field: string; op: "in"; value: unknown }[] = [];
|
|
68
|
-
for (const [field, values] of Object.entries(urlFilters)) {
|
|
69
|
-
if (values.length === 0) continue;
|
|
70
|
-
// `id` is a base column (not a declared facet), allowed as an id-set
|
|
71
|
-
// filter so a header-slot control — e.g. the tags TagFilter — can narrow
|
|
72
|
-
// ANY list to a resolved set of row ids without the host declaring a facet.
|
|
73
|
-
if (field === "id") {
|
|
74
|
-
out.push({ field, op: "in", value: values });
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
const type = typeOf(field);
|
|
78
|
-
if (type === undefined) continue;
|
|
79
|
-
const value = type === "boolean" ? values.map((v) => v === "true") : values;
|
|
80
|
-
out.push({ field, op: "in", value });
|
|
81
|
-
}
|
|
82
|
-
return out;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// projectionList/relatedList adapter — neither has an entity/i18n convention
|
|
86
|
-
// to derive labels from, so ListFacetSpec carries every label explicitly
|
|
87
|
-
// (fw#2224). Labels may be raw display strings or i18n keys; run them
|
|
88
|
-
// through translate like entity facets (passthrough when the key is missing).
|
|
89
|
-
// `featureName` resolves a reference facet's `entity` target the same way
|
|
90
|
-
// ListColumnSpec.refEntity does (same-feature short name, or "feature:entity").
|
|
91
|
-
export function resolveProjectionFacetSpecs(
|
|
92
|
-
facets: readonly ListFacetSpec[] | undefined,
|
|
93
|
-
translate: Translate,
|
|
94
|
-
featureName: string,
|
|
95
|
-
): ResolvedFacetSpec[] {
|
|
96
|
-
if (facets === undefined) return [];
|
|
97
|
-
const tr = (label: string): string => (isFacetI18nKey(label) ? translate(label) : label);
|
|
98
|
-
// dateRange carries no option list and no `filters` entry — it resolves
|
|
99
|
-
// through resolveDateRangeFacets into its own toolbar control instead.
|
|
100
|
-
return facets.flatMap((facet): ResolvedFacetSpec[] => {
|
|
101
|
-
if (facet.type === "dateRange") return [];
|
|
102
|
-
if (facet.type === "select") {
|
|
103
|
-
return [
|
|
104
|
-
{
|
|
105
|
-
field: facet.field,
|
|
106
|
-
type: "select",
|
|
107
|
-
label: tr(facet.label),
|
|
108
|
-
options: facet.options.map((opt) => ({
|
|
109
|
-
value: opt.value,
|
|
110
|
-
label: tr(opt.label),
|
|
111
|
-
})),
|
|
112
|
-
},
|
|
113
|
-
];
|
|
114
|
-
}
|
|
115
|
-
if (facet.type === "boolean") {
|
|
116
|
-
return [
|
|
117
|
-
{
|
|
118
|
-
field: facet.field,
|
|
119
|
-
type: "boolean",
|
|
120
|
-
label: tr(facet.label),
|
|
121
|
-
options: [
|
|
122
|
-
{ value: "true", label: tr(facet.trueLabel) },
|
|
123
|
-
{ value: "false", label: tr(facet.falseLabel) },
|
|
124
|
-
],
|
|
125
|
-
},
|
|
126
|
-
];
|
|
127
|
-
}
|
|
128
|
-
const target = parseRefTarget(facet.entity, featureName);
|
|
129
|
-
return [
|
|
130
|
-
{
|
|
131
|
-
field: facet.field,
|
|
132
|
-
type: "reference",
|
|
133
|
-
label: tr(facet.label),
|
|
134
|
-
options: [],
|
|
135
|
-
reference: {
|
|
136
|
-
refEntity: target.entityName,
|
|
137
|
-
refFeature: target.featureName,
|
|
138
|
-
labelField: facet.labelField ?? "id",
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
];
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// --- dateRange facet (fw#3104) ---
|
|
146
|
-
|
|
147
|
-
// A resolved dateRange facet. Kept out of ResolvedFacetSpec because it shares
|
|
148
|
-
// nothing with the option-dropdown facets: no options, no `filters` entry —
|
|
149
|
-
// the two bounds travel as named top-level query params.
|
|
150
|
-
export type ResolvedDateRangeFacet = {
|
|
151
|
-
readonly field: string;
|
|
152
|
-
readonly label: string;
|
|
153
|
-
readonly fromParam: string;
|
|
154
|
-
readonly toParam: string;
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
export type DateRangeBound = "from" | "to";
|
|
158
|
-
|
|
159
|
-
export type DateRangeValue = { readonly from: string; readonly to: string };
|
|
160
|
-
|
|
161
|
-
export function resolveDateRangeFacets(
|
|
162
|
-
facets: readonly ListFacetSpec[] | undefined,
|
|
163
|
-
translate: Translate,
|
|
164
|
-
): ResolvedDateRangeFacet[] {
|
|
165
|
-
if (facets === undefined) return [];
|
|
166
|
-
return facets.flatMap((facet): ResolvedDateRangeFacet[] =>
|
|
167
|
-
facet.type === "dateRange"
|
|
168
|
-
? [
|
|
169
|
-
{
|
|
170
|
-
field: facet.field,
|
|
171
|
-
label: isFacetI18nKey(facet.label) ? translate(facet.label) : facet.label,
|
|
172
|
-
fromParam: facet.params.from,
|
|
173
|
-
toParam: facet.params.to,
|
|
174
|
-
},
|
|
175
|
-
]
|
|
176
|
-
: [],
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// The bounds live in the same `<screenId>.f.<key>` URL namespace the option
|
|
181
|
-
// facets use, so setFilter's page-reset and clearFilters cover them for free.
|
|
182
|
-
// buildFilterPayload drops them again: a dotted key resolves to no facet
|
|
183
|
-
// type, which is its "unknown field" case.
|
|
184
|
-
export function dateRangeUrlField(field: string, bound: DateRangeBound): string {
|
|
185
|
-
return `${field}.${bound}`;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export function readDateRange(
|
|
189
|
-
urlFilters: Readonly<Record<string, readonly string[]>>,
|
|
190
|
-
field: string,
|
|
191
|
-
): DateRangeValue {
|
|
192
|
-
return {
|
|
193
|
-
from: urlFilters[dateRangeUrlField(field, "from")]?.[0] ?? "",
|
|
194
|
-
to: urlFilters[dateRangeUrlField(field, "to")]?.[0] ?? "",
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Keeps from <= to by pushing the opposite bound along, so an inverted range
|
|
199
|
-
// can't reach the server and trip a handler's `from <= to` refine (a 422 the
|
|
200
|
-
// user would see as a broken list, not as a correction).
|
|
201
|
-
export function clampDateRange(
|
|
202
|
-
current: DateRangeValue,
|
|
203
|
-
bound: DateRangeBound,
|
|
204
|
-
value: string,
|
|
205
|
-
): DateRangeValue {
|
|
206
|
-
if (bound === "from") {
|
|
207
|
-
const to = current.to !== "" && value !== "" && value > current.to ? value : current.to;
|
|
208
|
-
return { from: value, to };
|
|
209
|
-
}
|
|
210
|
-
const from = current.from !== "" && value !== "" && value < current.from ? value : current.from;
|
|
211
|
-
return { from, to: value };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// A calendar date covers a whole day in the viewer's zone: "from the 14th"
|
|
215
|
-
// starts at that day's first instant, "to the 14th" includes everything up to
|
|
216
|
-
// its last. Going through the next day's start keeps both ends right across a
|
|
217
|
-
// DST boundary, where the day is not 24h long.
|
|
218
|
-
function dayBoundInstant(date: string, bound: DateRangeBound, timeZone: string): string | null {
|
|
219
|
-
let plainDate: Temporal.PlainDate;
|
|
220
|
-
try {
|
|
221
|
-
plainDate = Temporal.PlainDate.from(date);
|
|
222
|
-
} catch {
|
|
223
|
-
// Half-typed or hand-crafted URL value — drop the bound rather than send
|
|
224
|
-
// something the handler's ISO-datetime schema would reject.
|
|
225
|
-
return null;
|
|
226
|
-
}
|
|
227
|
-
const start = plainDate.toZonedDateTime(timeZone).toInstant();
|
|
228
|
-
return bound === "from"
|
|
229
|
-
? start.toString()
|
|
230
|
-
: plainDate
|
|
231
|
-
.add({ days: 1 })
|
|
232
|
-
.toZonedDateTime(timeZone)
|
|
233
|
-
.toInstant()
|
|
234
|
-
.subtract({
|
|
235
|
-
nanoseconds: 1,
|
|
236
|
-
})
|
|
237
|
-
.toString();
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
export function buildDateRangePayload(
|
|
241
|
-
specs: readonly ResolvedDateRangeFacet[],
|
|
242
|
-
urlFilters: Readonly<Record<string, readonly string[]>>,
|
|
243
|
-
timeZone: string,
|
|
244
|
-
): Record<string, string> {
|
|
245
|
-
const out: Record<string, string> = {};
|
|
246
|
-
for (const spec of specs) {
|
|
247
|
-
const range = readDateRange(urlFilters, spec.field);
|
|
248
|
-
for (const [bound, param] of [
|
|
249
|
-
["from", spec.fromParam],
|
|
250
|
-
["to", spec.toParam],
|
|
251
|
-
] as const) {
|
|
252
|
-
const value = range[bound];
|
|
253
|
-
if (value === "") continue;
|
|
254
|
-
const instant = dayBoundInstant(value, bound, timeZone);
|
|
255
|
-
if (instant !== null) out[param] = instant;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
return out;
|
|
259
|
-
}
|
package/src/app/nav.tsx
DELETED
|
@@ -1,258 +0,0 @@
|
|
|
1
|
-
import { createContext, type ReactNode, useCallback, useContext, useEffect, useState } from "react";
|
|
2
|
-
import type { FeatureSchema } from "./feature-schema";
|
|
3
|
-
import { lastSegment } from "./qn";
|
|
4
|
-
|
|
5
|
-
// Navigation-Contract, plattform-neutral. Types + Context + Hook leben
|
|
6
|
-
// hier; die konkrete Implementation (window.history im Web,
|
|
7
|
-
// react-navigation im Native) kommt via `<NavProvider value={...}>`
|
|
8
|
-
// vom Plattform-Package rein.
|
|
9
|
-
//
|
|
10
|
-
// Pfad-Format hat zwei Modi je nach App-Schema:
|
|
11
|
-
// * ohne Workspaces: `/<screenId>[/<entityId>]`
|
|
12
|
-
// * mit Workspaces: `/<workspaceId>/<screenId>[/<entityId>]`
|
|
13
|
-
// Der Mode wird beim parsePath() per `hasWorkspaces` Flag durchgereicht
|
|
14
|
-
// — Schema-Layer entscheidet, parsePath bleibt dumm/pure. formatPath()
|
|
15
|
-
// braucht keinen Hint: target.workspaceId vorhanden → Prefix; sonst flach.
|
|
16
|
-
|
|
17
|
-
export type NavRoute = {
|
|
18
|
-
// Active workspace short id, present iff the app runs in workspace
|
|
19
|
-
// mode (schema.workspaces non-empty). undefined for non-workspace apps.
|
|
20
|
-
readonly workspaceId?: string;
|
|
21
|
-
readonly screenId: string;
|
|
22
|
-
readonly entityId?: string;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export type ScreenTarget = {
|
|
26
|
-
// Optional in workspace-aware navigate calls. Omit for cross-workspace
|
|
27
|
-
// navigation (current workspace stays); set to switch workspaces in the
|
|
28
|
-
// same call as picking a screen — e.g. WorkspaceSwitcher does this.
|
|
29
|
-
readonly workspaceId?: string;
|
|
30
|
-
readonly screenId: string;
|
|
31
|
-
readonly entityId?: string;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// Object-based nav target — callers name WHAT they want to see (an entity's
|
|
35
|
-
// row) instead of WHICH screen shows it. resolveTarget() below turns this
|
|
36
|
-
// into a ScreenTarget by looking up the screen with matching `detailFor`.
|
|
37
|
-
// No edit-form resolution here: an entity can have several entityEdit
|
|
38
|
-
// screens (e.g. solon's "property" has three), so "the" edit screen isn't
|
|
39
|
-
// well-defined — callers that want a form still name its screenId directly.
|
|
40
|
-
export type ObjectTarget = {
|
|
41
|
-
readonly workspaceId?: string;
|
|
42
|
-
readonly entity: string;
|
|
43
|
-
readonly id: string;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
// No `kind` tag: `"screenId" in target` narrows cleanly, and a tag would
|
|
47
|
-
// force every existing NavTarget literal (every navigate/hrefFor call in
|
|
48
|
-
// every app) to add one for an additive ticket. See resolveTarget().
|
|
49
|
-
export type NavTarget = ScreenTarget | ObjectTarget;
|
|
50
|
-
|
|
51
|
-
export type NavApi = {
|
|
52
|
-
/** Current route — `undefined` when the URL is at the root / there's
|
|
53
|
-
* no route selected. Caller's initial fallback kicks in then. */
|
|
54
|
-
readonly route: NavRoute | undefined;
|
|
55
|
-
/** Push a new route. Platform-specific Impl writes to
|
|
56
|
-
* history/stack and notifies subscribers. */
|
|
57
|
-
readonly navigate: (target: NavTarget) => void;
|
|
58
|
-
/** Replace the current route in place. Same effect as navigate from
|
|
59
|
-
* the user's perspective, but doesn't add a history entry — used for
|
|
60
|
-
* mount-time URL fills (e.g. WorkspaceShell defaulting to `/admin/x`
|
|
61
|
-
* when the user typed `/`). Browser Back must take the user out of
|
|
62
|
-
* the app, not back to the original empty path. */
|
|
63
|
-
readonly replace: (target: NavTarget) => void;
|
|
64
|
-
/** Build the href a click on {target} would produce. Used by
|
|
65
|
-
* platform-specific Link-Komponenten (Web: `<a href>`; Native
|
|
66
|
-
* typically doesn't need this). */
|
|
67
|
-
readonly hrefFor: (target: NavTarget) => string;
|
|
68
|
-
/** Lese-Snapshot der aktuellen Search-Params (Browser: ?key=value-
|
|
69
|
-
* Pairs nach dem Pfad). Native-Impls die kein URL-Konzept haben
|
|
70
|
-
* liefern ein leeres Object. Wert ist ein Plain-Record (kein Map)
|
|
71
|
-
* damit React-Subscribers shallow-compare können. */
|
|
72
|
-
readonly searchParams: Readonly<Record<string, string>>;
|
|
73
|
-
/** Mergt Updates in die aktuellen Search-Params. Wert `null` löscht
|
|
74
|
-
* den Key. Ändert NICHT den Pfad. Plattform-Impls nutzen
|
|
75
|
-
* history.replaceState (kein Push) — Sort/Filter-Toggles sollen
|
|
76
|
-
* nicht die Back-Navigation fluten. Native-Impls können no-op'en
|
|
77
|
-
* wenn das Konzept nicht existiert. */
|
|
78
|
-
readonly setSearchParams: (updates: Readonly<Record<string, string | null>>) => void;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
// Pfad-Format:
|
|
82
|
-
// ohne workspaces: `/task-list`, `/task-edit/abc-123`, `/` → undefined
|
|
83
|
-
// mit workspaces: `/admin/task-list`, `/admin/task-edit/abc`,
|
|
84
|
-
// `/admin` → workspace-only (no screen yet),
|
|
85
|
-
// `/` → undefined.
|
|
86
|
-
// Alles nach den ersten 2/3 Segmenten wird ignoriert — kumikos
|
|
87
|
-
// Navigation-Grammatik nested nicht weiter. Nested-Routes wäre eine
|
|
88
|
-
// Spec-Änderung, nicht ein URL-Shape-Unfall.
|
|
89
|
-
export function parsePath(pathname: string, hasWorkspaces?: boolean): NavRoute | undefined {
|
|
90
|
-
const parts = pathname.split("/").filter((p) => p !== "");
|
|
91
|
-
if (hasWorkspaces === true) {
|
|
92
|
-
const [workspaceId, screenId, entityId] = parts;
|
|
93
|
-
if (workspaceId === undefined || workspaceId === "") return undefined;
|
|
94
|
-
if (screenId === undefined || screenId === "") {
|
|
95
|
-
// Workspace-only URL ("/admin") — caller resolves the default screen
|
|
96
|
-
// for that workspace. We carry workspaceId WITHOUT a screen so the
|
|
97
|
-
// shell can branch instead of making something up.
|
|
98
|
-
return { workspaceId, screenId: "" };
|
|
99
|
-
}
|
|
100
|
-
return {
|
|
101
|
-
workspaceId,
|
|
102
|
-
screenId,
|
|
103
|
-
...(entityId !== undefined && { entityId }),
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
const [screenId, entityId] = parts;
|
|
107
|
-
if (screenId === undefined || screenId === "") return undefined;
|
|
108
|
-
return { screenId, ...(entityId !== undefined && { entityId }) };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export function formatPath(target: ScreenTarget): string {
|
|
112
|
-
// Workspace-Mode: prefix the workspace short id. Order matters —
|
|
113
|
-
// workspace before screen mirrors parsePath's segment order.
|
|
114
|
-
const segments: string[] = [];
|
|
115
|
-
if (target.workspaceId !== undefined) segments.push(target.workspaceId);
|
|
116
|
-
segments.push(target.screenId);
|
|
117
|
-
if (target.entityId !== undefined) segments.push(target.entityId);
|
|
118
|
-
return `/${segments.join("/")}`;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Shared entity→detailFor-screen lookup — resolveTarget's ObjectTarget
|
|
122
|
-
// branch and hasDetailScreen (create-app's row-click default, fw#2164) both
|
|
123
|
-
// need "is there a detail screen for this entity", one to resolve it, the
|
|
124
|
-
// other to just check before falling back to a different default.
|
|
125
|
-
function findDetailForScreen(
|
|
126
|
-
features: readonly FeatureSchema[],
|
|
127
|
-
entity: string,
|
|
128
|
-
): { readonly featureName: string; readonly screenId: string } | undefined {
|
|
129
|
-
for (const feature of features) {
|
|
130
|
-
for (const screen of feature.screens) {
|
|
131
|
-
if (screen.detailFor === entity)
|
|
132
|
-
return { featureName: feature.featureName, screenId: screen.id };
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return undefined;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/** Non-throwing existence check for findDetailForScreen — lets a caller pick
|
|
139
|
-
* a different default (fw#2164's row-click fallback) instead of resolving. */
|
|
140
|
-
export function hasDetailScreen(features: readonly FeatureSchema[], entity: string): boolean {
|
|
141
|
-
return findDetailForScreen(features, entity) !== undefined;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Resolves a NavTarget to the ScreenTarget form navigate/replace/hrefFor
|
|
145
|
-
// operate on. Pure — no context, no I/O — so callers can use it outside
|
|
146
|
-
// React too (see resolve-at-NavApi-build alternative in renderer-web).
|
|
147
|
-
export function resolveTarget(features: readonly FeatureSchema[], target: NavTarget): ScreenTarget {
|
|
148
|
-
if ("screenId" in target) return target;
|
|
149
|
-
|
|
150
|
-
const found = findDetailForScreen(features, target.entity);
|
|
151
|
-
if (found !== undefined) {
|
|
152
|
-
// Short form (globally unique per validateScreenShortIdCollisions) —
|
|
153
|
-
// ScreenTarget.screenId round-trips through formatPath/parsePath as a
|
|
154
|
-
// short id; qualifying it here double-qualifies at RoutedScreen.
|
|
155
|
-
return {
|
|
156
|
-
...(target.workspaceId !== undefined && { workspaceId: target.workspaceId }),
|
|
157
|
-
screenId: found.screenId,
|
|
158
|
-
entityId: target.id,
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
throw new Error(
|
|
163
|
-
`resolveTarget: no detail screen for entity "${target.entity}". Add detailFor: "${target.entity}" to the screen that shows it.`,
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Context + Hook. Default ist `undefined` damit fehlender Provider
|
|
168
|
-
// laut kracht statt ein silent-no-op NavApi mit toten navigate()
|
|
169
|
-
// Aufrufen anzubieten.
|
|
170
|
-
const NavContext = createContext<NavApi | undefined>(undefined);
|
|
171
|
-
|
|
172
|
-
export type NavProviderProps = {
|
|
173
|
-
readonly children: ReactNode;
|
|
174
|
-
readonly value: NavApi;
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
type InitialValuesHandoff = {
|
|
178
|
-
readonly screenId: string;
|
|
179
|
-
readonly values: Readonly<Record<string, unknown>>;
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
// A mutable slot instead of state: offering must not re-render the app, and
|
|
183
|
-
// the target form reads it once on mount.
|
|
184
|
-
type InitialValuesHandoffSlot = { current: InitialValuesHandoff | undefined };
|
|
185
|
-
|
|
186
|
-
const InitialValuesHandoffContext = createContext<InitialValuesHandoffSlot | undefined>(undefined);
|
|
187
|
-
|
|
188
|
-
export function NavProvider({ children, value }: NavProviderProps): ReactNode {
|
|
189
|
-
const [handoffSlot] = useState<InitialValuesHandoffSlot>(() => ({ current: undefined }));
|
|
190
|
-
const routeScreenId = value.route?.screenId;
|
|
191
|
-
// An offer whose form never mounted (e.g. already on that screen) must not
|
|
192
|
-
// resurface on a later, unrelated visit — drop it once the route moves elsewhere.
|
|
193
|
-
useEffect(() => {
|
|
194
|
-
const pending = handoffSlot.current;
|
|
195
|
-
if (pending === undefined || routeScreenId === undefined) return;
|
|
196
|
-
if (lastSegment(routeScreenId) !== pending.screenId) handoffSlot.current = undefined;
|
|
197
|
-
}, [handoffSlot, routeScreenId]);
|
|
198
|
-
return (
|
|
199
|
-
<NavContext.Provider value={value}>
|
|
200
|
-
<InitialValuesHandoffContext.Provider value={handoffSlot}>
|
|
201
|
-
{children}
|
|
202
|
-
</InitialValuesHandoffContext.Provider>
|
|
203
|
-
</NavContext.Provider>
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Navigates to a form screen (actionForm, secretMint, entityEdit-create) with
|
|
208
|
-
// initial values carried in memory — never in the URL, so a link cannot
|
|
209
|
-
// forge them and they are not limited to the screen's urlPrefillFields.
|
|
210
|
-
// sensitive and password fields are still never prefilled.
|
|
211
|
-
export function useNavigateWithInitialValues(): (
|
|
212
|
-
target: ScreenTarget,
|
|
213
|
-
initialValues: Readonly<Record<string, unknown>>,
|
|
214
|
-
) => void {
|
|
215
|
-
const nav = useNav();
|
|
216
|
-
const handoffSlot = useContext(InitialValuesHandoffContext);
|
|
217
|
-
return useCallback(
|
|
218
|
-
(target, initialValues) => {
|
|
219
|
-
if (handoffSlot !== undefined) {
|
|
220
|
-
handoffSlot.current = { screenId: lastSegment(target.screenId), values: initialValues };
|
|
221
|
-
}
|
|
222
|
-
nav.navigate(target);
|
|
223
|
-
},
|
|
224
|
-
[nav, handoffSlot],
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
export function useInitialValuesHandoff(
|
|
229
|
-
screenId: string,
|
|
230
|
-
): Readonly<Record<string, unknown>> | undefined {
|
|
231
|
-
const handoffSlot = useContext(InitialValuesHandoffContext);
|
|
232
|
-
const [values] = useState(() => {
|
|
233
|
-
const pending = handoffSlot?.current;
|
|
234
|
-
return pending !== undefined && pending.screenId === lastSegment(screenId)
|
|
235
|
-
? pending.values
|
|
236
|
-
: undefined;
|
|
237
|
-
});
|
|
238
|
-
useEffect(() => {
|
|
239
|
-
if (
|
|
240
|
-
handoffSlot !== undefined &&
|
|
241
|
-
values !== undefined &&
|
|
242
|
-
handoffSlot.current?.values === values
|
|
243
|
-
) {
|
|
244
|
-
handoffSlot.current = undefined;
|
|
245
|
-
}
|
|
246
|
-
}, [handoffSlot, values]);
|
|
247
|
-
return values;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export function useNav(): NavApi {
|
|
251
|
-
const api = useContext(NavContext);
|
|
252
|
-
if (api === undefined) {
|
|
253
|
-
throw new Error(
|
|
254
|
-
"useNav: no <NavProvider> mounted above this component. Plattform-Packages (z.B. @cosmicdrift/kumiko-renderer-web) liefern eine Default-Impl über createKumikoApp.",
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
return api;
|
|
258
|
-
}
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
// Shim für ProjectionDetail-Renderer (analog zu projection-list-shim.ts /
|
|
2
|
-
// config-edit-shim.ts).
|
|
3
|
-
//
|
|
4
|
-
// RenderEdit verlangt ein `entity: EntityDefinition` + ein
|
|
5
|
-
// `screen: EntityEditScreenDefinition`-Pair, nutzt davon aber nur
|
|
6
|
-
// `entity.fields` (Feld-Typ für den Input-Renderer) und `screen.layout`
|
|
7
|
-
// (welche Felder in welchen Sections). Ein projectionDetail-Screen ist
|
|
8
|
-
// query-getrieben und hat KEINE Entity — die zwei Helper hier shapen den
|
|
9
|
-
// Input ad-hoc um, damit RenderEdit reused werden kann.
|
|
10
|
-
//
|
|
11
|
-
// Der strukturelle Read-Only-Beweis liegt in synthesizeProjectionDetailScreen:
|
|
12
|
-
// JEDES Feld wird hart auf readOnly:true gesetzt (nicht nur was der Author im
|
|
13
|
-
// Layout gesetzt hat) — hasEditableSection() liest genau dieses Flag und
|
|
14
|
-
// blendet den Save-Button aus, wenn kein Feld editierbar ist. Der Author kann
|
|
15
|
-
// diese Garantie nicht versehentlich umgehen.
|
|
16
|
-
//
|
|
17
|
-
// Selbe Schulden-Reservation wie bei den anderen Shims: greift RenderEdit
|
|
18
|
-
// künftig auf weitere EntityDefinition-Felder (transitions, idType) zu, oder
|
|
19
|
-
// cross-referenziert ein Boot-Validator die schema.entities-Map, brechen die
|
|
20
|
-
// Type-Lies hier silent — dann ist Zeit für eine echte RenderProjectionDetail-
|
|
21
|
-
// Komponente.
|
|
22
|
-
|
|
23
|
-
import type {
|
|
24
|
-
EditFieldSpec,
|
|
25
|
-
EditLayout,
|
|
26
|
-
EntityDefinition,
|
|
27
|
-
EntityEditScreenDefinition,
|
|
28
|
-
ProjectionDetailScreenDefinition,
|
|
29
|
-
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
30
|
-
import {
|
|
31
|
-
isFieldsEditSection,
|
|
32
|
-
normalizeEditField,
|
|
33
|
-
PROJECTION_DETAIL_ENTITY as PROJECTION_DETAIL_PSEUDO_ENTITY,
|
|
34
|
-
sectionFieldSpecs,
|
|
35
|
-
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
36
|
-
|
|
37
|
-
/** Minimale EntityDefinition aus den Layout-Feldern: jedes Feld ein Text-
|
|
38
|
-
* Feld — computeEditViewModel liest nur `fields[<f>].type`, Text reicht für
|
|
39
|
-
* eine reine Anzeige (kein Select/Number-spezifisches Rendering nötig). */
|
|
40
|
-
export function synthesizeProjectionDetailEntity(layout: EditLayout): EntityDefinition {
|
|
41
|
-
const fields: Record<string, { type: "text" }> = {};
|
|
42
|
-
for (const section of layout.sections) {
|
|
43
|
-
// relatedList and extension sections carry no `fields` to synthesize.
|
|
44
|
-
if (!isFieldsEditSection(section)) continue;
|
|
45
|
-
for (const spec of sectionFieldSpecs(section)) {
|
|
46
|
-
fields[normalizeEditField(spec).field] = { type: "text" };
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return { fields } as unknown as EntityDefinition;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Wandelt ein ProjectionDetailScreenDefinition in die EntityEditScreen-Shape
|
|
53
|
-
* die RenderEdit erwartet — mit jedem Feld hart auf readOnly:true erzwungen. */
|
|
54
|
-
export function synthesizeProjectionDetailScreen(
|
|
55
|
-
screen: ProjectionDetailScreenDefinition,
|
|
56
|
-
): EntityEditScreenDefinition {
|
|
57
|
-
const forceReadOnly = (spec: EditFieldSpec) => ({ ...normalizeEditField(spec), readOnly: true });
|
|
58
|
-
const sections = screen.layout.sections.map((section) => {
|
|
59
|
-
// relatedList and extension sections pass through unchanged.
|
|
60
|
-
if (!isFieldsEditSection(section)) return section;
|
|
61
|
-
return {
|
|
62
|
-
...section,
|
|
63
|
-
// kumiko-lint-ignore section-fields-raw rewrites both sources in place — the groups branch below does the same for groups[].fields
|
|
64
|
-
fields: section.fields.map(forceReadOnly),
|
|
65
|
-
...(section.groups !== undefined && {
|
|
66
|
-
groups: section.groups.map((group) => ({
|
|
67
|
-
...group,
|
|
68
|
-
fields: group.fields.map(forceReadOnly),
|
|
69
|
-
})),
|
|
70
|
-
}),
|
|
71
|
-
};
|
|
72
|
-
});
|
|
73
|
-
return {
|
|
74
|
-
id: screen.id,
|
|
75
|
-
type: "entityEdit",
|
|
76
|
-
entity: PROJECTION_DETAIL_PSEUDO_ENTITY,
|
|
77
|
-
layout: { sections, ...(screen.layout.width !== undefined && { width: screen.layout.width }) },
|
|
78
|
-
allowCreate: false,
|
|
79
|
-
allowDelete: false,
|
|
80
|
-
...(screen.description !== undefined && { description: screen.description }),
|
|
81
|
-
...(screen.fieldLabels !== undefined && { fieldLabels: screen.fieldLabels }),
|
|
82
|
-
...(screen.slots !== undefined && { slots: screen.slots }),
|
|
83
|
-
...(screen.access !== undefined && { access: screen.access }),
|
|
84
|
-
};
|
|
85
|
-
}
|