@cosmicdrift/kumiko-framework 0.222.0 → 0.224.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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.224.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>",
|
|
@@ -198,7 +198,7 @@
|
|
|
198
198
|
"./package.json": "./package.json"
|
|
199
199
|
},
|
|
200
200
|
"dependencies": {
|
|
201
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
201
|
+
"@cosmicdrift/kumiko-types": "0.224.0",
|
|
202
202
|
"bullmq": "^5.76.7",
|
|
203
203
|
"bun-types": "^1.3.13",
|
|
204
204
|
"hono": "^4.13.1",
|
|
@@ -214,7 +214,7 @@
|
|
|
214
214
|
"zod": "^4.4.3"
|
|
215
215
|
},
|
|
216
216
|
"devDependencies": {
|
|
217
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
217
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.224.0",
|
|
218
218
|
"bun-types": "^1.3.13",
|
|
219
219
|
"pino-pretty": "^13.1.3"
|
|
220
220
|
},
|
|
@@ -1355,6 +1355,119 @@ describe("boot-validator", () => {
|
|
|
1355
1355
|
];
|
|
1356
1356
|
expect(() => validateBoot(features)).not.toThrow();
|
|
1357
1357
|
});
|
|
1358
|
+
|
|
1359
|
+
test("accepts display: checkboxes with columns and maxRows", () => {
|
|
1360
|
+
const features = [
|
|
1361
|
+
defineFeature("driver", (r) => {
|
|
1362
|
+
r.entity(
|
|
1363
|
+
"profile",
|
|
1364
|
+
createEntity({
|
|
1365
|
+
fields: {
|
|
1366
|
+
tags: createMultiSelectField({
|
|
1367
|
+
options: ["a", "b", "c"] as const,
|
|
1368
|
+
display: "checkboxes",
|
|
1369
|
+
columns: 2,
|
|
1370
|
+
maxRows: 3,
|
|
1371
|
+
}),
|
|
1372
|
+
},
|
|
1373
|
+
}),
|
|
1374
|
+
);
|
|
1375
|
+
}),
|
|
1376
|
+
];
|
|
1377
|
+
expect(() => validateBoot(features)).not.toThrow();
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
test("rejects columns without display: checkboxes", () => {
|
|
1381
|
+
const features = [
|
|
1382
|
+
defineFeature("driver", (r) => {
|
|
1383
|
+
r.entity(
|
|
1384
|
+
"profile",
|
|
1385
|
+
createEntity({
|
|
1386
|
+
fields: {
|
|
1387
|
+
tags: createMultiSelectField({ options: ["a", "b"] as const, columns: 2 }),
|
|
1388
|
+
},
|
|
1389
|
+
}),
|
|
1390
|
+
);
|
|
1391
|
+
}),
|
|
1392
|
+
];
|
|
1393
|
+
expect(() => validateBoot(features)).toThrow(/columns.*display.*checkboxes/);
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
test("rejects columns when display is explicitly dropdown", () => {
|
|
1397
|
+
const features = [
|
|
1398
|
+
defineFeature("driver", (r) => {
|
|
1399
|
+
r.entity(
|
|
1400
|
+
"profile",
|
|
1401
|
+
createEntity({
|
|
1402
|
+
fields: {
|
|
1403
|
+
tags: createMultiSelectField({
|
|
1404
|
+
options: ["a", "b"] as const,
|
|
1405
|
+
display: "dropdown",
|
|
1406
|
+
columns: 2,
|
|
1407
|
+
}),
|
|
1408
|
+
},
|
|
1409
|
+
}),
|
|
1410
|
+
);
|
|
1411
|
+
}),
|
|
1412
|
+
];
|
|
1413
|
+
expect(() => validateBoot(features)).toThrow(/columns.*display.*checkboxes/);
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
test("rejects maxRows without display: checkboxes", () => {
|
|
1417
|
+
const features = [
|
|
1418
|
+
defineFeature("driver", (r) => {
|
|
1419
|
+
r.entity(
|
|
1420
|
+
"profile",
|
|
1421
|
+
createEntity({
|
|
1422
|
+
fields: {
|
|
1423
|
+
tags: createMultiSelectField({ options: ["a", "b"] as const, maxRows: 3 }),
|
|
1424
|
+
},
|
|
1425
|
+
}),
|
|
1426
|
+
);
|
|
1427
|
+
}),
|
|
1428
|
+
];
|
|
1429
|
+
expect(() => validateBoot(features)).toThrow(/maxRows.*display.*checkboxes/);
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
test("rejects maxRows below 1", () => {
|
|
1433
|
+
const features = [
|
|
1434
|
+
defineFeature("driver", (r) => {
|
|
1435
|
+
r.entity(
|
|
1436
|
+
"profile",
|
|
1437
|
+
createEntity({
|
|
1438
|
+
fields: {
|
|
1439
|
+
tags: createMultiSelectField({
|
|
1440
|
+
options: ["a", "b"] as const,
|
|
1441
|
+
display: "checkboxes",
|
|
1442
|
+
maxRows: 0,
|
|
1443
|
+
}),
|
|
1444
|
+
},
|
|
1445
|
+
}),
|
|
1446
|
+
);
|
|
1447
|
+
}),
|
|
1448
|
+
];
|
|
1449
|
+
expect(() => validateBoot(features)).toThrow(/invalid maxRows/);
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
test("rejects non-integer maxRows", () => {
|
|
1453
|
+
const features = [
|
|
1454
|
+
defineFeature("driver", (r) => {
|
|
1455
|
+
r.entity(
|
|
1456
|
+
"profile",
|
|
1457
|
+
createEntity({
|
|
1458
|
+
fields: {
|
|
1459
|
+
tags: createMultiSelectField({
|
|
1460
|
+
options: ["a", "b"] as const,
|
|
1461
|
+
display: "checkboxes",
|
|
1462
|
+
maxRows: 1.5,
|
|
1463
|
+
}),
|
|
1464
|
+
},
|
|
1465
|
+
}),
|
|
1466
|
+
);
|
|
1467
|
+
}),
|
|
1468
|
+
];
|
|
1469
|
+
expect(() => validateBoot(features)).toThrow(/invalid maxRows/);
|
|
1470
|
+
});
|
|
1358
1471
|
});
|
|
1359
1472
|
|
|
1360
1473
|
describe("image variants", () => {
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { BlurRegion, VariantSpec } from "@cosmicdrift/kumiko-types/derivatives-types";
|
|
2
2
|
import { VARIANT_NAME_PATTERN } from "../../derivatives/variant-key";
|
|
3
3
|
import { parseRefTarget } from "../parse-ref-target";
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
EmbeddedFieldDef,
|
|
6
|
+
EntityDefinition,
|
|
7
|
+
FeatureDefinition,
|
|
8
|
+
MultiSelectFieldDef,
|
|
9
|
+
} from "../types";
|
|
5
10
|
|
|
6
11
|
export const FILE_FIELD_TYPES = new Set(["file", "image", "files", "images"]);
|
|
7
12
|
|
|
@@ -676,6 +681,32 @@ function validateEmbeddedListMetadata(
|
|
|
676
681
|
// default — wenn gesetzt — ist eine Teilmenge der options. Beides würde
|
|
677
682
|
// auch im Zod-Schema bei runtime fehlschlagen, der Boot-Catch ist nur
|
|
678
683
|
// die früheste Stelle für klare Fehlermeldungen.
|
|
684
|
+
function validateCheckboxDisplayOptions(
|
|
685
|
+
field: MultiSelectFieldDef,
|
|
686
|
+
fieldName: string,
|
|
687
|
+
entityName: string,
|
|
688
|
+
): void {
|
|
689
|
+
if (field.display === "checkboxes") {
|
|
690
|
+
if (field.maxRows !== undefined && (!Number.isInteger(field.maxRows) || field.maxRows < 1)) {
|
|
691
|
+
throw new Error(
|
|
692
|
+
`MultiSelect field "${fieldName}" on entity "${entityName}" has invalid maxRows "${field.maxRows}" — must be a positive integer.`,
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
// skip: checkboxes is the valid display for these options — the checks below only apply to other displays
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
if (field.columns !== undefined) {
|
|
699
|
+
throw new Error(
|
|
700
|
+
`MultiSelect field "${fieldName}" on entity "${entityName}" sets columns, which is only valid with display: "checkboxes".`,
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
if (field.maxRows !== undefined) {
|
|
704
|
+
throw new Error(
|
|
705
|
+
`MultiSelect field "${fieldName}" on entity "${entityName}" sets maxRows, which is only valid with display: "checkboxes".`,
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
679
710
|
export function validateMultiSelectFields(feature: FeatureDefinition): void {
|
|
680
711
|
for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
|
|
681
712
|
for (const [fieldName, field] of Object.entries(entity.fields)) {
|
|
@@ -697,6 +728,8 @@ export function validateMultiSelectFields(feature: FeatureDefinition): void {
|
|
|
697
728
|
}
|
|
698
729
|
}
|
|
699
730
|
}
|
|
731
|
+
|
|
732
|
+
validateCheckboxDisplayOptions(field, fieldName, entityName);
|
|
700
733
|
}
|
|
701
734
|
}
|
|
702
735
|
}
|
package/src/engine/factories.ts
CHANGED
|
@@ -162,25 +162,32 @@ export function createSelectField<
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
/**
|
|
165
|
-
* Multi-
|
|
165
|
+
* Multi-select field — N values from a fixed option list.
|
|
166
166
|
*
|
|
167
|
-
* Storage: jsonb-Array<string>.
|
|
168
|
-
*
|
|
169
|
-
*
|
|
167
|
+
* Storage: jsonb-Array<string>. Every entry must be in `options`
|
|
168
|
+
* (boot-validator). Renders as a combobox dropdown by default; set
|
|
169
|
+
* `display: "checkboxes"` to render every option as a visible checkbox with
|
|
170
|
+
* a select-all toggle instead.
|
|
170
171
|
*
|
|
171
172
|
* ```ts
|
|
172
173
|
* licenceClasses: createMultiSelectField({
|
|
173
174
|
* options: ["B", "BE", "C", "C1", "CE", "C1E", "D", "D1"] as const,
|
|
174
175
|
* default: ["B"],
|
|
175
176
|
* }),
|
|
177
|
+
*
|
|
178
|
+
* languages: createMultiSelectField({
|
|
179
|
+
* options: ["en", "de", "es", "fr", "it"] as const,
|
|
180
|
+
* display: "checkboxes",
|
|
181
|
+
* columns: 2,
|
|
182
|
+
* }),
|
|
176
183
|
* ```
|
|
177
184
|
*
|
|
178
185
|
* Caller-API:
|
|
179
186
|
* Write: `{ licenceClasses: ["B", "BE", "C1"] }`
|
|
180
187
|
* Read: `{ licenceClasses: ["B", "BE", "C1"] }`
|
|
181
188
|
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
189
|
+
* Use instead of `select` when more than one value can be chosen at once.
|
|
190
|
+
* Use instead of `embedded` with booleans once there are more than ~5 options.
|
|
184
191
|
*/
|
|
185
192
|
export function createMultiSelectField<const TOptions extends readonly string[]>(
|
|
186
193
|
opts: { options: TOptions } & Partial<
|