Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
1410020
feat: add version history menu item to drive context menu
terrerox Dec 4, 2025
1090978
feat: implement version history sidebar for drive items
terrerox Dec 4, 2025
b4b5565
refactor: restructure version history sidebar with modular components
terrerox Dec 5, 2025
b1d81a3
refactor: rename version history dialog state and actions to sidebar
terrerox Dec 5, 2025
aea78d9
feat: add internationalization support to version history module
terrerox Dec 5, 2025
3f2091e
refactor: simplify version history components and remove unused code
terrerox Dec 5, 2025
c47b83e
feature: implement file version history with restore, delete, and d…
terrerox Dec 10, 2025
5d54b7e
refactor: update skeleton component to use Array.from for better read…
terrerox Dec 10, 2025
ac309af
feature: improve file version downloads with multipart support and …
terrerox Dec 10, 2025
8c1579c
refactor: remove redundant error handling in version download
terrerox Dec 10, 2025
c5ba49e
feature: enhance internationalization for version management with add…
terrerox Dec 10, 2025
3e31c8e
feature: enhance version history with batch operations and SDK inte…
terrerox Dec 12, 2025
09f8a34
feature: refactor file upload logic and enhance version info handling…
terrerox Dec 12, 2025
16ffd60
feature: update currentVersion state handling in Sidebar to reflect i…
terrerox Dec 12, 2025
efa5e40
feature: integrate versioning limits context and enhance version hi…
terrerox Dec 16, 2025
87d41e4
feature: implement versioning limits context and enhance file replace…
terrerox Dec 16, 2025
6b94c39
refactor: implement Redux-based caching for file versions and remov…
terrerox Dec 16, 2025
d8bb143
feature: enhance version history menu item to handle availability bas…
terrerox Dec 16, 2025
ba4ec35
refactor: improve file versions state management and naming convent…
terrerox Dec 17, 2025
636c509
refactor: remove unused selectors from fileVersionsSelectors
terrerox Dec 17, 2025
4e0af53
refactor: add default empty array return to getVersionsByFileId selector
terrerox Dec 18, 2025
1eaed50
feature: add file extension validation for version replacement and im…
terrerox Jan 8, 2026
054cf3f
refactor: add user avatar support and update border styling in versio…
terrerox Jan 9, 2026
802d5a9
fix: add backdrop overlay to prevent dropdown hover bleed-through in …
terrerox Jan 9, 2026
6c178b7
fix: pin version size label to the right edge
terrerox Jan 9, 2026
08a4c95
test: add comprehensive test coverage for file version history feature
terrerox Dec 18, 2025
b6beafe
test: improve test descriptions using Given-When-Then pattern
terrerox Dec 22, 2025
52bc06b
test: refine test descriptions for consistency and clarity
terrerox Dec 23, 2025
7966f01
test: add unit tests for Redux file versions slice and version item a…
terrerox Dec 18, 2025
b9f976b
refactor: simplify test descriptions in file versions slice tests
terrerox Dec 18, 2025
1c1f529
test: refactor test descriptions to use Given-When-Then pattern
terrerox Dec 22, 2025
365e798
feat: add locked feature modal and versioning enhancements
terrerox Jan 10, 2026
1708f3e
feat: add locked feature localization for multiple languages
terrerox Jan 10, 2026
1e3e929
feature: update LockedFeatureModal styles and improve accessibility
terrerox Jan 10, 2026
c611ef5
feat: add timestamped filename format for version downloads
terrerox Jan 10, 2026
3a2dea6
test: update date format in version item menu tests
terrerox Jan 12, 2026
39fe918
feature: simplify download name generation for single items
terrerox Jan 12, 2026
aa07cc5
refactor: simplify handleUpgrade function in Sidebar component
terrerox Jan 12, 2026
3bc8d3f
refactor: streamline LockedFeatureModal layout and remove unused styles
terrerox Jan 13, 2026
469c291
style: enhance hover effect for unselected version item in dark mode
terrerox Jan 13, 2026
7e2608c
style: adjust padding and text styles in LockedFeatureModal for impro…
terrerox Jan 13, 2026
08e496b
fix: improve sidebar visibility logic by consolidating open state checks
terrerox Jan 15, 2026
565ee07
feat: add loading state for file limits in Sidebar component
terrerox Jan 20, 2026
330c14d
feat: implement smart polling for version limits after plan changes
terrerox Feb 1, 2026
b6c7ee7
fix: handle file extension correctly in version download filename
terrerox Feb 4, 2026
808ae27
refactor: optimize version extension checking and clean up imports
terrerox Feb 11, 2026
23b70f8
feature: filter expired versions and optimize re-renders in version h…
terrerox Feb 11, 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
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import DriveItemInfoMenu from 'app/drive/components/DriveItemInfoMenu/DriveItemI
import SharedFolderTooBigDialog from '../../../drive/components/SharedFolderTooBigDialog/SharedFolderTooBigDialog';
import { getAppConfig } from 'services/config.service';
import ShareItemDialog from '../../../../views/Shared/components/ShareItemDialog/ShareItemDialog';
import { Sidebar as VersionHistorySidebar } from '../../../../views/Drive/components/VersionHistory';

