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
This commit is contained in:
Kim Santiago
2020-12-14 13:24:36 -08:00
committed by GitHub
parent c2de462955
commit 1aaf80c3ab
8 changed files with 186 additions and 41 deletions
@@ -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<T> {
resolve: (result: T | Promise<T>) => void;
@@ -18,8 +20,9 @@ export abstract class DialogBase {
protected _dialogObject: azdata.window.Dialog;
protected initDialogComplete: Deferred<void> | undefined;
protected initDialogPromise: Promise<void> = new Promise<void>((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<azdata.TextComponentProperties>({
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<azdata.InputBoxProperties>({
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<azdata.ButtonProperties>({
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<boolean> {
// 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;
}
}
@@ -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<azdata.InputBoxProperties>({
@@ -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<azdata.ButtonProperties>({
@@ -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();
@@ -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<void> {
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,