Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-cap-cards-store.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sap-ux/preview-middleware": patch
---

Fix card generator endpoints using wrong paths for CAP projects.
14 changes: 10 additions & 4 deletions packages/preview-middleware/src/base/flp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type http from 'node:http';
import type { Request, Response, Router, NextFunction } from 'express';
import { Router as createRouter, static as serveStatic, json } from 'express';
import type connect from 'connect';
import path, { dirname, join, posix } from 'node:path';
import { dirname, join, posix } from 'node:path';
import type { Logger, ToolsLogger } from '@sap-ux/logger';
// eslint-disable-next-line sonarjs/no-implicit-dependencies
import type { MiddlewareUtils } from '@ui5/server';
Expand Down Expand Up @@ -1026,7 +1026,11 @@ export class FlpSandbox {
fileName?: string;
manifests: MultiCardsPayload[];
};
const webappPath = await getWebappPath(path.resolve(), this.fs);

// getSourcePath() returns the webapp path directly for all project types
const webappPath = this.utils.getProject().getSourcePath();
const projectRoot = dirname(webappPath);

const fullPath = join(webappPath, localPath);
const filePath = fileName.endsWith('.json') ? join(fullPath, fileName) : `${join(fullPath, fileName)}.json`;
const integrationCard = getIntegrationCard(manifests);
Expand All @@ -1046,7 +1050,7 @@ export class FlpSandbox {
}
} satisfies ManifestNamespace.EmbedsSettings;

