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 diff --git a/force-app/main/default/classes/ProgramEngagementSelector.cls b/force-app/main/default/classes/ProgramEngagementSelector.cls index 5ac052935..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 ORDER BY LastName 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 c25f445e1..11bf46dfb 100755 --- a/force-app/main/default/classes/ProgramEngagementSelector_TEST.cls +++ b/force-app/main/default/classes/ProgramEngagementSelector_TEST.cls @@ -171,21 +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 Contact__r.Name LIKE '%Contact%' - 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 - WHERE Name LIKE '%Contact%' - ]) { + for (Contact con : [SELECT Id, Name, Email FROM Contact]) { soslTestIds.add(con.Id); } Test.setFixedSearchResults(soslTestIds); @@ -240,11 +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 - WHERE Name LIKE '%Contact%' - ]) { + for (Contact con : [SELECT Id, Name, Email FROM Contact]) { soslTestIds.add(con.Id); } Test.setFixedSearchResults(soslTestIds); 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/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.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/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 191efe86a..0ee4b4901 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 upsertRows from "@salesforce/apex/ServiceDeliveryController.upsertServiceDeliveries"; import pmmFolder from "@salesforce/resourceUrl/pmm"; export default class BulkServiceDeliveryUI extends NavigationMixin(LightningElement) { @@ -208,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 : ""; @@ -222,50 +216,94 @@ 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 = []; this.savedCount = 0; this.errorCount = 0; this.targetSaveCount = 0; - this.currentSaveCount = 0; rows.forEach(row => { 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.dispatchEvent(new CustomEvent("done")); + return; } + + this.upsertDeliveries(deliveries); } - // eslint-disable-next-line no-unused-vars - handleRowSuccess(event) { - this.savedCount++; + upsertDeliveries(deliveries) { + if (deliveries.length === 0) { + return; + } + + this.isSaving = true; + upsertRows({ + serviceDeliveries: deliveries, + allOrNone: false, + }) + .then(results => { + let resultByIndex = this.processResults(results, deliveries); + this.updateRows(resultByIndex); + }) + .catch(error => { + handleError(error); + }) + .finally(() => { + this.isSaving = false; + this.showSaveSummaryToast(); + this.dispatchEvent(new CustomEvent("done")); + }); + } - if (this.savingComplete()) { - this.showSaveSummaryToast(); - this.isSaving = false; + 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]; } + return resultByIndex; + } - if (this.savedCount === this.targetSaveCount) { - this.dispatchEvent(new CustomEvent("done")); + 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) { + this.savedCount++; + row.handleSuccess(delivery); + } else { + this.errorCount++; + row.handleSaveErrors(delivery.result.errors); + } + } + }); } } diff --git a/force-app/main/default/lwc/serviceDeliveryRow/serviceDeliveryRow.html b/force-app/main/default/lwc/serviceDeliveryRow/serviceDeliveryRow.html index 72177e063..aff61ba3c 100644 --- a/force-app/main/default/lwc/serviceDeliveryRow/serviceDeliveryRow.html +++ b/force-app/main/default/lwc/serviceDeliveryRow/serviceDeliveryRow.html @@ -11,9 +11,6 @@
@@ -43,6 +40,7 @@ 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; + }); } - let deliverySubmit = this.template.querySelector(".sd-submit"); - if (deliverySubmit) { - deliverySubmit.click(); + + if (this.programEngagementId) { + row[PROGRAMENGAGEMENT_FIELD.fieldApiName] = this.programEngagementId; + } + + if (this.serviceId) { + row[SERVICE_FIELD.fieldApiName] = this.serviceId; } + + row.isError = this.reportValidity(); + return row; + } + + reportValidity() { + if (this.hasProgramEngagementField && !this.programEngagementId) { + this.isError = true; + this.errorMessage = handleError( + this.labels.selectEngagement, + false, + "dismissible", + true + ); + } + + 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 +330,63 @@ 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.errorMessage = handleError(event, false, "dismissible", true); - this.errorRetryCount = 0; - this.isDirty = false; - this.isSaving = false; - this.isSaved = false; - this.isError = true; + this.errorByField = new Map(); + errors.forEach(e => { + if (e.fields?.length > 0) { + e.fields.forEach(field => { + this.errorByField.set(field, e.message); + }); + } + }); - event.detail.index = this.index; - this.dispatchEvent(new CustomEvent("error", { detail: event.detail })); + this.errorMessage = handleError(errors, false, "dismissible", false); + this.isDirty = false; + this.isSaving = false; + this.isSaved = false; + this.isError = true; + this.setCustomValidity(); + } + + setCustomValidity() { + if (this.errorByField?.size > 0) { + this.errorByField.keys().forEach(fieldName => { + const input = this.getFieldInput(fieldName); + if (input && typeof input.setErrors === "function") { + const outputErrors = { + body: { + output: { + fieldErrors: {}, + }, + }, + }; + outputErrors.body.output.fieldErrors[fieldName] = [ + { message: this.errorByField.get(fieldName) }, + ]; + input.setErrors(outputErrors); + } else if (input && typeof input.setCustomValidity === "function") { + input.setCustomValidity(this.errorByField.get(fieldName)); + input.reportValidity(); + } + }); } } - handleSuccess(event) { - this.recordId = event.detail.id; - this.setSaved(); - this.setDisabledAttribute(); - fireEvent(this.pageRef, "serviceDeliveryUpsert", event.detail); + getFieldInput(fieldName) { + return this.template.querySelector(`[data-name=${fieldName}]`); } - 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(); - } + @api + handleSuccess(savedRow) { + this.recordId = savedRow.id; + this.setSaved(); + this.setDisabledAttribute(); + fireEvent(this.pageRef, "serviceDeliveryUpsert", savedRow); } handleSaveNewPE(event) { @@ -384,6 +442,19 @@ export default class ServiceDeliveryRow extends LightningElement { resetError() { this.isError = false; this.errorMessage = ""; + + if (this.errorByField?.size > 0) { + this.errorByField.keys().forEach(fieldName => { + const input = this.getFieldInput(fieldName); + if (input && typeof input.setErrors === "function") { + input.setErrors(""); + } else if (input && typeof input.setCustomValidity === "function") { + input.setCustomValidity(""); + input.reportValidity(); + } + }); + } + this.errorByField = undefined; } resetQuantityLabel() { @@ -569,6 +640,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..f7d91eb7b 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)) { + // database 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 &&