From 16869a333c3c995a5e302038e795c22d065a9617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Mon, 24 Feb 2025 10:35:28 +0100 Subject: [PATCH] Better cursor rules (#10431) Move to the new cursor rule folder style and make it more granular --- .cursor/rules/README.md | 65 +++++ .cursor/rules/architecture.md | 97 +++++++ .cursor/rules/code-style-guidelines.md | 259 ++++++++++++++++++ .cursor/rules/file-structure-guidelines.md | 207 ++++++++++++++ .cursor/rules/react-general-guidelines.md | 220 +++++++++++++++ .../react-state-management-guidelines.md | 219 +++++++++++++++ .cursor/rules/testing-guidelines.md | 253 +++++++++++++++++ .cursor/rules/translations.md | 162 +++++++++++ .cursor/rules/typescript-guidelines.md | 172 ++++++++++++ .cursorrules | 99 ------- 10 files changed, 1654 insertions(+), 99 deletions(-) create mode 100644 .cursor/rules/README.md create mode 100644 .cursor/rules/architecture.md create mode 100644 .cursor/rules/code-style-guidelines.md create mode 100644 .cursor/rules/file-structure-guidelines.md create mode 100644 .cursor/rules/react-general-guidelines.md create mode 100644 .cursor/rules/react-state-management-guidelines.md create mode 100644 .cursor/rules/testing-guidelines.md create mode 100644 .cursor/rules/translations.md create mode 100644 .cursor/rules/typescript-guidelines.md delete mode 100644 .cursorrules diff --git a/.cursor/rules/README.md b/.cursor/rules/README.md new file mode 100644 index 000000000..c861f3d6e --- /dev/null +++ b/.cursor/rules/README.md @@ -0,0 +1,65 @@ +# Twenty Development Rules + +This directory contains Twenty's development guidelines and best practices. The rules are organized into several key categories: + +## Guidelines Structure + +### 1. Architecture and Structure +- `architecture.md`: Project overview, technology stack, and infrastructure setup +- `file-structure-guidelines.md`: File and directory organization patterns + +### 2. Code Style and Development +- `typescript-guidelines.md`: TypeScript best practices and conventions +- `code-style-guidelines.md`: General coding standards and style guide + +### 3. React Development +- `react-general-guidelines.md`: Core React development principles and patterns +- `react-state-management-guidelines.md`: State management approaches and best practices + +### 4. Testing +- `testing-guidelines.md`: Testing strategies, patterns, and best practices + +### 5. Internationalization +- `translations.md`: Translation workflow, i18n setup, and string management + +## Common Development Commands + +### Frontend Commands +```bash +# Testing +npx nx test twenty-front # Run unit tests +npx nx storybook:build twenty-front # Build Storybook +npx nx storybook:serve-and-test:static # Run Storybook tests + +# Development +npx nx lint twenty-front # Run linter +npx nx typecheck twenty-front # Type checking +npx nx run twenty-front:graphql:generate # Generate GraphQL types +``` + +### Backend Commands +```bash +# Database +npx nx database:reset twenty-server # Reset database +npx nx run twenty-server:database:init:prod # Initialize database +npx nx run twenty-server:database:migrate:prod # Run migrations + +# Development +npx nx run twenty-server:start # Start the server +npx nx run twenty-server:lint # Run linter (add --fix to auto-fix) +npx nx run twenty-server:typecheck # Type checking +npx nx run twenty-server:test # Run unit tests +npx nx run twenty-server:test:integration:with-db-reset # Run integration tests + +# Migrations +npx nx run twenty-server:typeorm migration:generate src/database/typeorm/metadata/migrations/[name] -d src/database/typeorm/metadata/metadata.datasource.ts + +# Workspace +npx nx run twenty-server:command workspace:sync-metadata -f # Sync metadata +``` + +## Usage + +These rules are automatically attached to relevant files in your workspace through Cursor's context system. They help maintain consistency and quality across the Twenty codebase. + +For the most up-to-date version of these guidelines, always refer to the files in this directory. \ No newline at end of file diff --git a/.cursor/rules/architecture.md b/.cursor/rules/architecture.md new file mode 100644 index 000000000..4ed78dd4d --- /dev/null +++ b/.cursor/rules/architecture.md @@ -0,0 +1,97 @@ +# Twenty Project Architecture + +## Overview +Twenty is an open-source CRM built with modern technologies, using TypeScript for both frontend and backend development. This document outlines the core architectural decisions and structure of the project. + +## Monorepo Structure +The project is organized as a monorepo using nx, with the following main packages: + +### Main Packages +- `packages/twenty-front`: Main Frontend application + - Technology: React + - Purpose: Provides the main user interface for the CRM + - Key responsibilities: User interactions, state management, data display + +- `packages/twenty-server`: Main Backend application + - Technology: NestJS + - Purpose: Handles business logic, data persistence, and API + - Key responsibilities: Data processing, authentication, API endpoints + +- `packages/twenty-website`: Marketing Website and Documentation + - Technology: NextJS + - Purpose: Public-facing website and documentation + - Key responsibilities: Marketing content, documentation, SEO + +- `packages/twenty-ui`: UI Component Library + - Technology: React + - Purpose: Shared UI components and design system + - Key responsibilities: Reusable components, design consistency + +- `packages/twenty-shared`: Shared Utilities + - Purpose: Cross-package shared code between frontend and backend + - Contents: Utils, constants, types, interfaces + +## Core Technology Stack + +### Package Management +- Package Manager: yarn +- Monorepo Tool: nx +- Benefits: Consistent dependency management, shared configurations + +### Database Layer +- Primary Database: PostgreSQL +- Schema Structure: + - Core schema: Main application data + - Metadata schema: Configuration and customization data + - Workspace schemas: One schema per tenant, containing tenant-specific data +- ORM Layer: + - TypeORM: For core and metadata schemas + - Purpose: Type-safe database operations for system data + - Benefits: Strong typing, migration support + - TwentyORM: For workspace schemas + - Purpose: Manages tenant-specific entities and customizations + - Benefits: Dynamic entity management, per-tenant customization + - Example: Entities like CompanyWorkspaceEntity are managed per workspace + +### State Management +- Frontend State: Recoil + - Purpose: Global state management + - Use cases: User preferences, UI state, cached data + +### Data Layer +- API Technology: GraphQL +- Client: Apollo Client + - Purpose: Data fetching and caching + - Benefits: Type safety, efficient data loading + +### Infrastructure +- Cache: Redis + - Purpose: High-performance caching layer + - Use cases: Session data, frequent queries + +- Authentication: JWT + - Purpose: Secure user authentication + - Implementation: Token-based auth flow + +- Queue System: BullMQ + - Purpose: Background job processing + - Use cases: Emails, exports, imports + +- Storage: S3/Local Filesystem + - Purpose: File storage and management + - Flexibility: Configurable for cloud or local storage + +### Testing Infrastructure +- Backend Testing: + - Framework: Jest + - API Testing: Supertest + - Coverage: Unit tests, integration tests + +- Frontend Testing: + - Framework: Jest + - Component Testing: Storybook + - API Mocking: MSW (Mock Service Worker) + +- End-to-End Testing: + - Framework: Playwright + - Coverage: Critical user journeys \ No newline at end of file diff --git a/.cursor/rules/code-style-guidelines.md b/.cursor/rules/code-style-guidelines.md new file mode 100644 index 000000000..b8fe184a2 --- /dev/null +++ b/.cursor/rules/code-style-guidelines.md @@ -0,0 +1,259 @@ +# Code Style Guidelines + +## Core Code Style Principles +Twenty emphasizes clean, readable, and maintainable code. This document outlines our code style conventions and best practices. + +## Control Flow + +### Early Returns +- Use early returns to reduce nesting +- Handle edge cases first + ```typescript + // ✅ Correct + const processUser = (user: User | null) => { + if (!user) return null; + if (!user.isActive) return null; + + return { + id: user.id, + name: user.name, + }; + }; + + // ❌ Incorrect + const processUser = (user: User | null) => { + if (user) { + if (user.isActive) { + return { + id: user.id, + name: user.name, + }; + } + } + return null; + }; + ``` + +### No Nested Ternaries +- Avoid nested ternary operators +- Use if statements or early returns + ```typescript + // ✅ Correct + const getUserDisplay = (user: User) => { + if (!user.name) return 'Anonymous'; + if (!user.isActive) return 'Inactive User'; + return user.name; + }; + + // ❌ Incorrect + const getUserDisplay = (user: User) => + user.name + ? user.isActive + ? user.name + : 'Inactive User' + : 'Anonymous'; + ``` + +### No Else-If Chains +- Use switch statements or lookup objects +- Keep conditions flat + ```typescript + // ✅ Correct + const getStatusColor = (status: Status): string => { + switch (status) { + case 'success': + return 'green'; + case 'warning': + return 'yellow'; + case 'error': + return 'red'; + default: + return 'gray'; + } + }; + + // Or using a lookup object + const statusColors: Record = { + success: 'green', + warning: 'yellow', + error: 'red', + default: 'gray', + }; + + // ❌ Incorrect + const getStatusColor = (status: Status): string => { + if (status === 'success') { + return 'green'; + } else if (status === 'warning') { + return 'yellow'; + } else if (status === 'error') { + return 'red'; + } else { + return 'gray'; + } + }; + ``` + +## Operators and Expressions + +### Optional Chaining Over && +- Use optional chaining for null/undefined checks +- Clearer intent and better type safety + ```typescript + // ✅ Correct + const userName = user?.name; + const userAddress = user?.address?.street; + + // ❌ Incorrect + const userName = user && user.name; + const userAddress = user && user.address && user.address.street; + ``` + +## Function Design + +### Small Focused Functions +- Keep functions small and single-purpose +- Extract complex logic into helper functions + ```typescript + // ✅ Correct + const validateUser = (user: User) => { + if (!isValidName(user.name)) return false; + if (!isValidEmail(user.email)) return false; + if (!isValidAge(user.age)) return false; + return true; + }; + + const isValidName = (name: string) => { + return name.length >= 2 && /^[a-zA-Z\s]*$/.test(name); + }; + + const isValidEmail = (email: string) => { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); + }; + + const isValidAge = (age: number) => { + return age >= 18 && age <= 120; + }; + + // ❌ Incorrect + const validateUser = (user: User) => { + if (user.name.length < 2 || !/^[a-zA-Z\s]*$/.test(user.name)) return false; + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(user.email)) return false; + if (user.age < 18 || user.age > 120) return false; + return true; + }; + ``` + +## Naming and Documentation + +### Clear Variable Names +- Use descriptive, intention-revealing names +- Avoid abbreviations unless common + ```typescript + // ✅ Correct + const isUserActive = user.status === 'active'; + const hasRequiredPermissions = user.permissions.includes('admin'); + const userDisplayName = user.name || 'Anonymous'; + + // ❌ Incorrect + const active = user.status === 'active'; + const hasPerm = user.permissions.includes('admin'); + const udn = user.name || 'Anonymous'; + ``` + +### No Console.logs in Commits +- Remove all console.logs before committing +- Use proper logging/error tracking in production + ```typescript + // ❌ Incorrect - Don't commit these + console.log('user:', user); + console.log('debug:', someValue); + + // ✅ Correct - Use proper logging + logger.info('User action completed', { userId: user.id }); + logger.error('Operation failed', { error }); + ``` + +### Minimal Comments +- Write self-documenting code +- Use comments only for complex business logic + ```typescript + // ✅ Correct + // Calculate pro-rated amount based on billing cycle + const calculateProRatedAmount = (amount: number, daysLeft: number, totalDays: number) => { + return (amount * daysLeft) / totalDays; + }; + + // ❌ Incorrect - Unnecessary comments + // Get the user's name + const getUserName = (user: User) => user.name; + + // Check if user is active + const isUserActive = (user: User) => user.status === 'active'; + ``` + +## Error Handling + +### Proper Error Handling +- Use try-catch blocks appropriately +- Provide meaningful error messages + ```typescript + // ✅ Correct + const fetchUserData = async (userId: string) => { + try { + const response = await api.get(`/users/${userId}`); + return response.data; + } catch (error) { + logger.error('Failed to fetch user data', { + userId, + error: error instanceof Error ? error.message : 'Unknown error', + }); + throw new UserFetchError('Failed to fetch user data'); + } + }; + + // ❌ Incorrect + const fetchUserData = async (userId: string) => { + try { + const response = await api.get(`/users/${userId}`); + return response.data; + } catch (error) { + console.log('error:', error); + throw error; + } + }; + ``` + +## Code Organization + +### Logical Grouping +- Group related code together +- Maintain consistent organization + ```typescript + // ✅ Correct + class UserService { + // Properties + private readonly api: Api; + private readonly logger: Logger; + + // Constructor + constructor(api: Api, logger: Logger) { + this.api = api; + this.logger = logger; + } + + // Public methods + public async getUser(id: string): Promise { + // Implementation + } + + public async updateUser(user: User): Promise { + // Implementation + } + + // Private helpers + private validateUser(user: User): boolean { + // Implementation + } + } + ``` \ No newline at end of file diff --git a/.cursor/rules/file-structure-guidelines.md b/.cursor/rules/file-structure-guidelines.md new file mode 100644 index 000000000..617b9b5e6 --- /dev/null +++ b/.cursor/rules/file-structure-guidelines.md @@ -0,0 +1,207 @@ +# File Structure Guidelines + +## Core File Structure Principles +Twenty follows a modular and organized file structure that promotes maintainability and scalability. This document outlines our file organization conventions and best practices. + +## Component Organization + +### One Component Per File +- Each component should have its own file +- File name should match component name + ```typescript + // ✅ Correct + // UserProfile.tsx + export const UserProfile = () => { + return
...
; + }; + + // ❌ Incorrect + // users.tsx + export const UserProfile = () => { + return
...
; + }; + + export const UserList = () => { + return
...
; + }; + ``` + +## Directory Structure + +### Feature Modules +- Place features in `modules/` directory +- Group related components and logic + ``` + modules/ + ├── users/ + │ ├── components/ + │ │ ├── UserList.tsx + │ │ ├── UserCard.tsx + │ │ └── UserProfile.tsx + │ ├── hooks/ + │ │ └── useUser.ts + │ ├── states/ + │ │ └── userStates.ts + │ └── types/ + │ └── user.ts + ├── workspace/ + │ ├── components/ + │ ├── hooks/ + │ └── states/ + └── settings/ + ├── components/ + ├── hooks/ + └── states/ + ``` + +### Hooks Organization +- Place hooks in `hooks/` directory +- Group by feature or global usage + ``` + hooks/ + ├── useClickOutside.ts + ├── useDebounce.ts + └── features/ + ├── users/ + │ ├── useUserActions.ts + │ └── useUserData.ts + └── workspace/ + └── useWorkspaceSettings.ts + ``` + +### State Management +- Place state definitions in `states/` directory +- Organize by feature + ``` + states/ + ├── global/ + │ ├── theme.ts + │ └── navigation.ts + ├── users/ + │ ├── atoms.ts + │ └── selectors.ts + └── workspace/ + ├── atoms.ts + └── selectors.ts + ``` + +### Types Organization +- Place types in `types/` directory +- Group by domain or feature + ``` + types/ + ├── common.ts + ├── api.ts + └── features/ + ├── user.ts + ├── workspace.ts + └── settings.ts + ``` + +## Naming Conventions + +### Component Files +- Use PascalCase for component files +- Use descriptive, feature-specific names + ``` + components/ + ├── UserProfile.tsx + ├── UserProfileHeader.tsx + └── UserProfileContent.tsx + ``` + +### Non-Component Files +- Use camelCase for non-component files +- Use clear, descriptive names + ``` + hooks/ + ├── useClickOutside.ts + └── useDebounce.ts + + utils/ + ├── dateFormatter.ts + └── stringUtils.ts + ``` + +## Module Structure + +### Feature Module Organization +- Consistent structure across features +- Clear separation of concerns + ``` + modules/users/ + ├── components/ + │ ├── UserList/ + │ │ ├── UserList.tsx + │ │ ├── UserListItem.tsx + │ │ └── UserListHeader.tsx + │ └── UserProfile/ + │ ├── UserProfile.tsx + │ └── UserProfileHeader.tsx + ├── hooks/ + │ ├── useUserList.ts + │ └── useUserProfile.ts + ├── states/ + │ ├── atoms.ts + │ └── selectors.ts + ├── types/ + │ └── user.ts + └── utils/ + └── userFormatter.ts + ``` + +## Best Practices + +### Import Organization +- Group imports by type +- Maintain consistent order + ```typescript + // External dependencies + import { useState } from 'react'; + import { styled } from '@emotion/styled'; + + // Internal modules + import { useUser } from '~/modules/users/hooks'; + import { userState } from '~/modules/users/states'; + + // Local imports + import { UserAvatar } from './UserAvatar'; + import { type UserProfileProps } from './types'; + ``` + +### Path Aliases +- Use path aliases for better imports +- Avoid deep relative paths + ```typescript + // ✅ Correct + import { Button } from '~/components/Button'; + import { useUser } from '~/modules/users/hooks'; + + // ❌ Incorrect + import { Button } from '../../../components/Button'; + import { useUser } from '../../../modules/users/hooks'; + ``` + +### Component Co-location +- Keep related files close together +- Use index files for public APIs + ``` + components/UserProfile/ + ├── UserProfile.tsx + ├── UserProfileHeader.tsx + ├── UserProfileContent.tsx + ├── styles.ts + ├── types.ts + └── index.ts + ``` + +### Test File Location +- Place test files next to implementation +- Use `.test.ts` or `.spec.ts` extension + ``` + components/ + ├── UserProfile.tsx + ├── UserProfile.test.tsx + ├── UserProfile.stories.tsx + └── types.ts + ``` \ No newline at end of file diff --git a/.cursor/rules/react-general-guidelines.md b/.cursor/rules/react-general-guidelines.md new file mode 100644 index 000000000..13d739cf1 --- /dev/null +++ b/.cursor/rules/react-general-guidelines.md @@ -0,0 +1,220 @@ +# React Guidelines + +## Core React Principles +Twenty follows modern React best practices with a focus on functional components and clean, maintainable code. This document outlines our React conventions and best practices. + +## Component Structure + +### Functional Components Only +- Use functional components exclusively +- No class components allowed + ```typescript + // ✅ Correct + export const UserProfile = ({ user }: UserProfileProps) => { + return ( + +

