From 33edcc04a66b8776ef4927b6a900feca78ff391e Mon Sep 17 00:00:00 2001 From: Francis Terrero Date: Tue, 6 Jan 2026 23:54:47 -0400 Subject: [PATCH 1/5] feature: add auto-delete column to trash view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new "Auto-delete" column in the trash list that displays the number of days until an item is permanently deleted. The column shows "In X day(s)" with a warning icon, and highlights items in red when they have 2 days or less remaining. Changes: - Add caducityDate field to DriveFileData and DriveFolderData types - Add "Auto-delete" translation key to English locale - Create skinSkeletonTrash for trash view loading state - Add auto-delete column header to trash list (non-orderable) - Display auto-delete countdown with WarningCircle icon in list items - Calculate days until deletion from caducityDate field - Apply red text styling for urgent items (≤2 days remaining) --- src/app/drive/types/index.ts | 2 ++ src/app/i18n/locales/en.json | 3 +- src/components/Skeleton.tsx | 10 ++++++ .../components/DriveExplorerList.tsx | 15 ++++++-- .../components/DriveExplorerListItem.tsx | 35 ++++++++++++++++++- 5 files changed, 61 insertions(+), 4 deletions(-) diff --git a/src/app/drive/types/index.ts b/src/app/drive/types/index.ts index 64f0be9bd3..34dd3b9ebb 100644 --- a/src/app/drive/types/index.ts +++ b/src/app/drive/types/index.ts @@ -30,6 +30,7 @@ export interface DriveFolderData { uuid: string; type?: string; user?: UserResumeData; + caducityDate?: string; } export interface DriveFolderMetadataPayload { @@ -63,6 +64,7 @@ export interface DriveFileData { sharings?: { type: string; id: string }[]; uuid: string; user?: UserResumeData; + caducityDate?: string; } interface Thumbnail { diff --git a/src/app/i18n/locales/en.json b/src/app/i18n/locales/en.json index 40c2e4f912..726e2f2612 100644 --- a/src/app/i18n/locales/en.json +++ b/src/app/i18n/locales/en.json @@ -1536,7 +1536,8 @@ "name": "Name", "modified": "Modified", "size": "Size", - "actions": "Actions" + "actions": "Actions", + "autoDelete": "Auto-delete" } }, "viewMode": { diff --git a/src/components/Skeleton.tsx b/src/components/Skeleton.tsx index 76110e6c73..56bdbd421d 100644 --- a/src/components/Skeleton.tsx +++ b/src/components/Skeleton.tsx @@ -7,3 +7,13 @@ export const skinSkeleton = [
,
, ]; + +export const skinSkeletonTrash = [ +
+
+
+
, +
, +
, +
, +]; diff --git a/src/views/Drive/components/DriveExplorer/components/DriveExplorerList.tsx b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList.tsx index b3749e34ba..597e7de33a 100644 --- a/src/views/Drive/components/DriveExplorer/components/DriveExplorerList.tsx +++ b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList.tsx @@ -7,7 +7,7 @@ import { connect, useSelector } from 'react-redux'; import { ListShareLinksItem, Role } from '@internxt/sdk/dist/drive/share/types'; import navigationService from 'services/navigation.service'; import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; -import { skinSkeleton } from 'components/Skeleton'; +import { skinSkeleton, skinSkeletonTrash } from 'components/Skeleton'; import { moveItemsToTrash } from '../../../../../views/Trash/services'; import { OrderDirection, OrderSettings } from 'app/core/types'; import shareService from 'app/share/services/share.service'; @@ -462,6 +462,17 @@ const DriveExplorerList: React.FC = memo((props) => { buttonDataCy: 'driveListHeaderNameButton', textDataCy: 'driveListHeaderNameButtonText', }, + ...(isTrash + ? [ + { + label: translate('drive.list.columns.autoDelete'), + width: 'w-date', + name: 'updatedAt' as const, + orderable: false, + defaultDirection: 'ASC' as const, + }, + ] + : []), { label: translate('drive.list.columns.modified'), width: 'w-date', @@ -485,7 +496,7 @@ const DriveExplorerList: React.FC = memo((props) => { itemComposition={[(item) => createDriveListItem(item, props.isTrash)]} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} - skinSkeleton={skinSkeleton} + skinSkeleton={isTrash ? skinSkeletonTrash : skinSkeleton} emptyState={<>} onNextPage={onEndOfScroll} onEnterPressed={(driveItem) => { diff --git a/src/views/Drive/components/DriveExplorer/components/DriveExplorerListItem.tsx b/src/views/Drive/components/DriveExplorer/components/DriveExplorerListItem.tsx index 8ce573bc36..982d50557a 100644 --- a/src/views/Drive/components/DriveExplorer/components/DriveExplorerListItem.tsx +++ b/src/views/Drive/components/DriveExplorer/components/DriveExplorerListItem.tsx @@ -9,6 +9,7 @@ import iconService from 'app/drive/services/icon.service'; import { useDriveItemActions, useDriveItemDrag, useDriveItemDrop, useDriveItemStoreProps } from '../../../hooks'; import './DriveExplorerListItem.scss'; import { t } from 'i18next'; +import { WarningCircle } from '@phosphor-icons/react'; const getItemClassNames = (isSelected: boolean, isDraggingOver: boolean, isDragging: boolean): string => { const selectedClass = isSelected ? 'selected' : ''; @@ -21,7 +22,26 @@ const isItemInteractive = (item: DriveExplorerItemProps['item']): boolean => { return (item.isFolder && !item.deleted) || (!item.isFolder && item.status === 'EXISTS'); }; -const DriveExplorerListItem = ({ item }: DriveExplorerItemProps): JSX.Element => { +const calculateDaysUntilAutoDelete = (caducityDate?: string): number => { + if (!caducityDate) return 0; + + const deletionDate = new Date(caducityDate); + const now = new Date(); + const diffTime = deletionDate.getTime() - now.getTime(); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + return Math.max(diffDays, 0); +}; + +const getAutoDeleteDisplay = (days: number): { text: string; isUrgent: boolean } => { + const isUrgent = days <= 2; + const dayText = days === 1 ? 'day' : 'days'; + return { + text: `In ${days} ${dayText}`, + isUrgent, + }; +}; + +const DriveExplorerListItem = ({ item, isTrash }: DriveExplorerItemProps): JSX.Element => { const { isItemSelected, isEditingName } = useDriveItemStoreProps(); const { nameInputRef, onNameClicked, onItemClicked, onItemDoubleClicked, downloadAndSetThumbnail } = useDriveItemActions(item); @@ -30,6 +50,9 @@ const DriveExplorerListItem = ({ item }: DriveExplorerItemProps): JSX.Element => const { connectDropTarget, isDraggingOverThisItem } = useDriveItemDrop(item); const ItemIconComponent = iconService.getItemIcon(item.isFolder, item.type); + const daysUntilDelete = isTrash ? calculateDaysUntilAutoDelete(item.caducityDate) : 0; + const autoDeleteInfo = isTrash && daysUntilDelete > 0 ? getAutoDeleteDisplay(daysUntilDelete) : null; + useEffect(() => { if (isEditingName(item)) { const current = nameInputRef.current; @@ -111,6 +134,16 @@ const DriveExplorerListItem = ({ item }: DriveExplorerItemProps): JSX.Element => isInteractive && connectDropTarget(
) } + {/* AUTO-DELETE (only for trash) */} + {isTrash && autoDeleteInfo && ( +
+
+ + {autoDeleteInfo.text} +
+
+ )} + {/* DATE */}
{dateService.formatDefaultDate(item.updatedAt, t)} From df53d43e176019564a13aeef558492a0f3505106 Mon Sep 17 00:00:00 2001 From: Francis Terrero Date: Tue, 6 Jan 2026 23:59:12 -0400 Subject: [PATCH 2/5] refactor: improve auto-delete column implementation and add tests --- src/services/date.service.test.ts | 31 ++++++ src/services/date.service.ts | 10 ++ .../components/DriveExplorerList.tsx | 100 +++++++++++------- .../components/DriveExplorerListItem.tsx | 12 +-- 4 files changed, 104 insertions(+), 49 deletions(-) diff --git a/src/services/date.service.test.ts b/src/services/date.service.test.ts index d29e6215ca..b1526e9d1a 100644 --- a/src/services/date.service.test.ts +++ b/src/services/date.service.test.ts @@ -35,4 +35,35 @@ describe('dateService', () => { expect(isBefore).toBe(false); }); + + describe('calculateDaysUntilDate', () => { + test('when target date is undefined, then returns 0', () => { + const result = dateService.calculateDaysUntilDate(); + expect(result).toBe(0); + }); + + test('when target date is in the past, then returns 0', () => { + const pastDate = dayjs().subtract(5, 'day').toISOString(); + const result = dateService.calculateDaysUntilDate(pastDate); + expect(result).toBe(0); + }); + + test('when target date is 10 days in the future, then returns 10', () => { + const futureDate = dayjs().add(10, 'day').toISOString(); + const result = dateService.calculateDaysUntilDate(futureDate); + expect(result).toBe(10); + }); + + test('when target date is tomorrow, then returns 1', () => { + const tomorrow = dayjs().add(1, 'day').toISOString(); + const result = dateService.calculateDaysUntilDate(tomorrow); + expect(result).toBe(1); + }); + + test('when target date is a Date object 30 days ahead, then returns 30', () => { + const futureDate = dayjs().add(30, 'day').toDate(); + const result = dateService.calculateDaysUntilDate(futureDate); + expect(result).toBe(30); + }); + }); }); diff --git a/src/services/date.service.ts b/src/services/date.service.ts index 518395ed7f..75a7599988 100644 --- a/src/services/date.service.ts +++ b/src/services/date.service.ts @@ -28,6 +28,15 @@ export const formatDefaultDate = (date: Date | string | number, translate: (key: return dayjs(date).format(`D MMM, YYYY [${translatedAt}] HH:mm`); }; +function calculateDaysUntilDate(targetDate?: string | Date): number { + if (!targetDate) return 0; + + const target = dayjs(targetDate); + const now = dayjs(); + const diffDays = target.diff(now, 'day'); + return Math.max(diffDays, 0); +} + const dateService = { format, fromNow, @@ -35,6 +44,7 @@ const dateService = { getCurrentDate, getExpirationDate, formatDefaultDate, + calculateDaysUntilDate, }; export default dateService; diff --git a/src/views/Drive/components/DriveExplorer/components/DriveExplorerList.tsx b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList.tsx index 597e7de33a..bed99ac6c3 100644 --- a/src/views/Drive/components/DriveExplorer/components/DriveExplorerList.tsx +++ b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList.tsx @@ -57,7 +57,7 @@ interface DriveExplorerListProps { type ObjectWithId = { id: string | number }; -type SortField = 'type' | 'name' | 'updatedAt' | 'size'; +type SortField = 'type' | 'name' | 'updatedAt' | 'size' | 'caducityDate'; type ContextMenuDriveItem = DriveItemData | Pick | (ListShareLinksItem & { code: string }); @@ -103,6 +103,59 @@ const resetDriveOrder = ({ dispatch(fetchSortedFolderContentThunk(currentFolderId)); }; +interface ListHeaderItem { + label: string; + width: string; + name: 'type' | 'name' | 'updatedAt' | 'size' | 'caducityDate'; + orderable: boolean; + defaultDirection: 'ASC' | 'DESC'; + buttonDataCy?: string; + textDataCy?: string; +} + +const getListHeaders = (translate: (key: string) => string, isRecents: boolean, isTrash: boolean): ListHeaderItem[] => { + const headers: ListHeaderItem[] = [ + { + label: translate('drive.list.columns.name'), + width: 'flex grow items-center min-w-driveNameHeader', + name: 'name', + orderable: !isRecents, + defaultDirection: 'ASC', + buttonDataCy: 'driveListHeaderNameButton', + textDataCy: 'driveListHeaderNameButtonText', + }, + ]; + + if (isTrash) { + headers.push({ + label: translate('drive.list.columns.autoDelete'), + width: 'w-date', + name: 'caducityDate', + orderable: true, + defaultDirection: 'ASC', + }); + } + + headers.push( + { + label: translate('drive.list.columns.modified'), + width: 'w-date', + name: 'updatedAt', + orderable: !isRecents, + defaultDirection: 'ASC', + }, + { + label: translate('drive.list.columns.size'), + orderable: !isRecents && !isTrash, + defaultDirection: 'ASC', + width: 'w-size', + name: 'size', + }, + ); + + return headers; +}; + const DriveExplorerList: React.FC = memo((props) => { const { dispatch, isLoading, order, hasMoreItems, onEndOfScroll, forceLoading, roles } = props; const selectedWorkspace = useSelector(workspacesSelectors.getSelectedWorkspace); @@ -167,6 +220,12 @@ const DriveExplorerList: React.FC = memo((props) => { if (value.field === 'size') { resetDriveOrder({ dispatch, orderType: 'size', direction, currentFolderId }); } + + if (value.field === 'caducityDate') { + if (isTrash) { + props.resetPaginationState(); + } + } }; function handleMouseEnter() { @@ -451,43 +510,8 @@ const DriveExplorerList: React.FC = memo((props) => { /> )} - - header={[ - { - label: translate('drive.list.columns.name'), - width: 'flex grow items-center min-w-driveNameHeader', - name: 'name', - orderable: !isRecents, - defaultDirection: 'ASC', - buttonDataCy: 'driveListHeaderNameButton', - textDataCy: 'driveListHeaderNameButtonText', - }, - ...(isTrash - ? [ - { - label: translate('drive.list.columns.autoDelete'), - width: 'w-date', - name: 'updatedAt' as const, - orderable: false, - defaultDirection: 'ASC' as const, - }, - ] - : []), - { - label: translate('drive.list.columns.modified'), - width: 'w-date', - name: 'updatedAt', - orderable: !isRecents, - defaultDirection: 'ASC', - }, - { - label: translate('drive.list.columns.size'), - orderable: !isRecents && !isTrash, - defaultDirection: 'ASC', - width: 'w-size', - name: 'size', - }, - ]} + + header={getListHeaders(translate, isRecents, isTrash)} checkboxDataCy="driveListHeaderCheckbox" disableKeyboardShortcuts={props.disableKeyboardShortcuts || props.showStopSharingConfirmation} items={props.items} diff --git a/src/views/Drive/components/DriveExplorer/components/DriveExplorerListItem.tsx b/src/views/Drive/components/DriveExplorer/components/DriveExplorerListItem.tsx index 982d50557a..7358cd4cd4 100644 --- a/src/views/Drive/components/DriveExplorer/components/DriveExplorerListItem.tsx +++ b/src/views/Drive/components/DriveExplorer/components/DriveExplorerListItem.tsx @@ -22,16 +22,6 @@ const isItemInteractive = (item: DriveExplorerItemProps['item']): boolean => { return (item.isFolder && !item.deleted) || (!item.isFolder && item.status === 'EXISTS'); }; -const calculateDaysUntilAutoDelete = (caducityDate?: string): number => { - if (!caducityDate) return 0; - - const deletionDate = new Date(caducityDate); - const now = new Date(); - const diffTime = deletionDate.getTime() - now.getTime(); - const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); - return Math.max(diffDays, 0); -}; - const getAutoDeleteDisplay = (days: number): { text: string; isUrgent: boolean } => { const isUrgent = days <= 2; const dayText = days === 1 ? 'day' : 'days'; @@ -50,7 +40,7 @@ const DriveExplorerListItem = ({ item, isTrash }: DriveExplorerItemProps): JSX.E const { connectDropTarget, isDraggingOverThisItem } = useDriveItemDrop(item); const ItemIconComponent = iconService.getItemIcon(item.isFolder, item.type); - const daysUntilDelete = isTrash ? calculateDaysUntilAutoDelete(item.caducityDate) : 0; + const daysUntilDelete = isTrash ? dateService.calculateDaysUntilDate(item.caducityDate) : 0; const autoDeleteInfo = isTrash && daysUntilDelete > 0 ? getAutoDeleteDisplay(daysUntilDelete) : null; useEffect(() => { From ef0c1cba9506677a9e2d14bdb6846ef267998bf7 Mon Sep 17 00:00:00 2001 From: Francis Terrero Date: Wed, 7 Jan 2026 00:14:27 -0400 Subject: [PATCH 3/5] feature: add i18n support for auto-delete countdown --- src/app/i18n/locales/de.json | 7 ++++++- src/app/i18n/locales/en.json | 4 ++++ src/app/i18n/locales/es.json | 7 ++++++- src/app/i18n/locales/fr.json | 7 ++++++- src/app/i18n/locales/it.json | 7 ++++++- src/app/i18n/locales/ru.json | 7 ++++++- src/app/i18n/locales/tw.json | 7 ++++++- src/app/i18n/locales/zh.json | 7 ++++++- .../DriveExplorer/components/DriveExplorerList.tsx | 3 ++- .../DriveExplorer/components/DriveExplorerListItem.tsx | 10 ++++++---- 10 files changed, 54 insertions(+), 12 deletions(-) diff --git a/src/app/i18n/locales/de.json b/src/app/i18n/locales/de.json index 3576fd6721..22ef4311b3 100644 --- a/src/app/i18n/locales/de.json +++ b/src/app/i18n/locales/de.json @@ -1164,6 +1164,10 @@ "restore": "Wiederherstellen", "delete-permanently": "Lösche dauerhaft" }, + "autoDelete": { + "inDays_one": "In {{count}} Tag", + "inDays_other": "In {{count}} Tagen" + }, "automaticDisposal": { "badge": "Neues Update", "title": "Automatische Papierkorb-Entsorgung", @@ -1454,7 +1458,8 @@ "name": "Name", "modified": "Modifiziert", "size": "Größe", - "actions": "Aktionen" + "actions": "Aktionen", + "autoDelete": "Automatisches Löschen" } }, "viewMode": { diff --git a/src/app/i18n/locales/en.json b/src/app/i18n/locales/en.json index 726e2f2612..e1844a2e8c 100644 --- a/src/app/i18n/locales/en.json +++ b/src/app/i18n/locales/en.json @@ -1249,6 +1249,10 @@ "restore": "Restore", "delete-permanently": "Delete permanently" }, + "autoDelete": { + "inDays_one": "In {{count}} day", + "inDays_other": "In {{count}} days" + }, "automaticDisposal": { "badge": "New update", "title": "Automatic Trash disposal", diff --git a/src/app/i18n/locales/es.json b/src/app/i18n/locales/es.json index 914c273228..ee2eb3d0ff 100644 --- a/src/app/i18n/locales/es.json +++ b/src/app/i18n/locales/es.json @@ -1226,6 +1226,10 @@ "restore": "Restaurar", "delete-permanently": "Eliminar permanentemente" }, + "autoDelete": { + "inDays_one": "En {{count}} día", + "inDays_other": "En {{count}} días" + }, "automaticDisposal": { "badge": "Nueva actualización", "title": "Eliminación automática de la Papelera", @@ -1514,7 +1518,8 @@ "name": "Nombre", "modified": "Modificado", "size": "Tamaño", - "actions": "Acciones" + "actions": "Acciones", + "autoDelete": "Eliminación automática" } }, "viewMode": { diff --git a/src/app/i18n/locales/fr.json b/src/app/i18n/locales/fr.json index 96bfef8e97..3709a48908 100644 --- a/src/app/i18n/locales/fr.json +++ b/src/app/i18n/locales/fr.json @@ -1177,6 +1177,10 @@ "restore": "Restaurer", "delete-permanently": "Supprimer définitivement" }, + "autoDelete": { + "inDays_one": "Dans {{count}} jour", + "inDays_other": "Dans {{count}} jours" + }, "automaticDisposal": { "badge": "Nouvelle mise à jour", "title": "Élimination automatique de la Corbeille", @@ -1460,7 +1464,8 @@ "name": "Nom", "modified": "Modifié", "size": "Taille", - "actions": "Actions" + "actions": "Actions", + "autoDelete": "Suppression automatique" } }, "viewMode": { diff --git a/src/app/i18n/locales/it.json b/src/app/i18n/locales/it.json index 0969174182..9d37c04d25 100644 --- a/src/app/i18n/locales/it.json +++ b/src/app/i18n/locales/it.json @@ -1284,6 +1284,10 @@ "restore": "Ripristino", "delete-permanently": "Elimina definitivamente" }, + "autoDelete": { + "inDays_one": "Tra {{count}} giorno", + "inDays_other": "Tra {{count}} giorni" + }, "automaticDisposal": { "badge": "Nuovo aggiornamento", "title": "Eliminazione automatica del Cestino", @@ -1567,7 +1571,8 @@ "name": "Nome", "modified": "Ultima modifica", "size": "Dimensione", - "actions": "Azioni" + "actions": "Azioni", + "autoDelete": "Eliminazione automatica" } }, "viewMode": { diff --git a/src/app/i18n/locales/ru.json b/src/app/i18n/locales/ru.json index 5e549f2f51..91db029fbc 100644 --- a/src/app/i18n/locales/ru.json +++ b/src/app/i18n/locales/ru.json @@ -1190,6 +1190,10 @@ "restore": "Восстановить", "delete-permanently": "Удалить навсегда" }, + "autoDelete": { + "inDays_one": "Через {{count}} день", + "inDays_other": "Через {{count}} дней" + }, "automaticDisposal": { "badge": "Новое обновление", "title": "Автоматическая очистка Корзины", @@ -1473,7 +1477,8 @@ "name": "Имя", "modified": "Изменено", "size": "Размер", - "actions": "Действия" + "actions": "Действия", + "autoDelete": "Автоматическое удаление" } }, "viewMode": { diff --git a/src/app/i18n/locales/tw.json b/src/app/i18n/locales/tw.json index da9c8fe92e..eabbd975e4 100644 --- a/src/app/i18n/locales/tw.json +++ b/src/app/i18n/locales/tw.json @@ -1179,6 +1179,10 @@ "restore": "還原", "delete-permanently": "永久刪除" }, + "autoDelete": { + "inDays_one": "{{count}} 天後", + "inDays_other": "{{count}} 天後" + }, "automaticDisposal": { "badge": "新更新", "title": "自動清空垃圾桶", @@ -1466,7 +1470,8 @@ "name": "名稱", "modified": "修改日期", "size": "大小", - "actions": "操作" + "actions": "操作", + "autoDelete": "自動刪除" } }, "viewMode": { diff --git a/src/app/i18n/locales/zh.json b/src/app/i18n/locales/zh.json index be90f57298..8bec6dc6e1 100644 --- a/src/app/i18n/locales/zh.json +++ b/src/app/i18n/locales/zh.json @@ -1214,6 +1214,10 @@ "restore": "恢复", "delete-permanently": "永久删除" }, + "autoDelete": { + "inDays_one": "{{count}} 天后", + "inDays_other": "{{count}} 天后" + }, "automaticDisposal": { "badge": "新更新", "title": "自动清空垃圾箱", @@ -1501,7 +1505,8 @@ "name": "名称", "modified": "修改日期", "size": "大小", - "actions": "操作" + "actions": "操作", + "autoDelete": "自动删除" } }, "viewMode": { diff --git a/src/views/Drive/components/DriveExplorer/components/DriveExplorerList.tsx b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList.tsx index bed99ac6c3..9d624a08f4 100644 --- a/src/views/Drive/components/DriveExplorer/components/DriveExplorerList.tsx +++ b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList.tsx @@ -192,6 +192,7 @@ const DriveExplorerList: React.FC = memo((props) => { const currentFolderId = useAppSelector(storageSelectors.currentFolderId); const isRecents = props.title === translate('views.recents.head'); const isTrash = props.title === translate('trash.trash'); + const skeleton = isTrash ? skinSkeletonTrash : skinSkeleton; const sortBy = (value: { field: SortField; direction: 'ASC' | 'DESC' }) => { let direction = OrderDirection.Asc; @@ -520,7 +521,7 @@ const DriveExplorerList: React.FC = memo((props) => { itemComposition={[(item) => createDriveListItem(item, props.isTrash)]} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} - skinSkeleton={isTrash ? skinSkeletonTrash : skinSkeleton} + skinSkeleton={skeleton} emptyState={<>} onNextPage={onEndOfScroll} onEnterPressed={(driveItem) => { diff --git a/src/views/Drive/components/DriveExplorer/components/DriveExplorerListItem.tsx b/src/views/Drive/components/DriveExplorer/components/DriveExplorerListItem.tsx index 7358cd4cd4..e391e56134 100644 --- a/src/views/Drive/components/DriveExplorer/components/DriveExplorerListItem.tsx +++ b/src/views/Drive/components/DriveExplorer/components/DriveExplorerListItem.tsx @@ -22,11 +22,13 @@ const isItemInteractive = (item: DriveExplorerItemProps['item']): boolean => { return (item.isFolder && !item.deleted) || (!item.isFolder && item.status === 'EXISTS'); }; -const getAutoDeleteDisplay = (days: number): { text: string; isUrgent: boolean } => { +const getAutoDeleteDisplay = ( + days: number, + translate: (key: string, options?: { count?: number }) => string, +): { text: string; isUrgent: boolean } => { const isUrgent = days <= 2; - const dayText = days === 1 ? 'day' : 'days'; return { - text: `In ${days} ${dayText}`, + text: translate('trash.autoDelete.inDays', { count: days }), isUrgent, }; }; @@ -41,7 +43,7 @@ const DriveExplorerListItem = ({ item, isTrash }: DriveExplorerItemProps): JSX.E const ItemIconComponent = iconService.getItemIcon(item.isFolder, item.type); const daysUntilDelete = isTrash ? dateService.calculateDaysUntilDate(item.caducityDate) : 0; - const autoDeleteInfo = isTrash && daysUntilDelete > 0 ? getAutoDeleteDisplay(daysUntilDelete) : null; + const autoDeleteInfo = isTrash && daysUntilDelete > 0 ? getAutoDeleteDisplay(daysUntilDelete, t) : null; useEffect(() => { if (isEditingName(item)) { From aa07b6e67594e8ddf950b1d2f2c4458baa1467d7 Mon Sep 17 00:00:00 2001 From: Francis Terrero Date: Wed, 14 Jan 2026 10:42:26 -0400 Subject: [PATCH 4/5] refactor: reorganize DriveExplorerList into dedicated directory with tests --- .../DriveExplorerList.tsx | 58 +------------------ .../DriveExplorerListItem.scss | 0 .../DriveExplorerListItem.tsx | 23 +++++--- .../DriveExplorerList/getListHeaders.test.ts | 36 ++++++++++++ .../DriveExplorerList/getListHeaders.ts | 56 ++++++++++++++++++ .../components/DriveExplorerList/index.ts | 1 + 6 files changed, 110 insertions(+), 64 deletions(-) rename src/views/Drive/components/DriveExplorer/components/{ => DriveExplorerList}/DriveExplorerList.tsx (92%) rename src/views/Drive/components/DriveExplorer/components/{ => DriveExplorerList}/DriveExplorerListItem.scss (100%) rename src/views/Drive/components/DriveExplorer/components/{ => DriveExplorerList}/DriveExplorerListItem.tsx (89%) create mode 100644 src/views/Drive/components/DriveExplorer/components/DriveExplorerList/getListHeaders.test.ts create mode 100644 src/views/Drive/components/DriveExplorer/components/DriveExplorerList/getListHeaders.ts create mode 100644 src/views/Drive/components/DriveExplorer/components/DriveExplorerList/index.ts diff --git a/src/views/Drive/components/DriveExplorer/components/DriveExplorerList.tsx b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerList.tsx similarity index 92% rename from src/views/Drive/components/DriveExplorer/components/DriveExplorerList.tsx rename to src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerList.tsx index 9d624a08f4..574be21919 100644 --- a/src/views/Drive/components/DriveExplorer/components/DriveExplorerList.tsx +++ b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerList.tsx @@ -3,12 +3,13 @@ import storageSelectors from 'app/store/slices/storage/storage.selectors'; import { fetchSortedFolderContentThunk } from 'app/store/slices/storage/storage.thunks/fetchSortedFolderContentThunk'; import React, { memo, useCallback, useState } from 'react'; import { connect, useSelector } from 'react-redux'; +import { getListHeaders } from './getListHeaders'; import { ListShareLinksItem, Role } from '@internxt/sdk/dist/drive/share/types'; import navigationService from 'services/navigation.service'; import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; import { skinSkeleton, skinSkeletonTrash } from 'components/Skeleton'; -import { moveItemsToTrash } from '../../../../../views/Trash/services'; +import { moveItemsToTrash } from 'views/Trash/services'; import { OrderDirection, OrderSettings } from 'app/core/types'; import shareService from 'app/share/services/share.service'; import { AppDispatch, RootState } from 'app/store'; @@ -31,7 +32,7 @@ import { contextMenuTrashItems, contextMenuWorkspaceFile, contextMenuWorkspaceFolder, -} from './DriveItemContextMenu'; +} from '../DriveItemContextMenu'; import { List } from '@internxt/ui'; import { DownloadManager } from 'app/network/DownloadManager'; @@ -103,59 +104,6 @@ const resetDriveOrder = ({ dispatch(fetchSortedFolderContentThunk(currentFolderId)); }; -interface ListHeaderItem { - label: string; - width: string; - name: 'type' | 'name' | 'updatedAt' | 'size' | 'caducityDate'; - orderable: boolean; - defaultDirection: 'ASC' | 'DESC'; - buttonDataCy?: string; - textDataCy?: string; -} - -const getListHeaders = (translate: (key: string) => string, isRecents: boolean, isTrash: boolean): ListHeaderItem[] => { - const headers: ListHeaderItem[] = [ - { - label: translate('drive.list.columns.name'), - width: 'flex grow items-center min-w-driveNameHeader', - name: 'name', - orderable: !isRecents, - defaultDirection: 'ASC', - buttonDataCy: 'driveListHeaderNameButton', - textDataCy: 'driveListHeaderNameButtonText', - }, - ]; - - if (isTrash) { - headers.push({ - label: translate('drive.list.columns.autoDelete'), - width: 'w-date', - name: 'caducityDate', - orderable: true, - defaultDirection: 'ASC', - }); - } - - headers.push( - { - label: translate('drive.list.columns.modified'), - width: 'w-date', - name: 'updatedAt', - orderable: !isRecents, - defaultDirection: 'ASC', - }, - { - label: translate('drive.list.columns.size'), - orderable: !isRecents && !isTrash, - defaultDirection: 'ASC', - width: 'w-size', - name: 'size', - }, - ); - - return headers; -}; - const DriveExplorerList: React.FC = memo((props) => { const { dispatch, isLoading, order, hasMoreItems, onEndOfScroll, forceLoading, roles } = props; const selectedWorkspace = useSelector(workspacesSelectors.getSelectedWorkspace); diff --git a/src/views/Drive/components/DriveExplorer/components/DriveExplorerListItem.scss b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerListItem.scss similarity index 100% rename from src/views/Drive/components/DriveExplorer/components/DriveExplorerListItem.scss rename to src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerListItem.scss diff --git a/src/views/Drive/components/DriveExplorer/components/DriveExplorerListItem.tsx b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerListItem.tsx similarity index 89% rename from src/views/Drive/components/DriveExplorer/components/DriveExplorerListItem.tsx rename to src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerListItem.tsx index e391e56134..04b851e014 100644 --- a/src/views/Drive/components/DriveExplorer/components/DriveExplorerListItem.tsx +++ b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerListItem.tsx @@ -1,12 +1,12 @@ import { items } from '@internxt/lib'; import usersIcon from 'assets/icons/users.svg'; -import { useEffect } from 'react'; -import { DriveExplorerItemProps } from '../types'; +import { useEffect, useMemo } from 'react'; +import { DriveExplorerItemProps } from '../../types'; import dateService from 'services/date.service'; import transformItemService from 'app/drive/services/item-transform.service'; import sizeService from 'app/drive/services/size.service'; import iconService from 'app/drive/services/icon.service'; -import { useDriveItemActions, useDriveItemDrag, useDriveItemDrop, useDriveItemStoreProps } from '../../../hooks'; +import { useDriveItemActions, useDriveItemDrag, useDriveItemDrop, useDriveItemStoreProps } from '../../../../hooks'; import './DriveExplorerListItem.scss'; import { t } from 'i18next'; import { WarningCircle } from '@phosphor-icons/react'; @@ -22,11 +22,13 @@ const isItemInteractive = (item: DriveExplorerItemProps['item']): boolean => { return (item.isFolder && !item.deleted) || (!item.isFolder && item.status === 'EXISTS'); }; -const getAutoDeleteDisplay = ( +const URGENT_AUTO_DELETE_THRESHOLD_DAYS = 2; + +const getAutoDeleteStatusInfo = ( days: number, translate: (key: string, options?: { count?: number }) => string, ): { text: string; isUrgent: boolean } => { - const isUrgent = days <= 2; + const isUrgent = days <= URGENT_AUTO_DELETE_THRESHOLD_DAYS; return { text: translate('trash.autoDelete.inDays', { count: days }), isUrgent, @@ -43,7 +45,10 @@ const DriveExplorerListItem = ({ item, isTrash }: DriveExplorerItemProps): JSX.E const ItemIconComponent = iconService.getItemIcon(item.isFolder, item.type); const daysUntilDelete = isTrash ? dateService.calculateDaysUntilDate(item.caducityDate) : 0; - const autoDeleteInfo = isTrash && daysUntilDelete > 0 ? getAutoDeleteDisplay(daysUntilDelete, t) : null; + const autoDeleteStatusInfo = useMemo( + () => (isTrash && daysUntilDelete > 0 ? getAutoDeleteStatusInfo(daysUntilDelete, t) : null), + [isTrash, daysUntilDelete], + ); useEffect(() => { if (isEditingName(item)) { @@ -127,11 +132,11 @@ const DriveExplorerListItem = ({ item, isTrash }: DriveExplorerItemProps): JSX.E } {/* AUTO-DELETE (only for trash) */} - {isTrash && autoDeleteInfo && ( + {isTrash && autoDeleteStatusInfo && (
-
+
- {autoDeleteInfo.text} + {autoDeleteStatusInfo.text}
)} diff --git a/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/getListHeaders.test.ts b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/getListHeaders.test.ts new file mode 100644 index 0000000000..dfd6e7311b --- /dev/null +++ b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/getListHeaders.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, test, vi } from 'vitest'; +import { getListHeaders } from './getListHeaders'; + +describe('List column headers', () => { + const mockTranslate = vi.fn((key: string) => key); + + test('when viewing the regular drive, then it returns name, modified date, and size columns', () => { + const headers = getListHeaders(mockTranslate, false, false); + + expect(headers).toHaveLength(3); + expect(headers[0].name).toBe('name'); + expect(headers[1].name).toBe('updatedAt'); + expect(headers[2].name).toBe('size'); + }); + + test('when viewing the trash, then it includes a sortable auto-delete column and disables size sorting', () => { + const headers = getListHeaders(mockTranslate, false, true); + + expect(headers).toHaveLength(4); + + const caducityHeader = headers.find((h) => h.name === 'caducityDate'); + expect(caducityHeader?.label).toBe('drive.list.columns.autoDelete'); + expect(caducityHeader?.orderable).toBe(true); + + const sizeHeader = headers.find((h) => h.name === 'size'); + expect(sizeHeader?.orderable).toBe(false); + }); + + test('when viewing recent files, then all columns cannot be sorted', () => { + const headers = getListHeaders(mockTranslate, true, false); + + headers.forEach((header) => { + expect(header.orderable).toBe(false); + }); + }); +}); diff --git a/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/getListHeaders.ts b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/getListHeaders.ts new file mode 100644 index 0000000000..00a60f2997 --- /dev/null +++ b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/getListHeaders.ts @@ -0,0 +1,56 @@ +interface ListHeaderItem { + label: string; + width: string; + name: 'type' | 'name' | 'updatedAt' | 'size' | 'caducityDate'; + orderable: boolean; + defaultDirection: 'ASC' | 'DESC'; + buttonDataCy?: string; + textDataCy?: string; +} + +export const getListHeaders = ( + translate: (key: string) => string, + isRecents: boolean, + isTrash: boolean, +): ListHeaderItem[] => { + const headers: ListHeaderItem[] = [ + { + label: translate('drive.list.columns.name'), + width: 'flex grow items-center min-w-driveNameHeader', + name: 'name', + orderable: !isRecents, + defaultDirection: 'ASC', + buttonDataCy: 'driveListHeaderNameButton', + textDataCy: 'driveListHeaderNameButtonText', + }, + ]; + + if (isTrash) { + headers.push({ + label: translate('drive.list.columns.autoDelete'), + width: 'w-date', + name: 'caducityDate', + orderable: true, + defaultDirection: 'ASC', + }); + } + + headers.push( + { + label: translate('drive.list.columns.modified'), + width: 'w-date', + name: 'updatedAt', + orderable: !isRecents, + defaultDirection: 'ASC', + }, + { + label: translate('drive.list.columns.size'), + orderable: !isRecents && !isTrash, + defaultDirection: 'ASC', + width: 'w-size', + name: 'size', + }, + ); + + return headers; +}; diff --git a/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/index.ts b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/index.ts new file mode 100644 index 0000000000..048c45e3b8 --- /dev/null +++ b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/index.ts @@ -0,0 +1 @@ +export { default } from './DriveExplorerList'; From 049a563b6356e795ed415345d125ed8ffb0b8878 Mon Sep 17 00:00:00 2001 From: Francis Terrero Date: Sat, 31 Jan 2026 13:14:37 -0400 Subject: [PATCH 5/5] refactor: replace i18n import with context provider and update translation usage --- .../DriveExplorerList/DriveExplorerListItem.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerListItem.tsx b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerListItem.tsx index 04b851e014..85973f7b24 100644 --- a/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerListItem.tsx +++ b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerListItem.tsx @@ -8,7 +8,7 @@ import sizeService from 'app/drive/services/size.service'; import iconService from 'app/drive/services/icon.service'; import { useDriveItemActions, useDriveItemDrag, useDriveItemDrop, useDriveItemStoreProps } from '../../../../hooks'; import './DriveExplorerListItem.scss'; -import { t } from 'i18next'; +import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; import { WarningCircle } from '@phosphor-icons/react'; const getItemClassNames = (isSelected: boolean, isDraggingOver: boolean, isDragging: boolean): string => { @@ -36,6 +36,7 @@ const getAutoDeleteStatusInfo = ( }; const DriveExplorerListItem = ({ item, isTrash }: DriveExplorerItemProps): JSX.Element => { + const { translate } = useTranslationContext(); const { isItemSelected, isEditingName } = useDriveItemStoreProps(); const { nameInputRef, onNameClicked, onItemClicked, onItemDoubleClicked, downloadAndSetThumbnail } = useDriveItemActions(item); @@ -46,8 +47,8 @@ const DriveExplorerListItem = ({ item, isTrash }: DriveExplorerItemProps): JSX.E const daysUntilDelete = isTrash ? dateService.calculateDaysUntilDate(item.caducityDate) : 0; const autoDeleteStatusInfo = useMemo( - () => (isTrash && daysUntilDelete > 0 ? getAutoDeleteStatusInfo(daysUntilDelete, t) : null), - [isTrash, daysUntilDelete], + () => (isTrash && daysUntilDelete > 0 ? getAutoDeleteStatusInfo(daysUntilDelete, translate) : null), + [isTrash, daysUntilDelete, translate], ); useEffect(() => { @@ -143,7 +144,7 @@ const DriveExplorerListItem = ({ item, isTrash }: DriveExplorerItemProps): JSX.E {/* DATE */}
- {dateService.formatDefaultDate(item.updatedAt, t)} + {dateService.formatDefaultDate(item.updatedAt, translate)}
{/* SIZE */}