Skip to content

Commit 4d35ce3

Browse files
authored
fix: append forward slash to folder types manifest entries (#1407)
* chore: lint fixes * fix: update manifest resolver to identify InFolder parents with trailing slashes * fix: only reports and dashboards have this behavior * fix: add EmailTemplateFolder * chore: bump core dep * chore: update yarn.lock
1 parent 40dda6e commit 4d35ce3

File tree

6 files changed

+103
-16
lines changed

6 files changed

+103
-16
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"node": ">=18.0.0"
2626
},
2727
"dependencies": {
28-
"@salesforce/core": "^8.5.2",
28+
"@salesforce/core": "^8.5.4",
2929
"@salesforce/kit": "^3.2.2",
3030
"@salesforce/ts-types": "^2.0.12",
3131
"fast-levenshtein": "^3.0.0",

src/collections/componentSet.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -443,11 +443,7 @@ export class ComponentSet extends LazyCollection<MetadataComponent> {
443443
addToTypeMap({
444444
typeMap,
445445
type: type.folderContentType ? this.registry.getTypeByName(type.folderContentType) : type,
446-
fullName:
447-
this.registry.getParentType(type.name)?.strategies?.recomposition === 'startEmpty' && fullName.includes('.')
448-
? // they're reassembled like CustomLabels.MyLabel
449-
fullName.split('.')[1]
450-
: fullName,
446+
fullName: constructFullName(this.registry, type, fullName),
451447
destructiveType,
452448
});
453449
});
@@ -738,6 +734,17 @@ const splitOnFirstDelimiter = (input: string): [string, string] => {
738734
return [input.substring(0, indexOfSplitChar), input.substring(indexOfSplitChar + 1)];
739735
};
740736

737+
const constructFullName = (registry: RegistryAccess, type: MetadataType, fullName: string): string =>
738+
// Some InFolder types are different. e.g., Report/ReportFolder & Dashboard/DashboardFolder.
739+
// ReportFolders are deployed/retrieved as Reports. If a ReportFolder is being added append
740+
// a "/" so the metadata API can identify it as a folder.
741+
['DashboardFolder', 'ReportFolder', 'EmailTemplateFolder'].includes(type.name) && !fullName.endsWith('/')
742+
? `${fullName}/`
743+
: registry.getParentType(type.name)?.strategies?.recomposition === 'startEmpty' && fullName.includes('.')
744+
? // they're reassembled like CustomLabels.MyLabel
745+
fullName.split('.')[1]
746+
: fullName;
747+
741748
/** side effect: mutates the typeMap property */
742749
const addToTypeMap = ({
743750
typeMap,

src/resolve/manifestResolver.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ export class ManifestResolver {
7171
const type = this.registry.getTypeByName(typeMembers.name);
7272
const parentType = type.folderType ? this.registry.getTypeByName(type.folderType) : undefined;
7373
return ensureArray(typeMembers.members).map((fullName, _index, members) => ({
74-
fullName,
75-
type: parentType && isMemberNestedInFolder(fullName, type, parentType, members) ? parentType : type,
74+
fullName: resolveFullName(fullName, parentType),
75+
type: !parentType ? type : resolveType(fullName, type, members, parentType),
7676
}));
7777
});
7878

@@ -113,6 +113,33 @@ const getValidatedType =
113113
return typeMembers;
114114
};
115115

