Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7fe6c1c
feat: add support for cf flow in flp generator
nikmace Mar 24, 2026
0464c2e
chore: add cset
nikmace Mar 24, 2026
cec3c7b
Linting auto fix commit
github-actions[bot] Mar 24, 2026
17bc1cd
refactor: change texts
nikmace Mar 25, 2026
131a880
test: add missing test
nikmace Mar 25, 2026
1cb32c3
Merge branch 'feat/4452/adjust-flp-gen-and-cli' of https://github.com…
nikmace Mar 25, 2026
596a71c
test: remove extra test
nikmace Mar 25, 2026
f0887cf
fix: change project root path for flp gen
nikmace Mar 25, 2026
3b1f267
fix: updating flp steps
nikmace Mar 25, 2026
855b4ca
refactor: change texts
nikmace Mar 26, 2026
a4b5bc8
Linting auto fix commit
github-actions[bot] Mar 26, 2026
af04586
test: add missing test
nikmace Mar 26, 2026
658236e
Merge branch 'feat/4452/adjust-flp-gen-and-cli' of https://github.com…
nikmace Mar 26, 2026
ef9676c
refactor: improve code
nikmace Mar 26, 2026
1890faf
refactor: improve code
nikmace Mar 26, 2026
6f20adc
refactor: remove dead code
nikmace Mar 26, 2026
d477e53
refactor: change text
nikmace Mar 26, 2026
8bf95c5
fix: review comments
nikmace Mar 27, 2026
9b03604
Merge branch 'main' into feat/4452/adjust-flp-gen-and-cli
heimwege Mar 30, 2026
75c1312
Merge branch 'main' into feat/4452/adjust-flp-gen-and-cli
nikmace Mar 30, 2026
6fa1466
Merge branch 'main' into feat/4452/adjust-flp-gen-and-cli
nikmace Mar 31, 2026
34836ba
Merge branch 'main' into feat/4452/adjust-flp-gen-and-cli
nikmace Mar 31, 2026
a7eb8ae
Merge branch 'main' into feat/4452/adjust-flp-gen-and-cli
nikmace Mar 31, 2026
b0698c1
Merge branch 'main' into feat/4452/adjust-flp-gen-and-cli
nikmace Apr 1, 2026
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
8 changes: 8 additions & 0 deletions .changeset/whole-towns-worry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@sap-ux/adp-flp-config-sub-generator': patch
'@sap-ux/generator-adp': patch
'@sap-ux/adp-tooling': patch
'@sap-ux/create': patch
---

