Skip to content

Commit

Permalink
feat: Add quick action for create new annotation (#2618)
Browse files Browse the repository at this point in the history
* feat: add quick action for create new annotation
---------

Co-authored-by: Nafees Ahmed <[email protected]>
Co-authored-by: Nafees Ahmed <[email protected]>
Co-authored-by: GLOBAL\C5293748 <[email protected]>
Co-authored-by: Nikita B. <[email protected]>
  • Loading branch information
5 people authored Jan 8, 2025
1 parent 47db364 commit 19d51f3
Show file tree
Hide file tree
Showing 40 changed files with 1,408 additions and 68 deletions.
9 changes: 9 additions & 0 deletions .changeset/long-dragons-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@sap-ux-private/control-property-editor-common': patch
'@sap-ux-private/preview-middleware-client': patch
'@sap-ux/preview-middleware': patch
'@sap-ux/adp-tooling': patch
'@sap-ux/types': patch
---

feat: Quick Action For Add New Annotation File
5 changes: 5 additions & 0 deletions .changeset/wet-oranges-do.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sap-ux/control-property-editor': patch
---

feat: Quick Action For Add New Annotation File
1 change: 1 addition & 0 deletions packages/adp-tooling/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@sap-ux/project-input-validator": "workspace:*",
"@sap-ux/system-access": "workspace:*",
"@sap-ux/ui5-config": "workspace:*",
"@sap-ux/odata-service-writer": "workspace:*",
"@sap-ux/i18n": "workspace:*",
"adm-zip": "0.5.10",
"ejs": "3.1.10",
Expand Down
2 changes: 1 addition & 1 deletion packages/adp-tooling/src/base/abap/manifest-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { isAxiosError, type AbapServiceProvider, type Ui5AppInfoContent } from '
import { getWebappFiles } from '../helper';
import type { DescriptorVariant } from '../../types';

type DataSources = Record<string, ManifestNamespace.DataSource>;
export type DataSources = Record<string, ManifestNamespace.DataSource>;

/**
* Retrieves the inbound navigation configurations from the project's manifest.
Expand Down
12 changes: 7 additions & 5 deletions packages/adp-tooling/src/base/change-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,18 @@ export function writeAnnotationChange(
projectPath: string,
timestamp: number,
annotation: AnnotationsData['annotation'],
change: ManifestChangeProperties,
change: ManifestChangeProperties | undefined,
fs: Editor
): void {
try {
const changeFileName = `id_${timestamp}_addAnnotationsToOData.change`;
const changesFolderPath = path.join(projectPath, DirName.Webapp, DirName.Changes);
const changeFilePath = path.join(changesFolderPath, changeFileName);
const annotationsFolderPath = path.join(changesFolderPath, DirName.Annotations);

writeChangeToFile(changeFilePath, change, fs);
if (change) {
const changeFileName = `id_${timestamp}_addAnnotationsToOData.change`;
const changeFilePath = path.join(changesFolderPath, changeFileName);
change.fileName = `${change.fileName}_addAnnotationsToOData`;
writeChangeToFile(changeFilePath, change, fs);
}

if (!annotation.filePath) {
const annotationsTemplate = path.join(
Expand Down
26 changes: 24 additions & 2 deletions packages/adp-tooling/src/preview/adp-preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@ import type { LayeredRepositoryService, MergedAppDescriptor } from '@sap-ux/axio
import RoutesHandler from './routes-handler';
import type { AdpPreviewConfig, CommonChangeProperties, DescriptorVariant, OperationType } from '../types';
import type { Editor } from 'mem-fs-editor';
import { addXmlFragment, isAddXMLChange, moduleNameContentMap, tryFixChange } from './change-handler';
import {
addAnnotationFile,
addXmlFragment,
isAddAnnotationChange,
isAddXMLChange,
moduleNameContentMap,
tryFixChange
} from './change-handler';
declare global {
// false positive, const can't be used here https://github.com/eslint/eslint/issues/15896
// eslint-disable-next-line no-var
Expand All @@ -21,7 +28,8 @@ declare global {
export const enum ApiRoutes {
FRAGMENT = '/adp/api/fragment',
CONTROLLER = '/adp/api/controller',
CODE_EXT = '/adp/api/code_ext/:controllerName'
CODE_EXT = '/adp/api/code_ext/:controllerName',
ANNOTATION = '/adp/api/annotation'
}

/**
Expand Down Expand Up @@ -198,6 +206,11 @@ export class AdpPreview {
router.post(ApiRoutes.CONTROLLER, this.routesHandler.handleWriteControllerExt as RequestHandler);

router.get(ApiRoutes.CODE_EXT, this.routesHandler.handleGetControllerExtensionData as RequestHandler);
router.post(ApiRoutes.ANNOTATION, this.routesHandler.handleCreateAnnotationFile as RequestHandler);
router.get(
ApiRoutes.ANNOTATION,
this.routesHandler.handleGetAllAnnotationFilesMappedByDataSource as RequestHandler
);
}

/**
Expand Down Expand Up @@ -225,6 +238,15 @@ export class AdpPreview {
if (isAddXMLChange(change)) {
addXmlFragment(this.util.getProject().getSourcePath(), change, fs, logger);
}
if (isAddAnnotationChange(change)) {
await addAnnotationFile(
this.util.getProject().getSourcePath(),
this.util.getProject().getRootPath(),
change,
fs,
logger
);
}
break;
default:
// no need to handle delete changes
Expand Down
85 changes: 81 additions & 4 deletions packages/adp-tooling/src/preview/change-handler.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import type { Editor } from 'mem-fs-editor';
import type { AddXMLChange, CommonChangeProperties, CodeExtChange } from '../types';
import { join } from 'path';
import { DirName } from '@sap-ux/project-access';
import type { Logger } from '@sap-ux/logger';
import type { AddXMLChange, CommonChangeProperties, CodeExtChange, AnnotationFileChange } from '../types';
import { ChangeType } from '../types';
import { basename, join } from 'path';
import { DirName, FileName } from '@sap-ux/project-access';
import type { Logger, ToolsLogger } from '@sap-ux/logger';
import { render } from 'ejs';
import { randomBytes } from 'crypto';
import { ManifestService } from '../base/abap/manifest-service';
import { getAdpConfig, getVariant } from '../base/helper';
import { createAbapServiceProvider } from '@sap-ux/system-access';
import { getAnnotationNamespaces } from '@sap-ux/odata-service-writer';
import { generateChange } from '../writer/editors';

const OBJECT_PAGE_CUSTOM_SECTION = 'OBJECT_PAGE_CUSTOM_SECTION';
const CUSTOM_ACTION = 'CUSTOM_ACTION';
Expand Down Expand Up @@ -167,6 +173,17 @@ export function isAddXMLChange(change: CommonChangeProperties): change is AddXML
return change.changeType === 'addXML' || change.changeType === 'addXMLAtExtensionPoint';
}

/**
* Determines whether a given change is of type `AnnotationFileChange`.
*
* @param {CommonChangeProperties} change - The change object to check.
* @returns {boolean} `true` if the `changeType` is either 'appdescr_app_addAnnotationsToOData',
* indicating the change is of type `AnnotationFileChange`.
*/
export function isAddAnnotationChange(change: CommonChangeProperties): change is AnnotationFileChange {
return change.changeType === 'appdescr_app_addAnnotationsToOData';
}

/**
* Asynchronously adds an XML fragment to the project if it doesn't already exist.
*
Expand Down Expand Up @@ -196,3 +213,63 @@ export function addXmlFragment(basePath: string, change: AddXMLChange, fs: Edito
logger.error(`Failed to create XML Fragment "${fragmentPath}": ${error}`);
}
}

/**
* Asynchronously adds an XML fragment to the project if it doesn't already exist.
*
* @param {string} basePath - The base path of the project.
* @param {string} projectRoot - The root path of the project.
* @param {AddXMLChange} change - The change data, including the fragment path.
* @param {Editor} fs - The mem-fs-editor instance.
* @param {Logger} logger - The logging instance.
*/
export async function addAnnotationFile(
basePath: string,
projectRoot: string,
change: AnnotationFileChange,
fs: Editor,
logger: Logger
): Promise<void> {
const { dataSourceId, annotations, dataSource } = change.content;
const annotationDataSourceKey = annotations[0];
const annotationUriSegments = dataSource[annotationDataSourceKey].uri.split('/');
annotationUriSegments.shift();
const fullPath = join(basePath, DirName.Changes, ...annotationUriSegments);
try {
const manifestService = await getManifestService(projectRoot, logger);
const metadata = await manifestService.getDataSourceMetadata(dataSourceId);
const datasoruces = await manifestService.getManifestDataSources();
const namespaces = getAnnotationNamespaces({ metadata });
await generateChange<ChangeType.ADD_ANNOTATIONS_TO_ODATA>(
projectRoot,
ChangeType.ADD_ANNOTATIONS_TO_ODATA,
{
annotation: {
dataSource: dataSourceId,
namespaces,
serviceUrl: datasoruces[dataSourceId].uri,
fileName: basename(dataSource[annotationDataSourceKey].uri)
},
variant: getVariant(projectRoot),
isCommand: false
},
fs
);
} catch (error) {
logger.error(`Failed to create Local Annotation File "${fullPath}": ${error}`);
}
}

/**
* Returns manifest service.
*
* @param {string} basePath - The base path of the project.
* @param {Logger} logger - The logging instance.
* @returns Promise<ManifestService>
*/
async function getManifestService(basePath: string, logger: Logger): Promise<ManifestService> {
const variant = getVariant(basePath);
const { target, ignoreCertErrors = false } = await getAdpConfig(basePath, join(basePath, FileName.Ui5Yaml));
const provider = await createAbapServiceProvider(target, { ignoreCertErrors }, true, logger);
return await ManifestService.initMergedManifest(provider, basePath, variant, logger as unknown as ToolsLogger);
}
161 changes: 159 additions & 2 deletions packages/adp-tooling/src/preview/routes-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,35 @@ import type { ReaderCollection, Resource } from '@ui5/fs';
import type { NextFunction, Request, Response } from 'express';

import { TemplateFileName, HttpStatusCodes } from '../types';
import { DirName } from '@sap-ux/project-access';
import type { CodeExtChange } from '../types';
import { DirName, FileName } from '@sap-ux/project-access';
import { ChangeType, type CodeExtChange } from '../types';
import { generateChange } from '../writer/editors';
import { ManifestService } from '../base/abap/manifest-service';
import type { DataSources } from '../base/abap/manifest-service';
import { getAdpConfig, getVariant } from '../base/helper';
import { getAnnotationNamespaces } from '@sap-ux/odata-service-writer';
import { createAbapServiceProvider } from '@sap-ux/system-access';

interface WriteControllerBody {
controllerName: string;
projectId: string;
}

interface AnnotationFileDetails {
fileName?: string;
annotationPath?: string;
annotationPathFromRoot?: string;
annotationExistsInWS: boolean;
}

interface AnnotationDataSourceMap {
[key: string]: {
serviceUrl: string;
isRunningInBAS: boolean;
annotationDetails: AnnotationFileDetails;
};
}

/**
* @description Handles API Routes
*/
Expand Down Expand Up @@ -244,4 +265,140 @@ export default class RoutesHandler {
next(e);
}
};
/**
* Handler for writing an annotation file to the workspace.
*
* @param req Request
* @param res Response
* @param next Next Function
*/
public handleCreateAnnotationFile = async (req: Request, res: Response, next: NextFunction) => {
try {
const { dataSource, serviceUrl } = req.body as { dataSource: string; serviceUrl: string };

if (!dataSource) {
res.status(HttpStatusCodes.BAD_REQUEST).send('No datasource found in manifest!');
this.logger.debug('Bad request. Could not find a datasource in manifest!');
return;
}
const project = this.util.getProject();
const projectRoot = project.getRootPath();
const manifestService = await this.getManifestService();
const metadata = await manifestService.getDataSourceMetadata(dataSource);
const namespaces = getAnnotationNamespaces({ metadata });
const fsEditor = await generateChange<ChangeType.ADD_ANNOTATIONS_TO_ODATA>(
projectRoot,
ChangeType.ADD_ANNOTATIONS_TO_ODATA,
{
annotation: {
dataSource,
namespaces,
serviceUrl: serviceUrl
},
variant: getVariant(projectRoot),
isCommand: false
}
);
fsEditor.commit((err) => this.logger.error(err));

const message = 'Annotation file created!';
res.status(HttpStatusCodes.CREATED).send(message);
} catch (e) {
const sanitizedMsg = sanitize(e.message);
this.logger.error(sanitizedMsg);
res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).send(sanitizedMsg);
next(e);
}
};

