@cosmicdrift/kumiko-renderer 0.328.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 -46
- 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 -41
- 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/{src/inside-drawer.ts → dist/inside-drawer.js} +2 -5
- package/dist/page-header-slot.d.ts +2 -0
- package/{src/page-header-slot.ts → dist/page-header-slot.js} +2 -5
- 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/__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-fill-height.test.tsx +0 -118
- 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 -128
- 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 -3801
- 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 -86
- package/src/app/reference-facet-bridge.tsx +0 -58
- package/src/app/return-to.tsx +0 -234
- package/src/app/row-actions.ts +0 -640
- 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 -1733
- 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 -88
- 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 -491
- package/src/components/render-edit-action-button.tsx +0 -109
- package/src/components/render-edit-logic.ts +0 -85
- package/src/components/render-edit-types.ts +0 -226
- package/src/components/render-edit.tsx +0 -1871
- package/src/components/render-field.tsx +0 -951
- package/src/components/render-list.tsx +0 -593
- package/src/components/write-form-section.tsx +0 -198
- 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-query.test.tsx +0 -149
- 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 -207
- package/src/hooks/use-mutation.ts +0 -61
- package/src/hooks/use-query.ts +0 -166
- 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 -240
- package/src/i18n.tsx +0 -286
- package/src/index.ts +0 -279
- package/src/primitives.tsx +0 -1567
- package/src/screen-fills-height.ts +0 -3
- 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
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { EditFieldSpec, EntityEditScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
export declare function layoutEditFields(screen: EntityEditScreenDefinition): readonly Exclude<EditFieldSpec, string>[];
|
|
3
|
+
export declare function layoutFieldNames(screen: EntityEditScreenDefinition): ReadonlySet<string>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { isFieldsEditSection, normalizeEditField, sectionFieldSpecs, } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
// Normalized field specs actually rendered by the screen's layout, extension
|
|
3
|
+
// (and relatedList) sections skipped. Both this and `layoutFieldNames` key
|
|
4
|
+
// off "rendered by the layout" for the same reason: a field the user never
|
|
5
|
+
// sees gets no chance to review/correct a value nor to fix a presence error
|
|
6
|
+
// (search-param merge, #1708; presence schema in form-schema.ts).
|
|
7
|
+
export function layoutEditFields(screen) {
|
|
8
|
+
const specs = [];
|
|
9
|
+
for (const section of screen.layout.sections) {
|
|
10
|
+
if (!isFieldsEditSection(section))
|
|
11
|
+
continue;
|
|
12
|
+
for (const spec of sectionFieldSpecs(section)) {
|
|
13
|
+
specs.push(normalizeEditField(spec));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return specs;
|
|
17
|
+
}
|
|
18
|
+
export function layoutFieldNames(screen) {
|
|
19
|
+
return new Set(layoutEditFields(screen).map((spec) => spec.field));
|
|
20
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ListFacetSpec } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import type { Translate } from "@cosmicdrift/kumiko-headless";
|
|
3
|
+
import type { DataTableFacet } from "../primitives.js";
|
|
4
|
+
export type ResolvedFacetSpec = {
|
|
5
|
+
readonly field: string;
|
|
6
|
+
readonly type: "select" | "boolean" | "reference";
|
|
7
|
+
readonly label: string;
|
|
8
|
+
/** Empty for a "reference" facet until `ReferenceFacetBridges` resolves it
|
|
9
|
+
* at render time, unlike select/boolean's author-declared options. */
|
|
10
|
+
readonly options: readonly {
|
|
11
|
+
readonly value: string;
|
|
12
|
+
readonly label: string;
|
|
13
|
+
}[];
|
|
14
|
+
/** Present only for a "reference" facet — the resolved lookup target
|
|
15
|
+
* `ReferenceFacetBridges` needs to fetch its options. */
|
|
16
|
+
readonly reference?: {
|
|
17
|
+
readonly refEntity: string;
|
|
18
|
+
readonly refFeature: string;
|
|
19
|
+
readonly labelField: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
export declare function mergeReferenceFacetOptions(specs: readonly ResolvedFacetSpec[], optionsByField: Readonly<Record<string, readonly {
|
|
23
|
+
readonly value: string;
|
|
24
|
+
readonly label: string;
|
|
25
|
+
}[]>>): ResolvedFacetSpec[];
|
|
26
|
+
export declare function buildFilterFacets(specs: readonly ResolvedFacetSpec[]): DataTableFacet[];
|
|
27
|
+
export declare function buildFilterPayload(urlFilters: Readonly<Record<string, readonly string[]>>, typeOf: (field: string) => string | undefined): {
|
|
28
|
+
field: string;
|
|
29
|
+
op: "in";
|
|
30
|
+
value: unknown;
|
|
31
|
+
}[];
|
|
32
|
+
export declare function resolveProjectionFacetSpecs(facets: readonly ListFacetSpec[] | undefined, translate: Translate, featureName: string): ResolvedFacetSpec[];
|
|
33
|
+
export type ResolvedDateRangeFacet = {
|
|
34
|
+
readonly field: string;
|
|
35
|
+
readonly label: string;
|
|
36
|
+
readonly fromParam: string;
|
|
37
|
+
readonly toParam: string;
|
|
38
|
+
};
|
|
39
|
+
export type DateRangeBound = "from" | "to";
|
|
40
|
+
export type DateRangeValue = {
|
|
41
|
+
readonly from: string;
|
|
42
|
+
readonly to: string;
|
|
43
|
+
};
|
|
44
|
+
export declare function resolveDateRangeFacets(facets: readonly ListFacetSpec[] | undefined, translate: Translate): ResolvedDateRangeFacet[];
|
|
45
|
+
export declare function dateRangeUrlField(field: string, bound: DateRangeBound): string;
|
|
46
|
+
export declare function readDateRange(urlFilters: Readonly<Record<string, readonly string[]>>, field: string): DateRangeValue;
|
|
47
|
+
export declare function clampDateRange(current: DateRangeValue, bound: DateRangeBound, value: string): DateRangeValue;
|
|
48
|
+
export declare function buildDateRangePayload(specs: readonly ResolvedDateRangeFacet[], urlFilters: Readonly<Record<string, readonly string[]>>, timeZone: string): Record<string, string>;
|
|
@@ -0,0 +1,185 @@
|
|
|
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
|
+
import { parseRefTarget } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
4
|
+
import { Temporal } from "temporal-polyfill";
|
|
5
|
+
// Fills in a "reference" spec's initially-empty `options` from the
|
|
6
|
+
// field-keyed options ReferenceFacetBridges resolved, shared by every caller.
|
|
7
|
+
export function mergeReferenceFacetOptions(specs, optionsByField) {
|
|
8
|
+
return specs.map((spec) => spec.reference !== undefined
|
|
9
|
+
? { ...spec, options: optionsByField[spec.field] ?? spec.options }
|
|
10
|
+
: spec);
|
|
11
|
+
}
|
|
12
|
+
export function buildFilterFacets(specs) {
|
|
13
|
+
return specs.map((spec) => ({ field: spec.field, label: spec.label, options: spec.options }));
|
|
14
|
+
}
|
|
15
|
+
// User-selected faceted filters from URL-state → payload.filters. Boolean
|
|
16
|
+
// fields coerce "true"/"false" strings to real booleans (DB column is
|
|
17
|
+
// boolean); everything else stays string[] under op:"in" (multi-select
|
|
18
|
+
// semantics). `typeOf` resolves a field to its known type string —
|
|
19
|
+
// undefined means "unknown field", so it's dropped (typo-safe: a stale/
|
|
20
|
+
// hand-crafted URL param for an undeclared field never reaches the
|
|
21
|
+
// server). Deliberately NOT gated on the field being a *facet* — entityList
|
|
22
|
+
// passes through any field present in entity.fields, matching its
|
|
23
|
+
// pre-fw#2224 behavior; only the boolean-coercion branch cares about type.
|
|
24
|
+
function isFacetI18nKey(label) {
|
|
25
|
+
return !/\s/.test(label) && (label.includes(".") || label.includes(":"));
|
|
26
|
+
}
|
|
27
|
+
export function buildFilterPayload(urlFilters, typeOf) {
|
|
28
|
+
const out = [];
|
|
29
|
+
for (const [field, values] of Object.entries(urlFilters)) {
|
|
30
|
+
if (values.length === 0)
|
|
31
|
+
continue;
|
|
32
|
+
// `id` is a base column (not a declared facet), allowed as an id-set
|
|
33
|
+
// filter so a header-slot control — e.g. the tags TagFilter — can narrow
|
|
34
|
+
// ANY list to a resolved set of row ids without the host declaring a facet.
|
|
35
|
+
if (field === "id") {
|
|
36
|
+
out.push({ field, op: "in", value: values });
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const type = typeOf(field);
|
|
40
|
+
if (type === undefined)
|
|
41
|
+
continue;
|
|
42
|
+
const value = type === "boolean" ? values.map((v) => v === "true") : values;
|
|
43
|
+
out.push({ field, op: "in", value });
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
// projectionList/relatedList adapter — neither has an entity/i18n convention
|
|
48
|
+
// to derive labels from, so ListFacetSpec carries every label explicitly
|
|
49
|
+
// (fw#2224). Labels may be raw display strings or i18n keys; run them
|
|
50
|
+
// through translate like entity facets (passthrough when the key is missing).
|
|
51
|
+
// `featureName` resolves a reference facet's `entity` target the same way
|
|
52
|
+
// ListColumnSpec.refEntity does (same-feature short name, or "feature:entity").
|
|
53
|
+
export function resolveProjectionFacetSpecs(facets, translate, featureName) {
|
|
54
|
+
if (facets === undefined)
|
|
55
|
+
return [];
|
|
56
|
+
const tr = (label) => (isFacetI18nKey(label) ? translate(label) : label);
|
|
57
|
+
// dateRange carries no option list and no `filters` entry — it resolves
|
|
58
|
+
// through resolveDateRangeFacets into its own toolbar control instead.
|
|
59
|
+
return facets.flatMap((facet) => {
|
|
60
|
+
if (facet.type === "dateRange")
|
|
61
|
+
return [];
|
|
62
|
+
if (facet.type === "select") {
|
|
63
|
+
return [
|
|
64
|
+
{
|
|
65
|
+
field: facet.field,
|
|
66
|
+
type: "select",
|
|
67
|
+
label: tr(facet.label),
|
|
68
|
+
options: facet.options.map((opt) => ({
|
|
69
|
+
value: opt.value,
|
|
70
|
+
label: tr(opt.label),
|
|
71
|
+
})),
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
if (facet.type === "boolean") {
|
|
76
|
+
return [
|
|
77
|
+
{
|
|
78
|
+
field: facet.field,
|
|
79
|
+
type: "boolean",
|
|
80
|
+
label: tr(facet.label),
|
|
81
|
+
options: [
|
|
82
|
+
{ value: "true", label: tr(facet.trueLabel) },
|
|
83
|
+
{ value: "false", label: tr(facet.falseLabel) },
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
const target = parseRefTarget(facet.entity, featureName);
|
|
89
|
+
return [
|
|
90
|
+
{
|
|
91
|
+
field: facet.field,
|
|
92
|
+
type: "reference",
|
|
93
|
+
label: tr(facet.label),
|
|
94
|
+
options: [],
|
|
95
|
+
reference: {
|
|
96
|
+
refEntity: target.entityName,
|
|
97
|
+
refFeature: target.featureName,
|
|
98
|
+
labelField: facet.labelField ?? "id",
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
export function resolveDateRangeFacets(facets, translate) {
|
|
105
|
+
if (facets === undefined)
|
|
106
|
+
return [];
|
|
107
|
+
return facets.flatMap((facet) => facet.type === "dateRange"
|
|
108
|
+
? [
|
|
109
|
+
{
|
|
110
|
+
field: facet.field,
|
|
111
|
+
label: isFacetI18nKey(facet.label) ? translate(facet.label) : facet.label,
|
|
112
|
+
fromParam: facet.params.from,
|
|
113
|
+
toParam: facet.params.to,
|
|
114
|
+
},
|
|
115
|
+
]
|
|
116
|
+
: []);
|
|
117
|
+
}
|
|
118
|
+
// The bounds live in the same `<screenId>.f.<key>` URL namespace the option
|
|
119
|
+
// facets use, so setFilter's page-reset and clearFilters cover them for free.
|
|
120
|
+
// buildFilterPayload drops them again: a dotted key resolves to no facet
|
|
121
|
+
// type, which is its "unknown field" case.
|
|
122
|
+
export function dateRangeUrlField(field, bound) {
|
|
123
|
+
return `${field}.${bound}`;
|
|
124
|
+
}
|
|
125
|
+
export function readDateRange(urlFilters, field) {
|
|
126
|
+
return {
|
|
127
|
+
from: urlFilters[dateRangeUrlField(field, "from")]?.[0] ?? "",
|
|
128
|
+
to: urlFilters[dateRangeUrlField(field, "to")]?.[0] ?? "",
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
// Keeps from <= to by pushing the opposite bound along, so an inverted range
|
|
132
|
+
// can't reach the server and trip a handler's `from <= to` refine (a 422 the
|
|
133
|
+
// user would see as a broken list, not as a correction).
|
|
134
|
+
export function clampDateRange(current, bound, value) {
|
|
135
|
+
if (bound === "from") {
|
|
136
|
+
const to = current.to !== "" && value !== "" && value > current.to ? value : current.to;
|
|
137
|
+
return { from: value, to };
|
|
138
|
+
}
|
|
139
|
+
const from = current.from !== "" && value !== "" && value < current.from ? value : current.from;
|
|
140
|
+
return { from, to: value };
|
|
141
|
+
}
|
|
142
|
+
// A calendar date covers a whole day in the viewer's zone: "from the 14th"
|
|
143
|
+
// starts at that day's first instant, "to the 14th" includes everything up to
|
|
144
|
+
// its last. Going through the next day's start keeps both ends right across a
|
|
145
|
+
// DST boundary, where the day is not 24h long.
|
|
146
|
+
function dayBoundInstant(date, bound, timeZone) {
|
|
147
|
+
let plainDate;
|
|
148
|
+
try {
|
|
149
|
+
plainDate = Temporal.PlainDate.from(date);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Half-typed or hand-crafted URL value — drop the bound rather than send
|
|
153
|
+
// something the handler's ISO-datetime schema would reject.
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const start = plainDate.toZonedDateTime(timeZone).toInstant();
|
|
157
|
+
return bound === "from"
|
|
158
|
+
? start.toString()
|
|
159
|
+
: plainDate
|
|
160
|
+
.add({ days: 1 })
|
|
161
|
+
.toZonedDateTime(timeZone)
|
|
162
|
+
.toInstant()
|
|
163
|
+
.subtract({
|
|
164
|
+
nanoseconds: 1,
|
|
165
|
+
})
|
|
166
|
+
.toString();
|
|
167
|
+
}
|
|
168
|
+
export function buildDateRangePayload(specs, urlFilters, timeZone) {
|
|
169
|
+
const out = {};
|
|
170
|
+
for (const spec of specs) {
|
|
171
|
+
const range = readDateRange(urlFilters, spec.field);
|
|
172
|
+
for (const [bound, param] of [
|
|
173
|
+
["from", spec.fromParam],
|
|
174
|
+
["to", spec.toParam],
|
|
175
|
+
]) {
|
|
176
|
+
const value = range[bound];
|
|
177
|
+
if (value === "")
|
|
178
|
+
continue;
|
|
179
|
+
const instant = dayBoundInstant(value, bound, timeZone);
|
|
180
|
+
if (instant !== null)
|
|
181
|
+
out[param] = instant;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { FeatureSchema } from "./feature-schema.js";
|
|
3
|
+
export type NavRoute = {
|
|
4
|
+
readonly workspaceId?: string;
|
|
5
|
+
readonly screenId: string;
|
|
6
|
+
readonly entityId?: string;
|
|
7
|
+
};
|
|
8
|
+
export type ScreenTarget = {
|
|
9
|
+
readonly workspaceId?: string;
|
|
10
|
+
readonly screenId: string;
|
|
11
|
+
readonly entityId?: string;
|
|
12
|
+
};
|
|
13
|
+
export type ObjectTarget = {
|
|
14
|
+
readonly workspaceId?: string;
|
|
15
|
+
readonly entity: string;
|
|
16
|
+
readonly id: string;
|
|
17
|
+
};
|
|
18
|
+
export type NavTarget = ScreenTarget | ObjectTarget;
|
|
19
|
+
export type NavApi = {
|
|
20
|
+
/** Current route — `undefined` when the URL is at the root / there's
|
|
21
|
+
* no route selected. Caller's initial fallback kicks in then. */
|
|
22
|
+
readonly route: NavRoute | undefined;
|
|
23
|
+
/** Push a new route. Platform-specific Impl writes to
|
|
24
|
+
* history/stack and notifies subscribers. */
|
|
25
|
+
readonly navigate: (target: NavTarget) => void;
|
|
26
|
+
/** Replace the current route in place. Same effect as navigate from
|
|
27
|
+
* the user's perspective, but doesn't add a history entry — used for
|
|
28
|
+
* mount-time URL fills (e.g. WorkspaceShell defaulting to `/admin/x`
|
|
29
|
+
* when the user typed `/`). Browser Back must take the user out of
|
|
30
|
+
* the app, not back to the original empty path. */
|
|
31
|
+
readonly replace: (target: NavTarget) => void;
|
|
32
|
+
/** Build the href a click on {target} would produce. Used by
|
|
33
|
+
* platform-specific Link-Komponenten (Web: `<a href>`; Native
|
|
34
|
+
* typically doesn't need this). */
|
|
35
|
+
readonly hrefFor: (target: NavTarget) => string;
|
|
36
|
+
/** Lese-Snapshot der aktuellen Search-Params (Browser: ?key=value-
|
|
37
|
+
* Pairs nach dem Pfad). Native-Impls die kein URL-Konzept haben
|
|
38
|
+
* liefern ein leeres Object. Wert ist ein Plain-Record (kein Map)
|
|
39
|
+
* damit React-Subscribers shallow-compare können. */
|
|
40
|
+
readonly searchParams: Readonly<Record<string, string>>;
|
|
41
|
+
/** Mergt Updates in die aktuellen Search-Params. Wert `null` löscht
|
|
42
|
+
* den Key. Ändert NICHT den Pfad. Plattform-Impls nutzen
|
|
43
|
+
* history.replaceState (kein Push) — Sort/Filter-Toggles sollen
|
|
44
|
+
* nicht die Back-Navigation fluten. Native-Impls können no-op'en
|
|
45
|
+
* wenn das Konzept nicht existiert. */
|
|
46
|
+
readonly setSearchParams: (updates: Readonly<Record<string, string | null>>) => void;
|
|
47
|
+
};
|
|
48
|
+
export declare function parsePath(pathname: string, hasWorkspaces?: boolean): NavRoute | undefined;
|
|
49
|
+
export declare function formatPath(target: ScreenTarget): string;
|
|
50
|
+
/** Non-throwing existence check for findDetailForScreen — lets a caller pick
|
|
51
|
+
* a different default (fw#2164's row-click fallback) instead of resolving. */
|
|
52
|
+
export declare function hasDetailScreen(features: readonly FeatureSchema[], entity: string): boolean;
|
|
53
|
+
export declare function resolveTarget(features: readonly FeatureSchema[], target: NavTarget): ScreenTarget;
|
|
54
|
+
export type NavProviderProps = {
|
|
55
|
+
readonly children: ReactNode;
|
|
56
|
+
readonly value: NavApi;
|
|
57
|
+
};
|
|
58
|
+
export declare function NavProvider({ children, value }: NavProviderProps): ReactNode;
|
|
59
|
+
export declare function useNavigateWithInitialValues(): (target: ScreenTarget, initialValues: Readonly<Record<string, unknown>>) => void;
|
|
60
|
+
export declare function useInitialValuesHandoff(screenId: string): Readonly<Record<string, unknown>> | undefined;
|
|
61
|
+
export declare function useNav(): NavApi;
|
package/dist/app/nav.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useCallback, useContext, useEffect, useState } from "react";
|
|
3
|
+
import { lastSegment } from "./qn.js";
|
|
4
|
+
// Pfad-Format:
|
|
5
|
+
// ohne workspaces: `/task-list`, `/task-edit/abc-123`, `/` → undefined
|
|
6
|
+
// mit workspaces: `/admin/task-list`, `/admin/task-edit/abc`,
|
|
7
|
+
// `/admin` → workspace-only (no screen yet),
|
|
8
|
+
// `/` → undefined.
|
|
9
|
+
// Alles nach den ersten 2/3 Segmenten wird ignoriert — kumikos
|
|
10
|
+
// Navigation-Grammatik nested nicht weiter. Nested-Routes wäre eine
|
|
11
|
+
// Spec-Änderung, nicht ein URL-Shape-Unfall.
|
|
12
|
+
export function parsePath(pathname, hasWorkspaces) {
|
|
13
|
+
const parts = pathname.split("/").filter((p) => p !== "");
|
|
14
|
+
if (hasWorkspaces === true) {
|
|
15
|
+
const [workspaceId, screenId, entityId] = parts;
|
|
16
|
+
if (workspaceId === undefined || workspaceId === "")
|
|
17
|
+
return undefined;
|
|
18
|
+
if (screenId === undefined || screenId === "") {
|
|
19
|
+
// Workspace-only URL ("/admin") — caller resolves the default screen
|
|
20
|
+
// for that workspace. We carry workspaceId WITHOUT a screen so the
|
|
21
|
+
// shell can branch instead of making something up.
|
|
22
|
+
return { workspaceId, screenId: "" };
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
workspaceId,
|
|
26
|
+
screenId,
|
|
27
|
+
...(entityId !== undefined && { entityId }),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const [screenId, entityId] = parts;
|
|
31
|
+
if (screenId === undefined || screenId === "")
|
|
32
|
+
return undefined;
|
|
33
|
+
return { screenId, ...(entityId !== undefined && { entityId }) };
|
|
34
|
+
}
|
|
35
|
+
export function formatPath(target) {
|
|
36
|
+
// Workspace-Mode: prefix the workspace short id. Order matters —
|
|
37
|
+
// workspace before screen mirrors parsePath's segment order.
|
|
38
|
+
const segments = [];
|
|
39
|
+
if (target.workspaceId !== undefined)
|
|
40
|
+
segments.push(target.workspaceId);
|
|
41
|
+
segments.push(target.screenId);
|
|
42
|
+
if (target.entityId !== undefined)
|
|
43
|
+
segments.push(target.entityId);
|
|
44
|
+
return `/${segments.join("/")}`;
|
|
45
|
+
}
|
|
46
|
+
// Shared entity→detailFor-screen lookup — resolveTarget's ObjectTarget
|
|
47
|
+
// branch and hasDetailScreen (create-app's row-click default, fw#2164) both
|
|
48
|
+
// need "is there a detail screen for this entity", one to resolve it, the
|
|
49
|
+
// other to just check before falling back to a different default.
|
|
50
|
+
function findDetailForScreen(features, entity) {
|
|
51
|
+
for (const feature of features) {
|
|
52
|
+
for (const screen of feature.screens) {
|
|
53
|
+
if (screen.detailFor === entity)
|
|
54
|
+
return { featureName: feature.featureName, screenId: screen.id };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
/** Non-throwing existence check for findDetailForScreen — lets a caller pick
|
|
60
|
+
* a different default (fw#2164's row-click fallback) instead of resolving. */
|
|
61
|
+
export function hasDetailScreen(features, entity) {
|
|
62
|
+
return findDetailForScreen(features, entity) !== undefined;
|
|
63
|
+
}
|
|
64
|
+
// Resolves a NavTarget to the ScreenTarget form navigate/replace/hrefFor
|
|
65
|
+
// operate on. Pure — no context, no I/O — so callers can use it outside
|
|
66
|
+
// React too (see resolve-at-NavApi-build alternative in renderer-web).
|
|
67
|
+
export function resolveTarget(features, target) {
|
|
68
|
+
if ("screenId" in target)
|
|
69
|
+
return target;
|
|
70
|
+
const found = findDetailForScreen(features, target.entity);
|
|
71
|
+
if (found !== undefined) {
|
|
72
|
+
// Short form (globally unique per validateScreenShortIdCollisions) —
|
|
73
|
+
// ScreenTarget.screenId round-trips through formatPath/parsePath as a
|
|
74
|
+
// short id; qualifying it here double-qualifies at RoutedScreen.
|
|
75
|
+
return {
|
|
76
|
+
...(target.workspaceId !== undefined && { workspaceId: target.workspaceId }),
|
|
77
|
+
screenId: found.screenId,
|
|
78
|
+
entityId: target.id,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
throw new Error(`resolveTarget: no detail screen for entity "${target.entity}". Add detailFor: "${target.entity}" to the screen that shows it.`);
|
|
82
|
+
}
|
|
83
|
+
// Context + Hook. Default ist `undefined` damit fehlender Provider
|
|
84
|
+
// laut kracht statt ein silent-no-op NavApi mit toten navigate()
|
|
85
|
+
// Aufrufen anzubieten.
|
|
86
|
+
const NavContext = createContext(undefined);
|
|
87
|
+
const InitialValuesHandoffContext = createContext(undefined);
|
|
88
|
+
export function NavProvider({ children, value }) {
|
|
89
|
+
const [handoffSlot] = useState(() => ({ current: undefined }));
|
|
90
|
+
const routeScreenId = value.route?.screenId;
|
|
91
|
+
// An offer whose form never mounted (e.g. already on that screen) must not
|
|
92
|
+
// resurface on a later, unrelated visit — drop it once the route moves elsewhere.
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
const pending = handoffSlot.current;
|
|
95
|
+
if (pending === undefined || routeScreenId === undefined)
|
|
96
|
+
return;
|
|
97
|
+
if (lastSegment(routeScreenId) !== pending.screenId)
|
|
98
|
+
handoffSlot.current = undefined;
|
|
99
|
+
}, [handoffSlot, routeScreenId]);
|
|
100
|
+
return (_jsx(NavContext.Provider, { value: value, children: _jsx(InitialValuesHandoffContext.Provider, { value: handoffSlot, children: children }) }));
|
|
101
|
+
}
|
|
102
|
+
// Navigates to a form screen (actionForm, secretMint, entityEdit-create) with
|
|
103
|
+
// initial values carried in memory — never in the URL, so a link cannot
|
|
104
|
+
// forge them and they are not limited to the screen's urlPrefillFields.
|
|
105
|
+
// sensitive and password fields are still never prefilled.
|
|
106
|
+
export function useNavigateWithInitialValues() {
|
|
107
|
+
const nav = useNav();
|
|
108
|
+
const handoffSlot = useContext(InitialValuesHandoffContext);
|
|
109
|
+
return useCallback((target, initialValues) => {
|
|
110
|
+
if (handoffSlot !== undefined) {
|
|
111
|
+
handoffSlot.current = { screenId: lastSegment(target.screenId), values: initialValues };
|
|
112
|
+
}
|
|
113
|
+
nav.navigate(target);
|
|
114
|
+
}, [nav, handoffSlot]);
|
|
115
|
+
}
|
|
116
|
+
export function useInitialValuesHandoff(screenId) {
|
|
117
|
+
const handoffSlot = useContext(InitialValuesHandoffContext);
|
|
118
|
+
const [values] = useState(() => {
|
|
119
|
+
const pending = handoffSlot?.current;
|
|
120
|
+
return pending !== undefined && pending.screenId === lastSegment(screenId)
|
|
121
|
+
? pending.values
|
|
122
|
+
: undefined;
|
|
123
|
+
});
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (handoffSlot !== undefined &&
|
|
126
|
+
values !== undefined &&
|
|
127
|
+
handoffSlot.current?.values === values) {
|
|
128
|
+
handoffSlot.current = undefined;
|
|
129
|
+
}
|
|
130
|
+
}, [handoffSlot, values]);
|
|
131
|
+
return values;
|
|
132
|
+
}
|
|
133
|
+
export function useNav() {
|
|
134
|
+
const api = useContext(NavContext);
|
|
135
|
+
if (api === undefined) {
|
|
136
|
+
throw new Error("useNav: no <NavProvider> mounted above this component. Plattform-Packages (z.B. @cosmicdrift/kumiko-renderer-web) liefern eine Default-Impl über createKumikoApp.");
|
|
137
|
+
}
|
|
138
|
+
return api;
|
|
139
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EditLayout, EntityDefinition, EntityEditScreenDefinition, ProjectionDetailScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
/** Minimale EntityDefinition aus den Layout-Feldern: jedes Feld ein Text-
|
|
3
|
+
* Feld — computeEditViewModel liest nur `fields[<f>].type`, Text reicht für
|
|
4
|
+
* eine reine Anzeige (kein Select/Number-spezifisches Rendering nötig). */
|
|
5
|
+
export declare function synthesizeProjectionDetailEntity(layout: EditLayout): EntityDefinition;
|
|
6
|
+
/** Wandelt ein ProjectionDetailScreenDefinition in die EntityEditScreen-Shape
|
|
7
|
+
* die RenderEdit erwartet — mit jedem Feld hart auf readOnly:true erzwungen. */
|
|
8
|
+
export declare function synthesizeProjectionDetailScreen(screen: ProjectionDetailScreenDefinition): EntityEditScreenDefinition;
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
import { isFieldsEditSection, normalizeEditField, PROJECTION_DETAIL_ENTITY as PROJECTION_DETAIL_PSEUDO_ENTITY, sectionFieldSpecs, } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
23
|
+
/** Minimale EntityDefinition aus den Layout-Feldern: jedes Feld ein Text-
|
|
24
|
+
* Feld — computeEditViewModel liest nur `fields[<f>].type`, Text reicht für
|
|
25
|
+
* eine reine Anzeige (kein Select/Number-spezifisches Rendering nötig). */
|
|
26
|
+
export function synthesizeProjectionDetailEntity(layout) {
|
|
27
|
+
const fields = {};
|
|
28
|
+
for (const section of layout.sections) {
|
|
29
|
+
// relatedList and extension sections carry no `fields` to synthesize.
|
|
30
|
+
if (!isFieldsEditSection(section))
|
|
31
|
+
continue;
|
|
32
|
+
for (const spec of sectionFieldSpecs(section)) {
|
|
33
|
+
fields[normalizeEditField(spec).field] = { type: "text" };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { fields };
|
|
37
|
+
}
|
|
38
|
+
/** Wandelt ein ProjectionDetailScreenDefinition in die EntityEditScreen-Shape
|
|
39
|
+
* die RenderEdit erwartet — mit jedem Feld hart auf readOnly:true erzwungen. */
|
|
40
|
+
export function synthesizeProjectionDetailScreen(screen) {
|
|
41
|
+
const forceReadOnly = (spec) => ({ ...normalizeEditField(spec), readOnly: true });
|
|
42
|
+
const sections = screen.layout.sections.map((section) => {
|
|
43
|
+
// relatedList and extension sections pass through unchanged.
|
|
44
|
+
if (!isFieldsEditSection(section))
|
|
45
|
+
return section;
|
|
46
|
+
return {
|
|
47
|
+
...section,
|
|
48
|
+
// kumiko-lint-ignore section-fields-raw rewrites both sources in place — the groups branch below does the same for groups[].fields
|
|
49
|
+
fields: section.fields.map(forceReadOnly),
|
|
50
|
+
...(section.groups !== undefined && {
|
|
51
|
+
groups: section.groups.map((group) => ({
|
|
52
|
+
...group,
|
|
53
|
+
fields: group.fields.map(forceReadOnly),
|
|
54
|
+
})),
|
|
55
|
+
}),
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
id: screen.id,
|
|
60
|
+
type: "entityEdit",
|
|
61
|
+
entity: PROJECTION_DETAIL_PSEUDO_ENTITY,
|
|
62
|
+
layout: { sections, ...(screen.layout.width !== undefined && { width: screen.layout.width }) },
|
|
63
|
+
allowCreate: false,
|
|
64
|
+
allowDelete: false,
|
|
65
|
+
...(screen.description !== undefined && { description: screen.description }),
|
|
66
|
+
...(screen.fieldLabels !== undefined && { fieldLabels: screen.fieldLabels }),
|
|
67
|
+
...(screen.slots !== undefined && { slots: screen.slots }),
|
|
68
|
+
...(screen.access !== undefined && { access: screen.access }),
|
|
69
|
+
...(screen.fillHeight !== undefined && { fillHeight: screen.fillHeight }),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { EntityDefinition, EntityListScreenDefinition, ListColumnSpec, ProjectionListScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
/** Minimal EntityDefinition from the column list: every field is a text
|
|
3
|
+
* field. `sortable` applies uniformly to all columns — buildAppSchema
|
|
4
|
+
* derives it from the query's Zod schema (`screen.sortable`, fw#2165); the
|
|
5
|
+
* query itself has no per-field server-sort guarantee.
|
|
6
|
+
* computeListViewModel only reads `fields[<col>].type` — text is enough,
|
|
7
|
+
* presentation comes from the column renderer + explicit label. */
|
|
8
|
+
export declare function synthesizeProjectionEntity(columns: readonly ListColumnSpec[], sortable: boolean): EntityDefinition;
|
|
9
|
+
/** Wandelt ein ProjectionListScreenDefinition in die EntityListScreen-Shape die
|
|
10
|
+
* RenderList erwartet. `type` = "entityList" + Pseudo-Entity halten den Type-
|
|
11
|
+
* Constraint; RenderList branched nicht auf `type` und liest `entity` nur für
|
|
12
|
+
* Fehlermeldungen. Die List-Felder werden 1:1 durchgereicht. */
|
|
13
|
+
export declare function synthesizeProjectionScreen(screen: ProjectionListScreenDefinition): EntityListScreenDefinition;
|
|
@@ -13,58 +13,43 @@
|
|
|
13
13
|
// derivedFields) oder cross-referenziert ein Boot-Validator die schema.entities-
|
|
14
14
|
// Map, brechen die Type-Lies hier silent — dann ist Zeit für eine echte
|
|
15
15
|
// query-native ListView-Komponente.
|
|
16
|
-
|
|
17
|
-
import type {
|
|
18
|
-
EntityDefinition,
|
|
19
|
-
EntityListScreenDefinition,
|
|
20
|
-
ListColumnSpec,
|
|
21
|
-
ProjectionListScreenDefinition,
|
|
22
|
-
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
23
16
|
import { normalizeListColumn } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
24
|
-
|
|
25
17
|
const PROJECTION_PSEUDO_ENTITY = "__projection__";
|
|
26
|
-
|
|
27
18
|
/** Minimal EntityDefinition from the column list: every field is a text
|
|
28
19
|
* field. `sortable` applies uniformly to all columns — buildAppSchema
|
|
29
20
|
* derives it from the query's Zod schema (`screen.sortable`, fw#2165); the
|
|
30
21
|
* query itself has no per-field server-sort guarantee.
|
|
31
22
|
* computeListViewModel only reads `fields[<col>].type` — text is enough,
|
|
32
23
|
* presentation comes from the column renderer + explicit label. */
|
|
33
|
-
export function synthesizeProjectionEntity(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
):
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
fields[normalizeListColumn(col).field] = { type: "text", sortable };
|
|
40
|
-
}
|
|
41
|
-
return { fields } as unknown as EntityDefinition;
|
|
24
|
+
export function synthesizeProjectionEntity(columns, sortable) {
|
|
25
|
+
const fields = {};
|
|
26
|
+
for (const col of columns) {
|
|
27
|
+
fields[normalizeListColumn(col).field] = { type: "text", sortable };
|
|
28
|
+
}
|
|
29
|
+
return { fields };
|
|
42
30
|
}
|
|
43
|
-
|
|
44
31
|
/** Wandelt ein ProjectionListScreenDefinition in die EntityListScreen-Shape die
|
|
45
32
|
* RenderList erwartet. `type` = "entityList" + Pseudo-Entity halten den Type-
|
|
46
33
|
* Constraint; RenderList branched nicht auf `type` und liest `entity` nur für
|
|
47
34
|
* Fehlermeldungen. Die List-Felder werden 1:1 durchgereicht. */
|
|
48
|
-
export function synthesizeProjectionScreen(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
...(screen.fillHeight !== undefined && { fillHeight: screen.fillHeight }),
|
|
69
|
-
};
|
|
35
|
+
export function synthesizeProjectionScreen(screen) {
|
|
36
|
+
return {
|
|
37
|
+
id: screen.id,
|
|
38
|
+
type: "entityList",
|
|
39
|
+
entity: PROJECTION_PSEUDO_ENTITY,
|
|
40
|
+
columns: screen.columns,
|
|
41
|
+
...(screen.rowRenderer !== undefined && { rowRenderer: screen.rowRenderer }),
|
|
42
|
+
...(screen.cardRenderer !== undefined && { cardRenderer: screen.cardRenderer }),
|
|
43
|
+
...(screen.rowActions !== undefined && { rowActions: screen.rowActions }),
|
|
44
|
+
...(screen.toolbarActions !== undefined && { toolbarActions: screen.toolbarActions }),
|
|
45
|
+
...(screen.pagination !== undefined && { pagination: screen.pagination }),
|
|
46
|
+
...(screen.pageSize !== undefined && { pageSize: screen.pageSize }),
|
|
47
|
+
...(screen.defaultSort !== undefined && { defaultSort: screen.defaultSort }),
|
|
48
|
+
...(screen.searchable !== undefined && { searchable: screen.searchable }),
|
|
49
|
+
...(screen.createLabel !== undefined && { createLabel: screen.createLabel }),
|
|
50
|
+
...(screen.searchPlaceholder !== undefined && { searchPlaceholder: screen.searchPlaceholder }),
|
|
51
|
+
...(screen.slots !== undefined && { slots: screen.slots }),
|
|
52
|
+
...(screen.access !== undefined && { access: screen.access }),
|
|
53
|
+
...(screen.fillHeight !== undefined && { fillHeight: screen.fillHeight }),
|
|
54
|
+
};
|
|
70
55
|
}
|
package/dist/app/qn.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function lastSegment(qn: string): string;
|
|
2
|
+
/**
|
|
3
|
+
* Client-safe copy of framework `toKebab` (engine/qualified-name.ts).
|
|
4
|
+
* Must stay in sync — do NOT import from `@cosmicdrift/kumiko-framework/engine`
|
|
5
|
+
* here: that barrel pulls server deps into the browser bundle.
|
|
6
|
+
*/
|
|
7
|
+
export declare function toKebab(input: string): string;
|