@cosmicdrift/kumiko-renderer 0.353.0 → 0.354.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/form-schema.d.ts +1 -0
- package/dist/app/form-schema.js +14 -1
- package/dist/app/kumiko-screen.d.ts +2 -1
- package/dist/app/kumiko-screen.js +58 -23
- package/dist/app/layout-fields.d.ts +2 -1
- package/dist/app/layout-fields.js +15 -0
- package/dist/app/secret-mint-body.js +4 -2
- package/dist/components/render-edit-types.d.ts +8 -0
- package/dist/components/render-edit.js +19 -4
- package/dist/i18n-defaults.js +1 -0
- package/dist/i18n.d.ts +3 -0
- package/dist/i18n.js +12 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/primitives.d.ts +8 -6
- package/package.json +5 -5
- package/src/changes.json +37 -0
|
@@ -2,4 +2,5 @@ import type { EntityDefinition, EntityEditScreenDefinition } from "@cosmicdrift/
|
|
|
2
2
|
import * as z from "zod";
|
|
3
3
|
export declare function isPresent(value: unknown): boolean;
|
|
4
4
|
export declare const REQUIRED_FIELD_I18N_KEY = "kumiko.validation.required";
|
|
5
|
+
export declare const OPTION_NOT_AVAILABLE_I18N_KEY = "kumiko.validation.optionNotAvailable";
|
|
5
6
|
export declare function buildFormSchema(entity: EntityDefinition, screen: EntityEditScreenDefinition, unchangedFrom?: Readonly<Record<string, unknown>>): z.ZodType;
|
package/dist/app/form-schema.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { evalFieldCondition, NO_WIDGET_FIELD_TYPES } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
1
|
+
import { evalFieldCondition, findUnavailableSelectOptions, NO_WIDGET_FIELD_TYPES, } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
2
|
import { I18N_KEY_PARAM } from "@cosmicdrift/kumiko-headless";
|
|
3
3
|
import * as z from "zod";
|
|
4
4
|
import { layoutEditFields } from "./layout-fields.js";
|
|
@@ -16,6 +16,7 @@ function isEmbeddedListField(field) {
|
|
|
16
16
|
}
|
|
17
17
|
// Renders raw to the user without a de+en default in i18n-defaults.ts.
|
|
18
18
|
export const REQUIRED_FIELD_I18N_KEY = "kumiko.validation.required";
|
|
19
|
+
export const OPTION_NOT_AVAILABLE_I18N_KEY = "kumiko.validation.optionNotAvailable";
|
|
19
20
|
// Client-side presence validation for the auto-wired entityEdit path —
|
|
20
21
|
// checks that every rendered required field HAS a value, not that the
|
|
21
22
|
// value has the right shape. Format/range/type validation stays server-
|
|
@@ -54,6 +55,7 @@ export function buildFormSchema(entity, screen, unchangedFrom) {
|
|
|
54
55
|
// its keys — the runtime object always carries every form field.
|
|
55
56
|
// @cast-boundary form-values
|
|
56
57
|
const record = values;
|
|
58
|
+
const selectFieldsToCheck = {};
|
|
57
59
|
for (const spec of fields) {
|
|
58
60
|
const field = entity.fields[spec.field];
|
|
59
61
|
if (!field)
|
|
@@ -62,6 +64,7 @@ export function buildFormSchema(entity, screen, unchangedFrom) {
|
|
|
62
64
|
// same reason as the NO_WIDGET_FIELD_TYPES check below.
|
|
63
65
|
if (spec.readOnly !== undefined && evalFieldCondition(spec.readOnly, record))
|
|
64
66
|
continue;
|
|
67
|
+
selectFieldsToCheck[spec.field] = field;
|
|
65
68
|
// Screen-spec `required` overrides the entity default, mirroring
|
|
66
69
|
// `view-model/edit.ts` — the rendered form is the reference, and a
|
|
67
70
|
// presence check stricter than the form blocks the user for nothing.
|
|
@@ -90,5 +93,15 @@ export function buildFormSchema(entity, screen, unchangedFrom) {
|
|
|
90
93
|
params: { [I18N_KEY_PARAM]: REQUIRED_FIELD_I18N_KEY },
|
|
91
94
|
});
|
|
92
95
|
}
|
|
96
|
+
// Against `unchangedFrom` the helper still checks a stored value whose
|
|
97
|
+
// discriminator changed — the server validates the merged row.
|
|
98
|
+
for (const issue of findUnavailableSelectOptions(selectFieldsToCheck, record, unchangedFrom)) {
|
|
99
|
+
ctx.addIssue({
|
|
100
|
+
code: "custom",
|
|
101
|
+
path: [issue.field],
|
|
102
|
+
message: `"${issue.field}" is not available for the current selection.`,
|
|
103
|
+
params: { [I18N_KEY_PARAM]: OPTION_NOT_AVAILABLE_I18N_KEY, allowed: issue.allowed },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
93
106
|
});
|
|
94
107
|
}
|
|
@@ -22,9 +22,10 @@ export declare function KumikoScreen({ schema, qn, translate, entityId, onRowCli
|
|
|
22
22
|
export declare function buildInitialValues(fields: Readonly<Record<string, unknown>>, defaultCurrency?: string, moneyCurrencyOverrides?: Readonly<Record<string, string>>): Readonly<Record<string, unknown>>;
|
|
23
23
|
export declare function tenantCurrencyMoneyFieldNames(fields: Readonly<Record<string, unknown>>): readonly string[];
|
|
24
24
|
export declare function literalCurrencyOverrides(fields: Readonly<Record<string, unknown>>): Readonly<Record<string, string>>;
|
|
25
|
-
export declare function useMoneyCurrencyOverrides(fields: Readonly<Record<string, unknown>>, tenantFieldNames: readonly string[], fallback: string): {
|
|
25
|
+
export declare function useMoneyCurrencyOverrides(fields: Readonly<Record<string, unknown>>, tenantFieldNames: readonly string[], fallback: string | undefined): {
|
|
26
26
|
readonly overrides: Readonly<Record<string, string>> | undefined;
|
|
27
27
|
readonly loading: boolean;
|
|
28
|
+
readonly unresolved: boolean;
|
|
28
29
|
};
|
|
29
30
|
export type InitialValueSources = {
|
|
30
31
|
readonly searchParams: Readonly<Record<string, string>>;
|
|
@@ -30,7 +30,7 @@ import { useDashboardBody } from "./dashboard-body.js";
|
|
|
30
30
|
import { EntityListExpandedRow } from "./entity-list-expanded-row.js";
|
|
31
31
|
import { FacetCountBridges } from "./facet-count-bridge.js";
|
|
32
32
|
import { buildFormSchema } from "./form-schema.js";
|
|
33
|
-
import { layoutFieldNames } from "./layout-fields.js";
|
|
33
|
+
import { hiddenRequiredFieldsFilled, layoutFieldNames } from "./layout-fields.js";
|
|
34
34
|
import { buildDateRangePayload, buildEntityFilterFacets, buildFacetCountRequests, buildFilterFacets, buildFilterPayload, clampDateRange, mergeReferenceFacetOptions, normalizeDefaultFilters, readDateRange, resolveDateRangeFacets, resolveProjectionFacetSpecs, } from "./list-facets.js";
|
|
35
35
|
import { useInitialValuesHandoff, useNav } from "./nav.js";
|
|
36
36
|
import { synthesizeProjectionDetailEntity, synthesizeProjectionDetailScreen, } from "./projection-detail-shim.js";
|
|
@@ -202,12 +202,16 @@ function pageSizeOptionsFor(currentLimit) {
|
|
|
202
202
|
? PAGE_SIZE_OPTIONS
|
|
203
203
|
: [...PAGE_SIZE_OPTIONS, currentLimit].sort((a, b) => a - b);
|
|
204
204
|
}
|
|
205
|
-
function useNavigateToCreateFor(schema, entityName, createScreen) {
|
|
205
|
+
function useNavigateToCreateFor(schema, entityName, createScreen, createUnavailable) {
|
|
206
206
|
const nav = useNav();
|
|
207
207
|
const host = useReturnHost();
|
|
208
208
|
const editScreenId = useMemo(() => {
|
|
209
209
|
if (createScreen !== undefined)
|
|
210
210
|
return createScreen;
|
|
211
|
+
// The role may not open the declared create target; falling back to the
|
|
212
|
+
// entity's edit form would bypass the flow the app prescribes (e.g. a wizard).
|
|
213
|
+
if (createUnavailable === true)
|
|
214
|
+
return undefined;
|
|
211
215
|
// allowCreate:false = update-only Edit-Screen (Create läuft über einen
|
|
212
216
|
// Lifecycle-Write) — der zählt nicht als „+ Neu"-Ziel. singleton:true
|
|
213
217
|
// öffnet ohne entityId den vorhandenen Record (EntityEditSingletonBody)
|
|
@@ -217,7 +221,7 @@ function useNavigateToCreateFor(schema, entityName, createScreen) {
|
|
|
217
221
|
s.allowCreate !== false &&
|
|
218
222
|
s.singleton !== true);
|
|
219
223
|
return edit !== undefined ? lastSegment(edit.id) : undefined;
|
|
220
|
-
}, [schema.screens, entityName, createScreen]);
|
|
224
|
+
}, [schema.screens, entityName, createScreen, createUnavailable]);
|
|
221
225
|
const navigate = useCallback(() => {
|
|
222
226
|
if (editScreenId !== undefined)
|
|
223
227
|
navigateWithReturnTo(nav, { screenId: editScreenId }, host);
|
|
@@ -283,17 +287,20 @@ export function tenantCurrencyMoneyFieldNames(fields) {
|
|
|
283
287
|
function resolveTenantCurrency(fieldNames, query, fallback) {
|
|
284
288
|
if (fieldNames.length === 0)
|
|
285
289
|
return { status: "not-needed" };
|
|
286
|
-
if (query.error)
|
|
287
|
-
return
|
|
290
|
+
if (query.error) {
|
|
291
|
+
return fallback === undefined
|
|
292
|
+
? { status: "unresolved" }
|
|
293
|
+
: { status: "ready", currency: fallback };
|
|
294
|
+
}
|
|
288
295
|
if (query.data === null)
|
|
289
296
|
return { status: "loading" };
|
|
290
297
|
const raw = query.data[TENANT_CURRENCY_CONFIG_KEY]?.value;
|
|
291
|
-
|
|
298
|
+
if (typeof raw === "string" && raw !== "")
|
|
299
|
+
return { status: "ready", currency: raw };
|
|
300
|
+
return fallback === undefined
|
|
301
|
+
? { status: "unresolved" }
|
|
302
|
+
: { status: "ready", currency: fallback };
|
|
292
303
|
}
|
|
293
|
-
// An entity-less form screen has no `entity.defaultCurrency` to fall back on,
|
|
294
|
-
// so a tenant-declared field whose config query errors or returns no value
|
|
295
|
-
// lands here — same last resort as the entityEdit path's `?? "EUR"`.
|
|
296
|
-
const ACTION_FORM_CURRENCY_FALLBACK = "EUR";
|
|
297
304
|
const HEADER_CARD_DEFAULT_WIDTH = "4xl";
|
|
298
305
|
// Shared by the update payload shape and the server-error path prefix so both can't drift.
|
|
299
306
|
const UPDATE_CHANGES_KEY = "changes";
|
|
@@ -318,7 +325,10 @@ export function literalCurrencyOverrides(fields) {
|
|
|
318
325
|
// on a fast click submit, the wrong currency. Callers pass the tenant field
|
|
319
326
|
// names themselves because an edit form only needs the fetch for fields the
|
|
320
327
|
// record has no stored value for.
|
|
321
|
-
export function useMoneyCurrencyOverrides(fields, tenantFieldNames,
|
|
328
|
+
export function useMoneyCurrencyOverrides(fields, tenantFieldNames,
|
|
329
|
+
// `undefined` = no safe last resort (entity-less forms): an unresolvable
|
|
330
|
+
// tenant currency is reported via `unresolved` instead of guessing a code.
|
|
331
|
+
fallback) {
|
|
322
332
|
const query = useQuery("config:query:values", { keys: [TENANT_CURRENCY_CONFIG_KEY] }, { enabled: tenantFieldNames.length > 0 });
|
|
323
333
|
const resolution = resolveTenantCurrency(tenantFieldNames, query, fallback);
|
|
324
334
|
const resolvedTenantCurrency = resolution.status === "ready" ? resolution.currency : undefined;
|
|
@@ -330,7 +340,11 @@ export function useMoneyCurrencyOverrides(fields, tenantFieldNames, fallback) {
|
|
|
330
340
|
}
|
|
331
341
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
332
342
|
}, [fields, tenantFieldNames, resolvedTenantCurrency]);
|
|
333
|
-
return {
|
|
343
|
+
return {
|
|
344
|
+
overrides,
|
|
345
|
+
loading: resolution.status === "loading",
|
|
346
|
+
unresolved: resolution.status === "unresolved",
|
|
347
|
+
};
|
|
334
348
|
}
|
|
335
349
|
function multiSelectOptionValues(shape) {
|
|
336
350
|
if (shape.options === undefined || shape.options.length === 0)
|
|
@@ -822,6 +836,9 @@ function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onRelo
|
|
|
822
836
|
onReload,
|
|
823
837
|
screen.entity,
|
|
824
838
|
]);
|
|
839
|
+
const sameEntityScreenId = useCallback((targetScreen) => isEntityEditScreenOfOtherEntity(screen.entity, targetScreen, schema, appFeatures)
|
|
840
|
+
? undefined
|
|
841
|
+
: entityId, [screen.entity, schema, appFeatures, entityId]);
|
|
825
842
|
const buildSectionActions = useCallback((actions) => buildRecordActions({
|
|
826
843
|
actions,
|
|
827
844
|
record,
|
|
@@ -832,7 +849,7 @@ function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onRelo
|
|
|
832
849
|
openDrawer,
|
|
833
850
|
onWriteSuccess: onReload,
|
|
834
851
|
onRecordLeft: handleRecordLeft,
|
|
835
|
-
|
|
852
|
+
sameEntityScreenId,
|
|
836
853
|
defaultWritePayloadId: entityId,
|
|
837
854
|
}), [
|
|
838
855
|
record,
|
|
@@ -843,6 +860,7 @@ function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onRelo
|
|
|
843
860
|
openDrawer,
|
|
844
861
|
onReload,
|
|
845
862
|
handleRecordLeft,
|
|
863
|
+
sameEntityScreenId,
|
|
846
864
|
entityId,
|
|
847
865
|
]);
|
|
848
866
|
const headerActions = useMemo(() => buildSectionActions(screen.actions ?? []), [buildSectionActions, screen.actions]);
|
|
@@ -1105,7 +1123,7 @@ function EntityListScreen({ schema, screen, translate, onRowClick, }) {
|
|
|
1105
1123
|
function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
1106
1124
|
const isEmbeddedScreen = useIsEmbeddedScreen();
|
|
1107
1125
|
const featureName = schema.featureName;
|
|
1108
|
-
const onCreate = useNavigateToCreateFor(schema, screen.entity, screen.createScreen);
|
|
1126
|
+
const onCreate = useNavigateToCreateFor(schema, screen.entity, screen.createScreen, screen.createUnavailable);
|
|
1109
1127
|
const { Banner } = usePrimitives();
|
|
1110
1128
|
const queryType = entityQueryCommand(featureName, screen.entity, "list");
|
|
1111
1129
|
const nav = useNav();
|
|
@@ -1322,9 +1340,7 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
1322
1340
|
// Default entityId für entityEdit-Targets: row["id"] wenn kein expliziter
|
|
1323
1341
|
// entityId-Feldname gesetzt ist. Nur für Targets DERSELBEN Entity — sonst
|
|
1324
1342
|
// bekäme ein Cross-Entity-Edit-Screen die falsche row.id injiziert.
|
|
1325
|
-
const targetIsEntityEdit =
|
|
1326
|
-
s.entity === screen.entity &&
|
|
1327
|
-
lastSegment(s.id) === action.screen);
|
|
1343
|
+
const targetIsEntityEdit = isEntityEditScreenOfEntity(screen.entity, action.screen, schema, appFeatures);
|
|
1328
1344
|
const explicit = action.entityId !== undefined ? String(row.values[action.entityId] ?? "") : undefined;
|
|
1329
1345
|
const fallback = targetIsEntityEdit ? String(row.values["id"] ?? "") : undefined;
|
|
1330
1346
|
const entityId = explicit ?? fallback;
|
|
@@ -1339,7 +1355,7 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
1339
1355
|
const params = navigateActionSearchParams(action, row.values);
|
|
1340
1356
|
navigateWithReturnTo(nav, target, host, params);
|
|
1341
1357
|
}
|
|
1342
|
-
}, [nav, schema
|
|
1358
|
+
}, [nav, schema, appFeatures, screen.entity, host]);
|
|
1343
1359
|
const rowActions = useMemo(() => {
|
|
1344
1360
|
const declared = screen.rowActions ?? [];
|
|
1345
1361
|
// Prepended unless a declared rowAction already has id "edit" — declared wins.
|
|
@@ -2230,6 +2246,20 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
2230
2246
|
function redirectScreenTarget(redirect) {
|
|
2231
2247
|
return typeof redirect === "string" ? redirect : redirect.screen;
|
|
2232
2248
|
}
|
|
2249
|
+
// Same-entity detection for navigate defaults: only an entityEdit screen of the
|
|
2250
|
+
// SAME entity may receive the current record's id (it opens in UPDATE mode, which
|
|
2251
|
+
// is what the boot validator assumes). Searched across all features.
|
|
2252
|
+
function isEntityEditScreenOfEntity(entity, targetScreen, schema, appFeatures) {
|
|
2253
|
+
if (entity === undefined)
|
|
2254
|
+
return false;
|
|
2255
|
+
return [schema, ...appFeatures].some((feature) => feature.screens.some((s) => s.type === "entityEdit" && s.entity === entity && lastSegment(s.id) === targetScreen));
|
|
2256
|
+
}
|
|
2257
|
+
// Record-scoped targets (actionForm, secretMint) take the shown record's id; an
|
|
2258
|
+
// entityEdit of ANOTHER entity must not, or it would open that entity's UPDATE
|
|
2259
|
+
// mode on the wrong id instead of create.
|
|
2260
|
+
function isEntityEditScreenOfOtherEntity(entity, targetScreen, schema, appFeatures) {
|
|
2261
|
+
return [schema, ...appFeatures].some((feature) => feature.screens.some((s) => s.type === "entityEdit" && s.entity !== entity && lastSegment(s.id) === targetScreen));
|
|
2262
|
+
}
|
|
2233
2263
|
// A qualified redirect names its feature, so the search space is THAT feature
|
|
2234
2264
|
// only — the current schema's own screens must not shadow it with a same
|
|
2235
2265
|
// short-id screen of another type. An unqualified redirect searches this
|
|
@@ -2331,11 +2361,11 @@ function ActionFormBody({ schema, screen, translate, initialOverrides, onSuccess
|
|
|
2331
2361
|
const handoffValues = onSuccess === undefined ? pendingHandoff : undefined;
|
|
2332
2362
|
// This screen has no entity, so a money field here declares its own currency
|
|
2333
2363
|
// source (fw#2839, boot-enforced) — without one the form would seed a bare
|
|
2334
|
-
// `0` the handler's schema rejects.
|
|
2335
|
-
//
|
|
2336
|
-
//
|
|
2364
|
+
// `0` the handler's schema rejects. When a tenant-declared field's config
|
|
2365
|
+
// query errors or holds no value there is no entity default to use, and
|
|
2366
|
+
// submitting a guessed currency would mis-book the amount — block instead.
|
|
2337
2367
|
const tenantCurrencyFieldNames = useMemo(() => tenantCurrencyMoneyFieldNames(screen.fields), [screen.fields]);
|
|
2338
|
-
const { overrides: moneyCurrencyOverrides, loading: currencyLoading } = useMoneyCurrencyOverrides(screen.fields, tenantCurrencyFieldNames,
|
|
2368
|
+
const { overrides: moneyCurrencyOverrides, loading: currencyLoading, unresolved: currencyUnresolved, } = useMoneyCurrencyOverrides(screen.fields, tenantCurrencyFieldNames, undefined);
|
|
2339
2369
|
const initial = useMemo(() => mergeSearchParamsIntoInitial(screen.fields, {
|
|
2340
2370
|
searchParams: nav.searchParams,
|
|
2341
2371
|
urlPrefillFields: screen.urlPrefillFields,
|
|
@@ -2424,12 +2454,17 @@ function ActionFormBody({ schema, screen, translate, initialOverrides, onSuccess
|
|
|
2424
2454
|
if (currencyLoading) {
|
|
2425
2455
|
return (_jsx(Banner, { padded: true, variant: "loading", testId: "kumiko-screen-loading", children: "Loading\u2026" }));
|
|
2426
2456
|
}
|
|
2457
|
+
if (currencyUnresolved) {
|
|
2458
|
+
return (_jsx(Banner, { padded: true, variant: "error", testId: "kumiko-screen-currency-unresolved", children: "The tenant currency could not be loaded, so this form cannot be submitted." }));
|
|
2459
|
+
}
|
|
2427
2460
|
return (_jsx(RenderEdit, { screen: synthScreen, entity: synthEntity, featureName: schema.featureName, initial: initial,
|
|
2428
2461
|
// Extension-Sections im actionForm sehen die initialen Form-Values
|
|
2429
2462
|
// (inkl. searchParams-Prefill, z.B. ?incidentId=…) als initialValues
|
|
2430
2463
|
// — anders als im entityEdit gibt es hier keinen record, aus dem
|
|
2431
2464
|
// sie Kontext ziehen könnten.
|
|
2432
|
-
extensionInitialValues: initial, ...(
|
|
2465
|
+
extensionInitialValues: initial, ...(screen.submitPrefilled === true && {
|
|
2466
|
+
submitWithoutChanges: hiddenRequiredFieldsFilled(synthScreen, screen.fields, initial),
|
|
2467
|
+
}), ...(successText !== undefined && {
|
|
2433
2468
|
leadContent: (_jsx(Banner, { variant: "info", testId: "action-form-success", children: successText })),
|
|
2434
2469
|
}), writeCommand: screen.handler, payloadMode: "values", onSubmit: handleSubmitted, ...(handleCancel !== undefined && { onCancel: handleCancel }), ...(onDirtyChange !== undefined && { onDirtyChange }), ...((insideDrawer || screenFillsHeight(screen)) && { fillScreenHeight: true }), ...((screen.submitLabel ?? submitLabelFallback) !== undefined && {
|
|
2435
2470
|
submitLabel: screen.submitLabel ?? submitLabelFallback,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import type { EditFieldSpec, EntityEditScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
1
|
+
import type { EditFieldSpec, EntityEditScreenDefinition, SecretMintScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
2
|
export declare function layoutEditFields(screen: EntityEditScreenDefinition): readonly Exclude<EditFieldSpec, string>[];
|
|
3
3
|
export declare function layoutFieldNames(screen: EntityEditScreenDefinition): ReadonlySet<string>;
|
|
4
|
+
export declare function hiddenRequiredFieldsFilled(screen: EntityEditScreenDefinition, fields: SecretMintScreenDefinition["fields"], initial: Readonly<Record<string, unknown>>): boolean;
|
|
@@ -18,3 +18,18 @@ export function layoutEditFields(screen) {
|
|
|
18
18
|
export function layoutFieldNames(screen) {
|
|
19
19
|
return new Set(layoutEditFields(screen).map((spec) => spec.field));
|
|
20
20
|
}
|
|
21
|
+
function isRequiredField(field) {
|
|
22
|
+
return field !== undefined && "required" in field && field.required === true;
|
|
23
|
+
}
|
|
24
|
+
function hasValue(value) {
|
|
25
|
+
if (value === undefined || value === null || value === "")
|
|
26
|
+
return false;
|
|
27
|
+
return !(Array.isArray(value) && value.length === 0);
|
|
28
|
+
}
|
|
29
|
+
// A layout-hidden field can never be edited, so a required one without a seeded
|
|
30
|
+
// value would leave the form permanently unsubmittable.
|
|
31
|
+
export function hiddenRequiredFieldsFilled(screen, fields, initial) {
|
|
32
|
+
return layoutEditFields(screen).every((spec) => spec.visible !== false ||
|
|
33
|
+
!isRequiredField(fields[spec.field]) ||
|
|
34
|
+
hasValue(initial[spec.field]));
|
|
35
|
+
}
|
|
@@ -5,7 +5,7 @@ import { useTranslation } from "../i18n.js";
|
|
|
5
5
|
import { usePrimitives } from "../primitives.js";
|
|
6
6
|
import { synthesizeActionFormEntity, synthesizeActionFormScreen, synthesizeSecretMintConfirmScreen, } from "./action-form-shim.js";
|
|
7
7
|
import { buildInitialValues, mergeSearchParamsIntoInitial, tenantCurrencyMoneyFieldNames, useMoneyCurrencyOverrides, } from "./kumiko-screen.js";
|
|
8
|
-
import { layoutFieldNames } from "./layout-fields.js";
|
|
8
|
+
import { hiddenRequiredFieldsFilled, layoutFieldNames } from "./layout-fields.js";
|
|
9
9
|
import { useInitialValuesHandoff, useNav } from "./nav.js";
|
|
10
10
|
import { lastSegment } from "./qn.js";
|
|
11
11
|
import { navigateToReturnOr, useReturnTarget } from "./return-to.js";
|
|
@@ -193,5 +193,7 @@ export function SecretMintBody({ schema, screen, translate }) {
|
|
|
193
193
|
if (mintCurrencyLoading) {
|
|
194
194
|
return (_jsx(Banner, { padded: true, variant: "loading", testId: "kumiko-screen-loading", children: "Loading\u2026" }));
|
|
195
195
|
}
|
|
196
|
-
return (_jsx(RenderEdit, { screen: synthScreen, entity: synthEntity, featureName: schema.featureName, initial: initial, extensionInitialValues: initial,
|
|
196
|
+
return (_jsx(RenderEdit, { screen: synthScreen, entity: synthEntity, featureName: schema.featureName, initial: initial, extensionInitialValues: initial, ...(screen.submitPrefilled === true && {
|
|
197
|
+
submitWithoutChanges: hiddenRequiredFieldsFilled(synthScreen, screen.fields, initial),
|
|
198
|
+
}), writeCommand: screen.handler, payloadMode: "values", onSubmit: handleSubmitted, fillScreenHeight: true, ...(handleCancel !== undefined && { onCancel: handleCancel }), ...(translate !== undefined && { translate }), ...(screen.submitLabel !== undefined && { submitLabel: screen.submitLabel }) }, "mint"));
|
|
197
199
|
}
|
|
@@ -76,6 +76,9 @@ export type RenderEditProps<TValues extends FormValues, TCtx = unknown> = {
|
|
|
76
76
|
* "Save" can be replaced by domain-specific strings ("Approve" /
|
|
77
77
|
* "Dispatch" / etc.). */
|
|
78
78
|
readonly submitLabel?: string;
|
|
79
|
+
/** Set by submitPrefilled screens: defined shows the submit button even when no field is
|
|
80
|
+
* editable (all hidden or read-only); `true` also enables it while the form is unchanged. */
|
|
81
|
+
readonly submitWithoutChanges?: boolean;
|
|
79
82
|
/** Visual style of the submit button (actionForm `submitStyle`). Default "primary". */
|
|
80
83
|
readonly submitVariant?: "primary" | "danger";
|
|
81
84
|
/** Extra footer buttons (actionForm `footerActions`): each sets its `patch`
|
|
@@ -241,4 +244,9 @@ export type RenderEditControls<TValues extends FormValues> = {
|
|
|
241
244
|
* guard — a host showing a pre-filled proposal must be able to accept it
|
|
242
245
|
* untouched. */
|
|
243
246
|
readonly submit: () => Promise<void>;
|
|
247
|
+
/** Wizard only (no-op elsewhere): validates the current step and advances,
|
|
248
|
+
* like the built-in Next button. A `hideActions` host steps with this. */
|
|
249
|
+
readonly next: () => void;
|
|
250
|
+
/** Wizard only (no-op elsewhere): goes back one step without validating. */
|
|
251
|
+
readonly back: () => void;
|
|
244
252
|
};
|
|
@@ -223,7 +223,7 @@ function resolveTitleTemplate(template, values, translate, fallback) {
|
|
|
223
223
|
return translate(template, values);
|
|
224
224
|
}
|
|
225
225
|
export function RenderEdit(props) {
|
|
226
|
-
const { screen, entity, featureName, initial, writeCommand, translate: translateProp, ctx, schema, onSubmit, payloadMode = "values", buildPayload, serverFieldPathPrefix, onDelete, onCancel, onReload, onDirtyChange, onCopyLink, actions, onRelatedListDrawerAction, submitLabel, submitVariant, footerActions, summary, labelAppendix, fieldAppendix, fieldAppendixPlacement, fieldDescription, fieldAccent, markChangedFields = false, entityId: entityIdProp, extensionInitialValues, onChange, onControlsReady, fields: fieldsFilter, disabled = false, hideActions, valueDisplay = "form", hideSectionTitles, fillScreenHeight, dirtyFooter, validateOnChange, headerRegion, buildSectionActions, leadContent, } = props;
|
|
226
|
+
const { screen, entity, featureName, initial, writeCommand, translate: translateProp, ctx, schema, onSubmit, payloadMode = "values", buildPayload, serverFieldPathPrefix, onDelete, onCancel, onReload, onDirtyChange, onCopyLink, actions, onRelatedListDrawerAction, submitLabel, submitVariant, submitWithoutChanges, footerActions, summary, labelAppendix, fieldAppendix, fieldAppendixPlacement, fieldDescription, fieldAccent, markChangedFields = false, entityId: entityIdProp, extensionInitialValues, onChange, onControlsReady, fields: fieldsFilter, disabled = false, hideActions, valueDisplay = "form", hideSectionTitles, fillScreenHeight, dirtyFooter, validateOnChange, headerRegion, buildSectionActions, leadContent, } = props;
|
|
227
227
|
const i18nScreenId = props.i18nScreenId ?? screen.id;
|
|
228
228
|
const { customSubmit } = props;
|
|
229
229
|
// Translate-Fallback: wenn der Caller keine Translate-Fn übergibt,
|
|
@@ -398,6 +398,8 @@ export function RenderEdit(props) {
|
|
|
398
398
|
// snapshot/extensionDirty, while the onControlsReady effect fires once per
|
|
399
399
|
// mount — the ref lets controls.submit() always reach the current one.
|
|
400
400
|
const handleSubmitRef = useRef(async () => { });
|
|
401
|
+
const wizardNextRef = useRef(() => { });
|
|
402
|
+
const wizardBackRef = useRef(() => { });
|
|
401
403
|
const currentStepRef = useRef(0);
|
|
402
404
|
const draftSaveTimerRef = useRef(null);
|
|
403
405
|
useEffect(() => {
|
|
@@ -452,7 +454,11 @@ export function RenderEdit(props) {
|
|
|
452
454
|
patch: patchAndScheduleDraftSave,
|
|
453
455
|
validate: scopedValidate,
|
|
454
456
|
getValues: () => controller.getSnapshot().values,
|
|
455
|
-
|
|
457
|
+
// saveNow: a host-driven submit must save on a wizard's intermediate
|
|
458
|
+
// steps too instead of silently advancing; stepping is next()/back().
|
|
459
|
+
submit: () => handleSubmitRef.current({ saveNow: true }),
|
|
460
|
+
next: () => wizardNextRef.current(),
|
|
461
|
+
back: () => wizardBackRef.current(),
|
|
456
462
|
});
|
|
457
463
|
// Dep on boolean presence only — inline-arrow identity must not redeliver.
|
|
458
464
|
}, [hasControlsReady, controller, scopedValidate, patchAndScheduleDraftSave]);
|
|
@@ -1011,6 +1017,8 @@ export function RenderEdit(props) {
|
|
|
1011
1017
|
}
|
|
1012
1018
|
}
|
|
1013
1019
|
handleSubmitRef.current = handleSubmit;
|
|
1020
|
+
wizardNextRef.current = isWizard ? handleWizardNext : () => { };
|
|
1021
|
+
wizardBackRef.current = isWizard ? handleWizardBack : () => { };
|
|
1014
1022
|
// Screen forms (entityEdit/actionForm filling the shell height) drop the card;
|
|
1015
1023
|
// the title then lives in the shell header when the shell offers a slot.
|
|
1016
1024
|
const isScreenForm = fillScreenHeight === true && hideSectionTitles !== true && !insideDrawer;
|
|
@@ -1089,7 +1097,10 @@ export function RenderEdit(props) {
|
|
|
1089
1097
|
// draw a no-op Save. Extension registrations still get their Save, since
|
|
1090
1098
|
// persistExtensions() runs on the customSubmit path too.
|
|
1091
1099
|
const isFieldlessSubmitForm = isFieldless && screen.entity !== PROJECTION_DETAIL_ENTITY;
|
|
1092
|
-
const showsSubmit = isFormEditable ||
|
|
1100
|
+
const showsSubmit = isFormEditable ||
|
|
1101
|
+
hasExtensionRegistrations ||
|
|
1102
|
+
isFieldlessSubmitForm ||
|
|
1103
|
+
submitWithoutChanges !== undefined;
|
|
1093
1104
|
const footerSlot = screen.slots?.footer;
|
|
1094
1105
|
// An unregistered footer component renders null, so only a resolvable one may
|
|
1095
1106
|
// count as an action (else read-only screens get an empty footer strip).
|
|
@@ -1131,7 +1142,11 @@ export function RenderEdit(props) {
|
|
|
1131
1142
|
footerActions?.map((action) => (_jsx(Button, { type: "button", variant: action.variant ?? "secondary", ...(action.icon !== undefined && { icon: action.icon }), disabled: isSubmitting || disabled, onClick: () => {
|
|
1132
1143
|
controller.setValues(action.patch); // @cast-boundary form-values
|
|
1133
1144
|
void handleSubmit();
|
|
1134
|
-
}, testId: `render-edit-footer-action-${action.id}`, children: translate(action.label) }, action.id))), hasDirtyFooter && showsSubmit && unsavedCount !== undefined && unsavedCount > 0 && (_jsx(Button, { type: "button", variant: "ghost", disabled: isSubmitting || disabled, onClick: () => controller.reset(), testId: "render-edit-discard", [STICKY_PRIMARY_ACTION_PROP]: true, children: translate("kumiko.form.discard") })), showSubmit && (_jsx(Button, { type: "submit", disabled: (snapshot.isUnchanged &&
|
|
1145
|
+
}, testId: `render-edit-footer-action-${action.id}`, children: translate(action.label) }, action.id))), hasDirtyFooter && showsSubmit && unsavedCount !== undefined && unsavedCount > 0 && (_jsx(Button, { type: "button", variant: "ghost", disabled: isSubmitting || disabled, onClick: () => controller.reset(), testId: "render-edit-discard", [STICKY_PRIMARY_ACTION_PROP]: true, children: translate("kumiko.form.discard") })), showSubmit && (_jsx(Button, { type: "submit", disabled: (snapshot.isUnchanged &&
|
|
1146
|
+
!extensionDirty &&
|
|
1147
|
+
!isFieldlessSubmitForm &&
|
|
1148
|
+
!insideDrawer &&
|
|
1149
|
+
submitWithoutChanges !== true) ||
|
|
1135
1150
|
isSubmitting ||
|
|
1136
1151
|
disabled, loading: isSubmitting, variant: submitVariant ?? "primary", testId: "render-edit-submit", children: translate(submitLabel ??
|
|
1137
1152
|
(isWizard
|
package/dist/i18n-defaults.js
CHANGED
|
@@ -209,6 +209,7 @@ export const kumikoDefaultTranslations = {
|
|
|
209
209
|
"kumiko.secretMint.copied": "Copied",
|
|
210
210
|
"kumiko.secretMint.done": "Done.",
|
|
211
211
|
"kumiko.validation.required": "Required.",
|
|
212
|
+
"kumiko.validation.optionNotAvailable": "Not available for the current selection.",
|
|
212
213
|
"kumiko.validation.invalid": "Invalid value.",
|
|
213
214
|
"kumiko.validation.too-short": "Too short (at least {min} characters).",
|
|
214
215
|
"kumiko.validation.too-long": "Too long (at most {max} characters).",
|
package/dist/i18n.d.ts
CHANGED
|
@@ -11,6 +11,9 @@ export type TranslationsByLocale = Readonly<Record<string, TranslationBundle>>;
|
|
|
11
11
|
export type TranslationsByKey = Readonly<Record<string, Readonly<Record<string, TranslationValue>>>>;
|
|
12
12
|
/** Pivot key-first server translations to locale-first client bundles. */
|
|
13
13
|
export declare function translationsByLocaleFromKeys(source: TranslationsByKey): TranslationsByLocale;
|
|
14
|
+
/** Inverse of `translationsByLocaleFromKeys`: pivot locale-first bundles to the key-first
|
|
15
|
+
* shape `r.translations({ keys })` expects, so one source feeds server and client. */
|
|
16
|
+
export declare function translationsByKeyFromLocales(source: TranslationsByLocale): TranslationsByKey;
|
|
14
17
|
/** Merged zwei TranslationsByLocale-Maps — der override gewinnt pro Key,
|
|
15
18
|
* die Locales werden zusammengeführt. Standard-Baustein für Client-
|
|
16
19
|
* Plugins, die App-Overrides über ihre Default-Bundles legen. */
|
package/dist/i18n.js
CHANGED
|
@@ -27,6 +27,18 @@ export function translationsByLocaleFromKeys(source) {
|
|
|
27
27
|
}
|
|
28
28
|
return out;
|
|
29
29
|
}
|
|
30
|
+
/** Inverse of `translationsByLocaleFromKeys`: pivot locale-first bundles to the key-first
|
|
31
|
+
* shape `r.translations({ keys })` expects, so one source feeds server and client. */
|
|
32
|
+
export function translationsByKeyFromLocales(source) {
|
|
33
|
+
const out = {};
|
|
34
|
+
for (const [locale, bundle] of Object.entries(source)) {
|
|
35
|
+
for (const [key, value] of Object.entries(bundle)) {
|
|
36
|
+
out[key] ??= {};
|
|
37
|
+
out[key][locale] = value;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
30
42
|
/** Merged zwei TranslationsByLocale-Maps — der override gewinnt pro Key,
|
|
31
43
|
* die Locales werden zusammengeführt. Standard-Baustein für Client-
|
|
32
44
|
* Plugins, die App-Overrides über ihre Default-Bundles legen. */
|
package/dist/index.d.ts
CHANGED
|
@@ -67,7 +67,7 @@ export { useStore, useStoreSelector } from "./hooks/use-store.js";
|
|
|
67
67
|
export type { StreamStatus, UseStreamHandlerOptions, UseStreamHandlerResult, } from "./hooks/use-stream-handler.js";
|
|
68
68
|
export { useStreamHandler } from "./hooks/use-stream-handler.js";
|
|
69
69
|
export type { LocaleProviderProps, TranslationBundle, TranslationsByKey, TranslationsByLocale, } from "./i18n.js";
|
|
70
|
-
export { createStaticLocaleResolver, FormalityProvider, LocaleProvider, mergeTranslations, translationsByLocaleFromKeys, useFormality, useLocale, useOptionalLocale, useOptionalTranslation, useTranslation, } from "./i18n.js";
|
|
70
|
+
export { createStaticLocaleResolver, FormalityProvider, LocaleProvider, mergeTranslations, translationsByKeyFromLocales, translationsByLocaleFromKeys, useFormality, useLocale, useOptionalLocale, useOptionalTranslation, useTranslation, } from "./i18n.js";
|
|
71
71
|
export { kumikoDefaultTranslations } from "./i18n-defaults.js";
|
|
72
72
|
export { InsideDrawerProvider, useInsideDrawer } from "./inside-drawer.js";
|
|
73
73
|
export { PageHeaderCompactProvider, PageHeaderSlotAvailableProvider, usePageHeaderCompact, usePageHeaderSlotAvailable, } from "./page-header-slot.js";
|
package/dist/index.js
CHANGED
|
@@ -48,7 +48,7 @@ export { entityFromQueryType, useQuery } from "./hooks/use-query.js";
|
|
|
48
48
|
export { useReportStepComplete } from "./hooks/use-report-step-complete.js";
|
|
49
49
|
export { useStore, useStoreSelector } from "./hooks/use-store.js";
|
|
50
50
|
export { useStreamHandler } from "./hooks/use-stream-handler.js";
|
|
51
|
-
export { createStaticLocaleResolver, FormalityProvider, LocaleProvider, mergeTranslations, translationsByLocaleFromKeys, useFormality, useLocale, useOptionalLocale, useOptionalTranslation, useTranslation, } from "./i18n.js";
|
|
51
|
+
export { createStaticLocaleResolver, FormalityProvider, LocaleProvider, mergeTranslations, translationsByKeyFromLocales, translationsByLocaleFromKeys, useFormality, useLocale, useOptionalLocale, useOptionalTranslation, useTranslation, } from "./i18n.js";
|
|
52
52
|
export { kumikoDefaultTranslations } from "./i18n-defaults.js";
|
|
53
53
|
export { InsideDrawerProvider, useInsideDrawer } from "./inside-drawer.js";
|
|
54
54
|
export { PageHeaderCompactProvider, PageHeaderSlotAvailableProvider, usePageHeaderCompact, usePageHeaderSlotAvailable, } from "./page-header-slot.js";
|
package/dist/primitives.d.ts
CHANGED
|
@@ -28,7 +28,14 @@ export type FooterActionMarker = {
|
|
|
28
28
|
* und sollte mit `disabled` kombiniert werden, wenn die Action wirklich
|
|
29
29
|
* blockiert bis das Loading durch ist (z.B. async submit). Native-
|
|
30
30
|
* Impls können den Spinner als Activity-Indicator rendern. */
|
|
31
|
-
export type ButtonProps = {
|
|
31
|
+
export type ButtonProps = ButtonBaseProps & ({
|
|
32
|
+
readonly children: ReactNode;
|
|
33
|
+
} | {
|
|
34
|
+
readonly children?: undefined;
|
|
35
|
+
readonly icon: NavIconKey;
|
|
36
|
+
readonly ariaLabel: string;
|
|
37
|
+
});
|
|
38
|
+
type ButtonBaseProps = {
|
|
32
39
|
readonly type?: "button" | "submit";
|
|
33
40
|
readonly onClick?: () => void | Promise<void>;
|
|
34
41
|
readonly disabled?: boolean;
|
|
@@ -59,11 +66,6 @@ export type ButtonProps = {
|
|
|
59
66
|
* die Container-Breite (Karten/Panels). Andere Breiten sind Layout-Sache
|
|
60
67
|
* des Containers, kein Button-Prop (Kit hält arbiträres Sizing draußen). */
|
|
61
68
|
readonly width?: "full" | "auto";
|
|
62
|
-
/** Optional for icon-only buttons (`size="icon"` with a resolved `icon`)
|
|
63
|
-
* — the icon carries the content then, `ariaLabel` the accessible name.
|
|
64
|
-
* Required in practice otherwise: without children and without `icon`
|
|
65
|
-
* the button stays empty. */
|
|
66
|
-
readonly children?: ReactNode;
|
|
67
69
|
readonly testId?: string;
|
|
68
70
|
/** Layout extras — Web merges via cn(), native impls ignore it
|
|
69
71
|
* (precedent: LinkProps.className). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.354.1",
|
|
4
4
|
"description": "Platform-agnostic React renderer for Kumiko screens. Contains the shared logic — primitives-contract, hooks, KumikoScreen, navigation & SSE abstractions — that any platform-specific renderer (web, native) composes. No DOM, no EventSource, no react-dom.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
19
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
20
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
18
|
+
"@cosmicdrift/kumiko-framework": "0.354.1",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.354.1",
|
|
20
|
+
"@cosmicdrift/kumiko-types": "0.354.1",
|
|
21
21
|
"react": "^19.2.6",
|
|
22
22
|
"temporal-polyfill": "^0.3.2",
|
|
23
23
|
"zod": "^4.4.3"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@types/react-dom": "^19.2.3",
|
|
29
29
|
"jsdom": "^29.1.1",
|
|
30
30
|
"react-dom": "^19.2.6",
|
|
31
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
31
|
+
"@cosmicdrift/kumiko-locale-de": "0.354.1"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,41 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.354.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Navigate no longer injects the current id into entityEdit targets of another entity",
|
|
6
|
+
"detail": "Navigate actions no longer inject the current record's id into entityEdit targets of another entity; entityList rowActions now recognise same-entity entityEdit targets across features and open them in UPDATE mode"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.354.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "translationsByKeyFromLocales export and email-template subpath exports",
|
|
12
|
+
"detail": "Renderer exports translationsByKeyFromLocales (inverse of translationsByLocaleFromKeys); bundled-features exposes the auth-email-password and user-data-rights email-templates as subpath exports"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.354.0",
|
|
16
|
+
"type": "breaking",
|
|
17
|
+
"title": "Grid columns is number | \"auto\"",
|
|
18
|
+
"detail": "Final review batch F: renderer, renderer-web, types\nThe boot validator rejects a `listScreenId` that matches no mounted screen, a `sortable` column that the list cannot sort (entityList: any value; projectionList: `true`) and an authored `urlPrefillFields`. `TreeAction` is a union: exactly one of `screen` or `target`. `Button` without children needs `icon` and `ariaLabel`. `RenderEditControls` gains `next` and `back`, and `controls.submit()` saves on an intermediate wizard step. An actionForm money field with a tenant currency that cannot be loaded shows an error instead of submitting with a guessed EUR. The MFA setup and recovery-code buttons sit below the content. The boot context passes Redis to the delivery service, so `notify` with an `idempotencyKey` dedupes. `user:query:user:detail` skips the tenants label when the tenant feature is not mounted.",
|
|
19
|
+
"migration": "Pass a number or \"auto\" to Grid columns; other values no longer type-check."
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"version": "0.354.0",
|
|
23
|
+
"type": "breaking",
|
|
24
|
+
"title": "Button without children requires icon and ariaLabel",
|
|
25
|
+
"migration": "A custom wrapper that forwards ButtonProps must pass either children or both icon and ariaLabel."
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"version": "0.354.0",
|
|
29
|
+
"type": "fix",
|
|
30
|
+
"title": "RenderEditControls has next and back; submit saves on an intermediate wizard step",
|
|
31
|
+
"migration": "No action needed. A host that renders its own wizard buttons with hideActions can call controls.next(), controls.back() and controls.submit()."
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"version": "0.354.0",
|
|
35
|
+
"type": "fix",
|
|
36
|
+
"title": "actionForm money fields with a tenant currency block the form when the currency cannot be loaded",
|
|
37
|
+
"migration": "No action needed. secretMint and the reference create dialog keep their fallback currency."
|
|
38
|
+
},
|
|
2
39
|
{
|
|
3
40
|
"version": "0.353.0",
|
|
4
41
|
"type": "fix",
|