From cbbf02a13594e824b4426b2c81724995a181eb3f Mon Sep 17 00:00:00 2001 From: Eric Smith Date: Tue, 26 Nov 2024 13:55:07 -0500 Subject: [PATCH 01/18] New Get Index and Upsert Actions --- .../main/default/classes/GetIndexFromKey.cls | 91 +++++++++++++ .../classes/GetIndexFromKey.cls-meta.xml | 5 + .../default/classes/UpsertRecordByKey.cls | 124 ++++++++++++++++++ .../classes/UpsertRecordByKey.cls-meta.xml | 5 + 4 files changed, 225 insertions(+) create mode 100644 flow_action_components/CollectionProcessors/force-app/main/default/classes/GetIndexFromKey.cls create mode 100644 flow_action_components/CollectionProcessors/force-app/main/default/classes/GetIndexFromKey.cls-meta.xml create mode 100644 flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls create mode 100644 flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls-meta.xml diff --git a/flow_action_components/CollectionProcessors/force-app/main/default/classes/GetIndexFromKey.cls b/flow_action_components/CollectionProcessors/force-app/main/default/classes/GetIndexFromKey.cls new file mode 100644 index 000000000..b7276f2fb --- /dev/null +++ b/flow_action_components/CollectionProcessors/force-app/main/default/classes/GetIndexFromKey.cls @@ -0,0 +1,91 @@ +/** + * Get Index from Key - Flow Action + * + * Eric Smith - 11/26/24 - v1.0 + * + * This class is a Collection Utility designed to find the index of a record in a collection of records based on the value of a field + * + * It takes for input a record collection, the API name of a field and the target value of that field. + * + * It will then loop through the collection and compare the field value to the target value. If the field value matches the target value, + * the index of that record is returned. If a matching record is not found, an index value of -1 will be returned. + * + * The primary use case for this action is to find the index of a record based on the value of a key field so it can be used in + * other collecion actions that require an index value such as Add or Insert Record or Remove Recod from Collection. + * +**/ + +global inherited sharing class GetIndexFromKey { + + // Attributes passed in from the Flow + global class Requests { + + @InvocableVariable(label='Input Collection' required=true) + global List inputCollection; + + @InvocableVariable(label='Field API Name') + global String fieldAPIName; + + @InvocableVariable(label='Field API Value' required=true) + global String fieldValue; + + } + + // Attributes passed back to the Flow + global class Results { + + @InvocableVariable(label='Index') + global Integer index; + + } + + // Standard Exception Handling + global class InvocableActionException extends Exception {} + + // Expose this Action to the Flow + @InvocableMethod(label='Get Index from Keyfield Value [USF Collection Processor]' category='Util' iconName='resource:CollectionProcessorsSVG:colproc') + global static List getIndexFromKey(List requestList) { + + // Prepare the response to send back to the Flow + Results response = new Results(); + List responseWrapper = new List(); + + // Bulkify proccessing of multiple requests + for (Requests curRequest : requestList) { + + // Get Input Value(s) + List inputCollection = curRequest.inputCollection; + String fieldAPIName = curRequest.fieldAPIName; + Object fieldValue = curRequest.fieldValue; + + // Process input attributes + if (fieldAPIName == '' || fieldAPIName == null) { + fieldAPIName = 'Id'; + } +System.debug('fieldAPIName: '+fieldAPIName); +System.debug('fieldValue: '+fieldValue); + + // Define working variables + Integer index = -1; + Integer counter = 0; + + // Start processing + for (SObject record: inputCollection) { + if (record.get(fieldAPIName)?.toString() == fieldValue) { + index = counter; + break; + } + counter++; + } +System.debug('counter: '+counter); + + // Set Output Values + response.index = counter; + responseWrapper.add(response); + } + + // Return values back to the Flow + return responseWrapper; + } + +} \ No newline at end of file diff --git a/flow_action_components/CollectionProcessors/force-app/main/default/classes/GetIndexFromKey.cls-meta.xml b/flow_action_components/CollectionProcessors/force-app/main/default/classes/GetIndexFromKey.cls-meta.xml new file mode 100644 index 000000000..998805a82 --- /dev/null +++ b/flow_action_components/CollectionProcessors/force-app/main/default/classes/GetIndexFromKey.cls-meta.xml @@ -0,0 +1,5 @@ + + + 62.0 + Active + diff --git a/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls b/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls new file mode 100644 index 000000000..9ae61f7b1 --- /dev/null +++ b/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls @@ -0,0 +1,124 @@ +/** + * Upsert Record by Key - Flow Action + * + * Eric Smith - 11/26/24 - v1.0 + * + * This class is a Collection Utility designed to upsert a record into a collection of records based on the value of a field in the source record. + * + * It takes for input a record collection, a single record and the API name of a key field. + * + * If it finds a matching record in the collection based on the value of the key field, it will replace that record in the collection. + * If a matching record is not found,the record will be added to the collection. + * + * The primary use case for this action is to update a record collection in a flow prior to saving the changes to the database. + * +**/ + +global inherited sharing class UpsertRecordByKey { + + // Attributes passed in from the Flow + global class Requests { + + @InvocableVariable(label='Origginal record collection' required=true) + global List inputCollection; + + @InvocableVariable(label='New or replacement record' required=true) + global SObject inputRecord; + + @InvocableVariable(label='API name of keyfield (Default = Id)') + global String fieldAPIName; + + } + + // Attributes passed back to the Flow + global class Results { + + @InvocableVariable(Label='Collection with upserted record') + global List outputCollection; + + } + + // Standard Exception Handling + global class InvocableActionException extends Exception {} + + // Expose this Action to the Flow + @InvocableMethod(label='Upsert Record by Key [USF Collection Processor]' category='Util' iconName='resource:CollectionProcessorsSVG:colproc') + global static List upsertRecordByKey(List requestList) { + + // Prepare the response to send back to the Flow + Results response = new Results(); + List responseWrapper = new List(); + + // Bulkify proccessing of multiple requests + for (Requests curRequest : requestList) { + + // Get Input Value(s) + List inputCollection = curRequest.inputCollection; + SObject inputRecord = curRequest.inputRecord; + String fieldAPIName = curRequest.fieldAPIName; + + // Process input attributes + if (fieldAPIName == '' || fieldAPIName == null) { + fieldAPIName = 'Id'; + } + String fieldValue = inputRecord.get(fieldAPIName)?.toString(); + + // Set initial values + getIndexResults idx = new getIndexResults(); + + // Start processing + idx = getIndexFromKey(inputCollection, fieldAPIName, fieldValue); + Integer index = idx.index; + + if (inputCollection != null && inputRecord != null) { + if (index == -1 || index == null || index >= inputCollection.size()) { + inputCollection.add(inputRecord); + } else { + inputCollection.remove(index); + inputCollection.add(index, inputRecord); + } + + // Set Output Values + response.outputCollection = inputCollection.clone(); + } + + responseWrapper.add(response); + + } + + // Return values back to the Flow + return responseWrapper; + } + + public class getIndexResults { + Integer index; + } + + public static getIndexResults getIndexFromKey(List inputCollection, String fieldAPIName, String fieldValue) { + + Invocable.Action indexAction = Invocable.Action.createCustomAction('apex', 'GetIndexFromKey'); + + indexAction.setInvocationParameter('inputCollection', inputCollection); + indexAction.setInvocationParameter('fieldAPIName', fieldAPIName); + indexAction.setInvocationParameter('fieldValue', fieldValue); + + List results = indexAction.invoke(); + System.debug('results: '+results); + + getIndexResults gir = new getIndexResults(); + + gir.index = -1; + if (results.size() > 0 && results[0].isSuccess()) { + gir.index = Integer.valueOf(results[0].getOutputParameters().get('index')); + } + System.debug('gir.index: '+gir.index); + + return gir; + } + + // Convert an object to an integer + // private static Integer objToInteger(Object obj) { + // return Integer.valueof(obj); + // } + +} \ No newline at end of file diff --git a/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls-meta.xml b/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls-meta.xml new file mode 100644 index 000000000..998805a82 --- /dev/null +++ b/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls-meta.xml @@ -0,0 +1,5 @@ + + + 62.0 + Active + From bfe71c2e9fbfec26639a555710bb6ad1f1e8ad24 Mon Sep 17 00:00:00 2001 From: Eric Smith Date: Tue, 26 Nov 2024 15:28:02 -0500 Subject: [PATCH 02/18] Add Test Class --- .../main/default/classes/GetIndexFromKey.cls | 3 - .../default/classes/UpsertRecordByKey.cls | 7 -- .../default/classes/UpsertRecordByKeyTest.cls | 69 +++++++++++++++++++ .../UpsertRecordByKeyTest.cls-meta.xml | 5 ++ 4 files changed, 74 insertions(+), 10 deletions(-) create mode 100644 flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyTest.cls create mode 100644 flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyTest.cls-meta.xml diff --git a/flow_action_components/CollectionProcessors/force-app/main/default/classes/GetIndexFromKey.cls b/flow_action_components/CollectionProcessors/force-app/main/default/classes/GetIndexFromKey.cls index b7276f2fb..bf5c91590 100644 --- a/flow_action_components/CollectionProcessors/force-app/main/default/classes/GetIndexFromKey.cls +++ b/flow_action_components/CollectionProcessors/force-app/main/default/classes/GetIndexFromKey.cls @@ -62,8 +62,6 @@ global inherited sharing class GetIndexFromKey { if (fieldAPIName == '' || fieldAPIName == null) { fieldAPIName = 'Id'; } -System.debug('fieldAPIName: '+fieldAPIName); -System.debug('fieldValue: '+fieldValue); // Define working variables Integer index = -1; @@ -77,7 +75,6 @@ System.debug('fieldValue: '+fieldValue); } counter++; } -System.debug('counter: '+counter); // Set Output Values response.index = counter; diff --git a/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls b/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls index 9ae61f7b1..3bccd664f 100644 --- a/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls +++ b/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls @@ -103,7 +103,6 @@ global inherited sharing class UpsertRecordByKey { indexAction.setInvocationParameter('fieldValue', fieldValue); List results = indexAction.invoke(); - System.debug('results: '+results); getIndexResults gir = new getIndexResults(); @@ -111,14 +110,8 @@ global inherited sharing class UpsertRecordByKey { if (results.size() > 0 && results[0].isSuccess()) { gir.index = Integer.valueOf(results[0].getOutputParameters().get('index')); } - System.debug('gir.index: '+gir.index); return gir; } - // Convert an object to an integer - // private static Integer objToInteger(Object obj) { - // return Integer.valueof(obj); - // } - } \ No newline at end of file diff --git a/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyTest.cls b/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyTest.cls new file mode 100644 index 000000000..1e5ae4e2d --- /dev/null +++ b/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyTest.cls @@ -0,0 +1,69 @@ +/** + * Upsert Record by Key Test - Apex Tests + * + * Eric Smith - 11/26/24 - v1.0 + * + * This will test both GetIndexFromKey.cls and UpsertRecordByKey.cls + * +**/ + +@isTest +public with sharing class UpsertRecordByKeyTest { + + @isTest + static void upsertTest() { + + Account acct = new Account(Name='Test Account1', AccountNumber='1'); + insert acct; + List accts = new List(); + accts.add(acct); + acct = new Account(Name='Test Account2', AccountNumber='2'); + insert acct; + Id acct2id = acct.Id; + accts.add(acct); + acct = new Account(Name='Test Account3', AccountNumber='3'); + insert acct; + accts.add(acct); + + UpsertRecordByKey.Requests testUpsert = new UpsertRecordByKey.Requests(); + List testUpsertList = new List(); + + // Test match with field API name provided + Account upd_acct1 = new Account(Name='Test Account1', AccountNumber='11'); + testUpsert.inputCollection = accts; + testUpsert.inputRecord = upd_acct1; + testUpsert.fieldAPIName = 'Name'; + testUpsertList.add(testUpsert); + List testResultList = UpsertRecordByKey.upsertRecordByKey(testUpsertList); + System.Assert.areEqual('Test Account1', testResultList[0].outputCollection[0].get('Name')); + System.Assert.areEqual('11', testResultList[0].outputCollection[0].get('AccountNumber')); + System.Assert.areEqual(3, testResultList[0].outputCollection.size()); + testUpsertList.clear(); + + // Test match of Id field by default + Account upd_acct2 = accts[1]; + upd_acct2.AccountNumber='22'; + testUpsert.inputCollection = accts; + testUpsert.inputRecord = upd_acct2; + testUpsert.fieldAPIName = ''; + testUpsertList.add(testUpsert); + testResultList = UpsertRecordByKey.upsertRecordByKey(testUpsertList); + System.Assert.areEqual(acct2Id, testResultList[0].outputCollection[1].Id); + System.Assert.areEqual('Test Account2', testResultList[0].outputCollection[1].get('Name')); + System.Assert.areEqual('22', testResultList[0].outputCollection[1].get('AccountNumber')); + System.Assert.areEqual(3, testResultList[0].outputCollection.size()); + testUpsertList.clear(); + + // Test add new record when no match + Account acct4 = new Account(Name='Test Account4', AccountNumber='4'); + insert acct4; + testUpsert.inputCollection = accts; + testUpsert.inputRecord = acct4; + testUpsert.fieldAPIName = 'Name'; + testUpsertList.add(testUpsert); + testResultList = UpsertRecordByKey.upsertRecordByKey(testUpsertList); + System.Assert.areEqual(4, testResultList[0].outputCollection.size()); + + } + +} \ No newline at end of file diff --git a/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyTest.cls-meta.xml b/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyTest.cls-meta.xml new file mode 100644 index 000000000..998805a82 --- /dev/null +++ b/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 62.0 + Active + From dcb40a4e6176a88ee19d504b3ec648aec9db6048 Mon Sep 17 00:00:00 2001 From: Eric Smith Date: Tue, 26 Nov 2024 17:30:54 -0500 Subject: [PATCH 03/18] Added skip insert flag and not found return falg - updated test class --- .../default/classes/UpsertRecordByKey.cls | 20 ++++++++++-- .../default/classes/UpsertRecordByKeyTest.cls | 31 +++++++++++++------ 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls b/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls index 3bccd664f..d09c9041f 100644 --- a/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls +++ b/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls @@ -8,7 +8,9 @@ * It takes for input a record collection, a single record and the API name of a key field. * * If it finds a matching record in the collection based on the value of the key field, it will replace that record in the collection. - * If a matching record is not found,the record will be added to the collection. + * If a matching record is not found,the record will be added to the collection unless the option to skip the insert is set to True. + * + * The updated record colllection is returned by the action along with a noMatchFound flag which will be true if no matching record was found. * * The primary use case for this action is to update a record collection in a flow prior to saving the changes to the database. * @@ -28,6 +30,9 @@ global inherited sharing class UpsertRecordByKey { @InvocableVariable(label='API name of keyfield (Default = Id)') global String fieldAPIName; + @InvocableVariable(label='Do you want to skip insert if no match is found? (Default = False)') + global Boolean skipInsertIfNoMatchFound; + } // Attributes passed back to the Flow @@ -36,6 +41,9 @@ global inherited sharing class UpsertRecordByKey { @InvocableVariable(Label='Collection with upserted record') global List outputCollection; + @InvocableVariable(Label='No matching record was found') + global Boolean noMatchFound; + } // Standard Exception Handling @@ -56,11 +64,15 @@ global inherited sharing class UpsertRecordByKey { List inputCollection = curRequest.inputCollection; SObject inputRecord = curRequest.inputRecord; String fieldAPIName = curRequest.fieldAPIName; + Boolean skipInsertIfNoMatchFound = curRequest.skipInsertIfNoMatchFound; // Process input attributes if (fieldAPIName == '' || fieldAPIName == null) { fieldAPIName = 'Id'; } + if (skipInsertIfNoMatchFound == null) { + skipInsertIfNoMatchFound = false; + } String fieldValue = inputRecord.get(fieldAPIName)?.toString(); // Set initial values @@ -71,8 +83,12 @@ global inherited sharing class UpsertRecordByKey { Integer index = idx.index; if (inputCollection != null && inputRecord != null) { + response.noMatchFound = false; if (index == -1 || index == null || index >= inputCollection.size()) { - inputCollection.add(inputRecord); + response.noMatchFound = true; + if (!skipInsertIfNoMatchFound) { + inputCollection.add(inputRecord); + } } else { inputCollection.remove(index); inputCollection.add(index, inputRecord); diff --git a/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyTest.cls b/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyTest.cls index 1e5ae4e2d..20416241a 100644 --- a/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyTest.cls +++ b/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyTest.cls @@ -35,9 +35,9 @@ public with sharing class UpsertRecordByKeyTest { testUpsert.fieldAPIName = 'Name'; testUpsertList.add(testUpsert); List testResultList = UpsertRecordByKey.upsertRecordByKey(testUpsertList); - System.Assert.areEqual('Test Account1', testResultList[0].outputCollection[0].get('Name')); - System.Assert.areEqual('11', testResultList[0].outputCollection[0].get('AccountNumber')); - System.Assert.areEqual(3, testResultList[0].outputCollection.size()); + Assert.areEqual('Test Account1', testResultList[0].outputCollection[0].get('Name')); + Assert.areEqual('11', testResultList[0].outputCollection[0].get('AccountNumber')); + Assert.areEqual(3, testResultList[0].outputCollection.size()); testUpsertList.clear(); // Test match of Id field by default @@ -48,22 +48,35 @@ public with sharing class UpsertRecordByKeyTest { testUpsert.fieldAPIName = ''; testUpsertList.add(testUpsert); testResultList = UpsertRecordByKey.upsertRecordByKey(testUpsertList); - System.Assert.areEqual(acct2Id, testResultList[0].outputCollection[1].Id); - System.Assert.areEqual('Test Account2', testResultList[0].outputCollection[1].get('Name')); - System.Assert.areEqual('22', testResultList[0].outputCollection[1].get('AccountNumber')); - System.Assert.areEqual(3, testResultList[0].outputCollection.size()); + Assert.areEqual(acct2Id, testResultList[0].outputCollection[1].Id); + Assert.areEqual('Test Account2', testResultList[0].outputCollection[1].get('Name')); + Assert.areEqual('22', testResultList[0].outputCollection[1].get('AccountNumber')); + Assert.areEqual(3, testResultList[0].outputCollection.size()); + Assert.isFalse(testResultList[0].noMatchFound); testUpsertList.clear(); - // Test add new record when no match + // Test skip add new record when no match Account acct4 = new Account(Name='Test Account4', AccountNumber='4'); insert acct4; testUpsert.inputCollection = accts; testUpsert.inputRecord = acct4; testUpsert.fieldAPIName = 'Name'; + testUpsert.skipInsertIfNoMatchFound = true; testUpsertList.add(testUpsert); testResultList = UpsertRecordByKey.upsertRecordByKey(testUpsertList); - System.Assert.areEqual(4, testResultList[0].outputCollection.size()); + Assert.areEqual(3, testResultList[0].outputCollection.size()); + Assert.IsTrue(testResultList[0].noMatchFound); + testUpsertList.clear(); + // Test add new record when no match + testUpsert.inputCollection = accts; + testUpsert.inputRecord = acct4; + testUpsert.fieldAPIName = 'Name'; + testUpsert.skipInsertIfNoMatchFound = false; + testUpsertList.add(testUpsert); + testResultList = UpsertRecordByKey.upsertRecordByKey(testUpsertList); + Assert.areEqual(4, testResultList[0].outputCollection.size()); + Assert.IsTrue(testResultList[0].noMatchFound); } } \ No newline at end of file From 5ddd245de620cde779b3020b44d1c9ce837c118c Mon Sep 17 00:00:00 2001 From: Eric Smith Date: Wed, 27 Nov 2024 16:06:42 -0500 Subject: [PATCH 04/18] Allow Field API value to be null --- .../force-app/main/default/classes/GetIndexFromKey.cls | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/flow_action_components/CollectionProcessors/force-app/main/default/classes/GetIndexFromKey.cls b/flow_action_components/CollectionProcessors/force-app/main/default/classes/GetIndexFromKey.cls index bf5c91590..10e7a53f1 100644 --- a/flow_action_components/CollectionProcessors/force-app/main/default/classes/GetIndexFromKey.cls +++ b/flow_action_components/CollectionProcessors/force-app/main/default/classes/GetIndexFromKey.cls @@ -13,6 +13,10 @@ * The primary use case for this action is to find the index of a record based on the value of a key field so it can be used in * other collecion actions that require an index value such as Add or Insert Record or Remove Recod from Collection. * + * Release Notes: + * v1.0.0 - 11/26/24 - Initial Version + * v1.0.1 - 11/27/24 - Allow Field API value to be null + * **/ global inherited sharing class GetIndexFromKey { @@ -26,7 +30,7 @@ global inherited sharing class GetIndexFromKey { @InvocableVariable(label='Field API Name') global String fieldAPIName; - @InvocableVariable(label='Field API Value' required=true) + @InvocableVariable(label='Field API Value') global String fieldValue; } From 95413798e0a4c4dc15c54604a2f270d15c36484d Mon Sep 17 00:00:00 2001 From: Eric Smith Date: Wed, 27 Nov 2024 16:36:43 -0500 Subject: [PATCH 05/18] Handle replacing last record in the collection --- .../main/default/classes/UpsertRecordByKey.cls | 10 +++++++++- .../main/default/classes/UpsertRecordByKeyTest.cls | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls b/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls index d09c9041f..a02bfbcc4 100644 --- a/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls +++ b/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls @@ -14,6 +14,10 @@ * * The primary use case for this action is to update a record collection in a flow prior to saving the changes to the database. * + * Release Notes: + * v1.0.0 - 11/26/24 - Initial Version + * v1.0.1 - 11/27/24 - Handle replacing last record in the collection + * **/ global inherited sharing class UpsertRecordByKey { @@ -91,7 +95,11 @@ global inherited sharing class UpsertRecordByKey { } } else { inputCollection.remove(index); - inputCollection.add(index, inputRecord); + if (index == inputCollection.size()) { // Removed last record + inputCollection.add(inputRecord); + } else { + inputCollection.add(index, inputRecord); + } } // Set Output Values diff --git a/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyTest.cls b/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyTest.cls index 20416241a..4db4afbd3 100644 --- a/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyTest.cls +++ b/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyTest.cls @@ -55,6 +55,20 @@ public with sharing class UpsertRecordByKeyTest { Assert.isFalse(testResultList[0].noMatchFound); testUpsertList.clear(); + // Test upsert last record in collection + Account upd_acct3 = accts[2]; + upd_acct3.AccountNumber='33'; + testUpsert.inputCollection = accts; + testUpsert.inputRecord = upd_acct3; + testUpsert.fieldAPIName = 'Name'; + testUpsertList.add(testUpsert); + testResultList = UpsertRecordByKey.upsertRecordByKey(testUpsertList); + Assert.areEqual('Test Account3', testResultList[0].outputCollection[2].get('Name')); + Assert.areEqual('33', testResultList[0].outputCollection[2].get('AccountNumber')); + Assert.areEqual(3, testResultList[0].outputCollection.size()); + Assert.isFalse(testResultList[0].noMatchFound); + testUpsertList.clear(); + // Test skip add new record when no match Account acct4 = new Account(Name='Test Account4', AccountNumber='4'); insert acct4; From a3534837bb0ef1fc473a03e002957fc6979e88e5 Mon Sep 17 00:00:00 2001 From: Eric Smith Date: Wed, 27 Nov 2024 21:24:02 -0500 Subject: [PATCH 06/18] UpsertRecordByKey Controller & Test Classes --- .../classes/UpsertRecordByKeyController.cls | 94 +++++++++++++++++++ .../UpsertRecordByKeyController.cls-meta.xml | 5 + .../UpsertRecordByKeyControllerTest.cls | 31 ++++++ ...sertRecordByKeyControllerTest.cls-meta.xml | 5 + 4 files changed, 135 insertions(+) create mode 100644 flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyController.cls create mode 100644 flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyController.cls-meta.xml create mode 100644 flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyControllerTest.cls create mode 100644 flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyControllerTest.cls-meta.xml diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyController.cls b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyController.cls new file mode 100644 index 000000000..616e386a1 --- /dev/null +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyController.cls @@ -0,0 +1,94 @@ +/** + * Call an Invocable Action from a Reactive Flow Screen Component + * + * Sample Controller with an AuraEnabled class, that calls an Invocable Flow Action, designed to be called by an LWC that is + * exposed to flow as a reactive Flow Screen Component + * + * Created By: Eric Smith + * + * 11/27/24 Version: 1.0.0 Initial Release + * + * LWC: upsertRecordByKey_rsc + * Controller: UpsertRecordByKeyController, UpsertRecordByKeyControllerTest + * Action: UpsertRecordByKey + * Collection Processors (https://unofficialsf.com/list-actions-for-flow/) + * +**/ + +// Code commented this way is a standard part of the template and should stay as is +// * Code commented this way should be adjusted to fit your use case + +// * Give the class a name similar to the invocable action +public with sharing class UpsertRecordByKeyController { + + // * Define each of the attributes to be returned by the invocable action and then passed back to the calling LWC + public class ReturnResultsWrapper { + List outputCollection; + Boolean noMatchFound; + } + + @AuraEnabled + // * Give the method a name similar to the invocable action + public static String upsertByKey( + // * Define each of the arguments to be passed into this controller by the LWC and then directly on to the invocable action + List inputCollection, + SObject inputRecord, + String fieldAPIName, + Boolean skipInsertIfNoMatchFound + ) { + + // Initialize the return results object + ReturnResultsWrapper curRR = new ReturnResultsWrapper(); + + // * Set the 2nd argument to the name of the Invocable Apex Action + Invocable.Action action = Invocable.Action.createCustomAction('apex', 'UpsertRecordByKey'); + + // * For each of the action's input attributes (Request), set the 1st argument to the name of the InvocableVariable + // * and the 2nd argument to the corresponding value passed into this controller + action.setInvocationParameter('inputCollection', inputCollection); + action.setInvocationParameter('inputRecord', inputRecord); + action.setInvocationParameter('fieldAPIName', fieldAPIName); + action.setInvocationParameter('skipInsertIfNoMatchFound', skipInsertIfNoMatchFound); + + // Invoke the action + List results = action.invoke(); + + // If a result was returned ... + if (results.size() > 0 && results[0].isSuccess()) { + + // * Assign each of the returned attributes to the corresponding value in the ReturnResultsWrapper + curRR.outputCollection = objToObj(results[0].getOutputParameters().get('outputCollection')); + curRR.noMatchFound = objToBoolean(results[0].getOutputParameters().get('noMatchFound')); + + } + // Return the results wrapper to the calling LWC + return JSON.serialize(curRR); + + } + + // Convert an object to a list of objects and fix date format + private static List objToObj(Object obj) { + return (List) JSON.deserialize(JSON.serialize(obj).replace('+0000','Z'), List.class); + } + + // Convert an object to a list of strings + // private static List objToList(Object obj) { + // return (List) JSON.deserialize(JSON.serialize(obj), List.class); + // } + + // Convert an object to a String + // private static String objToString(Object obj) { + // return String.valueof(obj); + // } + + // Convert an object to an integer + // private static Integer objToInteger(Object obj) { + // return Integer.valueof(obj); + // } + + // Convert an object to a boolean + private static Boolean objToBoolean(Object obj) { + return Boolean.valueof(obj); + } + +} \ No newline at end of file diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyController.cls-meta.xml b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyController.cls-meta.xml new file mode 100644 index 000000000..998805a82 --- /dev/null +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyController.cls-meta.xml @@ -0,0 +1,5 @@ + + + 62.0 + Active + diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyControllerTest.cls b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyControllerTest.cls new file mode 100644 index 000000000..df539e73b --- /dev/null +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyControllerTest.cls @@ -0,0 +1,31 @@ +@isTest +public with sharing class UpsertRecordByKeyControllerTest { + + @isTest + static void tryUpsertRecordByKey() { + + Account acct = new Account(Name='Test Account1', AccountNumber='1'); + insert acct; + List accts = new List(); + accts.add(acct); + acct = new Account(Name='Test Account2', AccountNumber='2'); + insert acct; + accts.add(acct); + acct = new Account(Name='Test Account3', AccountNumber='3'); + insert acct; + accts.add(acct); + + Account upd_acct1 = new Account(Name='Test Account1', AccountNumber='11'); + + String result = UpsertRecordByKeyController.upsertByKey(accts, upd_acct1, 'Name', false); + Map resultMap = (Map) JSON.deserializeUntyped(result); + Assert.areEqual('11', objToObj(resultMap.get('outputCollection'))[0].get('AccountNumber')); + + } + + // Convert an object to a list of objects + private static List objToObj(Object obj) { + return (List) JSON.deserialize(JSON.serialize(obj).replace('+0000','Z'), List.class); + } + +} \ No newline at end of file diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyControllerTest.cls-meta.xml b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyControllerTest.cls-meta.xml new file mode 100644 index 000000000..998805a82 --- /dev/null +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyControllerTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 62.0 + Active + From 6615ece007ee21b12f0e66b80d6ea2337e3dc251 Mon Sep 17 00:00:00 2001 From: Eric Smith Date: Wed, 27 Nov 2024 22:22:34 -0500 Subject: [PATCH 07/18] Reactive Screen Component & don't require input record --- .../default/classes/UpsertRecordByKey.cls | 10 +- .../upsertRecordByKey_rsc.html | 12 ++ .../upsertRecordByKey_rsc.js | 126 ++++++++++++++++++ .../upsertRecordByKey_rsc.js-meta.xml | 29 ++++ 4 files changed, 172 insertions(+), 5 deletions(-) create mode 100644 flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/upsertRecordByKey_rsc/upsertRecordByKey_rsc.html create mode 100644 flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/upsertRecordByKey_rsc/upsertRecordByKey_rsc.js create mode 100644 flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/upsertRecordByKey_rsc/upsertRecordByKey_rsc.js-meta.xml diff --git a/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls b/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls index a02bfbcc4..55579fcc5 100644 --- a/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls +++ b/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls @@ -25,10 +25,10 @@ global inherited sharing class UpsertRecordByKey { // Attributes passed in from the Flow global class Requests { - @InvocableVariable(label='Origginal record collection' required=true) + @InvocableVariable(label='Original record collection' required=true) global List inputCollection; - @InvocableVariable(label='New or replacement record' required=true) + @InvocableVariable(label='New or replacement record') global SObject inputRecord; @InvocableVariable(label='API name of keyfield (Default = Id)') @@ -101,10 +101,10 @@ global inherited sharing class UpsertRecordByKey { inputCollection.add(index, inputRecord); } } - - // Set Output Values - response.outputCollection = inputCollection.clone(); } + + // Set Output Values + response.outputCollection = inputCollection.clone(); responseWrapper.add(response); diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/upsertRecordByKey_rsc/upsertRecordByKey_rsc.html b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/upsertRecordByKey_rsc/upsertRecordByKey_rsc.html new file mode 100644 index 000000000..c7c82f506 --- /dev/null +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/upsertRecordByKey_rsc/upsertRecordByKey_rsc.html @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/upsertRecordByKey_rsc/upsertRecordByKey_rsc.js b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/upsertRecordByKey_rsc/upsertRecordByKey_rsc.js new file mode 100644 index 000000000..d75eb99b2 --- /dev/null +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/upsertRecordByKey_rsc/upsertRecordByKey_rsc.js @@ -0,0 +1,126 @@ +/** + * Lightning Web Component for Flow Screens: upsertRecordByKey + * + * Sample Reactive Flow Screen Component LWC, that calls an AuraEnabled Apex Method in a Controller, that calls an Invocable Flow Action + * + * Created By: Eric Smith + * + * 11/27/24 Version: 1.0.0 Initial Release + * + * LWC: upsertRecordByKey_rsc + * Controller: UpsertRecordByKeyController, UpsertRecordByKeyControllerTest + * Action: UpsertRecordByKey + * Collection Processors (https://unofficialsf.com/list-actions-for-flow/) + * +**/ + +// Code commented this way is a standard part of the template and should stay as is +// * Code commented this way should be adjusted to fit your use case + +// Standard lWC import +import { api, track, LightningElement } from 'lwc'; +// Standard import for notifying flow of changes in attribute values +import { FlowAttributeChangeEvent } from 'lightning/flowSupport'; + +// * Import the AuraEnabled Method from the Controller +import upsertByKey from '@salesforce/apex/UpsertRecordByKeyController.upsertByKey'; + +// * Define the name of the Component +export default class UpsertRecordByKey_rsc extends LightningElement { + + // * Define each of the LWC's attributes, with defaults if needed + @api inputCollection; + @api inputRecord; + @api fieldAPIName = 'Id'; + @api skipInsertIfNoMatchFound = false; + @api outputCollection; + @api noMatchFound = false; + + // Define the attribute used to store an error message + @api error; + + // Track prior value(s) for reactive attributes + @track oldReactiveValue; + + // Get the Reactive Attribute Value + get reactiveValue() { + // * Return reactive attributes as a string to be used in tracking + return JSON.stringify(this.inputCollection) + JSON.stringify(this.inputRecord) + this.fieldAPIName; + } + + // On rendering, check for a value or change in value of reactive attribute(s) and execute the handler + renderedCallback() { + if (this.reactiveValue && this.reactiveValue != this.oldReactiveValue) { + this._callAuraEnabledMethod(); + } + } + + // On a change in the reactive attribut(s), call the debounce handler for the AuraEnabledMethod handler + handleOnChange() { + this._debounceHandler(); + } + + // Call the Aura Enabled Method in the Controller + _callAuraEnabledMethod() { + // * Identify the Aura Enabled Method + upsertByKey({ + // * For each attribute to be passed to the controller - methodAttributeName: value from LWC + inputCollection: this.inputCollection, + inputRecord: this.inputRecord, + fieldAPIName: this.fieldAPIName, + skipInsertIfNoMatchFound: this.skipInsertIfNoMatchFound + }) + + // If a valid result is returned, + .then(result => { + + // parse the result into individual attributes and fix the date format + let returnResults = JSON.parse(result.replace(/\+0000/g, "Z")); + + // * LWC Output Attribute Name, value returned from the method + // * If the attribute is a record collection, call the _removeAttr function on the result value + this._fireFlowEvent("outputCollection", this._removeAttr(returnResults.outputCollection)); + this._fireFlowEvent("noMatchFound", returnResults.noMatchFound); + + }) + + // This template includes a standard 'error' output attribute that will be exposed on the flow screen + // If an error is returned, extract error message, and expose the error in the browser console + .catch(error => { + this.error = error?.body?.message ?? JSON.stringify(error); + // Skip if the error is undefined or empty + if (this.error.length > 2) { + console.error(this.error); + this._fireFlowEvent("error", this.error); + } else { + this.error = ""; + } + }); + + // Save the current value(s) of the reactive attribute(s) + this.oldReactiveValue = this.reactiveValue; + + } + + // Debounce the processing of the reactive changes + _debounceHandler() { + this._debounceTimer && clearTimeout(this._debounceTimer); + if (this.reactiveValue){ + this._debounceTimer = setTimeout(() => this._callAuraEnabledMethod(), 300); + } + } + + // Remove 'attributes' that get added by the JSON conversion from a record collection + _removeAttr(obj) { + obj.forEach(rec => { + delete rec['attributes']; + }); + return obj; + } + + // Dispatch the value of a changed attribute back to the flow + _fireFlowEvent(attributeName, data) { + this.dispatchEvent(new FlowAttributeChangeEvent(attributeName, data)); + } + +} \ No newline at end of file diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/upsertRecordByKey_rsc/upsertRecordByKey_rsc.js-meta.xml b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/upsertRecordByKey_rsc/upsertRecordByKey_rsc.js-meta.xml new file mode 100644 index 000000000..52572ef13 --- /dev/null +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/upsertRecordByKey_rsc/upsertRecordByKey_rsc.js-meta.xml @@ -0,0 +1,29 @@ + + + 62.0 + true + + + CP - Upsert Record By Key + + + lightning__FlowScreen + + + + + + + + + + + + + + + + + + + \ No newline at end of file From b8e75140f09eaccaf4aae28bf7bd8ec1a60ac59d Mon Sep 17 00:00:00 2001 From: Eric Smith Date: Wed, 27 Nov 2024 22:50:48 -0500 Subject: [PATCH 08/18] Allow for null record to support reactivity initial load on screen --- .../force-app/main/default/classes/UpsertRecordByKey.cls | 5 +++-- .../upsertRecordByKey_rsc/upsertRecordByKey_rsc.js-meta.xml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls b/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls index 55579fcc5..d2d3a41c4 100644 --- a/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls +++ b/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls @@ -77,7 +77,7 @@ global inherited sharing class UpsertRecordByKey { if (skipInsertIfNoMatchFound == null) { skipInsertIfNoMatchFound = false; } - String fieldValue = inputRecord.get(fieldAPIName)?.toString(); + String fieldValue = inputRecord?.get(fieldAPIName)?.toString(); // Set initial values getIndexResults idx = new getIndexResults(); @@ -86,6 +86,7 @@ global inherited sharing class UpsertRecordByKey { idx = getIndexFromKey(inputCollection, fieldAPIName, fieldValue); Integer index = idx.index; + response.noMatchFound = true; if (inputCollection != null && inputRecord != null) { response.noMatchFound = false; if (index == -1 || index == null || index >= inputCollection.size()) { @@ -102,7 +103,7 @@ global inherited sharing class UpsertRecordByKey { } } } - + // Set Output Values response.outputCollection = inputCollection.clone(); diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/upsertRecordByKey_rsc/upsertRecordByKey_rsc.js-meta.xml b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/upsertRecordByKey_rsc/upsertRecordByKey_rsc.js-meta.xml index 52572ef13..3e8df0406 100644 --- a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/upsertRecordByKey_rsc/upsertRecordByKey_rsc.js-meta.xml +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/upsertRecordByKey_rsc/upsertRecordByKey_rsc.js-meta.xml @@ -15,7 +15,7 @@ - + From ffcd00f5a6914b882df4cde6fb1afcf46a532703 Mon Sep 17 00:00:00 2001 From: Eric Smith Date: Tue, 3 Dec 2024 14:15:14 -0500 Subject: [PATCH 09/18] Get First Record reactive screen component --- .../default/classes/GetFirstController.cls | 93 +++++++++++++ .../classes/GetFirstController.cls-meta.xml | 5 + .../classes/GetFirstControllerTest.cls | 26 ++++ .../GetFirstControllerTest.cls-meta.xml | 5 + .../UpsertRecordByKeyControllerTest.cls | 2 +- .../lwc/getFirst_rsc/getFirst_rsc.html | 12 ++ .../default/lwc/getFirst_rsc/getFirst_rsc.js | 127 ++++++++++++++++++ .../lwc/getFirst_rsc/getFirst_rsc.js-meta.xml | 26 ++++ 8 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/GetFirstController.cls create mode 100644 flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/GetFirstController.cls-meta.xml create mode 100644 flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/GetFirstControllerTest.cls create mode 100644 flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/GetFirstControllerTest.cls-meta.xml create mode 100644 flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/getFirst_rsc/getFirst_rsc.html create mode 100644 flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/getFirst_rsc/getFirst_rsc.js create mode 100644 flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/getFirst_rsc/getFirst_rsc.js-meta.xml diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/GetFirstController.cls b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/GetFirstController.cls new file mode 100644 index 000000000..1e14b7fa8 --- /dev/null +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/GetFirstController.cls @@ -0,0 +1,93 @@ +/** + * Call an Invocable Action from a Reactive Flow Screen Component + * + * Sample Controller with an AuraEnabled class, that calls an Invocable Flow Action, designed to be called by an LWC that is + * exposed to flow as a reactive Flow Screen Component + * + * Created By: Eric Smith + * + * 12/3/24 Version: 1.0.0 Initial Release + * + * LWC: getFirst_rsc + * Controller: GetFirstController, GetFirstControllerTest + * Action: GetFirst + * Collection Processors (https://unofficialsf.com/list-actions-for-flow/) + * +**/ + +// Code commented this way is a standard part of the template and should stay as is +// * Code commented this way should be adjusted to fit your use case + +// * Give the class a name similar to the invocable action +public with sharing class GetFirstController { + + // * Define each of the attributes to be returned by the invocable action and then passed back to the calling LWC + public class ReturnResultsWrapper { + SObject outputMember; + } + + @AuraEnabled + // * Give the method a name similar to the invocable action + public static String getFirstRecord( + // * Define each of the arguments to be passed into this controller by the LWC and then directly on to the invocable action + List inputCollection, + Boolean enforceSingleMember + ) { + + // Initialize the return results object + ReturnResultsWrapper curRR = new ReturnResultsWrapper(); + + // * Set the 2nd argument to the name of the Invocable Apex Action + Invocable.Action action = Invocable.Action.createCustomAction('apex', 'GetFirst'); + + // * For each of the action's input attributes (Request), set the 1st argument to the name of the InvocableVariable + // * and the 2nd argument to the corresponding value passed into this controller + action.setInvocationParameter('inputCollection', inputCollection); + action.setInvocationParameter('enforceSingleMember', enforceSingleMember); + + // Invoke the action + List results = action.invoke(); + + // If a result was returned ... + if (results.size() > 0 && results[0].isSuccess()) { + + // * Assign each of the returned attributes to the corresponding value in the ReturnResultsWrapper + curRR.outputMember = objToRec(results[0].getOutputParameters().get('outputMember')); + + } + // Return the results wrapper to the calling LWC + return JSON.serialize(curRR); + + } + + // Convert an object to a list of objects and fix date format + // private static List objToObj(Object obj) { + // return (List) JSON.deserialize(JSON.serialize(obj).replace('+0000','Z'), List.class); + // } + + // Convert an object to an object and fix date format + private static SObject objToRec(Object obj) { + return (SObject) JSON.deserialize(JSON.serialize(obj).replace('+0000','Z'), SObject.class); + } + + // Convert an object to a list of strings + // private static List objToList(Object obj) { + // return (List) JSON.deserialize(JSON.serialize(obj), List.class); + // } + + // Convert an object to a String + // private static String objToString(Object obj) { + // return String.valueof(obj); + // } + + // Convert an object to an integer + // private static Integer objToInteger(Object obj) { + // return Integer.valueof(obj); + // } + + // Convert an object to a boolean + // private static Boolean objToBoolean(Object obj) { + // return Boolean.valueof(obj); + // } + +} \ No newline at end of file diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/GetFirstController.cls-meta.xml b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/GetFirstController.cls-meta.xml new file mode 100644 index 000000000..998805a82 --- /dev/null +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/GetFirstController.cls-meta.xml @@ -0,0 +1,5 @@ + + + 62.0 + Active + diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/GetFirstControllerTest.cls b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/GetFirstControllerTest.cls new file mode 100644 index 000000000..f64b24c31 --- /dev/null +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/GetFirstControllerTest.cls @@ -0,0 +1,26 @@ +@isTest +public with sharing class GetFirstControllerTest { + + @isTest + static void tryExtractFirstRecord() { + + Account acct = new Account(Name='Test Account1', AccountNumber='1'); + insert acct; + List accts = new List(); + accts.add(acct); + acct = new Account(Name='Test Account2', AccountNumber='2'); + insert acct; + accts.add(acct); + + String result = GetFirstController.getFirstRecord(accts, false); + Map resultMap = (Map) JSON.deserializeUntyped(result); + Assert.areEqual('1', objToRec(resultMap.get('outputMember')).get('AccountNumber')); + + } + + // Convert an object to an object and fix date format + private static SObject objToRec(Object obj) { + return (SObject) JSON.deserialize(JSON.serialize(obj).replace('+0000','Z'), SObject.class); + } + +} \ No newline at end of file diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/GetFirstControllerTest.cls-meta.xml b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/GetFirstControllerTest.cls-meta.xml new file mode 100644 index 000000000..998805a82 --- /dev/null +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/GetFirstControllerTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 62.0 + Active + diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyControllerTest.cls b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyControllerTest.cls index df539e73b..22479014a 100644 --- a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyControllerTest.cls +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyControllerTest.cls @@ -1,5 +1,5 @@ @isTest -public with sharing class UpsertRecordByKeyControllerTest { +public with sharing class UpsertRecordByKeyControllerTest { @isTest static void tryUpsertRecordByKey() { diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/getFirst_rsc/getFirst_rsc.html b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/getFirst_rsc/getFirst_rsc.html new file mode 100644 index 000000000..c1803e515 --- /dev/null +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/getFirst_rsc/getFirst_rsc.html @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/getFirst_rsc/getFirst_rsc.js b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/getFirst_rsc/getFirst_rsc.js new file mode 100644 index 000000000..93f24c837 --- /dev/null +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/getFirst_rsc/getFirst_rsc.js @@ -0,0 +1,127 @@ +/** + * Lightning Web Component for Flow Screens: getFirst + * + * Sample Reactive Flow Screen Component LWC, that calls an AuraEnabled Apex Method in a Controller, that calls an Invocable Flow Action + * + * Created By: Eric Smith + * + * 12/3/24 Version: 1.0.0 Initial Release + * + * LWC: getFirst_rsc + * Controller: GetFirstController, GetFirstControllerTest + * Action: GetFirst + * Collection Processors (https://unofficialsf.com/list-actions-for-flow/) + * +**/ + +// Code commented this way is a standard part of the template and should stay as is +// * Code commented this way should be adjusted to fit your use case + +// Standard lWC import +import { api, track, LightningElement } from 'lwc'; +// Standard import for notifying flow of changes in attribute values +import { FlowAttributeChangeEvent } from 'lightning/flowSupport'; + +// * Import the AuraEnabled Method from the Controller +import getFirstRecord from '@salesforce/apex/GetFirstController.getFirstRecord'; + +// * Define the name of the Component +export default class GetFirst_rsc extends LightningElement { + + // * Define each of the LWC's attributes, with defaults if needed + @api inputCollection; + @api enforceSingleMember = false; + @api outputMember; + + + // Define the attribute used to store an error message + @api error; + + // Track prior value(s) for reactive attributes + @track oldReactiveValue; + + // Get the Reactive Attribute Value + get reactiveValue() { + // * Return reactive attributes as a string to be used in tracking + return JSON.stringify(this.inputCollection); + } + + // On rendering, check for a value or change in value of reactive attribute(s) and execute the handler + renderedCallback() { + if (this.reactiveValue && this.reactiveValue != this.oldReactiveValue) { + this._callAuraEnabledMethod(); + } + } + + // On a change in the reactive attribut(s), call the debounce handler for the AuraEnabledMethod handler + handleOnChange() { + this._debounceHandler(); + } + + // Call the Aura Enabled Method in the Controller + _callAuraEnabledMethod() { + // * Identify the Aura Enabled Method + getFirstRecord({ + // * For each attribute to be passed to the controller - methodAttributeName: value from LWC + inputCollection: this.inputCollection, + enforceSingleMember: this.enforceSingleMember + }) + + // If a valid result is returned, + .then(result => { + + // parse the result into individual attributes and fix the date format + let returnResults = JSON.parse(result.replace(/\+0000/g, "Z")); + + // * LWC Output Attribute Name, value returned from the method + // * If the attribute is a record collection, call the _removeAttr function on the result value + this._fireFlowEvent("outputMember", this._removeAttrRec(returnResults.outputMember)); + + }) + + // This template includes a standard 'error' output attribute that will be exposed on the flow screen + // If an error is returned, extract error message, and expose the error in the browser console + .catch(error => { + this.error = error?.body?.message ?? JSON.stringify(error); + // Skip if the error is undefined or empty + if (this.error.length > 2) { + console.error(this.error); + this._fireFlowEvent("error", this.error); + } else { + this.error = ""; + } + }); + + // Save the current value(s) of the reactive attribute(s) + this.oldReactiveValue = this.reactiveValue; + + } + + // Debounce the processing of the reactive changes + _debounceHandler() { + this._debounceTimer && clearTimeout(this._debounceTimer); + if (this.reactiveValue){ + this._debounceTimer = setTimeout(() => this._callAuraEnabledMethod(), 300); + } + } + + // Remove 'attributes' that get added by the JSON conversion from a record collection + _removeAttr(obj) { + obj.forEach(rec => { + delete rec['attributes']; + }); + return obj; + } + + // Remove 'attributes' that get added by the JSON conversion from a single record + _removeAttrRec(rec) { + delete rec['attributes']; + return rec; + } + + // Dispatch the value of a changed attribute back to the flow + _fireFlowEvent(attributeName, data) { + this.dispatchEvent(new FlowAttributeChangeEvent(attributeName, data)); + } + +} \ No newline at end of file diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/getFirst_rsc/getFirst_rsc.js-meta.xml b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/getFirst_rsc/getFirst_rsc.js-meta.xml new file mode 100644 index 000000000..ffc2bde9a --- /dev/null +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/getFirst_rsc/getFirst_rsc.js-meta.xml @@ -0,0 +1,26 @@ + + + 62.0 + true + + + CP - Get First Record + + + lightning__FlowScreen + + + + + + + + + + + + + + + + \ No newline at end of file From f29fa039212f046f4655fef9946be1e09fe73884 Mon Sep 17 00:00:00 2001 From: Eric Smith Date: Sat, 7 Dec 2024 10:55:43 -0500 Subject: [PATCH 10/18] Remove comments --- .../reactiveRecordCollection_rsc.html | 12 +++ .../reactiveRecordCollection_rsc.js | 84 +++++++++++++++++++ .../reactiveRecordCollection_rsc.js-meta.xml | 26 ++++++ 3 files changed, 122 insertions(+) create mode 100644 flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecordCollection_rsc/reactiveRecordCollection_rsc.html create mode 100644 flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecordCollection_rsc/reactiveRecordCollection_rsc.js create mode 100644 flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecordCollection_rsc/reactiveRecordCollection_rsc.js-meta.xml diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecordCollection_rsc/reactiveRecordCollection_rsc.html b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecordCollection_rsc/reactiveRecordCollection_rsc.html new file mode 100644 index 000000000..377fef8b2 --- /dev/null +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecordCollection_rsc/reactiveRecordCollection_rsc.html @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecordCollection_rsc/reactiveRecordCollection_rsc.js b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecordCollection_rsc/reactiveRecordCollection_rsc.js new file mode 100644 index 000000000..4b3193b87 --- /dev/null +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecordCollection_rsc/reactiveRecordCollection_rsc.js @@ -0,0 +1,84 @@ +/** + * Lightning Web Component for Flow Screens: Reactive Record Collection + * + * Reactive Flow Screen Component LWC designed to seed a reactive screen component with an alternate record collection when the screen loads + * then on subsequent reactive refreshes of the component, the primary record collection is used. + * + * Created By: Eric Smith + * + * 12/24/24 Version: 1.0.0 Initial Release + * + * LWC: reactiveRecordCollection_rscc + * +**/ + +// Code commented this way is a standard part of the template and should stay as is +// * Code commented this way should be adjusted to fit your use case + +// Standard lWC import +import { api, track, LightningElement } from 'lwc'; +// Standard import for notifying flow of changes in attribute values +import { FlowAttributeChangeEvent } from 'lightning/flowSupport'; + +// * Define the name of the Component +export default class ReactiveRecordCollection_rsc extends LightningElement { + + // * Define each of the LWC's attributes, with defaults if needed + @api inputCollection; + @api alternateCollection; + @api outputCollection; + + + // Define the attribute used to store an error message + @api error; + + // Track prior value(s) for reactive attributes + @track oldReactiveValue; + + // Get the Reactive Attribute Value + get reactiveValue() { + // * Return reactive attributes as a string to be used in tracking + return JSON.stringify(this.inputCollection); + } + + // On rendering, check for a value or change in value of reactive attribute(s) and execute the handler + renderedCallback() { + if ((this.reactiveValue && this.reactiveValue != this.oldReactiveValue) || (this.alternateCollection && this.alternateCollection != null)) { + this._callReactiveMethod(); + } + } + + // On a change in the reactive attribut(s), call the debounce handler for the AuraEnabledMethod handler + handleOnChange() { + this._debounceHandler(); + } + + // Handle reactive processing logic + _callReactiveMethod() { + + if(this.inputCollection && this.inputCollection != null & this.inputCollection?.length > 0) { + this.outputCollection = [...this.inputCollection]; + } else { + this.outputCollection = [...this.alternateCollection]; + } + this._fireFlowEvent("outputCollection", this.outputCollection); + + // Save the current value(s) of the reactive attribute(s) + this.oldReactiveValue = this.reactiveValue; + + } + + // Debounce the processing of the reactive changes + _debounceHandler() { + this._debounceTimer && clearTimeout(this._debounceTimer); + if (this.reactiveValue){ + this._debounceTimer = setTimeout(() => this._callAuraEnabledMethod(), 300); + } + } + + // Dispatch the value of a changed attribute back to the flow + _fireFlowEvent(attributeName, data) { + this.dispatchEvent(new FlowAttributeChangeEvent(attributeName, data)); + } + +} \ No newline at end of file diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecordCollection_rsc/reactiveRecordCollection_rsc.js-meta.xml b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecordCollection_rsc/reactiveRecordCollection_rsc.js-meta.xml new file mode 100644 index 000000000..37cb95264 --- /dev/null +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecordCollection_rsc/reactiveRecordCollection_rsc.js-meta.xml @@ -0,0 +1,26 @@ + + + 62.0 + true + + + CP - Reactive Record Collection + Return the first of two record collections unless it is undefined or empty, otherwise return the alternate collection. + + lightning__FlowScreen + + + + + + + + + + + + + + + + \ No newline at end of file From 1d460f46b2e3273b9f912928a898a40eb377cf0d Mon Sep 17 00:00:00 2001 From: Eric Smith Date: Sat, 7 Dec 2024 10:56:17 -0500 Subject: [PATCH 11/18] remove extra blank line --- .../reactiveRecordCollection_rsc/reactiveRecordCollection_rsc.js | 1 - 1 file changed, 1 deletion(-) diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecordCollection_rsc/reactiveRecordCollection_rsc.js b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecordCollection_rsc/reactiveRecordCollection_rsc.js index 4b3193b87..ff643a55f 100644 --- a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecordCollection_rsc/reactiveRecordCollection_rsc.js +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecordCollection_rsc/reactiveRecordCollection_rsc.js @@ -27,7 +27,6 @@ export default class ReactiveRecordCollection_rsc extends LightningElement { @api inputCollection; @api alternateCollection; @api outputCollection; - // Define the attribute used to store an error message @api error; From 199aca202c1083d00a86c03728161d81edc211b1 Mon Sep 17 00:00:00 2001 From: Eric Smith Date: Sat, 7 Dec 2024 13:07:49 -0500 Subject: [PATCH 12/18] Reactive Record component --- .../reactiveRecord_rsc.html | 12 +++ .../reactiveRecord_rsc/reactiveRecord_rsc.js | 83 +++++++++++++++++++ .../reactiveRecord_rsc.js-meta.xml | 26 ++++++ 3 files changed, 121 insertions(+) create mode 100644 flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecord_rsc/reactiveRecord_rsc.html create mode 100644 flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecord_rsc/reactiveRecord_rsc.js create mode 100644 flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecord_rsc/reactiveRecord_rsc.js-meta.xml diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecord_rsc/reactiveRecord_rsc.html b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecord_rsc/reactiveRecord_rsc.html new file mode 100644 index 000000000..ffe5e27e2 --- /dev/null +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecord_rsc/reactiveRecord_rsc.html @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecord_rsc/reactiveRecord_rsc.js b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecord_rsc/reactiveRecord_rsc.js new file mode 100644 index 000000000..a9467c5aa --- /dev/null +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecord_rsc/reactiveRecord_rsc.js @@ -0,0 +1,83 @@ +/** + * Lightning Web Component for Flow Screens: Reactive Record + * + * Reactive Flow Screen Component LWC designed to seed a reactive screen component with an alternate record when the screen loads + * then on subsequent reactive refreshes of the component, the primary record is used. + * + * Created By: Eric Smith + * + * 12/24/24 Version: 1.0.0 Initial Release + * + * LWC: reactiveRecord_rscc + * +**/ + +// Code commented this way is a standard part of the template and should stay as is +// * Code commented this way should be adjusted to fit your use case + +// Standard lWC import +import { api, track, LightningElement } from 'lwc'; +// Standard import for notifying flow of changes in attribute values +import { FlowAttributeChangeEvent } from 'lightning/flowSupport'; + +// * Define the name of the Component +export default class ReactiveRecord_rsc extends LightningElement { + + // * Define each of the LWC's attributes, with defaults if needed + @api inputRecord; + @api alternateRecord; + @api outputRecord; + + // Define the attribute used to store an error message + @api error; + + // Track prior value(s) for reactive attributes + @track oldReactiveValue; + + // Get the Reactive Attribute Value + get reactiveValue() { + // * Return reactive attributes as a string to be used in tracking + return JSON.stringify(this.inputRecord); + } + + // On rendering, check for a value or change in value of reactive attribute(s) and execute the handler + renderedCallback() { + if ((this.reactiveValue && this.reactiveValue != this.oldReactiveValue) || (this.alternateRecord && this.alternateRecord != null)) { + this._callReactiveMethod(); + } + } + + // On a change in the reactive attribut(s), call the debounce handler for the AuraEnabledMethod handler + handleOnChange() { + this._debounceHandler(); + } + + // Handle reactive processing logic + _callReactiveMethod() { + + if(this.inputRecord && this.inputRecord != null) { + this.outputRecord = this.inputRecord; + } else { + this.outputRecord = this.alternateRecord; + } + this._fireFlowEvent("outputRecord", this.outputRecord); + + // Save the current value(s) of the reactive attribute(s) + this.oldReactiveValue = this.reactiveValue; + + } + + // Debounce the processing of the reactive changes + _debounceHandler() { + this._debounceTimer && clearTimeout(this._debounceTimer); + if (this.reactiveValue){ + this._debounceTimer = setTimeout(() => this._callAuraEnabledMethod(), 300); + } + } + + // Dispatch the value of a changed attribute back to the flow + _fireFlowEvent(attributeName, data) { + this.dispatchEvent(new FlowAttributeChangeEvent(attributeName, data)); + } + +} \ No newline at end of file diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecord_rsc/reactiveRecord_rsc.js-meta.xml b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecord_rsc/reactiveRecord_rsc.js-meta.xml new file mode 100644 index 000000000..621117152 --- /dev/null +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecord_rsc/reactiveRecord_rsc.js-meta.xml @@ -0,0 +1,26 @@ + + + 62.0 + true + + + CP - Reactive Record + Return the first of two records unless it is undefined or empty, otherwise return the alternate record. + + lightning__FlowScreen + + + + + + + + + + + + + + + + \ No newline at end of file From 09f5247c542c86f507225550afd33de65262e008 Mon Sep 17 00:00:00 2001 From: Eric Smith Date: Sat, 7 Dec 2024 13:43:02 -0500 Subject: [PATCH 13/18] GetFirst update inputCollection description --- .../main/default/lwc/getFirst_rsc/getFirst_rsc.js-meta.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/getFirst_rsc/getFirst_rsc.js-meta.xml b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/getFirst_rsc/getFirst_rsc.js-meta.xml index ffc2bde9a..55378ac3f 100644 --- a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/getFirst_rsc/getFirst_rsc.js-meta.xml +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/getFirst_rsc/getFirst_rsc.js-meta.xml @@ -14,7 +14,7 @@ - + From 67ff0f069f3c82dbaf96e10a6a2413fc93d52eef Mon Sep 17 00:00:00 2001 From: Eric Smith Date: Sun, 8 Dec 2024 13:43:45 -0500 Subject: [PATCH 14/18] fix debouncehandler --- .../reactiveRecordCollection_rsc.js | 2 +- .../main/default/lwc/reactiveRecord_rsc/reactiveRecord_rsc.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecordCollection_rsc/reactiveRecordCollection_rsc.js b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecordCollection_rsc/reactiveRecordCollection_rsc.js index ff643a55f..9ec6cb3a5 100644 --- a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecordCollection_rsc/reactiveRecordCollection_rsc.js +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecordCollection_rsc/reactiveRecordCollection_rsc.js @@ -71,7 +71,7 @@ export default class ReactiveRecordCollection_rsc extends LightningElement { _debounceHandler() { this._debounceTimer && clearTimeout(this._debounceTimer); if (this.reactiveValue){ - this._debounceTimer = setTimeout(() => this._callAuraEnabledMethod(), 300); + this._debounceTimer = setTimeout(() => this._callReactiveMethod(), 300); } } diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecord_rsc/reactiveRecord_rsc.js b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecord_rsc/reactiveRecord_rsc.js index a9467c5aa..8dab47839 100644 --- a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecord_rsc/reactiveRecord_rsc.js +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecord_rsc/reactiveRecord_rsc.js @@ -71,7 +71,7 @@ export default class ReactiveRecord_rsc extends LightningElement { _debounceHandler() { this._debounceTimer && clearTimeout(this._debounceTimer); if (this.reactiveValue){ - this._debounceTimer = setTimeout(() => this._callAuraEnabledMethod(), 300); + this._debounceTimer = setTimeout(() => this._callReactiveMethod(), 300); } } From 6271aa80769108264b4063389f738de65b42caa1 Mon Sep 17 00:00:00 2001 From: Eric Smith Date: Sun, 8 Dec 2024 13:52:49 -0500 Subject: [PATCH 15/18] Don't call GetFirst with an empty collection --- .../default/classes/GetFirstController.cls | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/GetFirstController.cls b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/GetFirstController.cls index 1e14b7fa8..17b8789cc 100644 --- a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/GetFirstController.cls +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/GetFirstController.cls @@ -45,16 +45,19 @@ public with sharing class GetFirstController { action.setInvocationParameter('inputCollection', inputCollection); action.setInvocationParameter('enforceSingleMember', enforceSingleMember); - // Invoke the action - List results = action.invoke(); - - // If a result was returned ... - if (results.size() > 0 && results[0].isSuccess()) { - - // * Assign each of the returned attributes to the corresponding value in the ReturnResultsWrapper - curRR.outputMember = objToRec(results[0].getOutputParameters().get('outputMember')); - + if (inputCollection.size() > 0) { // * Custom check - not part of template + // Invoke the action + List results = action.invoke(); + + // If a result was returned ... + if (results.size() > 0 && results[0].isSuccess()) { + + // * Assign each of the returned attributes to the corresponding value in the ReturnResultsWrapper + curRR.outputMember = objToRec(results[0].getOutputParameters().get('outputMember')); + + } } + // Return the results wrapper to the calling LWC return JSON.serialize(curRR); From 75b04535c0835d46d2062728e5c7f773d26ac45e Mon Sep 17 00:00:00 2001 From: Eric Smith Date: Sun, 15 Dec 2024 18:18:10 -0500 Subject: [PATCH 16/18] more debugging --- .../main/default/classes/GetIndexFromKey.cls | 2 ++ .../main/default/classes/UpsertRecordByKey.cls | 3 +++ .../classes/UpsertRecordByKeyController.cls | 2 ++ .../default/lwc/getFirst_rsc/getFirst_rsc.js | 4 ++++ .../upsertRecordByKey_rsc.js | 16 ++++++++++++---- 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/flow_action_components/CollectionProcessors/force-app/main/default/classes/GetIndexFromKey.cls b/flow_action_components/CollectionProcessors/force-app/main/default/classes/GetIndexFromKey.cls index 10e7a53f1..faee7e283 100644 --- a/flow_action_components/CollectionProcessors/force-app/main/default/classes/GetIndexFromKey.cls +++ b/flow_action_components/CollectionProcessors/force-app/main/default/classes/GetIndexFromKey.cls @@ -61,6 +61,8 @@ global inherited sharing class GetIndexFromKey { List inputCollection = curRequest.inputCollection; String fieldAPIName = curRequest.fieldAPIName; Object fieldValue = curRequest.fieldValue; + system.debug('INDEX inputCollection: ' + inputCollection); + system.debug('INDEX fieldValue: ' + fieldValue); // Process input attributes if (fieldAPIName == '' || fieldAPIName == null) { diff --git a/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls b/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls index d2d3a41c4..a63ee208f 100644 --- a/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls +++ b/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls @@ -69,6 +69,8 @@ global inherited sharing class UpsertRecordByKey { SObject inputRecord = curRequest.inputRecord; String fieldAPIName = curRequest.fieldAPIName; Boolean skipInsertIfNoMatchFound = curRequest.skipInsertIfNoMatchFound; + system.debug('CLASS inputCollection: ' + inputCollection); + system.debug('CLASS inputRecord: ' + inputRecord); // Process input attributes if (fieldAPIName == '' || fieldAPIName == null) { @@ -126,6 +128,7 @@ global inherited sharing class UpsertRecordByKey { indexAction.setInvocationParameter('inputCollection', inputCollection); indexAction.setInvocationParameter('fieldAPIName', fieldAPIName); indexAction.setInvocationParameter('fieldValue', fieldValue); + system.debug('CLASS fieldValue: ' + fieldValue); List results = indexAction.invoke(); diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyController.cls b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyController.cls index 616e386a1..217c0cd3c 100644 --- a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyController.cls +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyController.cls @@ -49,6 +49,8 @@ public with sharing class UpsertRecordByKeyController { action.setInvocationParameter('inputRecord', inputRecord); action.setInvocationParameter('fieldAPIName', fieldAPIName); action.setInvocationParameter('skipInsertIfNoMatchFound', skipInsertIfNoMatchFound); +system.debug('CONTROLLER inputCollection: ' + inputCollection); +system.debug('CONTROLLER inputRecord: ' + inputRecord); // Invoke the action List results = action.invoke(); diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/getFirst_rsc/getFirst_rsc.js b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/getFirst_rsc/getFirst_rsc.js index 93f24c837..cc8c78b4c 100644 --- a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/getFirst_rsc/getFirst_rsc.js +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/getFirst_rsc/getFirst_rsc.js @@ -48,6 +48,8 @@ export default class GetFirst_rsc extends LightningElement { // On rendering, check for a value or change in value of reactive attribute(s) and execute the handler renderedCallback() { + console.log("🚀 ~ GetFirst_rsc ~ renderedCallback ~ this.reactiveValue:", this.reactiveValue); + console.log("🚀 ~ GetFirst_rsc ~ renderedCallback ~ this.oldReactiveValue:", this.oldReactiveValue); if (this.reactiveValue && this.reactiveValue != this.oldReactiveValue) { this._callAuraEnabledMethod(); } @@ -60,6 +62,7 @@ export default class GetFirst_rsc extends LightningElement { // Call the Aura Enabled Method in the Controller _callAuraEnabledMethod() { + console.log("🚀 ~ GetFirst_rsc ~ _callAuraEnabledMethod ~ _callAuraEnabledMethod:"); // * Identify the Aura Enabled Method getFirstRecord({ // * For each attribute to be passed to the controller - methodAttributeName: value from LWC @@ -72,6 +75,7 @@ export default class GetFirst_rsc extends LightningElement { // parse the result into individual attributes and fix the date format let returnResults = JSON.parse(result.replace(/\+0000/g, "Z")); + console.log("🚀 ~ GetFirst_rsc ~ _callAuraEnabledMethod ~ returnResults:", returnResults); // * LWC Output Attribute Name, value returned from the method // * If the attribute is a record collection, call the _removeAttr function on the result value diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/upsertRecordByKey_rsc/upsertRecordByKey_rsc.js b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/upsertRecordByKey_rsc/upsertRecordByKey_rsc.js index d75eb99b2..35ed38cee 100644 --- a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/upsertRecordByKey_rsc/upsertRecordByKey_rsc.js +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/upsertRecordByKey_rsc/upsertRecordByKey_rsc.js @@ -44,13 +44,18 @@ export default class UpsertRecordByKey_rsc extends LightningElement { // Get the Reactive Attribute Value get reactiveValue() { + console.log("🚀 ~ UpsertRecordByKey_rsc ~ getreactiveValue ~ reactiveValue:", JSON.stringify(this.inputRecord)); // * Return reactive attributes as a string to be used in tracking - return JSON.stringify(this.inputCollection) + JSON.stringify(this.inputRecord) + this.fieldAPIName; + const rv = + (this.inputRecord) ? JSON.stringify(this.inputRecord) : '' + + (this.inputCollection) ? JSON.stringify(this.inputCollection) : ''; + return rv; } // On rendering, check for a value or change in value of reactive attribute(s) and execute the handler renderedCallback() { - if (this.reactiveValue && this.reactiveValue != this.oldReactiveValue) { + console.log("🚀 ~ UpsertRecordByKey_rsc ~ renderedCallback ~ this.reactiveValue, this.oldReactiveValue:", this.reactiveValue, this.oldReactiveValue); + if (this.reactiveValue && this.reactiveValue != this.oldReactiveValue && this.inputRecord && this.inputCollection) { this._callAuraEnabledMethod(); } } @@ -62,17 +67,20 @@ export default class UpsertRecordByKey_rsc extends LightningElement { // Call the Aura Enabled Method in the Controller _callAuraEnabledMethod() { + console.log("🚀 ~ UpsertRecordByKey_rsc ~ _callAuraEnabledMethod ~ this.inputRecord:", {...this.inputRecord}); + console.log("🚀 ~ UpsertRecordByKey_rsc ~ _callAuraEnabledMethod ~ this.inputCollection.length:", this.inputCollection.length); // * Identify the Aura Enabled Method upsertByKey({ // * For each attribute to be passed to the controller - methodAttributeName: value from LWC - inputCollection: this.inputCollection, - inputRecord: this.inputRecord, + inputCollection: [...this.inputCollection], + inputRecord: {...this.inputRecord}, fieldAPIName: this.fieldAPIName, skipInsertIfNoMatchFound: this.skipInsertIfNoMatchFound }) // If a valid result is returned, .then(result => { + console.log("🚀 ~ UpsertRecordByKey_rsc ~ _callAuraEnabledMethod ~ result:", result); // parse the result into individual attributes and fix the date format let returnResults = JSON.parse(result.replace(/\+0000/g, "Z")); From 58a8366141d5aef764001e0de19bc527aa2f5d4a Mon Sep 17 00:00:00 2001 From: Eric Smith Date: Sun, 22 Dec 2024 09:26:26 -0500 Subject: [PATCH 17/18] finalize reactiveRecord --- .gitignore | 1 + .../default/lwc/reactiveRecord_rsc/reactiveRecord_rsc.js | 4 ++-- .../lwc/reactiveRecord_rsc/reactiveRecord_rsc.js-meta.xml | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 21a63ee2d..f5195d103 100644 --- a/.gitignore +++ b/.gitignore @@ -41,5 +41,6 @@ $RECYCLE.BIN/ .gitignore package-lock.json jsconfig.json +.lwc/jsconfig.json .sf/config.json .pmdCache diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecord_rsc/reactiveRecord_rsc.js b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecord_rsc/reactiveRecord_rsc.js index 8dab47839..a7ced8887 100644 --- a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecord_rsc/reactiveRecord_rsc.js +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecord_rsc/reactiveRecord_rsc.js @@ -56,9 +56,9 @@ export default class ReactiveRecord_rsc extends LightningElement { _callReactiveMethod() { if(this.inputRecord && this.inputRecord != null) { - this.outputRecord = this.inputRecord; + this.outputRecord = {...this.inputRecord}; } else { - this.outputRecord = this.alternateRecord; + this.outputRecord = {...this.alternateRecord}; } this._fireFlowEvent("outputRecord", this.outputRecord); diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecord_rsc/reactiveRecord_rsc.js-meta.xml b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecord_rsc/reactiveRecord_rsc.js-meta.xml index 621117152..54f7f7a83 100644 --- a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecord_rsc/reactiveRecord_rsc.js-meta.xml +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/reactiveRecord_rsc/reactiveRecord_rsc.js-meta.xml @@ -14,9 +14,9 @@ - - - + + + From 1b037693dcf5b243af60e4ff455648d33329ff1c Mon Sep 17 00:00:00 2001 From: Eric Smith Date: Sun, 22 Dec 2024 11:55:01 -0500 Subject: [PATCH 18/18] Remove debug logging --- .../force-app/main/default/classes/GetIndexFromKey.cls | 2 -- .../force-app/main/default/classes/UpsertRecordByKey.cls | 3 --- .../main/default/classes/UpsertRecordByKeyController.cls | 2 -- .../force-app/main/default/lwc/getFirst_rsc/getFirst_rsc.js | 4 ---- .../lwc/upsertRecordByKey_rsc/upsertRecordByKey_rsc.js | 6 +----- 5 files changed, 1 insertion(+), 16 deletions(-) diff --git a/flow_action_components/CollectionProcessors/force-app/main/default/classes/GetIndexFromKey.cls b/flow_action_components/CollectionProcessors/force-app/main/default/classes/GetIndexFromKey.cls index faee7e283..10e7a53f1 100644 --- a/flow_action_components/CollectionProcessors/force-app/main/default/classes/GetIndexFromKey.cls +++ b/flow_action_components/CollectionProcessors/force-app/main/default/classes/GetIndexFromKey.cls @@ -61,8 +61,6 @@ global inherited sharing class GetIndexFromKey { List inputCollection = curRequest.inputCollection; String fieldAPIName = curRequest.fieldAPIName; Object fieldValue = curRequest.fieldValue; - system.debug('INDEX inputCollection: ' + inputCollection); - system.debug('INDEX fieldValue: ' + fieldValue); // Process input attributes if (fieldAPIName == '' || fieldAPIName == null) { diff --git a/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls b/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls index a63ee208f..d2d3a41c4 100644 --- a/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls +++ b/flow_action_components/CollectionProcessors/force-app/main/default/classes/UpsertRecordByKey.cls @@ -69,8 +69,6 @@ global inherited sharing class UpsertRecordByKey { SObject inputRecord = curRequest.inputRecord; String fieldAPIName = curRequest.fieldAPIName; Boolean skipInsertIfNoMatchFound = curRequest.skipInsertIfNoMatchFound; - system.debug('CLASS inputCollection: ' + inputCollection); - system.debug('CLASS inputRecord: ' + inputRecord); // Process input attributes if (fieldAPIName == '' || fieldAPIName == null) { @@ -128,7 +126,6 @@ global inherited sharing class UpsertRecordByKey { indexAction.setInvocationParameter('inputCollection', inputCollection); indexAction.setInvocationParameter('fieldAPIName', fieldAPIName); indexAction.setInvocationParameter('fieldValue', fieldValue); - system.debug('CLASS fieldValue: ' + fieldValue); List results = indexAction.invoke(); diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyController.cls b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyController.cls index 217c0cd3c..616e386a1 100644 --- a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyController.cls +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/classes/UpsertRecordByKeyController.cls @@ -49,8 +49,6 @@ public with sharing class UpsertRecordByKeyController { action.setInvocationParameter('inputRecord', inputRecord); action.setInvocationParameter('fieldAPIName', fieldAPIName); action.setInvocationParameter('skipInsertIfNoMatchFound', skipInsertIfNoMatchFound); -system.debug('CONTROLLER inputCollection: ' + inputCollection); -system.debug('CONTROLLER inputRecord: ' + inputRecord); // Invoke the action List results = action.invoke(); diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/getFirst_rsc/getFirst_rsc.js b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/getFirst_rsc/getFirst_rsc.js index cc8c78b4c..93f24c837 100644 --- a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/getFirst_rsc/getFirst_rsc.js +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/getFirst_rsc/getFirst_rsc.js @@ -48,8 +48,6 @@ export default class GetFirst_rsc extends LightningElement { // On rendering, check for a value or change in value of reactive attribute(s) and execute the handler renderedCallback() { - console.log("🚀 ~ GetFirst_rsc ~ renderedCallback ~ this.reactiveValue:", this.reactiveValue); - console.log("🚀 ~ GetFirst_rsc ~ renderedCallback ~ this.oldReactiveValue:", this.oldReactiveValue); if (this.reactiveValue && this.reactiveValue != this.oldReactiveValue) { this._callAuraEnabledMethod(); } @@ -62,7 +60,6 @@ export default class GetFirst_rsc extends LightningElement { // Call the Aura Enabled Method in the Controller _callAuraEnabledMethod() { - console.log("🚀 ~ GetFirst_rsc ~ _callAuraEnabledMethod ~ _callAuraEnabledMethod:"); // * Identify the Aura Enabled Method getFirstRecord({ // * For each attribute to be passed to the controller - methodAttributeName: value from LWC @@ -75,7 +72,6 @@ export default class GetFirst_rsc extends LightningElement { // parse the result into individual attributes and fix the date format let returnResults = JSON.parse(result.replace(/\+0000/g, "Z")); - console.log("🚀 ~ GetFirst_rsc ~ _callAuraEnabledMethod ~ returnResults:", returnResults); // * LWC Output Attribute Name, value returned from the method // * If the attribute is a record collection, call the _removeAttr function on the result value diff --git a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/upsertRecordByKey_rsc/upsertRecordByKey_rsc.js b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/upsertRecordByKey_rsc/upsertRecordByKey_rsc.js index 35ed38cee..602d79d83 100644 --- a/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/upsertRecordByKey_rsc/upsertRecordByKey_rsc.js +++ b/flow_screen_components/ReactiveCollectionProcessors/force-app/main/default/lwc/upsertRecordByKey_rsc/upsertRecordByKey_rsc.js @@ -44,7 +44,6 @@ export default class UpsertRecordByKey_rsc extends LightningElement { // Get the Reactive Attribute Value get reactiveValue() { - console.log("🚀 ~ UpsertRecordByKey_rsc ~ getreactiveValue ~ reactiveValue:", JSON.stringify(this.inputRecord)); // * Return reactive attributes as a string to be used in tracking const rv = (this.inputRecord) ? JSON.stringify(this.inputRecord) : '' + @@ -54,7 +53,6 @@ export default class UpsertRecordByKey_rsc extends LightningElement { // On rendering, check for a value or change in value of reactive attribute(s) and execute the handler renderedCallback() { - console.log("🚀 ~ UpsertRecordByKey_rsc ~ renderedCallback ~ this.reactiveValue, this.oldReactiveValue:", this.reactiveValue, this.oldReactiveValue); if (this.reactiveValue && this.reactiveValue != this.oldReactiveValue && this.inputRecord && this.inputCollection) { this._callAuraEnabledMethod(); } @@ -67,8 +65,7 @@ export default class UpsertRecordByKey_rsc extends LightningElement { // Call the Aura Enabled Method in the Controller _callAuraEnabledMethod() { - console.log("🚀 ~ UpsertRecordByKey_rsc ~ _callAuraEnabledMethod ~ this.inputRecord:", {...this.inputRecord}); - console.log("🚀 ~ UpsertRecordByKey_rsc ~ _callAuraEnabledMethod ~ this.inputCollection.length:", this.inputCollection.length); + // * Identify the Aura Enabled Method upsertByKey({ // * For each attribute to be passed to the controller - methodAttributeName: value from LWC @@ -80,7 +77,6 @@ export default class UpsertRecordByKey_rsc extends LightningElement { // If a valid result is returned, .then(result => { - console.log("🚀 ~ UpsertRecordByKey_rsc ~ _callAuraEnabledMethod ~ result:", result); // parse the result into individual attributes and fix the date format let returnResults = JSON.parse(result.replace(/\+0000/g, "Z"));