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:
+35
-3
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user