Skip to content

Commit b9edd15

Browse files
fix: deletes singular CL when from CLs when specified
* fix: deletes singular CL when from CLs when specified * test: fix logic caught by UT * test: move NUT to it's own describe * test: use TestSession * test: create and set default SO * test: devhub auth - auto * test: if you're asserting on json, use the --json flag * test: use my fork for consistency * chore: nicely handle last CL delete, allow retrieve:start to only delete, print CL/line * test: remove deprecated command from NUTs * chore: use method in STLgp * chore: consume new STL changes * chore: optimize a bit more * chore: qa package for STL * chore: fix delete source output, errors * chore: bump stl, consume label delete return type * chore: bump stl --------- Co-authored-by: mshanemc <[email protected]>
1 parent 8958a14 commit b9edd15

File tree

13 files changed

+809
-919
lines changed

13 files changed

+809
-919
lines changed

CHANGELOG.md

Lines changed: 163 additions & 489 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
"dependencies": {
88
"@oclif/core": "^2.8.2",
99
"@salesforce/apex-node": "^1.6.0",
10-
"@salesforce/core": "^3.36.0",
10+
"@salesforce/core": "^3.36.1",
1111
"@salesforce/kit": "^1.9.2",
1212
"@salesforce/sf-plugins-core": "^2.4.2",
13-
"@salesforce/source-deploy-retrieve": "^8.0.2",
14-
"@salesforce/source-tracking": "^3.1.0",
13+
"@salesforce/source-deploy-retrieve": "^8.4.0",
14+
"@salesforce/source-tracking": "^3.1.5",
1515
"chalk": "^4.1.2",
1616
"fs-extra": "^10.0.1",
1717
"shelljs": "^0.8.5",
@@ -270,4 +270,4 @@
270270
"output": []
271271
}
272272
}
273-
}
273+
}

