Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions messages/delete.source.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ This operation will delete the following metadata in your org:
This operation will deploy the following:
%s

# cascadeDeleteWarning

When you delete components of type "%s", the org also deletes the following related metadata (known as a "cascade delete"): %s.

# areYouSure

Are you sure you want to proceed?
Expand Down
26 changes: 26 additions & 0 deletions src/commands/project/delete/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,27 @@ Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'delete.source');
const xorFlags = ['metadata', 'source-dir'];

/**
* Metadata types that trigger cascade deletion in the org.
*/
const CASCADE_DELETE_TYPES: Record<string, string[]> = {
AiAuthoringBundle: ['Bot, BotVersion, GenAiPlannerBundle'],
};

/**
* Returns warning messages for components that trigger cascade deletion in the org.
*/
const getCascadeDeleteWarnings = (typesBeingDeleted: Set<string>): string[] => {
const warnings: string[] = [];
for (const typeName of typesBeingDeleted) {
const cascadeTypes = CASCADE_DELETE_TYPES[typeName];
if (cascadeTypes?.length) {
warnings.push(messages.getMessage('cascadeDeleteWarning', [typeName, cascadeTypes.join(', ')]));
}
}
return warnings;
};

type MixedDeployDelete = { deploy: string[]; delete: FileResponseSuccess[] };
export class Source extends SfCommand<DeleteSourceJson> {
public static readonly summary = messages.getMessage('summary');
Expand Down Expand Up @@ -459,6 +480,9 @@ Update the .forceignore file and try again.`);
)
.concat(this.mixedDeployDelete.delete.map((fr) => `${fr.fullName} (${fr.filePath})`));

const typesBeingDeleted = new Set((this.components ?? []).map((comp) => comp.type.name));
const cascadeWarnings = getCascadeDeleteWarnings(typesBeingDeleted);

const message: string[] = [
...(this.mixedDeployDelete.deploy.length
? [messages.getMessage('deployPrompt', [[...new Set(this.mixedDeployDelete.deploy)].join('\n')])]
Expand All @@ -470,6 +494,8 @@ Update the .forceignore file and try again.`);
...(local.length && (this.mixedDeployDelete.deploy.length || remote.length) ? ['\n'] : []),
...(local.length ? [messages.getMessage('localPrompt', [[...new Set(local)].join('\n')])] : []),

...(cascadeWarnings.length ? [...cascadeWarnings] : []),

this.flags['check-only'] ?? false
? messages.getMessage('areYouSureCheckOnly')
: messages.getMessage('areYouSure'),
Expand Down
38 changes: 37 additions & 1 deletion test/commands/delete/source.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ const agentComponents: SourceComponent[] = [
}),
];

// Component with type that triggers cascade delete warning (AiAuthoringBundle)
const aiAuthoringBundleComponent = new SourceComponent({
name: 'MyAiBundle',
type: registry.getTypeByName('AiAuthoringBundle'),
xml: '/dreamhouse-lwc/force-app/main/default/aiAuthoringBundles/MyAiBundle.agent-meta.xml',
});

export const exampleDeleteResponse = {
// required but ignored by the delete UT
getFileResponses: (): void => {},
Expand Down Expand Up @@ -168,6 +175,7 @@ describe('project delete source', () => {
options?: {
sourceApiVersion?: string;
inquirerMock?: { checkbox: sinon.SinonStub };
captureConfirmMessage?: { ref: { message: string } };
}
) => {
const cmd = new TestDelete(params, oclifConfigStub);
Expand All @@ -192,7 +200,15 @@ describe('project delete source', () => {
onCancel: () => {},
onError: () => {},
});
handlePromptStub = stubMethod($$.SANDBOX, cmd, 'handlePrompt').returns(confirm);
if (options?.captureConfirmMessage) {
const messageRef = options.captureConfirmMessage.ref;
stubMethod($$.SANDBOX, SfCommand.prototype, 'confirm').callsFake(async (opts: { message: string }) => {
messageRef.message = opts.message;
return true;
});
} else {
handlePromptStub = stubMethod($$.SANDBOX, cmd, 'handlePrompt').returns(confirm);
}
if (options?.inquirerMock) {
// @ts-expect-error stubbing private member of the command
cmd.inquirer = options.inquirerMock;
Expand Down Expand Up @@ -371,4 +387,24 @@ describe('project delete source', () => {
});
ensureHookArgs();
});

it('should include cascade delete warning in prompt when deleting AiAuthoringBundle', async () => {
buildComponentSetStub.restore();
buildComponentSetStub = stubMethod($$.SANDBOX, ComponentSetBuilder, 'build').resolves({
toArray: () => [aiAuthoringBundleComponent],
forceIgnoredPaths: undefined,
apiVersion: '65.0',
sourceApiVersion: '65.0',
});

const captured = { message: '' };
await runDeleteCmd(['--metadata', 'AiAuthoringBundle:MyAiBundle', '--json'], {
captureConfirmMessage: { ref: captured },
});

expect(captured.message).to.include('AiAuthoringBundle');
expect(captured.message).to.include('cascade');
expect(captured.message).to.include('Bot, BotVersion, GenAiPlannerBundle');
ensureHookArgs();
});
});
Loading