Fix issues with one to many activity targets (#15656)

This PR fixes issues with one to many activity target bugs described
here :
https://github.com/twentyhq/twenty/issues/14280#issuecomment-3490170714

It also improves the request that fetches notes and tasks in index
pages, to only fetch the notes and tasks identifier fields, thus
reducing the amount of network load.

Fixes https://github.com/twentyhq/twenty/issues/14280
This commit is contained in:
Lucas Bordeau
2025-11-11 17:19:20 +01:00
committed by GitHub
parent de978960d0
commit 5f3253d5a5
11 changed files with 147 additions and 194 deletions
@@ -74,15 +74,6 @@ export const RecordChip = ({
// TODO temporary until we create a record show page for Workspaces members
const avatarChip = (
<AvatarChip
placeholder={recordChipData.name}
placeholderColorSeed={record.id}
avatarType={recordChipData.avatarType}
avatarUrl={recordChipData.avatarUrl ?? ''}
/>
);
if (
forceDisableClick ||
objectNameSingular === CoreObjectNameSingular.WorkspaceMember
@@ -94,7 +85,16 @@ export const RecordChip = ({
maxWidth={maxWidth}
className={className}
variant={ChipVariant.Transparent}
leftComponent={isIconHidden ? null : avatarChip}
leftComponent={
isIconHidden ? null : (
<AvatarChip
placeholder={recordChipData.name}
placeholderColorSeed={record.id}
avatarType={recordChipData.avatarType}
avatarUrl={recordChipData.avatarUrl ?? ''}
/>
)
}
/>
);
}
@@ -105,7 +105,16 @@ export const RecordChip = ({
maxWidth={maxWidth}
label={recordChipData.name}
isLabelHidden={isLabelHidden}
leftComponent={isIconHidden ? null : avatarChip}
leftComponent={
isIconHidden ? null : (
<AvatarChip
placeholder={recordChipData.name}
placeholderColorSeed={record.id}
avatarType={recordChipData.avatarType}
avatarUrl={recordChipData.avatarUrl ?? ''}
/>
)
}
className={className}
variant={
variant ??
@@ -0,0 +1,68 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
import { generateDepthRecordGqlFieldsFromFields } from '@/object-record/graphql/record-gql-fields/utils/generateDepthRecordGqlFieldsFromFields';
import { isDefined } from 'twenty-shared/utils';
export type GenerateDepthRecordGqlFields = {
objectMetadataItems: ObjectMetadataItem[];
activityObjectNameSingular:
| CoreObjectNameSingular.Note
| CoreObjectNameSingular.Task;
depth: 0 | 1;
shouldOnlyLoadActivityIdentifiers?: boolean;
};
export const generateActivityTargetGqlFields = ({
objectMetadataItems,
activityObjectNameSingular,
depth,
shouldOnlyLoadActivityIdentifiers = true,
}: GenerateDepthRecordGqlFields) => {
const isNote = activityObjectNameSingular === CoreObjectNameSingular.Note;
const activityTargetNameSingular = isNote
? CoreObjectNameSingular.NoteTarget
: CoreObjectNameSingular.TaskTarget;
const activityTargetObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular === activityTargetNameSingular,
);
const activityObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular === activityObjectNameSingular,
);
if (
!isDefined(activityTargetObjectMetadataItem) ||
!isDefined(activityObjectMetadataItem)
) {
return {};
}
if (shouldOnlyLoadActivityIdentifiers) {
const activityLabelIdentifierFieldMetadataItem =
getLabelIdentifierFieldMetadataItem(activityObjectMetadataItem);
return {
id: true,
[activityObjectNameSingular]: {
id: true,
...(isDefined(activityLabelIdentifierFieldMetadataItem)
? { [activityLabelIdentifierFieldMetadataItem.name]: true }
: {}),
},
};
} else {
return {
...generateDepthRecordGqlFieldsFromFields({
depth,
fields: activityTargetObjectMetadataItem.fields,
objectMetadataItems,
shouldOnlyLoadRelationIdentifiers: false,
}),
[activityObjectNameSingular]: true,
};
}
};
@@ -59,61 +59,12 @@ const mocks: MockedResponse[] = [
edges {
node {
__typename
company {
__typename
domainName {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
id
name
}
companyId
createdAt
deletedAt
id
note {
__typename
id
title
}
noteId
opportunity {
__typename
id
name
}
opportunityId
person {
__typename
avatarUrl
id
name {
firstName
lastName
}
}
personId
pet {
__typename
id
name
}
petId
rocket {
__typename
id
name
}
rocketId
surveyResult {
__typename
id
name
}
surveyResultId
updatedAt
}
}
}
@@ -122,61 +73,12 @@ const mocks: MockedResponse[] = [
edges {
node {
__typename
company {
__typename
domainName {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
id
name
}
companyId
createdAt
deletedAt
id
opportunity {
__typename
id
name
}
opportunityId
person {
__typename
avatarUrl
id
name {
firstName
lastName
}
}
personId
pet {
__typename
id
name
}
petId
rocket {
__typename
id
name
}
rocketId
surveyResult {
__typename
id
name
}
surveyResultId
task {
__typename
id
title
}
taskId
updatedAt
}
}
}
@@ -1,12 +1,11 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getImageIdentifierFieldMetadataItem } from '@/object-metadata/utils/getImageIdentifierFieldMetadataItem';
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
import { hasObjectMetadataItemPositionField } from '@/object-metadata/utils/hasObjectMetadataItemPositionField';
import { generateActivityTargetGqlFields } from '@/object-record/graphql/record-gql-fields/utils/generateActivityTargetGqlFields';
import { generateDepthRecordGqlFieldsFromFields } from '@/object-record/graphql/record-gql-fields/utils/generateDepthRecordGqlFieldsFromFields';
import { generateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/utils/generateDepthRecordGqlFieldsFromObject';
import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
@@ -29,15 +28,6 @@ export const useRecordsFieldVisibleGqlFields = ({
useRecordIndexContextOrThrow();
const { objectMetadataItems } = useObjectMetadataItems();
const { objectMetadataItem: noteTargetObjectMetadataItem } =
useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.NoteTarget,
});
const { objectMetadataItem: taskTargetObjectMetadataItem } =
useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.TaskTarget,
});
const allDepthOneGqlFields = generateDepthRecordGqlFieldsFromFields({
objectMetadataItems,
@@ -75,15 +65,17 @@ export const useRecordsFieldVisibleGqlFields = ({
createdAt: true,
updatedAt: true,
deletedAt: true,
noteTargets: generateDepthRecordGqlFieldsFromObject({
objectMetadataItem: noteTargetObjectMetadataItem,
noteTargets: generateActivityTargetGqlFields({
activityObjectNameSingular: CoreObjectNameSingular.Note,
depth: 0,
objectMetadataItems,
depth: 1,
shouldOnlyLoadActivityIdentifiers: true,
}),
taskTargets: generateDepthRecordGqlFieldsFromObject({
taskTargets: generateActivityTargetGqlFields({
activityObjectNameSingular: CoreObjectNameSingular.Task,
depth: 0,
objectMetadataItems,
objectMetadataItem: taskTargetObjectMetadataItem,
depth: 1,
shouldOnlyLoadActivityIdentifiers: true,
}),
};
};
@@ -46,14 +46,6 @@ const StyledValueContainer = styled.div<{ readonly: boolean }>`
min-width: 0;
position: relative;
width: 100%;
${({ readonly }) =>
!readonly &&
`
&:hover .record-inline-cell-value-display {
opacity: 0;
}
`}
`;
const StyledLabelContainer = styled.div<{ width?: number }>`
@@ -123,7 +115,7 @@ export const RecordInlineCellContainer = () => {
<IconLabel stroke={theme.icon.stroke.sm} />
</StyledIconContainer>
)}
{showLabel && label && (
{label && (
<StyledLabelContainer width={labelWidth}>
<OverflowingTextWithTooltip text={label} displayedMaxRows={1} />
</StyledLabelContainer>
@@ -144,7 +136,7 @@ export const RecordInlineCellContainer = () => {
)}
<StyledValueContainer readonly={readonly ?? false} id={anchorId}>
<RecordInlineCellValue className="record-inline-cell-value-display" />
<RecordInlineCellValue />
</StyledValueContainer>
</StyledInlineCellBaseContainer>
);
@@ -1,4 +1,3 @@
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { useIsFieldEmpty } from '@/object-record/record-field/ui/hooks/useIsFieldEmpty';
@@ -8,6 +7,7 @@ import {
type RecordInlineCellContextProps,
} from '@/object-record/record-inline-cell/components/RecordInlineCellContext';
import { RecordInlineCellButton } from '@/object-record/record-inline-cell/components/RecordInlineCellEditButton';
import { css } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
const StyledRecordInlineCellNormalModeOuterContainer = styled.div<
@@ -60,6 +60,8 @@ const StyledEmptyField = styled.div`
color: ${({ theme }) => theme.font.color.light};
display: flex;
height: 20px;
background-color: ${({ theme }) => theme.background.secondary};
`;
export const RecordInlineCellDisplayMode = ({
@@ -89,6 +91,9 @@ export const RecordInlineCellDisplayMode = ({
const emptyPlaceHolder = showLabel ? t`Empty` : label;
const shouldShowEmptyPlaceholder =
(isDisplayModeContentEmpty && !shouldDisplayEditModeOnFocus) || !children;
return (
<>
<StyledRecordInlineCellNormalModeOuterContainer
@@ -97,8 +102,7 @@ export const RecordInlineCellDisplayMode = ({
onClick={onClick}
>
<StyledRecordInlineCellNormalModeInnerContainer>
{(isDisplayModeContentEmpty && !shouldDisplayEditModeOnFocus) ||
!children ? (
{shouldShowEmptyPlaceholder ? (
<StyledEmptyField>{emptyPlaceHolder}</StyledEmptyField>
) : (
children
@@ -28,13 +28,7 @@ const StyledClickableContainer = styled.div<{
`};
`;
type RecordInlineCellValueProps = {
className?: string;
};
export const RecordInlineCellValue = ({
className,
}: RecordInlineCellValueProps) => {
export const RecordInlineCellValue = () => {
const { readonly, loading, isCentered } = useRecordInlineCellContext();
if (loading === true) {
@@ -42,11 +36,7 @@ export const RecordInlineCellValue = ({
}
return (
<StyledClickableContainer
readonly={readonly}
isCentered={isCentered}
className={className}
>
<StyledClickableContainer readonly={readonly} isCentered={isCentered}>
<RecordInlineCellDisplayMode isHovered={false}>
<FieldDisplay />
</RecordInlineCellDisplayMode>
@@ -1,6 +1,6 @@
import { generateActivityTargetMorphFieldKeys } from '@/activities/utils/generateActivityTargetMorphFieldKeys';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { generateActivityTargetGqlFields } from '@/object-record/graphql/record-gql-fields/utils/generateActivityTargetGqlFields';
import { generateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/utils/generateDepthRecordGqlFieldsFromObject';
import { type RecordGqlOperationSignatureFactory } from '@/object-record/graphql/types/RecordGqlOperationSignatureFactory';
@@ -26,31 +26,17 @@ export const buildFindOneRecordForShowPageOperationSignature: RecordGqlOperation
objectMetadataItems,
depth: 1,
}),
...(objectMetadataItem.nameSingular === CoreObjectNameSingular.Task
? {
taskTargets: {
id: true,
__typename: true,
createdAt: true,
updatedAt: true,
note: true,
noteId: true,
...generateActivityTargetMorphFieldKeys(objectMetadataItems),
},
}
: {}),
...(objectMetadataItem.nameSingular === CoreObjectNameSingular.Note
? {
noteTargets: {
id: true,
__typename: true,
createdAt: true,
updatedAt: true,
task: true,
taskId: true,
...generateActivityTargetMorphFieldKeys(objectMetadataItems),
},
}
: {}),
noteTargets: generateActivityTargetGqlFields({
activityObjectNameSingular: CoreObjectNameSingular.Note,
depth: 1,
objectMetadataItems,
shouldOnlyLoadActivityIdentifiers: false,
}),
taskTargets: generateActivityTargetGqlFields({
activityObjectNameSingular: CoreObjectNameSingular.Task,
depth: 1,
objectMetadataItems,
shouldOnlyLoadActivityIdentifiers: false,
}),
},
});