From 1aaf80c3ab58b5d010e1edf188b013c04dc19761 Mon Sep 17 00:00:00 2001 From: Kim Santiago <31145923+kisantia@users.noreply.github.com> Date: Mon, 14 Dec 2020 13:24:36 -0800 Subject: [PATCH] Make project workspace selectable if no workspace is open yet (#13508) * allow new workspace location to be editable * fix workspace inputbox not showing up after toggling open workspace radio buttons * add a few tests * cleanup * fix errors * addressing comments * fix filter for windows * add error message if existing workspace file is selected and change picker to be folder only * address comments * fix typos and update tests --- .../data-workspace/src/common/constants.ts | 12 +-- .../data-workspace/src/common/interfaces.ts | 6 +- .../data-workspace/src/dialogs/dialogBase.ts | 89 ++++++++++++++++--- .../src/dialogs/newProjectDialog.ts | 18 ++-- .../src/dialogs/openExistingDialog.ts | 23 +++-- .../src/services/workspaceService.ts | 15 ++-- .../src/test/dialogs/newProjectDialog.test.ts | 34 ++++++- .../test/dialogs/openExistingDialog.test.ts | 30 +++++++ 8 files changed, 186 insertions(+), 41 deletions(-) diff --git a/extensions/data-workspace/src/common/constants.ts b/extensions/data-workspace/src/common/constants.ts index 6a89484181..f37ac52a74 100644 --- a/extensions/data-workspace/src/common/constants.ts +++ b/extensions/data-workspace/src/common/constants.ts @@ -22,6 +22,7 @@ export const EnterWorkspaceConfirmation = localize('dataworkspace.enterWorkspace export const OkButtonText = localize('dataworkspace.ok', "OK"); export const CancelButtonText = localize('dataworkspace.cancel', "Cancel"); export const BrowseButtonText = localize('dataworkspace.browse', "Browse"); +export const WorkspaceFileExtension = '.code-workspace'; export const DefaultInputWidth = '400px'; export const DefaultButtonWidth = '80px'; @@ -35,19 +36,20 @@ export const ProjectLocationPlaceholder = localize('dataworkspace.projectLocatio export const AddProjectToCurrentWorkspace = localize('dataworkspace.AddProjectToCurrentWorkspace', "This project will be added to the current workspace."); export const NewWorkspaceWillBeCreated = localize('dataworkspace.NewWorkspaceWillBeCreated', "A new workspace will be created for this project."); export const WorkspaceLocationTitle = localize('dataworkspace.workspaceLocationTitle', "Workspace location"); -export const ProjectParentDirectoryNotExistError = (location: string): string => { return localize('dataworkspace.projectParentDirectoryNotExistError', "The selected location: '{0}' does not exist or is not a directory.", 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 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); }; //Open Existing Dialog export const OpenExistingDialogTitle = localize('dataworkspace.openExistingDialogTitle', "Open existing"); -export const ProjectFileNotExistError = (projectFilePath: string): string => { return localize('dataworkspace.projectFileNotExistError', "The selected project file '{0}' does not exist or is not a file.", projectFilePath); }; -export const WorkspaceFileNotExistError = (workspaceFilePath: string): string => { return localize('dataworkspace.workspaceFileNotExistError', "The selected workspace file '{0}' does not exist or is not a file.", workspaceFilePath); }; +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); }; export const Project = localize('dataworkspace.project', "Project"); export const Workspace = localize('dataworkspace.workspace', "Workspace"); export const LocationSelectorTitle = localize('dataworkspace.locationSelectorTitle', "Location"); export const ProjectFilePlaceholder = localize('dataworkspace.projectFilePlaceholder', "Select project (.sqlproj) file"); -export const WorkspacePlaceholder = localize('dataworkspace.workspacePlaceholder', "Select workspace (.code-workspace) file"); -export const WorkspaceFileExtension = 'code-workspace'; +export const WorkspacePlaceholder = localize('dataworkspace.workspacePlaceholder', "Select workspace ({0}) file", WorkspaceFileExtension); // Workspace settings for saving new projects export const ProjectConfigurationKey = 'projects'; diff --git a/extensions/data-workspace/src/common/interfaces.ts b/extensions/data-workspace/src/common/interfaces.ts index f47d7ecf76..b16eb09f64 100644 --- a/extensions/data-workspace/src/common/interfaces.ts +++ b/extensions/data-workspace/src/common/interfaces.ts @@ -62,8 +62,9 @@ export interface IWorkspaceService { /** * Adds the projects to workspace, if a project is not in the workspace folder, its containing folder will be added to the workspace * @param projectFiles the list of project files to be added, the project file should be absolute path. + * @param workspaceFilePath The workspace file to create if a workspace isn't currently open */ - addProjectsToWorkspace(projectFiles: vscode.Uri[]): Promise; + addProjectsToWorkspace(projectFiles: vscode.Uri[], workspaceFilePath?: vscode.Uri): Promise; /** * Remove the project from workspace @@ -76,8 +77,9 @@ export interface IWorkspaceService { * @param name The name of the project * @param location The location of the project * @param projectTypeId The project type id + * @param workspaceFile The workspace file to create if a workspace isn't currently open */ - createProject(name: string, location: vscode.Uri, projectTypeId: string): Promise; + createProject(name: string, location: vscode.Uri, projectTypeId: string, workspaceFile?: vscode.Uri): Promise; readonly isProjectProviderAvailable: boolean; diff --git a/extensions/data-workspace/src/dialogs/dialogBase.ts b/extensions/data-workspace/src/dialogs/dialogBase.ts index 7cdbfdb176..3d8bb2f7c9 100644 --- a/extensions/data-workspace/src/dialogs/dialogBase.ts +++ b/extensions/data-workspace/src/dialogs/dialogBase.ts @@ -7,6 +7,8 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import * as path from 'path'; import * as constants from '../common/constants'; +import { IconPathHelper } from '../common/iconHelper'; +import { directoryExist, fileExist } from '../common/utils'; interface Deferred { resolve: (result: T | Promise) => void; @@ -18,8 +20,9 @@ export abstract class DialogBase { protected _dialogObject: azdata.window.Dialog; protected initDialogComplete: Deferred | undefined; protected initDialogPromise: Promise = new Promise((resolve, reject) => this.initDialogComplete = { resolve, reject }); - protected workspaceFormComponent: azdata.FormComponent | undefined; - protected workspaceInputBox: azdata.InputBoxComponent | undefined; + protected workspaceDescriptionFormComponent: azdata.FormComponent | undefined; + public workspaceInputBox: azdata.InputBoxComponent | undefined; + protected workspaceInputFormComponent: azdata.FormComponent | undefined; constructor(dialogTitle: string, dialogName: string, dialogWidth: azdata.window.DialogWidth = 600) { this._dialogObject = azdata.window.createModelViewDialog(dialogTitle, dialogName, dialogWidth); @@ -83,31 +86,64 @@ export abstract class DialogBase { * created if no workspace is currently open * @param view */ - protected createWorkspaceContainer(view: azdata.ModelView): azdata.FormComponent { + protected createWorkspaceContainer(view: azdata.ModelView): void { const workspaceDescription = view.modelBuilder.text().withProperties({ value: vscode.workspace.workspaceFile ? constants.AddProjectToCurrentWorkspace : constants.NewWorkspaceWillBeCreated, - CSSStyles: { 'margin-top': '3px', 'margin-bottom': '10px' } + CSSStyles: { 'margin-top': '3px', 'margin-bottom': '0px' } }).component(); this.workspaceInputBox = view.modelBuilder.inputBox().withProperties({ ariaLabel: constants.WorkspaceLocationTitle, width: constants.DefaultInputWidth, - enabled: false, + enabled: !vscode.workspace.workspaceFile, // want it editable if no workspace is open value: vscode.workspace.workspaceFile?.fsPath ?? '', title: vscode.workspace.workspaceFile?.fsPath ?? '' // hovertext for if file path is too long to be seen in textbox }).component(); - const container = view.modelBuilder.flexContainer() - .withItems([workspaceDescription, this.workspaceInputBox]) - .withLayout({ flexFlow: 'column' }) - .component(); + const browseFolderButton = view.modelBuilder.button().withProperties({ + ariaLabel: constants.BrowseButtonText, + iconPath: IconPathHelper.folder, + height: '16px', + width: '18px' + }).component(); - this.workspaceFormComponent = { + this.register(browseFolderButton.onDidClick(async () => { + const currentFileName = path.parse(this.workspaceInputBox!.value!).base; + + // let user select folder for workspace file to be created in + const folderUris = await vscode.window.showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + defaultUri: vscode.Uri.file(path.parse(this.workspaceInputBox!.value!).dir) + }); + if (!folderUris || folderUris.length === 0) { + return; + } + const selectedFolder = folderUris[0].fsPath; + + const selectedFile = path.join(selectedFolder, currentFileName); + this.workspaceInputBox!.value = selectedFile; + this.workspaceInputBox!.title = selectedFile; + })); + + if (vscode.workspace.workspaceFile) { + this.workspaceInputFormComponent = { + component: this.workspaceInputBox + }; + } else { + // have browse button to help select where the workspace file should be created + const horizontalContainer = this.createHorizontalContainer(view, [this.workspaceInputBox, browseFolderButton]); + this.workspaceInputFormComponent = { + component: horizontalContainer + }; + } + + this.workspaceDescriptionFormComponent = { title: constants.Workspace, - component: container + component: workspaceDescription, + required: true }; - - return this.workspaceFormComponent; } /** @@ -122,4 +158,31 @@ export abstract class DialogBase { this.workspaceInputBox!.title = fileLocation; } } + + protected async validateNewWorkspace(sameFolderAsNewProject: boolean): Promise { + // workspace file should end in .code-workspace + const workspaceValid = this.workspaceInputBox!.value!.endsWith(constants.WorkspaceFileExtension); + if (!workspaceValid) { + this.showErrorMessage(constants.WorkspaceFileInvalidError(this.workspaceInputBox!.value!)); + return false; + } + + // if the workspace file is not going to be in the same folder as the newly created project, then check that it's a valid folder + if (!sameFolderAsNewProject) { + const workspaceParentDirectoryExists = await directoryExist(path.dirname(this.workspaceInputBox!.value!)); + if (!workspaceParentDirectoryExists) { + this.showErrorMessage(constants.WorkspaceParentDirectoryNotExistError(this.workspaceInputBox!.value!)); + return false; + } + } + + // workspace file should not be an existing workspace file + const workspaceFileExists = await fileExist(this.workspaceInputBox!.value!); + if (workspaceFileExists) { + this.showErrorMessage(constants.WorkspaceFileAlreadyExistsError(this.workspaceInputBox!.value!)); + return false; + } + + return true; + } } diff --git a/extensions/data-workspace/src/dialogs/newProjectDialog.ts b/extensions/data-workspace/src/dialogs/newProjectDialog.ts index adaf181e21..33dd6e5eab 100644 --- a/extensions/data-workspace/src/dialogs/newProjectDialog.ts +++ b/extensions/data-workspace/src/dialogs/newProjectDialog.ts @@ -43,6 +43,11 @@ export class NewProjectDialog extends DialogBase { return false; } + const sameFolderAsNewProject = path.join(this.model.location, this.model.name) === path.dirname(this.workspaceInputBox!.value!); + if (this.workspaceInputBox!.enabled && !await this.validateNewWorkspace(sameFolderAsNewProject)) { + return false; + } + return true; } catch (err) { @@ -55,7 +60,7 @@ export class NewProjectDialog extends DialogBase { try { const validateWorkspace = await this.workspaceService.validateWorkspace(); if (validateWorkspace) { - await this.workspaceService.createProject(this.model.name, vscode.Uri.file(this.model.location), this.model.projectTypeId); + await this.workspaceService.createProject(this.model.name, vscode.Uri.file(this.model.location), this.model.projectTypeId, vscode.Uri.file(this.workspaceInputBox!.value!)); } } catch (err) { @@ -109,7 +114,7 @@ export class NewProjectDialog extends DialogBase { this.model.name = projectNameTextBox.value!; projectNameTextBox.updateProperty('title', projectNameTextBox.value); - this.updateWorkspaceInputbox(this.model.location, this.model.name); + this.updateWorkspaceInputbox(path.join(this.model.location, this.model.name), this.model.name); })); const locationTextBox = view.modelBuilder.inputBox().withProperties({ @@ -122,7 +127,7 @@ export class NewProjectDialog extends DialogBase { this.register(locationTextBox.onTextChanged(() => { this.model.location = locationTextBox.value!; locationTextBox.updateProperty('title', locationTextBox.value); - this.updateWorkspaceInputbox(this.model.location, this.model.name); + this.updateWorkspaceInputbox(path.join(this.model.location, this.model.name), this.model.name); })); const browseFolderButton = view.modelBuilder.button().withProperties({ @@ -145,9 +150,11 @@ export class NewProjectDialog extends DialogBase { locationTextBox.value = selectedFolder; this.model.location = selectedFolder; - this.updateWorkspaceInputbox(this.model.location, this.model.name); + this.updateWorkspaceInputbox(path.join(this.model.location, this.model.name), this.model.name); })); + this.createWorkspaceContainer(view); + const form = view.modelBuilder.formContainer().withFormItems([ { title: constants.TypeTitle, @@ -163,7 +170,8 @@ export class NewProjectDialog extends DialogBase { required: true, component: this.createHorizontalContainer(view, [locationTextBox, browseFolderButton]) }, - this.createWorkspaceContainer(view) + this.workspaceDescriptionFormComponent!, + this.workspaceInputFormComponent! ]).component(); await view.initializeModel(form); this.initDialogComplete?.resolve(); diff --git a/extensions/data-workspace/src/dialogs/openExistingDialog.ts b/extensions/data-workspace/src/dialogs/openExistingDialog.ts index bfc242f7a2..adac9632ba 100644 --- a/extensions/data-workspace/src/dialogs/openExistingDialog.ts +++ b/extensions/data-workspace/src/dialogs/openExistingDialog.ts @@ -39,13 +39,17 @@ export class OpenExistingDialog extends DialogBase { if (this._targetTypeRadioCardGroup?.selectedCardId === constants.Project) { const fileExists = await fileExist(this._projectFile); if (!fileExists) { - this.showErrorMessage(constants.ProjectFileNotExistError(this._projectFile)); + this.showErrorMessage(constants.FileNotExistError(constants.Project.toLowerCase(), this._projectFile)); + return false; + } + + if (this.workspaceInputBox!.enabled && !await this.validateNewWorkspace(false)) { return false; } } else if (this._targetTypeRadioCardGroup?.selectedCardId === constants.Workspace) { const fileExists = await fileExist(this._workspaceFile); if (!fileExists) { - this.showErrorMessage(constants.WorkspaceFileNotExistError(this._workspaceFile)); + this.showErrorMessage(constants.FileNotExistError(constants.Workspace.toLowerCase(), this._workspaceFile)); return false; } } @@ -65,7 +69,7 @@ export class OpenExistingDialog extends DialogBase { } else { const validateWorkspace = await this.workspaceService.validateWorkspace(); if (validateWorkspace) { - await this.workspaceService.addProjectsToWorkspace([vscode.Uri.file(this._projectFile)]); + await this.workspaceService.addProjectsToWorkspace([vscode.Uri.file(this._projectFile)], vscode.Uri.file(this.workspaceInputBox!.value!)); } } } @@ -130,16 +134,20 @@ export class OpenExistingDialog extends DialogBase { this.register(this._targetTypeRadioCardGroup.onSelectionChanged(({ cardId }) => { if (cardId === constants.Project) { this._filePathTextBox!.placeHolder = constants.ProjectFilePlaceholder; - this.formBuilder?.addFormItem(this.workspaceFormComponent!); + this.formBuilder?.addFormItem(this.workspaceDescriptionFormComponent!); + this.formBuilder?.addFormItem(this.workspaceInputFormComponent!); } else if (cardId === constants.Workspace) { this._filePathTextBox!.placeHolder = constants.WorkspacePlaceholder; - this.formBuilder?.removeFormItem(this.workspaceFormComponent!); + this.formBuilder?.removeFormItem(this.workspaceDescriptionFormComponent!); + this.formBuilder?.removeFormItem(this.workspaceInputFormComponent!); } // clear selected file textbox this._filePathTextBox!.value = ''; })); + this.createWorkspaceContainer(view); + this.formBuilder = view.modelBuilder.formContainer().withFormItems([ { title: constants.TypeTitle, @@ -150,14 +158,15 @@ export class OpenExistingDialog extends DialogBase { required: true, component: this.createHorizontalContainer(view, [this._filePathTextBox, browseFolderButton]) }, - this.createWorkspaceContainer(view) + this.workspaceDescriptionFormComponent!, + this.workspaceInputFormComponent! ]); await view.initializeModel(this.formBuilder?.component()); this.initDialogComplete?.resolve(); } public async workspaceBrowse(): Promise { - const filters: { [name: string]: string[] } = { [constants.Workspace]: [constants.WorkspaceFileExtension] }; + const filters: { [name: string]: string[] } = { [constants.Workspace]: [constants.WorkspaceFileExtension.substring(1)] }; // filter already adds a period before the extension const fileUris = await vscode.window.showOpenDialog({ canSelectFiles: true, canSelectFolders: false, diff --git a/extensions/data-workspace/src/services/workspaceService.ts b/extensions/data-workspace/src/services/workspaceService.ts index 80583812ab..7eeb0f6898 100644 --- a/extensions/data-workspace/src/services/workspaceService.ts +++ b/extensions/data-workspace/src/services/workspaceService.ts @@ -40,17 +40,16 @@ export class WorkspaceService implements IWorkspaceService { } /** - * Creates a new workspace in the same folder as the project. Because the extension host gets restared when + * Creates a new workspace in the same folder as the project. Because the extension host gets restarted when * a new workspace is created and opened, the project needs to be saved as the temp project that will be loaded * when the extension gets restarted * @param projectFileFsPath project to add to the workspace */ - async CreateNewWorkspaceForProject(projectFileFsPath: string): Promise { + async CreateNewWorkspaceForProject(projectFileFsPath: string, workspaceFile: vscode.Uri | undefined): Promise { // save temp project await this._context.globalState.update(TempProject, [projectFileFsPath]); - // create a new workspace - the workspace file will be created in the same folder as the project - const workspaceFile = vscode.Uri.file(path.join(path.dirname(projectFileFsPath), `${path.parse(projectFileFsPath).name}.code-workspace`)); + // create a new workspace const projectFolder = vscode.Uri.file(path.dirname(projectFileFsPath)); await azdata.workspace.createWorkspace(projectFolder, workspaceFile); } @@ -95,14 +94,14 @@ export class WorkspaceService implements IWorkspaceService { } } - async addProjectsToWorkspace(projectFiles: vscode.Uri[]): Promise { + async addProjectsToWorkspace(projectFiles: vscode.Uri[], workspaceFilePath?: vscode.Uri): Promise { if (!projectFiles || projectFiles.length === 0) { return; } // a workspace needs to be open to add projects if (!vscode.workspace.workspaceFile) { - await this.CreateNewWorkspaceForProject(projectFiles[0].fsPath); + await this.CreateNewWorkspaceForProject(projectFiles[0].fsPath, workspaceFilePath); // this won't get hit since the extension host will get restarted, but helps with testing return; @@ -171,11 +170,11 @@ export class WorkspaceService implements IWorkspaceService { } } - async createProject(name: string, location: vscode.Uri, projectTypeId: string): Promise { + async createProject(name: string, location: vscode.Uri, projectTypeId: string, workspaceFile?: vscode.Uri): Promise { const provider = ProjectProviderRegistry.getProviderByProjectType(projectTypeId); if (provider) { const projectFile = await provider.createProject(name, location, projectTypeId); - this.addProjectsToWorkspace([projectFile]); + this.addProjectsToWorkspace([projectFile], workspaceFile); this._onDidWorkspaceProjectsChange.fire(); return projectFile; } else { diff --git a/extensions/data-workspace/src/test/dialogs/newProjectDialog.test.ts b/extensions/data-workspace/src/test/dialogs/newProjectDialog.test.ts index dbd0495a5a..2329e1973a 100644 --- a/extensions/data-workspace/src/test/dialogs/newProjectDialog.test.ts +++ b/extensions/data-workspace/src/test/dialogs/newProjectDialog.test.ts @@ -24,7 +24,8 @@ suite('New Project Dialog', function (): void { dialog.model.name = 'TestProject'; dialog.model.location = ''; - should.equal(await dialog.validate(), false, 'Validation should fail becausee the parent directory does not exist'); + dialog.workspaceInputBox!.value = 'test.code-workspace'; + should.equal(await dialog.validate(), false, 'Validation should fail because the parent directory does not exist'); // create a folder with the same name const folderPath = path.join(os.tmpdir(), dialog.model.name); @@ -37,6 +38,37 @@ suite('New Project Dialog', function (): void { should.equal(await dialog.validate(), true, 'Validation should pass because name is unique and parent directory exists'); }); + test('Should validate new workspace location', async function (): Promise { + const workspaceServiceMock = TypeMoq.Mock.ofType(); + workspaceServiceMock.setup(x => x.getAllProjectTypes()).returns(() => Promise.resolve([testProjectType])); + + const dialog = new NewProjectDialog(workspaceServiceMock.object); + await dialog.open(); + + dialog.model.name = `TestProject_${new Date().getTime()}`; + dialog.model.location = os.tmpdir(); + dialog.workspaceInputBox!.value = 'test'; + should.equal(await dialog.validate(), false, 'Validation should fail because workspace does not end in .code-workspace'); + + // use invalid folder + dialog.workspaceInputBox!.value = 'invalidLocation/test.code-workspace'; + should.equal(await dialog.validate(), false, 'Validation should fail because the folder is invalid'); + + // use already existing workspace + const existingWorkspaceFilePath = path.join(os.tmpdir(), `${dialog.model.name}.code-workspace`); + await fs.writeFile(existingWorkspaceFilePath, ''); + dialog.workspaceInputBox!.value = existingWorkspaceFilePath; + should.equal(await dialog.validate(), false, 'Validation should fail because the selected workspace file already exists'); + + // same folder as the project should be valid even if the project folder isn't created yet + dialog.workspaceInputBox!.value = path.join(dialog.model.location, dialog.model.name, 'test.code-workspace'); + should.equal(await dialog.validate(), true, 'Validation should pass if the file location is the same folder as the project'); + + // change workspace name to something that should pass + dialog.workspaceInputBox!.value = path.join(os.tmpdir(), `TestWorkspace_${new Date().getTime()}.code-workspace`); + should.equal(await dialog.validate(), true, 'Validation should pass because the parent directory exists, workspace filepath is unique, and the file extension is correct'); + }); + test('Should validate workspace in onComplete', async function (): Promise { const workspaceServiceMock = TypeMoq.Mock.ofType(); workspaceServiceMock.setup(x => x.validateWorkspace()).returns(() => Promise.resolve(true)); diff --git a/extensions/data-workspace/src/test/dialogs/openExistingDialog.test.ts b/extensions/data-workspace/src/test/dialogs/openExistingDialog.test.ts index 4504e49d60..58b39c4937 100644 --- a/extensions/data-workspace/src/test/dialogs/openExistingDialog.test.ts +++ b/extensions/data-workspace/src/test/dialogs/openExistingDialog.test.ts @@ -7,6 +7,8 @@ import * as should from 'should'; import * as TypeMoq from 'typemoq'; import * as sinon from 'sinon'; import * as vscode from 'vscode'; +import * as os from 'os'; +import * as path from 'path'; import * as constants from '../../common/constants'; import { promises as fs } from 'fs'; import { WorkspaceService } from '../../services/workspaceService'; @@ -27,6 +29,8 @@ suite('Open Existing Dialog', function (): void { dialog._targetTypeRadioCardGroup?.updateProperty( 'selectedCardId', constants.Project); dialog._projectFile = ''; + dialog.workspaceInputBox!.value = 'test.code-workspace'; + should.equal(await dialog.validate(), false, 'Validation fail because project file does not exist'); // create a project file @@ -49,6 +53,32 @@ suite('Open Existing Dialog', function (): void { should.equal(await dialog.validate(), true, 'Validation pass because workspace file exists'); }); + test('Should validate new workspace location', async function (): Promise { + const workspaceServiceMock = TypeMoq.Mock.ofType(); + workspaceServiceMock.setup(x => x.getAllProjectTypes()).returns(() => Promise.resolve([testProjectType])); + + const dialog = new OpenExistingDialog(workspaceServiceMock.object, mockExtensionContext.object); + await dialog.open(); + + dialog._projectFile = await createProjectFile('testproj'); + dialog.workspaceInputBox!.value = 'test'; + should.equal(await dialog.validate(), false, 'Validation should fail because workspace does not end in code-workspace'); + + // use invalid folder + dialog.workspaceInputBox!.value = 'invalidLocation/test.code-workspace'; + should.equal(await dialog.validate(), false, 'Validation should fail because the folder is invalid'); + + // use already existing workspace + const existingWorkspaceFilePath = path.join(os.tmpdir(), `test.code-workspace`); + await fs.writeFile(existingWorkspaceFilePath, ''); + dialog.workspaceInputBox!.value = existingWorkspaceFilePath; + should.equal(await dialog.validate(), false, 'Validation should fail because the selected workspace file already exists'); + + // change workspace name to something that should pass + dialog.workspaceInputBox!.value = path.join(os.tmpdir(), `TestWorkspace_${new Date().getTime()}.code-workspace`); + should.equal(await dialog.validate(), true, 'Validation should pass because the parent directory exists, workspace filepath is unique, and the file extension is correct'); + }); + test('Should validate workspace in onComplete when opening project', async function (): Promise { const workspaceServiceMock = TypeMoq.Mock.ofType(); workspaceServiceMock.setup(x => x.validateWorkspace()).returns(() => Promise.resolve(true));