fix: show disabled file chip when file no longer exists in timeline (#19045)

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>
This commit is contained in:
BugIsGod
2026-03-31 11:07:19 +01:00
committed by GitHub
parent e0630b8653
commit bcca5d0002
5 changed files with 71 additions and 18 deletions
@@ -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 <></>;
@@ -335,4 +335,5 @@ export type FieldFilesValue = {
extension?: string;
url?: string;
fileCategory?: FileCategory;
isDeleted?: boolean;
};
@@ -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 (
<StyledClickableContainer
clickable={isClickable}
onMouseDown={handleMouseDown}
>
<Chip
label={file.label ?? ''}
maxWidth={MAX_WIDTH}
leftComponent={fileIcon}
variant={ChipVariant.Highlighted}
<>
<StyledClickableContainer
clickable={isClickable}
/>
</StyledClickableContainer>
onMouseDown={handleMouseDown}
>
<Chip
label={file.label}
alwaysShowTooltip={isDeleted}
tooltipLabel={
isDeleted ? t`File no longer exists - ${file.label}` : undefined
}
disabled={isDeleted}
maxWidth={MAX_WIDTH}
leftComponent={fileIcon}
variant={isDeleted ? ChipVariant.Static : ChipVariant.Highlighted}
clickable={isClickable}
/>
</StyledClickableContainer>
</>
);
};
@@ -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) ? (
<OverflowingTextWithTooltip size={size} text={label} />
<OverflowingTextWithTooltip
size={size}
text={label}
tooltipContent={tooltipLabel}
alwaysShowTooltip={alwaysShowTooltip}
/>
) : !forceEmptyText && !isLabelHidden ? (
<StyledDiv>{emptyLabel}</StyledDiv>
) : (
@@ -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(
<div onClick={handleTooltipClick}>