mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-13 17:22:15 -05:00
Data workspace projects changes (#13466)
* Fix project context menu actions (#12541) * delete works again * make fewer changes * update all sql db project commands * cleanup * Remove old projects view (#12563) * remove old projects view from file explorer view * fix tests failing * remove projects in open folder opening up in old view * Update db reference dialog to show projects in the workspace (#12580) * update database reference dialog to show projects in the workspace in the project dropdown * remove workspace stuff from sql projects extension * undo change * add class that implements IExtension * undo a change * update DataWorkspaceExtension to take workspaceService as a parameter * add type * Update sql database project commands (#12595) * remove sql proj's open and create new project from comman palette * hook up create project from database to data workspace * rename the remaining import databases to create project from database * remove open, new, and close commands * expose addProjectsToWorkspace() in IExtension instead of calling command * Addressing comments * fix failing sql project tests (#12651) * update SSDT projects opened in projects viewlet (#12669) * fix action not refreshing the tree issue (#12692) * fix adding project references in new projects viewlet (#12688) * Remove old projects tree provider (#12702) * Remove old projects tree provider and fix tests * formatting * update refreshProjectsTree() to accept workspaceTreeItem() * Cleanup ProjectsController (#12718) * remove openProject from ProjectController and some cleanup * rename * add project and open project dialogs (#12729) * empty dialogs * wip * new project dialog implementation * revert gitattributes * open project dialog * implement add project * remove icon helper * refactor * revert script change * adjust views * more updates * make data-workspace a builtin extension * show the view only when project provider is detected (#12819) * only show the view when proj provider is available * update * fix sql project tests after merge (#12793) * Update dialogs to be closer to mockups (#12879) * small UI changes to dialogs * center radio card group text * Create workspace if needed when opening/new project (#12930) * empty dialogs * wip * new project dialog implementation * revert gitattributes * open project dialog * implement add project * remove icon helper * refactor * revert script change * create workspace * initial changes * create new workspace working * fix tests * cleanup * remove showWorkspaceRequiredNotification() * Add test for no workspace open * update blue buttons * move loading temp project to activate() instead of workspaceService constructor * move workspace creation warning message to before project is created * pass uri to createWorkspace * add tests Co-authored-by: Alan Ren <alanren@microsoft.com> * Additional create workspace changes (#13004) * Dialogs workspace updates (#13010) * adding workspace text boxes * match new project dialog to mockups * Add validation error message for workspace file * add enterWorkspace api * add warning message for opening workspace * cleanup * update commands to remove project so they're more generic * remove 'empty' from string * Move default project location setting to data workspace extension (#13022) * remove project location setting and notification from sql database projects extension * add default project location setting to data workspace extension * fix typo * Add back project name incrementing * other merge fixes * fix strings from other PR * default to last opened directory instead of home directory if no specified default location * A few small updates (#13092) * fix build error * update title for inputboxes * add missing file * Add tests for data workspace dialogs (#13324) * add tests for dialogs * create helper functions * New project dialog workspace inputbox fixes (#13407) * workspace inputbox fixes * fix folder icons * Update package.jsons and readme (#13451) * update package.jsons * update readme * add workspace information to open existing dialog (#13455) Co-authored-by: Alan Ren <alanren@microsoft.com>
This commit is contained in:
@@ -216,7 +216,6 @@ const externalExtensions = [
|
||||
'azurehybridtoolkit',
|
||||
'cms',
|
||||
'dacpac',
|
||||
'data-workspace',
|
||||
'import',
|
||||
'kusto',
|
||||
'liveshare',
|
||||
|
||||
@@ -250,7 +250,6 @@ const externalExtensions = [
|
||||
'azurehybridtoolkit',
|
||||
'cms',
|
||||
'dacpac',
|
||||
'data-workspace',
|
||||
'import',
|
||||
'kusto',
|
||||
'liveshare',
|
||||
|
||||
@@ -35,7 +35,7 @@ export class DacFxTestService implements mssql.IDacFxService {
|
||||
this.dacfxResult.operationId = extractOperationId;
|
||||
return Promise.resolve(this.dacfxResult);
|
||||
}
|
||||
importDatabaseProject(databaseName: string, targetFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, extractTarget: mssql.ExtractTarget, taskExecutionMode: azdata.TaskExecutionMode): Promise<mssql.DacFxResult> {
|
||||
createProjectFromDatabase(databaseName: string, targetFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, extractTarget: mssql.ExtractTarget, taskExecutionMode: azdata.TaskExecutionMode): Promise<mssql.DacFxResult> {
|
||||
this.dacfxResult.operationId = importOperationId;
|
||||
return Promise.resolve(this.dacfxResult);
|
||||
}
|
||||
|
||||
1
extensions/data-workspace/images/file.svg
Normal file
1
extensions/data-workspace/images/file.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#231f20;}.cls-2{fill:#212121;}</style></defs><title>file_16x16</title><polygon class="cls-1" points="13.59 2.21 13.58 2.22 13.58 2.2 13.59 2.21"/><path class="cls-2" d="M8.71,0,14,5.29V16H2V0ZM3,15H13V6H8V1H3ZM9,1.71V5h3.29Z"/></svg>
|
||||
|
After Width: | Height: | Size: 351 B |
1
extensions/data-workspace/images/file_inverse.svg
Normal file
1
extensions/data-workspace/images/file_inverse.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>file_inverse_16x16</title><polygon class="cls-1" points="13.59 2.21 13.58 2.22 13.58 2.2 13.59 2.21"/><path class="cls-1" d="M8.71,0,14,5.29V16H2V0ZM3,15H13V6H8V1H3ZM9,1.71V5h3.29Z"/></svg>
|
||||
|
After Width: | Height: | Size: 335 B |
3
extensions/data-workspace/images/folder.svg
Normal file
3
extensions/data-workspace/images/folder.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="17" height="12" viewBox="0 0 17 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.8457 3V11C16.8457 11.1406 16.8197 11.2708 16.7676 11.3906C16.7155 11.5104 16.6426 11.6172 16.5488 11.7109C16.4603 11.7995 16.3561 11.8698 16.2363 11.9219C16.1165 11.974 15.9863 12 15.8457 12H1.8457C1.70508 12 1.57487 11.974 1.45508 11.9219C1.33529 11.8698 1.22852 11.7995 1.13477 11.7109C1.04622 11.6172 0.975911 11.5104 0.923828 11.3906C0.871745 11.2708 0.845703 11.1406 0.845703 11V1C0.845703 0.859375 0.871745 0.729167 0.923828 0.609375C0.975911 0.489583 1.04622 0.385417 1.13477 0.296875C1.22852 0.203125 1.33529 0.130208 1.45508 0.078125C1.57487 0.0260417 1.70508 0 1.8457 0H7.5957C7.78841 0 7.9681 0.0364583 8.13477 0.109375C8.30143 0.177083 8.45247 0.270833 8.58789 0.390625C8.72852 0.505208 8.85352 0.638021 8.96289 0.789062C9.07747 0.934896 9.18164 1.08594 9.27539 1.24219C9.3431 1.36198 9.4082 1.46875 9.4707 1.5625C9.53841 1.65625 9.61133 1.73698 9.68945 1.80469C9.77279 1.86719 9.86393 1.91667 9.96289 1.95312C10.0671 1.98438 10.1947 2 10.3457 2H15.8457C15.9863 2 16.1165 2.02604 16.2363 2.07812C16.3561 2.13021 16.4603 2.20312 16.5488 2.29688C16.6426 2.38542 16.7155 2.48958 16.7676 2.60938C16.8197 2.72917 16.8457 2.85938 16.8457 3ZM7.5957 1H1.8457V3H7.5957C7.73633 3 7.85352 2.97656 7.94727 2.92969C8.04622 2.88281 8.13737 2.82552 8.2207 2.75781C8.30924 2.6901 8.39779 2.61719 8.48633 2.53906C8.57487 2.45573 8.67643 2.38281 8.79102 2.32031C8.71289 2.23177 8.62956 2.11458 8.54102 1.96875C8.45768 1.81771 8.36654 1.67188 8.26758 1.53125C8.16862 1.38542 8.06185 1.26042 7.94727 1.15625C7.83789 1.05208 7.7207 1 7.5957 1ZM15.8457 11V3H10.3457C10.054 3 9.81706 3.02604 9.63477 3.07812C9.45768 3.125 9.30664 3.1849 9.18164 3.25781C9.06185 3.33073 8.95768 3.41146 8.86914 3.5C8.7806 3.58854 8.68164 3.66927 8.57227 3.74219C8.4681 3.8151 8.34049 3.8776 8.18945 3.92969C8.03841 3.97656 7.84049 4 7.5957 4H1.8457V11H15.8457Z" fill="#0078D4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -10,10 +10,10 @@
|
||||
"aiKey": "AIF-37eefaf0-8022-4671-a3fb-64752724682e",
|
||||
"engines": {
|
||||
"vscode": "*",
|
||||
"azdata": ">=1.22.0"
|
||||
"azdata": ">=1.25.0"
|
||||
},
|
||||
"activationEvents": [
|
||||
"onView:dataworkspace.views.main"
|
||||
"*"
|
||||
],
|
||||
"main": "./out/main",
|
||||
"repository": {
|
||||
@@ -30,17 +30,27 @@
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"description": ""
|
||||
},
|
||||
"projects.defaultProjectSaveLocation": {
|
||||
"type": "string",
|
||||
"description": "%projects.defaultProjectSaveLocation%"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"commands": [
|
||||
{
|
||||
"command": "projects.addProject",
|
||||
"title": "%add-project-command%",
|
||||
"category": "",
|
||||
"command": "projects.new",
|
||||
"title": "%new-command%",
|
||||
"category": "%data-workspace-view-container-name%",
|
||||
"icon": "$(add)"
|
||||
},
|
||||
{
|
||||
"command": "projects.openExisting",
|
||||
"title": "%open-existing-command%",
|
||||
"category": "%data-workspace-view-container-name%",
|
||||
"icon": "$(folder-opened)"
|
||||
},
|
||||
{
|
||||
"command": "dataworkspace.refresh",
|
||||
"title": "%refresh-workspace-command%",
|
||||
@@ -57,18 +67,22 @@
|
||||
{
|
||||
"command": "dataworkspace.refresh",
|
||||
"when": "view == dataworkspace.views.main",
|
||||
"group": "secondary"
|
||||
},
|
||||
{
|
||||
"command": "projects.new",
|
||||
"when": "view == dataworkspace.views.main",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "projects.addProject",
|
||||
"command": "projects.openExisting",
|
||||
"when": "view == dataworkspace.views.main",
|
||||
"group": "navigation"
|
||||
}
|
||||
],
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "projects.addProject",
|
||||
"when": "false"
|
||||
"command": "projects.new"
|
||||
},
|
||||
{
|
||||
"command": "dataworkspace.refresh",
|
||||
@@ -77,6 +91,9 @@
|
||||
{
|
||||
"command": "projects.removeProject",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "projects.openExisting"
|
||||
}
|
||||
],
|
||||
"view/item/context": [
|
||||
@@ -102,7 +119,8 @@
|
||||
"id": "dataworkspace.views.main",
|
||||
"name": "%main-view-name%",
|
||||
"contextualTitle": "%data-workspace-view-container-name%",
|
||||
"icon": "images/data-workspace.svg"
|
||||
"icon": "images/data-workspace.svg",
|
||||
"when": "isProjectProviderAvailable"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -111,6 +129,11 @@
|
||||
"view": "dataworkspace.views.main",
|
||||
"contents": "%projects-view-no-workspace-content%",
|
||||
"when": "workbenchState != workspace"
|
||||
},
|
||||
{
|
||||
"view": "dataworkspace.views.main",
|
||||
"contents": "%projects-view-no-project-content%",
|
||||
"when": "workbenchState == workspace && isProjectsViewEmpty"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
"extension-description": "Data workspace",
|
||||
"data-workspace-view-container-name": "Projects",
|
||||
"main-view-name": "Projects",
|
||||
"add-project-command": "Add Project",
|
||||
"new-command": "New",
|
||||
"refresh-workspace-command": "Refresh",
|
||||
"remove-project-command": "Remove Project",
|
||||
"projects-view-no-workspace-content": "To use projects, open a workspace and add projects to it, or use the 'Add Project' feature and we will create a workspace for you.\n[Open Workspace](command:workbench.action.openWorkspace)\n[Add Project](command:projects.addProject)"
|
||||
"projects-view-no-workspace-content": "[Create new](command:projects.new)\n[Open existing](command:projects.openExisting)\n",
|
||||
"projects-view-no-project-content": "No projects found in current workspace.\n[Create new](command:projects.new)\n[Open existing](command:projects.openExisting)\n",
|
||||
"open-existing-command": "Open existing",
|
||||
"projects.defaultProjectSaveLocation": "Full path to folder where new projects are saved by default."
|
||||
}
|
||||
|
||||
@@ -7,9 +7,48 @@ import { EOL } from 'os';
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export const ExtensionActivationErrorMessage = (extensionId: string, err: any): string => { return localize('activateExtensionFailed', "Failed to load the project provider extension '{0}'. Error message: {1}", extensionId, err.message ?? err); };
|
||||
export const UnknownProjectsErrorMessage = (projectFiles: string[]): string => { return localize('UnknownProjectsError', "No provider was found for the following projects: {0}", projectFiles.join(EOL)); };
|
||||
export const ExtensionActivationError = (extensionId: string, err: any): string => { return localize('activateExtensionFailed', "Failed to load the project provider extension '{0}'. Error message: {1}", extensionId, err.message ?? err); };
|
||||
export const UnknownProjectsError = (projectFiles: string[]): string => { return localize('UnknownProjectsError', "No provider was found for the following projects: {0}", projectFiles.join(EOL)); };
|
||||
|
||||
export const SelectProjectFileActionName = localize('SelectProjectFileActionName', "Select");
|
||||
export const AllProjectTypes = localize('AllProjectTypes', "All Project Types");
|
||||
export const ProviderNotFoundForProjectTypeError = (projectType: string): string => { return localize('UnknownProjectTypeError', "No provider was found for project type with id: '{0}'", projectType); };
|
||||
export const WorkspaceRequiredMessage = localize('dataworkspace.workspaceRequiredMessage', "A workspace is required in order to use the project feature.");
|
||||
export const OpenWorkspace = localize('dataworkspace.openWorkspace', "Open Workspace…");
|
||||
export const CreateWorkspaceConfirmation = localize('dataworkspace.createWorkspaceConfirmation', "A new workspace will be created and opened in order to open project. The Extension Host will restart and if there is a folder currently open, it will be closed.");
|
||||
export const EnterWorkspaceConfirmation = localize('dataworkspace.enterWorkspaceConfirmation', "To open this workspace, the Extension Host will restart and if there is a workspace or folder currently open, it will be closed.");
|
||||
|
||||
// UI
|
||||
export const OkButtonText = localize('dataworkspace.ok', "OK");
|
||||
export const CancelButtonText = localize('dataworkspace.cancel', "Cancel");
|
||||
export const BrowseButtonText = localize('dataworkspace.browse', "Browse");
|
||||
export const DefaultInputWidth = '400px';
|
||||
export const DefaultButtonWidth = '80px';
|
||||
|
||||
// New Project Dialog
|
||||
export const NewProjectDialogTitle = localize('dataworkspace.NewProjectDialogTitle', "Create new project");
|
||||
export const TypeTitle = localize('dataworkspace.Type', "Type");
|
||||
export const ProjectNameTitle = localize('dataworkspace.projectNameTitle', "Name");
|
||||
export const ProjectNamePlaceholder = localize('dataworkspace.projectNamePlaceholder', "Enter project name");
|
||||
export const ProjectLocationTitle = localize('dataworkspace.projectLocationTitle', "Location");
|
||||
export const ProjectLocationPlaceholder = localize('dataworkspace.projectLocationPlaceholder', "Enter project location");
|
||||
export const AddProjectToCurrentWorkspace = localize('dataworkspace.AddProjectToCurrentWorkspace', "This project will be added to the current workspace.");
|
||||
export const NewWorkspaceWillBeCreated = localize('dataworkspace.NewWorkspaceWillBeCreated', "A new workspace will be created for this project.");
|
||||
export const WorkspaceLocationTitle = localize('dataworkspace.workspaceLocationTitle', "Workspace location");
|
||||
export const ProjectParentDirectoryNotExistError = (location: string): string => { return localize('dataworkspace.projectParentDirectoryNotExistError', "The selected location: '{0}' does not exist or is not a directory.", location); };
|
||||
export const ProjectDirectoryAlreadyExistError = (projectName: string, location: string): string => { return localize('dataworkspace.projectDirectoryAlreadyExistError', "There is already a directory named '{0}' in the selected location: '{1}'.", projectName, location); };
|
||||
|
||||
//Open Existing Dialog
|
||||
export const OpenExistingDialogTitle = localize('dataworkspace.openExistingDialogTitle', "Open existing");
|
||||
export const ProjectFileNotExistError = (projectFilePath: string): string => { return localize('dataworkspace.projectFileNotExistError', "The selected project file '{0}' does not exist or is not a file.", projectFilePath); };
|
||||
export const WorkspaceFileNotExistError = (workspaceFilePath: string): string => { return localize('dataworkspace.workspaceFileNotExistError', "The selected workspace file '{0}' does not exist or is not a file.", workspaceFilePath); };
|
||||
export const Project = localize('dataworkspace.project', "Project");
|
||||
export const Workspace = localize('dataworkspace.workspace', "Workspace");
|
||||
export const LocationSelectorTitle = localize('dataworkspace.locationSelectorTitle', "Location");
|
||||
export const ProjectFilePlaceholder = localize('dataworkspace.projectFilePlaceholder', "Enter project location");
|
||||
export const WorkspacePlaceholder = localize('dataworkspace.workspacePlaceholder', "Enter workspace location");
|
||||
export const WorkspaceFileExtension = 'code-workspace';
|
||||
|
||||
// Workspace settings for saving new projects
|
||||
export const ProjectConfigurationKey = 'projects';
|
||||
export const ProjectSaveLocationKey = 'defaultProjectSaveLocation';
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { IExtension } from 'dataworkspace';
|
||||
import { WorkspaceService } from '../services/workspaceService';
|
||||
import { defaultProjectSaveLocation } from './projectLocationHelper';
|
||||
|
||||
export class DataWorkspaceExtension implements IExtension {
|
||||
constructor(private workspaceService: WorkspaceService) {
|
||||
}
|
||||
|
||||
getProjectsInWorkspace(): vscode.Uri[] {
|
||||
return this.workspaceService.getProjectsInWorkspace();
|
||||
}
|
||||
|
||||
addProjectsToWorkspace(projectFiles: vscode.Uri[]): Promise<void> {
|
||||
return this.workspaceService.addProjectsToWorkspace(projectFiles);
|
||||
}
|
||||
|
||||
showProjectsView(): void {
|
||||
vscode.commands.executeCommand('dataworkspace.views.main.focus');
|
||||
}
|
||||
|
||||
get defaultProjectSaveLocation(): vscode.Uri | undefined {
|
||||
return defaultProjectSaveLocation();
|
||||
}
|
||||
}
|
||||
38
extensions/data-workspace/src/common/iconHelper.ts
Normal file
38
extensions/data-workspace/src/common/iconHelper.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export interface IconPath {
|
||||
dark: string;
|
||||
light: string;
|
||||
}
|
||||
|
||||
export class IconPathHelper {
|
||||
private static extensionContext: vscode.ExtensionContext;
|
||||
public static folder: IconPath;
|
||||
|
||||
public static setExtensionContext(extensionContext: vscode.ExtensionContext) {
|
||||
IconPathHelper.extensionContext = extensionContext;
|
||||
|
||||
IconPathHelper.folder = IconPathHelper.makeIcon('folder', true);
|
||||
}
|
||||
|
||||
private static makeIcon(name: string, sameIcon: boolean = false) {
|
||||
const folder = 'images';
|
||||
|
||||
if (sameIcon) {
|
||||
return {
|
||||
dark: IconPathHelper.extensionContext.asAbsolutePath(`${folder}/${name}.svg`),
|
||||
light: IconPathHelper.extensionContext.asAbsolutePath(`${folder}/${name}.svg`)
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
dark: IconPathHelper.extensionContext.asAbsolutePath(`${folder}/dark/${name}.svg`),
|
||||
light: IconPathHelper.extensionContext.asAbsolutePath(`${folder}/light/${name}.svg`)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,9 +26,15 @@ export interface IProjectProviderRegistry {
|
||||
*/
|
||||
readonly providers: IProjectProvider[];
|
||||
|
||||
/**
|
||||
* Gets the project provider for the specified project extension
|
||||
* @param extension The file extension of the project
|
||||
*/
|
||||
getProviderByProjectExtension(extension: string): IProjectProvider | undefined;
|
||||
|
||||
/**
|
||||
* Gets the project provider for the specified project type
|
||||
* @param projectType The project type, file extension of the project
|
||||
* @param projectType The id of the project type
|
||||
*/
|
||||
getProviderByProjectType(projectType: string): IProjectProvider | undefined;
|
||||
}
|
||||
@@ -45,7 +51,7 @@ export interface IWorkspaceService {
|
||||
/**
|
||||
* Gets the project files in current workspace
|
||||
*/
|
||||
getProjectsInWorkspace(): Promise<vscode.Uri[]>;
|
||||
getProjectsInWorkspace(): vscode.Uri[];
|
||||
|
||||
/**
|
||||
* Gets the project provider by project file
|
||||
@@ -65,8 +71,28 @@ export interface IWorkspaceService {
|
||||
*/
|
||||
removeProject(projectFile: vscode.Uri): Promise<void>;
|
||||
|
||||
/**
|
||||
* Creates a new project from workspace
|
||||
* @param name The name of the project
|
||||
* @param location The location of the project
|
||||
* @param projectTypeId The project type id
|
||||
*/
|
||||
createProject(name: string, location: vscode.Uri, projectTypeId: string): Promise<vscode.Uri>;
|
||||
|
||||
readonly isProjectProviderAvailable: boolean;
|
||||
|
||||
/**
|
||||
* Event fires when projects in workspace changes
|
||||
*/
|
||||
readonly onDidWorkspaceProjectsChange: vscode.Event<void>;
|
||||
|
||||
/**
|
||||
* Verify that a workspace is open or if one isn't, ask user to pick whether a workspace should be automatically created
|
||||
*/
|
||||
validateWorkspace(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Shows confirmation message that the extension host will be restarted and current workspace/file will be closed. If confirmed, the specified workspace will be entered.
|
||||
*/
|
||||
enterWorkspace(workspaceFile: vscode.Uri): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as constants from '../common/constants';
|
||||
|
||||
/**
|
||||
* Returns the default location to save a new database project
|
||||
*/
|
||||
export function defaultProjectSaveLocation(): vscode.Uri | undefined {
|
||||
return projectSaveLocationSettingIsValid() ? vscode.Uri.file(projectSaveLocationSetting()) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get workspace configurations for this extension
|
||||
*/
|
||||
function config(): vscode.WorkspaceConfiguration {
|
||||
return vscode.workspace.getConfiguration(constants.ProjectConfigurationKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the workspace setting on the default location to save new database projects
|
||||
*/
|
||||
function projectSaveLocationSetting(): string {
|
||||
return config()[constants.ProjectSaveLocationKey];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the default save location for new database projects workspace setting exists and is
|
||||
* a valid path
|
||||
*/
|
||||
function projectSaveLocationSettingIsValid(): boolean {
|
||||
return projectSaveLocationSettingExists() && fs.existsSync(projectSaveLocationSetting());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if a value for the default save location for new database projects exists
|
||||
*/
|
||||
function projectSaveLocationSettingExists(): boolean {
|
||||
return projectSaveLocationSetting() !== undefined && projectSaveLocationSetting() !== null
|
||||
&& projectSaveLocationSetting().trim() !== '';
|
||||
}
|
||||
|
||||
@@ -9,20 +9,24 @@ import { IProjectProviderRegistry } from './interfaces';
|
||||
|
||||
export const ProjectProviderRegistry: IProjectProviderRegistry = new class implements IProjectProviderRegistry {
|
||||
private _providers = new Array<IProjectProvider>();
|
||||
private _providerMapping: { [key: string]: IProjectProvider } = {};
|
||||
private _providerFileExtensionMapping: { [key: string]: IProjectProvider } = {};
|
||||
private _providerProjectTypeMapping: { [key: string]: IProjectProvider } = {};
|
||||
|
||||
|
||||
registerProvider(provider: IProjectProvider): vscode.Disposable {
|
||||
this.validateProvider(provider);
|
||||
this._providers.push(provider);
|
||||
provider.supportedProjectTypes.forEach(projectType => {
|
||||
this._providerMapping[projectType.projectFileExtension.toUpperCase()] = provider;
|
||||
this._providerFileExtensionMapping[projectType.projectFileExtension.toUpperCase()] = provider;
|
||||
this._providerProjectTypeMapping[projectType.id.toUpperCase()] = provider;
|
||||
});
|
||||
return new vscode.Disposable(() => {
|
||||
const idx = this._providers.indexOf(provider);
|
||||
if (idx >= 0) {
|
||||
this._providers.splice(idx, 1);
|
||||
provider.supportedProjectTypes.forEach(projectType => {
|
||||
delete this._providerMapping[projectType.projectFileExtension.toUpperCase()];
|
||||
delete this._providerFileExtensionMapping[projectType.projectFileExtension.toUpperCase()];
|
||||
delete this._providerProjectTypeMapping[projectType.id.toUpperCase()];
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -39,7 +43,11 @@ export const ProjectProviderRegistry: IProjectProviderRegistry = new class imple
|
||||
validateProvider(provider: IProjectProvider): void {
|
||||
}
|
||||
|
||||
getProviderByProjectExtension(extension: string): IProjectProvider | undefined {
|
||||
return extension ? this._providerFileExtensionMapping[extension.toUpperCase()] : undefined;
|
||||
}
|
||||
|
||||
getProviderByProjectType(projectType: string): IProjectProvider | undefined {
|
||||
return projectType ? this._providerMapping[projectType.toUpperCase()] : undefined;
|
||||
return projectType ? this._providerProjectTypeMapping[projectType.toUpperCase()] : undefined;
|
||||
}
|
||||
};
|
||||
|
||||
31
extensions/data-workspace/src/common/utils.ts
Normal file
31
extensions/data-workspace/src/common/utils.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
|
||||
export async function directoryExist(directoryPath: string): Promise<boolean> {
|
||||
const stats = await getFileStatus(directoryPath);
|
||||
return stats ? stats.isDirectory() : false;
|
||||
}
|
||||
|
||||
export async function fileExist(filePath: string): Promise<boolean> {
|
||||
const stats = await getFileStatus(filePath);
|
||||
return stats ? stats.isFile() : false;
|
||||
}
|
||||
|
||||
async function getFileStatus(path: string): Promise<fs.Stats | undefined> {
|
||||
try {
|
||||
const stats = await fs.promises.stat(path);
|
||||
return stats;
|
||||
}
|
||||
catch (e) {
|
||||
if (e.code === 'ENOENT') {
|
||||
return undefined;
|
||||
}
|
||||
else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { IWorkspaceService } from './interfaces';
|
||||
import { UnknownProjectsErrorMessage } from './constants';
|
||||
import { UnknownProjectsError } from './constants';
|
||||
import { WorkspaceTreeItem } from 'dataworkspace';
|
||||
|
||||
/**
|
||||
@@ -37,6 +37,7 @@ export class WorkspaceTreeDataProvider implements vscode.TreeDataProvider<Worksp
|
||||
else {
|
||||
// if the element is undefined return the project tree items
|
||||
const projects = await this._workspaceService.getProjectsInWorkspace();
|
||||
await vscode.commands.executeCommand('setContext', 'isProjectsViewEmpty', projects.length === 0);
|
||||
const unknownProjects: string[] = [];
|
||||
const treeItems: WorkspaceTreeItem[] = [];
|
||||
for (const project of projects) {
|
||||
@@ -60,7 +61,7 @@ export class WorkspaceTreeDataProvider implements vscode.TreeDataProvider<Worksp
|
||||
});
|
||||
}
|
||||
if (unknownProjects.length > 0) {
|
||||
vscode.window.showErrorMessage(UnknownProjectsErrorMessage(unknownProjects));
|
||||
vscode.window.showErrorMessage(UnknownProjectsError(unknownProjects));
|
||||
}
|
||||
return treeItems;
|
||||
}
|
||||
|
||||
39
extensions/data-workspace/src/dataworkspace.d.ts
vendored
39
extensions/data-workspace/src/dataworkspace.d.ts
vendored
@@ -14,11 +14,25 @@ declare module 'dataworkspace' {
|
||||
*/
|
||||
export interface IExtension {
|
||||
/**
|
||||
* register a project provider
|
||||
* @param provider new project provider
|
||||
* @requires a disposable object, upon disposal, the provider will be unregistered.
|
||||
* Returns all the projects in the workspace
|
||||
*/
|
||||
registerProjectProvider(provider: IProjectProvider): vscode.Disposable;
|
||||
getProjectsInWorkspace(): vscode.Uri[];
|
||||
|
||||
/**
|
||||
* Add projects to the workspace
|
||||
* @param projectFiles Uris of project files to add
|
||||
*/
|
||||
addProjectsToWorkspace(projectFiles: vscode.Uri[]): Promise<void>
|
||||
|
||||
/**
|
||||
* Change focus to Projects view
|
||||
*/
|
||||
showProjectsView(): void;
|
||||
|
||||
/**
|
||||
* Returns the default location to save projects
|
||||
*/
|
||||
defaultProjectSaveLocation: vscode.Uri | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,6 +51,13 @@ declare module 'dataworkspace' {
|
||||
*/
|
||||
RemoveProject(projectFile: vscode.Uri): Promise<void>;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param name Create a project
|
||||
* @param location the parent directory of the project
|
||||
*/
|
||||
createProject(name: string, location: vscode.Uri): Promise<vscode.Uri>;
|
||||
|
||||
/**
|
||||
* Gets the supported project types
|
||||
*/
|
||||
@@ -47,11 +68,21 @@ declare module 'dataworkspace' {
|
||||
* Defines the project type
|
||||
*/
|
||||
export interface IProjectType {
|
||||
/**
|
||||
* id of the project type
|
||||
*/
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* display name of the project type
|
||||
*/
|
||||
readonly displayName: string;
|
||||
|
||||
/**
|
||||
* description of the project type
|
||||
*/
|
||||
readonly description: string;
|
||||
|
||||
/**
|
||||
* project file extension, e.g. sqlproj
|
||||
*/
|
||||
|
||||
125
extensions/data-workspace/src/dialogs/dialogBase.ts
Normal file
125
extensions/data-workspace/src/dialogs/dialogBase.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 path from 'path';
|
||||
import * as constants from '../common/constants';
|
||||
|
||||
interface Deferred<T> {
|
||||
resolve: (result: T | Promise<T>) => void;
|
||||
reject: (reason: any) => void;
|
||||
}
|
||||
|
||||
export abstract class DialogBase {
|
||||
protected _toDispose: vscode.Disposable[] = [];
|
||||
protected _dialogObject: azdata.window.Dialog;
|
||||
protected initDialogComplete: Deferred<void> | undefined;
|
||||
protected initDialogPromise: Promise<void> = new Promise<void>((resolve, reject) => this.initDialogComplete = { resolve, reject });
|
||||
protected workspaceFormComponent: azdata.FormComponent | undefined;
|
||||
protected workspaceInputBox: azdata.InputBoxComponent | undefined;
|
||||
|
||||
constructor(dialogTitle: string, dialogName: string, dialogWidth: azdata.window.DialogWidth = 600) {
|
||||
this._dialogObject = azdata.window.createModelViewDialog(dialogTitle, dialogName, dialogWidth);
|
||||
this._dialogObject.okButton.label = constants.OkButtonText;
|
||||
this.register(this._dialogObject.cancelButton.onClick(() => this.onCancelButtonClicked()));
|
||||
this.register(this._dialogObject.okButton.onClick(() => this.onOkButtonClicked()));
|
||||
this._dialogObject.registerCloseValidator(async () => {
|
||||
return this.validate();
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract initialize(view: azdata.ModelView): Promise<void>;
|
||||
|
||||
protected async validate(): Promise<boolean> {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
public async open(): Promise<void> {
|
||||
const tab = azdata.window.createTab('');
|
||||
tab.registerContent(async (view: azdata.ModelView) => {
|
||||
return this.initialize(view);
|
||||
});
|
||||
this._dialogObject.content = [tab];
|
||||
azdata.window.openDialog(this._dialogObject);
|
||||
await this.initDialogPromise;
|
||||
}
|
||||
|
||||
private onCancelButtonClicked(): void {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
private async onOkButtonClicked(): Promise<void> {
|
||||
await this.onComplete();
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
protected async onComplete(): Promise<void> {
|
||||
}
|
||||
|
||||
protected dispose(): void {
|
||||
this._toDispose.forEach(disposable => disposable.dispose());
|
||||
}
|
||||
|
||||
protected register(disposable: vscode.Disposable): void {
|
||||
this._toDispose.push(disposable);
|
||||
}
|
||||
|
||||
protected showErrorMessage(message: string): void {
|
||||
this._dialogObject.message = {
|
||||
text: message,
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
}
|
||||
|
||||
protected createHorizontalContainer(view: azdata.ModelView, items: azdata.Component[]): azdata.FlexContainer {
|
||||
return view.modelBuilder.flexContainer().withItems(items, { CSSStyles: { 'margin-right': '5px', 'margin-bottom': '10px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates container with information on which workspace the project will be added to and where the workspace will be
|
||||
* created if no workspace is currently open
|
||||
* @param view
|
||||
*/
|
||||
protected createWorkspaceContainer(view: azdata.ModelView): azdata.FormComponent {
|
||||
const workspaceDescription = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: vscode.workspace.workspaceFile ? constants.AddProjectToCurrentWorkspace : constants.NewWorkspaceWillBeCreated,
|
||||
CSSStyles: { 'margin-top': '3px', 'margin-bottom': '10px' }
|
||||
}).component();
|
||||
|
||||
this.workspaceInputBox = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
|
||||
ariaLabel: constants.WorkspaceLocationTitle,
|
||||
width: constants.DefaultInputWidth,
|
||||
enabled: false,
|
||||
value: vscode.workspace.workspaceFile?.fsPath ?? '',
|
||||
title: vscode.workspace.workspaceFile?.fsPath ?? '' // hovertext for if file path is too long to be seen in textbox
|
||||
}).component();
|
||||
|
||||
const container = view.modelBuilder.flexContainer()
|
||||
.withItems([workspaceDescription, this.workspaceInputBox])
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
|
||||
this.workspaceFormComponent = {
|
||||
title: constants.Workspace,
|
||||
component: container
|
||||
};
|
||||
|
||||
return this.workspaceFormComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the workspace inputbox based on the passed in location and name if there isn't a workspace currently open
|
||||
* @param location
|
||||
* @param name
|
||||
*/
|
||||
protected updateWorkspaceInputbox(location: string, name: string): void {
|
||||
if (!vscode.workspace.workspaceFile) {
|
||||
const fileLocation = location && name ? path.join(location, `${name}.code-workspace`) : '';
|
||||
this.workspaceInputBox!.value = fileLocation;
|
||||
this.workspaceInputBox!.title = fileLocation;
|
||||
}
|
||||
}
|
||||
}
|
||||
171
extensions/data-workspace/src/dialogs/newProjectDialog.ts
Normal file
171
extensions/data-workspace/src/dialogs/newProjectDialog.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 path from 'path';
|
||||
import { DialogBase } from './dialogBase';
|
||||
import { IWorkspaceService } from '../common/interfaces';
|
||||
import * as constants from '../common/constants';
|
||||
import { IProjectType } from 'dataworkspace';
|
||||
import { directoryExist } from '../common/utils';
|
||||
import { IconPathHelper } from '../common/iconHelper';
|
||||
import { defaultProjectSaveLocation } from '../common/projectLocationHelper';
|
||||
|
||||
class NewProjectDialogModel {
|
||||
projectTypeId: string = '';
|
||||
projectFileExtension: string = '';
|
||||
name: string = '';
|
||||
location: string = '';
|
||||
}
|
||||
export class NewProjectDialog extends DialogBase {
|
||||
public model: NewProjectDialogModel = new NewProjectDialogModel();
|
||||
|
||||
constructor(private workspaceService: IWorkspaceService) {
|
||||
super(constants.NewProjectDialogTitle, 'NewProject');
|
||||
}
|
||||
|
||||
async validate(): Promise<boolean> {
|
||||
try {
|
||||
// the selected location should be an existing directory
|
||||
const parentDirectoryExists = await directoryExist(this.model.location);
|
||||
if (!parentDirectoryExists) {
|
||||
this.showErrorMessage(constants.ProjectParentDirectoryNotExistError(this.model.location));
|
||||
return false;
|
||||
}
|
||||
|
||||
// there shouldn't be an existing sub directory with the same name as the project in the selected location
|
||||
const projectDirectoryExists = await directoryExist(path.join(this.model.location, this.model.name));
|
||||
if (projectDirectoryExists) {
|
||||
this.showErrorMessage(constants.ProjectDirectoryAlreadyExistError(this.model.name, this.model.location));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (err) {
|
||||
this.showErrorMessage(err?.message ? err.message : err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async onComplete(): Promise<void> {
|
||||
try {
|
||||
const validateWorkspace = await this.workspaceService.validateWorkspace();
|
||||
if (validateWorkspace) {
|
||||
await this.workspaceService.createProject(this.model.name, vscode.Uri.file(this.model.location), this.model.projectTypeId);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
vscode.window.showErrorMessage(err?.message ? err.message : err);
|
||||
}
|
||||
}
|
||||
|
||||
protected async initialize(view: azdata.ModelView): Promise<void> {
|
||||
const allProjectTypes = await this.workspaceService.getAllProjectTypes();
|
||||
const projectTypeRadioCardGroup = view.modelBuilder.radioCardGroup().withProperties<azdata.RadioCardGroupComponentProperties>({
|
||||
cards: allProjectTypes.map((projectType: IProjectType) => {
|
||||
return <azdata.RadioCard>{
|
||||
id: projectType.id,
|
||||
label: projectType.displayName,
|
||||
icon: projectType.icon,
|
||||
descriptions: [
|
||||
{
|
||||
textValue: projectType.displayName,
|
||||
textStyles: {
|
||||
'font-size': '13px',
|
||||
'font-weight': 'bold'
|
||||
}
|
||||
}, {
|
||||
textValue: projectType.description
|
||||
}
|
||||
]
|
||||
};
|
||||
}),
|
||||
iconHeight: '50px',
|
||||
iconWidth: '50px',
|
||||
cardWidth: '170px',
|
||||
cardHeight: '170px',
|
||||
ariaLabel: constants.TypeTitle,
|
||||
width: '500px',
|
||||
iconPosition: 'top',
|
||||
selectedCardId: allProjectTypes.length > 0 ? allProjectTypes[0].id : undefined
|
||||
}).component();
|
||||
|
||||
this.register(projectTypeRadioCardGroup.onSelectionChanged((e) => {
|
||||
this.model.projectTypeId = e.cardId;
|
||||
}));
|
||||
|
||||
const projectNameTextBox = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
|
||||
ariaLabel: constants.ProjectNameTitle,
|
||||
placeHolder: constants.ProjectNamePlaceholder,
|
||||
required: true,
|
||||
width: constants.DefaultInputWidth
|
||||
}).component();
|
||||
|
||||
this.register(projectNameTextBox.onTextChanged(() => {
|
||||
this.model.name = projectNameTextBox.value!;
|
||||
projectNameTextBox.updateProperty('title', projectNameTextBox.value);
|
||||
|
||||
this.updateWorkspaceInputbox(this.model.location, this.model.name);
|
||||
}));
|
||||
|
||||
const locationTextBox = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
|
||||
ariaLabel: constants.ProjectLocationTitle,
|
||||
placeHolder: constants.ProjectLocationPlaceholder,
|
||||
required: true,
|
||||
width: constants.DefaultInputWidth
|
||||
}).component();
|
||||
|
||||
this.register(locationTextBox.onTextChanged(() => {
|
||||
this.model.location = locationTextBox.value!;
|
||||
locationTextBox.updateProperty('title', locationTextBox.value);
|
||||
this.updateWorkspaceInputbox(this.model.location, this.model.name);
|
||||
}));
|
||||
|
||||
const browseFolderButton = view.modelBuilder.button().withProperties<azdata.ButtonProperties>({
|
||||
ariaLabel: constants.BrowseButtonText,
|
||||
iconPath: IconPathHelper.folder,
|
||||
height: '16px',
|
||||
width: '18px'
|
||||
}).component();
|
||||
this.register(browseFolderButton.onDidClick(async () => {
|
||||
let folderUris = await vscode.window.showOpenDialog({
|
||||
canSelectFiles: false,
|
||||
canSelectFolders: true,
|
||||
canSelectMany: false,
|
||||
defaultUri: defaultProjectSaveLocation()
|
||||
});
|
||||
if (!folderUris || folderUris.length === 0) {
|
||||
return;
|
||||
}
|
||||
const selectedFolder = folderUris[0].fsPath;
|
||||
locationTextBox.value = selectedFolder;
|
||||
this.model.location = selectedFolder;
|
||||
|
||||
this.updateWorkspaceInputbox(this.model.location, this.model.name);
|
||||
}));
|
||||
|
||||
const form = view.modelBuilder.formContainer().withFormItems([
|
||||
{
|
||||
title: constants.TypeTitle,
|
||||
required: true,
|
||||
component: projectTypeRadioCardGroup
|
||||
},
|
||||
{
|
||||
title: constants.ProjectNameTitle,
|
||||
required: true,
|
||||
component: this.createHorizontalContainer(view, [projectNameTextBox])
|
||||
}, {
|
||||
title: constants.ProjectLocationTitle,
|
||||
required: true,
|
||||
component: this.createHorizontalContainer(view, [locationTextBox, browseFolderButton])
|
||||
},
|
||||
this.createWorkspaceContainer(view)
|
||||
]).component();
|
||||
await view.initializeModel(form);
|
||||
this.initDialogComplete?.resolve();
|
||||
}
|
||||
}
|
||||
208
extensions/data-workspace/src/dialogs/openExistingDialog.ts
Normal file
208
extensions/data-workspace/src/dialogs/openExistingDialog.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 path from 'path';
|
||||
import { DialogBase } from './dialogBase';
|
||||
import * as constants from '../common/constants';
|
||||
import { IWorkspaceService } from '../common/interfaces';
|
||||
import { fileExist } from '../common/utils';
|
||||
import { IconPathHelper } from '../common/iconHelper';
|
||||
|
||||
export class OpenExistingDialog extends DialogBase {
|
||||
public _projectFile: string = '';
|
||||
public _workspaceFile: string = '';
|
||||
public _targetTypeRadioCardGroup: azdata.RadioCardGroupComponent | undefined;
|
||||
public _filePathTextBox: azdata.InputBoxComponent | undefined;
|
||||
public formBuilder: azdata.FormBuilder | undefined;
|
||||
|
||||
private _targetTypes = [
|
||||
{
|
||||
name: constants.Project,
|
||||
icon: {
|
||||
dark: this.extensionContext.asAbsolutePath('images/file_inverse.svg'),
|
||||
light: this.extensionContext.asAbsolutePath('images/file.svg')
|
||||
}
|
||||
}, {
|
||||
name: constants.Workspace,
|
||||
icon: {
|
||||
dark: this.extensionContext.asAbsolutePath('images/file_inverse.svg'), // temporary - still waiting for real icon from UX
|
||||
light: this.extensionContext.asAbsolutePath('images/file.svg')
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
constructor(private workspaceService: IWorkspaceService, private extensionContext: vscode.ExtensionContext) {
|
||||
super(constants.OpenExistingDialogTitle, 'OpenProject');
|
||||
}
|
||||
|
||||
async validate(): Promise<boolean> {
|
||||
try {
|
||||
// the selected location should be an existing directory
|
||||
if (this._targetTypeRadioCardGroup?.selectedCardId === constants.Project) {
|
||||
const fileExists = await fileExist(this._projectFile);
|
||||
if (!fileExists) {
|
||||
this.showErrorMessage(constants.ProjectFileNotExistError(this._projectFile));
|
||||
return false;
|
||||
}
|
||||
} else if (this._targetTypeRadioCardGroup?.selectedCardId === constants.Workspace) {
|
||||
const fileExists = await fileExist(this._workspaceFile);
|
||||
if (!fileExists) {
|
||||
this.showErrorMessage(constants.WorkspaceFileNotExistError(this._workspaceFile));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (err) {
|
||||
this.showErrorMessage(err?.message ? err.message : err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async onComplete(): Promise<void> {
|
||||
try {
|
||||
if (this._targetTypeRadioCardGroup?.selectedCardId === constants.Workspace) {
|
||||
await this.workspaceService.enterWorkspace(vscode.Uri.file(this._workspaceFile));
|
||||
} else {
|
||||
const validateWorkspace = await this.workspaceService.validateWorkspace();
|
||||
if (validateWorkspace) {
|
||||
await this.workspaceService.addProjectsToWorkspace([vscode.Uri.file(this._projectFile)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
vscode.window.showErrorMessage(err?.message ? err.message : err);
|
||||
}
|
||||
}
|
||||
|
||||
protected async initialize(view: azdata.ModelView): Promise<void> {
|
||||
this._targetTypeRadioCardGroup = view.modelBuilder.radioCardGroup().withProperties<azdata.RadioCardGroupComponentProperties>({
|
||||
cards: this._targetTypes.map((targetType) => {
|
||||
return <azdata.RadioCard>{
|
||||
id: targetType.name,
|
||||
label: targetType.name,
|
||||
icon: targetType.icon,
|
||||
descriptions: [
|
||||
{
|
||||
textValue: targetType.name,
|
||||
textStyles: {
|
||||
'font-size': '13px'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}),
|
||||
iconHeight: '50px',
|
||||
iconWidth: '50px',
|
||||
cardWidth: '170px',
|
||||
cardHeight: '170px',
|
||||
ariaLabel: constants.TypeTitle,
|
||||
width: '500px',
|
||||
iconPosition: 'top',
|
||||
selectedCardId: constants.Project
|
||||
}).component();
|
||||
|
||||
this._filePathTextBox = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
|
||||
ariaLabel: constants.LocationSelectorTitle,
|
||||
placeHolder: constants.ProjectFilePlaceholder,
|
||||
required: true,
|
||||
width: constants.DefaultInputWidth
|
||||
}).component();
|
||||
this.register(this._filePathTextBox.onTextChanged(() => {
|
||||
this._projectFile = this._filePathTextBox!.value!;
|
||||
this._filePathTextBox!.updateProperty('title', this._projectFile);
|
||||
this.updateWorkspaceInputbox(path.dirname(this._projectFile), path.basename(this._projectFile, path.extname(this._projectFile)));
|
||||
}));
|
||||
|
||||
const browseFolderButton = view.modelBuilder.button().withProperties<azdata.ButtonProperties>({
|
||||
ariaLabel: constants.BrowseButtonText,
|
||||
iconPath: IconPathHelper.folder,
|
||||
width: '18px',
|
||||
height: '16px',
|
||||
}).component();
|
||||
this.register(browseFolderButton.onDidClick(async () => {
|
||||
if (this._targetTypeRadioCardGroup?.selectedCardId === constants.Project) {
|
||||
await this.projectBrowse();
|
||||
} else if (this._targetTypeRadioCardGroup?.selectedCardId === constants.Workspace) {
|
||||
await this.workspaceBrowse();
|
||||
}
|
||||
}));
|
||||
|
||||
this.register(this._targetTypeRadioCardGroup.onSelectionChanged(({ cardId }) => {
|
||||
if (cardId === constants.Project) {
|
||||
this._filePathTextBox!.placeHolder = constants.ProjectFilePlaceholder;
|
||||
this.formBuilder?.addFormItem(this.workspaceFormComponent!);
|
||||
} else if (cardId === constants.Workspace) {
|
||||
this._filePathTextBox!.placeHolder = constants.WorkspacePlaceholder;
|
||||
this.formBuilder?.removeFormItem(this.workspaceFormComponent!);
|
||||
}
|
||||
|
||||
// clear selected file textbox
|
||||
this._filePathTextBox!.value = '';
|
||||
}));
|
||||
|
||||
this.formBuilder = view.modelBuilder.formContainer().withFormItems([
|
||||
{
|
||||
title: constants.TypeTitle,
|
||||
required: true,
|
||||
component: this._targetTypeRadioCardGroup,
|
||||
}, {
|
||||
title: constants.LocationSelectorTitle,
|
||||
required: true,
|
||||
component: this.createHorizontalContainer(view, [this._filePathTextBox, browseFolderButton])
|
||||
},
|
||||
this.createWorkspaceContainer(view)
|
||||
]);
|
||||
await view.initializeModel(this.formBuilder?.component());
|
||||
this.initDialogComplete?.resolve();
|
||||
}
|
||||
|
||||
public async workspaceBrowse(): Promise<void> {
|
||||
const filters: { [name: string]: string[] } = { [constants.Workspace]: [constants.WorkspaceFileExtension] };
|
||||
const fileUris = await vscode.window.showOpenDialog({
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: false,
|
||||
openLabel: constants.SelectProjectFileActionName,
|
||||
filters: filters
|
||||
});
|
||||
|
||||
if (!fileUris || fileUris.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const workspaceFilePath = fileUris[0].fsPath;
|
||||
this._filePathTextBox!.value = workspaceFilePath;
|
||||
this._workspaceFile = workspaceFilePath;
|
||||
}
|
||||
|
||||
public async projectBrowse(): Promise<void> {
|
||||
const filters: { [name: string]: string[] } = {};
|
||||
const projectTypes = await this.workspaceService.getAllProjectTypes();
|
||||
filters[constants.AllProjectTypes] = projectTypes.map(type => type.projectFileExtension);
|
||||
projectTypes.forEach(type => {
|
||||
filters[type.displayName] = [type.projectFileExtension];
|
||||
});
|
||||
|
||||
const fileUris = await vscode.window.showOpenDialog({
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: false,
|
||||
openLabel: constants.SelectProjectFileActionName,
|
||||
filters: filters
|
||||
});
|
||||
|
||||
if (!fileUris || fileUris.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const projectFilePath = fileUris[0].fsPath;
|
||||
this._filePathTextBox!.value = projectFilePath;
|
||||
this._projectFile = projectFilePath;
|
||||
}
|
||||
}
|
||||
@@ -4,48 +4,48 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { WorkspaceTreeDataProvider } from './common/workspaceTreeDataProvider';
|
||||
import { WorkspaceService } from './services/workspaceService';
|
||||
import { AllProjectTypes, SelectProjectFileActionName } from './common/constants';
|
||||
import { WorkspaceTreeItem } from 'dataworkspace';
|
||||
import { WorkspaceTreeItem, IExtension } from 'dataworkspace';
|
||||
import { DataWorkspaceExtension } from './common/dataWorkspaceExtension';
|
||||
import { NewProjectDialog } from './dialogs/newProjectDialog';
|
||||
import { OpenExistingDialog } from './dialogs/openExistingDialog';
|
||||
import { IWorkspaceService } from './common/interfaces';
|
||||
import { IconPathHelper } from './common/iconHelper';
|
||||
|
||||
export function activate(context: vscode.ExtensionContext): void {
|
||||
const workspaceService = new WorkspaceService();
|
||||
export function activate(context: vscode.ExtensionContext): Promise<IExtension> {
|
||||
const workspaceService = new WorkspaceService(context);
|
||||
workspaceService.loadTempProjects();
|
||||
const workspaceTreeDataProvider = new WorkspaceTreeDataProvider(workspaceService);
|
||||
const dataWorkspaceExtension = new DataWorkspaceExtension(workspaceService);
|
||||
context.subscriptions.push(vscode.window.registerTreeDataProvider('dataworkspace.views.main', workspaceTreeDataProvider));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('projects.addProject', async () => {
|
||||
// To Sakshi - You can replace the implementation with your complete dialog implementation
|
||||
// but all the code here should be reusable by you
|
||||
if (vscode.workspace.workspaceFile) {
|
||||
const filters: { [name: string]: string[] } = {};
|
||||
const projectTypes = await workspaceService.getAllProjectTypes();
|
||||
filters[AllProjectTypes] = projectTypes.map(type => type.projectFileExtension);
|
||||
projectTypes.forEach(type => {
|
||||
filters[type.displayName] = [type.projectFileExtension];
|
||||
});
|
||||
let fileUris = await vscode.window.showOpenDialog({
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: false,
|
||||
defaultUri: vscode.Uri.file(path.dirname(vscode.workspace.workspaceFile.path)),
|
||||
openLabel: SelectProjectFileActionName,
|
||||
filters: filters
|
||||
});
|
||||
if (!fileUris || fileUris.length === 0) {
|
||||
return;
|
||||
}
|
||||
await workspaceService.addProjectsToWorkspace(fileUris);
|
||||
}
|
||||
context.subscriptions.push(vscode.extensions.onDidChange(() => {
|
||||
setProjectProviderContextValue(workspaceService);
|
||||
}));
|
||||
setProjectProviderContextValue(workspaceService);
|
||||
context.subscriptions.push(vscode.commands.registerCommand('projects.new', async () => {
|
||||
const dialog = new NewProjectDialog(workspaceService);
|
||||
await dialog.open();
|
||||
}));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('projects.openExisting', async () => {
|
||||
const dialog = new OpenExistingDialog(workspaceService, context);
|
||||
await dialog.open();
|
||||
|
||||
}));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('dataworkspace.refresh', () => {
|
||||
workspaceTreeDataProvider.refresh();
|
||||
}));
|
||||
|
||||
context.subscriptions.push(vscode.commands.registerCommand('projects.removeProject', async (treeItem: WorkspaceTreeItem) => {
|
||||
await workspaceService.removeProject(vscode.Uri.file(treeItem.element.project.projectFilePath));
|
||||
}));
|
||||
|
||||
IconPathHelper.setExtensionContext(context);
|
||||
|
||||
return Promise.resolve(dataWorkspaceExtension);
|
||||
}
|
||||
|
||||
function setProjectProviderContextValue(workspaceService: IWorkspaceService): void {
|
||||
vscode.commands.executeCommand('setContext', 'isProjectProviderAvailable', workspaceService.isProjectProviderAvailable);
|
||||
}
|
||||
|
||||
export function deactivate(): void {
|
||||
|
||||
@@ -3,50 +3,137 @@
|
||||
* 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 dataworkspace from 'dataworkspace';
|
||||
import * as path from 'path';
|
||||
import * as constants from '../common/constants';
|
||||
import { IWorkspaceService } from '../common/interfaces';
|
||||
import { ProjectProviderRegistry } from '../common/projectProviderRegistry';
|
||||
import Logger from '../common/logger';
|
||||
import { ExtensionActivationErrorMessage } from '../common/constants';
|
||||
|
||||
const WorkspaceConfigurationName = 'dataworkspace';
|
||||
const ProjectsConfigurationName = 'projects';
|
||||
const TempProject = 'tempProject';
|
||||
|
||||
export class WorkspaceService implements IWorkspaceService {
|
||||
private _onDidWorkspaceProjectsChange: vscode.EventEmitter<void> = new vscode.EventEmitter<void>();
|
||||
readonly onDidWorkspaceProjectsChange: vscode.Event<void> = this._onDidWorkspaceProjectsChange?.event;
|
||||
|
||||
async addProjectsToWorkspace(projectFiles: vscode.Uri[]): Promise<void> {
|
||||
if (vscode.workspace.workspaceFile) {
|
||||
const currentProjects: vscode.Uri[] = await this.getProjectsInWorkspace();
|
||||
const newWorkspaceFolders: string[] = [];
|
||||
let newProjectFileAdded = false;
|
||||
for (const projectFile of projectFiles) {
|
||||
if (currentProjects.findIndex((p: vscode.Uri) => p.fsPath === projectFile.fsPath) === -1) {
|
||||
currentProjects.push(projectFile);
|
||||
newProjectFileAdded = true;
|
||||
constructor(private _context: vscode.ExtensionContext) {
|
||||
}
|
||||
|
||||
// if the relativePath and the original path is the same, that means the project file is not under
|
||||
// any workspace folders, we should add the parent folder of the project file to the workspace
|
||||
const relativePath = vscode.workspace.asRelativePath(projectFile, false);
|
||||
if (vscode.Uri.file(relativePath).fsPath === projectFile.fsPath) {
|
||||
newWorkspaceFolders.push(path.dirname(projectFile.path));
|
||||
}
|
||||
/**
|
||||
* Load any temp project that needed to be loaded before the extension host was restarted
|
||||
* which would happen if a workspace was created in order open or create a project
|
||||
*/
|
||||
async loadTempProjects(): Promise<void> {
|
||||
const tempProjects: string[] | undefined = this._context.globalState.get(TempProject) ?? undefined;
|
||||
|
||||
if (tempProjects && vscode.workspace.workspaceFile) {
|
||||
// add project to workspace now that the workspace has been created and saved
|
||||
for (let project of tempProjects) {
|
||||
await this.addProjectsToWorkspace([vscode.Uri.file(<string>project)]);
|
||||
}
|
||||
await this._context.globalState.update(TempProject, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new workspace in the same folder as the project. Because the extension host gets restared when
|
||||
* a new workspace is created and opened, the project needs to be saved as the temp project that will be loaded
|
||||
* when the extension gets restarted
|
||||
* @param projectFileFsPath project to add to the workspace
|
||||
*/
|
||||
async CreateNewWorkspaceForProject(projectFileFsPath: string): Promise<void> {
|
||||
// save temp project
|
||||
await this._context.globalState.update(TempProject, [projectFileFsPath]);
|
||||
|
||||
// create a new workspace - the workspace file will be created in the same folder as the project
|
||||
const workspaceFile = vscode.Uri.file(path.join(path.dirname(projectFileFsPath), `${path.parse(projectFileFsPath).name}.code-workspace`));
|
||||
const projectFolder = vscode.Uri.file(path.dirname(projectFileFsPath));
|
||||
await azdata.workspace.createWorkspace(projectFolder, workspaceFile);
|
||||
}
|
||||
|
||||
get isProjectProviderAvailable(): boolean {
|
||||
for (const extension of vscode.extensions.all) {
|
||||
const projectTypes = extension.packageJSON.contributes && extension.packageJSON.contributes.projects as string[];
|
||||
if (projectTypes && projectTypes.length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that a workspace is open or that if one isn't, it's ok to create a workspace
|
||||
*/
|
||||
async validateWorkspace(): Promise<boolean> {
|
||||
if (!vscode.workspace.workspaceFile) {
|
||||
const result = await vscode.window.showWarningMessage(constants.CreateWorkspaceConfirmation, constants.OkButtonText, constants.CancelButtonText);
|
||||
if (result === constants.OkButtonText) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// workspace is open
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows confirmation message that the extension host will be restarted and current workspace/file will be closed. If confirmed, the specified workspace will be entered.
|
||||
* @param workspaceFile
|
||||
*/
|
||||
async enterWorkspace(workspaceFile: vscode.Uri): Promise<void> {
|
||||
const result = await vscode.window.showWarningMessage(constants.EnterWorkspaceConfirmation, constants.OkButtonText, constants.CancelButtonText);
|
||||
if (result === constants.OkButtonText) {
|
||||
await azdata.workspace.enterWorkspace(workspaceFile);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async addProjectsToWorkspace(projectFiles: vscode.Uri[]): Promise<void> {
|
||||
if (!projectFiles || projectFiles.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// a workspace needs to be open to add projects
|
||||
if (!vscode.workspace.workspaceFile) {
|
||||
await this.CreateNewWorkspaceForProject(projectFiles[0].fsPath);
|
||||
|
||||
// this won't get hit since the extension host will get restarted, but helps with testing
|
||||
return;
|
||||
}
|
||||
|
||||
const currentProjects: vscode.Uri[] = this.getProjectsInWorkspace();
|
||||
const newWorkspaceFolders: string[] = [];
|
||||
let newProjectFileAdded = false;
|
||||
for (const projectFile of projectFiles) {
|
||||
if (currentProjects.findIndex((p: vscode.Uri) => p.fsPath === projectFile.fsPath) === -1) {
|
||||
currentProjects.push(projectFile);
|
||||
newProjectFileAdded = true;
|
||||
|
||||
// if the relativePath and the original path is the same, that means the project file is not under
|
||||
// any workspace folders, we should add the parent folder of the project file to the workspace
|
||||
const relativePath = vscode.workspace.asRelativePath(projectFile, false);
|
||||
if (vscode.Uri.file(relativePath).fsPath === projectFile.fsPath) {
|
||||
newWorkspaceFolders.push(path.dirname(projectFile.path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newProjectFileAdded) {
|
||||
// Save the new set of projects to the workspace configuration.
|
||||
await this.setWorkspaceConfigurationValue(ProjectsConfigurationName, currentProjects.map(project => this.toRelativePath(project)));
|
||||
this._onDidWorkspaceProjectsChange.fire();
|
||||
}
|
||||
if (newProjectFileAdded) {
|
||||
// Save the new set of projects to the workspace configuration.
|
||||
await this.setWorkspaceConfigurationValue(ProjectsConfigurationName, currentProjects.map(project => this.toRelativePath(project)));
|
||||
this._onDidWorkspaceProjectsChange.fire();
|
||||
}
|
||||
|
||||
if (newWorkspaceFolders.length > 0) {
|
||||
// second parameter is null means don't remove any workspace folders
|
||||
vscode.workspace.updateWorkspaceFolders(vscode.workspace.workspaceFolders!.length, null, ...(newWorkspaceFolders.map(folder => ({ uri: vscode.Uri.file(folder) }))));
|
||||
}
|
||||
if (newWorkspaceFolders.length > 0) {
|
||||
// second parameter is null means don't remove any workspace folders
|
||||
vscode.workspace.updateWorkspaceFolders(vscode.workspace.workspaceFolders!.length, null, ...(newWorkspaceFolders.map(folder => ({ uri: vscode.Uri.file(folder) }))));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,22 +146,22 @@ export class WorkspaceService implements IWorkspaceService {
|
||||
return projectTypes;
|
||||
}
|
||||
|
||||
async getProjectsInWorkspace(): Promise<vscode.Uri[]> {
|
||||
getProjectsInWorkspace(): vscode.Uri[] {
|
||||
return vscode.workspace.workspaceFile ? this.getWorkspaceConfigurationValue<string[]>(ProjectsConfigurationName).map(project => this.toUri(project)) : [];
|
||||
}
|
||||
|
||||
async getProjectProvider(projectFile: vscode.Uri): Promise<dataworkspace.IProjectProvider | undefined> {
|
||||
const projectType = path.extname(projectFile.path).replace(/\./g, '');
|
||||
let provider = ProjectProviderRegistry.getProviderByProjectType(projectType);
|
||||
let provider = ProjectProviderRegistry.getProviderByProjectExtension(projectType);
|
||||
if (!provider) {
|
||||
await this.ensureProviderExtensionLoaded(projectType);
|
||||
}
|
||||
return ProjectProviderRegistry.getProviderByProjectType(projectType);
|
||||
return ProjectProviderRegistry.getProviderByProjectExtension(projectType);
|
||||
}
|
||||
|
||||
async removeProject(projectFile: vscode.Uri): Promise<void> {
|
||||
if (vscode.workspace.workspaceFile) {
|
||||
const currentProjects: vscode.Uri[] = await this.getProjectsInWorkspace();
|
||||
const currentProjects: vscode.Uri[] = this.getProjectsInWorkspace();
|
||||
const projectIdx = currentProjects.findIndex((p: vscode.Uri) => p.fsPath === projectFile.fsPath);
|
||||
if (projectIdx !== -1) {
|
||||
currentProjects.splice(projectIdx, 1);
|
||||
@@ -84,6 +171,18 @@ export class WorkspaceService implements IWorkspaceService {
|
||||
}
|
||||
}
|
||||
|
||||
async createProject(name: string, location: vscode.Uri, projectTypeId: string): Promise<vscode.Uri> {
|
||||
const provider = ProjectProviderRegistry.getProviderByProjectType(projectTypeId);
|
||||
if (provider) {
|
||||
const projectFile = await provider.createProject(name, location);
|
||||
this.addProjectsToWorkspace([projectFile]);
|
||||
this._onDidWorkspaceProjectsChange.fire();
|
||||
return projectFile;
|
||||
} else {
|
||||
throw new Error(constants.ProviderNotFoundForProjectTypeError(projectTypeId));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the project provider extension for the specified project is loaded
|
||||
* @param projectType The file extension of the project, if not specified, all project provider extensions will be loaded.
|
||||
@@ -113,7 +212,7 @@ export class WorkspaceService implements IWorkspaceService {
|
||||
await extension.activate();
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(ExtensionActivationErrorMessage(extension.id, err));
|
||||
Logger.error(constants.ExtensionActivationError(extension.id, err));
|
||||
}
|
||||
|
||||
if (extension.isActive && extension.exports && !ProjectProviderRegistry.providers.includes(extension.exports)) {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 TypeMoq from 'typemoq';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import * as sinon from 'sinon';
|
||||
import { promises as fs } from 'fs';
|
||||
import { NewProjectDialog } from '../../dialogs/newProjectDialog';
|
||||
import { WorkspaceService } from '../../services/workspaceService';
|
||||
import { testProjectType } from '../testUtils';
|
||||
|
||||
suite('New Project Dialog', function (): void {
|
||||
test('Should validate project location', async function (): Promise<void> {
|
||||
const workspaceServiceMock = TypeMoq.Mock.ofType<WorkspaceService>();
|
||||
workspaceServiceMock.setup(x => x.getAllProjectTypes()).returns(() => Promise.resolve([testProjectType]));
|
||||
|
||||
const dialog = new NewProjectDialog(workspaceServiceMock.object);
|
||||
await dialog.open();
|
||||
|
||||
dialog.model.name = 'TestProject';
|
||||
dialog.model.location = '';
|
||||
should.equal(await dialog.validate(), false, 'Validation should fail becausee the parent directory does not exist');
|
||||
|
||||
// create a folder with the same name
|
||||
const folderPath = path.join(os.tmpdir(), dialog.model.name);
|
||||
await fs.mkdir(folderPath, { recursive: true });
|
||||
dialog.model.location = os.tmpdir();
|
||||
should.equal(await dialog.validate(), false, 'Validation should fail because a folder with the same name exists');
|
||||
|
||||
// change project name to be unique
|
||||
dialog.model.name = `TestProject_${new Date().getTime()}`;
|
||||
should.equal(await dialog.validate(), true, 'Validation should pass because name is unique and parent directory exists');
|
||||
});
|
||||
|
||||
test('Should validate workspace in onComplete', async function (): Promise<void> {
|
||||
const workspaceServiceMock = TypeMoq.Mock.ofType<WorkspaceService>();
|
||||
workspaceServiceMock.setup(x => x.validateWorkspace()).returns(() => Promise.resolve(true));
|
||||
workspaceServiceMock.setup(x => x.getAllProjectTypes()).returns(() => Promise.resolve([testProjectType]));
|
||||
|
||||
const dialog = new NewProjectDialog(workspaceServiceMock.object);
|
||||
await dialog.open();
|
||||
|
||||
dialog.model.name = 'TestProject';
|
||||
dialog.model.location = '';
|
||||
should.doesNotThrow(async () => await dialog.onComplete());
|
||||
|
||||
workspaceServiceMock.setup(x => x.validateWorkspace()).throws(new Error('test error'));
|
||||
const spy = sinon.spy(vscode.window, 'showErrorMessage');
|
||||
should.doesNotThrow(async () => await dialog.onComplete(), 'Error should be caught');
|
||||
should(spy.calledOnce).be.true();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 TypeMoq from 'typemoq';
|
||||
import * as sinon from 'sinon';
|
||||
import * as vscode from 'vscode';
|
||||
import * as constants from '../../common/constants';
|
||||
import { promises as fs } from 'fs';
|
||||
import { WorkspaceService } from '../../services/workspaceService';
|
||||
import { OpenExistingDialog } from '../../dialogs/openExistingDialog';
|
||||
import { createProjectFile, generateUniqueProjectFilePath, generateUniqueWorkspaceFilePath, testProjectType } from '../testUtils';
|
||||
|
||||
suite('Open Existing Dialog', function (): void {
|
||||
const mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
|
||||
|
||||
this.afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
test('Should validate project file exists', async function (): Promise<void> {
|
||||
const workspaceServiceMock = TypeMoq.Mock.ofType<WorkspaceService>();
|
||||
const dialog = new OpenExistingDialog(workspaceServiceMock.object, mockExtensionContext.object);
|
||||
await dialog.open();
|
||||
|
||||
dialog._targetTypeRadioCardGroup?.updateProperty( 'selectedCardId', constants.Project);
|
||||
dialog._projectFile = '';
|
||||
should.equal(await dialog.validate(), false, 'Validation fail because project file does not exist');
|
||||
|
||||
// create a project file
|
||||
dialog._projectFile = await createProjectFile('testproj');
|
||||
should.equal(await dialog.validate(), true, 'Validation pass because project file exists');
|
||||
});
|
||||
|
||||
test('Should validate workspace file exists', async function (): Promise<void> {
|
||||
const workspaceServiceMock = TypeMoq.Mock.ofType<WorkspaceService>();
|
||||
const dialog = new OpenExistingDialog(workspaceServiceMock.object, mockExtensionContext.object);
|
||||
await dialog.open();
|
||||
|
||||
dialog._targetTypeRadioCardGroup?.updateProperty( 'selectedCardId', constants.Workspace);
|
||||
dialog._workspaceFile = '';
|
||||
should.equal(await dialog.validate(), false, 'Validation fail because workspace file does not exist');
|
||||
|
||||
// create a workspace file
|
||||
dialog._workspaceFile = generateUniqueWorkspaceFilePath();
|
||||
await fs.writeFile(dialog._workspaceFile, '');
|
||||
should.equal(await dialog.validate(), true, 'Validation pass because workspace file exists');
|
||||
});
|
||||
|
||||
test('Should validate workspace in onComplete when opening project', async function (): Promise<void> {
|
||||
const workspaceServiceMock = TypeMoq.Mock.ofType<WorkspaceService>();
|
||||
workspaceServiceMock.setup(x => x.validateWorkspace()).returns(() => Promise.resolve(true));
|
||||
workspaceServiceMock.setup(x => x.addProjectsToWorkspace(TypeMoq.It.isAny())).returns(() => Promise.resolve());
|
||||
|
||||
const dialog = new OpenExistingDialog(workspaceServiceMock.object, mockExtensionContext.object);
|
||||
await dialog.open();
|
||||
|
||||
dialog._projectFile = generateUniqueProjectFilePath('testproj');
|
||||
should.doesNotThrow(async () => await dialog.onComplete());
|
||||
|
||||
workspaceServiceMock.setup(x => x.validateWorkspace()).throws(new Error('test error'));
|
||||
const spy = sinon.spy(vscode.window, 'showErrorMessage');
|
||||
should.doesNotThrow(async () => await dialog.onComplete(), 'Error should be caught');
|
||||
should(spy.calledOnce).be.true();
|
||||
});
|
||||
|
||||
test('workspace browse', async function (): Promise<void> {
|
||||
const workspaceServiceMock = TypeMoq.Mock.ofType<WorkspaceService>();
|
||||
sinon.stub(vscode.window, 'showOpenDialog').returns(Promise.resolve([]));
|
||||
|
||||
const dialog = new OpenExistingDialog(workspaceServiceMock.object, mockExtensionContext.object);
|
||||
await dialog.open();
|
||||
should.equal(dialog._workspaceFile, '');
|
||||
await dialog.workspaceBrowse();
|
||||
should.equal(dialog._workspaceFile, '', 'Workspace file should not be set when no file is selected');
|
||||
|
||||
sinon.restore();
|
||||
const workspaceFile = vscode.Uri.file(generateUniqueWorkspaceFilePath());
|
||||
sinon.stub(vscode.window, 'showOpenDialog').returns(Promise.resolve([workspaceFile]));
|
||||
await dialog.workspaceBrowse();
|
||||
should.equal(dialog._workspaceFile, workspaceFile.fsPath, 'Workspace file should get set');
|
||||
should.equal(dialog._filePathTextBox?.value, workspaceFile.fsPath);
|
||||
});
|
||||
|
||||
test('project browse', async function (): Promise<void> {
|
||||
const workspaceServiceMock = TypeMoq.Mock.ofType<WorkspaceService>();
|
||||
workspaceServiceMock.setup(x => x.getAllProjectTypes()).returns(() => Promise.resolve([testProjectType]));
|
||||
sinon.stub(vscode.window, 'showOpenDialog').returns(Promise.resolve([]));
|
||||
|
||||
const dialog = new OpenExistingDialog(workspaceServiceMock.object, mockExtensionContext.object);
|
||||
await dialog.open();
|
||||
should.equal(dialog._projectFile, '');
|
||||
await dialog.projectBrowse();
|
||||
should.equal(dialog._projectFile, '', 'Project file should not be set when no file is selected');
|
||||
|
||||
sinon.restore();
|
||||
const projectFile = vscode.Uri.file(generateUniqueProjectFilePath('testproj'));
|
||||
sinon.stub(vscode.window, 'showOpenDialog').returns(Promise.resolve([projectFile]));
|
||||
await dialog.projectBrowse();
|
||||
should.equal(dialog._projectFile, projectFile.fsPath, 'Project file should be set');
|
||||
should.equal(dialog._filePathTextBox?.value, projectFile.fsPath);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,6 +28,9 @@ export function createProjectProvider(projectTypes: IProjectType[]): IProjectPro
|
||||
},
|
||||
getProjectTreeDataProvider: (projectFile: vscode.Uri): Promise<vscode.TreeDataProvider<any>> => {
|
||||
return Promise.resolve(treeDataProvider);
|
||||
},
|
||||
createProject: (name: string, location: vscode.Uri): Promise<vscode.Uri> => {
|
||||
return Promise.resolve(location);
|
||||
}
|
||||
};
|
||||
return projectProvider;
|
||||
@@ -37,51 +40,57 @@ suite('ProjectProviderRegistry Tests', function (): void {
|
||||
test('register and unregister project providers', async () => {
|
||||
const provider1 = createProjectProvider([
|
||||
{
|
||||
id: 'tp1',
|
||||
projectFileExtension: 'testproj',
|
||||
icon: '',
|
||||
displayName: 'test project'
|
||||
displayName: 'test project',
|
||||
description: ''
|
||||
}, {
|
||||
id: 'tp2',
|
||||
projectFileExtension: 'testproj1',
|
||||
icon: '',
|
||||
displayName: 'test project 1'
|
||||
displayName: 'test project 1',
|
||||
description: ''
|
||||
}
|
||||
]);
|
||||
const provider2 = createProjectProvider([
|
||||
{
|
||||
id: 'sp1',
|
||||
projectFileExtension: 'sqlproj',
|
||||
icon: '',
|
||||
displayName: 'sql project'
|
||||
displayName: 'sql project',
|
||||
description: ''
|
||||
}
|
||||
]);
|
||||
should.strictEqual(ProjectProviderRegistry.providers.length, 0, 'there should be no project provider at the beginning of the test');
|
||||
const disposable1 = ProjectProviderRegistry.registerProvider(provider1);
|
||||
let providerResult = ProjectProviderRegistry.getProviderByProjectType('testproj');
|
||||
let providerResult = ProjectProviderRegistry.getProviderByProjectExtension('testproj');
|
||||
should.equal(providerResult, provider1, 'provider1 should be returned for testproj project type');
|
||||
// make sure the project type is case-insensitive for getProviderByProjectType method
|
||||
providerResult = ProjectProviderRegistry.getProviderByProjectType('TeStProJ');
|
||||
providerResult = ProjectProviderRegistry.getProviderByProjectExtension('TeStProJ');
|
||||
should.equal(providerResult, provider1, 'provider1 should be returned for testproj project type');
|
||||
providerResult = ProjectProviderRegistry.getProviderByProjectType('testproj1');
|
||||
providerResult = ProjectProviderRegistry.getProviderByProjectExtension('testproj1');
|
||||
should.equal(providerResult, provider1, 'provider1 should be returned for testproj1 project type');
|
||||
should.strictEqual(ProjectProviderRegistry.providers.length, 1, 'there should be only one project provider at this time');
|
||||
const disposable2 = ProjectProviderRegistry.registerProvider(provider2);
|
||||
providerResult = ProjectProviderRegistry.getProviderByProjectType('sqlproj');
|
||||
providerResult = ProjectProviderRegistry.getProviderByProjectExtension('sqlproj');
|
||||
should.equal(providerResult, provider2, 'provider2 should be returned for sqlproj project type');
|
||||
should.strictEqual(ProjectProviderRegistry.providers.length, 2, 'there should be 2 project providers at this time');
|
||||
|
||||
// unregister provider1
|
||||
disposable1.dispose();
|
||||
providerResult = ProjectProviderRegistry.getProviderByProjectType('testproj');
|
||||
providerResult = ProjectProviderRegistry.getProviderByProjectExtension('testproj');
|
||||
should.equal(providerResult, undefined, 'undefined should be returned for testproj project type');
|
||||
providerResult = ProjectProviderRegistry.getProviderByProjectType('testproj1');
|
||||
providerResult = ProjectProviderRegistry.getProviderByProjectExtension('testproj1');
|
||||
should.equal(providerResult, undefined, 'undefined should be returned for testproj1 project type');
|
||||
providerResult = ProjectProviderRegistry.getProviderByProjectType('sqlproj');
|
||||
providerResult = ProjectProviderRegistry.getProviderByProjectExtension('sqlproj');
|
||||
should.equal(providerResult, provider2, 'provider2 should be returned for sqlproj project type after provider1 is disposed');
|
||||
should.strictEqual(ProjectProviderRegistry.providers.length, 1, 'there should be only one project provider after unregistering a provider');
|
||||
should.strictEqual(ProjectProviderRegistry.providers[0].supportedProjectTypes[0].projectFileExtension, 'sqlproj', 'the remaining project provider should be sqlproj');
|
||||
|
||||
// unregister provider2
|
||||
disposable2.dispose();
|
||||
providerResult = ProjectProviderRegistry.getProviderByProjectType('sqlproj');
|
||||
providerResult = ProjectProviderRegistry.getProviderByProjectExtension('sqlproj');
|
||||
should.equal(providerResult, undefined, 'undefined should be returned for sqlproj project type after provider2 is disposed');
|
||||
should.strictEqual(ProjectProviderRegistry.providers.length, 0, 'there should be no project provider after unregistering the providers');
|
||||
});
|
||||
@@ -89,9 +98,11 @@ suite('ProjectProviderRegistry Tests', function (): void {
|
||||
test('Clear the project provider registry', async () => {
|
||||
const provider = createProjectProvider([
|
||||
{
|
||||
id: 'tp1',
|
||||
projectFileExtension: 'testproj',
|
||||
icon: '',
|
||||
displayName: 'test project'
|
||||
displayName: 'test project',
|
||||
description: ''
|
||||
}
|
||||
]);
|
||||
should.strictEqual(ProjectProviderRegistry.providers.length, 0, 'there should be no project provider at the beginning of the test');
|
||||
|
||||
37
extensions/data-workspace/src/test/testUtils.ts
Normal file
37
extensions/data-workspace/src/test/testUtils.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { IProjectType } from 'dataworkspace';
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
export const testProjectType: IProjectType = {
|
||||
id: 'tp1',
|
||||
description: '',
|
||||
projectFileExtension: 'testproj',
|
||||
icon: '',
|
||||
displayName: 'test project'
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a unique test project file
|
||||
* @param fileExt
|
||||
* @param contents
|
||||
*/
|
||||
export async function createProjectFile(fileExt: string, contents?: string): Promise<string> {
|
||||
const filepath = generateUniqueProjectFilePath(fileExt);
|
||||
await fs.writeFile(filepath, contents ?? '');
|
||||
|
||||
return filepath;
|
||||
}
|
||||
|
||||
export function generateUniqueProjectFilePath(fileExt: string): string {
|
||||
return path.join(os.tmpdir(), `TestProject_${new Date().getTime()}.${fileExt}`);
|
||||
}
|
||||
|
||||
export function generateUniqueWorkspaceFilePath(): string {
|
||||
return path.join(os.tmpdir(), `TestWorkspace_${new Date().getTime()}.code-workspace`);
|
||||
}
|
||||
@@ -5,9 +5,11 @@
|
||||
|
||||
import 'mocha';
|
||||
import * as vscode from 'vscode';
|
||||
import * as azdata from 'azdata';
|
||||
import * as sinon from 'sinon';
|
||||
import * as should from 'should';
|
||||
import * as path from 'path';
|
||||
import * as TypeMoq from 'typemoq';
|
||||
import { WorkspaceService } from '../services/workspaceService';
|
||||
import { ProjectProviderRegistry } from '../common/projectProviderRegistry';
|
||||
import { createProjectProvider } from './projectProviderRegistry.test';
|
||||
@@ -61,7 +63,12 @@ function createMockExtension(id: string, isActive: boolean, projectTypes: string
|
||||
}
|
||||
|
||||
suite('WorkspaceService Tests', function (): void {
|
||||
const service = new WorkspaceService();
|
||||
const mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
|
||||
const mockGlobalState = TypeMoq.Mock.ofType<vscode.Memento>();
|
||||
mockGlobalState.setup(x => x.update(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve());
|
||||
mockExtensionContext.setup(x => x.globalState).returns(() => mockGlobalState.object);
|
||||
|
||||
const service = new WorkspaceService(mockExtensionContext.object);
|
||||
|
||||
this.afterEach(() => {
|
||||
sinon.restore();
|
||||
@@ -111,10 +118,14 @@ suite('WorkspaceService Tests', function (): void {
|
||||
|
||||
const provider1 = createProjectProvider([
|
||||
{
|
||||
id: 'tp1',
|
||||
description: '',
|
||||
projectFileExtension: 'testproj',
|
||||
icon: '',
|
||||
displayName: 'test project'
|
||||
}, {
|
||||
id: 'tp2',
|
||||
description: '',
|
||||
projectFileExtension: 'testproj1',
|
||||
icon: '',
|
||||
displayName: 'test project 1'
|
||||
@@ -122,6 +133,8 @@ suite('WorkspaceService Tests', function (): void {
|
||||
]);
|
||||
const provider2 = createProjectProvider([
|
||||
{
|
||||
id: 'sp1',
|
||||
description: '',
|
||||
projectFileExtension: 'sqlproj',
|
||||
icon: '',
|
||||
displayName: 'sql project'
|
||||
@@ -149,10 +162,12 @@ suite('WorkspaceService Tests', function (): void {
|
||||
const extension2 = createMockExtension('ext2', false, ['sqlproj']);
|
||||
const extension3 = createMockExtension('ext3', false, ['dbproj']);
|
||||
stubAllExtensions([extension1, extension2, extension3].map(ext => ext.extension));
|
||||
const getProviderByProjectTypeStub = sinon.stub(ProjectProviderRegistry, 'getProviderByProjectType');
|
||||
const getProviderByProjectTypeStub = sinon.stub(ProjectProviderRegistry, 'getProviderByProjectExtension');
|
||||
getProviderByProjectTypeStub.onFirstCall().returns(undefined);
|
||||
getProviderByProjectTypeStub.onSecondCall().returns(createProjectProvider([
|
||||
{
|
||||
id: 'sp1',
|
||||
description: '',
|
||||
projectFileExtension: 'sqlproj',
|
||||
icon: '',
|
||||
displayName: 'test project'
|
||||
@@ -167,6 +182,8 @@ suite('WorkspaceService Tests', function (): void {
|
||||
|
||||
getProviderByProjectTypeStub.reset();
|
||||
getProviderByProjectTypeStub.returns(createProjectProvider([{
|
||||
id: 'tp2',
|
||||
description: '',
|
||||
projectFileExtension: 'csproj',
|
||||
icon: '',
|
||||
displayName: 'test cs project'
|
||||
@@ -215,6 +232,45 @@ suite('WorkspaceService Tests', function (): void {
|
||||
onWorkspaceProjectsChangedDisposable.dispose();
|
||||
});
|
||||
|
||||
test('test addProjectsToWorkspace when no workspace open', async () => {
|
||||
stubWorkspaceFile(undefined);
|
||||
const onWorkspaceProjectsChangedStub = sinon.stub();
|
||||
const onWorkspaceProjectsChangedDisposable = service.onDidWorkspaceProjectsChange(() => {
|
||||
onWorkspaceProjectsChangedStub();
|
||||
});
|
||||
const createWorkspaceStub = sinon.stub(azdata.workspace, 'createWorkspace').resolves(undefined);
|
||||
|
||||
await service.addProjectsToWorkspace([
|
||||
vscode.Uri.file('/test/folder/proj1.sqlproj')
|
||||
]);
|
||||
|
||||
should.strictEqual(createWorkspaceStub.calledOnce, true, 'createWorkspace should have been called once');
|
||||
should.strictEqual(onWorkspaceProjectsChangedStub.notCalled, true, 'the onDidWorkspaceProjectsChange event should not have been fired');
|
||||
onWorkspaceProjectsChangedDisposable.dispose();
|
||||
});
|
||||
|
||||
test('test loadTempProjects', async () => {
|
||||
const processPath = (original: string): string => {
|
||||
return original.replace(/\//g, path.sep);
|
||||
};
|
||||
stubWorkspaceFile('/test/folder/proj1.code-workspace');
|
||||
const updateConfigurationStub = sinon.stub();
|
||||
const getConfigurationStub = sinon.stub().returns([processPath('folder1/proj2.sqlproj')]);
|
||||
const onWorkspaceProjectsChangedStub = sinon.stub();
|
||||
const onWorkspaceProjectsChangedDisposable = service.onDidWorkspaceProjectsChange(() => {
|
||||
onWorkspaceProjectsChangedStub();
|
||||
});
|
||||
stubGetConfigurationValue(getConfigurationStub, updateConfigurationStub);
|
||||
sinon.stub(azdata.workspace, 'createWorkspace').resolves(undefined);
|
||||
sinon.stub(vscode.workspace, 'workspaceFolders').value(['folder1']);
|
||||
mockGlobalState.setup(x => x.get(TypeMoq.It.isAny())).returns(() => [processPath('folder1/proj2.sqlproj')]);
|
||||
|
||||
await service.loadTempProjects();
|
||||
|
||||
should.strictEqual(onWorkspaceProjectsChangedStub.calledOnce, true, 'the onDidWorkspaceProjectsChange event should have been fired');
|
||||
onWorkspaceProjectsChangedDisposable.dispose();
|
||||
});
|
||||
|
||||
test('test removeProject', async () => {
|
||||
const processPath = (original: string): string => {
|
||||
return original.replace(/\//g, path.sep);
|
||||
|
||||
@@ -7,13 +7,19 @@ import 'mocha';
|
||||
import * as sinon from 'sinon';
|
||||
import * as vscode from 'vscode';
|
||||
import * as should from 'should';
|
||||
import * as TypeMoq from 'typemoq';
|
||||
import { WorkspaceTreeDataProvider } from '../common/workspaceTreeDataProvider';
|
||||
import { WorkspaceService } from '../services/workspaceService';
|
||||
import { IProjectProvider, WorkspaceTreeItem } from 'dataworkspace';
|
||||
import { MockTreeDataProvider } from './projectProviderRegistry.test';
|
||||
|
||||
suite('workspaceTreeDataProvider Tests', function (): void {
|
||||
const workspaceService = new WorkspaceService();
|
||||
const mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
|
||||
const mockGlobalState = TypeMoq.Mock.ofType<vscode.Memento>();
|
||||
mockGlobalState.setup(x => x.update(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve());
|
||||
mockExtensionContext.setup(x => x.globalState).returns(() => mockGlobalState.object);
|
||||
|
||||
const workspaceService = new WorkspaceService(mockExtensionContext.object);
|
||||
const treeProvider = new WorkspaceTreeDataProvider(workspaceService);
|
||||
|
||||
this.afterEach(() => {
|
||||
@@ -66,15 +72,20 @@ suite('workspaceTreeDataProvider Tests', function (): void {
|
||||
const treeDataProvider = new MockTreeDataProvider();
|
||||
const projectProvider: IProjectProvider = {
|
||||
supportedProjectTypes: [{
|
||||
id: 'sp1',
|
||||
projectFileExtension: 'sqlproj',
|
||||
icon: '',
|
||||
displayName: 'sql project'
|
||||
displayName: 'sql project',
|
||||
description: ''
|
||||
}],
|
||||
RemoveProject: (projectFile: vscode.Uri): Promise<void> => {
|
||||
return Promise.resolve();
|
||||
},
|
||||
getProjectTreeDataProvider: (projectFile: vscode.Uri): Promise<vscode.TreeDataProvider<any>> => {
|
||||
return Promise.resolve(treeDataProvider);
|
||||
},
|
||||
createProject: (name: string, location: vscode.Uri): Promise<vscode.Uri> => {
|
||||
return Promise.resolve(location);
|
||||
}
|
||||
};
|
||||
const getProjectProviderStub = sinon.stub(workspaceService, 'getProjectProvider');
|
||||
|
||||
@@ -65,7 +65,7 @@ export class DacFxService implements mssql.IDacFxService {
|
||||
);
|
||||
}
|
||||
|
||||
public importDatabaseProject(databaseName: string, targetFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, extractTarget: mssql.ExtractTarget, taskExecutionMode: azdata.TaskExecutionMode): Thenable<mssql.DacFxResult> {
|
||||
public createProjectFromDatabase(databaseName: string, targetFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, extractTarget: mssql.ExtractTarget, taskExecutionMode: azdata.TaskExecutionMode): Thenable<mssql.DacFxResult> {
|
||||
const params: contracts.ExtractParams = { databaseName: databaseName, packageFilePath: targetFilePath, applicationName: applicationName, applicationVersion: applicationVersion, ownerUri: ownerUri, extractTarget: extractTarget, taskExecutionMode: taskExecutionMode };
|
||||
return this.client.sendRequest(contracts.ExtractRequest.type, params).then(
|
||||
undefined,
|
||||
|
||||
2
extensions/mssql/src/mssql.d.ts
vendored
2
extensions/mssql/src/mssql.d.ts
vendored
@@ -334,7 +334,7 @@ export interface IDacFxService {
|
||||
exportBacpac(databaseName: string, packageFilePath: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable<DacFxResult>;
|
||||
importBacpac(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable<DacFxResult>;
|
||||
extractDacpac(databaseName: string, packageFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable<DacFxResult>;
|
||||
importDatabaseProject(databaseName: string, targetFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, extractTarget: ExtractTarget, taskExecutionMode: azdata.TaskExecutionMode): Thenable<DacFxResult>;
|
||||
createProjectFromDatabase(databaseName: string, targetFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, extractTarget: ExtractTarget, taskExecutionMode: azdata.TaskExecutionMode): Thenable<DacFxResult>;
|
||||
deployDacpac(packageFilePath: string, databaseName: string, upgradeExisting: boolean, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode, sqlCommandVariableValues?: Record<string, string>, deploymentOptions?: DeploymentOptions): Thenable<DacFxResult>;
|
||||
generateDeployScript(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode, sqlCommandVariableValues?: Record<string, string>, deploymentOptions?: DeploymentOptions): Thenable<DacFxResult>;
|
||||
generateDeployPlan(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable<GenerateDeployPlanResult>;
|
||||
|
||||
@@ -9,8 +9,8 @@ Please report issues and feature requests [here.](https://github.com/microsoft/a
|
||||
|
||||
## Getting Started with Database Projects
|
||||
|
||||
* Create a new database project by going to the **Projects** viewlet under Explorer, or by searching for **New Database Project** in the command palette.
|
||||
* Existing database projects can be opened via **Open Database Project** in the command palette.
|
||||
* Create a new database project by going to the **Projects** viewlet or by searching **Projects: New** in the command palette.
|
||||
* Existing database projects can be opened by going to the **Projects** viewlet or by searching **Projects: Open Existing** in the command palette.
|
||||
* Start from an existing database by using **Create Project From Database** from the command palette or database context menu.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M50.0322 37.756C38.8472 37.727 29.7902 34.4 29.8002 30.326L29.7002 69.573C29.7002 73.614 38.6002 76.917 49.6562 77H49.9322C61.1172 77.028 70.1932 73.75 70.2022 69.674L70.3022 30.427C70.2922 34.506 61.2172 37.784 50.0322 37.756Z" fill="url(#paint0_linear)"/>
|
||||
<path d="M70.2998 30.43C70.2998 34.503 61.2148 37.784 50.0298 37.756C38.8448 37.728 29.7898 34.4 29.7998 30.326C29.8098 26.252 38.8818 22.972 50.0698 23C61.2578 23.028 70.3108 26.355 70.2998 30.43Z" fill="#E6E6E6"/>
|
||||
<path d="M65.5662 29.82C65.5662 32.413 58.6022 34.493 50.0322 34.471C41.4622 34.449 34.5202 32.333 34.5322 29.741C34.5442 27.149 41.4952 25.068 50.0692 25.09C58.6432 25.112 65.5862 27.228 65.5802 29.82" fill="#50E6FF"/>
|
||||
<path d="M50.048 30.862C45.8792 30.753 41.7215 31.3438 37.748 32.61C41.7069 33.9382 45.8665 34.5679 50.041 34.471C54.2144 34.588 58.3763 33.979 62.341 32.671C58.3734 31.3848 54.218 30.7733 50.048 30.862Z" fill="#198AB3"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="29.8815" y1="53.777" x2="70.3845" y2="53.8985" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#005BA1"/>
|
||||
<stop offset="0.068" stop-color="#0060A9"/>
|
||||
<stop offset="0.356" stop-color="#0071C8"/>
|
||||
<stop offset="0.517" stop-color="#0078D4"/>
|
||||
<stop offset="0.642" stop-color="#0074CD"/>
|
||||
<stop offset="0.82" stop-color="#006ABB"/>
|
||||
<stop offset="1" stop-color="#005BA1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -7,7 +7,7 @@
|
||||
"preview": true,
|
||||
"engines": {
|
||||
"vscode": "^1.30.1",
|
||||
"azdata": ">=1.24.0"
|
||||
"azdata": ">=1.25.0"
|
||||
},
|
||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
|
||||
"icon": "images/sqlDatabaseProjects.png",
|
||||
@@ -15,7 +15,7 @@
|
||||
"activationEvents": [
|
||||
"onCommand:sqlDatabaseProjects.new",
|
||||
"onCommand:sqlDatabaseProjects.open",
|
||||
"onCommand:sqlDatabaseProjects.importDatabase",
|
||||
"onCommand:sqlDatabaseProjects.createProjectFromDatabase",
|
||||
"workspaceContains:**/*.sqlproj",
|
||||
"onView:dataworkspace.views.main"
|
||||
],
|
||||
@@ -39,37 +39,11 @@
|
||||
"sqlDatabaseProjects.netCoreSDKLocation": {
|
||||
"type": "string",
|
||||
"description": "%sqlDatabaseProjects.netCoreInstallLocation%"
|
||||
},
|
||||
"sqlDatabaseProjects.defaultProjectSaveLocation": {
|
||||
"type": "string",
|
||||
"description": "%sqlDatabaseProjects.defaultProjectSaveLocation%"
|
||||
},
|
||||
"sqlDatabaseProjects.showUpdateSaveLocationPrompt": {
|
||||
"type": "boolean",
|
||||
"description": "%sqlDatabaseProjects.showUpdateSaveLocationPrompt%",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"commands": [
|
||||
{
|
||||
"command": "sqlDatabaseProjects.new",
|
||||
"title": "%sqlDatabaseProjects.new%",
|
||||
"category": "%sqlDatabaseProjects.displayName%",
|
||||
"icon": "$(add)"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.open",
|
||||
"title": "%sqlDatabaseProjects.open%",
|
||||
"category": "%sqlDatabaseProjects.displayName%",
|
||||
"icon": "$(folder-opened)"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.close",
|
||||
"title": "%sqlDatabaseProjects.close%",
|
||||
"category": "%sqlDatabaseProjects.displayName%"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.newScript",
|
||||
"title": "%sqlDatabaseProjects.newScript%",
|
||||
@@ -146,8 +120,8 @@
|
||||
"category": "%sqlDatabaseProjects.displayName%"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.importDatabase",
|
||||
"title": "%sqlDatabaseProjects.importDatabase%",
|
||||
"command": "sqlDatabaseProjects.createProjectFromDatabase",
|
||||
"title": "%sqlDatabaseProjects.createProjectFromDatabase%",
|
||||
"category": "%sqlDatabaseProjects.displayName%",
|
||||
"icon": "images/databaseProjectToolbar.svg"
|
||||
},
|
||||
@@ -179,16 +153,6 @@
|
||||
],
|
||||
"menus": {
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "sqlDatabaseProjects.new"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.open"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.close",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.newScript",
|
||||
"when": "false"
|
||||
@@ -246,7 +210,7 @@
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.importDatabase"
|
||||
"command": "sqlDatabaseProjects.createProjectFromDatabase"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.addDatabaseReference",
|
||||
@@ -273,57 +237,45 @@
|
||||
"when": "false"
|
||||
}
|
||||
],
|
||||
"view/title": [
|
||||
{
|
||||
"command": "sqlDatabaseProjects.new",
|
||||
"when": "view == sqlDatabaseProjectsView",
|
||||
"group": "navigation@1"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.open",
|
||||
"when": "view == sqlDatabaseProjectsView",
|
||||
"group": "navigation@2"
|
||||
}
|
||||
],
|
||||
"view/item/context": [
|
||||
{
|
||||
"command": "sqlDatabaseProjects.build",
|
||||
"when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project",
|
||||
"when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.project",
|
||||
"group": "1_dbProjectsFirst@1"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.publish",
|
||||
"when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project",
|
||||
"when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.project",
|
||||
"group": "1_dbProjectsFirst@2"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.schemaCompare",
|
||||
"when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project",
|
||||
"when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.project",
|
||||
"group": "1_dbProjectsFirst@3"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.newItem",
|
||||
"when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder",
|
||||
"when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder",
|
||||
"group": "2_dbProjects_newMain@1"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.newFolder",
|
||||
"when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder",
|
||||
"when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder",
|
||||
"group": "2_dbProjects_newMain@2"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.newTable",
|
||||
"when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder",
|
||||
"when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder",
|
||||
"group": "3_dbProjects_newItem@1"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.newView",
|
||||
"when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder",
|
||||
"when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder",
|
||||
"group": "3_dbProjects_newItem@2"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.newStoredProcedure",
|
||||
"when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder",
|
||||
"when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder",
|
||||
"group": "3_dbProjects_newItem@3"
|
||||
},
|
||||
{
|
||||
@@ -333,22 +285,22 @@
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.newScript",
|
||||
"when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder",
|
||||
"when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder",
|
||||
"group": "3_dbProjects_newItem@7"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.newPreDeploymentScript",
|
||||
"when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder",
|
||||
"when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder",
|
||||
"group": "3_dbProjects_newItem@8"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.newPostDeploymentScript",
|
||||
"when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder",
|
||||
"when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder",
|
||||
"group": "3_dbProjects_newItem@9"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.addDatabaseReference",
|
||||
"when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.referencesRoot",
|
||||
"when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.referencesRoot",
|
||||
"group": "4_dbProjects_addDatabaseReference"
|
||||
},
|
||||
{
|
||||
@@ -373,66 +325,36 @@
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.editProjectFile",
|
||||
"when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project",
|
||||
"when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.project",
|
||||
"group": "9_dbProjectsLast@7"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.openContainingFolder",
|
||||
"when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project",
|
||||
"when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.project",
|
||||
"group": "9_dbProjectsLast@8"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.close",
|
||||
"when": "view == sqlDatabaseProjectsView && viewItem == databaseProject.itemType.project",
|
||||
"group": "9_dbProjectsLast@9"
|
||||
}
|
||||
],
|
||||
"objectExplorer/item/context": [
|
||||
{
|
||||
"command": "sqlDatabaseProjects.importDatabase",
|
||||
"when": "connectionProvider == MSSQL && nodeType && nodeType == Database && mssql:engineedition != 11",
|
||||
"group": "export"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.importDatabase",
|
||||
"when": "connectionProvider == MSSQL && nodeType && nodeType == Server && mssql:engineedition != 11",
|
||||
"command": "sqlDatabaseProjects.createProjectFromDatabase",
|
||||
"when": "nodeType =~ /^(Database|Server)$/ && connectionProvider == MSSQL && mssql:engineedition != 11",
|
||||
"group": "export"
|
||||
}
|
||||
],
|
||||
"dataExplorer/context": [
|
||||
{
|
||||
"command": "sqlDatabaseProjects.importDatabase",
|
||||
"when": "connectionProvider == MSSQL && nodeType && nodeType == Database && mssql:engineedition != 11",
|
||||
"group": "export"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.importDatabase",
|
||||
"when": "connectionProvider == MSSQL && nodeType && nodeType == Server && mssql:engineedition != 11",
|
||||
"command": "sqlDatabaseProjects.createProjectFromDatabase",
|
||||
"when": "nodeType =~ /^(Database|Server)$/ && connectionProvider == MSSQL && mssql:engineedition != 11",
|
||||
"group": "export"
|
||||
}
|
||||
],
|
||||
"dashboard/toolbar": [
|
||||
{
|
||||
"command": "sqlDatabaseProjects.importDatabase",
|
||||
"command": "sqlDatabaseProjects.createProjectFromDatabase",
|
||||
"when": "connectionProvider == 'MSSQL' && mssql:engineedition != 11"
|
||||
}
|
||||
]
|
||||
},
|
||||
"views": {
|
||||
"explorer": [
|
||||
{
|
||||
"id": "sqlDatabaseProjectsView",
|
||||
"name": "%title.projectsView%",
|
||||
"when": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"viewsWelcome": [
|
||||
{
|
||||
"view": "sqlDatabaseProjectsView",
|
||||
"contents": "%sqlDatabaseProjects.welcome%"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/xml-formatter": "^1.1.0",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"sqlDatabaseProjects.close": "Close Database Project",
|
||||
"sqlDatabaseProjects.build": "Build",
|
||||
"sqlDatabaseProjects.publish": "Publish",
|
||||
"sqlDatabaseProjects.importDatabase": "Create Project From Database",
|
||||
"sqlDatabaseProjects.createProjectFromDatabase": "Create Project From Database",
|
||||
"sqlDatabaseProjects.properties": "Properties",
|
||||
"sqlDatabaseProjects.schemaCompare": "Schema Compare",
|
||||
"sqlDatabaseProjects.delete": "Delete",
|
||||
@@ -33,7 +33,5 @@
|
||||
|
||||
"sqlDatabaseProjects.Settings": "Database Projects",
|
||||
"sqlDatabaseProjects.netCoreInstallLocation": "Full path to .Net Core SDK on the machine.",
|
||||
"sqlDatabaseProjects.defaultProjectSaveLocation": "Full path to folder where new database projects are saved by default.",
|
||||
"sqlDatabaseProjects.showUpdateSaveLocationPrompt": "After creating a new database project, always prompt to update the location where new projects are saved by default.",
|
||||
"sqlDatabaseProjects.welcome": "No database projects currently open.\n[New Project](command:sqlDatabaseProjects.new)\n[Open Project](command:sqlDatabaseProjects.open)\n[Create Project From Database](command:sqlDatabaseProjects.importDatabase)"
|
||||
}
|
||||
|
||||
@@ -23,12 +23,13 @@ export const MicrosoftDatatoolsSchemaSqlSql = 'Microsoft.Data.Tools.Schema.Sql.S
|
||||
export const databaseSchemaProvider = 'DatabaseSchemaProvider';
|
||||
|
||||
// Project Provider
|
||||
export const projectTypeDisplayName = localize('projectTypeDisplayName', 'Database Project');
|
||||
export const sqlDatabaseProjectTypeId = 'sqldbproj';
|
||||
export const projectTypeDisplayName = localize('projectTypeDisplayName', "SQL Database");
|
||||
export const projectTypeDescription = localize('projectTypeDescription', "Design, edit, and publish schemas for SQL databases");
|
||||
|
||||
// commands
|
||||
export const revealFileInOsCommand = 'revealFileInOS';
|
||||
export const schemaCompareStartCommand = 'schemaCompare.start';
|
||||
export const sqlDatabaseProjectsViewFocusCommand = 'sqlDatabaseProjectsView.focus';
|
||||
export const vscodeOpenCommand = 'vscode.open';
|
||||
|
||||
// UI Strings
|
||||
@@ -54,10 +55,6 @@ export const objectType = localize('objectType', "Object Type");
|
||||
export const schema = localize('schema', "Schema");
|
||||
export const schemaObjectType = localize('schemaObjectType', "Schema/Object Type");
|
||||
export const defaultProjectNameStarter = localize('defaultProjectNameStarter', "DatabaseProject");
|
||||
export const newDefaultProjectSaveLocation = localize('newDefaultProjectSaveLocation', "Would you like to update the default location to save new database projects?");
|
||||
export const invalidDefaultProjectSaveLocation = localize('invalidDefaultProjectSaveLocation', "Default location to save new database projects is invalid. Would you like to update it?");
|
||||
export const openWorkspaceSettings = localize('openWorkspaceSettings', "Yes, open Settings");
|
||||
export const doNotPromptAgain = localize('doNotPromptAgain', "Don't ask again");
|
||||
export const reloadProject = localize('reloadProject', "Would you like to reload your database project?");
|
||||
export function newObjectNamePrompt(objectType: string) { return localize('newObjectNamePrompt', 'New {0} name:', objectType); }
|
||||
export function deleteConfirmation(toDelete: string) { return localize('deleteConfirmation', "Are you sure you want to delete {0}?", toDelete); }
|
||||
@@ -95,7 +92,7 @@ export const defaultUser = localize('default', "default");
|
||||
export const addDatabaseReferenceDialogName = localize('addDatabaseReferencedialogName', "Add database reference");
|
||||
export const addDatabaseReferenceOkButtonText = localize('addDatabaseReferenceOkButtonText', "Add reference");
|
||||
export const referenceRadioButtonsGroupTitle = localize('referenceRadioButtonsGroupTitle', "Type");
|
||||
export const projectRadioButtonTitle = localize('projectRadioButtonTitle', "Database project in folder");
|
||||
export const projectRadioButtonTitle = localize('projectRadioButtonTitle', "Project");
|
||||
export const systemDatabaseRadioButtonTitle = localize('systemDatabaseRadioButtonTitle', "System database");
|
||||
export const dacpacText = localize('dacpacText', "Data-tier application (.dacpac)");
|
||||
export const dacpacPlaceholder = localize('dacpacPlaceholder', "Select .dacpac");
|
||||
@@ -259,11 +256,6 @@ export const activeDirectoryInteractive = 'active directory interactive';
|
||||
export const userIdSetting = 'User ID';
|
||||
export const passwordSetting = 'Password';
|
||||
|
||||
// Workspace settings for saving new database projects
|
||||
export const dbProjectConfigurationKey = 'sqlDatabaseProjects';
|
||||
export const projectSaveLocationKey = 'defaultProjectSaveLocation';
|
||||
export const showUpdatePromptKey = 'showUpdateSaveLocationPrompt';
|
||||
|
||||
// Authentication types
|
||||
export const integratedAuth = 'Integrated';
|
||||
export const azureMfaAuth = 'AzureMFA';
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface IconPath {
|
||||
export class IconPathHelper {
|
||||
private static extensionContext: vscode.ExtensionContext;
|
||||
public static databaseProject: IconPath;
|
||||
public static colorfulSqlProject: IconPath;
|
||||
|
||||
public static dataSourceGroup: IconPath;
|
||||
public static dataSourceSql: IconPath;
|
||||
@@ -31,6 +32,7 @@ export class IconPathHelper {
|
||||
IconPathHelper.extensionContext = extensionContext;
|
||||
|
||||
IconPathHelper.databaseProject = IconPathHelper.makeIcon('databaseProject');
|
||||
IconPathHelper.colorfulSqlProject = IconPathHelper.makeIcon('colorfulSqlProject', true);
|
||||
|
||||
IconPathHelper.dataSourceGroup = IconPathHelper.makeIcon('dataSourceGroup');
|
||||
IconPathHelper.dataSourceSql = IconPathHelper.makeIcon('dataSource-sql');
|
||||
|
||||
@@ -8,6 +8,7 @@ import * as os from 'os';
|
||||
import * as constants from './constants';
|
||||
import * as path from 'path';
|
||||
import * as glob from 'fast-glob';
|
||||
import * as dataworkspace from 'dataworkspace';
|
||||
import * as mssql from '../../../mssql';
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
@@ -217,7 +218,7 @@ export function isValidSqlCmdVariableName(name: string | undefined): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* Recursively gets all the sqlproj files at any depth in a folder
|
||||
* @param folderPath
|
||||
*/
|
||||
@@ -231,6 +232,19 @@ export async function getSqlProjectFilesInFolder(folderPath: string): Promise<st
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the projects in the workspace that are sqlproj
|
||||
*/
|
||||
export function getSqlProjectsInWorkspace(): vscode.Uri[] {
|
||||
const api = getDataWorkspaceExtensionApi();
|
||||
return api.getProjectsInWorkspace().filter((p: vscode.Uri) => path.extname(p.fsPath) === constants.sqlprojExtension);
|
||||
}
|
||||
|
||||
export function getDataWorkspaceExtensionApi(): dataworkspace.IExtension {
|
||||
const extension = vscode.extensions.getExtension(dataworkspace.extension.name)!;
|
||||
return extension.exports;
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns the default deployment options from DacFx
|
||||
*/
|
||||
export async function GetDefaultDeploymentOptions(): Promise<mssql.DeploymentOptions> {
|
||||
|
||||
@@ -28,6 +28,10 @@ export class SqlDatabaseProjectTreeViewProvider implements vscode.TreeDataProvid
|
||||
this.roots = [];
|
||||
}
|
||||
|
||||
public notifyTreeDataChanged() {
|
||||
this._onDidChangeTreeData.fire(undefined);
|
||||
}
|
||||
|
||||
public getTreeItem(element: BaseProjectTreeItem): vscode.TreeItem {
|
||||
return element.treeItem;
|
||||
}
|
||||
|
||||
@@ -6,33 +6,23 @@
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import * as templates from '../templates/templates';
|
||||
import * as constants from '../common/constants';
|
||||
import * as path from 'path';
|
||||
import * as newProjectTool from '../tools/newProjectTool';
|
||||
|
||||
import { SqlDatabaseProjectTreeViewProvider } from './databaseProjectTreeViewProvider';
|
||||
import { getErrorMessage, getSqlProjectFilesInFolder } from '../common/utils';
|
||||
import { ProjectsController } from './projectController';
|
||||
import { BaseProjectTreeItem } from '../models/tree/baseTreeItem';
|
||||
import { NetCoreTool } from '../tools/netcoreTool';
|
||||
import { Project } from '../models/project';
|
||||
import { ExternalStreamingJobFileNode, FileNode, FolderNode } from '../models/tree/fileFolderTreeItem';
|
||||
import { IconPathHelper } from '../common/iconHelper';
|
||||
import { IProjectProvider } from 'dataworkspace';
|
||||
import { IProjectProvider, WorkspaceTreeItem } from 'dataworkspace';
|
||||
import { SqlDatabaseProjectProvider } from '../projectProvider/projectProvider';
|
||||
|
||||
const SQL_DATABASE_PROJECTS_VIEW_ID = 'sqlDatabaseProjectsView';
|
||||
|
||||
/**
|
||||
* The main controller class that initializes the extension
|
||||
*/
|
||||
export default class MainController implements vscode.Disposable {
|
||||
protected dbProjectTreeViewProvider: SqlDatabaseProjectTreeViewProvider = new SqlDatabaseProjectTreeViewProvider();
|
||||
protected projectsController: ProjectsController;
|
||||
protected netcoreTool: NetCoreTool;
|
||||
|
||||
public constructor(private context: vscode.ExtensionContext) {
|
||||
this.projectsController = new ProjectsController(this.dbProjectTreeViewProvider);
|
||||
this.projectsController = new ProjectsController();
|
||||
this.netcoreTool = new NetCoreTool();
|
||||
}
|
||||
|
||||
@@ -49,147 +39,42 @@ export default class MainController implements vscode.Disposable {
|
||||
|
||||
public async activate(): Promise<IProjectProvider> {
|
||||
await this.initializeDatabaseProjects();
|
||||
return new SqlDatabaseProjectProvider();
|
||||
return new SqlDatabaseProjectProvider(this.projectsController);
|
||||
}
|
||||
|
||||
private async initializeDatabaseProjects(): Promise<void> {
|
||||
// init commands
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.new', async () => { await this.createNewProject(); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.open', async () => { await this.openProjectFromFile(); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.close', (node: BaseProjectTreeItem) => { this.projectsController.closeProject(node); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.properties', async (node: BaseProjectTreeItem) => { await vscode.window.showErrorMessage(`Properties not yet implemented: ${node.uri.path}`); }); // TODO
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.properties', async (node: WorkspaceTreeItem) => { await vscode.window.showErrorMessage(`Properties not yet implemented: ${node.element.uri.path}`); }); // TODO
|
||||
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.build', async (node: BaseProjectTreeItem) => { await this.projectsController.buildProject(node); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.publish', async (node: BaseProjectTreeItem) => { await this.projectsController.publishProject(node); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.schemaCompare', async (node: BaseProjectTreeItem) => { await this.projectsController.schemaCompare(node); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.importDatabase', async (profile: azdata.IConnectionProfile) => { await this.projectsController.importNewDatabaseProject(profile); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.build', async (node: WorkspaceTreeItem) => { await this.projectsController.buildProject(node); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.publish', async (node: WorkspaceTreeItem) => { await this.projectsController.publishProject(node); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.schemaCompare', async (node: WorkspaceTreeItem) => { await this.projectsController.schemaCompare(node); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.createProjectFromDatabase', async (profile: azdata.IConnectionProfile) => { await this.projectsController.createProjectFromDatabase(profile); });
|
||||
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.newScript', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.script); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.newPreDeploymentScript', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.preDeployScript); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.newPostDeploymentScript', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.postDeployScript); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.newTable', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.table); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.newView', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.view); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.newStoredProcedure', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.storedProcedure); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.newExternalStreamingJob', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.externalStreamingJob); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.newItem', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.newFolder', async (node: BaseProjectTreeItem) => { await this.projectsController.addFolderPrompt(node); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.newScript', async (node: WorkspaceTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.script); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.newPreDeploymentScript', async (node: WorkspaceTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.preDeployScript); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.newPostDeploymentScript', async (node: WorkspaceTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.postDeployScript); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.newTable', async (node: WorkspaceTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.table); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.newView', async (node: WorkspaceTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.view); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.newStoredProcedure', async (node: WorkspaceTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.storedProcedure); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.newExternalStreamingJob', async (node: WorkspaceTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.externalStreamingJob); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.newItem', async (node: WorkspaceTreeItem) => { await this.projectsController.addItemPromptFromNode(node); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.newFolder', async (node: WorkspaceTreeItem) => { await this.projectsController.addFolderPrompt(node); });
|
||||
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.addDatabaseReference', async (node: BaseProjectTreeItem) => { await this.projectsController.addDatabaseReference(node); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.openContainingFolder', async (node: BaseProjectTreeItem) => { await this.projectsController.openContainingFolder(node); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.editProjectFile', async (node: BaseProjectTreeItem) => { await this.projectsController.editProjectFile(node); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.delete', async (node: BaseProjectTreeItem) => { await this.projectsController.delete(node); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.exclude', async (node: FileNode | FolderNode) => { await this.projectsController.exclude(node); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.changeTargetPlatform', async (node: BaseProjectTreeItem) => { await this.projectsController.changeTargetPlatform(node); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.validateExternalStreamingJob', async (node: ExternalStreamingJobFileNode) => { await this.projectsController.validateExternalStreamingJob(node); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.addDatabaseReference', async (node: WorkspaceTreeItem) => { await this.projectsController.addDatabaseReference(node); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.openContainingFolder', async (node: WorkspaceTreeItem) => { await this.projectsController.openContainingFolder(node); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.editProjectFile', async (node: WorkspaceTreeItem) => { await this.projectsController.editProjectFile(node); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.delete', async (node: WorkspaceTreeItem) => { await this.projectsController.delete(node); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.exclude', async (node: WorkspaceTreeItem) => { await this.projectsController.exclude(node); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.changeTargetPlatform', async (node: WorkspaceTreeItem) => { await this.projectsController.changeTargetPlatform(node); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.validateExternalStreamingJob', async (node: WorkspaceTreeItem) => { await this.projectsController.validateExternalStreamingJob(node); });
|
||||
|
||||
IconPathHelper.setExtensionContext(this.extensionContext);
|
||||
|
||||
// init view
|
||||
const treeView = vscode.window.createTreeView(SQL_DATABASE_PROJECTS_VIEW_ID, {
|
||||
treeDataProvider: this.dbProjectTreeViewProvider,
|
||||
showCollapseAll: true
|
||||
});
|
||||
this.dbProjectTreeViewProvider.setTreeView(treeView);
|
||||
|
||||
this.extensionContext.subscriptions.push(treeView);
|
||||
|
||||
await templates.loadTemplates(path.join(this.context.extensionPath, 'resources', 'templates'));
|
||||
|
||||
// ensure .net core is installed
|
||||
await this.netcoreTool.findOrInstallNetCore();
|
||||
|
||||
// set the user settings around saving new projects to default value
|
||||
await newProjectTool.initializeSaveLocationSetting();
|
||||
|
||||
// load any sql projects that are open in workspace folder
|
||||
await this.loadProjectsInWorkspace();
|
||||
}
|
||||
|
||||
public async loadProjectsInWorkspace(): Promise<void> {
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (workspaceFolders?.length) {
|
||||
await Promise.all(workspaceFolders.map(async (workspaceFolder) => {
|
||||
await this.loadProjectsInFolder(workspaceFolder.uri.fsPath);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
public async loadProjectsInFolder(folderPath: string): Promise<void> {
|
||||
const results = await getSqlProjectFilesInFolder(folderPath);
|
||||
|
||||
for (let f in results) {
|
||||
// open the project, but don't switch focus to the file explorer viewlet
|
||||
await this.projectsController.openProject(vscode.Uri.file(results[f]), false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts the user to select a .sqlproj file to open
|
||||
* TODO: define behavior once projects are automatically opened from workspace
|
||||
*/
|
||||
public async openProjectFromFile(): Promise<void> {
|
||||
try {
|
||||
let filter: { [key: string]: string[] } = {};
|
||||
|
||||
filter[constants.sqlDatabaseProject] = ['sqlproj'];
|
||||
|
||||
let files: vscode.Uri[] | undefined = await vscode.window.showOpenDialog({ filters: filter });
|
||||
|
||||
if (files) {
|
||||
for (const file of files) {
|
||||
await this.projectsController.openProject(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
vscode.window.showErrorMessage(getErrorMessage(err));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new SQL database project from a template, prompting the user for a name and location
|
||||
*/
|
||||
public async createNewProject(): Promise<Project | undefined> {
|
||||
try {
|
||||
let newProjName = await vscode.window.showInputBox({
|
||||
prompt: constants.newDatabaseProjectName,
|
||||
value: newProjectTool.defaultProjectNameNewProj()
|
||||
});
|
||||
|
||||
newProjName = newProjName?.trim();
|
||||
|
||||
if (!newProjName) {
|
||||
// TODO: is this case considered an intentional cancellation (shouldn't warn) or an error case (should warn)?
|
||||
vscode.window.showErrorMessage(constants.projectNameRequired);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let selectionResult = await vscode.window.showOpenDialog({
|
||||
canSelectFiles: false,
|
||||
canSelectFolders: true,
|
||||
canSelectMany: false,
|
||||
defaultUri: newProjectTool.defaultProjectSaveLocation()
|
||||
});
|
||||
|
||||
if (!selectionResult) {
|
||||
vscode.window.showErrorMessage(constants.projectLocationRequired);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// TODO: what if the selected folder is outside the workspace?
|
||||
|
||||
const newProjFolderUri = (selectionResult as vscode.Uri[])[0];
|
||||
const newProjFilePath = await this.projectsController.createNewProject(<string>newProjName, newProjFolderUri, true);
|
||||
const proj = await this.projectsController.openProject(vscode.Uri.file(newProjFilePath));
|
||||
|
||||
newProjectTool.updateSaveLocationSetting();
|
||||
|
||||
return proj;
|
||||
}
|
||||
catch (err) {
|
||||
vscode.window.showErrorMessage(getErrorMessage(err));
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as constants from '../common/constants';
|
||||
import * as dataSources from '../models/dataSources/dataSources';
|
||||
import * as mssql from '../../../mssql';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
@@ -14,12 +13,13 @@ 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';
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
import { PublishDatabaseDialog } from '../dialogs/publishDatabaseDialog';
|
||||
import { Project, reservedProjectFolders, FileProjectEntry, SqlProjectReferenceProjectEntry, IDatabaseReferenceProjectEntry } from '../models/project';
|
||||
import { SqlDatabaseProjectTreeViewProvider } from './databaseProjectTreeViewProvider';
|
||||
import { FolderNode, FileNode, ExternalStreamingJobFileNode } from '../models/tree/fileFolderTreeItem';
|
||||
import { FolderNode, FileNode } from '../models/tree/fileFolderTreeItem';
|
||||
import { IPublishSettings, IGenerateScriptSettings } from '../models/IPublishSettings';
|
||||
import { BaseProjectTreeItem } from '../models/tree/baseTreeItem';
|
||||
import { ProjectRootTreeItem } from '../models/tree/projectTreeItem';
|
||||
@@ -30,97 +30,24 @@ 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 { WorkspaceTreeItem } from 'dataworkspace';
|
||||
|
||||
/**
|
||||
* Controller for managing project lifecycle
|
||||
*/
|
||||
export class ProjectsController {
|
||||
private projectTreeViewProvider: SqlDatabaseProjectTreeViewProvider;
|
||||
private netCoreTool: NetCoreTool;
|
||||
private buildHelper: BuildHelper;
|
||||
|
||||
projects: Project[] = [];
|
||||
projFileWatchers = new Map<string, vscode.FileSystemWatcher>();
|
||||
|
||||
constructor(projTreeViewProvider: SqlDatabaseProjectTreeViewProvider) {
|
||||
this.projectTreeViewProvider = projTreeViewProvider;
|
||||
constructor() {
|
||||
this.netCoreTool = new NetCoreTool();
|
||||
this.buildHelper = new BuildHelper();
|
||||
}
|
||||
|
||||
public refreshProjectsTree() {
|
||||
this.projectTreeViewProvider.load(this.projects);
|
||||
}
|
||||
|
||||
public async openProject(projectFile: vscode.Uri, focusProject: boolean = true, isReferencedProject: boolean = false): Promise<Project> {
|
||||
for (const proj of this.projects) {
|
||||
if (proj.projectFilePath === projectFile.fsPath) {
|
||||
if (!isReferencedProject) {
|
||||
vscode.window.showInformationMessage(constants.projectAlreadyOpened(projectFile.fsPath));
|
||||
return proj;
|
||||
} else {
|
||||
throw new Error(constants.projectAlreadyOpened(projectFile.fsPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let newProject: Project;
|
||||
|
||||
try {
|
||||
// Read project file
|
||||
newProject = await Project.openProject(projectFile.fsPath);
|
||||
this.projects.push(newProject);
|
||||
|
||||
// open any reference projects (don't need to worry about circular dependencies because those aren't allowed)
|
||||
const referencedProjects = newProject.databaseReferences.filter(r => r instanceof SqlProjectReferenceProjectEntry);
|
||||
for (const proj of referencedProjects) {
|
||||
const projUri = vscode.Uri.file(path.join(newProject.projectFolderPath, proj.fsUri.fsPath));
|
||||
try {
|
||||
await this.openProject(projUri, false, true);
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
// Update for round tripping as needed
|
||||
await this.updateProjectForRoundTrip(newProject);
|
||||
|
||||
// Read datasources.json (if present)
|
||||
const dataSourcesFilePath = path.join(path.dirname(projectFile.fsPath), constants.dataSourcesFileName);
|
||||
|
||||
try {
|
||||
newProject.dataSources = await dataSources.load(dataSourcesFilePath);
|
||||
}
|
||||
catch (err) {
|
||||
if (err instanceof dataSources.NoDataSourcesFileError) {
|
||||
// TODO: prompt to create new datasources.json; for now, swallow
|
||||
}
|
||||
else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
this.refreshProjectsTree();
|
||||
|
||||
if (focusProject) {
|
||||
await this.focusProject(newProject);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
// if the project didnt load - remove it from the list of open projects
|
||||
this.projects = this.projects.filter((e) => { return e !== newProject; });
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
return newProject!;
|
||||
}
|
||||
|
||||
public async focusProject(project?: Project): Promise<void> {
|
||||
if (project && this.projects.includes(project)) {
|
||||
await this.projectTreeViewProvider.focus(project);
|
||||
await vscode.commands.executeCommand(constants.sqlDatabaseProjectsViewFocusCommand);
|
||||
}
|
||||
public refreshProjectsTree(workspaceTreeItem: dataworkspace.WorkspaceTreeItem): void {
|
||||
(workspaceTreeItem.treeDataProvider as SqlDatabaseProjectTreeViewProvider).notifyTreeDataChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,31 +93,19 @@ export class ProjectsController {
|
||||
return newProjFilePath;
|
||||
}
|
||||
|
||||
public closeProject(treeNode: BaseProjectTreeItem) {
|
||||
const project = this.getProjectFromContext(treeNode);
|
||||
this.projects = this.projects.filter((e) => { return e !== project; });
|
||||
|
||||
if (this.projFileWatchers.has(project.projectFilePath)) {
|
||||
this.projFileWatchers.get(project.projectFilePath)!.dispose();
|
||||
this.projFileWatchers.delete(project.projectFilePath);
|
||||
}
|
||||
|
||||
this.refreshProjectsTree();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a project, producing a dacpac
|
||||
* @param treeNode a treeItem in a project's hierarchy, to be used to obtain a Project
|
||||
* @returns path of the built dacpac
|
||||
*/
|
||||
public async buildProject(treeNode: BaseProjectTreeItem): Promise<string>;
|
||||
public async buildProject(treeNode: dataworkspace.WorkspaceTreeItem): Promise<string>;
|
||||
/**
|
||||
* Builds a project, producing a dacpac
|
||||
* @param project Project to be built
|
||||
* @returns path of the built dacpac
|
||||
*/
|
||||
public async buildProject(project: Project): Promise<string>;
|
||||
public async buildProject(context: Project | BaseProjectTreeItem | WorkspaceTreeItem): Promise<string | undefined> {
|
||||
public async buildProject(context: Project | dataworkspace.WorkspaceTreeItem): Promise<string> {
|
||||
const project: Project = this.getProjectFromContext(context);
|
||||
|
||||
// Check mssql extension for project dlls (tracking issue #10273)
|
||||
@@ -208,7 +123,7 @@ export class ProjectsController {
|
||||
}
|
||||
catch (err) {
|
||||
vscode.window.showErrorMessage(constants.projBuildFailed(utils.getErrorMessage(err)));
|
||||
return undefined;
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,18 +131,18 @@ export class ProjectsController {
|
||||
* Builds and publishes a project
|
||||
* @param treeNode a treeItem in a project's hierarchy, to be used to obtain a Project
|
||||
*/
|
||||
public publishProject(treeNode: BaseProjectTreeItem): PublishDatabaseDialog;
|
||||
public publishProject(treeNode: dataworkspace.WorkspaceTreeItem): PublishDatabaseDialog;
|
||||
/**
|
||||
* Builds and publishes a project
|
||||
* @param project Project to be built and published
|
||||
*/
|
||||
public publishProject(project: Project): PublishDatabaseDialog;
|
||||
public publishProject(context: Project | BaseProjectTreeItem): PublishDatabaseDialog {
|
||||
public publishProject(context: Project | dataworkspace.WorkspaceTreeItem): PublishDatabaseDialog {
|
||||
const project: Project = this.getProjectFromContext(context);
|
||||
let publishDatabaseDialog = this.getPublishDialog(project);
|
||||
|
||||
publishDatabaseDialog.publish = async (proj, prof) => await this.executionCallback(proj, prof);
|
||||
publishDatabaseDialog.generateScript = async (proj, prof) => await this.executionCallback(proj, prof);
|
||||
publishDatabaseDialog.publish = async (proj, prof) => await this.publishProjectCallback(proj, prof);
|
||||
publishDatabaseDialog.generateScript = async (proj, prof) => await this.publishProjectCallback(proj, prof);
|
||||
publishDatabaseDialog.readPublishProfile = async (profileUri) => await this.readPublishProfileCallback(profileUri);
|
||||
|
||||
publishDatabaseDialog.openDialog();
|
||||
@@ -235,7 +150,7 @@ export class ProjectsController {
|
||||
return publishDatabaseDialog;
|
||||
}
|
||||
|
||||
public async executionCallback(project: Project, settings: IPublishSettings | IGenerateScriptSettings): Promise<mssql.DacFxResult | undefined> {
|
||||
public async publishProjectCallback(project: Project, settings: IPublishSettings | IGenerateScriptSettings): Promise<mssql.DacFxResult | undefined> {
|
||||
const dacpacPath = await this.buildProject(project);
|
||||
|
||||
if (!dacpacPath) {
|
||||
@@ -268,7 +183,7 @@ export class ProjectsController {
|
||||
}
|
||||
}
|
||||
|
||||
public async schemaCompare(treeNode: BaseProjectTreeItem): Promise<void> {
|
||||
public async schemaCompare(treeNode: dataworkspace.WorkspaceTreeItem): Promise<void> {
|
||||
// check if schema compare extension is installed
|
||||
if (vscode.extensions.getExtension(constants.schemaCompareExtensionId)) {
|
||||
// build project
|
||||
@@ -285,9 +200,9 @@ export class ProjectsController {
|
||||
}
|
||||
}
|
||||
|
||||
public async addFolderPrompt(treeNode: BaseProjectTreeItem) {
|
||||
public async addFolderPrompt(treeNode: dataworkspace.WorkspaceTreeItem): Promise<void> {
|
||||
const project = this.getProjectFromContext(treeNode);
|
||||
const relativePathToParent = this.getRelativePath(treeNode);
|
||||
const relativePathToParent = this.getRelativePath(treeNode.element);
|
||||
const absolutePathToParent = path.join(project.projectFolderPath, relativePathToParent);
|
||||
const newFolderName = await this.promptForNewObjectName(new templates.ProjectScriptType(templates.folder, constants.folderFriendlyName, ''),
|
||||
project, absolutePathToParent);
|
||||
@@ -308,7 +223,7 @@ export class ProjectsController {
|
||||
}
|
||||
|
||||
await project.addFolderItem(relativeFolderPath);
|
||||
this.refreshProjectsTree();
|
||||
this.refreshProjectsTree(treeNode);
|
||||
} catch (err) {
|
||||
vscode.window.showErrorMessage(utils.getErrorMessage(err));
|
||||
}
|
||||
@@ -320,11 +235,11 @@ export class ProjectsController {
|
||||
return sameName && sameLocation;
|
||||
}
|
||||
|
||||
public async addItemPromptFromNode(treeNode: BaseProjectTreeItem, itemTypeName?: string) {
|
||||
await this.addItemPrompt(this.getProjectFromContext(treeNode), this.getRelativePath(treeNode), itemTypeName);
|
||||
public async addItemPromptFromNode(treeNode: dataworkspace.WorkspaceTreeItem, itemTypeName?: string): Promise<void> {
|
||||
await this.addItemPrompt(this.getProjectFromContext(treeNode), this.getRelativePath(treeNode.element), itemTypeName, treeNode.treeDataProvider as SqlDatabaseProjectTreeViewProvider);
|
||||
}
|
||||
|
||||
public async addItemPrompt(project: Project, relativePath: string, itemTypeName?: string) {
|
||||
public async addItemPrompt(project: Project, relativePath: string, itemTypeName?: string, treeDataProvider?: SqlDatabaseProjectTreeViewProvider): Promise<void> {
|
||||
if (!itemTypeName) {
|
||||
const items: vscode.QuickPickItem[] = [];
|
||||
|
||||
@@ -366,37 +281,38 @@ export class ProjectsController {
|
||||
const newEntry = await project.addScriptItem(relativeFilePath, newFileText, itemType.type);
|
||||
|
||||
await vscode.commands.executeCommand(constants.vscodeOpenCommand, newEntry.fsUri);
|
||||
|
||||
this.refreshProjectsTree();
|
||||
treeDataProvider?.notifyTreeDataChanged();
|
||||
} catch (err) {
|
||||
vscode.window.showErrorMessage(utils.getErrorMessage(err));
|
||||
}
|
||||
}
|
||||
|
||||
public async exclude(context: FileNode | FolderNode): Promise<void> {
|
||||
const project = this.getProjectFromContext(context);
|
||||
public async exclude(context: dataworkspace.WorkspaceTreeItem): Promise<void> {
|
||||
const node = context.element as BaseProjectTreeItem;
|
||||
const project = this.getProjectFromContext(node);
|
||||
|
||||
const fileEntry = this.getFileProjectEntry(project, context);
|
||||
const fileEntry = this.getFileProjectEntry(project, node);
|
||||
|
||||
if (fileEntry) {
|
||||
await project.exclude(fileEntry);
|
||||
} else {
|
||||
vscode.window.showErrorMessage(constants.unableToPerformAction(constants.excludeAction, context.uri.path));
|
||||
vscode.window.showErrorMessage(constants.unableToPerformAction(constants.excludeAction, node.uri.path));
|
||||
}
|
||||
|
||||
this.refreshProjectsTree();
|
||||
this.refreshProjectsTree(context);
|
||||
}
|
||||
|
||||
public async delete(context: BaseProjectTreeItem): Promise<void> {
|
||||
const project = this.getProjectFromContext(context);
|
||||
public async delete(context: dataworkspace.WorkspaceTreeItem): Promise<void> {
|
||||
const node = context.element as BaseProjectTreeItem;
|
||||
const project = this.getProjectFromContext(node);
|
||||
|
||||
let confirmationPrompt;
|
||||
if (context instanceof DatabaseReferenceTreeItem) {
|
||||
confirmationPrompt = constants.deleteReferenceConfirmation(context.friendlyName);
|
||||
} else if (context instanceof FolderNode) {
|
||||
confirmationPrompt = constants.deleteConfirmationContents(context.friendlyName);
|
||||
if (node instanceof DatabaseReferenceTreeItem) {
|
||||
confirmationPrompt = constants.deleteReferenceConfirmation(node.friendlyName);
|
||||
} else if (node instanceof FolderNode) {
|
||||
confirmationPrompt = constants.deleteConfirmationContents(node.friendlyName);
|
||||
} else {
|
||||
confirmationPrompt = constants.deleteConfirmation(context.friendlyName);
|
||||
confirmationPrompt = constants.deleteConfirmation(node.friendlyName);
|
||||
}
|
||||
|
||||
const response = await vscode.window.showWarningMessage(confirmationPrompt, { modal: true }, constants.yesString);
|
||||
@@ -407,15 +323,15 @@ export class ProjectsController {
|
||||
|
||||
let success = false;
|
||||
|
||||
if (context instanceof DatabaseReferenceTreeItem) {
|
||||
const databaseReference = this.getDatabaseReference(project, context);
|
||||
if (node instanceof DatabaseReferenceTreeItem) {
|
||||
const databaseReference = this.getDatabaseReference(project, node);
|
||||
|
||||
if (databaseReference) {
|
||||
await project.deleteDatabaseReference(databaseReference);
|
||||
success = true;
|
||||
}
|
||||
} else if (context instanceof FileNode || FolderNode) {
|
||||
const fileEntry = this.getFileProjectEntry(project, context);
|
||||
} else if (node instanceof FileNode || FolderNode) {
|
||||
const fileEntry = this.getFileProjectEntry(project, node);
|
||||
|
||||
if (fileEntry) {
|
||||
await project.deleteFileFolder(fileEntry);
|
||||
@@ -424,9 +340,9 @@ export class ProjectsController {
|
||||
}
|
||||
|
||||
if (success) {
|
||||
this.refreshProjectsTree();
|
||||
this.refreshProjectsTree(context);
|
||||
} else {
|
||||
vscode.window.showErrorMessage(constants.unableToPerformAction(constants.deleteAction, context.uri.path));
|
||||
vscode.window.showErrorMessage(constants.unableToPerformAction(constants.deleteAction, node.uri.path));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,7 +373,7 @@ export class ProjectsController {
|
||||
* Opens the folder containing the project
|
||||
* @param context a treeItem in a project's hierarchy, to be used to obtain a Project
|
||||
*/
|
||||
public async openContainingFolder(context: BaseProjectTreeItem): Promise<void> {
|
||||
public async openContainingFolder(context: dataworkspace.WorkspaceTreeItem): Promise<void> {
|
||||
const project = this.getProjectFromContext(context);
|
||||
await vscode.commands.executeCommand(constants.revealFileInOsCommand, vscode.Uri.file(project.projectFilePath));
|
||||
}
|
||||
@@ -467,7 +383,7 @@ export class ProjectsController {
|
||||
* reload their project.
|
||||
* @param context a treeItem in a project's hierarchy, to be used to obtain a Project
|
||||
*/
|
||||
public async editProjectFile(context: BaseProjectTreeItem): Promise<void> {
|
||||
public async editProjectFile(context: dataworkspace.WorkspaceTreeItem): Promise<void> {
|
||||
const project = this.getProjectFromContext(context);
|
||||
|
||||
try {
|
||||
@@ -475,11 +391,11 @@ export class ProjectsController {
|
||||
const projFileWatcher: vscode.FileSystemWatcher = vscode.workspace.createFileSystemWatcher(project.projectFilePath);
|
||||
this.projFileWatchers.set(project.projectFilePath, projFileWatcher);
|
||||
|
||||
projFileWatcher.onDidChange(async (projectFileUri: vscode.Uri) => {
|
||||
projFileWatcher.onDidChange(async () => {
|
||||
const result = await vscode.window.showInformationMessage(constants.reloadProject, constants.yesString, constants.noString);
|
||||
|
||||
if (result === constants.yesString) {
|
||||
this.reloadProject(projectFileUri);
|
||||
this.reloadProject(context);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -500,12 +416,12 @@ export class ProjectsController {
|
||||
* Reloads the given project. Throws an error if given project is not a valid open project.
|
||||
* @param projectFileUri the uri of the project to be reloaded
|
||||
*/
|
||||
public async reloadProject(projectFileUri: vscode.Uri) {
|
||||
const project = this.projects.find((e) => e.projectFilePath === projectFileUri.fsPath);
|
||||
public async reloadProject(context: dataworkspace.WorkspaceTreeItem): Promise<void> {
|
||||
const project = this.getProjectFromContext(context);
|
||||
if (project) {
|
||||
// won't open any newly referenced projects, but otherwise matches the behavior of reopening the project
|
||||
await project.readProjFile();
|
||||
this.refreshProjectsTree();
|
||||
this.refreshProjectsTree(context);
|
||||
} else {
|
||||
throw new Error(constants.invalidProjectReload);
|
||||
}
|
||||
@@ -515,7 +431,7 @@ export class ProjectsController {
|
||||
* Changes the project's DSP to the selected target platform
|
||||
* @param context a treeItem in a project's hierarchy, to be used to obtain a Project
|
||||
*/
|
||||
public async changeTargetPlatform(context: Project | BaseProjectTreeItem): Promise<void> {
|
||||
public async changeTargetPlatform(context: Project | dataworkspace.WorkspaceTreeItem): Promise<void> {
|
||||
const project = this.getProjectFromContext(context);
|
||||
const selectedTargetPlatform = (await vscode.window.showQuickPick((Array.from(constants.targetPlatformToVersion.keys())).map(version => { return { label: version }; }),
|
||||
{
|
||||
@@ -533,23 +449,24 @@ export class ProjectsController {
|
||||
* Adds a database reference to the project
|
||||
* @param context a treeItem in a project's hierarchy, to be used to obtain a Project
|
||||
*/
|
||||
public async addDatabaseReference(context: Project | BaseProjectTreeItem): Promise<AddDatabaseReferenceDialog> {
|
||||
public async addDatabaseReference(context: Project | dataworkspace.WorkspaceTreeItem): Promise<AddDatabaseReferenceDialog> {
|
||||
const project = this.getProjectFromContext(context);
|
||||
|
||||
const addDatabaseReferenceDialog = this.getAddDatabaseReferenceDialog(project);
|
||||
addDatabaseReferenceDialog.addReference = async (proj, prof) => await this.addDatabaseReferenceCallback(proj, prof);
|
||||
addDatabaseReferenceDialog.addReference = async (proj, prof) => await this.addDatabaseReferenceCallback(proj, prof, context as dataworkspace.WorkspaceTreeItem);
|
||||
|
||||
addDatabaseReferenceDialog.openDialog();
|
||||
|
||||
return addDatabaseReferenceDialog;
|
||||
}
|
||||
|
||||
public async addDatabaseReferenceCallback(project: Project, settings: ISystemDatabaseReferenceSettings | IDacpacReferenceSettings | IProjectReferenceSettings): Promise<void> {
|
||||
public async addDatabaseReferenceCallback(project: Project, settings: ISystemDatabaseReferenceSettings | IDacpacReferenceSettings | IProjectReferenceSettings, context: dataworkspace.WorkspaceTreeItem): Promise<void> {
|
||||
try {
|
||||
if ((<IProjectReferenceSettings>settings).projectName !== undefined) {
|
||||
// get project path and guid
|
||||
const projectReferenceSettings = settings as IProjectReferenceSettings;
|
||||
const referencedProject = this.projects.find(p => p.projectFileName === projectReferenceSettings.projectName);
|
||||
const workspaceProjects = await utils.getSqlProjectsInWorkspace();
|
||||
const referencedProject = await Project.openProject(workspaceProjects.filter(p => path.parse(p.fsPath).name === projectReferenceSettings.projectName)[0].fsPath);
|
||||
const relativePath = path.relative(project.projectFolderPath, referencedProject?.projectFilePath!);
|
||||
projectReferenceSettings.projectRelativePath = vscode.Uri.file(relativePath);
|
||||
projectReferenceSettings.projectGuid = referencedProject?.projectGuid!;
|
||||
@@ -571,23 +488,64 @@ export class ProjectsController {
|
||||
await project.addDatabaseReference(<IDacpacReferenceSettings>settings);
|
||||
}
|
||||
|
||||
this.refreshProjectsTree();
|
||||
this.refreshProjectsTree(context);
|
||||
} catch (err) {
|
||||
vscode.window.showErrorMessage(utils.getErrorMessage(err));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
|
||||
public async validateExternalStreamingJob(node: ExternalStreamingJobFileNode): Promise<mssql.ValidateStreamingJobResult> {
|
||||
// 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);
|
||||
|
||||
let dacpacPath: string = project.dacpacOutputPath;
|
||||
|
||||
if (!await utils.exists(dacpacPath)) {
|
||||
dacpacPath = await this.buildProject(node);
|
||||
dacpacPath = await this.buildProject(project);
|
||||
}
|
||||
|
||||
const streamingJobDefinition: string = (await fs.readFile(node.fileSystemUri.fsPath)).toString();
|
||||
const streamingJobDefinition: string = (await fs.readFile(node.element.fileSystemUri.fsPath)).toString();
|
||||
|
||||
const dacFxService = await this.getDaxFxService();
|
||||
const result: mssql.ValidateStreamingJobResult = await dacFxService.validateStreamingJob(dacpacPath, streamingJobDefinition);
|
||||
@@ -631,9 +589,9 @@ export class ProjectsController {
|
||||
}
|
||||
}
|
||||
|
||||
private getProjectFromContext(context: Project | BaseProjectTreeItem | WorkspaceTreeItem): Project {
|
||||
private getProjectFromContext(context: Project | BaseProjectTreeItem | dataworkspace.WorkspaceTreeItem): Project {
|
||||
if ('element' in context) {
|
||||
return context.element.project;
|
||||
return context.element.root.project;
|
||||
}
|
||||
|
||||
if (context instanceof Project) {
|
||||
@@ -642,8 +600,7 @@ export class ProjectsController {
|
||||
|
||||
if (context.root instanceof ProjectRootTreeItem) {
|
||||
return (<ProjectRootTreeItem>context.root).project;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
throw new Error(constants.unexpectedProjectContext(context.uri.path));
|
||||
}
|
||||
}
|
||||
@@ -692,45 +649,6 @@ export class ProjectsController {
|
||||
return treeNode instanceof FolderNode ? utils.trimUri(treeNode.root.uri, treeNode.uri) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a new SQL database project from the existing database,
|
||||
* prompting the user for a name, file path location and extract target
|
||||
*/
|
||||
public async importNewDatabaseProject(context: azdata.IConnectionProfile | any): Promise<void> {
|
||||
|
||||
// TODO: Refactor code
|
||||
try {
|
||||
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';
|
||||
|
||||
newProjectTool.updateSaveLocationSetting();
|
||||
|
||||
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.importApiCall(model); // Call ExtractAPI in DacFx Service
|
||||
let fileFolderList: string[] = model.extractTarget === mssql.ExtractTarget.file ? [model.filePath] : await this.generateList(model.filePath); // Create a list of all the files and directories to be added to project
|
||||
|
||||
await project.addToProject(fileFolderList); // Add generated file structure to the project
|
||||
await this.openProject(vscode.Uri.file(newProjFilePath));
|
||||
}
|
||||
catch (err) {
|
||||
vscode.window.showErrorMessage(utils.getErrorMessage(err));
|
||||
}
|
||||
}
|
||||
|
||||
public async getModelFromContext(context: any): Promise<ImportDataModel | undefined> {
|
||||
let model = <ImportDataModel>{};
|
||||
|
||||
@@ -861,13 +779,13 @@ export class ProjectsController {
|
||||
return projUri;
|
||||
}
|
||||
|
||||
public async importApiCall(model: ImportDataModel): Promise<void> {
|
||||
public async createProjectFromDatabaseApiCall(model: ImportDataModel): Promise<void> {
|
||||
let ext = vscode.extensions.getExtension(mssql.extension.name)!;
|
||||
|
||||
const service = (await ext.activate() as mssql.IExtension).dacFx;
|
||||
const ownerUri = await azdata.connection.getUriForConnection(model.serverId);
|
||||
|
||||
await service.importDatabaseProject(model.database, model.filePath, model.projName, model.version, ownerUri, model.extractTarget, azdata.TaskExecutionMode.execute);
|
||||
await service.createProjectFromDatabase(model.database, model.filePath, model.projName, model.version, ownerUri, model.extractTarget, azdata.TaskExecutionMode.execute);
|
||||
|
||||
// TODO: Check for success; throw error
|
||||
}
|
||||
|
||||
@@ -261,26 +261,12 @@ export class AddDatabaseReferenceDialog {
|
||||
this.setDefaultDatabaseValues();
|
||||
});
|
||||
|
||||
// get projects in workspace
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (workspaceFolders?.length) {
|
||||
let projectFiles = await utils.getSqlProjectFilesInFolder(workspaceFolders[0].uri.fsPath);
|
||||
// get projects in workspace and filter to only sql projects
|
||||
let projectFiles: vscode.Uri[] = utils.getSqlProjectsInWorkspace();
|
||||
|
||||
// check if current project is in same open folder (should only be able to add a reference to another project in
|
||||
// the folder if the current project is also in the folder)
|
||||
if (projectFiles.find(p => p === utils.getPlatformSafeFileEntryPath(this.project.projectFilePath))) {
|
||||
// filter out current project
|
||||
projectFiles = projectFiles.filter(p => p !== utils.getPlatformSafeFileEntryPath(this.project.projectFilePath));
|
||||
|
||||
projectFiles.forEach(p => {
|
||||
projectFiles[projectFiles.indexOf(p)] = path.parse(p).name;
|
||||
});
|
||||
|
||||
this.projectDropdown.values = projectFiles;
|
||||
} else {
|
||||
this.projectDropdown.values = [];
|
||||
}
|
||||
}
|
||||
// filter out current project
|
||||
projectFiles = projectFiles.filter(p => p.fsPath !== this.project.projectFilePath);
|
||||
this.projectDropdown.values = projectFiles.map(p => path.parse(p.fsPath).name);
|
||||
|
||||
return {
|
||||
component: this.projectDropdown,
|
||||
|
||||
@@ -510,7 +510,7 @@ export class PublishDatabaseDialog {
|
||||
|
||||
private async updateConnectionComponents(connectionTextboxValue: string, connectionId: string) {
|
||||
this.targetConnectionTextBox!.value = connectionTextboxValue;
|
||||
this.targetConnectionTextBox!.placeHolder = connectionTextboxValue;
|
||||
this.targetConnectionTextBox!.updateProperty('title', connectionTextboxValue);
|
||||
|
||||
// populate database dropdown with the databases for this connection
|
||||
if (connectionId) {
|
||||
@@ -583,7 +583,7 @@ export class PublishDatabaseDialog {
|
||||
|
||||
// show file path in text box and hover text
|
||||
this.loadProfileTextBox!.value = fileUris[0].fsPath;
|
||||
this.loadProfileTextBox!.placeHolder = fileUris[0].fsPath;
|
||||
this.loadProfileTextBox!.updateProperty('title', fileUris[0].fsPath);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
import * as constants from '../../common/constants';
|
||||
import { SqlConnectionDataSource } from './sqlConnectionStringSource';
|
||||
|
||||
/**
|
||||
* Abstract class for a datasource in a project
|
||||
@@ -53,11 +52,11 @@ export async function load(dataSourcesFilePath: string): Promise<DataSource[]> {
|
||||
// TODO: do we have a construct for parsing version numbers?
|
||||
switch (rawJsonContents.version) {
|
||||
case '0.0.0':
|
||||
const dataSources: DataSourceFileJson = rawJsonContents as DataSourceFileJson;
|
||||
// const dataSources: DataSourceFileJson = rawJsonContents as DataSourceFileJson;
|
||||
|
||||
for (const source of dataSources.datasources) {
|
||||
output.push(createDataSource(source));
|
||||
}
|
||||
// for (const source of dataSources.datasources) {
|
||||
// output.push(createDataSource(source));
|
||||
// }
|
||||
|
||||
break;
|
||||
default:
|
||||
@@ -70,11 +69,12 @@ export async function load(dataSourcesFilePath: string): Promise<DataSource[]> {
|
||||
/**
|
||||
* Creates DataSource object from JSON
|
||||
*/
|
||||
function createDataSource(json: DataSourceJson): DataSource {
|
||||
switch (json.type) {
|
||||
case SqlConnectionDataSource.type:
|
||||
return SqlConnectionDataSource.fromJson(json);
|
||||
default:
|
||||
throw new Error(constants.unknownDataSourceType + json.type);
|
||||
}
|
||||
}
|
||||
// Commenting this out because circular dependency with SqlConnectionDataSource was causing extension to not activate
|
||||
// function createDataSource(json: DataSourceJson): DataSource {
|
||||
// switch (json.type) {
|
||||
// case SqlConnectionDataSource.type:
|
||||
// return SqlConnectionDataSource.fromJson(json);
|
||||
// default:
|
||||
// throw new Error(constants.unknownDataSourceType + json.type);
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -53,6 +53,7 @@ export class Project {
|
||||
public static async openProject(projectFilePath: string): Promise<Project> {
|
||||
const proj = new Project(projectFilePath);
|
||||
await proj.readProjFile();
|
||||
await proj.updateProjectForRoundTrip();
|
||||
|
||||
return proj;
|
||||
}
|
||||
@@ -60,7 +61,7 @@ export class Project {
|
||||
/**
|
||||
* Reads the project setting and contents from the file
|
||||
*/
|
||||
public async readProjFile() {
|
||||
public async readProjFile(): Promise<void> {
|
||||
this.resetProject();
|
||||
|
||||
const projFileText = await fs.readFile(this.projectFilePath);
|
||||
@@ -178,7 +179,7 @@ export class Project {
|
||||
}
|
||||
}
|
||||
|
||||
private resetProject() {
|
||||
private resetProject(): void {
|
||||
this.files = [];
|
||||
this.importedTargets = [];
|
||||
this.databaseReferences = [];
|
||||
@@ -189,11 +190,27 @@ export class Project {
|
||||
this.projFileXmlDoc = undefined;
|
||||
}
|
||||
|
||||
public async updateProjectForRoundTrip() {
|
||||
await fs.copyFile(this.projectFilePath, this.projectFilePath + '_backup');
|
||||
await this.updateImportToSupportRoundTrip();
|
||||
await this.updatePackageReferenceInProjFile();
|
||||
await this.updateAfterCleanTargetInProjFile();
|
||||
public async updateProjectForRoundTrip(): Promise<void> {
|
||||
if (this.importedTargets.includes(constants.NetCoreTargets) && !this.containsSSDTOnlySystemDatabaseReferences()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.importedTargets.includes(constants.NetCoreTargets)) {
|
||||
const result = await window.showWarningMessage(constants.updateProjectForRoundTrip, constants.yesString, constants.noString);
|
||||
if (result === constants.yesString) {
|
||||
await fs.copyFile(this.projectFilePath, this.projectFilePath + '_backup');
|
||||
await this.updateImportToSupportRoundTrip();
|
||||
await this.updatePackageReferenceInProjFile();
|
||||
await this.updateAfterCleanTargetInProjFile();
|
||||
await this.updateSystemDatabaseReferencesInProjFile();
|
||||
}
|
||||
} else if (this.containsSSDTOnlySystemDatabaseReferences()) {
|
||||
const result = await window.showWarningMessage(constants.updateProjectDatabaseReferencesForRoundTrip, constants.yesString, constants.noString);
|
||||
if (result === constants.yesString) {
|
||||
await fs.copyFile(this.projectFilePath, this.projectFilePath + '_backup');
|
||||
await this.updateSystemDatabaseReferencesInProjFile();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async updateImportToSupportRoundTrip(): Promise<void> {
|
||||
@@ -460,7 +477,7 @@ export class Project {
|
||||
* @param name name of the variable
|
||||
* @param defaultValue
|
||||
*/
|
||||
public async addSqlCmdVariable(name: string, defaultValue: string) {
|
||||
public async addSqlCmdVariable(name: string, defaultValue: string): Promise<void> {
|
||||
const sqlCmdVariableEntry = new SqlCmdVariableProjectEntry(name, defaultValue);
|
||||
await this.addToProjFile(sqlCmdVariableEntry);
|
||||
}
|
||||
@@ -851,7 +868,7 @@ export class Project {
|
||||
await this.serializeToProjFile(this.projFileXmlDoc);
|
||||
}
|
||||
|
||||
private async removeFromProjFile(entries: ProjectEntry | ProjectEntry[]) {
|
||||
private async removeFromProjFile(entries: ProjectEntry | ProjectEntry[]): Promise<void> {
|
||||
if (entries instanceof ProjectEntry) {
|
||||
entries = [entries];
|
||||
}
|
||||
@@ -876,7 +893,7 @@ export class Project {
|
||||
await this.serializeToProjFile(this.projFileXmlDoc);
|
||||
}
|
||||
|
||||
private async serializeToProjFile(projFileContents: any) {
|
||||
private async serializeToProjFile(projFileContents: any): Promise<void> {
|
||||
let xml = new xmldom.XMLSerializer().serializeToString(projFileContents);
|
||||
xml = xmlFormat(xml, <any>{ collapseContent: true, indentation: ' ', lineSeparator: os.EOL }); // TODO: replace <any>
|
||||
|
||||
|
||||
@@ -5,14 +5,19 @@
|
||||
|
||||
import * as dataworkspace from 'dataworkspace';
|
||||
import * as vscode from 'vscode';
|
||||
import { sqlprojExtension, projectTypeDisplayName } from '../common/constants';
|
||||
import { sqlprojExtension, projectTypeDisplayName, projectTypeDescription, sqlDatabaseProjectTypeId } from '../common/constants';
|
||||
import { IconPathHelper } from '../common/iconHelper';
|
||||
import { SqlDatabaseProjectTreeViewProvider } from '../controllers/databaseProjectTreeViewProvider';
|
||||
import { ProjectsController } from '../controllers/projectController';
|
||||
import { Project } from '../models/project';
|
||||
import { BaseProjectTreeItem } from '../models/tree/baseTreeItem';
|
||||
|
||||
export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvider {
|
||||
|
||||
constructor(private projectController: ProjectsController) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the project tree data provider
|
||||
* @param projectFile The project file Uri
|
||||
@@ -39,9 +44,22 @@ export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvide
|
||||
*/
|
||||
get supportedProjectTypes(): dataworkspace.IProjectType[] {
|
||||
return [{
|
||||
id: sqlDatabaseProjectTypeId,
|
||||
projectFileExtension: sqlprojExtension.replace(/\./g, ''),
|
||||
displayName: projectTypeDisplayName,
|
||||
icon: IconPathHelper.databaseProject
|
||||
description: projectTypeDescription,
|
||||
icon: IconPathHelper.colorfulSqlProject
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a project
|
||||
* @param name name of the project
|
||||
* @param location the parent directory
|
||||
* @returns Uri of the newly created project file
|
||||
*/
|
||||
async createProject(name: string, location: vscode.Uri): Promise<vscode.Uri> {
|
||||
const projectFile = await this.projectController.createNewProject(name, location, true);
|
||||
return vscode.Uri.file(projectFile);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
|
||||
import * as should from 'should';
|
||||
import * as path from 'path';
|
||||
import * as sinon from 'sinon';
|
||||
import * as vscode from 'vscode';
|
||||
import * as TypeMoq from 'typemoq';
|
||||
import * as dataworkspace from 'dataworkspace';
|
||||
import * as baselines from '../baselines/baselines';
|
||||
import * as templates from '../../templates/templates';
|
||||
import * as testUtils from '../testUtils';
|
||||
@@ -17,6 +21,16 @@ describe('Add Database Reference Dialog', () => {
|
||||
await baselines.loadBaselines();
|
||||
});
|
||||
|
||||
beforeEach(function (): void {
|
||||
const dataWorkspaceMock = TypeMoq.Mock.ofType<dataworkspace.IExtension>();
|
||||
dataWorkspaceMock.setup(x => x.getProjectsInWorkspace()).returns(() => []);
|
||||
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: dataWorkspaceMock.object });
|
||||
});
|
||||
|
||||
afterEach(function (): void {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('Should open dialog successfully', async function (): Promise<void> {
|
||||
const project = await testUtils.createTestProject(baselines.newProjectFileBaseline);
|
||||
const dialog = new AddDatabaseReferenceDialog(project);
|
||||
|
||||
@@ -14,7 +14,6 @@ import * as TypeMoq from 'typemoq';
|
||||
|
||||
import { PublishDatabaseDialog } from '../../dialogs/publishDatabaseDialog';
|
||||
import { Project } from '../../models/project';
|
||||
import { SqlDatabaseProjectTreeViewProvider } from '../../controllers/databaseProjectTreeViewProvider';
|
||||
import { ProjectsController } from '../../controllers/projectController';
|
||||
import { IPublishSettings, IGenerateScriptSettings } from '../../models/IPublishSettings';
|
||||
|
||||
@@ -25,7 +24,7 @@ describe.skip('Publish Database Dialog', () => {
|
||||
});
|
||||
|
||||
it('Should open dialog successfully ', async function (): Promise<void> {
|
||||
const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider());
|
||||
const projController = new ProjectsController();
|
||||
const projFileDir = path.join(os.tmpdir(), `TestProject_${new Date().getTime()}`);
|
||||
|
||||
const projFilePath = await projController.createNewProject('TestProjectName', vscode.Uri.file(projFileDir), true, 'BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575');
|
||||
@@ -36,7 +35,7 @@ describe.skip('Publish Database Dialog', () => {
|
||||
});
|
||||
|
||||
it('Should create default database name correctly ', async function (): Promise<void> {
|
||||
const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider());
|
||||
const projController = new ProjectsController();
|
||||
const projFolder = `TestProject_${new Date().getTime()}`;
|
||||
const projFileDir = path.join(os.tmpdir(), projFolder);
|
||||
|
||||
|
||||
@@ -5,15 +5,11 @@
|
||||
|
||||
import * as should from 'should';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import * as vscode from 'vscode';
|
||||
import * as sinon from 'sinon';
|
||||
import * as baselines from './baselines/baselines';
|
||||
import * as templates from '../templates/templates';
|
||||
import * as constants from '../common/constants';
|
||||
import { createContext, TestContext } from './testContext';
|
||||
import MainController from '../controllers/mainController';
|
||||
import { generateTestFolderPath, createTestProject } from './testUtils';
|
||||
|
||||
let testContext: TestContext;
|
||||
|
||||
@@ -28,42 +24,6 @@ describe('MainController: main controller operations', function (): void {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('Should create new project through MainController', async function (): Promise<void> {
|
||||
const projFileDir = path.join(os.tmpdir(), `TestProject_${new Date().getTime()}`);
|
||||
|
||||
sinon.stub(vscode.window, 'showInputBox').resolves('MyProjectName');
|
||||
sinon.stub(vscode.window, 'showOpenDialog').resolves([vscode.Uri.file(projFileDir)]);
|
||||
sinon.replaceGetter(vscode.workspace, 'workspaceFolders', () => undefined);
|
||||
|
||||
const controller = new MainController(testContext.context);
|
||||
const proj = await controller.createNewProject();
|
||||
|
||||
should(proj).not.equal(undefined);
|
||||
});
|
||||
|
||||
it('Should show error when no project name', async function (): Promise<void> {
|
||||
for (const name of ['', ' ', undefined]) {
|
||||
const stub = sinon.stub(vscode.window, 'showInputBox').resolves(name);
|
||||
const spy = sinon.spy(vscode.window, 'showErrorMessage');
|
||||
const controller = new MainController(testContext.context);
|
||||
await controller.createNewProject();
|
||||
should(spy.calledOnce).be.true('showErrorMessage should have been called exactly once');
|
||||
should(spy.calledWith(constants.projectNameRequired)).be.true(`showErrorMessage not called with expected message '${constants.projectNameRequired}' Actual '${spy.getCall(0).args[0]}'`);
|
||||
stub.restore();
|
||||
spy.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it('Should show error when no location name', async function (): Promise<void> {
|
||||
sinon.stub(vscode.window, 'showInputBox').resolves('MyProjectName');
|
||||
sinon.stub(vscode.window, 'showOpenDialog').resolves(undefined);
|
||||
const spy = sinon.spy(vscode.window, 'showErrorMessage');
|
||||
const controller = new MainController(testContext.context);
|
||||
await controller.createNewProject();
|
||||
should(spy.calledOnce).be.true('showErrorMessage should be called exactly once');
|
||||
should(spy.calledWith(constants.projectLocationRequired)).be.true(`showErrorMessage not called with expected message '${constants.projectLocationRequired}' Actual '${spy.getCall(0).args[0]}'`);
|
||||
});
|
||||
|
||||
it('Should create new instance without error', async function (): Promise<void> {
|
||||
should.doesNotThrow(() => new MainController(testContext.context), 'Creating controller should not throw an error');
|
||||
});
|
||||
@@ -75,27 +35,4 @@ describe('MainController: main controller operations', function (): void {
|
||||
should.doesNotThrow(() => controller.activate(), 'activate() should not throw an error');
|
||||
should.doesNotThrow(() => controller.dispose(), 'dispose() should not throw an error');
|
||||
});
|
||||
|
||||
it('Should load projects in workspace', async function (): Promise<void> {
|
||||
const rootFolderPath = await generateTestFolderPath();
|
||||
const project = await createTestProject(baselines.openProjectFileBaseline, rootFolderPath);
|
||||
const nestedFolder = path.join(rootFolderPath, 'nestedProject');
|
||||
const nestedProject = await createTestProject(baselines.openProjectFileBaseline, nestedFolder);
|
||||
|
||||
const workspaceFolder: vscode.WorkspaceFolder = {
|
||||
uri: vscode.Uri.file(rootFolderPath),
|
||||
name: '',
|
||||
index: 0
|
||||
};
|
||||
sinon.replaceGetter(vscode.workspace, 'workspaceFolders', () => [workspaceFolder]);
|
||||
|
||||
const controller = new MainController(testContext.context);
|
||||
should(controller.projController.projects.length).equal(0);
|
||||
|
||||
await controller.loadProjectsInWorkspace();
|
||||
|
||||
should(controller.projController.projects.length).equal(2);
|
||||
should(controller.projController.projects[0].projectFolderPath).equal(project.projectFolderPath);
|
||||
should(controller.projController.projects[1].projectFolderPath).equal(nestedProject.projectFolderPath);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,31 +5,41 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as should from 'should';
|
||||
import * as TypeMoq from 'typemoq';
|
||||
import * as sinon from 'sinon';
|
||||
import * as dataworkspace from 'dataworkspace';
|
||||
import * as newProjectTool from '../tools/newProjectTool';
|
||||
import * as constants from '../common/constants';
|
||||
import { generateTestFolderPath, createTestFile } from './testUtils';
|
||||
|
||||
let previousSetting : string;
|
||||
let testFolderPath : string;
|
||||
|
||||
describe('NewProjectTool: New project tool tests', function (): void {
|
||||
const projectConfigurationKey = 'projects';
|
||||
const projectSaveLocationKey= 'defaultProjectSaveLocation';
|
||||
|
||||
beforeEach(async function () {
|
||||
previousSetting = await vscode.workspace.getConfiguration(constants.dbProjectConfigurationKey)[constants.projectSaveLocationKey];
|
||||
previousSetting = await vscode.workspace.getConfiguration(projectConfigurationKey)[projectSaveLocationKey];
|
||||
testFolderPath = await generateTestFolderPath();
|
||||
|
||||
const dataWorkspaceMock = TypeMoq.Mock.ofType<dataworkspace.IExtension>();
|
||||
dataWorkspaceMock.setup(x => x.defaultProjectSaveLocation).returns(() => vscode.Uri.file(testFolderPath));
|
||||
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: dataWorkspaceMock.object});
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
await vscode.workspace.getConfiguration(constants.dbProjectConfigurationKey).update(constants.projectSaveLocationKey, previousSetting, true);
|
||||
await vscode.workspace.getConfiguration(projectConfigurationKey).update(projectSaveLocationKey, previousSetting, true);
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('Should generate correct default project names', async function (): Promise<void> {
|
||||
await vscode.workspace.getConfiguration(constants.dbProjectConfigurationKey).update(constants.projectSaveLocationKey, testFolderPath, true);
|
||||
await vscode.workspace.getConfiguration(projectConfigurationKey).update(projectSaveLocationKey, testFolderPath, true);
|
||||
should(newProjectTool.defaultProjectNameNewProj()).equal('DatabaseProject1');
|
||||
should(newProjectTool.defaultProjectNameFromDb('master')).equal('DatabaseProjectmaster');
|
||||
});
|
||||
|
||||
it('Should auto-increment default project names for new projects', async function (): Promise<void> {
|
||||
await vscode.workspace.getConfiguration(constants.dbProjectConfigurationKey).update(constants.projectSaveLocationKey, testFolderPath, true);
|
||||
await vscode.workspace.getConfiguration(projectConfigurationKey).update(projectSaveLocationKey, testFolderPath, true);
|
||||
should(newProjectTool.defaultProjectNameNewProj()).equal('DatabaseProject1');
|
||||
|
||||
await createTestFile('', 'DatabaseProject1', testFolderPath);
|
||||
@@ -40,7 +50,7 @@ describe('NewProjectTool: New project tool tests', function (): void {
|
||||
});
|
||||
|
||||
it('Should auto-increment default project names for import projects', async function (): Promise<void> {
|
||||
await vscode.workspace.getConfiguration(constants.dbProjectConfigurationKey).update(constants.projectSaveLocationKey, testFolderPath, true);
|
||||
await vscode.workspace.getConfiguration(projectConfigurationKey).update(projectSaveLocationKey, testFolderPath, true);
|
||||
should(newProjectTool.defaultProjectNameFromDb("master")).equal('DatabaseProjectmaster');
|
||||
|
||||
await createTestFile('', 'DatabaseProjectmaster', testFolderPath);
|
||||
|
||||
@@ -91,6 +91,8 @@ describe('Project: sqlproj content operations', function (): void {
|
||||
should(project.postDeployScripts.find(f => f.type === EntryType.File && f.relativePath === 'Script.PostDeployment1.sql')).not.equal(undefined, 'File Script.PostDeployment1.sql not read');
|
||||
should(project.preDeployScripts.find(f => f.type === EntryType.File && f.relativePath === 'Script.PreDeployment2.sql')).not.equal(undefined, 'File Script.PostDeployment2.sql not read');
|
||||
should(project.noneDeployScripts.find(f => f.type === EntryType.File && f.relativePath === 'Tables\\Script.PostDeployment1.sql')).not.equal(undefined, 'File Tables\\Script.PostDeployment1.sql not read');
|
||||
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('Should add Folder and Build entries to sqlproj', async function (): Promise<void> {
|
||||
@@ -580,43 +582,58 @@ describe('Project: round trip updates', function (): void {
|
||||
});
|
||||
|
||||
it('Should update SSDT project to work in ADS', async function (): Promise<void> {
|
||||
await testUpdateInRoundTrip(baselines.SSDTProjectFileBaseline, baselines.SSDTProjectAfterUpdateBaseline, true, true);
|
||||
await testUpdateInRoundTrip(baselines.SSDTProjectFileBaseline, baselines.SSDTProjectAfterUpdateBaseline);
|
||||
});
|
||||
|
||||
it('Should update SSDT project with new system database references', async function (): Promise<void> {
|
||||
await testUpdateInRoundTrip(baselines.SSDTUpdatedProjectBaseline, baselines.SSDTUpdatedProjectAfterSystemDbUpdateBaseline, false, true);
|
||||
await testUpdateInRoundTrip(baselines.SSDTUpdatedProjectBaseline, baselines.SSDTUpdatedProjectAfterSystemDbUpdateBaseline);
|
||||
});
|
||||
|
||||
it('Should update SSDT project to work in ADS handling pre-exsiting targets', async function (): Promise<void> {
|
||||
await testUpdateInRoundTrip(baselines.SSDTProjectBaselineWithCleanTarget, baselines.SSDTProjectBaselineWithCleanTargetAfterUpdate, true, false);
|
||||
await testUpdateInRoundTrip(baselines.SSDTProjectBaselineWithCleanTarget, baselines.SSDTProjectBaselineWithCleanTargetAfterUpdate);
|
||||
});
|
||||
|
||||
it('Should not update project and no backup file should be created when update to project is rejected', async function (): Promise<void> {
|
||||
sinon.stub(window, 'showWarningMessage').returns(<any>Promise.resolve(constants.noString));
|
||||
// setup test files
|
||||
const folderPath = await testUtils.generateTestFolderPath();
|
||||
const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.SSDTProjectFileBaseline, folderPath);
|
||||
await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, folderPath);
|
||||
|
||||
const project = await Project.openProject(Uri.file(sqlProjPath).fsPath);
|
||||
|
||||
should(await exists(sqlProjPath + '_backup')).equal(false); // backup file should not be generated
|
||||
should(project.importedTargets.length).equal(2); // additional target should not be added by updateProjectForRoundTrip method
|
||||
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('Should not show warning message for non-SSDT projects that have the additional information for Build', async function (): Promise<void> {
|
||||
// setup test files
|
||||
const folderPath = await testUtils.generateTestFolderPath();
|
||||
const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline, folderPath);
|
||||
await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, folderPath);
|
||||
|
||||
const project = await Project.openProject(Uri.file(sqlProjPath).fsPath); // no error thrown
|
||||
|
||||
should(project.importedTargets.length).equal(3); // additional target should exist by default
|
||||
});
|
||||
});
|
||||
|
||||
async function testUpdateInRoundTrip(fileBeforeupdate: string, fileAfterUpdate: string, testTargets: boolean, testReferences: boolean): Promise<void> {
|
||||
async function testUpdateInRoundTrip(fileBeforeupdate: string, fileAfterUpdate: string): Promise<void> {
|
||||
const stub = sinon.stub(window, 'showWarningMessage').returns(<any>Promise.resolve(constants.yesString));
|
||||
|
||||
projFilePath = await testUtils.createTestSqlProjFile(fileBeforeupdate);
|
||||
const project = await Project.openProject(projFilePath);
|
||||
const project = await Project.openProject(projFilePath); // project gets updated if needed in openProject()
|
||||
|
||||
if (testTargets) {
|
||||
await testUpdateTargetsImportsRoundTrip(project);
|
||||
}
|
||||
|
||||
if (testReferences) {
|
||||
await testAddReferencesInRoundTrip(project);
|
||||
}
|
||||
should(await exists(projFilePath + '_backup')).equal(true, 'Backup file should have been generated before the project was updated');
|
||||
should(project.importedTargets.length).equal(3); // additional target added by updateProjectForRoundTrip method
|
||||
|
||||
let projFileText = (await fs.readFile(projFilePath)).toString();
|
||||
should(projFileText).equal(fileAfterUpdate.trim());
|
||||
|
||||
should(stub.calledOnce).be.true('showWarningMessage should have been called exactly once');
|
||||
sinon.restore();
|
||||
}
|
||||
|
||||
async function testUpdateTargetsImportsRoundTrip(project: Project): Promise<void> {
|
||||
should(project.importedTargets.length).equal(2);
|
||||
await project.updateProjectForRoundTrip();
|
||||
should(await exists(projFilePath + '_backup')).equal(true); // backup file should be generated before the project is updated
|
||||
should(project.importedTargets.length).equal(3); // additional target added by updateProjectForRoundTrip method
|
||||
}
|
||||
|
||||
async function testAddReferencesInRoundTrip(project: Project): Promise<void> {
|
||||
// updating system db refs is separate from updating for roundtrip because new db refs could be added even after project is updated for roundtrip
|
||||
should(project.containsSSDTOnlySystemDatabaseReferences()).equal(true);
|
||||
await project.updateSystemDatabaseReferencesInProjFile();
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import * as TypeMoq from 'typemoq';
|
||||
import * as sinon from 'sinon';
|
||||
import * as dataworkspace from 'dataworkspace';
|
||||
import * as baselines from './baselines/baselines';
|
||||
import * as templates from '../templates/templates';
|
||||
import * as testUtils from './testUtils';
|
||||
@@ -65,7 +66,7 @@ describe('ProjectsController', function (): void {
|
||||
describe('project controller operations', function (): void {
|
||||
describe('Project file operations and prompting', function (): void {
|
||||
it('Should create new sqlproj file with correct values', async function (): Promise<void> {
|
||||
const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider());
|
||||
const projController = new ProjectsController();
|
||||
const projFileDir = path.join(os.tmpdir(), `TestProject_${new Date().getTime()}`);
|
||||
|
||||
const projFilePath = await projController.createNewProject('TestProjectName', vscode.Uri.file(projFileDir), false, 'BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575');
|
||||
@@ -75,54 +76,11 @@ describe('ProjectsController', function (): void {
|
||||
should(projFileText).equal(baselines.newProjectFileBaseline);
|
||||
});
|
||||
|
||||
it('Should load Project and associated DataSources', async function (): Promise<void> {
|
||||
// setup test files
|
||||
const folderPath = await testUtils.generateTestFolderPath();
|
||||
const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline, folderPath);
|
||||
await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, folderPath);
|
||||
|
||||
const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider());
|
||||
|
||||
const project = await projController.openProject(vscode.Uri.file(sqlProjPath));
|
||||
|
||||
should(project.files.length).equal(10); // detailed sqlproj tests in their own test file
|
||||
should(project.dataSources.length).equal(3); // detailed datasources tests in their own test file
|
||||
});
|
||||
|
||||
it('Should load both project and referenced project', async function (): Promise<void> {
|
||||
// setup test projects
|
||||
const folderPath = await testUtils.generateTestFolderPath();
|
||||
await fs.mkdir(path.join(folderPath, 'proj1'));
|
||||
await fs.mkdir(path.join(folderPath, 'ReferencedProject'));
|
||||
|
||||
const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.openProjectWithProjectReferencesBaseline, path.join(folderPath, 'proj1'));
|
||||
await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline, path.join(folderPath, 'ReferencedProject'));
|
||||
|
||||
const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider());
|
||||
await projController.openProject(vscode.Uri.file(sqlProjPath));
|
||||
|
||||
should(projController.projects.length).equal(2, 'Referenced project should have been opened when the project referencing it was opened');
|
||||
});
|
||||
|
||||
it('Should not keep failed to load project in project list.', async function (): Promise<void> {
|
||||
const folderPath = await testUtils.generateTestFolderPath();
|
||||
const sqlProjPath = await testUtils.createTestSqlProjFile('empty file with no valid xml', folderPath);
|
||||
const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider());
|
||||
|
||||
try {
|
||||
await projController.openProject(vscode.Uri.file(sqlProjPath));
|
||||
should.fail(null, null, 'The given project not expected to open');
|
||||
}
|
||||
catch {
|
||||
should(projController.projects.length).equal(0, 'The added project should be removed');
|
||||
}
|
||||
});
|
||||
|
||||
it('Should return silently when no SQL object name provided in prompts', async function (): Promise<void> {
|
||||
for (const name of ['', ' ', undefined]) {
|
||||
const showInputBoxStub = sinon.stub(vscode.window, 'showInputBox').resolves(name);
|
||||
const showErrorMessageSpy = sinon.spy(vscode.window, 'showErrorMessage');
|
||||
const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider());
|
||||
const projController = new ProjectsController();
|
||||
const project = new Project('FakePath');
|
||||
|
||||
should(project.files.length).equal(0);
|
||||
@@ -138,7 +96,7 @@ describe('ProjectsController', function (): void {
|
||||
const tableName = 'table1';
|
||||
sinon.stub(vscode.window, 'showInputBox').resolves(tableName);
|
||||
const spy = sinon.spy(vscode.window, 'showErrorMessage');
|
||||
const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider());
|
||||
const projController = new ProjectsController();
|
||||
const project = await testUtils.createTestProject(baselines.newProjectFileBaseline);
|
||||
|
||||
should(project.files.length).equal(0, 'There should be no files');
|
||||
@@ -154,14 +112,13 @@ describe('ProjectsController', function (): void {
|
||||
const folderName = 'folder1';
|
||||
const stub = sinon.stub(vscode.window, 'showInputBox').resolves(folderName);
|
||||
|
||||
const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider());
|
||||
const projController = new ProjectsController();
|
||||
const project = await testUtils.createTestProject(baselines.newProjectFileBaseline);
|
||||
const projectRoot = new ProjectRootTreeItem(project);
|
||||
|
||||
should(project.files.length).equal(0, 'There should be no other folders');
|
||||
await projController.addFolderPrompt(projectRoot);
|
||||
await projController.addFolderPrompt(createWorkspaceTreeItem(projectRoot));
|
||||
should(project.files.length).equal(1, 'Folder should be successfully added');
|
||||
projController.refreshProjectsTree();
|
||||
stub.restore();
|
||||
await verifyFolderNotAdded(folderName, projController, project, projectRoot);
|
||||
|
||||
@@ -175,7 +132,7 @@ describe('ProjectsController', function (): void {
|
||||
const folderName = 'folder1';
|
||||
const stub = sinon.stub(vscode.window, 'showInputBox').resolves(folderName);
|
||||
|
||||
const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider());
|
||||
const projController = new ProjectsController();
|
||||
const project = await testUtils.createTestProject(baselines.openProjectFileBaseline);
|
||||
const projectRoot = new ProjectRootTreeItem(project);
|
||||
|
||||
@@ -190,7 +147,7 @@ describe('ProjectsController', function (): void {
|
||||
async function verifyFolderAdded(folderName: string, projController: ProjectsController, project: Project, node: BaseProjectTreeItem): Promise<void> {
|
||||
const beforeFileCount = project.files.length;
|
||||
const stub = sinon.stub(vscode.window, 'showInputBox').resolves(folderName);
|
||||
await projController.addFolderPrompt(node);
|
||||
await projController.addFolderPrompt(createWorkspaceTreeItem(node));
|
||||
should(project.files.length).equal(beforeFileCount + 1, `File count should be increased by one after adding the folder ${folderName}`);
|
||||
stub.restore();
|
||||
}
|
||||
@@ -199,7 +156,7 @@ describe('ProjectsController', function (): void {
|
||||
const beforeFileCount = project.files.length;
|
||||
const showInputBoxStub = sinon.stub(vscode.window, 'showInputBox').resolves(folderName);
|
||||
const showErrorMessageSpy = sinon.spy(vscode.window, 'showErrorMessage');
|
||||
await projController.addFolderPrompt(node);
|
||||
await projController.addFolderPrompt(createWorkspaceTreeItem(node));
|
||||
should(showErrorMessageSpy.calledOnce).be.true('showErrorMessage should have been called exactly once');
|
||||
const msg = constants.folderAlreadyExists(folderName);
|
||||
should(showErrorMessageSpy.calledWith(msg)).be.true(`showErrorMessage not called with expected message '${msg}' Actual '${showErrorMessageSpy.getCall(0).args[0]}'`);
|
||||
@@ -213,13 +170,13 @@ describe('ProjectsController', function (): void {
|
||||
const setupResult = await setupDeleteExcludeTest(proj);
|
||||
const scriptEntry = setupResult[0], projTreeRoot = setupResult[1], preDeployEntry = setupResult[2], postDeployEntry = setupResult[3], noneEntry = setupResult[4];
|
||||
|
||||
const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider());
|
||||
const projController = new ProjectsController();
|
||||
|
||||
await projController.delete(projTreeRoot.children.find(x => x.friendlyName === 'UpperFolder')!.children[0] /* LowerFolder */);
|
||||
await projController.delete(projTreeRoot.children.find(x => x.friendlyName === 'anotherScript.sql')!);
|
||||
await projController.delete(projTreeRoot.children.find(x => x.friendlyName === 'Script.PreDeployment1.sql')!);
|
||||
await projController.delete(projTreeRoot.children.find(x => x.friendlyName === 'Script.PreDeployment2.sql')!);
|
||||
await projController.delete(projTreeRoot.children.find(x => x.friendlyName === 'Script.PostDeployment1.sql')!);
|
||||
await projController.delete(createWorkspaceTreeItem(projTreeRoot.children.find(x => x.friendlyName === 'UpperFolder')!.children[0]) /* LowerFolder */);
|
||||
await projController.delete(createWorkspaceTreeItem(projTreeRoot.children.find(x => x.friendlyName === 'anotherScript.sql')!));
|
||||
await projController.delete(createWorkspaceTreeItem(projTreeRoot.children.find(x => x.friendlyName === 'Script.PreDeployment1.sql')!));
|
||||
await projController.delete(createWorkspaceTreeItem(projTreeRoot.children.find(x => x.friendlyName === 'Script.PreDeployment2.sql')!));
|
||||
await projController.delete(createWorkspaceTreeItem(projTreeRoot.children.find(x => x.friendlyName === 'Script.PostDeployment1.sql')!));
|
||||
|
||||
proj = await Project.openProject(proj.projectFilePath); // reload edited sqlproj from disk
|
||||
|
||||
@@ -239,7 +196,7 @@ describe('ProjectsController', function (): void {
|
||||
it('Should delete database references', async function (): Promise<void> {
|
||||
// setup - openProject baseline has a system db reference to master
|
||||
const proj = await testUtils.createTestProject(baselines.openProjectFileBaseline);
|
||||
const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider());
|
||||
const projController = new ProjectsController();
|
||||
sinon.stub(vscode.window, 'showWarningMessage').returns(<any>Promise.resolve(constants.yesString));
|
||||
|
||||
// add dacpac reference
|
||||
@@ -261,9 +218,9 @@ describe('ProjectsController', function (): void {
|
||||
should(proj.databaseReferences.length).equal(3, 'Should start with 3 database references');
|
||||
|
||||
const databaseReferenceNodeChildren = projTreeRoot.children.find(x => x.friendlyName === constants.databaseReferencesNodeName)?.children;
|
||||
await projController.delete(databaseReferenceNodeChildren?.find(x => x.friendlyName === 'master')!); // system db reference
|
||||
await projController.delete(databaseReferenceNodeChildren?.find(x => x.friendlyName === 'test2')!); // dacpac reference
|
||||
await projController.delete(databaseReferenceNodeChildren?.find(x => x.friendlyName === 'project1')!); // project reference
|
||||
await projController.delete(createWorkspaceTreeItem(databaseReferenceNodeChildren?.find(x => x.friendlyName === 'master')!)); // system db reference
|
||||
await projController.delete(createWorkspaceTreeItem(databaseReferenceNodeChildren?.find(x => x.friendlyName === 'test2')!)); // dacpac reference
|
||||
await projController.delete(createWorkspaceTreeItem(databaseReferenceNodeChildren?.find(x => x.friendlyName === 'project1')!)); // project reference
|
||||
|
||||
// confirm result
|
||||
should(proj.databaseReferences.length).equal(0, 'All database references should have been deleted');
|
||||
@@ -274,13 +231,13 @@ describe('ProjectsController', function (): void {
|
||||
const setupResult = await setupDeleteExcludeTest(proj);
|
||||
const scriptEntry = setupResult[0], projTreeRoot = setupResult[1], preDeployEntry = setupResult[2], postDeployEntry = setupResult[3], noneEntry = setupResult[4];
|
||||
|
||||
const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider());
|
||||
const projController = new ProjectsController();
|
||||
|
||||
await projController.exclude(<FolderNode>projTreeRoot.children.find(x => x.friendlyName === 'UpperFolder')!.children[0] /* LowerFolder */);
|
||||
await projController.exclude(<FileNode>projTreeRoot.children.find(x => x.friendlyName === 'anotherScript.sql')!);
|
||||
await projController.exclude(<FileNode>projTreeRoot.children.find(x => x.friendlyName === 'Script.PreDeployment1.sql')!);
|
||||
await projController.exclude(<FileNode>projTreeRoot.children.find(x => x.friendlyName === 'Script.PreDeployment2.sql')!);
|
||||
await projController.exclude(<FileNode>projTreeRoot.children.find(x => x.friendlyName === 'Script.PostDeployment1.sql')!);
|
||||
await projController.exclude(createWorkspaceTreeItem(<FolderNode>projTreeRoot.children.find(x => x.friendlyName === 'UpperFolder')!.children[0]) /* LowerFolder */);
|
||||
await projController.exclude(createWorkspaceTreeItem(<FileNode>projTreeRoot.children.find(x => x.friendlyName === 'anotherScript.sql')!));
|
||||
await projController.exclude(createWorkspaceTreeItem(<FileNode>projTreeRoot.children.find(x => x.friendlyName === 'Script.PreDeployment1.sql')!));
|
||||
await projController.exclude(createWorkspaceTreeItem(<FileNode>projTreeRoot.children.find(x => x.friendlyName === 'Script.PreDeployment2.sql')!));
|
||||
await projController.exclude(createWorkspaceTreeItem(<FileNode>projTreeRoot.children.find(x => x.friendlyName === 'Script.PostDeployment1.sql')!));
|
||||
|
||||
proj = await Project.openProject(proj.projectFilePath); // reload edited sqlproj from disk
|
||||
|
||||
@@ -302,18 +259,22 @@ describe('ProjectsController', function (): void {
|
||||
const folderPath = await testUtils.generateTestFolderPath();
|
||||
const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline, folderPath);
|
||||
const treeProvider = new SqlDatabaseProjectTreeViewProvider();
|
||||
const projController = new ProjectsController(treeProvider);
|
||||
const project = await projController.openProject(vscode.Uri.file(sqlProjPath));
|
||||
const projController = new ProjectsController();
|
||||
const project = await Project.openProject(vscode.Uri.file(sqlProjPath).fsPath);
|
||||
treeProvider.load([project]);
|
||||
|
||||
// change the sql project file
|
||||
await fs.writeFile(sqlProjPath, baselines.newProjectFileWithScriptBaseline);
|
||||
should(project.files.length).equal(0);
|
||||
|
||||
// call reload project
|
||||
await projController.reloadProject(vscode.Uri.file(project.projectFilePath));
|
||||
should(project.files.length).equal(1);
|
||||
await projController.reloadProject({ treeDataProvider: treeProvider, element: { root: { project: project } } });
|
||||
// calling this because this gets called in the projectProvider.getProjectTreeDataProvider(), which is called by workspaceTreeDataProvider
|
||||
// when notifyTreeDataChanged() happens
|
||||
treeProvider.load([project]);
|
||||
|
||||
// check that the new project is in the tree
|
||||
should(project.files.length).equal(1);
|
||||
should(treeProvider.getChildren()[0].children.find(c => c.friendlyName === 'Script1.sql')).not.equal(undefined);
|
||||
});
|
||||
|
||||
@@ -321,7 +282,7 @@ describe('ProjectsController', function (): void {
|
||||
const preDeployScriptName = 'PreDeployScript1.sql';
|
||||
const postDeployScriptName = 'PostDeployScript1.sql';
|
||||
|
||||
const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider());
|
||||
const projController = new ProjectsController();
|
||||
const project = await testUtils.createTestProject(baselines.newProjectFileBaseline);
|
||||
|
||||
sinon.stub(vscode.window, 'showInputBox').resolves(preDeployScriptName);
|
||||
@@ -339,9 +300,9 @@ describe('ProjectsController', function (): void {
|
||||
it('Should change target platform', async function (): Promise<void> {
|
||||
sinon.stub(vscode.window, 'showQuickPick').resolves({ label: constants.sqlAzure });
|
||||
|
||||
const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider());
|
||||
const projController = new ProjectsController();
|
||||
const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline);
|
||||
const project = await projController.openProject(vscode.Uri.file(sqlProjPath));
|
||||
const project = await Project.openProject(sqlProjPath);
|
||||
should(project.getProjectTargetVersion()).equal(constants.targetPlatformToVersion.get(constants.sqlServer2019));
|
||||
should(project.databaseReferences.length).equal(1, 'Project should have one database reference to master');
|
||||
should(project.databaseReferences[0].fsUri.fsPath).containEql(constants.targetPlatformToVersion.get(constants.sqlServer2019));
|
||||
@@ -387,12 +348,12 @@ describe('ProjectsController', function (): void {
|
||||
let projController = TypeMoq.Mock.ofType(ProjectsController);
|
||||
projController.callBase = true;
|
||||
projController.setup(x => x.getPublishDialog(TypeMoq.It.isAny())).returns(() => publishDialog.object);
|
||||
projController.setup(x => x.executionCallback(TypeMoq.It.isAny(), TypeMoq.It.is((_): _ is IPublishSettings => true))).returns(() => {
|
||||
projController.setup(x => x.publishProjectCallback(TypeMoq.It.isAny(), TypeMoq.It.is((_): _ is IPublishSettings => true))).returns(() => {
|
||||
holler = publishHoller;
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
|
||||
projController.setup(x => x.executionCallback(TypeMoq.It.isAny(), TypeMoq.It.is((_): _ is IGenerateScriptSettings => true))).returns(() => {
|
||||
projController.setup(x => x.publishProjectCallback(TypeMoq.It.isAny(), TypeMoq.It.is((_): _ is IGenerateScriptSettings => true))).returns(() => {
|
||||
holler = generateHoller;
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
@@ -430,7 +391,7 @@ describe('ProjectsController', function (): void {
|
||||
|
||||
projController.setup(x => x.getDaxFxService()).returns(() => Promise.resolve(testContext.dacFxService.object));
|
||||
|
||||
await projController.object.executionCallback(new Project(''), { connectionUri: '', databaseName: '' });
|
||||
await projController.object.publishProjectCallback(new Project(''), { connectionUri: '', databaseName: '' });
|
||||
|
||||
should(builtDacpacPath).not.equal('', 'built dacpac path should be set');
|
||||
should(publishedDacpacPath).not.equal('', 'published dacpac path should be set');
|
||||
@@ -440,11 +401,15 @@ describe('ProjectsController', function (): void {
|
||||
});
|
||||
});
|
||||
|
||||
describe('import operations', function (): void {
|
||||
describe('Create project from database', function (): void {
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('Should create list of all files and folders correctly', async function (): Promise<void> {
|
||||
const testFolderPath = await testUtils.createDummyFileStructure();
|
||||
|
||||
const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider());
|
||||
const projController = new ProjectsController();
|
||||
const fileList = await projController.generateList(testFolderPath);
|
||||
|
||||
should(fileList.length).equal(15); // Parent folder + 2 files under parent folder + 2 directories with 5 files each
|
||||
@@ -456,7 +421,7 @@ describe('ProjectsController', function (): void {
|
||||
let testFolderPath = await testUtils.generateTestFolderPath();
|
||||
testFolderPath += '_nonexistentFolder'; // Modify folder path to point to a nonexistent location
|
||||
|
||||
const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider());
|
||||
const projController = new ProjectsController();
|
||||
|
||||
await projController.generateList(testFolderPath);
|
||||
should(spy.calledOnce).be.true('showErrorMessage should have been called');
|
||||
@@ -466,11 +431,16 @@ describe('ProjectsController', function (): void {
|
||||
|
||||
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');
|
||||
|
||||
const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider());
|
||||
await projController.importNewDatabaseProject({ connectionProfile: mockConnectionProfile });
|
||||
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();
|
||||
@@ -478,37 +448,52 @@ describe('ProjectsController', function (): void {
|
||||
});
|
||||
|
||||
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(new SqlDatabaseProjectTreeViewProvider());
|
||||
await projController.importNewDatabaseProject({ connectionProfile: mockConnectionProfile });
|
||||
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(new SqlDatabaseProjectTreeViewProvider());
|
||||
await projController.importNewDatabaseProject({ connectionProfile: mockConnectionProfile });
|
||||
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(new SqlDatabaseProjectTreeViewProvider());
|
||||
await projController.importNewDatabaseProject({ connectionProfile: mockConnectionProfile });
|
||||
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]}'`);
|
||||
});
|
||||
@@ -517,6 +502,11 @@ describe('ProjectsController', function (): 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)]));
|
||||
@@ -526,9 +516,9 @@ describe('ProjectsController', function (): void {
|
||||
let projController = TypeMoq.Mock.ofType(ProjectsController, undefined, undefined, new SqlDatabaseProjectTreeViewProvider());
|
||||
projController.callBase = true;
|
||||
|
||||
projController.setup(x => x.importApiCall(TypeMoq.It.isAny())).returns(async (model) => { importPath = model.filePath; });
|
||||
projController.setup(x => x.createProjectFromDatabaseApiCall(TypeMoq.It.isAny())).returns(async (model) => { importPath = model.filePath; });
|
||||
|
||||
await projController.object.importNewDatabaseProject({ connectionProfile: mockConnectionProfile });
|
||||
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
|
||||
@@ -536,7 +526,7 @@ describe('ProjectsController', function (): void {
|
||||
folderPath = await testUtils.generateTestFolderPath();
|
||||
showQuickPickStub.resolves({ label: constants.schemaObjectType });
|
||||
|
||||
await projController.object.importNewDatabaseProject({ connectionProfile: mockConnectionProfile });
|
||||
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}`);
|
||||
});
|
||||
|
||||
@@ -552,7 +542,7 @@ describe('ProjectsController', function (): void {
|
||||
options: {}
|
||||
});
|
||||
|
||||
let projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider());
|
||||
let projController = new ProjectsController();
|
||||
|
||||
let result = await projController.getModelFromContext(undefined);
|
||||
|
||||
@@ -590,14 +580,16 @@ describe('ProjectsController', function (): void {
|
||||
const addDbReferenceDialog = TypeMoq.Mock.ofType(AddDatabaseReferenceDialog, undefined, undefined, proj);
|
||||
addDbReferenceDialog.callBase = true;
|
||||
addDbReferenceDialog.setup(x => x.addReferenceClick()).returns(() => {
|
||||
projController.object.addDatabaseReferenceCallback(proj, { systemDb: SystemDatabase.master, databaseName: 'master', suppressMissingDependenciesErrors: false });
|
||||
projController.object.addDatabaseReferenceCallback(proj,
|
||||
{ systemDb: SystemDatabase.master, databaseName: 'master', suppressMissingDependenciesErrors: false },
|
||||
{ treeDataProvider: new SqlDatabaseProjectTreeViewProvider(), element: undefined });
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
|
||||
const projController = TypeMoq.Mock.ofType(ProjectsController);
|
||||
projController.callBase = true;
|
||||
projController.setup(x => x.getAddDatabaseReferenceDialog(TypeMoq.It.isAny())).returns(() => addDbReferenceDialog.object);
|
||||
projController.setup(x => x.addDatabaseReferenceCallback(TypeMoq.It.isAny(), TypeMoq.It.is((_): _ is IDacpacReferenceSettings => true))).returns(() => {
|
||||
projController.setup(x => x.addDatabaseReferenceCallback(TypeMoq.It.isAny(), TypeMoq.It.is((_): _ is IDacpacReferenceSettings => true), TypeMoq.It.isAny())).returns(() => {
|
||||
holler = addDbRefHoller;
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
@@ -609,14 +601,16 @@ describe('ProjectsController', function (): void {
|
||||
});
|
||||
|
||||
it('Should not allow adding circular project references', async function (): Promise<void> {
|
||||
const showErrorMessageSpy = sinon.spy(vscode.window, 'showErrorMessage');
|
||||
|
||||
const projPath1 = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline);
|
||||
const projPath2 = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline);
|
||||
const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider());
|
||||
const projController = new ProjectsController();
|
||||
|
||||
const project1 = await projController.openProject(vscode.Uri.file(projPath1));
|
||||
const project2 = await projController.openProject(vscode.Uri.file(projPath2));
|
||||
const project1 = await Project.openProject(vscode.Uri.file(projPath1).fsPath);
|
||||
const project2 = await Project.openProject(vscode.Uri.file(projPath2).fsPath);
|
||||
const showErrorMessageSpy = sinon.spy(vscode.window, 'showErrorMessage');
|
||||
const dataWorkspaceMock = TypeMoq.Mock.ofType<dataworkspace.IExtension>();
|
||||
dataWorkspaceMock.setup(x => x.getProjectsInWorkspace()).returns(() => [vscode.Uri.file(project1.projectFilePath), vscode.Uri.file(project2.projectFilePath)]);
|
||||
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: dataWorkspaceMock.object });
|
||||
|
||||
// add project reference from project1 to project2
|
||||
await projController.addDatabaseReferenceCallback(project1, {
|
||||
@@ -624,7 +618,8 @@ describe('ProjectsController', function (): void {
|
||||
projectName: 'TestProject',
|
||||
projectRelativePath: undefined,
|
||||
suppressMissingDependenciesErrors: false
|
||||
});
|
||||
},
|
||||
{ treeDataProvider: new SqlDatabaseProjectTreeViewProvider(), element: undefined });
|
||||
should(showErrorMessageSpy.notCalled).be.true('showErrorMessage should not have been called');
|
||||
|
||||
// try to add circular reference
|
||||
@@ -633,75 +628,14 @@ describe('ProjectsController', function (): void {
|
||||
projectName: 'TestProjectName',
|
||||
projectRelativePath: undefined,
|
||||
suppressMissingDependenciesErrors: false
|
||||
});
|
||||
},
|
||||
{ treeDataProvider: new SqlDatabaseProjectTreeViewProvider(), element: undefined });
|
||||
should(showErrorMessageSpy.called).be.true('showErrorMessage should have been called');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip('ProjectsController: round trip feature with SSDT', function (): void {
|
||||
it('Should show warning message for SSDT project opened in Azure Data Studio', async function (): Promise<void> {
|
||||
const stub = sinon.stub(vscode.window, 'showWarningMessage').returns(<any>Promise.resolve(constants.noString));
|
||||
|
||||
// setup test files
|
||||
const folderPath = await testUtils.generateTestFolderPath();
|
||||
const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.SSDTProjectFileBaseline, folderPath);
|
||||
await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, folderPath);
|
||||
|
||||
const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider());
|
||||
|
||||
await projController.openProject(vscode.Uri.file(sqlProjPath));
|
||||
should(stub.calledOnce).be.true('showWarningMessage should have been called exactly once');
|
||||
should(stub.calledWith(constants.updateProjectForRoundTrip)).be.true(`showWarningMessage not called with expected message '${constants.updateProjectForRoundTrip}' Actual '${stub.getCall(0).args[0]}'`);
|
||||
});
|
||||
|
||||
it('Should not show warning message for non-SSDT projects that have the additional information for Build', async function (): Promise<void> {
|
||||
// setup test files
|
||||
const folderPath = await testUtils.generateTestFolderPath();
|
||||
const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline, folderPath);
|
||||
await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, folderPath);
|
||||
|
||||
const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider());
|
||||
|
||||
const project = await projController.openProject(vscode.Uri.file(sqlProjPath)); // no error thrown
|
||||
|
||||
should(project.importedTargets.length).equal(3); // additional target should exist by default
|
||||
});
|
||||
|
||||
it('Should not update project and no backup file should be created when update to project is rejected', async function (): Promise<void> {
|
||||
sinon.stub(vscode.window, 'showWarningMessage').returns(<any>Promise.resolve(constants.noString));
|
||||
// setup test files
|
||||
const folderPath = await testUtils.generateTestFolderPath();
|
||||
const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.SSDTProjectFileBaseline, folderPath);
|
||||
await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, folderPath);
|
||||
|
||||
const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider());
|
||||
|
||||
const project = await projController.openProject(vscode.Uri.file(sqlProjPath));
|
||||
|
||||
should(await exists(sqlProjPath + '_backup')).equal(false); // backup file should not be generated
|
||||
should(project.importedTargets.length).equal(2); // additional target should not be added by updateProjectForRoundTrip method
|
||||
});
|
||||
|
||||
it('Should load Project and associated import targets when update to project is accepted', async function (): Promise<void> {
|
||||
sinon.stub(vscode.window, 'showWarningMessage').returns(<any>Promise.resolve(constants.yesString));
|
||||
|
||||
// setup test files
|
||||
const folderPath = await testUtils.generateTestFolderPath();
|
||||
const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.SSDTProjectFileBaseline, folderPath);
|
||||
await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, folderPath);
|
||||
|
||||
const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider());
|
||||
|
||||
const project = await projController.openProject(vscode.Uri.file(sqlProjPath));
|
||||
|
||||
should(await exists(sqlProjPath + '_backup')).equal(true); // backup file should be generated before the project is updated
|
||||
should(project.importedTargets.length).equal(3); // additional target added by updateProjectForRoundTrip method
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
async function setupDeleteExcludeTest(proj: Project): Promise<[FileProjectEntry, ProjectRootTreeItem, FileProjectEntry, FileProjectEntry, FileProjectEntry]> {
|
||||
await proj.addFolderItem('UpperFolder');
|
||||
await proj.addFolderItem('UpperFolder/LowerFolder');
|
||||
@@ -725,3 +659,10 @@ async function setupDeleteExcludeTest(proj: Project): Promise<[FileProjectEntry,
|
||||
|
||||
return [scriptEntry, projTreeRoot, preDeployEntry, postDeployEntry, noneEntry];
|
||||
}
|
||||
|
||||
function createWorkspaceTreeItem(node: BaseProjectTreeItem): dataworkspace.WorkspaceTreeItem {
|
||||
return {
|
||||
element: node,
|
||||
treeDataProvider: new SqlDatabaseProjectTreeViewProvider()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import * as baselines from './baselines/baselines';
|
||||
import * as testUtils from './testUtils';
|
||||
import * as constants from '../common/constants';
|
||||
import { ProjectsController } from '../controllers/projectController';
|
||||
import { SqlDatabaseProjectTreeViewProvider } from '../controllers/databaseProjectTreeViewProvider';
|
||||
import { TestContext, createContext, mockDacFxOptionsResult } from './testContext';
|
||||
import { load } from '../models/publishProfile/publishProfile';
|
||||
|
||||
@@ -82,7 +81,7 @@ describe('Publish profile tests', function (): void {
|
||||
it('Should throw error when connecting does not work', async function (): Promise<void> {
|
||||
await baselines.loadBaselines();
|
||||
let profilePath = await testUtils.createTestFile(baselines.publishProfileIntegratedSecurityBaseline, 'publishProfile.publish.xml');
|
||||
const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider());
|
||||
const projController = new ProjectsController();
|
||||
|
||||
sinon.stub(azdata.connection, 'connect').throws(new Error('Could not connect'));
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ export class MockDacFxService implements mssql.IDacFxService {
|
||||
public exportBacpac(_: string, __: string, ___: string, ____: azdata.TaskExecutionMode): Thenable<mssql.DacFxResult> { return Promise.resolve(mockDacFxResult); }
|
||||
public importBacpac(_: string, __: string, ___: string, ____: azdata.TaskExecutionMode): Thenable<mssql.DacFxResult> { return Promise.resolve(mockDacFxResult); }
|
||||
public extractDacpac(_: string, __: string, ___: string, ____: string, _____: string, ______: azdata.TaskExecutionMode): Thenable<mssql.DacFxResult> { return Promise.resolve(mockDacFxResult); }
|
||||
public importDatabaseProject(_: string, __: string, ___: string, ____: string, _____: string, ______: mssql.ExtractTarget, _______: azdata.TaskExecutionMode): Thenable<mssql.DacFxResult> { return Promise.resolve(mockDacFxResult); }
|
||||
public createProjectFromDatabase(_: string, __: string, ___: string, ____: string, _____: string, ______: mssql.ExtractTarget, _______: azdata.TaskExecutionMode): Thenable<mssql.DacFxResult> { return Promise.resolve(mockDacFxResult); }
|
||||
public deployDacpac(_: string, __: string, ___: boolean, ____: string, _____: azdata.TaskExecutionMode, ______?: Record<string, string>): Thenable<mssql.DacFxResult> { return Promise.resolve(mockDacFxResult); }
|
||||
public generateDeployScript(_: string, __: string, ___: string, ____: azdata.TaskExecutionMode, ______?: Record<string, string>): Thenable<mssql.DacFxResult> { return Promise.resolve(mockDacFxResult); }
|
||||
public generateDeployPlan(_: string, __: string, ___: string, ____: azdata.TaskExecutionMode): Thenable<mssql.GenerateDeployPlanResult> { return Promise.resolve(mockDacFxResult); }
|
||||
|
||||
@@ -5,24 +5,17 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import * as constants from '../common/constants';
|
||||
|
||||
/**
|
||||
* Sets workspace setting on the default save location to the user's home directory
|
||||
*/
|
||||
export async function initializeSaveLocationSetting() {
|
||||
if (!projectSaveLocationSettingExists()) {
|
||||
await config().update(constants.projectSaveLocationKey, os.homedir(), true);
|
||||
}
|
||||
}
|
||||
import * as utils from '../common/utils';
|
||||
|
||||
/**
|
||||
* Returns the default location to save a new database project
|
||||
*/
|
||||
export function defaultProjectSaveLocation(): vscode.Uri {
|
||||
return projectSaveLocationSettingIsValid() ? vscode.Uri.file(projectSaveLocationSetting()) : vscode.Uri.file(os.homedir());
|
||||
export function defaultProjectSaveLocation(): vscode.Uri | undefined {
|
||||
const workspaceApi = utils.getDataWorkspaceExtensionApi();
|
||||
return workspaceApi.defaultProjectSaveLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,7 +34,8 @@ export function defaultProjectNameNewProj(): string {
|
||||
*/
|
||||
export function defaultProjectNameFromDb(dbName: string): string {
|
||||
const projectNameStarter = constants.defaultProjectNameStarter + dbName;
|
||||
const projectPath: string = path.join(defaultProjectSaveLocation().fsPath, projectNameStarter);
|
||||
const defaultLocation = defaultProjectSaveLocation() ?? vscode.Uri.file(os.homedir());
|
||||
const projectPath: string = path.join(defaultLocation.fsPath, projectNameStarter);
|
||||
if (!fs.existsSync(projectPath)) {
|
||||
return projectNameStarter;
|
||||
}
|
||||
@@ -49,58 +43,6 @@ export function defaultProjectNameFromDb(dbName: string): string {
|
||||
return defaultProjectName(projectNameStarter, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts user to update workspace settings
|
||||
*/
|
||||
export async function updateSaveLocationSetting(): Promise<void> {
|
||||
const showPrompt: boolean = config()[constants.showUpdatePromptKey];
|
||||
if (showPrompt) {
|
||||
const openSettingsMessage = projectSaveLocationSettingIsValid() ?
|
||||
constants.newDefaultProjectSaveLocation : constants.invalidDefaultProjectSaveLocation;
|
||||
const result = await vscode.window.showInformationMessage(openSettingsMessage, constants.openWorkspaceSettings,
|
||||
constants.doNotPromptAgain);
|
||||
|
||||
if (result === constants.openWorkspaceSettings || result === constants.doNotPromptAgain) {
|
||||
// if user either opens settings or clicks "don't ask again", do not prompt for save location again
|
||||
await config().update(constants.showUpdatePromptKey, false, true);
|
||||
|
||||
if (result === constants.openWorkspaceSettings) {
|
||||
await vscode.commands.executeCommand('workbench.action.openGlobalSettings'); //open settings
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get workspace configurations for this extension
|
||||
*/
|
||||
function config(): vscode.WorkspaceConfiguration {
|
||||
return vscode.workspace.getConfiguration(constants.dbProjectConfigurationKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the workspace setting on the default location to save new database projects
|
||||
*/
|
||||
function projectSaveLocationSetting(): string {
|
||||
return config()[constants.projectSaveLocationKey];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the default save location for new database projects workspace setting exists and is
|
||||
* a valid path
|
||||
*/
|
||||
function projectSaveLocationSettingIsValid(): boolean {
|
||||
return projectSaveLocationSettingExists() && fs.existsSync(projectSaveLocationSetting());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if a value for the default save location for new database projects exists
|
||||
*/
|
||||
function projectSaveLocationSettingExists(): boolean {
|
||||
return projectSaveLocationSetting() !== undefined && projectSaveLocationSetting() !== null
|
||||
&& projectSaveLocationSetting().trim() !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a project name that begins with the given nameStarter, and ends in a number, such as
|
||||
* 'DatabaseProject1'. Number begins at the given counter, but auto-increments if a project of
|
||||
@@ -112,7 +54,8 @@ function projectSaveLocationSettingExists(): boolean {
|
||||
function defaultProjectName(nameStarter: string, counter: number): string {
|
||||
while (counter < Number.MAX_SAFE_INTEGER) {
|
||||
const name: string = nameStarter + counter;
|
||||
const projectPath: string = path.join(defaultProjectSaveLocation().fsPath, name);
|
||||
const defaultLocation = defaultProjectSaveLocation() ?? vscode.Uri.file(os.homedir());
|
||||
const projectPath: string = path.join(defaultLocation.fsPath, name);
|
||||
if (!fs.existsSync(projectPath)) {
|
||||
return name;
|
||||
}
|
||||
|
||||
13
src/sql/azdata.proposed.d.ts
vendored
13
src/sql/azdata.proposed.d.ts
vendored
@@ -815,6 +815,19 @@ declare module 'azdata' {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export namespace workspace {
|
||||
/**
|
||||
* Creates and enters a workspace at the specified location
|
||||
*/
|
||||
export function createWorkspace(location: vscode.Uri, workspaceFile?: vscode.Uri): Promise<void>;
|
||||
|
||||
/**
|
||||
* Enters the workspace with the provided path
|
||||
* @param workspacefile
|
||||
*/
|
||||
export function enterWorkspace(workspaceFile: vscode.Uri): Promise<void>;
|
||||
}
|
||||
|
||||
export interface TableComponentProperties {
|
||||
/**
|
||||
* Specifies whether to use headerFilter plugin
|
||||
|
||||
@@ -20,3 +20,4 @@ import './mainThreadObjectExplorer';
|
||||
import './mainThreadQueryEditor';
|
||||
import './mainThreadResourceProvider';
|
||||
import './mainThreadTasks';
|
||||
import './mainThreadWorkspace';
|
||||
|
||||
33
src/sql/workbench/api/browser/mainThreadWorkspace.ts
Normal file
33
src/sql/workbench/api/browser/mainThreadWorkspace.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { SqlMainContext, MainThreadWorkspaceShape } from 'sql/workbench/api/common/sqlExtHost.protocol';
|
||||
import { IExtHostContext } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing';
|
||||
|
||||
@extHostNamedCustomer(SqlMainContext.MainThreadWorkspace)
|
||||
export class MainThreadWorkspace extends Disposable implements MainThreadWorkspaceShape {
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
$createWorkspace(folder: URI, workspaceFile?: URI): Promise<void> {
|
||||
folder = URI.revive(folder);
|
||||
workspaceFile = URI.revive(workspaceFile);
|
||||
return this.workspaceEditingService.createAndEnterWorkspace([{ uri: folder }], workspaceFile);
|
||||
}
|
||||
|
||||
$enterWorkspace(workspaceFile: URI): Promise<void> {
|
||||
workspaceFile = URI.revive(workspaceFile);
|
||||
return this.workspaceEditingService.enterWorkspace(workspaceFile);
|
||||
}
|
||||
}
|
||||
27
src/sql/workbench/api/common/extHostWorkspace.ts
Normal file
27
src/sql/workbench/api/common/extHostWorkspace.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IMainContext } from 'vs/workbench/api/common/extHost.protocol';
|
||||
|
||||
import { ExtHostWorkspaceShape, MainThreadWorkspaceShape, SqlMainContext } from 'sql/workbench/api/common/sqlExtHost.protocol';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
|
||||
export class ExtHostWorkspace implements ExtHostWorkspaceShape {
|
||||
|
||||
private readonly _proxy: MainThreadWorkspaceShape;
|
||||
|
||||
constructor(_mainContext: IMainContext) {
|
||||
this._proxy = _mainContext.getProxy(SqlMainContext.MainThreadWorkspace);
|
||||
}
|
||||
|
||||
$createWorkspace(folder: URI, workspaceFile: URI): Promise<void> {
|
||||
return this._proxy.$createWorkspace(folder, workspaceFile);
|
||||
}
|
||||
|
||||
$enterWorkspace(workspaceFile: URI): Promise<void> {
|
||||
return this._proxy.$enterWorkspace(workspaceFile);
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IExtensionApiFactory as vsIApiFactory, createApiFactoryAndRegisterActors as vsApiFactory } from 'vs/workbench/api/common/extHost.api.impl';
|
||||
import { IExtHostCommands } from 'vs/workbench/api/common/extHostCommands';
|
||||
import { ExtHostWorkspace } from 'sql/workbench/api/common/extHostWorkspace';
|
||||
|
||||
export interface IAzdataExtensionApiFactory {
|
||||
(extension: IExtensionDescription): typeof azdata;
|
||||
@@ -92,6 +93,7 @@ export function createAdsApiFactory(accessor: ServicesAccessor): IAdsExtensionAp
|
||||
const extHostNotebook = rpcProtocol.set(SqlExtHostContext.ExtHostNotebook, new ExtHostNotebook(rpcProtocol));
|
||||
const extHostNotebookDocumentsAndEditors = rpcProtocol.set(SqlExtHostContext.ExtHostNotebookDocumentsAndEditors, new ExtHostNotebookDocumentsAndEditors(rpcProtocol));
|
||||
const extHostExtensionManagement = rpcProtocol.set(SqlExtHostContext.ExtHostExtensionManagement, new ExtHostExtensionManagement(rpcProtocol));
|
||||
const extHostWorkspace = rpcProtocol.set(SqlExtHostContext.ExtHostWorkspace, new ExtHostWorkspace(rpcProtocol));
|
||||
|
||||
return {
|
||||
azdata: function (extension: IExtensionDescription): typeof azdata {
|
||||
@@ -459,6 +461,12 @@ export function createAdsApiFactory(accessor: ServicesAccessor): IAdsExtensionAp
|
||||
onDidChangeToDashboard: extHostDashboard.onDidChangeToDashboard,
|
||||
createModelViewEditor(title: string, options?: azdata.ModelViewEditorOptions, name?: string): azdata.workspace.ModelViewEditor {
|
||||
return extHostModelViewDialog.createModelViewEditor(title, extension, name, options);
|
||||
},
|
||||
createWorkspace(location: vscode.Uri, workspaceFile: vscode.Uri): Promise<void> {
|
||||
return extHostWorkspace.$createWorkspace(location, workspaceFile);
|
||||
},
|
||||
enterWorkspace(workspaceFile: vscode.Uri): Promise<void> {
|
||||
return extHostWorkspace.$enterWorkspace(workspaceFile);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -658,7 +658,8 @@ export const SqlMainContext = {
|
||||
MainThreadQueryEditor: createMainId<MainThreadQueryEditorShape>('MainThreadQueryEditor'),
|
||||
MainThreadNotebook: createMainId<MainThreadNotebookShape>('MainThreadNotebook'),
|
||||
MainThreadNotebookDocumentsAndEditors: createMainId<MainThreadNotebookDocumentsAndEditorsShape>('MainThreadNotebookDocumentsAndEditors'),
|
||||
MainThreadExtensionManagement: createMainId<MainThreadExtensionManagementShape>('MainThreadExtensionManagement')
|
||||
MainThreadExtensionManagement: createMainId<MainThreadExtensionManagementShape>('MainThreadExtensionManagement'),
|
||||
MainThreadWorkspace: createMainId<MainThreadWorkspaceShape>('MainThreadWorkspace')
|
||||
};
|
||||
|
||||
export const SqlExtHostContext = {
|
||||
@@ -679,7 +680,8 @@ export const SqlExtHostContext = {
|
||||
ExtHostQueryEditor: createExtId<ExtHostQueryEditorShape>('ExtHostQueryEditor'),
|
||||
ExtHostNotebook: createExtId<ExtHostNotebookShape>('ExtHostNotebook'),
|
||||
ExtHostNotebookDocumentsAndEditors: createExtId<ExtHostNotebookDocumentsAndEditorsShape>('ExtHostNotebookDocumentsAndEditors'),
|
||||
ExtHostExtensionManagement: createExtId<ExtHostExtensionManagementShape>('ExtHostExtensionManagement')
|
||||
ExtHostExtensionManagement: createExtId<ExtHostExtensionManagementShape>('ExtHostExtensionManagement'),
|
||||
ExtHostWorkspace: createExtId<ExtHostWorkspaceShape>('ExtHostWorkspace')
|
||||
};
|
||||
|
||||
export interface MainThreadDashboardShape extends IDisposable {
|
||||
@@ -756,6 +758,16 @@ export interface ExtHostBackgroundTaskManagementShape {
|
||||
$removeTask(operationId: string): void;
|
||||
}
|
||||
|
||||
export interface ExtHostWorkspaceShape {
|
||||
$createWorkspace(folder: vscode.Uri, workspaceFile: vscode.Uri): Promise<void>;
|
||||
$enterWorkspace(workspaceFile: vscode.Uri): Promise<void>;
|
||||
}
|
||||
|
||||
export interface MainThreadWorkspaceShape {
|
||||
$createWorkspace(folder: vscode.Uri, workspaceFile: vscode.Uri): Promise<void>;
|
||||
$enterWorkspace(workspaceFile: vscode.Uri): Promise<void>;
|
||||
}
|
||||
|
||||
export interface MainThreadBackgroundTaskManagementShape extends IDisposable {
|
||||
$registerTask(taskInfo: azdata.TaskInfo): void;
|
||||
$updateTask(taskProgressInfo: azdata.TaskProgressInfo): void;
|
||||
|
||||
@@ -32,11 +32,11 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
height: auto;
|
||||
width: auto;
|
||||
padding: 5px 5px 5px 5px;
|
||||
min-height: 130px;
|
||||
min-width: 130px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.horizontal .model-card .card-label {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<div *ngIf="card.icon" class="icon-container">
|
||||
<div [class]="getIconClass(card.id)" [style.width]="iconWidth" [style.height]="iconHeight"> </div>
|
||||
</div>
|
||||
<div class="text-container">
|
||||
<div class="text-container" [style.height]="textHeight">
|
||||
<div *ngFor="let description of card.descriptions" class="inner-text-content">
|
||||
<span class="text-value" [ngStyle]="description.textStyles">{{description.textValue}}</span>
|
||||
<a *ngIf="description.linkDisplayValue" class="link-value" href="#"
|
||||
|
||||
@@ -117,6 +117,22 @@ export default class RadioCardGroup extends ComponentBase<azdata.RadioCardGroupC
|
||||
return this.getProperties().iconHeight ?? undefined;
|
||||
}
|
||||
|
||||
public get textHeight(): string | undefined {
|
||||
return this.calculateTextContainerHeight();
|
||||
}
|
||||
|
||||
public calculateTextContainerHeight(): string | undefined {
|
||||
if (this.cardHeight.endsWith('px') && this.iconHeight.endsWith('px')) {
|
||||
const padding = 30; // icon-container padding + text-container padding
|
||||
let height = Number.parseInt(this.cardHeight.substr(0, this.cardHeight.length - 2)) - Number.parseInt(this.iconHeight.substr(0, this.cardHeight.length - 2));
|
||||
height = height - padding;
|
||||
|
||||
return height.toString() + 'px';
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public get selectedCardId(): string | undefined {
|
||||
return this.getProperties().selectedCardId ?? undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user