From 25ec0abb520df9b254ad3ab2f779693fe1b9bf4f Mon Sep 17 00:00:00 2001 From: Ramon Candel Segura Date: Thu, 23 Oct 2025 09:07:10 +0200 Subject: [PATCH 1/4] Updted readme --- README.md | 308 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 240 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index ecf99a1869..c6fcf4c25b 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ We aim to have: ## Scripts -### `yarn run dev` +### `yarn dev` Runs the app in the development mode.\ Open [http://localhost:3000](http://localhost:3000) to view it in the browser. @@ -33,7 +33,7 @@ Open [http://localhost:3000](http://localhost:3000) to view it in the browser. The page will reload if you make edits.\ You will also see any lint errors in the console. -### `yarn run preview` +### `yarn start` Serves the built application locally to preview the production output. Open [http://localhost:3000](http://localhost:3000) to view it in the browser. @@ -41,7 +41,7 @@ Open [http://localhost:3000](http://localhost:3000) to view it in the browser. - Useful for testing the result of a production build. - No hot reloading or development tools included. -> Before running `yarn run preview`, make sure you have already built the application using: +> Before running `yarn start`, make sure you have already built the application using: > `yarn run build` > The preview command serves the latest build output, so if you haven't run build beforehand, it will either fail or serve outdated files. @@ -50,7 +50,7 @@ Open [http://localhost:3000](http://localhost:3000) to view it in the browser. - Runs .ts linter - Runs .scss linter -### `yarn test` (`yarn test:unit`) +### `yarn test` - Runs unit tests with [Vitest](https://vitest.dev/) @@ -100,106 +100,278 @@ The [/src](./src) folder contains the source code. # New Project Structure -This project is organized following a **visual and functional hierarchy** approach. Each view (or page) has its own folder containing its specific components, styles, and logic. Additionally, reusable components, custom hooks, utilities, and global styles are stored in separate directories to enhance reusability and maintainability. +This project is organized following a **view-based hierarchy** approach. Each view (or page) has its own folder containing its specific components, styles, and logic. Additionally, reusable components, custom hooks, utilities, and global styles are stored in separate directories to enhance reusability and maintainability. Example: ``` src/ -├── components/ # Common reusable components across the application -│ ├── Button.tsx -│ ├── Modal.tsx -│ ├── Loader.tsx -│ └── index.ts -├── views/ # Main application views -│ ├── Login/ # Login view and its internal components -│ │ ├── LoginForm.tsx -│ │ ├── SocialLoginButtons.tsx -│ │ ├── styles.css -│ │ └── Login.tsx -│ └── hooks/ # Custom Login React hooks -│ └── useAuth.ts -│ ├── Signup/ # Signup view and its internal components -│ │ ├── SignupForm.tsx -│ │ ├── TermsCheckbox.tsx -│ │ ├── styles.css -│ │ └── Signup.tsx -│ ├── Home/ # Home view with its main components -│ │ ├── Topbar.tsx # Top navigation bar -│ │ ├── Sidenav.tsx # Side navigation menu -│ │ ├── Dashboard.tsx # Main panel of the Home view -│ │ ├── Settings/ # Settings (subfolder within Home) -│ │ │ ├── SettingsModal.tsx # Main settings page -│ │ │ ├── LanguageOptions.tsx -│ │ │ ├── ThemeSwitcher.tsx -│ │ │ └── styles.css -│ │ ├── styles.css # General styles for Home -│ │ └── Home.tsx # Main component for the Home view -├── hooks/ # Custom React hooks -│ ├── useTheme.ts -│ └── useFetch.ts -├── services/ # Logic for interacting with external APIs or services -│ ├── authService.ts -│ └── userService.ts -├── utils/ # Utility functions and helpers -│ ├── formatDate.ts # Date formatting functions -│ └── validateForm.ts # Form validation helpers -├── styles/ # Global styles -│ ├── variables.scss -│ └── global.css -├── types/ # Global and component-specific types -│ └── global.d.ts # Global types (e.g., user, environment) -├── App.jsx # Main application entry point -└── index.ts +├── views/ +│ ├── Login/ +│ │ ├── index.tsx # Main login view +│ │ ├── Login.module.css +│ │ ├── components/ # Login-specific components +│ │ │ ├── LoginForm.tsx +│ │ │ └── SocialLogin.tsx +│ │ ├── hooks/ # Custom hooks for login +│ │ │ └── useLogin.ts +│ │ ├── services/ # API calls for authentication +│ │ │ └── authService.ts +│ │ ├── store/ # Redux slice for login state +│ │ │ └── loginSlice.ts +│ │ └── types/ # TypeScript types/interfaces +│ │ └── login.types.ts +│ │ +│ ├── Signup/ +│ │ ├── index.tsx # Main signup view +│ │ ├── components/ # Signup-specific components +│ │ │ ├── SignupForm.tsx +│ │ │ └── PlanSelector.tsx +│ │ ├── hooks/ # Custom hooks for signup +│ │ │ └── useSignup.ts +│ │ ├── services/ # API calls for registration +│ │ │ └── registrationService.ts +│ │ ├── store/ # Redux slice for signup state +│ │ │ └── signupSlice.ts +│ │ └── types/ # TypeScript types/interfaces +│ │ └── signup.types.ts +│ │ +│ ├── Home/ # Main layout wrapper +│ │ ├── index.tsx # Home layout component +│ │ ├── components/ # Layout components +│ │ │ ├── Sidebar.tsx +│ │ │ ├── TopBar.tsx +│ │ │ └── UserMenu.tsx +│ │ ├── store/ # Redux slice for UI state +│ │ │ └── uiSlice.ts +│ │ ├── types/ # TypeScript types/interfaces +│ │ │ └── ui.types.ts +│ │ └── Home.module.css +│ │ +│ ├── Drive/ # Main files view +│ │ ├── index.tsx # Drive page component +│ │ ├── components/ # Drive-specific components +│ │ │ ├── FileList.tsx +│ │ │ ├── FileItem.tsx +│ │ │ ├── FolderItem.tsx +│ │ │ ├── UploadButton.tsx +│ │ │ └── FilePreview.tsx +│ │ ├── hooks/ # Custom hooks for files +│ │ │ ├── useFiles.ts +│ │ │ ├── useUpload.ts +│ │ │ └── useFileActions.ts +│ │ ├── services/ # API calls for files +│ │ │ ├── fileService.ts +│ │ │ └── uploadService.ts +│ │ ├── store/ # Redux slices for Drive +│ │ │ ├── filesSlice.ts # Files state management +│ │ │ └── selectors.ts # Reselect selectors +│ │ ├── types/ # TypeScript types/interfaces +│ │ │ └── file.types.ts +│ │ └── utils/ # Helper functions +│ │ └── fileHelpers.ts +│ │ +│ ├── Recents/ # Recent files view +│ │ ├── index.tsx # Recents page component +│ │ ├── components/ # Recents-specific components +│ │ │ ├── RecentFilesList.tsx +│ │ │ └── TimelineView.tsx +│ │ ├── hooks/ # Custom hooks for recent files +│ │ │ └── useRecentFiles.ts +│ │ ├── services/ # API calls for recents +│ │ │ └── recentsService.ts +│ │ ├── store/ # Redux slice for recents +│ │ │ └── recentsSlice.ts +│ │ └── types/ # TypeScript types/interfaces +│ │ └── recents.types.ts +│ │ +│ ├── Backups/ # Backups view +│ │ ├── index.tsx # Backups page component +│ │ ├── components/ # Backup-specific components +│ │ │ ├── BackupList.tsx +│ │ │ ├── CreateBackup.tsx +│ │ │ └── RestoreDialog.tsx +│ │ ├── hooks/ # Custom hooks for backups +│ │ │ └── useBackups.ts +│ │ ├── services/ # API calls for backups +│ │ │ └── backupService.ts +│ │ ├── store/ # Redux slice for backups +│ │ │ └── backupsSlice.ts +│ │ └── types/ # TypeScript types/interfaces +│ │ └── backup.types.ts +│ │ +│ ├── Shared/ # Shared files view +│ │ ├── index.tsx # Shared page component +│ │ ├── components/ # Shared-specific components +│ │ │ ├── SharedFilesList.tsx +│ │ │ └── ShareDialog.tsx +│ │ ├── hooks/ # Custom hooks for sharing +│ │ │ └── useSharedFiles.ts +│ │ ├── services/ # API calls for sharing +│ │ │ └── shareService.ts +│ │ ├── store/ # Redux slice for shared files +│ │ │ └── sharedSlice.ts +│ │ └── types/ # TypeScript types/interfaces +│ │ └── shared.types.ts +│ │ +│ └── Trash/ # Trash view +│ ├── index.tsx # Trash page component +│ ├── components/ # Trash-specific components +│ │ ├── TrashList.tsx +│ │ └── RestoreButton.tsx +│ ├── hooks/ # Custom hooks for trash +│ │ └── useTrash.ts +│ ├── services/ # API calls for trash +│ │ └── trashService.ts +│ ├── store/ # Redux slice for trash +│ │ └── trashSlice.ts +│ └── types/ # TypeScript types/interfaces +│ └── trash.types.ts +│ +├── shared/ # Shared code across views +│ ├── components/ # Reusable UI components +│ │ ├── Button/ +│ │ │ ├── Button.tsx +│ │ │ └── Button.types.ts +│ │ ├── Modal/ +│ │ │ ├── Modal.tsx +│ │ │ └── Modal.types.ts +│ │ ├── Dropdown/ +│ │ │ ├── Dropdown.tsx +│ │ │ └── Dropdown.types.ts +│ │ └── SearchBar/ +│ │ ├── SearchBar.tsx +│ │ └── SearchBar.types.ts +│ ├── hooks/ # Global custom hooks +│ │ ├── useAuth.ts +│ │ └── useTheme.ts +│ ├── store/ # Global Redux slices +│ │ ├── authSlice.ts # Global auth state +│ │ ├── userSlice.ts # User profile state +│ │ └── notificationsSlice.ts # App notifications +│ ├── types/ # Global TypeScript types +│ │ ├── user.types.ts +│ │ ├── auth.types.ts +│ │ └── api.types.ts +│ ├── utils/ # Global utility functions +│ │ ├── formatDate.ts +│ │ └── formatFileSize.ts +│ └── constants/ # App constants +│ └── routes.ts +│ +├── store/ # Redux store configuration +│ ├── index.ts # Store setup & root reducer +│ ├── rootReducer.ts # Combine all reducers +│ ├── store.types.ts # Store type definitions +│ └── middleware.ts # Custom middleware +│ +├── config/ # App configuration +│ └── api.ts # API base configuration +│ +├── routes/ # Route definitions +│ ├── AppRoutes.tsx # React Router setup +│ └── routes.types.ts # Route types +│ +└── App.tsx # Root application component ``` ## **Folder Descriptions** -### **`components/`** +Following the example structure above, each view folder contains the following subdirectories: -This folder contains common and reusable components that are used across different views, such as buttons, modals, or loaders. These are atomic components and are not tied to any specific view. +--- + +### **`views/[ViewName]/components/`** + +View-specific UI components that are only used within that particular view. These components are tightly coupled to the view's functionality and are not meant to be reused across other views. + +**Example:** `views/Login/components/LoginForm.tsx`, `views/Drive/components/FileList.tsx` --- -### **`views/`** +### **`views/[ViewName]/hooks/`** -Each main application view has its own folder (e.g., `Login`, `Signup`, `Home`). Inside each folder: +Custom React hooks that encapsulate view-specific logic and state management. These hooks are designed to be used only within their corresponding view. -- Specific components related to the view are included at the same level. -- Local styles are kept in a dedicated CSS file. -- If a view contains complex subsections (e.g., `Settings` within `Home`), they are organized in subfolders. +**Example:** `views/Login/hooks/useLogin.ts`, `views/Drive/hooks/useFileActions.ts` --- -### **`hooks/`** +### **`views/[ViewName]/services/`** -Custom React hooks that encapsulate reusable logic. +API calls and business logic specific to the view. This folder provides an abstraction layer for external interactions (API endpoints, data fetching) related to the feature. + +**Example:** `views/Login/services/authService.ts`, `views/Drive/services/fileService.ts` --- -### **`services/`** +### **`views/[ViewName]/store/`** + +Redux slices and state management specific to the view. Each view can manage its own state using Redux Toolkit slices, keeping state logic close to where it's used. -This folder contains logic for interacting with external APIs or services. It provides an abstraction layer for API calls or other external integrations. +**Example:** `views/Login/store/loginSlice.ts`, `views/Drive/store/filesSlice.ts` --- -### **`utils/`** +### **`views/[ViewName]/types/`** + +TypeScript type definitions and interfaces specific to the view. This includes props interfaces, data models, and any type that is only relevant to this feature. -Utility functions, global constants, and helpers that are not tied to React. These utilities can be used across the entire application. +**Example:** `views/Login/types/login.types.ts`, `views/Drive/types/file.types.ts` --- -### **`styles/`** +### **`views/[ViewName]/utils/`** + +Helper functions and utilities specific to the view. These are not React hooks but pure functions that help with data transformation, validation, or other view-specific operations. + +**Example:** `views/Drive/utils/fileHelpers.ts` + +--- + +### **`shared/`** + +Contains global, reusable code that is shared across multiple views: -Global styles and variables for consistent theming across the application. +- **`shared/components/`**: Atomic UI components (Button, Modal, Dropdown) used throughout the app +- **`shared/hooks/`**: Global custom hooks (useAuth, useTheme) shared across views +- **`shared/store/`**: Global Redux slices (authSlice, userSlice, notificationsSlice) +- **`shared/types/`**: Global TypeScript types (user.types.ts, api.types.ts) +- **`shared/utils/`**: Global utility functions (formatDate, formatFileSize) +- **`shared/constants/`**: App-wide constants (routes, API endpoints) --- -### **`types/`** +### **`store/`** + +Redux store configuration and setup: + +- **`store/index.ts`**: Store setup and root reducer +- **`store/rootReducer.ts`**: Combines all reducers (from views and shared) +- **`store/middleware.ts`**: Custom Redux middleware +- **`store/store.types.ts`**: Store type definitions + +--- + +### **`config/`** + +Application-wide configuration files (API base URLs, environment settings, feature flags). + +--- + +### **`routes/`** + +React Router configuration and route definitions for the entire application. + +--- -This folder contains shared TypeScript types used throughout the project +This **view-based structure** ensures: -This structure ensures **modularity**, **scalability**, and **maintainability** while making the codebase easy to navigate and extend. 🚀 +- **Modularity**: Each view is self-contained with its own components, logic, and state +- **Scalability**: Adding new features doesn't affect existing ones +- **Maintainability**: Related code is co-located, making it easy to find and modify +- **Reusability**: Shared code is clearly separated in the `shared/` directory +- **Type Safety**: TypeScript types are organized alongside the code that uses them ## Config Tailwind CSS purge option From 86299f1b6b09576e8ffc47bbe5e9b92a561af367 Mon Sep 17 00:00:00 2001 From: Ramon Candel Segura Date: Fri, 24 Oct 2025 08:06:01 +0200 Subject: [PATCH 2/4] Updated readme --- README.md | 220 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 134 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index c6fcf4c25b..a0fabc8da7 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,8 @@ The [/src](./src) folder contains the source code. This project is organized following a **view-based hierarchy** approach. Each view (or page) has its own folder containing its specific components, styles, and logic. Additionally, reusable components, custom hooks, utilities, and global styles are stored in separate directories to enhance reusability and maintainability. +> **Note:** The following is a **simplified example** to illustrate the organizational structure. The actual project structure may vary, but follows the same principles described here. + Example: ``` @@ -111,158 +113,182 @@ src/ │ │ ├── index.tsx # Main login view │ │ ├── Login.module.css │ │ ├── components/ # Login-specific components -│ │ │ ├── LoginForm.tsx -│ │ │ └── SocialLogin.tsx +│ │ │ ├── LoginForm/ # Complex component (folder) +│ │ │ │ ├── LoginForm.tsx +│ │ │ │ ├── LoginForm.test.tsx +│ │ │ │ └── useLoginForm.ts +│ │ │ └── SocialLogin.tsx # Simple component (file) │ │ ├── hooks/ # Custom hooks for login │ │ │ └── useLogin.ts │ │ ├── services/ # API calls for authentication -│ │ │ └── authService.ts +│ │ │ └── auth.service.ts │ │ ├── store/ # Redux slice for login state -│ │ │ └── loginSlice.ts -│ │ └── types/ # TypeScript types/interfaces -│ │ └── login.types.ts +│ │ │ └── index.ts +│ │ └── types.ts # TypeScript types/interfaces │ │ │ ├── Signup/ │ │ ├── index.tsx # Main signup view │ │ ├── components/ # Signup-specific components -│ │ │ ├── SignupForm.tsx -│ │ │ └── PlanSelector.tsx +│ │ │ ├── SignupForm/ +│ │ │ │ └── SignupForm.tsx +│ │ │ └── PlanSelector/ +│ │ │ └── PlanSelector.tsx │ │ ├── hooks/ # Custom hooks for signup │ │ │ └── useSignup.ts │ │ ├── services/ # API calls for registration -│ │ │ └── registrationService.ts +│ │ │ └── user.service.ts │ │ ├── store/ # Redux slice for signup state -│ │ │ └── signupSlice.ts -│ │ └── types/ # TypeScript types/interfaces -│ │ └── signup.types.ts +│ │ │ └── index.ts +│ │ └── types.ts # TypeScript types/interfaces │ │ │ ├── Home/ # Main layout wrapper │ │ ├── index.tsx # Home layout component │ │ ├── components/ # Layout components -│ │ │ ├── Sidebar.tsx -│ │ │ ├── TopBar.tsx -│ │ │ └── UserMenu.tsx +│ │ │ ├── Sidebar/ +│ │ │ │ └── Sidebar.tsx +│ │ │ ├── TopBar/ +│ │ │ │ └── TopBar.tsx +│ │ │ └── UserMenu/ +│ │ │ └── UserMenu.tsx │ │ ├── store/ # Redux slice for UI state -│ │ │ └── uiSlice.ts -│ │ ├── types/ # TypeScript types/interfaces -│ │ │ └── ui.types.ts +│ │ │ └── index.ts +│ │ ├── types.ts # TypeScript types/interfaces │ │ └── Home.module.css │ │ -│ ├── Drive/ # Main files view +│ ├── Drive/ # Main files view (large module) │ │ ├── index.tsx # Drive page component │ │ ├── components/ # Drive-specific components -│ │ │ ├── FileList.tsx -│ │ │ ├── FileItem.tsx -│ │ │ ├── FolderItem.tsx -│ │ │ ├── UploadButton.tsx -│ │ │ └── FilePreview.tsx +│ │ │ ├── FileList/ # Complex component (folder) +│ │ │ │ ├── FileList.tsx +│ │ │ │ ├── FileList.test.tsx +│ │ │ │ └── FileList.scss +│ │ │ ├── FileItem.tsx # Simple component (file) +│ │ │ ├── FolderItem.tsx # Simple component (file) +│ │ │ ├── UploadButton/ # Complex component (folder) +│ │ │ │ ├── UploadButton.tsx +│ │ │ │ └── helpers.ts +│ │ │ └── FilePreview/ # Complex component (folder) +│ │ │ ├── FilePreview.tsx +│ │ │ └── utils.ts │ │ ├── hooks/ # Custom hooks for files │ │ │ ├── useFiles.ts │ │ │ ├── useUpload.ts │ │ │ └── useFileActions.ts │ │ ├── services/ # API calls for files -│ │ │ ├── fileService.ts -│ │ │ └── uploadService.ts +│ │ │ ├── file.service.ts +│ │ │ └── upload.service.ts │ │ ├── store/ # Redux slices for Drive -│ │ │ ├── filesSlice.ts # Files state management -│ │ │ └── selectors.ts # Reselect selectors -│ │ ├── types/ # TypeScript types/interfaces -│ │ │ └── file.types.ts +│ │ │ ├── index.ts # Files state management +│ │ │ └── storage.selectors.ts # Reselect selectors +│ │ ├── types/ # TypeScript types (large module) +│ │ │ ├── index.ts # Barrel export +│ │ │ ├── file.types.ts # File-related types +│ │ │ └── download.types.ts # Download-related types │ │ └── utils/ # Helper functions -│ │ └── fileHelpers.ts +│ │ └── fileUtils.ts │ │ │ ├── Recents/ # Recent files view │ │ ├── index.tsx # Recents page component │ │ ├── components/ # Recents-specific components -│ │ │ ├── RecentFilesList.tsx -│ │ │ └── TimelineView.tsx +│ │ │ ├── RecentFilesList/ +│ │ │ │ └── RecentFilesList.tsx +│ │ │ └── TimelineView/ +│ │ │ └── TimelineView.tsx │ │ ├── hooks/ # Custom hooks for recent files │ │ │ └── useRecentFiles.ts │ │ ├── services/ # API calls for recents -│ │ │ └── recentsService.ts +│ │ │ └── recents.service.ts │ │ ├── store/ # Redux slice for recents -│ │ │ └── recentsSlice.ts -│ │ └── types/ # TypeScript types/interfaces -│ │ └── recents.types.ts +│ │ │ └── index.ts +│ │ └── types.ts # TypeScript types/interfaces │ │ │ ├── Backups/ # Backups view │ │ ├── index.tsx # Backups page component │ │ ├── components/ # Backup-specific components -│ │ │ ├── BackupList.tsx -│ │ │ ├── CreateBackup.tsx -│ │ │ └── RestoreDialog.tsx +│ │ │ ├── BackupList/ +│ │ │ │ └── BackupList.tsx +│ │ │ ├── CreateBackup/ +│ │ │ │ └── CreateBackup.tsx +│ │ │ └── RestoreDialog/ +│ │ │ └── RestoreDialog.tsx │ │ ├── hooks/ # Custom hooks for backups │ │ │ └── useBackups.ts │ │ ├── services/ # API calls for backups -│ │ │ └── backupService.ts +│ │ │ └── backup.service.ts │ │ ├── store/ # Redux slice for backups -│ │ │ └── backupsSlice.ts -│ │ └── types/ # TypeScript types/interfaces -│ │ └── backup.types.ts +│ │ │ └── index.ts +│ │ └── types.ts # TypeScript types/interfaces │ │ │ ├── Shared/ # Shared files view │ │ ├── index.tsx # Shared page component │ │ ├── components/ # Shared-specific components -│ │ │ ├── SharedFilesList.tsx -│ │ │ └── ShareDialog.tsx +│ │ │ ├── SharedFilesList/ # Complex component +│ │ │ │ ├── SharedFilesList.tsx +│ │ │ │ └── SharedFilesList.scss +│ │ │ └── ShareDialog/ # Complex component +│ │ │ ├── ShareDialog.tsx +│ │ │ └── components/ +│ │ │ └── UserOptions.tsx │ │ ├── hooks/ # Custom hooks for sharing │ │ │ └── useSharedFiles.ts │ │ ├── services/ # API calls for sharing -│ │ │ └── shareService.ts +│ │ │ └── share.service.ts │ │ ├── store/ # Redux slice for shared files -│ │ │ └── sharedSlice.ts -│ │ └── types/ # TypeScript types/interfaces -│ │ └── shared.types.ts +│ │ │ └── index.ts +│ │ └── types.ts # TypeScript types/interfaces │ │ │ └── Trash/ # Trash view │ ├── index.tsx # Trash page component │ ├── components/ # Trash-specific components -│ │ ├── TrashList.tsx -│ │ └── RestoreButton.tsx +│ │ ├── TrashList/ +│ │ │ └── TrashList.tsx +│ │ └── RestoreButton/ +│ │ └── RestoreButton.tsx │ ├── hooks/ # Custom hooks for trash │ │ └── useTrash.ts │ ├── services/ # API calls for trash -│ │ └── trashService.ts +│ │ └── trash.service.ts │ ├── store/ # Redux slice for trash -│ │ └── trashSlice.ts -│ └── types/ # TypeScript types/interfaces -│ └── trash.types.ts +│ │ └── index.ts +│ └── types.ts # TypeScript types/interfaces │ ├── shared/ # Shared code across views │ ├── components/ # Reusable UI components -│ │ ├── Button/ -│ │ │ ├── Button.tsx -│ │ │ └── Button.types.ts -│ │ ├── Modal/ +│ │ ├── BaseDialog/ # Complex component (folder) +│ │ │ ├── BaseDialog.tsx +│ │ │ ├── BaseDialog.scss +│ │ │ └── index.ts +│ │ ├── Modal/ # Complex component (folder) │ │ │ ├── Modal.tsx -│ │ │ └── Modal.types.ts -│ │ ├── Dropdown/ -│ │ │ ├── Dropdown.tsx -│ │ │ └── Dropdown.types.ts -│ │ └── SearchBar/ -│ │ ├── SearchBar.tsx -│ │ └── SearchBar.types.ts +│ │ │ ├── Modal.test.tsx +│ │ │ └── index.ts +│ │ ├── AuthButton.tsx # Simple component (file) +│ │ ├── BaseButton.tsx # Simple component (file) +│ │ └── Tooltip/ # Complex component (folder) +│ │ ├── Tooltip.tsx +│ │ └── index.ts │ ├── hooks/ # Global custom hooks │ │ ├── useAuth.ts │ │ └── useTheme.ts │ ├── store/ # Global Redux slices -│ │ ├── authSlice.ts # Global auth state -│ │ ├── userSlice.ts # User profile state -│ │ └── notificationsSlice.ts # App notifications -│ ├── types/ # Global TypeScript types -│ │ ├── user.types.ts -│ │ ├── auth.types.ts -│ │ └── api.types.ts +│ │ ├── session/ +│ │ │ ├── index.ts # Auth slice +│ │ │ └── session.selectors.ts +│ │ ├── user/ +│ │ │ └── index.ts # User slice +│ │ └── ui/ +│ │ └── index.ts # UI notifications slice +│ ├── types.ts # Global TypeScript types │ ├── utils/ # Global utility functions -│ │ ├── formatDate.ts -│ │ └── formatFileSize.ts +│ │ ├── timeUtils.ts +│ │ └── stringUtils.ts │ └── constants/ # App constants │ └── routes.ts │ ├── store/ # Redux store configuration │ ├── index.ts # Store setup & root reducer │ ├── rootReducer.ts # Combine all reducers -│ ├── store.types.ts # Store type definitions +│ ├── store.ts # Store type definitions │ └── middleware.ts # Custom middleware │ ├── config/ # App configuration @@ -270,7 +296,7 @@ src/ │ ├── routes/ # Route definitions │ ├── AppRoutes.tsx # React Router setup -│ └── routes.types.ts # Route types +│ └── routes.ts # Route types │ └── App.tsx # Root application component ``` @@ -285,7 +311,13 @@ Following the example structure above, each view folder contains the following s View-specific UI components that are only used within that particular view. These components are tightly coupled to the view's functionality and are not meant to be reused across other views. -**Example:** `views/Login/components/LoginForm.tsx`, `views/Drive/components/FileList.tsx` +**Organization:** +- **Complex components** (with tests, styles, hooks, helpers) → Use a folder: `LoginForm/LoginForm.tsx` +- **Simple components** (just the component file) → Use a file directly: `SocialLogin.tsx` + +**Example:** +- Complex: `views/Drive/components/FileList/FileList.tsx` +- Simple: `views/Drive/components/FileItem.tsx` --- @@ -301,7 +333,7 @@ Custom React hooks that encapsulate view-specific logic and state management. Th API calls and business logic specific to the view. This folder provides an abstraction layer for external interactions (API endpoints, data fetching) related to the feature. -**Example:** `views/Login/services/authService.ts`, `views/Drive/services/fileService.ts` +**Example:** `views/Login/services/auth.service.ts`, `views/Drive/services/file.service.ts` --- @@ -309,15 +341,31 @@ API calls and business logic specific to the view. This folder provides an abstr Redux slices and state management specific to the view. Each view can manage its own state using Redux Toolkit slices, keeping state logic close to where it's used. -**Example:** `views/Login/store/loginSlice.ts`, `views/Drive/store/filesSlice.ts` +**Example:** `views/Login/store/index.ts`, `views/Drive/store/index.ts` --- -### **`views/[ViewName]/types/`** +### **`views/[ViewName]/types/` or `types.ts`** TypeScript type definitions and interfaces specific to the view. This includes props interfaces, data models, and any type that is only relevant to this feature. -**Example:** `views/Login/types/login.types.ts`, `views/Drive/types/file.types.ts` +**Organization:** +- **Small modules** (< 100 lines of types) → Use a single file: `types.ts` +- **Large modules** (> 100 lines or multiple contexts) → Use a folder: `types/` + +**Nomenclature when using `types/` folder:** +``` +types/ +├── index.ts # Barrel export for all types +├── file.types.ts # File-related types +├── folder.types.ts # Folder-related types +├── user.types.ts # User-related types +└── api.types.ts # API-related types +``` + +**Example:** +- Simple: `views/Login/types.ts` +- Complex: `views/Drive/types/file.types.ts`, `views/Drive/types/download.types.ts` --- @@ -325,7 +373,7 @@ TypeScript type definitions and interfaces specific to the view. This includes p Helper functions and utilities specific to the view. These are not React hooks but pure functions that help with data transformation, validation, or other view-specific operations. -**Example:** `views/Drive/utils/fileHelpers.ts` +**Example:** `views/Drive/utils/fileUtils.ts` --- @@ -336,7 +384,7 @@ Contains global, reusable code that is shared across multiple views: - **`shared/components/`**: Atomic UI components (Button, Modal, Dropdown) used throughout the app - **`shared/hooks/`**: Global custom hooks (useAuth, useTheme) shared across views - **`shared/store/`**: Global Redux slices (authSlice, userSlice, notificationsSlice) -- **`shared/types/`**: Global TypeScript types (user.types.ts, api.types.ts) +- **`shared/types.ts`**: Global TypeScript types and interfaces - **`shared/utils/`**: Global utility functions (formatDate, formatFileSize) - **`shared/constants/`**: App-wide constants (routes, API endpoints) @@ -349,7 +397,7 @@ Redux store configuration and setup: - **`store/index.ts`**: Store setup and root reducer - **`store/rootReducer.ts`**: Combines all reducers (from views and shared) - **`store/middleware.ts`**: Custom Redux middleware -- **`store/store.types.ts`**: Store type definitions +- **`store/store.ts`**: Store type definitions --- From 0b30af5bef25dc0b9583eb54e9f31a0d70d376bf Mon Sep 17 00:00:00 2001 From: Ramon Candel Segura Date: Thu, 20 Nov 2025 09:32:21 +0100 Subject: [PATCH 3/4] Removed unused workflows, updated readme, added architecture, code style and testing documents and updated package.json to check types --- .../workflows/build-and-publish-preview.yaml | 48 - .github/workflows/cd-cloudflare.yml | 127 --- .github/workflows/ci.yml | 6 +- README.md | 48 +- docs/ARCHITECTURE.md | 585 ++++++++++++ docs/CODE_STYLE.md | 858 ++++++++++++++++++ package.json | 9 +- testing-guidelines.md | 255 ++++++ 8 files changed, 1742 insertions(+), 194 deletions(-) delete mode 100644 .github/workflows/build-and-publish-preview.yaml delete mode 100644 .github/workflows/cd-cloudflare.yml create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/CODE_STYLE.md create mode 100644 testing-guidelines.md diff --git a/.github/workflows/build-and-publish-preview.yaml b/.github/workflows/build-and-publish-preview.yaml deleted file mode 100644 index 701f00a0ed..0000000000 --- a/.github/workflows/build-and-publish-preview.yaml +++ /dev/null @@ -1,48 +0,0 @@ -name: Build & Publish Stable Preview -on: - push: - branches: ["master"] -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - run: echo "registry=https://registry.yarnpkg.com/" > .npmrc - - run: echo "@internxt:registry=https://npm.pkg.github.com" >> .npmrc - # You cannot read packages from other private repos with GITHUB_TOKEN - # You have to use a PAT instead https://github.com/actions/setup-node/issues/49 - - run: echo //npm.pkg.github.com/:_authToken=${{ secrets.PERSONAL_ACCESS_TOKEN }} >> .npmrc - - run: echo "always-auth=true" >> .npmrc - - - name: Login to DockerHub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - name: Build and push to ${{ github.event.repository.name }}-dev - uses: docker/build-push-action@v2 - with: - context: ./ - file: ./infrastructure/preview.Dockerfile - push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}-dev:${{ github.sha }} - dispatch_update_preview_image: - needs: build - runs-on: ubuntu-latest - steps: - - name: Dispatch Update Preview Image Command - uses: myrotvorets/trigger-repository-dispatch-action@1.0.0 - with: - token: ${{ secrets.PAT }} - repo: internxt/environments - type: update-preview-image-command - payload: | - { - "image": { - "name": "${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}", - "newName": "${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}-dev", - "newTag": "${{ github.sha }}" - } - } \ No newline at end of file diff --git a/.github/workflows/cd-cloudflare.yml b/.github/workflows/cd-cloudflare.yml deleted file mode 100644 index 32dea09401..0000000000 --- a/.github/workflows/cd-cloudflare.yml +++ /dev/null @@ -1,127 +0,0 @@ -name: Drive Web Cloudflare CD -on: - push: - branches: ['*'] - pull_request: - types: [opened, synchronize, reopened] -jobs: - build: - runs-on: ubuntu-latest - permissions: - contents: read - packages: read - strategy: - matrix: - node-version: [18.x] - steps: - - name: Checkout repo - uses: actions/checkout@v4 - with: - registry-url: 'https://npm.pkg.github.com' - - run: echo REACT_APP_CRYPTO_SECRET=${{ secrets.REACT_APP_CRYPTO_SECRET }} >> ./.env - - run: echo REACT_APP_STRIPE_PK=${{ secrets.REACT_APP_STRIPE_PK }} >> ./.env - - run: echo REACT_APP_STRIPE_TEST_PK=${{ secrets.REACT_APP_STRIPE_TEST_PK }} >> ./.env - - run: echo REACT_APP_SEGMENT_KEY=${{ secrets.REACT_APP_SEGMENT_KEY }} >> ./.env - - run: echo REACT_APP_MAGIC_IV=${{ secrets.REACT_APP_MAGIC_IV }} >> ./.env - - run: echo REACT_APP_MAGIC_SALT=${{ secrets.REACT_APP_MAGIC_SALT }} >> ./.env - - run: echo REACT_APP_CRYPTO_SECRET2=${{ secrets.REACT_APP_CRYPTO_SECRET2 }} >> ./.env - - run: echo GENERATE_SOURCEMAP=${{ secrets.GENERATE_SOURCEMAP }} >> ./.env - - run: echo REACT_APP_RECAPTCHA_V3=${{ secrets.REACT_APP_RECAPTCHA_V3 }} >> ./.env - - run: echo REACT_APP_STORJ_BRIDGE=${{ secrets.REACT_APP_STORJ_BRIDGE }} >> ./.env - - run: echo REACT_APP_HOSTNAME=${{ secrets.REACT_APP_HOSTNAME }} >> ./.env - - run: echo REACT_APP_PROXY=${{ secrets.REACT_APP_PROXY }} >> ./.env - - run: echo REACT_APP_DONT_USE_PROXY=${{ secrets.REACT_APP_DONT_USE_PROXY }} >> ./.env - - run: echo REACT_APP_PHOTOS_API_URL=${{ secrets.REACT_APP_PHOTOS_API_URL }} >> ./.env - - run: echo REACT_APP_SENTRY_DSN=${{ secrets.REACT_APP_SENTRY_DSN }} >> ./.env - - run: echo REACT_APP_PAYMENTS_API_URL=${{ secrets.REACT_APP_PAYMENTS_API_URL }} >> ./.env - - run: echo REACT_APP_DRIVE_NEW_API_URL=${{ secrets.REACT_APP_DRIVE_NEW_API_URL }} >> ./.env - - run: echo REACT_APP_LOCATION_API_URL=${{ secrets.REACT_APP_LOCATION_API_URL }} >> ./.env - - run: echo REACT_APP_NODE_ENV=production >> ./.env - - run: echo "registry=https://registry.yarnpkg.com/" > .npmrc - - run: echo "@internxt:registry=https://npm.pkg.github.com" >> .npmrc - - run: echo //npm.pkg.github.com/:_authToken=${{ secrets.PERSONAL_ACCESS_TOKEN }} >> .npmrc - - run: echo "always-auth=true" >> .npmrc - - # Setup node - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - - # Setup dependencies - - run: yarn - env: - NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # Build - - run: yarn run build - env: - CI: false - - # Upload build directory as an artifact - - name: Upload build directory - uses: actions/upload-artifact@v4 - with: - name: build - path: build/ - - publish: - needs: build - runs-on: ubuntu-latest - permissions: - contents: read - deployments: write - pull-requests: write - name: Publish to Cloudflare Pages - steps: - - name: Checkout repo - uses: actions/checkout@v4 - - # Download the build artifact - - name: Download build artifact - uses: actions/download-artifact@v4 - with: - name: build - path: build - - - name: Publish to Cloudflare Pages - id: cloudflare - uses: cloudflare/pages-action@v1 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - projectName: drive-web - directory: build - gitHubToken: ${{ secrets.GITHUB_TOKEN }} - wranglerVersion: '3' - - - name: Show deployment info - run: | - echo "🚀 Deployment completed!" - echo "📱 Cloudflare URL: ${{ steps.cloudflare.outputs.url }}" - - - name: Find existing comment - if: github.event_name == 'pull_request' - uses: peter-evans/find-comment@v3 - id: find-comment - with: - issue-number: ${{ github.event.pull_request.number }} - comment-author: 'github-actions[bot]' - body-includes: 'Preview Deploy Ready!' - - - name: Create or update comment - if: github.event_name == 'pull_request' - uses: peter-evans/create-or-update-comment@v4 - with: - comment-id: ${{ steps.find-comment.outputs.comment-id }} - issue-number: ${{ github.event.pull_request.number }} - body: | - 🚀 **Preview Deploy Ready!** - - 📱 **Preview URL:** ${{ steps.cloudflare.outputs.url }} - - Built from commit: `${{ github.sha }}` - - --- - This preview will be automatically updated when you push new commits to this PR. - edit-mode: replace diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc3c5612c0..4e75bc9590 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,8 @@ jobs: - run: echo "always-auth=true" >> .npmrc - name: Install run: yarn + - name: Type check + run: yarn typecheck - name: Install Playwright Browsers run: yarn playwright install - name: Unit test run @@ -76,8 +78,10 @@ jobs: - run: echo "always-auth=true" >> .npmrc - name: Install run: yarn + - name: Type check + run: yarn typecheck - name: Build - run: yarn build + run: yarn build:only env: CI: false - name: Install netlify diff --git a/README.md b/README.md index a0fabc8da7..93165f0ca4 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,15 @@ [![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=internxt_drive-web&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=internxt_drive-web) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=internxt_drive-web&metric=coverage)](https://sonarcloud.io/summary/new_code?id=internxt_drive-web) +# Internxt Drive Web + +A secure, privacy-focused cloud storage web application built with React, TypeScript, and Redux. + +## Documentation + +- 📖 **[docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md)** - Project architecture and structure +- 📖 **[docs/CODE_STYLE.md](./docs/CODE_STYLE.md)** - Coding standards and conventions + # Project Maintenance We aim to have: @@ -225,10 +234,8 @@ src/ │ │ │ ├── SharedFilesList/ # Complex component │ │ │ │ ├── SharedFilesList.tsx │ │ │ │ └── SharedFilesList.scss -│ │ │ └── ShareDialog/ # Complex component -│ │ │ ├── ShareDialog.tsx -│ │ │ └── components/ -│ │ │ └── UserOptions.tsx +│ │ │ └── SharedItemActions/ # Complex component +│ │ │ └── SharedItemActions.tsx │ │ ├── hooks/ # Custom hooks for sharing │ │ │ └── useSharedFiles.ts │ │ ├── services/ # API calls for sharing @@ -252,8 +259,16 @@ src/ │ │ └── index.ts │ └── types.ts # TypeScript types/interfaces │ -├── shared/ # Shared code across views +├── common/ # Common code across views │ ├── components/ # Reusable UI components +│ │ ├── ShareDialog/ # Complex component (used in Drive & Shared views) +│ │ │ ├── ShareDialog.tsx +│ │ │ ├── ShareDialog.scss +│ │ │ ├── components/ +│ │ │ │ ├── User.tsx +│ │ │ │ └── InvitedUsersSkeletonLoader.tsx +│ │ │ ├── types.ts +│ │ │ └── index.ts │ │ ├── BaseDialog/ # Complex component (folder) │ │ │ ├── BaseDialog.tsx │ │ │ ├── BaseDialog.scss @@ -312,10 +327,12 @@ Following the example structure above, each view folder contains the following s View-specific UI components that are only used within that particular view. These components are tightly coupled to the view's functionality and are not meant to be reused across other views. **Organization:** + - **Complex components** (with tests, styles, hooks, helpers) → Use a folder: `LoginForm/LoginForm.tsx` - **Simple components** (just the component file) → Use a file directly: `SocialLogin.tsx` **Example:** + - Complex: `views/Drive/components/FileList/FileList.tsx` - Simple: `views/Drive/components/FileItem.tsx` @@ -350,10 +367,12 @@ Redux slices and state management specific to the view. Each view can manage its TypeScript type definitions and interfaces specific to the view. This includes props interfaces, data models, and any type that is only relevant to this feature. **Organization:** + - **Small modules** (< 100 lines of types) → Use a single file: `types.ts` - **Large modules** (> 100 lines or multiple contexts) → Use a folder: `types/` **Nomenclature when using `types/` folder:** + ``` types/ ├── index.ts # Barrel export for all types @@ -364,6 +383,7 @@ types/ ``` **Example:** + - Simple: `views/Login/types.ts` - Complex: `views/Drive/types/file.types.ts`, `views/Drive/types/download.types.ts` @@ -377,16 +397,18 @@ Helper functions and utilities specific to the view. These are not React hooks b --- -### **`shared/`** +### **`common/`** Contains global, reusable code that is shared across multiple views: -- **`shared/components/`**: Atomic UI components (Button, Modal, Dropdown) used throughout the app -- **`shared/hooks/`**: Global custom hooks (useAuth, useTheme) shared across views -- **`shared/store/`**: Global Redux slices (authSlice, userSlice, notificationsSlice) -- **`shared/types.ts`**: Global TypeScript types and interfaces -- **`shared/utils/`**: Global utility functions (formatDate, formatFileSize) -- **`shared/constants/`**: App-wide constants (routes, API endpoints) +- **`common/components/`**: Reusable UI components used across multiple views + - Atomic components (Button, Modal, Dropdown) + - Cross-view dialogs (ShareDialog used in Drive & Shared views) +- **`common/hooks/`**: Global custom hooks (useAuth, useTheme) shared across views +- **`common/store/`**: Global Redux slices (authSlice, userSlice, notificationsSlice) +- **`common/types.ts`**: Global TypeScript types and interfaces +- **`common/utils/`**: Global utility functions (formatDate, formatFileSize) +- **`common/constants/`**: App-wide constants (routes, API endpoints) --- @@ -418,7 +440,7 @@ This **view-based structure** ensures: - **Modularity**: Each view is self-contained with its own components, logic, and state - **Scalability**: Adding new features doesn't affect existing ones - **Maintainability**: Related code is co-located, making it easy to find and modify -- **Reusability**: Shared code is clearly separated in the `shared/` directory +- **Reusability**: Common code is clearly separated in the `common/` directory - **Type Safety**: TypeScript types are organized alongside the code that uses them ## Config Tailwind CSS purge option diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000000..3526beb378 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,585 @@ +# Project Architecture + +This document describes the architectural decisions, project structure, and organizational principles for Internxt Drive Web. + +## Table of Contents + +- [Overview](#overview) +- [Architecture Principles](#architecture-principles) +- [Project Structure](#project-structure) +- [View-Based Architecture](#view-based-architecture) +- [Shared Code Organization](#shared-code-organization) +- [State Management](#state-management) +- [Routing](#routing) +- [File Organization Rules](#file-organization-rules) +- [Examples](#examples) + +--- + +## Overview + +Internxt Drive Web follows a **view-based hierarchy** architecture. Each view (or page) is self-contained with its own components, logic, state, and styles. This approach ensures modularity, scalability, and maintainability. + +### Key Benefits + +- **Modularity**: Each view is independent and self-contained +- **Scalability**: Adding new features doesn't affect existing code +- **Maintainability**: Related code is co-located, easy to find and modify +- **Reusability**: Shared code is clearly separated in dedicated directories +- **Type Safety**: TypeScript types are organized alongside the code that uses them + +--- + +## Architecture Principles + +### 1. **Separation of Concerns** + +Code is organized by feature (view) rather than by type: + +``` +✅ Good: views/Drive/components/FileList.tsx +❌ Bad: components/drive/FileList.tsx +``` + +### 2. **Co-location** + +Related code lives together: + +``` +views/Drive/ +├── components/ # UI components for Drive +├── hooks/ # Custom hooks for Drive +├── services/ # API calls for Drive +├── store/ # State management for Drive +└── types.ts # Types for Drive +``` + +### 3. **Single Responsibility** + +Each directory has a clear, single purpose: + +- **`views/`**: View-specific code +- **`common/`**: Cross-feature reusable code +- **`store/`**: Redux configuration +- **`routes/`**: Routing configuration +- **`config/`**: App-wide configuration + +### 4. **Explicit Dependencies** + +Import paths should be clear and explicit: + +```tsx +// ✅ Good: Clear what's being imported and from where +import { FileList } from 'views/Drive/components/FileList'; +import { ShareDialog } from 'common/components/ShareDialog'; + +// ❌ Bad: Unclear source +import { FileList } from '../../../components'; +``` + +--- + +## Project Structure + +``` +src/ +├── views/ # Main application views +│ ├── Drive/ # Main files view +│ ├── Shared/ # Shared links view +│ ├── Recents/ # Recent files view +│ ├── Backups/ # Backups view +│ └── Trash/ # Trash view +│ +├── common/ # Common code across views +│ ├── components/ # Reusable UI components +│ ├── hooks/ # Global custom hooks +│ ├── store/ # Global Redux slices +│ ├── types.ts # Global TypeScript types +│ ├── utils/ # Global utility functions +│ └── constants/ # App-wide constants +│ +├── store/ # Redux store configuration +│ ├── index.ts # Store setup & root reducer +│ ├── rootReducer.ts # Combine all reducers +│ ├── store.ts # Store type definitions +│ └── middleware.ts # Custom middleware +│ +├── config/ # App configuration +│ └── api.ts # API base configuration +│ +├── routes/ # Route definitions +│ ├── AppRoutes.tsx # React Router setup +│ └── routes.ts # Route types +│ +└── App.tsx # Root application component +``` + +--- + +## View-Based Architecture + +Each view represents a major section of the application (a page or feature). + +### View Structure + +``` +views/[ViewName]/ +├── index.tsx # Main view component (entry point) +├── components/ # View-specific UI components +│ ├── ComplexComponent/ # Folder for complex components +│ │ ├── ComplexComponent.tsx +│ │ ├── ComplexComponent.test.tsx +│ │ ├── ComplexComponent.scss +│ │ └── index.ts +│ └── SimpleComponent.tsx # File for simple components +├── hooks/ # View-specific custom hooks +│ └── useViewLogic.ts +├── services/ # View-specific API calls +│ └── view.service.ts +├── store/ # View-specific Redux slices +│ ├── index.ts +│ └── selectors.ts +├── types.ts or types/ # View-specific TypeScript types +│ ├── index.ts # (if using folder) +│ └── entity.types.ts +└── utils/ # View-specific helper functions + └── helpers.ts +``` + +### When to Create a New View + +Create a new view when: + +- It represents a distinct page/route in the application +- It has its own unique URL path +- It has significant functionality that deserves isolation +- It will have multiple components and logic specific to it + +**Examples:** + +- ✅ `views/Drive/` - Main file explorer page +- ✅ `views/Shared/` - Shared links management page +- ✅ `views/Recents/` - Recent files page +- ❌ Don't create `views/FilePreview/` - This is a component, not a page + +--- + +## Common Code Organization + +### `common/components/` + +Reusable UI components used across multiple views. + +**Categories:** + +1. **Atomic Components**: Basic UI elements + + - `Button`, `Input`, `Checkbox`, `Dropdown` + +2. **Composite Components**: More complex UI elements + + - `Modal`, `Dialog`, `Tooltip` + +3. **Cross-View Components**: Feature-specific but used in multiple views + - `ShareDialog` (used in Drive & Shared views) + - `DeleteItemsDialog` (used in Drive, Shared, Trash views) + +**When to use `common/components/`:** + +- Component is used in **2 or more views** +- Component is generic enough to be reusable +- Component represents a common UI pattern + +**When NOT to use `common/components/`:** + +- Component is specific to one view → Use `views/[ViewName]/components/` +- Component is tightly coupled to view logic → Keep in view + +### `common/hooks/` + +Custom React hooks used across multiple views. + +**Examples:** + +```tsx +// common/hooks/useTheme.ts +export const useTheme = () => { + // Global theme management +}; +``` + +### `common/store/` + +Global Redux slices that manage app-wide state. + +**Examples:** + +- `common/store/session/` - Authentication state +- `common/store/user/` - User profile data +- `common/store/ui/` - Global UI state (modals, notifications) + +**When to use `common/store/`:** + +- State is needed across multiple views +- State represents global app concerns (auth, user, settings) + +**When NOT to use `common/store/`:** + +- State is specific to one view → Use `views/[ViewName]/store/` + +### `common/utils/` + +Pure utility functions used across the application. + +**Examples:** + +```tsx +// common/utils/formatDate.ts +export const formatDate = (date: Date): string => { ... }; + +// common/utils/formatFileSize.ts +export const formatFileSize = (bytes: number): string => { ... }; +``` + +### `common/types.ts` + +Global TypeScript types and interfaces. + +**Examples:** + +```tsx +// common/types.ts +export interface User { + id: string; + email: string; + name: string; +} + +export type ApiResponse = { + data: T; + error?: string; +}; +``` + +### `common/constants/` + +App-wide constants. + +**Examples:** + +```tsx +// common/constants/routes.ts +export const ROUTES = { + DRIVE: '/drive', + SHARED: '/shared', + RECENTS: '/recents', +}; + +// common/constants/api.ts +export const API_ENDPOINTS = { + FILES: '/api/files', + FOLDERS: '/api/folders', +}; +``` + +--- + +## State Management + +### Redux Structure + +``` +store/ +├── index.ts # Store configuration and setup +├── rootReducer.ts # Combines all reducers +└── middleware.ts # Custom middleware + +common/store/ # Global slices +├── session/ +│ ├── index.ts # Session slice +│ └── session.selectors.ts +└── user/ + └── index.ts # User slice + +views/Drive/store/ # View-specific slices +├── index.ts # Files slice +└── storage.selectors.ts +``` + +### State Organization Guidelines + +**Global State** (`common/store/`): + +- Authentication (session, tokens) +- User profile (name, email, settings) +- UI state (modals, notifications, theme) + +**View-Specific State** (`views/[ViewName]/store/`): + +- Data specific to the view +- UI state specific to the view +- Temporary state that doesn't need to persist + +**Local Component State** (`useState`): + +- Form inputs +- Toggle states (open/closed, expanded/collapsed) +- Ephemeral UI state + +--- + +## Routing + +The application uses a **configuration-based routing system** where routes are defined in JSON and dynamically mapped to views and layouts. + +### Route Structure + +``` +src/routes/ +├── routes.tsx # Dynamic route generator +├── paths.json # Route configuration (paths, layouts, auth) +└── hooks/ # Routing-related hooks +``` + +### Route Configuration + +Routes are defined in `src/routes/paths.json` with metadata: + +```json +{ + "views": [ + { + "id": "drive", + "layout": "header-and-sidenav", + "path": "/", + "exact": true, + "auth": true + }, + { + "id": "recents", + "layout": "header-and-sidenav", + "path": "/recents", + "exact": true, + "auth": true, + "hideSearch": true + }, + { + "id": "trash", + "layout": "header-and-sidenav", + "path": "/trash", + "exact": true, + "auth": true, + "hideSearch": true + } + ] +} +``` + +**Route Properties:** +- `id`: Unique identifier (mapped to AppView enum) +- `layout`: Layout wrapper to use (`empty`, `header-and-sidenav`, `share`) +- `path`: URL path pattern +- `exact`: Exact path matching +- `auth`: Requires authentication +- `hideSearch`: Hide search bar (optional) + +### View Registration + +Views are registered in `src/config/views.ts`: + +```tsx +// src/config/views.ts +import { AppView } from '../types'; +import DriveView from '../views/Drive'; +import RecentsView from '../views/Recents'; +import TrashView from '../views/Trash'; + +const views = [ + { id: AppView.Drive, component: DriveView }, + { id: AppView.Recents, component: RecentsView }, + { id: AppView.Trash, component: TrashView }, + // ... +]; + +export default views; +``` + +### Navigation Service + +Navigation is handled through a centralized `navigationService` instead of React Router's `useNavigate`. This provides type-safe navigation using the `AppView` enum. + +**Example:** + +```tsx +import navigationService from 'src/services/navigation.service'; +import { AppView } from 'src/types'; + +// Navigate to a view +navigationService.push(AppView.Drive); + +// Navigate with query parameters +navigationService.push(AppView.Recents, { filter: 'images' }); + +// Navigate to folder or file +navigationService.pushFolder('folder-uuid'); +navigationService.pushFile('file-uuid'); +``` + +For complete API reference, see the navigationService source code. + +--- + +## File Organization Rules + +### Components + +**Complex Component** (has tests, styles, sub-components, or helpers): + +``` +FileList/ +├── FileList.tsx +├── FileList.test.tsx +├── FileList.scss +├── components/ # Sub-components +│ └── FileListItem.tsx +├── helpers.ts # Helper functions +└── index.ts # Barrel export +``` + +**Simple Component** (just the component): + +``` +FileItem.tsx # Single file +``` + +### Types + +**Small Module** (< 100 lines of types): + +``` +views/Login/ +└── types.ts # All types in one file +``` + +**Large Module** (> 100 lines or multiple contexts): + +``` +views/Drive/ +└── types/ + ├── index.ts # Barrel export + ├── file.types.ts + ├── folder.types.ts + └── upload.types.ts +``` + +### Services + +**API Services** follow a consistent pattern: + +```tsx +// views/Drive/services/file.service.ts +class FileService { + async getFiles(folderId: string): Promise { ... } + async uploadFile(file: File): Promise { ... } + async deleteFile(fileId: string): Promise { ... } +} + +export default new FileService(); +``` + +--- + +## Examples + +### Example 1: Adding a New View + +**Scenario**: Add a "Photos" view to display images. + +``` +1. Create view structure: + views/Photos/ + ├── index.tsx + ├── components/ + │ ├── PhotoGrid/ + │ │ ├── PhotoGrid.tsx + │ │ └── PhotoGrid.scss + │ └── PhotoItem.tsx + ├── hooks/ + │ └── usePhotos.ts + ├── services/ + │ └── photo.service.ts + ├── store/ + │ └── index.ts + └── types.ts + +2. Add route: + routes/routes.ts: export const PHOTOS = '/photos'; + +3. Register in router: + routes/AppRoutes.tsx: } /> +``` + +### Example 2: Creating a Shared Component + +**Scenario**: Create a `ConfirmDialog` used in multiple views. + +``` +1. Check if it's truly shared (used in 2+ views): ✅ Yes + +2. Create in common/components/: + common/components/ConfirmDialog/ + ├── ConfirmDialog.tsx + ├── ConfirmDialog.test.tsx + ├── ConfirmDialog.scss + └── index.ts + +3. Use in views: + import { ConfirmDialog } from 'common/components/ConfirmDialog'; +``` + +### Example 3: View-Specific Component + +**Scenario**: Create a `FileUploadProgress` component for Drive view only. + +``` +1. Check if it's view-specific: ✅ Yes (only used in Drive) + +2. Create in view components: + views/Drive/components/ + └── FileUploadProgress/ + ├── FileUploadProgress.tsx + ├── FileUploadProgress.scss + └── index.ts + +3. Use only within Drive view: + import { FileUploadProgress } from '../components/FileUploadProgress'; +``` + +--- + +## Migration Strategy + +The project is currently migrating from the old structure (`src/app/`) to the new view-based structure (`src/views/`). + +### Migration Checklist + +When migrating a module: + +- [ ] Create new view directory in `src/views/` +- [ ] Move view component to `views/[ViewName]/index.tsx` +- [ ] Move view-specific components to `views/[ViewName]/components/` +- [ ] Move common components to `src/common/components/` +- [ ] Move services to appropriate location +- [ ] Move Redux slices to appropriate location +- [ ] Update all import paths +- [ ] Update tests +- [ ] Remove old module directory + +--- + +## Questions? + +If you have questions about the architecture, refer to: + +- [CODE_STYLE.md](./CODE_STYLE.md) - Coding standards and conventions +- [README.md](../README.md) - Project overview diff --git a/docs/CODE_STYLE.md b/docs/CODE_STYLE.md new file mode 100644 index 0000000000..3e41427890 --- /dev/null +++ b/docs/CODE_STYLE.md @@ -0,0 +1,858 @@ +# Code Style Guide + +This document defines the coding standards, conventions, and best practices for Internxt Drive Web. + +## Table of Contents + +- [TypeScript Guidelines](#typescript-guidelines) +- [React Guidelines](#react-guidelines) +- [Component Organization](#component-organization) +- [Hooks Guidelines](#hooks-guidelines) +- [Styling Guidelines](#styling-guidelines) +- [Naming Conventions](#naming-conventions) +- [File Organization](#file-organization) +- [Import Organization](#import-organization) +- [Testing Guidelines](#testing-guidelines) +- [Comments and Documentation](#comments-and-documentation) +- [Performance Best Practices](#performance-best-practices) +- [Accessibility](#accessibility) + +--- + +## TypeScript Guidelines + +### Always Use TypeScript + +Avoid `any` types. Use proper types or `unknown` if type is truly unknown. + +```tsx +// ❌ Bad +const data: any = fetchData(); + +// ✅ Good +const data: UserData = fetchData(); + +// ✅ Good (when type is unknown) +const data: unknown = fetchData(); +if (isUserData(data)) { + // Type guard + // data is UserData here +} +``` + +### Define Interfaces for Props + +Always define explicit interfaces for component props. + +```tsx +// ✅ Good +interface FileListProps { + files: File[]; + onFileClick: (file: File) => void; + isLoading?: boolean; + className?: string; +} + +const FileList: React.FC = ({ files, onFileClick, isLoading = false, className }) => { + // ... +}; + +export default FileList; +``` + +### Use Type Inference When Obvious + +Don't over-annotate when TypeScript can infer the type. + +```tsx +// ❌ Bad (unnecessary annotation) +const count: number = items.length; +const name: string = 'John'; + +// ✅ Good (let TypeScript infer) +const count = items.length; +const name = 'John'; + +// ✅ Good (annotation needed) +const [user, setUser] = useState(null); +``` + +### Use Union Types for String Literals + +```tsx +// ✅ Good +type FileStatus = 'pending' | 'uploading' | 'completed' | 'error'; + +interface File { + id: string; + status: FileStatus; +} + +// ❌ Bad +interface File { + id: string; + status: string; // Too loose +} +``` + +### Use `readonly` for Immutable Data + +```tsx +// ✅ Good +interface Config { + readonly apiUrl: string; + readonly timeout: number; +} + +type ReadonlyArray = readonly T[]; +``` + +### Generic Types + +```tsx +// ✅ Good +interface ApiResponse { + data: T; + error?: string; + loading: boolean; +} + +function fetchData(url: string): Promise> { + // ... +} +``` + +--- + +## React Guidelines + +### Component Structure + +Follow a consistent component structure: + +```tsx +import React, { useState, useEffect } from 'react'; +import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; + +interface ComponentNameProps { + // Props definition + title: string; + onAction: () => void; +} + +const ComponentName: React.FC = ({ title, onAction }) => { + // 1. Hooks (React hooks first, then custom hooks) + const { translate } = useTranslationContext(); + const [isOpen, setIsOpen] = useState(false); + + // 2. Effects + useEffect(() => { + // Side effects + }, []); + + // 3. Event Handlers + const handleClick = () => { + setIsOpen(true); + onAction(); + }; + + // 4. Render Helpers (if needed) + const renderContent = () => { + if (!isOpen) return null; + return
{title}
; + }; + + // 5. Return JSX + return ( +
+ + {renderContent()} +
+ ); +}; + +export default ComponentName; +``` + +### Functional Components Only + +Use functional components with hooks (no class components). + +```tsx +// ✅ Good +const FileList: React.FC = ({ files }) => { + const [selected, setSelected] = useState([]); + return
{/* ... */}
; +}; + +// ❌ Bad (don't use class components) +class FileList extends React.Component { + // ... +} +``` + +### Props Destructuring + +Destructure props in the function signature. + +```tsx +// ✅ Good +const FileItem: React.FC = ({ file, onSelect, isSelected = false }) => { + return
{file.name}
; +}; + +// ❌ Bad +const FileItem: React.FC = (props) => { + return
{props.file.name}
; +}; +``` + +### Default Props + +Use default parameter values instead of `defaultProps`. + +```tsx +// ✅ Good +const Button: React.FC = ({ variant = 'primary', size = 'medium', disabled = false }) => { + // ... +}; + +// ❌ Bad (deprecated pattern) +Button.defaultProps = { + variant: 'primary', + size: 'medium', +}; +``` + +### Conditional Rendering + +```tsx +// ✅ Good (using &&) +{ + isLoading && ; +} + +// ✅ Good (using ternary) +{ + isLoading ? : ; +} + +// ✅ Good (early return) +if (!data) return ; +return ; + +// ❌ Bad (unnecessary ternary) +{ + isLoading ? : null; +} // Use && instead +``` + +### Lists and Keys + +Always use unique, stable keys for list items. + +```tsx +// ✅ Good (using unique ID) +{ + files.map((file) => ); +} + +// ❌ Bad (using index) +{ + files.map((file, index) => ); +} +``` + +--- + +## Component Organization + +### Complex Components (Folder) + +Use a folder when component has: + +- Tests +- Sub-components +- Helper functions + +``` +FileList/ +├── FileList.tsx # Main component +├── FileList.test.tsx # Tests +├── components/ # Sub-components +│ ├── FileListItem.tsx +│ └── FileListHeader.tsx +├── helpers.ts # Helper functions +└── index.ts # Barrel export +``` + +**index.ts (barrel export):** + +```tsx +export { default } from './FileList'; +export type { FileListProps } from './FileList'; +``` + +### Simple Components (File) + +Use a single file when component is standalone. + +``` +FileItem.tsx # Single file component +``` + +--- + +## Hooks Guidelines + +### Hook Order + +Always call hooks at the top of the component in consistent order: + +1. React hooks (useState, useEffect, useRef, etc.) +2. Router hooks (useParams, useNavigate, etc.) +3. Redux hooks (useSelector, useDispatch) +4. Custom hooks + +```tsx +const MyComponent = () => { + // 1. React hooks + const [count, setCount] = useState(0); + const ref = useRef(null); + + // 2. Router hooks + const { id } = useParams(); + const navigate = useNavigate(); + + // 3. Redux hooks + const user = useSelector((state) => state.user); + const dispatch = useDispatch(); + + // 4. Custom hooks + const { data, loading } = useFiles(); + + // Rest of component... +}; +``` + +### Custom Hooks + +Custom hooks should: + +- Start with `use` prefix +- Return an object or array +- Be self-contained and reusable + +```tsx +// ✅ Good +export const useFiles = (folderId: string) => { + const [files, setFiles] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + fetchFiles(folderId).then(setFiles); + }, [folderId]); + + return { files, loading }; +}; + +// Usage +const { files, loading } = useFiles(folderId); +``` + +### Effect Dependencies + +Always include all dependencies in the dependency array. + +```tsx +// ✅ Good +useEffect(() => { + fetchFiles(folderId, userId); +}, [folderId, userId]); // All dependencies listed + +// ❌ Bad +useEffect(() => { + fetchFiles(folderId, userId); +}, []); // Missing dependencies +``` + +### Avoid Unnecessary Effects + +```tsx +// ❌ Bad (unnecessary effect) +const [count, setCount] = useState(0); +const [doubled, setDoubled] = useState(0); + +useEffect(() => { + setDoubled(count * 2); +}, [count]); + +// ✅ Good (derived state) +const [count, setCount] = useState(0); +const doubled = count * 2; +``` + +--- + +## Styling Guidelines + +### Tailwind CSS + +Use Tailwind for utility classes. + +```tsx +// ✅ Good (Tailwind utilities) +
+ Title + +
+``` + +### Avoid Inline Styles + +```tsx +// ❌ Bad +
+ Content +
+ +// ✅ Good (Tailwind) +
+ Content +
+``` + +--- + +## Naming Conventions + +### Files and Folders + +| Type | Convention | Example | +| ---------- | ----------------------------- | ------------------------------------ | +| Components | PascalCase | `FileList.tsx`, `ShareDialog.tsx` | +| Hooks | camelCase + `use` prefix | `useFiles.ts`, `useAuth.ts` | +| Services | camelCase + `.service` suffix | `file.service.ts`, `auth.service.ts` | +| Utils | camelCase | `formatDate.ts`, `fileUtils.ts` | +| Types | camelCase + `.types` suffix | `file.types.ts`, `user.types.ts` | +| Tests | Same as component + `.test` | `FileList.test.tsx` | +| Constants | camelCase or UPPER_SNAKE_CASE | `routes.ts`, `API_ENDPOINTS.ts` | + +### Variables + +```tsx +// Variables: camelCase +const fileCount = 10; +const isLoading = false; +const userName = 'John'; + +// Boolean variables: use is/has/should prefix +const isVisible = true; +const hasPermission = false; +const shouldRefresh = true; + +// Arrays: plural nouns +const files = []; +const users = []; + +// Functions: camelCase, verb prefix +const handleClick = () => {}; +const fetchFiles = async () => {}; +const validateForm = () => {}; +const isValidEmail = (email: string) => boolean; +``` + +### Functions + +Use descriptive verb prefixes: + +| Prefix | Meaning | Example | +| ---------- | -------------------- | ----------------------------------- | +| `get` | Return data | `getFiles()`, `getUserName()` | +| `set` | Set data | `setLoading()`, `setUser()` | +| `fetch` | Async data retrieval | `fetchFiles()`, `fetchUserData()` | +| `handle` | Event handlers | `handleClick()`, `handleSubmit()` | +| `validate` | Validation | `validateForm()`, `validateEmail()` | +| `is` | Boolean check | `isValid()`, `isLoading()` | +| `has` | Boolean check | `hasPermission()`, `hasAccess()` | +| `should` | Boolean check | `shouldRefresh()`, `shouldRender()` | +| `on` | Callback props | `onSubmit`, `onClick`, `onClose` | + +### Components + +```tsx +// Components: PascalCase +const FileList = () => {}; +const ShareDialog = () => {}; +const UserAvatar = () => {}; +``` + +### Interfaces and Types + +```tsx +// Interfaces: PascalCase +interface FileListProps { + files: File[]; +} + +interface UserData { + id: string; + name: string; +} + +// Types: PascalCase +type FileStatus = 'pending' | 'uploaded' | 'error'; +type ApiResponse = { + data: T; + error?: string; +}; + +// Props interface naming +interface ComponentNameProps {} // ✅ Good +interface IComponentNameProps {} // ❌ Bad (no I prefix) +``` + +### Constants + +```tsx +// App-wide constants: UPPER_SNAKE_CASE +export const MAX_FILE_SIZE = 1024 * 1024 * 100; // 100MB +export const API_BASE_URL = 'https://api.internxt.com'; +export const DEFAULT_LANGUAGE = 'en'; + +// Config objects: camelCase or PascalCase +export const API_ENDPOINTS = { + FILES: '/api/files', + FOLDERS: '/api/folders', +}; + +export const ROUTES = { + DRIVE: '/drive', + SHARED: '/shared', +}; +``` + +### Event Handlers + +```tsx +// ✅ Good +const handleClick = () => {}; +const handleSubmit = (e: FormEvent) => {}; +const handleFileSelect = (file: File) => {}; + +// Props: use 'on' prefix +interface ButtonProps { + onClick: () => void; + onSubmit: (data: FormData) => void; +} +``` + +--- + +## File Organization + +### Import Order + +Organize imports in the following order: + +```tsx +// 1. External libraries +import React, { useState, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; + +// 2. Internal aliases (app/) +import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; +import { useAppDispatch, useAppSelector } from 'app/store/hooks'; + +// 3. Shared (shared/) +import { Button } from 'shared/components/Button'; +import { useAuth } from 'shared/hooks/useAuth'; + +// 4. Relative imports (same view) +import { FileList } from './components/FileList'; +import { useFiles } from './hooks/useFiles'; +``` + +### Export Order + +```tsx +// 1. Type exports +export type { FileListProps }; +export type { FileItem }; + +// 2. Named exports +export { FileListItem }; +export { FileListHeader }; + +// 3. Default export (last) +export default FileList; +``` + +--- + +## Testing Guidelines + +### Philosophy + +Tests should focus on **behavior and outcomes** rather than implementation details. This makes tests more resilient to refactoring and easier to understand. + +For comprehensive guidelines, see [testing-guidelines.md](../testing-guidelines.md). + +### Test File Location + +Place test files next to the component: + +``` +FileList/ +├── FileList.tsx +└── FileList.test.tsx ← Here +``` + +### Test Structure Pattern + +#### 1. Main Describe Block + +Use clear, descriptive names that identify **what** is being tested, not **how** it works internally. + +```tsx +// ✅ Good - Describes what is being tested +describe('File list component', () => { ... }) +describe('User authentication service', () => { ... }) + +// ❌ Bad - Uses technical variable/function names +describe('FileList', () => { ... }) +describe('authService.login', () => { ... }) +``` + +#### 2. Nested Describe Blocks + +Use context-based groupings: + +```tsx +// ✅ Good - Describes context and scenarios +describe('File rendering', () => { ... }) +describe('When user clicks on a file', () => { ... }) +describe('File validation', () => { ... }) + +// ❌ Bad - Uses technical property/method names +describe('renderFiles method', () => { ... }) +describe('handleClick function', () => { ... }) +``` + +#### 3. Test Cases + +Use **"when X, then Y"** format to clearly express cause and effect: + +```tsx +// ✅ Good - Clear behavior with expected outcome +it('when files are provided, then all file names are displayed', () => { ... }) +it('when the file size exceeds the limit, then an error is shown', () => { ... }) +it('when the user clicks on a file, then the file details are displayed', () => { ... }) + +// ❌ Bad - Uses technical details or unclear expectations +it('should render file list correctly', () => { ... }) +it('should call onFileClick with file data', () => { ... }) +it('should set isLoading to false', () => { ... }) +``` + +### Complete Example + +```tsx +import { describe, it, expect, beforeEach } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import FileList from './FileList'; + +describe('File list component', () => { + const mockFiles = [ + { id: '1', name: 'file1.txt', size: 1024 }, + { id: '2', name: 'file2.txt', size: 2048 }, + ]; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('File rendering', () => { + it('when files are provided, then all file names are displayed', () => { + render(); + + expect(screen.getByText('file1.txt')).toBeInTheDocument(); + expect(screen.getByText('file2.txt')).toBeInTheDocument(); + }); + + it('when no files are provided, then an empty state message is shown', () => { + render(); + + expect(screen.getByText('No files')).toBeInTheDocument(); + }); + }); + + describe('File interaction', () => { + it('when a file is clicked, then the file selection handler is called with the correct file', () => { + const handleClick = vi.fn(); + render(); + + fireEvent.click(screen.getByText('file1.txt')); + + expect(handleClick).toHaveBeenCalledWith(mockFiles[0]); + }); + }); + + describe('Loading state', () => { + it('when the list is loading, then a loading indicator is displayed', () => { + render(); + + expect(screen.getByTestId('loader')).toBeInTheDocument(); + }); + }); +}); +``` + +### Key Principles + +#### ❌ Avoid + +- Variable names in descriptions (`isLoading`, `fileList`) +- Function/method names (`handleClick`, `renderFiles`) +- Technical implementation details +- Console log assertions (unless critical) +- "should" at the start of test cases + +#### ✅ Use + +- Domain language (file, user, authentication) +- Clear outcomes (is displayed, is shown, succeeds, fails) +- "when/then" format for test cases +- Plain English that non-technical people can understand +- Focus on behavior and user-facing outcomes + +--- + +## Comments and Documentation + +### JSDoc for Public APIs + +```tsx +/** + * Formats a file size in bytes to a human-readable string. + * + * @param bytes - The size in bytes + * @param decimals - Number of decimal places (default: 2) + * @returns Formatted file size (e.g., "1.5 MB") + * + * @example + * formatFileSize(1536000) // Returns "1.50 MB" + * formatFileSize(1024, 0) // Returns "1 KB" + */ +export const formatFileSize = (bytes: number, decimals: number = 2): string => { + // Implementation +}; +``` + +### Inline Comments + +```tsx +// ✅ Good (explains WHY, not WHAT) +// Delay is needed to prevent race condition with file upload +await delay(100); + +// Clear cache before fetching to ensure fresh data +cache.clear(); + +// ❌ Bad (explains WHAT, which is obvious) +// Set loading to true +setLoading(true); + +// Loop through files +files.forEach((file) => {}); +``` + +## Performance Best Practices + +### Memoization + +Use `useMemo` and `useCallback` for expensive computations: + +```tsx +// ✅ Good +const sortedFiles = useMemo(() => { + return files.sort((a, b) => a.name.localeCompare(b.name)); +}, [files]); + +const handleClick = useCallback(() => { + onFileClick(file); +}, [file, onFileClick]); +``` + +### React.memo for Pure Components + +```tsx +// ✅ Good +const FileItem = React.memo(({ file, onSelect }) => { + return
{file.name}
; +}); +``` + +### Lazy Loading + +```tsx +// ✅ Good +const FilePreview = lazy(() => import('./components/FilePreview')); + +}> + +; +``` + +--- + +## Accessibility + +### Semantic HTML + +```tsx +// ✅ Good + + + +// ❌ Bad +
Submit
+
+ navigate('/drive')}>Drive +
+``` + +### ARIA Labels + +```tsx +// ✅ Good + + + +``` + +### Keyboard Navigation + +```tsx +// ✅ Good +
e.key === 'Enter' && handleClick()}> + Click me +
+``` + +--- + +## Questions? + +For more information, see: + +- [ARCHITECTURE.md](./ARCHITECTURE.md) - Project architecture +- [README.md](../README.md) - Project overview diff --git a/package.json b/package.json index c39aa39616..734732179a 100644 --- a/package.json +++ b/package.json @@ -85,10 +85,12 @@ "preinstall": "node scripts/use-yarn.js", "prepare": "husky install", "dev": "vite", - "build": "vite build", + "build": "yarn typecheck && vite build", + "build:only": "vite build", "start": "vite preview", - "build:staging": "vite build --mode staging", + "build:staging": "yarn typecheck && vite build --mode staging", "vercel:install": "yarn run add:npmrc && yarn install", + "typecheck": "tsc --noEmit", "test:coverage": "vitest run --coverage", "test:playwright": "yarn playwright test", "test:chromium": "yarn playwright test --project=chromium", @@ -170,9 +172,6 @@ "vitest": "^2.1.9", "webpack-bundle-analyzer": "^4.9.1" }, - "engines": { - "node": ">=14.17.0" - }, "lint-staged": { "*.{js,jsx,tsx,ts}": [ "prettier --write" diff --git a/testing-guidelines.md b/testing-guidelines.md new file mode 100644 index 0000000000..f8f4db5ef2 --- /dev/null +++ b/testing-guidelines.md @@ -0,0 +1,255 @@ +# Testing Guidelines + +## Philosophy + +Tests should focus on **behavior and outcomes** rather than implementation details. This makes tests more resilient to refactoring and easier to understand for anyone reading the codebase. + +## Test Structure Pattern + +### 1. Main Describe Block + +Use clear, descriptive names that identify **what** is being tested, not **how** it works internally. + +```typescript +// ✅ Good - Describes what is being tested +describe('OAuth custom hook', () => { ... }) +describe('User authentication service', () => { ... }) +describe('File upload component', () => { ... }) + +// ❌ Bad - Uses technical variable/function names +describe('useOAuthFlow', () => { ... }) +describe('authService.login', () => { ... }) +describe('FileUpload', () => { ... }) +``` + +### 2. Nested Describe Blocks + +Use context-based groupings with **Given/When** format: + +```typescript +// ✅ Good - Describes context and scenarios +describe('Auth origin', () => { ... }) +describe('On component mount', () => { ... }) +describe('Handling successful OAuth', () => { ... }) +describe('When user is authenticated', () => { ... }) +describe('File validation', () => { ... }) + +// ❌ Bad - Uses technical property/method names +describe('isOAuthFlow property', () => { ... }) +describe('handleOAuthSuccess method', () => { ... }) +describe('validateFile function', () => { ... }) +``` + +### 3. Test Cases (it blocks) + +Use **"when X, then Y"** format to clearly express cause and effect: + +```typescript +// ✅ Good - Clear behavior with expected outcome +it('when an auth origin is provided, then the OAuth process is activated', () => { ... }) +it('when the file size exceeds the limit, then an error is shown', () => { ... }) +it('when the form is submitted with valid data, then the user is created successfully', () => { ... }) + +// ❌ Bad - Uses technical details or unclear expectations +it('should set isOAuthFlow to true when authOrigin is provided', () => { ... }) +it('should return false if file.size > MAX_SIZE', () => { ... }) +it('should call createUser with formData', () => { ... }) +``` + +## Complete Example + +```typescript +describe('OAuth custom hook', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('Auth origin', () => { + it('when an auth origin is provided, then the OAuth process is activated', () => { + const { result } = renderHook(() => useOAuthFlow({ authOrigin: 'https://meet.internxt.com' })); + + expect(result.current.isOAuthFlow).toBe(true); + }); + + it('when an auth origin is not provided, then the OAuth process remains inactive', () => { + const { result } = renderHook(() => useOAuthFlow({ authOrigin: null })); + + expect(result.current.isOAuthFlow).toBe(false); + }); + }); + + describe('On component mount', () => { + it('when OAuth is active and user credentials exist, then credentials are automatically sent to the parent window', () => { + vi.spyOn(localStorageService, 'getUser').mockReturnValue(mockUserSettings); + vi.spyOn(localStorageService, 'get').mockReturnValue('test-token'); + mockSendAuthSuccess.mockReturnValue(true); + + renderHook(() => useOAuthFlow({ authOrigin: 'https://meet.internxt.com' })); + + expect(mockSendAuthSuccess).toHaveBeenCalledWith(mockUserSettings, 'test-token'); + }); + + it('when OAuth is not active, then no credentials are sent', () => { + renderHook(() => useOAuthFlow({ authOrigin: null })); + + expect(mockSendAuthSuccess).not.toHaveBeenCalled(); + }); + }); + + describe('Handling successful OAuth', () => { + it('when the OAuth process completes successfully, then credentials are sent and success is reported', () => { + mockSendAuthSuccess.mockReturnValue(true); + + const { result } = renderHook(() => useOAuthFlow({ authOrigin: 'https://meet.internxt.com' })); + const returnValue = result.current.handleOAuthSuccess(mockUserSettings, 'token'); + + expect(returnValue).toBe(true); + }); + + it('when the OAuth process fails or is not completed, then failure is reported', () => { + mockSendAuthSuccess.mockReturnValue(false); + + const { result } = renderHook(() => useOAuthFlow({ authOrigin: 'https://meet.internxt.com' })); + const returnValue = result.current.handleOAuthSuccess(mockUserSettings, 'token'); + + expect(returnValue).toBe(false); + }); + }); +}); +``` + +## Service/Function Testing + +For services and utility functions, follow the same pattern: + +```typescript +describe('Authentication service', () => { + describe('User login', () => { + it('when credentials are valid, then the user is authenticated and a token is returned', () => { + const result = authService.login('user@example.com', 'password123'); + + expect(result.success).toBe(true); + expect(result.token).toBeDefined(); + }); + + it('when credentials are invalid, then authentication fails with an error message', () => { + const result = authService.login('user@example.com', 'wrongpassword'); + + expect(result.success).toBe(false); + expect(result.error).toBe('Invalid credentials'); + }); + }); + + describe('Password validation', () => { + it('when password meets all requirements, then validation passes', () => { + const isValid = authService.validatePassword('SecurePass123!'); + + expect(isValid).toBe(true); + }); + + it('when password is too short, then validation fails', () => { + const isValid = authService.validatePassword('short'); + + expect(isValid).toBe(false); + }); + }); +}); +``` + +## Key Principles + +### 1. Avoid Technical Implementation Details + +❌ **Bad:** +- Variable names: `isOAuthFlow`, `mockSendAuthSuccess`, `authOrigin` +- Function names: `handleOAuthSuccess`, `sendAuthSuccess` +- Property names: `result.current.isOAuthFlow` + +✅ **Good:** +- Behaviors: "OAuth process is activated", "credentials are sent" +- Outcomes: "success is reported", "error is shown" +- States: "user is authenticated", "form is valid" + +### 2. Use Domain Language + +Write tests in language that non-technical stakeholders could understand. + +❌ **Bad:** +```typescript +it('should call postMessage with credentials and close window', () => { ... }) +``` + +✅ **Good:** +```typescript +it('when authentication succeeds, then user credentials are transmitted to the parent application', () => { ... }) +``` + +### 3. Group by Behavior, Not by Code Structure + +❌ **Bad:** +```typescript +describe('sendAuthSuccess method', () => { + describe('when window.opener exists', () => { ... }) + describe('when window.opener is null', () => { ... }) +}) +``` + +✅ **Good:** +```typescript +describe('Successful authentication transmission', () => { + it('when the parent window is available, then credentials are transmitted successfully', () => { ... }) + it('when there is no parent window, then transmission fails gracefully', () => { ... }) +}) +``` + +### 4. Test Outcomes, Not Implementations + +❌ **Bad:** +```typescript +it('should call localStorage.setItem with user data', () => { + service.saveUser(userData); + expect(localStorage.setItem).toHaveBeenCalledWith('user', JSON.stringify(userData)); +}) +``` + +✅ **Good:** +```typescript +it('when user data is saved, then it persists across sessions', () => { + service.saveUser(userData); + const retrieved = service.getUser(); + expect(retrieved).toEqual(userData); +}) +``` + +### 5. Avoid Console Logging Assertions + +❌ **Bad:** +```typescript +it('should log warning when origin is invalid', () => { + expect(consoleWarnSpy).toHaveBeenCalledWith('[OAuth] Invalid origin'); +}) +``` + +✅ **Good:** +Focus on behavior that affects the user or system state, not internal logging. + +## Benefits + +1. **Resilient to Refactoring**: If you rename variables or functions, test descriptions remain accurate +2. **Self-Documenting**: Tests serve as behavior documentation +3. **Easier to Understand**: New team members can understand what the code does without knowing implementation +4. **Better Coverage**: Thinking in behaviors helps identify edge cases +5. **Maintainable**: When implementation changes, tests remain relevant if behavior is preserved + +## Checklist + +Before committing tests, verify: + +- [ ] Main describe block describes **what** is being tested, not implementation details +- [ ] Nested describe blocks use context-based grouping (Auth origin, On mount, etc.) +- [ ] Test cases use "when X, then Y" format +- [ ] No variable names, function names, or property names in descriptions +- [ ] Tests read like plain English sentences +- [ ] Someone unfamiliar with the code could understand what's being tested +- [ ] Tests focus on outcomes and behaviors, not internal implementation +- [ ] No assertions on console logs unless absolutely necessary From 7d08af803749386049ba40f69604fb075eb81f65 Mon Sep 17 00:00:00 2001 From: Ramon Candel Segura Date: Thu, 20 Nov 2025 10:32:55 +0100 Subject: [PATCH 4/4] Improved type handling in download and user services and fixed tests --- .../services/download.service/downloadFile.ts | 20 +++-- src/app/network/download.test.ts | 76 ++++++++++++++++--- src/app/network/download.ts | 46 +++++------ src/app/store/slices/user/index.ts | 9 ++- src/app/store/slices/user/userStore.test.ts | 7 +- src/views/Backups/store/backupsSlice.test.ts | 2 +- src/views/Drive/DriveView.tsx | 20 ++--- .../services/moveItemsToTrash.service.test.ts | 2 +- .../services/moveItemsToTrash.service.ts | 4 +- 9 files changed, 125 insertions(+), 61 deletions(-) diff --git a/src/app/drive/services/download.service/downloadFile.ts b/src/app/drive/services/download.service/downloadFile.ts index 396ed4151c..fda374608b 100644 --- a/src/app/drive/services/download.service/downloadFile.ts +++ b/src/app/drive/services/download.service/downloadFile.ts @@ -1,11 +1,11 @@ -import streamSaver from 'streamsaver'; -import { isFirefox } from 'react-device-detect'; +import { ErrorMessages } from 'app/core/constants'; import { ConnectionLostError } from 'app/network/requests'; +import { isFirefox } from 'react-device-detect'; +import streamSaver from 'streamsaver'; import { DriveFileData } from '../../types'; +import { BlobWritable, downloadFileAsBlob } from './downloadFileAsBlob'; import fetchFileStream from './fetchFileStream'; import fetchFileStreamUsingCredentials from './fetchFileStreamUsingCredentials'; -import { ErrorMessages } from 'app/core/constants'; -import { BlobWritable, downloadFileAsBlob } from './downloadFileAsBlob'; async function pipe(readable: ReadableStream, writable: BlobWritable): Promise { const reader = readable.getReader(); @@ -117,16 +117,20 @@ async function downloadToFs( } switch (supports) { - case DownloadSupport.StreamApi: - // eslint-disable-next-line no-case-declarations + case DownloadSupport.StreamApi: { const fsHandle = await window.showSaveFilePicker({ suggestedName: filename }).catch((_) => { abortController?.abort(); throw new Error(ErrorMessages.FilePickerCancelled); }); - // eslint-disable-next-line no-case-declarations - const destination = await fsHandle.createWritable({ keepExistingData: false }); + // TypeScript's FileSystemFileHandle definition is incomplete, so we assert the createWritable method exists + const destination = await ( + fsHandle as FileSystemFileHandle & { + createWritable(options?: { keepExistingData?: boolean }): Promise; + } + ).createWritable({ keepExistingData: false }); return downloadFileUsingStreamApi(await source, destination, abortController); + } case DownloadSupport.PartialStreamApi: return downloadFileUsingStreamApi(await source, streamSaver.createWriteStream(filename), abortController); diff --git a/src/app/network/download.test.ts b/src/app/network/download.test.ts index 1ea131bab8..4baf418c1d 100644 --- a/src/app/network/download.test.ts +++ b/src/app/network/download.test.ts @@ -1,4 +1,4 @@ -import { describe, test, expect, vi, beforeEach } from 'vitest'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; vi.mock('./download/v2'); vi.mock('./requests'); @@ -6,7 +6,7 @@ vi.mock('./crypto'); vi.mock('app/core/services/stream.service'); vi.mock('app/core/services/env.service'); -import { multipartDownloadFile, IDownloadParams } from './download'; +import { IDownloadParams, multipartDownloadFile } from './download'; import { DownloadFailedWithUnknownError } from './errors/download.errors'; describe('Multipart Download File', () => { @@ -14,13 +14,13 @@ describe('Multipart Download File', () => { vi.clearAllMocks(); }); - test('When multipart download is called, then it is called with correct params', async () => { + test('When multipart download is called with own file params, then it is called with correct params', async () => { const mockStream = new ReadableStream(); - const params: IDownloadParams & { fileSize?: number } = { + const params: IDownloadParams & { fileSize: number } = { bucketId: 'test-bucket', fileId: 'test-file', - token: 'test-token', - encryptionKey: Buffer.from('0'.repeat(64), 'hex'), + creds: { user: 'test-user', pass: 'test-pass' }, + mnemonic: 'test-mnemonic', fileSize: 1024, options: { notifyProgress: vi.fn(), @@ -33,12 +33,19 @@ describe('Multipart Download File', () => { const result = await multipartDownloadFile(params); - expect(multipartDownload).toHaveBeenCalledWith(params); + expect(multipartDownload).toHaveBeenCalledWith({ + bucketId: params.bucketId, + fileId: params.fileId, + creds: params.creds, + mnemonic: params.mnemonic, + fileSize: params.fileSize, + options: params.options, + }); expect(result).toStrictEqual(mockStream); }); - test('When there is an unknown error, then an error indicating so is thrown', async () => { - const unknownError = new DownloadFailedWithUnknownError(500); + test('When multipart download is called with shared file params, then it falls back to regular download', async () => { + const mockStream = new ReadableStream(); const params: IDownloadParams & { fileSize?: number } = { bucketId: 'test-bucket', fileId: 'test-file', @@ -51,10 +58,59 @@ describe('Multipart Download File', () => { }, }; + const { getFileInfoWithToken, getMirrors } = await import('./requests'); + vi.mocked(getFileInfoWithToken).mockResolvedValue({ + bucket: 'test-bucket', + mimetype: 'text/plain', + filename: 'test-file', + frame: 'test-frame', + size: 1024, + id: 'test-id', + created: new Date(), + hmac: { value: 'test-hmac', type: 'sha256' }, + index: '0'.repeat(64), + } as any); + vi.mocked(getMirrors).mockResolvedValue([{ url: 'http://test-mirror.com' }] as any); + + const { buildProgressStream } = await import('app/core/services/stream.service'); + vi.mocked(buildProgressStream).mockReturnValue(mockStream); + + global.fetch = vi.fn().mockResolvedValue({ + body: new ReadableStream(), + }); + + const result = await multipartDownloadFile(params); + + expect(result).toBeDefined(); + expect(result).toStrictEqual(mockStream); + expect(getFileInfoWithToken).toHaveBeenCalledWith(params.bucketId, params.fileId, params.token); + }); + + test('When there is an unknown error, then an error indicating so is thrown', async () => { + const unknownError = new DownloadFailedWithUnknownError(500); + const params: IDownloadParams & { fileSize: number } = { + bucketId: 'test-bucket', + fileId: 'test-file', + creds: { user: 'test-user', pass: 'test-pass' }, + mnemonic: 'test-mnemonic', + fileSize: 1024, + options: { + notifyProgress: vi.fn(), + abortController: new AbortController(), + }, + }; + const { multipartDownload } = await import('./download/v2'); vi.mocked(multipartDownload).mockRejectedValue(unknownError); await expect(multipartDownloadFile(params)).rejects.toThrow(unknownError); - expect(multipartDownload).toHaveBeenCalledWith(params); + expect(multipartDownload).toHaveBeenCalledWith({ + bucketId: params.bucketId, + fileId: params.fileId, + creds: params.creds, + mnemonic: params.mnemonic, + fileSize: params.fileSize, + options: params.options, + }); }); }); diff --git a/src/app/network/download.ts b/src/app/network/download.ts index adce56e357..baf8df21ed 100644 --- a/src/app/network/download.ts +++ b/src/app/network/download.ts @@ -13,27 +13,6 @@ import downloadFileV2, { multipartDownload } from './download/v2'; export type DownloadProgressCallback = (totalBytes: number, downloadedBytes: number) => void; export type Downloadable = { fileId: string; bucketId: string }; -type BinaryStream = ReadableStream; - -export async function binaryStreamToBlob(stream: BinaryStream): Promise { - const reader = stream.getReader(); - const slices: Uint8Array[] = []; - - let finish = false; - - while (!finish) { - const { done, value } = await reader.read(); - - if (!done) { - slices.push(value as Uint8Array); - } - - finish = done; - } - - return new Blob(slices); -} - interface FileInfo { bucket: string; mimetype: string; @@ -169,7 +148,30 @@ export function downloadFile(params: IDownloadParams): Promise> { - const downloadMultipartPromise = multipartDownload(params); + const { bucketId, fileId, creds, mnemonic, token, encryptionKey, options, fileSize } = params; + + // Only try multipart download if we have credentials and mnemonic (not for shared files with tokens) + if (token || encryptionKey) { + // For shared files, fall back to regular download + return _downloadFile(params); + } + + if (!creds || !mnemonic) { + throw new Error('Download error: credentials and mnemonic required for multipart download'); + } + + if (!fileSize) { + throw new Error('Download error: fileSize required for multipart download'); + } + + const downloadMultipartPromise = multipartDownload({ + bucketId, + fileId, + creds, + mnemonic, + fileSize, + options, + }); return downloadMultipartPromise.catch((err) => { if (err instanceof FileVersionOneError) { diff --git a/src/app/store/slices/user/index.ts b/src/app/store/slices/user/index.ts index fb046d64fd..4a6df0fe6c 100644 --- a/src/app/store/slices/user/index.ts +++ b/src/app/store/slices/user/index.ts @@ -3,27 +3,28 @@ import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; import dayjs from 'dayjs'; import { RootState } from '../..'; +import { saveAvatarToDatabase } from '../../../../views/NewSettings/components/Sections/Account/Account/components/AvatarWrapper'; import authService from '../../../auth/services/auth.service'; import userService from '../../../auth/services/user.service'; import localStorageService from '../../../core/services/local-storage.service'; import navigationService from '../../../core/services/navigation.service'; import { AppView, LocalStorageItem } from '../../../core/types'; import { deleteDatabaseProfileAvatar } from '../../../drive/services/database.service'; -import { saveAvatarToDatabase } from '../../../../views/NewSettings/components/Sections/Account/Account/components/AvatarWrapper'; import notificationsService, { ToastType } from '../../../notifications/services/notifications.service'; +import { workspacesActions } from '../../../store/slices/workspaces/workspacesStore'; import tasksService from '../../../tasks/services/tasks.service'; import { referralsActions } from '../referrals'; import { sessionActions } from '../session'; import { sessionSelectors } from '../session/session.selectors'; import { storageActions } from '../storage'; import { uiActions } from '../ui'; -import { workspacesActions } from '../../../store/slices/workspaces/workspacesStore'; import errorService from '../../../core/services/error.service'; -import { isTokenExpired } from '../../utils'; import { refreshAvatar } from '../../../utils/avatar/avatarUtils'; -import { ProductService, UserTierFeatures } from 'views/Checkout/services'; +import { isTokenExpired } from '../../utils'; + import { t } from 'i18next'; +import { ProductService, UserTierFeatures } from '../../../../views/Checkout/services/products.service'; export interface UserState { isInitializing: boolean; diff --git a/src/app/store/slices/user/userStore.test.ts b/src/app/store/slices/user/userStore.test.ts index 260ef2d218..ce994f26d5 100644 --- a/src/app/store/slices/user/userStore.test.ts +++ b/src/app/store/slices/user/userStore.test.ts @@ -1,8 +1,9 @@ -import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import { configureStore } from '@reduxjs/toolkit'; -import userReducer, { getUserTierFeaturesThunk } from './index'; -import { ProductService, UserTierFeatures } from 'views/Checkout/services'; +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; +import { ProductService } from '../../../../views/Checkout/services'; +import { UserTierFeatures } from '../../../../views/Checkout/services/products.service'; import notificationsService, { ToastType } from '../../../notifications/services/notifications.service'; +import userReducer, { getUserTierFeaturesThunk } from './index'; const MOCK_TRANSLATION_MESSAGE = 'Some features may be unavailable'; diff --git a/src/views/Backups/store/backupsSlice.test.ts b/src/views/Backups/store/backupsSlice.test.ts index a689868330..3caafee353 100644 --- a/src/views/Backups/store/backupsSlice.test.ts +++ b/src/views/Backups/store/backupsSlice.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import backupsReducer, { backupsActions, backupsThunks } from './backupsSlice'; import backupsService from '../services/backups.service'; import { Device } from '@internxt/sdk/dist/drive/backups/types'; -import { DriveFolderData } from '@internxt/sdk/dist/drive/storage/types'; +import { DriveFolderData } from 'app/drive/types'; vi.mock('../services/backups.service', () => ({ default: { diff --git a/src/views/Drive/DriveView.tsx b/src/views/Drive/DriveView.tsx index 1be0787154..563f6a4830 100644 --- a/src/views/Drive/DriveView.tsx +++ b/src/views/Drive/DriveView.tsx @@ -1,28 +1,28 @@ import { useEffect, useState } from 'react'; import { connect, useSelector } from 'react-redux'; +import envService from 'app/core/services/env.service'; import errorService from 'app/core/services/error.service'; +import localStorageService from 'app/core/services/local-storage.service'; import navigationService from 'app/core/services/navigation.service'; +import { STORAGE_KEYS } from 'app/core/services/storage-keys'; +import workspacesService from 'app/core/services/workspace.service'; import { AppView } from 'app/core/types'; import fileService from 'app/drive/services/file.service'; import newStorageService from 'app/drive/services/new-storage.service'; +import { DriveItemData, FolderPath } from 'app/drive/types'; +import useDriveNavigation from 'app/routes/hooks/Drive/useDrive'; import BreadcrumbsDriveView from 'app/shared/components/Breadcrumbs/Containers/BreadcrumbsDriveView'; import { AppDispatch, RootState } from 'app/store'; +import { useAppSelector } from 'app/store/hooks'; import { storageActions, storageSelectors } from 'app/store/slices/storage'; import storageThunks from 'app/store/slices/storage/storage.thunks'; import { uiActions } from 'app/store/slices/ui'; -import { Helmet } from 'react-helmet-async'; -import useDriveNavigation from 'app/routes/hooks/Drive/useDrive'; -import { useAppSelector } from 'app/store/hooks'; import workspacesSelectors from 'app/store/slices/workspaces/workspaces.selectors'; -import DriveExplorer from 'views/Drive/components/DriveExplorer/DriveExplorer'; -import { DriveItemData, FolderPath } from 'app/drive/types'; import { workspacesActions, workspaceThunks } from 'app/store/slices/workspaces/workspacesStore'; -import localStorageService from 'app/core/services/local-storage.service'; -import { STORAGE_KEYS } from 'app/core/services/storage-keys'; -import workspacesService from 'app/core/services/workspace.service'; +import { Helmet } from 'react-helmet-async'; import { useHistory } from 'react-router-dom'; -import envService from 'app/core/services/env.service'; +import DriveExplorer from 'views/Drive/components/DriveExplorer/DriveExplorer'; export interface DriveViewProps { namePath: FolderPath[]; @@ -193,7 +193,7 @@ const DriveView = (props: DriveViewProps) => { }; const sortFoldersFirst = (items: DriveItemData[]) => - items.toSorted((a, b) => Number(b?.isFolder ?? false) - Number(a?.isFolder ?? false)); + items.slice().sort((a, b) => Number(b?.isFolder ?? false) - Number(a?.isFolder ?? false)); export default connect((state: RootState) => { const currentFolderId = storageSelectors.currentFolderId(state); diff --git a/src/views/Trash/services/moveItemsToTrash.service.test.ts b/src/views/Trash/services/moveItemsToTrash.service.test.ts index 03e020b690..7b323a8a74 100644 --- a/src/views/Trash/services/moveItemsToTrash.service.test.ts +++ b/src/views/Trash/services/moveItemsToTrash.service.test.ts @@ -186,7 +186,7 @@ describe('moveItemsToTrash', () => { }); expect(errorService.reportError).toHaveBeenCalledWith(mockError, { extra: { - items: [{ uuid: 'file-1', type: 'file', id: null }], + items: [{ uuid: 'file-1', type: 'file', id: undefined }], }, }); }); diff --git a/src/views/Trash/services/moveItemsToTrash.service.ts b/src/views/Trash/services/moveItemsToTrash.service.ts index 244359f418..0115d21459 100644 --- a/src/views/Trash/services/moveItemsToTrash.service.ts +++ b/src/views/Trash/services/moveItemsToTrash.service.ts @@ -15,11 +15,11 @@ const MAX_CONCURRENT_REQUESTS = 3; const isFolder = (item: DriveItemData) => item?.type === 'folder' || item?.isFolder; const moveItemsToTrash = async (itemsToTrash: DriveItemData[], onSuccess?: () => void): Promise => { - const items: Array<{ uuid: string; type: 'file' | 'folder'; id: null }> = itemsToTrash.map((item) => { + const items: Array<{ uuid: string; type: 'file' | 'folder'; id?: string }> = itemsToTrash.map((item) => { return { uuid: item.uuid, type: isFolder(item) ? 'folder' : 'file', - id: null, + id: undefined, }; }); let movingItemsToastId;