@cosmicdrift/kumiko-renderer 0.356.0 → 0.357.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/kumiko-screen.js +24 -7
- package/dist/app/row-actions.d.ts +8 -0
- package/dist/app/row-actions.js +30 -8
- package/dist/app/secret-mint-body.js +8 -4
- package/dist/components/availability-options-select.d.ts +8 -0
- package/dist/components/availability-options-select.js +16 -0
- package/dist/components/related-list-section.d.ts +4 -1
- package/dist/components/related-list-section.js +9 -2
- package/dist/components/render-edit-types.d.ts +9 -0
- package/dist/components/render-edit.js +16 -4
- package/dist/components/render-field.js +6 -10
- package/dist/components/write-form-section.d.ts +5 -6
- package/dist/components/write-form-section.js +8 -6
- package/dist/i18n-defaults.js +2 -0
- package/dist/primitives.d.ts +2 -0
- package/package.json +5 -5
- package/src/changes.json +29 -0
|
@@ -799,6 +799,7 @@ function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onRelo
|
|
|
799
799
|
version: recordVersion,
|
|
800
800
|
[UPDATE_CHANGES_KEY]: snap.changes,
|
|
801
801
|
}), [entityId, recordVersion]);
|
|
802
|
+
const [formDirty, setFormDirty] = useState(false);
|
|
802
803
|
const nav = useNav();
|
|
803
804
|
const appFeatures = useAppFeatures();
|
|
804
805
|
const dispatcher = useDispatcher();
|
|
@@ -866,7 +867,14 @@ function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onRelo
|
|
|
866
867
|
onRecordLeft: handleRecordLeft,
|
|
867
868
|
sameEntityScreenId,
|
|
868
869
|
defaultWritePayloadId: entityId,
|
|
870
|
+
...(formDirty && {
|
|
871
|
+
discardChanges: {
|
|
872
|
+
confirm: effectiveTranslate("kumiko.form.unsavedChangesActionConfirm"),
|
|
873
|
+
confirmLabel: effectiveTranslate("kumiko.form.unsavedChangesActionConfirmLabel"),
|
|
874
|
+
},
|
|
875
|
+
}),
|
|
869
876
|
}), [
|
|
877
|
+
formDirty,
|
|
870
878
|
record,
|
|
871
879
|
effectiveTranslate,
|
|
872
880
|
nav,
|
|
@@ -928,7 +936,7 @@ function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onRelo
|
|
|
928
936
|
entityId: entityId,
|
|
929
937
|
// Stored customFields for the extension section, so editing shows the
|
|
930
938
|
// persisted values instead of behaving write-only.
|
|
931
|
-
extensionInitialValues: extensionInitialValues, schema: formSchema, writeCommand: writeCommand, payloadMode: "changes", buildPayload: buildPayload, serverFieldPathPrefix: `${UPDATE_CHANGES_KEY}.`, onSubmit: handleSubmitted, ...(screen.allowDelete !== false && { onDelete: handleDelete }), onCancel: handleCancel, onReload: () => void onReload(), ...(screenFillsHeight(screen) && { fillScreenHeight: true }), ...(screen.submitLabel !== undefined && { submitLabel: screen.submitLabel }), ...(translate !== undefined && { translate }), ...(onCopyLink !== undefined && { onCopyLink }), ...(headerActions !== undefined && { actions: headerActions }), ...(headerMenuActions !== undefined && { menuActions: headerMenuActions }), buildSectionActions: buildSectionActions }), _jsx(DrawerHost, { schema: schema, drawerAction: drawerAction, drawerScreen: drawerScreen, ...(drawerInitialValues !== undefined && { drawerInitialValues }), userRoles: userRoles, ...(translate !== undefined && { translate }), onClose: closeDrawer, onSuccess: () => {
|
|
939
|
+
extensionInitialValues: extensionInitialValues, schema: formSchema, writeCommand: writeCommand, payloadMode: "changes", buildPayload: buildPayload, serverFieldPathPrefix: `${UPDATE_CHANGES_KEY}.`, onSubmit: handleSubmitted, onDirtyChange: setFormDirty, ...(screen.allowDelete !== false && { onDelete: handleDelete }), onCancel: handleCancel, onReload: () => void onReload(), ...(screenFillsHeight(screen) && { fillScreenHeight: true }), ...(screen.submitLabel !== undefined && { submitLabel: screen.submitLabel }), ...(translate !== undefined && { translate }), ...(onCopyLink !== undefined && { onCopyLink }), ...(headerActions !== undefined && { actions: headerActions }), ...(headerMenuActions !== undefined && { menuActions: headerMenuActions }), buildSectionActions: buildSectionActions }), _jsx(DrawerHost, { schema: schema, drawerAction: drawerAction, drawerScreen: drawerScreen, ...(drawerInitialValues !== undefined && { drawerInitialValues }), userRoles: userRoles, ...(translate !== undefined && { translate }), onClose: closeDrawer, onSuccess: () => {
|
|
932
940
|
closeDrawer();
|
|
933
941
|
void onReload();
|
|
934
942
|
} })] }));
|
|
@@ -1605,7 +1613,14 @@ function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
|
|
|
1605
1613
|
// carry a value — a stale/hand-crafted URL (?…q=x on a non-searchable
|
|
1606
1614
|
// screen) must not smuggle a param the query's Zod schema doesn't accept.
|
|
1607
1615
|
const activeSearch = searchable ? urlState.q : "";
|
|
1608
|
-
|
|
1616
|
+
// Same predicate that makes a header clickable: a stale link onto a
|
|
1617
|
+
// sortable:false/unknown column must not reach the server (it answers 400).
|
|
1618
|
+
const urlSort = urlState.sort;
|
|
1619
|
+
const urlSortField = urlSort === null ? undefined : entity.fields[urlSort.field];
|
|
1620
|
+
const urlSortAllowed = urlSortField !== undefined && "sortable" in urlSortField && urlSortField.sortable === true;
|
|
1621
|
+
const activeSort = sortable
|
|
1622
|
+
? ((urlSortAllowed ? urlSort : null) ?? screen.defaultSort ?? null)
|
|
1623
|
+
: null;
|
|
1609
1624
|
const limit = urlState.pageSize ?? screen.pageSize ?? 50;
|
|
1610
1625
|
// Pages-mode only (see header comment) — an author-set pagination:
|
|
1611
1626
|
// "infinite" on a paginated projectionList is silently a no-op today.
|
|
@@ -1959,9 +1974,11 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
1959
1974
|
// A writeForm section's handler creates a new record (see EditWriteFormSection's
|
|
1960
1975
|
// doc) — both the outer record (header/metrics/other sections) and a sibling
|
|
1961
1976
|
// relatedList section's own independently-fetched rows must reflect it. A
|
|
1962
|
-
// query refetch alone only refreshes `record` below; bumping this
|
|
1963
|
-
// <RenderEdit>'s
|
|
1964
|
-
//
|
|
1977
|
+
// query refetch alone only refreshes `record` below; bumping this as
|
|
1978
|
+
// <RenderEdit>'s refreshToken makes RelatedListSection's own useQuery
|
|
1979
|
+
// re-run too (it has no `live` subscription of its own). Refetch instead of
|
|
1980
|
+
// remount (a `key`) so unsaved input in the other writeForm sections and
|
|
1981
|
+
// the related lists' local state survive.
|
|
1965
1982
|
const [reloadNonce, setReloadNonce] = useState(0);
|
|
1966
1983
|
const reloadDetail = useCallback(async () => {
|
|
1967
1984
|
await refetchAfterWrite(detailQuery.refetch);
|
|
@@ -2217,12 +2234,12 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
2217
2234
|
...(count !== undefined && { count }),
|
|
2218
2235
|
};
|
|
2219
2236
|
}), activeId: activeSection.id ?? "", onSelect: (id) => nav.setSearchParams({ tab: id }) }))] }));
|
|
2220
|
-
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 ||
|
|
2237
|
+
return (_jsxs(_Fragment, { children: [_jsx(RenderEdit, { refreshToken: reloadNonce, 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 ||
|
|
2221
2238
|
hasTabs ||
|
|
2222
2239
|
screen.slots?.header !== undefined ||
|
|
2223
2240
|
screen.recordTitleField !== undefined) && {
|
|
2224
2241
|
headerRegion: renderHeaderContent,
|
|
2225
|
-
}), buildSectionActions: buildSectionActions, valueDisplay: screen.valueDisplay ?? "text" },
|
|
2242
|
+
}), buildSectionActions: buildSectionActions, valueDisplay: screen.valueDisplay ?? "text" }, entityId), _jsx(DrawerHost, { schema: schema, drawerAction: drawerAction, drawerScreen: drawerScreen, ...(drawerInitialValues !== undefined && { drawerInitialValues }), userRoles: userRoles, ...(translate !== undefined && { translate }), onClose: closeDrawer, onSuccess: () => {
|
|
2226
2243
|
closeDrawer();
|
|
2227
2244
|
void reloadDetail();
|
|
2228
2245
|
} })] }));
|
|
@@ -53,6 +53,10 @@ export declare function buildProjectionToolbarActions(options: {
|
|
|
53
53
|
readonly record?: Readonly<Record<string, unknown>>;
|
|
54
54
|
readonly host: ReturnHost | undefined;
|
|
55
55
|
}): readonly ToolbarActionButton[] | undefined;
|
|
56
|
+
export type DiscardChanges = {
|
|
57
|
+
readonly confirm: string;
|
|
58
|
+
readonly confirmLabel: string;
|
|
59
|
+
};
|
|
56
60
|
export declare function deletesShownRecord(action: RowActionWriteHandler, payload: Readonly<Record<string, unknown>>, shownRecordId: unknown): boolean;
|
|
57
61
|
export type RecordLeftHandler = (action: RowActionWriteHandler, resultData: unknown, deletedShownRecord: boolean) => void;
|
|
58
62
|
export declare function buildRecordActions(options: {
|
|
@@ -85,5 +89,9 @@ export declare function buildRecordActions(options: {
|
|
|
85
89
|
* caller editing a fixed entity passes its route id so the payload does
|
|
86
90
|
* not depend on the loaded record carrying an `id` field. */
|
|
87
91
|
readonly defaultWritePayloadId?: string;
|
|
92
|
+
/** Translated hint added to the confirm of every writeHandler and drawer
|
|
93
|
+
* action (they reload or remount the surrounding form). Set while that
|
|
94
|
+
* form holds unsaved input so it is not lost silently. */
|
|
95
|
+
readonly discardChanges?: DiscardChanges;
|
|
88
96
|
}): readonly RenderEditAction[] | undefined;
|
|
89
97
|
export {};
|
package/dist/app/row-actions.js
CHANGED
|
@@ -341,14 +341,27 @@ function buildNavigateRecordAction(action, options) {
|
|
|
341
341
|
return buildNavigateScreenAction(action, action.screen, options);
|
|
342
342
|
return undefined;
|
|
343
343
|
}
|
|
344
|
+
// The action's own confirm text stays first; the discard hint follows after a
|
|
345
|
+
// blank line so both show in the single confirm dialog.
|
|
346
|
+
function withDiscardHint(ownConfirm, discardChanges) {
|
|
347
|
+
if (discardChanges === undefined)
|
|
348
|
+
return ownConfirm;
|
|
349
|
+
return ownConfirm === undefined
|
|
350
|
+
? discardChanges.confirm
|
|
351
|
+
: `${ownConfirm}\n\n${discardChanges.confirm}`;
|
|
352
|
+
}
|
|
344
353
|
function buildDrawerRecordAction(action, options) {
|
|
345
|
-
const { record, translate, actionIcon, openDrawer } = options;
|
|
354
|
+
const { record, translate, actionIcon, openDrawer, discardChanges } = options;
|
|
346
355
|
return {
|
|
347
356
|
id: action.id,
|
|
348
357
|
label: translate(action.label),
|
|
349
358
|
...(action.style !== undefined && { style: action.style }),
|
|
350
359
|
...(action.display !== undefined && { display: action.display }),
|
|
351
360
|
confirmRequired: false,
|
|
361
|
+
...(discardChanges !== undefined && {
|
|
362
|
+
confirm: discardChanges.confirm,
|
|
363
|
+
confirmLabel: discardChanges.confirmLabel,
|
|
364
|
+
}),
|
|
352
365
|
...(actionIcon !== undefined && { icon: actionIcon }),
|
|
353
366
|
onPress: () => {
|
|
354
367
|
const initialValues = action.params !== undefined ? evalRowExtractor(action.params, record) : undefined;
|
|
@@ -365,18 +378,20 @@ export function deletesShownRecord(action, payload, shownRecordId) {
|
|
|
365
378
|
payload["id"] === shownRecordId);
|
|
366
379
|
}
|
|
367
380
|
function buildWriteHandlerRecordAction(action, options) {
|
|
368
|
-
const { record, translate, actionIcon, dispatcher, onWriteSuccess, defaultWritePayloadId, onRecordLeft, } = options;
|
|
381
|
+
const { record, translate, actionIcon, dispatcher, onWriteSuccess, defaultWritePayloadId, onRecordLeft, discardChanges, } = options;
|
|
369
382
|
const shownRecordId = defaultWritePayloadId ?? record["id"];
|
|
383
|
+
const confirm = withDiscardHint(action.confirm !== undefined ? translate(action.confirm) : undefined, discardChanges);
|
|
384
|
+
// The discard label wins: the action then also throws away the unsaved input.
|
|
385
|
+
const confirmLabel = discardChanges?.confirmLabel ??
|
|
386
|
+
(action.confirmLabel !== undefined ? translate(action.confirmLabel) : undefined);
|
|
370
387
|
return {
|
|
371
388
|
id: action.id,
|
|
372
389
|
label: translate(action.label),
|
|
373
390
|
...(action.style !== undefined && { style: action.style }),
|
|
374
391
|
...(action.display !== undefined && { display: action.display }),
|
|
375
392
|
...(actionIcon !== undefined && { icon: actionIcon }),
|
|
376
|
-
...(
|
|
377
|
-
...(
|
|
378
|
-
confirmLabel: translate(action.confirmLabel),
|
|
379
|
-
}),
|
|
393
|
+
...(confirm !== undefined && { confirm }),
|
|
394
|
+
...(confirmLabel !== undefined && { confirmLabel }),
|
|
380
395
|
onPress: async () => {
|
|
381
396
|
const payload = action.payload !== undefined
|
|
382
397
|
? evalRowExtractor(action.payload, record)
|
|
@@ -402,7 +417,7 @@ function buildWriteHandlerRecordAction(action, options) {
|
|
|
402
417
|
// resolved against the record currently being viewed/edited, independent of
|
|
403
418
|
// any table row.
|
|
404
419
|
export function buildRecordActions(options) {
|
|
405
|
-
const { actions, record, translate, nav, host, dispatcher, openDrawer, onWriteSuccess, onRecordLeft, defaultScreenTargetEntityId, sameEntityScreenId, defaultWritePayloadId, } = options;
|
|
420
|
+
const { actions, record, translate, nav, host, dispatcher, openDrawer, onWriteSuccess, onRecordLeft, defaultScreenTargetEntityId, sameEntityScreenId, defaultWritePayloadId, discardChanges, } = options;
|
|
406
421
|
const out = [];
|
|
407
422
|
for (const action of actions) {
|
|
408
423
|
if (action.visible !== undefined && !evalFieldCondition(action.visible, record))
|
|
@@ -423,7 +438,13 @@ export function buildRecordActions(options) {
|
|
|
423
438
|
continue;
|
|
424
439
|
}
|
|
425
440
|
if (action.kind === "drawer") {
|
|
426
|
-
out.push(buildDrawerRecordAction(action, {
|
|
441
|
+
out.push(buildDrawerRecordAction(action, {
|
|
442
|
+
record,
|
|
443
|
+
translate,
|
|
444
|
+
actionIcon,
|
|
445
|
+
openDrawer,
|
|
446
|
+
discardChanges,
|
|
447
|
+
}));
|
|
427
448
|
continue;
|
|
428
449
|
}
|
|
429
450
|
// writeHandler — skip without a dispatcher instead of crashing.
|
|
@@ -437,6 +458,7 @@ export function buildRecordActions(options) {
|
|
|
437
458
|
onWriteSuccess,
|
|
438
459
|
defaultWritePayloadId,
|
|
439
460
|
onRecordLeft,
|
|
461
|
+
discardChanges,
|
|
440
462
|
}));
|
|
441
463
|
}
|
|
442
464
|
return out.length > 0 ? out : undefined;
|
|
@@ -47,7 +47,7 @@ function isBlank(value) {
|
|
|
47
47
|
export function SecretMintBody({ schema, screen, translate }) {
|
|
48
48
|
const nav = useNav();
|
|
49
49
|
const returnTarget = useReturnTarget(screen.id);
|
|
50
|
-
const { Card, Heading, Banner, Button, Text, Grid, GridCell, Section, SecretReveal } = usePrimitives();
|
|
50
|
+
const { Card, Heading, Banner, Button, Text, Grid, GridCell, Section, SecretReveal, Field, Input, } = usePrimitives();
|
|
51
51
|
const t = useTranslation();
|
|
52
52
|
const effectiveTranslate = translate ?? t;
|
|
53
53
|
const synthEntity = useMemo(() => synthesizeActionFormEntity(screen.fields), [screen.fields]);
|
|
@@ -75,6 +75,7 @@ export function SecretMintBody({ schema, screen, translate }) {
|
|
|
75
75
|
]);
|
|
76
76
|
const [revealed, setRevealed] = useState(null);
|
|
77
77
|
const [done, setDone] = useState(false);
|
|
78
|
+
const [acknowledged, setAcknowledged] = useState(false);
|
|
78
79
|
// The secret was minted server-side but none of the declared reveal fields
|
|
79
80
|
// came back; the mint form must not return (a re-mint would invalidate it).
|
|
80
81
|
const [revealMissing, setRevealMissing] = useState(false);
|
|
@@ -108,6 +109,7 @@ export function SecretMintBody({ schema, screen, translate }) {
|
|
|
108
109
|
setRevealMissing(true);
|
|
109
110
|
return;
|
|
110
111
|
}
|
|
112
|
+
setAcknowledged(false);
|
|
111
113
|
setRevealed(values);
|
|
112
114
|
carriedRef.current =
|
|
113
115
|
confirm?.carry !== undefined ? extractCarriedValues(result.data, confirm.carry) : {};
|
|
@@ -175,7 +177,9 @@ export function SecretMintBody({ schema, screen, translate }) {
|
|
|
175
177
|
},
|
|
176
178
|
];
|
|
177
179
|
});
|
|
178
|
-
const
|
|
180
|
+
const acknowledgeKey = screen.reveal.acknowledge;
|
|
181
|
+
const acknowledgeBlocked = acknowledgeKey !== undefined && !acknowledged;
|
|
182
|
+
const revealContent = (_jsxs(_Fragment, { children: [_jsx(Heading, { variant: "page", children: effectiveTranslate(screen.reveal.title ?? "kumiko.secretMint.title") }), _jsx(Banner, { variant: "warning", testId: "kumiko-screen-secret-mint-warning", children: effectiveTranslate(screen.reveal.warning ?? "kumiko.secretMint.warning") }), SecretReveal !== undefined ? (_jsx(SecretReveal, { values: values, copyLabel: effectiveTranslate("kumiko.secretMint.copy"), copiedLabel: effectiveTranslate("kumiko.secretMint.copied"), testId: "kumiko-screen-secret-mint-reveal" })) : (_jsx(Grid, { columns: 1, testId: "kumiko-screen-secret-mint-reveal", children: values.map((v) => (_jsxs(GridCell, { children: [_jsx(Text, { children: v.label }), _jsx(Text, { variant: "code", children: v.value })] }, v.label))) })), acknowledgeKey !== undefined && (_jsx(Field, { id: "secret-mint-acknowledge", label: effectiveTranslate(acknowledgeKey), layout: "inline", testId: "secret-mint-acknowledge", children: _jsx(Input, { kind: "boolean", id: "secret-mint-acknowledge", name: "secret-mint-acknowledge", value: acknowledged, onChange: setAcknowledged }) }))] }));
|
|
179
183
|
// One screen form like the mint phase: the reveal leads the form column
|
|
180
184
|
// instead of the confirm form nesting a second card inside a card. The
|
|
181
185
|
// keys stop the mint and confirm forms sharing one instance (and its
|
|
@@ -186,9 +190,9 @@ export function SecretMintBody({ schema, screen, translate }) {
|
|
|
186
190
|
if (confirmCurrencyLoading) {
|
|
187
191
|
return (_jsx(Banner, { padded: true, variant: "loading", testId: "kumiko-screen-loading", children: "Loading\u2026" }));
|
|
188
192
|
}
|
|
189
|
-
return (_jsx(RenderEdit, { screen: confirmScreen, i18nScreenId: screen.id, entity: confirmEntity, featureName: schema.featureName, initial: confirmInitial ?? {}, writeCommand: confirm.handler, payloadMode: "values", buildPayload: (snapshot) => ({ ...snapshot.values, ...carriedRef.current }), onSubmit: handleConfirmSubmitted, onCancel: handleCancel ?? restartMint, fillScreenHeight: true, leadContent: _jsx(Section, { testId: "kumiko-screen-secret-mint-card", children: revealContent }), ...(translate !== undefined && { translate }), ...(confirm.submitLabel !== undefined && { submitLabel: confirm.submitLabel }) }, "confirm"));
|
|
193
|
+
return (_jsx(RenderEdit, { screen: confirmScreen, i18nScreenId: screen.id, entity: confirmEntity, featureName: schema.featureName, initial: confirmInitial ?? {}, writeCommand: confirm.handler, payloadMode: "values", buildPayload: (snapshot) => ({ ...snapshot.values, ...carriedRef.current }), onSubmit: handleConfirmSubmitted, onCancel: handleCancel ?? restartMint, fillScreenHeight: true, submitBlocked: acknowledgeBlocked, leadContent: _jsx(Section, { testId: "kumiko-screen-secret-mint-card", children: revealContent }), ...(translate !== undefined && { translate }), ...(confirm.submitLabel !== undefined && { submitLabel: confirm.submitLabel }) }, "confirm"));
|
|
190
194
|
}
|
|
191
|
-
return (_jsxs(Card, { options: { screenBody: true }, testId: "kumiko-screen-secret-mint-card", children: [revealContent, _jsx(Button, { type: "button", variant: "primary", onClick: finishMint, testId: "kumiko-screen-secret-mint-confirm", children: effectiveTranslate(screen.reveal.confirmLabel ?? "kumiko.secretMint.confirm") })] }));
|
|
195
|
+
return (_jsxs(Card, { options: { screenBody: true }, testId: "kumiko-screen-secret-mint-card", children: [revealContent, _jsx(Button, { type: "button", variant: "primary", onClick: finishMint, disabled: acknowledgeBlocked, testId: "kumiko-screen-secret-mint-confirm", children: effectiveTranslate(screen.reveal.confirmLabel ?? "kumiko.secretMint.confirm") })] }));
|
|
192
196
|
}
|
|
193
197
|
if (mintCurrencyLoading) {
|
|
194
198
|
return (_jsx(Banner, { padded: true, variant: "loading", testId: "kumiko-screen-loading", children: "Loading\u2026" }));
|
|
@@ -9,6 +9,14 @@ export type SelectOptionEntry = {
|
|
|
9
9
|
};
|
|
10
10
|
export declare function staticSelectOptions(field: EditFieldViewModel): readonly SelectOptionEntry[];
|
|
11
11
|
export declare function mergeOptionAvailability(options: readonly SelectOptionEntry[], rows: readonly unknown[], current: string): readonly SelectOptionEntry[];
|
|
12
|
+
export declare function filterUnavailableOptions(options: readonly SelectOptionEntry[], rows: readonly unknown[], currentValues: readonly string[]): readonly SelectOptionEntry[];
|
|
13
|
+
type AvailabilityOptionsMultiSelectProps = {
|
|
14
|
+
readonly field: EditFieldViewModel;
|
|
15
|
+
readonly query: string;
|
|
16
|
+
readonly currentValues: readonly string[];
|
|
17
|
+
readonly children: (options: readonly SelectOptionEntry[]) => ReactNode;
|
|
18
|
+
};
|
|
19
|
+
export declare function AvailabilityOptionsMultiSelect({ field, query, currentValues, children, }: AvailabilityOptionsMultiSelectProps): ReactNode;
|
|
12
20
|
type AvailabilityOptionsSelectProps = {
|
|
13
21
|
readonly field: EditFieldViewModel;
|
|
14
22
|
readonly query: string;
|
|
@@ -36,6 +36,22 @@ export function mergeOptionAvailability(options, rows, current) {
|
|
|
36
36
|
};
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
|
+
// Combobox primitives cannot disable a single option, so unavailable ones are
|
|
40
|
+
// dropped; an already selected value stays listed so it can still be removed.
|
|
41
|
+
export function filterUnavailableOptions(options, rows, currentValues) {
|
|
42
|
+
const unavailable = new Set();
|
|
43
|
+
for (const row of rows) {
|
|
44
|
+
if (isAvailabilityRow(row) && row.disabled === true)
|
|
45
|
+
unavailable.add(row.value);
|
|
46
|
+
}
|
|
47
|
+
return options.filter((option) => !unavailable.has(option.value) || currentValues.includes(option.value));
|
|
48
|
+
}
|
|
49
|
+
// Until the result arrives, and when the query fails, every static option is
|
|
50
|
+
// shown; the write path enforces the rule either way.
|
|
51
|
+
export function AvailabilityOptionsMultiSelect({ field, query, currentValues, children, }) {
|
|
52
|
+
const { data } = useQuery(query, {});
|
|
53
|
+
return children(filterUnavailableOptions(staticSelectOptions(field), data?.rows ?? [], currentValues));
|
|
54
|
+
}
|
|
39
55
|
// Select with static options whose availability (SelectFieldDef.optionsAvailabilityQuery)
|
|
40
56
|
// comes from a query, so it has to be a mounted component for useQuery. Until the
|
|
41
57
|
// result arrives, and when the query fails, every option stays choosable; the write
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { RowActionDrawer } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
2
|
import type { EditRelatedListSectionViewModel, Translate } from "@cosmicdrift/kumiko-headless";
|
|
3
3
|
import { type ReactNode } from "react";
|
|
4
|
-
export declare function RelatedListSection({ section, parentId, record, featureName, translate, hideTitle, grow, onOpenDrawer, actions, embedded, onAfterWrite, }: {
|
|
4
|
+
export declare function RelatedListSection({ section, parentId, record, featureName, translate, hideTitle, grow, onOpenDrawer, actions, embedded, onAfterWrite, refreshToken, }: {
|
|
5
5
|
readonly section: EditRelatedListSectionViewModel;
|
|
6
6
|
readonly parentId: string;
|
|
7
7
|
/** The enclosing projectionDetail's own record (the "Akte") — evaluates
|
|
@@ -30,4 +30,7 @@ export declare function RelatedListSection({ section, parentId, record, featureN
|
|
|
30
30
|
* toolbar action, emptyState action) succeeded — lets a host reload data
|
|
31
31
|
* the write also changed. */
|
|
32
32
|
readonly onAfterWrite?: () => void | Promise<void>;
|
|
33
|
+
/** A change (not the mount) refetches the rows in place, keeping local
|
|
34
|
+
* sort/search/filter state — the host's record changed without a remount. */
|
|
35
|
+
readonly refreshToken?: number;
|
|
33
36
|
}): ReactNode;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { evalFieldCondition, normalizeListColumn, parseRefTarget, relatedListGroupHeaderLabel, relatedListGroupKey, } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
3
|
-
import { useCallback, useMemo, useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { useAppFeatures } from "../app/app-features-context.js";
|
|
5
5
|
import { buildFilterFacets, buildFilterPayload, mergeReferenceFacetOptions, resolveProjectionFacetSpecs, } from "../app/list-facets.js";
|
|
6
6
|
import { useNav } from "../app/nav.js";
|
|
@@ -82,7 +82,7 @@ function buildRowTone(rule) {
|
|
|
82
82
|
return undefined;
|
|
83
83
|
return (row) => (evalFieldCondition(rule, { ...row.values }) ? rule.tone : undefined);
|
|
84
84
|
}
|
|
85
|
-
export function RelatedListSection({ section, parentId, record, featureName, translate, hideTitle, grow, onOpenDrawer, actions, embedded, onAfterWrite, }) {
|
|
85
|
+
export function RelatedListSection({ section, parentId, record, featureName, translate, hideTitle, grow, onOpenDrawer, actions, embedded, onAfterWrite, refreshToken, }) {
|
|
86
86
|
const { Banner, Section, Card, FillContainer, Text, Button, Dialog } = usePrimitives();
|
|
87
87
|
const [emptyStateActionError, setEmptyStateActionError] = useState(null);
|
|
88
88
|
const t = useTranslation();
|
|
@@ -161,6 +161,13 @@ export function RelatedListSection({ section, parentId, record, featureName, tra
|
|
|
161
161
|
]);
|
|
162
162
|
const rowsQuery = useQuery(section.query, payload);
|
|
163
163
|
const ownRefetch = rowsQuery.refetch;
|
|
164
|
+
const lastRefreshTokenRef = useRef(refreshToken);
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
if (lastRefreshTokenRef.current === refreshToken)
|
|
167
|
+
return;
|
|
168
|
+
lastRefreshTokenRef.current = refreshToken;
|
|
169
|
+
void ownRefetch();
|
|
170
|
+
}, [refreshToken, ownRefetch]);
|
|
164
171
|
const refetchSelfAndHost = useCallback(async () => {
|
|
165
172
|
await ownRefetch();
|
|
166
173
|
await onAfterWrite?.();
|
|
@@ -48,6 +48,11 @@ export type RenderEditProps<TValues extends FormValues, TCtx = unknown> = {
|
|
|
48
48
|
readonly onDelete?: () => Promise<DispatcherError | void> | DispatcherError | void;
|
|
49
49
|
readonly onCancel?: () => void;
|
|
50
50
|
readonly onReload?: () => void;
|
|
51
|
+
/** Bumped by the host after it refetched `initial`. A change (not the mount)
|
|
52
|
+
* re-seeds a clean form with the new `initial` and makes embedded relatedList
|
|
53
|
+
* sections refetch, instead of the host remounting the whole form via `key`
|
|
54
|
+
* (which would drop unsaved input in sibling writeForm sections). */
|
|
55
|
+
readonly refreshToken?: number;
|
|
51
56
|
/** Fires when the form gains or loses unsaved input (field changes or a
|
|
52
57
|
* dirty extension section). Lets a host such as a drawer guard closing. */
|
|
53
58
|
readonly onDirtyChange?: (dirty: boolean) => void;
|
|
@@ -193,6 +198,10 @@ export type RenderEditProps<TValues extends FormValues, TCtx = unknown> = {
|
|
|
193
198
|
* rhythm — for content that belongs to the form but is no field (e.g. the
|
|
194
199
|
* secret a confirm step refers to). */
|
|
195
200
|
readonly leadContent?: ReactNode;
|
|
201
|
+
/** Blocks submit (button and Enter) while fields stay editable — unlike
|
|
202
|
+
* `disabled`, which locks the whole form. For a gate the host owns, e.g.
|
|
203
|
+
* an acknowledgement checkbox in `leadContent`. */
|
|
204
|
+
readonly submitBlocked?: boolean;
|
|
196
205
|
/** Show the unsaved-changes footer (count, Discard, "Save changes") even
|
|
197
206
|
* without an entity id or a screen-height form — for hosts that edit an
|
|
198
207
|
* existing server-side record through `customSubmit` (configEdit, also when
|
|
@@ -224,7 +224,7 @@ function resolveTitleTemplate(template, values, translate, fallback) {
|
|
|
224
224
|
return translate(template, values);
|
|
225
225
|
}
|
|
226
226
|
export function RenderEdit(props) {
|
|
227
|
-
const { screen, entity, featureName, initial, writeCommand, translate: translateProp, ctx, schema, onSubmit, payloadMode = "values", buildPayload, serverFieldPathPrefix, onDelete, onCancel, onReload, onDirtyChange, onCopyLink, actions, menuActions, 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
|
+
const { screen, entity, featureName, initial, writeCommand, translate: translateProp, ctx, schema, onSubmit, payloadMode = "values", buildPayload, serverFieldPathPrefix, onDelete, onCancel, onReload, refreshToken, onDirtyChange, onCopyLink, actions, menuActions, 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, submitBlocked = false, } = props;
|
|
228
228
|
const i18nScreenId = props.i18nScreenId ?? screen.id;
|
|
229
229
|
const { customSubmit } = props;
|
|
230
230
|
// Translate-Fallback: wenn der Caller keine Translate-Fn übergibt,
|
|
@@ -357,6 +357,17 @@ export function RenderEdit(props) {
|
|
|
357
357
|
...(ctx !== undefined && { ctx }),
|
|
358
358
|
...(submitConfig !== undefined && { submit: submitConfig }),
|
|
359
359
|
});
|
|
360
|
+
const lastRefreshTokenRef = useRef(refreshToken);
|
|
361
|
+
useEffect(() => {
|
|
362
|
+
if (lastRefreshTokenRef.current === refreshToken)
|
|
363
|
+
return;
|
|
364
|
+
lastRefreshTokenRef.current = refreshToken;
|
|
365
|
+
// Never overwrite user input: a dirty form keeps its values.
|
|
366
|
+
if (controller.getSnapshot().isDirty)
|
|
367
|
+
return;
|
|
368
|
+
controller.setValues(initial);
|
|
369
|
+
controller.rebase();
|
|
370
|
+
}, [refreshToken, controller, initial]);
|
|
360
371
|
// Derived from the screen id + the host entity id only — never from
|
|
361
372
|
// `vm.id`, which lives in the form values a restore mutates (load under one
|
|
362
373
|
// key, save under another). Edit-mode: `${screen.id}:${entityIdProp}`,
|
|
@@ -902,7 +913,7 @@ export function RenderEdit(props) {
|
|
|
902
913
|
// Locked state (#1896): the submit button is visibly disabled, but a
|
|
903
914
|
// native form submit (Enter key) reaches this handler regardless of the
|
|
904
915
|
// button's disabled attribute — block it here too, not just in the UI.
|
|
905
|
-
if (disabled)
|
|
916
|
+
if (disabled || submitBlocked)
|
|
906
917
|
return;
|
|
907
918
|
// Sync guard — React state `isSubmitting` is too late for double-clicks
|
|
908
919
|
// in the same tick (customSubmit has no submitInFlight of its own).
|
|
@@ -1161,7 +1172,8 @@ export function RenderEdit(props) {
|
|
|
1161
1172
|
!insideDrawer &&
|
|
1162
1173
|
submitWithoutChanges !== true) ||
|
|
1163
1174
|
isSubmitting ||
|
|
1164
|
-
disabled
|
|
1175
|
+
disabled ||
|
|
1176
|
+
submitBlocked, loading: isSubmitting, variant: submitVariant ?? "primary", testId: "render-edit-submit", children: translate(submitLabel ??
|
|
1165
1177
|
(isWizard
|
|
1166
1178
|
? "kumiko.actions.finish"
|
|
1167
1179
|
: hasDirtyFooter
|
|
@@ -1342,7 +1354,7 @@ export function RenderEdit(props) {
|
|
|
1342
1354
|
return null;
|
|
1343
1355
|
const mount = (_jsx(RelatedListSection, { section: section, parentId: parentId,
|
|
1344
1356
|
// @cast-boundary form-values: TValues ist strukturell ein Record.
|
|
1345
|
-
record: snapshot.values, featureName: featureName, translate: translate, hideTitle: hideSectionTitles, ...(fillScreenHeight === true && { grow: true }), ...(hideSectionTitles !== true &&
|
|
1357
|
+
record: snapshot.values, featureName: featureName, translate: translate, hideTitle: hideSectionTitles, ...(refreshToken !== undefined && { refreshToken }), ...(fillScreenHeight === true && { grow: true }), ...(hideSectionTitles !== true &&
|
|
1346
1358
|
sectionActionsEl !== undefined && { actions: sectionActionsEl }), ...(onRelatedListDrawerAction !== undefined && {
|
|
1347
1359
|
onOpenDrawer: onRelatedListDrawerAction,
|
|
1348
1360
|
}) }, section.title));
|
|
@@ -13,7 +13,7 @@ import { useQuery } from "../hooks/use-query.js";
|
|
|
13
13
|
import { referenceOptionSource } from "../hooks/use-reference-lookup.js";
|
|
14
14
|
import { useLocale, useTranslation } from "../i18n.js";
|
|
15
15
|
import { usePrimitives } from "../primitives.js";
|
|
16
|
-
import { AvailabilityOptionsSelect, staticSelectOptions } from "./availability-options-select.js";
|
|
16
|
+
import { AvailabilityOptionsMultiSelect, AvailabilityOptionsSelect, staticSelectOptions, } from "./availability-options-select.js";
|
|
17
17
|
import { EmbeddedListField } from "./embedded-list-field.js";
|
|
18
18
|
import { MultiSelectCheckboxes } from "./multi-select-checkboxes.js";
|
|
19
19
|
import { QueryOptionsSelect } from "./query-options-select.js";
|
|
@@ -348,16 +348,12 @@ function renderInput({ field, id, hasError, onChange, Input, appLocale, Banner,
|
|
|
348
348
|
case "tz":
|
|
349
349
|
return (_jsx(Input, { kind: "tz", ...common, value: stringValue(field.value), onChange: (v) => onChange(v) }));
|
|
350
350
|
case "multiSelect": {
|
|
351
|
-
const rawOptions = field.options ?? [];
|
|
352
|
-
const labels = field.optionLabels;
|
|
353
|
-
const multiSelectOptions = labels !== undefined
|
|
354
|
-
? rawOptions.map((value) => ({ value, label: labels[value] ?? value }))
|
|
355
|
-
: rawOptions.map((value) => ({ value, label: value }));
|
|
356
351
|
const arrayValue = Array.isArray(field.value) ? field.value : [];
|
|
357
|
-
|
|
358
|
-
|
|
352
|
+
const renderMultiSelect = (options) => field.display === "checkboxes" ? (_jsx(MultiSelectCheckboxes, { field: field, id: id, options: options, value: arrayValue, onChange: (v) => onChange(v) })) : (_jsx(Input, { kind: "combobox", ...common, multiple: true, value: arrayValue, onChange: (v) => onChange(v), options: options }));
|
|
353
|
+
if (field.selectOptionsAvailabilityQuery !== undefined && field.readOnly !== true) {
|
|
354
|
+
return (_jsx(AvailabilityOptionsMultiSelect, { field: field, query: field.selectOptionsAvailabilityQuery.query, currentValues: arrayValue, children: renderMultiSelect }));
|
|
359
355
|
}
|
|
360
|
-
return (
|
|
356
|
+
return renderMultiSelect(staticSelectOptions(field));
|
|
361
357
|
}
|
|
362
358
|
case "money": {
|
|
363
359
|
const currency = resolveMoneyCurrency(field.value, field.currency);
|
|
@@ -421,7 +417,7 @@ function renderInput({ field, id, hasError, onChange, Input, appLocale, Banner,
|
|
|
421
417
|
return (_jsx(Input, { kind: "textarea", ...common, value: stringValue(field.value), onChange: (v) => onChange(v), ...(rows !== undefined && { rows }) }));
|
|
422
418
|
}
|
|
423
419
|
const icon = resolveFieldIcon(field);
|
|
424
|
-
return (_jsx(Input, { kind: "text", ...common, value: stringValue(field.value), onChange: (v) => onChange(v), ...(icon !== undefined && { icon }) }));
|
|
420
|
+
return (_jsx(Input, { kind: "text", ...common, value: stringValue(field.value), onChange: (v) => onChange(v), ...(icon !== undefined && { icon }), ...(field.autoComplete !== undefined && { autoComplete: field.autoComplete }) }));
|
|
425
421
|
}
|
|
426
422
|
}
|
|
427
423
|
}
|
|
@@ -5,17 +5,16 @@ export type WriteFormSectionProps = {
|
|
|
5
5
|
readonly featureName: string;
|
|
6
6
|
readonly translate?: Translate;
|
|
7
7
|
readonly hideTitle?: boolean;
|
|
8
|
-
/** Fired after a successful submit
|
|
9
|
-
*
|
|
10
|
-
* ProjectionDetailBody's reloadNonce
|
|
8
|
+
/** Fired after a successful submit, once this section reset itself to its
|
|
9
|
+
* initial values — projectionDetail refetches its record (a new one now
|
|
10
|
+
* exists), see ProjectionDetailBody's reloadNonce in kumiko-screen.tsx. */
|
|
11
11
|
readonly onSubmitted: () => void;
|
|
12
12
|
/** section.actions, already resolved into buttons by the caller —
|
|
13
|
-
* rendered
|
|
14
|
-
* title-row actions slot. */
|
|
13
|
+
* rendered in the title row, apart from the section's own submit button. */
|
|
15
14
|
readonly actions?: ReactNode;
|
|
16
15
|
/** Set when this section is a whole tab: the submit button is handed to
|
|
17
16
|
* the host form's footer (pinned like every other screen's save) instead
|
|
18
|
-
* of the section
|
|
17
|
+
* of the end of the section body. Called with undefined on unmount. */
|
|
19
18
|
readonly onFooterAction?: (action: ReactNode | undefined) => void;
|
|
20
19
|
};
|
|
21
20
|
export declare function WriteFormSection({ section, featureName, translate, hideTitle, onSubmitted, actions, onFooterAction, }: WriteFormSectionProps): ReactNode;
|
|
@@ -55,6 +55,11 @@ export function WriteFormSection({ section, featureName, translate, hideTitle, o
|
|
|
55
55
|
const result = await controller.submit();
|
|
56
56
|
if (result.isSuccess) {
|
|
57
57
|
setError(null);
|
|
58
|
+
// Edits typed while the write was in flight stay; only a clean form is reset.
|
|
59
|
+
if (!controller.getSnapshot().isDirty) {
|
|
60
|
+
controller.setValues(initial);
|
|
61
|
+
controller.rebase();
|
|
62
|
+
}
|
|
58
63
|
onSubmitted();
|
|
59
64
|
return;
|
|
60
65
|
}
|
|
@@ -73,7 +78,6 @@ export function WriteFormSection({ section, featureName, translate, hideTitle, o
|
|
|
73
78
|
setIsSubmitting(false);
|
|
74
79
|
}
|
|
75
80
|
}
|
|
76
|
-
const content = (_jsxs(_Fragment, { children: [_jsx(Grid, { columns: section.columns, children: section.fields.map((field) => (_jsx(GridCellForField, { field: field, columns: section.columns, issues: snapshot.errors[field.field], onChange: (v) => controller.setField(field.field, v), GridCell: GridCell, featureName: featureName, allIssues: snapshot.errors, valueDisplay: "form", row: snapshot.values }, field.field))) }), error !== null && (_jsx(Banner, { variant: "error", testId: "write-form-section-error", children: error }))] }));
|
|
77
81
|
// type="button" (not "submit") is load-bearing: this section is deliberately
|
|
78
82
|
// NOT a nested <form> (see the component doc above), so a "submit" type
|
|
79
83
|
// would instead trigger the host RenderEdit's own form submit.
|
|
@@ -90,17 +94,15 @@ export function WriteFormSection({ section, featureName, translate, hideTitle, o
|
|
|
90
94
|
onFooterAction(submitButton);
|
|
91
95
|
return () => onFooterAction(undefined);
|
|
92
96
|
}, [onFooterAction, isSubmitting, submitLabel]);
|
|
93
|
-
const
|
|
97
|
+
const content = (_jsxs(_Fragment, { children: [_jsx(Grid, { columns: section.columns, children: section.fields.map((field) => (_jsx(GridCellForField, { field: { ...field, value: snapshot.values[field.field] }, columns: section.columns, issues: snapshot.errors[field.field], onChange: (v) => controller.setField(field.field, v), GridCell: GridCell, featureName: featureName, allIssues: snapshot.errors, valueDisplay: "form", row: snapshot.values }, field.field))) }), error !== null && (_jsx(Banner, { variant: "error", testId: "write-form-section-error", children: error })), onFooterAction === undefined && (_jsx(Card, { options: { framed: false, footerBordered: false, footerInset: false }, slots: { footer: submitButton }, testId: "write-form-section-submit-row" }))] }));
|
|
94
98
|
// Section always flattens to a borderless divider when rendered inside
|
|
95
99
|
// RenderEdit's own <Form> in tabs mode (hideTitle) — an unframed Card panel
|
|
96
100
|
// stands in, like render-edit.tsx's tabs-mode fields/extension branches.
|
|
97
101
|
if (hideTitle) {
|
|
98
102
|
return (_jsx(Card, { options: { framed: false }, slots: {
|
|
99
103
|
...(section.description !== undefined && { subtitle: section.description }),
|
|
100
|
-
headerActions:
|
|
104
|
+
...(actions !== undefined && { headerActions: actions }),
|
|
101
105
|
}, testId: `write-form-${section.title ?? section.handler}`, children: content }));
|
|
102
106
|
}
|
|
103
|
-
|
|
104
|
-
// lifted into the footer instead when onFooterAction is set).
|
|
105
|
-
return (_jsx(Section, { ...(section.title !== undefined && { title: section.title }), ...(section.description !== undefined && { subtitle: section.description }), ...(section.icon !== undefined && { icon: section.icon }), actions: titleRowActions, testId: `write-form-${section.title ?? section.handler}`, children: content }));
|
|
107
|
+
return (_jsx(Section, { ...(section.title !== undefined && { title: section.title }), ...(section.description !== undefined && { subtitle: section.description }), ...(section.icon !== undefined && { icon: section.icon }), ...(actions !== undefined && { actions }), testId: `write-form-${section.title ?? section.handler}`, children: content }));
|
|
106
108
|
}
|
package/dist/i18n-defaults.js
CHANGED
|
@@ -95,6 +95,8 @@ export const kumikoDefaultTranslations = {
|
|
|
95
95
|
"kumiko.form.saveChanges": "Save changes",
|
|
96
96
|
"kumiko.form.unsaved.one": "1 unsaved change",
|
|
97
97
|
"kumiko.form.unsaved.other": "{count} unsaved changes",
|
|
98
|
+
"kumiko.form.unsavedChangesActionConfirm": "You have unsaved changes in this form. They will be lost if you continue.",
|
|
99
|
+
"kumiko.form.unsavedChangesActionConfirmLabel": "Discard changes and continue",
|
|
98
100
|
"kumiko.wizard.next-with-title": "Next: {title}",
|
|
99
101
|
"kumiko.wizard.save-and-close": "Save and close",
|
|
100
102
|
"kumiko.pager.previousPage": "Previous page",
|
package/dist/primitives.d.ts
CHANGED
|
@@ -1157,6 +1157,8 @@ export type CardOptions = {
|
|
|
1157
1157
|
readonly radius?: "lg" | "xl";
|
|
1158
1158
|
/** Footer abgehoben mit border-t + bg-muted/30. Default true. */
|
|
1159
1159
|
readonly footerBordered?: boolean;
|
|
1160
|
+
/** Footer horizontal padding. false = none, so footer content aligns with the card edge. Default true. */
|
|
1161
|
+
readonly footerInset?: boolean;
|
|
1160
1162
|
/** Joins the ancestor fillHeight flex chain (fw#2722/#2778 — a tabs-mode
|
|
1161
1163
|
* relatedList table sizing to the panel instead of the page) instead of
|
|
1162
1164
|
* normal document-flow height. Default false. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.357.0",
|
|
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.357.0",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.357.0",
|
|
20
|
+
"@cosmicdrift/kumiko-types": "0.357.0",
|
|
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.357.0"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,33 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.357.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "MultiSelect inputs load optionsAvailabilityQuery and hide unavailable options"
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"version": "0.357.0",
|
|
9
|
+
"type": "improvement",
|
|
10
|
+
"title": "HeaderActionGroup replaces the overflow menu child selector for header action wrappers",
|
|
11
|
+
"detail": "App wrappers around headerActions are no longer restyled into a stacked column inside the phone overflow menu (the [&>div] child selector reached into app markup). Use HeaderActionGroup instead: a horizontal row in the header, a stacked column in the overflow menu."
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"version": "0.357.0",
|
|
15
|
+
"type": "breaking",
|
|
16
|
+
"title": "List card meta values wrap instead of being clipped; more than 3 meta columns fail boot",
|
|
17
|
+
"migration": "Narrow list cards (entityList, projectionList, relatedList sections, expandableRow) now render meta values as a dot-separated row that wraps onto further lines instead of being cut off after two lines (and no longer subject to a silent runtime cap of 3 values); only a single value wider than the card is truncated. Meta columns are all columns except the title (first column), the status select and columns with hideOnNarrow: true. validateBoot now rejects a list with more than 3 meta columns and names feature, screen, section and fields. Fix: mark the extra columns hideOnNarrow: true (tables keep them) or drop them. collectCardMetaOverflow(features) from @cosmicdrift/kumiko-framework/engine lists all offenders at once. headless now also honors hideOnNarrow on reference, derived, row-meta and renderer-only columns, which it previously ignored there."
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"version": "0.357.0",
|
|
21
|
+
"type": "fix",
|
|
22
|
+
"title": "projectionList ignores a URL sort param on columns that are not sortable",
|
|
23
|
+
"detail": "A stale link like `?<screen>.sort=<col>` onto a `sortable: false` or unknown column is no longer sent to the server (which answers 400 since 0.355); the screen falls back to its defaultSort."
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"version": "0.357.0",
|
|
27
|
+
"type": "breaking",
|
|
28
|
+
"title": "Section actions render in the title row instead of a footer; WriteFormSection submit moves below its fields",
|
|
29
|
+
"migration": "Section.actions (renderer-web DefaultSection) render top right in the title row since #3241, no longer as a footer. A button that depends on content below it (a confirmation checkbox, a list it acts on) belongs in the section's children instead of actions. WriteFormSection's own submit button now sits below its fields; caller actions stay in the title row."
|
|
30
|
+
},
|
|
2
31
|
{
|
|
3
32
|
"version": "0.356.0",
|
|
4
33
|
"type": "improvement",
|