Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
0e61cc3
feat(yeoman-ui): implement generator progress notifications
korotkovao Jul 28, 2026
47510da
fix(yeoman-ui): add .js extensions to test imports for ESM
korotkovao Jul 29, 2026
480aefd
fix(yeoman-ui): add .js extension to external package import
korotkovao Jul 29, 2026
814dd4f
chore(yeoman-ui): remove debug console.log statements
korotkovao Jul 29, 2026
65f14e5
test(yeoman-ui): add comprehensive tests for project name feature
korotkovao Jul 29, 2026
f08d0ce
fix(yeoman-ui): remove problematic loggerWrapperMock from test setup
korotkovao Jul 29, 2026
7fa25a5
fix(yeoman-ui): restore logger initialization in test setup
korotkovao Jul 29, 2026
a53cea8
fix(yeoman-ui): fix lodash import for ESM compatibility
korotkovao Jul 29, 2026
040b8c7
fix(yeoman-ui): fix fs mock conflicts and remove incorrect tests
korotkovao Jul 29, 2026
5b4b8e1
fix(yeoman-ui): replace all fsMock.expects with sandbox.stub
korotkovao Jul 29, 2026
a7a1a56
fix(yeoman-ui): fix ES module mocking and improve test coverage
korotkovao Jul 29, 2026
add6245
test(yeoman-ui): add tests for edge cases to improve coverage
korotkovao Jul 29, 2026
3bf95cd
style(yeoman-ui): fix prettier formatting in test file
korotkovao Jul 29, 2026
cf568f2
chore(yeoman-ui): add clarifying comment
korotkovao Jul 29, 2026
b60e499
perf(yeoman-ui): reduce finalizing delay from 1000ms to 100ms
korotkovao Jul 29, 2026
fb39b26
fix(yeoman-ui): remove setTimeout to fix CI test timeout
korotkovao Jul 29, 2026
595127e
refactor(yeoman-ui): simplify doGeneratorDone return
korotkovao Jul 29, 2026
1044f04
test(yeoman-ui): add test to improve coverage to 92.02%
korotkovao Jul 29, 2026
85e9502
fix(yeoman-ui): convert Thenable to Promise<void> in doGeneratorDone
korotkovao Jul 29, 2026
41ee0c7
fix(yeoman-ui): add explicit return type annotation to then callback
korotkovao Jul 29, 2026
3adb1cf
fix(yeoman-ui): use type assertion instead of Promise wrapper
korotkovao Jul 29, 2026
02e31f2
fix(yeoman-ui): change return type to Thenable<any> to match implemen…
korotkovao Jul 29, 2026
885319f
fix(yeoman-ui): update YouiEvents interface to return Thenable<any>
korotkovao Jul 29, 2026
46622df
fix(yeoman-ui): stub fs.writeFileSync in tests to prevent CI failures
korotkovao Jul 29, 2026
c5206df
fix(yeoman-ui): stub WorkspaceFile methods to prevent CI filesystem e…
korotkovao Jul 29, 2026
f1b594f
feat(yeoman-ui): add VS Code setting, localization, and generator opt-in
korotkovao Aug 5, 2026
1b710a6
feat(yeoman-ui): check generator option for progress notification opt-in
korotkovao Aug 5, 2026
e0fc041
fix(yeoman-ui): address code review issues
korotkovao Aug 5, 2026
e2ba3a4
feat(yeoman-ui): add VS Code setting for Application Info Page auto-open
korotkovao Aug 5, 2026
9967137
feat(yeoman-ui): add frontend WebSocket handler and phase/analytics t…
korotkovao Aug 5, 2026
df2d017
Merge remote-tracking branch 'origin/main' into feat/yeoman-ui/genera…
korotkovao Aug 5, 2026
d093e12
fix(yeoman-ui): fix TypeScript types and remove async from sync methods
korotkovao Aug 5, 2026
8b61e70
style(yeoman-ui): fix prettier formatting in yeomanui.ts
korotkovao Aug 5, 2026
e462baf
test(yeoman-ui): add coverage tests to reach 92% threshold
korotkovao Aug 5, 2026
589d745
feat(yeoman-ui): add minimum visible duration for each progress phase
korotkovao Aug 5, 2026
cd7db22
fix(yeoman-ui): add required IBannerProps fields in test
korotkovao Aug 5, 2026
094eab8
test(yeoman-ui): add frontend tests for generatorProgress method
korotkovao Aug 6, 2026
38c6aa3
feat(yeoman-ui): add questionnaire control example to foodq generator
korotkovao Aug 6, 2026
f09ad35
feat(yeoman-ui): add 1-second delay to make Finalising phase visible
korotkovao Aug 7, 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
18 changes: 16 additions & 2 deletions projects/yeoman-ui/packages/backend/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "yeoman-ui",
"displayName": "Application Wizard",
"version": "1.26.1",
"version": "1.27.0-local",
"private": true,
"description": "Provides rich user experience for Yeoman generators.",
"categories": [
Expand Down Expand Up @@ -140,6 +140,18 @@
"default": true,
"description": "Automatically update installed generators."
},
"ApplicationWizard.showGeneratorProgress": {
"type": "boolean",
"default": true,
"description": "Show progress notification during project generation with project name and phase updates.",
"scope": "resource"
},
"ApplicationWizard.autoOpenApplicationInfoPage": {
"type": "boolean",
"default": true,
"description": "Launch 'Application Info Page' as soon as the application has been generated.",
"scope": "resource"
},
"ApplicationWizard.searchQuery": {
"type": "array",
"default": [
Expand Down Expand Up @@ -177,7 +189,9 @@
]
}
},
"activationEvents": [],
"activationEvents": [
"*"
],
"dependencies": {
"@sap-devx/webview-rpc": "0.4.1",
"@sap-devx/yeoman-ui-types": "workspace:^",
Expand Down
4 changes: 4 additions & 0 deletions projects/yeoman-ui/packages/backend/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export default {
`The '${artifactName}' project has been generated.`,
show_progress_button: "Open Output View",
show_progress_message: "Generating...",
progress_preparing: "Preparing...",
progress_writing_files: "Creating project files...",
progress_installing: "Installing dependencies...",
progress_finalising: "Finalising...",
add_to_workspace: "Open the project in a multi-root workspace",
open_in_a_new_workspace: "Open the project in a stand-alone",
create_and_close: "Create the project and close it for future use",
Expand Down
164 changes: 141 additions & 23 deletions projects/yeoman-ui/packages/backend/src/vscode-youi-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { getFileSchemeWorkspaceFolders } from "./utils/workspaceFolders.js";

const { isEmpty, isNil, set } = lodash;

// App Wizard wrapper that delegates to VSCodeYouiEvents
class YoUiAppWizard extends AppWizard {
constructor(private readonly events: VSCodeYouiEvents) {
super();
Expand Down Expand Up @@ -60,7 +61,13 @@ export class VSCodeYouiEvents implements YouiEvents {
private readonly rpc: IRpc;
private webviewPanel: WebviewPanel;
private readonly messages: any;
private resolveFunc: any;
private resolveFunc: (() => void) | undefined;
private progressReporter: {
report(value: { message?: string; increment?: number }): void;
} | null = null;
private currentProjectName: string | undefined;
private phaseStartTime: number = 0;
private currentPhase: "writing" | "install" | "end" | null = null;
public output: GeneratorOutput;
private readonly logger: IChildLogger;
private readonly appWizard: AppWizard;
Expand Down Expand Up @@ -88,27 +95,99 @@ export class VSCodeYouiEvents implements YouiEvents {
void this.rpc.invoke("setBanner", [bannerProps]);
}

public doGeneratorDone(
public async doGeneratorDone(
success: boolean,
message: string,
selectedWorkspace: string,
type: string,
targetFolderPath?: string
): void {
): Promise<any> {
// Show "Finalising..." before closing
if (this.progressReporter) {
this.progressReporter.report({
message: this.messages.progress_finalising,
});
// Add a brief delay so "Finalising..." is visible to users
await new Promise((resolve) => setTimeout(resolve, 1000));
}

this.resolveInstallingProgress();
set(this.webviewPanel, Constants.GENERATOR_COMPLETED, success);
this.doClose();
void this.showDoneMessage(
return this.showDoneMessage(
success,
message,
selectedWorkspace,
type,
targetFolderPath
targetFolderPath,
true // Skip resolving progress since we already did it
);
}

public doGeneratorInstall(): void {
this.doClose();
this.showInstallMessage();
public doGeneratorProgress(
projectName: string | undefined,
phase: "writing" | "install" | "end",
showProgress: boolean = false
): void {
// Check VS Code setting (default: true)
const config = vscode.workspace.getConfiguration();
const settingEnabled = config.get<boolean>(
"ApplicationWizard.showGeneratorProgress",
true
);

// Only show if both the setting is enabled AND the generator opts in
if (!settingEnabled || !showProgress) {
return; // Don't show progress notification if disabled or not opted in
}

// Map phases to localized messages
const phaseMessages = {
writing: this.messages.progress_writing_files,
install: this.messages.progress_installing,
end: this.messages.progress_finalising,
};

// Minimum duration for each phase (milliseconds)
const MIN_DURATIONS = {
writing: 2000, // 2 seconds
install: 0, // No minimum - let npm install take as long as it needs
end: 1000, // 1 second
};

const message = phaseMessages[phase];

// If this is the first phase (writing) AND no progress notification exists yet
if (phase === "writing" && !this.progressReporter) {
// Close the webview panel (showing the question form) before showing progress
this.doClose();
this.currentPhase = phase;
this.phaseStartTime = Date.now();
this.showInstallMessage(projectName, message);
} else if (this.progressReporter) {
// Calculate time elapsed in current phase
const elapsed = Date.now() - this.phaseStartTime;
const minDuration = this.currentPhase
? MIN_DURATIONS[this.currentPhase]
: 0;
const remainingTime = Math.max(0, minDuration - elapsed);

if (remainingTime > 0) {
// Wait for minimum duration before showing next phase
setTimeout(() => {
if (this.progressReporter) {
this.progressReporter.report({ message });
this.currentPhase = phase;
this.phaseStartTime = Date.now();
}
}, remainingTime);
} else {
// Minimum duration already elapsed, update immediately
this.progressReporter.report({ message });
this.currentPhase = phase;
this.phaseStartTime = Date.now();
}
}
}

public getAppWizard(): AppWizard {
Expand Down Expand Up @@ -182,16 +261,36 @@ export class VSCodeYouiEvents implements YouiEvents {
}
}

private showInstallMessage(): void {
private showInstallMessage(
projectName?: string,
initialMessage: string = this.messages.progress_preparing
): void {
// Store project name for later use in success message
this.currentProjectName = projectName;

// Use "Generating {projectName}" as the title
const title = projectName
? `Generating ${projectName}`
: "Application Generator";

void vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: "Installing dependencies...",
title: title,
cancellable: false,
},
async () => {
await new Promise((resolve) => {
async (progress) => {
// Store the progress reporter so we can update it
this.progressReporter = progress;
progress.report({ message: initialMessage });

// Keep the notification open until generation completes
await new Promise<void>((resolve) => {
this.resolveFunc = resolve;
});

// Clean up the progress reporter
this.progressReporter = null;
}
);
}
Expand All @@ -207,9 +306,12 @@ export class VSCodeYouiEvents implements YouiEvents {
errorMmessage: string,
selectedWorkspace: string,
type: string,
targetFolderPath?: string
targetFolderPath?: string,
skipResolve: boolean = false
): Thenable<any> {
this.resolveInstallingProgress();
if (!skipResolve) {
this.resolveInstallingProgress();
}

if (success) {
if (!isNil(targetFolderPath)) {
Expand Down Expand Up @@ -319,17 +421,33 @@ export class VSCodeYouiEvents implements YouiEvents {
selectedWorkspace: string,
type: string
): string {
let successInfoMessage: string = this.messages.artifact_generated_files;
// Default message with project name if available
let successInfoMessage: string = this.currentProjectName
? `Project ${this.currentProjectName} has been generated.`
: this.messages.artifact_generated_files;

if (type === "project") {
if (selectedWorkspace === this.messages.open_in_a_new_workspace) {
successInfoMessage =
this.messages.artifact_generated_project_open_in_a_new_workspace;
} else if (selectedWorkspace === this.messages.add_to_workspace) {
successInfoMessage =
this.messages.artifact_generated_project_add_to_workspace;
// For project type, use project name and add workspace-specific detail
if (this.currentProjectName) {
if (selectedWorkspace === this.messages.open_in_a_new_workspace) {
successInfoMessage = `Project ${this.currentProjectName} has been generated. The project will be opened in a new workspace.`;
} else if (selectedWorkspace === this.messages.add_to_workspace) {
successInfoMessage = `Project ${this.currentProjectName} has been generated. The project has been added to workspace.`;
} else {
successInfoMessage = `Project ${this.currentProjectName} has been generated.`;
}
} else {
successInfoMessage =
this.messages.artifact_generated_project_saved_for_future;
// Fallback to original messages if no project name
if (selectedWorkspace === this.messages.open_in_a_new_workspace) {
successInfoMessage =
this.messages.artifact_generated_project_open_in_a_new_workspace;
} else if (selectedWorkspace === this.messages.add_to_workspace) {
successInfoMessage =
this.messages.artifact_generated_project_add_to_workspace;
} else {
successInfoMessage =
this.messages.artifact_generated_project_saved_for_future;
}
}
} else if (type === "module") {
successInfoMessage = this.messages.artifact_generated_module;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,27 @@ export class ServerYouiEvents implements YouiEvents {
selectedWorkspace: string,
type: string,
targetPath = ""
): void {
void this.rpc.invoke("generatorDone", [
): Promise<void> {
return this.rpc.invoke("generatorDone", [
suceeded,
message,
selectedWorkspace,
type,
targetPath,
]);
]) as Promise<void>;
}

public doGeneratorInstall(): void {
void this.rpc.invoke("generatorInstall");
public doGeneratorProgress(
projectName: string | undefined,
phase: "writing" | "install" | "end",
showProgress: boolean = false
): void {
// Only invoke if generator opts in (WebSocket doesn't have VS Code settings)
if (!showProgress) {
return;
}
// WebSocket implementation - invoke RPC method with progress info
void this.rpc.invoke("generatorProgress", [projectName, phase]);
}

public showProgress(): void {
Expand Down
47 changes: 44 additions & 3 deletions projects/yeoman-ui/packages/backend/src/yeomanui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ export class YeomanUI {
);
AnalyticsWrapper.updateGeneratorEnded(generatorName);
// when targetFolderPath is undefined and no files are generated, send type = '' to get the empty toast message
this.youiEvents.doGeneratorDone(
void this.youiEvents.doGeneratorDone(
true,
message,
selectedWorkspace,
Expand All @@ -567,7 +567,7 @@ export class YeomanUI {
const messagePrefix = `${generatorName} generator failed`;
const errorMsg = error?.message || error;
this.logError(error, messagePrefix);
this.youiEvents.doGeneratorDone(
void this.youiEvents.doGeneratorDone(
false,
`${messagePrefix} - ${errorMsg}`,
"",
Expand All @@ -579,8 +579,49 @@ export class YeomanUI {
}

private onGenInstall(gen: any) {
// Extract project name
const getProjectName = () => {
return (
_.get(gen, "state.project.name") ||
_.get(gen, "options.projectName") ||
_.get(gen, "answers.projectName") ||
_.get(gen, "answers.app.name") ||
_.get(gen, "props.projectName") ||
_.get(gen, "props.app.name")
);
};

// Check if generator opts in to progress notifications
const showProgress = _.get(gen, "options.showGeneratorProgress", false);

// Listen to writing phase
gen.on("method:writing", () => {
const projectName = getProjectName();
void this.youiEvents.doGeneratorProgress(
projectName,
"writing",
showProgress
);
});

// Listen to install phase
gen.on("method:install", () => {
this.youiEvents.doGeneratorInstall();
const projectName = getProjectName();
void this.youiEvents.doGeneratorProgress(
projectName,
"install",
showProgress
);
});

// Listen to end phase
gen.on("method:end", () => {
const projectName = getProjectName();
void this.youiEvents.doGeneratorProgress(
projectName,
"end",
showProgress
);
});
}

Expand Down
2 changes: 1 addition & 1 deletion projects/yeoman-ui/packages/backend/src/youi-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export class YouiAdapter {
try {
return await cb(result); // eslint-disable-line @typescript-eslint/await-thenable
} catch (err) {
this.youiEvents.doGeneratorDone(
void this.youiEvents.doGeneratorDone(
false,
get(err, "message", "Template Wizard detected an error"),
"",
Expand Down
Loading
Loading