@vectoriox/iox-ui 5.68.0 → 5.69.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/fesm2022/vectoriox-iox-ui-generation-core.mjs +39 -7
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
- package/generation-core/index.d.ts +3 -0
- package/package.json +1 -1
- package/src/generation-core/compose-site.ts +2 -1
- package/src/generation-core/plan-wire.ts +14 -5
- package/src/generation-core/plan-words.ts +3 -1
- package/src/generation-core/site-plan.model.ts +2 -0
- package/src/generation-core/site-spec.generated.ts +1 -0
- package/src/generation-core/validate-site-plan.ts +11 -0
|
@@ -69,6 +69,7 @@ declare const SPEC_SITE_PLAN_SHAPE: {
|
|
|
69
69
|
readonly count: "integer ≥ 0";
|
|
70
70
|
readonly sample: "boolean — items are flagged isSample";
|
|
71
71
|
readonly isPublished: "boolean";
|
|
72
|
+
readonly "perItem?": "per field, one string per sample: real content that wins over the template";
|
|
72
73
|
readonly "[field]?": "the field's value — strings are templates: {n} = the item's 1-based number, {sampleImage:n} / {sampleImage:n+k} = a bundled sample image";
|
|
73
74
|
};
|
|
74
75
|
readonly cmsDataSource: {
|
|
@@ -4371,6 +4372,8 @@ interface ContentSeedPlan {
|
|
|
4371
4372
|
readonly count: number;
|
|
4372
4373
|
readonly sample: boolean;
|
|
4373
4374
|
readonly isPublished: boolean;
|
|
4375
|
+
/** Per field, one value per sample: real content where the items are about a subject anyone can know. Wins over the field's template. */
|
|
4376
|
+
readonly perItem?: Readonly<Record<string, ReadonlyArray<string>>>;
|
|
4374
4377
|
readonly [field: string]: unknown;
|
|
4375
4378
|
}
|
|
4376
4379
|
/** The part a field plays (contract `site.roles`, AI plan L0-3): what a pattern binds instead of a field name, so one
|
package/package.json
CHANGED
|
@@ -1109,7 +1109,8 @@ function composeItems(type: ContentTypePlan, fillEmpty: boolean): ComposedItem[]
|
|
|
1109
1109
|
for (const field of type.fields) {
|
|
1110
1110
|
const given = seed[field.name];
|
|
1111
1111
|
const empty = !(field.name in seed) || given === '' || (Array.isArray(given) && !given.length);
|
|
1112
|
-
const
|
|
1112
|
+
const own = seed.perItem?.[field.name]?.[n - 1];
|
|
1113
|
+
const value = own || (empty && fillEmpty ? defaultSeed(type, field) : given);
|
|
1113
1114
|
if (value !== undefined) item[field.name] = expandSeedValue(value, n);
|
|
1114
1115
|
}
|
|
1115
1116
|
items.push({ ...item, isPublished: seed.isPublished, isSample: seed.sample });
|
|
@@ -130,6 +130,7 @@ const WIRE_KEYS: Readonly<Record<`${PlanWireEntry}`, Readonly<Record<string, Wir
|
|
|
130
130
|
seedValue: {
|
|
131
131
|
field: { refs: ['seed.[field]'], text: 'string — a field of the type' },
|
|
132
132
|
value: { refs: ['seed.[field]'], text: `string | string[] — ${describeShapeKey(PLAN_SHAPE['seed'], '[field]')} (a list for an array field)` },
|
|
133
|
+
each: { refs: ['seed.perItem'], text: 'string[] — one value per sample, in order, when each sample has its own content', none: '[]' },
|
|
133
134
|
},
|
|
134
135
|
dataSource: {
|
|
135
136
|
alias: { refs: ['cmsDataSource.alias', 'relativeDataSource.alias'] },
|
|
@@ -284,7 +285,7 @@ const shapedAs = (entry: `${PlanWireEntry}`, options: WireOptions, properties: R
|
|
|
284
285
|
};
|
|
285
286
|
|
|
286
287
|
export function wireContentTypeJsonSchema(options: WireOptions = {}): JsonSchema {
|
|
287
|
-
const value = shaped(PlanWireEntry.SeedValue, { field: jsonText(), value: jsonAnyOf([jsonText(), jsonListOf(jsonText())]) });
|
|
288
|
+
const value = shaped(PlanWireEntry.SeedValue, { field: jsonText(), value: jsonAnyOf([jsonText(), jsonListOf(jsonText())]), each: jsonListOf(jsonText()) });
|
|
288
289
|
return shapedAs(PlanWireEntry.ContentType, options, {
|
|
289
290
|
name: jsonText(),
|
|
290
291
|
label: jsonText(),
|
|
@@ -412,8 +413,16 @@ export function expandWireContentType(wire: unknown, options: WireOptions = {}):
|
|
|
412
413
|
if (!isPlainObject(seed)) return seed === undefined ? out : { ...out, seed };
|
|
413
414
|
const { values, ...fixed } = seed;
|
|
414
415
|
if (fixed['count'] === 0 && emptyList(values)) return out;
|
|
415
|
-
const
|
|
416
|
-
|
|
416
|
+
const perItem: Record<string, unknown> = {};
|
|
417
|
+
const plain = Array.isArray(values) ? values.map(row => {
|
|
418
|
+
if (!isPlainObject(row)) return row;
|
|
419
|
+
const { each, ...rest } = row;
|
|
420
|
+
if (Array.isArray(each) && each.length) perItem[String(rest['field'])] = each;
|
|
421
|
+
return rest;
|
|
422
|
+
}) : values;
|
|
423
|
+
const map = listToMap(plain ?? [], 'field', 'value');
|
|
424
|
+
const items = Object.keys(perItem).length ? { perItem } : {};
|
|
425
|
+
return { ...out, seed: isPlainObject(map) ? { ...fixed, ...items, ...map } : { ...fixed, values } };
|
|
417
426
|
}
|
|
418
427
|
|
|
419
428
|
export function expandWireDataSource(wire: unknown): DataSourcePlan | unknown {
|
|
@@ -501,10 +510,10 @@ export function fieldToWire(field: ContentFieldPlan): PlainObject {
|
|
|
501
510
|
export function contentTypeToWire(type: ContentTypePlan, options: WireOptions = {}): PlainObject {
|
|
502
511
|
const played = type.roles as Readonly<Record<string, string | undefined>>;
|
|
503
512
|
const roles = ROLE_KEYS.filter(role => played[role]).map(role => ({ role, field: played[role] }));
|
|
504
|
-
const { count = 0, sample = false, isPublished = false, ...values } = type.seed ?? {};
|
|
513
|
+
const { count = 0, sample = false, isPublished = false, perItem = {}, ...values } = type.seed ?? {};
|
|
505
514
|
return {
|
|
506
515
|
name: type.name, label: type.label, roles, ...(options.children === false ? {} : { fields: type.fields.map(fieldToWire) }),
|
|
507
|
-
seed: { count, sample, isPublished, values: mapToList(values, 'field', 'value') },
|
|
516
|
+
seed: { count, sample, isPublished, values: mapToList(values, 'field', 'value').map(row => ({ ...row, each: perItem[String(row['field'])] ?? [] })) },
|
|
508
517
|
};
|
|
509
518
|
}
|
|
510
519
|
|
|
@@ -45,7 +45,9 @@ export function writtenTexts(plan: SitePlan): WrittenText[] {
|
|
|
45
45
|
});
|
|
46
46
|
plan.contentTypes?.forEach((type, t) => {
|
|
47
47
|
add(`contentTypes[${t}].label`, type.label);
|
|
48
|
-
|
|
48
|
+
const { perItem, ...seed }: Record<string, unknown> & { perItem?: Readonly<Record<string, ReadonlyArray<string>>> } = type.seed ?? {};
|
|
49
|
+
for (const [field, list] of Object.entries(perItem ?? {})) list.forEach((v, k) => add(`contentTypes[${t}].seed.perItem.${field}[${k}]`, v));
|
|
50
|
+
for (const [field, value] of Object.entries(seed)) {
|
|
49
51
|
const values = Array.isArray(value) ? value : [value];
|
|
50
52
|
values.forEach((v, k) => add(`contentTypes[${t}].seed.${field}${Array.isArray(value) ? `[${k}]` : ''}`, v));
|
|
51
53
|
}
|
|
@@ -79,6 +79,8 @@ export interface ContentSeedPlan {
|
|
|
79
79
|
readonly count: number;
|
|
80
80
|
readonly sample: boolean;
|
|
81
81
|
readonly isPublished: boolean;
|
|
82
|
+
/** Per field, one value per sample: real content where the items are about a subject anyone can know. Wins over the field's template. */
|
|
83
|
+
readonly perItem?: Readonly<Record<string, ReadonlyArray<string>>>;
|
|
82
84
|
readonly [field: string]: unknown;
|
|
83
85
|
}
|
|
84
86
|
|
|
@@ -69,6 +69,7 @@ export const SPEC_SITE_PLAN_SHAPE = {
|
|
|
69
69
|
"count": "integer ≥ 0",
|
|
70
70
|
"sample": "boolean — items are flagged isSample",
|
|
71
71
|
"isPublished": "boolean",
|
|
72
|
+
"perItem?": "per field, one string per sample: real content that wins over the template",
|
|
72
73
|
"[field]?": "the field's value — strings are templates: {n} = the item's 1-based number, {sampleImage:n} / {sampleImage:n+k} = a bundled sample image"
|
|
73
74
|
},
|
|
74
75
|
"cmsDataSource": {
|
|
@@ -103,6 +103,7 @@ const ROLE_KEYS = shapeKeys(SHAPE['roles']);
|
|
|
103
103
|
const PAGE_SEO_KEYS = shapeKeys(SHAPE['pageSeo']);
|
|
104
104
|
const ARRAY_TYPE = 'array';
|
|
105
105
|
const FIELD_KEYS = shapeKeys(SHAPE['field']);
|
|
106
|
+
const SLUG_FIELD = 'slug';
|
|
106
107
|
const SEED_FIXED_KEYS = shapeKeys(SHAPE['seed']);
|
|
107
108
|
const CMS_SOURCE_KEYS = shapeKeys(SHAPE['cmsDataSource']);
|
|
108
109
|
const RELATIVE_SOURCE_KEYS = shapeKeys(SHAPE['relativeDataSource']);
|
|
@@ -226,6 +227,16 @@ function validateContentTypes(c: Collector, list: unknown[]): Map<string, Conten
|
|
|
226
227
|
else {
|
|
227
228
|
if (!Number.isInteger(seed['count']) || (seed['count'] as number) < 0) c.add(SitePlanIssueCode.Shape, `${path}.seed.count`, 'must be a whole number');
|
|
228
229
|
for (const key of ['sample', 'isPublished']) if (typeof seed[key] !== 'boolean') c.add(SitePlanIssueCode.Shape, `${path}.seed.${key}`, 'must be a boolean');
|
|
230
|
+
const perItem = seed['perItem'];
|
|
231
|
+
if (perItem !== undefined) {
|
|
232
|
+
if (!isObject(perItem)) c.add(SitePlanIssueCode.Shape, `${path}.seed.perItem`, 'must be an object');
|
|
233
|
+
else for (const [field, list] of Object.entries(perItem)) {
|
|
234
|
+
const at = `${path}.seed.perItem.${field}`;
|
|
235
|
+
if (!fieldSet.has(field)) c.add(SitePlanIssueCode.UnknownField, at, `"${field}" is not a field of ${name}`);
|
|
236
|
+
else if (!Array.isArray(list) || list.length !== seed['count'] || list.some(v => typeof v !== 'string')) c.add(SitePlanIssueCode.Shape, at, `must be ${String(seed['count'])} strings, one per sample`);
|
|
237
|
+
else if (field === SLUG_FIELD && new Set(list).size !== list.length) c.add(SitePlanIssueCode.Shape, at, 'each sample needs its own slug');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
229
240
|
for (const key of Object.keys(seed)) {
|
|
230
241
|
if (!SEED_FIXED_KEYS.includes(key) && !fieldSet.has(key)) c.add(SitePlanIssueCode.UnknownField, `${path}.seed.${key}`, `"${key}" is not a field of ${name}`);
|
|
231
242
|
}
|