mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-16 17:22:29 -05:00
Database Projects - Add existing file (#18066)
* Add existing file to sqlproj * Address PR comments * Fix failing test * Add convertSlashesForSqlProj to test failure
This commit is contained in:
@@ -81,6 +81,7 @@ export const noString = localize('noString', "No");
|
||||
export const noStringDefault = localize('noStringDefault', "No (default)");
|
||||
export const okString = localize('okString', "Ok");
|
||||
export const selectString = localize('selectString', "Select");
|
||||
export const selectFileString = localize('selectFileString', "Select File");
|
||||
export const dacpacFiles = localize('dacpacFiles', "dacpac Files");
|
||||
export const publishSettingsFiles = localize('publishSettingsFiles', "Publish Settings File");
|
||||
export const file = localize('file', "File");
|
||||
|
||||
@@ -78,6 +78,7 @@ export default class MainController implements vscode.Disposable {
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.newStoredProcedure', async (node: WorkspaceTreeItem) => { return this.projectsController.addItemPromptFromNode(node, templates.storedProcedure); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.newExternalStreamingJob', async (node: WorkspaceTreeItem) => { return this.projectsController.addItemPromptFromNode(node, templates.externalStreamingJob); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.newItem', async (node: WorkspaceTreeItem) => { return this.projectsController.addItemPromptFromNode(node); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.addExistingItem', async (node: WorkspaceTreeItem) => { return this.projectsController.addExistingItemPrompt(node); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.newFolder', async (node: WorkspaceTreeItem) => { return this.projectsController.addFolderPrompt(node); });
|
||||
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.addDatabaseReference', async (node: WorkspaceTreeItem) => { return this.projectsController.addDatabaseReference(node); });
|
||||
|
||||
@@ -651,6 +651,29 @@ export class ProjectsController {
|
||||
}
|
||||
}
|
||||
|
||||
public async addExistingItemPrompt(treeNode: dataworkspace.WorkspaceTreeItem): Promise<void> {
|
||||
const project = this.getProjectFromContext(treeNode);
|
||||
|
||||
const uris = await vscode.window.showOpenDialog({
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: false,
|
||||
openLabel: constants.selectString,
|
||||
title: constants.selectFileString
|
||||
});
|
||||
|
||||
if (!uris) {
|
||||
return; // user cancelled
|
||||
}
|
||||
|
||||
try {
|
||||
await project.addExistingItem(uris[0].fsPath);
|
||||
this.refreshProjectsTree(treeNode);
|
||||
} catch (err) {
|
||||
void vscode.window.showErrorMessage(utils.getErrorMessage(err));
|
||||
}
|
||||
}
|
||||
|
||||
public async exclude(context: dataworkspace.WorkspaceTreeItem): Promise<void> {
|
||||
const node = context.element as BaseProjectTreeItem;
|
||||
const project = this.getProjectFromContext(node);
|
||||
|
||||
@@ -712,6 +712,36 @@ export class Project implements ISqlProject {
|
||||
return fileEntry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a file to the project, and saves the project file
|
||||
*
|
||||
* @param filePath Absolute path of the file
|
||||
*/
|
||||
public async addExistingItem(filePath: string): Promise<FileProjectEntry> {
|
||||
const exists = await utils.exists(filePath);
|
||||
if (!exists) {
|
||||
throw new Error(constants.noFileExist(filePath));
|
||||
}
|
||||
|
||||
// Check if file already has been added to sqlproj
|
||||
const normalizedRelativeFilePath = utils.convertSlashesForSqlProj(path.relative(this.projectFolderPath, filePath));
|
||||
const existingEntry = this.files.find(f => f.relativePath.toUpperCase() === normalizedRelativeFilePath.toUpperCase());
|
||||
if (existingEntry) {
|
||||
return existingEntry;
|
||||
}
|
||||
|
||||
// Ensure that parent folder item exist in the project for the corresponding file path
|
||||
await this.ensureFolderItems(path.relative(this.projectFolderPath, path.dirname(filePath)));
|
||||
|
||||
// Update sqlproj XML
|
||||
const fileEntry = this.createFileProjectEntry(normalizedRelativeFilePath, EntryType.File);
|
||||
const xmlTag = path.extname(filePath) === constants.sqlFileExtension ? constants.Build : constants.None;
|
||||
await this.addToProjFile(fileEntry, xmlTag);
|
||||
this._files.push(fileEntry);
|
||||
|
||||
return fileEntry;
|
||||
}
|
||||
|
||||
public async exclude(entry: FileProjectEntry): Promise<void> {
|
||||
const toExclude: FileProjectEntry[] = this._files.concat(this._preDeployScripts).concat(this._postDeployScripts).concat(this._noneDeployScripts).filter(x => x.fsUri.fsPath.startsWith(entry.fsUri.fsPath));
|
||||
await this.removeFromProjFile(toExclude);
|
||||
@@ -939,6 +969,10 @@ export class Project implements ISqlProject {
|
||||
}
|
||||
|
||||
private async addFileToProjFile(filePath: string, xmlTag: string, attributes?: Map<string, string>): Promise<void> {
|
||||
|
||||
// delete Remove node if a file has been previously excluded
|
||||
await this.undoExcludeFileFromProjFile(xmlTag, filePath);
|
||||
|
||||
let itemGroup;
|
||||
|
||||
if (xmlTag === constants.PreDeploy || xmlTag === constants.PostDeploy) {
|
||||
@@ -969,7 +1003,7 @@ export class Project implements ISqlProject {
|
||||
|
||||
// don't need to add an entry if it's already included by a glob pattern
|
||||
// unless it has an attribute that needs to be added, like external streaming job which needs it so it can be determined if validation can run on it
|
||||
if (attributes?.size === 0 && currentFiles.find(f => f.relativePath === utils.convertSlashesForSqlProj(filePath))) {
|
||||
if ((!attributes || attributes.size === 0) && currentFiles.find(f => f.relativePath === utils.convertSlashesForSqlProj(filePath))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1040,12 +1074,22 @@ export class Project implements ISqlProject {
|
||||
throw new Error(constants.unableToFindObject(path, constants.fileObject));
|
||||
}
|
||||
|
||||
private removeNode(includeString: string, nodes: HTMLCollectionOf<Element>): boolean {
|
||||
/**
|
||||
* Deletes a node from the project file similar to <Compile Include="{includeString}" />
|
||||
* @param includeString Path of the file that matches the Include portion of the node
|
||||
* @param nodes The collection of XML nodes to search from
|
||||
* @param undoRemove When true, will remove a node similar to <Compile Remove="{includeString}" />
|
||||
* @returns True when a node has been removed, false otherwise.
|
||||
*/
|
||||
private removeNode(includeString: string, nodes: HTMLCollectionOf<Element>, undoRemove: boolean = false): boolean {
|
||||
// Default function behavior removes nodes like <Compile Include="..." />
|
||||
// However when undoRemove is true, this function removes <Compile Remove="..." />
|
||||
const xmlAttribute = undoRemove ? constants.Remove : constants.Include;
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const parent = nodes[i].parentNode;
|
||||
|
||||
if (parent) {
|
||||
if (nodes[i].getAttribute(constants.Include) === utils.convertSlashesForSqlProj(includeString)) {
|
||||
if (nodes[i].getAttribute(xmlAttribute) === utils.convertSlashesForSqlProj(includeString)) {
|
||||
parent.removeChild(nodes[i]);
|
||||
|
||||
// delete ItemGroup if this was the only entry
|
||||
@@ -1064,6 +1108,18 @@ export class Project implements ISqlProject {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a Remove node from the sqlproj, ex: <Build Remove="Table1.sql" />
|
||||
* @param xmlTag The XML tag of the node (Build, None, PreDeploy, PostDeploy)
|
||||
* @param relativePath The relative path of the previously excluded file
|
||||
*/
|
||||
private async undoExcludeFileFromProjFile(xmlTag: string, relativePath: string): Promise<void> {
|
||||
const nodes = this.projFileXmlDoc!.documentElement.getElementsByTagName(xmlTag);
|
||||
if (await this.removeNode(relativePath, nodes, true)) {
|
||||
await this.serializeToProjFile(this.projFileXmlDoc!);
|
||||
}
|
||||
}
|
||||
|
||||
private async addFolderToProjFile(folderPath: string): Promise<void> {
|
||||
if (this.isSdkStyleProject) {
|
||||
// if there's a folder entry for the folder containing this folder, remove it from the sqlproj because the folder will now be
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import * as should from 'should';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import * as sinon from 'sinon';
|
||||
import * as baselines from './baselines/baselines';
|
||||
import * as templates from '../templates/templates';
|
||||
@@ -826,6 +827,38 @@ describe('Project: sqlproj content operations', function (): void {
|
||||
{ type: EntryType.Folder, relativePath: 'foo\\bar\\' },
|
||||
{ type: EntryType.File, relativePath: 'foo\\bar\\test.sql' }]);
|
||||
});
|
||||
|
||||
it('Should handle adding existing items to project', async function (): Promise<void> {
|
||||
// Create new sqlproj
|
||||
projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline);
|
||||
const projectFolder = path.dirname(projFilePath);
|
||||
|
||||
// Create 2 new files, a sql file and a txt file
|
||||
const sqlFile = path.join(projectFolder, 'test.sql');
|
||||
const txtFile = path.join(projectFolder, 'foo', 'test.txt');
|
||||
await fs.writeFile(sqlFile, '');
|
||||
await fs.mkdir(path.dirname(txtFile));
|
||||
await fs.writeFile(txtFile, '');
|
||||
|
||||
const project: Project = await Project.openProject(projFilePath);
|
||||
|
||||
// Add them as existing files
|
||||
await project.addExistingItem(sqlFile);
|
||||
await project.addExistingItem(txtFile);
|
||||
|
||||
// Validate files should have been added to project
|
||||
should(project.files.length).equal(3, 'Three entries are expected in the project');
|
||||
should(project.files.map(f => ({ type: f.type, relativePath: f.relativePath })))
|
||||
.containDeep([
|
||||
{ type: EntryType.Folder, relativePath: 'foo\\' },
|
||||
{ type: EntryType.File, relativePath: 'test.sql' },
|
||||
{ type: EntryType.File, relativePath: 'foo\\test.txt' }]);
|
||||
|
||||
// Validate project file XML
|
||||
const projFileText = (await fs.readFile(projFilePath)).toString();
|
||||
should(projFileText.includes('<Build Include="test.sql" />')).equal(true, projFileText);
|
||||
should(projFileText.includes('<None Include="foo\\test.txt" />')).equal(true, projFileText);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Project: sdk style project content operations', function (): void {
|
||||
@@ -1397,6 +1430,60 @@ describe('Project: sdk style project content operations', function (): void {
|
||||
should(project.projectGuid).not.equal(undefined);
|
||||
should(projFileText.includes(constants.ProjectGuid)).equal(true);
|
||||
});
|
||||
|
||||
it('Should handle adding existing items to project', async function (): Promise<void> {
|
||||
projFilePath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectBaseline);
|
||||
const projectFolder = path.dirname(projFilePath);
|
||||
|
||||
// Create a sql file inside project root
|
||||
const sqlFile = path.join(projectFolder, 'test.sql');
|
||||
await fs.writeFile(sqlFile, '');
|
||||
|
||||
const project: Project = await Project.openProject(projFilePath);
|
||||
|
||||
// Add it as existing file
|
||||
await project.addExistingItem(sqlFile);
|
||||
|
||||
// Validate it has been added to project
|
||||
should(project.files.length).equal(1, 'Only one entry is expected in the project');
|
||||
const sqlFileEntry = project.files.find(f => f.type === EntryType.File && f.relativePath === 'test.sql');
|
||||
should(sqlFileEntry).not.equal(undefined);
|
||||
|
||||
// Validate project XML should not have changed as the file falls under default glob
|
||||
let projFileText = (await fs.readFile(projFilePath)).toString();
|
||||
should(projFileText.includes('<Build Include="test.sql" />')).equal(false, projFileText);
|
||||
|
||||
// Exclude this file, verify the <Build Remove=...> is added
|
||||
await project.exclude(sqlFileEntry!);
|
||||
should(project.files.length).equal(0, 'Project should not have any files remaining.');
|
||||
projFileText = (await fs.readFile(projFilePath)).toString();
|
||||
should(projFileText.includes('<Build Remove="test.sql" />')).equal(true, projFileText);
|
||||
|
||||
// Add the file back, verify the <Build Remove=...> is no longer there
|
||||
await project.addExistingItem(sqlFile);
|
||||
projFileText = (await fs.readFile(projFilePath)).toString();
|
||||
should(projFileText.includes('<Build Remove="test.sql" />')).equal(false, projFileText);
|
||||
should(projFileText.includes('<Build Include="test.sql" />')).equal(false, projFileText);
|
||||
|
||||
// Now create a txt file and add it to sqlproj
|
||||
const txtFile = path.join(projectFolder, 'test.txt');
|
||||
await fs.writeFile(txtFile, '');
|
||||
await project.addExistingItem(txtFile);
|
||||
|
||||
// Validate the txt file is added as <None Include=...>
|
||||
should(project.files.find(f => f.type === EntryType.File && f.relativePath === 'test.txt')).not.equal(undefined);
|
||||
projFileText = (await fs.readFile(projFilePath)).toString();
|
||||
should(projFileText.includes('<None Include="test.txt" />')).equal(true, projFileText);
|
||||
|
||||
// Test with a sql file that's outside project root
|
||||
const externalSqlFile = path.join(os.tmpdir(), `Test_${new Date().getTime()}.sql`);
|
||||
const externalFileRelativePath = convertSlashesForSqlProj(path.relative(projectFolder, externalSqlFile));
|
||||
await fs.writeFile(externalSqlFile, '');
|
||||
await project.addExistingItem(externalSqlFile);
|
||||
should(project.files.find(f => f.type === EntryType.File && f.relativePath === externalFileRelativePath)).not.equal(undefined);
|
||||
projFileText = (await fs.readFile(projFilePath)).toString();
|
||||
should(projFileText.includes(`<Build Include="${externalFileRelativePath}" />`)).equal(true, projFileText);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Project: add SQLCMD Variables', function (): void {
|
||||
|
||||
Reference in New Issue
Block a user