/**
* Handler for mapping annotation files with datasoruce.
*
* @param _req Request
* @param res Response
* @param next Next Function
*/
public handleGetAllAnnotationFilesMappedByDataSource = async (_req: Request, res: Response, next: NextFunction) => {
try {
const isRunningInBAS = isAppStudio();

const manifestService = await this.getManifestService();
const dataSources = manifestService.getManifestDataSources();
const apiResponse: AnnotationDataSourceMap = {};

for (const dataSourceId in dataSources) {
if (dataSources[dataSourceId].type === 'OData') {
apiResponse[dataSourceId] = {
annotationDetails: {
annotationExistsInWS: false
},
serviceUrl: dataSources[dataSourceId].uri,
isRunningInBAS: isRunningInBAS
};
}
this.fillAnnotationDataSourceMap(dataSources, dataSourceId, apiResponse);
}
this.sendFilesResponse(res, apiResponse);
} catch (e) {
this.handleErrorMessage(res, next, e);
}
};

/**
* Add local annotation details to api response.
*
* @param dataSources DataSources
* @param dataSourceId string
* @param apiResponse AnnotationDataSourceMap
*/
private fillAnnotationDataSourceMap(
dataSources: DataSources,
dataSourceId: string,
apiResponse: AnnotationDataSourceMap
): void {
const project = this.util.getProject();
const getPath = (projectPath: string, relativePath: string): string =>
path.join(projectPath, DirName.Changes, relativePath).split(path.sep).join(path.posix.sep);
const annotations = dataSources[dataSourceId].settings?.annotations
? [...dataSources[dataSourceId].settings.annotations].reverse()
: [];
for (const annotation of annotations) {
const annotationSetting = dataSources[annotation];
if (annotationSetting.type === 'ODataAnnotation') {
const ui5NamespaceUri = `ui5://${project.getNamespace()}`;
if (annotationSetting.uri.startsWith(ui5NamespaceUri)) {
const localAnnotationUri = annotationSetting.uri.replace(ui5NamespaceUri, '');
const annotationPath = getPath(project.getSourcePath(), localAnnotationUri);
const annotationPathFromRoot = getPath(project.getName(), localAnnotationUri);
const annotationExists = fs.existsSync(annotationPath);
apiResponse[dataSourceId].annotationDetails = {
fileName: path.parse(localAnnotationUri).base,
annotationPath: os.platform() === 'win32' ? `/${annotationPath}` : annotationPath,
annotationPathFromRoot,
annotationExistsInWS: annotationExists
};
}
if (apiResponse[dataSourceId].annotationDetails.annotationExistsInWS) {
break;
}
}
}
}

/**
* Returns manifest service.
*
* @returns Promise<ManifestService>
*/
private async getManifestService(): Promise<ManifestService> {
const project = this.util.getProject();
const basePath = project.getRootPath();
const variant = getVariant(basePath);
const { target, ignoreCertErrors = false } = await getAdpConfig(
basePath,
path.join(basePath, FileName.Ui5Yaml)
);
const provider = await createAbapServiceProvider(target, { ignoreCertErrors }, true, this.logger);
return await ManifestService.initMergedManifest(provider, basePath, variant, this.logger);
}
}
Loading

0 comments on commit 19d51f3

Please sign in to comment.