From 29ff6ca16c5d98ce16e1cf761bc31a88dc09a902 Mon Sep 17 00:00:00 2001 From: Benjin Dubishar Date: Fri, 28 Apr 2023 16:05:38 -0700 Subject: [PATCH] Adding Move, Exclude, and Rename support for folders (#22867) * Adding exclude folder and base for move folder * checkpoint * rename * Fixing up tests * Adding exclude test to projController * Adding tests * fixing order of service.moveX() calls * Updating move() order in sqlproj service * PR feedback * unskipping * reskipping test * Fixing folder move conditional * updating comments --- extensions/mssql/src/mssql.d.ts | 22 +++--- .../src/sqlProjects/sqlProjectsService.ts | 24 +++--- extensions/sql-database-projects/package.json | 4 +- .../src/common/constants.ts | 2 +- .../src/controllers/projectController.ts | 75 ++++++------------ .../src/models/project.ts | 44 +++++++++-- .../src/test/project.test.ts | 79 +++++-------------- .../src/test/projectController.test.ts | 64 ++++++++++++--- extensions/types/vscode-mssql.d.ts | 22 +++--- 9 files changed, 175 insertions(+), 161 deletions(-) diff --git a/extensions/mssql/src/mssql.d.ts b/extensions/mssql/src/mssql.d.ts index 43ed493bc0..034ac53554 100644 --- a/extensions/mssql/src/mssql.d.ts +++ b/extensions/mssql/src/mssql.d.ts @@ -396,10 +396,10 @@ declare module 'mssql' { /** * Move a folder and its contents within a project * @param projectUri Absolute path of the project, including .sqlproj - * @param destinationPath Path of the folder, typically relative to the .sqlproj file - * @param path Path of the folder, typically relative to the .sqlproj file + * @param sourcePath Source path of the folder, typically relative to the .sqlproj file + * @param destinationPath Destination path of the folder, typically relative to the .sqlproj file */ - moveFolder(projectUri: string, destinationPath: string, path: string): Promise; + moveFolder(projectUri: string, sourcePath: string, destinationPath: string): Promise; /** * Add a post-deployment script to a project @@ -446,18 +446,18 @@ declare module 'mssql' { /** * Move a post-deployment script in a project * @param projectUri Absolute path of the project, including .sqlproj - * @param destinationPath Destination path of the file or folder, relative to the .sqlproj * @param path Path of the script, including .sql, relative to the .sqlproj + * @param destinationPath Destination path of the file or folder, relative to the .sqlproj */ - movePostDeploymentScript(projectUri: string, destinationPath: string, path: string): Promise; + movePostDeploymentScript(projectUri: string, path: string, destinationPath: string): Promise; /** * Move a pre-deployment script in a project * @param projectUri Absolute path of the project, including .sqlproj - * @param destinationPath Destination path of the file or folder, relative to the .sqlproj * @param path Path of the script, including .sql, relative to the .sqlproj + * @param destinationPath Destination path of the file or folder, relative to the .sqlproj */ - movePreDeploymentScript(projectUri: string, destinationPath: string, path: string): Promise; + movePreDeploymentScript(projectUri: string, path: string, destinationPath: string): Promise; /** * Close a SQL project @@ -561,10 +561,10 @@ declare module 'mssql' { /** * Move a SQL object script in a project * @param projectUri Absolute path of the project, including .sqlproj - * @param destinationPath Destination path of the file or folder, relative to the .sqlproj * @param path Path of the script, including .sql, relative to the .sqlproj + * @param destinationPath Destination path of the file or folder, relative to the .sqlproj */ - moveSqlObjectScript(projectUri: string, destinationPath: string, path: string): Promise; + moveSqlObjectScript(projectUri: string, path: string, destinationPath: string): Promise; /** * Get all the database references in a project @@ -632,10 +632,10 @@ declare module 'mssql' { /** * Move a None item in a project * @param projectUri Absolute path of the project, including .sqlproj - * @param destinationPath Destination path of the file or folder, relative to the .sqlproj * @param path Path of the item, including extension, relative to the .sqlproj + * @param destinationPath Destination path of the file or folder, relative to the .sqlproj */ - moveNoneItem(projectUri: string, destinationPath: string, path: string): Promise; + moveNoneItem(projectUri: string, path: string, destinationPath: string): Promise; } diff --git a/extensions/mssql/src/sqlProjects/sqlProjectsService.ts b/extensions/mssql/src/sqlProjects/sqlProjectsService.ts index 8f9f7c5ad0..b9a5d0a3dd 100644 --- a/extensions/mssql/src/sqlProjects/sqlProjectsService.ts +++ b/extensions/mssql/src/sqlProjects/sqlProjectsService.ts @@ -185,10 +185,10 @@ export class SqlProjectsService extends BaseService implements mssql.ISqlProject /** * Move a post-deployment script in a project * @param projectUri Absolute path of the project, including .sqlproj - * @param destinationPath Destination path of the file or folder, relative to the .sqlproj * @param path Path of the script, including .sql, relative to the .sqlproj + * @param destinationPath Destination path of the file or folder, relative to the .sqlproj */ - public async movePostDeploymentScript(projectUri: string, destinationPath: string, path: string): Promise { + public async movePostDeploymentScript(projectUri: string, path: string, destinationPath: string): Promise { const params: contracts.MoveItemParams = { projectUri: projectUri, destinationPath: destinationPath, path: path }; return await this.runWithErrorHandling(contracts.MovePostDeploymentScriptRequest.type, params); } @@ -196,10 +196,10 @@ export class SqlProjectsService extends BaseService implements mssql.ISqlProject /** * Move a pre-deployment script in a project * @param projectUri Absolute path of the project, including .sqlproj - * @param destinationPath Destination path of the file or folder, relative to the .sqlproj * @param path Path of the script, including .sql, relative to the .sqlproj + * @param destinationPath Destination path of the file or folder, relative to the .sqlproj */ - public async movePreDeploymentScript(projectUri: string, destinationPath: string, path: string): Promise { + public async movePreDeploymentScript(projectUri: string, path: string, destinationPath: string): Promise { const params: contracts.MoveItemParams = { projectUri: projectUri, destinationPath: destinationPath, path: path }; return await this.runWithErrorHandling(contracts.MovePreDeploymentScriptRequest.type, params); } @@ -350,10 +350,10 @@ export class SqlProjectsService extends BaseService implements mssql.ISqlProject /** * Move a SQL object script in a project * @param projectUri Absolute path of the project, including .sqlproj - * @param destinationPath Destination path of the file or folder, relative to the .sqlproj * @param path Path of the script, including .sql, relative to the .sqlproj + * @param destinationPath Destination path of the file or folder, relative to the .sqlproj */ - public async moveSqlObjectScript(projectUri: string, destinationPath: string, path: string): Promise { + public async moveSqlObjectScript(projectUri: string, path: string, destinationPath: string): Promise { const params: contracts.MoveItemParams = { projectUri: projectUri, destinationPath: destinationPath, path: path }; return await this.runWithErrorHandling(contracts.MoveSqlObjectScriptRequest.type, params); } @@ -454,10 +454,10 @@ export class SqlProjectsService extends BaseService implements mssql.ISqlProject /** * Move a SQL object script in a project * @param projectUri Absolute path of the project, including .sqlproj + * @param path Path of the script, including .sql, relative to the .sqlproj * @param destinationPath Destination path of the file or folder, relative to the .sqlproj - * @param path Path of the file, including extension, relative to the .sqlproj */ - public async moveNoneItem(projectUri: string, destinationPath: string, path: string): Promise { + public async moveNoneItem(projectUri: string, path: string, destinationPath: string): Promise { const params: contracts.MoveItemParams = { projectUri: projectUri, destinationPath: destinationPath, path: path }; return await this.runWithErrorHandling(contracts.MoveNoneItemRequest.type, params); } @@ -475,11 +475,11 @@ export class SqlProjectsService extends BaseService implements mssql.ISqlProject /** * Move a folder and its contents within a project * @param projectUri Absolute path of the project, including .sqlproj - * @param destinationPath Path of the folder, typically relative to the .sqlproj file - * @param path Path of the folder, typically relative to the .sqlproj file + * @param sourcePath Source path of the folder, typically relative to the .sqlproj file + * @param destinationPath Destination path of the folder, typically relative to the .sqlproj file */ - public async moveFolder(projectUri: string, destinationPath: string, path: string): Promise { - const params: contracts.MoveFolderParams = { projectUri: projectUri, destinationPath: destinationPath, path: path }; + public async moveFolder(projectUri: string, sourcePath: string, destinationPath: string): Promise { + const params: contracts.MoveFolderParams = { projectUri: projectUri, path: sourcePath, destinationPath: destinationPath }; return await this.runWithErrorHandling(contracts.MoveFolderRequest.type, params); } } diff --git a/extensions/sql-database-projects/package.json b/extensions/sql-database-projects/package.json index 6c083f691b..d34179286a 100644 --- a/extensions/sql-database-projects/package.json +++ b/extensions/sql-database-projects/package.json @@ -419,7 +419,7 @@ }, { "command": "sqlDatabaseProjects.exclude", - "when": "view == dataworkspace.views.main && viewItem =~ /^databaseProject.itemType.file/", + "when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.folder || viewItem =~ /^databaseProject.itemType.file/", "group": "9_dbProjectsLast@1" }, { @@ -429,7 +429,7 @@ }, { "command": "sqlDatabaseProjects.rename", - "when": "view == dataworkspace.views.main && viewItem =~ /^databaseProject.itemType.file/", + "when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.folder || viewItem =~ /^databaseProject.itemType.file/", "group": "9_dbProjectsLast@3" }, { diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index 252a026148..7df2533adf 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -679,7 +679,7 @@ export function errorExtracting(path: string, error: string) { return localize(' //#endregion //#region move -export const onlyMoveSqlFilesSupported = localize('onlyMoveSqlFilesSupported', "Only moving .sql files is supported"); +export const onlyMoveFilesFoldersSupported = localize('onlyMoveFilesFoldersSupported', "Only moving files and folders are supported"); export const movingFilesBetweenProjectsNotSupported = localize('movingFilesBetweenProjectsNotSupported', "Moving files between projects is not supported"); export function errorMovingFile(source: string, destination: string, error: string) { return localize('errorMovingFile', "Error when moving file from {0} to {1}. Error: {2}", source, destination, error); } export function moveConfirmationPrompt(source: string, destination: string) { return localize('moveConfirmationPrompt', "Are you sure you want to move {0} to {1}?", source, destination); } diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index c1e7aa2e58..ca743e1bca 100644 --- a/extensions/sql-database-projects/src/controllers/projectController.ts +++ b/extensions/sql-database-projects/src/controllers/projectController.ts @@ -797,31 +797,27 @@ export class ProjectsController { const node = context.element as BaseProjectTreeItem; const project = await this.getProjectFromContext(node); - const fileEntry = this.getFileProjectEntry(project, node); - - if (fileEntry) { + if (node.entryKey) { TelemetryReporter.sendActionEvent(TelemetryViews.ProjectTree, TelemetryActions.excludeFromProject); switch (node.type) { case constants.DatabaseProjectItemType.sqlObjectScript: case constants.DatabaseProjectItemType.table: case constants.DatabaseProjectItemType.externalStreamingJob: - await project.excludeSqlObjectScript(fileEntry.relativePath); + await project.excludeSqlObjectScript(node.entryKey); break; case constants.DatabaseProjectItemType.folder: - // TODO: not yet supported in DacFx - //await project.excludeFolder(fileEntry.relativePath); - void vscode.window.showErrorMessage(constants.excludeFolderNotSupported); + await project.excludeFolder(node.entryKey); break; case constants.DatabaseProjectItemType.preDeploymentScript: - await project.excludePreDeploymentScript(fileEntry.relativePath); + await project.excludePreDeploymentScript(node.entryKey); break; case constants.DatabaseProjectItemType.postDeploymentScript: - await project.excludePostDeploymentScript(fileEntry.relativePath); + await project.excludePostDeploymentScript(node.entryKey); break; case constants.DatabaseProjectItemType.noneFile: case constants.DatabaseProjectItemType.publishProfile: - await project.excludeNoneItem(fileEntry.relativePath); + await project.excludeNoneItem(node.entryKey); break; default: throw new Error(constants.unhandledExcludeType(node.type)); @@ -904,25 +900,21 @@ export class ProjectsController { public async rename(context: dataworkspace.WorkspaceTreeItem): Promise { const node = context.element as BaseProjectTreeItem; const project = await this.getProjectFromContext(node); - const file = this.getFileProjectEntry(project, node); - const baseName = path.basename(node.friendlyName); - let fileExtension: string; - if (utils.isPublishProfile(baseName)) { - fileExtension = constants.publishProfileExtension; - } else { - fileExtension = constants.sqlFileExtension; - } + const originalAbsolutePath = utils.getPlatformSafeFileEntryPath(node.projectFileUri.fsPath); + const originalName = path.basename(node.friendlyName); + const originalExt = path.extname(node.friendlyName); // need to use quickpick because input box isn't supported in treeviews // https://github.com/microsoft/vscode/issues/117502 and https://github.com/microsoft/vscode/issues/97190 const newFileName = await vscode.window.showInputBox( { title: constants.enterNewName, - value: path.basename(baseName, fileExtension), + value: originalName, + valueSelection: [0, path.basename(originalName, originalExt).length], ignoreFocusOut: true, - validateInput: async (value) => { - return await this.fileAlreadyExists(value, file?.fsUri.fsPath!) ? constants.fileAlreadyExists(value) : undefined; + validateInput: async (newName) => { + return await this.fileAlreadyExists(newName, originalAbsolutePath) ? constants.fileAlreadyExists(newName) : undefined; } }); @@ -930,22 +922,21 @@ export class ProjectsController { return; } - const newFilePath = path.join(path.dirname(utils.getPlatformSafeFileEntryPath(file?.relativePath!)), `${newFileName}${fileExtension}`); + const newFilePath = path.join(path.dirname(utils.getPlatformSafeFileEntryPath(node.relativeProjectUri.fsPath!)), newFileName); + const result = await project.move(node, newFilePath); - const renameResult = await project.move(node, newFilePath); - - if (renameResult?.success) { + if (result?.success) { TelemetryReporter.sendActionEvent(TelemetryViews.ProjectTree, TelemetryActions.rename); } else { TelemetryReporter.sendErrorEvent2(TelemetryViews.ProjectTree, TelemetryActions.rename); - void vscode.window.showErrorMessage(constants.errorRenamingFile(file?.relativePath!, newFilePath, utils.getErrorMessage(renameResult?.errorMessage))); + void vscode.window.showErrorMessage(constants.errorRenamingFile(node.entryKey!, newFilePath, result?.errorMessage)); } this.refreshProjectsTree(context); } private fileAlreadyExists(newFileName: string, previousFilePath: string): Promise { - return utils.exists(path.join(path.dirname(previousFilePath), `${newFileName}.sql`)); + return utils.exists(path.join(path.dirname(previousFilePath), newFileName)); } /** @@ -1488,21 +1479,6 @@ export class ProjectsController { //#region Helper methods - private getFileProjectEntry(project: Project, context: BaseProjectTreeItem): FileProjectEntry | undefined { - const fileOrFolder = context as FileNode ? context as FileNode : context as FolderNode; - - if (fileOrFolder) { - // use relative path and not tree paths for files and folder - const allFileEntries = project.files.concat(project.preDeployScripts).concat(project.postDeployScripts).concat(project.noneDeployScripts).concat(project.publishProfiles); - - // trim trailing slash since folders with and without a trailing slash are allowed in a sqlproj - const trimmedUri = utils.trimChars(utils.getPlatformSafeFileEntryPath(utils.trimUri(fileOrFolder.projectFileUri, fileOrFolder.fileSystemUri)), '/'); - return allFileEntries.find(x => utils.trimChars(utils.getPlatformSafeFileEntryPath(x.relativePath), '/') === trimmedUri); - } - const projectRelativeUri = vscode.Uri.file(path.basename(context.projectFileUri.fsPath, constants.sqlprojExtension)); - return project.files.find(x => utils.getPlatformSafeFileEntryPath(x.relativePath) === utils.getPlatformSafeFileEntryPath(utils.trimUri(projectRelativeUri, context.relativeProjectUri))); - } - private async getProjectFromContext(context: Project | BaseProjectTreeItem | dataworkspace.WorkspaceTreeItem): Promise { if ('element' in context) { context = context.element; @@ -1881,23 +1857,22 @@ export class ProjectsController { //#endregion /** - * Move a file in the project tree + * Move a file or folder in the project tree * @param projectUri URI of the project * @param source * @param target */ public async moveFile(projectUri: vscode.Uri, source: any, target: dataworkspace.WorkspaceTreeItem): Promise { - const sourceFileNode = source as FileNode; + const sourceFileNode = source as FileNode | FolderNode; const project = await this.getProjectFromContext(sourceFileNode); - // only moving files is supported - if (!sourceFileNode || !(sourceFileNode instanceof FileNode)) { - void vscode.window.showErrorMessage(constants.onlyMoveSqlFilesSupported); + // only moving files and folders are supported + if (!sourceFileNode || !(sourceFileNode instanceof FileNode || sourceFileNode instanceof FolderNode)) { + void vscode.window.showErrorMessage(constants.onlyMoveFilesFoldersSupported); return; } - // Moving files to the SQLCMD variables and Database references folders isn't allowed - // TODO: should there be an error displayed if a file attempting to move a file to sqlcmd variables or database references? Or just silently fail and do nothing? + // Moving files/folders to the SQLCMD variables and Database references folders isn't allowed if (!target.element.fileSystemUri) { return; } @@ -1930,7 +1905,7 @@ export class ProjectsController { return; } - // Move the file + // Move the file/folder const moveResult = await project.move(sourceFileNode, newPath); if (moveResult?.success) { diff --git a/extensions/sql-database-projects/src/models/project.ts b/extensions/sql-database-projects/src/models/project.ts index 3f7eb9db86..f74790fa6c 100644 --- a/extensions/sql-database-projects/src/models/project.ts +++ b/extensions/sql-database-projects/src/models/project.ts @@ -20,7 +20,7 @@ import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/t import { DacpacReferenceProjectEntry, FileProjectEntry, NugetPackageReferenceProjectEntry, SqlProjectReferenceProjectEntry, SystemDatabaseReferenceProjectEntry } from './projectEntry'; import { ResultStatus } from 'azdata'; import { BaseProjectTreeItem } from './tree/baseTreeItem'; -import { NoneNode, PostDeployNode, PreDeployNode, PublishProfileNode, SqlObjectFileNode } from './tree/fileFolderTreeItem'; +import { FolderNode, NoneNode, PostDeployNode, PreDeployNode, PublishProfileNode, SqlObjectFileNode } from './tree/fileFolderTreeItem'; import { ProjectType, GetScriptsResult, GetFoldersResult } from '../common/typeHelper'; @@ -531,6 +531,9 @@ export class Project implements ISqlProject { const result = await this.sqlProjService.addFolder(this.projectFilePath, relativeFolderPath); this.throwIfFailed(result); + // Note: adding a folder does not mean adding the contents of the folder. + // SDK projects may still need to adjust their include/exclude globs, and Legacy projects must still include each file + // in order for the contents of the folders to be added. await this.readFolders(); } @@ -538,6 +541,32 @@ export class Project implements ISqlProject { const result = await this.sqlProjService.deleteFolder(this.projectFilePath, relativeFolderPath); this.throwIfFailed(result); + await this.readFilesInProject(); + await this.readPreDeployScripts(); + await this.readPostDeployScripts(); + await this.readNoneItems(); + await this.readFolders(); + } + + public async excludeFolder(relativeFolderPath: string): Promise { + const result = await this.sqlProjService.excludeFolder(this.projectFilePath, relativeFolderPath); + this.throwIfFailed(result); + + await this.readFilesInProject(); + await this.readPreDeployScripts(); + await this.readPostDeployScripts(); + await this.readNoneItems(); + await this.readFolders(); + } + + public async moveFolder(relativeSourcePath: string, relativeDestinationPath: string): Promise { + const result = await this.sqlProjService.moveFolder(this.projectFilePath, relativeSourcePath, relativeDestinationPath); + this.throwIfFailed(result); + + await this.readFilesInProject(); + await this.readPreDeployScripts(); + await this.readPostDeployScripts(); + await this.readNoneItems(); await this.readFolders(); } @@ -861,9 +890,8 @@ export class Project implements ISqlProject { } const databaseLiteral = settings.databaseVariable ? undefined : settings.databaseName; + let result, referenceName; - let result; - let referenceName; if (reference instanceof SqlProjectReferenceProjectEntry) { referenceName = (settings).projectName; result = await this.sqlProjService.addSqlProjectReference(this.projectFilePath, reference.pathForSqlProj(), reference.projectGuid, settings.suppressMissingDependenciesErrors, settings.databaseVariable, settings.serverVariable, databaseLiteral) @@ -1026,13 +1054,15 @@ export class Project implements ISqlProject { let result; if (node instanceof SqlObjectFileNode) { - result = await this.sqlProjService.moveSqlObjectScript(this.projectFilePath, destinationRelativePath, originalRelativePath) + result = await this.sqlProjService.moveSqlObjectScript(this.projectFilePath, originalRelativePath, destinationRelativePath) } else if (node instanceof PreDeployNode) { - result = await this.sqlProjService.movePreDeploymentScript(this.projectFilePath, destinationRelativePath, originalRelativePath) + result = await this.sqlProjService.movePreDeploymentScript(this.projectFilePath, originalRelativePath, destinationRelativePath) } else if (node instanceof PostDeployNode) { - result = await this.sqlProjService.movePostDeploymentScript(this.projectFilePath, destinationRelativePath, originalRelativePath) + result = await this.sqlProjService.movePostDeploymentScript(this.projectFilePath, originalRelativePath, destinationRelativePath) } else if (node instanceof NoneNode || node instanceof PublishProfileNode) { - result = await this.sqlProjService.moveNoneItem(this.projectFilePath, destinationRelativePath, originalRelativePath); + result = await this.sqlProjService.moveNoneItem(this.projectFilePath, originalRelativePath, destinationRelativePath); + } else if (node instanceof FolderNode) { + result = await this.sqlProjService.moveFolder(this.projectFilePath, originalRelativePath, destinationRelativePath); } else { result = { success: false, errorMessage: constants.unhandledMoveNode } } diff --git a/extensions/sql-database-projects/src/test/project.test.ts b/extensions/sql-database-projects/src/test/project.test.ts index 5f6bcfac35..e2c2cf0f96 100644 --- a/extensions/sql-database-projects/src/test/project.test.ts +++ b/extensions/sql-database-projects/src/test/project.test.ts @@ -349,8 +349,7 @@ describe('Project: sdk style project content operations', function (): void { should(project.files.length).equal(0, 'There should not be any SQL object scripts after the excludes'); }); - // skipped because exclude folder not yet supported - it.skip('Should handle excluding glob included folders', async function (): Promise { + it('Should handle excluding glob included folders', async function (): Promise { const testFolderPath = await testUtils.generateTestFolderPath(this.test); const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.openSdkStyleSqlProjectBaseline, testFolderPath); await testUtils.createDummyFileStructureWithPrePostDeployScripts(this.test, false, undefined, path.dirname(projFilePath)); @@ -362,23 +361,16 @@ describe('Project: sdk style project content operations', function (): void { should(project.noneDeployScripts.length).equal(2); // try to exclude a glob included folder - //await project.excludeFolder('folder1\\'); + await project.excludeFolder('folder1'); // verify folder and contents are excluded should(project.folders.length).equal(1); should(project.files.length).equal(6); should(project.noneDeployScripts.length).equal(1, 'Script.PostDeployment2.sql should have been excluded'); - should(project.files.find(f => f.relativePath === 'folder1\\')).equal(undefined); - - // verify sqlproj has glob exclude for folder, but not for files and inner folder - const projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText.includes('')).equal(true, projFileText); - should(projFileText.includes('')).equal(false, projFileText); - should(projFileText.includes('')).equal(false, projFileText); + should(project.folders.find(f => f.relativePath === 'folder1')).equal(undefined); }); - // skipped because exclude folder not yet supported - it.skip('Should handle excluding nested glob included folders', async function (): Promise { + it('Should handle excluding folders', async function (): Promise { const testFolderPath = await testUtils.generateTestFolderPath(this.test,); const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.openSdkStyleSqlProjectBaseline, testFolderPath); await testUtils.createDummyFileStructureWithPrePostDeployScripts(this.test, false, undefined, path.dirname(projFilePath)); @@ -389,21 +381,16 @@ describe('Project: sdk style project content operations', function (): void { should(project.folders.length).equal(3); // try to exclude a glob included folder - //await project.excludeFolder('folder1\\nestedFolder\\'); + await project.excludeFolder('folder1\\nestedFolder'); // verify folder and contents are excluded should(project.folders.length).equal(2); should(project.files.length).equal(11); - should(project.files.find(f => f.relativePath === 'folder1\\nestedFolder\\')).equal(undefined); - - // verify sqlproj has glob exclude for folder, but not for files - const projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText.includes('')).equal(true, projFileText); - should(projFileText.includes('')).equal(false, projFileText); + should(project.folders.find(f => f.relativePath === 'folder1\\nestedFolder')).equal(undefined); }); // skipped because exclude folder not yet supported - it.skip('Should handle excluding explicitly included folders', async function (): Promise { + it('Should handle excluding explicitly included folders', async function (): Promise { const testFolderPath = await testUtils.generateTestFolderPath(this.test,); const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.openSdkStyleSqlProjectWithFilesSpecifiedBaseline, testFolderPath); await testUtils.createDummyFileStructure(this.test, false, undefined, path.dirname(projFilePath)); @@ -412,36 +399,27 @@ describe('Project: sdk style project content operations', function (): void { should(project.files.length).equal(11); should(project.folders.length).equal(2); - should(project.files.find(f => f.relativePath === 'folder1\\')!).not.equal(undefined); - should(project.files.find(f => f.relativePath === 'folder2\\')!).not.equal(undefined); + should(project.folders.find(f => f.relativePath === 'folder1')!).not.equal(undefined); + should(project.folders.find(f => f.relativePath === 'folder2')!).not.equal(undefined); // try to exclude an explicitly included folder without trailing \ in sqlproj - //await project.excludeFolder('folder1\\'); + await project.excludeFolder('folder1'); // verify folder and contents are excluded should(project.folders.length).equal(1); should(project.files.length).equal(6); - should(project.files.find(f => f.relativePath === 'folder1\\')).equal(undefined); + should(project.folders.find(f => f.relativePath === 'folder1')).equal(undefined); // try to exclude an explicitly included folder with trailing \ in sqlproj - //await project.excludeFolder('folder2\\'); + await project.excludeFolder('folder2'); // verify folder and contents are excluded should(project.folders.length).equal(0); should(project.files.length).equal(1); - should(project.files.find(f => f.relativePath === 'folder2\\')).equal(undefined); - - // make sure both folders are removed from sqlproj and remove entry is added - const projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText.includes('')).equal(false, projFileText); - should(projFileText.includes('')).equal(false, projFileText); - - should(projFileText.includes('')).equal(true, projFileText); - should(projFileText.includes('')).equal(true, projFileText); + should(project.folders.find(f => f.relativePath === 'folder2')).equal(undefined); }); - // TODO: skipped until fix for folder trailing slashes comes in from DacFx - it.skip('Should handle deleting explicitly included folders', async function (): Promise { + it('Should handle deleting explicitly included folders', async function (): Promise { const testFolderPath = await testUtils.generateTestFolderPath(this.test,); const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.openSdkStyleSqlProjectWithFilesSpecifiedBaseline, testFolderPath); await testUtils.createDummyFileStructureWithPrePostDeployScripts(this.test, false, undefined, path.dirname(projFilePath)); @@ -450,32 +428,24 @@ describe('Project: sdk style project content operations', function (): void { should(project.files.length).equal(13); should(project.folders.length).equal(3); - should(project.files.find(f => f.relativePath === 'folder1\\')!).not.equal(undefined); - should(project.files.find(f => f.relativePath === 'folder2\\')!).not.equal(undefined); + should(project.folders.find(f => f.relativePath === 'folder1')!).not.equal(undefined); + should(project.folders.find(f => f.relativePath === 'folder2')!).not.equal(undefined); // try to delete an explicitly included folder with the trailing \ in sqlproj - await project.deleteFolder('folder2\\'); + await project.deleteFolder('folder2'); // verify the project not longer has folder2 and its contents should(project.folders.length).equal(2); should(project.files.length).equal(8); - should(project.files.find(f => f.relativePath === 'folder2\\')).equal(undefined); + should(project.folders.find(f => f.relativePath === 'folder2')).equal(undefined); // try to delete an explicitly included folder without trailing \ in sqlproj - await project.deleteFolder('folder1\\'); + await project.deleteFolder('folder1'); // verify the project not longer has folder1 and its contents should(project.folders.length).equal(0); should(project.files.length).equal(1); - should(project.files.find(f => f.relativePath === 'folder1\\')).equal(undefined); - - // make sure both folders are removed from sqlproj and Build Remove entries were not added - const projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText.includes('')).equal(false, projFileText); - should(projFileText.includes('')).equal(false, projFileText); - - should(projFileText.includes('')).equal(false, projFileText); - should(projFileText.includes('')).equal(false, projFileText); + should(project.folders.find(f => f.relativePath === 'folder1')).equal(undefined); }); // TODO: remove once DacFx exposes both absolute and relative outputPath @@ -890,7 +860,7 @@ describe('Project: database references', function (): void { should(project.databaseReferences.length).equal(1, 'There should be one database reference after trying to add a reference to testProject again'); }); - it.skip('Should update sqlcmd variable values if value changes', async function (): Promise { + it('Should update sqlcmd variable values if value changes', async function (): Promise { const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.newProjectFileBaseline); const project = await Project.openProject(projFilePath); const databaseVariable = 'test3Db'; @@ -933,12 +903,6 @@ describe('Project: database references', function (): void { should(project.databaseReferences.length).equal(1, 'There should be a database reference after adding a reference to test3'); should(project.databaseReferences[0].referenceName).equal('test3', 'The database reference should be test3'); should(project.sqlCmdVariables.size).equal(2, 'There should still be 2 sqlcmdvars after adding the dacpac reference again with different sqlcmdvar values'); - - projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText).containEql(''); - should(projFileText).containEql('newDbName'); - should(projFileText).containEql(''); - should(projFileText).containEql('newServerName'); }); }); @@ -1140,7 +1104,6 @@ describe('Project: round trip updates', function (): void { await testUpdateInRoundTrip(this.test, baselines.SSDTProjectFileBaseline); }); - // skipped until https://mssqltools.visualstudio.com/SQL%20Tools%20Semester%20Work%20Tracking/_workitems/edit/15749 is fixed it.skip('Should update SSDT project with new system database references', async function (): Promise { await testUpdateInRoundTrip(this.test, baselines.SSDTUpdatedProjectBaseline); }); diff --git a/extensions/sql-database-projects/src/test/projectController.test.ts b/extensions/sql-database-projects/src/test/projectController.test.ts index 3460828faf..7f83217abb 100644 --- a/extensions/sql-database-projects/src/test/projectController.test.ts +++ b/extensions/sql-database-projects/src/test/projectController.test.ts @@ -318,7 +318,7 @@ describe('ProjectsController', function (): void { proj = await Project.openProject(proj.projectFilePath); // reload edited sqlproj from disk // confirm result - should(proj.files.length).equal(2, 'number of file entries'); // LowerFolder and the contained scripts should be deleted + should(proj.files.length).equal(0, 'number of file entries'); // LowerFolder and the contained scripts should be excluded should(proj.folders.find(f => f.relativePath === 'UpperFolder')).not.equal(undefined, 'UpperFolder should still be there'); should(proj.preDeployScripts.length).equal(0, 'Pre deployment scripts'); should(proj.postDeployScripts.length).equal(0, 'Post deployment scripts'); @@ -330,6 +330,29 @@ describe('ProjectsController', function (): void { should(await utils.exists(noneEntry.fsUri.fsPath)).equal(true, 'none entry pre-deployment script is supposed to still exist on disk'); }); + it('Should exclude a folder', async function (): Promise { + let proj = await testUtils.createTestSqlProject(this.test); + await proj.addScriptItem('SomeFolder\\MyTable.sql', 'CREATE TABLE [NotARealTable]'); + + const projController = new ProjectsController(testContext.outputChannel); + const projTreeRoot = new ProjectRootTreeItem(proj); + + should(await utils.exists(path.join(proj.projectFolderPath, 'SomeFolder\\MyTable.sql'))).be.true('File should exist in original location'); + (proj.files.length).should.equal(1, 'Starting number of files'); + (proj.folders.length).should.equal(1, 'Starting number of folders'); + + // exclude folder + const folderNode = projTreeRoot.children.find(f => f.friendlyName === 'SomeFolder'); + await projController.exclude(createWorkspaceTreeItem(folderNode!)); + + // reload project and verify files were renamed + proj = await Project.openProject(proj.projectFilePath); + + should(await utils.exists(path.join(proj.projectFolderPath, 'SomeFolder\\MyTable.sql'))).be.true('File should still exist on disk'); + (proj.files.length).should.equal(0, 'Number of files should not have changed'); + (proj.folders.length).should.equal(0, 'Number of folders should not have changed'); + }); + // TODO: move test to DacFx and fix delete it.skip('Should delete folders with excluded items', async function (): Promise { let proj = await testUtils.createTestProject(this.test, templates.newSqlProjectTemplate); @@ -879,7 +902,7 @@ describe('ProjectsController', function (): void { await projController.moveFile(vscode.Uri.file(proj.projectFilePath), sqlcmdVarNode, projectRootWorkspaceTreeItem); should(spy.calledOnce).be.true('showErrorMessage should have been called exactly once when trying to move a sqlcmd variable'); - should(spy.calledWith(constants.onlyMoveSqlFilesSupported)).be.true(`showErrorMessage not called with expected message '${constants.onlyMoveSqlFilesSupported}' Actual '${spy.getCall(0).args[0]}'`); + should(spy.calledWith(constants.onlyMoveFilesFoldersSupported)).be.true(`showErrorMessage not called with expected message '${constants.onlyMoveFilesFoldersSupported}' Actual '${spy.getCall(0).args[0]}'`); spy.restore(); // try moving a database reference @@ -887,7 +910,7 @@ describe('ProjectsController', function (): void { await projController.moveFile(vscode.Uri.file(proj.projectFilePath), dbRefNode, projectRootWorkspaceTreeItem); should(spy.calledOnce).be.true('showErrorMessage should have been called exactly once when trying to move a database reference'); - should(spy.calledWith(constants.onlyMoveSqlFilesSupported)).be.true(`showErrorMessage not called with expected message '${constants.onlyMoveSqlFilesSupported}' Actual '${spy.getCall(0).args[0]}'`); + should(spy.calledWith(constants.onlyMoveFilesFoldersSupported)).be.true(`showErrorMessage not called with expected message '${constants.onlyMoveFilesFoldersSupported}' Actual '${spy.getCall(0).args[0]}'`); spy.restore(); // try moving a folder @@ -895,7 +918,7 @@ describe('ProjectsController', function (): void { await projController.moveFile(vscode.Uri.file(proj.projectFilePath), folderNode, projectRootWorkspaceTreeItem); should(spy.calledOnce).be.true('showErrorMessage should have been called exactly once when trying to move a folder'); - should(spy.calledWith(constants.onlyMoveSqlFilesSupported)).be.true(`showErrorMessage not called with expected message '${constants.onlyMoveSqlFilesSupported}' Actual '${spy.getCall(0).args[0]}'`); + should(spy.calledWith(constants.onlyMoveFilesFoldersSupported)).be.true(`showErrorMessage not called with expected message '${constants.onlyMoveFilesFoldersSupported}' Actual '${spy.getCall(0).args[0]}'`); spy.restore(); }); @@ -942,7 +965,7 @@ describe('ProjectsController', function (): void { }); it('Should rename a sql object file', async function (): Promise { - sinon.stub(vscode.window, 'showInputBox').resolves('newName'); + sinon.stub(vscode.window, 'showInputBox').resolves('newName.sql'); let proj = await testUtils.createTestProject(this.test, baselines.openSdkStyleSqlProjectBaseline); const projTreeRoot = await setupMoveTest(proj); const projController = new ProjectsController(testContext.outputChannel); @@ -966,12 +989,12 @@ describe('ProjectsController', function (): void { const projTreeRoot = new ProjectRootTreeItem(proj); // try to rename a file from the root folder - sinon.stub(vscode.window, 'showInputBox').resolves('predeployNewName'); + sinon.stub(vscode.window, 'showInputBox').resolves('predeployNewName.sql'); const preDeployScriptNode = projTreeRoot.children.find(x => x.friendlyName === 'Script.PreDeployment1.sql'); await projController.rename(createWorkspaceTreeItem(preDeployScriptNode!)); sinon.restore(); - sinon.stub(vscode.window, 'showInputBox').resolves('postdeployNewName'); + sinon.stub(vscode.window, 'showInputBox').resolves('postdeployNewName.sql'); const postDeployScriptNode = projTreeRoot.children.find(x => x.friendlyName === 'Script.PostDeployment1.sql'); await projController.rename(createWorkspaceTreeItem(postDeployScriptNode!)); @@ -984,8 +1007,31 @@ describe('ProjectsController', function (): void { should(await utils.exists(path.join(proj.projectFolderPath, 'postdeployNewName.sql'))).be.true('The moved post deploy script file should exist'); }); + it('Should rename a folder', async function (): Promise { + let proj = await testUtils.createTestSqlProject(this.test); + await proj.addScriptItem('SomeFolder\\MyTable.sql', 'CREATE TABLE [NotARealTable]'); - // TODO: add test for renaming a file in a folder after fix from DacFx for slashes is brought over + const projController = new ProjectsController(testContext.outputChannel); + const projTreeRoot = new ProjectRootTreeItem(proj); + + sinon.stub(vscode.window, 'showInputBox').resolves('RenamedFolder'); + should(await utils.exists(path.join(proj.projectFolderPath, 'SomeFolder\\MyTable.sql'))).be.true('File should exist in original location'); + (proj.files.length).should.equal(1, 'Starting number of files'); + (proj.folders.length).should.equal(1, 'Starting number of folders'); + + // rename folder + const folderNode = projTreeRoot.children.find(f => f.friendlyName === 'SomeFolder'); + await projController.rename(createWorkspaceTreeItem(folderNode!)); + + // reload project and verify files were renamed + proj = await Project.openProject(proj.projectFilePath); + + should(await utils.exists(path.join(proj.projectFolderPath, 'RenamedFolder\\MyTable.sql'))).be.true('File should exist in new location'); + (proj.files.length).should.equal(1, 'Number of files should not have changed'); + (proj.folders.length).should.equal(1, 'Number of folders should not have changed'); + should(proj.folders.find(f => f.relativePath === 'RenamedFolder') !== undefined).be.true('The folder path should have been updated'); + should(proj.files.find(f => f.relativePath === 'RenamedFolder\\MyTable.sql') !== undefined).be.true('Path of the script in the folder should have been updated'); + }); }); describe('SqlCmd Variables', function (): void { @@ -1000,7 +1046,7 @@ describe('ProjectsController', function (): void { should(project.sqlCmdVariables.size).equal(2, 'The project should start with 2 sqlcmd variables'); sinon.stub(vscode.window, 'showWarningMessage').returns(Promise.resolve('Cancel')); - await projController.delete(createWorkspaceTreeItem(projRoot.children.find(x => x.friendlyName === constants.sqlcmdVariablesNodeName)!.children[0])); + await projController.delete(createWorkspaceTreeItem(projRoot.children.find(x => x.friendlyName === constants.sqlcmdVariablesNodeName)!.children[0] /* LowerFolder */)); // reload project project = await Project.openProject(project.projectFilePath); diff --git a/extensions/types/vscode-mssql.d.ts b/extensions/types/vscode-mssql.d.ts index a686be106a..9766d1c506 100644 --- a/extensions/types/vscode-mssql.d.ts +++ b/extensions/types/vscode-mssql.d.ts @@ -530,10 +530,10 @@ declare module 'vscode-mssql' { /** * Move a folder and its contents within a project * @param projectUri Absolute path of the project, including .sqlproj - * @param destinationPath Path of the folder, typically relative to the .sqlproj file - * @param path Path of the folder, typically relative to the .sqlproj file + * @param sourcePath Source path of the folder, typically relative to the .sqlproj file + * @param destinationPath Destination path of the folder, typically relative to the .sqlproj file */ - moveFolder(projectUri: string, destinationPath: string, path: string): Promise; + moveFolder(projectUri: string, sourcePath: string, destinationPath: string): Promise; /** * Add a post-deployment script to a project @@ -580,18 +580,18 @@ declare module 'vscode-mssql' { /** * Move a post-deployment script in a project * @param projectUri Absolute path of the project, including .sqlproj - * @param destinationPath Destination path of the file or folder, relative to the .sqlproj * @param path Path of the script, including .sql, relative to the .sqlproj + * @param destinationPath Destination path of the file or folder, relative to the .sqlproj */ - movePostDeploymentScript(projectUri: string, destinationPath: string, path: string): Promise; + movePostDeploymentScript(projectUri: string, path: string, destinationPath: string): Promise; /** * Move a pre-deployment script in a project * @param projectUri Absolute path of the project, including .sqlproj - * @param destinationPath Destination path of the file or folder, relative to the .sqlproj * @param path Path of the script, including .sql, relative to the .sqlproj + * @param destinationPath Destination path of the file or folder, relative to the .sqlproj */ - movePreDeploymentScript(projectUri: string, destinationPath: string, path: string): Promise; + movePreDeploymentScript(projectUri: string, path: string, destinationPath: string): Promise; /** * Close a SQL project @@ -695,10 +695,10 @@ declare module 'vscode-mssql' { /** * Move a SQL object script in a project * @param projectUri Absolute path of the project, including .sqlproj - * @param destinationPath Destination path of the file or folder, relative to the .sqlproj * @param path Path of the script, including .sql, relative to the .sqlproj + * @param destinationPath Destination path of the file or folder, relative to the .sqlproj */ - moveSqlObjectScript(projectUri: string, destinationPath: string, path: string): Promise; + moveSqlObjectScript(projectUri: string, path: string, destinationPath: string): Promise; /** * Get all the database references in a project @@ -766,10 +766,10 @@ declare module 'vscode-mssql' { /** * Move a None item in a project * @param projectUri Absolute path of the project, including .sqlproj - * @param destinationPath Destination path of the file or folder, relative to the .sqlproj * @param path Path of the item, including extension, relative to the .sqlproj + * @param destinationPath Destination path of the file or folder, relative to the .sqlproj */ - moveNoneItem(projectUri: string, destinationPath: string, path: string): Promise; + moveNoneItem(projectUri: string, path: string, destinationPath: string): Promise; } /**