{user.name}

+
+ ); + }; + + // ❌ Incorrect + export class UserProfile extends React.Component { + render() { + return ( + +

{this.props.user.name}

+
+ ); + } + } + ``` + +### Named Exports +- Use named exports exclusively +- No default exports + ```typescript + // ✅ Correct + export const Button = ({ label }: ButtonProps) => { + return ; + }; + + // ❌ Incorrect + export default function Button({ label }: ButtonProps) { + return ; + } + ``` + +## State and Effects + +### Event Handlers Over useEffect +- Prefer event handlers for state updates +- Avoid useEffect for state synchronization + ```typescript + // ✅ Correct + const UserForm = () => { + const handleSubmit = async (data: FormData) => { + await updateUser(data); + refreshUserList(); + }; + + return
; + }; + + // ❌ Incorrect + const UserForm = () => { + useEffect(() => { + if (formData) { + updateUser(formData); + } + }, [formData]); + + return ; + }; + ``` + +## Component Design + +### Small, Focused Components +- Keep components small and single-purpose +- Extract reusable logic into custom hooks + ```typescript + // ✅ Correct + const UserCard = ({ user }: UserCardProps) => { + return ( + + + + + + ); + }; + + // ❌ Incorrect + const UserCard = ({ user }: UserCardProps) => { + return ( + + {/* Too much logic in one component */} + +
{user.name}
+
{user.email}
+ + + {/* More complex logic... */} +
+ ); + }; + ``` + +## Props + +### Prop Naming +- Use clear, descriptive prop names +- Follow React conventions (onClick, isActive, etc.) + ```typescript + // ✅ Correct + type ButtonProps = { + onClick: () => void; + isDisabled?: boolean; + isLoading?: boolean; + }; + + // ❌ Incorrect + type ButtonProps = { + clickHandler: () => void; + disabled?: boolean; + loading?: boolean; + }; + ``` + +### Prop Destructuring +- Destructure props with proper typing +- Use TypeScript for prop types + ```typescript + // ✅ Correct + const Button = ({ onClick, isDisabled, children }: ButtonProps) => { + return ( + + ); + }; + + // ❌ Incorrect + const Button = (props: ButtonProps) => { + return ( + + ); + }; + ``` + +## Performance Optimization + +### Memoization +- Use memo for expensive computations +- Avoid premature optimization + ```typescript + // ✅ Correct - Complex computation + const MemoizedChart = memo(({ data }: ChartProps) => { + // Complex rendering logic + return ; + }); + + // ❌ Incorrect - Unnecessary memoization + const MemoizedText = memo(({ text }: { text: string }) => { + return {text}; + }); + ``` + +### Event Handlers +- Use callback refs for DOM manipulation +- Memoize callbacks when needed + ```typescript + // ✅ Correct + const UserList = () => { + const handleScroll = useCallback((event: UIEvent) => { + // Complex scroll handling + }, []); + + return
{/* content */}
; + }; + ``` + +## Error Handling + +### Error Boundaries +- Use error boundaries for component error handling +- Provide meaningful fallback UIs + ```typescript + // ✅ Correct + const ErrorFallback = ({ error }: { error: Error }) => ( + +

