diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cc7961c43..afa15e77d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,5 @@ +#GUSINFO:NPC Asteroids, SFDO Program Management Module + # Python *.py @SalesforceFoundation/release-engineering-reviewers diff --git a/.gitignore b/.gitignore index 75a96fbfc..577fc12bb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Salesforce / SFDX / CCI .cci .sfdx +.sf /src.orig /src diff --git a/README.md b/README.md index 6b8f2de8d..44df95191 100644 --- a/README.md +++ b/README.md @@ -26,5 +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/cumulusci.yml b/cumulusci.yml index 153100101..5512bb6fb 100644 --- a/cumulusci.yml +++ b/cumulusci.yml @@ -1,4 +1,4 @@ -minimum_cumulusci_version: "3.36.0" +minimum_cumulusci_version: 3.74.0 project: name: PMM package: @@ -220,6 +220,11 @@ tasks: options: path: unpackaged/config/customer_profiles + github_release: + options: + release_content: | + Check out the [Salesforce Release Notes](https://sfdc.co/bnL4Cb) or [Known Issues](https://issues.salesforce.com/) for details. + flows: make_community: steps: @@ -429,6 +434,8 @@ flows: task: update_dependencies 2: task: install_managed + options: + security_type: NONE 3: task: deploy_customer_profiles ui_options: @@ -439,6 +446,11 @@ flows: reports: name: "Deploy Folder of Unmanaged Reports" + release_production: + steps: + 3: + task: None + plans: install: slug: install diff --git a/force-app/main/default/aura/addMoreParticipants/addMoreParticipants.cmp b/force-app/main/default/aura/addMoreParticipants/addMoreParticipants.cmp index 7a2abc9da..2a6afa738 100644 --- a/force-app/main/default/aura/addMoreParticipants/addMoreParticipants.cmp +++ b/force-app/main/default/aura/addMoreParticipants/addMoreParticipants.cmp @@ -11,7 +11,8 @@ .cuf-content { padding: 0 0rem !important; } .slds-p-around--medium { padding: - 0rem !important; } .slds-modal__content{ height:unset !important; max-height:unset - !important; } .slds-modal__container{ width: 80% !important; max-width: 80% !important; } + 0rem !important; } .quick-actions-panel .slds-modal__content{ height:unset + !important; max-height:unset !important; } .slds-modal__container{ width: 80% + !important; max-width: 80% !important; } diff --git a/force-app/main/default/aura/addMoreSessions/addMoreSessions.cmp b/force-app/main/default/aura/addMoreSessions/addMoreSessions.cmp index 2ed4c4eda..33e24052d 100644 --- a/force-app/main/default/aura/addMoreSessions/addMoreSessions.cmp +++ b/force-app/main/default/aura/addMoreSessions/addMoreSessions.cmp @@ -22,8 +22,8 @@ .cuf-content { padding: 0 0rem !important; } .slds-p-around--medium { padding: - 0rem !important; } .slds-modal__content{ height:unset !important; max-height:unset - !important; } .slds-modal__container{ width: 80% !important; max-width: 80% - !important; } + 0rem !important; } .quick-actions-panel .slds-modal__content{ height:unset + !important; max-height:unset !important; } .slds-modal__container{ width: 80% + !important; max-width: 80% !important; } diff --git a/force-app/main/default/aura/createServiceSchedule/createServiceSchedule.cmp b/force-app/main/default/aura/createServiceSchedule/createServiceSchedule.cmp index f86c08630..f688fca1d 100644 --- a/force-app/main/default/aura/createServiceSchedule/createServiceSchedule.cmp +++ b/force-app/main/default/aura/createServiceSchedule/createServiceSchedule.cmp @@ -23,8 +23,6 @@ .slds-accordion__list-item { border-top: none; } .slds-table_header-fixed_container { overflow-x: hidden; } .cuf-content { padding: 0 0rem !important; } .slds-p-around--medium { padding: 0rem !important; } - .slds-modal__content{ height:unset !important; max-height:unset !important; } - .slds-modal__container{ width: 80% !important; max-width: 80% !important; } queriedEngagements = [ - SELECT Id, Name, Program__c, Program__r.Name + SELECT Id, Name, Program__c, Program__r.Name, Stage__c FROM ProgramEngagement__c WHERE Contact__c = :contactId ]; - + queriedEngagements.sort(); return Security.stripInaccessible(AccessType.READABLE, queriedEngagements) .getRecords(); } + public ProgramEngagement__c getProgramEngagementById(Id peId) { + if ( + !(Schema.SObjectType.ProgramEngagement__c.isAccessible() && + PermissionValidator.getInstance() + .hasFieldReadAccess(ProgramEngagement__c.Contact__c.getDescribe()) && + PermissionValidator.getInstance() + .hasFieldReadAccess(ProgramEngagement__c.Program__c.getDescribe())) + ) { + return null; + } + + List queriedEngagements = [ + SELECT + Id, + Name, + Program__c, + Program__r.Name, + ProgramCohort__c, + Stage__c, + Contact__c, + Contact__r.Name, + Contact__r.Email + FROM ProgramEngagement__c + WHERE Id = :peId + ]; + List securityResult = Security.stripInaccessible( + AccessType.READABLE, + queriedEngagements + ) + .getRecords(); + return securityResult.isEmpty() ? null : securityResult[0]; + } + // Using strip inaccessible and performing access checks for // all fields involved in the query. Id, Name must be true if object // access is true. @@ -72,8 +105,12 @@ public with sharing class ProgramEngagementSelector { public List getProgramEngagementsByProgramId( Id programId, Set fields, - Set stages + Set stages, + String searchText, + Id cohortId ) { + final Integer LIMIT_TO = 1000; + if ( !(Schema.SObjectType.ProgramEngagement__c.isAccessible() && PermissionValidator.getInstance() @@ -84,8 +121,9 @@ public with sharing class ProgramEngagementSelector { return new List(); } - Integer limitTo = - System.Limits.getLimitQueryRows() - System.Limits.getQueryRows(); + String programEngagementName = Schema.SObjectType.ProgramEngagement__c.getName(); + List programEngagements; + Set targetIds; queryBuilder .reset() @@ -95,13 +133,97 @@ public with sharing class ProgramEngagementSelector { String.valueOf(ProgramEngagement__c.Program__c) + ' = :programId' ) .addCondition(String.valueOf(ProgramEngagement__c.Stage__c) + ' IN :stages') - .withLimit(limitTo); + .withLimit(LIMIT_TO); + if (cohortId != null) { + queryBuilder.addCondition( + String.valueOf(ProgramEngagement__c.ProgramCohort__c) + ' = :cohortId' + ); + } + if (!String.isBlank(searchText)) { + targetIds = getEngagementIdsBySearchTerm( + searchText, + fields, + programId, + stages, + cohortId + ); + queryBuilder.addCondition( + String.valueOf(ProgramEngagement__c.Id) + ' IN :targetIds' + ); + } - List programEngagements = Database.query( - queryBuilder.buildSoqlQuery() - ); + programEngagements = Database.query(queryBuilder.buildSoqlQuery()); return Security.stripInaccessible(AccessType.READABLE, programEngagements) .getRecords(); } + + private Set getEngagementIdsBySearchTerm( + String searchText, + Set fields, + Id programId, + Set stages, + Id cohortId + ) { + final Integer SEARCH_LIMIT = 1000; + searchText = String.escapeSingleQuotes(searchText); + + String peSearchString = 'FIND :searchText IN ALL FIELDS RETURNING {0}({1} {2} LIMIT :SEARCH_LIMIT)'; + String whereClause = + ' WHERE ' + + String.valueOf(ProgramEngagement__c.Program__c) + + ' = :programId' + + ' AND ' + + String.valueOf(ProgramEngagement__c.Stage__c) + + ' IN :stages '; + if (cohortId != null) { + whereClause += + ' AND ' + + String.valueOf(ProgramEngagement__c.ProgramCohort__c) + + ' =: cohortId '; + } + + String queryString = String.format( + peSearchString, + new List{ + Schema.SObjectType.ProgramEngagement__c.getName(), + String.join(new List(fields), ', '), + whereClause + } + ); + + List> engagementResult = Search.query(queryString); + List> contactResult = [ + FIND :searchText + IN ALL FIELDS + RETURNING Contact(FirstName, LastName, Email LIMIT :SEARCH_LIMIT) + ]; + + return getEngagementIdsFromSOSLResult( + programId, + engagementResult[0], + contactResult[0] + ); + } + + private Set getEngagementIdsFromSOSLResult( + Id programId, + List engagements, + List contacts + ) { + Set resultIds = new Set(); + resultIds.addAll((new Map(engagements)).keySet()); + + Set contactIds = (new Map(contacts)).keySet(); + + for (ProgramEngagement__c engagement : [ + SELECT Id + FROM ProgramEngagement__c + WHERE Contact__c IN :contactIds AND Program__c = :programId + ]) { + resultIds.add(engagement.Id); + } + + return resultIds; + } } diff --git a/force-app/main/default/classes/ProgramEngagementSelector_TEST.cls b/force-app/main/default/classes/ProgramEngagementSelector_TEST.cls index 7a51c32a1..11bf46dfb 100755 --- a/force-app/main/default/classes/ProgramEngagementSelector_TEST.cls +++ b/force-app/main/default/classes/ProgramEngagementSelector_TEST.cls @@ -74,6 +74,54 @@ public with sharing class ProgramEngagementSelector_TEST { Test.stopTest(); } + @IsTest + private static void testGetProgramEngagementsById() { + ProgramEngagement__c expected = [ + SELECT Id, Name + FROM ProgramEngagement__c + LIMIT 1 + ][0]; + + Test.startTest(); + ProgramEngagementSelector selector = new ProgramEngagementSelector(); + ProgramEngagement__c actual = selector.getProgramEngagementById(expected.Id); + Test.stopTest(); + System.assertEquals(expected.Id, actual.Id); + } + + @IsTest + private static void testGetProgramEngagementsByIdNoAccess() { + Profile p = [SELECT Id FROM Profile WHERE Name = 'Standard User']; + Integer random = Integer.valueOf(math.rint(math.random() * 1000000)); + User u = new User( + Alias = 'stand', + Email = 'standarduser2@' + random + '.example.com', + EmailEncodingKey = 'UTF-8', + LastName = 'StandardUser', + LanguageLocaleKey = 'en_US', + LocaleSidKey = 'en_US', + ProfileId = p.Id, + TimeZoneSidKey = 'America/Los_Angeles', + UserName = 'standarduser2@' + random + '.example.com' + ); + + ProgramEngagement__c engagement = [ + SELECT Id, Name + FROM ProgramEngagement__c + LIMIT 1 + ][0]; + + Test.startTest(); + System.runAs(u) { + ProgramEngagementSelector selector = new ProgramEngagementSelector(); + ProgramEngagement__c actual = selector.getProgramEngagementById( + engagement.Id + ); + System.assertEquals(null, actual); + } + Test.stopTest(); + } + @IsTest private static void testGetProgramEngagementsByProgramId() { Program__c program = [SELECT Id, Name FROM Program__c LIMIT 1]; @@ -94,7 +142,9 @@ public with sharing class ProgramEngagementSelector_TEST { 'Name', String.valueOf(ProgramEngagement__c.ProgramCohort__c) }, - ACTIVE_ENGAGEMENT_STAGES + ACTIVE_ENGAGEMENT_STAGES, + null, + null ); Test.stopTest(); System.assert( @@ -113,6 +163,107 @@ public with sharing class ProgramEngagementSelector_TEST { ); } + @IsTest + private static void testGetProgramEngagementsByProgramIdWithSearch() { + Program__c program = [SELECT Id, Name FROM Program__c LIMIT 1]; + + List expected = new List( + [ + SELECT Id, Contact__r.Name, Contact__r.Email, Stage__c, ProgramCohort__c + FROM ProgramEngagement__c + 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]) { + soslTestIds.add(con.Id); + } + Test.setFixedSearchResults(soslTestIds); + + Test.startTest(); + + ProgramEngagementSelector selector = new ProgramEngagementSelector(); + List actual = selector.getProgramEngagementsByProgramId( + program.Id, + new Set{ + 'Name', + String.valueOf(ProgramEngagement__c.ProgramCohort__c) + }, + new Set{ 'Enrolled' }, + 'Test Contact', + null + ); + Test.stopTest(); + + System.assert( + !actual.isEmpty(), + 'Expected at least one program engagement to be returned.' + ); + System.assertEquals( + expected.size(), + actual.size(), + 'Expected that both the actual and expected list size are the same' + ); + } + + @IsTest + private static void testGetProgramEngagementsByProgramIdWithSeaerchAndCohort() { + Program__c program = [SELECT Id, Name FROM Program__c LIMIT 1]; + ProgramCohort__c cohort = [ + SELECT Id, Name + FROM ProgramCohort__c + WHERE Program__c = :program.Id + LIMIT 1 + ]; + + List expected = new List( + [ + SELECT Id, Contact__r.Name, Contact__r.Email, Stage__c, ProgramCohort__c + FROM ProgramEngagement__c + WHERE + Stage__c IN :ACTIVE_ENGAGEMENT_STAGES + AND Program__c = :program.Id + AND ProgramCohort__c = :cohort.Id + ] + ); + + //Search results must be populated manually for SOSL + List soslTestIds = new List{ expected[0].Id }; + + for (Contact con : [SELECT Id, Name, Email FROM Contact]) { + soslTestIds.add(con.Id); + } + Test.setFixedSearchResults(soslTestIds); + + Test.startTest(); + + ProgramEngagementSelector selector = new ProgramEngagementSelector(); + List actual = selector.getProgramEngagementsByProgramId( + program.Id, + new Set{ + 'Name', + String.valueOf(ProgramEngagement__c.ProgramCohort__c) + }, + ACTIVE_ENGAGEMENT_STAGES, + 'Test Contact', + cohort.Id + ); + Test.stopTest(); + + System.assert( + !actual.isEmpty(), + 'Expected at least one program engagement to be returned.' + ); + System.assertEquals( + expected.size(), + actual.size(), + 'Expected that both the actual and expected list size are the same' + ); + } + @IsTest private static void testGetProgramEngagementsByProgramIdNoAccess() { Profile p = [SELECT Id FROM Profile WHERE Name = 'Standard User']; @@ -145,7 +296,9 @@ public with sharing class ProgramEngagementSelector_TEST { List actual = selector.getProgramEngagementsByProgramId( program.Id, new Set{ 'Name' }, - ACTIVE_ENGAGEMENT_STAGES + ACTIVE_ENGAGEMENT_STAGES, + null, + null ); System.assertEquals( new List(), diff --git a/force-app/main/default/classes/ProgramEngagementService.cls b/force-app/main/default/classes/ProgramEngagementService.cls index 22ad465f6..1a004441a 100644 --- a/force-app/main/default/classes/ProgramEngagementService.cls +++ b/force-app/main/default/classes/ProgramEngagementService.cls @@ -10,6 +10,8 @@ public with sharing class ProgramEngagementService { @TestVisible private FieldBucketSelector bucketSelector = new FieldBucketSelector(); + @TestVisible + private ProgramEngagementSelector selector = new ProgramEngagementSelector(); @TestVisible private Set activeStages { @@ -43,4 +45,21 @@ public with sharing class ProgramEngagementService { return activeStages; } + + public Map getActiveStagesByValue() { + Set apiNames = activeStages; + Map result = new Map(); + Schema.DescribeFieldResult fieldResult = ProgramEngagement__c.Stage__c.getDescribe(); + List ple = fieldResult.getPicklistValues(); + for (Schema.PicklistEntry f : ple) { + if (apiNames.contains(f.getValue())) { + result.put(f.getValue(), f.getLabel()); + } + } + return result; + } + + public ProgramEngagement__c getProgramEngagementById(Id peId) { + return selector.getProgramEngagementById(peId); + } } diff --git a/force-app/main/default/classes/ProgramEngagementService_TEST.cls b/force-app/main/default/classes/ProgramEngagementService_TEST.cls index bc0fc4e7d..032aa11a5 100644 --- a/force-app/main/default/classes/ProgramEngagementService_TEST.cls +++ b/force-app/main/default/classes/ProgramEngagementService_TEST.cls @@ -10,6 +10,7 @@ @IsTest public with sharing class ProgramEngagementService_TEST { private static TestStub bucketSelectorStub; + private static TestStub peSelectorStub; private static ProgramEngagementService service = new ProgramEngagementService(); @@ -52,6 +53,49 @@ public with sharing class ProgramEngagementService_TEST { bucketSelectorStub.assertCalledAsExpected(); } + @IsTest + private static void shouldGetActiveStagesByValue() { + Schema.SObjectType programEngagementSObjType = ProgramEngagement__c.SObjectType; + Schema.SObjectField stageField = ProgramEngagement__c.Stage__c; + + Map expectedStageMap = new Map(); + expectedStageMap.put('Active', 'Active'); + expectedStageMap.put('Enrolled', 'Enrolled'); + + Test.startTest(); + Map actualStageMap = service.getActiveStagesByValue(); + Test.stopTest(); + System.assertEquals( + expectedStageMap.size(), + actualStageMap.size(), + 'Returned incorrect number of mappings' + ); + } + + @IsTest + private static void shouldGetProgramEngagementById() { + Id peId = TestUtil.mockId(ProgramEngagement__c.SObjectType); + ProgramEngagement__c expected = new ProgramEngagement__c(); + expected.Id = peId; + + peSelectorStub = new StubBuilder(ProgramEngagementSelector.class) + .when('getProgramEngagementById', Id.class) + .calledWith(peId) + .thenReturn(expected) + .build(); + service.selector = (ProgramEngagementSelector) peSelectorStub.create(); + + Test.startTest(); + ProgramEngagement__c actual = service.getProgramEngagementById(peId); + Test.stopTest(); + + System.assertEquals( + expected, + actual, + 'Did not return the expected Program Engagement' + ); + } + private static List createBuckets() { BucketedField__mdt bucketedField = (BucketedField__mdt) new TestUtil.BucketedFieldBuilder() .withDeveloperName('ProgramEngagementStage') diff --git a/force-app/main/default/classes/ProgramSelector.cls b/force-app/main/default/classes/ProgramSelector.cls index d9391c411..b0e48e172 100644 --- a/force-app/main/default/classes/ProgramSelector.cls +++ b/force-app/main/default/classes/ProgramSelector.cls @@ -41,9 +41,8 @@ public with sharing class ProgramSelector { ' IN :allowedProgramCohortStatuses' ); } - programCohorts = Database.query(queryBuilder.buildSoqlQuery()); - + programCohorts.sort(); return Security.stripInaccessible(AccessType.READABLE, programCohorts) .getRecords(); } 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/classes/ServiceDeliveryService.cls b/force-app/main/default/classes/ServiceDeliveryService.cls index bbde07e14..bcdf87756 100644 --- a/force-app/main/default/classes/ServiceDeliveryService.cls +++ b/force-app/main/default/classes/ServiceDeliveryService.cls @@ -182,12 +182,17 @@ public with sharing class ServiceDeliveryService { ServiceParticipant__c participant ) { SObject particpantRecord = participant; + + Date deliveryDate = session.SessionStart__c == null + ? null + : session.SessionStart__c.date(); + SObject deliveryRecord = new ServiceDelivery__c( ServiceSession__c = session.Id, AttendanceStatus__c = DEFAULT_STATUS, Contact__r = participant.Contact__r, Service_Provider__c = session.PrimaryServiceProvider__c, - DeliveryDate__c = Date.valueOf(session.SessionStart__c), + DeliveryDate__c = deliveryDate, Service__c = session.ServiceSchedule__r.Service__c, Quantity__c = session.ServiceSchedule__r.DefaultServiceQuantity__c ); diff --git a/force-app/main/default/classes/ServiceScheduleCreatorController.cls b/force-app/main/default/classes/ServiceScheduleCreatorController.cls index ae57ce2ee..ce221b6e3 100644 --- a/force-app/main/default/classes/ServiceScheduleCreatorController.cls +++ b/force-app/main/default/classes/ServiceScheduleCreatorController.cls @@ -10,6 +10,8 @@ public with sharing class ServiceScheduleCreatorController { @TestVisible private static ServiceScheduleService service = new ServiceScheduleService(); + @TestVisible + private static ProgramEngagementService peService = new ProgramEngagementService(); @AuraEnabled(cacheable=true) public static ServiceScheduleModel getServiceScheduleModel( @@ -32,6 +34,15 @@ public with sharing class ServiceScheduleCreatorController { } } + @AuraEnabled + public static ProgramEngagement__c getProgramEngagementById(Id peId) { + try { + return peService.getProgramEngagementById(peId); + } catch (Exception ex) { + throw Util.getAuraHandledException(ex); + } + } + @AuraEnabled // Doing fls checks in the validateCreateAccess method in the domain /* sfca-disable-stack ApexFlsViolationRule */ @@ -47,9 +58,31 @@ public with sharing class ServiceScheduleCreatorController { } @AuraEnabled(cacheable=true) - public static SelectParticipantModel getSelectParticipantModel(Id serviceId) { + public static Map getActiveStages() { + try { + return peService.getActiveStagesByValue(); + } catch (Exception ex) { + throw Util.getAuraHandledException(ex); + } + } + + @AuraEnabled(cacheable=true) + public static SelectParticipantModel getSelectParticipantModel( + Id serviceId, + String searchText, + String stage, + String cohortId + ) { + if (cohortId == '') { + cohortId = null; + } try { - return service.getSelectParticipantModel(serviceId); + return service.getSelectParticipantModel( + serviceId, + searchText, + stage, + cohortId + ); } catch (Exception ex) { throw Util.getAuraHandledException(ex); } diff --git a/force-app/main/default/classes/ServiceScheduleCreatorController_TEST.cls b/force-app/main/default/classes/ServiceScheduleCreatorController_TEST.cls index a2a63d30b..c33120e3d 100644 --- a/force-app/main/default/classes/ServiceScheduleCreatorController_TEST.cls +++ b/force-app/main/default/classes/ServiceScheduleCreatorController_TEST.cls @@ -10,6 +10,105 @@ @IsTest public with sharing class ServiceScheduleCreatorController_TEST { private static BasicStub serviceStub = new BasicStub(ServiceScheduleService.class); + private static BasicStub peServiceStub = new BasicStub( + ProgramEngagementService.class + ); + + @IsTest + private static void shouldGetActiveStages() { + String methodName = 'getActiveStagesByValue'; + Map expectedStageMap = new Map{ + 'Active' => 'Active', + 'Enrolled' => 'Enrolled' + }; + + peServiceStub.withReturnValue(methodName, expectedStageMap); + ServiceScheduleCreatorController.peService = (ProgramEngagementService) peServiceStub.createMock(); + + Test.startTest(); + Map actualStageMap = ServiceScheduleCreatorController.getActiveStages(); + Test.stopTest(); + + System.assertEquals( + expectedStageMap, + actualStageMap, + 'Expected stage map was not returned' + ); + peServiceStub.assertCalled(methodName); + } + + @IsTest + private static void shouldGetActiveStagesException() { + String methodName = 'getActiveStagesByValue'; + peServiceStub.withThrowException(methodName); + Exception actualException; + + ServiceScheduleCreatorController.peService = (ProgramEngagementService) peServiceStub.createMock(); + Map actualStageMap; + Test.startTest(); + try { + actualStageMap = ServiceScheduleCreatorController.getActiveStages(); + } catch (Exception ex) { + actualException = ex; + } + Test.stopTest(); + + System.assertEquals( + peServiceStub.testExceptionMessage, + actualException.getMessage(), + 'Expected the controller to rethrow the exception from the service.' + ); + peServiceStub.assertCalled(methodName); + } + + @IsTest + private static void shouldGetProgramEngagementById() { + String methodName = 'getProgramEngagementById'; + Id peId = TestUtil.mockId(ProgramEngagement__c.SObjectType); + ProgramEngagement__c expected = new ProgramEngagement__c(); + expected.Id = peId; + + peServiceStub.withReturnValue(methodName, new List{ Id.class }, expected); + ServiceScheduleCreatorController.peService = (ProgramEngagementService) peServiceStub.createMock(); + + Test.startTest(); + ProgramEngagement__c actual = ServiceScheduleCreatorController.getProgramEngagementById( + peId + ); + Test.stopTest(); + + System.assertEquals( + expected, + actual, + 'Expected Program Engagement was not returned' + ); + peServiceStub.assertCalled(methodName, new List{ Id.class }); + } + + @IsTest + private static void shouldGetProgramEngagementByIdException() { + String methodName = 'getProgramEngagementById'; + peServiceStub.withThrowException(methodName, new List{ Id.class }); + Exception actualException; + Id peId = TestUtil.mockId(ProgramEngagement__c.SObjectType); + ProgramEngagement__c actual; + ServiceScheduleCreatorController.peService = (ProgramEngagementService) peServiceStub.createMock(); + + Test.startTest(); + try { + actual = ServiceScheduleCreatorController.getProgramEngagementById(peId); + } catch (Exception ex) { + actualException = ex; + } + Test.stopTest(); + + System.assertEquals( + peServiceStub.testExceptionMessage, + actualException.getMessage(), + 'Expected the controller to rethrow the exception from the service.' + ); + peServiceStub.assertCalled(methodName, new List{ Id.class }); + } @IsTest private static void shouldGetModelFromService() { @@ -491,14 +590,21 @@ public with sharing class ServiceScheduleCreatorController_TEST { SelectParticipantModel modelToReturn = new SelectParticipantModel(); String methodName = 'getSelectParticipantModel'; - serviceStub.withReturnValue(methodName, Id.class, modelToReturn); + serviceStub.withReturnValue( + methodName, + new List{ Id.class, String.class, String.class, Id.class }, + modelToReturn + ); Test.startTest(); ServiceScheduleCreatorController.service = (ServiceScheduleService) serviceStub.createMock(); SelectParticipantModel actual = ServiceScheduleCreatorController.getSelectParticipantModel( - serviceId + serviceId, + null, + null, + null ); Test.stopTest(); @@ -514,7 +620,102 @@ public with sharing class ServiceScheduleCreatorController_TEST { 'Expected the model returned from the service is what is returned by the controller' ); - serviceStub.assertCalledWith(methodName, Id.class, serviceId); + serviceStub.assertCalledWith( + methodName, + new List{ Id.class, String.class, String.class, Id.class }, + new List{ serviceId, null, null, null } + ); + } + + @IsTest + private static void testGetSelectParticipantModelWithEmptyCohort() { + Id serviceId = TestUtil.mockId(Service__c.SObjectType); + String searchString = 'Test'; + String stageString = ''; + SelectParticipantModel modelToReturn = new SelectParticipantModel(); + String methodName = 'getSelectParticipantModel'; + + serviceStub.withReturnValue( + methodName, + new List{ Id.class, String.class, String.class, Id.class }, + modelToReturn + ); + + Test.startTest(); + + ServiceScheduleCreatorController.service = (ServiceScheduleService) serviceStub.createMock(); + + SelectParticipantModel actual = ServiceScheduleCreatorController.getSelectParticipantModel( + serviceId, + searchString, + stageString, + '' + ); + + Test.stopTest(); + + System.assertNotEquals( + null, + actual, + 'Expected that actual participant model is not null' + ); + System.assertEquals( + true, + modelToReturn === actual, + 'Expected the model returned from the service is what is returned by the controller' + ); + + serviceStub.assertCalledWith( + methodName, + new List{ Id.class, String.class, String.class, Id.class }, + new List{ serviceId, searchString, null, null } + ); + } + + @IsTest + private static void testGetSelectParticipantModelWithSearchAndCohort() { + Id serviceId = TestUtil.mockId(Service__c.SObjectType); + Id cohortId = TestUtil.mockId(ProgramCohort__c.SObjectType); + String searchString = 'Test'; + String stageString = ''; + SelectParticipantModel modelToReturn = new SelectParticipantModel(); + String methodName = 'getSelectParticipantModel'; + + serviceStub.withReturnValue( + methodName, + new List{ Id.class, String.class, String.class, Id.class }, + modelToReturn + ); + + Test.startTest(); + + ServiceScheduleCreatorController.service = (ServiceScheduleService) serviceStub.createMock(); + + SelectParticipantModel actual = ServiceScheduleCreatorController.getSelectParticipantModel( + serviceId, + searchString, + stageString, + cohortId + ); + + Test.stopTest(); + + System.assertNotEquals( + null, + actual, + 'Expected that actual participant model is not null' + ); + System.assertEquals( + true, + modelToReturn === actual, + 'Expected the model returned from the service is what is returned by the controller' + ); + + serviceStub.assertCalledWith( + methodName, + new List{ Id.class, String.class, String.class, Id.class }, + new List{ serviceId, searchString, stageString, cohortId } + ); } @IsTest @@ -524,13 +725,19 @@ public with sharing class ServiceScheduleCreatorController_TEST { SelectParticipantModel actual; Exception actualException; - serviceStub.withThrowException(methodName, Id.class); + serviceStub.withThrowException( + methodName, + new List{ Id.class, String.class, String.class, Id.class } + ); ServiceScheduleCreatorController.service = (ServiceScheduleService) serviceStub.createMock(); Test.startTest(); try { actual = ServiceScheduleCreatorController.getSelectParticipantModel( - serviceId + serviceId, + null, + null, + null ); } catch (Exception e) { actualException = e; @@ -545,7 +752,11 @@ public with sharing class ServiceScheduleCreatorController_TEST { System.assertEquals(null, actual, 'Expected that the actual value is null'); - serviceStub.assertCalledWith(methodName, Id.class, serviceId); + serviceStub.assertCalledWith( + methodName, + new List{ Id.class, String.class, String.class, Id.class }, + new List{ serviceId, null, null, null } + ); } @IsTest diff --git a/force-app/main/default/classes/ServiceScheduleService.cls b/force-app/main/default/classes/ServiceScheduleService.cls index 0c7517f06..214d77628 100644 --- a/force-app/main/default/classes/ServiceScheduleService.cls +++ b/force-app/main/default/classes/ServiceScheduleService.cls @@ -98,9 +98,14 @@ public with sharing class ServiceScheduleService { domain.insertParticipants(engagements, schedule); } - public SelectParticipantModel getSelectParticipantModel(Id serviceId) { + public SelectParticipantModel getSelectParticipantModel( + Id serviceId, + String searchText, + String stage, + Id cohortId + ) { SelectParticipantModel model = new SelectParticipantModel(); - loadAndPopulateParticipantRecords(serviceId, model); + loadAndPopulateParticipantRecords(serviceId, model, searchText, stage, cohortId); return model; } @@ -248,8 +253,16 @@ public with sharing class ServiceScheduleService { private void loadAndPopulateParticipantRecords( Id serviceId, - SelectParticipantModel model + SelectParticipantModel model, + String searchText, + String stage, + Id cohortId ) { + Set stages; + stages = String.isEmpty(stage) + ? progEngagementService.getActiveStages() + : new Set{ stage }; + model.program = programEngagementSelector.getProgramByServiceId(serviceId); if (model.program == null) { return; @@ -262,7 +275,9 @@ public with sharing class ServiceScheduleService { model.programEngagements = programEngagementSelector.getProgramEngagementsByProgramId( model.program.Id, getSelectFieldsWithToLabel(model), - progEngagementService.getActiveStages() + stages, + searchText, + cohortId ); } diff --git a/force-app/main/default/classes/ServiceScheduleService_TEST.cls b/force-app/main/default/classes/ServiceScheduleService_TEST.cls index a12869ed0..4e4d7e112 100644 --- a/force-app/main/default/classes/ServiceScheduleService_TEST.cls +++ b/force-app/main/default/classes/ServiceScheduleService_TEST.cls @@ -906,7 +906,13 @@ public with sharing class ServiceScheduleService_TEST { programEngagementSelectorStub.withReturnValue( programEngagementsMethodName, - new List{ Id.class, Set.class, Set.class }, + new List{ + Id.class, + Set.class, + Set.class, + String.class, + Id.class + }, programEngagementsToReturn ); @@ -920,7 +926,12 @@ public with sharing class ServiceScheduleService_TEST { service.programSelector = (ProgramSelector) programSelectorStub.createMock(); Test.startTest(); - SelectParticipantModel actual = service.getSelectParticipantModel(serviceId); + SelectParticipantModel actual = service.getSelectParticipantModel( + serviceId, + null, + null, + null + ); Test.stopTest(); System.assertNotEquals( @@ -968,11 +979,19 @@ public with sharing class ServiceScheduleService_TEST { programEngagementSelectorStub.assertCalledWith( programEngagementsMethodName, - new List{ Id.class, Set.class, Set.class }, + new List{ + Id.class, + Set.class, + Set.class, + String.class, + Id.class + }, new List{ programId, service.getSelectFieldsWithToLabel(actual), - new Set{ 'Active', 'Enrolled' } + new Set{ 'Active', 'Enrolled' }, + null, + null } ); @@ -983,7 +1002,10 @@ public with sharing class ServiceScheduleService_TEST { private static void shouldReturnNullProgramWhenProgramNotFound() { Test.startTest(); SelectParticipantModel actualModel = service.getSelectParticipantModel( - TestUtil.mockId(Service__c.SObjectType) + TestUtil.mockId(Service__c.SObjectType), + null, + 'Active', + null ); Test.stopTest(); @@ -1051,7 +1073,10 @@ public with sharing class ServiceScheduleService_TEST { // Step 3 SelectParticipantModel participantModel = service.getSelectParticipantModel( - serviceId + serviceId, + null, + null, + null ); System.assert( !participantModel.programEngagements.isEmpty(), diff --git a/force-app/main/default/classes/ServiceSelector.cls b/force-app/main/default/classes/ServiceSelector.cls index 09e15c255..b1fa15381 100755 --- a/force-app/main/default/classes/ServiceSelector.cls +++ b/force-app/main/default/classes/ServiceSelector.cls @@ -16,11 +16,11 @@ public with sharing class ServiceSelector { return new List(); } List queriedServices = [ - SELECT Id, Name, Program__c + SELECT Id, Name, Program__c, Status__c FROM Service__c WHERE Program__c IN :programIds ]; - + queriedServices.sort(); return Security.stripInaccessible(AccessType.READABLE, queriedServices) .getRecords(); } @@ -40,7 +40,7 @@ public with sharing class ServiceSelector { WHERE Id = :programEngagementId ) ]; - + services.sort(); return Security.stripInaccessible(AccessType.READABLE, services).getRecords(); } diff --git a/force-app/main/default/classes/ServiceService.cls b/force-app/main/default/classes/ServiceService.cls index 0f6d4c778..2baf2ea1c 100644 --- a/force-app/main/default/classes/ServiceService.cls +++ b/force-app/main/default/classes/ServiceService.cls @@ -11,36 +11,118 @@ public with sharing class ServiceService { public ServiceService() { } + private static final String SERVICE_STATUS_ACTIVE = 'ServiceStatusActive'; + private static final String ENGAGEMENTS = 'engagements'; + private static final String SERVICES = 'services'; + private static final String LABEL = 'label'; + private static final String VALUE = 'value'; + private static final String NAME = 'Name'; + private static final String Id = 'Id'; + private static final String PROGRAM = 'program'; + private static final String BSDT_ACTIVE_FILTER = 'BSDTActiveFilter'; + + @TestVisible + private FieldBucketSelector bucketSelector = new FieldBucketSelector(); + @TestVisible private FieldSetService fieldSetService = new FieldSetService(); @TestVisible private ServiceSelector serviceSelector = new ServiceSelector(); + @TestVisible + private ProgramEngagementService engagementService = new ProgramEngagementService(); + @TestVisible private ProgramEngagementSelector engagementSelector = new ProgramEngagementSelector(); + @TestVisible + private Set activeStatuses { + get { + if (activeStatuses == null) { + activeStatuses = getActiveStatuses(); + } + return activeStatuses; + } + set; + } + + private Set getActiveStatuses() { + List bucketNames = new List{ SERVICE_STATUS_ACTIVE }; + Set activeStatuses = new Set(); + + Schema.SObjectType serviceSObjType = Service__c.SObjectType; + Schema.SObjectField statusField = Service__c.Status__c; + + for ( + Bucket__mdt bucket : bucketSelector.getBuckets( + bucketNames, + serviceSObjType, + statusField + ) + ) { + for (BucketedValue__mdt value : bucket.BucketedValues__r) { + activeStatuses.add(value.Value__c); + } + } + + return activeStatuses; + } + + private Boolean isBsdtActiveFilterActive() { + List features = CustomMetadataSelector.getInstance() + .getAllFeatureGates(); + + for (FeatureGate__mdt feature : features) { + if ( + feature.IsActive__c && BSDT_ACTIVE_FILTER.contains(feature.DeveloperName) + ) { + return feature.IsActive__c; + } + } + + return false; + } + public Map> getServicesEngagementsByContactId(Id contactId) { Set programIds = new Set(); String serviceProgram = Schema.SObjectType.Service__c.Fields.Program__c.getName(); String engageProgram = Schema.SObjectType.ProgramEngagement__c.Fields.Program__c.getName(); + Set activeServiceStatuses = new Set(); + Set activeProgramEngagementStages = new Set(); + Boolean bsdtFilterIsActive = isBsdtActiveFilterActive(); + + if (bsdtFilterIsActive) { + activeServiceStatuses = activeStatuses; + activeProgramEngagementStages = engagementService.getActiveStages(); + } Map> result = new Map>(); - result.put('engagements', new List()); - result.put('services', new List()); + result.put(ENGAGEMENTS, new List()); + result.put(SERVICES, new List()); for ( ProgramEngagement__c engagement : engagementSelector.getProgramEngagementsByContactId( contactId ) ) { - programIds.add(engagement.Program__c); - result.get('engagements') - .add(convertObjectToOption(engagement, engageProgram)); + if ( + activeProgramEngagementStages.isEmpty() || + activeProgramEngagementStages.contains(engagement.Stage__c) + ) { + programIds.add(engagement.Program__c); + result.get(ENGAGEMENTS) + .add(convertObjectToOption(engagement, engageProgram)); + } } for (Service__c service : serviceSelector.getServicesByProgramIds(programIds)) { - result.get('services').add(convertObjectToOption(service, serviceProgram)); + if ( + activeServiceStatuses.isEmpty() || + activeServiceStatuses.contains(service.Status__c) + ) { + result.get(SERVICES).add(convertObjectToOption(service, serviceProgram)); + } } return result; @@ -48,9 +130,9 @@ public with sharing class ServiceService { private Map convertObjectToOption(sObject obj, String programField) { Map result = new Map(); - result.put('label', (String) obj.get('Name')); - result.put('value', (String) obj.get('Id')); - result.put('program', (String) obj.get(programField)); + result.put(LABEL, (String) obj.get(NAME)); + result.put(VALUE, (String) obj.get(ID)); + result.put(PROGRAM, (String) obj.get(programField)); return result; } diff --git a/force-app/main/default/classes/ServiceService_TEST.cls b/force-app/main/default/classes/ServiceService_TEST.cls index 51aad9973..05ceb8e23 100644 --- a/force-app/main/default/classes/ServiceService_TEST.cls +++ b/force-app/main/default/classes/ServiceService_TEST.cls @@ -10,7 +10,7 @@ @isTest public with sharing class ServiceService_TEST { @IsTest - private static void testGetServicesEngagementsByContactId() { + private static void testGetServicesEngagementsByContactIdFeatureOn() { Id contactId = TestUtil.mockId(Contact.SObjectType); Program__c program1 = new Program__c( @@ -23,6 +23,14 @@ public with sharing class ServiceService_TEST { ProgramEngagement__c engagement1 = new ProgramEngagement__c( Name = 'Engagement 1', + Stage__c = 'Active', + Contact__c = contactId, + Program__c = program1.Id, + Role__c = 'Client' + ); + + ProgramEngagement__c engagement2 = new ProgramEngagement__c( + Name = 'Engagement 2', Stage__c = 'Enrolled', Contact__c = contactId, Program__c = program1.Id, @@ -37,10 +45,155 @@ public with sharing class ServiceService_TEST { UnitOfMeasurement__c = 'Hours' ); + Service__c service2 = new Service__c( + Id = TestUtil.mockId(Service__c.SObjectType), + Name = 'Service 2', + Program__c = program1.Id, + Status__c = 'Planned', + UnitOfMeasurement__c = 'Hours' + ); + List engagements = new List{ - engagement1 + engagement1, + engagement2 }; - List services = new List{ service1 }; + List services = new List{ service1, service2 }; + List bucketNames = new List{ 'ServiceStatusActive' }; + Schema.SObjectType serviceSObjType = Service__c.SObjectType; + Schema.SObjectField statusField = Service__c.Status__c; + List statusBuckets = createBuckets(); + Set activeStages = new Set{ 'Active' }; + + TestStub engagementServiceStub = new StubBuilder(ProgramEngagementService.class) + .when('getActiveStages') + .called() + .thenReturn(activeStages) + .build(); + TestStub engagementSelectorStub = new StubBuilder(ProgramEngagementSelector.class) + .when('getProgramEngagementsByContactId', Id.class) + .calledWith(contactId) + .thenReturn(engagements) + .build(); + TestStub serviceSelectorStub = new StubBuilder(ServiceSelector.class) + .when('getServicesByProgramIds', Set.class) + .calledWith(new Set{ program1.Id }) + .thenReturn(services) + .build(); + TestStub bucketSelectorStub = new StubBuilder(FieldBucketSelector.class) + .when( + 'getBuckets', + List.class, + Schema.SObjectType.class, + Schema.SObjectField.class + ) + .calledWith(bucketNames, serviceSObjType, statusField) + .thenReturn(statusBuckets) + .build(); + + ServiceService service = new ServiceService(); + service.serviceSelector = (ServiceSelector) serviceSelectorStub.create(); + service.engagementSelector = (ProgramEngagementSelector) engagementSelectorStub.create(); + service.bucketSelector = (FieldBucketSelector) bucketSelectorStub.create(); + service.engagementService = (ProgramEngagementService) engagementServiceStub.create(); + + // By turning off none, we turn on all + TestUtil.turnOffFeatureGates(new Set{}); + + Test.startTest(); + Map> actual = service.getServicesEngagementsByContactId( + contactId + ); + Test.stopTest(); + + Set expectedKeySet = new Set{ 'engagements', 'services' }; + System.assertEquals( + expectedKeySet, + actual.keySet(), + 'Expected both keys to be returned.' + ); + + for (List objList : actual.values()) { + System.assert(!objList.isEmpty()); + } + + System.assertEquals( + 1, + actual.get('engagements').size(), + 'Only active Program Engagements should be returned' + ); + + System.assertEquals( + 1, + actual.get('services').size(), + 'Only active Services should be returned' + ); + + engagementSelectorStub.assertCalledAsExpected(); + serviceSelectorStub.assertCalledAsExpected(); + bucketSelectorStub.assertCalledAsExpected(); + } + + @IsTest + private static void testGetServicesEngagementsByContactIdFeatureOff() { + final String BSDT_ACTIVE_FILTER = 'BSDTActiveFilter'; + Id contactId = TestUtil.mockId(Contact.SObjectType); + + Program__c program1 = new Program__c( + Id = TestUtil.mockId(Program__c.SObjectType), + Name = 'Program 1', + Status__c = 'Active', + StartDate__c = Date.today(), + EndDate__c = Date.today().addDays(30) + ); + + ProgramEngagement__c engagement1 = new ProgramEngagement__c( + Name = 'Engagement 1', + Stage__c = 'Active', + Contact__c = contactId, + Program__c = program1.Id, + Role__c = 'Client' + ); + + ProgramEngagement__c engagement2 = new ProgramEngagement__c( + Name = 'Engagement 2', + Stage__c = 'Enrolled', + Contact__c = contactId, + Program__c = program1.Id, + Role__c = 'Client' + ); + + Service__c service1 = new Service__c( + Id = TestUtil.mockId(Service__c.SObjectType), + Name = 'Service 1', + Program__c = program1.Id, + Status__c = 'Active', + UnitOfMeasurement__c = 'Hours' + ); + + Service__c service2 = new Service__c( + Id = TestUtil.mockId(Service__c.SObjectType), + Name = 'Service 2', + Program__c = program1.Id, + Status__c = 'Planned', + UnitOfMeasurement__c = 'Hours' + ); + + List engagements = new List{ + engagement1, + engagement2 + }; + List services = new List{ service1, service2 }; + List bucketNames = new List{ 'ServiceStatusActive' }; + Schema.SObjectType serviceSObjType = Service__c.SObjectType; + Schema.SObjectField statusField = Service__c.Status__c; + List statusBuckets = createBuckets(); + Set activeStages = new Set{ 'Active' }; + + TestStub engagementServiceStub = new StubBuilder(ProgramEngagementService.class) + .when('getActiveStages') + .called() + .thenReturn(activeStages) + .build(); TestStub engagementSelectorStub = new StubBuilder(ProgramEngagementSelector.class) .when('getProgramEngagementsByContactId', Id.class) .calledWith(contactId) @@ -51,10 +204,24 @@ public with sharing class ServiceService_TEST { .calledWith(new Set{ program1.Id }) .thenReturn(services) .build(); + TestStub bucketSelectorStub = new StubBuilder(FieldBucketSelector.class) + .when( + 'getBuckets', + List.class, + Schema.SObjectType.class, + Schema.SObjectField.class + ) + .calledWith(bucketNames, serviceSObjType, statusField) + .thenReturn(statusBuckets) + .build(); ServiceService service = new ServiceService(); service.serviceSelector = (ServiceSelector) serviceSelectorStub.create(); service.engagementSelector = (ProgramEngagementSelector) engagementSelectorStub.create(); + service.bucketSelector = (FieldBucketSelector) bucketSelectorStub.create(); + service.engagementService = (ProgramEngagementService) engagementServiceStub.create(); + + TestUtil.turnOffFeatureGates(new Set{ BSDT_ACTIVE_FILTER }); Test.startTest(); Map> actual = service.getServicesEngagementsByContactId( @@ -73,6 +240,18 @@ public with sharing class ServiceService_TEST { System.assert(!objList.isEmpty()); } + System.assertEquals( + 2, + actual.get('engagements').size(), + 'All Program Engagements should be returned' + ); + + System.assertEquals( + 2, + actual.get('services').size(), + 'All Services should be returned' + ); + engagementSelectorStub.assertCalledAsExpected(); serviceSelectorStub.assertCalledAsExpected(); } @@ -701,4 +880,35 @@ public with sharing class ServiceService_TEST { fieldSetServiceStub.assertCalledAsExpected(); } + + private static List createBuckets() { + BucketedField__mdt bucketedField = (BucketedField__mdt) new TestUtil.BucketedFieldBuilder() + .withDeveloperName('ServiceStatuses') + .withQualifiedApiName(Util.prefixNamespace('ServiceStatuses')) + .withField('Status__c') + .withObject('Service__c') + .withMockId() + .build(); + + List activeValues = new List(); + activeValues.add( + (BucketedValue__mdt) new TestUtil.BucketedValueBuilder() + .withDeveloperName('ServiceStatusActive') + .withQualifiedApiName(Util.prefixNamespace('ServiceStatusActive')) + .withBucket('ServiceStatusActive') + .withValue('Active') + .withMockId() + .build() + ); + + Bucket__mdt activeBucket = (Bucket__mdt) new TestUtil.BucketBuilder() + .withBucketedField(bucketedField) + .withBucketedValues(activeValues) + .withDeveloperName('ServiceStatusActive') + .withQualifiedApiName(Util.prefixNamespace('ServiceStatusActive')) + .withMockId() + .build(); + + return new List{ activeBucket }; + } } diff --git a/force-app/main/default/classes/TestUtil.cls b/force-app/main/default/classes/TestUtil.cls index ad1ab1073..aff65f9aa 100755 --- a/force-app/main/default/classes/TestUtil.cls +++ b/force-app/main/default/classes/TestUtil.cls @@ -13,6 +13,7 @@ */ public with sharing class TestUtil { private static final Set featureGateNames = new Set{ + 'BSDTActiveFilter', 'ServiceDeliveriesToContact', 'ServiceDeliveriesToService', 'ServiceDeliveriesToServiceSession', diff --git a/force-app/main/default/customMetadata/Bucket.ServiceStatusActive.md-meta.xml b/force-app/main/default/customMetadata/Bucket.ServiceStatusActive.md-meta.xml new file mode 100644 index 000000000..361af2506 --- /dev/null +++ b/force-app/main/default/customMetadata/Bucket.ServiceStatusActive.md-meta.xml @@ -0,0 +1,9 @@ + + + Service Status: Active + false + + BucketedField__c + ServiceStatuses + + diff --git a/force-app/main/default/customMetadata/BucketedField.ServiceStatuses.md-meta.xml b/force-app/main/default/customMetadata/BucketedField.ServiceStatuses.md-meta.xml new file mode 100644 index 000000000..030117da0 --- /dev/null +++ b/force-app/main/default/customMetadata/BucketedField.ServiceStatuses.md-meta.xml @@ -0,0 +1,13 @@ + + + Service Statuses + false + + Field__c + Status__c + + + Object__c + Service__c + + diff --git a/force-app/main/default/customMetadata/BucketedValue.ServiceStatusActive.md-meta.xml b/force-app/main/default/customMetadata/BucketedValue.ServiceStatusActive.md-meta.xml new file mode 100644 index 000000000..9b409ac87 --- /dev/null +++ b/force-app/main/default/customMetadata/BucketedValue.ServiceStatusActive.md-meta.xml @@ -0,0 +1,13 @@ + + + Service Status: Active + false + + Bucket__c + ServiceStatusActive + + + Value__c + Active + + diff --git a/force-app/main/default/customMetadata/FeatureGate.BSDTActiveFilter.md-meta.xml b/force-app/main/default/customMetadata/FeatureGate.BSDTActiveFilter.md-meta.xml new file mode 100644 index 000000000..79917bd92 --- /dev/null +++ b/force-app/main/default/customMetadata/FeatureGate.BSDTActiveFilter.md-meta.xml @@ -0,0 +1,9 @@ + + + BSDT Active Filter + false + + IsActive__c + false + + diff --git a/force-app/main/default/labels/CustomLabels.labels-meta.xml b/force-app/main/default/labels/CustomLabels.labels-meta.xml index 06be9fca2..b86144dc8 100644 --- a/force-app/main/default/labels/CustomLabels.labels-meta.xml +++ b/force-app/main/default/labels/CustomLabels.labels-meta.xml @@ -413,8 +413,15 @@ Filter_by_Record en_US true - Filter by {SObject Label} - Filter by {0} + Filter by: {SObject Label} + Filter by: {0} + + + Filter_by_Stage + en_US + true + Filter by Stage + Filter by Stage Finish @@ -1096,4 +1103,18 @@ This feature is only available on the web application This feature is only available on the web application. + + Too_Many_Participants + en_US + true + Too many results returned from participant selector + The results have returned more than a 1000 participants. Refine the results by searching or filtering by Program Cohort. + + + Service_Schedule_Wizard + en_US + true + Service Schedule Wizard + Service Schedule Wizard + diff --git a/force-app/main/default/lwc/__tests__/lightning/modal/modal.html b/force-app/main/default/lwc/__tests__/lightning/modal/modal.html new file mode 100644 index 000000000..b9ee2f3f4 --- /dev/null +++ b/force-app/main/default/lwc/__tests__/lightning/modal/modal.html @@ -0,0 +1,5 @@ + + + + + diff --git a/force-app/main/default/lwc/__tests__/lightning/modal/modal.js b/force-app/main/default/lwc/__tests__/lightning/modal/modal.js new file mode 100644 index 000000000..7d8bf68c2 --- /dev/null +++ b/force-app/main/default/lwc/__tests__/lightning/modal/modal.js @@ -0,0 +1,7 @@ +import { LightningElement, api } from "lwc"; + +export default class Modal extends LightningElement { + @api content; + @api open() {} + @api close() {} +} diff --git a/force-app/main/default/lwc/__tests__/lightning/modalBody/modalBody.html b/force-app/main/default/lwc/__tests__/lightning/modalBody/modalBody.html new file mode 100644 index 000000000..53c4b8d5c --- /dev/null +++ b/force-app/main/default/lwc/__tests__/lightning/modalBody/modalBody.html @@ -0,0 +1,4 @@ + + + diff --git a/force-app/main/default/lwc/__tests__/lightning/modalBody/modalBody.js b/force-app/main/default/lwc/__tests__/lightning/modalBody/modalBody.js new file mode 100644 index 000000000..a8a972680 --- /dev/null +++ b/force-app/main/default/lwc/__tests__/lightning/modalBody/modalBody.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from "lwc"; + +export default class ModalBody extends LightningElement { + @api content; +} diff --git a/force-app/main/default/lwc/__tests__/lightning/modalFooter/modalFooter.html b/force-app/main/default/lwc/__tests__/lightning/modalFooter/modalFooter.html new file mode 100644 index 000000000..02ece5b64 --- /dev/null +++ b/force-app/main/default/lwc/__tests__/lightning/modalFooter/modalFooter.html @@ -0,0 +1,5 @@ + + + + + diff --git a/force-app/main/default/lwc/__tests__/lightning/modalFooter/modalFooter.js b/force-app/main/default/lwc/__tests__/lightning/modalFooter/modalFooter.js new file mode 100644 index 000000000..31663a7ba --- /dev/null +++ b/force-app/main/default/lwc/__tests__/lightning/modalFooter/modalFooter.js @@ -0,0 +1,3 @@ +import { LightningElement } from "lwc"; + +export default class ModalFooter extends LightningElement {} diff --git a/force-app/main/default/lwc/__tests__/lightning/modalHeader/modalHeader.html b/force-app/main/default/lwc/__tests__/lightning/modalHeader/modalHeader.html new file mode 100644 index 000000000..fcaa99007 --- /dev/null +++ b/force-app/main/default/lwc/__tests__/lightning/modalHeader/modalHeader.html @@ -0,0 +1,8 @@ + + + + {label} + + + + diff --git a/force-app/main/default/lwc/__tests__/lightning/modalHeader/modalHeader.js b/force-app/main/default/lwc/__tests__/lightning/modalHeader/modalHeader.js new file mode 100644 index 000000000..9f99ebced --- /dev/null +++ b/force-app/main/default/lwc/__tests__/lightning/modalHeader/modalHeader.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from "lwc"; + +export default class ModalHeader extends LightningElement { + @api label; +} 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 1a3e60eec..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) { @@ -103,7 +104,9 @@ export default class BulkServiceDeliveryUI extends NavigationMixin(LightningElem set defaultValues(value) { let serviceDelivery = this.serviceDeliveries[0]; this._defaultValues = value; - Object.assign(serviceDelivery, this._defaultValues); + if (serviceDelivery) { + Object.assign(serviceDelivery, this._defaultValues); + } } @api @@ -178,6 +181,10 @@ export default class BulkServiceDeliveryUI extends NavigationMixin(LightningElem return "error"; } + get isSaveDisabled() { + return this.isSaving; + } + addDelivery() { let serviceDelivery = { index: this._nextIndex, @@ -202,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 : ""; @@ -216,48 +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(); - } - } - 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++; + 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); + } + + 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")); + }); } - // eslint-disable-next-line no-unused-vars - handleRowSuccess(event) { - this.savedCount++; + processResults(results, deliveries) { + let resultByIndex = {}; + results = JSON.parse(results); - if (this.savingComplete()) { - this.showSaveSummaryToast(); + 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/participantSelector/participantSelector.html b/force-app/main/default/lwc/participantSelector/participantSelector.html index 2fb913c4c..b3e939ea0 100644 --- a/force-app/main/default/lwc/participantSelector/participantSelector.html +++ b/force-app/main/default/lwc/participantSelector/participantSelector.html @@ -28,7 +28,10 @@ class="slds-var-p-right_small slds-var-p-top_xx-small" > - + - + + + + @@ -78,8 +89,14 @@ + + + - - - - - {labels.noRecordsFound} + + {labels.noRecordsFound} + 2) { + return 5; + } + return 6; + } + + get showStageInput() { + if (this.stageOptions && this.stageOptions.length > 2) { + return true; + } + return false; + } + get showCapacityWarning() { return ( this.capacity !== undefined && @@ -161,7 +184,19 @@ export default class ParticipantSelector extends LightningElement { return `${name} (${this.participantCount}/${this.capacity})`; } - @wire(getSelectParticipantModel, { serviceId: "$serviceId" }) + @wire(getActiveStages, {}) + setupStages(result) { + if (result.data) { + this.loadStageOptions(result.data); + } + } + + @wire(getSelectParticipantModel, { + serviceId: "$serviceId", + searchText: "$wiredSearchValue", + stage: "$selectedStage", + cohortId: "$cohortId", + }) dataSetup(result) { this.wiredData = result; if (!(result.data || result.error)) { @@ -203,36 +238,43 @@ export default class ParticipantSelector extends LightningElement { this.labels.filterByCohort = format(filterByRecord, [ this.objectLabels.programCohort.objectLabel, ]); + this.labels.filterByStage = format(filterByRecord, [ + this.fields.engagementStage.label, + ]); } loadTableRows(data) { let selectedIds = this.selectedEngagements.map(engagement => engagement.Id); this.allEngagements = data.programEngagements.slice(0); + this.show1kMessage = this.allEngagements.length >= 1000 ? true : false; this.availableEngagementRows = this.allEngagements .filter(engagement => !selectedIds.includes(engagement.Id)) .map(engagement => { // Flatten relationship fields let programEngagement = { ...engagement }; - for (const [field, value] of Object.entries(programEngagement)) { - let isTimeField = - this.fieldByFieldPath[field] && - this.fieldByFieldPath[field].type === TIME; - if (isTimeField) { - programEngagement[field] = formatTime(value); - } - if (typeof value === "object") { - for (const [parentField, parentValue] of Object.entries(value)) { - programEngagement[field + parentField] = parentValue; - } - } - } - - return programEngagement; + return this.flattenProgramEngagement(programEngagement); }); this.sortData(this.availableEngagementRows); } + flattenProgramEngagement(programEngagement) { + for (const [field, value] of Object.entries(programEngagement)) { + let isTimeField = + this.fieldByFieldPath[field] && + this.fieldByFieldPath[field].type === TIME; + if (isTimeField) { + programEngagement[field] = formatTime(value); + } + if (typeof value === "object") { + for (const [parentField, parentValue] of Object.entries(value)) { + programEngagement[field + parentField] = parentValue; + } + } + } + return programEngagement; + } + dispatchLoaded() { if (this.isLoaded) { return; @@ -255,9 +297,26 @@ export default class ParticipantSelector extends LightningElement { this.processNewParticipant(event.detail); } - async processNewParticipant(id) { + async processNewParticipant(peId) { await refreshApex(this.wiredData); - this.handleSelectById(id); + let row = this.availableEngagementRows.find(element => element.Id === peId); + if (row) { + this.handleSelectById(peId); + } else { + this.loadNewProgramEngagement(peId); + } + } + + loadNewProgramEngagement(peId) { + getProgramEngagementById({ peId }) + .then(result => { + let thisPE = this.flattenProgramEngagement(result); + this.availableEngagementRows.push(thisPE); + this.handleSelectById(peId); + }) + .catch(error => { + handleError(error); + }); } handleLoadMore() { @@ -301,6 +360,16 @@ export default class ParticipantSelector extends LightningElement { this.handleSelectParticipants(); } + loadStageOptions(data) { + this.stageOptions = Object.entries(data).map(([key, value]) => { + let newObj = {}; + newObj.label = value; + newObj.value = key; + return newObj; + }); + this.stageOptions.unshift({ label: this.labels.none, value: "" }); + } + loadProgramCohorts(data) { this.cohorts = data.programCohorts.slice(0); this.searchOptions = this.cohorts.map(element => { @@ -362,10 +431,18 @@ export default class ParticipantSelector extends LightningElement { }); } - handleCohortChange(event) { - this.cohortId = event.detail.value; + handleStageChange(event) { + if (this.selectedStage !== event.detail.value) { + this.displaySpinner(); + this.selectedStage = event.detail.value; + } + } - this.applyFilters(); + handleCohortChange(event) { + if (this.cohortId !== event.detail.value) { + this.displaySpinner(); + this.cohortId = event.detail.value; + } } handleSelectAll() { @@ -430,10 +507,13 @@ export default class ParticipantSelector extends LightningElement { this.selectedEngagements = tempSelectedEngagements; - this.availableEngagementRows = [ - ...this.availableEngagementRows, - event.detail.row, - ]; + //Verify the deselected row is in the current dataset before displaying + if (this.allEngagements.some(row => row.Id === event.detail.row.Id)) { + this.availableEngagementRows = [ + ...this.availableEngagementRows, + event.detail.row, + ]; + } this.sortData(this.availableEngagementRows); //filter previouslySelectedEngagements to remove program engagement that we deselected so it does not add the deselected value back @@ -451,8 +531,15 @@ export default class ParticipantSelector extends LightningElement { } handleInputChange(event) { - this.searchValue = event.target.value; - this.debounceSearch(); + if (event.target && event.target.value) { + this.searchValue = event.target.value; + if (this.searchValue.length !== 1) { + this.debounceSearch(); + } + } else { + this.searchValue = ""; + this.debounceSearch(); + } } debounceSearch() { @@ -470,27 +557,19 @@ export default class ParticipantSelector extends LightningElement { startFilter() { this.displaySpinner(); // eslint-disable-next-line @lwc/lwc/no-async-operation - setTimeout(this.applyFilters.bind(this)); + setTimeout(this.updateSearchValue.bind(this)); } - applyFilters() { - let searchText = this.searchValue ? this.searchValue.toLowerCase() : ""; - - this.filteredEngagements = this.availableEngagementRows.filter( - row => - JSON.stringify(row) - .toLowerCase() - .includes(searchText) && - (this.cohortId - ? row[this.fields.programCohort.apiName] === this.cohortId - : true) - ); + updateSearchValue() { + this.wiredSearchValue = this.searchValue; + } + applyFilters() { + this.filteredEngagements = this.availableEngagementRows; this.availableEngagementsForSelection = this.filteredEngagements.slice( 0, Math.min(this.filteredEngagements.length, this.offset) ); - this.hideSpinner(); } 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/serviceScheduleCreator/serviceScheduleCreator.js b/force-app/main/default/lwc/serviceScheduleCreator/serviceScheduleCreator.js index 17eb6be17..7297d8a68 100644 --- a/force-app/main/default/lwc/serviceScheduleCreator/serviceScheduleCreator.js +++ b/force-app/main/default/lwc/serviceScheduleCreator/serviceScheduleCreator.js @@ -354,8 +354,8 @@ export default class ServiceScheduleCreator extends NavigationMixin(LightningEle this.navigate(); } else { this.init(); + this.dispatchEvent(new CustomEvent("close", { bubbles: true })); } - this.dispatchEvent(new CustomEvent("close", { bubbles: true })); } reset() { @@ -414,6 +414,15 @@ export default class ServiceScheduleCreator extends NavigationMixin(LightningEle actionName: "view", }, }); + this.dispatchEvent( + new CustomEvent("navigate", { + bubbles: true, + detail: { + recordId, + objectApiName, + }, + }) + ); } navigateToList() { diff --git a/force-app/main/default/lwc/serviceScheduleCreatorModal/serviceScheduleCreatorModal.html b/force-app/main/default/lwc/serviceScheduleCreatorModal/serviceScheduleCreatorModal.html new file mode 100644 index 000000000..534d0c258 --- /dev/null +++ b/force-app/main/default/lwc/serviceScheduleCreatorModal/serviceScheduleCreatorModal.html @@ -0,0 +1,12 @@ + + + + + + diff --git a/force-app/main/default/lwc/serviceScheduleCreatorModal/serviceScheduleCreatorModal.js b/force-app/main/default/lwc/serviceScheduleCreatorModal/serviceScheduleCreatorModal.js new file mode 100644 index 000000000..d166a274c --- /dev/null +++ b/force-app/main/default/lwc/serviceScheduleCreatorModal/serviceScheduleCreatorModal.js @@ -0,0 +1,28 @@ +import { api } from "lwc"; +import LightningModal from "lightning/modal"; +import { NavigationMixin } from "lightning/navigation"; + +import newServiceSchedule from "@salesforce/label/c.New_Service_Schedule"; + +export default class ServiceScheduleCreatorModal extends NavigationMixin(LightningModal) { + labels = { + newServiceSchedule, + }; + @api serviceId; + @api recordTypeId; + @api isCommunity = false; + + handleNavigate(event) { + const recordId = event.detail.recordId; + const objectApiName = event.detail.objectApiName; + this.dispatchEvent( + new CustomEvent("navigate", { + detail: { + recordId, + objectApiName, + }, + }) + ); + this.close("success"); + } +} diff --git a/force-app/main/default/lwc/serviceScheduleCreatorModal/serviceScheduleCreatorModal.js-meta.xml b/force-app/main/default/lwc/serviceScheduleCreatorModal/serviceScheduleCreatorModal.js-meta.xml new file mode 100644 index 000000000..eac275d08 --- /dev/null +++ b/force-app/main/default/lwc/serviceScheduleCreatorModal/serviceScheduleCreatorModal.js-meta.xml @@ -0,0 +1,5 @@ + + + 55.0 + false + \ No newline at end of file diff --git a/force-app/main/default/lwc/serviceScheduleCreatorWrapper/__tests__/serviceScheduleCreatorWrapper.test.js b/force-app/main/default/lwc/serviceScheduleCreatorWrapper/__tests__/serviceScheduleCreatorWrapper.test.js index 6d5ea3df7..e5f96a425 100644 --- a/force-app/main/default/lwc/serviceScheduleCreatorWrapper/__tests__/serviceScheduleCreatorWrapper.test.js +++ b/force-app/main/default/lwc/serviceScheduleCreatorWrapper/__tests__/serviceScheduleCreatorWrapper.test.js @@ -21,22 +21,6 @@ describe("c-service-schedule-creator", () => { }); }); - it("modal appears in non-experience-cloud context and element is accessible", () => { - element.isCommunity = false; - document.body.appendChild(element); - - return global.flushPromises().then(async () => { - const modal = element.shadowRoot.querySelector("c-modal"); - - // Modal will only display with a spinner loaded - expect(modal).not.toBeNull(); - modal.dispatchEvent(new CustomEvent("dialogclose")); - - // TODO: Validate accessibility when each step is loads. - global.isAccessible(element); - }); - }); - it("modal does not appear in experience-cloud context and element is accessible", () => { element.isCommunity = true; document.body.appendChild(element); diff --git a/force-app/main/default/lwc/serviceScheduleCreatorWrapper/serviceScheduleCreatorWrapper.html b/force-app/main/default/lwc/serviceScheduleCreatorWrapper/serviceScheduleCreatorWrapper.html index 9da97955c..b6b40c2b4 100644 --- a/force-app/main/default/lwc/serviceScheduleCreatorWrapper/serviceScheduleCreatorWrapper.html +++ b/force-app/main/default/lwc/serviceScheduleCreatorWrapper/serviceScheduleCreatorWrapper.html @@ -8,20 +8,6 @@ --> - - - - { + this.navigateToRecord(event.detail.recordId, event.detail.objectApiName); + }, + onclose: e => { + e.stopPropagation(); + }, + }); + // if modal closed with X button, promise returns result = 'undefined' + if (result === undefined) { + this.navigateToList(); + } + } + + navigateToList() { + this[NavigationMixin.Navigate]({ + type: "standard__objectPage", + attributes: { + objectApiName: SERVICE_SCHEDULE.objectApiName, + actionName: "list", + }, + state: { + filterName: "Recent", + }, + }); + } - handleClose() { - this.template.querySelector("c-service-schedule-creator").handleClose(); + navigateToRecord(recordId, objectApiName) { + this[NavigationMixin.Navigate]({ + type: "standard__recordPage", + attributes: { + recordId: recordId, + objectApiName: objectApiName, + actionName: "view", + }, + }); } } 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 && diff --git a/force-app/main/default/objectTranslations/ServiceSchedule__c-de/ServiceScheduleEnds__c.fieldTranslation-meta.xml b/force-app/main/default/objectTranslations/ServiceSchedule__c-de/ServiceScheduleEnds__c.fieldTranslation-meta.xml index 0ea9e9d7e..a1d83458c 100644 --- a/force-app/main/default/objectTranslations/ServiceSchedule__c-de/ServiceScheduleEnds__c.fieldTranslation-meta.xml +++ b/force-app/main/default/objectTranslations/ServiceSchedule__c-de/ServiceScheduleEnds__c.fieldTranslation-meta.xml @@ -4,7 +4,7 @@ Ende des Leistungszeitplans On - Ein + Am After diff --git a/force-app/main/default/translations/de.translation-meta.xml b/force-app/main/default/translations/de.translation-meta.xml index 16ebb8793..589a8b1ae 100644 --- a/force-app/main/default/translations/de.translation-meta.xml +++ b/force-app/main/default/translations/de.translation-meta.xml @@ -234,7 +234,11 @@ Filter_by_Record - Nach {0} filtern + Filtern nach: {0} + + + Filter_by_Stage + Nach Phase filtern Finish @@ -612,6 +616,18 @@ Action Aktion + + Feature_Only_Webapp + Dieses Feature ist nur in der Webanwendung verfügbar. + + + Too_Many_Participants + Die Ergebnisse haben mehr als 1.000 Teilnehmer zurückgegeben. Schränken Sie die Ergebnisse ein, indem Sie nach der Programmuntergruppe suchen oder filtern. + + + Service_Schedule_Wizard + Leistungszeitplan-Assistent + Contacts_with_and_without_Program_Engagements Kontakte mit und ohne Programmteilnahmen diff --git a/force-app/main/default/translations/en_GB.translation-meta.xml b/force-app/main/default/translations/en_GB.translation-meta.xml index 2aedc0e44..49ef1b599 100644 --- a/force-app/main/default/translations/en_GB.translation-meta.xml +++ b/force-app/main/default/translations/en_GB.translation-meta.xml @@ -234,7 +234,11 @@ Filter_by_Record - Filter by {0} + Filter by: {0} + + + Filter_by_Stage + Filter by Stage Finish @@ -612,6 +616,18 @@ Action Action + + Feature_Only_Webapp + This feature is only available on the web application. + + + Too_Many_Participants + The results have returned more than a 1000 participants. Refine the results by searching or filtering by Program Cohort. + + + Service_Schedule_Wizard + Service Schedule Wizard + Contacts_with_and_without_Program_Engagements Contacts with and without Programme Engagements diff --git a/force-app/main/default/translations/es.translation-meta.xml b/force-app/main/default/translations/es.translation-meta.xml index 06854666e..13f1bdb2e 100644 --- a/force-app/main/default/translations/es.translation-meta.xml +++ b/force-app/main/default/translations/es.translation-meta.xml @@ -234,7 +234,11 @@ Filter_by_Record - Filtrar por {0} + Filtrar por: {0} + + + Filter_by_Stage + Filtrar por etapa Finish @@ -612,6 +616,18 @@ Action Acción + + Feature_Only_Webapp + Esta función está disponible únicamente en la aplicación web. + + + Too_Many_Participants + Los resultados han devuelto más de 1000 participantes. Ajuste los resultados buscando o filtrando por Cohorte de programa. + + + Service_Schedule_Wizard + Asistente de Programación de servicio + Contacts_with_and_without_Program_Engagements Contactos con y sin participaciones en programa diff --git a/force-app/main/default/translations/es_MX.translation-meta.xml b/force-app/main/default/translations/es_MX.translation-meta.xml index 1da82c375..1cd60eb70 100644 --- a/force-app/main/default/translations/es_MX.translation-meta.xml +++ b/force-app/main/default/translations/es_MX.translation-meta.xml @@ -234,7 +234,11 @@ Filter_by_Record - Filtrar por {0} + Filtrar por: {0} + + + Filter_by_Stage + Filtrar por etapa Finish @@ -612,6 +616,18 @@ Action Acción + + Feature_Only_Webapp + Esta función está disponible únicamente en la aplicación web. + + + Too_Many_Participants + Los resultados devolvieron más de 1000 participantes. Ajuste los resultados buscando o filtrando por Cohorte de programa. + + + Service_Schedule_Wizard + Asistente de Programación de servicio + Contacts_with_and_without_Program_Engagements Contactos con y sin participaciones en programa diff --git a/force-app/main/default/translations/fr.translation-meta.xml b/force-app/main/default/translations/fr.translation-meta.xml index d8cf2b9e8..18337117c 100644 --- a/force-app/main/default/translations/fr.translation-meta.xml +++ b/force-app/main/default/translations/fr.translation-meta.xml @@ -234,7 +234,11 @@ Filter_by_Record - Filtrer par {0} + Filtrer par : {0} + + + Filter_by_Stage + Filtrer par étape Finish @@ -612,6 +616,18 @@ Action Action + + Feature_Only_Webapp + Cette fonctionnalité n’est disponible que sur l’application Web. + + + Too_Many_Participants + Les résultats ont renvoyé plus de 1 000 participants. Affinez les résultats en recherchant ou en filtrant par cohorte de programmes. + + + Service_Schedule_Wizard + Assistant du calendrier de service + Contacts_with_and_without_Program_Engagements Contacts avec et sans engagements dans le programme diff --git a/force-app/main/default/translations/fr_CA.translation-meta.xml b/force-app/main/default/translations/fr_CA.translation-meta.xml index d8cf2b9e8..18337117c 100644 --- a/force-app/main/default/translations/fr_CA.translation-meta.xml +++ b/force-app/main/default/translations/fr_CA.translation-meta.xml @@ -234,7 +234,11 @@ Filter_by_Record - Filtrer par {0} + Filtrer par : {0} + + + Filter_by_Stage + Filtrer par étape Finish @@ -612,6 +616,18 @@ Action Action + + Feature_Only_Webapp + Cette fonctionnalité n’est disponible que sur l’application Web. + + + Too_Many_Participants + Les résultats ont renvoyé plus de 1 000 participants. Affinez les résultats en recherchant ou en filtrant par cohorte de programmes. + + + Service_Schedule_Wizard + Assistant du calendrier de service + Contacts_with_and_without_Program_Engagements Contacts avec et sans engagements dans le programme diff --git a/force-app/main/default/translations/ja.translation-meta.xml b/force-app/main/default/translations/ja.translation-meta.xml index ed45c9550..3b5fe5e30 100644 --- a/force-app/main/default/translations/ja.translation-meta.xml +++ b/force-app/main/default/translations/ja.translation-meta.xml @@ -234,7 +234,11 @@ Filter_by_Record - {0} で絞り込み + 検索条件: {0} + + + Filter_by_Stage + フェーズによる絞り込み Finish @@ -612,6 +616,18 @@ Action アクション + + Feature_Only_Webapp + この機能は、Web アプリケーションでのみ使用できます。 + + + Too_Many_Participants + 結果に 1000 人を超える参加者が返されました。プログラムコホートで検索または絞り込んで、結果を絞り込んでください。 + + + Service_Schedule_Wizard + サービススケジュールウィザード + Contacts_with_and_without_Program_Engagements プログラムエンゲージメントがある取引先責任者とない取引先責任者 diff --git a/force-app/main/default/translations/nl_NL.translation-meta.xml b/force-app/main/default/translations/nl_NL.translation-meta.xml index df714aa84..f34a69618 100644 --- a/force-app/main/default/translations/nl_NL.translation-meta.xml +++ b/force-app/main/default/translations/nl_NL.translation-meta.xml @@ -234,7 +234,11 @@ Filter_by_Record - Filteren op {0} + Filteren op: {0} + + + Filter_by_Stage + Filteren op fase Finish @@ -612,6 +616,18 @@ Action Actie + + Feature_Only_Webapp + Deze voorziening is uitsluitend beschikbaar in de webtoepassing. + + + Too_Many_Participants + De resultaten hebben meer dan 1000 deelnemers geretourneerd. Verfijn de resultaten door te zoeken of filteren op programmacohort. + + + Service_Schedule_Wizard + Wizard voor serviceplanning + Contacts_with_and_without_Program_Engagements Contactpersonen met en zonder programmabetrokkenheden diff --git a/force-app/main/default/translations/pt_BR.translation-meta.xml b/force-app/main/default/translations/pt_BR.translation-meta.xml index 8c6380900..71a6d95de 100644 --- a/force-app/main/default/translations/pt_BR.translation-meta.xml +++ b/force-app/main/default/translations/pt_BR.translation-meta.xml @@ -234,7 +234,11 @@ Filter_by_Record - Filtrar por {0} + Filtrar por: {0} + + + Filter_by_Stage + Filtrar por estágio Finish @@ -612,6 +616,18 @@ Action Ação + + Feature_Only_Webapp + Esse recurso só está disponível no aplicativo Web. + + + Too_Many_Participants + Os resultados retornaram mais de 1.000 participantes. Refine os resultados pesquisando ou filtrando por Coorte do programa. + + + Service_Schedule_Wizard + Assistente do cronograma de serviços + Contacts_with_and_without_Program_Engagements Contatos com e sem Engajamentos em programas diff --git a/jest.config.js b/jest.config.js index 802c4904d..3e5b68af3 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,6 +1,16 @@ const { jestConfig } = require("@salesforce/sfdx-lwc-jest/config"); module.exports = { ...jestConfig, + moduleNameMapper: { + "^lightning/modalHeader$": + "/force-app/main/default/lwc/__tests__/lightning/modalHeader/modalHeader", + "^lightning/modalBody$": + "/force-app/main/default/lwc/__tests__/lightning/modalBody/modalBody", + "^lightning/modalFooter$": + "/force-app/main/default/lwc/__tests__/lightning/modalFooter/modalFooter", + "^lightning/modal$": + "/force-app/main/default/lwc/__tests__/lightning/modal/modal" + }, testPathIgnorePatterns: ["force-app/main/default/lwc/__tests__/", ".cci"], reporters: ["default", "jest-junit"], setupFilesAfterEnv: ["./jest.setup.js"] diff --git a/metadeploy/labels_de.json b/metadeploy/labels_de.json index 11537e0e1..bbba54d03 100644 --- a/metadeploy/labels_de.json +++ b/metadeploy/labels_de.json @@ -9,7 +9,7 @@ "description": "tagline of product" }, "description": { - "message": "Willkommen beim Installationsprogramm für das Program Management Module (PMM)!\r\n\r\nDas Program Management Module ist ein Standardframework zum Verfolgen, Verwalten und Bereitstellen von Programmen, Services und Leistungen.\r\n\r\n*WICHTIG*: Salesforce empfiehlt nicht, Upgrades direkt in Produktionsorganisationen zu installieren. Installieren Sie das Program Management Module stattdessen zunächst in einer Ihrer kostenlosen Sandbox-Umgebungen oder in einer Entwicklerorganisation.\r\n\r\nDas Program Management Module („PMM“) von salesforce.org ist eine erweiterbare Anwendung mit Open-Source- und Closed-Source-Komponenten, die auf die speziellen Kernanforderungen der Programmverwaltung ausgerichtet ist. PMM nutzt ein von salesforce.com, Inc. („Salesforce“) verwaltetes Paket, um gemeinnützigen Organisationen einen Weg zu bieten, programmatische Informationen zu zentralisieren, um elektronische Programme und Dienstleistungen effektiver verwalten und bereitstellen zu können.\r\n\r\nSehen Sie sich nach der Installation des Programm-Managements die Ressourcen in unserer Dokumentation an:\r\n\r\n* [Dokumentation zum Program Management Module](https://powerofus.force.com/PMM_Documentation)\r\n\r\nFragen? Werden Sie Mitglied der [Program Management Module](https://trailhead.salesforce.com/trailblazer-community/groups/0F94S000000kHjiSAE)-Kundengruppe in der Trailblazer Community.", + "message": "Willkommen beim Installationsprogramm für das Program Management Module (PMM)!\r\n\r\nDas Program Management Module ist ein Standardframework zum Verfolgen, Verwalten und Bereitstellen von Programmen, Services und Leistungen.\r\n\r\n*WICHTIG*: Salesforce empfiehlt nicht, Upgrades direkt in Produktionsorganisationen zu installieren. Installieren Sie das Program Management Module stattdessen zunächst in einer Ihrer kostenlosen Sandbox-Umgebungen oder in einer Entwicklerorganisation.\r\n\r\nDas Program Management Module („PMM“) von salesforce.org ist eine erweiterbare Anwendung mit Open-Source- und Closed-Source-Komponenten, die auf die speziellen Kernanforderungen der Programmverwaltung ausgerichtet ist. PMM nutzt ein von salesforce.com, Inc. („Salesforce“) verwaltetes Paket, um gemeinnützigen Organisationen einen Weg zu bieten, programmatische Informationen zu zentralisieren, um elektronische Programme und Dienstleistungen effektiver verwalten und bereitstellen zu können.", "description": "shown on product detail page (markdown)" }, "error_message": { diff --git a/metadeploy/labels_en-GB.json b/metadeploy/labels_en-GB.json index 82ed9be74..2e41d56d7 100644 --- a/metadeploy/labels_en-GB.json +++ b/metadeploy/labels_en-GB.json @@ -9,7 +9,7 @@ "description": "tagline of product" }, "description": { - "message": "Welcome to the Program Management Module (PMM) Installer!\r\n\r\nProgram Management Module is a standard framework to track, manage and deliver programs, services, and service deliveries.\r\n\r\n*IMPORTANT*: Salesforce does not recommend installing upgrades directly in production organizations. Instead, install Program Management Module in one of your free sandbox environments or a developer organisation first.\r\n\r\nSalesforce.org Program Management Module (“PMM”) is an extensible application with open source and closed source components to address the unique core needs of program management. PMM utilizes a Salesforce.com, Inc. (“Salesforce”) managed package to provide nonprofits with a path to centralise programmatic information in order to more effectively manage and deliver programs and services.\r\n\r\nOnce you've installed Program Management, check out the resources in our documentation:\r\n\r\n* [Program Management Module Documentation](https://powerofus.force.com/PMM_Documentation)\r\n\r\nQuestions? Become a member of the [Program Management Module](https://trailhead.salesforce.com/trailblazer-community/groups/0F94S000000kHjiSAE) customer group in the Trailblazer Community.", + "message": "Welcome to the Program Management Module (PMM) Installer!\r\n\r\nProgram Management Module is a standard framework to track, manage and deliver programs, services, and service deliveries.\r\n\r\n*IMPORTANT*: Salesforce does not recommend installing upgrades directly in production organizations. Instead, install Program Management Module in one of your free sandbox environments or a developer organization first.\r\n\r\nSalesforce.org Program Management Module (“PMM”) is an extensible application with open source and closed source components to address the unique core needs of program management. PMM utilizes a Salesforce.com, Inc. (“Salesforce”) managed package to provide nonprofits with a path to centralize programmatic information in order to more effectively manage and deliver programs and services.", "description": "shown on product detail page (markdown)" }, "error_message": { diff --git a/metadeploy/labels_es-MX.json b/metadeploy/labels_es-MX.json index 7a2cb557f..c7dd6e095 100644 --- a/metadeploy/labels_es-MX.json +++ b/metadeploy/labels_es-MX.json @@ -9,7 +9,7 @@ "description": "tagline of product" }, "description": { - "message": "¡Le damos la bienvenida al Instalador de Program Management Module (PMM)!\r\n\r\nProgram Management Module es un marco de trabajo estándar para el seguimiento, la gestión y la entrega de programas, servicios y entregas de servicio.\r\n\r\n*IMPORTANTE*: Salesforce no recomienda instalar actualizaciones directamente en organizaciones de producción. En su lugar, instale Program Management Module en uno de sus entornos sandbox u organizaciones de desarrollador gratuitos primero.\r\n\r\nProgram Management Module («PMM») de Salesforce.org es una aplicación ampliable con componentes de código abierto y cerrado para abordar las necesidades básicas específicas de la gestión de programas. PMM utiliza un paquete gestionado de salesforce.com, Inc. (en adelante, “Salesforce”) para proporcionar a las organizaciones sin fines de lucro una vía destinada a centralizar la información sobre programas con el fin de gestionar y ofrecer programas y servicios electrónicos de manera más eficaz.\r\n\r\nUna vez instalado Program Management Module, consulte los recursos en nuestra documentación.\r\n\r\n* [Documentación de Program Management Module](https://powerofus.force.com/PMM_Documentation)\r\n\r\n¿Preguntas? Conviértase en miembro del grupo de clientes [Program Management Module](https://trailhead.salesforce.com/trailblazer-community/groups/0F94S000000kHjiSAE) en Trailblazer Community.", + "message": "¡Le damos la bienvenida al Instalador de Program Management Module (PMM)!\r\n\r\nProgram Management Module es un marco de trabajo estándar para el seguimiento, la gestión y la entrega de programas, servicios y entregas de servicio.\r\n\r\n*IMPORTANTE*: Salesforce no recomienda instalar actualizaciones directamente en organizaciones de producción. En su lugar, instale Program Management Module en uno de sus entornos sandbox u organizaciones de desarrollador gratuitos primero.\r\n\r\nProgram Management Module («PMM») de Salesforce.org es una aplicación ampliable con componentes de código abierto y cerrado para abordar las necesidades básicas específicas de la gestión de programas. PMM utiliza un paquete gestionado de salesforce.com, Inc. (en adelante, “Salesforce”) para proporcionar a las organizaciones sin fines de lucro una vía destinada a centralizar la información sobre programas con el fin de gestionar y ofrecer programas y servicios electrónicos de manera más eficaz.", "description": "shown on product detail page (markdown)" }, "error_message": { diff --git a/metadeploy/labels_es.json b/metadeploy/labels_es.json index 420d23856..f8b90d6fc 100644 --- a/metadeploy/labels_es.json +++ b/metadeploy/labels_es.json @@ -9,7 +9,7 @@ "description": "tagline of product" }, "description": { - "message": "¡Le damos la bienvenida al Instalador de Program Management Module (PMM)!\r\n\r\nProgram Management Module es un marco de trabajo estándar para el seguimiento, la gestión y la entrega de programas, servicios y entregas de servicio.\r\n\r\n*IMPORTANTE*: Salesforce no recomienda instalar actualizaciones directamente en organizaciones de producción. En su lugar, instale Program Management Module en uno de sus entornos sandbox u organizaciones de desarrollador gratuitos primero.\r\n\r\nProgram Management Module («PMM») de Salesforce.org es una aplicación ampliable con componentes de código abierto y cerrado para abordar las necesidades básicas específicas de la gestión de programas. PMM utiliza un paquete gestionado de salesforce.com, Inc. (en adelante, «Salesforce») para proporcionar a las organizaciones sin ánimo de lucro una vía destinada a centralizar la información sobre programas con el fin de gestionar y ofrecer programas y servicios electrónicos de manera más eficaz.\r\n\r\nUna vez instalado Program Management Module, consulte los recursos en nuestra documentación.\r\n\r\n* [Documentación de Program Management Module](https://powerofus.force.com/PMM_Documentation)\r\n\r\n¿Preguntas? Conviértase en miembro del grupo de clientes [Program Management Module](https://trailhead.salesforce.com/trailblazer-community/groups/0F94S000000kHjiSAE) en Trailblazer Community.", + "message": "¡Le damos la bienvenida al Instalador de Program Management Module (PMM)!\r\n\r\nProgram Management Module es un marco de trabajo estándar para el seguimiento, la gestión y la entrega de programas, servicios y entregas de servicio.\r\n\r\n*IMPORTANTE*: Salesforce no recomienda instalar actualizaciones directamente en organizaciones de producción. En su lugar, instale Program Management Module en uno de sus entornos sandbox u organizaciones de desarrollador gratuitos primero.\r\n\r\nProgram Management Module (“PMM”) de Salesforce.org es una aplicación ampliable con componentes de código abierto y cerrado para abordar las necesidades básicas específicas de la gestión de programas. PMM utiliza un paquete gestionado de salesforce.com, Inc. (en adelante, “Salesforce”) para proporcionar a las organizaciones sin ánimo de lucro una vía destinada a centralizar la información sobre programas con el fin de gestionar y ofrecer programas y servicios electrónicos de manera más eficaz.", "description": "shown on product detail page (markdown)" }, "error_message": { diff --git a/metadeploy/labels_fr-CA.json b/metadeploy/labels_fr-CA.json index c73cbcc02..af1d3c31c 100644 --- a/metadeploy/labels_fr-CA.json +++ b/metadeploy/labels_fr-CA.json @@ -9,7 +9,7 @@ "description": "tagline of product" }, "description": { - "message": "Bienvenue dans le programme d’installation de Program Management Module (PMM) !\r\n\r\nProgram Management Module est un cadre standard pour assurer le suivi, la gestion et la livraison de programmes, de services et de livraison de services.\r\n\r\n*IMPORTANT* : Salesforce recommande de ne pas installer les mises à niveau directement dans les organisations de production. Au lieu de cela, installez d’abord Program Management Module dans l’un de vos environnements sandbox gratuits ou dans une organisation de développeurs.\r\n\r\nProgram Management Module (« PMM ») de Salesforce.org est une application extensible intégrant des composants open source (libres) et closed source (fermés) qui a vocation à répondre aux besoins fondamentaux uniques liés à la gestion de programmes. Le PMM utilise un progiciel géré par Salesforce.com, Inc. (« Salesforce ») pour doter les organisations à but non lucratif d’un outil de centralisation des informations programmatiques, ce qui leur permet ainsi de gérer et de mettre en œuvre les programmes et les services plus efficacement.\r\n\r\nAprès avoir installé Program Management Module, consultez les ressources dans notre documentation :\r\n\r\n* [Documentation sur Program Management Module](https://powerofus.force.com/PMM_Documentation)\r\n\r\nVous avez des questions ? Devenez membre du groupe de clients [Program Management Module](https://trailhead.salesforce.com/trailblazer-community/groups/0F94S000000kHjiSAE) dans la Trailblazer Community.", + "message": "Bienvenue dans le programme d’installation de Program Management Module (PMM) !\r\n\r\nProgram Management Module est un cadre standard pour assurer le suivi, la gestion et la livraison de programmes, de services et de livraison de services.\r\n\r\n*IMPORTANT* : Salesforce recommande de ne pas installer les mises à niveau directement dans les organisations de production. Au lieu de cela, installez d’abord Program Management Module dans l’un de vos environnements sandbox gratuits ou dans une organisation de développeurs.\r\n\r\nProgram Management Module (« PMM ») de Salesforce.org est une application extensible intégrant des composants open source (libres) et closed source (fermés) qui a vocation à répondre aux besoins fondamentaux uniques liés à la gestion de programmes. PMM utilise un progiciel géré par Salesforce.com, Inc. (« Salesforce ») pour doter les organisations à but non lucratif d’un outil de centralisation des informations programmatiques, ce qui leur permet ainsi de gérer et de mettre en œuvre les programmes et les services plus efficacement.", "description": "shown on product detail page (markdown)" }, "error_message": { diff --git a/metadeploy/labels_fr.json b/metadeploy/labels_fr.json index c73cbcc02..af1d3c31c 100644 --- a/metadeploy/labels_fr.json +++ b/metadeploy/labels_fr.json @@ -9,7 +9,7 @@ "description": "tagline of product" }, "description": { - "message": "Bienvenue dans le programme d’installation de Program Management Module (PMM) !\r\n\r\nProgram Management Module est un cadre standard pour assurer le suivi, la gestion et la livraison de programmes, de services et de livraison de services.\r\n\r\n*IMPORTANT* : Salesforce recommande de ne pas installer les mises à niveau directement dans les organisations de production. Au lieu de cela, installez d’abord Program Management Module dans l’un de vos environnements sandbox gratuits ou dans une organisation de développeurs.\r\n\r\nProgram Management Module (« PMM ») de Salesforce.org est une application extensible intégrant des composants open source (libres) et closed source (fermés) qui a vocation à répondre aux besoins fondamentaux uniques liés à la gestion de programmes. Le PMM utilise un progiciel géré par Salesforce.com, Inc. (« Salesforce ») pour doter les organisations à but non lucratif d’un outil de centralisation des informations programmatiques, ce qui leur permet ainsi de gérer et de mettre en œuvre les programmes et les services plus efficacement.\r\n\r\nAprès avoir installé Program Management Module, consultez les ressources dans notre documentation :\r\n\r\n* [Documentation sur Program Management Module](https://powerofus.force.com/PMM_Documentation)\r\n\r\nVous avez des questions ? Devenez membre du groupe de clients [Program Management Module](https://trailhead.salesforce.com/trailblazer-community/groups/0F94S000000kHjiSAE) dans la Trailblazer Community.", + "message": "Bienvenue dans le programme d’installation de Program Management Module (PMM) !\r\n\r\nProgram Management Module est un cadre standard pour assurer le suivi, la gestion et la livraison de programmes, de services et de livraison de services.\r\n\r\n*IMPORTANT* : Salesforce recommande de ne pas installer les mises à niveau directement dans les organisations de production. Au lieu de cela, installez d’abord Program Management Module dans l’un de vos environnements sandbox gratuits ou dans une organisation de développeurs.\r\n\r\nProgram Management Module (« PMM ») de Salesforce.org est une application extensible intégrant des composants open source (libres) et closed source (fermés) qui a vocation à répondre aux besoins fondamentaux uniques liés à la gestion de programmes. PMM utilise un progiciel géré par Salesforce.com, Inc. (« Salesforce ») pour doter les organisations à but non lucratif d’un outil de centralisation des informations programmatiques, ce qui leur permet ainsi de gérer et de mettre en œuvre les programmes et les services plus efficacement.", "description": "shown on product detail page (markdown)" }, "error_message": { diff --git a/metadeploy/labels_ja.json b/metadeploy/labels_ja.json index 1f27dcc03..bcf9f7544 100644 --- a/metadeploy/labels_ja.json +++ b/metadeploy/labels_ja.json @@ -9,7 +9,7 @@ "description": "tagline of product" }, "description": { - "message": "Program Management Module (PMM) インストーラをご利用いただきありがとうございます。\r\n\r\nProgram Management Module はプログラム、サービス、サービス提供を追跡、管理、提供するための標準プラットフォームです。\r\n\r\n*重要*: Salesforce は、本番組織にアップグレードを直接インストールすることを推奨しません。まず、Program Management Module を無料の Sandbox 環境または Developer Edition 組織にインストールしてください。\r\n\r\nSalesforce.org Program Management Module (以下「PMM」といいます) は、プログラム管理に特有の核心的ニーズに対処するためのオープンソース及びクローズドソースコンポーネントから成る拡張可能なアプリケーションです。PMMは、プログラム情報を一元化して、プログラム及びサービスをより効果的に管理・提供するための方策を非営利団体に提供するために、salesforce.com, Inc. (以下「Salesforce」といいます) の管理パッケージを利用しています。\r\n\r\nプログラム管理のインストールが完了したら、各種資料を確認してください。\r\n\r\n* [Program Management Module ドキュメント](https://powerofus.force.com/PMM_Documentation)\r\n\r\nご質問がある場合は、Trailblazer Community の[「Program Management Module」](https://trailhead.salesforce.com/trailblazer-community/groups/0F94S000000kHjiSAE)カスタマーグループにご参加ください。", + "message": "Program Management Module (PMM) インストーラをご利用いただきありがとうございます。\r\n\r\nProgram Management Module はプログラム、サービス、サービス提供を追跡、管理、提供するための標準フレームワークです。\r\n\r\n*重要*: Salesforce は、本番組織にアップグレードを直接インストールすることを推奨しません。まず、Program Management Module を無料の Sandbox 環境または Developer Edition 組織にインストールしてください。\r\n\r\nSalesforce.org Program Management Module (以下「PMM」といいます) は、プログラム管理に特有の核心的ニーズに対処するためのオープンソース及びクローズドソースコンポーネントから成る拡張可能なアプリケーションです。PMM は、プログラム情報を一元化して、プログラム及びサービスをより効果的に管理・提供するための方策を非営利団体に提供するために、Salesforce.com, Inc. (以下「Salesforce」といいます) の管理パッケージを利用しています。", "description": "shown on product detail page (markdown)" }, "error_message": { diff --git a/metadeploy/labels_nl.json b/metadeploy/labels_nl.json index 0a3748bc1..4038c7539 100644 --- a/metadeploy/labels_nl.json +++ b/metadeploy/labels_nl.json @@ -9,7 +9,7 @@ "description": "tagline of product" }, "description": { - "message": "Welkom bij de installer van de Program Management Module (PMM)!\r\n\r\nProgram Management Module is een standaard framework voor het bijhouden, beheren en leveren van programma's, diensten en bestellingen.\r\n\r\n*BELANGRIJK*: Salesforce raadt niet aan, de upgrades direct in productieorganisaties te installeren. Installeer de Program Management Module in plaats hiervan eerst in een van uw vrije sandboxomgevingen of in een ontwikkelorganisatie.\r\n\r\nSalesforce.org Program Management Module ('PMM') is een uitbreidbare toepassing met open source- en closed source-onderdelen voor de unieke kernbehoeften van programmabeheer. PMM maakt gebruik van een door salesforce.com, Inc. ('Salesforce') beheerd pakket om organisaties zonder winstbejag te voorzien van een manier om programma-informatie te centraliseren en programma's en diensten efficiënter te beheren en leveren.\r\n\r\nKijk na het installeren van Programmabeheer bij de resources in onze documentatie:\r\n\r\n* [Documentatie Program Management Module](https://powerofus.force.com/PMM_Documentation)\r\n\r\nVragen? Word lid van de klantengroep [Program Management Module](https://trailhead.salesforce.com/trailblazer-community/groups/0F94S000000kHjiSAE) in de Trailblazer Community.", + "message": "Welkom bij de installer van de Program Management Module (PMM)!\r\n\r\nProgram Management Module is een standaard framework voor het bijhouden, beheren en leveren van programma's, diensten en bestellingen.\r\n\r\n*BELANGRIJK*: Salesforce raadt niet aan, de upgrades direct in productieorganisaties te installeren. Installeer de Program Management Module in plaats hiervan eerst in een van uw vrije sandboxomgevingen of in een ontwikkelorganisatie.\r\n\r\nSalesforce.org Program Management Module ('PMM') is een uitbreidbare toepassing met open source- en closed source-onderdelen voor de unieke kernbehoeften van programmabeheer. PMM maakt gebruik van een door salesforce.com, Inc. ('Salesforce') beheerd pakket om organisaties zonder winstbejag te voorzien van een manier om programma-informatie te centraliseren en programma's en diensten efficiënter te beheren en leveren.", "description": "shown on product detail page (markdown)" }, "error_message": { diff --git a/metadeploy/labels_pt-BR.json b/metadeploy/labels_pt-BR.json index c7b297ea0..927ef8e98 100644 --- a/metadeploy/labels_pt-BR.json +++ b/metadeploy/labels_pt-BR.json @@ -9,7 +9,7 @@ "description": "tagline of product" }, "description": { - "message": "Bem-vindo ao Instalador do Módulo de gerenciamento de programas (PMM)!\r\n\r\nO Módulo de gerenciamento de programas é uma estrutura padrão para acompanhar, gerenciar e entregar programas, serviços e prestações de serviço.\r\n\r\n*IMPORTANTE*: a Salesforce não recomenda a instalação de atualizações diretamente em organizações de produção. Em vez disso, instale o Módulo de gerenciamento de programas primeiro em um de seus ambientes gratuitos de sandbox ou em uma organização de desenvolvedores.\r\n\r\nO Módulo de gerenciamento de programas (\"PMM\") da Salesforce.org é um aplicativo extensível com componentes de código aberto e de código fechado, para atender às necessidades principais exclusivas do gerenciamento de programas. O PMM utiliza um pacote gerenciado da Salesforce.com, Inc. (\"Salesforce\") para fornecer às organizações sem fins lucrativos um caminho que centraliza informações programáticas, a fim de gerenciar e entregar programas e serviços de forma mais eficaz.\r\n\r\nDepois de instalar o gerenciamento de programas, confira os recursos em nossa documentação:\r\n\r\n* [Documentação do Módulo de gerenciamento de programas](https://powerofus.force.com/PMM_Documentation)\r\n\r\nDúvidas? Torne-se membro do grupo de clientes [Program Management Module](https://trailhead.salesforce.com/trailblazer-community/groups/0F94S00000kHjiSAE) na Comunidade Trailblazer.", + "message": "Bem-vindo ao Instalador do Módulo de gerenciamento de programas (PMM)!\r\n\r\nO Módulo de gerenciamento de programas é uma estrutura padrão para acompanhar, gerenciar e entregar programas, serviços e prestações de serviço.\r\n\r\n*IMPORTANTE*: a Salesforce não recomenda a instalação de atualizações diretamente em organizações de produção. Em vez disso, instale o Módulo de gerenciamento de programas primeiro em um de seus ambientes gratuitos de sandbox ou em uma organização de desenvolvedores.\r\n\r\nO Módulo de gerenciamento de programas (\"PMM\") da Salesforce.org é um aplicativo extensível com componentes de código aberto e de código fechado, para atender às necessidades principais exclusivas do gerenciamento de programas. O PMM utiliza um pacote gerenciado da Salesforce.com, Inc. (\"Salesforce\") para fornecer às organizações sem fins lucrativos um caminho que centraliza informações programáticas, a fim de gerenciar e entregar programas e serviços de forma mais eficaz.", "description": "shown on product detail page (markdown)" }, "error_message": { diff --git a/robot/pmm/resources/ServiceSchedulePageObject.py b/robot/pmm/resources/ServiceSchedulePageObject.py index ef781ad62..4fdc321fb 100644 --- a/robot/pmm/resources/ServiceSchedulePageObject.py +++ b/robot/pmm/resources/ServiceSchedulePageObject.py @@ -167,6 +167,20 @@ def filter_participants(self, filter_value): self.selenium.get_webelement(locator).send_keys(filter_value) self.selenium.wait_until_page_contains(filter_value) + def service_schedule_ends(self, option, value): + """Selects service schedule end date on wizard""" + locator = pmm_lex_locators["service_schedule"]["wizard_radio_button"].format( + option + ) + self.selenium.set_focus_to_element(locator) + self.selenium.get_webelement(locator).click() + if option == "After": + self.salesforce.populate_field("Number of Service Sessions", value) + elif option == "On": + self.pmm.select_date_from_datepicker("Service Schedule End Date", value) + else: + raise Exception("Enter valid Service Schedule End option") + @pageobject("Details", "ServiceSchedule__c") class ServiceScheduleDetailPage(BasePMMPage, DetailPage): diff --git a/robot/pmm/resources/locators_52.py b/robot/pmm/resources/locators_52.py index e2efd07b1..18e53111a 100644 --- a/robot/pmm/resources/locators_52.py +++ b/robot/pmm/resources/locators_52.py @@ -10,6 +10,14 @@ "text": "//*[contains(text(), '{}')]", "page_title": "//h2[contains(@class,'slds-text-heading') or contains(@class,'truncate title')]", "placeholder": "//*[contains(@placeholder,'{}')]", + "frame": "//iframe[contains(@id, '{}') or contains(@title, '{}') or contains(@name, '{}')]", + "frame_link": "//li[@data-node-id='CustomMetadata']//div[contains(@title, '{}')]//a", + "custom_metadata_button": "//div[@class='pbHeader']//div[@class='listHeader']//span//input[@title='{}']", + "custom_metadata_button_title": "//div[@class='pbHeader']//td/input[@title='{}']", + "custom_metadata_field": "//tr[.//td[contains(@class,'labelCol')]//label[text()='{}']]//td[contains(@class,'dataCol')]//input", + "custom_metadata_value": "//tr[.//td[contains(@class,'labelCol')]//label[text()='{}']]//td[contains(@class,'dataCol')]/div[@class='requiredInput']//input", + "custom_metadata_lookup_field": "//tr[.//td[contains(@class,'labelCol')]//label[text()='{}']]//td[@class='dataCol col02']//span//input", + "custom_metadata_link": "//div[@class='pbBody']//table//tbody//tr//td[@class='actionColumn']//a[contains(@title,'Bucketed Value')]", "placeholder_lookup": { "lookup1": "//div[@class='slds-lookup__result-text' and contains(text(), '{}')]", "lookup2": "//mark[text() = '{}']/ancestor::a", @@ -110,6 +118,7 @@ "first_session_end": "//lightning-input[contains(@class,'slds-form-element')][./descendant::*[text()='{}']]/descendant::div//input[@class='slds-input']", "remove_session": "//tr[@class='slds-hint-parent'][./th/descendant::*[text()='{}']]//lightning-primitive-cell-button/descendant::lightning-primitive-icon", "filter": "//div[contains(@class,'slds-form-element__control')]/input[@placeholder='Search this list']", + "wizard_radio_button": "//div[@class='slds-form-element__control']/span[@class='slds-radio']//span[@class='slds-radio_faux'][./following-sibling::span[@class='slds-form-element__label' and text()='{}']]", }, "attendance": { "attendance_text": "//lightning-layout[.//div[contains(@id,'Contact') and text()='{}']]//lightning-input[@class='slds-form-element'][.//label[text()='{}']]//div/input", diff --git a/robot/pmm/resources/pmm.py b/robot/pmm/resources/pmm.py index 14445cbbc..b00f991a5 100644 --- a/robot/pmm/resources/pmm.py +++ b/robot/pmm/resources/pmm.py @@ -348,6 +348,7 @@ def click_dialog_button(self, label): self.selenium.wait_until_element_is_enabled( locator, error="Button is not enabled" ) + self.scroll_element_into_view(locator) self.selenium.set_focus_to_element(locator) element = self.selenium.driver.find_element_by_xpath(locator) self.selenium.driver.execute_script("arguments[0].click()", element) @@ -478,3 +479,107 @@ def select_app_from_all_items(self, app_name): ) search_input_box.send_keys(app_name) self.selenium.click_element(locator_app_name) + + def format_all(self, loc, value): + """Formats the given locator with the value for all {} occurrences""" + count = loc.count("{") + + if count == 1: + return loc.format(value) + elif count == 2: + return loc.format(value, value) + elif count == 3: + return loc.format(value, value, value) + + def populate_setup_search_box(self, loc, value): + """Populates placeholder element with a value + Finds the placeholder element, inputs value + and waits for the suggestion and clicks on it + """ + xpath_lookup = pmm_lex_locators["placeholder"].format(loc) + field = self.selenium.get_webelement(xpath_lookup) + field.click() + field.send_keys(value) + time.sleep(2) + field.send_keys(Keys.ENTER) + xpath_value = pmm_lex_locators["frame_link"].format(value) + element_menu = self.selenium.driver.find_element_by_xpath(xpath_value) + self.salesforce._jsclick(element_menu) + self.selenium.wait_until_location_contains( + "/lightning/setup/CustomMetadata/home" + ) + + def select_frame_with_value(self, value): + """Selects frame identified by the given value + value should be the 'id', 'title' or 'name' attribute value of the web element used to identify the frame + """ + locator = pmm_lex_locators["frame"] + locator = self.format_all(locator, value) + self.selenium.select_frame(locator) + + def manage_records_bucketed_value(self): + """click on Manage Records for Bucketed Value""" + self.select_frame_with_value( + "All Custom Metadata Types ~ Salesforce - Developer Edition" + ) + locator = pmm_lex_locators["custom_metadata_link"] + self.selenium.wait_until_page_contains_element(locator) + self.selenium.click_element(locator) + + def click_custom_metadata_button(self, button): + """Clicks on a button within the custom metadata iFrame""" + time.sleep(3) + if button == "New": + locator = pmm_lex_locators["custom_metadata_button"].format(button) + self.select_frame_with_value( + "Bucketed Values ~ Salesforce - Developer Edition" + ) + self.selenium.wait_until_page_contains_element(locator) + self.selenium.click_element(locator) + else: + locator = pmm_lex_locators["custom_metadata_button_title"].format(button) + self.selenium.wait_until_page_contains_element(locator) + self.selenium.click_element(locator) + + def populate_bucketed_values(self, **kwargs): + """Populates text field and lookup values within the custom metadata iFrame""" + for key, value in kwargs.items(): + if key == "Bucket": + locator_field = pmm_lex_locators["custom_metadata_lookup_field"].format( + key + ) + elif key == "Label": + locator_field = pmm_lex_locators["custom_metadata_field"].format(key) + self.select_frame_with_value( + "Bucketed Value ~ Salesforce - Developer Edition" + ) + elif key == "Bucketed Value Name" or "Value": + locator_field = pmm_lex_locators["custom_metadata_value"].format(key) + else: + self.builtin.log(f"Element {key} not found") + if self.check_if_element_exists(locator_field): + self.selenium.get_webelement(locator_field).click() + self.selenium.set_focus_to_element(locator_field) + self.selenium.clear_element_text(locator_field) + self.selenium.get_webelement(locator_field).send_keys(value) + else: + raise Exception("Locator not found") + + def verify_dropdown_options(self, dropdown, status, value): + """Verifies if a picklist value is listed in a dropdown""" + locator = pmm_lex_locators["new_record"]["dropdown_field"].format(dropdown) + popup_loc = pmm_lex_locators["new_record"]["dropdown_popup"] + self.scroll_element_into_view(locator) + self.selenium.get_webelement(locator).click() + self.selenium.wait_until_page_contains_element( + popup_loc, error="Dropdown options are not displayed" + ) + if status == "contains": + value_loc = pmm_lex_locators["new_record"]["dropdown_value"].format(value) + actual_value = self.selenium.get_webelement(value_loc).text + assert value in actual_value, f"Dropdown should have contained {value}" + elif status == "does not contain": + self.selenium.get_webelement(locator).click() + self.selenium.page_should_not_contain_element(value) + else: + raise Exception("Valid status not entered") diff --git a/robot/pmm/resources/pmm.robot b/robot/pmm/resources/pmm.robot index d729cdb8a..d041e10a6 100644 --- a/robot/pmm/resources/pmm.robot +++ b/robot/pmm/resources/pmm.robot @@ -163,6 +163,19 @@ API Update Records &{Id} = Get From List ${records} 0 [return] &{Id} +Setup Custom Metadata Bucketed Value + [Arguments] ${Label} ${Bucketed Value Name} ${Bucket} ${Value} + Go To Setup Home + Wait Until Loading Is Complete + Populate Setup Search Box Quick Find Custom Metadata Types + Manage Records Bucketed Value + Click Custom Metadata Button New + Populate Bucketed Values Label=${Label} + ... Bucketed Value Name=${Bucketed Value Name} + ... Bucket=${Bucket} + ... Value=${Value} + Click Custom Metadata Button Save + Capture Screenshot and Delete Records and Close Browser [Documentation] This keyword will capture a screenshot before closing the browser and deleting records when test fails Run Keyword If Any Tests Failed Capture Page Screenshot diff --git a/robot/pmm/tests/browser/Attendance/update_attendance.robot b/robot/pmm/tests/browser/Attendance/update_attendance.robot index 75fe0a6ce..626798cd7 100644 --- a/robot/pmm/tests/browser/Attendance/update_attendance.robot +++ b/robot/pmm/tests/browser/Attendance/update_attendance.robot @@ -48,7 +48,7 @@ Setup Test Data UA1: Update attendance when service session status is Pending [Documentation] This test updates attendance for a service session record with Pending Status [tags] W-8607484 perm:admin perm:manage feature:Attendance - Go To Page Details ServiceSession__c object_id=${service_session1}[Id] + Go To Page Details ServiceSession__c quadrant:Q2 object_id=${service_session1}[Id] Populate Attendance Field ${contact1}[Name] Hours 10 Populate Attendance Dropdown ${contact1}[Name] Attendance Status Present Populate Attendance Field ${contact2}[Name] Hours 10 @@ -66,7 +66,7 @@ UA1: Update attendance when service session status is Pending UA2: Update attendance when service session status is Complete [Documentation] This test updates attendance for a service session record with Complete Status - [tags] W-8611541 perm:admin perm:manage feature:Attendance + [tags] W-8611541 perm:admin perm:manage quadrant:Q2 feature:Attendance Go To Page Details ServiceSession__c object_id=${service_session2}[Id] Click Dialog Button Update Page Should Contain Track Attendance @@ -84,7 +84,7 @@ UA2: Update attendance when service session status is Complete UA3: Validate fields added to AttendanceServiceDeliveries Fieldset [Documentation] This test updates attendance for a service session record with Complete Status - [tags] W-9505038 quadrant:Q2 perm:admin feature:Attendance + [tags] W-9505038 quadrant:Q2 perm:admin quadrant:Q3 feature:Attendance Run task add_fields_to_field_set ... field_set=${ns}ServiceDelivery__c.${ns}Attendance_Service_Deliveries ... fields=${{ ["${ns}Service_Provider__c"] }} diff --git a/robot/pmm/tests/browser/Bulk_Service_Delivery/group_service_delivery.robot b/robot/pmm/tests/browser/Bulk_Service_Delivery/group_service_delivery.robot index 83f0622ad..8fa618554 100644 --- a/robot/pmm/tests/browser/Bulk_Service_Delivery/group_service_delivery.robot +++ b/robot/pmm/tests/browser/Bulk_Service_Delivery/group_service_delivery.robot @@ -50,7 +50,53 @@ Setup Test Data ${service_schedule_name} = Generate New String Set suite variable ${service_schedule_name} + ${program1} = API Create Program + Set suite variable ${program1} + ${service1} = API Create Service ${Program1}[Id] + Set suite variable ${service1} + ${program_cohort1} = API Create Program Cohort ${Program1}[Id] + Set suite variable ${program_cohort1} + ${program_cohort2} = API Create Program Cohort ${Program1}[Id] + Set suite variable ${program_cohort2} + ${program_cohort3} = API Create Program Cohort ${Program1}[Id] + Set suite variable ${program_cohort3} + API Update Records ${ns}ProgramCohort__c ${program_cohort1}[Id] ${ns}Status__c=Completed + ${program_engagement4} = API Create Program Engagement ${Program1}[Id] ${contact1}[Id] ${ns}ProgramCohort__c=${program_cohort1}[Id] + Set suite variable ${program_engagement4} + ${program_engagement5} = API Create Program Engagement ${Program1}[Id] ${contact2}[Id] ${ns}ProgramCohort__c=${program_cohort2}[Id] + Set suite variable ${program_engagement5} + ${program_engagement6} = API Create Program Engagement ${Program1}[Id] ${contact3}[Id] ${ns}ProgramCohort__c=${program_cohort3}[Id] + Set suite variable ${program_engagement6} + API Update Records ${ns}ProgramEngagement__c ${program_engagement4}[Id] ${ns}Stage__c=Applied + *** Test Cases *** +Setup custom bucketed values and validate on participant selector component + [Documentation] Create custom bucketed values for Program Cohort Status and Program Engagement Stages and + ... validate the same on the participant selector component on BSDT + [tags] quadrant:Q3 perm:admin + Setup Custom Metadata Bucketed Value Completed Completed ProgramCohortStatusActive Completed + Setup Custom Metadata Bucketed Value Applied Applied Active Applied + Go To Page Custom Bulk_Service_Deliveries + Click Dialog Button Create by Group + Page Should Contain Text Default Service Delivery Values + Populate Lookup Field Service ${service1}[Name] + Page Should Contain ${service1}[${ns}UnitOfMeasurement__c] + Populate Field Quantity ${quantity} + Click Dialog Button Next + Page Should Contain Text Contact Selection + Load Page Object New ServiceSchedule__c + Page Should Contain Applied + Verify dropdown Options Filter by: Program Cohort contains ${program_cohort1}[Name] + sleep 3s + Verify dropdown Options Filter by: Program Cohort does not contain ${program_cohort2}[Name] + sleep 3s + Verify dropdown Options Filter by: Program Cohort does not contain ${program_cohort3}[Name] + sleep 3s + Select Service Participant ${contact1}[Name] + Validate Participant Is Added ${contact1}[Name] + Click Dialog Button Next + Page Should Contain Delivery Date + GSD1: Create service delivery using BSDT Wizard [Documentation] Clicks on Create by Group on BSDT, selects default service delivery values, adds contact ... and creates service delivery using wizard @@ -78,13 +124,11 @@ GSD1: Create service delivery using BSDT Wizard Verify Details Service Delivery Name contains ${contact1}[FirstName] ${contact1}[LastName] ${today}: ${service}[Name] Verify Details Quantity contains ${quantity}.00 Verify Details Unit of Measurement contains ${service}[${ns}UnitOfMeasurement__c] - Save Current Record ID For Deletion ${ns}ServiceDelivery__c Go To Page Listing ${ns}ServiceDelivery__c Click Listview Link ${contact2}[FirstName] ${contact2}[LastName] ${today}: ${service}[Name] Verify Details Service Delivery Name contains ${contact2}[FirstName] ${contact2}[LastName] ${today}: ${service}[Name] Verify Details Quantity contains ${quantity}.00 Verify Details Unit of Measurement contains ${service}[${ns}UnitOfMeasurement__c] - Save Current Record ID For Deletion ${ns}ServiceDelivery__c GSD2: Filter on bsdt wizard based on Program Cohort [Documentation] On BSDT wizard contact selection screen,filter based on program cohort and @@ -100,7 +144,7 @@ GSD2: Filter on bsdt wizard based on Program Cohort Page Should Contain Text Contact Selection Load Page Object New ServiceSchedule__c Page Should Contain No records selected - Select Value From Dropdown Filter by Program Cohort ${program_cohort2}[Name] + Select Value From Dropdown Filter by: Program Cohort ${program_cohort2}[Name] Page Should Not Contain ${contact1}[Name] Page Should Not Contain ${contact2}[Name] Page Should Contain Text ${contact3}[Name] diff --git a/robot/pmm/tests/browser/Bulk_Service_Delivery/individual_service_delivery.robot b/robot/pmm/tests/browser/Bulk_Service_Delivery/individual_service_delivery.robot index 8190550d4..be5898e55 100644 --- a/robot/pmm/tests/browser/Bulk_Service_Delivery/individual_service_delivery.robot +++ b/robot/pmm/tests/browser/Bulk_Service_Delivery/individual_service_delivery.robot @@ -35,6 +35,17 @@ Setup Test Data ${service1} = API Create Service ${Program1}[Id] Set suite variable ${service1} + ${contact4} = API Create Contact + Set suite variable ${contact4} + ${program4} = API Create Program + Set suite variable ${program4} + ${service4} = API Create Service ${Program4}[Id] + Set suite variable ${service4} + API Update Records ${ns}Service__c ${service4}[Id] ${ns}Status__c=Canceled + ${program_engagement4} = API Create Program Engagement ${Program4}[Id] ${contact4}[Id] + Set suite variable ${program_engagement4} + API Update Records ${ns}ProgramEngagement__c ${program_engagement4}[Id] ${ns}Stage__c=Withdrawn + ${contact2} = API Create Contact Set suite variable ${contact2} ${program2} = API Create Program @@ -57,7 +68,9 @@ ISD1: Add service delivery on bulk service delivery [Documentation] This test adds two service deliveries on bulk service delivery and ... navigates to service delivery listview and verifies that the service delivery ... records are displayed - [tags] W-040316 quadrant:Q2 perm:admin perm:manage perm:deliver feature:Service Delivery + [tags] W-040316 quadrant:Q3 perm:admin perm:manage perm:deliver feature:Service Delivery + Setup Custom Metadata Bucketed Value Canceled Service Canceled_Service ServiceStatusActive Canceled + Setup Custom Metadata Bucketed Value PE:Withdrawn PE_Withrawn Active Withdrawn Go To Page Custom Bulk_Service_Deliveries Click Dialog Button Create by Individual Populate Bsdt Lookup 1 Client ${contact1}[FirstName] ${contact1}[LastName] @@ -69,15 +82,20 @@ ISD1: Add service delivery on bulk service delivery Populate Bsdt Dropdown 2 Program Engagement ${program_engagement2}[Name] Populate Bsdt Dropdown 2 Service ${service2}[Name] Populate Bsdt Field 2 Quantity ${quantity2} + Click Bsdt Button Add Entry + Populate Bsdt Lookup 3 Client ${contact4}[FirstName] ${contact4}[LastName] + Populate Bsdt Dropdown 3 Program Engagement ${program_engagement4}[Name] + Populate Bsdt Dropdown 3 Service ${service4}[Name] + Populate Bsdt Field 3 Quantity ${quantity2} Click Bsdt Button Save Verify Persist Save Icon 1 Saved Verify Persist Save Icon 2 Saved + Verify Persist Save Icon 3 Saved Go To Page Listing ${ns}ServiceDelivery__c Page Should Contain ${contact1}[FirstName] ${contact1}[LastName] ${today}: ${service1}[Name] Page Should Contain ${contact2}[FirstName] ${contact2}[LastName] ${today}: ${service2}[Name] Click Listview Link ${contact1}[FirstName] ${contact1}[LastName] ${today}: ${service1}[Name] Verify Details Service Delivery Name contains ${contact1}[FirstName] ${contact1}[LastName] ${today}: ${service1}[Name] - Save Current Record ID For Deletion ${ns}ServiceDelivery__c ISD2: Verify error message when there are no services associated with the program [Documentation] This test verifies that an error message is displayed when there are no diff --git a/robot/pmm/tests/browser/ServiceSchedule/service_schedule_filter.robot b/robot/pmm/tests/browser/ServiceSchedule/service_schedule_filter.robot index 77bace0d0..3382c7605 100644 --- a/robot/pmm/tests/browser/ServiceSchedule/service_schedule_filter.robot +++ b/robot/pmm/tests/browser/ServiceSchedule/service_schedule_filter.robot @@ -59,7 +59,7 @@ SSF1: Filter based on Program Cohort Verify Wizard Screen Title Add Service Participants Page Should Contain ${service_schedule_name} Page Should Contain No records selected - Select Value From Dropdown Filter by Program Cohort ${program_cohort2}[Name] + Select Value From Dropdown Filter by: Program Cohort ${program_cohort2}[Name] Page Should Not Contain ${contact1}[Name] Page Should Not Contain ${contact2}[Name] Page Should Contain Text ${contact3}[Name] diff --git a/robot/pmm/tests/browser/ServiceSchedule/service_schedule_layout.robot b/robot/pmm/tests/browser/ServiceSchedule/service_schedule_layout.robot index c3ff4cca4..c466dcc3b 100644 --- a/robot/pmm/tests/browser/ServiceSchedule/service_schedule_layout.robot +++ b/robot/pmm/tests/browser/ServiceSchedule/service_schedule_layout.robot @@ -34,6 +34,19 @@ Setup Test Data ${service_participant1} = API Create Service Participant ${contact1}[Id] ${service_schedule}[Id] ${service}[Id] Set suite variable ${service_participant1} + ${start_date} = Get Current Date result_format=%-m/%-d/%Y increment=5 days + Set suite variable ${start_date} + ${service_schedule1} = API Create Service Schedule ${service}[Id] + Set suite variable ${service_schedule1} + ${service_session1} = API Create Service Session ${service_schedule1}[Id] Pending + Set suite variable ${service_session1} + ${service_session2} = API Create Service Session ${service_schedule1}[Id] Pending + Set suite variable ${service_session2} + ${session_start} = Get Current Date result_format=%-m/%-d/%Y increment=1 day + Set suite variable ${session_start} + ${session_end} = Get Current Date result_format=%-m/%-d/%Y increment=10 days + Set suite variable ${session_end} + *** Test Cases *** SSL1: Add Service Participant quick action @@ -69,3 +82,23 @@ SSL2: Verify Upcoming Service Schedule listview Page Should Contain Service Page Should Contain First Session Start Page Should Contain Participant Capacity + +Add more sessions on existing service schedule + [Documentation] Go to existing service schedule and add more sessions using the quick action button + [tags] quadrant:Q3 perm:admin + Go To Page Details ServiceSchedule__c object_id=${service_schedule1}[Id] + Click Quick Action Button Add More Sessions + Load Page Object New ServiceSchedule__c + Verify Wizard Screen Title Service Schedule Information + Page Should Contain You are updating an existing schedule any changes you make here will be saved to the schedule record. Please note that you will only be able to add sessions after the most current existing sessions. + Set Frequency Daily + Service Schedule Ends After 10 + Click Dialog Button Next + Page Should Contain ${session_start}: ${service_schedule1}[Name] + Page Should Contain ${session_end}: ${service_schedule1}[Name] + Click Dialog Button Next + Click Dialog Button Next + Page Should Contain Showing all future Sessions. To modify previously added Sessions, go to the Service Sessions list. + Click Dialog Button Save + Wait Until Modal is Closed + Page Should Contain ${session_start}: ${service_schedule1}[Name] diff --git a/robot/pmm/tests/browser/ServiceSchedule/service_schedule_screen3.robot b/robot/pmm/tests/browser/ServiceSchedule/service_schedule_screen3.robot index 23aa2986b..60ec8a330 100644 --- a/robot/pmm/tests/browser/ServiceSchedule/service_schedule_screen3.robot +++ b/robot/pmm/tests/browser/ServiceSchedule/service_schedule_screen3.robot @@ -38,6 +38,26 @@ Setup Test Data ${service_schedule_name} = Generate New String Set suite variable ${service_schedule_name} + ${program1} = API Create Program + Set suite variable ${program1} + ${service1} = API Create Service ${Program1}[Id] + Set suite variable ${service1} + ${program_cohort1} = API Create Program Cohort ${Program1}[Id] + Set suite variable ${program_cohort1} + ${program_cohort2} = API Create Program Cohort ${Program1}[Id] + Set suite variable ${program_cohort2} + ${program_cohort3} = API Create Program Cohort ${Program1}[Id] + Set suite variable ${program_cohort3} + API Update Records ${ns}ProgramCohort__c ${program_cohort1}[Id] ${ns}Status__c=Planned + ${program_engagement4} = API Create Program Engagement ${Program1}[Id] ${contact1}[Id] ${ns}ProgramCohort__c=${program_cohort1}[Id] + Set suite variable ${program_engagement4} + ${program_engagement5} = API Create Program Engagement ${Program1}[Id] ${contact2}[Id] ${ns}ProgramCohort__c=${program_cohort2}[Id] + Set suite variable ${program_engagement5} + ${program_engagement6} = API Create Program Engagement ${Program1}[Id] ${contact3}[Id] ${ns}ProgramCohort__c=${program_cohort3}[Id] + Set suite variable ${program_engagement6} + API Update Records ${ns}ProgramEngagement__c ${program_engagement4}[Id] ${ns}Stage__c=Waitlisted + + *** Test Cases *** SSS3.1: Add/remove service participants on Screen3 @@ -113,3 +133,30 @@ SSS3.3: Validate fields added to SessionParticipantView fieldset on Wizard Verify Wizard Screen Title Review Service Schedule Page Should Contain Role Page Should Contain Volunteer + +Setup custom bucketed values and validate on service schedule wizard + [Documentation] Create custom bucketed values for Program Cohort Status and Program Engagement Stages and + ... validate the same on the participant selector component on service schedule + [tags] quadrant:Q3 perm:admin + Setup Custom Metadata Bucketed Value Planned Planned ProgramCohortStatusActive Planned + Setup Custom Metadata Bucketed Value Waitlisted Waitlisted Active Waitlisted + Go To Page Details Service__c object_id=${service1}[Id] + Click Wrapper Related List Button Service Schedules New + Current Page Should Be New ServiceSchedule__c + Verify Wizard Screen Title Service Schedule Information + Populate Field Service Schedule Name ${service_schedule_name} + Click Dialog Button Next + Verify Wizard Screen Title Review Service Sessions + Click Dialog Button Next + Verify Wizard Screen Title Add Service Participants + Page Should Contain Waitlisted + Verify dropdown Options Filter by: Program Cohort contains ${program_cohort1}[Name] + Verify dropdown Options Filter by: Program Cohort does not contain ${program_cohort2}[Name] + Verify dropdown Options Filter by: Program Cohort does not contain ${program_cohort3}[Name] + Select Service Participant ${contact1}[Name] + Validate Participant Is Added ${contact1}[Name] + Click Dialog Button Next + Verify Wizard Screen Title Review Service Schedule + Click Dialog Button Save + Wait Until Modal is Closed + Page Should Contain ${contact1}[Name] - ${service_schedule_name}