@cosmicdrift/kumiko-framework 0.203.0 → 0.204.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 +3 -3
- package/src/db/sql-inventory.ts +16 -1
- package/src/engine/__tests__/boot-validator-action-wiring.test.ts +24 -0
- package/src/engine/__tests__/screen.test.ts +192 -0
- package/src/engine/boot-validator/action-wiring.ts +8 -2
- package/src/engine/boot-validator/screens.ts +50 -0
- package/src/engine/index.ts +6 -1
- package/src/engine/screen-helpers.ts +5 -0
- package/src/i18n/required-surface-keys.ts +11 -0
- package/src/testing/e2e-generator.ts +4 -4
- package/src/ui-types/index.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.204.0",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -190,7 +190,7 @@
|
|
|
190
190
|
"./package.json": "./package.json"
|
|
191
191
|
},
|
|
192
192
|
"dependencies": {
|
|
193
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
193
|
+
"@cosmicdrift/kumiko-types": "0.204.0",
|
|
194
194
|
"bullmq": "^5.76.7",
|
|
195
195
|
"bun-types": "^1.3.13",
|
|
196
196
|
"hono": "^4.13.1",
|
|
@@ -206,7 +206,7 @@
|
|
|
206
206
|
"zod": "^4.4.3"
|
|
207
207
|
},
|
|
208
208
|
"devDependencies": {
|
|
209
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
209
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.204.0",
|
|
210
210
|
"bun-types": "^1.3.13",
|
|
211
211
|
"pino-pretty": "^13.1.3"
|
|
212
212
|
},
|
package/src/db/sql-inventory.ts
CHANGED
|
@@ -48,7 +48,22 @@ export const RAW_SQL_ALLOWLIST: ReadonlyArray<RegExp> = [
|
|
|
48
48
|
/\/packages\/framework\/src\/db\/sql-inventory\.ts$/,
|
|
49
49
|
/\/packages\/framework\/src\/bun-db\/query\.ts$/,
|
|
50
50
|
/\/packages\/framework\/src\/testing\//,
|
|
51
|
-
|
|
51
|
+
// ponytail: explicit enumeration — blanket regex auto-allowed any new .unsafe()
|
|
52
|
+
/\/bundled-features\/src\/billing-foundation\/db\/queries\/subscription-projection\.ts$/,
|
|
53
|
+
/\/bundled-features\/src\/config\/db\/queries\/resolver\.ts$/,
|
|
54
|
+
/\/bundled-features\/src\/custom-fields\/db\/queries\/field-access\.ts$/,
|
|
55
|
+
/\/bundled-features\/src\/custom-fields\/db\/queries\/projection\.ts$/,
|
|
56
|
+
/\/bundled-features\/src\/custom-fields\/db\/queries\/quota\.ts$/,
|
|
57
|
+
/\/bundled-features\/src\/custom-fields\/db\/queries\/retention\.ts$/,
|
|
58
|
+
/\/bundled-features\/src\/custom-fields\/db\/queries\/user-data-rights\.ts$/,
|
|
59
|
+
/\/bundled-features\/src\/delivery\/db\/queries\/preferences\.ts$/,
|
|
60
|
+
/\/bundled-features\/src\/form-draft\/db\/queries\/cleanup\.ts$/,
|
|
61
|
+
/\/bundled-features\/src\/form-draft\/db\/queries\/draft-count\.ts$/,
|
|
62
|
+
/\/bundled-features\/src\/form-draft\/db\/queries\/owned-file-refs\.ts$/,
|
|
63
|
+
/\/bundled-features\/src\/inbound-mail-foundation\/db\/queries\/inbound-projections\.ts$/,
|
|
64
|
+
/\/bundled-features\/src\/secrets\/db\/queries\/read\.ts$/,
|
|
65
|
+
/\/bundled-features\/src\/sessions\/db\/queries\/cleanup\.ts$/,
|
|
66
|
+
/\/bundled-features\/src\/user\/db\/queries\/stream-tenant-backfill\.ts$/,
|
|
52
67
|
/\/packages\/framework\/src\/engine\/steps\/unsafe-projection-/,
|
|
53
68
|
/\/samples\/(apps|recipes)\/[^/]+\/src\/db\/queries\//,
|
|
54
69
|
/\/bin\/commands\//,
|
|
@@ -240,6 +240,30 @@ describe("validateBoot — action wiring (no function values)", () => {
|
|
|
240
240
|
expect(() => validateBoot([feature])).toThrow(/column "amount" renderer is a function/);
|
|
241
241
|
});
|
|
242
242
|
|
|
243
|
+
test("projectionDetail relatedList column renderer as function → Throw (fw#2166)", () => {
|
|
244
|
+
const feature = defineFeature("shop", (r) => {
|
|
245
|
+
r.screen({
|
|
246
|
+
id: "order-detail",
|
|
247
|
+
type: "projectionDetail",
|
|
248
|
+
query: "shop:query:order:detail",
|
|
249
|
+
layout: {
|
|
250
|
+
sections: [
|
|
251
|
+
{
|
|
252
|
+
kind: "relatedList",
|
|
253
|
+
title: "Payments",
|
|
254
|
+
query: "shop:query:order:payments",
|
|
255
|
+
columns: [
|
|
256
|
+
// biome-ignore lint/suspicious/noExplicitAny: intentional type violation under test
|
|
257
|
+
{ field: "amount", renderer: ((v: unknown) => String(v)) as any },
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
expect(() => validateBoot([feature])).toThrow(/column "amount" renderer is a function/);
|
|
265
|
+
});
|
|
266
|
+
|
|
243
267
|
test("projectionList rowAction payload as function → Throw", () => {
|
|
244
268
|
const feature = defineFeature("shop", (r) => {
|
|
245
269
|
r.screen({
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { z } from "zod";
|
|
2
3
|
import { withBootValidatorFixture } from "../../testing/boot-validator-fixture";
|
|
3
4
|
import { validateBoot as validateBootRaw } from "../boot-validator";
|
|
5
|
+
import { createTenantConfig } from "../config-helpers";
|
|
4
6
|
import { defineFeature } from "../define-feature";
|
|
5
7
|
import { createDerivedField, createEntity, createTextField } from "../factories";
|
|
6
8
|
import { createRegistry } from "../registry";
|
|
@@ -165,6 +167,196 @@ describe("r.screen() — registration", () => {
|
|
|
165
167
|
expect(() => validateBoot(features)).toThrow(/extension section/i);
|
|
166
168
|
});
|
|
167
169
|
|
|
170
|
+
test("validateBoot rejects a projectionDetail relatedList section with an empty query (fw#2166)", () => {
|
|
171
|
+
const features = [
|
|
172
|
+
defineFeature("app", (r) => {
|
|
173
|
+
r.screen({
|
|
174
|
+
id: "x",
|
|
175
|
+
type: "projectionDetail",
|
|
176
|
+
query: "app:query:foo:detail",
|
|
177
|
+
layout: {
|
|
178
|
+
sections: [{ kind: "relatedList", title: "s", query: "", columns: ["name"] }],
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
}),
|
|
182
|
+
];
|
|
183
|
+
expect(() => validateBoot(features)).toThrow(
|
|
184
|
+
/Screen "x".*relatedList.*empty or non-string query/,
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("validateBoot rejects a relatedList rowClick with no matching detailFor screen (fw#2166)", () => {
|
|
189
|
+
const features = [
|
|
190
|
+
defineFeature("app", (r) => {
|
|
191
|
+
r.screen({
|
|
192
|
+
id: "x",
|
|
193
|
+
type: "projectionDetail",
|
|
194
|
+
query: "app:query:foo:detail",
|
|
195
|
+
layout: {
|
|
196
|
+
sections: [
|
|
197
|
+
{
|
|
198
|
+
kind: "relatedList",
|
|
199
|
+
title: "s",
|
|
200
|
+
query: "app:query:foo:list",
|
|
201
|
+
columns: ["name"],
|
|
202
|
+
rowClick: { entity: "invoice" },
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
}),
|
|
208
|
+
];
|
|
209
|
+
expect(() => validateBoot(features)).toThrow(
|
|
210
|
+
/Add detailFor: "invoice" to the screen that shows it\./,
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("validateBoot accepts a relatedList rowClick with a detailFor screen in another feature (fw#2166)", () => {
|
|
215
|
+
const features = [
|
|
216
|
+
defineFeature("app", (r) => {
|
|
217
|
+
r.screen({
|
|
218
|
+
id: "x",
|
|
219
|
+
type: "projectionDetail",
|
|
220
|
+
query: "app:query:foo:detail",
|
|
221
|
+
layout: {
|
|
222
|
+
sections: [
|
|
223
|
+
{
|
|
224
|
+
kind: "relatedList",
|
|
225
|
+
title: "s",
|
|
226
|
+
query: "app:query:foo:list",
|
|
227
|
+
columns: ["name"],
|
|
228
|
+
rowClick: { entity: "invoice" },
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
}),
|
|
234
|
+
defineFeature("billing", (r) => {
|
|
235
|
+
r.entity(
|
|
236
|
+
"invoice",
|
|
237
|
+
createEntity({ table: "invoices", fields: { name: createTextField() } }),
|
|
238
|
+
);
|
|
239
|
+
r.screen({
|
|
240
|
+
id: "invoice-detail",
|
|
241
|
+
type: "custom",
|
|
242
|
+
renderer: { react: { __component: "stub" } },
|
|
243
|
+
detailFor: "invoice",
|
|
244
|
+
});
|
|
245
|
+
}),
|
|
246
|
+
];
|
|
247
|
+
expect(() => validateBoot(features)).not.toThrow();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("validateBoot rejects a relatedList section in a wizard layout (fw#2166)", () => {
|
|
251
|
+
const features = [
|
|
252
|
+
defineFeature("app", (r) => {
|
|
253
|
+
r.screen({
|
|
254
|
+
id: "x",
|
|
255
|
+
type: "projectionDetail",
|
|
256
|
+
query: "app:query:foo:detail",
|
|
257
|
+
layout: {
|
|
258
|
+
mode: "wizard",
|
|
259
|
+
sections: [
|
|
260
|
+
{ kind: "relatedList", title: "s", query: "app:query:foo:list", columns: ["name"] },
|
|
261
|
+
],
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
}),
|
|
265
|
+
];
|
|
266
|
+
expect(() => validateBoot(features)).toThrow(/relatedList.*wizard layout/);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("validateBoot accepts a valid relatedList section without rowClick (fw#2166)", () => {
|
|
270
|
+
const features = [
|
|
271
|
+
defineFeature("app", (r) => {
|
|
272
|
+
r.screen({
|
|
273
|
+
id: "x",
|
|
274
|
+
type: "projectionDetail",
|
|
275
|
+
query: "app:query:foo:detail",
|
|
276
|
+
layout: {
|
|
277
|
+
sections: [
|
|
278
|
+
{ kind: "relatedList", title: "s", query: "app:query:foo:list", columns: ["name"] },
|
|
279
|
+
],
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
}),
|
|
283
|
+
];
|
|
284
|
+
expect(() => validateBoot(features)).not.toThrow();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("validateBoot rejects a relatedList section on entityEdit (fw#2166)", () => {
|
|
288
|
+
const features = [
|
|
289
|
+
defineFeature("shop", (r) => {
|
|
290
|
+
r.entity("product", productEntity());
|
|
291
|
+
r.screen({
|
|
292
|
+
id: "product-edit",
|
|
293
|
+
type: "entityEdit",
|
|
294
|
+
entity: "product",
|
|
295
|
+
layout: {
|
|
296
|
+
sections: [
|
|
297
|
+
{ kind: "relatedList", title: "s", query: "shop:query:foo:list", columns: ["name"] },
|
|
298
|
+
],
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
}),
|
|
302
|
+
];
|
|
303
|
+
expect(() => validateBoot(features)).toThrow(
|
|
304
|
+
/\(entityEdit\) relatedList section "s".*projectionDetail-only/,
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("validateBoot rejects a relatedList section on configEdit (fw#2166)", () => {
|
|
309
|
+
const features = [
|
|
310
|
+
defineFeature("shop", (r) => {
|
|
311
|
+
r.config({
|
|
312
|
+
keys: { "site-name": createTenantConfig("text", { default: "" }) },
|
|
313
|
+
});
|
|
314
|
+
r.screen({
|
|
315
|
+
id: "settings",
|
|
316
|
+
type: "configEdit",
|
|
317
|
+
scope: "tenant",
|
|
318
|
+
configKeys: { siteName: "shop:config:site-name" },
|
|
319
|
+
fields: { siteName: { type: "text" } } as never,
|
|
320
|
+
layout: {
|
|
321
|
+
sections: [
|
|
322
|
+
{ kind: "relatedList", title: "s", query: "shop:query:foo:list", columns: ["name"] },
|
|
323
|
+
],
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
}),
|
|
327
|
+
];
|
|
328
|
+
expect(() => validateBoot(features)).toThrow(
|
|
329
|
+
/\(configEdit\) relatedList section "s".*projectionDetail-only/,
|
|
330
|
+
);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("validateBoot rejects a relatedList section on actionForm (fw#2166)", () => {
|
|
334
|
+
const features = [
|
|
335
|
+
defineFeature("shop", (r) => {
|
|
336
|
+
r.writeHandler(
|
|
337
|
+
"restock",
|
|
338
|
+
z.object({}),
|
|
339
|
+
async () => ({ isSuccess: true as const, data: null }),
|
|
340
|
+
{ access: { roles: ["Admin"] } },
|
|
341
|
+
);
|
|
342
|
+
r.screen({
|
|
343
|
+
id: "restock",
|
|
344
|
+
type: "actionForm",
|
|
345
|
+
handler: "shop:write:restock",
|
|
346
|
+
fields: { qty: { type: "number" } } as never,
|
|
347
|
+
layout: {
|
|
348
|
+
sections: [
|
|
349
|
+
{ kind: "relatedList", title: "s", query: "shop:query:foo:list", columns: ["name"] },
|
|
350
|
+
],
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
}),
|
|
354
|
+
];
|
|
355
|
+
expect(() => validateBoot(features)).toThrow(
|
|
356
|
+
/\(actionForm\) relatedList section "s".*projectionDetail-only/,
|
|
357
|
+
);
|
|
358
|
+
});
|
|
359
|
+
|
|
168
360
|
test("stores an entityEdit screen with sections + conditional fields", () => {
|
|
169
361
|
const feature = defineFeature("shop", (r) => {
|
|
170
362
|
r.entity("product", productEntity());
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isFieldsEditSection, normalizeEditField, normalizeListColumn } from "../screen-helpers";
|
|
2
2
|
import type {
|
|
3
3
|
EditFieldSpec,
|
|
4
4
|
EditLayout,
|
|
@@ -112,7 +112,13 @@ function validateEditLayoutNoFunctions(
|
|
|
112
112
|
layout: EditLayout,
|
|
113
113
|
): void {
|
|
114
114
|
for (const section of layout.sections) {
|
|
115
|
-
|
|
115
|
+
// relatedList carries `columns` instead of `fields` — route it through
|
|
116
|
+
// the same column check entityList/projectionList top-level columns use.
|
|
117
|
+
if (section.kind === "relatedList") {
|
|
118
|
+
validateColumnsNoFunctions(featureName, screenId, screenType, section.columns);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (!isFieldsEditSection(section)) continue;
|
|
116
122
|
for (const fieldSpec of section.fields) {
|
|
117
123
|
validateEditFieldNoFunctions(featureName, screenId, screenType, fieldSpec);
|
|
118
124
|
}
|
|
@@ -381,6 +381,35 @@ export function validateScreens(
|
|
|
381
381
|
`section to persist against.`,
|
|
382
382
|
);
|
|
383
383
|
}
|
|
384
|
+
if (section.kind === "relatedList") {
|
|
385
|
+
if (!section.query || typeof section.query !== "string") {
|
|
386
|
+
throw new Error(
|
|
387
|
+
`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) section "${section.title}" ` +
|
|
388
|
+
`(relatedList) has empty or non-string query.`,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
if (screen.layout.mode === "wizard") {
|
|
392
|
+
throw new Error(
|
|
393
|
+
`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) section "${section.title}" ` +
|
|
394
|
+
`is kind "relatedList" in a wizard layout — a read-only list is not supported in a ` +
|
|
395
|
+
`stepped form. Remove mode: "wizard" or drop the relatedList section.`,
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
if (section.rowClick !== undefined) {
|
|
399
|
+
const targetEntity = section.rowClick.entity;
|
|
400
|
+
const hasDetailScreen = [...featureMap.values()].some((f) =>
|
|
401
|
+
Object.values(f.screens).some((s) => s.detailFor === targetEntity),
|
|
402
|
+
);
|
|
403
|
+
if (!hasDetailScreen) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) section "${section.title}" ` +
|
|
406
|
+
`(relatedList) rowClick targets entity "${targetEntity}", but no screen declares ` +
|
|
407
|
+
`detailFor: "${targetEntity}". Add detailFor: "${targetEntity}" to the screen that shows it.`,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
384
413
|
if (section.fields.length === 0) {
|
|
385
414
|
throw new Error(
|
|
386
415
|
`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) has a section "${section.title}" ` +
|
|
@@ -474,6 +503,13 @@ export function validateScreens(
|
|
|
474
503
|
}
|
|
475
504
|
continue;
|
|
476
505
|
}
|
|
506
|
+
if (section.kind === "relatedList") {
|
|
507
|
+
throw new Error(
|
|
508
|
+
`[Feature ${feature.name}] Screen "${screenId}" (configEdit) relatedList section ` +
|
|
509
|
+
`"${section.title}" is not supported — relatedList is a projectionDetail-only ` +
|
|
510
|
+
`primitive (fw#2166).`,
|
|
511
|
+
);
|
|
512
|
+
}
|
|
477
513
|
if (section.fields.length === 0) {
|
|
478
514
|
throw new Error(
|
|
479
515
|
`[Feature ${feature.name}] Screen "${screenId}" (configEdit) has a section "${section.title}" ` +
|
|
@@ -582,6 +618,13 @@ export function validateScreens(
|
|
|
582
618
|
}
|
|
583
619
|
continue;
|
|
584
620
|
}
|
|
621
|
+
if (section.kind === "relatedList") {
|
|
622
|
+
throw new Error(
|
|
623
|
+
`[Feature ${feature.name}] Screen "${screenId}" (actionForm) relatedList section ` +
|
|
624
|
+
`"${section.title}" is not supported — relatedList is a projectionDetail-only ` +
|
|
625
|
+
`primitive (fw#2166).`,
|
|
626
|
+
);
|
|
627
|
+
}
|
|
585
628
|
if (section.fields.length === 0) {
|
|
586
629
|
throw new Error(
|
|
587
630
|
`[Feature ${feature.name}] Screen "${screenId}" (actionForm) has a section "${section.title}" ` +
|
|
@@ -909,6 +952,13 @@ export function validateScreens(
|
|
|
909
952
|
}
|
|
910
953
|
continue;
|
|
911
954
|
}
|
|
955
|
+
if (section.kind === "relatedList") {
|
|
956
|
+
throw new Error(
|
|
957
|
+
`[Feature ${feature.name}] Screen "${screenId}" (entityEdit) relatedList section ` +
|
|
958
|
+
`"${section.title}" is not supported — relatedList is a projectionDetail-only ` +
|
|
959
|
+
`primitive (fw#2166).`,
|
|
960
|
+
);
|
|
961
|
+
}
|
|
912
962
|
if (section.fields.length === 0) {
|
|
913
963
|
throw new Error(
|
|
914
964
|
`[Feature ${feature.name}] Screen "${screenId}" (entityEdit) has a section "${section.title}" ` +
|
package/src/engine/index.ts
CHANGED
|
@@ -227,7 +227,12 @@ export { runsInLane } from "./run-in";
|
|
|
227
227
|
export type { StepListOutcome } from "./run-pipeline";
|
|
228
228
|
export { runPipeline, runStepList } from "./run-pipeline";
|
|
229
229
|
export { buildInsertSchema, buildUpdateSchema, fieldToZod } from "./schema-builder";
|
|
230
|
-
export {
|
|
230
|
+
export {
|
|
231
|
+
isExtensionEditSection,
|
|
232
|
+
isFieldsEditSection,
|
|
233
|
+
normalizeEditField,
|
|
234
|
+
normalizeListColumn,
|
|
235
|
+
} from "./screen-helpers";
|
|
231
236
|
export type { TransitionGraph } from "./state-machine";
|
|
232
237
|
export { defineTransitions, guardTransition } from "./state-machine";
|
|
233
238
|
export {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
EditExtensionSection,
|
|
3
3
|
EditFieldSpec,
|
|
4
|
+
EditFieldsSection,
|
|
4
5
|
EditSectionSpec,
|
|
5
6
|
FieldCondition,
|
|
6
7
|
FormatSpec,
|
|
@@ -11,6 +12,10 @@ export function isExtensionEditSection(section: EditSectionSpec): section is Edi
|
|
|
11
12
|
return section.kind === "extension";
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
export function isFieldsEditSection(section: EditSectionSpec): section is EditFieldsSection {
|
|
16
|
+
return section.kind === undefined || section.kind === "fields";
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
// Type guard — narrows FieldRenderer to FormatSpec. Useful for renderer
|
|
15
20
|
// authors who branch on the three FieldRenderer variants without manual
|
|
16
21
|
// "format" in renderer checks.
|
|
@@ -153,6 +153,7 @@ export function requiredKeysFromScreen(
|
|
|
153
153
|
pushKey(out, section.title);
|
|
154
154
|
continue;
|
|
155
155
|
}
|
|
156
|
+
if (section.kind === "relatedList") continue; // rejected at boot, unreachable here
|
|
156
157
|
pushKey(out, section.title);
|
|
157
158
|
for (const f of section.fields) {
|
|
158
159
|
const fieldName = editFieldName(f);
|
|
@@ -174,6 +175,7 @@ export function requiredKeysFromScreen(
|
|
|
174
175
|
pushKey(out, section.title);
|
|
175
176
|
continue;
|
|
176
177
|
}
|
|
178
|
+
if (section.kind === "relatedList") continue; // rejected at boot, unreachable here
|
|
177
179
|
pushKey(out, section.title);
|
|
178
180
|
for (const f of section.fields) {
|
|
179
181
|
const fieldName = editFieldName(f);
|
|
@@ -195,6 +197,7 @@ export function requiredKeysFromScreen(
|
|
|
195
197
|
pushKey(out, section.title);
|
|
196
198
|
continue;
|
|
197
199
|
}
|
|
200
|
+
if (section.kind === "relatedList") continue; // rejected at boot, unreachable here
|
|
198
201
|
pushKey(out, section.title);
|
|
199
202
|
for (const f of section.fields) {
|
|
200
203
|
const fieldName = editFieldName(f);
|
|
@@ -209,6 +212,14 @@ export function requiredKeysFromScreen(
|
|
|
209
212
|
const detail = screen as ProjectionDetailScreenDefinition;
|
|
210
213
|
for (const section of detail.layout.sections) {
|
|
211
214
|
if (isExtensionEditSection(section)) continue; // rejected at boot, unreachable here
|
|
215
|
+
if (section.kind === "relatedList") {
|
|
216
|
+
pushKey(out, section.title);
|
|
217
|
+
for (const col of section.columns) {
|
|
218
|
+
const normalized = normalizeListColumn(col);
|
|
219
|
+
if (normalized.label !== undefined) pushKey(out, normalized.label);
|
|
220
|
+
}
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
212
223
|
pushKey(out, section.title);
|
|
213
224
|
for (const f of section.fields) {
|
|
214
225
|
const fieldName = editFieldName(f);
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
type EntityEditScreenDefinition,
|
|
31
31
|
type EntityListScreenDefinition,
|
|
32
32
|
type FieldDefinition,
|
|
33
|
-
|
|
33
|
+
isFieldsEditSection,
|
|
34
34
|
normalizeEditField,
|
|
35
35
|
normalizeListColumn,
|
|
36
36
|
parseQn,
|
|
@@ -243,7 +243,7 @@ function collectRequiredEditFields(
|
|
|
243
243
|
): string[] {
|
|
244
244
|
const out: string[] = [];
|
|
245
245
|
for (const section of screen.layout.sections) {
|
|
246
|
-
if (
|
|
246
|
+
if (!isFieldsEditSection(section)) continue;
|
|
247
247
|
for (const rawField of section.fields) {
|
|
248
248
|
const { field } = normalizeEditField(rawField);
|
|
249
249
|
const def = entity.fields[field];
|
|
@@ -281,7 +281,7 @@ function buildEditFillOps(
|
|
|
281
281
|
// form; Felder ohne Fixture-Wert (file/image/…) werden übersprungen.
|
|
282
282
|
const ops: EditFillOp[] = [];
|
|
283
283
|
for (const section of screen.layout.sections) {
|
|
284
|
-
if (
|
|
284
|
+
if (!isFieldsEditSection(section)) continue;
|
|
285
285
|
for (const raw of section.fields) {
|
|
286
286
|
const { field } = normalizeEditField(raw);
|
|
287
287
|
const def = entity.fields[field];
|
|
@@ -329,7 +329,7 @@ function pickIdentifyingForEdit(
|
|
|
329
329
|
: undefined;
|
|
330
330
|
|
|
331
331
|
for (const section of editScreen.layout.sections) {
|
|
332
|
-
if (
|
|
332
|
+
if (!isFieldsEditSection(section)) continue;
|
|
333
333
|
for (const rawField of section.fields) {
|
|
334
334
|
const { field } = normalizeEditField(rawField);
|
|
335
335
|
if (entity.fields[field]?.type !== "text") continue;
|
package/src/ui-types/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ export { parseRefTarget } from "../engine/parse-ref-target";
|
|
|
28
28
|
export {
|
|
29
29
|
evalFieldCondition,
|
|
30
30
|
isExtensionEditSection,
|
|
31
|
+
isFieldsEditSection,
|
|
31
32
|
isFormatSpec,
|
|
32
33
|
normalizeEditField,
|
|
33
34
|
normalizeListColumn,
|
|
@@ -71,6 +72,7 @@ export type {
|
|
|
71
72
|
EditFieldSpec,
|
|
72
73
|
EditFieldsSection,
|
|
73
74
|
EditLayout,
|
|
75
|
+
EditRelatedListSection,
|
|
74
76
|
EditSectionSpec,
|
|
75
77
|
EntityEditScreenDefinition,
|
|
76
78
|
EntityListScreenDefinition,
|