Skip to content

Conversation

@terrerox
Copy link
Contributor

@terrerox terrerox commented Dec 15, 2025

Description

  • Implement file version history sidebar with restore, delete, download, and batch operations
  • Add Redux-based caching for file versions and limits to prevent redundant API calls
  • Implement cache invalidation on version create, delete, and restore operations
  • Add smart file replacement: creates versions when enabled, falls back to trash+upload otherwise
  • Integrate premium feature lock UI with upgrade prompts in context menus
  • Add extension filtering for version history (pdf, docx, xlsx, csv)
  • Replace React Context API with Redux for consistent state management
  • Optimize performance with React.memo and proper dependency management
  • Update SDK to 1.11.22 with enhanced version management support
  • Add internationalization support for version management (7 languages)
  • Display version metadata including timestamps, sizes, and expiration dates
  • Add date utilities for calculating days until version expiration
  • Fix version timestamp display to use updatedAt instead of createdAt

Related Issues

Related Pull Requests

Checklist

  • Changes have been tested locally.
  • Unit tests have been written or updated as necessary.
  • The code adheres to the repository's coding standards.
  • Relevant documentation has been added or updated.
  • No new warnings or errors have been introduced.
  • SonarCloud issues have been reviewed and addressed.
  • QA Passed

Testing Process

Additional Notes

@terrerox terrerox self-assigned this Dec 15, 2025
@terrerox terrerox marked this pull request as draft December 15, 2025 22:26
…tory menu with lock state

  Add VersioningLimitsProvider to DriveView and implement version history menu configuration with locked
  state support. The version history menu now shows a lock icon and handles locked state when versioning
  is not available or extension is not allowed.
@terrerox terrerox force-pushed the feature/file-version-history-v4 branch from 7ae0637 to 9d6956e Compare December 16, 2025 22:09
@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Dec 16, 2025

Deploying drive-web with  Cloudflare Pages  Cloudflare Pages

Latest commit: ea961f7
Status: ✅  Deploy successful!
Preview URL: https://088b5735.drive-web.pages.dev
Branch Preview URL: https://feature-file-version-history-fgbg.drive-web.pages.dev

View logs

… context API

  Replace VersioningLimitsContext with Redux store for consistent state management and improved
  performance. Add fileVersions slice to cache API responses and prevent redundant fetches.

  - Create fileVersions Redux slice with caching for versions and limits
  - Add thunks for fetching file versions and versioning limits
  - Implement cache invalidation on version create, delete, and restore
  - Optimize Sidebar to use cached data and avoid infinite loops
  - Memoize VersionItem component to prevent unnecessary re-renders
  - Remove VersioningLimitsContext and consolidate all state in Redux
  - Update all components to use Redux selectors instead of context
@terrerox terrerox marked this pull request as ready for review December 16, 2025 23:28
@terrerox
Copy link
Contributor Author

Quality Gate Failed Quality Gate failed

Failed conditions 16.9% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

Tests will be added in a separate PR.

() => moveDestinationFolderId ?? currentFolderId,
[moveDestinationFolderId, currentFolderId],
);
const limits = useAppSelector((state: RootState) => state.fileVersions.limits);
Copy link
Collaborator

Choose a reason for hiding this comment

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

create selector for get fields of a redux state, I see that this state is accessed from diferent files


