From 67d3fb5feb9facfdffc7a170412ab4c59b56c739 Mon Sep 17 00:00:00 2001 From: "jennifer.bennett_sfemu" Date: Fri, 26 Jul 2024 07:47:10 -0500 Subject: [PATCH 01/12] Small change to readme to create the feature branch --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 69ba36bfb..44df95191 100644 --- a/README.md +++ b/README.md @@ -26,4 +26,4 @@ PMM AND SFDO BASE ARE NON-SFDC APPLICATIONS OR THIRD-PARTY APPLICATIONS, AND NOT SFDC WILL NOT HAVE ANY LIABILITY ARISING OUT OF OR RELATED TO YOUR USE OF PMM OR SFDO BASE FOR ANY DIRECT DAMAGES OR FOR ANY LOST PROFITS, REVENUES, GOODWILL OR INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL, EXEMPLARY, COVER, BUSINESS INTERRUPTION OR PUNITIVE DAMAGES, WHETHER AN ACTION IS IN CONTRACT OR TORT AND REGARDLESS OF THE THEORY OF LIABILITY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES OR IF A REMEDY OTHERWISE FAILS OF ITS ESSENTIAL PURPOSE. THE FOREGOING DISCLAIMER WILL NOT APPLY TO THE EXTENT PROHIBITED BY LAW. SFDC DISCLAIMS ALL LIABILITY AND INDEMNIFICATION OBLIGATIONS FOR ANY HARM OR DAMAGES CAUSED BY ANY THIRD-PARTY HOSTING PROVIDERS. -THIS AGREEMENT SHALL BE GOVERNED EXCLUSIVELY BY, AND CONSTRUED EXCLUSIVELY IN ACCORDANCE WITH, THE LAWS OF THE UNITED STATES AND THE STATE OF CALIFORNIA, WITHOUT REGARD TO ITS CONFLICT OF LAWS PROVISIONS. THE STATE AND FEDERAL COURTS LOCATED IN SAN FRANCISCO, CALIFORNIA SHALL HAVE EXCLUSIVE JURISDICTION TO ADJUDICATE ANY DISPUTE ARISING OUT OF OR RELATING TO THIS AGREEMENT. EACH PARTY HEREBY CONSENTS TO THE JURISDICTION OF SUCH COURTS AND WAIVES ANY RIGHT IT MAY OTHERWISE HAVE TO CHALLENGE THE APPROPRIATENESS OF SUCH FORUMS. +THIS AGREEMENT SHALL BE GOVERNED EXCLUSIVELY BY, AND CONSTRUED EXCLUSIVELY IN ACCORDANCE WITH, THE LAWS OF THE UNITED STATES AND THE STATE OF CALIFORNIA, WITHOUT REGARD TO ITS CONFLICT OF LAWS PROVISIONS. THE STATE AND FEDERAL COURTS LOCATED IN SAN FRANCISCO, CALIFORNIA SHALL HAVE EXCLUSIVE JURISDICTION TO ADJUDICATE ANY DISPUTE ARISING OUT OF OR RELATING TO THIS AGREEMENT. EACH PARTY HEREBY CONSENTS TO THE JURISDICTION OF SUCH COURTS AND WAIVES ANY RIGHT IT MAY OTHERWISE HAVE TO CHALLENGE THE APPROPRIATENESS OF SUCH FORUMS. \ No newline at end of file From 202202254e805ffda31818cd41702f0e964466f3 Mon Sep 17 00:00:00 2001 From: jjbennett Date: Fri, 26 Jul 2024 08:16:37 -0500 Subject: [PATCH 02/12] Switch install security to admin only --- cumulusci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cumulusci.yml b/cumulusci.yml index d70ffd9c1..3cf8fee12 100644 --- a/cumulusci.yml +++ b/cumulusci.yml @@ -434,6 +434,8 @@ flows: task: update_dependencies 2: task: install_managed + options: + security_type: NONE 3: task: deploy_customer_profiles ui_options: From 79a667f449fb6cd72f668b3678a5d7f4fc9bff07 Mon Sep 17 00:00:00 2001 From: Jennifer Bennett Date: Tue, 27 Aug 2024 08:14:03 -0500 Subject: [PATCH 03/12] Remove order by and filter by Contact name. --- force-app/main/default/classes/ProgramEngagementSelector.cls | 2 +- .../main/default/classes/ProgramEngagementSelector_TEST.cls | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/force-app/main/default/classes/ProgramEngagementSelector.cls b/force-app/main/default/classes/ProgramEngagementSelector.cls index 5ac052935..753641de0 100755 --- a/force-app/main/default/classes/ProgramEngagementSelector.cls +++ b/force-app/main/default/classes/ProgramEngagementSelector.cls @@ -197,7 +197,7 @@ public with sharing class ProgramEngagementSelector { FIND :searchText IN ALL FIELDS RETURNING - Contact(FirstName, LastName, Email ORDER BY LastName LIMIT :SEARCH_LIMIT) + Contact(FirstName, LastName, Email LIMIT :SEARCH_LIMIT) ]; return getEngagementIdsFromSOSLResult( diff --git a/force-app/main/default/classes/ProgramEngagementSelector_TEST.cls b/force-app/main/default/classes/ProgramEngagementSelector_TEST.cls index c25f445e1..e9bd1d537 100755 --- a/force-app/main/default/classes/ProgramEngagementSelector_TEST.cls +++ b/force-app/main/default/classes/ProgramEngagementSelector_TEST.cls @@ -173,7 +173,6 @@ public with sharing class ProgramEngagementSelector_TEST { FROM ProgramEngagement__c WHERE Program__c = :program.Id - AND Contact__r.Name LIKE '%Contact%' AND Stage__c = 'Enrolled' ] ); @@ -184,7 +183,6 @@ public with sharing class ProgramEngagementSelector_TEST { for (Contact con : [ SELECT Id, Name, Email FROM Contact - WHERE Name LIKE '%Contact%' ]) { soslTestIds.add(con.Id); } @@ -243,7 +241,6 @@ public with sharing class ProgramEngagementSelector_TEST { for (Contact con : [ SELECT Id, Name, Email FROM Contact - WHERE Name LIKE '%Contact%' ]) { soslTestIds.add(con.Id); } From decb704f5059e735dda82b052a8ec0cae15d88cb Mon Sep 17 00:00:00 2001 From: Jennifer Bennett Date: Tue, 27 Aug 2024 08:42:50 -0500 Subject: [PATCH 04/12] Prettify changes. --- .../default/classes/ProgramEngagementSelector.cls | 3 +-- .../classes/ProgramEngagementSelector_TEST.cls | 14 +++----------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/force-app/main/default/classes/ProgramEngagementSelector.cls b/force-app/main/default/classes/ProgramEngagementSelector.cls index 753641de0..25609b04b 100755 --- a/force-app/main/default/classes/ProgramEngagementSelector.cls +++ b/force-app/main/default/classes/ProgramEngagementSelector.cls @@ -196,8 +196,7 @@ public with sharing class ProgramEngagementSelector { List> contactResult = [ FIND :searchText IN ALL FIELDS - RETURNING - Contact(FirstName, LastName, Email LIMIT :SEARCH_LIMIT) + RETURNING Contact(FirstName, LastName, Email LIMIT :SEARCH_LIMIT) ]; return getEngagementIdsFromSOSLResult( diff --git a/force-app/main/default/classes/ProgramEngagementSelector_TEST.cls b/force-app/main/default/classes/ProgramEngagementSelector_TEST.cls index e9bd1d537..11bf46dfb 100755 --- a/force-app/main/default/classes/ProgramEngagementSelector_TEST.cls +++ b/force-app/main/default/classes/ProgramEngagementSelector_TEST.cls @@ -171,19 +171,14 @@ public with sharing class ProgramEngagementSelector_TEST { [ SELECT Id, Contact__r.Name, Contact__r.Email, Stage__c, ProgramCohort__c FROM ProgramEngagement__c - WHERE - Program__c = :program.Id - AND Stage__c = 'Enrolled' + WHERE Program__c = :program.Id AND Stage__c = 'Enrolled' ] ); //Search results must be populated manually for SOSL List soslTestIds = new List{ expected[0].Id }; - for (Contact con : [ - SELECT Id, Name, Email - FROM Contact - ]) { + for (Contact con : [SELECT Id, Name, Email FROM Contact]) { soslTestIds.add(con.Id); } Test.setFixedSearchResults(soslTestIds); @@ -238,10 +233,7 @@ public with sharing class ProgramEngagementSelector_TEST { //Search results must be populated manually for SOSL List soslTestIds = new List{ expected[0].Id }; - for (Contact con : [ - SELECT Id, Name, Email - FROM Contact - ]) { + for (Contact con : [SELECT Id, Name, Email FROM Contact]) { soslTestIds.add(con.Id); } Test.setFixedSearchResults(soslTestIds); From 03bb97cad7248a9fd39593e0ade9ceaa7bd87493 Mon Sep 17 00:00:00 2001 From: jjbennett Date: Tue, 17 Sep 2024 17:10:01 -0500 Subject: [PATCH 05/12] Move to bulk save. --- .../classes/ServiceDeliveryController.cls | 14 ++ .../default/classes/ServiceDeliveryDomain.cls | 64 +++++-- .../bulkServiceDeliveryUI.js | 68 ++++++- .../serviceDeliveryRow/serviceDeliveryRow.js | 176 ++++++++++++------ force-app/main/default/lwc/util/util.js | 8 + 5 files changed, 261 insertions(+), 69 deletions(-) diff --git a/force-app/main/default/classes/ServiceDeliveryController.cls b/force-app/main/default/classes/ServiceDeliveryController.cls index 1075a95ab..7306df583 100644 --- a/force-app/main/default/classes/ServiceDeliveryController.cls +++ b/force-app/main/default/classes/ServiceDeliveryController.cls @@ -33,6 +33,20 @@ public with sharing class ServiceDeliveryController { return true; } + @AuraEnabled + public static String upsertServiceDeliveries( + List serviceDeliveries, + Boolean allOrNone + ) { + try { + List results = deliveryDomain + .upsertServiceDeliveries(serviceDeliveries, allOrNone); + return JSON.serialize(results); + } catch (Exception e) { + throw Util.getAuraHandledException(e); + } + } + @AuraEnabled public static Integer deleteServiceDeliveriesForSession(Id sessionId) { try { diff --git a/force-app/main/default/classes/ServiceDeliveryDomain.cls b/force-app/main/default/classes/ServiceDeliveryDomain.cls index d2a3680a9..593e72682 100644 --- a/force-app/main/default/classes/ServiceDeliveryDomain.cls +++ b/force-app/main/default/classes/ServiceDeliveryDomain.cls @@ -51,15 +51,7 @@ public with sharing class ServiceDeliveryDomain { return; } - if ( - !PermissionValidator.getInstance() - .hasObjectAccess( - ServiceDelivery__c.SObjectType, - PermissionValidator.CRUDAccessType.CREATEABLE - ) - ) { - throw new ServiceDeliveryDomainException(Label.UpsertOperationException); - } + validateInsertAccess(); insert Security.stripInaccessible(AccessType.CREATABLE, serviceDeliveries) .getRecords(); @@ -70,6 +62,47 @@ public with sharing class ServiceDeliveryDomain { return; } + validateUpdateAccess(); + + update Security.stripInaccessible(AccessType.UPDATABLE, serviceDeliveries) + .getRecords(); + } + + public List upsertServiceDeliveries( + List serviceDeliveries, + Boolean allOrNone + ) { + Boolean hasNewRecords = false; + Boolean hasExistingRecords = false; + + for (ServiceDelivery__c delivery : serviceDeliveries) { + if (!hasNewRecords && delivery.Id == null) { + hasNewRecords = true; + } else if (!hasExistingRecords && delivery.Id != null) { + hasExistingRecords = true; + } + if (hasNewRecords && hasExistingRecords) { + break; + } + } + + if (hasNewRecords) { + validateInsertAccess(); + } + + if (hasExistingRecords) { + validateUpdateAccess(); + } + + List saveResults = Database.upsert( + Security.stripInaccessible(AccessType.UPSERTABLE, serviceDeliveries) + .getRecords(), + allOrNone + ); + return saveResults; + } + + private void validateUpdateAccess() { if ( !PermissionValidator.getInstance() .hasObjectAccess( @@ -79,8 +112,17 @@ public with sharing class ServiceDeliveryDomain { ) { throw new ServiceDeliveryDomainException(Label.UpsertOperationException); } + } - update Security.stripInaccessible(AccessType.UPDATABLE, serviceDeliveries) - .getRecords(); + private void validateInsertAccess() { + if ( + !PermissionValidator.getInstance() + .hasObjectAccess( + ServiceDelivery__c.SObjectType, + PermissionValidator.CRUDAccessType.CREATEABLE + ) + ) { + throw new ServiceDeliveryDomainException(Label.UpsertOperationException); + } } } diff --git a/force-app/main/default/lwc/bulkServiceDeliveryUI/bulkServiceDeliveryUI.js b/force-app/main/default/lwc/bulkServiceDeliveryUI/bulkServiceDeliveryUI.js index 191efe86a..45ce3fb50 100644 --- a/force-app/main/default/lwc/bulkServiceDeliveryUI/bulkServiceDeliveryUI.js +++ b/force-app/main/default/lwc/bulkServiceDeliveryUI/bulkServiceDeliveryUI.js @@ -39,6 +39,7 @@ import SERVICE_FIELD from "@salesforce/schema/ServiceDelivery__c.Service__c"; import SERVICEDELIVERY_OBJECT from "@salesforce/schema/ServiceDelivery__c"; import getFieldSets from "@salesforce/apex/ServiceDeliveryController.getServiceDeliveryFieldSets"; +import updateRows from "@salesforce/apex/ServiceDeliveryController.upsertServiceDeliveries"; import pmmFolder from "@salesforce/resourceUrl/pmm"; export default class BulkServiceDeliveryUI extends NavigationMixin(LightningElement) { @@ -233,6 +234,7 @@ export default class BulkServiceDeliveryUI extends NavigationMixin(LightningElem handleSave() { let rows = this.template.querySelectorAll("c-service-delivery-row"); + let deliveries = []; this.savedCount = 0; this.errorCount = 0; @@ -243,15 +245,73 @@ export default class BulkServiceDeliveryUI extends NavigationMixin(LightningElem if (row.isDirty || row.isError) { this.targetSaveCount++; } + if (row.isDirty) { - this.currentSaveCount++; - this.isSaving = true; + let delivery = row.row; + delivery.index = row.index; + if (!delivery.isError) { + deliveries.push(delivery); + row.setSaving(); + } else { + this.errorCount++; + } } - row.saveRow(); }); - if (this.targetSaveCount === 0) { + this.currentSaveCount = deliveries.length; + if (this.targetSaveCount === 0 || this.currentSaveCount === 0) { this.dispatchEvent(new CustomEvent("done")); + return; + } + + this.upsertDeliveries(deliveries); + } + + upsertDeliveries(deliveries) { + updateRows({ + serviceDeliveries: deliveries, + allOrNone: false, + }) + .then(results => { + let resultByIndex = this.processResults(results, deliveries); + this.updateRows(resultByIndex); + }) + .catch(error => { + handleError(error); + }); + } + + processResults(results, deliveries) { + let resultByIndex = {}; + results = JSON.parse(results); + + for (let i = 0; i < deliveries.length; i++) { + deliveries[i].id = results[i].id; + deliveries[i].result = results[i]; + resultByIndex[deliveries[i].index] = deliveries[i]; + } + console.log("find results: ", JSON.stringify(resultByIndex)); + return resultByIndex; + } + + updateRows(resultByIndex) { + let rows = this.template.querySelectorAll("c-service-delivery-row"); + if (rows) { + rows.forEach(row => { + if ( + row.isDirty && + Object.prototype.hasOwnProperty.call(resultByIndex, row.index) + ) { + let delivery = resultByIndex[row.index]; + + if (delivery.result.success) { + row.handleSuccess(delivery); + } else { + this.errorCount++; + row.handleSaveErrors(delivery.result.errors); + } + } + }); } } diff --git a/force-app/main/default/lwc/serviceDeliveryRow/serviceDeliveryRow.js b/force-app/main/default/lwc/serviceDeliveryRow/serviceDeliveryRow.js index 9ab642f70..6bfcffa5b 100644 --- a/force-app/main/default/lwc/serviceDeliveryRow/serviceDeliveryRow.js +++ b/force-app/main/default/lwc/serviceDeliveryRow/serviceDeliveryRow.js @@ -59,6 +59,7 @@ export default class ServiceDeliveryRow extends LightningElement { this._defaultValues = Object.assign(this.defaultValues, value); this._defaultsSet = false; this.setDefaults(); + if (value.Id === null) { // this allows full clear of the first row when the modal reopens this.isSaved = false; @@ -75,6 +76,7 @@ export default class ServiceDeliveryRow extends LightningElement { @api rowCount; @api isDirty = false; @api isError; + @api errorByField; @api shouldFocus = false; @track fieldSet; @@ -175,14 +177,70 @@ export default class ServiceDeliveryRow extends LightningElement { } @api - saveRow() { - if (!this.isDirty) { - return; + get row() { + let row = {}; + + if (this.recordId) { + row.Id = this.recordId; + } + + let comboboxes = this.template.querySelectorAll("lightning-combobox"); + if (comboboxes && comboboxes.length > 0) { + comboboxes.forEach(combobox => { + row[combobox.name] = combobox.value; + }); + } + + let inputFields = this.template.querySelectorAll("lightning-input-field"); + if (inputFields && inputFields.length > 0) { + inputFields.forEach(field => { + row[field.fieldName] = field.value; + }); + } + + if (this.programEngagementId) { + row[PROGRAMENGAGEMENT_FIELD.fieldApiName] = this.programEngagementId; + } + + if (this.serviceId) { + row[SERVICE_FIELD.fieldApiName] = this.serviceId; + } + + row.isError = this.reportValidity(); + return row; + } + + @api + reportValidity() { + if (this.hasProgramEngagementField && !this.programEngagementId) { + this.isError = true; + this.errorMessage = handleError( + this.labels.selectEngagement, + false, + "dismissible", + true + ); } - let deliverySubmit = this.template.querySelector(".sd-submit"); - if (deliverySubmit) { - deliverySubmit.click(); + + let comboboxes = this.template.querySelectorAll("lightning-combobox"); + if (comboboxes?.length > 0) { + comboboxes.forEach(combobox => { + if (!combobox.reportValidity()) { + this.isError = true; + } + }); + } + + let inputFields = this.template.querySelectorAll("lightning-input-field"); + if (inputFields?.length > 0) { + inputFields.forEach(field => { + if (!field.reportValidity()) { + this.isError = true; + } + }); } + + return this.isError; } get isDeleteDisabled() { @@ -273,62 +331,59 @@ export default class ServiceDeliveryRow extends LightningElement { this.setDisabledAttribute(); } - handleSaveError(event) { - if (!this.isError) { - if ( - JSON.stringify(event.detail).includes("UNABLE_TO_LOCK_ROW") && - this.errorRetryCount < this.errorRetryMax - ) { - this.errorRetryCount++; - this.saveRow(); - return; + @api + handleSaveErrors(errors) { + if (!errors?.length || this.isError) { + return; + } + + this.errorByField = new Map(); + errors.forEach(e => { + if (e.fields?.length > 0) { + e.fields.forEach(field => { + this.errorByField.set(field, e.message); + }); } + }); - this.errorMessage = handleError(event, false, "dismissible", true); - this.errorRetryCount = 0; - this.isDirty = false; - this.isSaving = false; - this.isSaved = false; - this.isError = true; + this.setCustomValidity(); + this.errorMessage = handleError(errors, false, "dismissible", false); - event.detail.index = this.index; - this.dispatchEvent(new CustomEvent("error", { detail: event.detail })); + this.isDirty = false; + this.isSaving = false; + this.isSaved = false; + this.isError = true; + } + + setCustomValidity() { + if (this.errorByField?.size > 0) { + this.errorByField.keys().forEach(fieldName => { + const input = this.template.querySelector(`[data-name=${fieldName}]`); + if (input && typeof input.setErrors) { + const outputErrors = { + body: { + output: { + fieldErrors: {}, + }, + }, + }; + outputErrors.body.output.fieldErrors[fieldName] = [ + { message: this.errorByField.get(fieldName) }, + ]; + input.setErrors(outputErrors); + } else if (input && typeof input.setCustomValidity) { + input.setCustomValidity(this.errorByField.get(fieldName)); + } + }); } } - handleSuccess(event) { - this.recordId = event.detail.id; + @api + handleSuccess(savedRow) { + this.recordId = savedRow.id; this.setSaved(); this.setDisabledAttribute(); - fireEvent(this.pageRef, "serviceDeliveryUpsert", event.detail); - } - - handleSubmit(event) { - let fields = event.detail.fields; - - if (this.hasProgramEngagementField && !this.programEngagementId) { - this.isError = true; - this.errorMessage = handleError( - this.labels.selectEngagement, - false, - "dismissible", - true - ); - } - - if (!this.isError) { - if (this.programEngagementId) { - fields[PROGRAMENGAGEMENT_FIELD.fieldApiName] = this.programEngagementId; - } - - if (this.serviceId) { - fields[SERVICE_FIELD.fieldApiName] = this.serviceId; - } - - this.template.querySelector("lightning-record-edit-form").submit(fields); - - this.setSaving(); - } + fireEvent(this.pageRef, "serviceDeliveryUpsert", savedRow); } handleSaveNewPE(event) { @@ -384,6 +439,18 @@ export default class ServiceDeliveryRow extends LightningElement { resetError() { this.isError = false; this.errorMessage = ""; + + if (this.errorByField?.size > 0) { + this.errorByField.keys().forEach(fieldName => { + const field = this.template.querySelector(`[data-name=${fieldName}]`); + if (field && typeof field.setErrors === "function") { + field.setErrors(""); + } else if (field && typeof field.setCustomValidity === "function") { + field.setCustomValidity(""); + } + }); + } + this.errorByField = undefined; } resetQuantityLabel() { @@ -569,6 +636,7 @@ export default class ServiceDeliveryRow extends LightningElement { } } + @api setSaving() { this.saveMessage = "..."; this.isSaving = true; diff --git a/force-app/main/default/lwc/util/util.js b/force-app/main/default/lwc/util/util.js index 10cced2a9..a35859e1a 100644 --- a/force-app/main/default/lwc/util/util.js +++ b/force-app/main/default/lwc/util/util.js @@ -186,6 +186,14 @@ const handleError = (error, fireShowToast = true, showToastMode, returnAsArray) message = error.body.map(e => e.message).join(", "); } else if (error.body && typeof error.body.message === "string") { message = error.body.message; + } else if (Array.isArray(error)) { + // databse save result errors + message = error.map(e => { + return (e.fields?.length > 0 ? e.fields.join() + ": " : "") + e.message; + }); + if (!returnAsArray || fireShowToast) { + message.join("; "); + } } else if ( error.detail && error.detail.output && From c94b006e71ab9680572df9c3b305f1cfceb70ed2 Mon Sep 17 00:00:00 2001 From: jjbennett Date: Tue, 17 Sep 2024 17:12:45 -0500 Subject: [PATCH 06/12] Remove console log and fix method name. --- .../lwc/bulkServiceDeliveryUI/bulkServiceDeliveryUI.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/force-app/main/default/lwc/bulkServiceDeliveryUI/bulkServiceDeliveryUI.js b/force-app/main/default/lwc/bulkServiceDeliveryUI/bulkServiceDeliveryUI.js index 45ce3fb50..3ce65049d 100644 --- a/force-app/main/default/lwc/bulkServiceDeliveryUI/bulkServiceDeliveryUI.js +++ b/force-app/main/default/lwc/bulkServiceDeliveryUI/bulkServiceDeliveryUI.js @@ -39,7 +39,7 @@ import SERVICE_FIELD from "@salesforce/schema/ServiceDelivery__c.Service__c"; import SERVICEDELIVERY_OBJECT from "@salesforce/schema/ServiceDelivery__c"; import getFieldSets from "@salesforce/apex/ServiceDeliveryController.getServiceDeliveryFieldSets"; -import updateRows from "@salesforce/apex/ServiceDeliveryController.upsertServiceDeliveries"; +import upsertRows from "@salesforce/apex/ServiceDeliveryController.upsertServiceDeliveries"; import pmmFolder from "@salesforce/resourceUrl/pmm"; export default class BulkServiceDeliveryUI extends NavigationMixin(LightningElement) { @@ -268,7 +268,7 @@ export default class BulkServiceDeliveryUI extends NavigationMixin(LightningElem } upsertDeliveries(deliveries) { - updateRows({ + upsertRows({ serviceDeliveries: deliveries, allOrNone: false, }) @@ -290,7 +290,6 @@ export default class BulkServiceDeliveryUI extends NavigationMixin(LightningElem deliveries[i].result = results[i]; resultByIndex[deliveries[i].index] = deliveries[i]; } - console.log("find results: ", JSON.stringify(resultByIndex)); return resultByIndex; } From ce9ee2ffc8943bbfd5cee0be0479cdbefacc8781 Mon Sep 17 00:00:00 2001 From: jjbennett Date: Wed, 18 Sep 2024 13:22:06 -0500 Subject: [PATCH 07/12] Fix combobox error reporting. --- .../serviceDeliveryRow.html | 1 + .../serviceDeliveryRow/serviceDeliveryRow.js | 24 ++++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/force-app/main/default/lwc/serviceDeliveryRow/serviceDeliveryRow.html b/force-app/main/default/lwc/serviceDeliveryRow/serviceDeliveryRow.html index 72177e063..8ef719f35 100644 --- a/force-app/main/default/lwc/serviceDeliveryRow/serviceDeliveryRow.html +++ b/force-app/main/default/lwc/serviceDeliveryRow/serviceDeliveryRow.html @@ -43,6 +43,7 @@ 0) { this.errorByField.keys().forEach(fieldName => { - const input = this.template.querySelector(`[data-name=${fieldName}]`); - if (input && typeof input.setErrors) { + const input = this.getFieldInput(fieldName); + if (input && typeof input.setErrors === "function") { const outputErrors = { body: { output: { @@ -371,13 +371,20 @@ export default class ServiceDeliveryRow extends LightningElement { { message: this.errorByField.get(fieldName) }, ]; input.setErrors(outputErrors); - } else if (input && typeof input.setCustomValidity) { + } else if (input && typeof input.setCustomValidity === "function") { input.setCustomValidity(this.errorByField.get(fieldName)); + input.reportValidity(); } }); } } + getFieldInput(fieldName) { + return this.template.querySelector( + `[data-name=${fieldName}], [name=${fieldName}]` + ); + } + @api handleSuccess(savedRow) { this.recordId = savedRow.id; @@ -442,11 +449,12 @@ export default class ServiceDeliveryRow extends LightningElement { if (this.errorByField?.size > 0) { this.errorByField.keys().forEach(fieldName => { - const field = this.template.querySelector(`[data-name=${fieldName}]`); - if (field && typeof field.setErrors === "function") { - field.setErrors(""); - } else if (field && typeof field.setCustomValidity === "function") { - field.setCustomValidity(""); + const input = this.getFieldInput(fieldName); + if (input && typeof input.setErrors === "function") { + input.setErrors(""); + } else if (input && typeof input.setCustomValidity === "function") { + input.setCustomValidity(""); + input.reportValidity(); } }); } From e2f0f791e1d253447613c215b69a380b8ca4796e Mon Sep 17 00:00:00 2001 From: jjbennett Date: Thu, 19 Sep 2024 10:15:28 -0500 Subject: [PATCH 08/12] Fixes from code review. --- .../default/lwc/serviceDeliveryRow/serviceDeliveryRow.html | 1 - .../main/default/lwc/serviceDeliveryRow/serviceDeliveryRow.js | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/force-app/main/default/lwc/serviceDeliveryRow/serviceDeliveryRow.html b/force-app/main/default/lwc/serviceDeliveryRow/serviceDeliveryRow.html index 8ef719f35..72177e063 100644 --- a/force-app/main/default/lwc/serviceDeliveryRow/serviceDeliveryRow.html +++ b/force-app/main/default/lwc/serviceDeliveryRow/serviceDeliveryRow.html @@ -43,7 +43,6 @@ Date: Thu, 19 Sep 2024 10:16:08 -0500 Subject: [PATCH 09/12] Fixes from code review. --- force-app/main/default/lwc/util/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/force-app/main/default/lwc/util/util.js b/force-app/main/default/lwc/util/util.js index a35859e1a..f7d91eb7b 100644 --- a/force-app/main/default/lwc/util/util.js +++ b/force-app/main/default/lwc/util/util.js @@ -187,7 +187,7 @@ const handleError = (error, fireShowToast = true, showToastMode, returnAsArray) } else if (error.body && typeof error.body.message === "string") { message = error.body.message; } else if (Array.isArray(error)) { - // databse save result errors + // database save result errors message = error.map(e => { return (e.fields?.length > 0 ? e.fields.join() + ": " : "") + e.message; }); From da921a46c8044c06a697f83038e4f7459965884c Mon Sep 17 00:00:00 2001 From: jjbennett Date: Thu, 19 Sep 2024 10:39:58 -0500 Subject: [PATCH 10/12] Fixes from code review. --- .../default/lwc/serviceDeliveryRow/serviceDeliveryRow.html | 1 + .../main/default/lwc/serviceDeliveryRow/serviceDeliveryRow.js | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/force-app/main/default/lwc/serviceDeliveryRow/serviceDeliveryRow.html b/force-app/main/default/lwc/serviceDeliveryRow/serviceDeliveryRow.html index 72177e063..8ef719f35 100644 --- a/force-app/main/default/lwc/serviceDeliveryRow/serviceDeliveryRow.html +++ b/force-app/main/default/lwc/serviceDeliveryRow/serviceDeliveryRow.html @@ -43,6 +43,7 @@ Date: Thu, 19 Sep 2024 10:48:54 -0500 Subject: [PATCH 11/12] Fixes from code review. --- .../default/lwc/bulkServiceDeliveryUI/bulkServiceDeliveryUI.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/force-app/main/default/lwc/bulkServiceDeliveryUI/bulkServiceDeliveryUI.js b/force-app/main/default/lwc/bulkServiceDeliveryUI/bulkServiceDeliveryUI.js index 3ce65049d..c6a814bb9 100644 --- a/force-app/main/default/lwc/bulkServiceDeliveryUI/bulkServiceDeliveryUI.js +++ b/force-app/main/default/lwc/bulkServiceDeliveryUI/bulkServiceDeliveryUI.js @@ -259,7 +259,7 @@ export default class BulkServiceDeliveryUI extends NavigationMixin(LightningElem }); this.currentSaveCount = deliveries.length; - if (this.targetSaveCount === 0 || this.currentSaveCount === 0) { + if (this.targetSaveCount === 0) { this.dispatchEvent(new CustomEvent("done")); return; } From 353cb15f677ced892df50db65e5818c7ac2e7fc2 Mon Sep 17 00:00:00 2001 From: jjbennett Date: Thu, 19 Sep 2024 11:48:21 -0500 Subject: [PATCH 12/12] Fixes from code review and cleanup of unused methods. --- .../ServiceDeliveryController_TEST.cls | 80 +++++++++++++++++++ .../classes/ServiceDeliveryDomain_TEST.cls | 53 ++++++++++++ .../bulkServiceDeliveryUI.html | 2 - .../bulkServiceDeliveryUI.js | 43 +++------- .../serviceDeliveryRow.html | 3 - 5 files changed, 144 insertions(+), 37 deletions(-) diff --git a/force-app/main/default/classes/ServiceDeliveryController_TEST.cls b/force-app/main/default/classes/ServiceDeliveryController_TEST.cls index cb208d60b..e9f4ec572 100644 --- a/force-app/main/default/classes/ServiceDeliveryController_TEST.cls +++ b/force-app/main/default/classes/ServiceDeliveryController_TEST.cls @@ -249,6 +249,86 @@ public with sharing class ServiceDeliveryController_TEST { ); } + @IsTest + private static void testUpsertServiceDeliveries() { + List serviceDeliveries = new List{ + new ServiceDelivery__c( + Name = 'Test1', + Id = TestUtil.mockId(ServiceDelivery__c.SObjectType) + ), + new ServiceDelivery__c( + Name = 'Test2', + Id = TestUtil.mockId(ServiceDelivery__c.SObjectType) + ), + new ServiceDelivery__c( + Name = 'Test3', + Id = TestUtil.mockId(ServiceDelivery__c.SObjectType) + ) + }; + + domainStub.withReturnValue( + 'upsertServiceDeliveries', + new List{ List.class, Boolean.class }, + null + ); + + Test.startTest(); + ServiceDeliveryController.deliveryDomain = (ServiceDeliveryDomain) domainStub.createMock(); + ServiceDeliveryController.upsertServiceDeliveries(serviceDeliveries, false); + Test.stopTest(); + + domainStub.assertCalledWith( + 'upsertServiceDeliveries', + new List{ List.class, Boolean.class }, + new List{ serviceDeliveries, false } + ); + } + + @IsTest + private static void testUpsertServiceDeliveriesException() { + List serviceDeliveries = new List{ + new ServiceDelivery__c( + Name = 'Test1', + Id = TestUtil.mockId(ServiceDelivery__c.SObjectType) + ), + new ServiceDelivery__c( + Name = 'Test2', + Id = TestUtil.mockId(ServiceDelivery__c.SObjectType) + ), + new ServiceDelivery__c( + Name = 'Test3', + Id = TestUtil.mockId(ServiceDelivery__c.SObjectType) + ) + }; + + domainStub.withThrowException( + 'upsertServiceDeliveries', + new List{ List.class, Boolean.class } + ); + + Test.startTest(); + ServiceDeliveryController.deliveryDomain = (ServiceDeliveryDomain) domainStub.createMock(); + + Exception actualException; + try { + ServiceDeliveryController.upsertServiceDeliveries(serviceDeliveries, false); + } catch (Exception e) { + actualException = e; + } + Test.stopTest(); + + System.assertEquals( + domainStub.testExceptionMessage, + actualException.getMessage(), + 'Expected the controller to rethrow the exception from the domain.' + ); + domainStub.assertCalledWith( + 'upsertServiceDeliveries', + new List{ List.class, Boolean.class }, + new List{ serviceDeliveries, false } + ); + } + @IsTest private static void testGetNumberOfServiceDeliveriesForSession() { Integer expectedNumberOfDeliveries = 5; diff --git a/force-app/main/default/classes/ServiceDeliveryDomain_TEST.cls b/force-app/main/default/classes/ServiceDeliveryDomain_TEST.cls index 0396f6af6..05b449274 100644 --- a/force-app/main/default/classes/ServiceDeliveryDomain_TEST.cls +++ b/force-app/main/default/classes/ServiceDeliveryDomain_TEST.cls @@ -159,6 +159,59 @@ private with sharing class ServiceDeliveryDomain_TEST { } } + @IsTest + private static void shouldReturnUpsertResultsOnUpsertServiceDeliveriesWithAllOrNone() { + TestDataFactory.generateServiceData(); + Service__c service = [SELECT Id FROM Service__c LIMIT 1]; + + List existingServiceDeliveries = [ + SELECT Id, Name + FROM ServiceDelivery__c + ]; + for (ServiceDelivery__c delivery : existingServiceDeliveries) { + System.assertNotEquals('Upserted', delivery.Name); + delivery.Name = 'Upserted'; + delivery.AutonameOverride__c = true; + } + + ServiceDelivery__c newServiceDelivery = new ServiceDelivery__c( + Name = 'Upserted', + AutonameOverride__c = true, + Service__c = service.Id + ); + + List serviceDeliveriesToUpsert = new List(); + serviceDeliveriesToUpsert.addAll(existingServiceDeliveries); + serviceDeliveriesToUpsert.add(newServiceDelivery); + + Test.startTest(); + List results = new ServiceDeliveryDomain() + .upsertServiceDeliveries(serviceDeliveriesToUpsert, false); + Test.stopTest(); + + List serviceDeliveriesAfter = [ + SELECT Id, Name + FROM ServiceDelivery__c + ]; + System.assertEquals( + existingServiceDeliveries.size() + 1, + serviceDeliveriesAfter.size(), + 'One new record should be inserted.' + ); + System.assertEquals( + serviceDeliveriesAfter.size(), + results.size(), + 'Results should be returned for each record upserted.' + ); + for (ServiceDelivery__c delivery : serviceDeliveriesAfter) { + System.assertEquals( + 'Upserted', + delivery.Name, + 'All records should be renamed.' + ); + } + } + @IsTest private static void shouldThrowExceptionWhenInsertPermissionCheckFails() { String methodName = 'hasObjectAccess'; diff --git a/force-app/main/default/lwc/bulkServiceDeliveryUI/bulkServiceDeliveryUI.html b/force-app/main/default/lwc/bulkServiceDeliveryUI/bulkServiceDeliveryUI.html index 75ac29938..3ecdc5a67 100644 --- a/force-app/main/default/lwc/bulkServiceDeliveryUI/bulkServiceDeliveryUI.html +++ b/force-app/main/default/lwc/bulkServiceDeliveryUI/bulkServiceDeliveryUI.html @@ -34,9 +34,7 @@ default-values={delivery} service-delivery-field-sets={serviceDeliveryFieldSets} index={delivery.index} - onsuccess={handleRowSuccess} ondelete={handleRowDelete} - onerror={handleRowError} row-count={rowCount} should-focus={delivery.shouldFocus} > diff --git a/force-app/main/default/lwc/bulkServiceDeliveryUI/bulkServiceDeliveryUI.js b/force-app/main/default/lwc/bulkServiceDeliveryUI/bulkServiceDeliveryUI.js index c6a814bb9..0ee4b4901 100644 --- a/force-app/main/default/lwc/bulkServiceDeliveryUI/bulkServiceDeliveryUI.js +++ b/force-app/main/default/lwc/bulkServiceDeliveryUI/bulkServiceDeliveryUI.js @@ -209,13 +209,6 @@ export default class BulkServiceDeliveryUI extends NavigationMixin(LightningElem } } - savingComplete() { - if (this.currentSaveCount - this.savedCount - this.errorCount === 0) { - return true; - } - return false; - } - showSaveSummaryToast() { let toastVariant = this.savingCompleteToastVariant; let toastTitle = toastVariant === "success" ? this.labels.success : ""; @@ -223,15 +216,6 @@ export default class BulkServiceDeliveryUI extends NavigationMixin(LightningElem showToast(toastTitle, this.savingCompleteMessage, toastVariant, "dismissible"); } - // eslint-disable-next-line no-unused-vars - handleRowError(event) { - this.errorCount++; - if (this.savingComplete()) { - this.showSaveSummaryToast(); - this.isSaving = false; - } - } - handleSave() { let rows = this.template.querySelectorAll("c-service-delivery-row"); let deliveries = []; @@ -239,7 +223,6 @@ export default class BulkServiceDeliveryUI extends NavigationMixin(LightningElem this.savedCount = 0; this.errorCount = 0; this.targetSaveCount = 0; - this.currentSaveCount = 0; rows.forEach(row => { if (row.isDirty || row.isError) { @@ -258,7 +241,6 @@ export default class BulkServiceDeliveryUI extends NavigationMixin(LightningElem } }); - this.currentSaveCount = deliveries.length; if (this.targetSaveCount === 0) { this.dispatchEvent(new CustomEvent("done")); return; @@ -268,6 +250,11 @@ export default class BulkServiceDeliveryUI extends NavigationMixin(LightningElem } upsertDeliveries(deliveries) { + if (deliveries.length === 0) { + return; + } + + this.isSaving = true; upsertRows({ serviceDeliveries: deliveries, allOrNone: false, @@ -278,6 +265,11 @@ export default class BulkServiceDeliveryUI extends NavigationMixin(LightningElem }) .catch(error => { handleError(error); + }) + .finally(() => { + this.isSaving = false; + this.showSaveSummaryToast(); + this.dispatchEvent(new CustomEvent("done")); }); } @@ -304,6 +296,7 @@ export default class BulkServiceDeliveryUI extends NavigationMixin(LightningElem let delivery = resultByIndex[row.index]; if (delivery.result.success) { + this.savedCount++; row.handleSuccess(delivery); } else { this.errorCount++; @@ -314,20 +307,6 @@ export default class BulkServiceDeliveryUI extends NavigationMixin(LightningElem } } - // eslint-disable-next-line no-unused-vars - handleRowSuccess(event) { - this.savedCount++; - - if (this.savingComplete()) { - this.showSaveSummaryToast(); - this.isSaving = false; - } - - if (this.savedCount === this.targetSaveCount) { - this.dispatchEvent(new CustomEvent("done")); - } - } - handleFinishWizard(event) { this.hideWizard = true; let selectedEngagements = event.detail.selectedEngagements; diff --git a/force-app/main/default/lwc/serviceDeliveryRow/serviceDeliveryRow.html b/force-app/main/default/lwc/serviceDeliveryRow/serviceDeliveryRow.html index 8ef719f35..aff61ba3c 100644 --- a/force-app/main/default/lwc/serviceDeliveryRow/serviceDeliveryRow.html +++ b/force-app/main/default/lwc/serviceDeliveryRow/serviceDeliveryRow.html @@ -11,9 +11,6 @@