@cosmicdrift/kumiko-renderer 0.258.1 → 0.260.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/package.json +4 -4
- package/src/app/__tests__/action-form-shim.test.ts +21 -0
- package/src/app/__tests__/row-default-edit-action.test.tsx +324 -0
- package/src/app/action-form-shim.ts +3 -0
- package/src/app/dashboard-body.tsx +1 -0
- package/src/app/extension-sections.tsx +9 -0
- package/src/app/kumiko-screen.tsx +186 -88
- package/src/app/row-actions.ts +71 -29
- package/src/app/screen-access.ts +22 -1
- package/src/app/use-embedded-screen.ts +27 -0
- package/src/components/__tests__/related-list-section.test.tsx +119 -1
- package/src/components/related-list-section.tsx +29 -2
- package/src/components/render-edit.tsx +87 -2
- package/src/index.ts +2 -0
- package/src/primitives.tsx +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.260.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,8 +15,8 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
19
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
18
|
+
"@cosmicdrift/kumiko-framework": "0.260.0",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.260.0",
|
|
20
20
|
"react": "^19.2.6",
|
|
21
21
|
"temporal-polyfill": "^0.3.2",
|
|
22
22
|
"zod": "^4.4.3"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"@types/react-dom": "^19.2.3",
|
|
28
28
|
"jsdom": "^29.1.1",
|
|
29
29
|
"react-dom": "^19.2.6",
|
|
30
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
30
|
+
"@cosmicdrift/kumiko-locale-de": "0.260.0"
|
|
31
31
|
},
|
|
32
32
|
"repository": {
|
|
33
33
|
"type": "git",
|
|
@@ -48,6 +48,27 @@ describe("synthesizeActionFormScreen", () => {
|
|
|
48
48
|
});
|
|
49
49
|
expect("description" in withoutDescription).toBe(false);
|
|
50
50
|
});
|
|
51
|
+
|
|
52
|
+
test("carries slots through so RenderEdit can mount header/footer slots on an actionForm", () => {
|
|
53
|
+
const withSlots = synthesizeActionFormScreen({
|
|
54
|
+
id: "invite-user",
|
|
55
|
+
type: "actionForm",
|
|
56
|
+
handler: "users:write:invite-user",
|
|
57
|
+
layout: { sections: [{ title: "Invite", fields: ["email"] }] },
|
|
58
|
+
fields: { email: { type: "text" } },
|
|
59
|
+
slots: { footer: { react: { __component: "f" } } },
|
|
60
|
+
});
|
|
61
|
+
expect(withSlots.slots).toEqual({ footer: { react: { __component: "f" } } });
|
|
62
|
+
|
|
63
|
+
const withoutSlots = synthesizeActionFormScreen({
|
|
64
|
+
id: "invite-user",
|
|
65
|
+
type: "actionForm",
|
|
66
|
+
handler: "users:write:invite-user",
|
|
67
|
+
layout: { sections: [{ title: "Invite", fields: ["email"] }] },
|
|
68
|
+
fields: { email: { type: "text" } },
|
|
69
|
+
});
|
|
70
|
+
expect("slots" in withoutSlots).toBe(false);
|
|
71
|
+
});
|
|
51
72
|
});
|
|
52
73
|
|
|
53
74
|
describe("synthesizeSecretMintConfirmScreen (fw#2838)", () => {
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
// kumiko-screen-akte-bedienkonzept: entityList and projectionList rowActions
|
|
2
|
+
// get a default "Edit" row action for free when the row's entity has a
|
|
3
|
+
// visible entityEdit screen — same cross-feature resolution as
|
|
4
|
+
// projectionDetail's header defaultEditAction (fw#2166), reused here via
|
|
5
|
+
// findEditScreenFor (screen-access.ts). Declared rowActions with id "edit"
|
|
6
|
+
// win; access-gating and a missing entityEdit screen both suppress the
|
|
7
|
+
// default. Mirrors entity-list-row-action-entity-target.test.tsx's harness.
|
|
8
|
+
|
|
9
|
+
import { describe, expect, test } from "bun:test";
|
|
10
|
+
import type {
|
|
11
|
+
EntityDefinition,
|
|
12
|
+
EntityEditScreenDefinition,
|
|
13
|
+
EntityListScreenDefinition,
|
|
14
|
+
ProjectionListScreenDefinition,
|
|
15
|
+
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
16
|
+
import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
|
|
17
|
+
import { render, waitFor } from "@testing-library/react";
|
|
18
|
+
import type { ComponentType, ReactNode } from "react";
|
|
19
|
+
import { DispatcherProvider } from "../../context/dispatcher-context";
|
|
20
|
+
import { UserRolesProvider } from "../../context/user-roles-context";
|
|
21
|
+
import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
|
|
22
|
+
import { kumikoDefaultTranslations } from "../../i18n-defaults";
|
|
23
|
+
import {
|
|
24
|
+
type CorePrimitives,
|
|
25
|
+
type DataTableProps,
|
|
26
|
+
type DataTableRowAction,
|
|
27
|
+
PrimitivesProvider,
|
|
28
|
+
} from "../../primitives";
|
|
29
|
+
import { AppFeaturesProvider } from "../app-features-context";
|
|
30
|
+
import type { FeatureSchema } from "../feature-schema";
|
|
31
|
+
import { KumikoScreen } from "../kumiko-screen";
|
|
32
|
+
import type { NavTarget } from "../nav";
|
|
33
|
+
import { NavProvider } from "../nav";
|
|
34
|
+
|
|
35
|
+
function stubDispatcher(rows: readonly Record<string, unknown>[]): Dispatcher {
|
|
36
|
+
return {
|
|
37
|
+
write: (async () => ({ isSuccess: true, data: {} })) as unknown as Dispatcher["write"],
|
|
38
|
+
query: (async () => ({
|
|
39
|
+
isSuccess: true,
|
|
40
|
+
data: { rows, nextCursor: null, total: rows.length },
|
|
41
|
+
})) as unknown as Dispatcher["query"],
|
|
42
|
+
batch: (async () => ({ isSuccess: true, results: [] })) as unknown as Dispatcher["batch"],
|
|
43
|
+
statusStore: {
|
|
44
|
+
getState: () => "online",
|
|
45
|
+
subscribe: () => () => {},
|
|
46
|
+
} as unknown as Dispatcher["statusStore"],
|
|
47
|
+
async *stream() {},
|
|
48
|
+
pendingWrites: () => [],
|
|
49
|
+
pendingFiles: () => [],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type Captured = {
|
|
54
|
+
readonly rowActions: readonly DataTableRowAction[] | undefined;
|
|
55
|
+
readonly rowCount: number;
|
|
56
|
+
};
|
|
57
|
+
let captured: Captured = { rowActions: undefined, rowCount: 0 };
|
|
58
|
+
const captureDataTable: ComponentType<DataTableProps> = (props) => {
|
|
59
|
+
captured = { rowActions: props.rowActions, rowCount: props.rows.length };
|
|
60
|
+
return null;
|
|
61
|
+
};
|
|
62
|
+
const noop = (): ReactNode => null;
|
|
63
|
+
const passChildren = ({ children }: { readonly children?: ReactNode }): ReactNode => children;
|
|
64
|
+
|
|
65
|
+
const testPrimitives: CorePrimitives = {
|
|
66
|
+
Button: noop,
|
|
67
|
+
Banner: passChildren,
|
|
68
|
+
Field: passChildren,
|
|
69
|
+
Input: noop,
|
|
70
|
+
DataTable: captureDataTable,
|
|
71
|
+
Form: passChildren,
|
|
72
|
+
Section: passChildren,
|
|
73
|
+
Card: passChildren,
|
|
74
|
+
Grid: passChildren,
|
|
75
|
+
GridCell: passChildren,
|
|
76
|
+
Text: passChildren,
|
|
77
|
+
Heading: noop,
|
|
78
|
+
Dialog: noop,
|
|
79
|
+
Modal: noop,
|
|
80
|
+
Lightbox: noop,
|
|
81
|
+
ConfigSourceBadge: noop,
|
|
82
|
+
ConfigCascadeView: noop,
|
|
83
|
+
Link: noop,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function editScreen(entity: string, roles?: readonly string[]): EntityEditScreenDefinition {
|
|
87
|
+
return {
|
|
88
|
+
id: "app:screen:rent-edit",
|
|
89
|
+
type: "entityEdit",
|
|
90
|
+
entity,
|
|
91
|
+
layout: { sections: [{ columns: 1, fields: ["name"] }] },
|
|
92
|
+
...(roles !== undefined && { access: { roles } }),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function requireAction(id: string): DataTableRowAction {
|
|
97
|
+
const action = captured.rowActions?.find((a) => a.id === id);
|
|
98
|
+
if (!action) throw new Error(`expected the '${id}' row action to be captured`);
|
|
99
|
+
return action;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function renderScreen(
|
|
103
|
+
schema: FeatureSchema,
|
|
104
|
+
qn: string,
|
|
105
|
+
navigateSpy: (target: NavTarget) => void,
|
|
106
|
+
userRoles: readonly string[],
|
|
107
|
+
): void {
|
|
108
|
+
render(
|
|
109
|
+
<LocaleProvider
|
|
110
|
+
resolver={createStaticLocaleResolver({ locale: "en" })}
|
|
111
|
+
fallbackBundles={[kumikoDefaultTranslations]}
|
|
112
|
+
>
|
|
113
|
+
<DispatcherProvider dispatcher={stubDispatcher([{ id: "row-1", name: "Alice" }])}>
|
|
114
|
+
<AppFeaturesProvider features={[schema]}>
|
|
115
|
+
<UserRolesProvider roles={userRoles}>
|
|
116
|
+
<NavProvider
|
|
117
|
+
value={{
|
|
118
|
+
route: undefined,
|
|
119
|
+
navigate: navigateSpy,
|
|
120
|
+
replace: () => {},
|
|
121
|
+
hrefFor: () => "",
|
|
122
|
+
searchParams: {},
|
|
123
|
+
setSearchParams: () => {},
|
|
124
|
+
}}
|
|
125
|
+
>
|
|
126
|
+
<PrimitivesProvider value={testPrimitives}>
|
|
127
|
+
<KumikoScreen schema={schema} qn={qn} />
|
|
128
|
+
</PrimitivesProvider>
|
|
129
|
+
</NavProvider>
|
|
130
|
+
</UserRolesProvider>
|
|
131
|
+
</AppFeaturesProvider>
|
|
132
|
+
</DispatcherProvider>
|
|
133
|
+
</LocaleProvider>,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const entity: EntityDefinition = {
|
|
138
|
+
fields: {
|
|
139
|
+
name: { type: "text", maxLength: 50, required: false, searchable: false, sortable: false },
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
describe("entityList default edit row action", () => {
|
|
144
|
+
function schemaWith(
|
|
145
|
+
rowActions?: EntityListScreenDefinition["rowActions"],
|
|
146
|
+
includeEdit = true,
|
|
147
|
+
): FeatureSchema {
|
|
148
|
+
const listScreen: EntityListScreenDefinition = {
|
|
149
|
+
id: "rent-list",
|
|
150
|
+
type: "entityList",
|
|
151
|
+
entity: "rent",
|
|
152
|
+
columns: ["name"],
|
|
153
|
+
...(rowActions !== undefined && { rowActions }),
|
|
154
|
+
};
|
|
155
|
+
return {
|
|
156
|
+
featureName: "app",
|
|
157
|
+
entities: { rent: entity },
|
|
158
|
+
screens: includeEdit ? [listScreen, editScreen("rent")] : [listScreen],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
test("adds a default edit action at the first position when an entityEdit screen exists", async () => {
|
|
163
|
+
captured = { rowActions: undefined, rowCount: 0 };
|
|
164
|
+
const navigateCalls: NavTarget[] = [];
|
|
165
|
+
renderScreen(schemaWith(undefined), "app:screen:rent-list", (t) => navigateCalls.push(t), []);
|
|
166
|
+
await waitFor(() => expect(captured.rowCount).toBe(1));
|
|
167
|
+
|
|
168
|
+
expect(captured.rowActions?.[0]?.id).toBe("edit");
|
|
169
|
+
expect(captured.rowActions?.[0]?.label).toBe("Edit");
|
|
170
|
+
|
|
171
|
+
await requireAction("edit").onTrigger({
|
|
172
|
+
id: "row-1",
|
|
173
|
+
values: { id: "row-1", name: "Alice" },
|
|
174
|
+
});
|
|
175
|
+
expect(navigateCalls).toEqual([{ screenId: "rent-edit", entityId: "row-1" }]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("a declared rowAction with id 'edit' wins — no doubling", async () => {
|
|
179
|
+
captured = { rowActions: undefined, rowCount: 0 };
|
|
180
|
+
renderScreen(
|
|
181
|
+
schemaWith([{ kind: "navigate", id: "edit", label: "custom-edit", screen: "rent-edit" }]),
|
|
182
|
+
"app:screen:rent-list",
|
|
183
|
+
() => {},
|
|
184
|
+
[],
|
|
185
|
+
);
|
|
186
|
+
await waitFor(() => expect(captured.rowCount).toBe(1));
|
|
187
|
+
|
|
188
|
+
const editActions = captured.rowActions?.filter((a) => a.id === "edit") ?? [];
|
|
189
|
+
expect(editActions).toHaveLength(1);
|
|
190
|
+
// "custom-edit" has no translation registered, so translate() returns
|
|
191
|
+
// the raw key — proving this is the declared action, not the default
|
|
192
|
+
// (whose label is the translated "kumiko.actions.edit" → "Edit").
|
|
193
|
+
expect(editActions[0]?.label).toBe("custom-edit");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("no entityEdit screen for the entity → no default edit action", async () => {
|
|
197
|
+
captured = { rowActions: undefined, rowCount: 0 };
|
|
198
|
+
renderScreen(schemaWith(undefined, false), "app:screen:rent-list", () => {}, []);
|
|
199
|
+
await waitFor(() => expect(captured.rowCount).toBe(1));
|
|
200
|
+
|
|
201
|
+
expect(captured.rowActions?.some((a) => a.id === "edit") ?? false).toBe(false);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("access denied to the entityEdit screen → no default edit action", async () => {
|
|
205
|
+
captured = { rowActions: undefined, rowCount: 0 };
|
|
206
|
+
const listScreen: EntityListScreenDefinition = {
|
|
207
|
+
id: "rent-list",
|
|
208
|
+
type: "entityList",
|
|
209
|
+
entity: "rent",
|
|
210
|
+
columns: ["name"],
|
|
211
|
+
};
|
|
212
|
+
const schema: FeatureSchema = {
|
|
213
|
+
featureName: "app",
|
|
214
|
+
entities: { rent: entity },
|
|
215
|
+
screens: [listScreen, editScreen("rent", ["admin"])],
|
|
216
|
+
};
|
|
217
|
+
renderScreen(schema, "app:screen:rent-list", () => {}, []);
|
|
218
|
+
await waitFor(() => expect(captured.rowCount).toBe(1));
|
|
219
|
+
|
|
220
|
+
expect(captured.rowActions?.some((a) => a.id === "edit") ?? false).toBe(false);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("projectionList default edit row action", () => {
|
|
225
|
+
function schemaWith(
|
|
226
|
+
rowActions?: ProjectionListScreenDefinition["rowActions"],
|
|
227
|
+
includeEdit = true,
|
|
228
|
+
): FeatureSchema {
|
|
229
|
+
const listScreen: ProjectionListScreenDefinition = {
|
|
230
|
+
id: "rent-projection-list",
|
|
231
|
+
type: "projectionList",
|
|
232
|
+
query: "app:query:rent:list",
|
|
233
|
+
detailFor: "rent",
|
|
234
|
+
columns: [{ field: "name", label: "Name" }],
|
|
235
|
+
...(rowActions !== undefined && { rowActions }),
|
|
236
|
+
};
|
|
237
|
+
return {
|
|
238
|
+
featureName: "app",
|
|
239
|
+
entities: {},
|
|
240
|
+
screens: includeEdit ? [listScreen, editScreen("rent")] : [listScreen],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
test("adds a default edit action at the first position when detailFor resolves an entityEdit screen", async () => {
|
|
245
|
+
captured = { rowActions: undefined, rowCount: 0 };
|
|
246
|
+
const navigateCalls: NavTarget[] = [];
|
|
247
|
+
renderScreen(
|
|
248
|
+
schemaWith(undefined),
|
|
249
|
+
"app:screen:rent-projection-list",
|
|
250
|
+
(t) => navigateCalls.push(t),
|
|
251
|
+
[],
|
|
252
|
+
);
|
|
253
|
+
await waitFor(() => expect(captured.rowCount).toBe(1));
|
|
254
|
+
|
|
255
|
+
expect(captured.rowActions?.[0]?.id).toBe("edit");
|
|
256
|
+
expect(captured.rowActions?.[0]?.label).toBe("Edit");
|
|
257
|
+
|
|
258
|
+
await requireAction("edit").onTrigger({
|
|
259
|
+
id: "row-1",
|
|
260
|
+
values: { id: "row-1", name: "Alice" },
|
|
261
|
+
});
|
|
262
|
+
expect(navigateCalls).toEqual([{ screenId: "rent-edit", entityId: "row-1" }]);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("a declared rowAction with id 'edit' wins — no doubling", async () => {
|
|
266
|
+
captured = { rowActions: undefined, rowCount: 0 };
|
|
267
|
+
renderScreen(
|
|
268
|
+
schemaWith([{ kind: "navigate", id: "edit", label: "custom-edit", screen: "rent-edit" }]),
|
|
269
|
+
"app:screen:rent-projection-list",
|
|
270
|
+
() => {},
|
|
271
|
+
[],
|
|
272
|
+
);
|
|
273
|
+
await waitFor(() => expect(captured.rowCount).toBe(1));
|
|
274
|
+
|
|
275
|
+
const editActions = captured.rowActions?.filter((a) => a.id === "edit") ?? [];
|
|
276
|
+
expect(editActions).toHaveLength(1);
|
|
277
|
+
expect(editActions[0]?.label).toBe("custom-edit");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("no detailFor → no default edit action", async () => {
|
|
281
|
+
captured = { rowActions: undefined, rowCount: 0 };
|
|
282
|
+
const listScreen: ProjectionListScreenDefinition = {
|
|
283
|
+
id: "rent-projection-list",
|
|
284
|
+
type: "projectionList",
|
|
285
|
+
query: "app:query:rent:list",
|
|
286
|
+
columns: [{ field: "name", label: "Name" }],
|
|
287
|
+
};
|
|
288
|
+
const schema: FeatureSchema = {
|
|
289
|
+
featureName: "app",
|
|
290
|
+
entities: {},
|
|
291
|
+
screens: [listScreen, editScreen("rent")],
|
|
292
|
+
};
|
|
293
|
+
renderScreen(schema, "app:screen:rent-projection-list", () => {}, []);
|
|
294
|
+
await waitFor(() => expect(captured.rowCount).toBe(1));
|
|
295
|
+
|
|
296
|
+
expect(captured.rowActions?.some((a) => a.id === "edit") ?? false).toBe(false);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("access denied to the entityEdit screen → no default edit action", async () => {
|
|
300
|
+
captured = { rowActions: undefined, rowCount: 0 };
|
|
301
|
+
renderScreen(
|
|
302
|
+
{
|
|
303
|
+
featureName: "app",
|
|
304
|
+
entities: {},
|
|
305
|
+
screens: [
|
|
306
|
+
{
|
|
307
|
+
id: "rent-projection-list",
|
|
308
|
+
type: "projectionList",
|
|
309
|
+
query: "app:query:rent:list",
|
|
310
|
+
detailFor: "rent",
|
|
311
|
+
columns: [{ field: "name", label: "Name" }],
|
|
312
|
+
},
|
|
313
|
+
editScreen("rent", ["admin"]),
|
|
314
|
+
],
|
|
315
|
+
},
|
|
316
|
+
"app:screen:rent-projection-list",
|
|
317
|
+
() => {},
|
|
318
|
+
[],
|
|
319
|
+
);
|
|
320
|
+
await waitFor(() => expect(captured.rowCount).toBe(1));
|
|
321
|
+
|
|
322
|
+
expect(captured.rowActions?.some((a) => a.id === "edit") ?? false).toBe(false);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
@@ -50,6 +50,9 @@ export function synthesizeActionFormScreen(
|
|
|
50
50
|
layout: screen.layout,
|
|
51
51
|
...(screen.description !== undefined && { description: screen.description }),
|
|
52
52
|
...(screen.access !== undefined && { access: screen.access }),
|
|
53
|
+
// Only ActionFormScreenDefinition carries slots — SecretMintScreenDefinition
|
|
54
|
+
// has none, so `in` is the narrowing (no cast).
|
|
55
|
+
...("slots" in screen && screen.slots !== undefined && { slots: screen.slots }),
|
|
53
56
|
};
|
|
54
57
|
}
|
|
55
58
|
|
|
@@ -8,6 +8,7 @@ import type { Translate } from "@cosmicdrift/kumiko-headless";
|
|
|
8
8
|
import { type ComponentType, createContext, type ReactNode, useContext } from "react";
|
|
9
9
|
|
|
10
10
|
export type DashboardBodyProps = {
|
|
11
|
+
readonly featureName: string;
|
|
11
12
|
readonly screen: DashboardScreenDefinition;
|
|
12
13
|
readonly translate?: Translate;
|
|
13
14
|
};
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// PlatformComponent über dieselbe Registry auf und mountet die passende
|
|
5
5
|
// Component — die Bundled-Feature-/App-Component lädt + persistiert dann
|
|
6
6
|
// ihre eigenen Daten (z.B. custom-fields, oder ein eigenständiger Chart).
|
|
7
|
+
// The entityEdit footer slot (`screen.slots.footer`) mounts through it too.
|
|
7
8
|
//
|
|
8
9
|
// Mounting analog zu CustomScreensProvider — createKumikoApp im
|
|
9
10
|
// renderer-web sammelt alle clientFeatures.extensionSectionComponents und
|
|
@@ -66,6 +67,14 @@ export type ExtensionSectionProps = {
|
|
|
66
67
|
* `snapshot.errors` on the field instead of a collective message.
|
|
67
68
|
* Undefined outside entityEdit sections. */
|
|
68
69
|
readonly validate?: () => boolean;
|
|
70
|
+
/** Whether the host form has unsaved changes. Only set in the entityEdit
|
|
71
|
+
* footer slot (`screen.slots.footer`); undefined in all other mounts. */
|
|
72
|
+
readonly hasUnsavedChanges?: boolean;
|
|
73
|
+
/** Current wizard step, only set for `layout.mode === "wizard"` forms.
|
|
74
|
+
* Only set in the entityEdit footer slot (`screen.slots.footer`);
|
|
75
|
+
* undefined in all other mounts, and undefined there too for a
|
|
76
|
+
* non-wizard form. */
|
|
77
|
+
readonly wizardStep?: { readonly index: number; readonly isLast: boolean };
|
|
69
78
|
};
|
|
70
79
|
|
|
71
80
|
export type ExtensionSectionComponent = ComponentType<ExtensionSectionProps>;
|