@sap-ux/fe-fpm-writer 1.3.8 → 1.3.9

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.
@@ -5,9 +5,9 @@ import { type IdGeneratorFunction } from '../common/file.js';
5
5
  /**
6
6
  * Returns the UI5 xml file document (view/fragment).
7
7
  *
8
- * @param {string} basePath - the base path
9
- * @param {string} viewPath - the path of the xml view relative to the base path
10
- * @param {Editor} fs - the memfs editor instance
8
+ * @param basePath - the base path
9
+ * @param viewPath - the path of the xml view relative to the base path
10
+ * @param fs - the memfs editor instance
11
11
  * @returns {Document} the view xml file document
12
12
  */
13
13
  export declare function getUI5XmlDocument(basePath: string, viewPath: string, fs: Editor): Document;
@@ -26,7 +26,6 @@ export declare function getPageAggregationNames(data: BuildingBlock): readonly P
26
26
  * @param manifest - the manifest content, or undefined if not available
27
27
  */
28
28
  export declare function validateFullPageTemplateVersion(manifest: Manifest | undefined): void;
29
- /** IDs needed per aggregation template, keyed by the variable name used in the EJS template. */
30
29
  /** Controls to generate IDs for per aggregation. `key` is the EJS template variable name, `base` is passed to generateId. */
