diff --git a/extensions/data-workspace/package.json b/extensions/data-workspace/package.json index 68e14c465c..a2805950e0 100644 --- a/extensions/data-workspace/package.json +++ b/extensions/data-workspace/package.json @@ -68,10 +68,6 @@ "category": "", "icon": "$(close)" }, - { - "command": "projects.removeProject", - "title": "%remove-project-command%" - }, { "command": "projects.manageProject", "title": "%manage-project-command%" @@ -112,10 +108,6 @@ "command": "dataworkspace.close", "when": "false" }, - { - "command": "projects.removeProject", - "when": "false" - }, { "command": "projects.openExisting" }, @@ -129,11 +121,6 @@ "command": "projects.manageProject", "when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.project", "group": "0_projectsFirst@1" - }, - { - "command": "projects.removeProject", - "when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.project", - "group": "9_dbProjectsLast@9" } ] }, diff --git a/extensions/data-workspace/src/common/constants.ts b/extensions/data-workspace/src/common/constants.ts index 28d94ac69e..bb6b91d44a 100644 --- a/extensions/data-workspace/src/common/constants.ts +++ b/extensions/data-workspace/src/common/constants.ts @@ -13,13 +13,7 @@ export const UnknownProjectsError = (projectFiles: string[]): string => { return 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 workspace will be created and opened in order to open the project. Azure Data Studio will restart and if there is a folder currently open, it will be closed."); -export const EnterWorkspaceConfirmation = localize('dataworkspace.enterWorkspaceConfirmation', "To open this workspace, Azure Data Studio will restart. If there is a workspace or folder currently open, it will be closed."); -export const WorkspaceContainsNotAddedProjects = localize('dataworkspace.workspaceContainsNotAddedProjects', "The current workspace contains one or more projects that have not been added to the workspace. Use the 'Open existing' dialog to add projects to the projects pane."); -export const LaunchOpenExisitingDialog = localize('dataworkspace.launchOpenExistingDialog', "Launch 'Open Existing' Dialog"); -export const DoNotAskAgain = localize('dataworkspace.doNotAskAgain', "Don't Ask Again"); +export const RestartConfirmation = localize('dataworkspace.restartConfirmation', "Azure Data Studio needs to be restarted for the project to be created and added to the workspace, do this now?"); export const ProjectsFailedToLoad = localize('dataworkspace.projectsFailedToLoad', "Some projects failed to load. To view more details, [open the developer console](command:workbench.action.toggleDevTools)"); export const fileDoesNotExist = (name: string): string => { return localize('fileDoesNotExist', "File '{0}' doesn't exist", name); }; export const projectNameNull = localize('projectNameNull', "Project name is null"); @@ -27,13 +21,8 @@ export const noPreviousData = (tableName: string): string => { return localize(' export const gitCloneMessage = (url: string): string => { return localize('gitCloneMessage', "Cloning git repository '{0}'...", url); }; export const gitCloneError = localize('gitCloneError', "Error during git clone. View git output for more details"); -// config settings -export const projectsConfigurationKey = 'projects'; -export const showNotAddedProjectsMessageKey = 'showNotAddedProjectsInWorkspacePrompt'; - // UI export const OkButtonText = localize('dataworkspace.ok', "OK"); -export const CancelButtonText = localize('dataworkspace.cancel', "Cancel"); export const BrowseButtonText = localize('dataworkspace.browse', "Browse"); export const BrowseEllipsis = localize('dataworkspace.browseEllipsis', "Browse..."); export const OpenButtonText = localize('dataworkspace.open', "Open"); @@ -51,20 +40,14 @@ export const ProjectNamePlaceholder = localize('dataworkspace.projectNamePlaceho export const EnterProjectName = localize('dataworkspace.enterProjectName', "Enter Project Name"); export const ProjectLocationTitle = localize('dataworkspace.projectLocationTitle', "Location"); export const ProjectLocationPlaceholder = localize('dataworkspace.projectLocationPlaceholder', "Select location to create project"); -export const AddProjectToCurrentWorkspace = localize('dataworkspace.AddProjectToCurrentWorkspace', "This project will be added to the current workspace."); -export const NewWorkspaceWillBeCreated = localize('dataworkspace.NewWorkspaceWillBeCreated', "A 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 project location '{0}' does not exist or is not a directory.", location); }; export const ProjectDirectoryAlreadyExistError = (projectName: string, location: string): string => { return localize('dataworkspace.projectDirectoryAlreadyExistError', "There is already a directory named '{0}' in the selected location: '{1}'.", projectName, location); }; export const ProjectDirectoryAlreadyExistErrorShort = (projectName: string) => { return localize('dataworkspace.projectDirectoryAlreadyExistErrorShort', "Directory '{0}' already exists in the selected location, please choose another", projectName); }; -export const WorkspaceFileInvalidError = (workspace: string): string => { return localize('dataworkspace.workspaceFileInvalidError', "The selected workspace file path '{0}' does not have the required file extension {1}.", workspace, WorkspaceFileExtension); }; -export const WorkspaceParentDirectoryNotExistError = (location: string): string => { return localize('dataworkspace.workspaceParentDirectoryNotExistError', "The selected workspace location '{0}' does not exist or is not a directory.", location); }; -export const WorkspaceFileAlreadyExistsError = (file: string): string => { return localize('dataworkspace.workspaceFileAlreadyExistsError', "The selected workspace file '{0}' already exists. To add the project to an existing workspace, use the Open Existing dialog to first open the workspace.", file); }; export const SelectProjectType = localize('dataworkspace.selectProjectType', "Select Project Type"); export const SelectProjectLocation = localize('dataworkspace.selectProjectLocation', "Select Project Location"); export const NameCannotBeEmpty = localize('dataworkspace.nameCannotBeEmpty', "Name cannot be empty"); //Open Existing Dialog -export const OpenExistingDialogTitle = localize('dataworkspace.openExistingDialogTitle', "Open existing"); +export const OpenExistingDialogTitle = localize('dataworkspace.openExistingDialogTitle', "Open Existing Project"); export const FileNotExistError = (fileType: string, filePath: string): string => { return localize('dataworkspace.fileNotExistError', "The selected {0} file '{1}' does not exist or is not a file.", fileType, filePath); }; export const CloneParentDirectoryNotExistError = (location: string): string => { return localize('dataworkspace.cloneParentDirectoryNotExistError', "The selected clone path '{0}' does not exist or is not a directory.", location); }; export const Project = localize('dataworkspace.project', "Project"); diff --git a/extensions/data-workspace/src/common/dataWorkspaceExtension.ts b/extensions/data-workspace/src/common/dataWorkspaceExtension.ts index e09f0bdee2..fe0ff2e2a6 100644 --- a/extensions/data-workspace/src/common/dataWorkspaceExtension.ts +++ b/extensions/data-workspace/src/common/dataWorkspaceExtension.ts @@ -12,12 +12,12 @@ export class DataWorkspaceExtension implements IExtension { constructor(private workspaceService: WorkspaceService) { } - getProjectsInWorkspace(ext?: string): vscode.Uri[] { + getProjectsInWorkspace(ext?: string): Promise { return this.workspaceService.getProjectsInWorkspace(ext); } - addProjectsToWorkspace(projectFiles: vscode.Uri[], workspaceFilePath?: vscode.Uri): Promise { - return this.workspaceService.addProjectsToWorkspace(projectFiles, workspaceFilePath); + addProjectsToWorkspace(projectFiles: vscode.Uri[]): Promise { + return this.workspaceService.addProjectsToWorkspace(projectFiles); } showProjectsView(): void { diff --git a/extensions/data-workspace/src/common/interfaces.ts b/extensions/data-workspace/src/common/interfaces.ts index 9611169f90..1fa696ee52 100644 --- a/extensions/data-workspace/src/common/interfaces.ts +++ b/extensions/data-workspace/src/common/interfaces.ts @@ -51,7 +51,7 @@ export interface IWorkspaceService { /** * Gets the project files in current workspace */ - getProjectsInWorkspace(): vscode.Uri[]; + getProjectsInWorkspace(): Promise; /** * Gets the project provider by project file @@ -66,28 +66,20 @@ export interface IWorkspaceService { */ addProjectsToWorkspace(projectFiles: vscode.Uri[], workspaceFilePath?: vscode.Uri): Promise; - /** - * Remove the project from workspace - * @param projectFile The project file to be removed - */ - removeProject(projectFile: vscode.Uri): Promise; - /** * 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 - * @param workspaceFile The workspace file to create if a workspace isn't currently open */ - createProject(name: string, location: vscode.Uri, projectTypeId: string, workspaceFile?: vscode.Uri): Promise; + createProject(name: string, location: vscode.Uri, projectTypeId: string): Promise; /** * Clones git repository and adds projects to workspace * @param url The url to clone from * @param localClonePath local path to clone repository to - * @param workspaceFile workspace file to add the projects to */ - gitCloneProject(url: string, localClonePath: string, workspaceFile: vscode.Uri): Promise; + gitCloneProject(url: string, localClonePath: string): Promise; readonly isProjectProviderAvailable: boolean; @@ -100,9 +92,4 @@ export interface IWorkspaceService { * Verify that a workspace is open or if one isn't, ask user to pick whether a workspace should be automatically created */ validateWorkspace(): Promise; - - /** - * 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; } diff --git a/extensions/data-workspace/src/common/telemetry.ts b/extensions/data-workspace/src/common/telemetry.ts index 9126154d35..aa7044eec0 100644 --- a/extensions/data-workspace/src/common/telemetry.ts +++ b/extensions/data-workspace/src/common/telemetry.ts @@ -4,9 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import AdsTelemetryReporter from '@microsoft/ads-extension-telemetry'; -import * as path from 'path'; import * as utils from './utils'; -import * as vscode from 'vscode'; const packageJson = require('../../package.json'); @@ -14,28 +12,6 @@ let packageInfo = utils.getPackageInfo(packageJson)!; export const TelemetryReporter = new AdsTelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey); -export function calculateRelativity(projectPath: string, workspacePath?: string): string { - workspacePath = workspacePath ?? vscode.workspace.workspaceFile?.fsPath; - - if (!workspacePath) { - return 'noWorkspace'; - } - - const relativePath = path.relative(path.dirname(projectPath), path.dirname(workspacePath)); - - if (relativePath.length === 0) { // no path difference - return 'sameFolder'; - } - - const pathParts = relativePath.split(path.sep); - - if (pathParts.every(x => x === '..')) { - return 'directAncestor'; - } - - return 'other'; // sibling, cousin, descendant, etc. -} - export enum TelemetryViews { WorkspaceTreePane = 'WorkspaceTreePane', diff --git a/extensions/data-workspace/src/common/utils.ts b/extensions/data-workspace/src/common/utils.ts index f42ee8fb14..7200b07baf 100644 --- a/extensions/data-workspace/src/common/utils.ts +++ b/extensions/data-workspace/src/common/utils.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs'; -import * as vscode from 'vscode'; import type * as azdataType from 'azdata'; export async function directoryExist(directoryPath: string): Promise { @@ -32,13 +31,6 @@ async function getFileStatus(path: string): Promise { } } -/** - * if the current workspace is untitled, the returned URI of vscode.workspace.workspaceFile will use the `untitled` scheme - */ -export function isCurrentWorkspaceUntitled(): boolean { - return !!vscode.workspace.workspaceFile && vscode.workspace.workspaceFile.scheme.toLowerCase() === 'untitled'; -} - export interface IPackageInfo { name: string; version: string; diff --git a/extensions/data-workspace/src/dataworkspace.d.ts b/extensions/data-workspace/src/dataworkspace.d.ts index 1fa01e073e..885074c88d 100644 --- a/extensions/data-workspace/src/dataworkspace.d.ts +++ b/extensions/data-workspace/src/dataworkspace.d.ts @@ -18,14 +18,13 @@ declare module 'dataworkspace' { * Returns all the projects in the workspace * @param ext project extension to filter on. If this is passed in, this will only return projects with this file extension */ - getProjectsInWorkspace(ext?: string): vscode.Uri[]; + getProjectsInWorkspace(ext?: string): Promise; /** * Add projects to the workspace - * @param projectFiles Uris of project files to add, - * @param workspaceFilePath workspace file to create if no workspace is open + * @param projectFiles Uris of project files to add */ - addProjectsToWorkspace(projectFiles: vscode.Uri[], workspaceFilePath?: vscode.Uri): Promise; + addProjectsToWorkspace(projectFiles: vscode.Uri[]): Promise; /** * Change focus to Projects view @@ -53,12 +52,6 @@ declare module 'dataworkspace' { */ getProjectTreeDataProvider(projectFile: vscode.Uri): Promise>; - /** - * Notify the project provider extension that the specified project file has been removed from the data workspace - * @param projectFile The Uri of the project file - */ - RemoveProject(projectFile: vscode.Uri): Promise; - /** * * @param name Create a project diff --git a/extensions/data-workspace/src/dialogs/dialogBase.ts b/extensions/data-workspace/src/dialogs/dialogBase.ts index b4adb6d15f..f4c0b1fa13 100644 --- a/extensions/data-workspace/src/dialogs/dialogBase.ts +++ b/extensions/data-workspace/src/dialogs/dialogBase.ts @@ -5,10 +5,7 @@ import type * as azdataType from 'azdata'; import * as vscode from 'vscode'; -import * as path from 'path'; -import * as constants from '../common/constants'; -import { IconPathHelper } from '../common/iconHelper'; -import { directoryExist, fileExist, getAzdataApi, isCurrentWorkspaceUntitled } from '../common/utils'; +import { getAzdataApi } from '../common/utils'; interface Deferred { resolve: (result: T | Promise) => void; @@ -20,9 +17,6 @@ export abstract class DialogBase { public dialogObject: azdataType.window.Dialog; protected initDialogComplete: Deferred | undefined; protected initDialogPromise: Promise = new Promise((resolve, reject) => this.initDialogComplete = { resolve, reject }); - protected workspaceDescriptionFormComponent: azdataType.FormComponent | undefined; - public workspaceInputBox: azdataType.InputBoxComponent | undefined; - protected workspaceInputFormComponent: azdataType.FormComponent | undefined; constructor(dialogTitle: string, dialogName: string, okButtonText: string, dialogWidth: azdataType.window.DialogWidth = 600) { this.dialogObject = getAzdataApi()!.window.createModelViewDialog(dialogTitle, dialogName, dialogWidth); @@ -82,106 +76,4 @@ export abstract class DialogBase { protected createHorizontalContainer(view: azdataType.ModelView, items: azdataType.Component[]): azdataType.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: azdataType.ModelView): void { - const workspaceDescription = view.modelBuilder.text().withProperties({ - value: vscode.workspace.workspaceFile ? constants.AddProjectToCurrentWorkspace : constants.NewWorkspaceWillBeCreated, - CSSStyles: { 'margin-top': '3px', 'margin-bottom': '0px' } - }).component(); - - const initialWorkspaceInputBoxValue = !!vscode.workspace.workspaceFile && !isCurrentWorkspaceUntitled() ? vscode.workspace.workspaceFile.fsPath : ''; - - this.workspaceInputBox = view.modelBuilder.inputBox().withProperties({ - ariaLabel: constants.WorkspaceLocationTitle, - width: constants.DefaultInputWidth, - enabled: !vscode.workspace.workspaceFile || isCurrentWorkspaceUntitled(), // want it editable if no saved workspace is open - value: initialWorkspaceInputBoxValue, - title: initialWorkspaceInputBoxValue // hovertext for if file path is too long to be seen in textbox - }).component(); - - const browseFolderButton = view.modelBuilder.button().withProperties({ - ariaLabel: constants.BrowseButtonText, - iconPath: IconPathHelper.folder, - height: '16px', - width: '18px' - }).component(); - - this.register(browseFolderButton.onDidClick(async () => { - const currentFileName = path.parse(this.workspaceInputBox!.value!).base; - - // let user select folder for workspace file to be created in - const folderUris = await vscode.window.showOpenDialog({ - canSelectFiles: false, - canSelectFolders: true, - canSelectMany: false, - defaultUri: vscode.Uri.file(path.parse(this.workspaceInputBox!.value!).dir) - }); - if (!folderUris || folderUris.length === 0) { - return; - } - const selectedFolder = folderUris[0].fsPath; - - const selectedFile = path.join(selectedFolder, currentFileName); - this.workspaceInputBox!.value = selectedFile; - this.workspaceInputBox!.title = selectedFile; - })); - - if (vscode.workspace.workspaceFile && !isCurrentWorkspaceUntitled()) { - this.workspaceInputFormComponent = { - component: this.workspaceInputBox - }; - } else { - // have browse button to help select where the workspace file should be created - const horizontalContainer = this.createHorizontalContainer(view, [this.workspaceInputBox, browseFolderButton]); - this.workspaceInputFormComponent = { - component: horizontalContainer - }; - } - - this.workspaceDescriptionFormComponent = { - title: constants.Workspace, - component: workspaceDescription, - required: true - }; - } - - /** - * 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 || isCurrentWorkspaceUntitled()) { - const fileLocation = location && name ? path.join(location, `${name}.code-workspace`) : ''; - this.workspaceInputBox!.value = fileLocation; - this.workspaceInputBox!.title = fileLocation; - } - } - - public async validateNewWorkspace(sameFolderAsNewProject: boolean): Promise { - // workspace file should end in .code-workspace - const workspaceValid = this.workspaceInputBox!.value!.endsWith(constants.WorkspaceFileExtension); - if (!workspaceValid) { - throw new Error(constants.WorkspaceFileInvalidError(this.workspaceInputBox!.value!)); - } - - // if the workspace file is not going to be in the same folder as the newly created project, then check that it's a valid folder - if (!sameFolderAsNewProject) { - const workspaceParentDirectoryExists = await directoryExist(path.dirname(this.workspaceInputBox!.value!)); - if (!workspaceParentDirectoryExists) { - throw new Error(constants.WorkspaceParentDirectoryNotExistError(path.dirname(this.workspaceInputBox!.value!))); - } - } - - // workspace file should not be an existing workspace file - const workspaceFileExists = await fileExist(this.workspaceInputBox!.value!); - if (workspaceFileExists) { - throw new Error(constants.WorkspaceFileAlreadyExistsError(this.workspaceInputBox!.value!)); - } - } } diff --git a/extensions/data-workspace/src/dialogs/newProjectDialog.ts b/extensions/data-workspace/src/dialogs/newProjectDialog.ts index 803b46bf62..68608523b3 100644 --- a/extensions/data-workspace/src/dialogs/newProjectDialog.ts +++ b/extensions/data-workspace/src/dialogs/newProjectDialog.ts @@ -34,6 +34,9 @@ export class NewProjectDialog extends DialogBase { } async validate(): Promise { + if (await this.workspaceService.validateWorkspace() === false) { + return false; + } try { // the selected location should be an existing directory const parentDirectoryExists = await directoryExist(this.model.location); @@ -49,11 +52,6 @@ export class NewProjectDialog extends DialogBase { return false; } - if (this.workspaceInputBox!.enabled) { - const sameFolderAsNewProject = path.join(this.model.location, this.model.name) === path.dirname(this.workspaceInputBox!.value!); - await this.validateNewWorkspace(sameFolderAsNewProject); - } - return true; } catch (err) { @@ -64,15 +62,12 @@ export class NewProjectDialog extends DialogBase { override async onComplete(): Promise { try { - const validateWorkspace = await this.workspaceService.validateWorkspace(); TelemetryReporter.createActionEvent(TelemetryViews.NewProjectDialog, TelemetryActions.NewProjectDialogCompleted) - .withAdditionalProperties({ projectFileExtension: this.model.projectFileExtension, projectTemplateId: this.model.projectTypeId, workspaceValidationPassed: validateWorkspace.toString() }) + .withAdditionalProperties({ projectFileExtension: this.model.projectFileExtension, projectTemplateId: this.model.projectTypeId }) .send(); - if (validateWorkspace) { - await this.workspaceService.createProject(this.model.name, vscode.Uri.file(this.model.location), this.model.projectTypeId, vscode.Uri.file(this.workspaceInputBox!.value!)); - } + await this.workspaceService.createProject(this.model.name, vscode.Uri.file(this.model.location), this.model.projectTypeId); } catch (err) { @@ -129,8 +124,6 @@ export class NewProjectDialog extends DialogBase { this.register(projectNameTextBox.onTextChanged(() => { this.model.name = projectNameTextBox.value!; projectNameTextBox.updateProperty('title', projectNameTextBox.value); - - this.updateWorkspaceInputbox(path.join(this.model.location, this.model.name), this.model.name); })); const locationTextBox = view.modelBuilder.inputBox().withProperties({ @@ -143,7 +136,6 @@ export class NewProjectDialog extends DialogBase { this.register(locationTextBox.onTextChanged(() => { this.model.location = locationTextBox.value!; locationTextBox.updateProperty('title', locationTextBox.value); - this.updateWorkspaceInputbox(path.join(this.model.location, this.model.name), this.model.name); })); const browseFolderButton = view.modelBuilder.button().withProperties({ @@ -165,12 +157,8 @@ export class NewProjectDialog extends DialogBase { const selectedFolder = folderUris[0].fsPath; locationTextBox.value = selectedFolder; this.model.location = selectedFolder; - - this.updateWorkspaceInputbox(path.join(this.model.location, this.model.name), this.model.name); })); - this.createWorkspaceContainer(view); - const form = view.modelBuilder.formContainer().withFormItems([ { title: constants.TypeTitle, @@ -185,9 +173,7 @@ export class NewProjectDialog extends DialogBase { title: constants.ProjectLocationTitle, required: true, component: this.createHorizontalContainer(view, [locationTextBox, browseFolderButton]) - }, - this.workspaceDescriptionFormComponent!, - this.workspaceInputFormComponent! + } ]).component(); await view.initializeModel(form); this.initDialogComplete?.resolve(); diff --git a/extensions/data-workspace/src/dialogs/openExistingDialog.ts b/extensions/data-workspace/src/dialogs/openExistingDialog.ts index ba88becfe2..401e3944b4 100644 --- a/extensions/data-workspace/src/dialogs/openExistingDialog.ts +++ b/extensions/data-workspace/src/dialogs/openExistingDialog.ts @@ -5,17 +5,15 @@ import type * as azdataType 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 { directoryExist, fileExist } from '../common/utils'; import { IconPathHelper } from '../common/iconHelper'; -import { calculateRelativity, TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry'; +import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry'; import { defaultProjectSaveLocation } from '../common/projectLocationHelper'; export class OpenExistingDialog extends DialogBase { - public targetTypeRadioCardGroup: azdataType.RadioCardGroupComponent | undefined; public filePathTextBox: azdataType.InputBoxComponent | undefined; public filePathAndButtonComponent: azdataType.FormComponent | undefined; public gitRepoTextBoxComponent: azdataType.FormComponent | undefined; @@ -26,17 +24,7 @@ export class OpenExistingDialog extends DialogBase { public locationRadioButtonFormComponent: azdataType.FormComponent | undefined; public formBuilder: azdataType.FormBuilder | undefined; - private _targetTypes = [ - { - name: constants.Project, - icon: this.extensionContext.asAbsolutePath('images/Open_existing_Project.svg') - }, { - name: constants.Workspace, - icon: this.extensionContext.asAbsolutePath('images/Open_existing_Workspace.svg') - } - ]; - - constructor(private workspaceService: IWorkspaceService, private extensionContext: vscode.ExtensionContext) { + constructor(private workspaceService: IWorkspaceService) { super(constants.OpenExistingDialogTitle, 'OpenProject', constants.OpenButtonText); // dialog launched from Welcome message button (only visible when no current workspace) vs. "add project" button @@ -47,27 +35,15 @@ export class OpenExistingDialog extends DialogBase { async validate(): Promise { try { - // the selected location should be an existing directory - if (this.targetTypeRadioCardGroup?.selectedCardId === constants.Project) { - if (this.localRadioButton?.checked) { - await this.validateFile(this.filePathTextBox!.value!, constants.Project.toLowerCase()); - } else { - await this.validateClonePath(this.localClonePathTextBox!.value); - } - - if (this.workspaceInputBox!.enabled) { - await this.validateNewWorkspace(false); - } - } else if (this.targetTypeRadioCardGroup?.selectedCardId === constants.Workspace) { - if (this.localRadioButton?.checked) { - await this.validateFile(this.filePathTextBox!.value!, constants.Workspace.toLowerCase()); - } else { - // validate clone location - // check if parent folder exists - await this.validateClonePath(this.localClonePathTextBox!.value); - } + if (await this.workspaceService.validateWorkspace() === false) { + return false; } + if (this.localRadioButton?.checked) { + await this.validateFile(this.filePathTextBox!.value!, constants.Project.toLowerCase()); + } else { + await this.validateClonePath(this.localClonePathTextBox!.value); + } return true; } catch (err) { @@ -94,55 +70,27 @@ export class OpenExistingDialog extends DialogBase { override async onComplete(): Promise { try { - if (this.targetTypeRadioCardGroup?.selectedCardId === constants.Workspace) { - // capture that workspace was selected, also if there's already an open workspace that's being replaced - TelemetryReporter.createActionEvent(TelemetryViews.OpenExistingDialog, TelemetryActions.OpeningWorkspace) - .withAdditionalProperties({ hasWorkspaceOpen: (vscode.workspace.workspaceFile !== undefined).toString() }) + // save datapoint now because it'll get set to new value during validateWorkspace() + const telemetryProps: any = { hasWorkspaceOpen: (vscode.workspace.workspaceFile !== undefined).toString() }; + + let addProjectsPromise: Promise; + + if (this.remoteGitRepoRadioButton!.checked) { + TelemetryReporter.createActionEvent(TelemetryViews.OpenExistingDialog, TelemetryActions.GitClone) + .withAdditionalProperties({ selectedTarget: 'project' }) .send(); - if (this.remoteGitRepoRadioButton!.checked) { - TelemetryReporter.createActionEvent(TelemetryViews.OpenExistingDialog, TelemetryActions.GitClone) - .withAdditionalProperties({ selectedTarget: 'workspace' }) - .send(); - - // show git output channel - vscode.commands.executeCommand('git.showOutput'); - // after this executes, the git extension will show a popup asking if you want to enter the workspace - await vscode.commands.executeCommand('git.clone', (this.gitRepoTextBoxComponent?.component).value, this.localClonePathTextBox!.value); - } else { - await this.workspaceService.enterWorkspace(vscode.Uri.file(this.filePathTextBox!.value!)); - } + addProjectsPromise = this.workspaceService.gitCloneProject((this.gitRepoTextBoxComponent?.component).value!, this.localClonePathTextBox!.value!); } else { - // save datapoint now because it'll get set to new value during validateWorkspace() - const telemetryProps: any = { hasWorkspaceOpen: (vscode.workspace.workspaceFile !== undefined).toString() }; - - const validateWorkspace = await this.workspaceService.validateWorkspace(); - let addProjectsPromise: Promise; - - if (this.remoteGitRepoRadioButton!.checked) { - TelemetryReporter.createActionEvent(TelemetryViews.OpenExistingDialog, TelemetryActions.GitClone) - .withAdditionalProperties({ selectedTarget: 'project' }) - .send(); - - addProjectsPromise = this.workspaceService.gitCloneProject((this.gitRepoTextBoxComponent?.component).value!, this.localClonePathTextBox!.value!, vscode.Uri.file(this.workspaceInputBox!.value!)); - } else { - if (validateWorkspace) { - telemetryProps.workspaceProjectRelativity = calculateRelativity(this.filePathTextBox!.value!, this.workspaceInputBox!.value!); - telemetryProps.cancelled = 'false'; - addProjectsPromise = this.workspaceService.addProjectsToWorkspace([vscode.Uri.file(this.filePathTextBox!.value!)], vscode.Uri.file(this.workspaceInputBox!.value!)); - } else { - telemetryProps.workspaceProjectRelativity = 'none'; - telemetryProps.cancelled = 'true'; - addProjectsPromise = this.workspaceService.addProjectsToWorkspace([vscode.Uri.file(this.filePathTextBox!.value!)], vscode.Uri.file(this.workspaceInputBox!.value!)); - } - } - - TelemetryReporter.createActionEvent(TelemetryViews.OpenExistingDialog, TelemetryActions.OpeningProject) - .withAdditionalProperties(telemetryProps) - .send(); - - await addProjectsPromise; + telemetryProps.cancelled = 'false'; + addProjectsPromise = this.workspaceService.addProjectsToWorkspace([vscode.Uri.file(this.filePathTextBox!.value!)]); } + + TelemetryReporter.createActionEvent(TelemetryViews.OpenExistingDialog, TelemetryActions.OpeningProject) + .withAdditionalProperties(telemetryProps) + .send(); + + await addProjectsPromise; } catch (err) { vscode.window.showErrorMessage(err?.message ? err.message : err); @@ -150,32 +98,6 @@ export class OpenExistingDialog extends DialogBase { } protected async initialize(view: azdataType.ModelView): Promise { - this.targetTypeRadioCardGroup = view.modelBuilder.radioCardGroup().withProperties({ - cards: this._targetTypes.map((targetType) => { - return { - id: targetType.name, - label: targetType.name, - icon: targetType.icon, - descriptions: [ - { - textValue: targetType.name, - textStyles: { - 'font-size': '13px' - } - } - ] - }; - }), - iconHeight: '100px', - iconWidth: '100px', - cardWidth: '170px', - cardHeight: '170px', - ariaLabel: constants.TypeTitle, - width: '500px', - iconPosition: 'top', - selectedCardId: constants.Project - }).component(); - this.localRadioButton = view.modelBuilder.radioButton().withProperties({ name: 'location', label: constants.Local, @@ -186,7 +108,7 @@ export class OpenExistingDialog extends DialogBase { if (checked) { this.formBuilder?.removeFormItem(this.gitRepoTextBoxComponent); this.formBuilder?.removeFormItem(this.localClonePathComponent); - this.formBuilder?.insertFormItem(this.filePathAndButtonComponent, 2); + this.formBuilder?.insertFormItem(this.filePathAndButtonComponent, 1); } })); @@ -207,8 +129,8 @@ export class OpenExistingDialog extends DialogBase { this.register(this.remoteGitRepoRadioButton.onDidChangeCheckedState(checked => { if (checked) { this.formBuilder?.removeFormItem(this.filePathAndButtonComponent); - this.formBuilder?.insertFormItem(this.gitRepoTextBoxComponent, 2); - this.formBuilder?.insertFormItem(this.localClonePathComponent, 3); + this.formBuilder?.insertFormItem(this.gitRepoTextBoxComponent, 1); + this.formBuilder?.insertFormItem(this.localClonePathComponent, 2); } })); @@ -221,7 +143,6 @@ export class OpenExistingDialog extends DialogBase { this.register(gitRepoTextBox.onTextChanged(() => { gitRepoTextBox.updateProperty('title', this.localClonePathTextBox!.value!); - this.updateWorkspaceInputbox(this.localClonePathTextBox!.value!, path.basename(gitRepoTextBox!.value!, '.git')); })); this.gitRepoTextBoxComponent = { @@ -238,7 +159,6 @@ export class OpenExistingDialog extends DialogBase { this.register(this.localClonePathTextBox.onTextChanged(() => { this.localClonePathTextBox!.updateProperty('title', this.localClonePathTextBox!.value!); - this.updateWorkspaceInputbox(this.localClonePathTextBox!.value!, path.basename(gitRepoTextBox!.value!, '.git')); })); const localClonePathBrowseFolderButton = view.modelBuilder.button().withProperties({ @@ -262,7 +182,6 @@ export class OpenExistingDialog extends DialogBase { const selectedFolder = folderUris[0].fsPath; this.localClonePathTextBox!.value = selectedFolder; this.localClonePathTextBox!.updateProperty('title', this.localClonePathTextBox!.value); - this.updateWorkspaceInputbox(path.dirname(this.localClonePathTextBox!.value!), path.basename((this.gitRepoTextBoxComponent?.component)!.value!, '.git')); })); this.localClonePathComponent = { @@ -280,7 +199,6 @@ export class OpenExistingDialog extends DialogBase { this.register(this.filePathTextBox.onTextChanged(() => { this.filePathTextBox!.updateProperty('title', this.filePathTextBox!.value!); - this.updateWorkspaceInputbox(path.dirname(this.filePathTextBox!.value!), path.basename(this.filePathTextBox!.value!, path.extname(this.filePathTextBox!.value!))); })); const localProjectBrowseFolderButton = view.modelBuilder.button().withProperties({ @@ -290,11 +208,7 @@ export class OpenExistingDialog extends DialogBase { height: '16px' }).component(); this.register(localProjectBrowseFolderButton.onDidClick(async () => { - if (this.targetTypeRadioCardGroup?.selectedCardId === constants.Project) { - await this.projectBrowse(); - } else if (this.targetTypeRadioCardGroup?.selectedCardId === constants.Workspace) { - await this.workspaceBrowse(); - } + await this.projectBrowse(); })); const flexContainer = this.createHorizontalContainer(view, [this.filePathTextBox, localProjectBrowseFolderButton]); @@ -303,74 +217,14 @@ export class OpenExistingDialog extends DialogBase { component: flexContainer }; - this.register(this.targetTypeRadioCardGroup.onSelectionChanged(({ cardId }) => { - if (cardId === constants.Project) { - this.filePathTextBox!.placeHolder = constants.ProjectFilePlaceholder; - - if (this.remoteGitRepoRadioButton!.checked) { - this.formBuilder?.removeFormItem(this.filePathAndButtonComponent); - this.formBuilder?.insertFormItem(this.gitRepoTextBoxComponent, 2); - this.formBuilder?.insertFormItem(this.localClonePathComponent, 3); - } else { - this.formBuilder?.removeFormItem(this.gitRepoTextBoxComponent); - this.formBuilder?.removeFormItem(this.localClonePathComponent); - this.formBuilder?.addFormItem(this.filePathAndButtonComponent!); - } - - this.formBuilder?.addFormItem(this.workspaceDescriptionFormComponent!); - this.formBuilder?.addFormItem(this.workspaceInputFormComponent!); - } else if (cardId === constants.Workspace) { - this.filePathTextBox!.placeHolder = constants.WorkspacePlaceholder; - - this.formBuilder?.removeFormItem(this.workspaceDescriptionFormComponent!); - this.formBuilder?.removeFormItem(this.workspaceInputFormComponent!); - - if (this.remoteGitRepoRadioButton!.checked) { - this.formBuilder?.removeFormItem(this.filePathAndButtonComponent); - this.formBuilder?.insertFormItem(this.gitRepoTextBoxComponent, 2); - this.formBuilder?.insertFormItem(this.localClonePathComponent, 3); - } - } - - // clear selected file textbox - this.filePathTextBox!.value = ''; - })); - - this.createWorkspaceContainer(view); - this.formBuilder = view.modelBuilder.formContainer().withFormItems([ - { - title: constants.TypeTitle, - required: true, - component: this.targetTypeRadioCardGroup, - }, this.locationRadioButtonFormComponent, this.filePathAndButtonComponent, - this.workspaceDescriptionFormComponent!, - this.workspaceInputFormComponent! ]); await view.initializeModel(this.formBuilder?.component()); this.initDialogComplete?.resolve(); } - public async workspaceBrowse(): Promise { - const filters: { [name: string]: string[] } = { [constants.Workspace]: [constants.WorkspaceFileExtension.substring(1)] }; // filter already adds a period before the extension - 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; - } - public async projectBrowse(): Promise { const filters: { [name: string]: string[] } = {}; const projectTypes = await this.workspaceService.getAllProjectTypes(); diff --git a/extensions/data-workspace/src/main.ts b/extensions/data-workspace/src/main.ts index c872169816..8e649d78fb 100644 --- a/extensions/data-workspace/src/main.ts +++ b/extensions/data-workspace/src/main.ts @@ -17,22 +17,12 @@ import { getAzdataApi } from './common/utils'; import { createNewProjectWithQuickpick } from './dialogs/newProjectQuickpick'; export async function activate(context: vscode.ExtensionContext): Promise { - const workspaceService = new WorkspaceService(context); - - // this is not being awaited to not block the rest of activate function from running while loading any temp projects and - // checking for projects not added to the workspace yet - workspaceService.loadTempProjects().then(() => { - return workspaceService.checkForProjectsNotAddedToWorkspace(); - }).catch(error => { - console.error(error); - vscode.window.showErrorMessage(error instanceof Error ? error.message : error); - }); - - context.subscriptions.push(vscode.workspace.onDidChangeWorkspaceFolders(() => { - workspaceService.checkForProjectsNotAddedToWorkspace(); - })); + const workspaceService = new WorkspaceService(); const workspaceTreeDataProvider = new WorkspaceTreeDataProvider(workspaceService); + context.subscriptions.push(vscode.workspace.onDidChangeWorkspaceFolders(() => { + workspaceTreeDataProvider.refresh(); + })); const dataWorkspaceExtension = new DataWorkspaceExtension(workspaceService); context.subscriptions.push(vscode.window.registerTreeDataProvider('dataworkspace.views.main', workspaceTreeDataProvider)); context.subscriptions.push(vscode.extensions.onDidChange(() => { @@ -51,7 +41,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { - const dialog = new OpenExistingDialog(workspaceService, context); + const dialog = new OpenExistingDialog(workspaceService); await dialog.open(); })); @@ -64,9 +54,6 @@ export async function activate(context: vscode.ExtensionContext): Promise { - await workspaceService.removeProject(vscode.Uri.file(treeItem.element.project.projectFilePath)); - })); context.subscriptions.push(vscode.commands.registerCommand('projects.manageProject', async (treeItem: WorkspaceTreeItem) => { const dashboard = new ProjectDashboard(workspaceService, treeItem); await dashboard.showDashboard(); diff --git a/extensions/data-workspace/src/services/workspaceService.ts b/extensions/data-workspace/src/services/workspaceService.ts index 82f9e27b89..a797862dda 100644 --- a/extensions/data-workspace/src/services/workspaceService.ts +++ b/extensions/data-workspace/src/services/workspaceService.ts @@ -12,62 +12,13 @@ import * as glob from 'fast-glob'; import { IWorkspaceService } from '../common/interfaces'; import { ProjectProviderRegistry } from '../common/projectProviderRegistry'; import Logger from '../common/logger'; -import { TelemetryReporter, TelemetryViews, calculateRelativity, TelemetryActions } from '../common/telemetry'; -import { getAzdataApi, isCurrentWorkspaceUntitled } from '../common/utils'; - -const WorkspaceConfigurationName = 'dataworkspace'; -const ProjectsConfigurationName = 'projects'; -const TempProject = 'tempProject'; +import { TelemetryReporter, TelemetryViews, TelemetryActions } from '../common/telemetry'; export class WorkspaceService implements IWorkspaceService { private _onDidWorkspaceProjectsChange: vscode.EventEmitter = new vscode.EventEmitter(); readonly onDidWorkspaceProjectsChange: vscode.Event = this._onDidWorkspaceProjectsChange?.event; - constructor(private _context: vscode.ExtensionContext) { - } - - /** - * Load any temp project that needed to be loaded before ADS was restarted - * which would happen if a workspace was created in order open or create a project - */ - async loadTempProjects(): Promise { - 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(project)]); - } - await this._context.globalState.update(TempProject, undefined); - } - } - - /** - * Creates a new workspace in the same folder as the project. Because ADS gets restarted 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, workspaceFile: vscode.Uri | undefined): Promise { - // create workspace - const projectFolder = vscode.Uri.file(path.dirname(projectFileFsPath)); - const azdataApi = getAzdataApi(); - if (azdataApi) { - // save temp project - await this._context.globalState.update(TempProject, [projectFileFsPath]); - if (isCurrentWorkspaceUntitled()) { - vscode.workspace.updateWorkspaceFolders(vscode.workspace.workspaceFolders!.length, null, { uri: projectFolder }); - await azdataApi.workspace.saveAndEnterWorkspace(workspaceFile!); - } else { - await azdataApi.workspace.createAndEnterWorkspace(projectFolder, workspaceFile); - } - } else { - // In VS Code we don't have access to the workspace APIs exposed by ADS and so can't actually create a new saved workspace. - // Instead we'll just always call this, which will either add it to the existing untitled workspace or create a new - // untitled workspace which the user can then save later on as they wish. - vscode.workspace.updateWorkspaceFolders(vscode.workspace.workspaceFolders?.length || 0, null, { uri: projectFolder }); - } - } + constructor() { } get isProjectProviderAvailable(): boolean { for (const extension of vscode.extensions.all) { @@ -83,8 +34,8 @@ export class WorkspaceService implements IWorkspaceService { * Verify that a workspace is open or that if one isn't, it's ok to create a workspace and restart ADS */ async validateWorkspace(): Promise { - if (!vscode.workspace.workspaceFile || isCurrentWorkspaceUntitled()) { - const result = await vscode.window.showWarningMessage(constants.CreateWorkspaceConfirmation, { modal: true }, constants.OkButtonText); + if (!vscode.workspace.workspaceFolders || vscode.workspace.workspaceFolders.length === 0) { + const result = await vscode.window.showWarningMessage(constants.RestartConfirmation, { modal: true }, constants.OkButtonText); if (result === constants.OkButtonText) { return true; } else { @@ -96,33 +47,12 @@ export class WorkspaceService implements IWorkspaceService { } } - /** - * Shows confirmation message that the ADS 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 { - const result = await vscode.window.showWarningMessage(constants.EnterWorkspaceConfirmation, { modal: true }, constants.OkButtonText); - if (result === constants.OkButtonText) { - await getAzdataApi()?.workspace.enterWorkspace(workspaceFile); - } else { - return; - } - } - - async addProjectsToWorkspace(projectFiles: vscode.Uri[], workspaceFilePath?: vscode.Uri): Promise { + async addProjectsToWorkspace(projectFiles: vscode.Uri[]): Promise { if (!projectFiles || projectFiles.length === 0) { return; } - // a workspace needs to be open to add projects - if (!vscode.workspace.workspaceFile || isCurrentWorkspaceUntitled()) { - await this.CreateNewWorkspaceForProject(projectFiles[0].fsPath, workspaceFilePath); - - // this won't get hit since ADS will get restarted, but helps with testing - return; - } - - const currentProjects: vscode.Uri[] = this.getProjectsInWorkspace(); + const currentProjects: vscode.Uri[] = await this.getProjectsInWorkspace(); const newWorkspaceFolders: string[] = []; let newProjectFileAdded = false; for (const projectFile of projectFiles) { @@ -132,7 +62,6 @@ export class WorkspaceService implements IWorkspaceService { TelemetryReporter.createActionEvent(TelemetryViews.WorkspaceTreePane, TelemetryActions.ProjectAddedToWorkspace) .withAdditionalProperties({ - workspaceProjectRelativity: calculateRelativity(projectFile.fsPath), projectType: path.extname(projectFile.fsPath) }).send(); @@ -148,14 +77,12 @@ export class WorkspaceService implements IWorkspaceService { } 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) })))); + // Add to the end of the workspace folders to avoid a restart of the extension host if we can + vscode.workspace.updateWorkspaceFolders(vscode.workspace.workspaceFolders?.length || 0, undefined, ...(newWorkspaceFolders.map(folder => ({ uri: vscode.Uri.file(folder) })))); } } @@ -168,8 +95,12 @@ export class WorkspaceService implements IWorkspaceService { return projectTypes; } - getProjectsInWorkspace(ext?: string): vscode.Uri[] { - let projects = vscode.workspace.workspaceFile ? this.getWorkspaceConfigurationValue(ProjectsConfigurationName).map(project => this.toUri(project)) : []; + async getProjectsInWorkspace(ext?: string): Promise { + const projectPromises = vscode.workspace.workspaceFolders?.map(f => this.getAllProjectsInFolder(f.uri)); + if (!projectPromises) { + return []; + } + let projects = (await Promise.all(projectPromises)).reduce((prev, curr) => prev.concat(curr), []); // filter by specified extension if (ext) { @@ -179,57 +110,12 @@ export class WorkspaceService implements IWorkspaceService { return projects; } - /** - * Check for projects that are in the workspace folders but have not been added to the workspace through the dialog or by editing the .code-workspace file - */ - async checkForProjectsNotAddedToWorkspace(): Promise { - const config = vscode.workspace.getConfiguration(constants.projectsConfigurationKey); - - // only check if the user hasn't selected not to show this prompt again - if (!config[constants.showNotAddedProjectsMessageKey]) { - return; - } - - // look for any projects that haven't been added to the workspace - const projectsInWorkspace = this.getProjectsInWorkspace(); - const workspaceFolders = vscode.workspace.workspaceFolders; - - if (!workspaceFolders) { - return; - } - - for (const folder of workspaceFolders) { - const results = await this.getAllProjectsInFolder(folder.uri); - - let containsNotAddedProject = false; - for (const projFile of results) { - // if any of the found projects aren't already in the workspace's projects, we can stop checking and show the info message - if (!projectsInWorkspace.find(p => p.fsPath === projFile)) { - containsNotAddedProject = true; - break; - } - } - - if (containsNotAddedProject) { - const result = await vscode.window.showInformationMessage(constants.WorkspaceContainsNotAddedProjects, constants.LaunchOpenExisitingDialog, constants.DoNotAskAgain); - if (result === constants.LaunchOpenExisitingDialog) { - // open settings - await vscode.commands.executeCommand('projects.openExisting'); - } else if (result === constants.DoNotAskAgain) { - await config.update(constants.showNotAddedProjectsMessageKey, false, true); - } - - return; - } - } - } - /** * Returns an array of all the supported projects in the folder * @param folder folder to look look for projects - * @returns array of file paths of supported projects + * @returns array of file URIs for supported projects */ - async getAllProjectsInFolder(folder: vscode.Uri): Promise { + async getAllProjectsInFolder(folder: vscode.Uri): Promise { // get the unique supported project extensions const supportedProjectExtensions = [...new Set((await this.getAllProjectTypes()).map(p => { return p.projectFileExtension; }))]; @@ -241,7 +127,7 @@ export class WorkspaceService implements IWorkspaceService { const projFilter = supportedProjectExtensions.length > 1 ? path.posix.join(escapedPath, '**', `*.{${supportedProjectExtensions.toString()}}`) : path.posix.join(escapedPath, '**', `*.${supportedProjectExtensions[0]}`); // glob will return an array of file paths with forward slashes, so they need to be converted back if on windows - return (await glob(projFilter)).map(p => path.resolve(p)); + return (await glob(projFilter)).map(p => vscode.Uri.file(path.resolve(p))); } async getProjectProvider(projectFile: vscode.Uri): Promise { @@ -253,29 +139,11 @@ export class WorkspaceService implements IWorkspaceService { return ProjectProviderRegistry.getProviderByProjectExtension(projectType); } - async removeProject(projectFile: vscode.Uri): Promise { - if (vscode.workspace.workspaceFile) { - const currentProjects: vscode.Uri[] = this.getProjectsInWorkspace(); - const projectIdx = currentProjects.findIndex((p: vscode.Uri) => p.fsPath === projectFile.fsPath); - if (projectIdx !== -1) { - currentProjects.splice(projectIdx, 1); - - TelemetryReporter.createActionEvent(TelemetryViews.WorkspaceTreePane, TelemetryActions.ProjectRemovedFromWorkspace) - .withAdditionalProperties({ - projectType: path.extname(projectFile.fsPath) - }).send(); - - await this.setWorkspaceConfigurationValue(ProjectsConfigurationName, currentProjects.map(project => this.toRelativePath(project))); - this._onDidWorkspaceProjectsChange.fire(); - } - } - } - async createProject(name: string, location: vscode.Uri, projectTypeId: string, workspaceFile?: vscode.Uri): Promise { const provider = ProjectProviderRegistry.getProviderByProjectType(projectTypeId); if (provider) { const projectFile = await provider.createProject(name, location, projectTypeId); - this.addProjectsToWorkspace([projectFile], workspaceFile); + this.addProjectsToWorkspace([projectFile]); this._onDidWorkspaceProjectsChange.fire(); return projectFile; } else { @@ -283,7 +151,7 @@ export class WorkspaceService implements IWorkspaceService { } } - async gitCloneProject(url: string, localClonePath: string, workspaceFile: vscode.Uri): Promise { + async gitCloneProject(url: string, localClonePath: string): Promise { const gitApi: git.API = (vscode.extensions.getExtension('vscode.git')!.exports).getAPI(1); const opts = { location: vscode.ProgressLocation.Notification, @@ -300,8 +168,8 @@ export class WorkspaceService implements IWorkspaceService { ); // get all the project files in the cloned repo and add them to workspace - const repoProjects = (await this.getAllProjectsInFolder(vscode.Uri.file(repositoryPath))).map(p => { return vscode.Uri.file(p); }); - this.addProjectsToWorkspace(repoProjects, workspaceFile); + const repoProjects = (await this.getAllProjectsInFolder(vscode.Uri.file(repositoryPath))); + this.addProjectsToWorkspace(repoProjects); } catch (e) { vscode.window.showErrorMessage(constants.gitCloneError); console.error(e); @@ -344,29 +212,4 @@ export class WorkspaceService implements IWorkspaceService { ProjectProviderRegistry.registerProvider(extension.exports, extension.id); } } - - getWorkspaceConfigurationValue(configurationName: string): T { - return vscode.workspace.getConfiguration(WorkspaceConfigurationName).get(configurationName) as T; - } - - async setWorkspaceConfigurationValue(configurationName: string, value: any): Promise { - await vscode.workspace.getConfiguration(WorkspaceConfigurationName).update(configurationName, value, vscode.ConfigurationTarget.Workspace); - } - - /** - * Gets the relative path to the workspace file - * @param filePath the absolute path - */ - private toRelativePath(filePath: vscode.Uri): string { - return path.relative(path.dirname(vscode.workspace.workspaceFile!.path!), filePath.path); - } - - /** - * Gets the Uri of the given relative path - * @param relativePath the relative path - */ - private toUri(relativePath: string): vscode.Uri { - const fullPath = path.join(path.dirname(vscode.workspace.workspaceFile!.path!), relativePath); - return vscode.Uri.file(fullPath); - } } diff --git a/extensions/data-workspace/src/test/dialogs/dialogBase.test.ts b/extensions/data-workspace/src/test/dialogs/dialogBase.test.ts deleted file mode 100644 index 86d2bc2a15..0000000000 --- a/extensions/data-workspace/src/test/dialogs/dialogBase.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 sinon from 'sinon'; -import * as utils from '../../common/utils'; -import * as constants from '../../common/constants'; -import { NewProjectDialog } from '../../dialogs/newProjectDialog'; -import { WorkspaceService } from '../../services/workspaceService'; -import { testProjectType } from '../testUtils'; - -suite('DialogBase - workspace validation', function (): void { - // DialogBase is an abstract class, so we'll just use a NewProjectDialog to test the common base class functions - let dialog: NewProjectDialog; - - this.beforeEach(async () => { - const workspaceServiceMock = TypeMoq.Mock.ofType(); - workspaceServiceMock.setup(x => x.getAllProjectTypes()).returns(() => Promise.resolve([testProjectType])); - - dialog = new NewProjectDialog(workspaceServiceMock.object); - await dialog.open(); - - dialog.model.name = `TestProject_${new Date().getTime()}`; - dialog.model.location = os.tmpdir(); - }); - - this.afterEach(() => { - sinon.restore(); - }); - - test('Should validate new workspace location missing file extension', async function (): Promise { - dialog.workspaceInputBox!.value = 'test'; - await should(dialog.validateNewWorkspace(false)).be.rejectedWith(constants.WorkspaceFileInvalidError(dialog.workspaceInputBox!.value)); - }); - - test('Should validate new workspace location with invalid location', async function (): Promise { - // use invalid folder - dialog.workspaceInputBox!.value = 'invalidLocation/test.code-workspace'; - await should(dialog.validateNewWorkspace(false)).be.rejectedWith(constants.WorkspaceParentDirectoryNotExistError(path.dirname(dialog.workspaceInputBox!.value))); - }); - - test('Should validate new workspace location that already exists', async function (): Promise { - // use already existing workspace - const fileExistStub = sinon.stub(utils, 'fileExist'); - fileExistStub.resolves(true); - const existingWorkspaceFilePath = path.join(os.tmpdir(), `${dialog.model.name}.code-workspace`); - dialog.workspaceInputBox!.value = existingWorkspaceFilePath; - await should(dialog.validateNewWorkspace(false)).be.rejectedWith(constants.WorkspaceFileAlreadyExistsError(existingWorkspaceFilePath)); - }); - - test('Should validate new workspace location that is valid', async function (): Promise { - // same folder as the project should be valid even if the project folder isn't created yet - dialog.workspaceInputBox!.value = path.join(dialog.model.location, dialog.model.name, 'test.code-workspace'); - await should(dialog.validateNewWorkspace(true)).not.be.rejected(); - - // a workspace not in the same folder as the project should also be valid - dialog.workspaceInputBox!.value = path.join(os.tmpdir(), `TestWorkspace_${new Date().getTime()}.code-workspace`); - await should(dialog.validateNewWorkspace(false)).not.be.rejected(); - }); -}); - diff --git a/extensions/data-workspace/src/test/dialogs/newProjectDialog.test.ts b/extensions/data-workspace/src/test/dialogs/newProjectDialog.test.ts index ec0c395098..76cfe72c42 100644 --- a/extensions/data-workspace/src/test/dialogs/newProjectDialog.test.ts +++ b/extensions/data-workspace/src/test/dialogs/newProjectDialog.test.ts @@ -7,7 +7,6 @@ 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'; @@ -28,7 +27,6 @@ suite('New Project Dialog', function (): void { dialog.model.name = 'TestProject'; dialog.model.location = ''; - dialog.workspaceInputBox!.value = 'test.code-workspace'; should.equal(await dialog.validate(), false, 'Validation should fail because the parent directory does not exist'); // create a folder with the same name @@ -41,24 +39,5 @@ suite('New Project Dialog', function (): void { 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 { - const workspaceServiceMock = TypeMoq.Mock.ofType(); - 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(); - }); }); diff --git a/extensions/data-workspace/src/test/dialogs/openExistingDialog.test.ts b/extensions/data-workspace/src/test/dialogs/openExistingDialog.test.ts index 0b16c61297..1a7505fa88 100644 --- a/extensions/data-workspace/src/test/dialogs/openExistingDialog.test.ts +++ b/extensions/data-workspace/src/test/dialogs/openExistingDialog.test.ts @@ -12,23 +12,19 @@ import * as constants from '../../common/constants'; import * as utils from '../../common/utils'; import { WorkspaceService } from '../../services/workspaceService'; import { OpenExistingDialog } from '../../dialogs/openExistingDialog'; -import { createProjectFile, generateUniqueProjectFilePath, generateUniqueWorkspaceFilePath, testProjectType } from '../testUtils'; +import { createProjectFile, generateUniqueProjectFilePath, testProjectType } from '../testUtils'; suite('Open Existing Dialog', function (): void { - const mockExtensionContext = TypeMoq.Mock.ofType(); - this.afterEach(() => { sinon.restore(); }); test('Should validate project file exists', async function (): Promise { const workspaceServiceMock = TypeMoq.Mock.ofType(); - const dialog = new OpenExistingDialog(workspaceServiceMock.object, mockExtensionContext.object); + const dialog = new OpenExistingDialog(workspaceServiceMock.object); await dialog.open(); - dialog.targetTypeRadioCardGroup?.updateProperty( 'selectedCardId', constants.Project); dialog.filePathTextBox!.value = 'nonExistentProjectFile'; - dialog.workspaceInputBox!.value = 'test.code-workspace'; const validateResult = await dialog.validate(); @@ -41,32 +37,12 @@ suite('Open Existing Dialog', function (): void { should.equal(await dialog.validate(), true, `Validation should pass because project file exists, but failed with: ${dialog.dialogObject.message.text}`); }); - test('Should validate workspace file exists', async function (): Promise { - const workspaceServiceMock = TypeMoq.Mock.ofType(); - const dialog = new OpenExistingDialog(workspaceServiceMock.object, mockExtensionContext.object); - await dialog.open(); - - dialog.targetTypeRadioCardGroup?.updateProperty( 'selectedCardId', constants.Workspace); - dialog.filePathTextBox!.value = 'nonExistentWorkspaceFile'; - const fileExistStub = sinon.stub(utils, 'fileExist').resolves(false); - - const validateResult = await dialog.validate(); - const msg = constants.FileNotExistError('workspace', 'nonExistentWorkspaceFile'); - should.equal(dialog.dialogObject.message.text, msg); - should.equal(validateResult, false, 'Validation should fail because workspace file does not exist, but passed'); - - // validation should pass if workspace file exists - dialog.filePathTextBox!.value = generateUniqueWorkspaceFilePath(); - fileExistStub.resolves(true); - should.equal(await dialog.validate(), true, `Validation should pass because workspace file exists, but failed with: ${dialog.dialogObject.message.text}`); - }); test('Should validate workspace git clone location', async function (): Promise { const workspaceServiceMock = TypeMoq.Mock.ofType(); - const dialog = new OpenExistingDialog(workspaceServiceMock.object, mockExtensionContext.object); + const dialog = new OpenExistingDialog(workspaceServiceMock.object); await dialog.open(); - dialog.targetTypeRadioCardGroup?.updateProperty( 'selectedCardId', constants.Workspace); dialog.localRadioButton!.checked = false; dialog.remoteGitRepoRadioButton!.checked = true; dialog.localClonePathTextBox!.value = 'invalidLocation'; @@ -74,7 +50,7 @@ suite('Open Existing Dialog', function (): void { const validateResult = await dialog.validate(); const msg = constants.CloneParentDirectoryNotExistError(dialog.localClonePathTextBox!.value); - should.equal(dialog.dialogObject.message.text, msg); + should.equal(dialog.dialogObject.message.text, msg, 'Dialog message should be correct'); should.equal(validateResult, false, 'Validation should fail because clone directory does not exist, but passed'); // validation should pass if directory exists @@ -83,58 +59,22 @@ suite('Open Existing Dialog', function (): void { should.equal(await dialog.validate(), true, `Validation should pass because clone directory exists, but failed with: ${dialog.dialogObject.message.text}`); }); - test('Should validate workspace in onComplete when opening project', async function (): Promise { - const workspaceServiceMock = TypeMoq.Mock.ofType(); - 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.filePathTextBox!.value = 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 { - const workspaceServiceMock = TypeMoq.Mock.ofType(); - sinon.stub(vscode.window, 'showOpenDialog').returns(Promise.resolve([])); - - const dialog = new OpenExistingDialog(workspaceServiceMock.object, mockExtensionContext.object); - await dialog.open(); - should.equal(dialog.filePathTextBox!.value, ''); - await dialog.workspaceBrowse(); - should.equal(dialog.filePathTextBox!.value, '', '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.filePathTextBox!.value, workspaceFile.fsPath, 'Workspace file should get set'); - should.equal(dialog.filePathTextBox?.value, workspaceFile.fsPath); - }); - test('project browse', async function (): Promise { const workspaceServiceMock = TypeMoq.Mock.ofType(); 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); + const dialog = new OpenExistingDialog(workspaceServiceMock.object); await dialog.open(); - should.equal(dialog.filePathTextBox!.value, ''); + should.equal(dialog.filePathTextBox!.value ?? '', '', 'Project file should initially be empty'); await dialog.projectBrowse(); - should.equal(dialog.filePathTextBox!.value, '', 'Project file should not be set when no file is selected'); + should.equal(dialog.filePathTextBox!.value ?? '', '', '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.filePathTextBox!.value, projectFile.fsPath, 'Project file should be set'); - should.equal(dialog.filePathTextBox?.value, projectFile.fsPath); }); }); diff --git a/extensions/data-workspace/src/test/projectProviderRegistry.test.ts b/extensions/data-workspace/src/test/projectProviderRegistry.test.ts index a311abbcc4..b8751bb415 100644 --- a/extensions/data-workspace/src/test/projectProviderRegistry.test.ts +++ b/extensions/data-workspace/src/test/projectProviderRegistry.test.ts @@ -23,9 +23,6 @@ export function createProjectProvider(projectTypes: IProjectType[], projectActio const treeDataProvider = new MockTreeDataProvider(); const projectProvider: IProjectProvider = { supportedProjectTypes: projectTypes, - RemoveProject: (projectFile: vscode.Uri): Promise => { - return Promise.resolve(); - }, getProjectTreeDataProvider: (projectFile: vscode.Uri): Promise> => { return Promise.resolve(treeDataProvider); }, diff --git a/extensions/data-workspace/src/test/testUtils.ts b/extensions/data-workspace/src/test/testUtils.ts index 1c03dfa8cf..4a41a92eda 100644 --- a/extensions/data-workspace/src/test/testUtils.ts +++ b/extensions/data-workspace/src/test/testUtils.ts @@ -31,7 +31,3 @@ export async function createProjectFile(fileExt: string, contents?: string): Pro 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`); -} diff --git a/extensions/data-workspace/src/test/utils.test.ts b/extensions/data-workspace/src/test/utils.test.ts deleted file mode 100644 index ee53f8eab2..0000000000 --- a/extensions/data-workspace/src/test/utils.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import 'mocha'; -import * as os from 'os'; -import * as path from 'path'; -import should = require('should'); -import { calculateRelativity } from '../common/telemetry'; - -suite('Utilities Tests', function (): void { - test('test CalculateRelativity', async () => { - const root = os.platform() === 'win32' ? 'Z:\\' : '/'; - const workspacePath = path.join(root, 'Source', 'Workspace', 'qwerty.code-workspace'); - - should.equal(calculateRelativity(path.join(root, 'Source', 'Workspace', 'qwerty.sqlproj'), workspacePath), 'sameFolder'); - should.equal(calculateRelativity(path.join(root, 'Source', 'Workspace', 'qwerty', 'asdfg', 'qwerty.sqlproj'), workspacePath), 'directAncestor'); - should.equal(calculateRelativity(path.join(root, 'Users', 'BillG', 'qwerty.sqlproj'), workspacePath), 'other'); - }); -}); diff --git a/extensions/data-workspace/src/test/workspaceService.test.ts b/extensions/data-workspace/src/test/workspaceService.test.ts index 601e60f749..0c15ea222d 100644 --- a/extensions/data-workspace/src/test/workspaceService.test.ts +++ b/extensions/data-workspace/src/test/workspaceService.test.ts @@ -5,42 +5,13 @@ 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 * as constants from '../common/constants'; -import * as utils from '../common/utils'; import { WorkspaceService } from '../services/workspaceService'; import { ProjectProviderRegistry } from '../common/projectProviderRegistry'; import { createProjectProvider } from './projectProviderRegistry.test'; -const DefaultWorkspaceFilePath = '/test/folder/ws.code-workspace'; - -/** - * Create a stub for vscode.workspace.workspaceFile - * @param workspaceFilePath The workspace file to return - */ -function stubWorkspaceFile(workspaceFilePath: string | undefined): sinon.SinonStub { - return sinon.stub(vscode.workspace, 'workspaceFile').value(workspaceFilePath ? vscode.Uri.file(workspaceFilePath) : undefined); -} - -/** - * Create a stub for vscode.workspace.getConfiguration - * @param returnValue the configuration value to return - */ -function stubGetConfigurationValue(getStub?: sinon.SinonStub, updateStub?: sinon.SinonStub): sinon.SinonStub { - return sinon.stub(vscode.workspace, 'getConfiguration').returns({ - get: (configurationName: string) => { - return getStub!(configurationName); - }, - update: (section: string, value: any, configurationTarget?: vscode.ConfigurationTarget | boolean, overrideInLanguage?: boolean) => { - updateStub!(section, value, configurationTarget); - } - } as vscode.WorkspaceConfiguration); -} - /** * Create a stub for vscode.extensions.all * @param extensions extensions to return @@ -64,41 +35,29 @@ function createMockExtension(id: string, isActive: boolean, projectTypes: string }; } -interface ExtensionGlobalMemento extends vscode.Memento { - setKeysForSync(keys: string[]): void; -} - -suite('WorkspaceService Tests', function (): void { - const mockExtensionContext = TypeMoq.Mock.ofType(); - const mockGlobalState = TypeMoq.Mock.ofType(); - 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); +suite('WorkspaceService', function (): void { + let service = new WorkspaceService(); this.afterEach(() => { sinon.restore(); }); - test('test getProjectsInWorkspace', async () => { + test('getProjectsInWorkspace', async () => { // No workspace is loaded - stubWorkspaceFile(undefined); let projects = await service.getProjectsInWorkspace(); should.strictEqual(projects.length, 0, 'no projects should be returned when no workspace is loaded'); - // from this point on, workspace is loaded - stubWorkspaceFile(DefaultWorkspaceFilePath); - // No projects are present in the workspace file - const getConfigurationStub = stubGetConfigurationValue(sinon.stub().returns([])); + const workspaceFoldersStub = sinon.stub(vscode.workspace, 'workspaceFolders').value([]); projects = await service.getProjectsInWorkspace(); should.strictEqual(projects.length, 0, 'no projects should be returned when projects are present in the workspace file'); - getConfigurationStub.restore(); + workspaceFoldersStub.restore(); // Projects are present - stubGetConfigurationValue(sinon.stub().returns(['abc.sqlproj', 'folder1/abc1.sqlproj', 'folder2/abc2.sqlproj'])); + sinon.stub(vscode.workspace, 'workspaceFolders').value([{ uri: vscode.Uri.file('')}]); + sinon.stub(service, 'getAllProjectsInFolder').resolves([vscode.Uri.file('/test/folder/abc.sqlproj'), vscode.Uri.file('/test/folder/folder1/abc1.sqlproj'), vscode.Uri.file('/test/folder/folder2/abc2.sqlproj')]); projects = await service.getProjectsInWorkspace(); - should.strictEqual(projects.length, 3, 'there should be 2 projects'); + should.strictEqual(projects.length, 3, 'there should be 3 projects'); const project1 = vscode.Uri.file('/test/folder/abc.sqlproj'); const project2 = vscode.Uri.file('/test/folder/folder1/abc1.sqlproj'); const project3 = vscode.Uri.file('/test/folder/folder2/abc2.sqlproj'); @@ -107,7 +66,7 @@ suite('WorkspaceService Tests', function (): void { should.strictEqual(projects[2].path, project3.path); }); - test('test getAllProjectTypes', async () => { + test('getAllProjectTypes', async () => { // extensions that are already activated const extension1 = createMockExtension('ext1', true, ['csproj']); // with projects contribution const extension2 = createMockExtension('ext2', true, []); // with empty projects contribution @@ -219,7 +178,7 @@ suite('WorkspaceService Tests', function (): void { should.strictEqual(consoleErrorStub.calledOnce, true, 'Logger.error should be called once'); }); - test('test getProjectProvider', async () => { + test('getProjectProvider', async () => { const extension1 = createMockExtension('ext1', true, ['csproj']); const extension2 = createMockExtension('ext2', false, ['sqlproj']); const extension3 = createMockExtension('ext3', false, ['dbproj']); @@ -297,147 +256,64 @@ suite('WorkspaceService Tests', function (): void { should.strictEqual(extension3.activationStub.notCalled, true, 'the ext3.activate() should not have been called for csproj'); }); - test('test addProjectsToWorkspace', async () => { - const processPath = (original: string): string => { - return original.replace(/\//g, path.sep); - }; - stubWorkspaceFile(DefaultWorkspaceFilePath); - const updateConfigurationStub = sinon.stub(); - const getConfigurationStub = sinon.stub().returns([processPath('folder1/proj2.sqlproj')]); + test('addProjectsToWorkspace', async () => { + sinon.stub(service, 'getProjectsInWorkspace').resolves([vscode.Uri.file('folder/folder1/proj2.sqlproj')]); const onWorkspaceProjectsChangedStub = sinon.stub(); const showInformationMessageStub = sinon.stub(vscode.window, 'showInformationMessage'); const onWorkspaceProjectsChangedDisposable = service.onDidWorkspaceProjectsChange(() => { onWorkspaceProjectsChangedStub(); }); - stubGetConfigurationValue(getConfigurationStub, updateConfigurationStub); const asRelativeStub = sinon.stub(vscode.workspace, 'asRelativePath'); sinon.stub(vscode.workspace, 'workspaceFolders').value(['.']); asRelativeStub.onFirstCall().returns(`proj1.sqlproj`); - asRelativeStub.onSecondCall().returns(processPath('/test/other/proj3.sqlproj')); + asRelativeStub.onSecondCall().returns('other/proj3.sqlproj'); const updateWorkspaceFoldersStub = sinon.stub(vscode.workspace, 'updateWorkspaceFolders'); await service.addProjectsToWorkspace([ - vscode.Uri.file('/test/folder/proj1.sqlproj'), // within the workspace folder - vscode.Uri.file('/test/folder/folder1/proj2.sqlproj'), //already exists - vscode.Uri.file('/test/other/proj3.sqlproj') // outside of workspace folder + vscode.Uri.file('folder/proj1.sqlproj'), // within the workspace folder + vscode.Uri.file('folder/folder1/proj2.sqlproj'), //already exists + vscode.Uri.file('other/proj3.sqlproj') // new workspace folder ]); - should.strictEqual(updateConfigurationStub.calledOnce, true, 'update configuration should have been called once'); should.strictEqual(updateWorkspaceFoldersStub.calledOnce, true, 'updateWorkspaceFolders should have been called once'); should.strictEqual(showInformationMessageStub.calledOnce, true, 'showInformationMessage should be called once'); - should(showInformationMessageStub.calledWith(constants.ProjectAlreadyOpened(processPath('/test/folder/folder1/proj2.sqlproj')))).be.true(`showInformationMessage not called with expected message '${constants.ProjectAlreadyOpened(processPath('/test/folder/folder1/proj2.sqlproj'))}' Actual '${showInformationMessageStub.getCall(0).args[0]}'`); - should.strictEqual(updateConfigurationStub.calledWith('projects', sinon.match.array.deepEquals([ - processPath('folder1/proj2.sqlproj'), - processPath('proj1.sqlproj'), - processPath('../other/proj3.sqlproj') - ]), vscode.ConfigurationTarget.Workspace), true, 'updateConfiguration parameters does not match expectation'); - should.strictEqual(updateWorkspaceFoldersStub.calledWith(1, null, sinon.match((arg) => { - return arg.uri.path === '/test/other'; + const expectedProjPath = vscode.Uri.file('folder/folder1/proj2.sqlproj').fsPath; + should(showInformationMessageStub.calledWith(constants.ProjectAlreadyOpened(expectedProjPath))).be.true(`showInformationMessage not called with expected message '${constants.ProjectAlreadyOpened(expectedProjPath)}' Actual '${showInformationMessageStub.getCall(0).args[0]}'`); + should.strictEqual(updateWorkspaceFoldersStub.calledWith(1, undefined, sinon.match((arg) => { + return arg.uri.path === vscode.Uri.file('other').path; })), true, 'updateWorkspaceFolder parameters does not match expectation'); should.strictEqual(onWorkspaceProjectsChangedStub.calledOnce, true, 'the onDidWorkspaceProjectsChange event should have been fired'); onWorkspaceProjectsChangedDisposable.dispose(); }); - test('test addProjectsToWorkspace when no workspace open', async () => { - stubWorkspaceFile(undefined); + test('addProjectsToWorkspace when no workspace open', async () => { const onWorkspaceProjectsChangedStub = sinon.stub(); const onWorkspaceProjectsChangedDisposable = service.onDidWorkspaceProjectsChange(() => { onWorkspaceProjectsChangedStub(); }); - const createWorkspaceStub = sinon.stub(azdata.workspace, 'createAndEnterWorkspace').resolves(undefined); + const updateWorkspaceFoldersStub = sinon.stub(vscode.workspace, 'updateWorkspaceFolders').returns(true); await service.addProjectsToWorkspace([ vscode.Uri.file('/test/folder/proj1.sqlproj') ]); - should.strictEqual(createWorkspaceStub.calledOnce, true, 'createAndEnterWorkspace should have been called once'); - should.strictEqual(onWorkspaceProjectsChangedStub.notCalled, true, 'the onDidWorkspaceProjectsChange event should not have been fired'); + should.strictEqual(onWorkspaceProjectsChangedStub.calledOnce, true, 'the onDidWorkspaceProjectsChange event should have been fired'); + should.strictEqual(updateWorkspaceFoldersStub.calledOnce, true, 'updateWorkspaceFolders should have been called'); onWorkspaceProjectsChangedDisposable.dispose(); }); - test('test addProjectsToWorkspace when untitled workspace is open', async () => { - stubWorkspaceFile(undefined); + test('addProjectsToWorkspace when untitled workspace is open', async () => { + sinon.stub(service, 'getProjectsInWorkspace').resolves([]); const onWorkspaceProjectsChangedStub = sinon.stub(); const onWorkspaceProjectsChangedDisposable = service.onDidWorkspaceProjectsChange(() => { onWorkspaceProjectsChangedStub(); }); - const saveWorkspaceStub = sinon.stub(azdata.workspace, 'saveAndEnterWorkspace').resolves(undefined); - sinon.stub(utils, 'isCurrentWorkspaceUntitled').returns(true); - sinon.stub(vscode.workspace, 'workspaceFolders').value(['folder1']); - + sinon.replaceGetter(vscode.workspace, 'workspaceFolders', () => [{ uri: vscode.Uri.file('folder1'), name: '', index: 0}]); + const updateWorkspaceFoldersStub = sinon.stub(vscode.workspace, 'updateWorkspaceFolders').returns(true); await service.addProjectsToWorkspace([ vscode.Uri.file('/test/folder/proj1.sqlproj') ]); - should.strictEqual(saveWorkspaceStub.calledOnce, true, 'saveAndEnterWorkspace 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, 'createAndEnterWorkspace').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'); + should.strictEqual(updateWorkspaceFoldersStub.calledOnce, true, 'updateWorkspaceFolders should have been called'); onWorkspaceProjectsChangedDisposable.dispose(); }); - - test('test removeProject', async () => { - const processPath = (original: string): string => { - return original.replace(/\//g, path.sep); - }; - stubWorkspaceFile(DefaultWorkspaceFilePath); - const updateConfigurationStub = sinon.stub(); - const getConfigurationStub = sinon.stub().returns([processPath('folder1/proj2.sqlproj'), processPath('folder2/proj3.sqlproj')]); - const onWorkspaceProjectsChangedStub = sinon.stub(); - const onWorkspaceProjectsChangedDisposable = service.onDidWorkspaceProjectsChange(() => { - onWorkspaceProjectsChangedStub(); - }); - stubGetConfigurationValue(getConfigurationStub, updateConfigurationStub); - await service.removeProject(vscode.Uri.file('/test/folder/folder1/proj2.sqlproj')); - should.strictEqual(updateConfigurationStub.calledWith('projects', sinon.match.array.deepEquals([ - processPath('folder2/proj3.sqlproj') - ]), vscode.ConfigurationTarget.Workspace), true, 'updateConfiguration parameters does not match expectation for remove project'); - should.strictEqual(onWorkspaceProjectsChangedStub.calledOnce, true, 'the onDidWorkspaceProjectsChange event should have been fired'); - onWorkspaceProjectsChangedDisposable.dispose(); - }); - - test('test checkForProjectsNotAddedToWorkspace', async () => { - const previousSetting = await vscode.workspace.getConfiguration(constants.projectsConfigurationKey)[constants.showNotAddedProjectsMessageKey]; - await vscode.workspace.getConfiguration(constants.projectsConfigurationKey).update(constants.showNotAddedProjectsMessageKey, true, true); - - sinon.stub(service, 'getProjectsInWorkspace').returns([vscode.Uri.file('abc.sqlproj'), vscode.Uri.file('folder1/abc1.sqlproj')]); - sinon.stub(vscode.workspace, 'workspaceFolders').value([{ uri: vscode.Uri.file('.') }]); - sinon.stub(service, 'getAllProjectTypes').resolves([{ - projectFileExtension: 'sqlproj', - id: 'sql project', - displayName: 'sql project', - description: '', - icon: '' - }]); - const infoMessageStub = sinon.stub(vscode.window, 'showInformationMessage').resolves(constants.DoNotAskAgain); - const getProjectsInwWorkspaceFolderStub = sinon.stub(service, 'getAllProjectsInFolder').resolves([vscode.Uri.file('abc.sqlproj').fsPath, vscode.Uri.file('folder1/abc1.sqlproj').fsPath]); - - await service.checkForProjectsNotAddedToWorkspace(); - should(infoMessageStub.notCalled).be.true('Should not have found projects not added to workspace'); - - // add a project to the workspace folder not added to the workspace yet - getProjectsInwWorkspaceFolderStub.resolves([vscode.Uri.file('abc.sqlproj').fsPath, vscode.Uri.file('folder1/abc1.sqlproj').fsPath, vscode.Uri.file('folder2/abc2.sqlproj').fsPath]); - await service.checkForProjectsNotAddedToWorkspace(); - should(infoMessageStub.calledOnce).be.true('Should have found a project that was not added to the workspace'); - - await vscode.workspace.getConfiguration(constants.projectsConfigurationKey).update(constants.showNotAddedProjectsMessageKey, previousSetting, true); - }); }); diff --git a/extensions/data-workspace/src/test/workspaceTreeDataProvider.test.ts b/extensions/data-workspace/src/test/workspaceTreeDataProvider.test.ts index 9c9f82fe6f..018c38b74d 100644 --- a/extensions/data-workspace/src/test/workspaceTreeDataProvider.test.ts +++ b/extensions/data-workspace/src/test/workspaceTreeDataProvider.test.ts @@ -7,23 +7,13 @@ import { IDashboardTable, IProjectProvider, WorkspaceTreeItem } from 'dataworksp import 'mocha'; import * as should from 'should'; import * as sinon from 'sinon'; -import * as TypeMoq from 'typemoq'; import * as vscode from 'vscode'; import { WorkspaceTreeDataProvider } from '../common/workspaceTreeDataProvider'; import { WorkspaceService } from '../services/workspaceService'; import { MockTreeDataProvider } from './projectProviderRegistry.test'; -interface ExtensionGlobalMemento extends vscode.Memento { - setKeysForSync(keys: string[]): void; -} - suite('workspaceTreeDataProvider Tests', function (): void { - const mockExtensionContext = TypeMoq.Mock.ofType(); - const mockGlobalState = TypeMoq.Mock.ofType(); - 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 workspaceService = new WorkspaceService(); const treeProvider = new WorkspaceTreeDataProvider(workspaceService); this.afterEach(() => { @@ -63,7 +53,7 @@ suite('workspaceTreeDataProvider Tests', function (): void { }; const children = await treeProvider.getChildren(element); should.strictEqual(children.length, 0, 'children count should be 0'); - should.strictEqual(getChildrenStub.calledWithExactly('obj1'), true, 'getChildren parameter should be obj1') + should.strictEqual(getChildrenStub.calledWithExactly('obj1'), true, 'getChildren parameter should be obj1'); }); test('test getChildren() for root element', async () => { @@ -82,9 +72,6 @@ suite('workspaceTreeDataProvider Tests', function (): void { displayName: 'sql project', description: '' }], - RemoveProject: (projectFile: vscode.Uri): Promise => { - return Promise.resolve(); - }, getProjectTreeDataProvider: (projectFile: vscode.Uri): Promise> => { return Promise.resolve(treeDataProvider); }, diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index a4f75a738c..41fefac963 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -153,16 +153,9 @@ export const projectLocationLabel = localize('projectLocationLabel', "Location") export const projectLocationPlaceholderText = localize('projectLocationPlaceholderText', "Select location to create project"); export const browseButtonText = localize('browseButtonText', "Browse folder"); export const folderStructureLabel = localize('folderStructureLabel', "Folder structure"); -export const addProjectToCurrentWorkspace = localize('addProjectToCurrentWorkspace', "This project will be added to the current workspace."); -export const newWorkspaceWillBeCreated = localize('newWorkspaceWillBeCreated', "A new workspace will be created for this project."); -export const workspaceLocationTitle = localize('workspaceLocationTitle', "Workspace location"); -export const workspace = localize('workspace', "Workspace"); export const WorkspaceFileExtension = '.code-workspace'; export const ProjectParentDirectoryNotExistError = (location: string): string => { return localize('dataworkspace.projectParentDirectoryNotExistError', "The selected project location '{0}' does not exist or is not a directory.", location); }; export const ProjectDirectoryAlreadyExistError = (projectName: string, location: string): string => { return localize('dataworkspace.projectDirectoryAlreadyExistError', "There is already a directory named '{0}' in the selected location: '{1}'.", projectName, location); }; -export const WorkspaceFileInvalidError = (workspace: string): string => { return localize('dataworkspace.workspaceFileInvalidError', "The selected workspace file path '{0}' does not have the required file extension {1}.", workspace, WorkspaceFileExtension); }; -export const WorkspaceParentDirectoryNotExistError = (location: string): string => { return localize('dataworkspace.workspaceParentDirectoryNotExistError', "The selected workspace location '{0}' does not exist or is not a directory.", location); }; -export const WorkspaceFileAlreadyExistsError = (file: string): string => { return localize('dataworkspace.workspaceFileAlreadyExistsError', "The selected workspace file '{0}' already exists. To add the project to an existing workspace, use the Open Existing dialog to first open the workspace.", file); }; // Error messages diff --git a/extensions/sql-database-projects/src/common/utils.ts b/extensions/sql-database-projects/src/common/utils.ts index 52786b66d5..88afe8e271 100644 --- a/extensions/sql-database-projects/src/common/utils.ts +++ b/extensions/sql-database-projects/src/common/utils.ts @@ -241,7 +241,7 @@ export async function getSqlProjectFilesInFolder(folderPath: string): Promise { const api = getDataWorkspaceExtensionApi(); return api.getProjectsInWorkspace(constants.sqlprojExtension); } @@ -251,13 +251,6 @@ export function getDataWorkspaceExtensionApi(): dataworkspace.IExtension { return extension.exports; } -/** - * if the current workspace is untitled, the returned URI of vscode.workspace.workspaceFile will use the `untitled` scheme - */ -export function isCurrentWorkspaceUntitled(): boolean { - return !!vscode.workspace.workspaceFile && vscode.workspace.workspaceFile.scheme.toLowerCase() === 'untitled'; -} - /* * Returns the default deployment options from DacFx */ diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index 4977672281..acc6e3e215 100644 --- a/extensions/sql-database-projects/src/controllers/projectController.ts +++ b/extensions/sql-database-projects/src/controllers/projectController.ts @@ -664,7 +664,7 @@ export class ProjectsController { if ((settings).projectName !== undefined) { // get project path and guid const projectReferenceSettings = settings as IProjectReferenceSettings; - const workspaceProjects = utils.getSqlProjectsInWorkspace(); + const workspaceProjects = await utils.getSqlProjectsInWorkspace(); const referencedProject = await Project.openProject(workspaceProjects.filter(p => path.parse(p.fsPath).name === projectReferenceSettings.projectName)[0].fsPath); const relativePath = path.relative(project.projectFolderPath, referencedProject?.projectFilePath!); projectReferenceSettings.projectRelativePath = vscode.Uri.file(relativePath); @@ -882,7 +882,7 @@ export class ProjectsController { // add project to workspace workspaceApi.showProjectsView(); - await workspaceApi.addProjectsToWorkspace([vscode.Uri.file(newProjFilePath)], model.newWorkspaceFilePath); + await workspaceApi.addProjectsToWorkspace([vscode.Uri.file(newProjFilePath)]); } } catch (err) { vscode.window.showErrorMessage(utils.getErrorMessage(err)); diff --git a/extensions/sql-database-projects/src/dialogs/addDatabaseReferenceDialog.ts b/extensions/sql-database-projects/src/dialogs/addDatabaseReferenceDialog.ts index 8e4a8f0d66..5538f54cf0 100644 --- a/extensions/sql-database-projects/src/dialogs/addDatabaseReferenceDialog.ts +++ b/extensions/sql-database-projects/src/dialogs/addDatabaseReferenceDialog.ts @@ -293,7 +293,7 @@ export class AddDatabaseReferenceDialog { }); // get projects in workspace and filter to only sql projects - let projectFiles: vscode.Uri[] = utils.getSqlProjectsInWorkspace(); + let projectFiles: vscode.Uri[] = await utils.getSqlProjectsInWorkspace(); // filter out current project projectFiles = projectFiles.filter(p => p.fsPath !== this.project.projectFilePath); diff --git a/extensions/sql-database-projects/src/dialogs/createProjectFromDatabaseDialog.ts b/extensions/sql-database-projects/src/dialogs/createProjectFromDatabaseDialog.ts index 1f8cfe8a41..73a33a6ebb 100644 --- a/extensions/sql-database-projects/src/dialogs/createProjectFromDatabaseDialog.ts +++ b/extensions/sql-database-projects/src/dialogs/createProjectFromDatabaseDialog.ts @@ -15,7 +15,7 @@ import { cssStyles } from '../common/uiConstants'; import { ImportDataModel } from '../models/api/import'; import { Deferred } from '../common/promise'; import { getConnectionName } from './utils'; -import { exists, getAzdataApi, isCurrentWorkspaceUntitled } from '../common/utils'; +import { exists, getAzdataApi, getDataWorkspaceExtensionApi } from '../common/utils'; export class CreateProjectFromDatabaseDialog { public dialog: azdataType.window.Dialog; @@ -26,7 +26,6 @@ export class CreateProjectFromDatabaseDialog { public projectNameTextBox: azdataType.InputBoxComponent | undefined; public projectLocationTextBox: azdataType.InputBoxComponent | undefined; public folderStructureDropDown: azdataType.DropDownComponent | undefined; - public workspaceInputBox: azdataType.InputBoxComponent | undefined; private formBuilder: azdataType.FormBuilder | undefined; private connectionId: string | undefined; private toDispose: vscode.Disposable[] = []; @@ -87,10 +86,6 @@ export class CreateProjectFromDatabaseDialog { const createProjectSettingsFormSection = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); createProjectSettingsFormSection.addItems([folderStructureRow]); - const workspaceContainerRow = this.createWorkspaceContainerRow(view); - const createworkspaceContainerFormSection = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); - createworkspaceContainerFormSection.addItems([workspaceContainerRow]); - this.formBuilder = view.modelBuilder.formContainer() .withFormItems([ { @@ -116,14 +111,6 @@ export class CreateProjectFromDatabaseDialog { component: createProjectSettingsFormSection, } ] - }, - { - title: constants.workspace, - components: [ - { - component: createworkspaceContainerFormSection, - } - ] } ], { horizontal: false, @@ -166,7 +153,6 @@ export class CreateProjectFromDatabaseDialog { this.sourceDatabaseDropDown.onValueChanged(() => { this.setProjectName(); - this.updateWorkspaceInputbox(path.join(this.projectLocationTextBox!.value!, this.projectNameTextBox!.value!), this.projectNameTextBox!.value!); this.tryEnableCreateButton(); }); @@ -264,7 +250,6 @@ export class CreateProjectFromDatabaseDialog { this.projectNameTextBox.onTextChanged(() => { this.projectNameTextBox!.value = this.projectNameTextBox!.value?.trim(); this.projectNameTextBox!.updateProperty('title', this.projectNameTextBox!.value); - this.updateWorkspaceInputbox(path.join(this.projectLocationTextBox!.value!, this.projectNameTextBox!.value!), this.projectNameTextBox!.value!); this.tryEnableCreateButton(); }); @@ -291,7 +276,6 @@ export class CreateProjectFromDatabaseDialog { this.projectLocationTextBox.onTextChanged(() => { this.projectLocationTextBox!.updateProperty('title', this.projectLocationTextBox!.value); - this.updateWorkspaceInputbox(path.join(this.projectLocationTextBox!.value!, this.projectNameTextBox!.value!), this.projectNameTextBox!.value!); this.tryEnableCreateButton(); }); @@ -329,7 +313,6 @@ export class CreateProjectFromDatabaseDialog { this.projectLocationTextBox!.value = folderUris[0].fsPath; this.projectLocationTextBox!.updateProperty('title', folderUris[0].fsPath); - this.updateWorkspaceInputbox(path.join(this.projectLocationTextBox!.value!, this.projectNameTextBox!.value!), this.projectNameTextBox!.value!); }); return browseFolderButton; @@ -359,80 +342,6 @@ export class CreateProjectFromDatabaseDialog { return folderStructureRow; } - /** - * Creates container with information on which workspace the project will be added to and where the workspace will be - * created if no workspace is currently open - * @param view - */ - private createWorkspaceContainerRow(view: azdataType.ModelView): azdataType.FlexContainer { - const initialWorkspaceInputBoxValue = !!vscode.workspace.workspaceFile && !isCurrentWorkspaceUntitled() ? vscode.workspace.workspaceFile.fsPath : ''; - - this.workspaceInputBox = view.modelBuilder.inputBox().withProps({ - ariaLabel: constants.workspaceLocationTitle, - enabled: !vscode.workspace.workspaceFile || isCurrentWorkspaceUntitled(), // want it editable if no saved workspace is open - value: initialWorkspaceInputBoxValue, - title: initialWorkspaceInputBoxValue, // hovertext for if file path is too long to be seen in textbox - width: '100%' - }).component(); - - const browseFolderButton = view.modelBuilder.button().withProperties({ - ariaLabel: constants.browseButtonText, - iconPath: IconPathHelper.folder_blue, - height: '16px', - width: '18px' - }).component(); - - this.toDispose.push(browseFolderButton.onDidClick(async () => { - const currentFileName = path.parse(this.workspaceInputBox!.value!).base; - - // let user select folder for workspace file to be created in - const folderUris = await vscode.window.showOpenDialog({ - canSelectFiles: false, - canSelectFolders: true, - canSelectMany: false, - defaultUri: vscode.Uri.file(path.parse(this.workspaceInputBox!.value!).dir) - }); - if (!folderUris || folderUris.length === 0) { - return; - } - const selectedFolder = folderUris[0].fsPath; - - const selectedFile = path.join(selectedFolder, currentFileName); - this.workspaceInputBox!.value = selectedFile; - this.workspaceInputBox!.title = selectedFile; - })); - - const workspaceLabel = view.modelBuilder.text().withProperties({ - value: vscode.workspace.workspaceFile ? constants.addProjectToCurrentWorkspace : constants.newWorkspaceWillBeCreated, - CSSStyles: { 'margin-top': '-10px', 'margin-bottom': '5px' } - }).component(); - - let workspaceContainerRow; - if (vscode.workspace.workspaceFile && !isCurrentWorkspaceUntitled()) { - workspaceContainerRow = view.modelBuilder.flexContainer().withItems([workspaceLabel, this.workspaceInputBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px', 'margin-top': '0px' } }).withLayout({ flexFlow: 'column' }).component(); - } else { - // have browse button to help select where the workspace file should be created - const workspaceInput = view.modelBuilder.flexContainer().withItems([this.workspaceInputBox], { CSSStyles: { 'margin-right': '10px', 'margin-bottom': '10px', 'width': '100%' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); - workspaceInput.addItem(browseFolderButton, { CSSStyles: { 'margin-top': '-10px' } }); - workspaceContainerRow = view.modelBuilder.flexContainer().withItems([workspaceLabel, workspaceInput], { flex: '0 0 auto', CSSStyles: { 'margin-top': '0px' } }).withLayout({ flexFlow: 'column' }).component(); - } - - return workspaceContainerRow; - } - - /** - * Update the workspace inputbox based on the passed in location and name if there isn't a workspace currently open - * @param location - * @param name - */ - public updateWorkspaceInputbox(location: string, name: string): void { - if (!vscode.workspace.workspaceFile || isCurrentWorkspaceUntitled()) { - const fileLocation = location && name ? path.join(location, `${name}.code-workspace`) : ''; - this.workspaceInputBox!.value = fileLocation; - this.workspaceInputBox!.title = fileLocation; - } - } - // only enable Create button if all fields are filled public tryEnableCreateButton(): void { if (this.sourceConnectionTextBox!.value && this.sourceDatabaseDropDown!.value @@ -450,8 +359,7 @@ export class CreateProjectFromDatabaseDialog { projName: this.projectNameTextBox!.value!, filePath: this.projectLocationTextBox!.value!, version: '1.0.0.0', - extractTarget: this.mapExtractTargetEnum(this.folderStructureDropDown!.value), - newWorkspaceFilePath: this.workspaceInputBox!.enabled ? vscode.Uri.file(this.workspaceInputBox!.value!) : undefined + extractTarget: this.mapExtractTargetEnum(this.folderStructureDropDown!.value) }; getAzdataApi()!.window.closeDialog(this.dialog); @@ -477,6 +385,9 @@ export class CreateProjectFromDatabaseDialog { async validate(): Promise { try { + if (await getDataWorkspaceExtensionApi().validateWorkspace() === false) { + return false; + } // the selected location should be an existing directory const parentDirectoryExists = await exists(this.projectLocationTextBox!.value!); if (!parentDirectoryExists) { @@ -490,11 +401,6 @@ export class CreateProjectFromDatabaseDialog { this.showErrorMessage(constants.ProjectDirectoryAlreadyExistError(this.projectNameTextBox!.value!, this.projectLocationTextBox!.value!)); return false; } - - if (this.workspaceInputBox!.enabled) { - await this.validateNewWorkspace(); - } - return true; } catch (err) { this.showErrorMessage(err?.message ? err.message : err); @@ -502,30 +408,6 @@ export class CreateProjectFromDatabaseDialog { } } - protected async validateNewWorkspace(): Promise { - const sameFolderAsNewProject = path.join(this.projectLocationTextBox!.value!, this.projectNameTextBox!.value!) === path.dirname(this.workspaceInputBox!.value!); - - // workspace file should end in .code-workspace - const workspaceValid = this.workspaceInputBox!.value!.endsWith(constants.WorkspaceFileExtension); - if (!workspaceValid) { - throw new Error(constants.WorkspaceFileInvalidError(this.workspaceInputBox!.value!)); - } - - // if the workspace file is not going to be in the same folder as the newly created project, then check that it's a valid folder - if (!sameFolderAsNewProject) { - const workspaceParentDirectoryExists = await exists(path.dirname(this.workspaceInputBox!.value!)); - if (!workspaceParentDirectoryExists) { - throw new Error(constants.WorkspaceParentDirectoryNotExistError(path.dirname(this.workspaceInputBox!.value!))); - } - } - - // workspace file should not be an existing workspace file - const workspaceFileExists = await exists(this.workspaceInputBox!.value!); - if (workspaceFileExists) { - throw new Error(constants.WorkspaceFileAlreadyExistsError(this.workspaceInputBox!.value!)); - } - } - protected showErrorMessage(message: string): void { this.dialog.message = { text: message, diff --git a/extensions/sql-database-projects/src/models/api/import.ts b/extensions/sql-database-projects/src/models/api/import.ts index 63a60ffd01..476d522604 100644 --- a/extensions/sql-database-projects/src/models/api/import.ts +++ b/extensions/sql-database-projects/src/models/api/import.ts @@ -3,7 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Uri } from 'vscode'; import { ExtractTarget } from '../../../../mssql'; /** @@ -16,5 +15,4 @@ export interface ImportDataModel { filePath: string; version: string; extractTarget: ExtractTarget; - newWorkspaceFilePath?: Uri; } diff --git a/extensions/sql-database-projects/src/projectProvider/projectProvider.ts b/extensions/sql-database-projects/src/projectProvider/projectProvider.ts index 95e2f15410..d7469461ba 100644 --- a/extensions/sql-database-projects/src/projectProvider/projectProvider.ts +++ b/extensions/sql-database-projects/src/projectProvider/projectProvider.ts @@ -30,16 +30,6 @@ export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvide return provider; } - /** - * Callback method when a project has been removed from the workspace view - * @param projectFile The Uri of the project file - */ - RemoveProject(projectFile: vscode.Uri): Promise { - // No resource release needed - console.log(`project file unloaded: ${projectFile.fsPath}`); - return Promise.resolve(); - } - /** * Gets the supported project types */ diff --git a/extensions/sql-database-projects/src/test/dialogs/addDatabaseReferenceDialog.test.ts b/extensions/sql-database-projects/src/test/dialogs/addDatabaseReferenceDialog.test.ts index f68bcae4db..82336f8acb 100644 --- a/extensions/sql-database-projects/src/test/dialogs/addDatabaseReferenceDialog.test.ts +++ b/extensions/sql-database-projects/src/test/dialogs/addDatabaseReferenceDialog.test.ts @@ -23,7 +23,7 @@ describe('Add Database Reference Dialog', () => { beforeEach(function (): void { const dataWorkspaceMock = TypeMoq.Mock.ofType(); - dataWorkspaceMock.setup(x => x.getProjectsInWorkspace(TypeMoq.It.isAny())).returns(() => []); + dataWorkspaceMock.setup(x => x.getProjectsInWorkspace(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); sinon.stub(vscode.extensions, 'getExtension').returns({ exports: dataWorkspaceMock.object }); }); diff --git a/extensions/sql-database-projects/src/test/dialogs/createProjectFromDatabaseDialog.test.ts b/extensions/sql-database-projects/src/test/dialogs/createProjectFromDatabaseDialog.test.ts index a4b970c578..b3f03235da 100644 --- a/extensions/sql-database-projects/src/test/dialogs/createProjectFromDatabaseDialog.test.ts +++ b/extensions/sql-database-projects/src/test/dialogs/createProjectFromDatabaseDialog.test.ts @@ -7,7 +7,6 @@ import * as should from 'should'; import * as azdata from 'azdata'; import * as mssql from '../../../../mssql'; import * as sinon from 'sinon'; -import * as path from 'path'; import { CreateProjectFromDatabaseDialog } from '../../dialogs/createProjectFromDatabaseDialog'; import { mockConnectionProfile } from '../testContext'; import { ImportDataModel } from '../../models/api/import'; @@ -83,22 +82,11 @@ describe('Create Project From Database Dialog', () => { should.equal(dialog.projectNameTextBox!.value, 'DatabaseProjectMy Database'); }); - it('Should update default workspace name correctly when location and project name are provided', async function (): Promise { - sinon.stub(azdata.connection, 'listDatabases').resolves(['My Database']); - const dialog = new CreateProjectFromDatabaseDialog(mockConnectionProfile); - await dialog.openDialog(); - dialog.updateWorkspaceInputbox('testLocation', 'testProjectName'); - - should.equal(dialog.workspaceInputBox!.value, path.join('testLocation', 'testProjectName.code-workspace')); - }); - it('Should include all info in import data model and connect to appropriate call back properties', async function (): Promise { const dialog = new CreateProjectFromDatabaseDialog(mockConnectionProfile); sinon.stub(azdata.connection, 'listDatabases').resolves(['My Database']); await dialog.openDialog(); - dialog.workspaceInputBox!.enabled = false; - dialog.projectNameTextBox!.value = 'testProject'; dialog.projectLocationTextBox!.value = 'testLocation'; @@ -110,8 +98,7 @@ describe('Create Project From Database Dialog', () => { projName: 'testProject', filePath: 'testLocation', version: '1.0.0.0', - extractTarget: mssql.ExtractTarget['schemaObjectType'], - newWorkspaceFilePath: undefined + extractTarget: mssql.ExtractTarget['schemaObjectType'] }; dialog.createProjectFromDatabaseCallback = (m) => { model = m; }; diff --git a/extensions/sql-database-projects/src/test/projectController.test.ts b/extensions/sql-database-projects/src/test/projectController.test.ts index 45866d752f..e62e2ca78b 100644 --- a/extensions/sql-database-projects/src/test/projectController.test.ts +++ b/extensions/sql-database-projects/src/test/projectController.test.ts @@ -579,7 +579,7 @@ describe('ProjectsController', function (): void { const project2 = await Project.openProject(vscode.Uri.file(projPath2).fsPath); const showErrorMessageSpy = sinon.spy(vscode.window, 'showErrorMessage'); const dataWorkspaceMock = TypeMoq.Mock.ofType(); - dataWorkspaceMock.setup(x => x.getProjectsInWorkspace(TypeMoq.It.isAny())).returns(() => [vscode.Uri.file(project1.projectFilePath), vscode.Uri.file(project2.projectFilePath)]); + dataWorkspaceMock.setup(x => x.getProjectsInWorkspace(TypeMoq.It.isAny())).returns(() => Promise.resolve([vscode.Uri.file(project1.projectFilePath), vscode.Uri.file(project2.projectFilePath)])); sinon.stub(vscode.extensions, 'getExtension').returns({ exports: dataWorkspaceMock.object }); // add project reference from project1 to project2