src/commands/project/delete/source.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
SourceComponent,
2323
} from '@salesforce/source-deploy-retrieve';
2424
import { Duration } from '@salesforce/kit';
25-
import { ChangeResult, ConflictResponse, SourceTracking } from '@salesforce/source-tracking';
25+
import { ChangeResult, ConflictResponse, deleteCustomLabels, SourceTracking } from '@salesforce/source-tracking';
2626
import {
2727
arrayWithDeprecation,
2828
Flags,
@@ -341,7 +341,14 @@ export class Source extends SfCommand<DeleteSourceJson> {
341341

342342
private async deleteFilesLocally(): Promise<void> {
343343
if (!this.flags['check-only'] && this.deployResult?.response?.status === RequestStatus.Succeeded) {
344-
const promises: Array<Promise<void>> = [];
344+
const promises: Array<Promise<void> | ReturnType<typeof deleteCustomLabels>> = [];
345+
const customLabels = this.componentSet
346+
.getSourceComponents()
347+
.toArray()
348+
.filter((comp) => comp.type.id === 'customlabel');
349+
if (customLabels.length && customLabels[0].xml) {
350+
promises.push(deleteCustomLabels(customLabels[0].xml, customLabels));
351+
}
345352
this.components?.filter(isSourceComponent).map((component: SourceComponent) => {
346353
// mixed delete/deploy operations have already been deleted and stashed
347354
if (!this.mixedDeployDelete.delete.length) {
@@ -354,7 +361,10 @@ export class Source extends SfCommand<DeleteSourceJson> {
354361
}
355362
}
356363
if (component.xml) {
357-
promises.push(fsPromises.unlink(component.xml));
364+
if (component.type.id !== 'customlabel') {
365+
// CustomLabels handled as a special case above
366+
promises.push(fsPromises.unlink(component.xml));
367+
}
358368
}
359369
}
360370
});
@@ -408,7 +418,12 @@ export class Source extends SfCommand<DeleteSourceJson> {
408418

409419
this.components?.flatMap((component) => {
410420
if (component instanceof SourceComponent) {
411-
local.push(component.xml as string, ...component.walkContent());
421+
if (component.type.name === 'CustomLabel') {
422+
// for custom labels, print each custom label to be deleted, not the whole file
423+
local.push(`${component.type.name}:${component.fullName}`);
424+
} else {
425+
local.push(component.xml as string, ...component.walkContent());
426+
}
412427
} else {
413428
// remote only metadata
414429
remote.push(`${component.type.name}:${component.fullName}`);

src/commands/project/retrieve/start.ts

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
RetrieveSetOptions,
1616
ComponentSet,
1717
FileResponse,
18+
MetadataApiRetrieveStatus,
1819
} from '@salesforce/source-deploy-retrieve';
1920
import { SfCommand, toHelpSection, Flags } from '@salesforce/sf-plugins-core';
2021
import { getString } from '@salesforce/ts-types';
@@ -151,40 +152,45 @@ export default class RetrieveMetadata extends SfCommand<RetrieveResultJson> {
151152
componentSetFromNonDeletes.sourceApiVersion ?? componentSetFromNonDeletes.apiVersion,
152153
]);
153154

154-
const retrieve = await componentSetFromNonDeletes.retrieve(retrieveOpts);
155+
this.retrieveResult = new RetrieveResult({} as MetadataApiRetrieveStatus, componentSetFromNonDeletes);
155156

156-
this.spinner.status = messages.getMessage('spinner.polling');
157+
if (componentSetFromNonDeletes.size !== 0) {
158+
const retrieve = await componentSetFromNonDeletes.retrieve(retrieveOpts);
159+
this.spinner.status = messages.getMessage('spinner.polling');
157160

158-
retrieve.onUpdate((data) => {
159-
this.spinner.status = mdTransferMessages.getMessage(data.status);
160-
});
161-
// any thing else should stop the progress bar
162-
retrieve.onFinish((data) => this.spinner.stop(mdTransferMessages.getMessage(data.response.status)));
163-
retrieve.onCancel((data) => this.spinner.stop(mdTransferMessages.getMessage(data?.status ?? 'Canceled')));
164-
retrieve.onError((error: Error) => {
165-
this.spinner.stop(error.name);
166-
throw error;
167-
});
161+
retrieve.onUpdate((data) => {
162+
this.spinner.status = mdTransferMessages.getMessage(data.status);
163+
});
164+
// any thing else should stop the progress bar
165+
retrieve.onFinish((data) => this.spinner.stop(mdTransferMessages.getMessage(data.response.status)));
166+
retrieve.onCancel((data) => this.spinner.stop(mdTransferMessages.getMessage(data?.status ?? 'Canceled')));
167+
retrieve.onError((error: Error) => {
168+
this.spinner.stop(error.name);
169+
throw error;
170+
});
171+
172+
await retrieve.start();
173+
this.retrieveResult = await retrieve.pollStatus(500, flags.wait.seconds);
174+
}
168175

169-
await retrieve.start();
170-
const result = await retrieve.pollStatus(500, flags.wait.seconds);
171176
this.spinner.stop();
172177

173178
// reference the flag instead of `format` so we get correct type
174179
const formatter = flags['target-metadata-dir']
175-
? new MetadataRetrieveResultFormatter(result, {
180+
? new MetadataRetrieveResultFormatter(this.retrieveResult, {
176181
'target-metadata-dir': flags['target-metadata-dir'],
177182
'zip-file-name': zipFileName,
178183
unzip: flags.unzip,
179184
})
180-
: new RetrieveResultFormatter(result, flags['package-name'], fileResponsesFromDelete);
185+
: new RetrieveResultFormatter(this.retrieveResult, flags['package-name'], fileResponsesFromDelete);
181186
if (!this.jsonEnabled()) {
182-
if (result.response.status === 'Succeeded') {
187+
// in the case where we didn't retrieve anything, check if we have any deletes
188+
if (this.retrieveResult.response.status === 'Succeeded' || fileResponsesFromDelete.length !== 0) {
183189
await formatter.display();
184190
} else {
185191
throw new SfError(
186-
getString(result.response, 'errorMessage', result.response.status),
187-
getString(result.response, 'errorStatusCode', 'unknown')
192+
getString(this.retrieveResult.response, 'errorMessage', this.retrieveResult.response.status),
193+
getString(this.retrieveResult.response, 'errorStatusCode', 'unknown')
188194
);
189195
}
190196
}

src/formatters/deleteResultFormatter.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
*/
77
import { ux } from '@oclif/core';
88
import * as chalk from 'chalk';
9-
import { DeployResult, FileResponse } from '@salesforce/source-deploy-retrieve';
9+
import { DeployResult, FileResponse, RequestStatus } from '@salesforce/source-deploy-retrieve';
1010
import { ensureArray } from '@salesforce/kit';
11+
import { bold } from 'chalk';
12+
import { StandardColors } from '@salesforce/sf-plugins-core';
1113
import { DeleteSourceJson, Formatter } from '../utils/types';
1214
import { sortFileResponses, asRelativePaths } from '../utils/output';
1315

@@ -74,6 +76,28 @@ export class DeleteResultFormatter implements Formatter<DeleteSourceJson> {
7476
filePath: { header: 'PROJECT PATH' },
7577
}
7678
);
79+
} else {
80+
this.displayFailures();
7781
}
7882
}
83+
84+
private displayFailures(): void {
85+
if (this.result.response.status === RequestStatus.Succeeded) return;
86+
87+
const failures = ensureArray(this.result.response.details.componentFailures);
88+
if (!failures.length) return;
89+
90+
const columns = {
91+
problemType: { header: 'Type' },
92+
fullName: { header: 'Name' },
93+
error: { header: 'Problem' },
94+
};
95+
const options = { title: StandardColors.error(bold(`Component Failures [${failures.length}]`)) };
96+
ux.log();
97+
ux.table(
98+
failures.map((f) => ({ problemType: f.problemType, fullName: f.fullName, error: f.problem })),
99+
columns,
100+
options
101+
);
102+
}
79103
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"orgName": "ACME",
3+
"edition": "Developer",
4+
"features": [],
5+
"settings": {}
6+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<CustomLabels xmlns="http://soap.sforce.com/2006/04/metadata">
3+
<labels>
4+
<fullName>DeleteMe</fullName>
5+
<language>en_US</language>
6+
<protected>true</protected>
7+
<shortDescription>DeleteMe</shortDescription>
8+
<value>Test</value>
9+
</labels>
10+
<labels>
11+
<fullName>KeepMe1</fullName>
12+
<language>en_US</language>
13+
<protected>true</protected>
14+
<shortDescription>KeepMe1</shortDescription>
15+
<value>Test</value>
16+
</labels>
17+
<labels>
18+
<fullName>KeepMe2</fullName>
19+
<language>en_US</language>
20+
<protected>true</protected>
21+
<shortDescription>KeepMe2</shortDescription>
22+
<value>Test</value>
23+
</labels>
24+
</CustomLabels>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"packageDirectories": [
3+
{
4+
"path": "force-app",
5+
"default": true
6+
}
7+
],
8+
"namespace": "",
9+
"sfdcLoginUrl": "https://login.salesforce.com",
10+
"sourceApiVersion": "57.0"
11+
}

test/nuts/delete/source.nut.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import * as fs from 'fs';
99
import * as path from 'path';
1010
import { expect } from 'chai';
11-
import { execCmd } from '@salesforce/cli-plugins-testkit';
11+
import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit';
1212
import { SourceTestkit } from '@salesforce/source-testkit';
1313
import { exec } from 'shelljs';
1414
import { FileResponse } from '@salesforce/source-deploy-retrieve';
@@ -28,6 +28,62 @@ const isNameObsolete = async (username: string, memberType: string, memberName:
2828
return res.IsNameObsolete;
2929
};
3030

31+
describe('CustomLabels', () => {
32+
let testkit: TestSession;
33+
34+
before(async () => {
35+
testkit = await TestSession.create({
36+
project: {
37+
gitClone: 'https://github.com/WillieRuemmele/sfdx-delete-customlabel',
38+
},
39+
scratchOrgs: [{ setDefault: true, config: path.join('config', 'project-scratch-def.json') }],
40+
devhubAuthStrategy: 'AUTO',
41+
});
42+
execCmd('force:source:deploy --sourcepath force-app', { ensureExitCode: 0 });
43+
});
44+
45+
after(async () => {
46+
await testkit?.clean();
47+
});
48+
it('will not delete the entire .xml file', () => {
49+
const clPath = path.join(
50+
testkit.project.dir,
51+
'force-app',
52+
'main',
53+
'default',
54+
'labels',
55+
'CustomLabels.labels-meta.xml'
56+
);
57+
const result = execCmd<DeleteSourceJson>(
58+
'project:delete:source --json --no-prompt --metadata CustomLabel:DeleteMe',
59+
{
60+
ensureExitCode: 0,
61+
}
62+
).jsonOutput?.result;
63+
expect(fs.existsSync(clPath)).to.be.true;
64+
expect(fs.readFileSync(clPath, 'utf8')).to.not.contain('<fullName>DeleteMe</fullName>');
65+
expect(fs.readFileSync(clPath, 'utf8')).to.contain('<fullName>KeepMe1</fullName>');
66+
expect(fs.readFileSync(clPath, 'utf8')).to.contain('<fullName>KeepMe2</fullName>');
67+
expect(result?.deletedSource).to.have.length(1);
68+
});
69+
70+
it('will delete the entire .xml file', () => {
71+
const clPath = path.join(
72+
testkit.project.dir,
73+
'force-app',
74+
'main',
75+
'default',
76+
'labels',
77+
'CustomLabels.labels-meta.xml'
78+
);
79+
const result = execCmd<DeleteSourceJson>('project:delete:source --json --no-prompt --metadata CustomLabels', {
80+
ensureExitCode: 0,
81+
}).jsonOutput?.result;
82+
expect(result?.deletedSource).to.have.length(3);
83+
expect(fs.existsSync(clPath)).to.be.false;
84+
});
85+
});
86+
3187
describe('project delete source NUTs', () => {
3288
let testkit: SourceTestkit;
3389

0 commit comments

Comments
 (0)