Something went wrong

+
{error.message}
+
+ ); + + const SafeComponent = () => ( + + + + ); + ``` + +### Loading States +- Handle loading states gracefully +- Provide meaningful loading indicators + ```typescript + // ✅ Correct + const UserProfile = () => { + const { data: user, isLoading, error } = useUser(); + + if (isLoading) return ; + if (error) return ; + if (!user) return ; + + return ; + }; + ``` \ No newline at end of file diff --git a/.cursor/rules/react-state-management-guidelines.md b/.cursor/rules/react-state-management-guidelines.md new file mode 100644 index 000000000..cb9909b68 --- /dev/null +++ b/.cursor/rules/react-state-management-guidelines.md @@ -0,0 +1,219 @@ +# State Management Guidelines + +## Core State Management Principles +Twenty uses a combination of Recoil for global state and Apollo Client for server state management. This document outlines our state management conventions and best practices. + +## Global State Management + +### Recoil Usage +- Use Recoil for global application state +- Keep atoms small and focused + ```typescript + // ✅ Correct + // states/theme.ts + export const themeState = atom<'light' | 'dark'>({ + key: 'themeState', + default: 'light', + }); + + // states/user.ts + export const userState = atom({ + key: 'userState', + default: null, + }); + + // ❌ Incorrect + // states/globalState.ts + export const globalState = atom({ + key: 'globalState', + default: { + theme: 'light', + user: null, + settings: {}, + // ... many other unrelated pieces of state + }, + }); + ``` + +### Atom Organization +- Place atoms in the `states/` directory +- Group related atoms in feature-specific files + ```typescript + // states/workspace/atoms.ts + export const workspaceIdState = atom({ + key: 'workspaceIdState', + default: '', + }); + + export const workspaceSettingsState = atom({ + key: 'workspaceSettingsState', + default: defaultSettings, + }); + ``` + +## Server State Management + +### Apollo Client Usage +- Use Apollo Client for all GraphQL operations +- Leverage Apollo's caching capabilities + ```typescript + // ✅ Correct + const { data, loading } = useQuery(GET_USER_QUERY, { + variables: { id }, + fetchPolicy: 'cache-first', + }); + + // ❌ Incorrect + const [user, setUser] = useState(null); + useEffect(() => { + fetch('/api/user/' + id).then(setUser); + }, [id]); + ``` + +### Query Organization +- Separate operation files +- Use fragments for shared fields + ```typescript + // queries/user.ts + export const UserFragment = gql` + fragment UserFields on User { + id + name + email + } + `; + + export const GET_USER = gql` + query GetUser($id: ID!) { + user(id: $id) { + ...UserFields + } + } + ${UserFragment} + `; + ``` + +## State Management Best Practices + +### Multiple Small Atoms +- Prefer multiple small atoms over prop drilling +- Keep atoms focused on specific features + ```typescript + // ✅ Correct + export const selectedViewState = atom({ + key: 'selectedViewState', + default: '', + }); + + export const viewFiltersState = atom({ + key: 'viewFiltersState', + default: {}, + }); + + // ❌ Incorrect - Prop drilling + const ViewContainer = ({ selectedView, filters, onViewChange }) => { + return ( + + + + + + ); + }; + ``` + +### No useRef for State +- Never use useRef for state management +- Use proper state management tools + ```typescript + // ✅ Correct + const [count, setCount] = useState(0); + // or + const [count, setCount] = useRecoilState(countState); + + // ❌ Incorrect + const countRef = useRef(0); + ``` + +### Data Fetching +- Extract data fetching to sibling components +- Keep components focused on presentation + ```typescript + // ✅ Correct + const UserProfileContainer = () => { + const { data, loading } = useQuery(GET_USER); + if (loading) return ; + return ; + }; + + const UserProfile = ({ user }: UserProfileProps) => { + return
{user.name}
; + }; + + // ❌ Incorrect + const UserProfile = () => { + const { data, loading } = useQuery(GET_USER); + if (loading) return ; + return
{data.user.name}
; + }; + ``` + +### Hook Usage +- Use appropriate hooks for state access +- Choose between useRecoilValue and useRecoilState based on needs + ```typescript + // ✅ Correct - Read-only access + const theme = useRecoilValue(themeState); + + // ✅ Correct - Read-write access + const [theme, setTheme] = useRecoilState(themeState); + + // ❌ Incorrect - Using state setter when only reading + const [theme, _] = useRecoilState(themeState); + ``` + +## Performance Considerations + +### Selector Usage +- Use selectors for derived state +- Memoize complex calculations + ```typescript + // ✅ Correct + const filteredUsersState = selector({ + key: 'filteredUsersState', + get: ({ get }) => { + const users = get(usersState); + const filter = get(userFilterState); + return users.filter(user => + user.name.toLowerCase().includes(filter.toLowerCase()) + ); + }, + }); + + // ❌ Incorrect - Calculating in component + const UserList = () => { + const users = useRecoilValue(usersState); + const filter = useRecoilValue(userFilterState); + const filteredUsers = users.filter(user => + user.name.toLowerCase().includes(filter.toLowerCase()) + ); + return ; + }; + ``` + +### Cache Management +- Configure appropriate cache policies +- Handle cache invalidation properly + ```typescript + // ✅ Correct + const [updateUser] = useMutation(UPDATE_USER, { + update: (cache, { data }) => { + cache.modify({ + id: cache.identify(data.updateUser), + fields: { + name: () => data.updateUser.name, + }, + }); + }, + }); + ``` \ No newline at end of file diff --git a/.cursor/rules/testing-guidelines.md b/.cursor/rules/testing-guidelines.md new file mode 100644 index 000000000..4ee86d6d1 --- /dev/null +++ b/.cursor/rules/testing-guidelines.md @@ -0,0 +1,253 @@ +# Testing Guidelines + +## Core Testing Principles +Twenty follows a comprehensive testing strategy across all packages, ensuring high-quality, maintainable code. This document outlines our testing conventions and best practices. + +## Testing Stack + +### Backend Testing +- Primary Framework: Jest +- API Testing: Supertest +- Coverage Requirements: 80% minimum + +### Frontend Testing +- Component Testing: Jest + React Testing Library +- Visual Testing: Storybook +- API Mocking: MSW (Mock Service Worker) + +### End-to-End Testing +- Framework: Playwright +- Coverage: Critical user journeys +- Cross-browser testing + +## Test Organization + +### Test File Location +- Co-locate tests with implementation files +- Use consistent naming patterns + ``` + src/ + ├── components/ + │ ├── UserProfile.tsx + │ ├── UserProfile.test.tsx + │ └── UserProfile.stories.tsx + ``` + +### Test File Naming +- Use `.test.ts(x)` for unit/integration tests +- Use `.spec.ts(x)` for E2E tests +- Use `.stories.tsx` for Storybook stories + +## Unit Testing + +### Component Testing +- Test behavior, not implementation +- Use React Testing Library best practices + ```typescript + // ✅ Correct + test('displays user name when provided', () => { + render(); + expect(screen.getByText('John Doe')).toBeInTheDocument(); + }); + + // ❌ Incorrect - Testing implementation details + test('sets the text content', () => { + const { container } = render(); + expect(container.querySelector('h1').textContent).toBe('John Doe'); + }); + ``` + +### Hook Testing +- Use `renderHook` from @testing-library/react-hooks +- Test all possible states + ```typescript + // ✅ Correct + test('useUser hook manages user state', () => { + const { result } = renderHook(() => useUser()); + + act(() => { + result.current.setUser({ id: '1', name: 'John' }); + }); + + expect(result.current.user).toEqual({ id: '1', name: 'John' }); + }); + ``` + +### Mocking +- Mock external dependencies +- Use jest.mock for module mocking + ```typescript + // ✅ Correct + jest.mock('~/services/api', () => ({ + fetchUser: jest.fn().mockResolvedValue({ id: '1', name: 'John' }), + })); + + test('fetches and displays user', async () => { + render(); + expect(await screen.findByText('John')).toBeInTheDocument(); + }); + ``` + +## Integration Testing + +### API Testing +- Test complete request/response cycles +- Use Supertest for backend API testing + ```typescript + // ✅ Correct + describe('GET /api/users/:id', () => { + it('returns user when found', async () => { + const response = await request(app) + .get('/api/users/1') + .expect(200); + + expect(response.body).toEqual({ + id: '1', + name: 'John Doe', + }); + }); + + it('returns 404 when user not found', async () => { + await request(app) + .get('/api/users/999') + .expect(404); + }); + }); + ``` + +## E2E Testing + +### Test Structure +- Organize by user journey +- Use page objects for reusability + ```typescript + // pages/login.ts + export class LoginPage { + async login(email: string, password: string) { + await this.page.fill('[data-testid="email-input"]', email); + await this.page.fill('[data-testid="password-input"]', password); + await this.page.click('[data-testid="login-button"]'); + } + } + + // tests/auth.spec.ts + test('user can login successfully', async ({ page }) => { + const loginPage = new LoginPage(page); + await loginPage.login('user@example.com', 'password'); + await expect(page).toHaveURL('/dashboard'); + }); + ``` + +### Test Data +- Use dedicated test environments +- Reset state between tests + ```typescript + // ✅ Correct + beforeEach(async () => { + await resetDatabase(); + await seedTestData(); + }); + + test('user workflow', async ({ page }) => { + // Test with clean, predictable state + }); + ``` + +## Visual Testing + +### Storybook Guidelines +- Create stories for all components +- Document component variants + ```typescript + // Button.stories.tsx + export default { + title: 'Components/Button', + component: Button, + } as Meta; + + export const Primary = { + args: { + variant: 'primary', + label: 'Primary Button', + }, + }; + + export const Secondary = { + args: { + variant: 'secondary', + label: 'Secondary Button', + }, + }; + ``` + +### Visual Regression +- Use Storybook's visual regression testing +- Review changes carefully + ```typescript + // jest.config.js + module.exports = { + preset: 'jest-image-snapshot', + setupFilesAfterEnv: ['/setup-tests.ts'], + }; + + // Button.visual.test.tsx + describe('Button', () => { + it('matches visual snapshot', async () => { + const image = await page.screenshot(); + expect(image).toMatchImageSnapshot(); + }); + }); + ``` + +## Test Quality + +### Test Data Attributes +- Use data-testid for test selectors +- Avoid selecting by CSS classes + ```typescript + // ✅ Correct + + + // In tests + const button = screen.getByTestId('submit-button'); + + // ❌ Incorrect + const button = container.querySelector('.submit-btn'); + ``` + +### Assertion Best Practices +- Use explicit assertions +- Test both positive and negative cases + ```typescript + // ✅ Correct + test('form validation', async () => { + render(); + + // Negative case + await userEvent.click(screen.getByText('Submit')); + expect(screen.getByText('Name is required')).toBeInTheDocument(); + + // Positive case + await userEvent.type(screen.getByLabelText('Name'), 'John Doe'); + await userEvent.click(screen.getByText('Submit')); + expect(screen.queryByText('Name is required')).not.toBeInTheDocument(); + }); + ``` + +### Coverage Requirements +- A new feature should have at least 80% coverage +- Focus on critical paths +- Run coverage reports in CI + ```typescript + // jest.config.js + module.exports = { + coverageThreshold: { + global: { + statements: 80, + branches: 80, + functions: 80, + lines: 80, + }, + }, + }; + ``` \ No newline at end of file diff --git a/.cursor/rules/translations.md b/.cursor/rules/translations.md new file mode 100644 index 000000000..e04e75c99 --- /dev/null +++ b/.cursor/rules/translations.md @@ -0,0 +1,162 @@ +# Translation Guidelines + +## Core Translation Principles +Twenty uses Lingui for internationalization (i18n) and Crowdin for translation management. This document outlines our translation workflow and best practices. + +## Technology Stack + +### Translation Tools +- **Framework**: @lingui/react +- **Translation Management**: Crowdin +- **Workflow**: GitHub Actions for automation + +### Package Structure +Translation files are managed in multiple packages: +- `twenty-front`: Frontend translations +- `twenty-server`: Backend translations +- `twenty-emails`: Email template translations + +## Translation Process + +### Adding New Strings + +#### Using Lingui Macros +- Use `` for components +- Use `t` macro for strings outside JSX + ```typescript + // ✅ Correct - In JSX + import { Trans } from '@lingui/react/macro'; + + const WelcomeMessage = () => ( +

