From 973991b3a7e5220ae3974fa0d0c89978100ceab1 Mon Sep 17 00:00:00 2001 From: Bryce Ito Date: Tue, 12 Mar 2019 14:25:26 -0700 Subject: [PATCH 1/3] Adding back button flow to wizard --- src/lambda/wizards/samInitWizard.ts | 214 +++++++++++++++++++--------- 1 file changed, 149 insertions(+), 65 deletions(-) diff --git a/src/lambda/wizards/samInitWizard.ts b/src/lambda/wizards/samInitWizard.ts index add799f7f7b..e3d761f07d7 100644 --- a/src/lambda/wizards/samInitWizard.ts +++ b/src/lambda/wizards/samInitWizard.ts @@ -13,6 +13,8 @@ import * as os from 'os' import * as path from 'path' import * as vscode from 'vscode' import { SamCliInitArgs } from '../../shared/sam/cli/samCliInit' +import * as input from '../../shared/ui/input' +import * as picker from '../../shared/ui/picker' import * as lambdaRuntime from '../models/samLambdaRuntime' import { MultiStepWizard, WizardStep } from '../wizards/multiStepWizard' @@ -20,6 +22,14 @@ export interface CreateNewSamAppWizardContext { readonly lambdaRuntimes: immutable.Set readonly workspaceFolders: vscode.WorkspaceFolder[] | undefined + promptUserForRuntime( + currRuntime?: lambdaRuntime.SamLambdaRuntime + ): Promise + + promptUserForLocation(): Promise + + promptUserForName(): Promise + showInputBox( options?: vscode.InputBoxOptions, token?: vscode.CancellationToken @@ -60,92 +70,117 @@ class DefaultCreateNewSamAppWizardContext implements CreateNewSamAppWizardContex public get workspaceFolders(): vscode.WorkspaceFolder[] | undefined { return vscode.workspace.workspaceFolders } -} - -export class CreateNewSamAppWizard extends MultiStepWizard { - private runtime?: lambdaRuntime.SamLambdaRuntime - private location?: vscode.Uri - private name?: string - public constructor( - private readonly context: CreateNewSamAppWizardContext = new DefaultCreateNewSamAppWizardContext() - ) { - super() - } + public async promptUserForRuntime( + currRuntime?: lambdaRuntime.SamLambdaRuntime + ): Promise { + + const quickPick = await picker.createQuickPick({ + options: { + ignoreFocusOut: true, + placeHolder: localize( + 'AWS.samcli.initWizard.runtime.prompt', + 'Select a SAM Application Runtime' + ), + value: String(currRuntime) || '' + }, + items: lambdaRuntime.samLambdaRuntimes + .toArray() + .sort() + .map(runtime => ({ + label: runtime, + alwaysShow: runtime === currRuntime, + description: runtime === currRuntime ? + localize('AWS.samcli.deploy.region.previousRegion', 'Selected Previously') : '' + })) + }) - protected get startStep() { - return this.RUNTIME - } + const choices = await picker.promptUser({ + picker: quickPick + }) - protected getResult(): SamCliInitArgs | undefined { - if (!this.runtime || !this.location || !this.name) { + if (!choices || choices.length === 0) { return undefined } - return { - runtime: this.runtime, - location: this.location, - name: this.name - } - } - - private readonly RUNTIME: WizardStep = async () => { - const runtimeItems = this.context.lambdaRuntimes - .toArray() - .sort() - .map(runtime => ({ label: runtime })) - - const result = await this.context.showQuickPick(runtimeItems, { - ignoreFocusOut: true, - placeHolder: localize( - 'AWS.samcli.initWizard.runtime.prompt', - 'Select a SAM Application Runtime' + if (choices.length > 1) { + console.error( + `Received ${choices.length} responses from user, expected 1.` + + ' Cancelling to prevent deployment of unexpected template.' ) - }) - if (!result) { return undefined } - this.runtime = result.label as lambdaRuntime.SamLambdaRuntime - - return this.LOCATION + return choices[0].label as lambdaRuntime.SamLambdaRuntime } - private readonly LOCATION: WizardStep = async () => { - const choices: FolderQuickPickItem[] = (this.context.workspaceFolders || []) + public async promptUserForLocation(): Promise { + const items: FolderQuickPickItem[] = (this.workspaceFolders || []) .map(f => new WorkspaceFolderQuickPickItem(f)) - .concat([new BrowseFolderQuickPickItem(this.context)]) + .concat([new BrowseFolderQuickPickItem(this)]) + + const quickPick = await picker.createQuickPick({ + options: { + ignoreFocusOut: true, + placeHolder: localize( + 'AWS.samcli.initWizard.location.prompt', + 'Select a location for your new project' + ) + }, + items: items, + buttons: [ + vscode.QuickInputButtons.Back + ] + }) - const selection = await this.context.showQuickPick(choices, { - ignoreFocusOut: true, - placeHolder: localize( - 'AWS.samcli.initWizard.location.prompt', - 'Select a location for your new project' - ) + const choices = await picker.promptUser({ + picker: quickPick, + onDidTriggerButton: (button, resolve, reject) => { + if (button === vscode.QuickInputButtons.Back) { + resolve(undefined) + } + } }) - if (!selection) { - return this.RUNTIME + + if (!choices || choices.length === 0) { + return undefined } - this.location = await selection.getUri() - return this.location ? this.NAME : this.RUNTIME + if (choices.length > 1) { + console.error( + `Received ${choices.length} responses from user, expected 1.` + + ' Cancelling to prevent deployment of unexpected template.' + ) + + return undefined + } + + return choices[0].getUri() } - private readonly NAME: WizardStep = async () => { - this.name = await this.context.showInputBox({ - value: 'my-sam-app', - prompt: localize( - 'AWS.samcli.initWizard.name.prompt', - 'Choose a name for your new application' - ), - placeHolder: localize( - 'AWS.samcli.initWizard.name.placeholder', - 'application name' - ), - ignoreFocusOut: true, + public async promptUserForName(): Promise { + const inputBox = await input.createInputBox({ + options: { + title: '', + prompt: localize( + 'AWS.samcli.initWizard.name.prompt', + 'Choose a name for your new application' + ), + placeHolder: localize( + 'AWS.samcli.initWizard.name.placeholder', + 'application name' + ), + ignoreFocusOut: true, + }, + buttons: [ + vscode.QuickInputButtons.Back + ] + }) - validateInput(value: string): string | undefined | null | Thenable { + return await input.promptUser({ + inputBox: inputBox, + onValidateInput: (value: string) => { if (!value) { return localize( 'AWS.samcli.initWizard.name.error.empty', @@ -162,8 +197,57 @@ export class CreateNewSamAppWizard extends MultiStepWizard { } return undefined + }, + onDidTriggerButton: (button, resolve, reject) => { + if (button === vscode.QuickInputButtons.Back) { + resolve(undefined) + } } }) + } +} + +export class CreateNewSamAppWizard extends MultiStepWizard { + private runtime?: lambdaRuntime.SamLambdaRuntime + private location?: vscode.Uri + private name?: string + + public constructor( + private readonly context: CreateNewSamAppWizardContext = new DefaultCreateNewSamAppWizardContext() + ) { + super() + } + + protected get startStep() { + return this.RUNTIME + } + + protected getResult(): SamCliInitArgs | undefined { + if (!this.runtime || !this.location || !this.name) { + return undefined + } + + return { + runtime: this.runtime, + location: this.location, + name: this.name + } + } + + private readonly RUNTIME: WizardStep = async () => { + this.runtime = await this.context.promptUserForRuntime(this.runtime) + + return this.runtime ? this.LOCATION : undefined + } + + private readonly LOCATION: WizardStep = async () => { + this.location = await this.context.promptUserForLocation() + + return this.location ? this.NAME : this.RUNTIME + } + + private readonly NAME: WizardStep = async () => { + this.name = await this.context.promptUserForName() return this.name ? undefined : this.LOCATION } From 5bdf4f419b799f1d6da8bbef18021fb13dbbd85f Mon Sep 17 00:00:00 2001 From: Bryce Ito Date: Wed, 13 Mar 2019 13:41:47 -0700 Subject: [PATCH 2/3] Fixed tests and refactored picker --- package.nls.json | 4 +- src/lambda/wizards/samDeployWizard.ts | 39 ++------- src/lambda/wizards/samInitWizard.ts | 83 ++++--------------- src/shared/ui/picker.ts | 21 +++++ src/test/lambda/wizards/samInitWizard.test.ts | 74 ++++++----------- 5 files changed, 70 insertions(+), 151 deletions(-) diff --git a/package.nls.json b/package.nls.json index 28af9ca2f06..3edd8484265 100644 --- a/package.nls.json +++ b/package.nls.json @@ -107,7 +107,6 @@ "AWS.samcli.detect.settings.updated": "Settings updated.", "AWS.samcli.detect.settings.not.updated": "No settings changes necessary.", "AWS.samcli.deploy.region.prompt": "Which AWS Region would you like to deploy to?", - "AWS.samcli.deploy.region.previousRegion": "Selected Previously", "AWS.samcli.deploy.s3Bucket.region": "S3 bucket must be in selected region: {0}", "AWS.samcli.deploy.s3Bucket.prompt": "Enter the AWS S3 bucket to which your code should be deployed", "AWS.samcli.deploy.s3Bucket.error": "S3 bucket cannot be empty", @@ -135,7 +134,7 @@ "AWS.samcli.userChoice.browse": "Locate SAM CLI...", "AWS.samcli.userChoice.visit.install.url": "Get SAM CLI", "AWS.samcli.userChoice.update.awstoolkit.url": "Update AWS Toolkit", - "AWS.samcli.initWizard.location.prompt": "Select a location for your new project", + "AWS.samcli.initWizard.location.prompt": "Select a workspace folder for your new project", "AWS.samcli.initWizard.name.prompt": "Choose a name for your new application", "AWS.samcli.initWizard.name.placeholder": "application name", "AWS.samcli.initWizard.name.browse.label": "Browse...", @@ -145,6 +144,7 @@ "AWS.samcli.initWizard.runtime.prompt": "Select a SAM Application Runtime", "AWS.samcli.initWizard.source.error.notFound": "Project created successfully, but main source code file not found: {0}", "AWS.samcli.initWizard.source.error.notInWorkspace": "Could not open file '{0}'. If this file exists on disk, try adding it to your workspace.", + "AWS.samcli.wizard.selectedPreviously": "Selected Previously", "AWS.generic.response.no": "No", "AWS.generic.response.yes": "Yes", "AWS.template.error.showErrorDetails.title": "Error details for", diff --git a/src/lambda/wizards/samDeployWizard.ts b/src/lambda/wizards/samDeployWizard.ts index 0aae4aa70c5..3ce62118b7a 100644 --- a/src/lambda/wizards/samDeployWizard.ts +++ b/src/lambda/wizards/samDeployWizard.ts @@ -80,7 +80,6 @@ class DefaultSamDeployWizardContext implements SamDeployWizardContext { * @returns vscode.Uri of a Sam Template. undefined represents cancel. */ public async promptUserForSamTemplate(initialValue?: vscode.Uri): Promise { - const logger = getLogger() const workspaceFolders = this.workspaceFolders || [] const quickPick = picker.createQuickPick({ @@ -96,28 +95,15 @@ class DefaultSamDeployWizardContext implements SamDeployWizardContext { const choices = await picker.promptUser({ picker: quickPick, }) + const val = picker.verifySinglePickerOutput(choices) - if (!choices || choices.length === 0) { - return undefined - } - - if (choices.length > 1) { - logger.warn( - `Received ${choices.length} responses from user, expected 1.` + - ' Cancelling to prevent deployment of unexpected template.' - ) - - return undefined - } - - return choices[0].uri + return val ? val.uri : undefined } public async promptUserForRegion( regionProvider: RegionProvider, initialRegionCode: string ): Promise { - const logger = getLogger() const regionData = await regionProvider.getRegionData() const quickPick = picker.createQuickPick({ @@ -138,7 +124,7 @@ class DefaultSamDeployWizardContext implements SamDeployWizardContext { // this will make it so it always shows even when searching for something else alwaysShow: r.regionCode === initialRegionCode, description: r.regionCode === initialRegionCode ? - localize('AWS.samcli.deploy.region.previousRegion', 'Selected Previously') : '' + localize('AWS.samcli.wizard.selectedPreviously', 'Selected Previously') : '' } )), buttons: [ @@ -154,21 +140,9 @@ class DefaultSamDeployWizardContext implements SamDeployWizardContext { } } }) + const val = picker.verifySinglePickerOutput(choices) - if (!choices || choices.length === 0) { - return undefined - } - - if (choices.length > 1) { - logger.warn( - `Received ${choices.length} responses from user, expected 1.` + - ' Cancelling to prevent deployment of unexpected template.' - ) - - return undefined - } - - return choices[0].detail + return val ? val.detail : undefined } /** @@ -351,6 +325,7 @@ class SamTemplateQuickPickItem implements vscode.QuickPickItem { } public static getLabel(uri: vscode.Uri): string { + const logger = getLogger() const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri) if (workspaceFolder) { @@ -360,7 +335,7 @@ class SamTemplateQuickPickItem implements vscode.QuickPickItem { } // We shouldn't find sam templates outside of a workspace folder. If we do, show the full path. - console.warn( + logger.warn( `Unexpected situation: detected SAM Template ${uri.fsPath} not found within a workspace folder.` ) diff --git a/src/lambda/wizards/samInitWizard.ts b/src/lambda/wizards/samInitWizard.ts index e3d761f07d7..29ecc048a7c 100644 --- a/src/lambda/wizards/samInitWizard.ts +++ b/src/lambda/wizards/samInitWizard.ts @@ -17,7 +17,6 @@ import * as input from '../../shared/ui/input' import * as picker from '../../shared/ui/picker' import * as lambdaRuntime from '../models/samLambdaRuntime' import { MultiStepWizard, WizardStep } from '../wizards/multiStepWizard' - export interface CreateNewSamAppWizardContext { readonly lambdaRuntimes: immutable.Set readonly workspaceFolders: vscode.WorkspaceFolder[] | undefined @@ -30,42 +29,14 @@ export interface CreateNewSamAppWizardContext { promptUserForName(): Promise - showInputBox( - options?: vscode.InputBoxOptions, - token?: vscode.CancellationToken - ): Thenable - showOpenDialog( options: vscode.OpenDialogOptions ): Thenable - - showQuickPick( - items: string[] | Thenable, - options: vscode.QuickPickOptions & { canPickMany: true }, - token?: vscode.CancellationToken - ): Thenable - showQuickPick( - items: string[] | Thenable, - options?: vscode.QuickPickOptions, - token?: vscode.CancellationToken - ): Thenable - showQuickPick( - items: T[] | Thenable, - options: vscode.QuickPickOptions & { canPickMany: true }, - token?: vscode.CancellationToken - ): Thenable - showQuickPick( - items: T[] | Thenable, - options?: vscode.QuickPickOptions, - token?: vscode.CancellationToken - ): Thenable } class DefaultCreateNewSamAppWizardContext implements CreateNewSamAppWizardContext { public readonly lambdaRuntimes = lambdaRuntime.samLambdaRuntimes - public readonly showInputBox = vscode.window.showInputBox public readonly showOpenDialog = vscode.window.showOpenDialog - public readonly showQuickPick = vscode.window.showQuickPick public get workspaceFolders(): vscode.WorkspaceFolder[] | undefined { return vscode.workspace.workspaceFolders @@ -74,45 +45,32 @@ class DefaultCreateNewSamAppWizardContext implements CreateNewSamAppWizardContex public async promptUserForRuntime( currRuntime?: lambdaRuntime.SamLambdaRuntime ): Promise { - - const quickPick = await picker.createQuickPick({ + const quickPick = picker.createQuickPick({ options: { ignoreFocusOut: true, - placeHolder: localize( + title: localize( 'AWS.samcli.initWizard.runtime.prompt', 'Select a SAM Application Runtime' ), - value: String(currRuntime) || '' + value: currRuntime ? currRuntime : '' }, - items: lambdaRuntime.samLambdaRuntimes + items: this.lambdaRuntimes .toArray() .sort() .map(runtime => ({ label: runtime, alwaysShow: runtime === currRuntime, description: runtime === currRuntime ? - localize('AWS.samcli.deploy.region.previousRegion', 'Selected Previously') : '' + localize('AWS.samcli.wizard.selectedPreviously', 'Selected Previously') : '' })) }) const choices = await picker.promptUser({ picker: quickPick }) + const val = picker.verifySinglePickerOutput(choices) - if (!choices || choices.length === 0) { - return undefined - } - - if (choices.length > 1) { - console.error( - `Received ${choices.length} responses from user, expected 1.` + - ' Cancelling to prevent deployment of unexpected template.' - ) - - return undefined - } - - return choices[0].label as lambdaRuntime.SamLambdaRuntime + return val ? val.label as lambdaRuntime.SamLambdaRuntime : undefined } public async promptUserForLocation(): Promise { @@ -120,12 +78,12 @@ class DefaultCreateNewSamAppWizardContext implements CreateNewSamAppWizardContex .map(f => new WorkspaceFolderQuickPickItem(f)) .concat([new BrowseFolderQuickPickItem(this)]) - const quickPick = await picker.createQuickPick({ + const quickPick = picker.createQuickPick({ options: { ignoreFocusOut: true, - placeHolder: localize( + title: localize( 'AWS.samcli.initWizard.location.prompt', - 'Select a location for your new project' + 'Select a workspace folder for your new project' ) }, items: items, @@ -142,28 +100,15 @@ class DefaultCreateNewSamAppWizardContext implements CreateNewSamAppWizardContex } } }) + const val = picker.verifySinglePickerOutput(choices) - if (!choices || choices.length === 0) { - return undefined - } - - if (choices.length > 1) { - console.error( - `Received ${choices.length} responses from user, expected 1.` + - ' Cancelling to prevent deployment of unexpected template.' - ) - - return undefined - } - - return choices[0].getUri() + return val ? val.getUri() : undefined } public async promptUserForName(): Promise { - const inputBox = await input.createInputBox({ + const inputBox = input.createInputBox({ options: { - title: '', - prompt: localize( + title: localize( 'AWS.samcli.initWizard.name.prompt', 'Choose a name for your new application' ), diff --git a/src/shared/ui/picker.ts b/src/shared/ui/picker.ts index bf882bd854e..6de1b84c2d0 100644 --- a/src/shared/ui/picker.ts +++ b/src/shared/ui/picker.ts @@ -6,6 +6,7 @@ 'use strict' import * as vscode from 'vscode' +import { getLogger } from '../logger' /** * Options to configure the behavior of the quick pick UI. @@ -125,3 +126,23 @@ export async function promptUser( picker.hide() } } + +export function verifySinglePickerOutput( + choices: T[] | undefined +): T | undefined { + const logger = getLogger() + if (!choices || choices.length === 0) { + return undefined + } + + if (choices.length > 1) { + logger.warn( + `Received ${choices.length} responses from user, expected 1.` + + ' Cancelling to prevent deployment of unexpected template.' + ) + + return undefined + } + + return choices[0] +} diff --git a/src/test/lambda/wizards/samInitWizard.test.ts b/src/test/lambda/wizards/samInitWizard.test.ts index 63ebe125bd6..9206ac0fc7d 100644 --- a/src/test/lambda/wizards/samInitWizard.test.ts +++ b/src/test/lambda/wizards/samInitWizard.test.ts @@ -88,21 +88,6 @@ class MockCreateNewSamAppWizardContext implements CreateNewSamAppWizardContext { } - public async showInputBox( - options?: vscode.InputBoxOptions | undefined, - token?: vscode.CancellationToken | undefined - ): Promise { - if (Array.isArray(this.inputBoxResult)) { - if (this.inputBoxResult.length <= 0) { - throw new Error('showInputBox was called more times than expected') - } - - return this.inputBoxResult.pop() - } - - return this.inputBoxResult - } - public async showOpenDialog( options: vscode.OpenDialogOptions ): Promise { @@ -117,41 +102,34 @@ class MockCreateNewSamAppWizardContext implements CreateNewSamAppWizardContext { return this.openDialogResult as vscode.Uri[] } - public showQuickPick( - items: string[] | Thenable, - options: vscode.QuickPickOptions & { canPickMany: true }, - token?: vscode.CancellationToken - ): Thenable - public showQuickPick( - items: string[] | Thenable, - options?: vscode.QuickPickOptions, - token?: vscode.CancellationToken - ): Thenable - public showQuickPick( - items: T[] | Thenable, - options: vscode.QuickPickOptions & { canPickMany: true }, - token?: vscode.CancellationToken - ): Thenable - public showQuickPick( - items: T[] | Thenable, - options?: vscode.QuickPickOptions, - token?: vscode.CancellationToken - ): Thenable - public async showQuickPick( - items: string[] | Thenable | T[] | Thenable, - options?: vscode.QuickPickOptions, - token?: vscode.CancellationToken - ): Promise { - // Just return the first item in `items`. - const resolvedItems: string[] | T[] = Array.isArray(items) ? - items as string[] | T[] : - await (items as (Thenable | Thenable)) - - if (resolvedItems.length <= 0) { - return undefined + public async promptUserForRuntime( + currRuntime?: SamLambdaRuntime + ): Promise { + return this.lambdaRuntimes.toArray().pop() + } + + public async promptUserForLocation(): Promise { + if (this.workspaceFolders && this.workspaceFolders.length > 0) { + const temp = this.workspaceFolders[0] + + return temp ? temp.uri : undefined + } else { + const locations = await this.showOpenDialog({}) + + return locations ? locations.pop() : undefined + } + } + + public async promptUserForName(): Promise { + if (typeof this.inputBoxResult === 'string') { + return this.inputBoxResult + } + + if (this.inputBoxResult.length <= 0) { + throw new Error('inputBoxResult was called more times than expected') } - return resolvedItems[0] + return this.inputBoxResult.pop() } } From 7a2220a46c7848187a2153ce461954f906e2ae2d Mon Sep 17 00:00:00 2001 From: Bryce Ito Date: Wed, 13 Mar 2019 15:56:01 -0700 Subject: [PATCH 3/3] Changed wording --- package.nls.json | 2 +- src/lambda/wizards/samInitWizard.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.nls.json b/package.nls.json index 3edd8484265..841c115a860 100644 --- a/package.nls.json +++ b/package.nls.json @@ -137,7 +137,7 @@ "AWS.samcli.initWizard.location.prompt": "Select a workspace folder for your new project", "AWS.samcli.initWizard.name.prompt": "Choose a name for your new application", "AWS.samcli.initWizard.name.placeholder": "application name", - "AWS.samcli.initWizard.name.browse.label": "Browse...", + "AWS.samcli.initWizard.name.browse.label": "Choose a different folder...", "AWS.samcli.initWizard.name.browse.openLabel": "Open", "AWS.samcli.initWizard.name.error.empty": "Application name cannot be empty", "AWS.samcli.initWizard.name.error.pathSep": "The path separator ({0}) is not allowed in application names", diff --git a/src/lambda/wizards/samInitWizard.ts b/src/lambda/wizards/samInitWizard.ts index 29ecc048a7c..1c12d3c8924 100644 --- a/src/lambda/wizards/samInitWizard.ts +++ b/src/lambda/wizards/samInitWizard.ts @@ -217,7 +217,7 @@ class WorkspaceFolderQuickPickItem implements FolderQuickPickItem { class BrowseFolderQuickPickItem implements FolderQuickPickItem { public readonly label = localize( 'AWS.samcli.initWizard.name.browse.label', - 'Browse...' + 'Choose a different folder...' ) public constructor(