export interface HeaderAndSidenavLayoutProps {
children: JSX.Element;
Expand Down Expand Up @@ -48,14 +49,18 @@ export default function HeaderAndSidenavLayout(props: HeaderAndSidenavLayoutProp

<div className="flex w-1 grow flex-col">
<Navbar hideSearch={hideSearch} />
<div className="flex h-1 w-full grow">
<div className="relative flex h-1 w-full grow">
{children}

{isDriveItemInfoMenuOpen && driveItemInfo && (
<DriveItemInfoMenu {...driveItemInfo} onClose={onDriveItemInfoMenuClosed} />
)}

<VersionHistorySidebar />
</div>
<div className="absolute bottom-0 right-0 z-50 w-80">
<TaskLogger />
</div>
<TaskLogger />
</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);
Copy link
Collaborator

Choose a reason for hiding this comment

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

it is necessary to mantain the old trashAndUpload logic or we can replace it with new replace file logic for all users?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We need to maintain the old logic for users who don’t have versioning enabled. You might ask: why not move this to the backend? That won’t be possible because the replace file endpoint is used by the CLI. If a user with versioning enabled creates another version and exceeds the limit, we need to permanently delete the older version of the file. That endpoint already handles this logic for the CLI, and now it’s being used for the web as well.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Perhaps that is the new desired behaviour. Please confirm with Product dept if the replacement behaviour is the new desired one :)
If not, we can leave it as it is

Copy link
Contributor

Choose a reason for hiding this comment

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

The replace file endpoint is open for everyone (including free users). Desktop, mobile, etc.

I think the same as Ramon, for me it makes more sense to actually replace the file always, as it is the expected behaviour (instead of trashing and uploading it again)

Copy link
Contributor Author

@terrerox terrerox Jan 13, 2026

Choose a reason for hiding this comment

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

That approach falls under criteria #2 and #3 of this story

}
});

dispatch(fetchSortedFolderContentThunk(folderId));
}
};

const keepAndUploadItem = async (itemsToUpload: (IRoot | File)[]) => {
Expand Down
1 change: 1 addition & 0 deletions src/app/drive/services/downloadManager.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,7 @@ describe('downloadManagerService', () => {
updateProgressCallback: mockUpdateProgress,
abortController: mockTask.abortController,
sharingOptions: mockTask.credentials,
downloadName: mockTask.options.downloadName,
});
});

