Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52c020d3f3
|
||
|
|
8dad04943f | ||
|
|
569e1e345c
|
||
|
|
6c61c35782 | ||
|
|
5ecfe1bd56 | ||
|
|
650e38f5a0 | ||
|
|
76b95c91ee | ||
|
|
88a25c5a72 | ||
|
|
e7d6050652 | ||
|
|
2a68ab4d14 |
No files matched your search
+2
-12
@@ -11,7 +11,7 @@ workflows:
|
||||
context: [ qqq-maven-registry-credentials ]
|
||||
filters:
|
||||
branches:
|
||||
ignore: /(develop|main|release/.*|hotfix/.*|integration.*)/
|
||||
ignore: /(develop|main|release/.*|integration.*)/
|
||||
tags:
|
||||
only: []
|
||||
|
||||
@@ -44,14 +44,4 @@ workflows:
|
||||
branches:
|
||||
only: [ main ]
|
||||
tags:
|
||||
only: [ /v.*/ ]
|
||||
|
||||
publish_hotfix_release:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
context: [ qqq-maven-registry-credentials ]
|
||||
branch_type: hotfix
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /hotfix\/.*/
|
||||
only: [ /^v\d+\.\d+\.\d+$/ ]
|
||||
@@ -0,0 +1 @@
|
||||
* @KofTwentyTwo
|
||||
@@ -0,0 +1,25 @@
|
||||
# Cut the next release candidate (QRun-IO/qqq docs/QRUN-QBIT-REPO-STANDARD.md, C6):
|
||||
# run this workflow by hand, or merge a develop PR labelled `release:rc`. It merges develop into
|
||||
# the release line and the release/* push runs CircleCI publish_release_candidate.
|
||||
name: Release candidate
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: Print the plan without merging or creating branches
|
||||
type: boolean
|
||||
default: false
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches: [develop]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release-rc:
|
||||
if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release:rc'))
|
||||
uses: QRun-IO/.github/.github/workflows/qbit-release-rc.yml@main
|
||||
with:
|
||||
dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}
|
||||
@@ -0,0 +1,15 @@
|
||||
# qbit-crm
|
||||
|
||||
## Knowledge base
|
||||
|
||||
Reviewed dossiers for this repo and the QQQ platform live in the second-brain vault:
|
||||
|
||||
- Hub / start here: `R:/Git.Local/KofTwentyTwo/second-brain/knowledge/qqq/qqq-hub.md`
|
||||
- This repo's dossier: `R:/Git.Local/KofTwentyTwo/second-brain/knowledge/qqq/repos/qbit-crm.md`
|
||||
(reviewed commit `76b95c91ee7e`, branch `develop`, 2026-07-04)
|
||||
- QBit mechanics refresher: `knowledge/qqq/architecture/metadata-model.md` in the same vault
|
||||
|
||||
Key facts captured there: parent pom `qbit-build-parent:1.4.0` pins qqq to 0.27.9 (via
|
||||
`qqq-bom-pom` import); two BREAK-04 compile breaks block a qqq 4.0 upgrade
|
||||
(`BaseTest.setAuthentication`, `EnrollInSequenceProcess input.getSession()`); Maven
|
||||
Central's published `0.1.0` predates the AGPL→Apache license switch and the audit fixes.
|
||||
@@ -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