@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectoriox/iox-ui",
3
- "version": "5.68.0",
3
+ "version": "5.69.0",
4
4
  "peerDependencies": {
5
5
  "@angular/common": ">=20.0.0",
6
6
  "@angular/core": ">=20.0.0",
@@ -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 value = empty && fillEmpty ? defaultSeed(type, field) : given;
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 map = listToMap(values ?? [], 'field', 'value');
416
- return { ...out, seed: isPlainObject(map) ? { ...fixed, ...map } : { ...fixed, values } };
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
- for (const [field, value] of Object.entries(type.seed ?? {})) {
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
  }