Add git clone option for opening existing workspace (#14828)

* added git option to dialog

* Add validation for cloning workspace and hide radio buttons for project

* add test

* cleanup
This commit is contained in:
Kim Santiago
2021-03-26 15:30:29 -07:00
committed by GitHub
parent bbee4a1f38
commit ca19f08582
4 changed files with 215 additions and 51 deletions

View File

@@ -9,13 +9,21 @@ import * as path from 'path';
import { DialogBase } from './dialogBase';
import * as constants from '../common/constants';
import { IWorkspaceService } from '../common/interfaces';
import { fileExist } from '../common/utils';
import { directoryExist, fileExist } from '../common/utils';
import { IconPathHelper } from '../common/iconHelper';
import { calculateRelativity, TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry';
import { defaultProjectSaveLocation } from '../common/projectLocationHelper';
export class OpenExistingDialog extends DialogBase {
public _targetTypeRadioCardGroup: azdata.RadioCardGroupComponent | undefined;
public _filePathTextBox: azdata.InputBoxComponent | undefined;
public targetTypeRadioCardGroup: azdata.RadioCardGroupComponent | undefined;
public filePathTextBox: azdata.InputBoxComponent | undefined;
public filePathAndButtonComponent: azdata.FormComponent | undefined;
public gitRepoTextBoxComponent: azdata.FormComponent | undefined;
public localClonePathComponent: azdata.FormComponent | undefined;
public localClonePathTextBox: azdata.InputBoxComponent | undefined;
public localRadioButton: azdata.RadioButtonComponent | undefined;
public remoteGitRepoRadioButton: azdata.RadioButtonComponent | undefined;
public locationRadioButtonFormComponent: azdata.FormComponent | undefined;
public formBuilder: azdata.FormBuilder | undefined;
private _targetTypes = [
@@ -40,14 +48,20 @@ export class OpenExistingDialog extends DialogBase {
async validate(): Promise<boolean> {
try {
// the selected location should be an existing directory
if (this._targetTypeRadioCardGroup?.selectedCardId === constants.Project) {
await this.validateFile(this._filePathTextBox!.value!, constants.Project.toLowerCase());
if (this.targetTypeRadioCardGroup?.selectedCardId === constants.Project) {
await this.validateFile(this.filePathTextBox!.value!, constants.Project.toLowerCase());
if (this.workspaceInputBox!.enabled) {
await this.validateNewWorkspace(false);
}
} else if (this._targetTypeRadioCardGroup?.selectedCardId === constants.Workspace) {
await this.validateFile(this._filePathTextBox!.value!, constants.Workspace.toLowerCase());
} else if (this.targetTypeRadioCardGroup?.selectedCardId === constants.Workspace) {
if (this.localRadioButton?.checked) {
await this.validateFile(this.filePathTextBox!.value!, constants.Workspace.toLowerCase());
} else {
// validate clone location
// check if parent folder exists
await this.validateClonePath(<string>this.localClonePathTextBox!.value);
}
}
return true;
@@ -65,15 +79,33 @@ export class OpenExistingDialog extends DialogBase {
}
}
public async validateClonePath(location: string): Promise<void> {
// only need to check if parent directory exists
// if the same repo has been cloned before, the git clone will append the next number to the folder
const parentDirectoryExists = await directoryExist(location);
if (!parentDirectoryExists) {
throw new Error(constants.CloneParentDirectoryNotExistError(location));
}
}
async onComplete(): Promise<void> {
try {
if (this._targetTypeRadioCardGroup?.selectedCardId === constants.Workspace) {
if (this.targetTypeRadioCardGroup?.selectedCardId === constants.Workspace) {
// capture that workspace was selected, also if there's already an open workspace that's being replaced
TelemetryReporter.createActionEvent(TelemetryViews.OpenExistingDialog, TelemetryActions.OpeningWorkspace)
.withAdditionalProperties({ hasWorkspaceOpen: (vscode.workspace.workspaceFile !== undefined).toString() })
.send();
await this.workspaceService.enterWorkspace(vscode.Uri.file(this._filePathTextBox!.value!));
if (this.remoteGitRepoRadioButton!.checked) {
TelemetryReporter.createActionEvent(TelemetryViews.OpenExistingDialog, TelemetryActions.GitClone)
.withAdditionalProperties({ selectedTarget: 'workspace' })
.send();
// after this executes, the git extension will show a popup asking if you want to enter the workspace
await vscode.commands.executeCommand('git.clone', (<azdata.InputBoxComponent>this.gitRepoTextBoxComponent?.component).value, this.localClonePathTextBox!.value);
} else {
await this.workspaceService.enterWorkspace(vscode.Uri.file(this.filePathTextBox!.value!));
}
} else {
// save datapoint now because it'll get set to new value during validateWorkspace()
const telemetryProps: any = { hasWorkspaceOpen: (vscode.workspace.workspaceFile !== undefined).toString() };
@@ -82,13 +114,13 @@ export class OpenExistingDialog extends DialogBase {
let addProjectsPromise: Promise<void>;
if (validateWorkspace) {
telemetryProps.workspaceProjectRelativity = calculateRelativity(this._filePathTextBox!.value!, this.workspaceInputBox!.value!);
telemetryProps.workspaceProjectRelativity = calculateRelativity(this.filePathTextBox!.value!, this.workspaceInputBox!.value!);
telemetryProps.cancelled = 'false';
addProjectsPromise = this.workspaceService.addProjectsToWorkspace([vscode.Uri.file(this._filePathTextBox!.value!)], vscode.Uri.file(this.workspaceInputBox!.value!));
addProjectsPromise = this.workspaceService.addProjectsToWorkspace([vscode.Uri.file(this.filePathTextBox!.value!)], vscode.Uri.file(this.workspaceInputBox!.value!));
} else {
telemetryProps.workspaceProjectRelativity = 'none';
telemetryProps.cancelled = 'true';
addProjectsPromise = this.workspaceService.addProjectsToWorkspace([vscode.Uri.file(this._filePathTextBox!.value!)], vscode.Uri.file(this.workspaceInputBox!.value!));
addProjectsPromise = this.workspaceService.addProjectsToWorkspace([vscode.Uri.file(this.filePathTextBox!.value!)], vscode.Uri.file(this.workspaceInputBox!.value!));
}
TelemetryReporter.createActionEvent(TelemetryViews.OpenExistingDialog, TelemetryActions.OpeningProject)
@@ -104,7 +136,7 @@ export class OpenExistingDialog extends DialogBase {
}
protected async initialize(view: azdata.ModelView): Promise<void> {
this._targetTypeRadioCardGroup = view.modelBuilder.radioCardGroup().withProperties<azdata.RadioCardGroupComponentProperties>({
this.targetTypeRadioCardGroup = view.modelBuilder.radioCardGroup().withProperties<azdata.RadioCardGroupComponentProperties>({
cards: this._targetTypes.map((targetType) => {
return <azdata.RadioCard>{
id: targetType.name,
@@ -130,44 +162,148 @@ export class OpenExistingDialog extends DialogBase {
selectedCardId: constants.Project
}).component();
this._filePathTextBox = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
ariaLabel: constants.LocationSelectorTitle,
placeHolder: constants.ProjectFilePlaceholder,
this.localRadioButton = view.modelBuilder.radioButton().withProperties<azdata.RadioButtonProperties>({
name: 'location',
label: constants.Local,
checked: true
}).component();
this.register(this.localRadioButton.onDidChangeCheckedState(checked => {
if (checked) {
this.formBuilder?.removeFormItem(<azdata.FormComponent>this.gitRepoTextBoxComponent);
this.formBuilder?.removeFormItem(<azdata.FormComponent>this.localClonePathComponent);
this.formBuilder?.insertFormItem(<azdata.FormComponent>this.filePathAndButtonComponent, 2);
}
}));
this.remoteGitRepoRadioButton = view.modelBuilder.radioButton().withProperties<azdata.RadioButtonProperties>({
name: 'location',
label: constants.RemoteGitRepo
}).component();
this.locationRadioButtonFormComponent = {
title: constants.LocationSelectorTitle,
required: true,
component: view.modelBuilder.flexContainer()
.withItems([this.localRadioButton, this.remoteGitRepoRadioButton], { flex: '0 0 auto', CSSStyles: { 'margin-right': '15px' } })
.withProperties({ ariaRole: 'radiogroup' })
.component()
};
this.register(this.remoteGitRepoRadioButton.onDidChangeCheckedState(checked => {
if (checked) {
this.formBuilder?.removeFormItem(<azdata.FormComponent>this.filePathAndButtonComponent);
this.formBuilder?.insertFormItem(<azdata.FormComponent>this.gitRepoTextBoxComponent, 2);
this.formBuilder?.insertFormItem(<azdata.FormComponent>this.localClonePathComponent, 3);
}
}));
this.gitRepoTextBoxComponent = {
title: constants.GitRepoUrlTitle,
component: view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
ariaLabel: constants.GitRepoUrlTitle,
placeHolder: constants.GitRepoUrlPlaceholder,
required: true,
width: constants.DefaultInputWidth
}).component()
};
this.localClonePathTextBox = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
ariaLabel: constants.LocalClonePathTitle,
placeHolder: constants.LocalClonePathPlaceholder,
required: true,
width: constants.DefaultInputWidth
}).component();
this.register(this._filePathTextBox.onTextChanged(() => {
this._filePathTextBox!.updateProperty('title', this._filePathTextBox!.value!);
this.updateWorkspaceInputbox(path.dirname(this._filePathTextBox!.value!), path.basename(this._filePathTextBox!.value!, path.extname(this._filePathTextBox!.value!)));
}));
const browseFolderButton = view.modelBuilder.button().withProperties<azdata.ButtonProperties>({
const localClonePathBrowseFolderButton = view.modelBuilder.button().withProperties<azdata.ButtonProperties>({
ariaLabel: constants.BrowseButtonText,
iconPath: IconPathHelper.folder,
width: '18px',
height: '16px',
}).component();
this.register(browseFolderButton.onDidClick(async () => {
if (this._targetTypeRadioCardGroup?.selectedCardId === constants.Project) {
this.register(localClonePathBrowseFolderButton.onDidClick(async () => {
const folderUris = await vscode.window.showOpenDialog({
canSelectFiles: false,
canSelectFolders: true,
canSelectMany: false,
defaultUri: defaultProjectSaveLocation()
});
if (!folderUris || folderUris.length === 0) {
return;
}
const selectedFolder = folderUris[0].fsPath;
this.localClonePathTextBox!.value = selectedFolder;
}));
this.localClonePathComponent = {
title: constants.LocalClonePathTitle,
component: this.createHorizontalContainer(view, [this.localClonePathTextBox, localClonePathBrowseFolderButton]),
required: true
};
this.filePathTextBox = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
ariaLabel: constants.LocationSelectorTitle,
placeHolder: constants.ProjectFilePlaceholder,
required: true,
width: constants.DefaultInputWidth
}).component();
this.register(this.filePathTextBox.onTextChanged(() => {
this.filePathTextBox!.updateProperty('title', this.filePathTextBox!.value!);
this.updateWorkspaceInputbox(path.dirname(this.filePathTextBox!.value!), path.basename(this.filePathTextBox!.value!, path.extname(this.filePathTextBox!.value!)));
}));
const localProjectBrowseFolderButton = view.modelBuilder.button().withProperties<azdata.ButtonProperties>({
ariaLabel: constants.BrowseButtonText,
iconPath: IconPathHelper.folder,
width: '18px',
height: '16px'
}).component();
this.register(localProjectBrowseFolderButton.onDidClick(async () => {
if (this.targetTypeRadioCardGroup?.selectedCardId === constants.Project) {
await this.projectBrowse();
} else if (this._targetTypeRadioCardGroup?.selectedCardId === constants.Workspace) {
} else if (this.targetTypeRadioCardGroup?.selectedCardId === constants.Workspace) {
await this.workspaceBrowse();
}
}));
this.register(this._targetTypeRadioCardGroup.onSelectionChanged(({ cardId }) => {
const flexContainer = this.createHorizontalContainer(view, [this.filePathTextBox, localProjectBrowseFolderButton]);
flexContainer.updateCssStyles({ 'margin-top': '-10px' });
this.filePathAndButtonComponent = {
component: flexContainer
};
this.register(this.targetTypeRadioCardGroup.onSelectionChanged(({ cardId }) => {
if (cardId === constants.Project) {
this._filePathTextBox!.placeHolder = constants.ProjectFilePlaceholder;
this.filePathTextBox!.placeHolder = constants.ProjectFilePlaceholder;
// hide these two radio buttons for now since git clone is just for workspaces
this.localRadioButton?.updateCssStyles({ 'display': 'none' });
this.remoteGitRepoRadioButton?.updateCssStyles({ 'display': 'none' });
this.formBuilder?.removeFormItem(<azdata.FormComponent>this.gitRepoTextBoxComponent);
this.formBuilder?.removeFormItem(<azdata.FormComponent>this.localClonePathComponent);
this.formBuilder?.addFormItem(this.filePathAndButtonComponent!);
this.formBuilder?.addFormItem(this.workspaceDescriptionFormComponent!);
this.formBuilder?.addFormItem(this.workspaceInputFormComponent!);
} else if (cardId === constants.Workspace) {
this._filePathTextBox!.placeHolder = constants.WorkspacePlaceholder;
this.filePathTextBox!.placeHolder = constants.WorkspacePlaceholder;
this.localRadioButton?.updateCssStyles({ 'display': 'block' });
this.remoteGitRepoRadioButton?.updateCssStyles({ 'display': 'block' });
this.formBuilder?.removeFormItem(this.workspaceDescriptionFormComponent!);
this.formBuilder?.removeFormItem(this.workspaceInputFormComponent!);
if (this.remoteGitRepoRadioButton!.checked) {
this.formBuilder?.removeFormItem(<azdata.FormComponent>this.filePathAndButtonComponent);
this.formBuilder?.insertFormItem(<azdata.FormComponent>this.gitRepoTextBoxComponent, 2);
this.formBuilder?.insertFormItem(<azdata.FormComponent>this.localClonePathComponent, 3);
}
}
// clear selected file textbox
this._filePathTextBox!.value = '';
this.filePathTextBox!.value = '';
}));
this.createWorkspaceContainer(view);
@@ -176,12 +312,10 @@ export class OpenExistingDialog extends DialogBase {
{
title: constants.TypeTitle,
required: true,
component: this._targetTypeRadioCardGroup,
}, {
title: constants.LocationSelectorTitle,
required: true,
component: this.createHorizontalContainer(view, [this._filePathTextBox, browseFolderButton])
component: this.targetTypeRadioCardGroup,
},
this.locationRadioButtonFormComponent,
this.filePathAndButtonComponent,
this.workspaceDescriptionFormComponent!,
this.workspaceInputFormComponent!
]);
@@ -204,7 +338,7 @@ export class OpenExistingDialog extends DialogBase {
}
const workspaceFilePath = fileUris[0].fsPath;
this._filePathTextBox!.value = workspaceFilePath;
this.filePathTextBox!.value = workspaceFilePath;
}
public async projectBrowse(): Promise<void> {
@@ -228,6 +362,6 @@ export class OpenExistingDialog extends DialogBase {
}
const projectFilePath = fileUris[0].fsPath;
this._filePathTextBox!.value = projectFilePath;
this.filePathTextBox!.value = projectFilePath;
}
}