Skip to content

Commit d164bf2

Browse files
authored
New 'custom metadata table' lightning web component (#10)
* New lwc that provides basic inline-edit functionality for a collection of CMDT records * Updated README with details about the new lwc
1 parent 7e65628 commit d164bf2

15 files changed

+690
-4
lines changed

README.md

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,48 @@
77

88
This is a small library that can be used in Salesforce to update & deploy changes to custom metadata types (CMDT) from Apex and Flow.
99

10-
## Flow
10+
---
11+
12+
## Use Included Lightning Web Component to Update CMDT Records
13+
14+
The included lwc `custom-metadata-table` is designed to work specifically with CMDT records. It includes 3 properties that you can set within Flow or your own lightning component
15+
16+
1. `records` - an instance of `List<SObject>` in Apex, or a 'record collection' within Flow, containing the CMDT records to display within the table.
17+
- All of the `SObject` records must be of the same `SObject Type`.
18+
- You can create or query the list of CMDT records - see README for details on how to create CMDT records in Apex and Flow.
19+
- When creating new CMDT records, you must populate both fields `DeveloperName` and `MasterLabel` in order to successfully deploy the new records.
20+
2. `fieldToDisplay` - a comma-separated list of field API names that should be shown in the table.
21+
- When the parameter `enableEditing == true`, all fields, except `DeveloperName` and lookup fields, will be editable within the table. This is done via the `lightning-datatable`'s inline-editing functionality
22+
3. `enableEditing` - Boolean value to control if CMDT records are displayed in read-only mode (`enableEditing == false`) or with inline-editing enabled (`enableEditing == true`)
23+
24+
Users can use this component to edit the CMDT records and simply click 'Save' to deploy any changes to the current org. The component then displays a loading-spinner while the deployment runs - it continues to check the deployment status until the deployment is done (either it suceeds or it fails).
25+
26+
In order to check the deployment's status, the component makes callouts to the REST API in your org - this is necessary because the deployment status cannot be checked natively within Apex. Depending on your org, some additional configuration may be needed to allow the callouts:
27+
28+
- Org uses "My Domain": no additional changes should be required in your org - orgs with the "My Domain" feature configured can already make callouts to the same org without any additional configuration
29+
- Org does not use "My Domain": in your org, configure a Remote Site Setting - the URL of the remote site setting should be the URL of your Salesforce org
30+
31+
This screenshot shows an example of the table, using the include CMDT object `CustomMetadataDeployTest__mdt`
32+
33+
![LWC: Custom Metadata Table](./content/lwc-custom-metadata-table.png)
34+
35+
### Using custom-metadata-table in Flow
36+
37+
Within Flow, you can leverage the lwc `custom-metadata-table` in 2 steps
38+
39+
1. Query the list of CMDT records to display and store them in a record collection variable
40+
41+
![LWC: Flow Example](./content/lwc-flow-builder.png)
42+
43+
2. Add a screen to display the `custom-metadata-table` component - here, you will provide the CMDT record collection variable and a comma-separated list of fields to display
44+
45+
![LWC: Flow Table Properties](./content/lwc-flow-table-properties.png)
46+
47+
---
48+
49+
## Use Backend Automations to Update CMDT Records
50+
51+
### Flow
1152

1253
Deploying CMDT changes from Flow consists of 2 actions
1354

@@ -27,7 +68,7 @@ Deploying CMDT changes from Flow consists of 2 actions
2768

2869
![Flow: Deploy CMDT Records](./content/flow-deploy-cmdt-records.png)
2970

30-
## Apex
71+
### Apex
3172

3273
Since Apex can already update custom metadata records (it just can't save the changes using DML statements), it's fairly straightforward process to deploy the changes from Apex.
3374

38 KB
Loading

content/lwc-flow-builder.png

68.8 KB
Loading
97.5 KB
Loading
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Flow xmlns="http://soap.sforce.com/2006/04/metadata">
3+
<apiVersion>52.0</apiVersion>
4+
<interviewLabel>CMDT Test Table {!$Flow.CurrentDateTime}</interviewLabel>
5+
<label>CMDT Test Table</label>
6+
<processMetadataValues>
7+
<name>BuilderType</name>
8+
<value>
9+
<stringValue>LightningFlowBuilder</stringValue>
10+
</value>
11+
</processMetadataValues>
12+
<processMetadataValues>
13+
<name>CanvasMode</name>
14+
<value>
15+
<stringValue>AUTO_LAYOUT_CANVAS</stringValue>
16+
</value>
17+
</processMetadataValues>
18+
<processMetadataValues>
19+
<name>OriginBuilderType</name>
20+
<value>
21+
<stringValue>LightningFlowBuilder</stringValue>
22+
</value>
23+
</processMetadataValues>
24+
<processType>Flow</processType>
25+
<recordLookups>
26+
<name>Get_CMDT_Records</name>
27+
<label>Get CMDT Records</label>
28+
<locationX>176</locationX>
29+
<locationY>158</locationY>
30+
<assignNullValuesIfNoRecordsFound>false</assignNullValuesIfNoRecordsFound>
31+
<connector>
32+
<targetReference>cmdtTableScreen</targetReference>
33+
</connector>
34+
<getFirstRecordOnly>false</getFirstRecordOnly>
35+
<object>CustomMetadataDeployTest__mdt</object>
36+
<storeOutputAutomatically>true</storeOutputAutomatically>
37+
</recordLookups>
38+
<screens>
39+
<name>cmdtTableScreen</name>
40+
<label>cmdtTableScreen</label>
41+
<locationX>176</locationX>
42+
<locationY>278</locationY>
43+
<allowBack>true</allowBack>
44+
<allowFinish>true</allowFinish>
45+
<allowPause>true</allowPause>
46+
<fields>
47+
<name>cmdtTable</name>
48+
<dataTypeMappings>
49+
<typeName>T</typeName>
50+
<typeValue>CustomMetadataDeployTest__mdt</typeValue>
51+
</dataTypeMappings>
52+
<extensionName>c:customMetadataTable</extensionName>
53+
<fieldType>ComponentInstance</fieldType>
54+
<inputParameters>
55+
<name>records</name>
56+
<value>
57+
<elementReference>Get_CMDT_Records</elementReference>
58+
</value>
59+
</inputParameters>
60+
<inputParameters>
61+
<name>enableEditing</name>
62+
<value>
63+
<booleanValue>true</booleanValue>
64+
</value>
65+
</inputParameters>
66+
<inputParameters>
67+
<name>fieldsToDisplay</name>
68+
<value>
69+
<stringValue>DeveloperName, MasterLabel, ExampleDateField__c, ExampleDatetimeField__c, ExampleCheckboxField__c, ExamplePicklistField__c</stringValue>
70+
</value>
71+
</inputParameters>
72+
<inputsOnNextNavToAssocScrn>UseStoredValues</inputsOnNextNavToAssocScrn>
73+
<isRequired>true</isRequired>
74+
<storeOutputAutomatically>true</storeOutputAutomatically>
75+
</fields>
76+
<showFooter>false</showFooter>
77+
<showHeader>false</showHeader>
78+
</screens>
79+
<start>
80+
<locationX>50</locationX>
81+
<locationY>0</locationY>
82+
<connector>
83+
<targetReference>Get_CMDT_Records</targetReference>
84+
</connector>
85+
</start>
86+
<status>Draft</status>
87+
</Flow>

force-app/main/custom-metadata-saver/classes/CustomMetadataSaver.cls

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ public inherited sharing class CustomMetadataSaver {
88
private static final Boolean DEFAULT_SEND_EMAIL_ON_SUCCESS = false;
99
private static final List<String> DEPLOYMENT_JOB_IDS = new List<String>();
1010
private static final Set<String> IGNORED_FIELD_NAMES = getIgnoredFieldNames();
11+
@testVisible
12+
private static final String MOCK_DEPLOYMENT_ID = 'fakeDeploymentId';
1113

1214
public class FlowInput {
1315
@InvocableVariable(required=true label='Custom Metadata Type Records to Deploy')
@@ -97,7 +99,7 @@ public inherited sharing class CustomMetadataSaver {
9799
deployment.addMetadata(customMetadata);
98100
}
99101

100-
String jobId = Test.isRunningTest() ? 'Fake Job ID' : Metadata.Operations.enqueueDeployment(deployment, callback);
102+
String jobId = Test.isRunningTest() ? MOCK_DEPLOYMENT_ID : Metadata.Operations.enqueueDeployment(deployment, callback);
101103
DEPLOYMENT_JOB_IDS.add(jobId);
102104
System.debug(LoggingLevel.INFO, 'Deployment Job ID: ' + jobId);
103105

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
//----------------------------------------------------------------------------------------------------//
2+
// This file is part of the Custom Metadata Saver project, released under the MIT License. //
3+
// See LICENSE file or go to https://github.com/jongpie/CustomMetadataSaver for full license details. //
4+
//----------------------------------------------------------------------------------------------------//
5+
6+
public without sharing class CustomMetadataTableController {
7+
private static String BASE_URL = System.Url.getOrgDomainUrl().toExternalForm() + '/services/data/v52.0';
8+
9+
@AuraEnabled
10+
public static String getSObjectApiName(SObject customMetadataRecord) {
11+
System.debug(LoggingLevel.INFO, 'CustomMetadataTableController.getSObjectApiName(' + customMetadataRecord + ')');
12+
return customMetadataRecord.getSObjectType().getDescribe().getName();
13+
}
14+
15+
// @AuraEnabled(cacheable=true)
16+
// public static List<PicklistOption> getPicklistOptions(String sobjectApiName, String fieldApiName) {
17+
// System.debug(LoggingLevel.INFO, 'CustomMetadataTableController.getPicklistOptions(' + sobjectApiName + ',' + fieldApiName + ')');
18+
19+
// // Schema.PicklistEntry can't be returned by @AuraEnabled methods, so use an inner class PicklistOption instead
20+
// Schema.SObjectType sobjectType = ((SObject) Type.forName(sobjectApiName).newInstance()).getSObjectType();
21+
// List<Schema.PicklistEntry> schemaPicklistEntries = sobjectType.getDescribe().fields.getMap().get(fieldApiName).getDescribe().getPicklistValues();
22+
// List<PicklistOption> picklistOptions = new List<PicklistOption>();
23+
// for (Schema.PicklistEntry schemaPicklistEntry : schemaPicklistEntries) {
24+
// PicklistOption picklistOption = new PicklistOption();
25+
// picklistOption.label = schemaPicklistEntry.label;
26+
// picklistOption.value = schemaPicklistEntry.value;
27+
28+
// picklistOptions.add(picklistOption);
29+
// }
30+
// return picklistOptions;
31+
// }
32+
33+
@AuraEnabled
34+
public static String deploy(List<SObject> customMetadataRecords) {
35+
System.debug(LoggingLevel.INFO, 'CustomMetadataTableController.deploy(customMetadataRecords)==' + customMetadataRecords);
36+
return CustomMetadataSaver.deploy(customMetadataRecords);
37+
}
38+
39+
@AuraEnabled
40+
public static DeploymentStatusResponse getDeploymentStatus(String deploymentJobId) {
41+
System.debug(LoggingLevel.INFO, 'deploymentJobId==' + deploymentJobId);
42+
43+
final String deploymentStatusUrl = BASE_URL + '/metadata/deployRequest/' + deploymentJobId + '?includeDetails=true';
44+
45+
HttpRequest request = new HttpRequest();
46+
request.setEndpoint(deploymentStatusUrl);
47+
request.setHeader('Authorization', 'Bearer ' + UserInfo.getSessionId());
48+
request.setHeader('Content-Type', 'application/json; charset=utf-8');
49+
request.setMethod('GET');
50+
51+
HttpResponse response = new Http().send(request);
52+
DeploymentStatusResponse deploymentStatusResponse = (DeploymentStatusResponse) JSON.deserialize(response.getBody(), DeploymentStatusResponse.class);
53+
System.debug(LoggingLevel.INFO, 'deploymentStatusResponse==' + deploymentStatusResponse);
54+
return deploymentStatusResponse;
55+
}
56+
57+
// DTO for picklist options since Schema.PicklistEntry isn't supported for aura-enabled methods
58+
// public class PicklistOption {
59+
// @AuraEnabled
60+
// public String label;
61+
// @AuraEnabled
62+
// public String value;
63+
// }
64+
65+
// DTOs based on the REST API's response for deployments
66+
// https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_rest_deploy_checkstatus.htm
67+
public class DeploymentStatusResponse {
68+
@AuraEnabled
69+
public String id;
70+
@AuraEnabled
71+
public String url;
72+
@AuraEnabled
73+
public DeployResult deployResult;
74+
}
75+
76+
public class DeployResult {
77+
@AuraEnabled
78+
public String status;
79+
@AuraEnabled
80+
public Integer numberComponentsDeployed;
81+
@AuraEnabled
82+
public Integer numberComponentsTotal;
83+
@AuraEnabled
84+
public Integer numberComponentErrors;
85+
@AuraEnabled
86+
public DeployResultDetails details;
87+
}
88+
89+
public class DeployResultDetails {
90+
@AuraEnabled
91+
public List<ComponentResultDetail> allComponentMessages;
92+
@AuraEnabled
93+
public List<ComponentResultDetail> componentFailures;
94+
@AuraEnabled
95+
public List<ComponentResultDetail> componentSuccesses;
96+
}
97+
98+
public class ComponentResultDetail {
99+
@AuraEnabled
100+
public String componentType;
101+
@AuraEnabled
102+
public String fullName;
103+
@AuraEnabled
104+
public String problem;
105+
@AuraEnabled
106+
public Boolean success;
107+
@AuraEnabled
108+
public Boolean warning;
109+
@AuraEnabled
110+
public Boolean created;
111+
@AuraEnabled
112+
public Boolean changed;
113+
@AuraEnabled
114+
public Boolean deleted;
115+
@AuraEnabled
116+
public Integer lineNumber;
117+
@AuraEnabled
118+
public Integer columnNumber;
119+
@AuraEnabled
120+
public Boolean requiresProductionTestRun;
121+
@AuraEnabled
122+
public Boolean knownPackagingProblem;
123+
@AuraEnabled
124+
public Boolean forPackageManifestFile;
125+
@AuraEnabled
126+
public String problemType;
127+
}
128+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
3+
<apiVersion>52.0</apiVersion>
4+
<status>Active</status>
5+
</ApexClass>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<template>
2+
<lightning-card title={title} icon-name="standard:bundle_config">
3+
<template if:true={deploymentStatus}>
4+
<div slot="actions" class="slds-text-align_right">
5+
Deployment Status: {deploymentStatus} <br />
6+
{deploymentMessage}
7+
<!-- <lightning-icon icon-name="action:approval" alternative-text="Approved" title="Approved" variant="success" size="xx-small"></lightning-icon> -->
8+
</div>
9+
</template>
10+
<!-- <div slot="actions">
11+
<lightning-button
12+
icon-name="action:new"
13+
label="New"
14+
onclick={showNewRecordModal}
15+
title="New Custom Metadata Record"
16+
variant="brand-outline"
17+
></lightning-button>
18+
</div> -->
19+
<lightning-datatable
20+
columns={columns}
21+
data={records}
22+
default-sort-direction={defaultSortDirection}
23+
hide-checkbox-column
24+
key-field="DeveloperName"
25+
onsave={handleSave}
26+
onsort={handleSort}
27+
show-row-number-column
28+
sorted-by={sortedBy}
29+
sorted-direction={sortedDirection}
30+
>
31+
</lightning-datatable>
32+
</lightning-card>
33+
34+
<template if:true={isDeploying}>
35+
<lightning-spinner title="Deploying..." alternative-text="Please wait while CMDT records are deployed"></lightning-spinner>
36+
</template>
37+
38+
<!-- <template if:true={shouldShowNewRecordModal}>
39+
<- Modal/Popup Box LWC starts here ->
40+
<section role="dialog" tabindex="-1" aria-labelledby="modal-heading-01" aria-modal="true" aria-describedby="modal-content-id-1" class="slds-modal slds-fade-in-open">
41+
<div class="slds-modal__container">
42+
<- Modal/Popup Box LWC header here ->
43+
<header class="slds-modal__header">
44+
<button class="slds-button slds-button_icon slds-modal__close slds-button_icon-inverse" title="Close" onclick={closeNewRecordModal}>
45+
<lightning-icon icon-name="utility:close"
46+
alternative-text="close"
47+
variant="inverse"
48+
size="small" ></lightning-icon>
49+
<span class="slds-assistive-text">Close</span>
50+
</button>
51+
<h2 id="modal-heading-01" class="slds-text-heading_medium slds-hyphenate">Modal/PopUp Box header LWC</h2>
52+
</header>
53+
<- Modal/Popup Box LWC body starts here ->
54+
<div class="slds-modal__content slds-p-around_medium" id="modal-content-id-1">
55+
<lightning-card>
56+
<lightning-input type="text" name="DeveloperName" required="true" label="Dev Name" value={newRecordDeveloperName}></lightning-input>
57+
<lightning-input type="text" name="MasterLabel" required="true" label="Label" value={newRecordMasterLabel}></lightning-input>
58+
</lightning-card>
59+
</div>
60+
<- Modal/Popup Box LWC footer starts here ->
61+
<footer class="slds-modal__footer">
62+
<button class="slds-button slds-button_neutral" onclick={closeNewRecordModal} title="Cancel">Cancel</button>
63+
<button class="slds-button slds-button_brand" onclick={addNewRecord} title="Add">Add</button>
64+
</footer>
65+
</div>
66+
</section>
67+
<div class="slds-backdrop slds-backdrop_open"></div>
68+
</template> -->
69+
</template>

0 commit comments

Comments
 (0)