interface FileVersionsState {
versionsByFileId: Record<string, FileVersion[]>;
loadingStates: Record<string, boolean>;
Copy link
Collaborator

Choose a reason for hiding this comment

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

This could lead to confusion. loadingState of what? maybe something as isLoadingFileById left clear what does, it is more verbose but more clear the purpose, and add to Record type the FileVersion['id'] field instead of string to make it more clearer.
Could do the same with versionsByFileId

versionsByFileId: Record<string, FileVersion[]>;
loadingStates: Record<string, boolean>;
errors: Record<string, string | null>;
limits: GetFileLimitsResponse | null;
Copy link
Collaborator

Choose a reason for hiding this comment

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

GetFileLimitsResponse seems a function name, not a type nam.
The type should be something like FileLimitsResponse

interface FileVersionsState {
versionsByFileId: Record<string, FileVersion[]>;
loadingStates: Record<string, boolean>;
errors: Record<string, string | null>;
Copy link
Collaborator

Choose a reason for hiding this comment

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

same here with string and file id, and the name of the variable

loadingStates: Record<string, boolean>;
errors: Record<string, string | null>;
limits: GetFileLimitsResponse | null;
limitsLoading: boolean;
Copy link
Collaborator

Choose a reason for hiding this comment

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

for boleean use the standard convention, isLimitsLoading

return dayjs(date).format(`D MMM, YYYY [${translatedAt}] HH:mm`);
};

function getDaysUntilExpiration(expiresAt: Date | string): number {
Copy link
Collaborator

Choose a reason for hiding this comment

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

use arrow function format

Comment on lines 98 to 100
locked?: boolean;
onLockedClick?: () => void;
allowedExtension?: boolean;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Remember that Booleans should be read as a question (using prefixes such as is, has, can)

Comment on lines 103 to 138
const getVersionHistoryMenuItem = (
viewVersionHistory: (target?) => void,
config?: VersionHistoryMenuConfig,
): MenuItemType<DriveItemData> => {
const isLocked = config?.locked ?? false;
const allowedExtension = config?.allowedExtension ?? true;
const action = isLocked ? (config?.onLockedClick ?? (() => undefined)) : viewVersionHistory;
const IconComponent = isLocked ? LockSimple : ClockCounterClockwise;

const isVersionHistoryUnavailable = (item: DriveItemData) => {
const isFolder = item.isFolder;
const isUnsupportedExtension = !allowedExtension;
const isDisabledForUnlockedItem = !isLocked && (isFolder || isUnsupportedExtension);
return isDisabledForUnlockedItem;
};

return {
name: t('drive.dropdown.versionHistory'),
icon: IconComponent,
action,
disabled: isVersionHistoryUnavailable,
...(isLocked && {
locked: true,
node: (
<div
className="flex flex-row items-center space-x-2"
data-locked="true"
style={{ opacity: 0.5, cursor: 'default' }}
>
<IconComponent size={20} />
<span>{t('drive.dropdown.versionHistory')}</span>
</div>
),
}),
} as MenuItemType<DriveItemData> & { locked?: boolean };
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

this function seems a bit chaotic, I'll propose to change to something similar to this:

export type VersionHistoryMenuConfig = {
    isLocked: boolean;
    isExtensionAllowed: boolean;
    onUpgradeClick?: () => void;
  };

const getVersionHistoryMenuItem = (
    viewVersionHistory: (target?) => void,
    config?: VersionHistoryMenuConfig,
  ): MenuItemType<DriveItemData> => {
    const isLocked = config?.isLocked ?? false;
    const isExtensionAllowed = config?.isExtensionAllowed ?? true;
    const onUpgradeClick = config?.onUpgradeClick;

    if (isLocked) {
      return {
        name: t('drive.dropdown.versionHistory'),
        icon: LockSimple,
        action: onUpgradeClick,
        disabled: () => false,
        className: 'opacity-50 cursor-pointer', 
      };
    }

    return {
      name: t('drive.dropdown.versionHistory'),
      icon: ClockCounterClockwise,
      action: viewVersionHistory,
      disabled: (item: DriveItemData) => item.isFolder || !isExtensionAllowed,
    };
  };

Perhaps I'm missing something, let me know

  - Create fileVersions selectors for centralized state access
  - Rename state properties for clarity:
    - loadingStates → isLoadingByFileId
    - errors → errorsByFileId
    - limitsLoading → isLimitsLoading
  - Update type imports to use FileLimitsResponse (SDK v1.11.24)
  - Use NonNullable for Record keys to prevent null index types
  - Refactor version history menu config:
    - Rename properties: locked → isLocked, allowedExtension → isExtensionAllowed, onLockedClick → onUpgradeClick
    - Simplify getVersionHistoryMenuItem with early return pattern
  - Replace direct state access with selectors across components
  - Export getDaysUntilExpiration utility function
  - Add type assertion for restored version fileId
@terrerox terrerox requested a review from CandelR December 17, 2025 16:19
Comment on lines 41 to 46
const versions =
useAppSelector((state: RootState) => (item ? fileVersionsSelectors.getVersionsByFileId(state, item.uuid) : [])) ||
[];
const isLoading =
useAppSelector((state: RootState) => (item ? fileVersionsSelectors.isLoadingByFileId(state, item.uuid) : false)) ||
false;
Copy link
Collaborator

Choose a reason for hiding this comment

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

If the undefined return is not necessaryin getVersionsByFileId selector, the alternative return of [] could be added to the selectors when it does not exist, as has been done in isLoadingByFileId.
That way, the logical OR operator here will not be necessary and could be removed, which will make the code cleaner

Comment on lines +117 to +127
node: (
<div
className="flex flex-row items-center space-x-2"
data-locked="true"
style={{ opacity: 0.5, cursor: 'default' }}
>
<LockSimple size={20} />
<span>{t('drive.dropdown.versionHistory')}</span>
</div>
),
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

why is this component necesary? with only passing icon field is not displayed correctly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It’s necessary for the option to look disabled but still be clickable to open the plans modal

@terrerox terrerox requested a review from CandelR December 18, 2025 12:44
@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
17.6% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants