Skip to content

Commit 0c34573

Browse files
utils: Fix 'Loading...' quick pick remaining visible during create callback (#2227)
* Fix 'Loading...' quick pick remaining visible during create callback When a user selects '+ Create new...' in the tree item picker, the outer wizard's loading quick pick re-appears after each inner wizard prompt resolves (via onDidFinishPrompt) and stays visible during the execute phase when no other quick pick overlays it. Add a suppressLoadingPrompt context flag that is set during the create callback execution in CompatibilityRecursiveQuickPickStep. AzureWizard checks this flag before re-showing the loading quick pick and before cancelling the wizard on hide. Fixes microsoft/vscode-azureappservice#2833 * Address PR review feedback for suppressLoadingPrompt - Save/restore previous suppressLoadingPrompt value instead of forcing false - Fix misleading test comment to describe actual failure path - Update doc comment to accurately reflect suppression scope * Extract repeated IInternalActionContext cast into local variable
1 parent 6504362 commit 0c34573

File tree

6 files changed

+133
-17
lines changed

6 files changed

+133
-17
lines changed

utils/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

utils/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@microsoft/vscode-azext-utils",
33
"author": "Microsoft Corporation",
4-
"version": "4.0.4",
4+
"version": "4.0.5",
55
"description": "Common UI tools for developing Azure extensions for VS Code",
66
"tags": [
77
"azure",

utils/src/pickTreeItem/contextValue/compatibility/CompatibilityRecursiveQuickPickStep.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import * as types from "../../../../index";
99
import { NoResourceFoundError, UserCancelledError } from "../../../errors";
1010
import { AzExtTreeItem } from "../../../tree/AzExtTreeItem";
1111
import { isAzExtParentTreeItem, isAzExtTreeItem } from "../../../tree/isAzExtTreeItem";
12+
import { IInternalActionContext } from "../../../userInput/IInternalActionContext";
1213
import { getLastNode } from "../../getLastNode";
1314
import { CompatibilityContextValueQuickPickStep } from './CompatibilityContextValueQuickPickStep';
1415

@@ -65,19 +66,31 @@ export class CompatibilityRecursiveQuickPickStep<TContext extends types.QuickPic
6566

6667
// context passed to callback must have the same `ui` as the wizardContext
6768
// to prevent the wizard from being cancelled unexpectedly
68-
const createdTreeItem = await callback?.(wizardContext) as AzExtTreeItem;
69-
70-
// convert created AzExtTreeItem to a BranchDataItem so the wizard can get its children in the next step
71-
const picks = await this.getPicks(wizardContext) as types.IAzureQuickPickItem<unknown>[];
72-
const createdPick = picks.find((pick) => {
73-
return (pick.data as Wrapper).unwrap<AzExtTreeItem>().fullId === createdTreeItem.fullId;
74-
});
75-
76-
if (createdPick) {
77-
return createdPick.data;
69+
//
70+
// Suppress the outer wizard's loading quick pick during the create callback.
71+
// Without this, the "Loading..." placeholder re-appears after each inner wizard
72+
// prompt resolves (via onDidFinishPrompt) and stays visible during the execute
73+
// phase when no other quick pick overlays it.
74+
const internalContext = wizardContext as unknown as IInternalActionContext;
75+
const previousSuppressLoadingPrompt = internalContext.suppressLoadingPrompt;
76+
internalContext.suppressLoadingPrompt = true;
77+
try {
78+
const createdTreeItem = await callback?.(wizardContext) as AzExtTreeItem;
79+
80+
// convert created AzExtTreeItem to a BranchDataItem so the wizard can get its children in the next step
81+
const picks = await this.getPicks(wizardContext) as types.IAzureQuickPickItem<unknown>[];
82+
const createdPick = picks.find((pick) => {
83+
return (pick.data as Wrapper).unwrap<AzExtTreeItem>().fullId === createdTreeItem.fullId;
84+
});
85+
86+
if (createdPick) {
87+
return createdPick.data;
88+
}
89+
90+
throw new UserCancelledError();
91+
} finally {
92+
internalContext.suppressLoadingPrompt = previousSuppressLoadingPrompt;
7893
}
79-
80-
throw new UserCancelledError();
8194
}
8295

8396
return selected.data;

utils/src/userInput/IInternalActionContext.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ import * as types from '../../index';
88

99
export interface IInternalActionContext extends types.IActionContext {
1010
ui: types.IAzureUserInput & { wizard?: IInternalAzureWizard, isPrompting?: boolean, isTesting?: boolean };
11+
12+
/**
13+
* When true, the outer wizard's "Loading..." quick pick will not be
14+
* re-shown after inner prompts resolve and cancel-on-hide will be
15+
* suppressed. Used internally to prevent the loading placeholder from
16+
* remaining visible during a nested create-child wizard execution.
17+
*/
18+
suppressLoadingPrompt?: boolean;
1119
}
1220

1321
export interface IInternalAzureWizard {

utils/src/wizard/AzureWizard.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export class AzureWizard<T extends (IInternalActionContext & Partial<types.Execu
119119
if (loadingQuickPick) {
120120
disposables.push(loadingQuickPick?.onDidHide(() => {
121121
// Avoid issuing cancels during tests - see: https://github.com/microsoft/vscode-azuretools/pull/1807
122-
if (!this._context.ui.isPrompting && !this._context.ui.isTesting) {
122+
if (!this._context.ui.isPrompting && !this._context.ui.isTesting && !this._context.suppressLoadingPrompt) {
123123
this._cancellationTokenSource.cancel();
124124
}
125125
}));
@@ -128,7 +128,9 @@ export class AzureWizard<T extends (IInternalActionContext & Partial<types.Execu
128128
disposables.push(this._context.ui.onDidFinishPrompt((result) => {
129129
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
130130
step!.prompted = true;
131-
loadingQuickPick?.show();
131+
if (!this._context.suppressLoadingPrompt) {
132+
loadingQuickPick?.show();
133+
}
132134
if (typeof result.value === 'string' && !result.matchesDefault && this.currentStepId && !step?.supportsDuplicateSteps) {
133135
this._cachedInputBoxValues[this.currentStepId] = result.value;
134136
}

utils/test/AzureWizard.test.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,4 +663,97 @@ suite("AzureWizard tests", () => {
663663
{ quickPick1: 'Pick 1', quickPick2: 'Pick 2', inputBox1: 'testValue2', subQuickPick1: 'Pick 3', execute1: 'executeValue1' }
664664
);
665665
});
666+
667+
test("showLoadingPrompt does not break normal wizard flow", async () => {
668+
const testUserInput: TestUserInput = new TestUserInput(vscode);
669+
const context: ITestWizardContext = { telemetry: { properties: {}, measurements: {} }, errorHandling: { issueProperties: {} }, ui: testUserInput, valuesToMask: [] };
670+
671+
const wizard = new AzureWizard<ITestWizardContext>(context, {
672+
promptSteps: [new QuickPickStep1(), new InputBoxStep1()],
673+
executeSteps: [new ExecuteStep1()],
674+
showLoadingPrompt: true,
675+
});
676+
677+
await testUserInput.runWithInputs(['Pick 1', 'testValue'], async () => {
678+
await wizard.prompt();
679+
});
680+
await wizard.execute();
681+
682+
assert.strictEqual(context.quickPick1, 'Pick 1');
683+
assert.strictEqual(context.inputBox1, 'testValue');
684+
assert.strictEqual(context.execute1, 'executeValue1');
685+
});
686+
687+
test("suppressLoadingPrompt prevents loading prompt re-show without breaking wizard", async () => {
688+
const testUserInput: TestUserInput = new TestUserInput(vscode);
689+
const context: ITestWizardContext = { telemetry: { properties: {}, measurements: {} }, errorHandling: { issueProperties: {} }, ui: testUserInput, valuesToMask: [] };
690+
691+
// Simulate what CompatibilityRecursiveQuickPickStep does:
692+
// set suppressLoadingPrompt before a prompt step that triggers an inner wizard
693+
const stepThatSuppresses = new class extends AzureWizardPromptStep<ITestWizardContext> {
694+
async prompt(wizardContext: ITestWizardContext): Promise<void> {
695+
// Set the flag like CompatibilityRecursiveQuickPickStep does before the create callback
696+
wizardContext.suppressLoadingPrompt = true;
697+
try {
698+
// Simulate inner wizard prompts (these would fire onDidFinishPrompt)
699+
wizardContext['innerResult'] = (await wizardContext.ui.showQuickPick(
700+
[{ label: 'Inner Pick 1' }, { label: 'Inner Pick 2' }],
701+
{ placeHolder: 'Inner wizard pick' }
702+
)).label;
703+
wizardContext['innerInput'] = await wizardContext.ui.showInputBox({ prompt: 'Inner wizard input' });
704+
} finally {
705+
wizardContext.suppressLoadingPrompt = false;
706+
}
707+
}
708+
shouldPrompt(): boolean { return true; }
709+
};
710+
711+
const wizard = new AzureWizard<ITestWizardContext>(context, {
712+
promptSteps: [stepThatSuppresses, new QuickPickStep1()],
713+
executeSteps: [new ExecuteStep1()],
714+
showLoadingPrompt: true,
715+
});
716+
717+
await testUserInput.runWithInputs(['Inner Pick 1', 'innerTestValue', 'Pick 2'], async () => {
718+
await wizard.prompt();
719+
});
720+
await wizard.execute();
721+
722+
assert.strictEqual(context.innerResult, 'Inner Pick 1');
723+
assert.strictEqual(context.innerInput, 'innerTestValue');
724+
assert.strictEqual(context.quickPick1, 'Pick 2');
725+
assert.strictEqual(context.execute1, 'executeValue1');
726+
// Flag should be reset after the step completes
727+
assert.strictEqual(context.suppressLoadingPrompt, false);
728+
});
729+
730+
test("suppressLoadingPrompt is reset even if inner prompts throw", async () => {
731+
const testUserInput: TestUserInput = new TestUserInput(vscode);
732+
const context: ITestWizardContext = { telemetry: { properties: {}, measurements: {} }, errorHandling: { issueProperties: {} }, ui: testUserInput, valuesToMask: [] };
733+
734+
const stepThatSuppressesAndFails = new class extends AzureWizardPromptStep<ITestWizardContext> {
735+
async prompt(wizardContext: ITestWizardContext): Promise<void> {
736+
wizardContext.suppressLoadingPrompt = true;
737+
try {
738+
// Ensure suppressLoadingPrompt is reset even when the step throws
739+
throw new Error('simulated inner wizard failure');
740+
} finally {
741+
wizardContext.suppressLoadingPrompt = false;
742+
}
743+
}
744+
shouldPrompt(): boolean { return true; }
745+
};
746+
747+
const wizard = new AzureWizard<ITestWizardContext>(context, {
748+
promptSteps: [stepThatSuppressesAndFails],
749+
showLoadingPrompt: true,
750+
});
751+
752+
await assert.rejects(async () => {
753+
await wizard.prompt();
754+
}, /simulated inner wizard failure/);
755+
756+
// Flag must be reset even after failure
757+
assert.strictEqual(context.suppressLoadingPrompt, false);
758+
});
666759
});

0 commit comments

Comments
 (0)