From 56b3fdc813247bc5d358f2a3abbc4c6bac2669d7 Mon Sep 17 00:00:00 2001 From: Francis Terrero Date: Mon, 2 Mar 2026 11:10:59 -0400 Subject: [PATCH 1/5] Revert "Revert "[PB-5649]: Add automatic trash disposal notification dialog"" This reverts commit 84877419fda7d8252b5956e25fb25f84cab79013. --- .../ItemDetailsDialog/ItemDetailsDialog.tsx | 110 ++++++++++++------ .../components/ItemDetailsSkeleton.tsx | 5 +- .../services/new-storage.service.test.ts | 20 ++++ src/app/drive/services/new-storage.service.ts | 7 ++ src/app/drive/types/index.ts | 3 + src/app/i18n/locales/de.json | 37 +++++- src/app/i18n/locales/en.json | 35 +++++- src/app/i18n/locales/es.json | 35 +++++- src/app/i18n/locales/fr.json | 35 +++++- src/app/i18n/locales/it.json | 35 +++++- src/app/i18n/locales/ru.json | 37 +++++- src/app/i18n/locales/tw.json | 35 +++++- src/app/i18n/locales/zh.json | 35 +++++- src/app/store/slices/ui/index.ts | 6 + src/components/Skeleton.tsx | 10 ++ src/services/storage-keys.ts | 1 + .../DriveExplorerList.tsx | 45 +++---- .../DriveExplorerListItem.scss | 0 .../DriveExplorerListItem.tsx | 45 +++++-- .../DriveExplorerList/getListHeaders.test.ts | 36 ++++++ .../DriveExplorerList/getListHeaders.ts | 56 +++++++++ .../components/DriveExplorerList/index.ts | 1 + src/views/Trash/Trash.tsx | 22 ++++ .../AutomaticTrashDisposalDialog.tsx | 51 ++++++++ 24 files changed, 612 insertions(+), 90 deletions(-) rename src/views/Drive/components/DriveExplorer/components/{ => DriveExplorerList}/DriveExplorerList.tsx (94%) rename src/views/Drive/components/DriveExplorer/components/{ => DriveExplorerList}/DriveExplorerListItem.scss (100%) rename src/views/Drive/components/DriveExplorer/components/{ => DriveExplorerList}/DriveExplorerListItem.tsx (75%) 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 create mode 100644 src/views/Trash/components/AutomaticTrashDisposalDialog.tsx diff --git a/src/app/drive/components/ItemDetailsDialog/ItemDetailsDialog.tsx b/src/app/drive/components/ItemDetailsDialog/ItemDetailsDialog.tsx index c98104d725..531ab76d9d 100644 --- a/src/app/drive/components/ItemDetailsDialog/ItemDetailsDialog.tsx +++ b/src/app/drive/components/ItemDetailsDialog/ItemDetailsDialog.tsx @@ -13,6 +13,8 @@ import { STORAGE_KEYS } from 'services/storage-keys'; import { DriveItemData, DriveItemDetails, ItemDetailsProps } from 'app/drive/types'; import newStorageService from 'app/drive/services/new-storage.service'; import errorService from 'services/error.service'; +import { FolderStatsResponse } from '@internxt/sdk/dist/drive/storage/types'; +import { ItemType } from '@internxt/sdk/dist/workspaces/types'; import ItemDetailsSkeleton from './components/ItemDetailsSkeleton'; import { AdvancedSharedItem } from 'app/share/types'; import { useSelector } from 'react-redux'; @@ -56,13 +58,27 @@ const ItemsDetails = ({ item, translate }: { item: ItemDetailsProps; translate: ); }; +const calculateItemSize = ( + item: DriveItemDetails, + folderStats: FolderStatsResponse | undefined, +): string | undefined => { + if (!item.isFolder) { + return bytesToString(item.size); + } + if (folderStats?.totalSize !== undefined) { + return bytesToString(folderStats.totalSize, false); + } + return undefined; +}; + /** * Return all the details of the item selected * The data is: * - Name * - Shared - * - Size (only for files) + * - Size (for files and folders) * - Type (only for files) + * - Number of files (only for folders) * - Uploaded * - Modified * - Uploaded by @@ -117,62 +133,86 @@ const ItemDetailsDialog = ({ }, 300); }; - function formateDate(dateString: string) { + const formateDate = (dateString: string) => { return dateService.formatDefaultDate(dateString, translate); - } + }; - function handleButtonItemClick() { + const handleButtonItemClick = () => { onDetailsButtonClicked(item as AdvancedSharedItem); onClose(); - } + }; + + const MAX_DISPLAYABLE_FILE_COUNT = 1000; + + const formatFileCount = (count: number | undefined) => { + if (count === undefined) return undefined; + if (count > MAX_DISPLAYABLE_FILE_COUNT) return translate('modals.itemDetailsModal.fileCountMoreThan1000'); + return translate('modals.itemDetailsModal.fileCount', { count }); + }; + + const getFolderStats = (item: DriveItemDetails, itemUuid: string) => { + return item.isFolder ? newStorageService.getFolderStats(itemUuid) : undefined; + }; + + const getItemLocation = async ( + item: DriveItemDetails, + itemType: ItemType, + itemUuid: string, + itemFolderUuid: string, + token: string | undefined, + ) => { + if (!isWorkspaceSelected) { + const ancestors = await newStorageService.getFolderAncestors(itemFolderUuid); + return getLocation(item, ancestors as unknown as DriveItemData[]); + } + + const itemCreatorUuid = item.user?.uuid; + const isUserOwner = itemCreatorUuid && user?.uuid === itemCreatorUuid; + + if (item.view === 'Drive' || (item.view === 'Shared' && isUserOwner)) { + const ancestors = await newStorageService.getFolderAncestorsInWorkspace( + workspaceSelected.workspace.id, + itemType, + itemUuid, + token, + ); + return getLocation(item, ancestors as unknown as DriveItemData[]); + } + + return '/Shared'; + }; - async function getDetailsData( + const getDetailsData = async ( item: DriveItemDetails, isShared: string, uploaded: string, modified: string, email: string, - ) { - const itemType = item.isFolder ? 'folder' : 'file'; + ) => { + const itemType: ItemType = item.isFolder ? 'folder' : 'file'; const itemUuid = item.uuid; const itemFolderUuid = item.isFolder ? itemUuid : item.folderUuid; - const itemCreatorUuid = item.user?.uuid; - const isUserOwner = (itemCreatorUuid && user && user.uuid === itemCreatorUuid) || false; const storageKey = item.isFolder ? STORAGE_KEYS.FOLDER_ACCESS_TOKEN : STORAGE_KEYS.FILE_ACCESS_TOKEN; const token = localStorageService.get(storageKey) || undefined; - let location = ''; + const [location, folderStats] = await Promise.all([ + getItemLocation(item, itemType, itemUuid, itemFolderUuid, token), + getFolderStats(item, itemUuid), + ]); + const size = calculateItemSize(item, folderStats); - if (isWorkspaceSelected) { - if (item.view === 'Drive' || (item.view === 'Shared' && isUserOwner)) { - const ancestors = await newStorageService.getFolderAncestorsInWorkspace( - workspaceSelected.workspace.id, - itemType, - itemUuid, - token, - ); - location = getLocation(item, ancestors as unknown as DriveItemData[]); - } else { - location = '/Shared'; - } - } else { - const ancestors = await newStorageService.getFolderAncestors(itemFolderUuid); - location = getLocation(item, ancestors as unknown as DriveItemData[]); - } - - const details: ItemDetailsProps = { + return { name: item.name, shared: isShared, type: item.isFolder ? undefined : item.type, - size: item.isFolder ? undefined : bytesToString(item.size), - uploaded: uploaded, - modified: modified, + numberOfFiles: item.isFolder ? formatFileCount(folderStats?.fileCount) : undefined, + size, + uploaded, + modified, uploadedBy: item.user?.email ?? item.userEmail ?? email, location, }; - - return details; - } + }; return ( diff --git a/src/app/drive/components/ItemDetailsDialog/components/ItemDetailsSkeleton.tsx b/src/app/drive/components/ItemDetailsDialog/components/ItemDetailsSkeleton.tsx index 54c8e944a5..ca86799b4e 100644 --- a/src/app/drive/components/ItemDetailsDialog/components/ItemDetailsSkeleton.tsx +++ b/src/app/drive/components/ItemDetailsDialog/components/ItemDetailsSkeleton.tsx @@ -13,8 +13,11 @@ const ItemDetailsSkeleton = ({ shared: '', ...(!isFolder && { type: '', - size: '', }), + ...(isFolder && { + numberOfFiles: '', + }), + size: '', uploaded: '', modified: '', uploadedBy: '', diff --git a/src/app/drive/services/new-storage.service.test.ts b/src/app/drive/services/new-storage.service.test.ts index 2215ffd642..a27603770e 100644 --- a/src/app/drive/services/new-storage.service.test.ts +++ b/src/app/drive/services/new-storage.service.test.ts @@ -96,4 +96,24 @@ describe('newStorageService', () => { expect(mockGetFolderContentByUuid).toHaveBeenCalledWith(params); }); }); + + describe('Get Folder Statistics', () => { + test('When requesting folder statistics, then it returns file count and total size', async () => { + const mockUuid = 'test-folder-uuid'; + const mockStatsResponse = { + fileCount: 42, + totalSize: 1024000, + }; + const mockGetFolderStats = vi.fn().mockResolvedValue(mockStatsResponse); + const mockStorageClient = { getFolderStats: mockGetFolderStats }; + (SdkFactory.getNewApiInstance as Mock).mockReturnValue({ + createNewStorageClient: () => mockStorageClient, + }); + + const result = await newStorageService.getFolderStats(mockUuid); + + expect(mockGetFolderStats).toHaveBeenCalledWith(mockUuid); + expect(result).toEqual(mockStatsResponse); + }); + }); }); diff --git a/src/app/drive/services/new-storage.service.ts b/src/app/drive/services/new-storage.service.ts index dd1bd04efe..d3a9c71f57 100644 --- a/src/app/drive/services/new-storage.service.ts +++ b/src/app/drive/services/new-storage.service.ts @@ -6,6 +6,7 @@ import { FolderAncestor, FolderMeta, FolderAncestorWorkspace, + FolderStatsResponse, } from '@internxt/sdk/dist/drive/storage/types'; import { SdkFactory } from 'app/core/factory/sdk'; import { RequestCanceler } from '@internxt/sdk/dist/shared/http/types'; @@ -37,6 +38,11 @@ export async function getFolderMeta(uuid: string, workspaceId?: string, resource return storageClient.getFolderMeta(uuid, workspaceId, resourcesToken); } +export async function getFolderStats(uuid: string): Promise { + const storageClient = SdkFactory.getNewApiInstance().createNewStorageClient(); + return storageClient.getFolderStats(uuid); +} + export async function checkDuplicatedFiles( folderUuid: string, filesList: FileStructure[], @@ -92,6 +98,7 @@ const newStorageService = { getFolderAncestors, getFolderAncestorsInWorkspace, getFolderMeta, + getFolderStats, checkDuplicatedFiles, checkDuplicatedFolders, getFolderContentByUuid, diff --git a/src/app/drive/types/index.ts b/src/app/drive/types/index.ts index 64f0be9bd3..7c776af45f 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 { @@ -169,4 +171,5 @@ export type ItemDetailsProps = { shared: string; type?: string; size?: string; + numberOfFiles?: string; }; diff --git a/src/app/i18n/locales/de.json b/src/app/i18n/locales/de.json index b4f1d2c22d..6fe06469a0 100644 --- a/src/app/i18n/locales/de.json +++ b/src/app/i18n/locales/de.json @@ -789,9 +789,13 @@ "uploadedBy": "Hochgeladen von", "shared": "Freigegeben", "size": "Größe", + "numberOfFiles": "Anzahl der Dateien", "modified": "Geändert", - "Ort": "Ort" - } + "location": "Ort" + }, + "fileCount_one": "{{count}} Datei", + "fileCount_other": "{{count}} Dateien", + "fileCountMoreThan1000": "Mehr als 1000 Dateien" }, "versionHistory": { "title": "Versionsverlauf", @@ -1198,6 +1202,32 @@ "item-menu": { "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", + "close": "Schließen", + "description": "Alle in den Papierkorb verschobenen Elemente werden nun\nautomatisch dauerhaft gelöscht nach:", + "freeUsers": { + "duration": "1 Tag", + "label": "für kostenlose Benutzer" + }, + "essentialUsers": { + "duration": "7 Tage", + "label": "für Essential-Benutzer" + }, + "premiumUsers": { + "duration": "15 Tage", + "label": "für Premium-Benutzer" + }, + "ultimateUsers": { + "duration": "30 Tage", + "label": "für Ultimate-Benutzer" + } } }, "tasks": { @@ -1470,7 +1500,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 9a9e1c5782..3966f80335 100644 --- a/src/app/i18n/locales/en.json +++ b/src/app/i18n/locales/en.json @@ -886,9 +886,13 @@ "uploadedBy": "Uploaded by", "shared": "Shared", "size": "Size", + "numberOfFiles": "Number of files", "modified": "Modified", "location": "Location" - } + }, + "fileCount_one": "{{count}} file", + "fileCount_other": "{{count}} files", + "fileCountMoreThan1000": "1000+ files" }, "versionHistory": { "title": "Version history", @@ -1283,6 +1287,32 @@ "item-menu": { "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", + "close": "Close", + "description": "Now all the items moved to Trash will be\npermanently deleted automatically after:", + "freeUsers": { + "duration": "1 day", + "label": "for free users" + }, + "essentialUsers": { + "duration": "7 days", + "label": "for essential users" + }, + "premiumUsers": { + "duration": "15 days", + "label": "for premium users" + }, + "ultimateUsers": { + "duration": "30 days", + "label": "for ultimate users" + } } }, "tasks": { @@ -1552,7 +1582,8 @@ "name": "Name", "modified": "Modified", "size": "Size", - "actions": "Actions" + "actions": "Actions", + "autoDelete": "Auto-delete" } }, "viewMode": { diff --git a/src/app/i18n/locales/es.json b/src/app/i18n/locales/es.json index c807902a07..aba3807d39 100644 --- a/src/app/i18n/locales/es.json +++ b/src/app/i18n/locales/es.json @@ -868,9 +868,13 @@ "uploadedBy": "Subido por", "shared": "Compartido", "size": "Tamaño", + "numberOfFiles": "Número de archivos", "modified": "Modificado", "location": "Ubicación" - } + }, + "fileCount_one": "{{count}} archivo", + "fileCount_other": "{{count}} archivos", + "fileCountMoreThan1000": "Más de 1000 archivos" }, "versionHistory": { "title": "Historial de versiones", @@ -1260,6 +1264,32 @@ "item-menu": { "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", + "close": "Cerrar", + "description": "Ahora todos los elementos movidos a la Papelera se eliminarán\npermanentemente de forma automática después de:", + "freeUsers": { + "duration": "1 día", + "label": "para usuarios gratuitos" + }, + "essentialUsers": { + "duration": "7 días", + "label": "para usuarios esenciales" + }, + "premiumUsers": { + "duration": "15 días", + "label": "para usuarios premium" + }, + "ultimateUsers": { + "duration": "30 días", + "label": "para usuarios ultimate" + } } }, "tasks": { @@ -1530,7 +1560,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 3449641b31..15a3873fcf 100644 --- a/src/app/i18n/locales/fr.json +++ b/src/app/i18n/locales/fr.json @@ -810,9 +810,13 @@ "uploadedBy": "Téléchargé par", "shared": "Partagé", "size": "Taille", + "numberOfFiles": "Nombre de fichiers", "modified": "Modifié", "location": "Emplacement" - } + }, + "fileCount_one": "{{count}} fichier", + "fileCount_other": "{{count}} fichiers", + "fileCountMoreThan1000": "Plus de 1000 fichiers" }, "versionHistory": { "title": "Historique des versions", @@ -1211,6 +1215,32 @@ "item-menu": { "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", + "close": "Fermer", + "description": "Désormais, tous les éléments déplacés dans la Corbeille seront\nsupprimés définitivement automatiquement après:", + "freeUsers": { + "duration": "1 jour", + "label": "pour les utilisateurs gratuits" + }, + "essentialUsers": { + "duration": "7 jours", + "label": "pour les utilisateurs essentiels" + }, + "premiumUsers": { + "duration": "15 jours", + "label": "pour les utilisateurs premium" + }, + "ultimateUsers": { + "duration": "30 jours", + "label": "pour les utilisateurs ultimate" + } } }, "tasks": { @@ -1476,7 +1506,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 80ddcdd04c..9165015df5 100644 --- a/src/app/i18n/locales/it.json +++ b/src/app/i18n/locales/it.json @@ -923,8 +923,12 @@ "uploaded": "Caricato", "uploadedBy": "Caricato da", "shared": "Condiviso", + "numberOfFiles": "Numero di file", "location": "Posizione" - } + }, + "fileCount_one": "{{count}} file", + "fileCount_other": "{{count}} file", + "fileCountMoreThan1000": "Più di 1000 file" }, "versionHistory": { "title": "Cronologia versioni", @@ -1318,6 +1322,32 @@ "item-menu": { "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", + "close": "Chiudi", + "description": "Ora tutti gli elementi spostati nel Cestino verranno\neliminati definitivamente automaticamente dopo:", + "freeUsers": { + "duration": "1 giorno", + "label": "per utenti gratuiti" + }, + "essentialUsers": { + "duration": "7 giorni", + "label": "per utenti essenziali" + }, + "premiumUsers": { + "duration": "15 giorni", + "label": "per utenti premium" + }, + "ultimateUsers": { + "duration": "30 giorni", + "label": "per utenti ultimate" + } } }, "tasks": { @@ -1583,7 +1613,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 2b9cd739c4..9d568d558f 100644 --- a/src/app/i18n/locales/ru.json +++ b/src/app/i18n/locales/ru.json @@ -829,9 +829,15 @@ "uploadedBy": "Загружено", "shared": "Общий", "size": "Размер", + "numberOfFiles": "Количество файлов", "modified": "Изменено", "location": "Расположение" - } + }, + "fileCount_one": "{{count}} файл", + "fileCount_few": "{{count}} файла", + "fileCount_many": "{{count}} файлов", + "fileCount_other": "{{count}} файлов", + "fileCountMoreThan1000": "Более 1000 файлов" }, "versionHistory": { "title": "История версий", @@ -1224,6 +1230,32 @@ "item-menu": { "restore": "Восстановить", "delete-permanently": "Удалить навсегда" + }, + "autoDelete": { + "inDays_one": "Через {{count}} день", + "inDays_other": "Через {{count}} дней" + }, + "automaticDisposal": { + "badge": "Новое обновление", + "title": "Автоматическая очистка Корзины", + "close": "Закрыть", + "description": "Теперь все элементы, перемещенные в Корзину, будут\nавтоматически удалены навсегда после:", + "freeUsers": { + "duration": "1 день", + "label": "для бесплатных пользователей" + }, + "essentialUsers": { + "duration": "7 дней", + "label": "для пользователей Essential" + }, + "premiumUsers": { + "duration": "15 дней", + "label": "для пользователей Premium" + }, + "ultimateUsers": { + "duration": "30 дней", + "label": "для пользователей Ultimate" + } } }, "tasks": { @@ -1489,7 +1521,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 48c961b090..38d4a952c8 100644 --- a/src/app/i18n/locales/tw.json +++ b/src/app/i18n/locales/tw.json @@ -816,9 +816,13 @@ "uploadedBy": "上傳者", "shared": "已共享", "size": "大小", + "numberOfFiles": "文件數量", "modified": "修改時間", "location": "位置" - } + }, + "fileCount_one": "{{count}} 個文件", + "fileCount_other": "{{count}} 個文件", + "fileCountMoreThan1000": "超過 1000 個文件" }, "versionHistory": { "title": "版本歷史", @@ -1213,6 +1217,32 @@ "item-menu": { "restore": "還原", "delete-permanently": "永久刪除" + }, + "autoDelete": { + "inDays_one": "{{count}} 天後", + "inDays_other": "{{count}} 天後" + }, + "automaticDisposal": { + "badge": "新更新", + "title": "自動清空垃圾桶", + "close": "關閉", + "description": "現在所有移至垃圾桶的項目將在以下時間後\n自動永久刪除:", + "freeUsers": { + "duration": "1天", + "label": "免費用戶" + }, + "essentialUsers": { + "duration": "7天", + "label": "基礎用戶" + }, + "premiumUsers": { + "duration": "15天", + "label": "高級用戶" + }, + "ultimateUsers": { + "duration": "30天", + "label": "終極用戶" + } } }, "tasks": { @@ -1482,7 +1512,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 affcdcf148..a75a4bcc79 100644 --- a/src/app/i18n/locales/zh.json +++ b/src/app/i18n/locales/zh.json @@ -852,9 +852,13 @@ "uploadedBy": "上传者", "shared": "共享", "size": "大小", + "numberOfFiles": "文件数量", "modified": "已修改", "location": "位置" - } + }, + "fileCount_one": "{{count}} 个文件", + "fileCount_other": "{{count}} 个文件", + "fileCountMoreThan1000": "超过 1000 个文件" }, "versionHistory": { "title": "版本历史", @@ -1248,6 +1252,32 @@ "item-menu": { "restore": "恢复", "delete-permanently": "永久删除" + }, + "autoDelete": { + "inDays_one": "{{count}} 天后", + "inDays_other": "{{count}} 天后" + }, + "automaticDisposal": { + "badge": "新更新", + "title": "自动清空垃圾箱", + "close": "关闭", + "description": "现在所有移至垃圾箱的项目将在以下时间后\n自动永久删除:", + "freeUsers": { + "duration": "1天", + "label": "免费用户" + }, + "essentialUsers": { + "duration": "7天", + "label": "基础用户" + }, + "premiumUsers": { + "duration": "15天", + "label": "高级用户" + }, + "ultimateUsers": { + "duration": "30天", + "label": "终极用户" + } } }, "tasks": { @@ -1517,7 +1547,8 @@ "name": "名称", "modified": "修改日期", "size": "大小", - "actions": "操作" + "actions": "操作", + "autoDelete": "自动删除" } }, "viewMode": { diff --git a/src/app/store/slices/ui/index.ts b/src/app/store/slices/ui/index.ts index 732f20632e..bb8a0ad3a8 100644 --- a/src/app/store/slices/ui/index.ts +++ b/src/app/store/slices/ui/index.ts @@ -39,6 +39,7 @@ interface UISliceState { currentEditingNameDirty: string; isGlobalSearch: boolean; isShareWhithTeamDialogOpen: boolean; + isAutomaticTrashDisposalDialogOpen: boolean; } const initialState: UISliceState = { @@ -77,6 +78,7 @@ const initialState: UISliceState = { currentEditingNameDirty: '', isGlobalSearch: false, isShareWhithTeamDialogOpen: false, + isAutomaticTrashDisposalDialogOpen: false, }; export const uiSlice = createSlice({ @@ -197,6 +199,9 @@ export const uiSlice = createSlice({ setIsShareWhithTeamDialogOpen: (state: UISliceState, action: PayloadAction) => { state.isShareWhithTeamDialogOpen = action.payload; }, + setIsAutomaticTrashDisposalDialogOpen: (state: UISliceState, action: PayloadAction) => { + state.isAutomaticTrashDisposalDialogOpen = action.payload; + }, }, }); @@ -223,6 +228,7 @@ export const { setIsGlobalSearch, setIsItemDetailsDialogOpen, setIsShareWhithTeamDialogOpen, + setIsAutomaticTrashDisposalDialogOpen, } = uiSlice.actions; export const uiActions = uiSlice.actions; diff --git a/src/components/Skeleton.tsx b/src/components/Skeleton.tsx index 845eb833bb..0fbd3f638c 100644 --- a/src/components/Skeleton.tsx +++ b/src/components/Skeleton.tsx @@ -6,3 +6,13 @@ export const skinSkeleton = [
,
, ]; + +export const skinSkeletonTrash = [ +
+
+
+
, +
, +
, +
, +]; diff --git a/src/services/storage-keys.ts b/src/services/storage-keys.ts index 561165bfea..2e7a08392b 100644 --- a/src/services/storage-keys.ts +++ b/src/services/storage-keys.ts @@ -5,6 +5,7 @@ export const STORAGE_KEYS = { FOLDER_ACCESS_TOKEN: 'folderAccessToken', FILE_ACCESS_TOKEN: 'fileAccessToken', GCLID: 'gclid', + HAS_SEEN_TRASH_DISPOSAL_DIALOG: 'hasSeenTrashDisposalDialog', THEMES: { MANAGEMENTID_THEME_AVAILABLE_LOCAL_STORAGE_KEY: 'managementid_theme_enabled', ID_MANAGEMENT_THEME_AVAILABLE_LOCAL_STORAGE_KEY: 'id_management_theme_enabled', diff --git a/src/views/Drive/components/DriveExplorer/components/DriveExplorerList.tsx b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerList.tsx similarity index 94% rename from src/views/Drive/components/DriveExplorer/components/DriveExplorerList.tsx rename to src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerList.tsx index 945fe48a04..cd886c2e33 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 } from 'components/Skeleton'; -import { moveItemsToTrash } from '../../../../../views/Trash/services'; +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'; 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'; import { useVersionHistoryMenuConfig } from 'views/Drive/hooks/useVersionHistoryMenuConfig'; @@ -58,7 +59,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 }); @@ -142,6 +143,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; @@ -166,6 +168,12 @@ const DriveExplorerList: React.FC = memo((props) => { resetDriveOrder({ dispatch, orderType: 'updatedAt', direction, currentFolderId }); } } + + if (value.field === 'caducityDate') { + if (isTrash) { + props.resetPaginationState(); + } + } }; function handleMouseEnter() { @@ -468,31 +476,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', - }, - { - label: translate('drive.list.columns.modified'), - width: 'w-date', - name: 'updatedAt', - orderable: !isRecents, - defaultDirection: 'ASC', - }, - { - label: translate('drive.list.columns.size'), - orderable: false, - width: 'w-size', - name: 'size', - }, - ]} + + header={getListHeaders(translate, isRecents, isTrash)} checkboxDataCy="driveListHeaderCheckbox" disableKeyboardShortcuts={props.disableKeyboardShortcuts || props.showStopSharingConfirmation} items={props.items} @@ -501,7 +486,7 @@ const DriveExplorerList: React.FC = memo((props) => { itemComposition={[(item) => createDriveListItem(item, props.isTrash)]} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} - skinSkeleton={skinSkeleton} + skinSkeleton={skeleton} emptyState={<>} onNextPage={onEndOfScroll} onEnterPressed={(driveItem) => { 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 75% rename from src/views/Drive/components/DriveExplorer/components/DriveExplorerListItem.tsx rename to src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerListItem.tsx index 9cc1090989..e2cfb5c7e0 100644 --- a/src/views/Drive/components/DriveExplorer/components/DriveExplorerListItem.tsx +++ b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerListItem.tsx @@ -1,14 +1,15 @@ 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 { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; +import { WarningCircle } from '@phosphor-icons/react'; const getItemClassNames = (isSelected: boolean, isDraggingOver: boolean, isDragging: boolean): string => { const selectedClass = isSelected ? 'selected' : ''; @@ -21,7 +22,21 @@ const isItemInteractive = (item: DriveExplorerItemProps['item']): boolean => { return (item.isFolder && !item.deleted) || (!item.isFolder && item.status === 'EXISTS'); }; -const DriveExplorerListItem = ({ item }: DriveExplorerItemProps): JSX.Element => { +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 <= URGENT_AUTO_DELETE_THRESHOLD_DAYS; + return { + text: translate('trash.autoDelete.inDays', { count: days }), + isUrgent, + }; +}; + +const DriveExplorerListItem = ({ item, isTrash }: DriveExplorerItemProps): JSX.Element => { + const { translate } = useTranslationContext(); const { isItemSelected, isEditingName } = useDriveItemStoreProps(); const { nameInputRef, onNameClicked, onItemClicked, onItemDoubleClicked, downloadAndSetThumbnail } = useDriveItemActions(item); @@ -30,6 +45,12 @@ const DriveExplorerListItem = ({ item }: DriveExplorerItemProps): JSX.Element => const { connectDropTarget, isDraggingOverThisItem } = useDriveItemDrop(item); const ItemIconComponent = iconService.getItemIcon(item.isFolder, item.type); + const daysUntilDelete = isTrash && item.caducityDate ? dateService.getDaysUntilExpiration(item.caducityDate) : 0; + const autoDeleteStatusInfo = useMemo( + () => (isTrash && daysUntilDelete > 0 ? getAutoDeleteStatusInfo(daysUntilDelete, translate) : null), + [isTrash, daysUntilDelete, translate], + ); + useEffect(() => { if (isEditingName(item)) { const current = nameInputRef.current; @@ -111,9 +132,19 @@ const DriveExplorerListItem = ({ item }: DriveExplorerItemProps): JSX.Element => isInteractive && connectDropTarget(
) } + {/* AUTO-DELETE (only for trash) */} + {isTrash && autoDeleteStatusInfo && ( +
+
+ + {autoDeleteStatusInfo.text} +
+
+ )} + {/* DATE */} -
- {dateService.formatDefaultDate(item.updatedAt, t)} +
+ {dateService.formatDefaultDate(item.updatedAt, translate)}
{/* SIZE */} 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..2709fd7c75 --- /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: false, + 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'; diff --git a/src/views/Trash/Trash.tsx b/src/views/Trash/Trash.tsx index 572055cf2f..39c6df97da 100644 --- a/src/views/Trash/Trash.tsx +++ b/src/views/Trash/Trash.tsx @@ -10,6 +10,12 @@ import { storageActions } from 'app/store/slices/storage'; import { getTrashPaginated, getWorkspaceTrashPaginated } from './services'; import storageThunks from 'app/store/slices/storage/storage.thunks'; import workspacesSelectors from 'app/store/slices/workspaces/workspaces.selectors'; +import { uiActions } from 'app/store/slices/ui'; +import { useAppSelector } from 'app/store/hooks'; +import AutomaticTrashDisposalDialog from './components/AutomaticTrashDisposalDialog'; +import { userSelectors } from 'app/store/slices/user'; +import localStorageService from 'services/local-storage.service'; +import { STORAGE_KEYS } from 'services/storage-keys'; export interface TrashViewProps { isLoadingItemsOnTrash: boolean; @@ -17,17 +23,32 @@ export interface TrashViewProps { dispatch: AppDispatch; } +const shouldShowTrashDisposalDialog = (hasSignedToday: boolean): boolean => { + const hasSeenDialog = localStorageService.get(STORAGE_KEYS.HAS_SEEN_TRASH_DISPOSAL_DIALOG); + return !hasSignedToday && !hasSeenDialog; +}; + +const markTrashDisposalDialogAsSeen = (): void => { + localStorageService.set(STORAGE_KEYS.HAS_SEEN_TRASH_DISPOSAL_DIALOG, 'true'); +}; + const TrashView = (props: TrashViewProps) => { const { items, isLoadingItemsOnTrash } = props; const { translate } = useTranslationContext(); const workspaceSelected = useSelector(workspacesSelectors.getSelectedWorkspace); + const hasSignedToday = useAppSelector(userSelectors.hasSignedToday); const getTrash = workspaceSelected ? getWorkspaceTrashPaginated : getTrashPaginated; useEffect(() => { const { dispatch } = props; dispatch(storageThunks.resetNamePathThunk()); dispatch(storageActions.clearSelectedItems()); + + if (shouldShowTrashDisposalDialog(hasSignedToday)) { + dispatch(uiActions.setIsAutomaticTrashDisposalDialogOpen(true)); + markTrashDisposalDialogAsSeen(); + } }, []); return ( @@ -41,6 +62,7 @@ const TrashView = (props: TrashViewProps) => { items={items} getTrashPaginated={getTrash} /> + ); }; diff --git a/src/views/Trash/components/AutomaticTrashDisposalDialog.tsx b/src/views/Trash/components/AutomaticTrashDisposalDialog.tsx new file mode 100644 index 0000000000..6c568d8485 --- /dev/null +++ b/src/views/Trash/components/AutomaticTrashDisposalDialog.tsx @@ -0,0 +1,51 @@ +import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; +import { Button, Modal } from '@internxt/ui'; +import { RootState } from 'app/store'; +import { useAppDispatch, useAppSelector } from 'app/store/hooks'; +import { uiActions } from 'app/store/slices/ui'; + +const USER_TIERS = ['freeUsers', 'essentialUsers', 'premiumUsers', 'ultimateUsers'] as const; + +const AutomaticTrashDisposalDialog = (): JSX.Element => { + const { translate } = useTranslationContext(); + const dispatch = useAppDispatch(); + const isOpen = useAppSelector((state: RootState) => state.ui.isAutomaticTrashDisposalDialogOpen); + + const onClose = (): void => { + dispatch(uiActions.setIsAutomaticTrashDisposalDialogOpen(false)); + }; + + return ( + +
+ + {translate('trash.automaticDisposal.badge')} + + +

