@sap-ux/fe-fpm-writer 1.0.8 → 1.1.1
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/dist/building-block/index.d.ts +10 -1
- package/dist/building-block/index.js +286 -6
- package/dist/building-block/prompts/questions/page.js +12 -1
- package/dist/building-block/types.d.ts +38 -0
- package/dist/building-block/types.js +19 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/prompts/translations/i18n.d.ts +5 -0
- package/dist/prompts/translations/i18n.js +5 -0
- package/package.json +3 -3
- package/templates/building-block/page/Controller.js +17 -0
- package/templates/building-block/page/Controller.ts +14 -0
- package/templates/building-block/page/View.xml +2 -1
- package/templates/building-block/page/actions.xml +4 -0
- package/templates/building-block/page/breadcrumbs.xml +4 -0
- package/templates/building-block/page/footer.xml +4 -0
- package/templates/building-block/page/headerContent.xml +4 -0
- package/templates/building-block/page/items.xml +9 -0
- package/templates/building-block/page/navigationActions.xml +4 -0
- package/templates/building-block/page/titleContent.xml +4 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Editor } from 'mem-fs-editor';
|
|
2
|
-
import { type BuildingBlock, type BuildingBlockConfig } from './types.js';
|
|
2
|
+
import { type BuildingBlock, type BuildingBlockConfig, type GenerateBuildingBlockAggregationConfig } from './types.js';
|
|
3
3
|
import { type CodeSnippet } from '../prompts/types.js';
|
|
4
4
|
/**
|
|
5
5
|
* Generates a building block into the provided xml view file.
|
|
@@ -10,6 +10,15 @@ import { type CodeSnippet } from '../prompts/types.js';
|
|
|
10
10
|
* @returns {Editor} the updated memfs editor instance
|
|
11
11
|
*/
|
|
12
12
|
export declare function generateBuildingBlock<T extends BuildingBlock>(basePath: string, config: BuildingBlockConfig<T>, fs?: Editor): Promise<Editor>;
|
|
13
|
+
/**
|
|
14
|
+
* Appends a single Page building block aggregation template to an existing `<macros:Page>` element in a view XML file.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} basePath - the base path of the application
|
|
17
|
+
* @param {GenerateBuildingBlockAggregationConfig} config - the aggregation configuration containing aggregationName and mContent
|
|
18
|
+
* @param {Editor} [fs] - the memfs editor instance
|
|
19
|
+
* @returns {Editor} the updated memfs editor instance
|
|
20
|
+
*/
|
|
21
|
+
export declare function generateBuildingBlockAggregation(basePath: string, config: GenerateBuildingBlockAggregationConfig, fs?: Editor): Promise<Editor>;
|
|
13
22
|
/**
|
|
14
23
|
* Method returns the manifest content for the required dependency library.
|
|
15
24
|
*
|
|
@@ -6,12 +6,12 @@ import { join, parse, relative } from 'node:path';
|
|
|
6
6
|
import { DOMParser, XMLSerializer } from '@xmldom/xmldom';
|
|
7
7
|
import format from 'xml-formatter';
|
|
8
8
|
import * as xpath from 'xpath';
|
|
9
|
-
import { getMinimumUI5Version } from '@sap-ux/project-access';
|
|
10
|
-
import { BuildingBlockType, bindingContextAbsolute } from './types.js';
|
|
9
|
+
import { getMinimumUI5Version, getAppProgrammingLanguage } from '@sap-ux/project-access';
|
|
10
|
+
import { BuildingBlockType, PAGE_AGGREGATIONS, PAGE_TEMPLATE_TYPE_FULL, bindingContextAbsolute } from './types.js';
|
|
11
11
|
import { getErrorMessage, validateBasePath, validateDependenciesLibs } from '../common/validate.js';
|
|
12
12
|
import { getTemplatePath } from '../templates.js';
|
|
13
13
|
import { CodeSnippetLanguage } from '../prompts/types.js';
|
|
14
|
-
import { CONFIG, createIdGenerator, detectTabSpacing, extendJSON, getRelativeTemplateComponentPath } from '../common/file.js';
|
|
14
|
+
import { CONFIG, copyTpl, createIdGenerator, detectTabSpacing, extendJSON, getRelativeTemplateComponentPath } from '../common/file.js';
|
|
15
15
|
import { getManifest, getManifestPath } from '../common/utils.js';
|
|
16
16
|
import { getOrAddNamespace } from './prompts/utils/xml.js';
|
|
17
17
|
import { i18nNamespaces, translate } from '../i18n.js';
|
|
@@ -21,6 +21,16 @@ const PLACEHOLDERS = {
|
|
|
21
21
|
'entitySet': 'REPLACE_WITH_ENTITY',
|
|
22
22
|
'qualifier': 'REPLACE_WITH_A_QUALIFIER'
|
|
23
23
|
};
|
|
24
|
+
const PAGE_TEMPLATE_COMMENT = 'This is a sample template, event handlers should be added for implementation';
|
|
25
|
+
/**
|
|
26
|
+
* Returns true if the building block data represents a Page building block with the full template type.
|
|
27
|
+
*
|
|
28
|
+
* @param data - the building block data
|
|
29
|
+
* @returns true if full Page template
|
|
30
|
+
*/
|
|
31
|
+
function isFullPageTemplate(data) {
|
|
32
|
+
return data.buildingBlockType === BuildingBlockType.Page && data.templateType === PAGE_TEMPLATE_TYPE_FULL;
|
|
33
|
+
}
|
|
24
34
|
/**
|
|
25
35
|
* Generates a building block into the provided xml view file.
|
|
26
36
|
*
|
|
@@ -47,6 +57,11 @@ export async function generateBuildingBlock(basePath, config, fs) {
|
|
|
47
57
|
aggregationNamespace
|
|
48
58
|
};
|
|
49
59
|
const templateDocument = getTemplateDocument({ ...processedBuildingBlockData, generateId: fnGenerateId }, xmlDocument, fs, manifest, templateConfig);
|
|
60
|
+
const fullPageTemplate = isFullPageTemplate(buildingBlockData);
|
|
61
|
+
if (fullPageTemplate) {
|
|
62
|
+
const pageData = buildingBlockData;
|
|
63
|
+
appendPageAggregations(fs, xmlDocument, templateDocument, fnGenerateId, pageData);
|
|
64
|
+
}
|
|
50
65
|
if (buildingBlockData.buildingBlockType === BuildingBlockType.RichTextEditor ||
|
|
51
66
|
buildingBlockData.buildingBlockType === BuildingBlockType.RichTextEditorButtonGroups) {
|
|
52
67
|
const minUI5Version = manifest ? coerce(getMinimumUI5Version(manifest)) : undefined;
|
|
@@ -57,6 +72,9 @@ export async function generateBuildingBlock(basePath, config, fs) {
|
|
|
57
72
|
getOrAddNamespace(xmlDocument, 'sap.fe.macros.richtexteditor', 'richtexteditor');
|
|
58
73
|
}
|
|
59
74
|
fs = updateViewFile(basePath, viewOrFragmentPath, updatedAggregationPath, xmlDocument, templateDocument, fs, config.replace);
|
|
75
|
+
if (fullPageTemplate) {
|
|
76
|
+
await applyPageControllerTemplate(fs, basePath, viewOrFragmentPath);
|
|
77
|
+
}
|
|
60
78
|
if (allowAutoAddDependencyLib && manifest && !validateDependenciesLibs(manifest, ['sap.fe.macros'])) {
|
|
61
79
|
// "sap.fe.macros" is missing - enhance manifest.json for missing "sap.fe.macros"
|
|
62
80
|
const manifestPath = await getManifestPath(basePath, fs);
|
|
@@ -71,6 +89,242 @@ export async function generateBuildingBlock(basePath, config, fs) {
|
|
|
71
89
|
}
|
|
72
90
|
return fs;
|
|
73
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Resolves the sap.fe.macros namespace prefix from the view document.
|
|
94
|
+
* If sap.fe.macros is the default namespace (no prefix), declares xmlns:macros on the document element
|
|
95
|
+
* so that generated prefixed elements like <macros:items> remain valid.
|
|
96
|
+
*
|
|
97
|
+
* @param xmlDocument - the view XML document
|
|
98
|
+
* @returns the resolved namespace prefix string (e.g. 'macros')
|
|
99
|
+
*/
|
|
100
|
+
function resolveMacrosPrefix(xmlDocument) {
|
|
101
|
+
const prefix = getOrAddNamespace(xmlDocument, 'sap.fe.macros', 'macros');
|
|
102
|
+
if (prefix === '') {
|
|
103
|
+
xmlDocument.documentElement.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:macros', 'sap.fe.macros');
|
|
104
|
+
return 'macros';
|
|
105
|
+
}
|
|
106
|
+
return prefix;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Renders a Page aggregation EJS template and parses it as an XML fragment document.
|
|
110
|
+
* Inherits all xmlns:* declarations from the view root so inner content can use any view-declared prefix.
|
|
111
|
+
*
|
|
112
|
+
* @param fs - the memfs editor instance
|
|
113
|
+
* @param aggName - the aggregation name (e.g. 'footer', 'items')
|
|
114
|
+
* @param aggContext - the EJS template context
|
|
115
|
+
* @param aggContext.macrosPrefix - the namespace prefix string (e.g. 'macros:')
|
|
116
|
+
* @param aggContext.mContent - optional inner XML content for the aggregation
|
|
117
|
+
* @param aggContext.aggId - the generated unique ID for the aggregation element
|
|
118
|
+
* @param fragMacrosNS - the namespace prefix resolved for sap.fe.macros
|
|
119
|
+
* @param xmlDocument - the view XML document (used to inherit namespace declarations)
|
|
120
|
+
* @returns parsed XML document whose documentElement contains the aggregation child nodes
|
|
121
|
+
*/
|
|
122
|
+
function buildPageAggregationFragment(fs, aggName, aggContext, fragMacrosNS, xmlDocument) {
|
|
123
|
+
const aggPath = getTemplatePath(`/building-block/page/${aggName}.xml`);
|
|
124
|
+
const aggContent = render(fs.read(aggPath), aggContext, {}); // NOSONAR - template is a controlled file on disk, not user input
|
|
125
|
+
const extraNamespaces = Array.from(xmlDocument.documentElement.attributes)
|
|
126
|
+
.filter((a) => a.name.startsWith('xmlns:') && a.name !== `xmlns:${fragMacrosNS}` && a.name !== 'xmlns:m')
|
|
127
|
+
.map((a) => `${a.name}="${a.value}"`)
|
|
128
|
+
.join(' ');
|
|
129
|
+
const wrapped = `<root xmlns:${fragMacrosNS}="sap.fe.macros" xmlns="sap.m" xmlns:m="sap.m" ${extraNamespaces}>${aggContent}</root>`;
|
|
130
|
+
const errorHandler = (level, message) => {
|
|
131
|
+
throw new Error(`Unable to parse page aggregation fragment '${aggName}'. Details: [${level}] - ${message}`);
|
|
132
|
+
};
|
|
133
|
+
return new DOMParser({ errorHandler }).parseFromString(wrapped, 'text/xml');
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Appends the 7 Page building block aggregation fragments as child elements of the templateDocument root.
|
|
137
|
+
*
|
|
138
|
+
* @param {Editor} fs - the memfs editor instance
|
|
139
|
+
* @param {Document} xmlDocument - the view XML document (used to resolve namespace prefixes)
|
|
140
|
+
* @param {Document} templateDocument - the template document whose root element receives the children
|
|
141
|
+
* @param {IdGeneratorFunction} generateId - function to generate unique IDs
|
|
142
|
+
* @param {Page} pageData - the Page building block data containing optional aggregation mContent
|
|
143
|
+
*/
|
|
144
|
+
function appendPageAggregations(fs, xmlDocument, templateDocument, generateId, pageData) {
|
|
145
|
+
const fragMacrosNS = resolveMacrosPrefix(xmlDocument);
|
|
146
|
+
const macrosPrefix = `${fragMacrosNS}:`;
|
|
147
|
+
const pageElement = templateDocument.documentElement;
|
|
148
|
+
pageElement.appendChild(templateDocument.createComment(PAGE_TEMPLATE_COMMENT));
|
|
149
|
+
for (const aggName of PAGE_AGGREGATIONS) {
|
|
150
|
+
const mContent = pageData.aggregations?.[aggName] ?? '';
|
|
151
|
+
const aggId = generateId(aggName);
|
|
152
|
+
const aggContext = { macrosPrefix, mContent, aggId };
|
|
153
|
+
const aggDoc = buildPageAggregationFragment(fs, aggName, aggContext, fragMacrosNS, xmlDocument);
|
|
154
|
+
for (const node of Array.from(aggDoc.documentElement.childNodes)) {
|
|
155
|
+
if (node.nodeType === 1 /* Element */) {
|
|
156
|
+
node.setAttribute('id', aggId);
|
|
157
|
+
pageElement.appendChild(templateDocument.importNode(node, true));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Returns the local name of an Element if it belongs to the sap.fe.macros namespace, otherwise an empty string.
|
|
164
|
+
* This ensures only Page aggregation elements are sorted by position; non-macros elements fall back to the items slot.
|
|
165
|
+
*
|
|
166
|
+
* @param el - the DOM Element
|
|
167
|
+
* @returns the local name string, or '' if not a sap.fe.macros element
|
|
168
|
+
*/
|
|
169
|
+
function getElementLocalName(el) {
|
|
170
|
+
if (el.namespaceURI !== 'sap.fe.macros') {
|
|
171
|
+
return '';
|
|
172
|
+
}
|
|
173
|
+
return typeof el.localName === 'string' ? el.localName : '';
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Builds a comparator for sorting XmlAggregationGroups by their position in aggNames.
|
|
177
|
+
* Unknown elements fall back to the position of 'items'. Ties are broken by original index.
|
|
178
|
+
*
|
|
179
|
+
* @param aggNames - ordered list of aggregation names
|
|
180
|
+
* @returns comparator function for Array.prototype.sort
|
|
181
|
+
*/
|
|
182
|
+
function buildAggregationComparator(aggNames) {
|
|
183
|
+
const itemsIdx = aggNames.indexOf('items');
|
|
184
|
+
const fallbackIdx = itemsIdx === -1 ? aggNames.length : itemsIdx;
|
|
185
|
+
return (a, b) => {
|
|
186
|
+
const aIdx = aggNames.indexOf(getElementLocalName(a.element));
|
|
187
|
+
const bIdx = aggNames.indexOf(getElementLocalName(b.element));
|
|
188
|
+
const aOrder = aIdx === -1 ? fallbackIdx : aIdx;
|
|
189
|
+
const bOrder = bIdx === -1 ? fallbackIdx : bIdx;
|
|
190
|
+
return aOrder === bOrder ? a.originalIndex - b.originalIndex : aOrder - bOrder;
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Reorders the child elements of a macros:Page node to match the canonical PAGE_AGGREGATIONS order.
|
|
195
|
+
* Preserves relative order of siblings with the same local name. Pure whitespace text nodes are dropped
|
|
196
|
+
* because the xml-formatter call that follows will regenerate proper indentation.
|
|
197
|
+
*
|
|
198
|
+
* @param pageElement - the macros:Page DOM node whose children should be sorted
|
|
199
|
+
*/
|
|
200
|
+
function sortPageAggregationChildren(pageElement) {
|
|
201
|
+
const allChildren = Array.from(pageElement.childNodes);
|
|
202
|
+
const aggNames = PAGE_AGGREGATIONS;
|
|
203
|
+
// Build pairs of [preceding comments, element] to preserve user comments.
|
|
204
|
+
// Comments that appear before the first element are treated as leading and will remain before all aggregation elements.
|
|
205
|
+
const groups = [];
|
|
206
|
+
const leadingComments = [];
|
|
207
|
+
let pendingComments = [];
|
|
208
|
+
let firstElementSeen = false;
|
|
209
|
+
for (const node of allChildren) {
|
|
210
|
+
if (node.nodeType === 8 /* Comment */) {
|
|
211
|
+
// Comments before the first element are leading; after, they are pending
|
|
212
|
+
(firstElementSeen ? pendingComments : leadingComments).push(node);
|
|
213
|
+
}
|
|
214
|
+
else if (node.nodeType === 1 /* Element */) {
|
|
215
|
+
firstElementSeen = true;
|
|
216
|
+
groups.push({ comments: pendingComments, element: node, originalIndex: groups.length });
|
|
217
|
+
pendingComments = [];
|
|
218
|
+
}
|
|
219
|
+
else if (node.nodeType === 3 /* Text */ && node.data?.trim()) {
|
|
220
|
+
// Preserve non-whitespace text nodes with their surrounding group
|
|
221
|
+
pendingComments.push(node);
|
|
222
|
+
}
|
|
223
|
+
// Pure whitespace text nodes are intentionally dropped (xml-formatter regenerates indentation)
|
|
224
|
+
}
|
|
225
|
+
groups.sort(buildAggregationComparator(aggNames));
|
|
226
|
+
while (pageElement.firstChild) {
|
|
227
|
+
pageElement.removeChild(pageElement.firstChild); // NOSONAR - xmldom nodes do not implement Node.remove()
|
|
228
|
+
}
|
|
229
|
+
// Re-insert leading comments first (always before any element)
|
|
230
|
+
for (const comment of leadingComments) {
|
|
231
|
+
pageElement.appendChild(comment);
|
|
232
|
+
}
|
|
233
|
+
for (const { comments, element } of groups) {
|
|
234
|
+
for (const comment of comments) {
|
|
235
|
+
pageElement.appendChild(comment);
|
|
236
|
+
}
|
|
237
|
+
pageElement.appendChild(element);
|
|
238
|
+
}
|
|
239
|
+
// Trailing orphan comments (after the last element)
|
|
240
|
+
for (const comment of pendingComments) {
|
|
241
|
+
pageElement.appendChild(comment);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Appends a single Page building block aggregation template to an existing `<macros:Page>` element in a view XML file.
|
|
246
|
+
*
|
|
247
|
+
* @param {string} basePath - the base path of the application
|
|
248
|
+
* @param {GenerateBuildingBlockAggregationConfig} config - the aggregation configuration containing aggregationName and mContent
|
|
249
|
+
* @param {Editor} [fs] - the memfs editor instance
|
|
250
|
+
* @returns {Editor} the updated memfs editor instance
|
|
251
|
+
*/
|
|
252
|
+
export async function generateBuildingBlockAggregation(basePath, config, fs) {
|
|
253
|
+
const { viewPath, buildingBlockType, aggregationName: aggName, mContent = '' } = config;
|
|
254
|
+
fs ??= create(createStorage());
|
|
255
|
+
if (buildingBlockType !== BuildingBlockType.Page) {
|
|
256
|
+
throw new Error(`generateBuildingBlockAggregation: unsupported building block type '${buildingBlockType}'. Only 'Page' is currently supported.`);
|
|
257
|
+
}
|
|
258
|
+
const xmlDocument = getUI5XmlDocument(basePath, viewPath, fs);
|
|
259
|
+
const generateId = await createIdGenerator({ basePath, fsEditor: fs });
|
|
260
|
+
const aggId = generateId(aggName);
|
|
261
|
+
const fragMacrosNS = resolveMacrosPrefix(xmlDocument);
|
|
262
|
+
const macrosPrefix = `${fragMacrosNS}:`;
|
|
263
|
+
const aggContext = { macrosPrefix, mContent, aggId };
|
|
264
|
+
const aggDoc = buildPageAggregationFragment(fs, aggName, aggContext, fragMacrosNS, xmlDocument);
|
|
265
|
+
const nsMap = xmlDocument.documentElement?._nsMap ?? {};
|
|
266
|
+
// Prefix-agnostic XPath — works regardless of the alias used in the view for sap.fe.macros.
|
|
267
|
+
const xpathSelect = xpath.useNamespaces(nsMap);
|
|
268
|
+
const pageNodes = xpathSelect(`//*[local-name()='Page' and namespace-uri()='sap.fe.macros']`, xmlDocument);
|
|
269
|
+
if (!pageNodes || !Array.isArray(pageNodes) || pageNodes.length === 0) {
|
|
270
|
+
throw new Error(`Page element (sap.fe.macros) not found in view ${viewPath}.`);
|
|
271
|
+
}
|
|
272
|
+
const pageElement = pageNodes[0];
|
|
273
|
+
if (aggName === 'footer' && pageElement.nodeType === 1 /* Element */) {
|
|
274
|
+
pageElement.setAttribute('showFooter', 'true');
|
|
275
|
+
}
|
|
276
|
+
const childNodes = Array.from(pageElement.childNodes);
|
|
277
|
+
const hasExistingAggregation = childNodes.some((node) => node.nodeType === 1 /* Element */ &&
|
|
278
|
+
node.localName === aggName &&
|
|
279
|
+
node.namespaceURI === 'sap.fe.macros');
|
|
280
|
+
if (hasExistingAggregation) {
|
|
281
|
+
sortPageAggregationChildren(pageElement);
|
|
282
|
+
const existingXmlContent = new XMLSerializer().serializeToString(xmlDocument);
|
|
283
|
+
fs.write(join(basePath, viewPath), format(existingXmlContent));
|
|
284
|
+
return fs;
|
|
285
|
+
}
|
|
286
|
+
const hasExistingElementChildren = childNodes.some((n) => n.nodeType === 1 /* Element */);
|
|
287
|
+
const hasTemplateComment = childNodes.some((n) => n.nodeType === 8 /* Comment */ && n.data?.includes(PAGE_TEMPLATE_COMMENT));
|
|
288
|
+
if (!hasExistingElementChildren && !hasTemplateComment) {
|
|
289
|
+
pageElement.appendChild(xmlDocument.createComment(PAGE_TEMPLATE_COMMENT));
|
|
290
|
+
}
|
|
291
|
+
for (const node of Array.from(aggDoc.documentElement.childNodes)) {
|
|
292
|
+
if (node.nodeType === 1 /* Element */) {
|
|
293
|
+
node.setAttribute('id', aggId);
|
|
294
|
+
pageElement.appendChild(xmlDocument.importNode(node, true));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
sortPageAggregationChildren(pageElement);
|
|
298
|
+
const newXmlContent = new XMLSerializer().serializeToString(xmlDocument);
|
|
299
|
+
fs.write(join(basePath, viewPath), format(newXmlContent));
|
|
300
|
+
return fs;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Copies the Page controller template (JS or TS) into the view directory if no controller file exists yet.
|
|
304
|
+
* Uses getAppProgrammingLanguage to decide whether to generate a JS or TS controller stub.
|
|
305
|
+
*
|
|
306
|
+
* @param {Editor} fs - the memfs editor instance
|
|
307
|
+
* @param {string} basePath - the base path of the application
|
|
308
|
+
* @param {string} viewOrFragmentPath - the relative path of the view/fragment file
|
|
309
|
+
*/
|
|
310
|
+
async function applyPageControllerTemplate(fs, basePath, viewOrFragmentPath) {
|
|
311
|
+
if (!viewOrFragmentPath.endsWith('.view.xml')) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const { dir: viewDir, name: viewName } = parse(viewOrFragmentPath);
|
|
315
|
+
const viewBaseName = viewName.replace(/\.view$/, '');
|
|
316
|
+
const tsControllerPath = join(basePath, viewDir, `${viewBaseName}.controller.ts`);
|
|
317
|
+
const jsControllerPath = join(basePath, viewDir, `${viewBaseName}.controller.js`);
|
|
318
|
+
// Skip if a controller already exists in either language to avoid duplicate stubs
|
|
319
|
+
if (fs.exists(tsControllerPath) || fs.exists(jsControllerPath)) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const detectedLanguage = await getAppProgrammingLanguage(basePath, fs);
|
|
323
|
+
const isTypeScript = detectedLanguage === 'TypeScript';
|
|
324
|
+
const controllerExt = isTypeScript ? 'ts' : 'js';
|
|
325
|
+
const controllerPath = isTypeScript ? tsControllerPath : jsControllerPath;
|
|
326
|
+
copyTpl(fs, getTemplatePath(`/building-block/page/Controller.${controllerExt}`), controllerPath);
|
|
327
|
+
}
|
|
74
328
|
/**
|
|
75
329
|
* Returns the UI5 xml file document (view/fragment).
|
|
76
330
|
*
|
|
@@ -259,7 +513,12 @@ function getTemplateDocument(buildingBlockData, viewDocument, fs, manifest, temp
|
|
|
259
513
|
* @returns {Editor} the updated memfs editor instance
|
|
260
514
|
*/
|
|
261
515
|
function updateViewFile(basePath, viewPath, aggregationPath, viewDocument, templateDocument, fs, replace = false) {
|
|
262
|
-
const
|
|
516
|
+
const firstChild = viewDocument.firstChild;
|
|
517
|
+
if (!firstChild) {
|
|
518
|
+
throw new Error(`Unable to read namespace map from view ${viewPath}.`);
|
|
519
|
+
}
|
|
520
|
+
const nsMap = firstChild?._nsMap ?? {};
|
|
521
|
+
const xpathSelect = xpath.useNamespaces(nsMap);
|
|
263
522
|
// Find target aggregated element and append template as child
|
|
264
523
|
const targetNodes = xpathSelect(aggregationPath, viewDocument);
|
|
265
524
|
if (targetNodes && Array.isArray(targetNodes) && targetNodes.length > 0) {
|
|
@@ -316,11 +575,32 @@ export async function getSerializedFileContent(basePath, config, fs) {
|
|
|
316
575
|
// Read the view xml and template files and get content of the view xml file
|
|
317
576
|
const xmlDocument = viewOrFragmentPath ? getUI5XmlDocument(basePath, viewOrFragmentPath, fs) : undefined;
|
|
318
577
|
const { content: manifest, path: manifestPath } = await getManifest(basePath, fs, false);
|
|
319
|
-
const
|
|
578
|
+
const fnGenerateId = buildingBlockData.generateId ?? (await createIdGenerator({ basePath, fsEditor: fs }));
|
|
579
|
+
const content = getTemplateContent({ ...buildingBlockData, generateId: fnGenerateId }, xmlDocument, manifest, fs, true);
|
|
580
|
+
// For the full Page template, augment the snippet with all 7 aggregations
|
|
581
|
+
let viewOrFragmentContent = content;
|
|
582
|
+
const pageData = buildingBlockData;
|
|
583
|
+
const isFullPage = isFullPageTemplate(buildingBlockData);
|
|
584
|
+
if (isFullPage) {
|
|
585
|
+
// Use the real view document for namespace resolution if available, otherwise create a minimal fallback
|
|
586
|
+
const nsDoc = xmlDocument ??
|
|
587
|
+
new DOMParser().parseFromString('<mvc:View xmlns:mvc="sap.ui.core.mvc" xmlns:macros="sap.fe.macros" xmlns="sap.m"/>', 'text/xml');
|
|
588
|
+
// Parse content directly so documentElement IS the <macros:Page> element,
|
|
589
|
+
// matching what appendPageAggregations expects as templateDocument.documentElement.
|
|
590
|
+
const snippetErrorHandler = (level, message) => {
|
|
591
|
+
throw new Error(`Unable to parse Page building block snippet. Details: [${level}] - ${message}`);
|
|
592
|
+
};
|
|
593
|
+
const snippetMacrosNS = getOrAddNamespace(nsDoc, 'sap.fe.macros', 'macros') || 'macros';
|
|
594
|
+
const snippetContent = `${content}`.replace(new RegExp(`^<(${snippetMacrosNS}:Page)`), `<$1 xmlns:${snippetMacrosNS}="sap.fe.macros"`);
|
|
595
|
+
const snippetDoc = new DOMParser({ errorHandler: snippetErrorHandler }).parseFromString(snippetContent, 'text/xml');
|
|
596
|
+
appendPageAggregations(fs, nsDoc, snippetDoc, fnGenerateId, pageData);
|
|
597
|
+
const resultNode = snippetDoc.documentElement;
|
|
598
|
+
viewOrFragmentContent = resultNode ? format(new XMLSerializer().serializeToString(resultNode)) : content;
|
|
599
|
+
}
|
|
320
600
|
const filePathProps = getFilePathProps(basePath, viewOrFragmentPath);
|
|
321
601
|
// Snippet for fragment xml
|
|
322
602
|
snippets['viewOrFragmentPath'] = {
|
|
323
|
-
content,
|
|
603
|
+
content: viewOrFragmentContent,
|
|
324
604
|
language: CodeSnippetLanguage.XML,
|
|
325
605
|
filePathProps
|
|
326
606
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i18nNamespaces, translate } from '../../../i18n.js';
|
|
2
2
|
import { getBuildingBlockIdPrompt, getViewOrFragmentPathPrompt, getAggregationPathPrompt } from '../utils/index.js';
|
|
3
|
-
import { BuildingBlockType } from '../../types.js';
|
|
3
|
+
import { BuildingBlockType, PAGE_TEMPLATE_TYPE_BASIC, PAGE_TEMPLATE_TYPE_FULL } from '../../types.js';
|
|
4
4
|
import { SapShortTextType, SapLongTextType } from '@sap-ux/i18n';
|
|
5
5
|
/**
|
|
6
6
|
* Returns a list of prompts required to generate a page building block.
|
|
@@ -12,6 +12,17 @@ export async function getPageBuildingBlockPrompts(context) {
|
|
|
12
12
|
const t = translate(i18nNamespaces.buildingBlock, 'prompts.page.');
|
|
13
13
|
return {
|
|
14
14
|
questions: [
|
|
15
|
+
{
|
|
16
|
+
type: 'list',
|
|
17
|
+
name: 'buildingBlockData.templateType',
|
|
18
|
+
message: t('templateType.message'),
|
|
19
|
+
default: PAGE_TEMPLATE_TYPE_BASIC,
|
|
20
|
+
choices: [
|
|
21
|
+
{ value: PAGE_TEMPLATE_TYPE_BASIC, name: t('templateType.basic') },
|
|
22
|
+
{ value: PAGE_TEMPLATE_TYPE_FULL, name: t('templateType.full') }
|
|
23
|
+
],
|
|
24
|
+
guiOptions: { mandatory: true }
|
|
25
|
+
},
|
|
15
26
|
getViewOrFragmentPathPrompt(context, t('viewOrFragmentPath.validate'), {
|
|
16
27
|
message: t('viewOrFragmentPath.message'),
|
|
17
28
|
guiOptions: {
|
|
@@ -367,6 +367,8 @@ export interface Table extends BuildingBlock {
|
|
|
367
367
|
* <macro:Page title="My Page Title" description="My Page Description" />
|
|
368
368
|
* @extends {BuildingBlock}
|
|
369
369
|
*/
|
|
370
|
+
export declare const PAGE_AGGREGATIONS: readonly ["breadcrumbs", "navigationActions", "titleContent", "actions", "headerContent", "items", "footer"];
|
|
371
|
+
export type PageAggregationName = (typeof PAGE_AGGREGATIONS)[number];
|
|
370
372
|
export interface Page extends BuildingBlock {
|
|
371
373
|
/**
|
|
372
374
|
* The title of the page.
|
|
@@ -376,6 +378,42 @@ export interface Page extends BuildingBlock {
|
|
|
376
378
|
* The description of the page.
|
|
377
379
|
*/
|
|
378
380
|
description?: string;
|
|
381
|
+
/**
|
|
382
|
+
* The template type for the page building block.
|
|
383
|
+
* 'full' generates a full page template with all aggregations and controller stubs.
|
|
384
|
+
* 'basic' generates a minimal self-closing tag (default behavior).
|
|
385
|
+
*/
|
|
386
|
+
templateType?: PageTemplateType;
|
|
387
|
+
/**
|
|
388
|
+
* Optional mContent strings keyed by aggregation name.
|
|
389
|
+
* When templateType is 'full', each entry is written as the inner XML of the corresponding aggregation.
|
|
390
|
+
*/
|
|
391
|
+
aggregations?: Partial<Record<PageAggregationName, string>>;
|
|
392
|
+
}
|
|
393
|
+
export declare const PAGE_TEMPLATE_TYPE_FULL: "full";
|
|
394
|
+
export declare const PAGE_TEMPLATE_TYPE_BASIC: "basic";
|
|
395
|
+
export type PageTemplateType = typeof PAGE_TEMPLATE_TYPE_FULL | typeof PAGE_TEMPLATE_TYPE_BASIC;
|
|
396
|
+
/**
|
|
397
|
+
* A group of XML nodes representing one Page aggregation element and its preceding sibling comments.
|
|
398
|
+
* Used when re-ordering aggregation children under a macros:Page element.
|
|
399
|
+
*/
|
|
400
|
+
export type XmlAggregationGroup = {
|
|
401
|
+
comments: Node[];
|
|
402
|
+
element: Element;
|
|
403
|
+
originalIndex: number;
|
|
404
|
+
};
|
|
405
|
+
/**
|
|
406
|
+
* Configuration for appending a named aggregation to an existing building block element in a view XML file.
|
|
407
|
+
*/
|
|
408
|
+
export interface GenerateBuildingBlockAggregationConfig {
|
|
409
|
+
/** Path to the view XML file, relative to basePath. */
|
|
410
|
+
viewPath: string;
|
|
411
|
+
/** Type of the building block whose aggregation should be appended. Currently only 'Page' is supported. */
|
|
412
|
+
buildingBlockType: BuildingBlockType;
|
|
413
|
+
/** Name of the aggregation to append. */
|
|
414
|
+
aggregationName: PageAggregationName;
|
|
415
|
+
/** Optional inner XML content for the aggregation. */
|
|
416
|
+
mContent?: string;
|
|
379
417
|
}
|
|
380
418
|
/**
|
|
381
419
|
* Represents a custom filter to be used inside the FilterBar.
|
|
@@ -20,4 +20,23 @@ export var BuildingBlockType;
|
|
|
20
20
|
})(BuildingBlockType || (BuildingBlockType = {}));
|
|
21
21
|
export const bindingContextAbsolute = 'absolute';
|
|
22
22
|
export const bindingContextRelative = 'relative';
|
|
23
|
+
/**
|
|
24
|
+
* Building block used to create a page.
|
|
25
|
+
* The page building block allows configuration of the title, and description.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* <macro:Page title="My Page Title" description="My Page Description" />
|
|
29
|
+
* @extends {BuildingBlock}
|
|
30
|
+
*/
|
|
31
|
+
export const PAGE_AGGREGATIONS = [
|
|
32
|
+
'breadcrumbs',
|
|
33
|
+
'navigationActions',
|
|
34
|
+
'titleContent',
|
|
35
|
+
'actions',
|
|
36
|
+
'headerContent',
|
|
37
|
+
'items',
|
|
38
|
+
'footer'
|
|
39
|
+
];
|
|
40
|
+
export const PAGE_TEMPLATE_TYPE_FULL = 'full';
|
|
41
|
+
export const PAGE_TEMPLATE_TYPE_BASIC = 'basic';
|
|
23
42
|
//# sourceMappingURL=types.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -20,8 +20,10 @@ export type { FPMConfig } from './app/index.js';
|
|
|
20
20
|
export { validateBasePath, validateVersion } from './common/validate.js';
|
|
21
21
|
export { createIdGenerator, type IdGeneratorFunction, getRelativeTemplateComponentPath } from './common/file.js';
|
|
22
22
|
export { BuildingBlockType } from './building-block/types.js';
|
|
23
|
-
export type { FilterBar, Form, Chart, Field, FieldFormatOptions, Table, BuildingBlockConfig, Page, CustomColumn, CustomFilterField, CustomFormField, RichTextEditor, ButtonGroupConfig, Action } from './building-block/types.js';
|
|
24
|
-
export {
|
|
23
|
+
export type { FilterBar, Form, Chart, Field, FieldFormatOptions, Table, BuildingBlockConfig, Page, CustomColumn, CustomFilterField, CustomFormField, RichTextEditor, ButtonGroupConfig, Action, PageTemplateType } from './building-block/types.js';
|
|
24
|
+
export { PAGE_TEMPLATE_TYPE_FULL, PAGE_TEMPLATE_TYPE_BASIC, PAGE_AGGREGATIONS } from './building-block/types.js';
|
|
25
|
+
export type { PageAggregationName, GenerateBuildingBlockAggregationConfig } from './building-block/types.js';
|
|
26
|
+
export { generateBuildingBlock, getSerializedFileContent, generateBuildingBlockAggregation } from './building-block/index.js';
|
|
25
27
|
export type { ChartPromptsAnswer, FilterBarPromptsAnswer, FormPromptsAnswer, TablePromptsAnswer, PagePromptsAnswer, RichTextEditorPromptsAnswer, RichTextEditorButtonGroupsPromptsAnswer, BuildingBlockTypePromptsAnswer } from './building-block/prompts/questions/index.js';
|
|
26
28
|
export { PromptsType, PromptsAPI } from './prompts/index.js';
|
|
27
29
|
export type { SupportedGeneratorAnswers, PromptsGroup, Prompts, ValidationResults, Answers, Subset, CodeSnippet } from './prompts/index.js';
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,8 @@ export { enableFPM } from './app/index.js';
|
|
|
12
12
|
export { validateBasePath, validateVersion } from './common/validate.js';
|
|
13
13
|
export { createIdGenerator, getRelativeTemplateComponentPath } from './common/file.js';
|
|
14
14
|
export { BuildingBlockType } from './building-block/types.js';
|
|
15
|
-
export {
|
|
15
|
+
export { PAGE_TEMPLATE_TYPE_FULL, PAGE_TEMPLATE_TYPE_BASIC, PAGE_AGGREGATIONS } from './building-block/types.js';
|
|
16
|
+
export { generateBuildingBlock, getSerializedFileContent, generateBuildingBlockAggregation } from './building-block/index.js';
|
|
16
17
|
export { PromptsType, PromptsAPI } from './prompts/index.js';
|
|
17
18
|
export { ControllerExtensionPageType } from './controller-extension/types.js';
|
|
18
19
|
export { generateControllerExtension } from './controller-extension/index.js';
|
|
@@ -278,6 +278,11 @@ const ns1 = {
|
|
|
278
278
|
'replaceDefaultButtonGroupsHint': 'Adding button groups replaces the default button groups in the Rich Text Editor with your chosen configuration.'
|
|
279
279
|
},
|
|
280
280
|
'page': {
|
|
281
|
+
'templateType': {
|
|
282
|
+
'message': 'Page Layout',
|
|
283
|
+
'basic': 'Basic',
|
|
284
|
+
'full': 'Full'
|
|
285
|
+
},
|
|
281
286
|
'id': {
|
|
282
287
|
'message': 'Building Block ID',
|
|
283
288
|
'validation': 'An ID is required to generate the page building block.'
|
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.
|
|
4
|
+
"version": "1.1.1",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/SAP/open-ux-tools.git",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"semver": "7.7.4",
|
|
32
32
|
"xml-formatter": "3.7.0",
|
|
33
33
|
"xpath": "0.0.34",
|
|
34
|
-
"@sap-ux/fiori-annotation-api": "1.0.
|
|
34
|
+
"@sap-ux/fiori-annotation-api": "1.0.9",
|
|
35
35
|
"@sap-ux/i18n": "1.0.1",
|
|
36
36
|
"@sap-ux/project-access": "2.1.2",
|
|
37
37
|
"@sap-ux/logger": "1.0.1"
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"@types/mem-fs-editor": "7.0.1",
|
|
45
45
|
"@types/semver": "7.7.1",
|
|
46
46
|
"@types/vinyl": "2.0.12",
|
|
47
|
-
"@sap-ux/ui-prompting": "1.0.
|
|
47
|
+
"@sap-ux/ui-prompting": "1.0.4"
|
|
48
48
|
},
|
|
49
49
|
"engines": {
|
|
50
50
|
"node": ">=22.x"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
sap.ui.define([], function () {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
return {
|
|
5
|
+
onPressHome: function (_event) {},
|
|
6
|
+
|
|
7
|
+
onPressPage1: function (_event) {},
|
|
8
|
+
|
|
9
|
+
onPressPage2: function (_event) {},
|
|
10
|
+
|
|
11
|
+
onFullScreen: function (_event) {},
|
|
12
|
+
|
|
13
|
+
onClickAction1: function (_event) {},
|
|
14
|
+
|
|
15
|
+
onClickAction2: function (_event) {}
|
|
16
|
+
};
|
|
17
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import ExtensionAPI from 'sap/fe/core/ExtensionAPI';
|
|
2
|
+
import Event from 'sap/ui/base/Event';
|
|
3
|
+
|
|
4
|
+
export function onPressHome(this: ExtensionAPI, _event: Event): void {}
|
|
5
|
+
|
|
6
|
+
export function onPressPage1(this: ExtensionAPI, _event: Event): void {}
|
|
7
|
+
|
|
8
|
+
export function onPressPage2(this: ExtensionAPI, _event: Event): void {}
|
|
9
|
+
|
|
10
|
+
export function onFullScreen(this: ExtensionAPI, _event: Event): void {}
|
|
11
|
+
|
|
12
|
+
export function onClickAction1(this: ExtensionAPI, _event: Event): void {}
|
|
13
|
+
|
|
14
|
+
export function onClickAction2(this: ExtensionAPI, _event: Event): void {}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<<%- macrosNamespace %>:Page
|
|
2
2
|
id="<%- data.id %>"<% if (data.title) { %>
|
|
3
3
|
title="<%- data.title %>"<% } %><% if (data.description) { %>
|
|
4
|
-
description="<%- data.description %>"<% } %>
|
|
4
|
+
description="<%- data.description %>"<% } %><% if (data.templateType === 'full') { %>
|
|
5
|
+
showFooter="true"<% } %>
|
|
5
6
|
/>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<<%- macrosPrefix %>items>
|
|
2
|
+
<!-- Main content -->
|
|
3
|
+
<IconTabBar id="iconTabBar<%- aggId %>" class="sapUiResponsiveContentPadding">
|
|
4
|
+
<items>
|
|
5
|
+
<IconTabFilter id="iconTabFilter<%- aggId %>" text="Tab 1">
|
|
6
|
+
</IconTabFilter>
|
|
7
|
+
</items>
|
|
8
|
+
</IconTabBar>
|
|
9
|
+
</<%- macrosPrefix %>items>
|