feat: Adjust FLP configuration wizard for CF scenario
62 changes: 57 additions & 5 deletions packages/adp-flp-config-sub-generator/src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import {
flpConfigurationExists,
SystemLookup,
getBaseAppInbounds,
getCfBaseAppInbounds,
loadCfConfig,
getAppParamsFromUI5Yaml,
type InternalInboundNavigation,
type AdpPreviewConfigWithTarget,
type DescriptorVariant,
Expand Down Expand Up @@ -82,6 +85,7 @@ export default class AdpFlpConfigGenerator extends Generator {
private variant: DescriptorVariant;
private tileSettingsAnswers?: TileSettingsAnswers;
private provider: AbapServiceProvider;
private isCfProject: boolean = false;

/**
* Creates an instance of the generator.
Expand All @@ -100,6 +104,7 @@ export default class AdpFlpConfigGenerator extends Generator {
this.vscode = opts.vscode;
this.inbounds = opts.inbounds;
this.layer = opts.layer;
this.isCfProject = !!opts.isCfProject;

initAppWizardCache(this.logger, this.appWizard);
this._setupFLPConfigPrompts();
Expand Down Expand Up @@ -141,7 +146,7 @@ export default class AdpFlpConfigGenerator extends Generator {
if (this.abort) {
return;
}
if (!this.launchAsSubGen) {
if (!this.launchAsSubGen && !this.isCfProject) {
await this._validateCloudProject();
if (this.abort) {
return;
Expand Down Expand Up @@ -450,9 +455,11 @@ export default class AdpFlpConfigGenerator extends Generator {
*/
private async _validateProjectType(): Promise<void> {
const isFioriAdaptation = (await getAppType(this.projectRootPath)) === 'Fiori Adaptation';
if (!isFioriAdaptation || (await isCFEnvironment(this.projectRootPath))) {
if (!isFioriAdaptation) {
this._abortExecution(t('error.projectNotSupported'));
return;
}
this.isCfProject = await isCFEnvironment(this.projectRootPath);
}

/**
Expand All @@ -474,14 +481,31 @@ export default class AdpFlpConfigGenerator extends Generator {
*/
private async _initializeStandAloneGenerator(): Promise<void> {
await this._validateProjectType();
if (this.abort) {
return;
}

this.variant = await getVariant(this.projectRootPath, this.fs);
this.appId = this.variant.reference;
this.layer = this.variant.layer;

if (this.isCfProject) {
await this._initializeCfGenerator();
} else {
await this._initializeAbapGenerator();
}
}

/**
* Initializes the ABAP-specific parts of the standalone generator.
*
* @returns {Promise<void>} A promise that resolves when initialization is complete.
*/
private async _initializeAbapGenerator(): Promise<void> {
this.ui5Yaml = await getAdpConfig<AdpPreviewConfigWithTarget>(
this.projectRootPath,
join(this.projectRootPath, FileName.Ui5Yaml)
);
this.variant = await getVariant(this.projectRootPath, this.fs);
this.appId = this.variant.reference;
this.layer = this.variant.layer;

await this._initAbapServiceProvider();

Expand All @@ -496,6 +520,34 @@ export default class AdpFlpConfigGenerator extends Generator {
}
}

/**
* Initializes the CF-specific parts of the standalone generator.
*
* @returns {Promise<void>} A promise that resolves when initialization is complete.
*/
private async _initializeCfGenerator(): Promise<void> {
const cfConfig = loadCfConfig(this.toolsLogger);
if (!cfConfig?.token) {
this._abortExecution(t('error.cfLoginRequired'));
return;
}

const appParams = getAppParamsFromUI5Yaml(this.projectRootPath);
if (!appParams.appHostId) {
this._abortExecution(t('error.cfAppHostIdMissing'));
return;
}

try {
this.inbounds =
this.inbounds ??
(await getCfBaseAppInbounds(this.appId, appParams.appHostId, cfConfig, this.toolsLogger));
} catch (e) {
this.toolsLogger.error(`CF inbounds fetching failed: ${e}`);
this._abortExecution(t('error.cfInboundsFetchFailed', { error: (e as Error).message }));
}
}

/**
* Initializes the AbapServiceProvider for the generator. If the generator is launched as a sub-generator, the provider is taken from the options.
* If the provider is cached in the app wizard, it is retrieved from the cache, otherwise, a new AbapServiceProvider is created using the ui5.yaml configuration.
Expand Down
5 changes: 4 additions & 1 deletion packages/adp-flp-config-sub-generator/src/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type Generator from 'yeoman-generator';
import type { TelemetryData } from '@sap-ux/fiori-generator-shared';
import type { AbapServiceProvider } from '@sap-ux/axios-extension';
import type { ManifestNamespace, UI5FlexLayer } from '@sap-ux/project-access';

export interface FlpConfigOptions extends Generator.GeneratorOptions {
/**
* VSCode instance
Expand Down Expand Up @@ -40,6 +39,10 @@ export interface FlpConfigOptions extends Generator.GeneratorOptions {
data?: {
projectRootPath: string;
};
/**
* Flag indicating whether this is a CF environment project
*/
isCfProject?: boolean;
}

export interface State {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
"authenticationFailed": "Authentication failed.",
"projectNotCloudReady": "FLP Configuration is supported for Cloud Ready adaptation projects only",
"baseAppInboundsFetching": "Error while fetching base application inbounds",
"cfConfigRequired": "Cloud Foundry configuration is required for Cloud Foundry adaptation projects.",
"cfLoginRequired": "Cloud Foundry login required. Please run `cf login` and try again.",
"cfAppHostIdMissing": "Could not determine `appHostId` from project `ui5.yaml` configuration.",
"cfInboundsFetchFailed": "Failed to fetch inbounds for the Cloud Foundry application: {{error}}",
"warningCachingNotSupported": "Warning: caching is not supported"
}
}
}
203 changes: 199 additions & 4 deletions packages/adp-flp-config-sub-generator/test/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ jest.mock('@sap-ux/adp-tooling', () => ({
getAdpConfig: jest.fn(),
generateInboundConfig: jest.fn(),
getBaseAppInbounds: jest.fn(),
getCfBaseAppInbounds: jest.fn(),
loadCfConfig: jest.fn().mockReturnValue({}),
getAppParamsFromUI5Yaml: jest.fn().mockReturnValue({ appHostId: '', appName: '', appVersion: '', spaceGuid: '' }),
SystemLookup: jest.fn().mockImplementation(() => ({
getSystemByName: jest.fn().mockResolvedValue({
name: 'testDestination'
Expand Down Expand Up @@ -623,7 +626,7 @@ describe('FLPConfigGenerator Integration Tests', () => {
await expect(runContext.run()).rejects.toThrow(t('error.updatingApp'));
});

it('Should result in an error message if the project is a CF project', async () => {
it('Should result in an error message if the project is a CF project without CF login', async () => {
jest.spyOn(adpTooling, 'isCFEnvironment').mockResolvedValueOnce(true);
jest.spyOn(adpTooling, 'getAdpConfig').mockResolvedValue({
target: {
Expand Down Expand Up @@ -652,10 +655,10 @@ describe('FLPConfigGenerator Integration Tests', () => {

await initI18n();
await runContext.run();
expect(vsCodeMessageSpy).toHaveBeenCalledWith(t('error.projectNotSupported'));
expect(vsCodeMessageSpy).toHaveBeenCalledWith(t('error.cfLoginRequired'));
});

it('Should result in an error message if the project is a CF project and use the logger in case of CLI', async () => {
it('Should result in an error message if the project is a CF project without CF login and use the logger in case of CLI', async () => {
jest.spyOn(adpTooling, 'isCFEnvironment').mockResolvedValueOnce(true);
jest.spyOn(adpTooling, 'getAdpConfig').mockResolvedValue({
target: {
Expand Down Expand Up @@ -685,7 +688,7 @@ describe('FLPConfigGenerator Integration Tests', () => {

await initI18n();
await runContext.run();
expect(toolsLoggerErrorSpy).toHaveBeenCalledWith(t('error.projectNotSupported'));
expect(toolsLoggerErrorSpy).toHaveBeenCalledWith(t('error.cfLoginRequired'));
});

it('Should result in an error message if the project is not a CloudReady project', async () => {
Expand Down Expand Up @@ -1202,4 +1205,196 @@ describe('FLPConfigGenerator Integration Tests', () => {
await runContext.run();
expect(vsCodeMessageSpy).toHaveBeenCalledWith('Network Error');
});

it('Should work in CF sub-gen mode with pre-fetched inbounds', async () => {
const mockPrompts = {
items: [
{
name: 'Tile settings',
description: 'Configure the tile settings for the application'
},
{
name: 'SAP Fiori Launchpad Configuration',
description: ''
}
],
splice: jest.fn()
};
const testPath = join(testOutputDir, 'test_project_cf_subgen');
fs.mkdirSync(testPath, { recursive: true });
fsextra.copySync(join(__dirname, 'fixtures/app.variant1'), join(testPath, 'app.variant1'));
const testProjectPath = join(testPath, 'app.variant1');

const sendTelemetrySpy = jest.spyOn(fioriGenShared, 'sendTelemetry');

const runContext = yeomanTest
.create(
adpFlpConfigGenerator,
{
resolved: generatorPath
},
{
cwd: testProjectPath
}
)
.withOptions({
vscode,
appWizard: mockAppWizard,
loggerMock,
launchAsSubGen: true,
inbounds: inbounds,
layer: adpTooling.FlexLayer.CUSTOMER_BASE,
prompts: mockPrompts,
isCfProject: true
})
.withPrompts(answers);

await expect(runContext.run()).resolves.not.toThrow();
expect(sendTelemetrySpy).toHaveBeenCalledWith(
EventName.ADP_FLP_CONFIG_ADDED,
expect.objectContaining({
OperatingSystem: 'testOS',
Platform: 'testPlatform'
}),
testProjectPath
);
expect(generateInboundConfigSpy).toHaveBeenCalled();
});

it('Should fetch CF inbounds when CF config is available', async () => {
jest.spyOn(adpTooling, 'isCFEnvironment').mockResolvedValueOnce(true);
const mockCfConfig = {
org: { GUID: 'test-org-guid', Name: 'test-org' },
space: { GUID: 'test-space-guid', Name: 'test-space' },
url: '/test.cf',
token: 'test-token'
};
const mockInbounds = { 'inbound-1': { semanticObject: 'SO', action: 'display' } };
jest.spyOn(adpTooling, 'loadCfConfig').mockReturnValueOnce(mockCfConfig);
jest.spyOn(adpTooling, 'getAppParamsFromUI5Yaml').mockReturnValueOnce({
appHostId: 'test-host',
appName: 'test-app',
appVersion: '1.0.0',
spaceGuid: 'test-space-guid'
});
const getCfBaseAppInboundsSpy = jest
.spyOn(adpTooling, 'getCfBaseAppInbounds')
.mockResolvedValueOnce(mockInbounds as unknown as projectAccess.ManifestNamespace.Inbound);

const testProjectPath = join(__dirname, 'fixtures/app.variant1');

const runContext = yeomanTest
.create(
adpFlpConfigGenerator,
{
resolved: generatorPath
},
{
cwd: testProjectPath
}
)
.withOptions({
vscode,
appWizard: mockAppWizard,
launchFlpConfigAsSubGenerator: false
})
.withPrompts(answers);

await runContext.run();
expect(getCfBaseAppInboundsSpy).toHaveBeenCalledWith(
'mockReference',
'test-host',
mockCfConfig,
expect.anything()
);
});

it('Should abort when CF inbound fetch fails', async () => {
jest.spyOn(adpTooling, 'isCFEnvironment').mockResolvedValueOnce(true);
const mockCfConfig = {
org: { GUID: 'test-org-guid', Name: 'test-org' },
space: { GUID: 'test-space-guid', Name: 'test-space' },
url: '/test.cf',
token: 'test-token'
};
jest.spyOn(adpTooling, 'loadCfConfig').mockReturnValueOnce(mockCfConfig);
jest.spyOn(adpTooling, 'getAppParamsFromUI5Yaml').mockReturnValueOnce({
appHostId: 'test-host',
appName: 'test-app',
appVersion: '1.0.0',
spaceGuid: 'test-space-guid'
});
jest.spyOn(adpTooling, 'getCfBaseAppInbounds').mockRejectedValueOnce(new Error('Connection failed'));

const testProjectPath = join(__dirname, 'fixtures/app.variant1');

const runContext = yeomanTest
.create(
adpFlpConfigGenerator,
{
resolved: generatorPath
},
{
cwd: testProjectPath
}
)
.withOptions({
vscode,
appWizard: mockAppWizard,
launchFlpConfigAsSubGenerator: false
})
.withPrompts(answers);

await initI18n();
await runContext.run();
expect(vsCodeMessageSpy).toHaveBeenCalledWith(t('error.cfInboundsFetchFailed', { error: 'Connection failed' }));
});

it('Should load CF config from local environment in standalone mode', async () => {
jest.spyOn(adpTooling, 'isCFEnvironment').mockResolvedValueOnce(true);
const mockCfConfig = {
org: { GUID: 'test-org-guid', Name: 'test-org' },
space: { GUID: 'test-space-guid', Name: 'test-space' },
url: '/test.cf',
token: 'test-token'
};
jest.spyOn(adpTooling, 'loadCfConfig').mockReturnValueOnce(mockCfConfig);
jest.spyOn(adpTooling, 'getAppParamsFromUI5Yaml').mockReturnValueOnce({
appHostId: 'auto-detected-host',
appName: 'test-app',
appVersion: '1.0.0',
spaceGuid: 'test-space-guid'
});
const mockInbounds = { 'inbound-1': { semanticObject: 'SO', action: 'display' } };
const getCfBaseAppInboundsSpy = jest
.spyOn(adpTooling, 'getCfBaseAppInbounds')
.mockResolvedValueOnce(mockInbounds as unknown as projectAccess.ManifestNamespace.Inbound);

const testProjectPath = join(__dirname, 'fixtures/app.variant1');

const runContext = yeomanTest
.create(
adpFlpConfigGenerator,
{
resolved: generatorPath
},
{
cwd: testProjectPath
}
)
.withOptions({
vscode,
appWizard: mockAppWizard,
launchFlpConfigAsSubGenerator: false
})
.withPrompts(answers);

await runContext.run();
expect(getCfBaseAppInboundsSpy).toHaveBeenCalledWith(
'mockReference',
'auto-detected-host',
mockCfConfig,
expect.anything()
);
});
});
Loading
Loading