Skip to content

Commit 43dc48c

Browse files
docirl815are
andauthored
feat(fpm-writer): table building block custom columns (#3665)
* feat(fpm-writer): building block custom columns * feat(fpm-writer): lint * feat(fpm-writer): tests * feat(fe-fpm-writer): move custom column building block logic * feat(fe-fpm-writer): fixes event handler, custom section * fix(fe-fpm-writer): lint and cleanup * fix(fe-fpm-writer): review comment/cleanup * fix(fe-fpm-writer): sonar cleanup * feat(fe-fpm-writer): code review, interface updates * fix(fe-fpm-writer): remove eventhandler from interface * fix(fe-fpm-writer): review comments * fix(fe-fpm-writer): review comments * fix: avoid unused property avoid unused property * test: adjust tests after unused property adjust tests after unused property * test: adjust test to generate correct example in snapshot adjust test to generate correct example in snapshot * test: missing call for generator and check new column is sibling of existing column missing call for generator and check new column is sibling of existing column * feat(fe-fpm-writer): changeset * fix: reuse calculated fragment folder path from common reuse calculated fragment folder path from common * fix: unused and deprecated line which is calculated within common functions unused and deprecated line which is calculated within common functions * test: additional expects additional expects --------- Co-authored-by: Andis Redmans <[email protected]>
1 parent 13ac70a commit 43dc48c

File tree

9 files changed

+619
-20
lines changed

9 files changed

+619
-20
lines changed

.changeset/clever-rocks-fly.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sap-ux/fe-fpm-writer': patch
3+
---
4+
5+
Add custom columns for building blocks

packages/fe-fpm-writer/src/building-block/index.ts

Lines changed: 152 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import {
88
type BuildingBlock,
99
type BuildingBlockConfig,
1010
type BuildingBlockMetaPath,
11+
type CustomColumn,
1112
type RichTextEditor,
12-
bindingContextAbsolute
13+
bindingContextAbsolute,
14+
type TemplateConfig
1315
} from './types';
1416
import { DOMParser, XMLSerializer } from '@xmldom/xmldom';
1517
import * as xpath from 'xpath';
@@ -22,8 +24,10 @@ import type { Manifest } from '../common/types';
2224
import { getMinimumUI5Version } from '@sap-ux/project-access';
2325
import { detectTabSpacing, extendJSON } from '../common/file';
2426
import { getManifest, getManifestPath } from '../common/utils';
27+
import { getDefaultFragmentContent, setCommonDefaults } from '../common/defaults';
2528
import { getOrAddNamespace } from './prompts/utils/xml';
2629
import { i18nNamespaces, translate } from '../i18n';
30+
import { applyEventHandlerConfiguration } from '../common/event-handler';
2731

2832
const PLACEHOLDERS = {
2933
'id': 'REPLACE_WITH_BUILDING_BLOCK_ID',
@@ -60,10 +64,17 @@ export async function generateBuildingBlock<T extends BuildingBlock>(
6064
throw new Error(`Invalid view path ${viewOrFragmentPath}.`);
6165
}
6266

67+
const { path: manifestPath, content: manifest } = await getManifest(basePath, fs);
68+
6369
// Read the view xml and template files and update contents of the view xml file
6470
const xmlDocument = getUI5XmlDocument(basePath, viewOrFragmentPath, fs);
65-
const { content: manifest } = await getManifest(basePath, fs);
66-
const templateDocument = getTemplateDocument(buildingBlockData, xmlDocument, fs, manifest);
71+
const { updatedAggregationPath, processedBuildingBlockData, hasAggregation, aggregationNamespace } =
72+
processBuildingBlock(buildingBlockData, xmlDocument, manifestPath, manifest, aggregationPath, fs);
73+
const templateConfig: TemplateConfig = {
74+
hasAggregation,
75+
aggregationNamespace
76+
};
77+
const templateDocument = getTemplateDocument(processedBuildingBlockData, xmlDocument, fs, manifest, templateConfig);
6778

6879
if (buildingBlockData.buildingBlockType === BuildingBlockType.RichTextEditor) {
6980
const minUI5Version = manifest ? coerce(getMinimumUI5Version(manifest)) : undefined;
@@ -77,7 +88,7 @@ export async function generateBuildingBlock<T extends BuildingBlock>(
7788
fs = updateViewFile(
7889
basePath,
7990
viewOrFragmentPath,
80-
aggregationPath,
91+
updatedAggregationPath,
8192
xmlDocument,
8293
templateDocument,
8394
fs,
@@ -100,6 +111,124 @@ export async function generateBuildingBlock<T extends BuildingBlock>(
100111
return fs;
101112
}
102113

114+
/**
115+
* Updates aggregation path for table columns based on XML document structure.
116+
*
117+
* @param {Document} xmlDocument - The XML document to analyze
118+
* @param {string} aggregationPath - The current aggregation path
119+
* @param {CustomColumn} buildingBlockData - The building block data with embedded fragment
120+
* @returns {object} Object containing the updated aggregation path
121+
*/
122+
function updateAggregationPathForTableColumns(
123+
xmlDocument: Document,
124+
aggregationPath: string,
125+
buildingBlockData: CustomColumn
126+
): { updatedAggregationPath: string; hasTableColumns: boolean } {
127+
if (!buildingBlockData.embededFragment) {
128+
return { updatedAggregationPath: aggregationPath, hasTableColumns: false };
129+
}
130+
131+
const xpathSelect = xpath.useNamespaces((xmlDocument.firstChild as any)._nsMap);
132+
const hasColumnsAggregation = xpathSelect("//*[local-name()='columns']", xmlDocument);
133+
if (hasColumnsAggregation && Array.isArray(hasColumnsAggregation) && hasColumnsAggregation.length > 0) {
134+
return {
135+
updatedAggregationPath: aggregationPath + `/${getOrAddNamespace(xmlDocument)}:columns`,
136+
hasTableColumns: true
137+
};
138+
} else {
139+
const useDefaultAggregation = xpathSelect("//*[local-name()='Column']", xmlDocument);
140+
if (useDefaultAggregation && Array.isArray(useDefaultAggregation) && useDefaultAggregation.length > 0) {
141+
return { updatedAggregationPath: aggregationPath, hasTableColumns: true };
142+
}
143+
}
144+
145+
return { updatedAggregationPath: aggregationPath, hasTableColumns: false };
146+
}
147+
148+
/**
149+
* Processes custom column building block configuration.
150+
*
151+
* @param {BuildingBlock} buildingBlockData - The building block data
152+
* @param {Document} xmlDocument - The XML document
153+
* @param {string} manifestPath - The manifest file path
154+
* @param {Manifest} manifest - The manifest object
155+
* @param {string} aggregationPath - The aggregation path
156+
* @param {Editor} fs - The memfs editor instance
157+
* @returns {object} Object containing updated aggregation path and processed building block data
158+
*/
159+
function processBuildingBlock<T extends BuildingBlock>(
160+
buildingBlockData: T,
161+
xmlDocument: Document,
162+
manifestPath: string,
163+
manifest: Manifest,
164+
aggregationPath: string,
165+
fs: Editor
166+
): {
167+
updatedAggregationPath: string;
168+
processedBuildingBlockData: T;
169+
hasAggregation: boolean;
170+
aggregationNamespace: string;
171+
} {
172+
let updatedAggregationPath = aggregationPath;
173+
let hasAggregation = false;
174+
let aggregationNamespace = 'macrosTable';
175+
176+
if (isCustomColumn(buildingBlockData) && buildingBlockData.embededFragment) {
177+
const embededFragment = setCommonDefaults(buildingBlockData.embededFragment, manifestPath, manifest);
178+
const viewPath = join(
179+
embededFragment.path,
180+
`${embededFragment.fragmentFile ?? embededFragment.name}.fragment.xml`
181+
);
182+
183+
// Apply event handler
184+
if (buildingBlockData.embededFragment.eventHandler) {
185+
buildingBlockData.embededFragment.eventHandler = applyEventHandlerConfiguration(
186+
fs,
187+
buildingBlockData.embededFragment,
188+
buildingBlockData.embededFragment.eventHandler,
189+
{
190+
controllerSuffix: false,
191+
typescript: buildingBlockData.embededFragment.typescript
192+
}
193+
);
194+
}
195+
buildingBlockData.embededFragment.content = getDefaultFragmentContent(
196+
'Sample Text',
197+
buildingBlockData.embededFragment.eventHandler
198+
);
199+
if (!fs.exists(viewPath)) {
200+
fs.copyTpl(getTemplatePath('common/Fragment.xml'), viewPath, buildingBlockData.embededFragment);
201+
}
202+
// check xmlDocument for macrosTable element
203+
const tableColumnsResult = updateAggregationPathForTableColumns(
204+
xmlDocument,
205+
aggregationPath,
206+
buildingBlockData
207+
);
208+
updatedAggregationPath = tableColumnsResult.updatedAggregationPath;
209+
hasAggregation = tableColumnsResult.hasTableColumns;
210+
211+
aggregationNamespace = getOrAddNamespace(xmlDocument, 'sap.fe.macros.table', 'macrosTable');
212+
}
213+
214+
return {
215+
updatedAggregationPath,
216+
processedBuildingBlockData: buildingBlockData,
217+
hasAggregation,
218+
aggregationNamespace
219+
};
220+
}
221+
222+
/**
223+
* Type guard to check if the building block data is a custom column.
224+
*
225+
* @param {BuildingBlock} data - The building block data to check
226+
* @returns {boolean} True if the data is a custom column
227+
*/
228+
function isCustomColumn(data: BuildingBlock): data is CustomColumn {
229+
return data.buildingBlockType === BuildingBlockType.CustomColumn;
230+
}
231+
103232
/**
104233
* Returns the UI5 xml file document (view/fragment).
105234
*
@@ -206,14 +335,16 @@ function getMetaPath(
206335
* @param {Manifest} manifest - the manifest content
207336
* @param {Editor} fs - the memfs editor instance
208337
* @param {boolean} usePlaceholders - apply placeholder values if value for attribute/property is not provided
338+
* @param {Record<string, unknown>} templateConfig - additional template configuration
209339
* @returns {string} the template xml file content
210340
*/
211341
function getTemplateContent<T extends BuildingBlock>(
212342
buildingBlockData: T,
213343
viewDocument: Document | undefined,
214344
manifest: Manifest | undefined,
215345
fs: Editor,
216-
usePlaceholders?: boolean
346+
usePlaceholders?: boolean,
347+
templateConfig?: TemplateConfig
217348
): string {
218349
const templateFolderName = buildingBlockData.buildingBlockType;
219350
const templateFilePath = getTemplatePath(`/building-block/${templateFolderName}/View.xml`);
@@ -245,7 +376,8 @@ function getTemplateContent<T extends BuildingBlock>(
245376
fs.read(templateFilePath),
246377
{
247378
macrosNamespace: viewDocument ? getOrAddNamespace(viewDocument, 'sap.fe.macros', 'macros') : 'macros',
248-
data: buildingBlockData
379+
data: buildingBlockData,
380+
config: templateConfig
249381
},
250382
{}
251383
);
@@ -255,12 +387,13 @@ function getTemplateContent<T extends BuildingBlock>(
255387
* Method returns the manifest content for the required dependency library.
256388
*
257389
* @param {Editor} fs - the memfs editor instance
390+
* @param {string} library - the dependency library
258391
* @returns {Promise<string>} Manifest content for the required dependency library.
259392
*/
260-
export async function getManifestContent(fs: Editor): Promise<string> {
393+
export async function getManifestContent(fs: Editor, library = 'sap.fe.macros'): Promise<string> {
261394
// "sap.fe.macros" is missing - enhance manifest.json for missing "sap.fe.macros"
262395
const templatePath = getTemplatePath('/building-block/common/manifest.json');
263-
return render(fs.read(templatePath), { libraries: { 'sap.fe.macros': {} } });
396+
return render(fs.read(templatePath), { libraries: { [library]: {} } });
264397
}
265398

266399
/**
@@ -270,15 +403,24 @@ export async function getManifestContent(fs: Editor): Promise<string> {
270403
* @param {Document} viewDocument - the view xml file document
271404
* @param {Editor} fs - the memfs editor instance
272405
* @param {Manifest} manifest - the manifest content
406+
* @param {Record<string, unknown>} templateConfig - additional template configuration
273407
* @returns {Document} the template xml file document
274408
*/
275409
function getTemplateDocument<T extends BuildingBlock>(
276410
buildingBlockData: T,
277411
viewDocument: Document | undefined,
278412
fs: Editor,
279-
manifest: Manifest | undefined
413+
manifest: Manifest | undefined,
414+
templateConfig: TemplateConfig
280415
): Document {
281-
const templateContent = getTemplateContent(buildingBlockData, viewDocument, manifest, fs);
416+
const templateContent = getTemplateContent(
417+
buildingBlockData,
418+
viewDocument,
419+
manifest,
420+
fs,
421+
undefined,
422+
templateConfig
423+
);
282424
const errorHandler = (level: string, message: string) => {
283425
throw new Error(`Unable to parse template file with building block data. Details: [${level}] - ${message}`);
284426
};

packages/fe-fpm-writer/src/building-block/prompts/utils/xml.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,6 @@ export async function getFilterBarIdsInFile(viewOrFragmentPath: string, fs: Edit
123123
}
124124

125125
/**
126-
* Finds the prefix associated with a given namespace URI in the root element's attributes.
127-
* Handles both default namespaces (xmlns="...") and prefixed namespaces (xmlns:prefix="...").
128-
*
129126
* @example
130127
* // Default namespace (no prefix)
131128
* // <core:FragmentDefinition xmlns="sap.fe.macros">

packages/fe-fpm-writer/src/building-block/types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { CustomElement, CustomFragment, EventHandler, FragmentContentData, Position } from '../common/types';
2+
13
/**
24
* Building block type.
35
*
@@ -9,6 +11,7 @@ export enum BuildingBlockType {
911
Field = 'field',
1012
Page = 'page',
1113
Table = 'table',
14+
CustomColumn = 'custom-column',
1215
RichTextEditor = 'rich-text-editor'
1316
}
1417

@@ -22,6 +25,11 @@ export type BindingContextType = 'absolute' | 'relative';
2225
export const bindingContextAbsolute: BindingContextType = 'absolute';
2326
export const bindingContextRelative: BindingContextType = 'relative';
2427

28+
export type TemplateConfig = {
29+
hasAggregation?: boolean;
30+
aggregationNamespace: string;
31+
};
32+
2533
/**
2634
* Represents a building block metaPath object.
2735
*/
@@ -409,6 +417,16 @@ export interface Page extends BuildingBlock {
409417
description?: string;
410418
}
411419

420+
export interface CustomColumn extends BuildingBlock {
421+
title: string;
422+
width?: string;
423+
columnKey?: string;
424+
position?: Position;
425+
embededFragment?: EmbededFragment;
426+
}
427+
428+
export type EmbededFragment = EventHandler & CustomFragment & CustomElement & FragmentContentData;
429+
412430
/**
413431
* Building block used to create a rich text editor based on the metadata provided by OData V4.
414432
* MetaPath construction example: metaPath="/EntitySet/targetProperty"

packages/fe-fpm-writer/src/column/index.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ export async function generateCustomColumn(
9191
// merge with defaults
9292
const completeColumn = enhanceConfig(fs, customColumn, manifestPath, manifest);
9393

94+
// add fragment
95+
const viewPath = join(completeColumn.path, `${completeColumn.fragmentFile ?? completeColumn.name}.fragment.xml`);
96+
if (completeColumn.control || !fs.exists(viewPath)) {
97+
fs.copyTpl(getTemplatePath('common/Fragment.xml'), viewPath, completeColumn);
98+
}
99+
94100
// enhance manifest with column definition
95101
const manifestRoot = getManifestRoot(customColumn.minUI5Version);
96102
const filledTemplate = render(fs.read(join(manifestRoot, `manifest.json`)), completeColumn, {});
@@ -100,11 +106,5 @@ export async function generateCustomColumn(
100106
tabInfo: customColumn.tabInfo
101107
});
102108

103-
// add fragment
104-
const viewPath = join(completeColumn.path, `${completeColumn.fragmentFile ?? completeColumn.name}.fragment.xml`);
105-
if (completeColumn.control || !fs.exists(viewPath)) {
106-
fs.copyTpl(getTemplatePath('common/Fragment.xml'), viewPath, completeColumn);
107-
}
108-
109109
return fs;
110110
}

packages/fe-fpm-writer/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export {
2929
Table,
3030
BuildingBlockConfig,
3131
Page,
32+
CustomColumn,
3233
RichTextEditor
3334
} from './building-block/types';
3435
export { generateBuildingBlock, getSerializedFileContent } from './building-block';
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<% if (!config?.hasAggregation) { %><<%- macrosNamespace %>:columns>
2+
<% } %><<%- config?.aggregationNamespace %>:Column header="<%- data.title %>" importance="High" <% if (data.width) { %>width="<%- data.width %>"<% } %> <% if (data.position.anchor) { %>anchor="<%- data.position.anchor %>"<% } %> <% if (data.position.placement) { %>placement="<%- data.position.placement %>"<% } %> key="<%- data.columnKey || 'myColumnKey' %>">
3+
<core:Fragment fragmentName="<%- data.embededFragment.ns %>.<%- data.embededFragment.name %>" type="XML" />
4+
</<%- config?.aggregationNamespace %>:Column>
5+
<% if (!config?.hasAggregation) { %></<%- macrosNamespace %>:columns><% } %>

0 commit comments

Comments
 (0)