It is known that we cannot delete components like Apex Classes, Triggers, etc. manually from production. The common approach to delete these components are through Force.com IDE or Force.com Migration tool. In this article we design a tool to delete these components. We build a Visualforce page which creates two XML files named package.xml and destructiveChanges.xml, later compress them and deploy.
To start with include JSZip as a static resource. Now lets create a Visualforce page as below,
How this works ?
The field “Component Type” contain elements like ApexClass, ApexPage, ApexTrigger, etc and in the field “Component” we have to enter the Object Name of the component to be deleted, click on the button “Add” and the component is added to the wrapper list. When confirmed click on the button “Delete”, a javascript function is triggered to add the wrapper list elements to a destructive changes XML string, package XML string is also initialised, both these strings are converted into files and zipped using JSZip. After this an apex Deploy method is invoked from the Visualforce page javascript, we are using MetadataService Class to make the deployment call.
Please use the below code for the Visualforce page and note that in the controller in the method createService we have to update service endpoint with the URL of the organisation. Also add the organisation url in the Remote site settings.
Controller:
[sourcecode language=”java” collapse=”true” title=”DeleteComponentsClass.apxc”]
public class DeleteComponentsClass {
// Initializing elements
public String selectedType{get;set;}
public String componentName{get;set;}
public List<DeleteComponentsWrpClass> componentItems{get;set;}
// Getting picklist elements
public static List<SelectOption> getDataTypes(){
List<String> itemTypes = new List<String>{‘ActionLinkGroupTemplate’, ‘AnalyticSnapshot’, ‘ApexClass’, ‘ApexComponent’, ‘ApexPage’,
‘ApexTrigger’, ‘AppMenu’, ‘ApprovalProcess’, ‘AssignmentRules’, ‘AuraDefinitionBundle’, ‘AuthProvider’, ‘AutoResponseRules’, ‘BusinessProcess’,
‘CallCenter’, ‘ChannelLayout’, ‘Community’, ‘CompactLayout’, ‘CorsWhitelistOrigin’, ‘CustomApplication’, ‘CustomField’, ‘CustomMetadata’,
‘CustomObject’, ‘CustomObjectTranslation’, ‘CustomPageWebLink’, ‘CustomPermission’, ‘CustomSite’, ‘CustomTab’, ‘Dashboard’, ‘Document’,
‘EmailTemplate’, ‘EscalationRules’, ‘ExternalDataSource’, ‘FieldSet’, ‘FlexiPage’, ‘FlowDefinition’, ‘Group’, ‘HomePageComponent’, ‘HomePageLayout’,
‘Layout’, ‘Letterhead’, ‘ListView’, ‘MatchingRule’, ‘NamedCredential’, ‘PermissionSet’, ‘PlatformCachePartition’, ‘Portal’, ‘PostTemplate’,
‘Profile’, ‘Queue’, ‘QuickAction’, ‘RecordType’, ‘RemoteSiteSetting’, ‘Report’, ‘ReportType’, ‘Role’, ‘Scontrol’, ‘Settings’, ‘SharingCriteriaRule’,
‘SharingOwnerRule’, ‘SharingReason’, ‘SharingRules’, ‘SharingSet’, ‘SiteDotCom’, ‘StaticResource’, ‘UserPermissions’, ‘ValidationRule’,
‘WebLink’, ‘Workflow’, ‘WorkflowAlert’, ‘WorkflowFieldUpdate’, ‘WorkflowOutboundMessage’, ‘WorkflowRule’, ‘WorkflowTask’};
List<SelectOption> customTypes = new List<SelectOption>();
customTypes.add(new SelectOption(”,’–None–‘));
for(String item : itemTypes){
customTypes.add(new SelectOption(item,item));
}
return customTypes;
}
// Adding Items to the List
public void add(){
List<String> splitItems = componentName.split(‘;’);
for(String Item : splitItems){
DeleteComponentsWrpClass compItem = new DeleteComponentsWrpClass();
compItem.compName = Item;
compItem.compType = selectedType;
if(componentItems == null){
componentItems = new List<DeleteComponentsWrpClass>();
}
componentItems.add(compItem);
}
componentName = null;
}
// Deploying zip file
@RemoteAction
public static String getDeployContent(String zipData){
MetadataService.MetadataPort service = createService();
MetadataService.DeployOptions deployOptions = new MetadataService.DeployOptions();
deployOptions.allowMissingFiles = false;
deployOptions.autoUpdatePackage = false;
deployOptions.checkOnly = false;
deployOptions.ignoreWarnings = false;
deployOptions.performRetrieve = false;
deployOptions.purgeOnDelete = false;
deployOptions.rollbackOnError = true;
deployOptions.testLevel = ‘NoTestRun’;
deployOptions.singlePackage = true;
MetadataService.AsyncResult AsyncResult = service.deploy(zipData, DeployOptions);
return (‘Success’);
}
// Cancel button action
public PageReference cancel(){
PageReference pageRef = new PageReference(‘/home/home.jsp’);
pageRef.setRedirect(true);
return pageRef;
}
// Delete button action
public PageReference deleteItems(){
Long startTime = System.now().getTime();
Integer delay = 5000;
do {
// Do nothing
}while (System.now().getTime() – startTime < delay);
PageReference pageRef = new PageReference(‘/changemgmt/monitorDeployment.apexp’);
pageRef.setRedirect(true);
return pageRef;
}
// Creating service
private static MetadataService.MetadataPort createService() {
MetadataService.MetadataPort service = new MetadataService.MetadataPort();
service.SessionHeader = new MetadataService.SessionHeader_element();
service.endpoint_x = ‘https://coyg-dev-ed.my.salesforce.com/services/Soap/m/35.0’;
service.timeout_x = 120000;
service.SessionHeader.sessionId = UserInfo.getSessionId();
return service;
}
// Wrapper Class
public class DeleteComponentsWrpClass {
public String compType{get;set;}
public String compName{get;set;}
public DeleteComponentsWrpClass(){
}
}
}
[/sourcecode]
Visualforce page:
[sourcecode language=”html” collapse=”true” title=”DeleteComponents.vfp”]
<apex:page id=”page” controller=”DeleteComponentsClass”>
<apex:includeScript value=”{!$Resource.jsZip}”/>
<script type=”text/javascript”>
var compMap = {};
function contentToMap(key,value) {
compMap[key] = compMap[key] || [];
compMap[key].push(value);
}
function getRemoteContent(){
var keys = Object.keys(compMap);
if(keys.length > 0){
var packageXml = ‘<?xml version=”1.0″ encoding=”UTF-8″?>’ +
‘<Package xmlns=”http://soap.sforce.com/2006/04/metadata”>’ +
‘<version>35.0</version>’ +
‘</Package>’ ;
var destructiveChanges = ‘<?xml version=”1.0″ encoding=”UTF-8″?>’ +
‘<Package xmlns=”http://soap.sforce.com/2006/04/metadata”>’ ;
for(var i=0;i<keys.length;i++){
destructiveChanges = destructiveChanges + ‘<types>’;
if(keys[i] == ‘Dashboard’ || keys[i] == ‘Report’ || keys[i] == ‘EmailTemplate’ ||keys[i] == ‘Document’){
for(var j=0;j<compMap[keys[i]].length;j++) {
var items = compMap[keys[i]][j].split(‘/’);
if((destructiveChanges.indexOf(‘<members>’ + items[0] + ‘</members>’) > -1) && (items[0] != ‘unfiled$public’)) {
destructiveChanges = destructiveChanges + ‘<members>’+items[0]+'</members>’;
}
destructiveChanges = destructiveChanges + ‘<members>’+compMap[keys[i]][j]+'</members>’;
}
} else {
for(var j=0;j<compMap[keys[i]].length;j++) {
destructiveChanges = destructiveChanges + ‘<members>’+compMap[keys[i]][j]+'</members>’;
}
}
destructiveChanges = destructiveChanges + ‘<name>’+keys[i]+'</name>’;
destructiveChanges = destructiveChanges + ‘</types>’;
}
destructiveChanges = destructiveChanges + ‘</Package>’;
zipFile = new JSZip();
zipFile.file(‘package.xml’,packageXml);
zipFile.file(‘destructiveChanges.xml’,destructiveChanges);
zipFile.generateAsync({type:”base64″}).then(function(data) {
Visualforce.remoting.Manager.invokeAction(
‘{!$RemoteAction.DeleteComponentsClass.getDeployContent}’,
data,
function(result, event){
if (event.status) {
} else {
alert(“Error”);
}
});
});
} else {
alert(‘Add Components’);
}
}
</script>
<apex:form id=”frm” >
<apex:pageBlock id=”pblck1″ title=”Delete Components”>
<apex:pageBlockSection id=”pbSectn1″ columns=”4″ showHeader=”false”>
<apex:outputLabel value=”Component Type” for=”picklistID” />
<apex:selectList id=”picklistID” value=”{!SelectedType}” title=”Component Type” multiselect=”false” size=”1″>
<apex:selectOptions value=”{!DataTypes}” />
</apex:selectList>
</apex:pageBlockSection>
<apex:pageBlockSection id=”pbSectn2″ columns=”4″ showHeader=”false”>
<apex:outputLabel value=”Component” for=”nameID” />
<apex:inputText id=”componentName” value=”{!ComponentName}” />
</apex:pageBlockSection>
<apex:pageBlockButtons location=”bottom”>
<apex:commandButton action=”{!add}” value=” Add “> </apex:commandButton>
<apex:commandButton onclick=”getRemoteContent()” action=”{!deleteItems}” value=” Delete “> </apex:commandButton>
<apex:commandButton action=”{!cancel}” value=” Cancel “></apex:commandButton>
</apex:pageBlockButtons>
</apex:pageBlock>
<apex:pageBlock id=”pblck2″ >
<table border=’1′>
<tr style=”background:#3498db;color:white”>
<th>Component Name</th>
<th>Component Type</th>
</tr>
<apex:repeat id=”itemId” value=”{!componentItems}” var=”item”>
<script>
contentToMap(‘{!item.compType}’,'{!item.compName}’);
</script>
<tr>
<td>{!item.compName}</td>
<td>{!item.compType}</td>
</tr>
</apex:repeat></table>
</apex:pageBlock>
<apex:pageBlock >
<apex:pageBlockSection columns=”1″>
<span style=”font-style:Italic”>
Note: Component is the Component Object Name. For Folder components like Dashboard, Document, EmailTemplate and Report, the Folder Name is added as prefix to the Component. For example my Dashboard folder is demoFolder and Dashboard is demoComponent, then the Component name is demoFolder/demoComponent.
You can add more than one Component Name separated by semicolon(‘;’).
</span>
</apex:pageBlockSection>
</apex:pageBlock>
</apex:form>
</apex:page>
[/sourcecode]
Let’s test this:
I have an Apex class “HelloWorld” and Apex Trigger “HelloTrigger”. (I am using the developer org not the production).
Add these components in the tool.
Click on the delete button we are redirected to the Deployment status page displaying record showing deployment succeeded.
We can also check in the Apex classes and Apex Triggers setup that the components are deleted.
This tool can also be used to delete other components like Apex pages, Dashboards, Documents, Reports, Workflows, etc,. The components Dashboard, Document, EmailTemplate and Report are folder components, so we have to mention they folder name in order to delete. For example, my dashboard is demoDashboard and its in folder demoFolder. Then the component name will be demoFolder/demoDashboard. We can also add multiple component names of same type separated by semicolon (;).
Related links:
Happy Coding!