Fix Opportunity email timeline relation traversal (#22064)
## Summary - Stop the related-person path walker from traversing system objects while deriving timeline people. - Keep direct `person` terminal paths valid so CRM relations still resolve. - Add a regression test covering the bad Opportunity owner -> workspace member -> message participant path. ## Root Cause PR #21684 introduced generic relation traversal for email and calendar timelines. That traversal walks relation paths from the current record to `person`, then the Emails tab loads message threads for those derived people. For Opportunities, the traversal was too broad because it could enter internal/system objects. In particular, it could follow: `opportunity.owner -> workspaceMember.messageParticipants -> messageParticipant.person` That path does not describe people related to the Opportunity. It describes people who appeared in messages involving the Opportunity owner. As a result, an Opportunity owned by Josh could show threads from Josh's broader mailbox activity, which matches the customer report: recently communicated people appeared in the Opportunity Emails tab even though they were not specifically related to that Opportunity. ## Behavior Before On an Opportunity record, the Emails tab could include message threads for: - the Opportunity point of contact; - people related through the Opportunity company; - people reached through internal/system relations, including the owner workspace member's message participants. The last category was the regression. It made the Opportunity Emails tab look like a broad inbox for the owner instead of a timeline for people related to the CRM record. ## Behavior After The traversal still allows valid CRM person paths, including: `opportunity.pointOfContact -> person` and non-system CRM paths such as: `opportunity.company -> company.people -> person` But it now stops before traversing system objects such as `workspaceMember` and `messageParticipant`. This blocks the bad owner-mailbox expansion path: `opportunity.owner -> workspaceMember.messageParticipants -> messageParticipant.person` Email sync is unchanged. This only changes which synced emails are displayed on a record timeline. ## Video https://github.com/user-attachments/assets/26de4cee-06d9-4f42-b91e-32e60a260b5b ## Validation - `yarn nx jest twenty-server src/engine/core-modules/related-person-ids/utils/__tests__/find-relation-paths-to-person.util.spec.ts --runInBand` - Focused `oxlint` and `oxfmt` on the touched files. - GitHub `server-lint-typecheck` passes on the updated branch. - Browser verification on local Apple seed workspace: fixed relation set renders `Inbox 280`; the excluded owner-derived path would have resolved `300` threads.
This commit is contained in:
committed by
GitHub
parent
eb53dee3be
commit
41d1b478b0
+62
-1
@@ -18,7 +18,11 @@ type RelationSpec = {
|
||||
inverseFieldName: string;
|
||||
};
|
||||
|
||||
const buildGraphFixtures = (graph: Record<string, RelationSpec[]>) => {
|
||||
const buildGraphFixtures = (
|
||||
graph: Record<string, RelationSpec[]>,
|
||||
options: { systemObjectNames?: string[] } = {},
|
||||
) => {
|
||||
const systemObjectNames = options.systemObjectNames ?? [];
|
||||
const fieldId = (objectNameSingular: string, fieldName: string) =>
|
||||
`${objectNameSingular}.${fieldName}`;
|
||||
|
||||
@@ -82,6 +86,7 @@ const buildGraphFixtures = (graph: Record<string, RelationSpec[]>) => {
|
||||
targetObjectMetadata: {
|
||||
id: spec.targetObjectNameSingular,
|
||||
nameSingular: spec.targetObjectNameSingular,
|
||||
isSystem: systemObjectNames.includes(spec.targetObjectNameSingular),
|
||||
},
|
||||
sourceFieldMetadata: { name: spec.fieldName },
|
||||
targetFieldMetadata: { name: spec.inverseFieldName },
|
||||
@@ -261,6 +266,62 @@ describe('findRelationPathsToPerson', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not traverse system objects while looking for related people', () => {
|
||||
const { flatObjectMetadataMaps, flatFieldMetadataMaps } =
|
||||
buildGraphFixtures(
|
||||
{
|
||||
opportunity: [
|
||||
{
|
||||
fieldName: 'pointOfContact',
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
targetObjectNameSingular: 'person',
|
||||
inverseFieldName: 'pointOfContactForOpportunities',
|
||||
},
|
||||
{
|
||||
fieldName: 'owner',
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
targetObjectNameSingular: 'workspaceMember',
|
||||
inverseFieldName: 'ownedOpportunities',
|
||||
},
|
||||
],
|
||||
workspaceMember: [
|
||||
{
|
||||
fieldName: 'messageParticipants',
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
targetObjectNameSingular: 'messageParticipant',
|
||||
inverseFieldName: 'workspaceMember',
|
||||
},
|
||||
],
|
||||
messageParticipant: [
|
||||
{
|
||||
fieldName: 'person',
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
targetObjectNameSingular: 'person',
|
||||
inverseFieldName: 'messageParticipants',
|
||||
},
|
||||
],
|
||||
person: [],
|
||||
},
|
||||
{ systemObjectNames: ['workspaceMember', 'messageParticipant'] },
|
||||
);
|
||||
|
||||
expect(
|
||||
findRelationPathsToPerson({
|
||||
rootObjectNameSingular: 'opportunity',
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
}),
|
||||
).toEqual([
|
||||
[
|
||||
{
|
||||
direction: RelationType.MANY_TO_ONE,
|
||||
queryObjectNameSingular: 'opportunity',
|
||||
joinColumnName: 'pointOfContactId',
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns no path when person is unreachable, terminating on relation cycles', () => {
|
||||
const { flatObjectMetadataMaps, flatFieldMetadataMaps } =
|
||||
buildGraphFixtures({
|
||||
|
||||
+13
-3
@@ -15,6 +15,9 @@ import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object
|
||||
const PERSON_OBJECT_NAME_SINGULAR = 'person';
|
||||
const DEFAULT_MAX_RELATION_DEPTH_TO_PERSON = 3;
|
||||
|
||||
const isSystemObjectMetadata = (objectMetadata: { isSystem?: boolean }) =>
|
||||
objectMetadata.isSystem === true;
|
||||
|
||||
export type RelationHopToPerson = {
|
||||
direction: RelationType;
|
||||
queryObjectNameSingular: string;
|
||||
@@ -103,14 +106,21 @@ export const findRelationPathsToPerson = ({
|
||||
},
|
||||
];
|
||||
|
||||
const targetObjectId = relation.targetObjectMetadata.id;
|
||||
|
||||
if (
|
||||
relation.targetObjectMetadata.nameSingular ===
|
||||
PERSON_OBJECT_NAME_SINGULAR
|
||||
) {
|
||||
pathsToPerson.push(nextPath);
|
||||
} else if (!visitedObjectIds.has(targetObjectId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isSystemObjectMetadata(relation.targetObjectMetadata)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetObjectId = relation.targetObjectMetadata.id;
|
||||
|
||||
if (!visitedObjectIds.has(targetObjectId)) {
|
||||
objectIdsReachedThisDepth.add(targetObjectId);
|
||||
nextFrontier.push({ objectId: targetObjectId, path: nextPath });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user