const appAccess = await createApplicationAccess(path.resolve(), this.fs);
const appAccess = await createApplicationAccess(projectRoot, this.fs);
await appAccess.updateManifestJSON(this.manifest, this.fs);
this.fs.commit(() => this.sendResponse(res, 'text/plain', 201, `Files were updated/created`));
} catch (error) {
Expand Down Expand Up @@ -1079,7 +1083,9 @@ export class FlpSandbox {
*/
private async storeI18nKeysHandler(req: Request, res: Response): Promise<void> {
try {
const webappPath = await getWebappPath(path.resolve(), this.fs);
// getSourcePath() returns the webapp path directly for all project types
const webappPath = this.utils.getProject().getSourcePath();

const i18nConfig = this.manifest['sap.app'].i18n;
let i18nPath = 'i18n/i18n.properties';
let fallbackLocale: string | undefined;
Expand Down
180 changes: 166 additions & 14 deletions packages/preview-middleware/test/unit/base/flp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ import connect = require('connect');
import { AdaptationProjectType } from '@sap-ux/axios-extension';

jest.spyOn(projectAccess, 'findProjectRoot').mockImplementation(() => Promise.resolve(process.cwd()));
jest.spyOn(projectAccess, 'getProjectType').mockImplementation(() => Promise.resolve('EDMXBackend'));
const getProjectTypeMock = jest
.spyOn(projectAccess, 'getProjectType')
.mockImplementation(() => Promise.resolve('EDMXBackend'));

jest.mock('@sap-ux/adp-tooling', () => {
return {
Expand Down Expand Up @@ -918,26 +920,41 @@ describe('FlpSandbox', () => {
}
];
const response = await server.post(CARD_GENERATOR_DEFAULT.i18nStore).send(newI18nEntry);
const webappPath = await getWebappPath(path.resolve());
const filePath = join(webappPath, 'i18n', 'i18n.properties');
// getSourcePath() returns tmpdir() in the mock
const expectedFilePath = join(tmpdir(), 'i18n', 'i18n.properties');

expect(response.status).toBe(201);
expect(response.text).toBe('i18n file updated.');
expect(createPropertiesI18nEntriesMock).toHaveBeenCalledTimes(1);
expect(createPropertiesI18nEntriesMock).toHaveBeenCalledWith(filePath, newI18nEntry);
expect(createPropertiesI18nEntriesMock).toHaveBeenCalledWith(
expectedFilePath,
expect.arrayContaining([
expect.objectContaining({
key: 'CardGeneratorGroupPropertyLabel_Groups_0_Items_0',
value: 'new Entry'
})
])
);
});
test('should handle string i18n path', async () => {
const newI18nEntry = [{ key: 'HELLO', value: 'Hello World' }];
const manifest = JSON.parse(readFileSync(join(fixtures, 'simple-app/webapp/manifest.json'), 'utf-8'));
manifest['sap.app'].i18n = 'i18n/custom.properties';
await flp.init(manifest);
const response = await server.post(`${CARD_GENERATOR_DEFAULT.i18nStore}?locale=de`).send(newI18nEntry);
const webappPath = await getWebappPath(path.resolve());
const expectedPath = join(webappPath, 'i18n', 'custom_de.properties');
// getSourcePath() returns tmpdir() in the mock
const expectedPath = join(tmpdir(), 'i18n', 'custom_de.properties');

expect(response.status).toBe(201);
expect(response.text).toBe('i18n file updated.');
expect(createPropertiesI18nEntriesMock).toHaveBeenCalledWith(expectedPath, newI18nEntry);
expect(createPropertiesI18nEntriesMock).toHaveBeenCalledWith(
expectedPath,
expect.arrayContaining([
expect.objectContaining({
key: 'HELLO',
value: 'Hello World'
})
])
);
});

test('should handle bundleUrl with supported and fallback locales', async () => {
Expand All @@ -951,12 +968,20 @@ describe('FlpSandbox', () => {
await flp.init(manifest);

const response = await server.post(`${CARD_GENERATOR_DEFAULT.i18nStore}?locale=de`).send(newI18nEntry);
const webappPath = await getWebappPath(path.resolve());
const expectedPath = join(webappPath, 'i18n', 'i18n_de.properties');
// getSourcePath() returns tmpdir() in the mock
const expectedPath = join(tmpdir(), 'i18n', 'i18n_de.properties');

expect(response.status).toBe(201);
expect(response.text).toBe('i18n file updated.');
expect(createPropertiesI18nEntriesMock).toHaveBeenCalledWith(expectedPath, newI18nEntry);
expect(createPropertiesI18nEntriesMock).toHaveBeenCalledWith(
expectedPath,
expect.arrayContaining([
expect.objectContaining({
key: 'GREETING',
value: 'Hallo Welt'
})
])
);
});

test('should reject unsupported locale', async () => {
Expand All @@ -982,12 +1007,139 @@ describe('FlpSandbox', () => {
await flp.init(manifest);
const response = await server.post(`${CARD_GENERATOR_DEFAULT.i18nStore}`).send(newI18nEntry);

const webappPath = await getWebappPath(path.resolve());
const expectedPath = join(webappPath, 'i18n', 'i18n.properties');
// getSourcePath() returns tmpdir() in the mock
const expectedPath = join(tmpdir(), 'i18n', 'i18n.properties');

expect(response.status).toBe(201);
expect(response.text).toBe('i18n file updated.');
expect(createPropertiesI18nEntriesMock).toHaveBeenCalledWith(
expectedPath,
expect.arrayContaining([
expect.objectContaining({
key: 'HELLO',
value: 'Hello World'
})
])
);
});
});

describe('router with enableCardGenerator for CAP projects', () => {
let server!: SuperTest<Test>;
const webappPath = join(tmpdir(), 'webapp');
const mockCAPUtils = {
getProject() {
return {
getSourcePath: () => webappPath
};
}
} as unknown as MiddlewareUtils;
const mockConfig = {
editors: {
cardGenerator: {
path: '/test/flpCardGeneratorSandbox.html'
}
}
};

let mockFsPromisesWriteFile: jest.Mock;
let flp: FlpSandbox;

const setupMiddleware = async (mockConfig: Partial<MiddlewareConfig>) => {
// Set project type to CAPNodejs before initializing
getProjectTypeMock.mockImplementation(() => Promise.resolve('CAPNodejs'));
flp = new FlpSandbox(mockConfig, mockProject, mockCAPUtils, logger);
const manifest = JSON.parse(readFileSync(join(fixtures, 'simple-app/webapp/manifest.json'), 'utf-8'));
await flp.init(manifest);

const app = express();
app.use(flp.router);

server = supertest(app);
};

beforeEach(() => {
mockFsPromisesWriteFile = jest.fn();
promises.writeFile = mockFsPromisesWriteFile;
createPropertiesI18nEntriesMock.mockClear();
});

afterEach(() => {
fetchMock.mockRestore();
// Reset to default project type
getProjectTypeMock.mockImplementation(() => Promise.resolve('EDMXBackend'));
});

test('POST /cards/store with payload for CAP project (CAPNodejs)', async () => {
await setupMiddleware(mockConfig as MiddlewareConfig);
const projectAccessMock = jest.spyOn(projectAccess, 'createApplicationAccess').mockImplementation(() => {
return Promise.resolve({
updateManifestJSON: () => {
return Promise.resolve({});
}
}) as unknown as Promise<ApplicationAccess>;
});
const payload = {
floorplan: 'ObjectPage',
localPath: 'cards/op/op1',
fileName: 'manifest.json',
manifests: [
{
type: 'integration',
manifest: {
'_version': '1.15.0',
'sap.card': {
'type': 'Object',
'header': {
'type': 'Numeric',
'title': 'Card title'
}
},
'sap.insights': {
'versions': {
'ui5': '1.120.1-202403281300'
},
'templateName': 'ObjectPage',
'parentAppId': 'sales.order.wd20',
'cardType': 'DT'
}
},
default: true,
entitySet: 'op1'
}
]
};
const response = await server.post(CARD_GENERATOR_DEFAULT.cardsStore).send(payload);
expect(projectAccessMock).toHaveBeenCalled();
// For CAP projects, createApplicationAccess should be called with the parent of webappPath
expect(projectAccessMock).toHaveBeenCalledWith(path.dirname(webappPath), expect.anything());
expect(response.status).toBe(201);
expect(response.text).toBe('Files were updated/created');
});

test('POST /editor/i18n with payload for CAP project (CAPNodejs)', async () => {
await setupMiddleware(mockConfig as MiddlewareConfig);
const newI18nEntry = [
{
'key': 'CardGeneratorGroupPropertyLabel_Groups_0_Items_0',
'value': 'new Entry'
}
];
const response = await server.post(CARD_GENERATOR_DEFAULT.i18nStore).send(newI18nEntry);
// For CAP projects, the webappPath should be used directly from getSourcePath()
const expectedFilePath = join(webappPath, 'i18n', 'i18n.properties');

expect(response.status).toBe(201);
expect(response.text).toBe('i18n file updated.');
expect(createPropertiesI18nEntriesMock).toHaveBeenCalledWith(expectedPath, newI18nEntry);
expect(createPropertiesI18nEntriesMock).toHaveBeenCalledWith(
expectedFilePath,
expect.arrayContaining([
expect.objectContaining({
key: 'CardGeneratorGroupPropertyLabel_Groups_0_Items_0',
value: 'new Entry'
})
])
);
});
});

Expand Down