Better cursor rules (#10431)
Move to the new cursor rule folder style and make it more granular
This commit is contained in:
65
.cursor/rules/README.md
Normal file
65
.cursor/rules/README.md
Normal 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.
|
||||||
97
.cursor/rules/architecture.md
Normal file
97
.cursor/rules/architecture.md
Normal 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
|
||||||
259
.cursor/rules/code-style-guidelines.md
Normal file
259
.cursor/rules/code-style-guidelines.md
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
207
.cursor/rules/file-structure-guidelines.md
Normal file
207
.cursor/rules/file-structure-guidelines.md
Normal 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
|
||||||
|
```
|
||||||
220
.cursor/rules/react-general-guidelines.md
Normal file
220
.cursor/rules/react-general-guidelines.md
Normal 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} />;
|
||||||
|
};
|
||||||
|
```
|
||||||
219
.cursor/rules/react-state-management-guidelines.md
Normal file
219
.cursor/rules/react-state-management-guidelines.md
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
253
.cursor/rules/testing-guidelines.md
Normal file
253
.cursor/rules/testing-guidelines.md
Normal 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
162
.cursor/rules/translations.md
Normal file
162
.cursor/rules/translations.md
Normal 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
|
||||||
172
.cursor/rules/typescript-guidelines.md
Normal file
172
.cursor/rules/typescript-guidelines.md
Normal 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]>;
|
||||||
|
};
|
||||||
|
```
|
||||||
99
.cursorrules
99
.cursorrules
@ -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 <Trans> 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
|
|
||||||
Reference in New Issue
Block a user