From 091c6700bc6dbcbf939419f4ce9f8a959b1760c0 Mon Sep 17 00:00:00 2001 From: Francis Terrero Date: Sat, 10 Jan 2026 00:00:06 -0400 Subject: [PATCH 01/14] feat: add locked feature modal and versioning enhancements - Added a locked feature modal to inform users about version restoration capabilities in paid plans. - Updated i18n translations for the new locked feature. - Refactored file version service imports and related tests. - Enhanced version history menu configuration to handle locked states. - Implemented dropdown positioning hook for better UI interactions. - Added tests for version item actions and dropdown positioning. - Integrated version limits fetching in the PlansSection for better user experience. --- src/app/i18n/locales/en.json | 8 ++- .../store/slices/fileVersions/index.test.ts | 4 +- src/app/store/slices/fileVersions/index.ts | 2 +- .../components/DriveExplorerList.tsx | 2 +- .../components/DriveItemContextMenu.tsx | 3 +- .../components/DriveTopBarActions.tsx | 6 +- .../components/VersionHistory/Sidebar.tsx | 42 +++++++++++-- .../components/LockedFeatureModal.tsx | 62 +++++++++++++++++++ .../VersionHistory/components/VersionItem.tsx | 3 +- .../VersionHistory/components/index.ts | 1 + .../components/VersionHistory/hooks/index.ts | 3 - .../hooks/useVersionHistoryMenuConfig.ts | 30 --------- src/views/Drive/hooks/index.ts | 3 + src/views/Drive/hooks/useDriveItemActions.tsx | 6 +- .../hooks/useDropdownPositioning.test.ts | 0 .../hooks/useDropdownPositioning.ts | 0 .../hooks/useVersionHistoryMenuConfig.test.ts | 47 +------------- .../hooks/useVersionHistoryMenuConfig.ts | 16 +++++ .../hooks/useVersionItemActions.test.ts | 2 +- .../hooks/useVersionItemActions.ts | 0 .../services/fileVersion.service.test.ts | 0 .../services/fileVersion.service.ts | 0 .../Sections/Account/Plans/PlansSection.tsx | 7 ++- 23 files changed, 144 insertions(+), 103 deletions(-) create mode 100644 src/views/Drive/components/VersionHistory/components/LockedFeatureModal.tsx delete mode 100644 src/views/Drive/components/VersionHistory/hooks/index.ts delete mode 100644 src/views/Drive/components/VersionHistory/hooks/useVersionHistoryMenuConfig.ts rename src/views/Drive/{components/VersionHistory => }/hooks/useDropdownPositioning.test.ts (100%) rename src/views/Drive/{components/VersionHistory => }/hooks/useDropdownPositioning.ts (100%) rename src/views/Drive/{components/VersionHistory => }/hooks/useVersionHistoryMenuConfig.test.ts (57%) create mode 100644 src/views/Drive/hooks/useVersionHistoryMenuConfig.ts rename src/views/Drive/{components/VersionHistory => }/hooks/useVersionItemActions.test.ts (98%) rename src/views/Drive/{components/VersionHistory => }/hooks/useVersionItemActions.ts (100%) rename src/views/Drive/{components/VersionHistory => }/services/fileVersion.service.test.ts (100%) rename src/views/Drive/{components/VersionHistory => }/services/fileVersion.service.ts (100%) diff --git a/src/app/i18n/locales/en.json b/src/app/i18n/locales/en.json index 3f61f38ddb..4c07b2c703 100644 --- a/src/app/i18n/locales/en.json +++ b/src/app/i18n/locales/en.json @@ -905,7 +905,13 @@ "restoreSuccess": "Version restored successfully", "restoreError": "Failed to restore version", "deleteSuccess": "Version deleted successfully", - "deleteError": "Failed to delete version" + "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}}\"", diff --git a/src/app/store/slices/fileVersions/index.test.ts b/src/app/store/slices/fileVersions/index.test.ts index 6ed3ea378f..ce4f7dc2a7 100644 --- a/src/app/store/slices/fileVersions/index.test.ts +++ b/src/app/store/slices/fileVersions/index.test.ts @@ -1,10 +1,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { FileVersion, FileLimitsResponse } from '@internxt/sdk/dist/drive/storage/types'; -import fileVersionService from 'views/Drive/components/VersionHistory/services/fileVersion.service'; +import fileVersionService from 'views/Drive/services/fileVersion.service'; import { fileVersionsActions, fileVersionsReducer, fetchFileVersionsThunk, fetchVersionLimitsThunk } from './index'; import { RootState } from '../..'; -vi.mock('views/Drive/components/VersionHistory/services/fileVersion.service', () => ({ +vi.mock('views/Drive/services/fileVersion.service', () => ({ default: { getFileVersions: vi.fn(), getLimits: vi.fn(), diff --git a/src/app/store/slices/fileVersions/index.ts b/src/app/store/slices/fileVersions/index.ts index 70e099e007..821015f2d0 100644 --- a/src/app/store/slices/fileVersions/index.ts +++ b/src/app/store/slices/fileVersions/index.ts @@ -1,6 +1,6 @@ import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit'; import { FileVersion, FileLimitsResponse } from '@internxt/sdk/dist/drive/storage/types'; -import fileVersionService from 'views/Drive/components/VersionHistory/services/fileVersion.service'; +import fileVersionService from 'views/Drive/services/fileVersion.service'; interface FileVersionsState { versionsByFileId: Record, FileVersion[]>; diff --git a/src/views/Drive/components/DriveExplorer/components/DriveExplorerList.tsx b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList.tsx index cf1b4cb277..0cf66c4be1 100644 --- a/src/views/Drive/components/DriveExplorer/components/DriveExplorerList.tsx +++ b/src/views/Drive/components/DriveExplorer/components/DriveExplorerList.tsx @@ -34,7 +34,7 @@ import { } from './DriveItemContextMenu'; import { List } from '@internxt/ui'; import { DownloadManager } from 'app/network/DownloadManager'; -import { useVersionHistoryMenuConfig } from '../../VersionHistory/hooks'; +import { useVersionHistoryMenuConfig } from 'views/Drive/hooks/useVersionHistoryMenuConfig'; interface DriveExplorerListProps { folderId: string; diff --git a/src/views/Drive/components/DriveExplorer/components/DriveItemContextMenu.tsx b/src/views/Drive/components/DriveExplorer/components/DriveItemContextMenu.tsx index 81e5b6f965..a77466cd88 100644 --- a/src/views/Drive/components/DriveExplorer/components/DriveItemContextMenu.tsx +++ b/src/views/Drive/components/DriveExplorer/components/DriveItemContextMenu.tsx @@ -106,13 +106,12 @@ const getVersionHistoryMenuItem = ( ): MenuItemType => { const isLocked = config?.isLocked ?? false; const isExtensionAllowed = config?.isExtensionAllowed ?? true; - const onUpgradeClick = config?.onUpgradeClick; if (isLocked) { return { name: t('drive.dropdown.versionHistory') as string, icon: LockSimple, - action: onUpgradeClick, + action: viewVersionHistory, disabled: () => false, node: (
{ - if (versionHistoryMenuConfig.locked) { - versionHistoryMenuConfig.onLockedClick?.(); - return; - } dispatch(uiActions.setVersionHistoryItem(selectedItems[0])); dispatch(uiActions.setIsVersionHistorySidebarOpen(true)); }; diff --git a/src/views/Drive/components/VersionHistory/Sidebar.tsx b/src/views/Drive/components/VersionHistory/Sidebar.tsx index 0d1ba45778..20604f2173 100644 --- a/src/views/Drive/components/VersionHistory/Sidebar.tsx +++ b/src/views/Drive/components/VersionHistory/Sidebar.tsx @@ -5,6 +5,8 @@ import { uiActions } from 'app/store/slices/ui'; import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; import storageSelectors from 'app/store/slices/storage/storage.selectors'; import { fetchSortedFolderContentThunk } from 'app/store/slices/storage/storage.thunks/fetchSortedFolderContentThunk'; +import workspacesSelectors from 'app/store/slices/workspaces/workspaces.selectors'; +import navigationService from 'services/navigation.service'; import { Header, CurrentVersionItem, @@ -12,8 +14,9 @@ import { AutosaveSection, VersionActionDialog, VersionHistorySkeleton, + LockedFeatureModal, } from './components'; -import fileVersionService from 'views/Drive/components/VersionHistory/services/fileVersion.service'; +import fileVersionService from 'views/Drive/services/fileVersion.service'; import errorService from 'services/error.service'; import notificationsService, { ToastType } from 'app/notifications/services/notifications.service'; import { @@ -35,6 +38,7 @@ const Sidebar = () => { const isRestoreVersionDialogOpen = useAppSelector((state: RootState) => state.ui.isRestoreVersionDialogOpen); const versionToRestore = useAppSelector((state: RootState) => state.ui.versionToRestore); const currentFolderId = useAppSelector(storageSelectors.currentFolderId); + const selectedWorkspace = useAppSelector(workspacesSelectors.getSelectedWorkspace); const { translate } = useTranslationContext(); const limits = useAppSelector(fileVersionsSelectors.getLimits); @@ -51,6 +55,24 @@ const Sidebar = () => { updatedAt: '', }); + const isVersioningEnabled = limits?.versioning?.enabled ?? false; + + const blurredBackgroundVersions = useMemo(() => { + if (isVersioningEnabled) return []; + + const fixedDate = new Date('2024-05-01').toISOString(); + return Array.from({ length: 10 }, (_, i) => ({ + id: `mock-${i}`, + fileId: '', + size: '1000000', + createdAt: fixedDate, + updatedAt: fixedDate, + type: 'file', + })); + }, [isVersioningEnabled]); + + const displayVersions = isVersioningEnabled ? versions : blurredBackgroundVersions; + useEffect(() => { if (item) { setCurrentVersion({ @@ -59,7 +81,7 @@ const Sidebar = () => { }); } }, [item]); - const totalVersionsCount = versions.length; + const totalVersionsCount = displayVersions.length; const selectedCount = selectedAutosaveVersions.size; const selectAllAutosave = selectedCount === totalVersionsCount && totalVersionsCount > 0; const totalAllowedVersions = limits?.versioning.maxVersions ?? 0; @@ -201,6 +223,15 @@ const Sidebar = () => { dispatch(uiActions.setIsDeleteVersionDialogOpen(true)); }, [item, selectedCount, dispatch]); + const handleUpgrade = useCallback(() => { + dispatch(uiActions.setIsPreferencesDialogOpen(true)); + navigationService.openPreferencesDialog({ + section: 'account', + subsection: 'plans', + workspaceUuid: selectedWorkspace?.workspaceUser.workspaceId, + }); + }, [dispatch, selectedWorkspace]); + if (!item) return null; return ( <> @@ -214,8 +245,8 @@ const Sidebar = () => {
-
- {isLoading ? ( +
+ {isVersioningEnabled && isLoading ? ( ) : ( <> @@ -235,7 +266,7 @@ const Sidebar = () => { onDeleteAll={handleDeleteSelectedVersions} /> - {versions.map((version) => ( + {displayVersions.map((version) => ( { ))} )} + {!isVersioningEnabled && }
diff --git a/src/views/Drive/components/VersionHistory/components/LockedFeatureModal.tsx b/src/views/Drive/components/VersionHistory/components/LockedFeatureModal.tsx new file mode 100644 index 0000000000..ab2ccb66ca --- /dev/null +++ b/src/views/Drive/components/VersionHistory/components/LockedFeatureModal.tsx @@ -0,0 +1,62 @@ +import { Button } from '@internxt/ui'; +import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; +import { ClockCounterClockwise, LockSimple } from '@phosphor-icons/react'; + +interface LockedFeatureModalProps { + onUpgrade: () => void; +} + +const MODAL_DIMENSIONS = { + width: '282px', + height: '333px', +} as const; + +const ICON_SIZES = { + clock: 64, + lock: 35, +} as const; + +const COLORS = { + border: '#474747', + lock: '#737373', +} as const; + +export const LockedFeatureModal = ({ onUpgrade }: LockedFeatureModalProps) => { + const { translate } = useTranslationContext(); + + return ( +
+
+
+
+ +
+
+ +
+
+ +
+

+ {translate('modals.versionHistory.lockedFeature.title')} +

+

+ {translate('modals.versionHistory.lockedFeature.description')} +

+

+ {translate('modals.versionHistory.lockedFeature.supportedFormats')} +

+
+ +
+ +
+
+
+ ); +}; diff --git a/src/views/Drive/components/VersionHistory/components/VersionItem.tsx b/src/views/Drive/components/VersionHistory/components/VersionItem.tsx index 8415d0cb01..728976d792 100644 --- a/src/views/Drive/components/VersionHistory/components/VersionItem.tsx +++ b/src/views/Drive/components/VersionHistory/components/VersionItem.tsx @@ -1,7 +1,8 @@ import { Info, DotsThree } from '@phosphor-icons/react'; import { Checkbox, Dropdown, Avatar } from '@internxt/ui'; import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; -import { useDropdownPositioning, useVersionItemActions } from '../hooks'; +import { useDropdownPositioning } from 'views/Drive/hooks/useDropdownPositioning'; +import { useVersionItemActions } from 'views/Drive/hooks/useVersionItemActions'; import { formatVersionDate, getDaysUntilExpiration } from '../utils'; import { FileVersion } from '@internxt/sdk/dist/drive/storage/types'; import { memo } from 'react'; diff --git a/src/views/Drive/components/VersionHistory/components/index.ts b/src/views/Drive/components/VersionHistory/components/index.ts index 9bc3ebe779..f63f5c3aee 100644 --- a/src/views/Drive/components/VersionHistory/components/index.ts +++ b/src/views/Drive/components/VersionHistory/components/index.ts @@ -4,3 +4,4 @@ export { VersionItem } from './VersionItem'; export { AutosaveSection } from './AutosaveSection'; export { VersionActionDialog } from './VersionActionDialog'; export { VersionHistorySkeleton } from './VersionHistorySkeleton'; +export { LockedFeatureModal } from './LockedFeatureModal'; diff --git a/src/views/Drive/components/VersionHistory/hooks/index.ts b/src/views/Drive/components/VersionHistory/hooks/index.ts deleted file mode 100644 index 5fb50562cd..0000000000 --- a/src/views/Drive/components/VersionHistory/hooks/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { useDropdownPositioning } from './useDropdownPositioning'; -export { useVersionItemActions } from './useVersionItemActions'; -export { useVersionHistoryMenuConfig } from './useVersionHistoryMenuConfig'; diff --git a/src/views/Drive/components/VersionHistory/hooks/useVersionHistoryMenuConfig.ts b/src/views/Drive/components/VersionHistory/hooks/useVersionHistoryMenuConfig.ts deleted file mode 100644 index 9b612a67d5..0000000000 --- a/src/views/Drive/components/VersionHistory/hooks/useVersionHistoryMenuConfig.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useSelector } from 'react-redux'; -import { useAppDispatch } from 'app/store/hooks'; -import { uiActions } from 'app/store/slices/ui'; -import workspacesSelectors from 'app/store/slices/workspaces/workspaces.selectors'; -import { fileVersionsSelectors } from 'app/store/slices/fileVersions'; -import navigationService from 'services/navigation.service'; -import { DriveItemData } from 'app/drive/types'; -import { isVersioningExtensionAllowed } from '../utils'; -import { VersionHistoryMenuConfig } from '../../DriveExplorer/components/DriveItemContextMenu'; - -export const useVersionHistoryMenuConfig = (selectedItem?: DriveItemData): VersionHistoryMenuConfig => { - const dispatch = useAppDispatch(); - const selectedWorkspace = useSelector(workspacesSelectors.getSelectedWorkspace); - const limits = useSelector(fileVersionsSelectors.getLimits); - const isVersioningEnabled = limits?.versioning?.enabled ?? false; - const isExtensionAllowed = selectedItem ? isVersioningExtensionAllowed(selectedItem) : true; - - return { - isLocked: !isVersioningEnabled, - isExtensionAllowed, - onUpgradeClick: () => { - dispatch(uiActions.setIsPreferencesDialogOpen(true)); - navigationService.openPreferencesDialog({ - section: 'account', - subsection: 'plans', - workspaceUuid: selectedWorkspace?.workspaceUser.workspaceId, - }); - }, - }; -}; diff --git a/src/views/Drive/hooks/index.ts b/src/views/Drive/hooks/index.ts index ae8b20a932..8a207b8786 100644 --- a/src/views/Drive/hooks/index.ts +++ b/src/views/Drive/hooks/index.ts @@ -3,4 +3,7 @@ export { useDriveItemDrag, useDriveItemDrop } from './useDriveItemDragAndDrop'; export { default as useDriveItemStoreProps } from './useDriveStoreProps'; export { usePaginationState } from './usePaginationState'; export { useTutorialState } from './useTutorialState'; +export { useVersionHistoryMenuConfig } from './useVersionHistoryMenuConfig'; +export { useVersionItemActions } from './useVersionItemActions'; +export { useDropdownPositioning } from './useDropdownPositioning'; export type { DriveItemActions } from './useDriveItemActions'; diff --git a/src/views/Drive/hooks/useDriveItemActions.tsx b/src/views/Drive/hooks/useDriveItemActions.tsx index 6460615b7e..30b5c1a432 100644 --- a/src/views/Drive/hooks/useDriveItemActions.tsx +++ b/src/views/Drive/hooks/useDriveItemActions.tsx @@ -11,7 +11,7 @@ import { storageActions } from 'app/store/slices/storage'; import { uiActions } from 'app/store/slices/ui'; import workspacesSelectors from 'app/store/slices/workspaces/workspaces.selectors'; import { DownloadManager } from 'app/network/DownloadManager'; -import { useVersionHistoryMenuConfig } from '../components/VersionHistory/hooks'; +import { useVersionHistoryMenuConfig } from './useVersionHistoryMenuConfig'; export interface DriveItemActions { nameInputRef: React.RefObject; @@ -104,10 +104,6 @@ const useDriveItemActions = (item): DriveItemActions => { }; const onViewVersionHistoryButtonClicked = () => { - if (versionHistoryConfig.locked) { - versionHistoryConfig.onLockedClick?.(); - return; - } dispatch(uiActions.setVersionHistoryItem(item as DriveItemData)); dispatch(uiActions.setIsVersionHistorySidebarOpen(true)); }; diff --git a/src/views/Drive/components/VersionHistory/hooks/useDropdownPositioning.test.ts b/src/views/Drive/hooks/useDropdownPositioning.test.ts similarity index 100% rename from src/views/Drive/components/VersionHistory/hooks/useDropdownPositioning.test.ts rename to src/views/Drive/hooks/useDropdownPositioning.test.ts diff --git a/src/views/Drive/components/VersionHistory/hooks/useDropdownPositioning.ts b/src/views/Drive/hooks/useDropdownPositioning.ts similarity index 100% rename from src/views/Drive/components/VersionHistory/hooks/useDropdownPositioning.ts rename to src/views/Drive/hooks/useDropdownPositioning.ts diff --git a/src/views/Drive/components/VersionHistory/hooks/useVersionHistoryMenuConfig.test.ts b/src/views/Drive/hooks/useVersionHistoryMenuConfig.test.ts similarity index 57% rename from src/views/Drive/components/VersionHistory/hooks/useVersionHistoryMenuConfig.test.ts rename to src/views/Drive/hooks/useVersionHistoryMenuConfig.test.ts index d45bb6e596..f8989ab120 100644 --- a/src/views/Drive/components/VersionHistory/hooks/useVersionHistoryMenuConfig.test.ts +++ b/src/views/Drive/hooks/useVersionHistoryMenuConfig.test.ts @@ -1,41 +1,13 @@ -import { renderHook, act } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi, Mock } from 'vitest'; import { useSelector } from 'react-redux'; -import { useAppDispatch } from 'app/store/hooks'; -import navigationService from 'services/navigation.service'; import { useVersionHistoryMenuConfig } from './useVersionHistoryMenuConfig'; import { FileLimitsResponse } from '@internxt/sdk/dist/drive/storage/types'; -import { WorkspaceData } from '@internxt/sdk/dist/workspaces'; vi.mock('react-redux', () => ({ useSelector: vi.fn(), })); -vi.mock('app/store/hooks', () => ({ - useAppDispatch: vi.fn(), -})); - -const mockSetIsPreferencesDialogOpen = vi.hoisted(() => - vi.fn((payload: boolean) => ({ type: 'setIsPreferencesDialogOpen', payload })), -); - -vi.mock('app/store/slices/ui', () => ({ - uiActions: { - setIsPreferencesDialogOpen: mockSetIsPreferencesDialogOpen, - }, -})); - -vi.mock('services/navigation.service', () => ({ - default: { - openPreferencesDialog: vi.fn(), - }, -})); - -const workspaceMock = { - workspaceUser: { workspaceId: 'workspace-1' }, - workspace: { id: 'workspace-1' }, -} as unknown as WorkspaceData; - const enabledLimits: FileLimitsResponse = { versioning: { enabled: true, maxFileSize: 0, retentionDays: 0, maxVersions: 0 }, }; @@ -46,38 +18,23 @@ const disabledLimits: FileLimitsResponse = { describe('Version history menu', () => { const mockUseSelector = useSelector as unknown as Mock; - const mockUseAppDispatch = useAppDispatch as unknown as Mock; - const dispatch = vi.fn(); const mockState = (limits: FileLimitsResponse | null) => ({ - workspaces: { selectedWorkspace: workspaceMock }, fileVersions: { limits }, }) as any; beforeEach(() => { vi.clearAllMocks(); - mockUseAppDispatch.mockReturnValue(dispatch); }); - it('when versioning is locked, then the upgrade flow opens', () => { + it('when versioning is locked, then the menu is marked as locked', () => { mockUseSelector.mockImplementation((selector: (state: any) => unknown) => selector(mockState(disabledLimits))); const { result } = renderHook(() => useVersionHistoryMenuConfig({ type: 'pdf' } as any)); expect(result.current.isLocked).toBe(true); expect(result.current.isExtensionAllowed).toBe(true); - - act(() => { - result.current.onUpgradeClick?.(); - }); - - expect(dispatch).toHaveBeenCalledWith({ type: 'setIsPreferencesDialogOpen', payload: true }); - expect(navigationService.openPreferencesDialog).toHaveBeenCalledWith({ - section: 'account', - subsection: 'plans', - workspaceUuid: 'workspace-1', - }); }); it('when versioning is enabled and the file is supported, then the menu is unlocked', () => { diff --git a/src/views/Drive/hooks/useVersionHistoryMenuConfig.ts b/src/views/Drive/hooks/useVersionHistoryMenuConfig.ts new file mode 100644 index 0000000000..215b94093d --- /dev/null +++ b/src/views/Drive/hooks/useVersionHistoryMenuConfig.ts @@ -0,0 +1,16 @@ +import { useSelector } from 'react-redux'; +import { fileVersionsSelectors } from 'app/store/slices/fileVersions'; +import { DriveItemData } from 'app/drive/types'; +import { isVersioningExtensionAllowed } from '../components/VersionHistory/utils'; +import { VersionHistoryMenuConfig } from '../components/DriveExplorer/components/DriveItemContextMenu'; + +export const useVersionHistoryMenuConfig = (selectedItem?: DriveItemData): VersionHistoryMenuConfig => { + const limits = useSelector(fileVersionsSelectors.getLimits); + const isVersioningEnabled = limits?.versioning?.enabled ?? false; + const isExtensionAllowed = selectedItem ? isVersioningExtensionAllowed(selectedItem) : true; + + return { + isLocked: !isVersioningEnabled, + isExtensionAllowed, + }; +}; diff --git a/src/views/Drive/components/VersionHistory/hooks/useVersionItemActions.test.ts b/src/views/Drive/hooks/useVersionItemActions.test.ts similarity index 98% rename from src/views/Drive/components/VersionHistory/hooks/useVersionItemActions.test.ts rename to src/views/Drive/hooks/useVersionItemActions.test.ts index f055c78c9d..5db7da8ab3 100644 --- a/src/views/Drive/components/VersionHistory/hooks/useVersionItemActions.test.ts +++ b/src/views/Drive/hooks/useVersionItemActions.test.ts @@ -20,7 +20,7 @@ vi.mock('app/store/hooks', () => ({ useAppSelector: vi.fn(), })); -vi.mock('views/Drive/components/VersionHistory/services/fileVersion.service', () => ({ +vi.mock('views/Drive/services/fileVersion.service', () => ({ default: { downloadVersion: vi.fn(), }, diff --git a/src/views/Drive/components/VersionHistory/hooks/useVersionItemActions.ts b/src/views/Drive/hooks/useVersionItemActions.ts similarity index 100% rename from src/views/Drive/components/VersionHistory/hooks/useVersionItemActions.ts rename to src/views/Drive/hooks/useVersionItemActions.ts diff --git a/src/views/Drive/components/VersionHistory/services/fileVersion.service.test.ts b/src/views/Drive/services/fileVersion.service.test.ts similarity index 100% rename from src/views/Drive/components/VersionHistory/services/fileVersion.service.test.ts rename to src/views/Drive/services/fileVersion.service.test.ts diff --git a/src/views/Drive/components/VersionHistory/services/fileVersion.service.ts b/src/views/Drive/services/fileVersion.service.ts similarity index 100% rename from src/views/Drive/components/VersionHistory/services/fileVersion.service.ts rename to src/views/Drive/services/fileVersion.service.ts diff --git a/src/views/NewSettings/components/Sections/Account/Plans/PlansSection.tsx b/src/views/NewSettings/components/Sections/Account/Plans/PlansSection.tsx index ff9f18d67b..355ae353ec 100644 --- a/src/views/NewSettings/components/Sections/Account/Plans/PlansSection.tsx +++ b/src/views/NewSettings/components/Sections/Account/Plans/PlansSection.tsx @@ -13,6 +13,7 @@ import { paymentService } from 'views/Checkout/services'; import { RootState } from 'app/store'; import { useAppDispatch } from 'app/store/hooks'; import { PlanState, planThunks } from 'app/store/slices/plan'; +import { fetchVersionLimitsThunk } from 'app/store/slices/fileVersions'; import CancelSubscriptionModal from '../../Workspace/Billing/CancelSubscriptionModal'; import { fetchPlanPrices, getStripe } from '../../../../services/plansApi'; import ChangePlanDialog from './components/ChangePlanDialog'; @@ -184,7 +185,10 @@ const PlansSection = ({ changeSection, onClosePreferences }: PlansSectionProps) const handlePaymentSuccess = () => { showSuccessSubscriptionNotification(); - setTimeout(() => dispatch(planThunks.initializeThunk()).unwrap(), 2000); + setTimeout(() => { + dispatch(planThunks.initializeThunk()).unwrap(); + dispatch(fetchVersionLimitsThunk()); + }, 2000); }; const handleSubscriptionPayment = async (priceId: string) => { @@ -262,6 +266,7 @@ const PlansSection = ({ changeSection, onClosePreferences }: PlansSectionProps) setCancellingSubscription(false); setTimeout(() => { dispatch(planThunks.initializeThunk()).unwrap(); + dispatch(fetchVersionLimitsThunk()); }, 1000); } } From 31eea3eba48745653b94256cb1653195f67b62fe Mon Sep 17 00:00:00 2001 From: Francis Terrero Date: Sat, 10 Jan 2026 00:01:43 -0400 Subject: [PATCH 02/14] feat: add locked feature localization for multiple languages --- src/app/i18n/locales/de.json | 8 +++++++- src/app/i18n/locales/es.json | 8 +++++++- src/app/i18n/locales/fr.json | 8 +++++++- src/app/i18n/locales/it.json | 8 +++++++- src/app/i18n/locales/ru.json | 8 +++++++- src/app/i18n/locales/tw.json | 8 +++++++- src/app/i18n/locales/zh.json | 8 +++++++- 7 files changed, 49 insertions(+), 7 deletions(-) diff --git a/src/app/i18n/locales/de.json b/src/app/i18n/locales/de.json index cb0f7c43d2..a23ffff15f 100644 --- a/src/app/i18n/locales/de.json +++ b/src/app/i18n/locales/de.json @@ -808,7 +808,13 @@ "restoreSuccess": "Version erfolgreich wiederhergestellt", "restoreError": "Fehler beim Wiederherstellen der Version", "deleteSuccess": "Version erfolgreich gelöscht", - "deleteError": "Fehler beim Löschen der Version" + "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": "Aktie \"{{name}}\"", diff --git a/src/app/i18n/locales/es.json b/src/app/i18n/locales/es.json index 18798f93ef..f73d92d1b1 100644 --- a/src/app/i18n/locales/es.json +++ b/src/app/i18n/locales/es.json @@ -887,7 +887,13 @@ "restoreSuccess": "Versión restaurada exitosamente", "restoreError": "Error al restaurar la versión", "deleteSuccess": "Versión eliminada exitosamente", - "deleteError": "Error al eliminar la versión" + "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}}\"", diff --git a/src/app/i18n/locales/fr.json b/src/app/i18n/locales/fr.json index 7fd403e323..e8ca3585e2 100644 --- a/src/app/i18n/locales/fr.json +++ b/src/app/i18n/locales/fr.json @@ -829,7 +829,13 @@ "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" + "deleteError": "Échec de la suppression de la version", + "lockedFeature": { + "title": "Fonctionnalité verrouillée", + "description": "Restaurez les versions précédentes de vos fichiers et suivez les modifications au fil du temps.", + "supportedFormats": "Disponible pour les fichiers PDF, Word, Excel et CSV dans les forfaits payants.", + "upgradeButton": "Mettre à niveau" + } }, "newFolderModal": { "title": "Nouveau dossier", diff --git a/src/app/i18n/locales/it.json b/src/app/i18n/locales/it.json index 916622c362..0a0d6f7929 100644 --- a/src/app/i18n/locales/it.json +++ b/src/app/i18n/locales/it.json @@ -941,7 +941,13 @@ "restoreSuccess": "Versione ripristinata con successo", "restoreError": "Impossibile ripristinare la versione", "deleteSuccess": "Versione eliminata con successo", - "deleteError": "Impossibile eliminare la versione" + "deleteError": "Impossibile eliminare la versione", + "lockedFeature": { + "title": "Funzione bloccata", + "description": "Ripristina le versioni precedenti dei tuoi file e monitora le modifiche nel tempo.", + "supportedFormats": "Disponibile per file PDF, Word, Excel e CSV nei piani a pagamento.", + "upgradeButton": "Aggiorna" + } }, "shareModal": { "title": "Condividi \"{{name}}\"", diff --git a/src/app/i18n/locales/ru.json b/src/app/i18n/locales/ru.json index e3d644be9d..4b719f2a87 100644 --- a/src/app/i18n/locales/ru.json +++ b/src/app/i18n/locales/ru.json @@ -848,7 +848,13 @@ "restoreSuccess": "Версия успешно восстановлена", "restoreError": "Не удалось восстановить версию", "deleteSuccess": "Версия успешно удалена", - "deleteError": "Не удалось удалить версию" + "deleteError": "Не удалось удалить версию", + "lockedFeature": { + "title": "Заблокированная функция", + "description": "Восстанавливайте предыдущие версии файлов и отслеживайте изменения со временем.", + "supportedFormats": "Доступно для файлов PDF, Word, Excel и CSV в платных тарифах.", + "upgradeButton": "Обновить" + } }, "shareModal": { "title": "Поделиться \"{{name}}\"", diff --git a/src/app/i18n/locales/tw.json b/src/app/i18n/locales/tw.json index 216d1e3050..4fbc275c3f 100644 --- a/src/app/i18n/locales/tw.json +++ b/src/app/i18n/locales/tw.json @@ -835,7 +835,13 @@ "restoreSuccess": "版本復原成功", "restoreError": "復原版本失敗", "deleteSuccess": "版本刪除成功", - "deleteError": "刪除版本失敗" + "deleteError": "刪除版本失敗", + "lockedFeature": { + "title": "功能已鎖定", + "description": "復原檔案的先前版本並追蹤隨時間的變化。", + "supportedFormats": "適用於付費方案中的PDF、Word、Excel和CSV檔案。", + "upgradeButton": "升級" + } }, "shareModal": { "title": "分享“{{name}}”", diff --git a/src/app/i18n/locales/zh.json b/src/app/i18n/locales/zh.json index 9c448049ba..8e2c0fe09a 100644 --- a/src/app/i18n/locales/zh.json +++ b/src/app/i18n/locales/zh.json @@ -871,7 +871,13 @@ "restoreSuccess": "版本恢复成功", "restoreError": "恢复版本失败", "deleteSuccess": "版本删除成功", - "deleteError": "删除版本失败" + "deleteError": "删除版本失败", + "lockedFeature": { + "title": "功能已锁定", + "description": "恢复文件的以前版本并跟踪随时间的变化。", + "supportedFormats": "适用于付费计划中的PDF、Word、Excel和CSV文件。", + "upgradeButton": "升级" + } }, "shareModal": { "title": "分享 \"{{name}}\"", From e6dfb81bcd8b93f3c1587dcc769c41750b38aafe Mon Sep 17 00:00:00 2001 From: Francis Terrero Date: Sat, 10 Jan 2026 14:24:18 -0400 Subject: [PATCH 03/14] feature: update LockedFeatureModal styles and improve accessibility --- .../components/LockedFeatureModal.tsx | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/views/Drive/components/VersionHistory/components/LockedFeatureModal.tsx b/src/views/Drive/components/VersionHistory/components/LockedFeatureModal.tsx index ab2ccb66ca..f7e118277c 100644 --- a/src/views/Drive/components/VersionHistory/components/LockedFeatureModal.tsx +++ b/src/views/Drive/components/VersionHistory/components/LockedFeatureModal.tsx @@ -16,26 +16,28 @@ const ICON_SIZES = { lock: 35, } as const; -const COLORS = { - border: '#474747', - lock: '#737373', -} as const; +const LIGHT_MODE_ICON_BG = '#F9F9FC'; export const LockedFeatureModal = ({ onUpgrade }: LockedFeatureModalProps) => { const { translate } = useTranslationContext(); return ( -
+
-
+
- +
@@ -43,10 +45,10 @@ export const LockedFeatureModal = ({ onUpgrade }: LockedFeatureModalProps) => {

{translate('modals.versionHistory.lockedFeature.title')}

-

+

{translate('modals.versionHistory.lockedFeature.description')}

-

+

{translate('modals.versionHistory.lockedFeature.supportedFormats')}

From 9674047cd81af87fb316c13c1a6199e128152905 Mon Sep 17 00:00:00 2001 From: Francis Terrero Date: Sat, 10 Jan 2026 14:55:30 -0400 Subject: [PATCH 04/14] feat: add timestamped filename format for version downloads - Add downloadName option to DownloadItem type to support custom filenames - Update downloadWorkerHandler to accept and use custom download names - Format version download filenames as "(DD-MM-YYYY at HH:mm) filename.ext" - Use dateService and itemsUtils from @internxt/lib for proper formatting - Pass custom filename through downloadOptions instead of modifying item name --- .../services/downloadManager.service.test.ts | 1 + .../drive/services/downloadManager.service.ts | 22 +++++++++++++------ .../worker.service/downloadWorkerHandler.ts | 4 +++- .../Drive/hooks/useVersionItemActions.test.ts | 3 ++- .../Drive/hooks/useVersionItemActions.ts | 12 ++++++++-- .../services/fileVersion.service.test.ts | 4 +++- .../Drive/services/fileVersion.service.ts | 4 +++- 7 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/app/drive/services/downloadManager.service.test.ts b/src/app/drive/services/downloadManager.service.test.ts index 6124f7bc9f..3da1b2a87f 100644 --- a/src/app/drive/services/downloadManager.service.test.ts +++ b/src/app/drive/services/downloadManager.service.test.ts @@ -879,6 +879,7 @@ describe('downloadManagerService', () => { updateProgressCallback: mockUpdateProgress, abortController: mockTask.abortController, sharingOptions: mockTask.credentials, + downloadName: mockTask.options.downloadName, }); }); }); diff --git a/src/app/drive/services/downloadManager.service.ts b/src/app/drive/services/downloadManager.service.ts index ce511cddb6..d299097c72 100644 --- a/src/app/drive/services/downloadManager.service.ts +++ b/src/app/drive/services/downloadManager.service.ts @@ -45,6 +45,7 @@ export type DownloadItem = { downloadOptions?: { areSharedItems?: boolean; showErrors?: boolean; + downloadName?: string; }; createFoldersIterator?: FolderIterator | SharedFolderIterator; createFilesIterator?: FileIterator | SharedFileIterator; @@ -117,13 +118,17 @@ 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 = `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; + } } } @@ -288,6 +293,7 @@ export class DownloadManagerService { updateProgressCallback, abortController, sharingOptions: credentials, + downloadName: options.downloadName, }); console.timeEnd(`download-file-${file.uuid}`); @@ -493,6 +499,7 @@ export class DownloadManagerService { updateProgressCallback: (progress: number) => void; abortController?: AbortController; sharingOptions: { credentials: { user: string; pass: string }; mnemonic: string }; + downloadName?: string; }) => { const isBrave = !!(navigator.brave && (await navigator.brave.isBrave())); @@ -514,6 +521,7 @@ export class DownloadManagerService { itemData: payload.file, updateProgressCallback: payload.updateProgressCallback, abortController: payload.abortController, + downloadName: payload.downloadName, }); }; } diff --git a/src/app/drive/services/worker.service/downloadWorkerHandler.ts b/src/app/drive/services/worker.service/downloadWorkerHandler.ts index 4db85a9619..300a3cde02 100644 --- a/src/app/drive/services/worker.service/downloadWorkerHandler.ts +++ b/src/app/drive/services/worker.service/downloadWorkerHandler.ts @@ -11,6 +11,7 @@ interface HandleWorkerMessagesPayload { abortController?: AbortController; itemData: DriveFileData; updateProgressCallback: (progress: number) => void; + downloadName?: string; } interface HandleMessagesPayload { @@ -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; diff --git a/src/views/Drive/hooks/useVersionItemActions.test.ts b/src/views/Drive/hooks/useVersionItemActions.test.ts index 5db7da8ab3..a943f0922d 100644 --- a/src/views/Drive/hooks/useVersionItemActions.test.ts +++ b/src/views/Drive/hooks/useVersionItemActions.test.ts @@ -60,6 +60,7 @@ describe('Version item menu', () => { fileId: 'file-uuid', networkFileId: 'network-file-id', size: '5', + createdAt: '2026-01-10T14:30:00.000Z', } as FileVersion; const fileItem = { id: 'file-id', @@ -142,7 +143,7 @@ describe('Version item menu', () => { expect(downloadVersionSpy).toHaveBeenCalledWith( version, fileItem, - fileItem.plainName, + '(10-01-2026 at 10:30) pretty-name', selectedWorkspace, workspaceCredentials, ); diff --git a/src/views/Drive/hooks/useVersionItemActions.ts b/src/views/Drive/hooks/useVersionItemActions.ts index 0400bdd476..4e1af1e039 100644 --- a/src/views/Drive/hooks/useVersionItemActions.ts +++ b/src/views/Drive/hooks/useVersionItemActions.ts @@ -8,6 +8,8 @@ import { uiActions } from 'app/store/slices/ui'; import { RootState } from 'app/store'; import workspacesSelectors from 'app/store/slices/workspaces/workspaces.selectors'; import { FileVersion } from '@internxt/sdk/dist/drive/storage/types'; +import dateService from 'services/date.service'; +import { items as itemsUtils } from '@internxt/lib'; interface UseVersionItemActionsParams { version: FileVersion; @@ -38,8 +40,14 @@ export const useVersionItemActions = ({ version, onDropdownClose }: UseVersionIt return; } - const fileName = item.plainName || item.name; - await fileVersionService.downloadVersion(version, item, fileName, selectedWorkspace, workspaceCredentials); + const entireFilename = item.plainName || item.name; + const formattedDate = dateService.format(version.createdAt, 'DD-MM-YYYY [at] HH:mm'); + const { filename, extension } = itemsUtils.getFilenameAndExt(entireFilename); + + const fileExtension = extension ? `.${extension}` : ''; + const versionFileName = `(${formattedDate}) ${filename}${fileExtension}`; + + await fileVersionService.downloadVersion(version, item, versionFileName, selectedWorkspace, workspaceCredentials); }; const handleDeleteClick = () => { diff --git a/src/views/Drive/services/fileVersion.service.test.ts b/src/views/Drive/services/fileVersion.service.test.ts index 8740e9b140..53cf50a133 100644 --- a/src/views/Drive/services/fileVersion.service.test.ts +++ b/src/views/Drive/services/fileVersion.service.test.ts @@ -102,11 +102,13 @@ describe('File version actions', () => { ...fileItem, fileId: version.networkFileId, size: Number(version.size), - name: 'custom-name', }, ], selectedWorkspace, workspaceCredentials, + downloadOptions: { + downloadName: 'custom-name', + }, }); }); }); diff --git a/src/views/Drive/services/fileVersion.service.ts b/src/views/Drive/services/fileVersion.service.ts index a5d085091f..6a82e43d43 100644 --- a/src/views/Drive/services/fileVersion.service.ts +++ b/src/views/Drive/services/fileVersion.service.ts @@ -33,13 +33,15 @@ export async function downloadVersion( ...fileItem, fileId: version.networkFileId, size: Number(version.size), - name: fileName, }; await DownloadManager.downloadItem({ payload: [versionFileData], selectedWorkspace, workspaceCredentials, + downloadOptions: { + downloadName: fileName, + }, }); } From 67767789b2b4edaf56d501476418251f013b0728 Mon Sep 17 00:00:00 2001 From: Francis Terrero Date: Mon, 12 Jan 2026 09:49:30 -0400 Subject: [PATCH 05/14] test: update date format in version item menu tests --- src/views/Drive/hooks/useVersionItemActions.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/views/Drive/hooks/useVersionItemActions.test.ts b/src/views/Drive/hooks/useVersionItemActions.test.ts index a943f0922d..0739f87ddc 100644 --- a/src/views/Drive/hooks/useVersionItemActions.test.ts +++ b/src/views/Drive/hooks/useVersionItemActions.test.ts @@ -35,6 +35,12 @@ vi.mock('app/notifications/services/notifications.service', () => ({ }, })); +vi.mock('services/date.service', () => ({ + default: { + format: vi.fn(() => '10-01-2026 at 14:30'), + }, +})); + const mockSetVersionToRestore = vi.hoisted(() => vi.fn((payload) => ({ type: 'setVersionToRestore', payload }))); const mockSetIsRestoreVersionDialogOpen = vi.hoisted(() => vi.fn((payload) => ({ type: 'setIsRestoreVersionDialogOpen', payload })), @@ -143,7 +149,7 @@ describe('Version item menu', () => { expect(downloadVersionSpy).toHaveBeenCalledWith( version, fileItem, - '(10-01-2026 at 10:30) pretty-name', + '(10-01-2026 at 14:30) pretty-name', selectedWorkspace, workspaceCredentials, ); From 96cb9ff69c36eac4bd93417526442ba3f00056ea Mon Sep 17 00:00:00 2001 From: Francis Terrero Date: Mon, 12 Jan 2026 10:26:12 -0400 Subject: [PATCH 06/14] feature: simplify download name generation for single items --- .../drive/services/downloadManager.service.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/app/drive/services/downloadManager.service.ts b/src/app/drive/services/downloadManager.service.ts index d299097c72..7c8af898ce 100644 --- a/src/app/drive/services/downloadManager.service.ts +++ b/src/app/drive/services/downloadManager.service.ts @@ -91,6 +91,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, @@ -121,15 +128,8 @@ export class DownloadManagerService { let downloadName = downloadItem.downloadOptions?.downloadName; if (!downloadName) { - 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; - } - } + downloadName = + itemsPayload.length === 1 ? this.getSingleItemName(itemsPayload[0]) : `Internxt (${formattedDate})`; } let taskId = downloadItem.taskId; From 5e1016703522034917cbf66d453a5443780fcd65 Mon Sep 17 00:00:00 2001 From: Francis Terrero Date: Mon, 12 Jan 2026 11:22:33 -0400 Subject: [PATCH 07/14] refactor: simplify handleUpgrade function in Sidebar component --- src/views/Drive/components/VersionHistory/Sidebar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/Drive/components/VersionHistory/Sidebar.tsx b/src/views/Drive/components/VersionHistory/Sidebar.tsx index 20604f2173..2c5554db2b 100644 --- a/src/views/Drive/components/VersionHistory/Sidebar.tsx +++ b/src/views/Drive/components/VersionHistory/Sidebar.tsx @@ -223,14 +223,14 @@ const Sidebar = () => { dispatch(uiActions.setIsDeleteVersionDialogOpen(true)); }, [item, selectedCount, dispatch]); - const handleUpgrade = useCallback(() => { + const handleUpgrade = () => { dispatch(uiActions.setIsPreferencesDialogOpen(true)); navigationService.openPreferencesDialog({ section: 'account', subsection: 'plans', workspaceUuid: selectedWorkspace?.workspaceUser.workspaceId, }); - }, [dispatch, selectedWorkspace]); + }; if (!item) return null; return ( From 275f8eb030b65368b9836c968f8e1d48e0057d62 Mon Sep 17 00:00:00 2001 From: Francis Terrero Date: Mon, 12 Jan 2026 23:49:25 -0400 Subject: [PATCH 08/14] refactor: streamline LockedFeatureModal layout and remove unused styles --- .../components/LockedFeatureModal.tsx | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/src/views/Drive/components/VersionHistory/components/LockedFeatureModal.tsx b/src/views/Drive/components/VersionHistory/components/LockedFeatureModal.tsx index f7e118277c..09e18fd678 100644 --- a/src/views/Drive/components/VersionHistory/components/LockedFeatureModal.tsx +++ b/src/views/Drive/components/VersionHistory/components/LockedFeatureModal.tsx @@ -6,34 +6,19 @@ interface LockedFeatureModalProps { onUpgrade: () => void; } -const MODAL_DIMENSIONS = { - width: '282px', - height: '333px', -} as const; - const ICON_SIZES = { clock: 64, lock: 35, } as const; -const LIGHT_MODE_ICON_BG = '#F9F9FC'; - export const LockedFeatureModal = ({ onUpgrade }: LockedFeatureModalProps) => { const { translate } = useTranslationContext(); return (
-
+
-
+
From c9847ad94da156db6bea6fddc19430330129cd5d Mon Sep 17 00:00:00 2001 From: Francis Terrero Date: Tue, 13 Jan 2026 10:16:16 -0400 Subject: [PATCH 09/14] style: enhance hover effect for unselected version item in dark mode --- .../components/VersionHistory/components/AutosaveSection.tsx | 2 +- .../components/VersionHistory/components/CurrentVersionItem.tsx | 2 +- .../Drive/components/VersionHistory/components/VersionItem.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/views/Drive/components/VersionHistory/components/AutosaveSection.tsx b/src/views/Drive/components/VersionHistory/components/AutosaveSection.tsx index 321a936375..4d404ae975 100644 --- a/src/views/Drive/components/VersionHistory/components/AutosaveSection.tsx +++ b/src/views/Drive/components/VersionHistory/components/AutosaveSection.tsx @@ -24,7 +24,7 @@ export const AutosaveSection = ({ const isIndeterminate = hasSelection && !selectAllAutosave; return ( -
+
+
{formatVersionDate(createdAt)} diff --git a/src/views/Drive/components/VersionHistory/components/VersionItem.tsx b/src/views/Drive/components/VersionHistory/components/VersionItem.tsx index 728976d792..f4a53c0ee6 100644 --- a/src/views/Drive/components/VersionHistory/components/VersionItem.tsx +++ b/src/views/Drive/components/VersionHistory/components/VersionItem.tsx @@ -53,7 +53,7 @@ export const VersionItem = memo( type="button" aria-pressed={isSelected} aria-label={`Version from ${formatVersionDate(version.createdAt)}`} - className={`group relative w-full px-6 cursor-pointer text-left ${isSelected ? 'bg-primary/10' : 'hover:bg-gray-1'}`} + className={`group relative w-full px-6 cursor-pointer text-left ${isSelected ? 'bg-primary/10' : 'hover:bg-gray-1 dark:hover:bg-white/3'}`} onClick={handleItemClick} >
From da27eba9e1d2ad04c067e340e854654e9467508f Mon Sep 17 00:00:00 2001 From: Francis Terrero Date: Tue, 13 Jan 2026 17:05:46 -0400 Subject: [PATCH 10/14] style: adjust padding and text styles in LockedFeatureModal for improved layout --- .../VersionHistory/components/LockedFeatureModal.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/views/Drive/components/VersionHistory/components/LockedFeatureModal.tsx b/src/views/Drive/components/VersionHistory/components/LockedFeatureModal.tsx index 09e18fd678..4150619b44 100644 --- a/src/views/Drive/components/VersionHistory/components/LockedFeatureModal.tsx +++ b/src/views/Drive/components/VersionHistory/components/LockedFeatureModal.tsx @@ -16,7 +16,7 @@ export const LockedFeatureModal = ({ onUpgrade }: LockedFeatureModalProps) => { return (
-
+
@@ -26,14 +26,14 @@ export const LockedFeatureModal = ({ onUpgrade }: LockedFeatureModalProps) => {
-
-

+
+

{translate('modals.versionHistory.lockedFeature.title')}

-

+

{translate('modals.versionHistory.lockedFeature.description')}

-

+

{translate('modals.versionHistory.lockedFeature.supportedFormats')}

From 16c839449d343d4c2b72250b26f375a7402a4b33 Mon Sep 17 00:00:00 2001 From: Francis Terrero Date: Thu, 15 Jan 2026 10:37:09 -0400 Subject: [PATCH 11/14] fix: improve sidebar visibility logic by consolidating open state checks --- src/views/Drive/components/VersionHistory/Sidebar.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/views/Drive/components/VersionHistory/Sidebar.tsx b/src/views/Drive/components/VersionHistory/Sidebar.tsx index 2c5554db2b..4932ca4073 100644 --- a/src/views/Drive/components/VersionHistory/Sidebar.tsx +++ b/src/views/Drive/components/VersionHistory/Sidebar.tsx @@ -232,13 +232,14 @@ const Sidebar = () => { }); }; - if (!item) return null; + const shouldShowSidebar = isOpen && item; + return ( <> - {isOpen &&
} + {shouldShowSidebar &&
}
From 4315ad25583591d69a78f2cb1ab11f01bb7d1e54 Mon Sep 17 00:00:00 2001 From: Francis Terrero Date: Tue, 20 Jan 2026 11:12:24 -0400 Subject: [PATCH 12/14] feat: add loading state for file limits in Sidebar component --- src/app/store/slices/fileVersions/fileVersions.selectors.ts | 3 +++ src/views/Drive/components/VersionHistory/Sidebar.tsx | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/app/store/slices/fileVersions/fileVersions.selectors.ts b/src/app/store/slices/fileVersions/fileVersions.selectors.ts index 03c545460f..55b0d24b2e 100644 --- a/src/app/store/slices/fileVersions/fileVersions.selectors.ts +++ b/src/app/store/slices/fileVersions/fileVersions.selectors.ts @@ -5,6 +5,9 @@ const fileVersionsSelectors = { getLimits(state: RootState): FileLimitsResponse | null { return state.fileVersions.limits; }, + isLimitsLoading(state: RootState): boolean { + return state.fileVersions.isLimitsLoading; + }, getVersionsByFileId(state: RootState, fileId: NonNullable): FileVersion[] { return state.fileVersions.versionsByFileId[fileId] ?? []; }, diff --git a/src/views/Drive/components/VersionHistory/Sidebar.tsx b/src/views/Drive/components/VersionHistory/Sidebar.tsx index 4932ca4073..ff0f501379 100644 --- a/src/views/Drive/components/VersionHistory/Sidebar.tsx +++ b/src/views/Drive/components/VersionHistory/Sidebar.tsx @@ -42,6 +42,7 @@ const Sidebar = () => { const { translate } = useTranslationContext(); const limits = useAppSelector(fileVersionsSelectors.getLimits); + const isLimitsLoading = useAppSelector(fileVersionsSelectors.isLimitsLoading); const versions = useAppSelector((state: RootState) => item ? fileVersionsSelectors.getVersionsByFileId(state, item.uuid) : [], ); @@ -56,6 +57,7 @@ const Sidebar = () => { }); const isVersioningEnabled = limits?.versioning?.enabled ?? false; + const isLoadingContent = (isVersioningEnabled && isLoading) || isLimitsLoading; const blurredBackgroundVersions = useMemo(() => { if (isVersioningEnabled) return []; @@ -247,7 +249,7 @@ const Sidebar = () => {
- {isVersioningEnabled && isLoading ? ( + {isLoadingContent ? ( ) : ( <> @@ -279,7 +281,7 @@ const Sidebar = () => { ))} )} - {!isVersioningEnabled && } + {!isVersioningEnabled && !isLimitsLoading && }
From cf7170deba5d7d403225a5856f0a4e49309758a3 Mon Sep 17 00:00:00 2001 From: Francis Terrero Date: Sat, 31 Jan 2026 22:39:53 -0400 Subject: [PATCH 13/14] feat: implement smart polling for version limits after plan changes Add intelligent exponential backoff polling that stops early when version limits are updated after subscription changes. Version limits are updated via webhook, so polling with change detection ensures the frontend receives updated state efficiently. - Add VERSION_LIMITS_POLL_MAX_ATTEMPTS (3) and VERSION_LIMITS_POLL_DELAYS (2s, 4s, 6s) - Add isSilent parameter to fetchVersionLimitsThunk to suppress loading skeleton during background polling - Implement pollVersionLimitsUntilChanged with limit comparison to detect when webhook has processed - Compare all versioning properties (enabled, maxFileSize, retentionDays, maxVersions) to detect changes - Stop polling early if limits change, continue until max attempts if unchanged - Apply polling to both payment success and subscription cancellation flows - Update tests to handle new payload structure { limits, isSilent } --- .../store/slices/fileVersions/index.test.ts | 14 ++++---- src/app/store/slices/fileVersions/index.ts | 31 ++++++++++------ .../Sections/Account/Plans/PlansSection.tsx | 35 +++++++++++++++++-- 3 files changed, 59 insertions(+), 21 deletions(-) diff --git a/src/app/store/slices/fileVersions/index.test.ts b/src/app/store/slices/fileVersions/index.test.ts index ce4f7dc2a7..c935495416 100644 --- a/src/app/store/slices/fileVersions/index.test.ts +++ b/src/app/store/slices/fileVersions/index.test.ts @@ -54,11 +54,11 @@ describe('File history state', () => { const getLimitsSpy = vi.spyOn(fileVersionService, 'getLimits').mockResolvedValueOnce(limits); const dispatch = vi.fn(); - const action = await fetchVersionLimitsThunk()(dispatch, () => ({}) as RootState, undefined); + const action = await fetchVersionLimitsThunk({})(dispatch, () => ({}) as RootState, undefined); expect(getLimitsSpy).toHaveBeenCalled(); expect(action.meta.requestStatus).toBe('fulfilled'); - expect(action.payload).toBe(limits); + expect(action.payload).toEqual({ limits, isSilent: false }); }); it('when version limits fail to load, then the error is reported', async () => { @@ -67,7 +67,7 @@ describe('File history state', () => { .mockRejectedValueOnce(new Error('limits unavailable')); const dispatch = vi.fn(); - const action = await fetchVersionLimitsThunk()(dispatch, () => ({}) as RootState, undefined); + const action = await fetchVersionLimitsThunk({})(dispatch, () => ({}) as RootState, undefined); expect(getLimitsSpy).toHaveBeenCalled(); expect(action.meta.requestStatus).toBe('rejected'); @@ -138,7 +138,7 @@ describe('File history state', () => { }); it('when limits are loading or finished, then the loading state updates', () => { - const pendingState = fileVersionsReducer(undefined, fetchVersionLimitsThunk.pending('', undefined)); + const pendingState = fileVersionsReducer(undefined, fetchVersionLimitsThunk.pending('', {})); expect(pendingState.isLimitsLoading).toBe(true); const limits: FileLimitsResponse = { @@ -146,14 +146,14 @@ describe('File history state', () => { }; const fulfilledState = fileVersionsReducer( pendingState, - fetchVersionLimitsThunk.fulfilled(limits, '', undefined), + fetchVersionLimitsThunk.fulfilled({ limits, isSilent: false }, '', {}), ); expect(fulfilledState.isLimitsLoading).toBe(false); - expect(fulfilledState.limits).toBe(limits); + expect(fulfilledState.limits).toEqual(limits); const rejectedState = fileVersionsReducer( pendingState, - fetchVersionLimitsThunk.rejected(new Error('err'), '', undefined), + fetchVersionLimitsThunk.rejected(new Error('err'), '', {}), ); expect(rejectedState.isLimitsLoading).toBe(false); }); diff --git a/src/app/store/slices/fileVersions/index.ts b/src/app/store/slices/fileVersions/index.ts index 821015f2d0..cee9190397 100644 --- a/src/app/store/slices/fileVersions/index.ts +++ b/src/app/store/slices/fileVersions/index.ts @@ -2,6 +2,9 @@ import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit'; import { FileVersion, FileLimitsResponse } from '@internxt/sdk/dist/drive/storage/types'; import fileVersionService from 'views/Drive/services/fileVersion.service'; +export const VERSION_LIMITS_POLL_MAX_ATTEMPTS = 3; +export const VERSION_LIMITS_POLL_DELAYS = [2000, 4000, 6000]; + interface FileVersionsState { versionsByFileId: Record, FileVersion[]>; isLoadingByFileId: Record, boolean>; @@ -30,14 +33,17 @@ export const fetchFileVersionsThunk = createAsyncThunk( }, ); -export const fetchVersionLimitsThunk = createAsyncThunk('fileVersions/fetchLimits', async (_, { rejectWithValue }) => { - try { - const limits = await fileVersionService.getLimits(); - return limits; - } catch (error) { - return rejectWithValue((error as Error).message); - } -}); +export const fetchVersionLimitsThunk = createAsyncThunk( + 'fileVersions/fetchLimits', + async ({ isSilent = false }: { isSilent?: boolean } = {}, { rejectWithValue }) => { + try { + const limits = await fileVersionService.getLimits(); + return { limits, isSilent }; + } catch (error) { + return rejectWithValue((error as Error).message); + } + }, +); export const fileVersionsSlice = createSlice({ name: 'fileVersions', @@ -75,11 +81,14 @@ export const fileVersionsSlice = createSlice({ state.isLoadingByFileId[action.meta.arg] = false; state.errorsByFileId[action.meta.arg] = action.payload as string; }) - .addCase(fetchVersionLimitsThunk.pending, (state) => { - state.isLimitsLoading = true; + .addCase(fetchVersionLimitsThunk.pending, (state, action) => { + const isSilent = action.meta.arg?.isSilent || false; + if (!isSilent) { + state.isLimitsLoading = true; + } }) .addCase(fetchVersionLimitsThunk.fulfilled, (state, action) => { - state.limits = action.payload; + state.limits = action.payload.limits; state.isLimitsLoading = false; }) .addCase(fetchVersionLimitsThunk.rejected, (state) => { diff --git a/src/views/NewSettings/components/Sections/Account/Plans/PlansSection.tsx b/src/views/NewSettings/components/Sections/Account/Plans/PlansSection.tsx index 355ae353ec..61c43eb7cd 100644 --- a/src/views/NewSettings/components/Sections/Account/Plans/PlansSection.tsx +++ b/src/views/NewSettings/components/Sections/Account/Plans/PlansSection.tsx @@ -13,7 +13,12 @@ import { paymentService } from 'views/Checkout/services'; import { RootState } from 'app/store'; import { useAppDispatch } from 'app/store/hooks'; import { PlanState, planThunks } from 'app/store/slices/plan'; -import { fetchVersionLimitsThunk } from 'app/store/slices/fileVersions'; +import { + fetchVersionLimitsThunk, + VERSION_LIMITS_POLL_MAX_ATTEMPTS, + VERSION_LIMITS_POLL_DELAYS, + fileVersionsSelectors, +} from 'app/store/slices/fileVersions'; import CancelSubscriptionModal from '../../Workspace/Billing/CancelSubscriptionModal'; import { fetchPlanPrices, getStripe } from '../../../../services/plansApi'; import ChangePlanDialog from './components/ChangePlanDialog'; @@ -47,6 +52,7 @@ const PlansSection = ({ changeSection, onClosePreferences }: PlansSectionProps) const selectedWorkspace = useSelector((state: RootState) => state.workspaces.selectedWorkspace); const plan = useSelector((state) => state.plan); const user = useSelector((state) => state.user.user); + const versionLimits = useSelector(fileVersionsSelectors.getLimits); const { individualSubscription, businessSubscription } = plan; let stripe; @@ -183,12 +189,34 @@ const PlansSection = ({ changeSection, onClosePreferences }: PlansSectionProps) [translate], ); + const pollVersionLimitsUntilChanged = useCallback( + async (attempt = 0, previousLimits = versionLimits) => { + if (attempt >= VERSION_LIMITS_POLL_MAX_ATTEMPTS) return; + + const delay = VERSION_LIMITS_POLL_DELAYS[attempt] || VERSION_LIMITS_POLL_DELAYS.at(-1); + await new Promise((resolve) => setTimeout(resolve, delay)); + const result = await dispatch(fetchVersionLimitsThunk({ isSilent: true })).unwrap(); + + const hasLimitsChanged = + previousLimits?.versioning.enabled !== result.limits.versioning.enabled || + previousLimits?.versioning.maxFileSize !== result.limits.versioning.maxFileSize || + previousLimits?.versioning.retentionDays !== result.limits.versioning.retentionDays || + previousLimits?.versioning.maxVersions !== result.limits.versioning.maxVersions; + + if (!hasLimitsChanged) { + pollVersionLimitsUntilChanged(attempt + 1, previousLimits); + } + }, + [dispatch, versionLimits], + ); + const handlePaymentSuccess = () => { showSuccessSubscriptionNotification(); setTimeout(() => { dispatch(planThunks.initializeThunk()).unwrap(); - dispatch(fetchVersionLimitsThunk()); }, 2000); + + pollVersionLimitsUntilChanged(); }; const handleSubscriptionPayment = async (priceId: string) => { @@ -266,8 +294,9 @@ const PlansSection = ({ changeSection, onClosePreferences }: PlansSectionProps) setCancellingSubscription(false); setTimeout(() => { dispatch(planThunks.initializeThunk()).unwrap(); - dispatch(fetchVersionLimitsThunk()); }, 1000); + + pollVersionLimitsUntilChanged(); } } From 4b664c8f911ed44a9d00ff78b393e1d403bab396 Mon Sep 17 00:00:00 2001 From: Francis Terrero Date: Wed, 4 Feb 2026 11:42:49 -0400 Subject: [PATCH 14/14] fix: handle file extension correctly in version download filename --- src/views/Drive/hooks/useVersionItemActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/Drive/hooks/useVersionItemActions.ts b/src/views/Drive/hooks/useVersionItemActions.ts index 4e1af1e039..6d8e0b1d1f 100644 --- a/src/views/Drive/hooks/useVersionItemActions.ts +++ b/src/views/Drive/hooks/useVersionItemActions.ts @@ -44,7 +44,7 @@ export const useVersionItemActions = ({ version, onDropdownClose }: UseVersionIt const formattedDate = dateService.format(version.createdAt, 'DD-MM-YYYY [at] HH:mm'); const { filename, extension } = itemsUtils.getFilenameAndExt(entireFilename); - const fileExtension = extension ? `.${extension}` : ''; + const fileExtension = extension || item.type ? `.${extension || item.type}` : ''; const versionFileName = `(${formattedDate}) ${filename}${fileExtension}`; await fileVersionService.downloadVersion(version, item, versionFileName, selectedWorkspace, workspaceCredentials);