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/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 40c2e4f912..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",
@@ -1536,7 +1540,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 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/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/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/DriveExplorerList.tsx
similarity index 93%
rename from src/views/Drive/components/DriveExplorer/components/DriveExplorerList.tsx
rename to src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerList.tsx
index b3749e34ba..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 } 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';
@@ -57,7 +58,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 });
@@ -139,6 +140,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;
@@ -167,6 +169,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,32 +459,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: !isRecents && !isTrash,
- defaultDirection: 'ASC',
- width: 'w-size',
- name: 'size',
- },
- ]}
+
+ header={getListHeaders(translate, isRecents, isTrash)}
checkboxDataCy="driveListHeaderCheckbox"
disableKeyboardShortcuts={props.disableKeyboardShortcuts || props.showStopSharingConfirmation}
items={props.items}
@@ -485,7 +469,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 76%
rename from src/views/Drive/components/DriveExplorer/components/DriveExplorerListItem.tsx
rename to src/views/Drive/components/DriveExplorer/components/DriveExplorerList/DriveExplorerListItem.tsx
index 8ce573bc36..85973f7b24 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 ? dateService.calculateDaysUntilDate(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..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';