Load all data workspace projects directly from workspace (#15921)

* Load all projects directly from workspace

* fixes

* Remove relativity and fix tests

* fix compile

* PR comments

* remove unused

* distro
This commit is contained in:
Charles Gagnon
2021-06-30 10:58:34 -07:00
committed by GitHub
parent 66c1fdc457
commit 7ce791d826
30 changed files with 124 additions and 1113 deletions

View File

@@ -5,10 +5,7 @@
import type * as azdataType 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, getAzdataApi, isCurrentWorkspaceUntitled } from '../common/utils';
import { getAzdataApi } from '../common/utils';
interface Deferred<T> {
resolve: (result: T | Promise<T>) => void;
@@ -20,9 +17,6 @@ export abstract class DialogBase {
public dialogObject: azdataType.window.Dialog;
protected initDialogComplete: Deferred<void> | undefined;
protected initDialogPromise: Promise<void> = new Promise<void>((resolve, reject) => this.initDialogComplete = { resolve, reject });
protected workspaceDescriptionFormComponent: azdataType.FormComponent | undefined;
public workspaceInputBox: azdataType.InputBoxComponent | undefined;
protected workspaceInputFormComponent: azdataType.FormComponent | undefined;
constructor(dialogTitle: string, dialogName: string, okButtonText: string, dialogWidth: azdataType.window.DialogWidth = 600) {
this.dialogObject = getAzdataApi()!.window.createModelViewDialog(dialogTitle, dialogName, dialogWidth);
@@ -82,106 +76,4 @@ export abstract class DialogBase {
protected createHorizontalContainer(view: azdataType.ModelView, items: azdataType.Component[]): azdataType.FlexContainer {
return view.modelBuilder.flexContainer().withItems(items, { CSSStyles: { 'margin-right': '5px', 'margin-bottom': '10px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
}
/**
* Creates container with information on which workspace the project will be added to and where the workspace will be
* created if no workspace is currently open
* @param view
*/
protected createWorkspaceContainer(view: azdataType.ModelView): void {
const workspaceDescription = view.modelBuilder.text().withProperties<azdataType.TextComponentProperties>({
value: vscode.workspace.workspaceFile ? constants.AddProjectToCurrentWorkspace : constants.NewWorkspaceWillBeCreated,
CSSStyles: { 'margin-top': '3px', 'margin-bottom': '0px' }
}).component();
const initialWorkspaceInputBoxValue = !!vscode.workspace.workspaceFile && !isCurrentWorkspaceUntitled() ? vscode.workspace.workspaceFile.fsPath : '';
this.workspaceInputBox = view.modelBuilder.inputBox().withProperties<azdataType.InputBoxProperties>({
ariaLabel: constants.WorkspaceLocationTitle,
width: constants.DefaultInputWidth,
enabled: !vscode.workspace.workspaceFile || isCurrentWorkspaceUntitled(), // want it editable if no saved workspace is open
value: initialWorkspaceInputBoxValue,
title: initialWorkspaceInputBoxValue // hovertext for if file path is too long to be seen in textbox
}).component();
const browseFolderButton = view.modelBuilder.button().withProperties<azdataType.ButtonProperties>({
ariaLabel: constants.BrowseButtonText,
iconPath: IconPathHelper.folder,
height: '16px',
width: '18px'
}).component();
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 && !isCurrentWorkspaceUntitled()) {
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: workspaceDescription,
required: true
};
}
/**
* Update the workspace inputbox based on the passed in location and name if there isn't a workspace currently open
* @param location
* @param name
*/
protected updateWorkspaceInputbox(location: string, name: string): void {
if (!vscode.workspace.workspaceFile || isCurrentWorkspaceUntitled()) {
const fileLocation = location && name ? path.join(location, `${name}.code-workspace`) : '';
this.workspaceInputBox!.value = fileLocation;
this.workspaceInputBox!.title = fileLocation;
}
}
public async validateNewWorkspace(sameFolderAsNewProject: boolean): Promise<void> {
// workspace file should end in .code-workspace
const workspaceValid = this.workspaceInputBox!.value!.endsWith(constants.WorkspaceFileExtension);
if (!workspaceValid) {
throw new Error(constants.WorkspaceFileInvalidError(this.workspaceInputBox!.value!));
}
// 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) {
throw new Error(constants.WorkspaceParentDirectoryNotExistError(path.dirname(this.workspaceInputBox!.value!)));
}
}
// workspace file should not be an existing workspace file
const workspaceFileExists = await fileExist(this.workspaceInputBox!.value!);
if (workspaceFileExists) {
throw new Error(constants.WorkspaceFileAlreadyExistsError(this.workspaceInputBox!.value!));
}
}
}

View File

@@ -34,6 +34,9 @@ export class NewProjectDialog extends DialogBase {
}
async validate(): Promise<boolean> {
if (await this.workspaceService.validateWorkspace() === false) {
return false;
}
try {
// the selected location should be an existing directory
const parentDirectoryExists = await directoryExist(this.model.location);
@@ -49,11 +52,6 @@ export class NewProjectDialog extends DialogBase {
return false;
}
if (this.workspaceInputBox!.enabled) {
const sameFolderAsNewProject = path.join(this.model.location, this.model.name) === path.dirname(this.workspaceInputBox!.value!);
await this.validateNewWorkspace(sameFolderAsNewProject);
}
return true;
}
catch (err) {
@@ -64,15 +62,12 @@ export class NewProjectDialog extends DialogBase {
override async onComplete(): Promise<void> {
try {
const validateWorkspace = await this.workspaceService.validateWorkspace();
TelemetryReporter.createActionEvent(TelemetryViews.NewProjectDialog, TelemetryActions.NewProjectDialogCompleted)
.withAdditionalProperties({ projectFileExtension: this.model.projectFileExtension, projectTemplateId: this.model.projectTypeId, workspaceValidationPassed: validateWorkspace.toString() })
.withAdditionalProperties({ projectFileExtension: this.model.projectFileExtension, projectTemplateId: this.model.projectTypeId })
.send();
if (validateWorkspace) {
await this.workspaceService.createProject(this.model.name, vscode.Uri.file(this.model.location), this.model.projectTypeId, vscode.Uri.file(this.workspaceInputBox!.value!));
}
await this.workspaceService.createProject(this.model.name, vscode.Uri.file(this.model.location), this.model.projectTypeId);
}
catch (err) {
@@ -129,8 +124,6 @@ export class NewProjectDialog extends DialogBase {
this.register(projectNameTextBox.onTextChanged(() => {
this.model.name = projectNameTextBox.value!;
projectNameTextBox.updateProperty('title', projectNameTextBox.value);
this.updateWorkspaceInputbox(path.join(this.model.location, this.model.name), this.model.name);
}));
const locationTextBox = view.modelBuilder.inputBox().withProperties<azdataType.InputBoxProperties>({
@@ -143,7 +136,6 @@ export class NewProjectDialog extends DialogBase {
this.register(locationTextBox.onTextChanged(() => {
this.model.location = locationTextBox.value!;
locationTextBox.updateProperty('title', locationTextBox.value);
this.updateWorkspaceInputbox(path.join(this.model.location, this.model.name), this.model.name);
}));
const browseFolderButton = view.modelBuilder.button().withProperties<azdataType.ButtonProperties>({
@@ -165,12 +157,8 @@ export class NewProjectDialog extends DialogBase {
const selectedFolder = folderUris[0].fsPath;
locationTextBox.value = selectedFolder;
this.model.location = selectedFolder;
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,
@@ -185,9 +173,7 @@ export class NewProjectDialog extends DialogBase {
title: constants.ProjectLocationTitle,
required: true,
component: this.createHorizontalContainer(view, [locationTextBox, browseFolderButton])
},
this.workspaceDescriptionFormComponent!,
this.workspaceInputFormComponent!
}
]).component();
await view.initializeModel(form);
this.initDialogComplete?.resolve();

View File

@@ -5,17 +5,15 @@
import type * as azdataType from 'azdata';
import * as vscode from 'vscode';
import * as path from 'path';
import { DialogBase } from './dialogBase';
import * as constants from '../common/constants';
import { IWorkspaceService } from '../common/interfaces';
import { directoryExist, fileExist } from '../common/utils';
import { IconPathHelper } from '../common/iconHelper';
import { calculateRelativity, TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry';
import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry';
import { defaultProjectSaveLocation } from '../common/projectLocationHelper';
export class OpenExistingDialog extends DialogBase {
public targetTypeRadioCardGroup: azdataType.RadioCardGroupComponent | undefined;
public filePathTextBox: azdataType.InputBoxComponent | undefined;
public filePathAndButtonComponent: azdataType.FormComponent | undefined;
public gitRepoTextBoxComponent: azdataType.FormComponent | undefined;
@@ -26,17 +24,7 @@ export class OpenExistingDialog extends DialogBase {
public locationRadioButtonFormComponent: azdataType.FormComponent | undefined;
public formBuilder: azdataType.FormBuilder | undefined;
private _targetTypes = [
{
name: constants.Project,
icon: this.extensionContext.asAbsolutePath('images/Open_existing_Project.svg')
}, {
name: constants.Workspace,
icon: this.extensionContext.asAbsolutePath('images/Open_existing_Workspace.svg')
}
];
constructor(private workspaceService: IWorkspaceService, private extensionContext: vscode.ExtensionContext) {
constructor(private workspaceService: IWorkspaceService) {
super(constants.OpenExistingDialogTitle, 'OpenProject', constants.OpenButtonText);
// dialog launched from Welcome message button (only visible when no current workspace) vs. "add project" button
@@ -47,27 +35,15 @@ export class OpenExistingDialog extends DialogBase {
async validate(): Promise<boolean> {
try {
// the selected location should be an existing directory
if (this.targetTypeRadioCardGroup?.selectedCardId === constants.Project) {
if (this.localRadioButton?.checked) {
await this.validateFile(this.filePathTextBox!.value!, constants.Project.toLowerCase());
} else {
await this.validateClonePath(<string>this.localClonePathTextBox!.value);
}
if (this.workspaceInputBox!.enabled) {
await this.validateNewWorkspace(false);
}
} 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);
}
if (await this.workspaceService.validateWorkspace() === false) {
return false;
}
if (this.localRadioButton?.checked) {
await this.validateFile(this.filePathTextBox!.value!, constants.Project.toLowerCase());
} else {
await this.validateClonePath(<string>this.localClonePathTextBox!.value);
}
return true;
}
catch (err) {
@@ -94,55 +70,27 @@ export class OpenExistingDialog extends DialogBase {
override async onComplete(): Promise<void> {
try {
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() })
// save datapoint now because it'll get set to new value during validateWorkspace()
const telemetryProps: any = { hasWorkspaceOpen: (vscode.workspace.workspaceFile !== undefined).toString() };
let addProjectsPromise: Promise<void>;
if (this.remoteGitRepoRadioButton!.checked) {
TelemetryReporter.createActionEvent(TelemetryViews.OpenExistingDialog, TelemetryActions.GitClone)
.withAdditionalProperties({ selectedTarget: 'project' })
.send();
if (this.remoteGitRepoRadioButton!.checked) {
TelemetryReporter.createActionEvent(TelemetryViews.OpenExistingDialog, TelemetryActions.GitClone)
.withAdditionalProperties({ selectedTarget: 'workspace' })
.send();
// show git output channel
vscode.commands.executeCommand('git.showOutput');
// after this executes, the git extension will show a popup asking if you want to enter the workspace
await vscode.commands.executeCommand('git.clone', (<azdataType.InputBoxComponent>this.gitRepoTextBoxComponent?.component).value, this.localClonePathTextBox!.value);
} else {
await this.workspaceService.enterWorkspace(vscode.Uri.file(this.filePathTextBox!.value!));
}
addProjectsPromise = this.workspaceService.gitCloneProject((<azdataType.InputBoxComponent>this.gitRepoTextBoxComponent?.component).value!, this.localClonePathTextBox!.value!);
} else {
// save datapoint now because it'll get set to new value during validateWorkspace()
const telemetryProps: any = { hasWorkspaceOpen: (vscode.workspace.workspaceFile !== undefined).toString() };
const validateWorkspace = await this.workspaceService.validateWorkspace();
let addProjectsPromise: Promise<void>;
if (this.remoteGitRepoRadioButton!.checked) {
TelemetryReporter.createActionEvent(TelemetryViews.OpenExistingDialog, TelemetryActions.GitClone)
.withAdditionalProperties({ selectedTarget: 'project' })
.send();
addProjectsPromise = this.workspaceService.gitCloneProject((<azdataType.InputBoxComponent>this.gitRepoTextBoxComponent?.component).value!, this.localClonePathTextBox!.value!, vscode.Uri.file(this.workspaceInputBox!.value!));
} else {
if (validateWorkspace) {
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!));
} else {
telemetryProps.workspaceProjectRelativity = 'none';
telemetryProps.cancelled = 'true';
addProjectsPromise = this.workspaceService.addProjectsToWorkspace([vscode.Uri.file(this.filePathTextBox!.value!)], vscode.Uri.file(this.workspaceInputBox!.value!));
}
}
TelemetryReporter.createActionEvent(TelemetryViews.OpenExistingDialog, TelemetryActions.OpeningProject)
.withAdditionalProperties(telemetryProps)
.send();
await addProjectsPromise;
telemetryProps.cancelled = 'false';
addProjectsPromise = this.workspaceService.addProjectsToWorkspace([vscode.Uri.file(this.filePathTextBox!.value!)]);
}
TelemetryReporter.createActionEvent(TelemetryViews.OpenExistingDialog, TelemetryActions.OpeningProject)
.withAdditionalProperties(telemetryProps)
.send();
await addProjectsPromise;
}
catch (err) {
vscode.window.showErrorMessage(err?.message ? err.message : err);
@@ -150,32 +98,6 @@ export class OpenExistingDialog extends DialogBase {
}
protected async initialize(view: azdataType.ModelView): Promise<void> {
this.targetTypeRadioCardGroup = view.modelBuilder.radioCardGroup().withProperties<azdataType.RadioCardGroupComponentProperties>({
cards: this._targetTypes.map((targetType) => {
return <azdataType.RadioCard>{
id: targetType.name,
label: targetType.name,
icon: targetType.icon,
descriptions: [
{
textValue: targetType.name,
textStyles: {
'font-size': '13px'
}
}
]
};
}),
iconHeight: '100px',
iconWidth: '100px',
cardWidth: '170px',
cardHeight: '170px',
ariaLabel: constants.TypeTitle,
width: '500px',
iconPosition: 'top',
selectedCardId: constants.Project
}).component();
this.localRadioButton = view.modelBuilder.radioButton().withProperties<azdataType.RadioButtonProperties>({
name: 'location',
label: constants.Local,
@@ -186,7 +108,7 @@ export class OpenExistingDialog extends DialogBase {
if (checked) {
this.formBuilder?.removeFormItem(<azdataType.FormComponent>this.gitRepoTextBoxComponent);
this.formBuilder?.removeFormItem(<azdataType.FormComponent>this.localClonePathComponent);
this.formBuilder?.insertFormItem(<azdataType.FormComponent>this.filePathAndButtonComponent, 2);
this.formBuilder?.insertFormItem(<azdataType.FormComponent>this.filePathAndButtonComponent, 1);
}
}));
@@ -207,8 +129,8 @@ export class OpenExistingDialog extends DialogBase {
this.register(this.remoteGitRepoRadioButton.onDidChangeCheckedState(checked => {
if (checked) {
this.formBuilder?.removeFormItem(<azdataType.FormComponent>this.filePathAndButtonComponent);
this.formBuilder?.insertFormItem(<azdataType.FormComponent>this.gitRepoTextBoxComponent, 2);
this.formBuilder?.insertFormItem(<azdataType.FormComponent>this.localClonePathComponent, 3);
this.formBuilder?.insertFormItem(<azdataType.FormComponent>this.gitRepoTextBoxComponent, 1);
this.formBuilder?.insertFormItem(<azdataType.FormComponent>this.localClonePathComponent, 2);
}
}));
@@ -221,7 +143,6 @@ export class OpenExistingDialog extends DialogBase {
this.register(gitRepoTextBox.onTextChanged(() => {
gitRepoTextBox.updateProperty('title', this.localClonePathTextBox!.value!);
this.updateWorkspaceInputbox(this.localClonePathTextBox!.value!, path.basename(gitRepoTextBox!.value!, '.git'));
}));
this.gitRepoTextBoxComponent = {
@@ -238,7 +159,6 @@ export class OpenExistingDialog extends DialogBase {
this.register(this.localClonePathTextBox.onTextChanged(() => {
this.localClonePathTextBox!.updateProperty('title', this.localClonePathTextBox!.value!);
this.updateWorkspaceInputbox(this.localClonePathTextBox!.value!, path.basename(gitRepoTextBox!.value!, '.git'));
}));
const localClonePathBrowseFolderButton = view.modelBuilder.button().withProperties<azdataType.ButtonProperties>({
@@ -262,7 +182,6 @@ export class OpenExistingDialog extends DialogBase {
const selectedFolder = folderUris[0].fsPath;
this.localClonePathTextBox!.value = selectedFolder;
this.localClonePathTextBox!.updateProperty('title', this.localClonePathTextBox!.value);
this.updateWorkspaceInputbox(path.dirname(this.localClonePathTextBox!.value!), path.basename((<azdataType.InputBoxComponent>this.gitRepoTextBoxComponent?.component)!.value!, '.git'));
}));
this.localClonePathComponent = {
@@ -280,7 +199,6 @@ export class OpenExistingDialog extends DialogBase {
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<azdataType.ButtonProperties>({
@@ -290,11 +208,7 @@ export class OpenExistingDialog extends DialogBase {
height: '16px'
}).component();
this.register(localProjectBrowseFolderButton.onDidClick(async () => {
if (this.targetTypeRadioCardGroup?.selectedCardId === constants.Project) {
await this.projectBrowse();
} else if (this.targetTypeRadioCardGroup?.selectedCardId === constants.Workspace) {
await this.workspaceBrowse();
}
await this.projectBrowse();
}));
const flexContainer = this.createHorizontalContainer(view, [this.filePathTextBox, localProjectBrowseFolderButton]);
@@ -303,74 +217,14 @@ export class OpenExistingDialog extends DialogBase {
component: flexContainer
};
this.register(this.targetTypeRadioCardGroup.onSelectionChanged(({ cardId }) => {
if (cardId === constants.Project) {
this.filePathTextBox!.placeHolder = constants.ProjectFilePlaceholder;
if (this.remoteGitRepoRadioButton!.checked) {
this.formBuilder?.removeFormItem(<azdataType.FormComponent>this.filePathAndButtonComponent);
this.formBuilder?.insertFormItem(<azdataType.FormComponent>this.gitRepoTextBoxComponent, 2);
this.formBuilder?.insertFormItem(<azdataType.FormComponent>this.localClonePathComponent, 3);
} else {
this.formBuilder?.removeFormItem(<azdataType.FormComponent>this.gitRepoTextBoxComponent);
this.formBuilder?.removeFormItem(<azdataType.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.formBuilder?.removeFormItem(this.workspaceDescriptionFormComponent!);
this.formBuilder?.removeFormItem(this.workspaceInputFormComponent!);
if (this.remoteGitRepoRadioButton!.checked) {
this.formBuilder?.removeFormItem(<azdataType.FormComponent>this.filePathAndButtonComponent);
this.formBuilder?.insertFormItem(<azdataType.FormComponent>this.gitRepoTextBoxComponent, 2);
this.formBuilder?.insertFormItem(<azdataType.FormComponent>this.localClonePathComponent, 3);
}
}
// clear selected file textbox
this.filePathTextBox!.value = '';
}));
this.createWorkspaceContainer(view);
this.formBuilder = view.modelBuilder.formContainer().withFormItems([
{
title: constants.TypeTitle,
required: true,
component: this.targetTypeRadioCardGroup,
},
this.locationRadioButtonFormComponent,
this.filePathAndButtonComponent,
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.substring(1)] }; // filter already adds a period before the extension
const fileUris = await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
openLabel: constants.SelectProjectFileActionName,
filters: filters
});
if (!fileUris || fileUris.length === 0) {
return;
}
const workspaceFilePath = fileUris[0].fsPath;
this.filePathTextBox!.value = workspaceFilePath;
}
public async projectBrowse(): Promise<void> {
const filters: { [name: string]: string[] } = {};
const projectTypes = await this.workspaceService.getAllProjectTypes();