Expand Down
24 changes: 16 additions & 8 deletions src/app/drive/services/downloadManager.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export type DownloadItem = {
downloadOptions?: {
areSharedItems?: boolean;
showErrors?: boolean;
downloadName?: string;
};
createFoldersIterator?: FolderIterator | SharedFolderIterator;
createFilesIterator?: FileIterator | SharedFileIterator;
Expand Down Expand Up @@ -92,6 +93,13 @@ export type DownloadTask = {
export class DownloadManagerService {
public static readonly instance: DownloadManagerService = new DownloadManagerService();

private getSingleItemName(item: DownloadItem['payload'][0]): string {
if (item.isFolder) {
return item.name;
}
return item.type ? `${item.name}.${item.type}` : item.name;
}

readonly getDownloadCredentialsFromWorkspace = (
selectedWorkspace: WorkspaceData | null,
workspaceCredentials: WorkspaceCredentialsDetails | null,
Expand Down Expand Up @@ -119,14 +127,11 @@ export class DownloadManagerService {
const abort = () => Promise.resolve(uploadFolderAbortController.abort('Download cancelled'));

const formattedDate = date.format(new Date(), 'YYYY-MM-DD_HHmmss');
let downloadName = `Internxt (${formattedDate})`;
if (itemsPayload.length === 1) {
const item = itemsPayload[0];
if (itemsPayload[0].isFolder) {
downloadName = item.name;
} else {
downloadName = item.type ? `${item.name}.${item.type}` : item.name;
}
let downloadName = downloadItem.downloadOptions?.downloadName;

if (!downloadName) {
downloadName =
itemsPayload.length === 1 ? this.getSingleItemName(itemsPayload[0]) : `Internxt (${formattedDate})`;
}

let taskId = downloadItem.taskId;
Expand Down Expand Up @@ -295,6 +300,7 @@ export class DownloadManagerService {
updateProgressCallback,
abortController,
sharingOptions: credentials,
downloadName: options.downloadName,
});

console.timeEnd(`download-file-${file.uuid}`);
Expand Down Expand Up @@ -508,6 +514,7 @@ export class DownloadManagerService {
updateProgressCallback: (progress: number) => void;
abortController?: AbortController;
sharingOptions: { credentials: { user: string; pass: string }; mnemonic: string };
downloadName?: string;
}) => {
const shouldDownloadUsingBlob =
!!(navigator.brave && (await navigator.brave.isBrave())) ||
Expand All @@ -531,6 +538,7 @@ export class DownloadManagerService {
itemData: payload.file,
updateProgressCallback: payload.updateProgressCallback,
abortController: payload.abortController,
downloadName: payload.downloadName,
});
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface HandleWorkerMessagesPayload {
abortController?: AbortController;
itemData: DriveFileData;
updateProgressCallback: (progress: number) => void;
downloadName?: string;
}

interface HandleMessagesPayload {
Expand All @@ -37,9 +38,10 @@ export class DownloadWorkerHandler {
abortController,
itemData,
updateProgressCallback,
downloadName,
}: HandleWorkerMessagesPayload) {
const fileName = itemData.plainName ?? itemData.name;
const completeFilename = itemData.type ? `${fileName}.${itemData.type}` : fileName;
const completeFilename = downloadName || (itemData.type ? `${fileName}.${itemData.type}` : fileName);
const downloadId = itemData.fileId;
const fileSize = itemData.size;

Expand Down
31 changes: 30 additions & 1 deletion src/app/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,34 @@
"Ort": "Ort"
}
},
"versionHistory": {
"title": "Versionsverlauf",
"current": "Aktuell",
"expiresInDays": "Läuft in {{days}} Tagen ab",
"autosaveVersions": "{{count}}/{{total}} automatisch gespeicherte Versionen",
"restoreVersion": "Version wiederherstellen",
"downloadVersion": "Version herunterladen",
"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",
"lockedFeature": {
"title": "Gesperrte Funktion",
"description": "Stellen Sie frühere Versionen Ihrer Dateien wieder her und verfolgen Sie Änderungen im Laufe der Zeit.",
"supportedFormats": "Verfügbar für PDF-, Word-, Excel- und CSV-Dateien in kostenpflichtigen Plänen.",
"upgradeButton": "Upgrade"
}
},
"shareModal": {
"title": "\"{{name}}\" teilen",
"list": {
Expand Down Expand Up @@ -1462,7 +1490,8 @@
"restore": "Wiederherstellen",
"deletePermanently": "Lösche dauerhaft",
"move": "Verschiebe",
"delete": "Lösche"
"delete": "Lösche",
"versionHistory": "Versionsverlauf"
},
"deleteItems": {
"title": "Dauerhaft löschen?",
Expand Down
29 changes: 29 additions & 0 deletions src/app/i18n/locales/en.json
Copy link
Contributor

Choose a reason for hiding this comment

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

You must add the translations for the other 7 languages.

Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,34 @@
"location": "Location"
}
},
"versionHistory": {
"title": "Version history",
"current": "Current",
"expiresInDays": "Expires in {{days}} days",
"autosaveVersions": "{{count}}/{{total}} autosave versions",
"restoreVersion": "Restore version",
"downloadVersion": "Download version",
"deleteVersion": "Delete version",
"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",
"lockedFeature": {
"title": "Locked feature",
"description": "Restore previous versions of your files and track changes over time.",
"supportedFormats": "Available for PDF, Word, Excel, and CSV files in paid plans.",
"upgradeButton": "Upgrade"
}
},
"shareModal": {
"title": "Share \"{{name}}\"",
"list": {
Expand Down Expand Up @@ -1538,6 +1566,7 @@
"share": "Share",
"manageAccess": "Manage access",
"download": "Download",
"versionHistory": "Version history",
"moveToTrash": "Move to trash",
"copyLink": "Copy link",
"linkSettings": "Link settings",
Expand Down
29 changes: 29 additions & 0 deletions src/app/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,34 @@
"location": "Ubicación"
}
},
"versionHistory": {
"title": "Historial de versiones",
"current": "Actual",
"expiresInDays": "Expira en {{days}} días",
"autosaveVersions": "{{count}}/{{total}} versiones de autoguardado",
"restoreVersion": "Restaurar versión",
"downloadVersion": "Descargar 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",
"lockedFeature": {
"title": "Función bloqueada",
"description": "Restaura versiones anteriores de tus archivos y realiza un seguimiento de los cambios a lo largo del tiempo.",
"supportedFormats": "Disponible para archivos PDF, Word, Excel y CSV en planes de pago.",
"upgradeButton": "Mejorar plan"
}
},
"shareModal": {
"title": "Compartir \"{{name}}\"",
"list": {
Expand Down Expand Up @@ -1515,6 +1543,7 @@
"manageLinkAccess": "Gestionar acceso",
"manageAccess": "Gestionar acceso",
"download": "Descargar",
"versionHistory": "Historial de versiones",
"moveToTrash": "Mover a la papelera",
"copyLink": "Copiar enlace",
"linkSettings": "Ajustes del enlace",
Expand Down
Loading
Loading