Skip to content

Commit 46ca865

Browse files
committed
Add multiStepInput.ts from Microsoft samples
1 parent e89c634 commit 46ca865

File tree

1 file changed

+312
-0
lines changed

1 file changed

+312
-0
lines changed
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { QuickPickItem, window, Disposable, CancellationToken, QuickInputButton, QuickInput, ExtensionContext, QuickInputButtons, Uri } from 'vscode';
7+
8+
/**
9+
* A multi-step input using window.createQuickPick() and window.createInputBox().
10+
*
11+
* This first part uses the helper class `MultiStepInput` that wraps the API for the multi-step case.
12+
*/
13+
export async function multiStepInput(context: ExtensionContext) {
14+
15+
class MyButton implements QuickInputButton {
16+
constructor(public iconPath: { light: Uri; dark: Uri; }, public tooltip: string) { }
17+
}
18+
19+
const createResourceGroupButton = new MyButton({
20+
dark: Uri.file(context.asAbsolutePath('resources/dark/add.svg')),
21+
light: Uri.file(context.asAbsolutePath('resources/light/add.svg')),
22+
}, 'Create Resource Group');
23+
24+
const resourceGroups: QuickPickItem[] = ['vscode-data-function', 'vscode-appservice-microservices', 'vscode-appservice-monitor', 'vscode-appservice-preview', 'vscode-appservice-prod']
25+
.map(label => ({ label }));
26+
27+
28+
interface State {
29+
title: string;
30+
step: number;
31+
totalSteps: number;
32+
resourceGroup: QuickPickItem | string;
33+
name: string;
34+
runtime: QuickPickItem;
35+
}
36+
37+
async function collectInputs() {
38+
const state = {} as Partial<State>;
39+
await MultiStepInput.run(input => pickResourceGroup(input, state));
40+
return state as State;
41+
}
42+
43+
const title = 'Create Application Service';
44+
45+
async function pickResourceGroup(input: MultiStepInput, state: Partial<State>) {
46+
const pick = await input.showQuickPick({
47+
title,
48+
step: 1,
49+
totalSteps: 3,
50+
placeholder: 'Pick a resource group',
51+
items: resourceGroups,
52+
activeItem: typeof state.resourceGroup !== 'string' ? state.resourceGroup : undefined,
53+
buttons: [createResourceGroupButton],
54+
shouldResume: shouldResume
55+
});
56+
if (pick instanceof MyButton) {
57+
return (input: MultiStepInput) => inputResourceGroupName(input, state);
58+
}
59+
state.resourceGroup = pick;
60+
return (input: MultiStepInput) => inputName(input, state);
61+
}
62+
63+
async function inputResourceGroupName(input: MultiStepInput, state: Partial<State>) {
64+
state.resourceGroup = await input.showInputBox({
65+
title,
66+
step: 2,
67+
totalSteps: 4,
68+
value: typeof state.resourceGroup === 'string' ? state.resourceGroup : '',
69+
prompt: 'Choose a unique name for the resource group',
70+
validate: validateNameIsUnique,
71+
shouldResume: shouldResume
72+
});
73+
return (input: MultiStepInput) => inputName(input, state);
74+
}
75+
76+
async function inputName(input: MultiStepInput, state: Partial<State>) {
77+
const additionalSteps = typeof state.resourceGroup === 'string' ? 1 : 0;
78+
// TODO: Remember current value when navigating back.
79+
state.name = await input.showInputBox({
80+
title,
81+
step: 2 + additionalSteps,
82+
totalSteps: 3 + additionalSteps,
83+
value: state.name || '',
84+
prompt: 'Choose a unique name for the Application Service',
85+
validate: validateNameIsUnique,
86+
shouldResume: shouldResume
87+
});
88+
return (input: MultiStepInput) => pickRuntime(input, state);
89+
}
90+
91+
async function pickRuntime(input: MultiStepInput, state: Partial<State>) {
92+
const additionalSteps = typeof state.resourceGroup === 'string' ? 1 : 0;
93+
const runtimes = await getAvailableRuntimes(state.resourceGroup!, undefined /* TODO: token */);
94+
// TODO: Remember currently active item when navigating back.
95+
state.runtime = await input.showQuickPick({
96+
title,
97+
step: 3 + additionalSteps,
98+
totalSteps: 3 + additionalSteps,
99+
placeholder: 'Pick a runtime',
100+
items: runtimes,
101+
activeItem: state.runtime,
102+
shouldResume: shouldResume
103+
});
104+
}
105+
106+
function shouldResume() {
107+
// Could show a notification with the option to resume.
108+
return new Promise<boolean>((resolve, reject) => {
109+
// noop
110+
});
111+
}
112+
113+
async function validateNameIsUnique(name: string) {
114+
// ...validate...
115+
await new Promise(resolve => setTimeout(resolve, 1000));
116+
return name === 'vscode' ? 'Name not unique' : undefined;
117+
}
118+
119+
async function getAvailableRuntimes(resourceGroup: QuickPickItem | string, token?: CancellationToken): Promise<QuickPickItem[]> {
120+
// ...retrieve...
121+
await new Promise(resolve => setTimeout(resolve, 1000));
122+
return ['Node 8.9', 'Node 6.11', 'Node 4.5']
123+
.map(label => ({ label }));
124+
}
125+
126+
const state = await collectInputs();
127+
window.showInformationMessage(`Creating Application Service '${state.name}'`);
128+
}
129+
130+
131+
// -------------------------------------------------------
132+
// Helper code that wraps the API for the multi-step case.
133+
// -------------------------------------------------------
134+
135+
136+
class InputFlowAction {
137+
static back = new InputFlowAction();
138+
static cancel = new InputFlowAction();
139+
static resume = new InputFlowAction();
140+
}
141+
142+
type InputStep = (input: MultiStepInput) => Thenable<InputStep | void>;
143+
144+
interface QuickPickParameters<T extends QuickPickItem> {
145+
title: string;
146+
step: number;
147+
totalSteps: number;
148+
items: T[];
149+
activeItem?: T;
150+
ignoreFocusOut?: boolean;
151+
placeholder: string;
152+
buttons?: QuickInputButton[];
153+
shouldResume: () => Thenable<boolean>;
154+
}
155+
156+
interface InputBoxParameters {
157+
title: string;
158+
step: number;
159+
totalSteps: number;
160+
value: string;
161+
prompt: string;
162+
validate: (value: string) => Promise<string | undefined>;
163+
buttons?: QuickInputButton[];
164+
ignoreFocusOut?: boolean;
165+
placeholder?: string;
166+
shouldResume: () => Thenable<boolean>;
167+
}
168+
169+
class MultiStepInput {
170+
171+
static async run<T>(start: InputStep) {
172+
const input = new MultiStepInput();
173+
return input.stepThrough(start);
174+
}
175+
176+
private current?: QuickInput;
177+
private steps: InputStep[] = [];
178+
179+
private async stepThrough<T>(start: InputStep) {
180+
let step: InputStep | void = start;
181+
while (step) {
182+
this.steps.push(step);
183+
if (this.current) {
184+
this.current.enabled = false;
185+
this.current.busy = true;
186+
}
187+
try {
188+
step = await step(this);
189+
} catch (err) {
190+
if (err === InputFlowAction.back) {
191+
this.steps.pop();
192+
step = this.steps.pop();
193+
} else if (err === InputFlowAction.resume) {
194+
step = this.steps.pop();
195+
} else if (err === InputFlowAction.cancel) {
196+
step = undefined;
197+
} else {
198+
throw err;
199+
}
200+
}
201+
}
202+
if (this.current) {
203+
this.current.dispose();
204+
}
205+
}
206+
207+
async showQuickPick<T extends QuickPickItem, P extends QuickPickParameters<T>>({ title, step, totalSteps, items, activeItem, ignoreFocusOut, placeholder, buttons, shouldResume }: P) {
208+
const disposables: Disposable[] = [];
209+
try {
210+
return await new Promise<T | (P extends { buttons: (infer I)[] } ? I : never)>((resolve, reject) => {
211+
const input = window.createQuickPick<T>();
212+
input.title = title;
213+
input.step = step;
214+
input.totalSteps = totalSteps;
215+
input.ignoreFocusOut = ignoreFocusOut ?? false;
216+
input.placeholder = placeholder;
217+
input.items = items;
218+
if (activeItem) {
219+
input.activeItems = [activeItem];
220+
}
221+
input.buttons = [
222+
...(this.steps.length > 1 ? [QuickInputButtons.Back] : []),
223+
...(buttons || [])
224+
];
225+
disposables.push(
226+
input.onDidTriggerButton(item => {
227+
if (item === QuickInputButtons.Back) {
228+
reject(InputFlowAction.back);
229+
} else {
230+
resolve(<any>item);
231+
}
232+
}),
233+
input.onDidChangeSelection(items => resolve(items[0])),
234+
input.onDidHide(() => {
235+
(async () => {
236+
reject(shouldResume && await shouldResume() ? InputFlowAction.resume : InputFlowAction.cancel);
237+
})()
238+
.catch(reject);
239+
})
240+
);
241+
if (this.current) {
242+
this.current.dispose();
243+
}
244+
this.current = input;
245+
this.current.show();
246+
});
247+
} finally {
248+
disposables.forEach(d => d.dispose());
249+
}
250+
}
251+
252+
async showInputBox<P extends InputBoxParameters>({ title, step, totalSteps, value, prompt, validate, buttons, ignoreFocusOut, placeholder, shouldResume }: P) {
253+
const disposables: Disposable[] = [];
254+
try {
255+
return await new Promise<string | (P extends { buttons: (infer I)[] } ? I : never)>((resolve, reject) => {
256+
const input = window.createInputBox();
257+
input.title = title;
258+
input.step = step;
259+
input.totalSteps = totalSteps;
260+
input.value = value || '';
261+
input.prompt = prompt;
262+
input.ignoreFocusOut = ignoreFocusOut ?? false;
263+
input.placeholder = placeholder;
264+
input.buttons = [
265+
...(this.steps.length > 1 ? [QuickInputButtons.Back] : []),
266+
...(buttons || [])
267+
];
268+
let validating = validate('');
269+
disposables.push(
270+
input.onDidTriggerButton(item => {
271+
if (item === QuickInputButtons.Back) {
272+
reject(InputFlowAction.back);
273+
} else {
274+
resolve(<any>item);
275+
}
276+
}),
277+
input.onDidAccept(async () => {
278+
const value = input.value;
279+
input.enabled = false;
280+
input.busy = true;
281+
if (!(await validate(value))) {
282+
resolve(value);
283+
}
284+
input.enabled = true;
285+
input.busy = false;
286+
}),
287+
input.onDidChangeValue(async text => {
288+
const current = validate(text);
289+
validating = current;
290+
const validationMessage = await current;
291+
if (current === validating) {
292+
input.validationMessage = validationMessage;
293+
}
294+
}),
295+
input.onDidHide(() => {
296+
(async () => {
297+
reject(shouldResume && await shouldResume() ? InputFlowAction.resume : InputFlowAction.cancel);
298+
})()
299+
.catch(reject);
300+
})
301+
);
302+
if (this.current) {
303+
this.current.dispose();
304+
}
305+
this.current = input;
306+
this.current.show();
307+
});
308+
} finally {
309+
disposables.forEach(d => d.dispose());
310+
}
311+
}
312+
}

0 commit comments

Comments
 (0)