diff --git a/packages/salesforce/src/__tests__/salesforcePackageBuilder.test.ts b/packages/salesforce/src/__tests__/salesforcePackageBuilder.test.ts index b2f7c9d2..6ba60daa 100644 --- a/packages/salesforce/src/__tests__/salesforcePackageBuilder.test.ts +++ b/packages/salesforce/src/__tests__/salesforcePackageBuilder.test.ts @@ -61,6 +61,9 @@ describe('SalesforcePackageBuilder', () => { 'src/destructiveChangesPre.xml': buildXml('Package', { types: [ { name: 'ApexClass', members: [ 'c', 'd' ] } ] }), 'src/destructiveChanges.xml': buildXml('Package', { types: [ { name: 'ApexClass', members: [ 'g', 'h' ] } ] }), 'src/destructiveChangesSingle.xml': buildXml('Package', { types: [ { name: 'ApexClass', members: 'a' } ] }), + // Dashboards + 'src/dashboards/MyFolder.dashboardFolder-meta.xml': buildXml('DashboardFolder', { name: 'MyFolder', accessType: 'Public', publicFolderAccess: 'ReadWrite' }), + 'src/dashboards/MyFolder/Board.dashboard-meta.xml': buildXml('Dashboard', { name: 'Board' }), }); beforeAll(() => container.registerAs(Logger.null, Logger)); @@ -473,7 +476,7 @@ describe('SalesforcePackageBuilder', () => { 'src/classes/myClass.cls-meta.xml', 'src/triggers/myTrigger.trigger-meta.xml', ])); - expect(manifest.list().length).toEqual(7); + expect(manifest.list().length).toEqual(9); expect(manifest.list('AuraDefinitionBundle').length).toEqual(1); expect(manifest.list('LightningComponentBundle').length).toEqual(1); expect(manifest.list('ApexClass').length).toEqual(1); @@ -481,6 +484,21 @@ describe('SalesforcePackageBuilder', () => { expect(manifest.list('CustomObject').length).toEqual(1); expect(manifest.list('CustomField').length).toEqual(1); expect(manifest.list('ListView').length).toEqual(1); + expect(manifest.list('Dashboard').length).toEqual(2); + }); + }); + describe('#dashboards', () => { + it('should add dashboards folders with -meta.xml suffix', async () => { + const packageBuilder = new SalesforcePackageBuilder(SalesforcePackageType.deploy, apiVersion, mockFs); + await packageBuilder.addFiles([ 'src/dashboards' ]); + + const manifest = packageBuilder.getManifest(); + const [ folder, dashBoard ] = [...packageBuilder.getPackage().sourceFiles()]; + + expect(folder.packagePath).toEqual('dashboards/MyFolder-meta.xml'); + expect(dashBoard.packagePath).toEqual('dashboards/MyFolder/Board.dashboard'); + expect(manifest.list().length).toEqual(2); + expect(manifest.list('Dashboard').length).toEqual(2); }); }); }); diff --git a/packages/salesforce/src/deploymentPackageBuilder.ts b/packages/salesforce/src/deploymentPackageBuilder.ts index f1b74f64..d81ab4ca 100644 --- a/packages/salesforce/src/deploymentPackageBuilder.ts +++ b/packages/salesforce/src/deploymentPackageBuilder.ts @@ -3,7 +3,7 @@ import chalk from 'chalk'; import ZipArchive from 'jszip'; import { Logger, injectable , LifecyclePolicy, CachedFileSystemAdapter , FileSystem, Container } from '@vlocode/core'; -import { cache, substringAfterLast , Iterable, XML, CancellationToken, FileSystemUri } from '@vlocode/util'; +import { cache, substringAfterLast , Iterable, XML, CancellationToken, FileSystemUri, endsWith, substringBeforeLast, stringEqualsIgnoreCase, stringEquals } from '@vlocode/util'; import { PackageManifest } from './deploy/packageXml'; import { SalesforcePackage, SalesforcePackageComponent } from './deploymentPackage'; @@ -88,9 +88,14 @@ export class SalesforcePackageBuilder { // get metadata type const xmlName = await this.getComponentType(file); - if (xmlName == 'Package' && path.basename(file).includes('destructive')) { - const destructiveChangeType = file.toLocaleLowerCase().includes('post') ? 'post' : 'pre'; - this.mdPackage.mergeDestructiveChanges(await this.fs.readFile(file), destructiveChangeType); + if (xmlName == 'Package') { + const lowercasePath = file.toLocaleLowerCase(); + if (!path.basename(lowercasePath).includes('destructive')) { + this.logger.warn(`${file} is a Package manifest which is not supported by the package builder`); + } else { + const destructiveChangeType = lowercasePath.includes('post') ? 'post' : 'pre'; + this.mdPackage.mergeDestructiveChanges(await this.fs.readFile(file), destructiveChangeType); + } continue; } @@ -104,13 +109,14 @@ export class SalesforcePackageBuilder { continue; } - if (metadataType.name != xmlName) { + const isFolderMetadata = stringEquals(metadataType.folderType, xmlName, { caseInsensitive: true }); + if (metadataType.name != xmlName && !isFolderMetadata) { // Support for SFDX formatted source code childMetadataFiles.push([ file, xmlName, metadataType]); continue; } - if (metadataType.id == 'staticresource' && file.endsWith('-meta.xml')) { + if (metadataType.id === 'staticresource' && file.endsWith('-meta.xml')) { const folder = this.stripFileExtension(file, 2); const isFolder = await this.fs.isDirectory(folder); if (isFolder) { @@ -236,12 +242,14 @@ export class SalesforcePackageBuilder { componentName = this.getPackageComponentName(file, metadataType); } + const componentType = await this.getPackageComponentType(file, metadataType); + if (this.type === SalesforcePackageType.destruct) { - this.mdPackage.addDestructiveChange(metadataType.name, componentName); + this.mdPackage.addDestructiveChange(componentType, componentName); } else { const packagePath = await this.getPackagePath(file, metadataType); this.mdPackage.add({ - componentType: metadataType.name, + componentType, componentName, packagePath, data: await this.fs.readFile(file), @@ -523,7 +531,7 @@ export class SalesforcePackageBuilder { return suffix?.toLowerCase() != 'xml' ? suffix : undefined; } - private async getComponentTypeFromSource(file: string) { + private async getComponentTypeFromSource(file: string) : Promise{ const isMetaFile = file.endsWith('-meta.xml'); if (!isMetaFile) { @@ -535,20 +543,19 @@ export class SalesforcePackageBuilder { } const metadataTypes = this.metadataRegistry.getMetadataTypes(); - let xmlName = await this.getRootElementName(file); + const xmlName = await this.getRootElementName(file); // Cannot detect certain metadata types properly so instead manually set the type - if (xmlName == 'EmailFolder') { - xmlName = 'EmailTemplate'; - } else if (xmlName && xmlName.endsWith('Folder')) { - // Handles document Folder and other folder cases - xmlName = xmlName.substr(0, xmlName.length - 6); - } else if (xmlName == 'Package') { + if (xmlName == 'Package') { // Package are considered valid types for destructive changes return xmlName; } - const metadataType = xmlName && metadataTypes.find(type => type.name == xmlName || type.childXmlNames?.includes(xmlName!)); + const metadataType = xmlName ? metadataTypes.find(type => + type.name == xmlName || + type.childXmlNames?.includes(xmlName!) + ) : undefined + if (metadataType) { return xmlName; } @@ -614,6 +621,11 @@ export class SalesforcePackageBuilder { const packageFolder = this.getPackageFolder(file, metadataType); const expectedSuffix = isMetaFile ? `${metadataType.suffix}-meta.xml` : `${metadataType.suffix}`; + if (metadataType.folderContentType) { + // For folder metadata the meta file name should match the folder name + return path.posix.join(packageFolder, `${substringBeforeLast(contentName,'.')}-meta.xml`); + } + if (isMetaFile && !metadataType.hasContent && !metadataType.isBundle) { // SFDX adds a '-meta.xml' to each file, when deploying we need to strip these // when the source does not have a meta data file @@ -636,6 +648,13 @@ export class SalesforcePackageBuilder { return path.posix.join(packageFolder, baseName); } + private async getPackageComponentType(file: string, metadataType: MetadataType) { + if (metadataType.folderContentType) { + return this.metadataRegistry.getMetadataType(metadataType.folderContentType)!.name; + } + return metadataType.name; + } + @cache({ unwrapPromise: true }) private async findContentFile(metaFile: string) { const metaFileSuggestedContentFile = metaFile.slice(0, -9); diff --git a/packages/util/src/string.ts b/packages/util/src/string.ts index f6134e0a..90f83b4d 100644 --- a/packages/util/src/string.ts +++ b/packages/util/src/string.ts @@ -7,7 +7,7 @@ import { getObjectProperty } from './object'; * @param b String b * @param caseInsensitive Wether or not to do a case insensitive or case-sensitive comparison */ -export function stringEquals(a : string | undefined | null, b: string | undefined | null, caseInsensitive: boolean = true) : boolean { +export function stringEquals(a : string | undefined | null, b: string | undefined | null, options?: { caseInsensitive: boolean } | boolean) : boolean { if (a === b) { return true; } @@ -17,7 +17,7 @@ export function stringEquals(a : string | undefined | null, b: string | undefine if (b === null || b === undefined) { return false; } - if (caseInsensitive) { + if (options === undefined || options === true || (options && options.caseInsensitive)) { return b.toLowerCase() === a.toLowerCase(); } return false; @@ -45,22 +45,25 @@ export function stringEqualsIgnoreCase(a : string | undefined | null, b: string } /** - * Determines if the string spcified ends with the other string, caseInsensitive by default + * Checks if the specified string {@link a} ends with the specified string {@link based}. + * By default the comparison is case sensitive unless specified otherwise by setting the `caseInsensitive` option to `true`. + * If either string is null or undefined returns `false`. * @param a String a * @param b String b - * @param caseInsensitive Wether or not to do a case insensitive or case-sensitive comparison + * @param options Options + * @param options.caseInsensitive Wether or not to do a case insensitive or case-sensitive comparison */ -export function endsWith(a : string | undefined | null, b: string | undefined | null, caseInsensitive: boolean = true) : boolean { +export function endsWith(a: string | undefined | null, b: string | undefined | null, options?: { caseInsensitive: boolean }): boolean { if (a === null || a === undefined) { return false; } if (b === null || b === undefined) { return false; } - if (caseInsensitive) { - return b.toLowerCase().endsWith(a.toLowerCase()); + if (options?.caseInsensitive) { + return a.toLowerCase().endsWith(b.toLowerCase()); } - return b.endsWith(a); + return a.endsWith(b); } export function format(formatStr: string, ...args: any[]) {