From bcca5d00025930ab85331b3c03f07c505a40c344 Mon Sep 17 00:00:00 2001 From: BugIsGod <87571967+bugisthegod@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:07:19 +0100 Subject: [PATCH] fix: show disabled file chip when file no longer exists in timeline (#19045) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: #18943 Follow-up pr: #19001 ## Summary: - Timeline activity shows file upload history, but deleted files had no signed URL and were still rendered as clickable — clicking did nothing - grab the fileId from properties.diff.after, look it up in the current record's files field: if present, use live signed URL; if absent, mark as deleted - Deleted file chips show line-through label, not-allowed cursor, and "File no longer exists" tooltip on hover https://github.com/user-attachments/assets/5df6a675-0003-4fd1-ad57-a07e4338923f --------- Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com> --- .../components/EventFieldDiffValueEffect.tsx | 38 +++++++++++++++++-- .../record-field/ui/types/FieldMetadata.ts | 1 + .../ui/field/display/components/FileChip.tsx | 33 ++++++++++------ .../twenty-ui/src/components/chip/Chip.tsx | 11 +++++- .../tooltip/OverflowingTextWithTooltip.tsx | 6 ++- 5 files changed, 71 insertions(+), 18 deletions(-) diff --git a/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffValueEffect.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffValueEffect.tsx index c2fb1bf523..a9e924da59 100644 --- a/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffValueEffect.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffValueEffect.tsx @@ -1,9 +1,13 @@ -import { useEffect } from 'react'; +import { useContext, useEffect } from 'react'; -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { TimelineActivityContext } from '@/activities/timeline-activities/contexts/TimelineActivityContext'; import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { type FieldFilesValue } from '@/object-record/record-field/ui/types/FieldMetadata'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; +import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue'; import { useSetAtomFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomFamilyState'; +import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; export const EventFieldDiffValueEffect = ({ @@ -22,13 +26,39 @@ export const EventFieldDiffValueEffect = ({ diffArtificialRecordStoreId, ); + const { recordId } = useContext(TimelineActivityContext); + const recordStore = useAtomFamilyStateValue(recordStoreFamilyState, recordId); + useEffect(() => { if (!isDefined(diffRecord)) return; + let fieldValue = diffRecord; + + if ( + fieldMetadataItem.type === FieldMetadataType.FILES && + isDefined(recordStore) && + Array.isArray(diffRecord) + ) { + const currentFiles = Array.isArray(recordStore[fieldMetadataItem.name]) + ? (recordStore[fieldMetadataItem.name] as FieldFilesValue[]) + : []; + const currentFileMap = new Map( + currentFiles.map((file) => [file.fileId, file]), + ); + + fieldValue = (diffRecord as FieldFilesValue[]).map((file) => { + const currentFile = currentFileMap.get(file.fileId); + if (isDefined(currentFile)) { + return { ...file, url: currentFile.url }; + } + return { ...file, isDeleted: true, url: undefined }; + }); + } + const forgedObjectRecord = { __typename: mainObjectMetadataItem.nameSingular, id: diffArtificialRecordStoreId, - [fieldMetadataItem.name]: diffRecord, + [fieldMetadataItem.name]: fieldValue, }; setRecordStore(forgedObjectRecord); @@ -36,8 +66,10 @@ export const EventFieldDiffValueEffect = ({ diffRecord, diffArtificialRecordStoreId, fieldMetadataItem.name, + fieldMetadataItem.type, mainObjectMetadataItem.nameSingular, setRecordStore, + recordStore, ]); return <>; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/types/FieldMetadata.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/types/FieldMetadata.ts index b51dabdf9b..bacd2a2b78 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/types/FieldMetadata.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/types/FieldMetadata.ts @@ -335,4 +335,5 @@ export type FieldFilesValue = { extension?: string; url?: string; fileCategory?: FileCategory; + isDeleted?: boolean; }; diff --git a/packages/twenty-front/src/modules/ui/field/display/components/FileChip.tsx b/packages/twenty-front/src/modules/ui/field/display/components/FileChip.tsx index 3e110798fb..e313e5ebb5 100644 --- a/packages/twenty-front/src/modules/ui/field/display/components/FileChip.tsx +++ b/packages/twenty-front/src/modules/ui/field/display/components/FileChip.tsx @@ -1,4 +1,5 @@ import { styled } from '@linaria/react'; +import { t } from '@lingui/core/macro'; import { FileIcon } from '@/file/components/FileIcon'; import { type FieldFilesValue } from '@/object-record/record-field/ui/types/FieldMetadata'; @@ -24,7 +25,8 @@ export const FileChip = ({ onClick, forceDisableClick, }: FileChipProps) => { - const isClickable = forceDisableClick !== true; + const isDeleted = file.isDeleted === true; + const isClickable = forceDisableClick !== true && !isDeleted; const handleMouseDown = (event: React.MouseEvent): void => { if (!isClickable) { @@ -45,17 +47,24 @@ export const FileChip = ({ ); return ( - - + - + onMouseDown={handleMouseDown} + > + + + ); }; diff --git a/packages/twenty-ui/src/components/chip/Chip.tsx b/packages/twenty-ui/src/components/chip/Chip.tsx index 3ef7f9883b..fac19b24a4 100644 --- a/packages/twenty-ui/src/components/chip/Chip.tsx +++ b/packages/twenty-ui/src/components/chip/Chip.tsx @@ -29,6 +29,8 @@ export type ChipProps = { disabled?: boolean; clickable?: boolean; label: string; + tooltipLabel?: string; + alwaysShowTooltip?: boolean; isLabelHidden?: boolean; isBold?: boolean; maxWidth?: number; @@ -172,6 +174,8 @@ const renderRightComponent = ( export const Chip = ({ size = ChipSize.Small, label, + tooltipLabel, + alwaysShowTooltip = false, isLabelHidden = false, isBold = false, disabled = false, @@ -200,7 +204,12 @@ export const Chip = ({ > {leftComponent} {!isLabelHidden && isDefined(label) && isNonEmptyString(label) ? ( - + ) : !forceEmptyText && !isLabelHidden ? ( {emptyLabel} ) : ( diff --git a/packages/twenty-ui/src/display/tooltip/OverflowingTextWithTooltip.tsx b/packages/twenty-ui/src/display/tooltip/OverflowingTextWithTooltip.tsx index fcd0f5550a..21303f0426 100644 --- a/packages/twenty-ui/src/display/tooltip/OverflowingTextWithTooltip.tsx +++ b/packages/twenty-ui/src/display/tooltip/OverflowingTextWithTooltip.tsx @@ -3,8 +3,8 @@ import { type ReactNode, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { isNonEmptyString } from '@sniptt/guards'; -import { isDefined } from 'twenty-shared/utils'; import { themeCssVariables } from '@ui/theme-constants'; +import { isDefined } from 'twenty-shared/utils'; import { AppTooltip, TooltipDelay } from './AppTooltip'; const spacing4 = themeCssVariables.spacing[4]; @@ -64,6 +64,7 @@ type OverflowingTextWithTooltipProps = { isTooltipMultiline?: boolean; displayedMaxRows?: number; tooltipDelay?: TooltipDelay; + alwaysShowTooltip?: boolean; } & ( | { text: string | null | undefined; @@ -82,6 +83,7 @@ export const OverflowingTextWithTooltip = ({ displayedMaxRows, tooltipContent, tooltipDelay = TooltipDelay.mediumDelay, + alwaysShowTooltip = false, }: OverflowingTextWithTooltipProps) => { const textElementId = `title-id-${+new Date()}`; @@ -146,7 +148,7 @@ export const OverflowingTextWithTooltip = ({ )} {shouldRenderTooltip && - isTitleOverflowing && + (isTitleOverflowing || alwaysShowTooltip) && isDefined(tooltipText) && createPortal(