+ Welcome to Twenty +

+ ); + + // ✅ Correct - Outside JSX + import { t } from '@lingui/react/macro'; + + const getMessage = () => { + return t`Welcome to Twenty`; + }; + + // ❌ Incorrect - Don't use raw strings + const WelcomeMessage = () => ( +

Welcome to Twenty

+ ); + ``` + +### String Guidelines + +#### What to Translate +- User interface text +- Error messages +- Notifications +- Email content + +#### What Not to Translate +- Variables +- Test data/mocks + +### Translation Workflow + +#### 1. Extracting Translations +- Automatically triggered on main branch changes +- Can be manually triggered in GitHub Actions +- Process: + ```bash + # Extract new strings + nx run twenty-front:lingui:extract + nx run twenty-server:lingui:extract + nx run twenty-emails:lingui:extract + ``` + +#### 2. Translation Management +- Translations are managed in Crowdin +- Changes are synced every 2 hours +- Process: + 1. New strings are uploaded to Crowdin + 2. Translators work on translations + 3. Translations are pulled back to the repository + +#### 3. Compiling Translations +- Happens automatically in CI/CD +- Required before running the application + ```bash + # Compile translations + nx run twenty-front:lingui:compile + nx run twenty-server:lingui:compile + nx run twenty-emails:lingui:compile + ``` + +## Best Practices + +### String Management + +#### Use Placeholders +- Use placeholders for dynamic content + ```typescript + // ✅ Correct + Hello {userName}, + + // ❌ Incorrect - String concatenation + Hello {userName}, + ``` + +#### Provide Context +- Lingui provides powerfulway to add context for translators but we don't use them as of today. + +### Code Organization + +#### Translation Files +- Keep translation files organized by feature +- Use consistent naming patterns + ``` + src/ + ├── locales/ + │ ├── en/ + │ │ ├── messages.po + │ │ └── messages.js + │ └── fr/ + │ ├── messages.po + │ └── messages.js + ``` + +### Quality Assurance + +#### Strict Mode +- Use --strict mode when compiling to identify missing translations + + +#### Testing Translations +- Test with different locales +- Verify string interpolation +- Check layout with different language lengths + +## Automation + +### GitHub Actions + +#### Pull Workflow +- Runs every 2 hours +- Downloads new translations from Crowdin +- Creates PR if changes detected +- Can be manually triggered with force pull option + +#### Push Workflow +- Runs on main branch changes +- Extracts and uploads new strings +- Compiles translations +- Creates PR with changes + +### Error Handling + +#### Missing Translations +- Development: Shown in original language +- Production: Falls back to default language +- Strict mode in CI catches missing translations + +#### Compilation Errors +- Addressed before merging +- PR created for fixing missing translations +- Automated testing in CI pipeline \ No newline at end of file diff --git a/.cursor/rules/typescript-guidelines.md b/.cursor/rules/typescript-guidelines.md new file mode 100644 index 000000000..56dcc24dc --- /dev/null +++ b/.cursor/rules/typescript-guidelines.md @@ -0,0 +1,172 @@ +# TypeScript Guidelines + +## Core TypeScript Principles +Twenty enforces strict TypeScript usage to ensure type safety and maintainable code. This document outlines our TypeScript conventions and best practices. + +## Type Safety + +### Strict Typing +- **No 'any' type allowed** +- TypeScript strict mode enabled +- noImplicitAny enabled + ```typescript + // ✅ Correct + function processUser(user: User) { + return user.name; + } + + // ❌ Incorrect + function processUser(user: any) { + return user.name; + } + ``` + +### Type Definitions + +#### Types over Interfaces +- Use `type` for all type definitions +- Exception: When extending third-party interfaces + ```typescript + // ✅ Correct + type User = { + id: string; + name: string; + email: string; + }; + + // ❌ Incorrect + interface User { + id: string; + name: string; + email: string; + } + ``` + +### String Literals over Enums +- Use string literal unions instead of enums +- Exception: GraphQL enums + ```typescript + // ✅ Correct + type UserRole = 'admin' | 'user' | 'guest'; + + // ❌ Incorrect + enum UserRole { + Admin = 'admin', + User = 'user', + Guest = 'guest', + } + ``` + +## Naming Conventions + +### Component Props +- Suffix component prop types with 'Props' +- Keep props focused and single-purpose + ```typescript + // ✅ Correct + type ButtonProps = { + label: string; + onClick: () => void; + variant?: 'primary' | 'secondary'; + }; + + // ❌ Incorrect + type ButtonParameters = { + label: string; + onClick: () => void; + variant?: 'primary' | 'secondary'; + }; + ``` + +## Type Inference + +### Leverage TypeScript Inference +- Use type inference when types are clear +- Explicitly type when inference is ambiguous + ```typescript + // ✅ Correct - Clear inference + const users = ['John', 'Jane']; // inferred as string[] + + // ✅ Correct - Explicit typing needed + const processUser = (user: User): UserResponse => { + // Complex processing + return response; + }; + + // ❌ Incorrect - Unnecessary explicit typing + const users: string[] = ['John', 'Jane']; + ``` + +## Best Practices + +### Type Guards +- Use type guards for runtime type checking +- Prefer discriminated unions + ```typescript + // ✅ Correct + type Success = { + type: 'success'; + data: User; + }; + + type Error = { + type: 'error'; + message: string; + }; + + type Result = Success | Error; + + function handleResult(result: Result) { + if (result.type === 'success') { + // TypeScript knows result.data exists + console.log(result.data); + } + } + ``` + +### Generics +- Use generics for reusable type patterns +- Keep generic names descriptive + ```typescript + // ✅ Correct + type ApiResponse = { + data: TData; + status: number; + message: string; + }; + + // ❌ Incorrect + type ApiResponse = { + data: T; + status: number; + message: string; + }; + ``` + +### Type Exports +- Export types when they're used across files +- Keep type definitions close to their usage + ```typescript + // types.ts + export type User = { + id: string; + name: string; + }; + + // UserComponent.tsx + import { type User } from './types'; + ``` + +### Utility Types +- Leverage TypeScript utility types +- Create custom utility types for repeated patterns + ```typescript + // Built-in utility types + type UserPartial = Partial; + type UserReadonly = Readonly; + + // Custom utility types + type NonNullableProperties = { + [P in keyof T]: NonNullable; + }; + ``` \ No newline at end of file diff --git a/.cursorrules b/.cursorrules deleted file mode 100644 index aed6eed69..000000000 --- a/.cursorrules +++ /dev/null @@ -1,99 +0,0 @@ -# Twenty Development Rules - -## General -- Twenty is an open source CRM built with Typescript (React, NestJS) - -### Main Packages / folders -- packages/twenty-front: Main Frontend (React) -- packages/twenty-server: Main Backend (NestJS) -- packages/twenty-website: Marketing website + docs (NextJS) -- packages/twenty-ui: UI library (React) -- packages/twenty-shared: Shared utils, constants, types - -### Development Stack -- Package Manager: yarn -- Monorepo: nx -- Database: PostgreSQL + TypeORM (core, metadata schemas) -- State Management: Recoil -- Data Management: Apollo / GraphQL -- Cache: redis -- Auth: JWT -- Queue: BullMQ -- Storage: S3/local filesystem -- Testing Backend: Jest, Supertest -- Testing Frontend: Jest, Storybook, MSW -- Testing E2E: Playwright - -## Styling -- Use @emotion/styled, never CSS classes/Tailwind -- Prefix styled components with 'Styled' -- Keep styled components at top of file -- Use Theme object for colors/spacing/typography -- Use Theme values instead of px/rem -- Use mq helper for media queries - -## TypeScript -- No 'any' type - use proper typing -- Use type over interface -- String literals over enums (except GraphQL) -- Props suffix for component props types -- Use type inference when possible -- Enable strict mode and noImplicitAny - -## React -- Only functional components -- Named exports only -- No useEffect, prefer event handlers -- Small focused components -- Proper prop naming (onClick, isActive) -- Destructure props with types - -## State Management -- Recoil for global state -- Apollo Client for GraphQL/server state -- Atoms in states/ directory -- Multiple small atoms over prop drilling -- No useRef for state -- Extract data fetching to siblings -- useRecoilValue/useRecoilState appropriately - -## File Structure -- One component per file -- Features in modules/ -- Hooks in hooks/ -- States in states/ -- Types in types/ -- PascalCase components, camelCase others - -## Translation -- Use @lingui/react/macro -- Use within components -- Use t`string` elsewhere (from useLingui hook) -- Don't translate metadata (field names, object names, etc) -- Don't translate mocks - -## Code Style -- Early returns -- No nested ternaries -- No else-if -- Optional chaining over && -- Small focused functions -- Clear variable names -- No console.logs in commits -- Few comments, prefer code readability - -## GraphQL -- Use gql tag -- Separate operation files -- Proper codegen typing -- Consistent naming (getX, updateX) -- Use fragments -- Use generated types - -## Testing -- Test every feature -- React Testing Library -- Test behavior not implementation -- Mock external deps -- Use data-testid -- Follow naming conventions