From 33ff661c6fb5cd24626c6153061725be7321cbac Mon Sep 17 00:00:00 2001 From: Charles Gagnon Date: Fri, 25 Jun 2021 10:46:40 -0700 Subject: [PATCH] Add VS Code native New Project create flow (#15906) * Add VS Code native New Project create flow * Update project name title * Ignore focus out * comments * ellipsis --- .../data-workspace/src/common/constants.ts | 10 ++- .../src/dialogs/newProjectQuickpick.ts | 84 +++++++++++++++++++ extensions/data-workspace/src/main.ts | 13 ++- .../src/services/workspaceService.ts | 22 +++-- 4 files changed, 116 insertions(+), 13 deletions(-) create mode 100644 extensions/data-workspace/src/dialogs/newProjectQuickpick.ts diff --git a/extensions/data-workspace/src/common/constants.ts b/extensions/data-workspace/src/common/constants.ts index ff9fa779c5..28d94ac69e 100644 --- a/extensions/data-workspace/src/common/constants.ts +++ b/extensions/data-workspace/src/common/constants.ts @@ -15,7 +15,7 @@ export const AllProjectTypes = localize('AllProjectTypes', "All Project Types"); export const ProviderNotFoundForProjectTypeError = (projectType: string): string => { return localize('UnknownProjectTypeError', "No provider was found for project type with id: '{0}'", projectType); }; export const WorkspaceRequiredMessage = localize('dataworkspace.workspaceRequiredMessage', "A workspace is required in order to use the project feature."); export const OpenWorkspace = localize('dataworkspace.openWorkspace', "Open Workspace…"); -export const CreateWorkspaceConfirmation = localize('dataworkspace.createWorkspaceConfirmation', "A workspace will be created and opened in order to open project. Azure Data Studio will restart and if there is a folder currently open, it will be closed."); +export const CreateWorkspaceConfirmation = localize('dataworkspace.createWorkspaceConfirmation', "A workspace will be created and opened in order to open the project. Azure Data Studio will restart and if there is a folder currently open, it will be closed."); export const EnterWorkspaceConfirmation = localize('dataworkspace.enterWorkspaceConfirmation', "To open this workspace, Azure Data Studio will restart. If there is a workspace or folder currently open, it will be closed."); export const WorkspaceContainsNotAddedProjects = localize('dataworkspace.workspaceContainsNotAddedProjects', "The current workspace contains one or more projects that have not been added to the workspace. Use the 'Open existing' dialog to add projects to the projects pane."); export const LaunchOpenExisitingDialog = localize('dataworkspace.launchOpenExistingDialog', "Launch 'Open Existing' Dialog"); @@ -35,8 +35,10 @@ export const showNotAddedProjectsMessageKey = 'showNotAddedProjectsInWorkspacePr export const OkButtonText = localize('dataworkspace.ok', "OK"); export const CancelButtonText = localize('dataworkspace.cancel', "Cancel"); export const BrowseButtonText = localize('dataworkspace.browse', "Browse"); +export const BrowseEllipsis = localize('dataworkspace.browseEllipsis', "Browse..."); export const OpenButtonText = localize('dataworkspace.open', "Open"); export const CreateButtonText = localize('dataworkspace.create', "Create"); +export const Select = localize('dataworkspace.select', "Select"); export const WorkspaceFileExtension = '.code-workspace'; export const DefaultInputWidth = '400px'; export const DefaultButtonWidth = '80px'; @@ -46,6 +48,7 @@ export const NewProjectDialogTitle = localize('dataworkspace.NewProjectDialogTit export const TypeTitle = localize('dataworkspace.Type', "Type"); export const ProjectNameTitle = localize('dataworkspace.projectNameTitle', "Name"); export const ProjectNamePlaceholder = localize('dataworkspace.projectNamePlaceholder', "Enter project name"); +export const EnterProjectName = localize('dataworkspace.enterProjectName', "Enter Project Name"); export const ProjectLocationTitle = localize('dataworkspace.projectLocationTitle', "Location"); export const ProjectLocationPlaceholder = localize('dataworkspace.projectLocationPlaceholder', "Select location to create project"); export const AddProjectToCurrentWorkspace = localize('dataworkspace.AddProjectToCurrentWorkspace', "This project will be added to the current workspace."); @@ -53,10 +56,13 @@ export const NewWorkspaceWillBeCreated = localize('dataworkspace.NewWorkspaceWil export const WorkspaceLocationTitle = localize('dataworkspace.workspaceLocationTitle', "Workspace location"); export const ProjectParentDirectoryNotExistError = (location: string): string => { return localize('dataworkspace.projectParentDirectoryNotExistError', "The selected project location '{0}' does not exist or is not a directory.", location); }; export const ProjectDirectoryAlreadyExistError = (projectName: string, location: string): string => { return localize('dataworkspace.projectDirectoryAlreadyExistError', "There is already a directory named '{0}' in the selected location: '{1}'.", projectName, location); }; +export const ProjectDirectoryAlreadyExistErrorShort = (projectName: string) => { return localize('dataworkspace.projectDirectoryAlreadyExistErrorShort', "Directory '{0}' already exists in the selected location, please choose another", projectName); }; export const WorkspaceFileInvalidError = (workspace: string): string => { return localize('dataworkspace.workspaceFileInvalidError', "The selected workspace file path '{0}' does not have the required file extension {1}.", workspace, WorkspaceFileExtension); }; export const WorkspaceParentDirectoryNotExistError = (location: string): string => { return localize('dataworkspace.workspaceParentDirectoryNotExistError', "The selected workspace location '{0}' does not exist or is not a directory.", location); }; export const WorkspaceFileAlreadyExistsError = (file: string): string => { return localize('dataworkspace.workspaceFileAlreadyExistsError', "The selected workspace file '{0}' already exists. To add the project to an existing workspace, use the Open Existing dialog to first open the workspace.", file); }; - +export const SelectProjectType = localize('dataworkspace.selectProjectType', "Select Project Type"); +export const SelectProjectLocation = localize('dataworkspace.selectProjectLocation', "Select Project Location"); +export const NameCannotBeEmpty = localize('dataworkspace.nameCannotBeEmpty', "Name cannot be empty"); //Open Existing Dialog export const OpenExistingDialogTitle = localize('dataworkspace.openExistingDialogTitle', "Open existing"); export const FileNotExistError = (fileType: string, filePath: string): string => { return localize('dataworkspace.fileNotExistError', "The selected {0} file '{1}' does not exist or is not a file.", fileType, filePath); }; diff --git a/extensions/data-workspace/src/dialogs/newProjectQuickpick.ts b/extensions/data-workspace/src/dialogs/newProjectQuickpick.ts new file mode 100644 index 0000000000..829bb927f4 --- /dev/null +++ b/extensions/data-workspace/src/dialogs/newProjectQuickpick.ts @@ -0,0 +1,84 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as constants from '../common/constants'; +import { directoryExist } from '../common/utils'; +import { defaultProjectSaveLocation } from '../common/projectLocationHelper'; +import { WorkspaceService } from '../services/workspaceService'; + +/** + * Create flow for a New Project using only VS Code-native APIs such as QuickPick + */ +export async function createNewProjectWithQuickpick(workspaceService: WorkspaceService): Promise { + // Refresh list of project types + const projectTypes = (await workspaceService.getAllProjectTypes()).map(projType => { + return { + label: projType.displayName, + description: projType.description, + id: projType.id + } as vscode.QuickPickItem & { id: string }; + }); + + // 1. Prompt for project type + const projectType = await vscode.window.showQuickPick(projectTypes, { title: constants.SelectProjectType, ignoreFocusOut: true }); + if (!projectType) { + return; + } + + // 2. Prompt for project name + const projectName = await vscode.window.showInputBox( + { + title: constants.EnterProjectName, + validateInput: (value) => { + return value ? undefined : constants.NameCannotBeEmpty; + }, + ignoreFocusOut: true + }); + if (!projectName) { + return; + } + + // 3. Prompt for Project location + // Show quick pick with just browse option to give user context about what the file dialog is for (since that doesn't always have a title) + const browseProjectLocation = await vscode.window.showQuickPick( + [constants.BrowseEllipsis], + { title: constants.SelectProjectLocation, ignoreFocusOut: true }); + if (!browseProjectLocation) { + return; + } + // We validate that the folder doesn't already exist, and if it does keep prompting them to pick a new one + let valid = false; + let projectLocation = ''; + while (!valid) { + const locations = await vscode.window.showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + openLabel: constants.Select, + title: constants.SelectProjectLocation, + defaultUri: defaultProjectSaveLocation() + }); + if (!locations) { + return; + } + projectLocation = locations[0].fsPath; + const exists = await directoryExist(path.join(projectLocation, projectName)); + if (exists) { + // Show the browse quick pick again with the title updated with the error + const browseProjectLocation = await vscode.window.showQuickPick( + [constants.BrowseEllipsis], + { title: constants.ProjectDirectoryAlreadyExistErrorShort(projectName), ignoreFocusOut: true }); + if (!browseProjectLocation) { + return; + } + } else { + valid = true; + } + } + + await workspaceService.createProject(projectName, vscode.Uri.file(projectLocation), projectType.id, undefined); +} diff --git a/extensions/data-workspace/src/main.ts b/extensions/data-workspace/src/main.ts index f8c60d84bf..2736cdbc66 100644 --- a/extensions/data-workspace/src/main.ts +++ b/extensions/data-workspace/src/main.ts @@ -13,11 +13,13 @@ import { OpenExistingDialog } from './dialogs/openExistingDialog'; import { IWorkspaceService } from './common/interfaces'; import { IconPathHelper } from './common/iconHelper'; import { ProjectDashboard } from './dialogs/projectDashboard'; +import { getAzdataApi } from './common/utils'; +import { createNewProjectWithQuickpick } from './dialogs/newProjectQuickpick'; export async function activate(context: vscode.ExtensionContext): Promise { const workspaceService = new WorkspaceService(context); await workspaceService.loadTempProjects(); - await workspaceService.checkForProjectsNotAddedToWorkspace(); + workspaceService.checkForProjectsNotAddedToWorkspace(); context.subscriptions.push(vscode.workspace.onDidChangeWorkspaceFolders(async () => { await workspaceService.checkForProjectsNotAddedToWorkspace(); })); @@ -31,8 +33,13 @@ export async function activate(context: vscode.ExtensionContext): Promise { - const dialog = new NewProjectDialog(workspaceService); - await dialog.open(); + if (getAzdataApi()) { + const dialog = new NewProjectDialog(workspaceService); + await dialog.open(); + } else { + await createNewProjectWithQuickpick(workspaceService); + } + })); context.subscriptions.push(vscode.commands.registerCommand('projects.openExisting', async () => { diff --git a/extensions/data-workspace/src/services/workspaceService.ts b/extensions/data-workspace/src/services/workspaceService.ts index 1a30a14b87..82f9e27b89 100644 --- a/extensions/data-workspace/src/services/workspaceService.ts +++ b/extensions/data-workspace/src/services/workspaceService.ts @@ -49,17 +49,23 @@ export class WorkspaceService implements IWorkspaceService { * @param projectFileFsPath project to add to the workspace */ async CreateNewWorkspaceForProject(projectFileFsPath: string, workspaceFile: vscode.Uri | undefined): Promise { - // save temp project - await this._context.globalState.update(TempProject, [projectFileFsPath]); - // create workspace const projectFolder = vscode.Uri.file(path.dirname(projectFileFsPath)); - - if (isCurrentWorkspaceUntitled()) { - vscode.workspace.updateWorkspaceFolders(vscode.workspace.workspaceFolders!.length, null, { uri: projectFolder }); - await getAzdataApi()?.workspace.saveAndEnterWorkspace(workspaceFile!); + const azdataApi = getAzdataApi(); + if (azdataApi) { + // save temp project + await this._context.globalState.update(TempProject, [projectFileFsPath]); + if (isCurrentWorkspaceUntitled()) { + vscode.workspace.updateWorkspaceFolders(vscode.workspace.workspaceFolders!.length, null, { uri: projectFolder }); + await azdataApi.workspace.saveAndEnterWorkspace(workspaceFile!); + } else { + await azdataApi.workspace.createAndEnterWorkspace(projectFolder, workspaceFile); + } } else { - await getAzdataApi()?.workspace.createAndEnterWorkspace(projectFolder, workspaceFile); + // In VS Code we don't have access to the workspace APIs exposed by ADS and so can't actually create a new saved workspace. + // Instead we'll just always call this, which will either add it to the existing untitled workspace or create a new + // untitled workspace which the user can then save later on as they wish. + vscode.workspace.updateWorkspaceFolders(vscode.workspace.workspaceFolders?.length || 0, null, { uri: projectFolder }); } }