Create project from database UI dialog (#13179)

* UI hook up

* Add tests

* Add back the missing statement for opening project

* Fix failures

* Add a few more tests

* Fix test failure

* Addressed comments

* Update UI to match the mocks

* Update UI to match updated mockups

* Addressed comments to match UI with mockup

* Updated all import strings to be called as Create Project From Database strings

* Fix a couple of test failures and one comment addressed

* Update one missed import string

* Skipping a failing test for now

* Fix failures. Fix alignment of icons

* Addressed PR comments

* Addressed couple more PR comments
This commit is contained in:
Sakshi Sharma
2020-11-20 09:38:16 -08:00
committed by GitHub
parent 8d42182db8
commit 749989cd0b
11 changed files with 672 additions and 312 deletions

View File

@@ -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");

View File

@@ -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<T> {
resolve: (result: T | Promise<T>) => void;
reject: (reason: any) => void;
}

View File

@@ -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';

View File

@@ -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<void> {
// 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<mssql.ValidateStreamingJobResult> {
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<ImportDataModel | undefined> {
let model = <ImportDataModel>{};
/**
* 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<CreateProjectFromDatabaseDialog> {
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 (<any>context).connectionProfile ? (<any>context).connectionProfile : context;
}
private async getProjectName(dbName: string): Promise<string> {
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<mssql.ExtractTarget> {
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<vscode.Uri> {
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<void> {
let ext = vscode.extensions.getExtension(mssql.extension.name)!;

View File

@@ -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<T> {
resolve: (result: T | Promise<T>) => void;
reject: (reason: any) => void;
}
export class AddDatabaseReferenceDialog {
public dialog: azdata.window.Dialog;
public addDatabaseReferenceTab: azdata.window.DialogTab;

View File

@@ -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<void>;
private initDialogPromise: Promise<void> = new Promise<void>((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<void> {
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 = <azdata.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<azdata.TextComponentProperties>({
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<azdata.TextComponentProperties>({
value: constants.databaseNameLabel,
requiredIndicator: true,
width: cssStyles.labelWidth,
CSSStyles: cssStyles.fontWeightBold
}).component();
const databaseRow = view.modelBuilder.flexContainer().withItems([databaseLabel, <azdata.DropDownComponent>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(<string>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<azdata.InputBoxProperties>({
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<azdata.TextComponentProperties>({
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<azdata.TextComponentProperties>({
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<azdata.ButtonProperties>({
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<azdata.TextComponentProperties>({
value: constants.folderStructureLabel,
requiredIndicator: true,
width: cssStyles.labelWidth,
CSSStyles: cssStyles.fontWeightBold
}).component();
const folderStructureRow = view.modelBuilder.flexContainer().withItems([folderStructureLabel, <azdata.DropDownComponent>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<void> {
const model: ImportDataModel = {
serverId: this.connectionId!,
database: <string>this.sourceDatabaseDropDown!.value,
projName: this.projectNameTextBox!.value!,
filePath: this.projectLocationTextBox!.value!,
version: '1.0.0.0',
extractTarget: this.mapExtractTargetEnum(<string>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);
}
}
}

View File

@@ -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<azdata.TextComponentProperties>({
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<azdata.TextComponentProperties>({
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<azdata.TextComponentProperties>({
value: constants.databaseNameLabel,
requiredIndicator: true,
width: cssStyles.publishDialogLabelWidth
width: cssStyles.labelWidth
}).component();
const databaseRow = view.modelBuilder.flexContainer().withItems([databaseLabel, <azdata.DropDownComponent>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);

View File

@@ -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;
}

View File

@@ -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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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);
});
});

View File

@@ -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<void> {
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<void> {
for (const name of ['', ' ', undefined]) {
const dataWorkspaceMock = TypeMoq.Mock.ofType<dataworkspace.IExtension>();
dataWorkspaceMock.setup(x => x.getProjectsInWorkspace()).returns(() => []);
dataWorkspaceMock.setup(x => x.defaultProjectSaveLocation).returns(() => vscode.Uri.file('/test/folder'));
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ 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<void> {
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<void> {
const dataWorkspaceMock = TypeMoq.Mock.ofType<dataworkspace.IExtension>();
dataWorkspaceMock.setup(x => x.getProjectsInWorkspace()).returns(() => []);
dataWorkspaceMock.setup(x => x.defaultProjectSaveLocation).returns(() => vscode.Uri.file('/test/folder'));
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ 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<void> {
const dataWorkspaceMock = TypeMoq.Mock.ofType<dataworkspace.IExtension>();
dataWorkspaceMock.setup(x => x.getProjectsInWorkspace()).returns(() => []);
dataWorkspaceMock.setup(x => x.defaultProjectSaveLocation).returns(() => vscode.Uri.file('/test/folder'));
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ 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<void> {
const dataWorkspaceMock = TypeMoq.Mock.ofType<dataworkspace.IExtension>();
dataWorkspaceMock.setup(x => x.getProjectsInWorkspace()).returns(() => []);
dataWorkspaceMock.setup(x => x.defaultProjectSaveLocation).returns(() => vscode.Uri.file('/test/folder'));
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ 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<void> {
const projectName = 'MyProjectName';
let folderPath = await testUtils.generateTestFolderPath();
const dataWorkspaceMock = TypeMoq.Mock.ofType<dataworkspace.IExtension>();
dataWorkspaceMock.setup(x => x.defaultProjectSaveLocation).returns(() => vscode.Uri.file('/test/folder'));
dataWorkspaceMock.setup(x => x.getProjectsInWorkspace()).returns(() => []);
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ 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<void> {
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<void> {
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<void> {
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<void> {
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}`);
});
});

View File

@@ -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'
}
};