116+
// Mostly for parents of InFolder types to strip off trailing "/" characters
117+
// in fullNames. Otherwise just returns the fullName.
118+
const resolveFullName = (fullName: string, parentType?: MetadataType): string =>
119+
parentType?.folderContentType && fullName.endsWith('/') ? fullName.substring(0, fullName.length - 1) : fullName;
120+
121+
// Resolve the correct metadata type from metadata entries in a manifest.
122+
// Parents of InFolder types can be detected by looking for a trailing "/"
123+
// character.
124+
const resolveType = (
125+
fullName: string,
126+
type: MetadataType,
127+
members: string[],
128+
parentType?: MetadataType
129+
): MetadataType => {
130+
// Quick short-circuit for non-parent types and non-folderTypes
131+
if (!parentType || !type.folderType) {
132+
return type;
133+
}
134+
135+
// Detect parents of InFolder types by looking for a trailing slash on InFolder types
136+
if (parentType?.folderContentType && fullName.endsWith('/')) {
137+
return parentType;
138+
}
139+
140+
return isMemberNestedInFolder(fullName, type, parentType, members) ? parentType : type;
141+
};
142+
116143
// Use the folderType instead of the type from the manifest when:
117144
// 1. InFolder types: (report, dashboard, emailTemplate, document)
118145
// 1a. type.inFolder === true (from metadataRegistry.json) AND
@@ -129,11 +156,6 @@ const isMemberNestedInFolder = (
129156
parentType: MetadataType,
130157
members: string[]
131158
): boolean => {
132-
// Quick short-circuit for non-folderTypes
133-
if (!type.folderType) {
134-
return false;
135-
}
136-
137159
const isInFolderType = type.inFolder;
138160
const isNestedInFolder = !fullName.includes('/') || members.some((m) => m.includes(`${fullName}/`));
139161
const isNonMatchingFolder = parentType && parentType.folderType !== parentType.id;

test/resolve/manifestResolver.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,64 @@ describe('ManifestResolver', () => {
195195
expect(result.components).to.deep.equal(expected);
196196
});
197197

198+
it('should resolve nested InFolder types with trailing slashes', async () => {
199+
const registry = new RegistryAccess();
200+
const reportType = registry.getTypeByName('report');
201+
const reportFolderType = registry.getTypeByName('reportFolder');
202+
const folderManifest: VirtualFile = {
203+
name: 'reports-package.xml',
204+
data: Buffer.from(`<?xml version="1.0" encoding="UTF-8"?>
205+
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
206+
<types>
207+
<members>foo/</members>
208+
<members>foo/subfoo/</members>
209+
<members>foo/subfoo/MySubFooReport1</members>
210+
<members>foo/subfoo/MySubFooReport2</members>
211+
<members>bar/MyBarReport1</members>
212+
<members>bar/MyBarReport2</members>
213+
<name>Report</name>
214+
</types>
215+
<version>52.0</version>
216+
</Package>\n`),
217+
};
218+
const tree = new VirtualTreeContainer([
219+
{
220+
dirPath: '.',
221+
children: [folderManifest],
222+
},
223+
]);
224+
const resolver = new ManifestResolver(tree);
225+
const result = await resolver.resolve(folderManifest.name);
226+
const expected: MetadataComponent[] = [
227+
{
228+
fullName: 'foo',
229+
type: reportFolderType,
230+
},
231+
{
232+
fullName: 'foo/subfoo',
233+
type: reportFolderType,
234+
},
235+
{
236+
fullName: 'foo/subfoo/MySubFooReport1',
237+
type: reportType,
238+
},
239+
{
240+
fullName: 'foo/subfoo/MySubFooReport2',
241+
type: reportType,
242+
},
243+
{
244+
fullName: 'bar/MyBarReport1',
245+
type: reportType,
246+
},
247+
{
248+
fullName: 'bar/MyBarReport2',
249+
type: reportType,
250+
},
251+
];
252+
253+
expect(result.components).to.deep.equal(expected);
254+
});
255+
198256
it('should resolve folderType types (Territory2*)', async () => {
199257
const registry = new RegistryAccess();
200258
const t2ModelType = registry.getTypeByName('Territory2Model');

test/snapshot/sampleProjects/nestedFolders/__snapshots__/verify-md-files.expected/package.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
<name>EmailTemplate</name>
88
</types>
99
<types>
10-
<members>TopFolder</members>
11-
<members>TopFolder/ChildFolder</members>
10+
<members>TopFolder/</members>
11+
<members>TopFolder/ChildFolder/</members>
1212
<members>TopFolder/ChildFolder/Report_in_Child_Folder_qz4</members>
1313
<members>TopFolder/Copy_of_Top_level_report_DOj</members>
1414
<members>unfiled$public/Top_level_report_cZJ</members>

yarn.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@
523523
strip-ansi "6.0.1"
524524
ts-retry-promise "^0.8.1"
525525

526-
"@salesforce/core@^8.5.2", "@salesforce/core@^8.5.4":
526+
"@salesforce/core@^8.5.4":
527527
version "8.5.4"
528528
resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-8.5.4.tgz#1cdd669462d2c2859b72135d1138a1b790b1fbcc"
529529
integrity sha512-dO8tzFxq811qNPeKPPO2OA2KPYW5rO0YRinW/+7zmRJW3EtNpe93dsQVGwBSAAYrSbYeBwiKdliNqNTN7tKJ0A==

0 commit comments

Comments
 (0)