Merge pull request #6 from QRun-IO/feature/contingent-possible-values
feat: contingent possible values for deal stage, activity outcome and list-member submission
This commit was merged in pull request #6.
This commit is contained in:
4 files changed
+149
No files matched your search
@@ -13,6 +13,9 @@ import com.kingsrook.qbits.crm.core.model.enums.CrmDirection;
|
||||
import com.kingsrook.qbits.crm.deals.model.Deal;
|
||||
import com.kingsrook.qbits.crm.email.model.EmailMessage;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
@@ -87,6 +90,12 @@ public class Activity extends QRecordEntity
|
||||
.withSection(SectionFactory.customT2("participants", new QIcon("group")).withWidgetName(participantChildJoinName))
|
||||
.withSection(SectionFactory.defaultT3("externalCalendarId", "createDate", "modifyDate"));
|
||||
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// Only offer outcomes belonging to the selected activity type. //
|
||||
//////////////////////////////////////////////////////////////////
|
||||
table.getField("outcomeId").withPossibleValueSourceFilter(new QQueryFilter(
|
||||
new QFilterCriteria("activityTypeId", QCriteriaOperator.EQUALS, "${input.activityTypeId}")));
|
||||
|
||||
return (table);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@ package com.kingsrook.qbits.crm.core.model;
|
||||
import java.time.Instant;
|
||||
import com.kingsrook.qbits.crm.core.model.enums.CrmListMemberStatus;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
@@ -55,6 +58,12 @@ public class EmailListMember extends QRecordEntity
|
||||
.withSection(SectionFactory.defaultT2("subscribedDate", "unsubscribedDate", "subscriptionSource", "formSubmissionId"))
|
||||
.withSection(SectionFactory.defaultT3("createDate", "modifyDate"));
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// Only offer submissions linked to the selected contact. //
|
||||
////////////////////////////////////////////////////////////
|
||||
table.getField("formSubmissionId").withPossibleValueSourceFilter(new QQueryFilter(
|
||||
new QFilterCriteria("contactId", QCriteriaOperator.EQUALS, "${input.contactId}")));
|
||||
|
||||
return (table);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ import com.kingsrook.qbits.crm.core.model.Company;
|
||||
import com.kingsrook.qbits.crm.core.model.LeadSource;
|
||||
import com.kingsrook.qbits.crm.core.model.enums.CrmDealPriority;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
@@ -129,6 +132,12 @@ public class Deal extends QRecordEntity
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
// Only offer stages belonging to the selected pipeline. //
|
||||
///////////////////////////////////////////////////////////
|
||||
table.getField("pipelineStageId").withPossibleValueSourceFilter(new QQueryFilter(
|
||||
new QFilterCriteria("pipelineId", QCriteriaOperator.EQUALS, "${input.pipelineId}")));
|
||||
|
||||
return (table);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/*******************************************************************************
|
||||
** Tests for CRM choices scoped by another editable field on the same form.
|
||||
*******************************************************************************/
|
||||
package com.kingsrook.qbits.crm;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.crm.activities.model.Activity;
|
||||
import com.kingsrook.qbits.crm.activities.model.ActivityOutcome;
|
||||
import com.kingsrook.qbits.crm.activities.processes.LogActivityProcess;
|
||||
import com.kingsrook.qbits.crm.core.model.EmailListMember;
|
||||
import com.kingsrook.qbits.crm.core.model.FormSubmission;
|
||||
import com.kingsrook.qbits.crm.deals.model.Deal;
|
||||
import com.kingsrook.qbits.crm.deals.model.PipelineStage;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.SearchPossibleValueSourceAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueSearchFilterUseCase;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore;
|
||||
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Exercise the generated metadata and real possible-value searches in memory.
|
||||
*******************************************************************************/
|
||||
class ContingentPossibleValuesTest extends BaseTest
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Every contingent field must point to an existing scope column and retain
|
||||
** its template after searches for either scope and either empty-input mode.
|
||||
*******************************************************************************/
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
Deal.TABLE_NAME + ", pipelineId, pipelineStageId, " + PipelineStage.TABLE_NAME,
|
||||
Activity.TABLE_NAME + ", activityTypeId, outcomeId, " + ActivityOutcome.TABLE_NAME,
|
||||
EmailListMember.TABLE_NAME + ", contactId, formSubmissionId, " + FormSubmission.TABLE_NAME
|
||||
})
|
||||
void testContingentSearch(String tableName, String parent, String child, String sourceTable) throws QException
|
||||
{
|
||||
QTableMetaData table = QContext.getQInstance().getTable(tableName);
|
||||
QFieldMetaData field = table.getField(child);
|
||||
assertThat(table.getField(parent).getIsEditable()).isTrue();
|
||||
assertThat(field.getIsEditable()).isTrue();
|
||||
assertThat(QContext.getQInstance().getPossibleValueSource(field.getPossibleValueSourceName()).getTableName()).isEqualTo(sourceTable);
|
||||
assertThat(QContext.getQInstance().getTable(sourceTable).getField(parent)).isNotNull();
|
||||
|
||||
QQueryFilter template = field.getPossibleValueSourceFilter();
|
||||
assertThat(template).isNotNull();
|
||||
QFilterCriteria expected = new QFilterCriteria(parent, QCriteriaOperator.EQUALS, "${input." + parent + "}");
|
||||
assertThat(template.getCriteria()).containsExactly(expected);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// Seed the search backend directly so insert customizers cannot relink scopes. //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
MemoryRecordStore.getInstance().insert(new InsertInput(sourceTable).withRecords(List.of(
|
||||
scopedRecord(11, parent, 1), scopedRecord(12, parent, 1), scopedRecord(21, parent, 2))), false);
|
||||
|
||||
assertThat(search(field, Map.of(parent, 1), PossibleValueSearchFilterUseCase.FORM)).containsExactlyInAnyOrder(11, 12);
|
||||
assertThat(search(field, Map.of(parent, 2), PossibleValueSearchFilterUseCase.FORM)).containsExactly(21);
|
||||
assertThat(search(field, Map.of(), PossibleValueSearchFilterUseCase.FORM)).isEmpty();
|
||||
assertThat(search(field, Map.of(), PossibleValueSearchFilterUseCase.FILTER)).containsExactlyInAnyOrder(11, 12, 21);
|
||||
assertThat(template.getCriteria()).containsExactly(expected);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Optional company links and backend-only process inputs stay unrestricted.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testSkippedFields()
|
||||
{
|
||||
QTableMetaData activity = QContext.getQInstance().getTable(Activity.TABLE_NAME);
|
||||
assertThat(activity.getField("companyId").getIsRequired()).isFalse();
|
||||
assertThat(activity.getField("contactId").getPossibleValueSourceFilter()).isNull();
|
||||
assertThat(activity.getField("dealId").getPossibleValueSourceFilter()).isNull();
|
||||
assertThat(QContext.getQInstance().getProcess(LogActivityProcess.NAME).getStepList())
|
||||
.allMatch(step -> step instanceof QBackendStepMetaData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Build a labeled row usable by each of the three table possible-value sources.
|
||||
*******************************************************************************/
|
||||
private QRecord scopedRecord(int id, String parent, int scope)
|
||||
{
|
||||
return new QRecord().withValue("id", id).withValue(parent, scope)
|
||||
.withValue("name", "Choice " + id).withValue("firstName", "Contact " + id)
|
||||
.withValue("email", "contact" + id + "@example.com").withValue("formType", 1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Interpret a cloned field filter just as a frontend form or filter does.
|
||||
*******************************************************************************/
|
||||
private List<Object> search(QFieldMetaData field, Map<String, Serializable> values, PossibleValueSearchFilterUseCase useCase) throws QException
|
||||
{
|
||||
QQueryFilter filter = field.getPossibleValueSourceFilter().clone();
|
||||
filter.interpretValues(MapBuilder.of("input", values), useCase);
|
||||
return new SearchPossibleValueSourceAction().execute(new SearchPossibleValueSourceInput()
|
||||
.withPossibleValueSourceName(field.getPossibleValueSourceName())
|
||||
.withDefaultQueryFilter(filter)).getResults().stream().<Object>map(QPossibleValue::getId).toList();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user