Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1612aff
feature: implement file version history with restore, delete, and d…
terrerox Dec 10, 2025
4a2ab35
feature: enhance internationalization for version management with add…
terrerox Dec 10, 2025
f59d219
refactor: update skeleton component to use Array.from for better read…
terrerox Dec 10, 2025
e348210
feature: improve file version downloads with multipart support and …
terrerox Dec 10, 2025
7195005
fix: update @internxt/sdk version to 1.11.19 in package.json and yarn…
terrerox Dec 10, 2025
e2b34bb
refactor: remove redundant error handling in version download
terrerox Dec 10, 2025
da946c8
Merge pull request #1773 from internxt/feature/file-version-history-v3
terrerox Dec 10, 2025
f3a74ba
feature: enhance version history with batch operations and SDK inte…
terrerox Dec 12, 2025
fb6a5fd
feature: refactor file upload logic and enhance version info handling…
terrerox Dec 12, 2025
0278fd6
feature: update currentVersion state handling in Sidebar to reflect i…
terrerox Dec 12, 2025
7f94127
feature: integrate versioning limits context and enhance version hi…
terrerox Dec 16, 2025
9d6956e
feature: implement versioning limits context and enhance file replace…
terrerox Dec 16, 2025
2c5b00a
refactor: implement Redux-based caching for file versions and remov…
terrerox Dec 16, 2025
79b26db
feature: enhance version history menu item to handle availability bas…
terrerox Dec 16, 2025
e3bffd7
refactor: improve file versions state management and naming convent…
terrerox Dec 17, 2025
02fe0fe
refactor: remove unused selectors from fileVersionsSelectors
terrerox Dec 17, 2025
700735a
test: add comprehensive test coverage for file version history feature
terrerox Dec 18, 2025
abe4124
test: add unit tests for Redux file versions slice and version item a…
terrerox Dec 18, 2025
f3f12d1
refactor: simplify test descriptions in file versions slice tests
terrerox Dec 18, 2025
d919cd2
refactor: add default empty array return to getVersionsByFileId selector
terrerox Dec 18, 2025
ea961f7
chore: update @internxt/sdk package registry source
terrerox Dec 18, 2025
564699e
test: improve test descriptions using Given-When-Then pattern
terrerox Dec 22, 2025
7084646
test: refactor test descriptions to use Given-When-Then pattern
terrerox Dec 22, 2025
0a418b4
test: refine test descriptions for consistency and clarity
terrerox Dec 23, 2025
6906b89
feature: add file extension validation for version replacement and im…
terrerox Jan 8, 2026
2bf1abf
refactor: add user avatar support and update border styling in versio…
terrerox Jan 9, 2026
e361615
fix: add backdrop overlay to prevent dropdown hover bleed-through in …
terrerox Jan 9, 2026
467836c
fix: pin version size label to the right edge
terrerox Jan 9, 2026
73de535
Merge pull request #1785 from internxt/feature/file-version-history-v6
terrerox Jan 9, 2026
59796c9
Merge pull request #1784 from internxt/feature/file-version-history-v5
terrerox Jan 9, 2026
8182d3b
Merge pull request #1779 from internxt/feature/file-version-history-v4
terrerox Jan 9, 2026
34d61ea
Merge pull request #1774 from internxt/feature/file-version-history-v3
terrerox Jan 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"@iconscout/react-unicons": "^1.1.6",
"@internxt/css-config": "1.1.0",
"@internxt/lib": "1.4.1",
"@internxt/sdk": "=1.11.17",
"@internxt/sdk": "=1.11.24",
"@internxt/ui": "0.1.1",
"@phosphor-icons/react": "^2.1.7",
"@popperjs/core": "^2.11.6",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ export default function HeaderAndSidenavLayout(props: HeaderAndSidenavLayoutProp

<VersionHistorySidebar />
</div>
<TaskLogger />
<div className="absolute bottom-0 right-0 z-50 w-80">
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this div wrapping the task logger in necessary? I think could cause wrong behaviours

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When downloading a version, the task logger was located under the File Versions sidebar, making it difficult to cancel the download.

<TaskLogger />
</div>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import { DriveItemData } from 'app/drive/types';
import { IRoot } from 'app/store/slices/storage/types';
import workspacesSelectors from 'app/store/slices/workspaces/workspaces.selectors';
import { uploadFoldersWithManager } from 'app/network/UploadFolderManager';
import replaceFileService from 'views/Drive/services/replaceFile.service';
import { Network, getEnvironmentConfig } from 'app/drive/services/network.service';
import { fileVersionsActions, fileVersionsSelectors } from 'app/store/slices/fileVersions';
import { isVersioningExtensionAllowed } from 'views/Drive/components/VersionHistory/utils';

type NameCollisionContainerProps = {
currentFolderId: string;
Expand Down Expand Up @@ -43,6 +47,8 @@ const NameCollisionContainer: FC<NameCollisionContainerProps> = ({
() => moveDestinationFolderId ?? currentFolderId,
[moveDestinationFolderId, currentFolderId],
);
const limits = useAppSelector(fileVersionsSelectors.getLimits);
const isVersioningEnabled = limits?.versioning?.enabled ?? false;

const handleNewItems = (files: (File | DriveItemData)[], folders: (IRoot | DriveItemData)[]) => [
...files,
Expand Down Expand Up @@ -122,18 +128,61 @@ const NameCollisionContainer: FC<NameCollisionContainerProps> = ({
);
};

const uploadFileAndGetFileId = async (file: File, itemToReplace: DriveItemData) => {
const { bridgeUser, bridgePass, encryptionKey, bucketId } = getEnvironmentConfig(!!selectedWorkspace);
const network = new Network(bridgeUser, bridgePass, encryptionKey);

const taskId = `replace-${itemToReplace.uuid}-${Date.now()}`;

const [uploadPromise] = network.uploadFile(
bucketId,
{
filecontent: file,
filesize: file.size,
progressCallback: () => {},
},
{ taskId },
);

return await uploadPromise;
};

const replaceFileVersion = async (file: File, itemToReplace: DriveItemData) => {
const newFileId = await uploadFileAndGetFileId(file, itemToReplace);
await replaceFileService.replaceFile(itemToReplace.uuid, {
fileId: newFileId,
size: file.size,
});
dispatch(fileVersionsActions.invalidateCache(itemToReplace.uuid));
};

const trashAndUpload = async (file: File, itemToReplace: DriveItemData) => {
await moveItemsToTrash([itemToReplace]);
await dispatch(
storageThunks.uploadItemsThunk({
files: [file],
parentFolderId: folderId,
options: {
disableDuplicatedNamesCheck: true,
},
}),
);
};

const replaceAndUploadItem = async ({
itemsToReplace,
itemsToUpload,
}: {
itemsToReplace: DriveItemData[];
itemsToUpload: (IRoot | File)[];
}) => {
await moveItemsToTrash(itemsToReplace);
for (let i = 0; i < itemsToUpload.length; i++) {
const itemToUpload = itemsToUpload[i];
const itemToReplace = itemsToReplace[i];

itemsToUpload.forEach((itemToUpload) => {
if ((itemToUpload as IRoot).fullPathEdited) {
uploadFoldersWithManager({
await moveItemsToTrash([itemToReplace]);
await uploadFoldersWithManager({
payload: [
{
root: { ...(itemToUpload as IRoot) },
Expand All @@ -142,23 +191,15 @@ const NameCollisionContainer: FC<NameCollisionContainerProps> = ({
],
selectedWorkspace,
dispatch,
}).then(() => {
dispatch(fetchSortedFolderContentThunk(folderId));
});
} else {
dispatch(
storageThunks.uploadItemsThunk({
files: [itemToUpload] as File[],
parentFolderId: folderId,
options: {
disableDuplicatedNamesCheck: true,
},
}),
).then(() => {
dispatch(fetchSortedFolderContentThunk(folderId));
});
const file = itemToUpload as File;
const canReplaceVersion = isVersioningEnabled && isVersioningExtensionAllowed(itemToReplace);
canReplaceVersion ? await replaceFileVersion(file, itemToReplace) : await trashAndUpload(file, itemToReplace);
}
});

dispatch(fetchSortedFolderContentThunk(folderId));
}
};

const keepAndUploadItem = async (itemsToUpload: (IRoot | File)[]) => {
Expand Down
15 changes: 14 additions & 1 deletion src/app/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -795,7 +795,20 @@
"autosaveVersions": "{{count}}/{{total}} automatisch gespeicherte Versionen",
"restoreVersion": "Version wiederherstellen",
"downloadVersion": "Version herunterladen",
"deleteVersion": "Version löschen"
"deleteVersion": "Version löschen",
"deleteVersionTitle": "Version löschen",
"deleteButton": "Löschen",
"restoreButton": "Wiederherstellen",
"downloadError": "Fehler beim Herunterladen der Version",
"deleteVersionAdvice": "Diese Version wird dauerhaft gelöscht. \nDiese Aktion kann nicht rückgängig gemacht werden.",
"deletingVersion": "Lösche",
"restoreVersionTitle": "Version wiederherstellen",
"restoreVersionAdvice": "Das Wiederherstellen dieser Version ersetzt die aktuelle Datei und entfernt alle neueren Versionen. \nSobald wiederhergestellt, kann diese Aktion nicht rückgängig gemacht werden.\n\nSie können eine Kopie neuerer Versionen herunterladen, bevor Sie wiederherstellen.",
"restoringVersion": "Wiederherstellung",
"restoreSuccess": "Version erfolgreich wiederhergestellt",
"restoreError": "Fehler beim Wiederherstellen der Version",
"deleteSuccess": "Version erfolgreich gelöscht",
"deleteError": "Fehler beim Löschen der Version"
},
"shareModal": {
"title": "Aktie \"{{name}}\"",
Expand Down
15 changes: 14 additions & 1 deletion src/app/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -892,7 +892,20 @@
"autosaveVersions": "{{count}}/{{total}} autosave versions",
"restoreVersion": "Restore version",
"downloadVersion": "Download version",
"deleteVersion": "Delete version"
"deleteVersion": "Delete version",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The remaining translations are included in this PR: #1773

"deleteVersionTitle": "Delete version",
"deleteButton": "Delete",
"restoreButton": "Restore",
"downloadError": "Failed to download version",
"deleteVersionAdvice": "This version will be permanently deleted. \nThis action cannot be undone.",
"deletingVersion": "Deleting",
"restoreVersionTitle": "Restore version",
"restoreVersionAdvice": "Restoring this version will replace the current file and remove all newer versions. \nOnce restored, this action cannot be undone.\n\nYou can download a copy of newer versions before restoring.",
"restoringVersion": "Restoring",
"restoreSuccess": "Version restored successfully",
"restoreError": "Failed to restore version",
"deleteSuccess": "Version deleted successfully",
"deleteError": "Failed to delete version"
},
"shareModal": {
"title": "Share \"{{name}}\"",
Expand Down
15 changes: 14 additions & 1 deletion src/app/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -874,7 +874,20 @@
"autosaveVersions": "{{count}}/{{total}} versiones de autoguardado",
"restoreVersion": "Restaurar versión",
"downloadVersion": "Descargar versión",
"deleteVersion": "Eliminar versión"
"deleteVersion": "Eliminar versión",
"deleteVersionTitle": "Eliminar versión",
"deleteButton": "Eliminar",
"restoreButton": "Restaurar",
"downloadError": "Error al descargar la versión",
"deleteVersionAdvice": "Esta versión será eliminada permanentemente. \nEsta acción no se puede deshacer.",
"deletingVersion": "Eliminando",
"restoreVersionTitle": "Restaurar versión",
"restoreVersionAdvice": "Restaurar esta versión reemplazará el archivo actual y eliminará todas las versiones más recientes. \nUna vez restaurada, esta acción no se puede deshacer.\n\nPuedes descargar una copia de las versiones más recientes antes de restaurar.",
"restoringVersion": "Restaurando",
"restoreSuccess": "Versión restaurada exitosamente",
"restoreError": "Error al restaurar la versión",
"deleteSuccess": "Versión eliminada exitosamente",
"deleteError": "Error al eliminar la versión"
},
"shareModal": {
"title": "Compartir \"{{name}}\"",
Expand Down
15 changes: 14 additions & 1 deletion src/app/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -816,7 +816,20 @@
"autosaveVersions": "{{count}}/{{total}} versions de sauvegarde automatique",
"restoreVersion": "Restaurer la version",
"downloadVersion": "Télécharger la version",
"deleteVersion": "Supprimer la version"
"deleteVersion": "Supprimer la version",
"deleteVersionTitle": "Supprimer la version",
"deleteButton": "Supprimer",
"restoreButton": "Restaurer",
"downloadError": "Échec du téléchargement de la version",
"deleteVersionAdvice": "Cette version sera définitivement supprimée. \nCette action ne peut pas être annulée.",
"deletingVersion": "Suppression",
"restoreVersionTitle": "Restaurer la version",
"restoreVersionAdvice": "La restauration de cette version remplacera le fichier actuel et supprimera toutes les versions plus récentes. \nUne fois restaurée, cette action ne peut pas être annulée.\n\nVous pouvez télécharger une copie des versions plus récentes avant de restaurer.",
"restoringVersion": "Restauration",
"restoreSuccess": "Version restaurée avec succès",
"restoreError": "Échec de la restauration de la version",
"deleteSuccess": "Version supprimée avec succès",
"deleteError": "Échec de la suppression de la version"
},
"newFolderModal": {
"title": "Nouveau dossier",
Expand Down
15 changes: 14 additions & 1 deletion src/app/i18n/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -928,7 +928,20 @@
"autosaveVersions": "{{count}}/{{total}} versioni di salvataggio automatico",
"restoreVersion": "Ripristina versione",
"downloadVersion": "Scarica versione",
"deleteVersion": "Elimina versione"
"deleteVersion": "Elimina versione",
"deleteVersionTitle": "Elimina versione",
"deleteButton": "Elimina",
"restoreButton": "Ripristina",
"downloadError": "Impossibile scaricare la versione",
"deleteVersionAdvice": "Questa versione sarà eliminata in modo permanente. \nQuesta azione non può essere annullata.",
"deletingVersion": "Eliminazione",
"restoreVersionTitle": "Ripristina versione",
"restoreVersionAdvice": "Il ripristino di questa versione sostituirà il file corrente e rimuoverà tutte le versioni più recenti. \nUna volta ripristinata, questa azione non può essere annullata.\n\nPuoi scaricare una copia delle versioni più recenti prima del ripristino.",
"restoringVersion": "Ripristino",
"restoreSuccess": "Versione ripristinata con successo",
"restoreError": "Impossibile ripristinare la versione",
"deleteSuccess": "Versione eliminata con successo",
"deleteError": "Impossibile eliminare la versione"
},
"shareModal": {
"title": "Condividi \"{{name}}\"",
Expand Down
15 changes: 14 additions & 1 deletion src/app/i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -835,7 +835,20 @@
"autosaveVersions": "{{count}}/{{total}} версий автосохранения",
"restoreVersion": "Восстановить версию",
"downloadVersion": "Скачать версию",
"deleteVersion": "Удалить версию"
"deleteVersion": "Удалить версию",
"deleteVersionTitle": "Удалить версию",
"deleteButton": "Удалить",
"restoreButton": "Восстановить",
"downloadError": "Не удалось загрузить версию",
"deleteVersionAdvice": "Эта версия будет удалена навсегда. \nЭто действие нельзя отменить.",
"deletingVersion": "Удаление",
"restoreVersionTitle": "Восстановить версию",
"restoreVersionAdvice": "Восстановление этой версии заменит текущий файл и удалит все более новые версии. \nПосле восстановления это действие нельзя отменить.\n\nВы можете скачать копию более новых версий перед восстановлением.",
"restoringVersion": "Восстановление",
"restoreSuccess": "Версия успешно восстановлена",
"restoreError": "Не удалось восстановить версию",
"deleteSuccess": "Версия успешно удалена",
"deleteError": "Не удалось удалить версию"
},
"shareModal": {
"title": "Поделиться \"{{name}}\"",
Expand Down
15 changes: 14 additions & 1 deletion src/app/i18n/locales/tw.json
Original file line number Diff line number Diff line change
Expand Up @@ -822,7 +822,20 @@
"autosaveVersions": "{{count}}/{{total}} 自動儲存版本",
"restoreVersion": "復原版本",
"downloadVersion": "下載版本",
"deleteVersion": "刪除版本"
"deleteVersion": "刪除版本",
"deleteVersionTitle": "刪除版本",
"deleteButton": "刪除",
"restoreButton": "復原",
"downloadError": "下載版本失敗",
"deleteVersionAdvice": "此版本將被永久刪除。\n此操作無法復原。",
"deletingVersion": "刪除中",
"restoreVersionTitle": "復原版本",
"restoreVersionAdvice": "復原此版本將取代目前檔案並刪除所有較新的版本。\n一旦復原,此操作無法撤銷。\n\n您可以在復原之前下載較新版本的副本。",
"restoringVersion": "復原中",
"restoreSuccess": "版本復原成功",
"restoreError": "復原版本失敗",
"deleteSuccess": "版本刪除成功",
"deleteError": "刪除版本失敗"
},
"shareModal": {
"title": "分享“{{name}}”",
Expand Down
15 changes: 14 additions & 1 deletion src/app/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -858,7 +858,20 @@
"autosaveVersions": "{{count}}/{{total}} 自动保存版本",
"restoreVersion": "恢复版本",
"downloadVersion": "下载版本",
"deleteVersion": "删除版本"
"deleteVersion": "删除版本",
"deleteVersionTitle": "删除版本",
"deleteButton": "删除",
"restoreButton": "恢复",
"downloadError": "下载版本失败",
"deleteVersionAdvice": "此版本将被永久删除。\n此操作无法撤销。",
"deletingVersion": "删除中",
"restoreVersionTitle": "恢复版本",
"restoreVersionAdvice": "恢复此版本将替换当前文件并删除所有较新的版本。\n一旦恢复,此操作无法撤销。\n\n您可以在恢复之前下载较新版本的副本。",
"restoringVersion": "恢复中",
"restoreSuccess": "版本恢复成功",
"restoreError": "恢复版本失败",
"deleteSuccess": "版本删除成功",
"deleteError": "删除版本失败"
},
"shareModal": {
"title": "分享 \"{{name}}\"",
Expand Down
2 changes: 2 additions & 0 deletions src/app/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import taskManagerReducer from './slices/taskManager';
import uiReducer from './slices/ui';
import userReducer from './slices/user';
import workspacesReducer from './slices/workspaces/workspacesStore';
import { fileVersionsReducer } from './slices/fileVersions';

export const store = configureStore({
reducer: {
Expand All @@ -25,6 +26,7 @@ export const store = configureStore({
referrals: referralsReducer,
shared: sharedReducer,
workspaces: workspacesReducer,
fileVersions: fileVersionsReducer,
},
});

Expand Down
16 changes: 16 additions & 0 deletions src/app/store/slices/fileVersions/fileVersions.selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { FileVersion, FileLimitsResponse } from '@internxt/sdk/dist/drive/storage/types';
import { RootState } from '../..';

const fileVersionsSelectors = {
getLimits(state: RootState): FileLimitsResponse | null {
return state.fileVersions.limits;
},
getVersionsByFileId(state: RootState, fileId: NonNullable<FileVersion['fileId']>): FileVersion[] {
return state.fileVersions.versionsByFileId[fileId] ?? [];
},
isLoadingByFileId(state: RootState, fileId: NonNullable<FileVersion['fileId']>): boolean {
return state.fileVersions.isLoadingByFileId[fileId] ?? false;
},
};

export default fileVersionsSelectors;
Loading
Loading