@cosmicdrift/kumiko-renderer 0.333.0 → 0.335.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/config-edit-shim.d.ts +7 -0
- package/dist/app/config-edit-shim.js +39 -0
- package/dist/app/content-preview.js +7 -9
- package/dist/app/extension-sections.d.ts +6 -0
- package/dist/app/form-schema.d.ts +2 -1
- package/dist/app/form-schema.js +11 -2
- package/dist/app/kumiko-screen.d.ts +5 -0
- package/dist/app/kumiko-screen.js +259 -101
- package/dist/app/nav.d.ts +5 -0
- package/dist/app/nav.js +17 -4
- package/dist/app/query-state-banners.d.ts +8 -0
- package/dist/app/query-state-banners.js +14 -0
- package/dist/app/reference-facet-bridge.js +8 -1
- package/dist/app/row-actions.d.ts +12 -0
- package/dist/app/row-actions.js +24 -7
- package/dist/app/screen-access.js +13 -9
- package/dist/app/secret-mint-body.js +41 -7
- package/dist/app/secrets-edit-body.js +89 -33
- package/dist/components/embedded-list-field.js +2 -4
- package/dist/components/grid-cell-for-field.d.ts +7 -2
- package/dist/components/grid-cell-for-field.js +2 -2
- package/dist/components/multi-select-checkboxes.js +4 -1
- package/dist/components/query-options-select.d.ts +12 -3
- package/dist/components/query-options-select.js +45 -2
- package/dist/components/reference-create-dialog.js +9 -4
- package/dist/components/related-list-section.js +8 -10
- package/dist/components/render-edit-action-button.d.ts +1 -1
- package/dist/components/render-edit-action-button.js +5 -1
- package/dist/components/render-edit-logic.d.ts +2 -1
- package/dist/components/render-edit-logic.js +8 -5
- package/dist/components/render-edit-types.d.ts +30 -1
- package/dist/components/render-edit.js +248 -76
- package/dist/components/render-field.d.ts +7 -1
- package/dist/components/render-field.js +40 -15
- package/dist/components/render-list.js +6 -6
- package/dist/components/write-form-section.d.ts +5 -1
- package/dist/components/write-form-section.js +29 -25
- package/dist/context/embedded-screen-context.d.ts +5 -0
- package/dist/context/embedded-screen-context.js +12 -0
- package/dist/hooks/use-reference-lookup.d.ts +7 -0
- package/dist/hooks/use-reference-lookup.js +10 -0
- package/dist/i18n-defaults.js +30 -5
- package/dist/i18n.js +8 -4
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -0
- package/dist/primitives.d.ts +70 -19
- package/dist/session/session-ended.d.ts +16 -0
- package/dist/session/session-ended.js +73 -0
- package/dist/sort-by-accessor.d.ts +6 -2
- package/dist/sort-by-accessor.js +13 -3
- package/package.json +5 -5
- package/src/changes.json +40 -0
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { evalFieldCondition, isWriteFormEditSection, metricField, parseRefTarget, } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
3
|
-
import { computeRelatedListSectionViewModel, fieldLabelKey, fieldOptionLabelKey, isSafeHref, } from "@cosmicdrift/kumiko-headless";
|
|
3
|
+
import { buildOptionLabels, computeRelatedListSectionViewModel, fieldLabelKey, fieldOptionLabelKey, isSafeHref, } from "@cosmicdrift/kumiko-headless";
|
|
4
4
|
import { resolveActionIcon } from "@cosmicdrift/kumiko-types/action-icon";
|
|
5
5
|
import { TENANT_CURRENCY_CONFIG_KEY } from "@cosmicdrift/kumiko-types/fields";
|
|
6
6
|
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
7
|
+
import { QueryOptionLabel, resolveOptionsQueryPayload, } from "../components/query-options-select.js";
|
|
7
8
|
import { extractCreatedId, extractIdField } from "../components/reference-create-dialog.js";
|
|
8
9
|
import { RenderEdit, resolveRecordTitle, } from "../components/render-edit.js";
|
|
9
10
|
import { needsActionConfirm, RenderEditActionButton, RenderEditActionConfirmDialog, } from "../components/render-edit-action-button.js";
|
|
10
11
|
import { RenderList } from "../components/render-list.js";
|
|
11
12
|
import { useDispatcher, useOptionalDispatcher } from "../context/dispatcher-context.js";
|
|
13
|
+
import { useIsEmbeddedScreen } from "../context/embedded-screen-context.js";
|
|
12
14
|
import { useUserRoles } from "../context/user-roles-context.js";
|
|
13
15
|
import { PAGE_SIZE_OPTIONS, useListUrlState } from "../hooks/use-list-url-state.js";
|
|
14
16
|
import { useQuery } from "../hooks/use-query.js";
|
|
@@ -19,7 +21,7 @@ import { statusToneForOptionTone, statusToneForValue, usePrimitives, } from "../
|
|
|
19
21
|
import { screenFillsHeight } from "../screen-fills-height.js";
|
|
20
22
|
import { synthesizeActionFormEntity, synthesizeActionFormScreen } from "./action-form-shim.js";
|
|
21
23
|
import { useAppFeatures } from "./app-features-context.js";
|
|
22
|
-
import { synthesizeConfigEditEntity, synthesizeConfigEditScreen } from "./config-edit-shim.js";
|
|
24
|
+
import { buildConfigEditSchema, synthesizeConfigEditEntity, synthesizeConfigEditScreen, } from "./config-edit-shim.js";
|
|
23
25
|
import { useCustomScreenComponent } from "./custom-screens.js";
|
|
24
26
|
import { useDashboardBody } from "./dashboard-body.js";
|
|
25
27
|
import { EntityListExpandedRow } from "./entity-list-expanded-row.js";
|
|
@@ -32,9 +34,10 @@ import { synthesizeProjectionDetailEntity, synthesizeProjectionDetailScreen, } f
|
|
|
32
34
|
import { synthesizeProjectionEntity, synthesizeProjectionScreen } from "./projection-list-shim.js";
|
|
33
35
|
import { lastSegment, toKebab } from "./qn.js";
|
|
34
36
|
import { featureNameFromQualifiedScreenId, qualifyScreenId } from "./qualify-screen-id.js";
|
|
37
|
+
import { QueryErrorBanner, QueryLoadingBanner } from "./query-state-banners.js";
|
|
35
38
|
import { ReferenceFacetBridges } from "./reference-facet-bridge.js";
|
|
36
39
|
import { navigateToReturn, navigateToReturnOr, navigateWithReturnTo, ReturnHostProvider, useReturnHost, useReturnTarget, } from "./return-to.js";
|
|
37
|
-
import { buildDefaultEditRowAction, buildProjectionRowActions, buildProjectionToolbarActions, buildRecordActions, evalRowExtractor, isWriteHandlerRowAction, navigateActionSearchParams, refetchAfterWrite, runProjectionRowNavigate, } from "./row-actions.js";
|
|
40
|
+
import { buildDefaultEditRowAction, buildDrawerRowAction, buildProjectionRowActions, buildProjectionToolbarActions, buildRecordActions, evalRowExtractor, isWriteHandlerRowAction, navigateActionSearchParams, refetchAfterWrite, runProjectionRowNavigate, } from "./row-actions.js";
|
|
38
41
|
import { findEditScreenFor, navigateTargetAllows, screenAccessAllows } from "./screen-access.js";
|
|
39
42
|
import { SecretMintBody } from "./secret-mint-body.js";
|
|
40
43
|
import { SecretsEditBody } from "./secrets-edit-body.js";
|
|
@@ -132,16 +135,19 @@ function entityWriteCommand(featureName, entity, verb) {
|
|
|
132
135
|
// entityList-Screen für die Entity und navigiert dahin. Wird von
|
|
133
136
|
// Create/Update/Delete genauso verwendet — alle drei haben "fertig
|
|
134
137
|
// editiert, raus hier" als sinnvolles Default-Verhalten.
|
|
138
|
+
function findEntityListScreen(schema, entityName) {
|
|
139
|
+
return schema.screens.find((s) => s.type === "entityList" && s.entity === entityName);
|
|
140
|
+
}
|
|
135
141
|
function useNavigateToListAfter(schema, entityName) {
|
|
136
142
|
const nav = useNav();
|
|
137
143
|
return useCallback(() => {
|
|
138
|
-
const list = schema
|
|
144
|
+
const list = findEntityListScreen(schema, entityName);
|
|
139
145
|
if (!list)
|
|
140
146
|
return;
|
|
141
147
|
// schema.screens.id ist QN-form (registry-stamped); nav.navigate
|
|
142
148
|
// erwartet Short-Form. Sonst landet die URL doppelt-qualifiziert.
|
|
143
149
|
nav.navigate({ screenId: lastSegment(list.id) });
|
|
144
|
-
}, [nav, schema
|
|
150
|
+
}, [nav, schema, entityName]);
|
|
145
151
|
}
|
|
146
152
|
// Default "+ Neu"-Navigation für die List-Toolbar: findet den ersten
|
|
147
153
|
// entityEdit-Screen ohne entityId-Anhang und navigiert dorthin.
|
|
@@ -220,7 +226,7 @@ export function buildInitialValues(fields, defaultCurrency, moneyCurrencyOverrid
|
|
|
220
226
|
// client-safe FieldDefinition subset, not the concrete MoneyFieldDef/
|
|
221
227
|
// MoneyCurrencySource types, same idiom as the other field-shape narrowings
|
|
222
228
|
// in this file (PrefillFieldShape etc.).
|
|
223
|
-
function tenantCurrencyMoneyFieldNames(fields) {
|
|
229
|
+
export function tenantCurrencyMoneyFieldNames(fields) {
|
|
224
230
|
const names = [];
|
|
225
231
|
for (const [name, def] of Object.entries(fields)) {
|
|
226
232
|
const shape = def;
|
|
@@ -248,6 +254,7 @@ function resolveTenantCurrency(fieldNames, query, fallback) {
|
|
|
248
254
|
// so a tenant-declared field whose config query errors or returns no value
|
|
249
255
|
// lands here — same last resort as the entityEdit path's `?? "EUR"`.
|
|
250
256
|
const ACTION_FORM_CURRENCY_FALLBACK = "EUR";
|
|
257
|
+
const HEADER_CARD_DEFAULT_WIDTH = "4xl";
|
|
251
258
|
// Shared by the update payload shape and the server-error path prefix so both can't drift.
|
|
252
259
|
const UPDATE_CHANGES_KEY = "changes";
|
|
253
260
|
// A money field naming a fixed ISO code (`currency: { kind: "literal", code }`,
|
|
@@ -271,7 +278,7 @@ export function literalCurrencyOverrides(fields) {
|
|
|
271
278
|
// on a fast click submit, the wrong currency. Callers pass the tenant field
|
|
272
279
|
// names themselves because an edit form only needs the fetch for fields the
|
|
273
280
|
// record has no stored value for.
|
|
274
|
-
function useMoneyCurrencyOverrides(fields, tenantFieldNames, fallback) {
|
|
281
|
+
export function useMoneyCurrencyOverrides(fields, tenantFieldNames, fallback) {
|
|
275
282
|
const query = useQuery("config:query:values", {}, { enabled: tenantFieldNames.length > 0 });
|
|
276
283
|
const resolution = resolveTenantCurrency(tenantFieldNames, query, fallback);
|
|
277
284
|
const resolvedTenantCurrency = resolution.status === "ready" ? resolution.currency : undefined;
|
|
@@ -478,7 +485,11 @@ export function mergeSearchParamsIntoInitial(fields, sources) {
|
|
|
478
485
|
continue;
|
|
479
486
|
if (shape.format === "password")
|
|
480
487
|
continue;
|
|
481
|
-
|
|
488
|
+
// An `undefined` override (row-extractor key whose row lacks the field)
|
|
489
|
+
// must not wipe the field default.
|
|
490
|
+
if (drawerOverrides !== undefined &&
|
|
491
|
+
Object.hasOwn(drawerOverrides, name) &&
|
|
492
|
+
drawerOverrides[name] !== undefined) {
|
|
482
493
|
merged[name] = drawerOverrides[name];
|
|
483
494
|
continue;
|
|
484
495
|
}
|
|
@@ -659,7 +670,7 @@ function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onRelo
|
|
|
659
670
|
}
|
|
660
671
|
return out;
|
|
661
672
|
}, [entity.fields, entityDefaultCurrency, moneyCurrencyOverrides, record]);
|
|
662
|
-
const formSchema = useMemo(() => buildFormSchema(entity, screen), [entity, screen]);
|
|
673
|
+
const formSchema = useMemo(() => buildFormSchema(entity, screen, initial), [entity, screen, initial]);
|
|
663
674
|
// Extension-Werte (z.B. customFields-jsonb) an extension-sections geben,
|
|
664
675
|
// damit sie beim Edit den Bestand zeigen statt write-only zu sein.
|
|
665
676
|
const extensionInitialValues = useMemo(() => {
|
|
@@ -692,6 +703,24 @@ function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onRelo
|
|
|
692
703
|
// pattern as ProjectionDetailBody.headerActions above (the edited record
|
|
693
704
|
// stands in for the "row"), minus the cross-feature defaultEditAction
|
|
694
705
|
// lookup that doesn't apply here (this screen already IS the edit form).
|
|
706
|
+
const handleRecordLeft = useCallback((action, resultData, deletedShownRecord) => {
|
|
707
|
+
// Never return onto the just-deleted record.
|
|
708
|
+
const target = deletedShownRecord && returnTarget?.entityId === entityId ? undefined : returnTarget;
|
|
709
|
+
if (action.redirect !== undefined) {
|
|
710
|
+
navigateAfterRedirect({
|
|
711
|
+
nav,
|
|
712
|
+
schema,
|
|
713
|
+
appFeatures,
|
|
714
|
+
returnTarget: target,
|
|
715
|
+
redirect: action.redirect,
|
|
716
|
+
resultData,
|
|
717
|
+
record,
|
|
718
|
+
});
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
navigateToReturnOr(nav, target, navigateToList);
|
|
722
|
+
onDeleted?.();
|
|
723
|
+
}, [nav, schema, appFeatures, returnTarget, record, entityId, navigateToList, onDeleted]);
|
|
695
724
|
const buildSectionActions = useCallback((actions) => buildRecordActions({
|
|
696
725
|
actions,
|
|
697
726
|
record,
|
|
@@ -701,33 +730,38 @@ function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onRelo
|
|
|
701
730
|
dispatcher,
|
|
702
731
|
openDrawer,
|
|
703
732
|
onWriteSuccess: onReload,
|
|
733
|
+
onRecordLeft: handleRecordLeft,
|
|
704
734
|
defaultScreenTargetEntityId: entityId,
|
|
705
|
-
|
|
735
|
+
defaultWritePayloadId: entityId,
|
|
736
|
+
}), [
|
|
737
|
+
record,
|
|
738
|
+
effectiveTranslate,
|
|
739
|
+
nav,
|
|
740
|
+
host,
|
|
741
|
+
dispatcher,
|
|
742
|
+
openDrawer,
|
|
743
|
+
onReload,
|
|
744
|
+
handleRecordLeft,
|
|
745
|
+
entityId,
|
|
746
|
+
]);
|
|
706
747
|
const headerActions = useMemo(() => buildSectionActions(screen.actions ?? []), [buildSectionActions, screen.actions]);
|
|
707
748
|
const handleSubmitted = useCallback((result) => {
|
|
708
749
|
if (!result.isSuccess)
|
|
709
750
|
return;
|
|
710
751
|
const redirect = screen.redirect;
|
|
711
752
|
if (redirect !== undefined) {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
}
|
|
724
|
-
else {
|
|
725
|
-
const { screenId, entityId: targetEntityId } = resolveRedirectTarget(redirect, result.data, schema, appFeatures, record);
|
|
726
|
-
nav.navigate({
|
|
727
|
-
screenId,
|
|
728
|
-
...(targetEntityId !== undefined && { entityId: targetEntityId }),
|
|
729
|
-
});
|
|
730
|
-
}
|
|
753
|
+
// The update handler's success payload usually reports only this
|
|
754
|
+
// record's own id (event-store-executor-write.ts), so a parent FK
|
|
755
|
+
// named by `idFrom` falls back to the already-loaded `record`.
|
|
756
|
+
navigateAfterRedirect({
|
|
757
|
+
nav,
|
|
758
|
+
schema,
|
|
759
|
+
appFeatures,
|
|
760
|
+
returnTarget,
|
|
761
|
+
redirect,
|
|
762
|
+
resultData: result.data,
|
|
763
|
+
record: overlayUpdatedProjection(record, result.data),
|
|
764
|
+
});
|
|
731
765
|
}
|
|
732
766
|
else {
|
|
733
767
|
navigateToReturnOr(nav, returnTarget, navigateToList);
|
|
@@ -967,6 +1001,7 @@ function EntityListScreen({ schema, screen, translate, onRowClick, }) {
|
|
|
967
1001
|
return (_jsx(EntityListBody, { schema: schema, screen: screen, entity: entity, ...(translate !== undefined && { translate }), ...(onRowClick !== undefined && { onRowClick }) }));
|
|
968
1002
|
}
|
|
969
1003
|
function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
1004
|
+
const isEmbeddedScreen = useIsEmbeddedScreen();
|
|
970
1005
|
const featureName = schema.featureName;
|
|
971
1006
|
const onCreate = useNavigateToCreateFor(schema, screen.entity, screen.createScreen);
|
|
972
1007
|
const { Banner } = usePrimitives();
|
|
@@ -1232,26 +1267,7 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
1232
1267
|
};
|
|
1233
1268
|
}
|
|
1234
1269
|
if (action.kind === "drawer") {
|
|
1235
|
-
|
|
1236
|
-
const actionVisible = action.visible;
|
|
1237
|
-
const actionIcon = resolveActionIcon(action.id, action.icon);
|
|
1238
|
-
return {
|
|
1239
|
-
id: action.id,
|
|
1240
|
-
label: effectiveTranslate(action.label),
|
|
1241
|
-
...(action.style !== undefined && { style: action.style }),
|
|
1242
|
-
...(action.display !== undefined && { display: action.display }),
|
|
1243
|
-
confirmRequired: false,
|
|
1244
|
-
...(actionIcon !== undefined && { icon: actionIcon }),
|
|
1245
|
-
onTrigger: (row) => {
|
|
1246
|
-
const initialValues = drawerAction.params !== undefined
|
|
1247
|
-
? evalRowExtractor(drawerAction.params, row.values)
|
|
1248
|
-
: undefined;
|
|
1249
|
-
openDrawer(drawerAction, initialValues);
|
|
1250
|
-
},
|
|
1251
|
-
...(actionVisible !== undefined && {
|
|
1252
|
-
isVisible: (row) => evalFieldCondition(actionVisible, row.values),
|
|
1253
|
-
}),
|
|
1254
|
-
};
|
|
1270
|
+
return buildDrawerRowAction(action, effectiveTranslate, openDrawer);
|
|
1255
1271
|
}
|
|
1256
1272
|
if (dispatcher === undefined)
|
|
1257
1273
|
return null;
|
|
@@ -1392,7 +1408,9 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
1392
1408
|
// Bei Infinite-Mode: rows kommen aus accumulated (über mehrere
|
|
1393
1409
|
// useQuery-Calls gesammelt), bei pages/false aus dem aktuellen Result.
|
|
1394
1410
|
const renderRows = useInfinite ? accumulated : (rowsQuery.data?.rows ?? []);
|
|
1395
|
-
return (_jsxs(_Fragment, { children: [_jsx(ReferenceFacetBridges, { specs: entityFacetSpecs, onOptions: handleEntityFacetOptions }), _jsx(FacetCountBridges, { queryType: queryType, queries: facetCountQueries, onCount: handleFacetCount }), _jsx(RenderList, { screen: screen, entity: entity, rows: renderRows, featureName: featureName, searchable: searchable, searchValue: urlState.q, onSearchChange: urlState.setQ, sort: effectiveSort, onSortChange: urlState.setSort,
|
|
1411
|
+
return (_jsxs(_Fragment, { children: [_jsx(ReferenceFacetBridges, { specs: entityFacetSpecs, onOptions: handleEntityFacetOptions }), _jsx(FacetCountBridges, { queryType: queryType, queries: facetCountQueries, onCount: handleFacetCount }), _jsx(RenderList, { screen: screen, entity: entity, rows: renderRows, featureName: featureName, searchable: searchable, searchValue: urlState.q, onSearchChange: urlState.setQ, sort: effectiveSort, onSortChange: urlState.setSort, ...(isEmbeddedScreen
|
|
1412
|
+
? { screenPadding: false, chromeless: true }
|
|
1413
|
+
: { screenPadding: true }), ...(screenFillsHeight(screen) && { scrollBody: true }), ...(pager !== undefined && { pager }), ...(rowActions !== undefined && { rowActions }), ...(screen.rowActionMode !== undefined && { rowActionMode: screen.rowActionMode }), ...(toolbarActions !== undefined && toolbarActions.length > 0 && { toolbarActions }), ...(useInfinite && {
|
|
1396
1414
|
onReachEnd: loadMore,
|
|
1397
1415
|
loadingMore: rowsQuery.loading,
|
|
1398
1416
|
hasMore,
|
|
@@ -1420,6 +1438,7 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
1420
1438
|
// declared here. Pages-mode pagination only — infinite-scroll accumulation
|
|
1421
1439
|
// isn't wired for projectionList.
|
|
1422
1440
|
function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
|
|
1441
|
+
const isEmbeddedScreen = useIsEmbeddedScreen();
|
|
1423
1442
|
const { Banner, Text } = usePrimitives();
|
|
1424
1443
|
const t = useTranslation();
|
|
1425
1444
|
const nav = useNav();
|
|
@@ -1591,7 +1610,9 @@ function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
|
|
|
1591
1610
|
onPageSizeChange: urlState.setPageSize,
|
|
1592
1611
|
}
|
|
1593
1612
|
: undefined;
|
|
1594
|
-
return (_jsxs(_Fragment, { children: [_jsx(ReferenceFacetBridges, { specs: facetSpecs, onOptions: handleProjectionFacetOptions }), _jsx(RenderList, { screen: listScreen, entity: entity, rows: rowsQuery.data?.rows ?? [], featureName: schema.featureName, searchable: searchable, searchValue: urlState.q, onSearchChange: urlState.setQ, sort: activeSort, onSortChange: urlState.setSort,
|
|
1613
|
+
return (_jsxs(_Fragment, { children: [_jsx(ReferenceFacetBridges, { specs: facetSpecs, onOptions: handleProjectionFacetOptions }), _jsx(RenderList, { screen: listScreen, entity: entity, rows: rowsQuery.data?.rows ?? [], featureName: schema.featureName, searchable: searchable, searchValue: urlState.q, onSearchChange: urlState.setQ, sort: activeSort, onSortChange: urlState.setSort, ...(isEmbeddedScreen
|
|
1614
|
+
? { screenPadding: false, chromeless: true }
|
|
1615
|
+
: { screenPadding: true }), ...(screenFillsHeight(listScreen) && { scrollBody: true }), ...(pager !== undefined && { pager }), ...(rowActions !== undefined && { rowActions }), ...(screen.rowActionMode !== undefined && { rowActionMode: screen.rowActionMode }), ...(toolbarActions !== undefined && { toolbarActions }), ...(translate !== undefined && { translate }), ...(wrappedOnRowClick !== undefined && { onRowClick: wrappedOnRowClick }), ...(filterFacets.length > 0 && {
|
|
1595
1616
|
filterFacets,
|
|
1596
1617
|
onFilterChange: urlState.setFilter,
|
|
1597
1618
|
}), ...(dateRangeFacets.length > 0 && {
|
|
@@ -1831,7 +1852,7 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
1831
1852
|
// useQuery call re-runs too (it has no `live` subscription of its own).
|
|
1832
1853
|
const [reloadNonce, setReloadNonce] = useState(0);
|
|
1833
1854
|
const reloadDetail = useCallback(async () => {
|
|
1834
|
-
await detailQuery.refetch
|
|
1855
|
+
await refetchAfterWrite(detailQuery.refetch);
|
|
1835
1856
|
setReloadNonce((n) => n + 1);
|
|
1836
1857
|
}, [detailQuery.refetch]);
|
|
1837
1858
|
// Header actions render directly via RenderEditActionButton (fw#2713,
|
|
@@ -1850,7 +1871,49 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
1850
1871
|
const userRoles = useUserRoles();
|
|
1851
1872
|
const dispatcher = useOptionalDispatcher();
|
|
1852
1873
|
const host = useReturnHost();
|
|
1874
|
+
const returnTargetParam = useReturnTarget(screen.id);
|
|
1875
|
+
// A singleton has no record route to come back to — same as entityEdit.
|
|
1876
|
+
const returnTarget = screen.singleton === true ? undefined : returnTargetParam;
|
|
1853
1877
|
const { drawerAction, drawerScreen, drawerInitialValues, openDrawer, closeDrawer } = useDrawerAction(schema);
|
|
1878
|
+
const handleRecordLeft = useCallback((action, resultData, deletedShownRecord) => {
|
|
1879
|
+
const record = detailQuery.data ?? {};
|
|
1880
|
+
// Never return onto the just-deleted record.
|
|
1881
|
+
const target = deletedShownRecord && returnTarget?.entityId === effectiveEntityId
|
|
1882
|
+
? undefined
|
|
1883
|
+
: returnTarget;
|
|
1884
|
+
if (action.redirect !== undefined) {
|
|
1885
|
+
navigateAfterRedirect({
|
|
1886
|
+
nav,
|
|
1887
|
+
schema,
|
|
1888
|
+
appFeatures,
|
|
1889
|
+
returnTarget: target,
|
|
1890
|
+
redirect: action.redirect,
|
|
1891
|
+
resultData,
|
|
1892
|
+
record,
|
|
1893
|
+
});
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
navigateToReturnOr(nav, target, () => {
|
|
1897
|
+
const listScreen = screen.listScreenId ??
|
|
1898
|
+
(screen.detailFor !== undefined
|
|
1899
|
+
? findEntityListScreen(schema, screen.detailFor)?.id
|
|
1900
|
+
: undefined);
|
|
1901
|
+
if (listScreen !== undefined)
|
|
1902
|
+
nav.navigate({ screenId: lastSegment(listScreen) });
|
|
1903
|
+
else
|
|
1904
|
+
void detailQuery.refetch();
|
|
1905
|
+
});
|
|
1906
|
+
}, [
|
|
1907
|
+
detailQuery.data,
|
|
1908
|
+
detailQuery.refetch,
|
|
1909
|
+
nav,
|
|
1910
|
+
schema,
|
|
1911
|
+
appFeatures,
|
|
1912
|
+
returnTarget,
|
|
1913
|
+
effectiveEntityId,
|
|
1914
|
+
screen.listScreenId,
|
|
1915
|
+
screen.detailFor,
|
|
1916
|
+
]);
|
|
1854
1917
|
const editScreen = useMemo(() => {
|
|
1855
1918
|
const detailFor = screen.detailFor;
|
|
1856
1919
|
if (detailFor === undefined)
|
|
@@ -1898,6 +1961,7 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
1898
1961
|
dispatcher,
|
|
1899
1962
|
openDrawer,
|
|
1900
1963
|
onWriteSuccess: detailQuery.refetch,
|
|
1964
|
+
onRecordLeft: handleRecordLeft,
|
|
1901
1965
|
sameEntityScreenId,
|
|
1902
1966
|
}), [
|
|
1903
1967
|
detailQuery.data,
|
|
@@ -1907,6 +1971,7 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
1907
1971
|
host,
|
|
1908
1972
|
dispatcher,
|
|
1909
1973
|
openDrawer,
|
|
1974
|
+
handleRecordLeft,
|
|
1910
1975
|
sameEntityScreenId,
|
|
1911
1976
|
]);
|
|
1912
1977
|
const headerActions = useMemo(() => {
|
|
@@ -1920,6 +1985,7 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
1920
1985
|
dispatcher,
|
|
1921
1986
|
openDrawer,
|
|
1922
1987
|
onWriteSuccess: detailQuery.refetch,
|
|
1988
|
+
onRecordLeft: handleRecordLeft,
|
|
1923
1989
|
sameEntityScreenId,
|
|
1924
1990
|
});
|
|
1925
1991
|
if (defaultEditAction === undefined || declaredHasEdit)
|
|
@@ -1935,8 +2001,25 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
1935
2001
|
detailQuery.data,
|
|
1936
2002
|
detailQuery.refetch,
|
|
1937
2003
|
openDrawer,
|
|
2004
|
+
handleRecordLeft,
|
|
1938
2005
|
sameEntityScreenId,
|
|
1939
2006
|
]);
|
|
2007
|
+
const hasHeader = screen.header !== undefined;
|
|
2008
|
+
const hasMetrics = screen.metrics !== undefined && screen.metrics.length > 0;
|
|
2009
|
+
// ?? [] rather than threading `headerActions !== undefined` through every
|
|
2010
|
+
// use below — an empty array is a safe no-op for .map/.length.
|
|
2011
|
+
const headerActionsList = headerActions ?? [];
|
|
2012
|
+
const hasHeaderActions = headerActionsList.length > 0;
|
|
2013
|
+
const hasHeaderCard = hasHeader || hasMetrics || hasHeaderActions;
|
|
2014
|
+
// A header card (metric band, status, actions) needs more room than the
|
|
2015
|
+
// 640px form column; screens with only a record title keep the narrow one.
|
|
2016
|
+
const widensScreenFormColumn = hasHeaderCard &&
|
|
2017
|
+
!hasTabs &&
|
|
2018
|
+
screenFillsHeight(screen) &&
|
|
2019
|
+
detailScreen.layout.width === undefined;
|
|
2020
|
+
const renderedDetailScreen = useMemo(() => widensScreenFormColumn
|
|
2021
|
+
? { ...detailScreen, layout: { ...detailScreen.layout, width: HEADER_CARD_DEFAULT_WIDTH } }
|
|
2022
|
+
: detailScreen, [detailScreen, widensScreenFormColumn]);
|
|
1940
2023
|
if (effectiveEntityId === undefined && screen.singleton !== true) {
|
|
1941
2024
|
return (_jsxs(Banner, { padded: true, variant: "error", testId: "kumiko-screen-projection-detail-missing-id", children: ["Screen ", _jsx(Text, { variant: "code", children: screen.id }), " (projectionDetail) needs a row id in the path \u2014 open it from a row action."] }));
|
|
1942
2025
|
}
|
|
@@ -1950,18 +2033,11 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
1950
2033
|
if (!record) {
|
|
1951
2034
|
return (_jsx(Banner, { padded: true, variant: "error", testId: "kumiko-screen-record-missing", children: screen.singleton === true ? ("Record not found.") : (_jsxs(_Fragment, { children: ["Record ", _jsx(Text, { variant: "code", children: entityId }), " not found."] })) }));
|
|
1952
2035
|
}
|
|
1953
|
-
const hasHeader = screen.header !== undefined;
|
|
1954
|
-
const hasMetrics = screen.metrics !== undefined && screen.metrics.length > 0;
|
|
1955
|
-
// ?? [] rather than threading `headerActions !== undefined` through every
|
|
1956
|
-
// use below — an empty array is a safe no-op for .map/.length.
|
|
1957
|
-
const headerActionsList = headerActions ?? [];
|
|
1958
|
-
const hasHeaderActions = headerActionsList.length > 0;
|
|
1959
2036
|
// Grouped into the head Card alongside title/status/metrics (fw#2713):
|
|
1960
2037
|
// these are actions on the record the head shows, not on whichever tab is
|
|
1961
2038
|
// open, so they must stay in place across tab switches instead of
|
|
1962
2039
|
// trailing the active tab's content in the card footer.
|
|
1963
2040
|
const headerActionsContent = hasHeaderActions && (_jsx(Grid, { columns: "auto", testId: "kumiko-screen-projection-detail-actions", children: _jsx(HeaderActionsBar, { actions: headerActionsList, Button: Button, Dialog: Dialog, ActionOverflowMenu: ActionOverflowMenu, onError: setActionError }) }));
|
|
1964
|
-
const hasHeaderCard = hasHeader || hasMetrics || hasHeaderActions;
|
|
1965
2041
|
// Rendered as RenderEdit's headerRegion (not as JSX siblings before it) so
|
|
1966
2042
|
// this shares the same page padding/width as the record's edit card below
|
|
1967
2043
|
// it, instead of sitting flush against the screen edge (fw record-screen
|
|
@@ -2023,7 +2099,7 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
2023
2099
|
...(count !== undefined && { count }),
|
|
2024
2100
|
};
|
|
2025
2101
|
}), activeId: activeSection.id ?? "", onSelect: (id) => nav.setSearchParams({ tab: id }) }))] }));
|
|
2026
|
-
return (_jsxs(_Fragment, { children: [_jsx(RenderEdit, { screen:
|
|
2102
|
+
return (_jsxs(_Fragment, { children: [_jsx(RenderEdit, { screen: renderedDetailScreen, entity: entity, featureName: schema.featureName, initial: record, entityId: effectiveEntityId, customSubmit: async () => ({ isSuccess: true, validationBlocked: false, data: undefined }), onReload: reloadDetail, onRelatedListDrawerAction: openDrawer, ...(translate !== undefined && { translate }), ...(hasTabs && { hideSectionTitles: true }), ...(screenFillsHeight(screen) && { fillScreenHeight: true }), ...((hasHeaderCard ||
|
|
2027
2103
|
hasTabs ||
|
|
2028
2104
|
screen.slots?.header !== undefined ||
|
|
2029
2105
|
screen.recordTitleField !== undefined) && {
|
|
@@ -2037,18 +2113,24 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
2037
2113
|
function redirectScreenTarget(redirect) {
|
|
2038
2114
|
return typeof redirect === "string" ? redirect : redirect.screen;
|
|
2039
2115
|
}
|
|
2040
|
-
//
|
|
2041
|
-
//
|
|
2116
|
+
// A qualified redirect names its feature, so the search space is THAT feature
|
|
2117
|
+
// only — the current schema's own screens must not shadow it with a same
|
|
2118
|
+
// short-id screen of another type. An unqualified redirect searches this
|
|
2119
|
+
// schema first (own screen wins), then every mounted feature. A named feature
|
|
2120
|
+
// that isn't mounted (no provider / isolated test) falls back to this schema.
|
|
2042
2121
|
function findRedirectTargetScreen(redirect, schema, appFeatures) {
|
|
2043
2122
|
const redirectScreen = redirectScreenTarget(redirect);
|
|
2044
2123
|
const targetId = lastSegment(redirectScreen);
|
|
2045
2124
|
const targetFeatureName = featureNameFromQualifiedScreenId(redirectScreen);
|
|
2046
|
-
|
|
2047
|
-
(
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2125
|
+
const targetFeature = targetFeatureName !== undefined
|
|
2126
|
+
? appFeatures.find((f) => f.featureName === targetFeatureName)
|
|
2127
|
+
: undefined;
|
|
2128
|
+
const searchSpace = targetFeature !== undefined
|
|
2129
|
+
? targetFeature.screens
|
|
2130
|
+
: targetFeatureName === undefined
|
|
2131
|
+
? [...schema.screens, ...appFeatures.flatMap((f) => f.screens)]
|
|
2132
|
+
: schema.screens;
|
|
2133
|
+
return searchSpace.find((s) => lastSegment(s.id) === targetId);
|
|
2052
2134
|
}
|
|
2053
2135
|
// A redirect to a record screen ("show the result") is a deliberate forward
|
|
2054
2136
|
// navigation and keeps precedence over returnTo.
|
|
@@ -2083,6 +2165,33 @@ function resolveRedirectTarget(redirect, submittedData, schema, appFeatures, fal
|
|
|
2083
2165
|
(fallbackRecord !== undefined ? extractIdField(fallbackRecord, idField) : undefined);
|
|
2084
2166
|
return { screenId: targetId, entityId };
|
|
2085
2167
|
}
|
|
2168
|
+
// Success navigation for a configured redirect — shared by entityEdit's
|
|
2169
|
+
// save and writeHandler record actions. The string form navigates without an
|
|
2170
|
+
// entityId; the object form resolves like actionForm's, with `record` as the
|
|
2171
|
+
// fallback id source.
|
|
2172
|
+
function navigateAfterRedirect(options) {
|
|
2173
|
+
const { nav, schema, appFeatures, returnTarget, redirect, resultData, record } = options;
|
|
2174
|
+
if (returnToWinsOverRedirect(returnTarget, redirect, schema, appFeatures)) {
|
|
2175
|
+
navigateToReturn(nav, returnTarget);
|
|
2176
|
+
return;
|
|
2177
|
+
}
|
|
2178
|
+
if (typeof redirect === "string") {
|
|
2179
|
+
nav.navigate({ screenId: lastSegment(redirect) });
|
|
2180
|
+
return;
|
|
2181
|
+
}
|
|
2182
|
+
const { screenId, entityId } = resolveRedirectTarget(redirect, resultData, schema, appFeatures, record);
|
|
2183
|
+
nav.navigate({ screenId, ...(entityId !== undefined && { entityId }) });
|
|
2184
|
+
}
|
|
2185
|
+
// The update payload nests the saved projection under `data`; a changed
|
|
2186
|
+
// parent FK lives there, not in the pre-edit `record`.
|
|
2187
|
+
function overlayUpdatedProjection(record, resultData) {
|
|
2188
|
+
if (typeof resultData !== "object" || resultData === null)
|
|
2189
|
+
return record;
|
|
2190
|
+
const projection = resultData.data;
|
|
2191
|
+
if (typeof projection !== "object" || projection === null)
|
|
2192
|
+
return record;
|
|
2193
|
+
return { ...record, ...projection };
|
|
2194
|
+
}
|
|
2086
2195
|
// Action-Form-Body — non-CRUD Write-Handler-driven Form. Re-uses
|
|
2087
2196
|
// RenderEdit über synthetisierte EntityDefinition + EntityEditScreen-
|
|
2088
2197
|
// Definition (siehe action-form-shim.ts für die Schulden-Doku). Die
|
|
@@ -2189,8 +2298,19 @@ function unwrapMoneyValue(value) {
|
|
|
2189
2298
|
}
|
|
2190
2299
|
return value;
|
|
2191
2300
|
}
|
|
2301
|
+
const CONFIG_SCOPE_SOURCE = {
|
|
2302
|
+
user: "user-row",
|
|
2303
|
+
tenant: "tenant-row",
|
|
2304
|
+
system: "system-row",
|
|
2305
|
+
};
|
|
2306
|
+
function isWriteOnlyTextField(fieldDef) {
|
|
2307
|
+
return fieldDef?.type === "text" && fieldDef.writeOnly === true;
|
|
2308
|
+
}
|
|
2309
|
+
function hasStoredValue(stored) {
|
|
2310
|
+
return stored !== undefined && stored !== null && stored !== "";
|
|
2311
|
+
}
|
|
2192
2312
|
function ConfigEditBody({ schema, screen, translate, }) {
|
|
2193
|
-
const {
|
|
2313
|
+
const { ConfigCascadeView } = usePrimitives();
|
|
2194
2314
|
const t = useTranslation();
|
|
2195
2315
|
const effectiveTranslate = translate ?? t;
|
|
2196
2316
|
const dispatcher = useDispatcher();
|
|
@@ -2201,6 +2321,7 @@ function ConfigEditBody({ schema, screen, translate, }) {
|
|
|
2201
2321
|
const cascadeQuery = useQuery("config:query:cascade", {});
|
|
2202
2322
|
const synthEntity = useMemo(() => synthesizeConfigEditEntity(screen.fields), [screen.fields]);
|
|
2203
2323
|
const synthScreen = useMemo(() => synthesizeConfigEditScreen(screen), [screen]);
|
|
2324
|
+
const boundsSchema = useMemo(() => buildConfigEditSchema(screen.fields), [screen.fields]);
|
|
2204
2325
|
// Initial-Values: pro Field-Name den Wert aus `values[qualifiedKey]`
|
|
2205
2326
|
// abholen. Fehlt der Key auf dem Server (= noch nie gesetzt), nutzen
|
|
2206
2327
|
// wir den Field-Default (createTextField/createNumberField/...).
|
|
@@ -2220,6 +2341,15 @@ function ConfigEditBody({ schema, screen, translate, }) {
|
|
|
2220
2341
|
continue;
|
|
2221
2342
|
}
|
|
2222
2343
|
const stored = valuesQuery.data[qualified]?.value;
|
|
2344
|
+
// The read carries a mask (or nothing); only whether a value exists is
|
|
2345
|
+
// meaningful, so the field gets the writeOnly wire shape true / null.
|
|
2346
|
+
// The read is the effective value: an inherited one is not "set" on this
|
|
2347
|
+
// scope, and a reset here would remove nothing.
|
|
2348
|
+
if (isWriteOnlyTextField(fieldDef)) {
|
|
2349
|
+
const ownedByThisScope = valuesQuery.data[qualified]?.source === CONFIG_SCOPE_SOURCE[screen.scope];
|
|
2350
|
+
out[shortName] = ownedByThisScope && hasStoredValue(stored) ? true : null;
|
|
2351
|
+
continue;
|
|
2352
|
+
}
|
|
2223
2353
|
if (stored === undefined) {
|
|
2224
2354
|
out[shortName] = defaults[shortName];
|
|
2225
2355
|
continue;
|
|
@@ -2238,7 +2368,7 @@ function ConfigEditBody({ schema, screen, translate, }) {
|
|
|
2238
2368
|
}
|
|
2239
2369
|
}
|
|
2240
2370
|
return out;
|
|
2241
|
-
}, [valuesQuery.data, screen.fields, screen.configKeys]);
|
|
2371
|
+
}, [valuesQuery.data, screen.fields, screen.configKeys, screen.scope]);
|
|
2242
2372
|
// Cascade-Lookup: qualifiedKey → ConfigCascade für die
|
|
2243
2373
|
// Cascade-Anzeige unter jedem Feld. Defensive `levels`-Shape-Check
|
|
2244
2374
|
// damit fremde Query-Mocks (z.B. configEdit-Unit-Tests die nur
|
|
@@ -2271,7 +2401,36 @@ function ConfigEditBody({ schema, screen, translate, }) {
|
|
|
2271
2401
|
const qualified = screen.configKeys[shortName];
|
|
2272
2402
|
if (qualified === undefined)
|
|
2273
2403
|
continue;
|
|
2274
|
-
const
|
|
2404
|
+
const fieldDef = screen.fields[shortName];
|
|
2405
|
+
const ftype = fieldDef?.type;
|
|
2406
|
+
if (isWriteOnlyTextField(screen.fields[shortName])) {
|
|
2407
|
+
// true (untouched/undone) and "" mean keep the stored secret; null removes it.
|
|
2408
|
+
if (value === null) {
|
|
2409
|
+
commands.push({
|
|
2410
|
+
type: "config:write:reset",
|
|
2411
|
+
payload: { key: qualified, scope: screen.scope },
|
|
2412
|
+
});
|
|
2413
|
+
}
|
|
2414
|
+
else if (typeof value === "string" && value !== "") {
|
|
2415
|
+
commands.push({
|
|
2416
|
+
type: "config:write:set",
|
|
2417
|
+
payload: { key: qualified, value, scope: screen.scope },
|
|
2418
|
+
});
|
|
2419
|
+
}
|
|
2420
|
+
continue;
|
|
2421
|
+
}
|
|
2422
|
+
// A query-backed select is cleared to "" when its dependent value became
|
|
2423
|
+
// invalid. config:write:set would store that as an empty override (the
|
|
2424
|
+
// badge shows "Mandant" with no value); reset falls back to the inherited
|
|
2425
|
+
// value. set can't do it: deleting the row is irreversible, reset is
|
|
2426
|
+
// the high-risk handler.
|
|
2427
|
+
if (ftype === "select" && fieldDef?.optionsQuery !== undefined && value === "") {
|
|
2428
|
+
commands.push({
|
|
2429
|
+
type: "config:write:reset",
|
|
2430
|
+
payload: { key: qualified, scope: screen.scope },
|
|
2431
|
+
});
|
|
2432
|
+
continue;
|
|
2433
|
+
}
|
|
2275
2434
|
commands.push({
|
|
2276
2435
|
type: "config:write:set",
|
|
2277
2436
|
payload: {
|
|
@@ -2302,41 +2461,40 @@ function ConfigEditBody({ schema, screen, translate, }) {
|
|
|
2302
2461
|
valuesQuery.refetch,
|
|
2303
2462
|
cascadeQuery.refetch,
|
|
2304
2463
|
]);
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
// expanded-State, der daher hier (pro Feld) statt in der Komponente lebt.
|
|
2308
|
-
const [expandedFields, setExpandedFields] = useState(new Set());
|
|
2309
|
-
const toggleExpanded = useCallback((fieldName) => {
|
|
2310
|
-
setExpandedFields((prev) => {
|
|
2311
|
-
const next = new Set(prev);
|
|
2312
|
-
if (next.has(fieldName))
|
|
2313
|
-
next.delete(fieldName);
|
|
2314
|
-
else
|
|
2315
|
-
next.add(fieldName);
|
|
2316
|
-
return next;
|
|
2317
|
-
});
|
|
2318
|
-
}, []);
|
|
2319
|
-
if (valuesQuery.loading && valuesQuery.data === null) {
|
|
2320
|
-
return (_jsx(Banner, { padded: true, variant: "loading", testId: "kumiko-screen-loading", children: "Loading\u2026" }));
|
|
2321
|
-
}
|
|
2464
|
+
if (valuesQuery.loading && valuesQuery.data === null)
|
|
2465
|
+
return _jsx(QueryLoadingBanner, {});
|
|
2322
2466
|
if (valuesQuery.error) {
|
|
2323
|
-
return (_jsx(
|
|
2467
|
+
return (_jsx(QueryErrorBanner, { error: valuesQuery.error, translate: effectiveTranslate, onRetry: valuesQuery.refetch }));
|
|
2324
2468
|
}
|
|
2325
|
-
if (initial === null)
|
|
2326
|
-
return
|
|
2327
|
-
|
|
2328
|
-
|
|
2469
|
+
if (initial === null)
|
|
2470
|
+
return _jsx(QueryLoadingBanner, {});
|
|
2471
|
+
// Cascade values of a select show the option label the select itself shows,
|
|
2472
|
+
// not the stored value (a UUID for optionsQuery selects).
|
|
2473
|
+
const cascadeValueRenderer = (fieldName) => {
|
|
2474
|
+
const fieldDef = screen.fields[fieldName];
|
|
2475
|
+
if (fieldDef?.type !== "select")
|
|
2476
|
+
return undefined;
|
|
2477
|
+
if (fieldDef.optionsQuery !== undefined) {
|
|
2478
|
+
const query = fieldDef.optionsQuery;
|
|
2479
|
+
const payload = resolveOptionsQueryPayload(fieldDef.optionsQueryPayload ?? {}, initial);
|
|
2480
|
+
return (value) => _jsx(QueryOptionLabel, { query: query, payload: payload, value: value });
|
|
2481
|
+
}
|
|
2482
|
+
if (fieldDef.options.length === 0)
|
|
2483
|
+
return undefined;
|
|
2484
|
+
return (value) => buildOptionLabels(effectiveTranslate, (option) => fieldOptionLabelKey(schema.featureName, synthScreen.entity, fieldName, option), [String(value)])[String(value)];
|
|
2485
|
+
};
|
|
2486
|
+
return (_jsx(RenderEdit, { screen: synthScreen, entity: synthEntity, featureName: schema.featureName, initial: initial, schema: boundsSchema, customSubmit: customSubmit, dirtyFooter: true, validateOnChange: true, ...(screenFillsHeight(screen) && { fillScreenHeight: true }), ...(screen.submitLabel !== undefined && { submitLabel: screen.submitLabel }), ...(translate !== undefined && { translate }), fieldAppendixPlacement: "below-control", fieldAccent: (fieldName) => cascades[fieldName]?.levels.find((level) => level.isActive)?.source ===
|
|
2487
|
+
CONFIG_SCOPE_SOURCE[screen.scope], markChangedFields: true, ...(screen.fieldDescriptions !== undefined && {
|
|
2488
|
+
fieldDescription: (fieldName) => {
|
|
2489
|
+
const key = screen.fieldDescriptions?.[fieldName];
|
|
2490
|
+
return key === undefined ? undefined : effectiveTranslate(key);
|
|
2491
|
+
},
|
|
2492
|
+
}), fieldAppendix: (fieldName) => {
|
|
2329
2493
|
const cascade = cascades[fieldName];
|
|
2330
2494
|
if (cascade === undefined)
|
|
2331
2495
|
return undefined;
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
const cascade = cascades[fieldName];
|
|
2335
|
-
// Panel nur rendern wenn aufgeklappt — sonst kein leerer Abstand
|
|
2336
|
-
// unter dem Input.
|
|
2337
|
-
if (cascade === undefined || !expandedFields.has(fieldName))
|
|
2338
|
-
return undefined;
|
|
2339
|
-
return (_jsx(ConfigCascadeView, { slot: "panel", cascade: cascade, screenScope: screen.scope, expanded: true, qualifiedKey: screen.configKeys[fieldName], onReset: async (key, scope) => {
|
|
2496
|
+
const renderValue = cascadeValueRenderer(fieldName);
|
|
2497
|
+
return (_jsx(ConfigCascadeView, { cascade: cascade, screenScope: screen.scope, qualifiedKey: screen.configKeys[fieldName], required: screen.requiredFields?.includes(fieldName) ?? false, ...(renderValue !== undefined && { renderValue }), onReset: async (key, scope) => {
|
|
2340
2498
|
await dispatcher.write("config:write:reset", { key, scope });
|
|
2341
2499
|
await Promise.allSettled([valuesQuery.refetch?.(), cascadeQuery.refetch?.()]);
|
|
2342
2500
|
} }));
|
package/dist/app/nav.d.ts
CHANGED
|
@@ -47,6 +47,11 @@ export type NavApi = {
|
|
|
47
47
|
};
|
|
48
48
|
export declare function parsePath(pathname: string, hasWorkspaces?: boolean): NavRoute | undefined;
|
|
49
49
|
export declare function formatPath(target: ScreenTarget): string;
|
|
50
|
+
export declare function findDetailForScreen(features: readonly FeatureSchema[], entity: string): {
|
|
51
|
+
readonly featureName: string;
|
|
52
|
+
readonly screenId: string;
|
|
53
|
+
readonly screen: FeatureSchema["screens"][number];
|
|
54
|
+
} | undefined;
|
|
50
55
|
/** Non-throwing existence check for findDetailForScreen — lets a caller pick
|
|
51
56
|
* a different default (fw#2164's row-click fallback) instead of resolving. */
|
|
52
57
|
export declare function hasDetailScreen(features: readonly FeatureSchema[], entity: string): boolean;
|