Drag and drop support for sql projects tree (#21956)

* Drag and drop working

* update comment

* move to projectController

* remove registerTreeDataProvider

* add tests

* fix dragging to project root

* cleanup

* addressing comments
This commit is contained in:
Kim Santiago
2023-02-21 15:45:25 -08:00
committed by GitHub
parent effdf4f538
commit a7f68ebd33
8 changed files with 260 additions and 7 deletions

View File

@@ -652,3 +652,10 @@ export function downloadingFromTo(from: string, to: string) { return localize('d
export function extractingDacFxDlls(location: string) { return localize('extractingDacFxDlls', "Extracting DacFx build DLLs to {0}", location); }
export function errorDownloading(url: string, error: string) { return localize('errorDownloading', "Error downloading {0}. Error: {1}", url, error); }
export function errorExtracting(path: string, error: string) { return localize('errorExtracting', "Error extracting files from {0}. Error: {1}", path, error); }
// move
export const onlyMoveSqlFilesSupported = localize('onlyMoveSqlFilesSupported', "Only moving .sql files is 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); }
export const move = localize('Move', "Move");

View File

@@ -14,6 +14,7 @@ import * as vscode from 'vscode';
import type * as azdataType from 'azdata';
import * as dataworkspace from 'dataworkspace';
import type * as mssqlVscode from 'vscode-mssql';
import * as fse from 'fs-extra';
import { promises as fs } from 'fs';
import { PublishDatabaseDialog } from '../dialogs/publishDatabaseDialog';
@@ -1824,6 +1825,68 @@ export class ProjectsController {
}
//#endregion
/**
* Move a file 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<void> {
const sourceFileNode = source as FileNode;
// only moving files is supported
if (!sourceFileNode || !(sourceFileNode instanceof FileNode)) {
void vscode.window.showErrorMessage(constants.onlyMoveSqlFilesSupported);
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?
if (!target.element.fileSystemUri) {
return;
}
// TODO: handle moving between different projects
if (projectUri.fsPath !== target.element.projectFileUri.fsPath) {
void vscode.window.showErrorMessage(constants.movingFilesBetweenProjectsNotSupported);
return;
}
// Calculate the new file path
let folderPath;
// target is the root of project, which is the .sqlproj
if (target.element.projectFileUri.fsPath === target.element.fileSystemUri.fsPath) {
folderPath = path.dirname(target.element.projectFileUri.fsPath!);
} else {
// target is another file or folder
folderPath = target.element.fileSystemUri.fsPath.endsWith(constants.sqlFileExtension) ? path.dirname(target.element.fileSystemUri.fsPath) : target.element.fileSystemUri.fsPath;
}
const newPath = path.join(folderPath!, sourceFileNode.friendlyName);
// don't do anything if the path is the same
if (newPath === sourceFileNode.fileSystemUri.fsPath) {
return;
}
const result = await vscode.window.showWarningMessage(constants.moveConfirmationPrompt(path.basename(sourceFileNode.fileSystemUri.fsPath), path.basename(folderPath)), { modal: true }, constants.move)
if (result !== constants.move) {
return;
}
// Move the file
try {
const project = await Project.openProject(projectUri.fsPath);
// TODO: swap out with DacFx projects apis - currently moving pre/post deploy scripts don't work, but they should work after the swap
await fse.move(sourceFileNode.fileSystemUri.fsPath, newPath!);
await project.exclude(project.files.find(f => f.fsUri.fsPath === sourceFileNode.fileSystemUri.fsPath)!);
await project.addExistingItem(newPath!);
} catch (e) {
void vscode.window.showErrorMessage(constants.errorMovingFile(sourceFileNode.fileSystemUri.fsPath, newPath, utils.getErrorMessage(e)));
}
}
}
export interface NewProjectParams {

View File

@@ -22,6 +22,18 @@ export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvide
}
supportsDragAndDrop: boolean = true;
/**
* Move a file in the project tree
* @param projectUri
* @param source
* @param target
*/
public async moveFile(projectUri: vscode.Uri, source: any, target: dataworkspace.WorkspaceTreeItem): Promise<void> {
return this.projectController.moveFile(projectUri, source, target);
}
/**
* Gets the project tree data provider
* @param projectFilePath The project file Uri

View File

@@ -810,6 +810,111 @@ describe('ProjectsController', function (): void {
should(project.files.filter(f => f.type === EntryType.Folder).length).equal(expectedFolders.length, 'Unexpected number of folders in project');
});
});
describe('Move file', function (): void {
it('Should move a file to another folder', async function (): Promise<void> {
const spy = sinon.spy(vscode.window, 'showErrorMessage');
sinon.stub(vscode.window, 'showWarningMessage').returns(<any>Promise.resolve(constants.move));
let proj = await testUtils.createTestProject(baselines.openSdkStyleSqlProjectBaseline);
const projTreeRoot = await setupMoveTest(proj);
const projController = new ProjectsController(testContext.outputChannel);
// try to move a file from the root folder into the UpperFolder
const sqlFileNode = projTreeRoot.children.find(x => x.friendlyName === 'script1.sql');
const folderWorkspaceTreeItem = createWorkspaceTreeItem(projTreeRoot.children.find(x => x.friendlyName === 'UpperFolder')!);
await projController.moveFile(vscode.Uri.file(proj.projectFilePath), sqlFileNode, folderWorkspaceTreeItem);
should(spy.notCalled).be.true('showErrorMessage should not have been called');
// reload project and verify file was moved
proj = await Project.openProject(proj.projectFilePath);
should(proj.files.find(f => f.relativePath === 'UpperFolder\\script1.sql') !== undefined).be.true('The file path should have been updated');
should(await utils.exists(path.join(proj.projectFolderPath, 'UpperFolder', 'script1.sql'))).be.true('The moved file should exist');
});
it('Should not allow moving a file to Database References or SQLCMD folder', async function (): Promise<void> {
const spy = sinon.spy(vscode.window, 'showErrorMessage');
sinon.stub(vscode.window, 'showWarningMessage').returns(<any>Promise.resolve(constants.move));
let proj = await testUtils.createTestProject(baselines.openSdkStyleSqlProjectBaseline);
const projTreeRoot = await setupMoveTest(proj);
const projController = new ProjectsController(testContext.outputChannel);
const foldersToTest = ['SQLCMD Variables', 'Database References'];
for (const folder of foldersToTest) {
// try to move a file from the root folder into the UpperFolder
const sqlFileNode = projTreeRoot.children.find(x => x.friendlyName === 'script1.sql');
const sqlCmdVariablesWorkspaceTreeItem = createWorkspaceTreeItem(projTreeRoot.children.find(x => x.friendlyName === folder)!);
await projController.moveFile(vscode.Uri.file(proj.projectFilePath), sqlFileNode, sqlCmdVariablesWorkspaceTreeItem);
// reload project and verify file was not moved
proj = await Project.openProject(proj.projectFilePath);
should(proj.files.find(f => f.relativePath === 'script1.sql') !== undefined).be.true(`The file path should not have been updated when trying to move script1.sql to ${folder}`);
should(spy.notCalled).be.true('showErrorMessage should not have been called.');
spy.restore();
}
});
it('Should only allow moving files', async function (): Promise<void> {
const spy = sinon.spy(vscode.window, 'showErrorMessage');
let proj = await testUtils.createTestProject(baselines.openSdkStyleSqlProjectBaseline);
const projTreeRoot = await setupMoveTest(proj);
const projController = new ProjectsController(testContext.outputChannel);
// try to move sqlcmd variable
const sqlcmdVarNode = projTreeRoot.children.find(x => x.friendlyName === 'SQLCMD Variables')!.children[0];
const projectRootWorkspaceTreeItem = createWorkspaceTreeItem(projTreeRoot);
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]}'`);
spy.restore();
// try moving a database reference
const dbRefNode = projTreeRoot.children.find(x => x.friendlyName === 'Database References')!.children[0];
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]}'`);
spy.restore();
// try moving a folder
const folderNode = projTreeRoot.children.find(x => x.friendlyName === 'UpperFolder');
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]}'`);
spy.restore();
});
it('Should not allow moving files between projects', async function (): Promise<void> {
const spy = sinon.spy(vscode.window, 'showErrorMessage');
sinon.stub(vscode.window, 'showWarningMessage').returns(<any>Promise.resolve(constants.move));
let proj1 = await testUtils.createTestProject(baselines.openSdkStyleSqlProjectBaseline);
let proj2 = await testUtils.createTestProject(baselines.openSdkStyleSqlProjectBaseline);
const projTreeRoot1 = await setupMoveTest(proj1);
const projTreeRoot2 = await setupMoveTest(proj2);
const projController = new ProjectsController(testContext.outputChannel);
// try to move a file from the root folder of proj1 to the UpperFolder of proj2
const proj1SqlFileNode = projTreeRoot1.children.find(x => x.friendlyName === 'script1.sql');
const proj2FolderWorkspaceTreeItem = createWorkspaceTreeItem(projTreeRoot2.children.find(x => x.friendlyName === 'UpperFolder')!);
await projController.moveFile(vscode.Uri.file(proj1.projectFilePath), proj1SqlFileNode, proj2FolderWorkspaceTreeItem);
should(spy.called).be.true('showErrorMessage should have been called');
should(spy.calledWith(constants.movingFilesBetweenProjectsNotSupported)).be.true(`showErrorMessage not called with expected message '${constants.movingFilesBetweenProjectsNotSupported}' Actual '${spy.getCall(0).args[0]}'`);
// verify script1.sql was not moved
proj1 = await Project.openProject(proj1.projectFilePath);
should(proj1.files.find(f => f.relativePath === 'script1.sql') !== undefined).be.true(`The file path should not have been updated when trying to move script1.sql to proj2`);
});
});
});
async function setupDeleteExcludeTest(proj: Project): Promise<[FileProjectEntry, ProjectRootTreeItem, FileProjectEntry, FileProjectEntry, FileProjectEntry]> {
@@ -836,6 +941,18 @@ async function setupDeleteExcludeTest(proj: Project): Promise<[FileProjectEntry,
return [scriptEntry, projTreeRoot, preDeployEntry, postDeployEntry, noneEntry];
}
async function setupMoveTest(proj: Project): Promise<ProjectRootTreeItem> {
await proj.addFolderItem('UpperFolder');
await proj.addFolderItem('UpperFolder/LowerFolder');
await proj.addScriptItem('UpperFolder/LowerFolder/someScript.sql', 'not a real script');
await proj.addScriptItem('UpperFolder/LowerFolder/someOtherScript.sql', 'Also not a real script');
await proj.addScriptItem('../anotherScript.sql', 'Also not a real script');
await proj.addScriptItem('script1.sql', 'Also not a real script');
const projTreeRoot = new ProjectRootTreeItem(proj);
return projTreeRoot;
}
function createWorkspaceTreeItem(node: BaseProjectTreeItem): dataworkspace.WorkspaceTreeItem {
return {
element: node,