31
30
  export declare const AGGREGATION_ID_KEYS: Partial<Record<PageAggregationName, {
32
31
  key: string;
@@ -38,16 +37,17 @@ export declare const AGGREGATION_ID_KEYS: Partial<Record<PageAggregationName, {
38
37
  *
39
38
  * @param aggName - the aggregation name
40
39
  * @param generateId - the project-aware ID generator
41
- * @returns an object mapping each template variable name to a unique ID string
40
+ * @param existingIds - IDs already present in the container (prevents collision on subsequent adds)
41
+ * @returns {Record<string, string>} an object mapping each template variable name to a unique ID string
42
42
  */
43
- export declare function buildAggregationIds(aggName: PageAggregationName, generateId: IdGeneratorFunction): Record<string, string>;
43
+ export declare function buildAggregationIds(aggName: PageAggregationName, generateId: IdGeneratorFunction, existingIds?: string[]): Record<string, string>;
44
44
  /**
45
45
  *
46
- * @param {Editor} fs - the memfs editor instance
47
- * @param {Document} xmlDocument - the view XML document (used to resolve namespace prefixes)
48
- * @param {Document} templateDocument - the template document whose root element receives the children
49
- * @param {IdGeneratorFunction} generateId - function to generate unique IDs
50
- * @param {readonly PageAggregationName[]} [aggNames] - aggregation names to append; defaults to all PAGE_AGGREGATIONS
46
+ * @param fs - the memfs editor instance
47
+ * @param xmlDocument - the view XML document (used to resolve namespace prefixes)
48
+ * @param templateDocument - the template document whose root element receives the children
49
+ * @param generateId - function to generate unique IDs
50
+ * @param [aggNames] - aggregation names to append; defaults to all PAGE_AGGREGATIONS
51
51
  * @param useDefaults - when true, the items aggregation renders its default IconTabBar content
52
52
  */
53
53
  export declare function appendPageAggregations(fs: Editor, xmlDocument: Document, templateDocument: Document, generateId: IdGeneratorFunction, aggNames?: readonly PageAggregationName[], useDefaults?: boolean): void;
@@ -65,16 +65,16 @@ export declare function sortPageAggregationChildren(pageElement: Node): void;
65
65
  * via the path prefix and the element is inserted in place. This allows generateBuildingBlock to
66
66
  * handle missing aggregation containers in a single write pass, avoiding a separate commit.
67
67
  *
68
- * @param {Document} xmlDocument - The XML document to mutate
69
- * @param {string} aggregationPath - Full XPath to the target aggregation (e.g. '/mvc:View/macros:Page/macros:items')
68
+ * @param xmlDocument - The XML document to mutate
69
+ * @param aggregationPath - Full XPath to the target aggregation (e.g. '/mvc:View/macros:Page/macros:items')
70
70
  */
71
71
  export declare function ensureMissingAggregation(xmlDocument: Document, aggregationPath: string): void;
72
72
  /**
73
73
  * Appends a single Page building block aggregation template to an existing `<macros:Page>` element in a view XML file.
74
74
  *
75
- * @param {string} basePath - the base path of the application
76
- * @param {GenerateBuildingBlockAggregationConfig} config - the aggregation configuration containing aggregationName
77
- * @param {Editor} [fs] - the memfs editor instance
75
+ * @param basePath - the base path of the application
76
+ * @param config - the aggregation configuration containing aggregationName
77
+ * @param [fs] - the memfs editor instance
78
78
  * @returns {Editor} the updated memfs editor instance
79
79
  */
80
80
  export declare function generateBuildingBlockAggregation(basePath: string, config: GenerateBuildingBlockAggregationConfig, fs?: Editor): Promise<Editor>;
@@ -17,9 +17,9 @@ import { resolveAggregationPath } from './processor.js';
17
17
  /**
18
18
  * Returns the UI5 xml file document (view/fragment).
19
19
  *
20
- * @param {string} basePath - the base path
21
- * @param {string} viewPath - the path of the xml view relative to the base path
22
- * @param {Editor} fs - the memfs editor instance
20
+ * @param basePath - the base path
21
+ * @param viewPath - the path of the xml view relative to the base path
22
+ * @param fs - the memfs editor instance
23
23
  * @returns {Document} the view xml file document
24
24
  */
25
25
  export function getUI5XmlDocument(basePath, viewPath, fs) {
@@ -71,7 +71,7 @@ export function validateFullPageTemplateVersion(manifest) {
71
71
  * so that generated prefixed elements like <macros:items> remain valid.
72
72
  *
73
73
  * @param xmlDocument - the view XML document
74
- * @returns the resolved namespace prefix string (e.g. 'macros')
74
+ * @returns {string} the resolved namespace prefix string (e.g. 'macros')
75
75
  */
76
76
  function resolveMacrosPrefix(xmlDocument) {
77
77
  const prefix = getOrAddNamespace(xmlDocument, 'sap.fe.macros', 'macros');
@@ -81,20 +81,24 @@ function resolveMacrosPrefix(xmlDocument) {
81
81
  }
82
82
  return prefix;
83
83
  }
84
- /** IDs needed per aggregation template, keyed by the variable name used in the EJS template. */
84
+ /**
85
+ * Page aggregations that can only appear once in the view.
86
+ * A second "Add" for these is a no-op; their creation form is disabled once present.
87
+ */
88
+ const SINGLE_INSTANCE_PAGE_AGGREGATIONS = new Set(['breadcrumbs', 'footer']);
85
89
  /** Controls to generate IDs for per aggregation. `key` is the EJS template variable name, `base` is passed to generateId. */
86
90
  export const AGGREGATION_ID_KEYS = {
87
91
  breadcrumbs: [
88
92
  { key: 'Breadcrumbs', base: 'breadcrumbs_breadcrumbs' },
89
93
  { key: 'Link', base: 'breadcrumbs_link' },
90
- { key: 'Link1', base: 'breadcrumbs_link_1' },
91
- { key: 'Link2', base: 'breadcrumbs_link_2' }
94
+ { key: 'Link1', base: 'breadcrumbs_link' },
95
+ { key: 'Link2', base: 'breadcrumbs_link' }
92
96
  ],
93
97
  navigationActions: [{ key: 'Button', base: 'navigationActions_button' }],
94
98
  titleContent: [{ key: 'GenericTag', base: 'titleContent_genericTag' }],
95
99
  actions: [
96
100
  { key: 'Button', base: 'actions_button' },
97
- { key: 'Button1', base: 'actions_button_1' }
101
+ { key: 'Button1', base: 'actions_button' }
98
102
  ],
99
103
  headerContent: [
100
104
  { key: 'VBox', base: 'headerContent_vbox' },
@@ -104,7 +108,7 @@ export const AGGREGATION_ID_KEYS = {
104
108
  { key: 'OverflowToolbar', base: 'footer_overflowToolbar' },
105
109
  { key: 'ToolbarSpacer', base: 'footer_toolbarSpacer' },
106
110
  { key: 'Button', base: 'footer_button' },
107
- { key: 'Button1', base: 'footer_button_1' }
111
+ { key: 'Button1', base: 'footer_button' }
108
112
  ]
109
113
  };
110
114
  /**
@@ -113,11 +117,12 @@ export const AGGREGATION_ID_KEYS = {
113
117
  *
114
118
  * @param aggName - the aggregation name
115
119
  * @param generateId - the project-aware ID generator
116
- * @returns an object mapping each template variable name to a unique ID string
120
+ * @param existingIds - IDs already present in the container (prevents collision on subsequent adds)
121
+ * @returns {Record<string, string>} an object mapping each template variable name to a unique ID string
117
122
  */
118
- export function buildAggregationIds(aggName, generateId) {
123
+ export function buildAggregationIds(aggName, generateId, existingIds = []) {
119
124
  const entries = AGGREGATION_ID_KEYS[aggName] ?? [];
120
- const validatedIds = [];
125
+ const validatedIds = [...existingIds];
121
126
  const ids = {};
122
127
  for (const { key, base } of entries) {
123
128
  const id = generateId(base, validatedIds);
@@ -137,6 +142,7 @@ export function buildAggregationIds(aggName, generateId) {
137
142
  * @param aggContext.aggId - the generated unique ID for the aggregation element
138
143
  * @param aggContext.showDefaultContent - when true, the items template renders the default IconTabBar
139
144
  * @param aggContext.ids - map of unique IDs for named controls in the template (e.g. ids.Button, ids.Link)
145
+ * @param aggContext.aggIndex - 1-based counter for this aggregation add (used to number text/press handler names)
140
146
  * @param fragMacrosNS - the namespace prefix resolved for sap.fe.macros
141
147
  * @param xmlDocument - the view XML document (used to inherit namespace declarations)
142
148
  * @returns parsed XML document whose documentElement contains the aggregation child nodes
@@ -155,11 +161,11 @@ function buildPageAggregationFragment(fs, aggName, aggContext, fragMacrosNS, xml
155
161
  }
156
162
  /**
157
163
  *
158
- * @param {Editor} fs - the memfs editor instance
159
- * @param {Document} xmlDocument - the view XML document (used to resolve namespace prefixes)
160
- * @param {Document} templateDocument - the template document whose root element receives the children
161
- * @param {IdGeneratorFunction} generateId - function to generate unique IDs
162
- * @param {readonly PageAggregationName[]} [aggNames] - aggregation names to append; defaults to all PAGE_AGGREGATIONS
164
+ * @param fs - the memfs editor instance
165
+ * @param xmlDocument - the view XML document (used to resolve namespace prefixes)
166
+ * @param templateDocument - the template document whose root element receives the children
167
+ * @param generateId - function to generate unique IDs
168
+ * @param [aggNames] - aggregation names to append; defaults to all PAGE_AGGREGATIONS
163
169
  * @param useDefaults - when true, the items aggregation renders its default IconTabBar content
164
170
  */
165
171
  export function appendPageAggregations(fs, xmlDocument, templateDocument, generateId, aggNames = PAGE_AGGREGATIONS, useDefaults = true) {
@@ -171,7 +177,7 @@ export function appendPageAggregations(fs, xmlDocument, templateDocument, genera
171
177
  const aggId = generateId(aggName);
172
178
  const showDefaultContent = aggName === 'items' && useDefaults;
173
179
  const ids = buildAggregationIds(aggName, generateId);
174
- const aggContext = { macrosPrefix, aggId, showDefaultContent, ids };
180
+ const aggContext = { macrosPrefix, aggId, showDefaultContent, ids, aggIndex: 1 };
175
181
  const aggDoc = buildPageAggregationFragment(fs, aggName, aggContext, fragMacrosNS, xmlDocument);
176
182
  for (const node of Array.from(aggDoc.documentElement.childNodes)) {
177
183
  if (node.nodeType === 1 /* Element */) {
@@ -268,8 +274,8 @@ export function sortPageAggregationChildren(pageElement) {
268
274
  * via the path prefix and the element is inserted in place. This allows generateBuildingBlock to
269
275
  * handle missing aggregation containers in a single write pass, avoiding a separate commit.
270
276
  *
271
- * @param {Document} xmlDocument - The XML document to mutate
272
- * @param {string} aggregationPath - Full XPath to the target aggregation (e.g. '/mvc:View/macros:Page/macros:items')
277
+ * @param xmlDocument - The XML document to mutate
278
+ * @param aggregationPath - Full XPath to the target aggregation (e.g. '/mvc:View/macros:Page/macros:items')
273
279
  */
274
280
  export function ensureMissingAggregation(xmlDocument, aggregationPath) {
275
281
  const nsMap = xmlDocument.documentElement?._nsMap ?? {};
@@ -339,12 +345,73 @@ function wrapLooseBuildingBlocksInItems(pageElement, xmlDocument, macrosNS, macr
339
345
  itemsEl.appendChild(child);
340
346
  }
341
347
  }
348
+ /**
349
+ * Collects all `id` attribute values from direct Element children of the given container.
350
+ * Used to seed the ID generator so new IDs don't collide with existing ones.
351
+ *
352
+ * @param container - the aggregation container element, or undefined if not present
353
+ * @returns {string[]} array of existing child element IDs
354
+ */
355
+ function getExistingContainerIds(container) {
356
+ if (!container) {
357
+ return [];
358
+ }
359
+ return Array.from(container.childNodes)
360
+ .filter((n) => n.nodeType === 1 /* Element */)
361
+ .map((n) => n.getAttribute('id'))
362
+ .filter((id) => !!id);
363
+ }
364
+ /**
365
+ * Derives the 1-based sequential index for a new aggregation add from the existing container.
366
+ * The index equals the number of existing child elements + 1, ensuring text/press handler
367
+ * names continue sequentially (e.g. "Action 3" after two buttons already exist).
368
+ *
369
+ * @param existingContainer - the existing aggregation container, or undefined on first add
370
+ * @returns {number} 1-based start index for text/press handler numbering
371
+ */
372
+ function deriveAggregationIndex(existingContainer) {
373
+ if (!existingContainer) {
374
+ return 1;
375
+ }
376
+ const existingChildCount = Array.from(existingContainer.childNodes).filter((n) => n.nodeType === 1 /* Element */).length;
377
+ return existingChildCount + 1;
378
+ }
379
+ /**
380
+ * Serializes and writes the XML document to the view file on disk.
381
+ *
382
+ * @param fs - the memfs editor instance
383
+ * @param basePath - the base path of the application
384
+ * @param viewPath - the path of the xml view relative to the base path
385
+ * @param xmlDocument - the XML document to serialize and write
386
+ */
387
+ function writeXmlDocument(fs, basePath, viewPath, xmlDocument) {
388
+ fs.write(join(basePath, viewPath), format(new XMLSerializer().serializeToString(xmlDocument)));
389
+ }
390
+ /**
391
+ * Appends child elements from the rendered aggregation template into an existing container element.
392
+ * aggDoc.documentElement is `<root xmlns:macros=...>`; its first Element child is `<macros:aggName>`.
393
+ *
394
+ * @param existingContainer - the existing aggregation container element in the view
395
+ * @param aggDoc - the rendered template document whose first Element child holds the new children
396
+ * @param xmlDocument - the owner document (used to import nodes)
397
+ */
398
+ function appendChildrenIntoContainer(existingContainer, aggDoc, xmlDocument) {
399
+ const renderedWrapper = Array.from(aggDoc.documentElement.childNodes).find((n) => n.nodeType === 1 /* Element */);
400
+ if (!renderedWrapper) {
401
+ return;
402
+ }
403
+ for (const child of Array.from(renderedWrapper.childNodes)) {
404
+ if (child.nodeType === 1 /* Element */) {
405
+ existingContainer.appendChild(xmlDocument.importNode(child, true));
406
+ }
407
+ }
408
+ }
342
409
  /**
343
410
  * Appends a single Page building block aggregation template to an existing `<macros:Page>` element in a view XML file.
344
411
  *
345
- * @param {string} basePath - the base path of the application
346
- * @param {GenerateBuildingBlockAggregationConfig} config - the aggregation configuration containing aggregationName
347
- * @param {Editor} [fs] - the memfs editor instance
412
+ * @param basePath - the base path of the application
413
+ * @param config - the aggregation configuration containing aggregationName
414
+ * @param [fs] - the memfs editor instance
348
415
  * @returns {Editor} the updated memfs editor instance
349
416
  */
350
417
  export async function generateBuildingBlockAggregation(basePath, config, fs) {
@@ -358,9 +425,6 @@ export async function generateBuildingBlockAggregation(basePath, config, fs) {
358
425
  const aggId = generateId(aggName);
359
426
  const fragMacrosNS = resolveMacrosPrefix(xmlDocument);
360
427
  const macrosPrefix = `${fragMacrosNS}:`;
361
- const ids = buildAggregationIds(aggName, generateId);
362
- const aggContext = { macrosPrefix, aggId, showDefaultContent: false, ids };
363
- const aggDoc = buildPageAggregationFragment(fs, aggName, aggContext, fragMacrosNS, xmlDocument);
364
428
  const nsMap = xmlDocument.documentElement?._nsMap ?? {};
365
429
  // Prefix-agnostic XPath — works regardless of the alias used in the view for sap.fe.macros.
366
430
  const xpathSelect = xpath.useNamespaces(nsMap);
@@ -373,20 +437,39 @@ export async function generateBuildingBlockAggregation(basePath, config, fs) {
373
437
  pageElement.setAttribute('showFooter', 'true');
374
438
  }
375
439
  const childNodes = Array.from(pageElement.childNodes);
440
+ const macrosNsUri = nsMap[fragMacrosNS] ?? 'sap.fe.macros';
376
441
  const hasExistingAggregation = childNodes.some((node) => node.nodeType === 1 /* Element */ &&
377
442
  node.localName === aggName &&
378
- node.namespaceURI === (nsMap[fragMacrosNS] ?? 'sap.fe.macros'));
443
+ node.namespaceURI === macrosNsUri);
444
+ // Find existing container early so its child IDs can seed the ID generator,
445
+ // preventing collisions when appending to an already-populated aggregation.
446
+ const existingContainer = hasExistingAggregation
447
+ ? childNodes.find((n) => n.nodeType === 1 /* Element */ &&
448
+ n.localName === aggName &&
449
+ n.namespaceURI === macrosNsUri)
450
+ : undefined;
451
+ if (hasExistingAggregation && SINGLE_INSTANCE_PAGE_AGGREGATIONS.has(aggName)) {
452
+ return fs;
453
+ }
454
+ const existingContainerIds = getExistingContainerIds(existingContainer);
455
+ const ids = buildAggregationIds(aggName, generateId, existingContainerIds);
456
+ const aggIndex = deriveAggregationIndex(existingContainer);
457
+ const aggContext = { macrosPrefix, aggId, showDefaultContent: false, ids, aggIndex };
458
+ const aggDoc = buildPageAggregationFragment(fs, aggName, aggContext, fragMacrosNS, xmlDocument);
379
459
  if (hasExistingAggregation) {
460
+ // Append-children aggregations (navigationActions, titleContent, actions, headerContent):
461
+ // append new children from the rendered template into the existing container.
462
+ if (existingContainer) {
463
+ appendChildrenIntoContainer(existingContainer, aggDoc, xmlDocument);
464
+ }
380
465
  sortPageAggregationChildren(pageElement);
381
- const existingXmlContent = new XMLSerializer().serializeToString(xmlDocument);
382
- fs.write(join(basePath, viewPath), format(existingXmlContent));
466
+ writeXmlDocument(fs, basePath, viewPath, xmlDocument);
383
467
  return fs;
384
468
  }
385
469
  const hasExistingElementChildren = childNodes.some((n) => n.nodeType === 1 /* Element */);
386
470
  const hasTemplateComment = childNodes.some((n) => n.nodeType === 8 /* Comment */ && n.data?.includes(PAGE_TEMPLATE_COMMENT));
387
471
  // Move any loose macros building blocks (e.g. macros:Form, macros:Table) into macros:items
388
472
  // before inserting the new named aggregation so the Page DOM stays well-formed.
389
- const macrosNsUri = nsMap[fragMacrosNS] ?? 'sap.fe.macros';
390
473
  wrapLooseBuildingBlocksInItems(pageElement, xmlDocument, macrosNsUri, fragMacrosNS);
391
474
  if (!hasExistingElementChildren && !hasTemplateComment) {
392
475
  pageElement.appendChild(xmlDocument.createComment(PAGE_TEMPLATE_COMMENT));
@@ -397,8 +480,7 @@ export async function generateBuildingBlockAggregation(basePath, config, fs) {
397
480
  }
398
481
  }
399
482
  sortPageAggregationChildren(pageElement);
400
- const newXmlContent = new XMLSerializer().serializeToString(xmlDocument);
401
- fs.write(join(basePath, viewPath), format(newXmlContent));
483
+ writeXmlDocument(fs, basePath, viewPath, xmlDocument);
402
484
  return fs;
403
485
  }
404
486
  //# sourceMappingURL=processAggregation.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sap-ux/fe-fpm-writer",
3
3
  "description": "SAP Fiori elements flexible programming model writer",
4
- "version": "1.3.8",
4
+ "version": "1.3.9",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/SAP/open-ux-tools.git",
@@ -31,8 +31,8 @@
31
31
  "semver": "7.8.5",
32
32
  "xml-formatter": "3.7.0",
33
33
  "xpath": "0.0.34",
34
- "@sap-ux/fiori-annotation-api": "1.0.20",
35
34
  "@sap-ux/i18n": "1.0.2",
35
+ "@sap-ux/fiori-annotation-api": "1.0.20",
36
36
  "@sap-ux/project-access": "2.1.9",
37
37
  "@sap-ux/logger": "1.0.3"
38
38
  },
@@ -1,5 +1,5 @@
1
1
  <<%- macrosPrefix %>actions>
2
2
  <!-- Actions aggregation -->
3
- <Button id="<%- ids.Button %>" text="Action 1" press=".onActionsClickAction1" type="Ghost" />
4
- <Button id="<%- ids.Button1 %>" text="Action 2" press=".onActionsClickAction2" type="Ghost" />
3
+ <Button id="<%- ids.Button %>" text="Action <%- aggIndex %>" press=".onActionsClickAction<%- aggIndex %>" type="Ghost" /><% if (aggIndex === 1) { %>
4
+ <Button id="<%- ids.Button1 %>" text="Action <%- aggIndex + 1 %>" press=".onActionsClickAction<%- aggIndex + 1 %>" type="Ghost" /><% } %>
5
5
  </<%- macrosPrefix %>actions>
@@ -1,6 +1,6 @@
1
1
  <<%- macrosPrefix %>headerContent>
2
2
  <!-- Header content aggregation (content after avatar) -->
3
3
  <VBox id="<%- ids.VBox %>">
4
- <Title id="<%- ids.Title %>" text="Page Title" />
4
+ <Title id="<%- ids.Title %>" text="Page Title <%- aggIndex %>" />
5
5
  </VBox>
6
6
  </<%- macrosPrefix %>headerContent>
@@ -1,4 +1,4 @@
1
1
  <<%- macrosPrefix %>navigationActions>
2
2
  <!-- Navigation actions aggregation -->
3
- <Button id="<%- ids.Button %>" icon="sap-icon://full-screen" press=".onNavigationActionsFullScreen" type="Transparent" />
3
+ <Button id="<%- ids.Button %>" icon="sap-icon://full-screen" press=".onNavigationActionsFullScreen<%- aggIndex %>" type="Transparent" />
4
4
  </<%- macrosPrefix %>navigationActions>
@@ -1,4 +1,4 @@
1
1
  <<%- macrosPrefix %>titleContent>
2
2
  <!-- Title content aggregation (content next to title) -->
3
- <GenericTag id="<%- ids.GenericTag %>" text="Status" design="StatusIconHidden" status="Information" />
3
+ <GenericTag id="<%- ids.GenericTag %>" text="Status <%- aggIndex %>" design="StatusIconHidden" status="Information" />
4
4
  </<%- macrosPrefix %>titleContent>