Better cursor rules (#10431)

Move to the new cursor rule folder style and make it more granular
This commit is contained in:
Félix Malfait
2025-02-24 10:35:28 +01:00
committed by GitHub
parent 1b64f87d75
commit 16869a333c
10 changed files with 1654 additions and 99 deletions

65
.cursor/rules/README.md Normal file
View File

@ -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.

View File

@ -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

View File

@ -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<Status, string> = {
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<User> {
// Implementation
}
public async updateUser(user: User): Promise<User> {
// Implementation
}
// Private helpers
private validateUser(user: User): boolean {
// Implementation
}
}
```

View File

@ -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 <div>...</div>;
};
// ❌ Incorrect
// users.tsx
export const UserProfile = () => {
return <div>...</div>;
};
export const UserList = () => {
return <div>...</div>;
};
```
## 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
```

View File

@ -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 (
<StyledContainer>
<h1>{user.name}</h1>
</StyledContainer>
);
};
// ❌ Incorrect
export class UserProfile extends React.Component<UserProfileProps> {
render() {
return (
<StyledContainer>
<h1>{this.props.user.name}</h1>
</StyledContainer>
);
}
}
```
### Named Exports
- Use named exports exclusively
- No default exports
```typescript
// ✅ Correct
export const Button = ({ label }: ButtonProps) => {
return <button>{label}</button>;
};
// ❌ Incorrect
export default function Button({ label }: ButtonProps) {
return <button>{label}</button>;
}
```
## 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 <Form onSubmit={handleSubmit} />;
};
// ❌ Incorrect
const UserForm = () => {
useEffect(() => {
if (formData) {
updateUser(formData);
}
}, [formData]);
return <Form />;
};
```
## Component Design
### Small, Focused Components
- Keep components small and single-purpose
- Extract reusable logic into custom hooks
```typescript
// ✅ Correct
const UserCard = ({ user }: UserCardProps) => {
return (
<StyledCard>
<UserAvatar user={user} />
<UserInfo user={user} />
<UserActions user={user} />
</StyledCard>
);
};
// ❌ Incorrect
const UserCard = ({ user }: UserCardProps) => {
return (
<StyledCard>
{/* Too much logic in one component */}
<img src={user.avatar} />
<div>{user.name}</div>
<div>{user.email}</div>
<button onClick={() => handleEdit(user)}>Edit</button>
<button onClick={() => handleDelete(user)}>Delete</button>
{/* More complex logic... */}
</StyledCard>
);
};
```
## 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 (
<button onClick={onClick} disabled={isDisabled}>
{children}
</button>
);
};
// ❌ Incorrect
const Button = (props: ButtonProps) => {
return (
<button onClick={props.onClick} disabled={props.isDisabled}>
{props.children}
</button>
);
};
```
## Performance Optimization
### Memoization
- Use memo for expensive computations
- Avoid premature optimization
```typescript
// ✅ Correct - Complex computation
const MemoizedChart = memo(({ data }: ChartProps) => {
// Complex rendering logic
return <ComplexChart data={data} />;
});
// ❌ Incorrect - Unnecessary memoization
const MemoizedText = memo(({ text }: { text: string }) => {
return <span>{text}</span>;
});
```
### 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 <div onScroll={handleScroll}>{/* content */}</div>;
};
```
## Error Handling
### Error Boundaries
- Use error boundaries for component error handling
- Provide meaningful fallback UIs
```typescript
// ✅ Correct
const ErrorFallback = ({ error }: { error: Error }) => (
<StyledError>
<h2>Something went wrong</h2>
<pre>{error.message}</pre>
</StyledError>
);
const SafeComponent = () => (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<ComponentThatMightError />
</ErrorBoundary>
);
```
### Loading States
- Handle loading states gracefully
- Provide meaningful loading indicators
```typescript
// ✅ Correct
const UserProfile = () => {
const { data: user, isLoading, error } = useUser();
if (isLoading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
if (!user) return <NotFound />;
return <UserProfileContent user={user} />;
};
```

View File

@ -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<User | null>({
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<string>({
key: 'workspaceIdState',
default: '',
});
export const workspaceSettingsState = atom<WorkspaceSettings>({
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<string>({
key: 'selectedViewState',
default: '',
});
export const viewFiltersState = atom<ViewFilters>({
key: 'viewFiltersState',
default: {},
});
// ❌ Incorrect - Prop drilling
const ViewContainer = ({ selectedView, filters, onViewChange }) => {
return (
<ViewHeader view={selectedView} onViewChange={onViewChange}>
<ViewContent>
<ViewFilters filters={filters} />
</ViewContent>
</ViewHeader>
);
};
```
### 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 <LoadingSpinner />;
return <UserProfile user={data.user} />;
};
const UserProfile = ({ user }: UserProfileProps) => {
return <div>{user.name}</div>;
};
// ❌ Incorrect
const UserProfile = () => {
const { data, loading } = useQuery(GET_USER);
if (loading) return <LoadingSpinner />;
return <div>{data.user.name}</div>;
};
```
### 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 <List users={filteredUsers} />;
};
```
### 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,
},
});
},
});
```

View File

@ -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(<UserProfile user={{ name: 'John Doe' }} />);
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
// ❌ Incorrect - Testing implementation details
test('sets the text content', () => {
const { container } = render(<UserProfile user={{ name: 'John Doe' }} />);
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(<UserProfile userId="1" />);
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: ['<rootDir>/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
<button data-testid="submit-button">Submit</button>
// 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(<UserForm />);
// 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,
},
},
};
```

View File

@ -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 `<Trans>` for components
- Use `t` macro for strings outside JSX
```typescript
// ✅ Correct - In JSX
import { Trans } from '@lingui/react/macro';
const WelcomeMessage = () => (
<h1>
<Trans>Welcome to Twenty</Trans>
</h1>
);
// ✅ Correct - Outside JSX
import { t } from '@lingui/react/macro';
const getMessage = () => {
return t`Welcome to Twenty`;
};
// ❌ Incorrect - Don't use raw strings
const WelcomeMessage = () => (
<h1>Welcome to Twenty</h1>
);
```
### 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
<Trans>Hello {userName},</Trans>
// ❌ Incorrect - String concatenation
<Trans>Hello </Trans>{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

View File

@ -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<TData> = {
data: TData;
status: number;
message: string;
};
// ❌ Incorrect
type ApiResponse<T> = {
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<User>;
type UserReadonly = Readonly<User>;
// Custom utility types
type NonNullableProperties<T> = {
[P in keyof T]: NonNullable<T[P]>;
};
```