diff --git a/extensions/data-workspace/src/common/dataWorkspaceExtension.ts b/extensions/data-workspace/src/common/dataWorkspaceExtension.ts index 11f0430e33..ab7f17ffb9 100644 --- a/extensions/data-workspace/src/common/dataWorkspaceExtension.ts +++ b/extensions/data-workspace/src/common/dataWorkspaceExtension.ts @@ -27,4 +27,9 @@ export class DataWorkspaceExtension implements IExtension { get defaultProjectSaveLocation(): vscode.Uri | undefined { return defaultProjectSaveLocation(); } + + validateWorkspace(): Promise { + return this.workspaceService.validateWorkspace(); + } + } diff --git a/extensions/data-workspace/src/dataworkspace.d.ts b/extensions/data-workspace/src/dataworkspace.d.ts index 46ac438675..7618d0f508 100644 --- a/extensions/data-workspace/src/dataworkspace.d.ts +++ b/extensions/data-workspace/src/dataworkspace.d.ts @@ -22,7 +22,7 @@ declare module 'dataworkspace' { * Add projects to the workspace * @param projectFiles Uris of project files to add */ - addProjectsToWorkspace(projectFiles: vscode.Uri[]): Promise + addProjectsToWorkspace(projectFiles: vscode.Uri[]): Promise; /** * Change focus to Projects view @@ -33,6 +33,11 @@ declare module 'dataworkspace' { * Returns the default location to save projects */ defaultProjectSaveLocation: vscode.Uri | undefined; + + /** + * Verifies that a workspace is open or if it should be automatically created + */ + validateWorkspace(): Promise; } /** diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index 67569f8021..1b1d86bcd9 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -133,6 +133,10 @@ 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"); // Error messages diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index 8d8c324acf..ec5de1b11e 100644 --- a/extensions/sql-database-projects/src/controllers/projectController.ts +++ b/extensions/sql-database-projects/src/controllers/projectController.ts @@ -637,26 +637,29 @@ export class ProjectsController { try { const workspaceApi = utils.getDataWorkspaceExtensionApi(); - const newProjFolderUri = model.filePath; + const validateWorkspace = await workspaceApi.validateWorkspace(); + if (validateWorkspace) { + const newProjFolderUri = model.filePath; - const newProjFilePath = await this.createNewProject({ - newProjName: model.projName, - folderUri: vscode.Uri.file(newProjFolderUri), - projectTypeId: constants.emptySqlDatabaseProjectTypeId - }); + const newProjFilePath = await this.createNewProject({ + newProjName: model.projName, + folderUri: vscode.Uri.file(newProjFolderUri), + projectTypeId: constants.emptySqlDatabaseProjectTypeId + }); - model.filePath = path.dirname(newProjFilePath); - this.setFilePath(model); + model.filePath = path.dirname(newProjFilePath); + this.setFilePath(model); - const project = await Project.openProject(newProjFilePath); - await this.createProjectFromDatabaseApiCall(model); // Call ExtractAPI in DacFx Service - let fileFolderList: string[] = model.extractTarget === mssql.ExtractTarget.file ? [model.filePath] : await this.generateList(model.filePath); // Create a list of all the files and directories to be added to project + const project = await Project.openProject(newProjFilePath); + await this.createProjectFromDatabaseApiCall(model); // Call ExtractAPI in DacFx Service + let fileFolderList: string[] = model.extractTarget === mssql.ExtractTarget.file ? [model.filePath] : await this.generateList(model.filePath); // Create a list of all the files and directories to be added to project - await project.addToProject(fileFolderList); // Add generated file structure to the project + await project.addToProject(fileFolderList); // Add generated file structure to the project - // add project to workspace - workspaceApi.showProjectsView(); - await workspaceApi.addProjectsToWorkspace([vscode.Uri.file(newProjFilePath)]); + // add project to workspace + workspaceApi.showProjectsView(); + await workspaceApi.addProjectsToWorkspace([vscode.Uri.file(newProjFilePath)]); + } } catch (err) { vscode.window.showErrorMessage(utils.getErrorMessage(err)); diff --git a/extensions/sql-database-projects/src/dialogs/createProjectFromDatabaseDialog.ts b/extensions/sql-database-projects/src/dialogs/createProjectFromDatabaseDialog.ts index 83aa78546d..6eaaaf7bfa 100644 --- a/extensions/sql-database-projects/src/dialogs/createProjectFromDatabaseDialog.ts +++ b/extensions/sql-database-projects/src/dialogs/createProjectFromDatabaseDialog.ts @@ -8,6 +8,7 @@ import * as vscode from 'vscode'; import * as constants from '../common/constants'; import * as newProjectTool from '../tools/newProjectTool'; import * as mssql from '../../../mssql'; +import * as path from 'path'; import { IconPathHelper } from '../common/iconHelper'; import { cssStyles } from '../common/uiConstants'; @@ -24,6 +25,7 @@ export class CreateProjectFromDatabaseDialog { public projectNameTextBox: azdata.InputBoxComponent | undefined; public projectLocationTextBox: azdata.InputBoxComponent | undefined; public folderStructureDropDown: azdata.DropDownComponent | undefined; + public workspaceInputBox: azdata.InputBoxComponent | undefined; private formBuilder: azdata.FormBuilder | undefined; private connectionId: string | undefined; private toDispose: vscode.Disposable[] = []; @@ -81,6 +83,10 @@ 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 = view.modelBuilder.formContainer() .withFormItems([ { @@ -106,6 +112,14 @@ export class CreateProjectFromDatabaseDialog { component: createProjectSettingsFormSection, } ] + }, + { + title: constants.workspace, + components: [ + { + component: createworkspaceContainerFormSection, + } + ] } ], { horizontal: false, @@ -150,6 +164,7 @@ export class CreateProjectFromDatabaseDialog { this.sourceDatabaseDropDown.onValueChanged(() => { this.setProjectName(); + this.updateWorkspaceInputbox(this.projectLocationTextBox!.value!, this.projectNameTextBox!.value!); this.tryEnableCreateButton(); }); @@ -236,6 +251,8 @@ export class CreateProjectFromDatabaseDialog { this.projectNameTextBox.onTextChanged(() => { this.projectNameTextBox!.value = this.projectNameTextBox!.value?.trim(); + this.projectNameTextBox!.updateProperty('title', this.projectNameTextBox!.value); + this.updateWorkspaceInputbox(this.projectLocationTextBox!.value!, this.projectNameTextBox!.value!); this.tryEnableCreateButton(); }); @@ -263,6 +280,7 @@ export class CreateProjectFromDatabaseDialog { this.projectLocationTextBox.onTextChanged(() => { this.projectLocationTextBox!.updateProperty('title', this.projectLocationTextBox!.value); + this.updateWorkspaceInputbox(this.projectLocationTextBox!.value!, this.projectNameTextBox!.value!); this.tryEnableCreateButton(); }); @@ -300,6 +318,7 @@ export class CreateProjectFromDatabaseDialog { this.projectLocationTextBox!.value = folderUris[0].fsPath; this.projectLocationTextBox!.updateProperty('title', folderUris[0].fsPath); + this.updateWorkspaceInputbox(this.projectLocationTextBox!.value!, this.projectNameTextBox!.value!); }); return browseFolderButton; @@ -329,6 +348,42 @@ 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: azdata.ModelView): azdata.FlexContainer { + this.workspaceInputBox = view.modelBuilder.inputBox().withProperties({ + ariaLabel: constants.workspaceLocationTitle, + enabled: false, + 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 workspaceLabel = view.modelBuilder.text().withProperties({ + value: vscode.workspace.workspaceFile ? constants.addProjectToCurrentWorkspace : constants.newWorkspaceWillBeCreated, + CSSStyles: { 'margin-top': '-10px', 'margin-bottom': '5px' } + }).component(); + + const workspaceContainerRow = view.modelBuilder.flexContainer().withItems([workspaceLabel, this.workspaceInputBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px', '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) { + 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 diff --git a/extensions/sql-database-projects/src/test/dialogs/createProjectFromDatabaseDialog.test.ts b/extensions/sql-database-projects/src/test/dialogs/createProjectFromDatabaseDialog.test.ts index b3f03235da..63dff8bc49 100644 --- a/extensions/sql-database-projects/src/test/dialogs/createProjectFromDatabaseDialog.test.ts +++ b/extensions/sql-database-projects/src/test/dialogs/createProjectFromDatabaseDialog.test.ts @@ -7,6 +7,7 @@ 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'; @@ -82,6 +83,15 @@ 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 { + 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 { const dialog = new CreateProjectFromDatabaseDialog(mockConnectionProfile); sinon.stub(azdata.connection, 'listDatabases').resolves(['My Database']);