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

@@ -153,16 +153,9 @@ export const projectLocationLabel = localize('projectLocationLabel', "Location")
export const projectLocationPlaceholderText = localize('projectLocationPlaceholderText', "Select location to create project");
export const browseButtonText = localize('browseButtonText', "Browse folder");
export const folderStructureLabel = localize('folderStructureLabel', "Folder structure");
export const addProjectToCurrentWorkspace = localize('addProjectToCurrentWorkspace', "This project will be added to the current workspace.");
export const newWorkspaceWillBeCreated = localize('newWorkspaceWillBeCreated', "A new workspace will be created for this project.");
export const workspaceLocationTitle = localize('workspaceLocationTitle', "Workspace location");
export const workspace = localize('workspace', "Workspace");
export const WorkspaceFileExtension = '.code-workspace';
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); };
// Error messages

View File

@@ -241,7 +241,7 @@ export async function getSqlProjectFilesInFolder(folderPath: string): Promise<st
/**
* Get all the projects in the workspace that are sqlproj
*/
export function getSqlProjectsInWorkspace(): vscode.Uri[] {
export function getSqlProjectsInWorkspace(): Promise<vscode.Uri[]> {
const api = getDataWorkspaceExtensionApi();
return api.getProjectsInWorkspace(constants.sqlprojExtension);
}
@@ -251,13 +251,6 @@ export function getDataWorkspaceExtensionApi(): dataworkspace.IExtension {
return extension.exports;
}
/**
* if the current workspace is untitled, the returned URI of vscode.workspace.workspaceFile will use the `untitled` scheme
*/
export function isCurrentWorkspaceUntitled(): boolean {
return !!vscode.workspace.workspaceFile && vscode.workspace.workspaceFile.scheme.toLowerCase() === 'untitled';
}
/*
* Returns the default deployment options from DacFx
*/

View File

@@ -664,7 +664,7 @@ export class ProjectsController {
if ((<IProjectReferenceSettings>settings).projectName !== undefined) {
// get project path and guid
const projectReferenceSettings = settings as IProjectReferenceSettings;
const workspaceProjects = utils.getSqlProjectsInWorkspace();
const workspaceProjects = await utils.getSqlProjectsInWorkspace();
const referencedProject = await Project.openProject(workspaceProjects.filter(p => path.parse(p.fsPath).name === projectReferenceSettings.projectName)[0].fsPath);
const relativePath = path.relative(project.projectFolderPath, referencedProject?.projectFilePath!);
projectReferenceSettings.projectRelativePath = vscode.Uri.file(relativePath);
@@ -882,7 +882,7 @@ export class ProjectsController {
// add project to workspace
workspaceApi.showProjectsView();
await workspaceApi.addProjectsToWorkspace([vscode.Uri.file(newProjFilePath)], model.newWorkspaceFilePath);
await workspaceApi.addProjectsToWorkspace([vscode.Uri.file(newProjFilePath)]);
}
} catch (err) {
vscode.window.showErrorMessage(utils.getErrorMessage(err));

View File

@@ -293,7 +293,7 @@ export class AddDatabaseReferenceDialog {
});
// get projects in workspace and filter to only sql projects
let projectFiles: vscode.Uri[] = utils.getSqlProjectsInWorkspace();
let projectFiles: vscode.Uri[] = await utils.getSqlProjectsInWorkspace();
// filter out current project
projectFiles = projectFiles.filter(p => p.fsPath !== this.project.projectFilePath);

View File

@@ -15,7 +15,7 @@ import { cssStyles } from '../common/uiConstants';
import { ImportDataModel } from '../models/api/import';
import { Deferred } from '../common/promise';
import { getConnectionName } from './utils';
import { exists, getAzdataApi, isCurrentWorkspaceUntitled } from '../common/utils';
import { exists, getAzdataApi, getDataWorkspaceExtensionApi } from '../common/utils';
export class CreateProjectFromDatabaseDialog {
public dialog: azdataType.window.Dialog;
@@ -26,7 +26,6 @@ export class CreateProjectFromDatabaseDialog {
public projectNameTextBox: azdataType.InputBoxComponent | undefined;
public projectLocationTextBox: azdataType.InputBoxComponent | undefined;
public folderStructureDropDown: azdataType.DropDownComponent | undefined;
public workspaceInputBox: azdataType.InputBoxComponent | undefined;
private formBuilder: azdataType.FormBuilder | undefined;
private connectionId: string | undefined;
private toDispose: vscode.Disposable[] = [];
@@ -87,10 +86,6 @@ export class CreateProjectFromDatabaseDialog {
const createProjectSettingsFormSection = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
createProjectSettingsFormSection.addItems([folderStructureRow]);
const workspaceContainerRow = this.createWorkspaceContainerRow(view);
const createworkspaceContainerFormSection = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
createworkspaceContainerFormSection.addItems([workspaceContainerRow]);
this.formBuilder = <azdataType.FormBuilder>view.modelBuilder.formContainer()
.withFormItems([
{
@@ -116,14 +111,6 @@ export class CreateProjectFromDatabaseDialog {
component: createProjectSettingsFormSection,
}
]
},
{
title: constants.workspace,
components: [
{
component: createworkspaceContainerFormSection,
}
]
}
], {
horizontal: false,
@@ -166,7 +153,6 @@ export class CreateProjectFromDatabaseDialog {
this.sourceDatabaseDropDown.onValueChanged(() => {
this.setProjectName();
this.updateWorkspaceInputbox(path.join(this.projectLocationTextBox!.value!, this.projectNameTextBox!.value!), this.projectNameTextBox!.value!);
this.tryEnableCreateButton();
});
@@ -264,7 +250,6 @@ export class CreateProjectFromDatabaseDialog {
this.projectNameTextBox.onTextChanged(() => {
this.projectNameTextBox!.value = this.projectNameTextBox!.value?.trim();
this.projectNameTextBox!.updateProperty('title', this.projectNameTextBox!.value);
this.updateWorkspaceInputbox(path.join(this.projectLocationTextBox!.value!, this.projectNameTextBox!.value!), this.projectNameTextBox!.value!);
this.tryEnableCreateButton();
});
@@ -291,7 +276,6 @@ export class CreateProjectFromDatabaseDialog {
this.projectLocationTextBox.onTextChanged(() => {
this.projectLocationTextBox!.updateProperty('title', this.projectLocationTextBox!.value);
this.updateWorkspaceInputbox(path.join(this.projectLocationTextBox!.value!, this.projectNameTextBox!.value!), this.projectNameTextBox!.value!);
this.tryEnableCreateButton();
});
@@ -329,7 +313,6 @@ export class CreateProjectFromDatabaseDialog {
this.projectLocationTextBox!.value = folderUris[0].fsPath;
this.projectLocationTextBox!.updateProperty('title', folderUris[0].fsPath);
this.updateWorkspaceInputbox(path.join(this.projectLocationTextBox!.value!, this.projectNameTextBox!.value!), this.projectNameTextBox!.value!);
});
return browseFolderButton;
@@ -359,80 +342,6 @@ export class CreateProjectFromDatabaseDialog {
return folderStructureRow;
}
/**
* 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
*/
private createWorkspaceContainerRow(view: azdataType.ModelView): azdataType.FlexContainer {
const initialWorkspaceInputBoxValue = !!vscode.workspace.workspaceFile && !isCurrentWorkspaceUntitled() ? vscode.workspace.workspaceFile.fsPath : '';
this.workspaceInputBox = view.modelBuilder.inputBox().withProps({
ariaLabel: constants.workspaceLocationTitle,
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
width: '100%'
}).component();
const browseFolderButton = view.modelBuilder.button().withProperties<azdataType.ButtonProperties>({
ariaLabel: constants.browseButtonText,
iconPath: IconPathHelper.folder_blue,
height: '16px',
width: '18px'
}).component();
this.toDispose.push(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;
}));
const workspaceLabel = view.modelBuilder.text().withProperties<azdataType.TextComponentProperties>({
value: vscode.workspace.workspaceFile ? constants.addProjectToCurrentWorkspace : constants.newWorkspaceWillBeCreated,
CSSStyles: { 'margin-top': '-10px', 'margin-bottom': '5px' }
}).component();
let workspaceContainerRow;
if (vscode.workspace.workspaceFile && !isCurrentWorkspaceUntitled()) {
workspaceContainerRow = view.modelBuilder.flexContainer().withItems([workspaceLabel, this.workspaceInputBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px', 'margin-top': '0px' } }).withLayout({ flexFlow: 'column' }).component();
} else {
// have browse button to help select where the workspace file should be created
const workspaceInput = view.modelBuilder.flexContainer().withItems([this.workspaceInputBox], { CSSStyles: { 'margin-right': '10px', 'margin-bottom': '10px', 'width': '100%' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
workspaceInput.addItem(browseFolderButton, { CSSStyles: { 'margin-top': '-10px' } });
workspaceContainerRow = view.modelBuilder.flexContainer().withItems([workspaceLabel, workspaceInput], { flex: '0 0 auto', CSSStyles: { 'margin-top': '0px' } }).withLayout({ flexFlow: 'column' }).component();
}
return workspaceContainerRow;
}
/**
* Update the workspace inputbox based on the passed in location and name if there isn't a workspace currently open
* @param location
* @param name
*/
public 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;
}
}
// only enable Create button if all fields are filled
public tryEnableCreateButton(): void {
if (this.sourceConnectionTextBox!.value && this.sourceDatabaseDropDown!.value
@@ -450,8 +359,7 @@ export class CreateProjectFromDatabaseDialog {
projName: this.projectNameTextBox!.value!,
filePath: this.projectLocationTextBox!.value!,
version: '1.0.0.0',
extractTarget: this.mapExtractTargetEnum(<string>this.folderStructureDropDown!.value),
newWorkspaceFilePath: this.workspaceInputBox!.enabled ? vscode.Uri.file(this.workspaceInputBox!.value!) : undefined
extractTarget: this.mapExtractTargetEnum(<string>this.folderStructureDropDown!.value)
};
getAzdataApi()!.window.closeDialog(this.dialog);
@@ -477,6 +385,9 @@ export class CreateProjectFromDatabaseDialog {
async validate(): Promise<boolean> {
try {
if (await getDataWorkspaceExtensionApi().validateWorkspace() === false) {
return false;
}
// the selected location should be an existing directory
const parentDirectoryExists = await exists(this.projectLocationTextBox!.value!);
if (!parentDirectoryExists) {
@@ -490,11 +401,6 @@ export class CreateProjectFromDatabaseDialog {
this.showErrorMessage(constants.ProjectDirectoryAlreadyExistError(this.projectNameTextBox!.value!, this.projectLocationTextBox!.value!));
return false;
}
if (this.workspaceInputBox!.enabled) {
await this.validateNewWorkspace();
}
return true;
} catch (err) {
this.showErrorMessage(err?.message ? err.message : err);
@@ -502,30 +408,6 @@ export class CreateProjectFromDatabaseDialog {
}
}
protected async validateNewWorkspace(): Promise<void> {
const sameFolderAsNewProject = path.join(this.projectLocationTextBox!.value!, this.projectNameTextBox!.value!) === path.dirname(this.workspaceInputBox!.value!);
// 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 exists(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 exists(this.workspaceInputBox!.value!);
if (workspaceFileExists) {
throw new Error(constants.WorkspaceFileAlreadyExistsError(this.workspaceInputBox!.value!));
}
}
protected showErrorMessage(message: string): void {
this.dialog.message = {
text: message,

View File

@@ -3,7 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Uri } from 'vscode';
import { ExtractTarget } from '../../../../mssql';
/**
@@ -16,5 +15,4 @@ export interface ImportDataModel {
filePath: string;
version: string;
extractTarget: ExtractTarget;
newWorkspaceFilePath?: Uri;
}

View File

@@ -30,16 +30,6 @@ export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvide
return provider;
}
/**
* Callback method when a project has been removed from the workspace view
* @param projectFile The Uri of the project file
*/
RemoveProject(projectFile: vscode.Uri): Promise<void> {
// No resource release needed
console.log(`project file unloaded: ${projectFile.fsPath}`);
return Promise.resolve();
}
/**
* Gets the supported project types
*/

View File

@@ -23,7 +23,7 @@ describe('Add Database Reference Dialog', () => {
beforeEach(function (): void {
const dataWorkspaceMock = TypeMoq.Mock.ofType<dataworkspace.IExtension>();
dataWorkspaceMock.setup(x => x.getProjectsInWorkspace(TypeMoq.It.isAny())).returns(() => []);
dataWorkspaceMock.setup(x => x.getProjectsInWorkspace(TypeMoq.It.isAny())).returns(() => Promise.resolve([]));
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: dataWorkspaceMock.object });
});

View File

@@ -7,7 +7,6 @@ import * as should from 'should';
import * as azdata from 'azdata';
import * as mssql from '../../../../mssql';
import * as sinon from 'sinon';
import * as path from 'path';
import { CreateProjectFromDatabaseDialog } from '../../dialogs/createProjectFromDatabaseDialog';
import { mockConnectionProfile } from '../testContext';
import { ImportDataModel } from '../../models/api/import';
@@ -83,22 +82,11 @@ describe('Create Project From Database Dialog', () => {
should.equal(dialog.projectNameTextBox!.value, 'DatabaseProjectMy Database');
});
it('Should update default workspace name correctly when location and project name are provided', async function (): Promise<void> {
sinon.stub(azdata.connection, 'listDatabases').resolves(['My Database']);
const dialog = new CreateProjectFromDatabaseDialog(mockConnectionProfile);
await dialog.openDialog();
dialog.updateWorkspaceInputbox('testLocation', 'testProjectName');
should.equal(dialog.workspaceInputBox!.value, path.join('testLocation', 'testProjectName.code-workspace'));
});
it('Should include all info in import data model and connect to appropriate call back properties', async function (): Promise<void> {
const dialog = new CreateProjectFromDatabaseDialog(mockConnectionProfile);
sinon.stub(azdata.connection, 'listDatabases').resolves(['My Database']);
await dialog.openDialog();
dialog.workspaceInputBox!.enabled = false;
dialog.projectNameTextBox!.value = 'testProject';
dialog.projectLocationTextBox!.value = 'testLocation';
@@ -110,8 +98,7 @@ describe('Create Project From Database Dialog', () => {
projName: 'testProject',
filePath: 'testLocation',
version: '1.0.0.0',
extractTarget: mssql.ExtractTarget['schemaObjectType'],
newWorkspaceFilePath: undefined
extractTarget: mssql.ExtractTarget['schemaObjectType']
};
dialog.createProjectFromDatabaseCallback = (m) => { model = m; };

View File

@@ -579,7 +579,7 @@ describe('ProjectsController', function (): void {
const project2 = await Project.openProject(vscode.Uri.file(projPath2).fsPath);
const showErrorMessageSpy = sinon.spy(vscode.window, 'showErrorMessage');
const dataWorkspaceMock = TypeMoq.Mock.ofType<dataworkspace.IExtension>();
dataWorkspaceMock.setup(x => x.getProjectsInWorkspace(TypeMoq.It.isAny())).returns(() => [vscode.Uri.file(project1.projectFilePath), vscode.Uri.file(project2.projectFilePath)]);
dataWorkspaceMock.setup(x => x.getProjectsInWorkspace(TypeMoq.It.isAny())).returns(() => Promise.resolve([vscode.Uri.file(project1.projectFilePath), vscode.Uri.file(project2.projectFilePath)]));
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: dataWorkspaceMock.object });
// add project reference from project1 to project2