{translate('trash.automaticDisposal.title')}

+ +
+

{translate('trash.automaticDisposal.description')}

+
    + {USER_TIERS.map((tier) => ( +
  • + + {translate(`trash.automaticDisposal.${tier}.duration`)} + {' '} + {translate(`trash.automaticDisposal.${tier}.label`)} +
  • + ))} +
+
+ +
+ +
+
+
+ ); +}; + +export default AutomaticTrashDisposalDialog; From 02263673a764b3c0cbd5da696c1c3e2b426e3aa5 Mon Sep 17 00:00:00 2001 From: Francis Terrero Date: Tue, 3 Mar 2026 11:12:53 -0400 Subject: [PATCH 2/5] fix: Rename 'caducityDate' to 'expiresAt' in Drive folder and file data types, update related components and tests --- src/app/drive/types/index.ts | 4 ++-- .../components/DriveExplorerList/DriveExplorerList.tsx | 6 +++--- .../components/DriveExplorerList/DriveExplorerListItem.tsx | 2 +- .../components/DriveExplorerList/getListHeaders.test.ts | 2 +- .../components/DriveExplorerList/getListHeaders.ts | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/app/drive/types/index.ts b/src/app/drive/types/index.ts index 7c776af45f..96ace2f643 100644 --- a/src/app/drive/types/index.ts +++ b/src/app/drive/types/index.ts @@ -30,7 +30,7 @@ export interface DriveFolderData { uuid: string; type?: string; user?: UserResumeData; - caducityDate?: string; + expiresAt?: string; } export interface DriveFolderMetadataPayload { @@ -64,7 +64,7 @@ export interface DriveFileData { sharings?: { type: string; id: string }[]; uuid: string; user?: UserResumeData; - caducityDate?: string; + expiresAt?: string; } interface Thumbnail { diff --git a/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerList.tsx b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerList.tsx index cd886c2e33..51fa6b6a04 100644 --- a/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerList.tsx +++ b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerList.tsx @@ -59,7 +59,7 @@ interface DriveExplorerListProps { type ObjectWithId = { id: string | number }; -type SortField = 'type' | 'name' | 'updatedAt' | 'size' | 'caducityDate'; +type SortField = 'type' | 'name' | 'updatedAt' | 'size' | 'expiresAt'; type ContextMenuDriveItem = DriveItemData | Pick | (ListShareLinksItem & { code: string }); @@ -169,7 +169,7 @@ const DriveExplorerList: React.FC = memo((props) => { } } - if (value.field === 'caducityDate') { + if (value.field === 'expiresAt') { if (isTrash) { props.resetPaginationState(); } @@ -476,7 +476,7 @@ const DriveExplorerList: React.FC = memo((props) => { /> )} - + header={getListHeaders(translate, isRecents, isTrash)} checkboxDataCy="driveListHeaderCheckbox" disableKeyboardShortcuts={props.disableKeyboardShortcuts || props.showStopSharingConfirmation} diff --git a/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerListItem.tsx b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerListItem.tsx index e2cfb5c7e0..b8b8f0e266 100644 --- a/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerListItem.tsx +++ b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerListItem.tsx @@ -45,7 +45,7 @@ const DriveExplorerListItem = ({ item, isTrash }: DriveExplorerItemProps): JSX.E const { connectDropTarget, isDraggingOverThisItem } = useDriveItemDrop(item); const ItemIconComponent = iconService.getItemIcon(item.isFolder, item.type); - const daysUntilDelete = isTrash && item.caducityDate ? dateService.getDaysUntilExpiration(item.caducityDate) : 0; + const daysUntilDelete = isTrash && item.expiresAt ? dateService.getDaysUntilExpiration(item.expiresAt) : 0; const autoDeleteStatusInfo = useMemo( () => (isTrash && daysUntilDelete > 0 ? getAutoDeleteStatusInfo(daysUntilDelete, translate) : null), [isTrash, daysUntilDelete, translate], diff --git a/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/getListHeaders.test.ts b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/getListHeaders.test.ts index dfd6e7311b..cab862a91c 100644 --- a/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/getListHeaders.test.ts +++ b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/getListHeaders.test.ts @@ -18,7 +18,7 @@ describe('List column headers', () => { expect(headers).toHaveLength(4); - const caducityHeader = headers.find((h) => h.name === 'caducityDate'); + const caducityHeader = headers.find((h) => h.name === 'expiresAt'); expect(caducityHeader?.label).toBe('drive.list.columns.autoDelete'); expect(caducityHeader?.orderable).toBe(true); diff --git a/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/getListHeaders.ts b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/getListHeaders.ts index 2709fd7c75..cbada6c410 100644 --- a/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/getListHeaders.ts +++ b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/getListHeaders.ts @@ -1,7 +1,7 @@ interface ListHeaderItem { label: string; width: string; - name: 'type' | 'name' | 'updatedAt' | 'size' | 'caducityDate'; + name: 'type' | 'name' | 'updatedAt' | 'size' | 'expiresAt'; orderable: boolean; defaultDirection: 'ASC' | 'DESC'; buttonDataCy?: string; @@ -29,7 +29,7 @@ export const getListHeaders = ( headers.push({ label: translate('drive.list.columns.autoDelete'), width: 'w-date', - name: 'caducityDate', + name: 'expiresAt', orderable: true, defaultDirection: 'ASC', }); From 348358a57a6a755f24e42c43db8a421bd5395efc Mon Sep 17 00:00:00 2001 From: Francis Terrero Date: Mon, 9 Mar 2026 08:38:42 -0400 Subject: [PATCH 3/5] fix: Update trash disposal duration for free users to 2 days in multiple language files --- src/app/i18n/locales/de.json | 2 +- src/app/i18n/locales/en.json | 2 +- src/app/i18n/locales/es.json | 2 +- src/app/i18n/locales/fr.json | 2 +- src/app/i18n/locales/it.json | 2 +- src/app/i18n/locales/ru.json | 2 +- src/app/i18n/locales/tw.json | 2 +- src/app/i18n/locales/zh.json | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/app/i18n/locales/de.json b/src/app/i18n/locales/de.json index 6fe06469a0..6aa3849f0b 100644 --- a/src/app/i18n/locales/de.json +++ b/src/app/i18n/locales/de.json @@ -1213,7 +1213,7 @@ "close": "Schließen", "description": "Alle in den Papierkorb verschobenen Elemente werden nun\nautomatisch dauerhaft gelöscht nach:", "freeUsers": { - "duration": "1 Tag", + "duration": "2 Tage", "label": "für kostenlose Benutzer" }, "essentialUsers": { diff --git a/src/app/i18n/locales/en.json b/src/app/i18n/locales/en.json index 3966f80335..13bcece10c 100644 --- a/src/app/i18n/locales/en.json +++ b/src/app/i18n/locales/en.json @@ -1298,7 +1298,7 @@ "close": "Close", "description": "Now all the items moved to Trash will be\npermanently deleted automatically after:", "freeUsers": { - "duration": "1 day", + "duration": "2 days", "label": "for free users" }, "essentialUsers": { diff --git a/src/app/i18n/locales/es.json b/src/app/i18n/locales/es.json index aba3807d39..b4a8c824d9 100644 --- a/src/app/i18n/locales/es.json +++ b/src/app/i18n/locales/es.json @@ -1275,7 +1275,7 @@ "close": "Cerrar", "description": "Ahora todos los elementos movidos a la Papelera se eliminarán\npermanentemente de forma automática después de:", "freeUsers": { - "duration": "1 día", + "duration": "2 días", "label": "para usuarios gratuitos" }, "essentialUsers": { diff --git a/src/app/i18n/locales/fr.json b/src/app/i18n/locales/fr.json index 15a3873fcf..09f8c1cf0b 100644 --- a/src/app/i18n/locales/fr.json +++ b/src/app/i18n/locales/fr.json @@ -1226,7 +1226,7 @@ "close": "Fermer", "description": "Désormais, tous les éléments déplacés dans la Corbeille seront\nsupprimés définitivement automatiquement après:", "freeUsers": { - "duration": "1 jour", + "duration": "2 jours", "label": "pour les utilisateurs gratuits" }, "essentialUsers": { diff --git a/src/app/i18n/locales/it.json b/src/app/i18n/locales/it.json index 9165015df5..9fa2155a0e 100644 --- a/src/app/i18n/locales/it.json +++ b/src/app/i18n/locales/it.json @@ -1333,7 +1333,7 @@ "close": "Chiudi", "description": "Ora tutti gli elementi spostati nel Cestino verranno\neliminati definitivamente automaticamente dopo:", "freeUsers": { - "duration": "1 giorno", + "duration": "2 giorni", "label": "per utenti gratuiti" }, "essentialUsers": { diff --git a/src/app/i18n/locales/ru.json b/src/app/i18n/locales/ru.json index 9d568d558f..733993acae 100644 --- a/src/app/i18n/locales/ru.json +++ b/src/app/i18n/locales/ru.json @@ -1241,7 +1241,7 @@ "close": "Закрыть", "description": "Теперь все элементы, перемещенные в Корзину, будут\nавтоматически удалены навсегда после:", "freeUsers": { - "duration": "1 день", + "duration": "2 дня", "label": "для бесплатных пользователей" }, "essentialUsers": { diff --git a/src/app/i18n/locales/tw.json b/src/app/i18n/locales/tw.json index 38d4a952c8..4ae6c21149 100644 --- a/src/app/i18n/locales/tw.json +++ b/src/app/i18n/locales/tw.json @@ -1228,7 +1228,7 @@ "close": "關閉", "description": "現在所有移至垃圾桶的項目將在以下時間後\n自動永久刪除:", "freeUsers": { - "duration": "1天", + "duration": "2天", "label": "免費用戶" }, "essentialUsers": { diff --git a/src/app/i18n/locales/zh.json b/src/app/i18n/locales/zh.json index a75a4bcc79..c9e189df4e 100644 --- a/src/app/i18n/locales/zh.json +++ b/src/app/i18n/locales/zh.json @@ -1263,7 +1263,7 @@ "close": "关闭", "description": "现在所有移至垃圾箱的项目将在以下时间后\n自动永久删除:", "freeUsers": { - "duration": "1天", + "duration": "2天", "label": "免费用户" }, "essentialUsers": { From 4f1c62712788981a962dcdc204e50caeee7da557 Mon Sep 17 00:00:00 2001 From: Francis Terrero Date: Mon, 9 Mar 2026 17:24:04 -0400 Subject: [PATCH 4/5] fix: Show hours instead of days for trash auto-delete when expiration is less than 24 hours --- src/app/i18n/locales/de.json | 2 ++ src/app/i18n/locales/en.json | 2 ++ src/app/i18n/locales/es.json | 2 ++ src/app/i18n/locales/fr.json | 2 ++ src/app/i18n/locales/it.json | 2 ++ src/app/i18n/locales/ru.json | 2 ++ src/app/i18n/locales/tw.json | 2 ++ src/app/i18n/locales/zh.json | 2 ++ src/services/date.service.test.ts | 21 ++++++++++++++++ src/services/date.service.ts | 8 +++++++ .../DriveExplorerListItem.tsx | 24 +++++++++++++++---- src/views/Trash/Trash.tsx | 10 ++++++-- 12 files changed, 72 insertions(+), 7 deletions(-) diff --git a/src/app/i18n/locales/de.json b/src/app/i18n/locales/de.json index 6aa3849f0b..125215d292 100644 --- a/src/app/i18n/locales/de.json +++ b/src/app/i18n/locales/de.json @@ -1204,6 +1204,8 @@ "delete-permanently": "Lösche dauerhaft" }, "autoDelete": { + "inHours_one": "In {{count}} Stunde", + "inHours_other": "In {{count}} Stunden", "inDays_one": "In {{count}} Tag", "inDays_other": "In {{count}} Tagen" }, diff --git a/src/app/i18n/locales/en.json b/src/app/i18n/locales/en.json index 13bcece10c..274dd9f741 100644 --- a/src/app/i18n/locales/en.json +++ b/src/app/i18n/locales/en.json @@ -1289,6 +1289,8 @@ "delete-permanently": "Delete permanently" }, "autoDelete": { + "inHours_one": "In {{count}} hour", + "inHours_other": "In {{count}} hours", "inDays_one": "In {{count}} day", "inDays_other": "In {{count}} days" }, diff --git a/src/app/i18n/locales/es.json b/src/app/i18n/locales/es.json index b4a8c824d9..ef14e32c8e 100644 --- a/src/app/i18n/locales/es.json +++ b/src/app/i18n/locales/es.json @@ -1266,6 +1266,8 @@ "delete-permanently": "Eliminar permanentemente" }, "autoDelete": { + "inHours_one": "En {{count}} hora", + "inHours_other": "En {{count}} horas", "inDays_one": "En {{count}} día", "inDays_other": "En {{count}} días" }, diff --git a/src/app/i18n/locales/fr.json b/src/app/i18n/locales/fr.json index 09f8c1cf0b..ce5fa4965a 100644 --- a/src/app/i18n/locales/fr.json +++ b/src/app/i18n/locales/fr.json @@ -1217,6 +1217,8 @@ "delete-permanently": "Supprimer définitivement" }, "autoDelete": { + "inHours_one": "Dans {{count}} heure", + "inHours_other": "Dans {{count}} heures", "inDays_one": "Dans {{count}} jour", "inDays_other": "Dans {{count}} jours" }, diff --git a/src/app/i18n/locales/it.json b/src/app/i18n/locales/it.json index 9fa2155a0e..ace76097cf 100644 --- a/src/app/i18n/locales/it.json +++ b/src/app/i18n/locales/it.json @@ -1324,6 +1324,8 @@ "delete-permanently": "Elimina definitivamente" }, "autoDelete": { + "inHours_one": "Tra {{count}} ora", + "inHours_other": "Tra {{count}} ore", "inDays_one": "Tra {{count}} giorno", "inDays_other": "Tra {{count}} giorni" }, diff --git a/src/app/i18n/locales/ru.json b/src/app/i18n/locales/ru.json index 733993acae..2f2eb6f15e 100644 --- a/src/app/i18n/locales/ru.json +++ b/src/app/i18n/locales/ru.json @@ -1232,6 +1232,8 @@ "delete-permanently": "Удалить навсегда" }, "autoDelete": { + "inHours_one": "Через {{count}} час", + "inHours_other": "Через {{count}} часов", "inDays_one": "Через {{count}} день", "inDays_other": "Через {{count}} дней" }, diff --git a/src/app/i18n/locales/tw.json b/src/app/i18n/locales/tw.json index 4ae6c21149..020caf60ab 100644 --- a/src/app/i18n/locales/tw.json +++ b/src/app/i18n/locales/tw.json @@ -1219,6 +1219,8 @@ "delete-permanently": "永久刪除" }, "autoDelete": { + "inHours_one": "{{count}} 小時後", + "inHours_other": "{{count}} 小時後", "inDays_one": "{{count}} 天後", "inDays_other": "{{count}} 天後" }, diff --git a/src/app/i18n/locales/zh.json b/src/app/i18n/locales/zh.json index c9e189df4e..8389417f91 100644 --- a/src/app/i18n/locales/zh.json +++ b/src/app/i18n/locales/zh.json @@ -1254,6 +1254,8 @@ "delete-permanently": "永久删除" }, "autoDelete": { + "inHours_one": "{{count}} 小时后", + "inHours_other": "{{count}} 小时后", "inDays_one": "{{count}} 天后", "inDays_other": "{{count}} 天后" }, diff --git a/src/services/date.service.test.ts b/src/services/date.service.test.ts index 334d2117e4..7e738e4604 100644 --- a/src/services/date.service.test.ts +++ b/src/services/date.service.test.ts @@ -52,5 +52,26 @@ describe('dateService', () => { const expiresAt = '2023-01-01T12:00:00Z'; expect(dateService.getDaysUntilExpiration(expiresAt)).toBe(1); }); + + test('when the expiration is in 5 hours, then 5 remaining hours are returned', () => { + const expiresAt = '2023-01-01T05:00:00Z'; + expect(dateService.getHoursUntilExpiration(expiresAt)).toBe(5); + }); + + test('when the expiration has passed, then zero remaining hours are returned', () => { + const expiresAt = '2022-12-31T23:00:00Z'; + expect(dateService.getHoursUntilExpiration(expiresAt)).toBe(0); + }); + + test('when the expiration is in 30 minutes, then remaining hours round up to 1', () => { + const expiresAt = '2023-01-01T00:30:00Z'; + expect(dateService.getHoursUntilExpiration(expiresAt)).toBe(1); + }); + + test('when the expiration is in 25 hours, then 25 remaining hours and 2 remaining days are returned', () => { + const expiresAt = '2023-01-02T01:00:00Z'; + expect(dateService.getHoursUntilExpiration(expiresAt)).toBe(25); + expect(dateService.getDaysUntilExpiration(expiresAt)).toBe(2); + }); }); }); diff --git a/src/services/date.service.ts b/src/services/date.service.ts index 2e2213ffd9..18363ee0e4 100644 --- a/src/services/date.service.ts +++ b/src/services/date.service.ts @@ -31,6 +31,13 @@ export const getDaysUntilExpiration = (expiresAt: Date | string): number => { return Math.max(0, Math.ceil(diffInDays)); }; +const getHoursUntilExpiration = (expiresAt: Date | string): number => { + const expirationDate = dayjs(expiresAt); + const now = dayjs(); + const diffInHours = expirationDate.diff(now, 'hour', true); + return Math.max(0, Math.ceil(diffInHours)); +}; + const dateService = { format, isDateOneBefore, @@ -38,6 +45,7 @@ const dateService = { getExpirationDate, formatDefaultDate, getDaysUntilExpiration, + getHoursUntilExpiration, }; export default dateService; diff --git a/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerListItem.tsx b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerListItem.tsx index b8b8f0e266..e86a54f7e2 100644 --- a/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerListItem.tsx +++ b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerListItem.tsx @@ -22,16 +22,27 @@ const isItemInteractive = (item: DriveExplorerItemProps['item']): boolean => { return (item.isFolder && !item.deleted) || (!item.isFolder && item.status === 'EXISTS'); }; +const HOURS_IN_A_DAY = 24; const URGENT_AUTO_DELETE_THRESHOLD_DAYS = 2; const getAutoDeleteStatusInfo = ( days: number, + expiresAt: string, translate: (key: string, options?: { count?: number }) => string, ): { text: string; isUrgent: boolean } => { - const isUrgent = days <= URGENT_AUTO_DELETE_THRESHOLD_DAYS; + const hours = dateService.getHoursUntilExpiration(expiresAt); + const isLessThanADay = hours < HOURS_IN_A_DAY; + + if (isLessThanADay) { + return { + text: translate('trash.autoDelete.inHours', { count: hours }), + isUrgent: true, + }; + } + return { text: translate('trash.autoDelete.inDays', { count: days }), - isUrgent, + isUrgent: days <= URGENT_AUTO_DELETE_THRESHOLD_DAYS, }; }; @@ -45,10 +56,13 @@ const DriveExplorerListItem = ({ item, isTrash }: DriveExplorerItemProps): JSX.E const { connectDropTarget, isDraggingOverThisItem } = useDriveItemDrop(item); const ItemIconComponent = iconService.getItemIcon(item.isFolder, item.type); - const daysUntilDelete = isTrash && item.expiresAt ? dateService.getDaysUntilExpiration(item.expiresAt) : 0; + const daysUntilDelete = isTrash && item.expiresAt ? dateService.getDaysUntilExpiration(item.expiresAt) : null; const autoDeleteStatusInfo = useMemo( - () => (isTrash && daysUntilDelete > 0 ? getAutoDeleteStatusInfo(daysUntilDelete, translate) : null), - [isTrash, daysUntilDelete, translate], + () => + isTrash && daysUntilDelete !== null && item.expiresAt + ? getAutoDeleteStatusInfo(daysUntilDelete, item.expiresAt, translate) + : null, + [isTrash, daysUntilDelete, item.expiresAt, translate], ); useEffect(() => { diff --git a/src/views/Trash/Trash.tsx b/src/views/Trash/Trash.tsx index 39c6df97da..99df80da93 100644 --- a/src/views/Trash/Trash.tsx +++ b/src/views/Trash/Trash.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; import { Helmet } from 'react-helmet-async'; import { connect, useSelector } from 'react-redux'; @@ -14,6 +14,7 @@ import { uiActions } from 'app/store/slices/ui'; import { useAppSelector } from 'app/store/hooks'; import AutomaticTrashDisposalDialog from './components/AutomaticTrashDisposalDialog'; import { userSelectors } from 'app/store/slices/user'; +import dateService from 'services/date.service'; import localStorageService from 'services/local-storage.service'; import { STORAGE_KEYS } from 'services/storage-keys'; @@ -36,6 +37,11 @@ const TrashView = (props: TrashViewProps) => { const { items, isLoadingItemsOnTrash } = props; const { translate } = useTranslationContext(); + const nonExpiredItems = useMemo( + () => items.filter((item) => !item.expiresAt || dateService.getHoursUntilExpiration(item.expiresAt) > 0), + [items], + ); + const workspaceSelected = useSelector(workspacesSelectors.getSelectedWorkspace); const hasSignedToday = useAppSelector(userSelectors.hasSignedToday); const getTrash = workspaceSelected ? getWorkspaceTrashPaginated : getTrashPaginated; @@ -59,7 +65,7 @@ const TrashView = (props: TrashViewProps) => { From 7d75c4ebf0040d15963e85003175ab5d365c3b47 Mon Sep 17 00:00:00 2001 From: Francis Terrero Date: Mon, 9 Mar 2026 17:48:58 -0400 Subject: [PATCH 5/5] chore: trigger pipeline