diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index 0b788a47b1..abfc0f82d5 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -116,6 +116,21 @@ export const otherServer = 'OtherServer'; export const otherSeverVariable = 'OtherServer'; export const databaseProject = localize('databaseProject', "Database project"); +// Create Project From Database dialog strings + +export const createProjectFromDatabaseDialogName = localize('createProjectFromDatabaseDialogName', "Create Project From Database"); +export const createProjectDialogOkButtonText = localize('createProjectDialogOkButtonText', "Create"); +export const sourceDatabase = localize('sourceDatabase', "Source database"); +export const targetProject = localize('targetProject', "Target project"); +export const createProjectSettings = localize('createProjectSettings', "Settings"); +export const projectNameLabel = localize('projectNameLabel', "Name"); +export const projectNamePlaceholderText = localize('projectNamePlaceholderText', "Enter project name"); +export const projectLocationLabel = localize('projectLocationLabel', "Location"); +export const projectLocationPlaceholderText = localize('projectLocationPlaceholderText', "Enter project location"); +export const browseButtonText = localize('browseButtonText', "Browse folder"); +export const folderStructureLabel = localize('folderStructureLabel', "Folder structure"); + + // Error messages export const multipleSqlProjFiles = localize('multipleSqlProjFilesSelected', "Multiple .sqlproj files selected; please select only one."); @@ -127,7 +142,6 @@ export const unknownDataSourceType = localize('unknownDataSourceType', "Unknown export const invalidSqlConnectionString = localize('invalidSqlConnectionString', "Invalid SQL connection string"); export const projectNameRequired = localize('projectNameRequired', "Name is required to create a new database project."); export const projectLocationRequired = localize('projectLocationRequired', "Location is required to create a new database project."); -export const projectLocationNotEmpty = localize('projectLocationNotEmpty', "Current project location is not empty. Select an empty folder for precise extraction."); export const extractTargetRequired = localize('extractTargetRequired', "Target information for extract is required to create database project."); export const schemaCompareNotInstalled = localize('schemaCompareNotInstalled', "Schema compare extension installation is required to run schema compare"); export const buildFailedCannotStartSchemaCompare = localize('buildFailedCannotStartSchemaCompare', "Schema compare could not start because build failed"); diff --git a/extensions/sql-database-projects/src/common/promise.ts b/extensions/sql-database-projects/src/common/promise.ts new file mode 100644 index 0000000000..241353c31c --- /dev/null +++ b/extensions/sql-database-projects/src/common/promise.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Deferred promise + */ +export interface Deferred { + resolve: (result: T | Promise) => void; + reject: (reason: any) => void; +} diff --git a/extensions/sql-database-projects/src/common/uiConstants.ts b/extensions/sql-database-projects/src/common/uiConstants.ts index a73391c948..98cf95283e 100644 --- a/extensions/sql-database-projects/src/common/uiConstants.ts +++ b/extensions/sql-database-projects/src/common/uiConstants.ts @@ -8,10 +8,11 @@ export namespace cssStyles { export const text = { 'user-select': 'text', 'cursor': 'text' }; export const tableHeader = { ...text, 'text-align': 'left', 'border': 'none', 'font-size': '12px', 'font-weight': 'normal', 'color': '#666666' }; export const tableRow = { ...text, 'border-top': 'solid 1px #ccc', 'border-bottom': 'solid 1px #ccc', 'border-left': 'none', 'border-right': 'none', 'font-size': '12px' }; + export const fontWeightBold = { 'font-weight': 'bold' }; export const titleFontSize = 13; - export const publishDialogLabelWidth = '205px'; - export const publishDialogTextboxWidth = '190px'; + export const labelWidth = '205px'; + export const textboxWidth = '190px'; export const addDatabaseReferenceDialogLabelWidth = '215px'; export const addDatabaseReferenceInputboxWidth = '220px'; diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index f591ce20ad..9982cd79e2 100644 --- a/extensions/sql-database-projects/src/controllers/projectController.ts +++ b/extensions/sql-database-projects/src/controllers/projectController.ts @@ -10,7 +10,6 @@ import * as path from 'path'; import * as utils from '../common/utils'; import * as UUID from 'vscode-languageclient/lib/utils/uuid'; import * as templates from '../templates/templates'; -import * as newProjectTool from '../tools/newProjectTool'; import * as vscode from 'vscode'; import * as azdata from 'azdata'; import * as dataworkspace from 'dataworkspace'; @@ -30,6 +29,7 @@ import { PublishProfile, load } from '../models/publishProfile/publishProfile'; import { AddDatabaseReferenceDialog } from '../dialogs/addDatabaseReferenceDialog'; import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings, IProjectReferenceSettings } from '../models/IDatabaseReferenceSettings'; import { DatabaseReferenceTreeItem } from '../models/tree/databaseReferencesTreeItem'; +import { CreateProjectFromDatabaseDialog } from '../dialogs/createProjectFromDatabaseDialog'; /** * Controller for managing project lifecycle @@ -494,48 +494,6 @@ export class ProjectsController { } } - /** - * Creates a new SQL database project from the existing database, - * prompting the user for a name, file path location and extract target - */ - public async createProjectFromDatabase(context: azdata.IConnectionProfile | any): Promise { - - // TODO: Refactor code - try { - const workspaceApi = utils.getDataWorkspaceExtensionApi(); - - const model: ImportDataModel | undefined = await this.getModelFromContext(context); - - if (!model) { - return; // cancelled by user - } - model.projName = await this.getProjectName(model.database); - let newProjFolderUri = (await this.getFolderLocation()).fsPath; - model.extractTarget = await this.getExtractTarget(); - model.version = '1.0.0.0'; - - const newProjFilePath = await this.createNewProject(model.projName, vscode.Uri.file(newProjFolderUri), true); - model.filePath = path.dirname(newProjFilePath); - - if (model.extractTarget === mssql.ExtractTarget.file) { - model.filePath = path.join(model.filePath, model.projName + '.sql'); // File extractTarget specifies the exact file rather than the containing folder - } - - 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 - - // add project to workspace - workspaceApi.showProjectsView(); - await workspaceApi.addProjectsToWorkspace([vscode.Uri.file(newProjFilePath)]); - } - catch (err) { - vscode.window.showErrorMessage(utils.getErrorMessage(err)); - } - } - public async validateExternalStreamingJob(node: dataworkspace.WorkspaceTreeItem): Promise { const project: Project = this.getProjectFromContext(node); @@ -649,50 +607,54 @@ export class ProjectsController { return treeNode instanceof FolderNode ? utils.trimUri(treeNode.root.uri, treeNode.uri) : ''; } - public async getModelFromContext(context: any): Promise { - let model = {}; + /** + * Creates a new SQL database project from the existing database, + * prompting the user for a name, file path location and extract target + */ + public async createProjectFromDatabase(context: azdata.IConnectionProfile | any): Promise { + const profile = this.getConnectionProfileFromContext(context); + let createProjectFromDatabaseDialog = this.getCreateProjectFromDatabaseDialog(profile); - let profile = this.getConnectionProfileFromContext(context); - let connectionId, database; - //TODO: Prompt for new connection addition and get database information if context information isn't provided. + createProjectFromDatabaseDialog.createProjectFromDatabaseCallback = async (model) => await this.createProjectFromDatabaseCallback(model); - if (profile) { - database = profile.databaseName; - connectionId = profile.id; + await createProjectFromDatabaseDialog.openDialog(); + + return createProjectFromDatabaseDialog; + } + + public getCreateProjectFromDatabaseDialog(profile: azdata.IConnectionProfile | undefined): CreateProjectFromDatabaseDialog { + return new CreateProjectFromDatabaseDialog(profile); + } + + public async createProjectFromDatabaseCallback(model: ImportDataModel) { + try { + const workspaceApi = utils.getDataWorkspaceExtensionApi(); + + const newProjFolderUri = model.filePath; + + const newProjFilePath = await this.createNewProject(model.projName, vscode.Uri.file(newProjFolderUri), true); + 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 + + await project.addToProject(fileFolderList); // Add generated file structure to the project + + // add project to workspace + workspaceApi.showProjectsView(); + await workspaceApi.addProjectsToWorkspace([vscode.Uri.file(newProjFilePath)]); } - else { - const connection = await azdata.connection.openConnectionDialog(); - - if (!connection) { - return undefined; - } - - connectionId = connection.connectionId; - - // use database that was connected to - if (connection.options['database']) { - database = connection.options['database']; - } + catch (err) { + vscode.window.showErrorMessage(utils.getErrorMessage(err)); } + } - // choose database if connection was to a server or master - if (!database || database === constants.master) { - const databaseList = await azdata.connection.listDatabases(connectionId); - database = (await vscode.window.showQuickPick(databaseList.map(dbName => { return { label: dbName }; }), - { - canPickMany: false, - placeHolder: constants.extractDatabaseSelection - }))?.label; - - if (!database) { - throw new Error(constants.databaseSelectionRequired); - } + public setFilePath(model: ImportDataModel) { + if (model.extractTarget === mssql.ExtractTarget.file) { + model.filePath = path.join(model.filePath, `${model.projName}.sql`); // File extractTarget specifies the exact file rather than the containing folder } - - model.database = database; - model.serverId = connectionId; - - return model; } private getConnectionProfileFromContext(context: azdata.IConnectionProfile | any): azdata.IConnectionProfile | undefined { @@ -705,80 +667,6 @@ export class ProjectsController { return (context).connectionProfile ? (context).connectionProfile : context; } - private async getProjectName(dbName: string): Promise { - let projName = await vscode.window.showInputBox({ - prompt: constants.newDatabaseProjectName, - value: newProjectTool.defaultProjectNameFromDb(dbName) - }); - - projName = projName?.trim(); - - if (!projName) { - throw new Error(constants.projectNameRequired); - } - - return projName; - } - - private mapExtractTargetEnum(inputTarget: any): mssql.ExtractTarget { - if (inputTarget) { - switch (inputTarget) { - case constants.file: return mssql.ExtractTarget['file']; - case constants.flat: return mssql.ExtractTarget['flat']; - case constants.objectType: return mssql.ExtractTarget['objectType']; - case constants.schema: return mssql.ExtractTarget['schema']; - case constants.schemaObjectType: return mssql.ExtractTarget['schemaObjectType']; - default: throw new Error(constants.invalidInput(inputTarget)); - } - } else { - throw new Error(constants.extractTargetRequired); - } - } - - private async getExtractTarget(): Promise { - let extractTarget: mssql.ExtractTarget; - - let extractTargetOptions: vscode.QuickPickItem[] = []; - - let keys = [constants.file, constants.flat, constants.objectType, constants.schema, constants.schemaObjectType]; - - // TODO: Create a wrapper class to handle the mapping - keys.forEach((targetOption: string) => { - extractTargetOptions.push({ label: targetOption }); - }); - - let input = await vscode.window.showQuickPick(extractTargetOptions, { - canPickMany: false, - placeHolder: constants.extractTargetInput - }); - let extractTargetInput = input?.label; - - extractTarget = this.mapExtractTargetEnum(extractTargetInput); - - return extractTarget; - } - - private async getFolderLocation(): Promise { - let projUri: vscode.Uri; - - const selectionResult = await vscode.window.showOpenDialog({ - canSelectFiles: false, - canSelectFolders: true, - canSelectMany: false, - openLabel: constants.selectString, - defaultUri: newProjectTool.defaultProjectSaveLocation() - }); - - if (selectionResult) { - projUri = (selectionResult as vscode.Uri[])[0]; - } - else { - throw new Error(constants.projectLocationRequired); - } - - return projUri; - } - public async createProjectFromDatabaseApiCall(model: ImportDataModel): Promise { let ext = vscode.extensions.getExtension(mssql.extension.name)!; diff --git a/extensions/sql-database-projects/src/dialogs/addDatabaseReferenceDialog.ts b/extensions/sql-database-projects/src/dialogs/addDatabaseReferenceDialog.ts index 15263dee60..f65786de51 100644 --- a/extensions/sql-database-projects/src/dialogs/addDatabaseReferenceDialog.ts +++ b/extensions/sql-database-projects/src/dialogs/addDatabaseReferenceDialog.ts @@ -13,6 +13,7 @@ import { Project, SystemDatabase } from '../models/project'; import { cssStyles } from '../common/uiConstants'; import { IconPathHelper } from '../common/iconHelper'; import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings, IProjectReferenceSettings } from '../models/IDatabaseReferenceSettings'; +import { Deferred } from '../common/promise'; export enum ReferenceType { project, @@ -20,11 +21,6 @@ export enum ReferenceType { dacpac } -interface Deferred { - resolve: (result: T | Promise) => void; - reject: (reason: any) => void; -} - export class AddDatabaseReferenceDialog { public dialog: azdata.window.Dialog; public addDatabaseReferenceTab: azdata.window.DialogTab; diff --git a/extensions/sql-database-projects/src/dialogs/createProjectFromDatabaseDialog.ts b/extensions/sql-database-projects/src/dialogs/createProjectFromDatabaseDialog.ts new file mode 100644 index 0000000000..61407f1436 --- /dev/null +++ b/extensions/sql-database-projects/src/dialogs/createProjectFromDatabaseDialog.ts @@ -0,0 +1,374 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as vscode from 'vscode'; +import * as constants from '../common/constants'; +import * as newProjectTool from '../tools/newProjectTool'; +import * as mssql from '../../../mssql'; + +import { IconPathHelper } from '../common/iconHelper'; +import { cssStyles } from '../common/uiConstants'; +import { ImportDataModel } from '../models/api/import'; +import { Deferred } from '../common/promise'; +import { getConnectionName } from './utils'; + +export class CreateProjectFromDatabaseDialog { + public dialog: azdata.window.Dialog; + public createProjectFromDatabaseTab: azdata.window.DialogTab; + public sourceConnectionTextBox: azdata.InputBoxComponent | undefined; + private selectConnectionButton: azdata.ButtonComponent | undefined; + public sourceDatabaseDropDown: azdata.DropDownComponent | undefined; + public projectNameTextBox: azdata.InputBoxComponent | undefined; + public projectLocationTextBox: azdata.InputBoxComponent | undefined; + public folderStructureDropDown: azdata.DropDownComponent | undefined; + private formBuilder: azdata.FormBuilder | undefined; + private connectionId: string | undefined; + private toDispose: vscode.Disposable[] = []; + private initDialogComplete!: Deferred; + private initDialogPromise: Promise = new Promise((resolve, reject) => this.initDialogComplete = { resolve, reject }); + + public createProjectFromDatabaseCallback: ((model: ImportDataModel) => any) | undefined; + + constructor(private profile: azdata.IConnectionProfile | undefined) { + this.dialog = azdata.window.createModelViewDialog(constants.createProjectFromDatabaseDialogName); + this.createProjectFromDatabaseTab = azdata.window.createTab(constants.createProjectFromDatabaseDialogName); + } + + public async openDialog(): Promise { + this.initializeDialog(); + this.dialog.okButton.label = constants.createProjectDialogOkButtonText; + this.dialog.okButton.enabled = false; + this.toDispose.push(this.dialog.okButton.onClick(async () => await this.handleCreateButtonClick())); + + this.dialog.cancelButton.label = constants.cancelButtonText; + + azdata.window.openDialog(this.dialog); + await this.initDialogPromise; + + if (this.profile) { + await this.updateConnectionComponents(getConnectionName(this.profile), this.profile.id, this.profile.databaseName!); + } + + this.tryEnableCreateButton(); + } + + private dispose(): void { + this.toDispose.forEach(disposable => disposable.dispose()); + } + + private initializeDialog(): void { + this.initializeCreateProjectFromDatabaseTab(); + this.dialog.content = [this.createProjectFromDatabaseTab]; + } + + private initializeCreateProjectFromDatabaseTab(): void { + this.createProjectFromDatabaseTab.registerContent(async view => { + + const connectionRow = this.createConnectionRow(view); + const databaseRow = this.createDatabaseRow(view); + const sourceDatabaseFormSection = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); + sourceDatabaseFormSection.addItems([connectionRow, databaseRow]); + + const projectNameRow = this.createProjectNameRow(view); + const projectLocationRow = this.createProjectLocationRow(view); + const targetProjectFormSection = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); + targetProjectFormSection.addItems([projectNameRow, projectLocationRow]); + + const folderStructureRow = this.createFolderStructureRow(view); + const createProjectSettingsFormSection = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); + createProjectSettingsFormSection.addItems([folderStructureRow]); + + this.formBuilder = view.modelBuilder.formContainer() + .withFormItems([ + { + title: constants.sourceDatabase, + components: [ + { + component: sourceDatabaseFormSection, + } + ] + }, + { + title: constants.targetProject, + components: [ + { + component: targetProjectFormSection, + } + ] + }, + { + title: constants.createProjectSettings, + components: [ + { + component: createProjectSettingsFormSection, + } + ] + } + ], { + horizontal: false, + titleFontSize: cssStyles.titleFontSize + }) + .withLayout({ + width: '100%' + }); + + let formModel = this.formBuilder.component(); + await view.initializeModel(formModel); + this.initDialogComplete?.resolve(); + }); + } + + private createConnectionRow(view: azdata.ModelView): azdata.FlexContainer { + const sourceConnectionTextBox = this.createSourceConnectionComponent(view); + const selectConnectionButton: azdata.Component = this.createSelectConnectionButton(view); + + const serverLabel = view.modelBuilder.text().withProperties({ + value: constants.server, + requiredIndicator: true, + width: cssStyles.labelWidth, + CSSStyles: cssStyles.fontWeightBold + }).component(); + + const connectionRow = view.modelBuilder.flexContainer().withItems([serverLabel, sourceConnectionTextBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px', 'margin-bottom': '-10px', 'margin-top': '-20px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); + connectionRow.insertItem(selectConnectionButton, 2, { CSSStyles: { 'margin-right': '0px', 'margin-bottom': '-10px', 'margin-top': '-20px' } }); + + return connectionRow; + } + + private createDatabaseRow(view: azdata.ModelView): azdata.FlexContainer { + this.sourceDatabaseDropDown = view.modelBuilder.dropDown().withProperties({ + ariaLabel: constants.databaseNameLabel, + required: true, + width: cssStyles.textboxWidth, + editable: true, + fireOnTextChange: true + }).component(); + + this.sourceDatabaseDropDown.onValueChanged(() => { + this.setProjectName(); + this.tryEnableCreateButton(); + }); + + const databaseLabel = view.modelBuilder.text().withProperties({ + value: constants.databaseNameLabel, + requiredIndicator: true, + width: cssStyles.labelWidth, + CSSStyles: cssStyles.fontWeightBold + }).component(); + + const databaseRow = view.modelBuilder.flexContainer().withItems([databaseLabel, this.sourceDatabaseDropDown], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px', 'margin-bottom': '-10px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); + + return databaseRow; + } + + public setProjectName() { + this.projectNameTextBox!.value = newProjectTool.defaultProjectNameFromDb(this.sourceDatabaseDropDown!.value); + } + + private createSourceConnectionComponent(view: azdata.ModelView): azdata.InputBoxComponent { + this.sourceConnectionTextBox = view.modelBuilder.inputBox().withProperties({ + value: '', + placeHolder: constants.selectConnection, + width: cssStyles.textboxWidth, + enabled: false + }).component(); + + this.sourceConnectionTextBox.onTextChanged(() => { + this.tryEnableCreateButton(); + }); + + return this.sourceConnectionTextBox; + } + + private createSelectConnectionButton(view: azdata.ModelView): azdata.Component { + this.selectConnectionButton = view.modelBuilder.button().withProperties({ + ariaLabel: constants.selectConnection, + iconPath: IconPathHelper.selectConnection, + height: '16px', + width: '16px' + }).component(); + + this.selectConnectionButton.onDidClick(async () => { + let connection = await azdata.connection.openConnectionDialog(); + this.connectionId = connection.connectionId; + + let connectionTextboxValue: string; + connectionTextboxValue = getConnectionName(connection); + + await this.updateConnectionComponents(connectionTextboxValue, this.connectionId, connection.options.database); + }); + + return this.selectConnectionButton; + } + + private async updateConnectionComponents(connectionTextboxValue: string, connectionId: string, databaseName?: string) { + this.sourceConnectionTextBox!.value = connectionTextboxValue; + this.sourceConnectionTextBox!.updateProperty('title', connectionTextboxValue); + + // populate database dropdown with the databases for this connection + if (connectionId) { + const databaseValues = await azdata.connection.listDatabases(connectionId); + + this.sourceDatabaseDropDown!.values = databaseValues; + this.connectionId = connectionId; + } + + // change the database inputbox value to the connection's database if there is one + if (databaseName && databaseName !== constants.master) { + this.sourceDatabaseDropDown!.value = databaseName; + } + + // change icon to the one without a plus sign + this.selectConnectionButton!.iconPath = IconPathHelper.connect; + } + + private createProjectNameRow(view: azdata.ModelView): azdata.FlexContainer { + this.projectNameTextBox = view.modelBuilder.inputBox().withProperties({ + ariaLabel: constants.projectNamePlaceholderText, + required: true, + width: cssStyles.textboxWidth, + validationErrorMessage: constants.projectNameRequired + }).component(); + + this.projectNameTextBox.onTextChanged(() => { + this.projectNameTextBox!.value = this.projectNameTextBox!.value?.trim(); + this.tryEnableCreateButton(); + }); + + const projectNameLabel = view.modelBuilder.text().withProperties({ + value: constants.projectNameLabel, + requiredIndicator: true, + width: cssStyles.labelWidth, + CSSStyles: cssStyles.fontWeightBold + }).component(); + + const projectNameRow = view.modelBuilder.flexContainer().withItems([projectNameLabel, this.projectNameTextBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px', 'margin-bottom': '-10px', 'margin-top': '-20px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); + + return projectNameRow; + } + + private createProjectLocationRow(view: azdata.ModelView): azdata.FlexContainer { + const browseFolderButton: azdata.Component = this.createBrowseFolderButton(view); + + this.projectLocationTextBox = view.modelBuilder.inputBox().withProperties({ + value: '', + ariaLabel: constants.projectLocationLabel, + placeHolder: constants.projectLocationPlaceholderText, + width: cssStyles.textboxWidth, + validationErrorMessage: constants.projectLocationRequired + }).component(); + + this.projectLocationTextBox.onTextChanged(() => { + this.projectLocationTextBox!.updateProperty('title', this.projectLocationTextBox!.value); + this.tryEnableCreateButton(); + }); + + const projectLocationLabel = view.modelBuilder.text().withProperties({ + value: constants.projectLocationLabel, + requiredIndicator: true, + width: cssStyles.labelWidth, + CSSStyles: cssStyles.fontWeightBold + }).component(); + + const projectLocationRow = view.modelBuilder.flexContainer().withItems([projectLocationLabel, this.projectLocationTextBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px', 'margin-bottom': '-10px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); + projectLocationRow.insertItem(browseFolderButton, 2, { CSSStyles: { 'margin-right': '0px', 'margin-bottom': '-10px' } }); + + return projectLocationRow; + } + + private createBrowseFolderButton(view: azdata.ModelView): azdata.ButtonComponent { + const browseFolderButton = view.modelBuilder.button().withProperties({ + ariaLabel: constants.browseButtonText, + iconPath: IconPathHelper.folder_blue, + height: '18px', + width: '18px' + }).component(); + + browseFolderButton.onDidClick(async () => { + let folderUris = await vscode.window.showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + openLabel: constants.selectString, + defaultUri: newProjectTool.defaultProjectSaveLocation() + }); + if (!folderUris || folderUris.length === 0) { + return; + } + + this.projectLocationTextBox!.value = folderUris[0].fsPath; + this.projectLocationTextBox!.updateProperty('title', folderUris[0].fsPath); + }); + + return browseFolderButton; + } + + private createFolderStructureRow(view: azdata.ModelView): azdata.FlexContainer { + this.folderStructureDropDown = view.modelBuilder.dropDown().withProperties({ + values: [constants.file, constants.flat, constants.objectType, constants.schema, constants.schemaObjectType], + value: constants.schemaObjectType, + ariaLabel: constants.folderStructureLabel, + required: true, + width: cssStyles.textboxWidth + }).component(); + + this.folderStructureDropDown.onValueChanged(() => { + this.tryEnableCreateButton(); + }); + + const folderStructureLabel = view.modelBuilder.text().withProperties({ + value: constants.folderStructureLabel, + requiredIndicator: true, + width: cssStyles.labelWidth, + CSSStyles: cssStyles.fontWeightBold + }).component(); + + const folderStructureRow = view.modelBuilder.flexContainer().withItems([folderStructureLabel, this.folderStructureDropDown], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px', 'margin-top': '-20px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); + + return folderStructureRow; + } + + // only enable Create button if all fields are filled + public tryEnableCreateButton(): void { + if (this.sourceConnectionTextBox!.value && this.sourceDatabaseDropDown!.value + && this.projectNameTextBox!.value && this.projectLocationTextBox!.value) { + this.dialog.okButton.enabled = true; + } else { + this.dialog.okButton.enabled = false; + } + } + + public async handleCreateButtonClick(): Promise { + const model: ImportDataModel = { + serverId: this.connectionId!, + database: this.sourceDatabaseDropDown!.value, + projName: this.projectNameTextBox!.value!, + filePath: this.projectLocationTextBox!.value!, + version: '1.0.0.0', + extractTarget: this.mapExtractTargetEnum(this.folderStructureDropDown!.value) + }; + + azdata.window.closeDialog(this.dialog); + await this.createProjectFromDatabaseCallback!(model); + + this.dispose(); + } + + private mapExtractTargetEnum(inputTarget: any): mssql.ExtractTarget { + if (inputTarget) { + switch (inputTarget) { + case constants.file: return mssql.ExtractTarget['file']; + case constants.flat: return mssql.ExtractTarget['flat']; + case constants.objectType: return mssql.ExtractTarget['objectType']; + case constants.schema: return mssql.ExtractTarget['schema']; + case constants.schemaObjectType: return mssql.ExtractTarget['schemaObjectType']; + default: throw new Error(constants.invalidInput(inputTarget)); + } + } else { + throw new Error(constants.extractTargetRequired); + } + } +} diff --git a/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts b/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts index 3db9a1c46a..ab7d22e728 100644 --- a/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts +++ b/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts @@ -14,6 +14,7 @@ import { IPublishSettings, IGenerateScriptSettings } from '../models/IPublishSet import { DeploymentOptions, SchemaObjectType } from '../../../mssql/src/mssql'; import { IconPathHelper } from '../common/iconHelper'; import { cssStyles } from '../common/uiConstants'; +import { getConnectionName } from './utils'; interface DataSourceDropdownValue extends azdata.CategoryValue { dataSource: SqlConnectionDataSource; @@ -286,7 +287,7 @@ export class PublishDatabaseDialog { value: '', ariaLabel: constants.targetConnectionLabel, placeHolder: constants.selectConnection, - width: cssStyles.publishDialogTextboxWidth, + width: cssStyles.textboxWidth, enabled: false }).component(); @@ -350,12 +351,12 @@ export class PublishDatabaseDialog { this.loadProfileTextBox = view.modelBuilder.inputBox().withProperties({ placeHolder: constants.loadProfilePlaceholderText, ariaLabel: constants.profile, - width: cssStyles.publishDialogTextboxWidth + width: cssStyles.textboxWidth }).component(); const profileLabel = view.modelBuilder.text().withProperties({ value: constants.profile, - width: cssStyles.publishDialogLabelWidth + width: cssStyles.labelWidth }).component(); const profileRow = view.modelBuilder.flexContainer().withItems([profileLabel, this.loadProfileTextBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); @@ -371,7 +372,7 @@ export class PublishDatabaseDialog { const serverLabel = view.modelBuilder.text().withProperties({ value: constants.server, requiredIndicator: true, - width: cssStyles.publishDialogLabelWidth + width: cssStyles.labelWidth }).component(); const connectionRow = view.modelBuilder.flexContainer().withItems([serverLabel, this.targetConnectionTextBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); @@ -386,7 +387,7 @@ export class PublishDatabaseDialog { value: this.getDefaultDatabaseName(), ariaLabel: constants.databaseNameLabel, required: true, - width: cssStyles.publishDialogTextboxWidth, + width: cssStyles.textboxWidth, editable: true, fireOnTextChange: true }).component(); @@ -398,7 +399,7 @@ export class PublishDatabaseDialog { const databaseLabel = view.modelBuilder.text().withProperties({ value: constants.databaseNameLabel, requiredIndicator: true, - width: cssStyles.publishDialogLabelWidth + width: cssStyles.labelWidth }).component(); const databaseRow = view.modelBuilder.flexContainer().withItems([databaseLabel, this.targetDatabaseDropDown], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); @@ -481,18 +482,7 @@ export class PublishDatabaseDialog { let connection = await azdata.connection.openConnectionDialog(); this.connectionId = connection.connectionId; - // show connection name if there is one, otherwise show connection in format that shows in OE - let connectionTextboxValue: string; - if (connection.options['connectionName']) { - connectionTextboxValue = connection.options['connectionName']; - } else { - let user = connection.options['user']; - if (!user) { - user = constants.defaultUser; - } - - connectionTextboxValue = `${connection.options['server']} (${user})`; - } + let connectionTextboxValue: string = getConnectionName(connection); this.updateConnectionComponents(connectionTextboxValue, this.connectionId); diff --git a/extensions/sql-database-projects/src/dialogs/utils.ts b/extensions/sql-database-projects/src/dialogs/utils.ts new file mode 100644 index 0000000000..78f27cce3b --- /dev/null +++ b/extensions/sql-database-projects/src/dialogs/utils.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as constants from '../common/constants'; + +/** + * Gets connection name from connection object if there is one, + * otherwise set connection name in format that shows in OE + */ +export function getConnectionName(connection: any): string { + let connectionName: string; + if (connection.options['connectionName']) { + connectionName = connection.options['connectionName']; + } else { + let user = connection.options['user']; + if (!user) { + user = constants.defaultUser; + } + + connectionName = `${connection.options['server']} (${user})`; + } + + return connectionName; +} diff --git a/extensions/sql-database-projects/src/test/dialogs/createProjectFromDatabaseDialog.test.ts b/extensions/sql-database-projects/src/test/dialogs/createProjectFromDatabaseDialog.test.ts new file mode 100644 index 0000000000..b3f03235da --- /dev/null +++ b/extensions/sql-database-projects/src/test/dialogs/createProjectFromDatabaseDialog.test.ts @@ -0,0 +1,109 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as should from 'should'; +import * as azdata from 'azdata'; +import * as mssql from '../../../../mssql'; +import * as sinon from 'sinon'; +import { CreateProjectFromDatabaseDialog } from '../../dialogs/createProjectFromDatabaseDialog'; +import { mockConnectionProfile } from '../testContext'; +import { ImportDataModel } from '../../models/api/import'; + +describe('Create Project From Database Dialog', () => { + afterEach(function (): void { + sinon.restore(); + }); + + it('Should open dialog successfully', async function (): Promise { + sinon.stub(azdata.connection, 'listDatabases').resolves([]); + const dialog = new CreateProjectFromDatabaseDialog(mockConnectionProfile); + await dialog.openDialog(); + should.notEqual(dialog.createProjectFromDatabaseTab, undefined); + }); + + it('Should enable ok button correctly with a connection profile', async function (): Promise { + sinon.stub(azdata.connection, 'listDatabases').resolves([]); + const dialog = new CreateProjectFromDatabaseDialog(mockConnectionProfile); + await dialog.openDialog(); // should set connection details + + should(dialog.dialog.okButton.enabled).equal(false); + + // fill in project name and ok button should not be enabled + dialog.projectNameTextBox!.value = 'testProject'; + dialog.tryEnableCreateButton(); + should(dialog.dialog.okButton.enabled).equal(false, 'Ok button should not be enabled because project location is not filled'); + + // fill in project location and ok button should be enabled + dialog.projectLocationTextBox!.value = 'testLocation'; + dialog.tryEnableCreateButton(); + should(dialog.dialog.okButton.enabled).equal(true, 'Ok button should be enabled since all the required fields are filled'); + }); + + it('Should enable ok button correctly without a connection profile', async function (): Promise { + const dialog = new CreateProjectFromDatabaseDialog(undefined); + await dialog.openDialog(); + + should(dialog.dialog.okButton.enabled).equal(false, 'Ok button should not be enabled because all the required details are not filled'); + + // fill in project name and ok button should not be enabled + dialog.projectNameTextBox!.value = 'testProject'; + dialog.tryEnableCreateButton(); + should(dialog.dialog.okButton.enabled).equal(false, 'Ok button should not be enabled because source database details and project location are not filled'); + + // fill in project location and ok button not should be enabled + dialog.projectLocationTextBox!.value = 'testLocation'; + dialog.tryEnableCreateButton(); + should(dialog.dialog.okButton.enabled).equal(false, 'Ok button should not be enabled because source database details are not filled'); + + // fill in server name and ok button not should be enabled + dialog.sourceConnectionTextBox!.value = 'testServer'; + dialog.tryEnableCreateButton(); + should(dialog.dialog.okButton.enabled).equal(false, 'Ok button should not be enabled because source database is not filled'); + + // fill in database name and ok button should be enabled + dialog.sourceDatabaseDropDown!.value = 'testDatabase'; + dialog.tryEnableCreateButton(); + should(dialog.dialog.okButton.enabled).equal(true, 'Ok button should be enabled since all the required fields are filled'); + + // update folder structure information and ok button should still be enabled + dialog.folderStructureDropDown!.value = 'Object Type'; + dialog.tryEnableCreateButton(); + should(dialog.dialog.okButton.enabled).equal(true, 'Ok button should be enabled since all the required fields are filled'); + }); + + it('Should create default project name correctly when database information is populated', async function (): Promise { + sinon.stub(azdata.connection, 'listDatabases').resolves(['My Database']); + const dialog = new CreateProjectFromDatabaseDialog(mockConnectionProfile); + await dialog.openDialog(); + dialog.setProjectName(); + + should.equal(dialog.projectNameTextBox!.value, 'DatabaseProjectMy Database'); + }); + + 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']); + await dialog.openDialog(); + + dialog.projectNameTextBox!.value = 'testProject'; + dialog.projectLocationTextBox!.value = 'testLocation'; + + let model: ImportDataModel; + + const expectedImportDataModel: ImportDataModel = { + serverId: 'My Id', + database: 'My Database', + projName: 'testProject', + filePath: 'testLocation', + version: '1.0.0.0', + extractTarget: mssql.ExtractTarget['schemaObjectType'] + }; + + dialog.createProjectFromDatabaseCallback = (m) => { model = m; }; + await dialog.handleCreateButtonClick(); + + should(model!).deepEqual(expectedImportDataModel); + }); +}); diff --git a/extensions/sql-database-projects/src/test/projectController.test.ts b/extensions/sql-database-projects/src/test/projectController.test.ts index 06e474e197..787458b62f 100644 --- a/extensions/sql-database-projects/src/test/projectController.test.ts +++ b/extensions/sql-database-projects/src/test/projectController.test.ts @@ -6,7 +6,6 @@ import * as should from 'should'; import * as path from 'path'; import * as os from 'os'; -import * as azdata from 'azdata'; import * as vscode from 'vscode'; import * as TypeMoq from 'typemoq'; import * as sinon from 'sinon'; @@ -15,11 +14,12 @@ import * as baselines from './baselines/baselines'; import * as templates from '../templates/templates'; import * as testUtils from './testUtils'; import * as constants from '../common/constants'; +import * as mssql from '../../../mssql'; import { SqlDatabaseProjectTreeViewProvider } from '../controllers/databaseProjectTreeViewProvider'; import { ProjectsController } from '../controllers/projectController'; import { promises as fs } from 'fs'; -import { createContext, TestContext, mockDacFxResult } from './testContext'; +import { createContext, TestContext, mockDacFxResult, mockConnectionProfile } from './testContext'; import { Project, reservedProjectFolders, SystemDatabase, FileProjectEntry, SystemDatabaseReferenceProjectEntry } from '../models/project'; import { PublishDatabaseDialog } from '../dialogs/publishDatabaseDialog'; import { IPublishSettings, IGenerateScriptSettings } from '../models/IPublishSettings'; @@ -29,26 +29,11 @@ import { FolderNode, FileNode } from '../models/tree/fileFolderTreeItem'; import { BaseProjectTreeItem } from '../models/tree/baseTreeItem'; import { AddDatabaseReferenceDialog } from '../dialogs/addDatabaseReferenceDialog'; import { IDacpacReferenceSettings } from '../models/IDatabaseReferenceSettings'; +import { CreateProjectFromDatabaseDialog } from '../dialogs/createProjectFromDatabaseDialog'; +import { ImportDataModel } from '../models/api/import'; let testContext: TestContext; -// Mock test data -const mockConnectionProfile: azdata.IConnectionProfile = { - connectionName: 'My Connection', - serverName: 'My Server', - databaseName: 'My Database', - userName: 'My User', - password: 'My Pwd', - authenticationType: 'SqlLogin', - savePassword: false, - groupFullName: 'My groupName', - groupId: 'My GroupId', - providerName: 'My Server', - saveProfile: true, - id: 'My Id', - options: undefined as any -}; - describe('ProjectsController', function (): void { before(async function (): Promise { await templates.loadTemplates(path.join(__dirname, '..', '..', 'resources', 'templates')); @@ -401,7 +386,7 @@ describe('ProjectsController', function (): void { }); }); - describe('Create project from database', function (): void { + describe('Create project from database operations and dialog', function (): void { afterEach(() => { sinon.restore(); }); @@ -429,128 +414,70 @@ describe('ProjectsController', function (): void { should(spy.calledWith(msg)).be.true(`showErrorMessage not called with expected message '${msg}' Actual '${spy.getCall(0).args[0]}'`); }); - it('Should show error when no project name provided', async function (): Promise { - for (const name of ['', ' ', undefined]) { - const dataWorkspaceMock = TypeMoq.Mock.ofType(); - dataWorkspaceMock.setup(x => x.getProjectsInWorkspace()).returns(() => []); - dataWorkspaceMock.setup(x => x.defaultProjectSaveLocation).returns(() => vscode.Uri.file('/test/folder')); - sinon.stub(vscode.extensions, 'getExtension').returns({ exports: dataWorkspaceMock.object}); - sinon.stub(vscode.workspace, 'workspaceFile').value(vscode.Uri.file('/test/folder/ws.code-workspace')); - sinon.stub(vscode.window, 'showInputBox').resolves(name); - const spy = sinon.spy(vscode.window, 'showErrorMessage'); + it('Create project from Database dialog should open from ProjectController', async function (): Promise { + let opened = false; - const projController = new ProjectsController(); - await projController.createProjectFromDatabase({ connectionProfile: mockConnectionProfile }); - should(spy.calledOnce).be.true('showErrorMessage should have been called'); - should(spy.calledWith(constants.projectNameRequired)).be.true(`showErrorMessage not called with expected message '${constants.projectNameRequired}' Actual '${spy.getCall(0).args[0]}'`); - sinon.restore(); - } - }); + let createProjectFromDatabaseDialog = TypeMoq.Mock.ofType(CreateProjectFromDatabaseDialog, undefined, undefined, mockConnectionProfile); + createProjectFromDatabaseDialog.setup(x => x.openDialog()).returns(() => { opened = true; return Promise.resolve(undefined); }); - it('Should show error when no target information provided', async function (): Promise { - const dataWorkspaceMock = TypeMoq.Mock.ofType(); - dataWorkspaceMock.setup(x => x.getProjectsInWorkspace()).returns(() => []); - dataWorkspaceMock.setup(x => x.defaultProjectSaveLocation).returns(() => vscode.Uri.file('/test/folder')); - sinon.stub(vscode.extensions, 'getExtension').returns({ exports: dataWorkspaceMock.object}); - sinon.stub(vscode.workspace, 'workspaceFile').value(vscode.Uri.file('/test/folder/ws.code-workspace')); - sinon.stub(vscode.window, 'showInputBox').resolves('MyProjectName'); - sinon.stub(vscode.window, 'showQuickPick').resolves(undefined); - sinon.stub(vscode.window, 'showOpenDialog').resolves([vscode.Uri.file('fakePath')]); - const spy = sinon.spy(vscode.window, 'showErrorMessage'); - - const projController = new ProjectsController(); - await projController.createProjectFromDatabase({ connectionProfile: mockConnectionProfile }); - should(spy.calledOnce).be.true('showErrorMessage should have been called'); - should(spy.calledWith(constants.extractTargetRequired)).be.true(`showErrorMessage not called with expected message '${constants.extractTargetRequired}' Actual '${spy.getCall(0).args[0]}'`); - }); - - it('Should show error when no location provided with ExtractTarget = File', async function (): Promise { - const dataWorkspaceMock = TypeMoq.Mock.ofType(); - dataWorkspaceMock.setup(x => x.getProjectsInWorkspace()).returns(() => []); - dataWorkspaceMock.setup(x => x.defaultProjectSaveLocation).returns(() => vscode.Uri.file('/test/folder')); - sinon.stub(vscode.extensions, 'getExtension').returns({ exports: dataWorkspaceMock.object}); - sinon.stub(vscode.workspace, 'workspaceFile').value(vscode.Uri.file('/test/folder/ws.code-workspace')); - sinon.stub(vscode.window, 'showInputBox').resolves('MyProjectName'); - sinon.stub(vscode.window, 'showOpenDialog').resolves(undefined); - sinon.stub(vscode.window, 'showQuickPick').resolves({ label: constants.file }); - const spy = sinon.spy(vscode.window, 'showErrorMessage'); - - const projController = new ProjectsController(); - await projController.createProjectFromDatabase({ connectionProfile: mockConnectionProfile }); - should(spy.calledOnce).be.true('showErrorMessage should have been called'); - should(spy.calledWith(constants.projectLocationRequired)).be.true(`showErrorMessage not called with expected message '${constants.projectLocationRequired}' Actual '${spy.getCall(0).args[0]}'`); - }); - - it('Should show error when no location provided with ExtractTarget = SchemaObjectType', async function (): Promise { - const dataWorkspaceMock = TypeMoq.Mock.ofType(); - dataWorkspaceMock.setup(x => x.getProjectsInWorkspace()).returns(() => []); - dataWorkspaceMock.setup(x => x.defaultProjectSaveLocation).returns(() => vscode.Uri.file('/test/folder')); - sinon.stub(vscode.extensions, 'getExtension').returns({ exports: dataWorkspaceMock.object}); - sinon.stub(vscode.workspace, 'workspaceFile').value(vscode.Uri.file('/test/folder/ws.code-workspace')); - sinon.stub(vscode.window, 'showInputBox').resolves('MyProjectName'); - sinon.stub(vscode.window, 'showQuickPick').resolves({ label: constants.schemaObjectType }); - sinon.stub(vscode.window, 'showOpenDialog').resolves(undefined); - const spy = sinon.spy(vscode.window, 'showErrorMessage'); - - const projController = new ProjectsController(); - await projController.createProjectFromDatabase({ connectionProfile: mockConnectionProfile }); - should(spy.calledOnce).be.true('showErrorMessage should have been called'); - should(spy.calledWith(constants.projectLocationRequired)).be.true(`showErrorMessage not called with expected message '${constants.projectLocationRequired}' Actual '${spy.getCall(0).args[0]}'`); - }); - - it('Should set model filePath correctly for ExtractType = File and not-File.', async function (): Promise { - const projectName = 'MyProjectName'; - let folderPath = await testUtils.generateTestFolderPath(); - - const dataWorkspaceMock = TypeMoq.Mock.ofType(); - dataWorkspaceMock.setup(x => x.defaultProjectSaveLocation).returns(() => vscode.Uri.file('/test/folder')); - dataWorkspaceMock.setup(x => x.getProjectsInWorkspace()).returns(() => []); - sinon.stub(vscode.extensions, 'getExtension').returns({ exports: dataWorkspaceMock.object}); - sinon.stub(vscode.workspace, 'workspaceFile').value(vscode.Uri.file('/test/folder/ws.code-workspace')); - sinon.stub(vscode.window, 'showInputBox').resolves(projectName); - const showQuickPickStub = sinon.stub(vscode.window, 'showQuickPick').resolves({ label: constants.file }); - sinon.stub(vscode.window, 'showOpenDialog').callsFake(() => Promise.resolve([vscode.Uri.file(folderPath)])); - - let importPath; - - let projController = TypeMoq.Mock.ofType(ProjectsController, undefined, undefined, new SqlDatabaseProjectTreeViewProvider()); + let projController = TypeMoq.Mock.ofType(ProjectsController); projController.callBase = true; + projController.setup(x => x.getCreateProjectFromDatabaseDialog(TypeMoq.It.isAny())).returns(() => createProjectFromDatabaseDialog.object); - projController.setup(x => x.createProjectFromDatabaseApiCall(TypeMoq.It.isAny())).returns(async (model) => { importPath = model.filePath; }); - - await projController.object.createProjectFromDatabase({ connectionProfile: mockConnectionProfile }); - should(importPath).equal(vscode.Uri.file(path.join(folderPath, projectName, projectName + '.sql')).fsPath, `model.filePath should be set to a specific file for ExtractTarget === file, but was ${importPath}`); - - // reset for counter-test - importPath = undefined; - folderPath = await testUtils.generateTestFolderPath(); - showQuickPickStub.resolves({ label: constants.schemaObjectType }); - - await projController.object.createProjectFromDatabase({ connectionProfile: mockConnectionProfile }); - should(importPath).equal(vscode.Uri.file(path.join(folderPath, projectName)).fsPath, `model.filePath should be set to a folder for ExtractTarget !== file, but was ${importPath}`); + await projController.object.createProjectFromDatabase(mockConnectionProfile); + should(opened).equal(true); }); - it('Should establish Import context correctly for ObjectExplorer and palette launch points', async function (): Promise { - const connectionId = 'BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575'; - // test welcome button and palette launch points (context-less) - let mockDbSelection = 'FakeDatabase'; - sinon.stub(azdata.connection, 'listDatabases').resolves([]); - sinon.stub(vscode.window, 'showQuickPick').resolves({ label: mockDbSelection }); - sinon.stub(azdata.connection, 'openConnectionDialog').resolves({ - providerName: 'MSSQL', - connectionId: connectionId, - options: {} + it.skip('Callbacks are hooked up and called from create project from database dialog', async function (): Promise { + const createProjectFromDbHoller = 'hello from callback for createProjectFromDatabase()'; + + let holler = 'nothing'; + + const createProjectFromDatabaseDialog = TypeMoq.Mock.ofType(CreateProjectFromDatabaseDialog, undefined, undefined, undefined); + createProjectFromDatabaseDialog.callBase = true; + createProjectFromDatabaseDialog.setup(x => x.handleCreateButtonClick()).returns(async () => { + await projController.object.createProjectFromDatabaseCallback( { serverId: 'My Id', database: 'My Database', projName: 'testProject', filePath: 'testLocation', version: '1.0.0.0', extractTarget: mssql.ExtractTarget['schemaObjectType'] }); + return Promise.resolve(undefined); }); - let projController = new ProjectsController(); + const projController = TypeMoq.Mock.ofType(ProjectsController); + projController.callBase = true; + projController.setup(x => x.getCreateProjectFromDatabaseDialog(TypeMoq.It.isAny())).returns(() => createProjectFromDatabaseDialog.object); + projController.setup(x => x.createProjectFromDatabaseCallback(TypeMoq.It.isAny())).returns(() => { + holler = createProjectFromDbHoller; + return Promise.resolve(undefined); + }); - let result = await projController.getModelFromContext(undefined); + let dialog = await projController.object.createProjectFromDatabase(undefined); + await dialog.handleCreateButtonClick(); - should(result).deepEqual({ database: mockDbSelection, serverId: connectionId }); + should(holler).equal(createProjectFromDbHoller, 'executionCallback() is supposed to have been setup and called for create project from database scenario'); + }); - // test launch via Object Explorer context - result = await projController.getModelFromContext(mockConnectionProfile); - should(result).deepEqual({ database: 'My Database', serverId: 'My Id' }); + it('Should set model filePath correctly for ExtractType = File', async function (): Promise { + let folderPath = await testUtils.generateTestFolderPath(); + let projectName = 'My Project'; + let importPath; + let model: ImportDataModel = { serverId: 'My Id', database: 'My Database', projName: projectName, filePath: folderPath, version: '1.0.0.0', extractTarget: mssql.ExtractTarget['file'] }; + + const projController = new ProjectsController(); + projController.setFilePath(model); + importPath = model.filePath; + + should(importPath.toUpperCase()).equal(vscode.Uri.file(path.join(folderPath, projectName + '.sql')).fsPath.toUpperCase(), `model.filePath should be set to a specific file for ExtractTarget === file, but was ${importPath}`); + }); + + it('Should set model filePath correctly for ExtractType = Schema/Object Type', async function (): Promise { + let folderPath = await testUtils.generateTestFolderPath(); + let projectName = 'My Project'; + let importPath; + let model: ImportDataModel = { serverId: 'My Id', database: 'My Database', projName: projectName, filePath: folderPath, version: '1.0.0.0', extractTarget: mssql.ExtractTarget['schemaObjectType'] }; + + const projController = new ProjectsController(); + projController.setFilePath(model); + importPath = model.filePath; + + should(importPath.toUpperCase()).equal(vscode.Uri.file(path.join(folderPath)).fsPath.toUpperCase(), `model.filePath should be set to a folder for ExtractTarget !== file, but was ${importPath}`); }); }); diff --git a/extensions/sql-database-projects/src/test/testContext.ts b/extensions/sql-database-projects/src/test/testContext.ts index 5aa5ccbab4..668c4fc6e9 100644 --- a/extensions/sql-database-projects/src/test/testContext.ts +++ b/extensions/sql-database-projects/src/test/testContext.ts @@ -147,3 +147,26 @@ export function createContext(): TestContext { dacFxService: TypeMoq.Mock.ofType(MockDacFxService) }; } + +// Mock test data +export const mockConnectionProfile: azdata.IConnectionProfile = { + connectionName: 'My Connection', + serverName: 'My Server', + databaseName: 'My Database', + userName: 'My User', + password: 'My Pwd', + authenticationType: 'SqlLogin', + savePassword: false, + groupFullName: 'My groupName', + groupId: 'My GroupId', + providerName: 'My Server', + saveProfile: true, + id: 'My Id', + options: { + server: 'My Server', + database: 'My Database', + user: 'My User', + password: 'My Pwd', + authenticationType: 'SqlLogin' + } +};