From 5f3253d5a5c6a3621ff3b399e014dedd52e00b1a Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Tue, 11 Nov 2025 17:19:20 +0100 Subject: [PATCH] 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 --- .../activities/tasks/components/TaskRow.tsx | 39 +++++--- .../object-record/components/RecordChip.tsx | 31 +++--- .../utils/generateActivityTargetGqlFields.ts | 68 +++++++++++++ .../useRecordIndexTableQuery.test.tsx | 98 ------------------- .../hooks/useRecordsFieldVisibleGqlFields.ts | 26 ++--- .../components/RecordInlineCellContainer.tsx | 12 +-- .../RecordInlineCellDisplayMode.tsx | 10 +- .../components/RecordInlineCellValue.tsx | 14 +-- ...ordForShowPageOperationSignatureFactory.ts | 40 +++----- .../components/ExpandableList.tsx | 2 + .../twenty-ui/src/components/chip/Chip.tsx | 1 + 11 files changed, 147 insertions(+), 194 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/graphql/record-gql-fields/utils/generateActivityTargetGqlFields.ts diff --git a/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx b/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx index 71b32eb086..98966ea8f8 100644 --- a/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx @@ -58,6 +58,11 @@ const StyledRightSideContainer = styled.div` max-width: 50%; `; +const StyledActivityTargetsContainer = styled.div` + overflow: clip; + width: 100%; +`; + const StyledPlaceholder = styled.div` color: ${({ theme }) => theme.font.color.light}; `; @@ -125,22 +130,24 @@ export const TaskRow = ({ task }: { task: Task }) => { )} { - - - - - + + + + + + + } diff --git a/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx b/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx index 9cb9583583..75d2672a8e 100644 --- a/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx +++ b/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx @@ -74,15 +74,6 @@ export const RecordChip = ({ // TODO temporary until we create a record show page for Workspaces members - const avatarChip = ( - - ); - 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 : ( + + ) + } /> ); } @@ -105,7 +105,16 @@ export const RecordChip = ({ maxWidth={maxWidth} label={recordChipData.name} isLabelHidden={isLabelHidden} - leftComponent={isIconHidden ? null : avatarChip} + leftComponent={ + isIconHidden ? null : ( + + ) + } className={className} variant={ variant ?? diff --git a/packages/twenty-front/src/modules/object-record/graphql/record-gql-fields/utils/generateActivityTargetGqlFields.ts b/packages/twenty-front/src/modules/object-record/graphql/record-gql-fields/utils/generateActivityTargetGqlFields.ts new file mode 100644 index 0000000000..6975cab153 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/graphql/record-gql-fields/utils/generateActivityTargetGqlFields.ts @@ -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, + }; + } +}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useRecordIndexTableQuery.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useRecordIndexTableQuery.test.tsx index abd528709b..a5be53e666 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useRecordIndexTableQuery.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useRecordIndexTableQuery.test.tsx @@ -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 } } } diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useRecordsFieldVisibleGqlFields.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useRecordsFieldVisibleGqlFields.ts index 73f09b6628..f49e61582f 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/useRecordsFieldVisibleGqlFields.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useRecordsFieldVisibleGqlFields.ts @@ -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, }), }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx index c81c52ab6f..ec54a18717 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx @@ -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 = () => { )} - {showLabel && label && ( + {label && ( @@ -144,7 +136,7 @@ export const RecordInlineCellContainer = () => { )} - + ); diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellDisplayMode.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellDisplayMode.tsx index 8507f8cb93..54ea90a104 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellDisplayMode.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellDisplayMode.tsx @@ -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 ( <> - {(isDisplayModeContentEmpty && !shouldDisplayEditModeOnFocus) || - !children ? ( + {shouldShowEmptyPlaceholder ? ( {emptyPlaceHolder} ) : ( children diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellValue.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellValue.tsx index a9c6d72d6a..94719c5036 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellValue.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellValue.tsx @@ -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 ( - + diff --git a/packages/twenty-front/src/modules/object-record/record-show/graphql/operations/factories/findOneRecordForShowPageOperationSignatureFactory.ts b/packages/twenty-front/src/modules/object-record/record-show/graphql/operations/factories/findOneRecordForShowPageOperationSignatureFactory.ts index 5bb7ffbd27..430dc7c64a 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/graphql/operations/factories/findOneRecordForShowPageOperationSignatureFactory.ts +++ b/packages/twenty-front/src/modules/object-record/record-show/graphql/operations/factories/findOneRecordForShowPageOperationSignatureFactory.ts @@ -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, + }), }, }); diff --git a/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandableList.tsx b/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandableList.tsx index 5cf97cb373..6939ac6f7d 100644 --- a/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandableList.tsx +++ b/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandableList.tsx @@ -45,6 +45,8 @@ const StyledChildContainer = styled.div` const StyledUnShrinkableContainer = styled.div` color: ${({ theme }) => theme.font.color.primary}; flex-shrink: 0; + + width: 24px; `; export type ExpandableListProps = { diff --git a/packages/twenty-ui/src/components/chip/Chip.tsx b/packages/twenty-ui/src/components/chip/Chip.tsx index 415dbf62f5..603281c83a 100644 --- a/packages/twenty-ui/src/components/chip/Chip.tsx +++ b/packages/twenty-ui/src/components/chip/Chip.tsx @@ -126,6 +126,7 @@ const StyledContainer = withTheme(styled.div< : 'var(--chip-horizontal-padding)'}; `); +// TODO: refactor this const renderRightComponent = ( rightComponent: (() => ReactNode) | ReactNode | null, ) => {