Migrate cursor rules (#12646)
Migrating rules to new format but they should be re-written entirely, I don't think they help much and are not auto-included (except architecture)
This commit is contained in:
@ -1,65 +0,0 @@
|
|||||||
# 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/core/migrations/[name] -d src/database/typeorm/core/core.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.
|
|
||||||
137
.cursor/rules/README.mdc
Normal file
137
.cursor/rules/README.mdc
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# Twenty Development Rules
|
||||||
|
|
||||||
|
This directory contains Twenty's development guidelines and best practices in the modern Cursor Rules format (MDC). These rules are automatically applied based on file patterns and provide context-aware guidance to AI assistants.
|
||||||
|
|
||||||
|
## Rules Overview
|
||||||
|
|
||||||
|
### Core Guidelines
|
||||||
|
- **architecture.mdc** - Project overview, technology stack, and infrastructure setup (Always Applied)
|
||||||
|
- **nx-rules.mdc** - Nx workspace guidelines and best practices (Auto-attached to Nx files)
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
- **typescript-guidelines.mdc** - TypeScript best practices and conventions (Auto-attached to .ts/.tsx files)
|
||||||
|
- **code-style.mdc** - General coding standards and style guide (Auto-attached to code files)
|
||||||
|
- **file-structure.mdc** - File and directory organization patterns (Auto-attached to config files)
|
||||||
|
|
||||||
|
### React Development
|
||||||
|
- **react-general-guidelines.mdc** - Core React development principles (Auto-attached to React files)
|
||||||
|
- **react-state-management.mdc** - State management approaches with Recoil (Auto-attached to state files)
|
||||||
|
|
||||||
|
### Testing & Quality
|
||||||
|
- **testing-guidelines.mdc** - Testing strategies and best practices (Auto-attached to test files)
|
||||||
|
|
||||||
|
### Internationalization
|
||||||
|
- **translations.mdc** - Translation workflow and i18n setup (Auto-attached to locale files)
|
||||||
|
|
||||||
|
## How Rules Work
|
||||||
|
|
||||||
|
### Automatic Attachment
|
||||||
|
Rules are automatically included in your AI context based on file patterns (globs). When you work on TypeScript files, the TypeScript guidelines are automatically loaded.
|
||||||
|
|
||||||
|
### Manual Reference
|
||||||
|
You can manually reference any rule using the `@ruleName` syntax:
|
||||||
|
- `@nx-rules` - Include Nx-specific guidance
|
||||||
|
- `@react-general-guidelines` - Load React best practices
|
||||||
|
- `@testing-guidelines` - Get testing recommendations
|
||||||
|
|
||||||
|
### Rule Types Used
|
||||||
|
- **Always Applied** - Loaded in every context (architecture.mdc, README.mdc)
|
||||||
|
- **Auto Attached** - Loaded when matching file patterns are referenced
|
||||||
|
- **Agent Requested** - Available for AI to include when relevant
|
||||||
|
- **Manual** - Only included when explicitly mentioned
|
||||||
|
|
||||||
|
## 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/core/migrations/[name] -d src/database/typeorm/core/core.datasource.ts
|
||||||
|
|
||||||
|
# Workspace
|
||||||
|
npx nx run twenty-server:command workspace:sync-metadata -f # Sync metadata
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Guidelines
|
||||||
|
|
||||||
|
### For Developers
|
||||||
|
- Rules are automatically applied based on file context
|
||||||
|
- Check rule descriptions to understand when they're activated
|
||||||
|
- Use manual references (`@ruleName`) for additional context
|
||||||
|
- Keep rules updated as the codebase evolves
|
||||||
|
|
||||||
|
### For AI Assistants
|
||||||
|
- Rules provide consistent guidance across conversations
|
||||||
|
- Use rule context to maintain coding standards
|
||||||
|
- Reference specific rules when making recommendations
|
||||||
|
- Apply rule principles in code suggestions and reviews
|
||||||
|
|
||||||
|
## Contributing to Rules
|
||||||
|
|
||||||
|
### Adding New Rules
|
||||||
|
1. Create a new `.mdc` file in this directory
|
||||||
|
2. Include proper metadata headers with description and globs
|
||||||
|
3. Write clear, actionable guidelines with examples
|
||||||
|
4. Test the rule with relevant file patterns
|
||||||
|
5. Update this README if needed
|
||||||
|
|
||||||
|
### Updating Existing Rules
|
||||||
|
1. Modify the rule content while preserving metadata
|
||||||
|
2. Test changes with affected file patterns
|
||||||
|
3. Ensure consistency with other rules
|
||||||
|
4. Update examples and best practices as needed
|
||||||
|
|
||||||
|
## Rule Format Reference
|
||||||
|
|
||||||
|
Each rule file uses the MDC format with metadata:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
description: Brief description of the rule's purpose
|
||||||
|
globs: ["**/*.ts", "**/*.tsx"] # File patterns for auto-attachment
|
||||||
|
alwaysApply: false # Whether to always include this rule
|
||||||
|
---
|
||||||
|
|
||||||
|
# Rule Title
|
||||||
|
|
||||||
|
Rule content in Markdown format...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration from Legacy Format
|
||||||
|
|
||||||
|
The rules have been migrated from the legacy `.md` format to the modern `.mdc` format, providing:
|
||||||
|
- Better context awareness through file pattern matching
|
||||||
|
- Improved organization with metadata headers
|
||||||
|
- More flexible rule application strategies
|
||||||
|
- Enhanced integration with Cursor's AI features
|
||||||
|
|
||||||
|
For the most up-to-date version of these guidelines, always refer to the files in this directory.
|
||||||
@ -1,97 +0,0 @@
|
|||||||
# 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
|
|
||||||
35
.cursor/rules/architecture.mdc
Normal file
35
.cursor/rules/architecture.mdc
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
---
|
||||||
|
description: Twenty CRM architecture overview - monorepo structure, tech stack, and development principles
|
||||||
|
globs: []
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Twenty Architecture
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
- **Frontend**: React 18, TypeScript, Recoil, Styled Components, Vite
|
||||||
|
- **Backend**: NestJS, TypeORM, PostgreSQL, Redis, GraphQL
|
||||||
|
- **Monorepo**: Nx workspace with yarn
|
||||||
|
|
||||||
|
## Package Structure
|
||||||
|
```
|
||||||
|
packages/
|
||||||
|
├── twenty-front/ # React app
|
||||||
|
├── twenty-server/ # NestJS API
|
||||||
|
├── twenty-ui/ # Shared components
|
||||||
|
├── twenty-shared/ # Common types/utils
|
||||||
|
└── twenty-emails/ # Email templates
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Principles
|
||||||
|
- **Functional components only** (no classes)
|
||||||
|
- **Named exports only** (no default exports)
|
||||||
|
- **Types over interfaces** (except for extending third-party)
|
||||||
|
- **String literals over enums** (except GraphQL)
|
||||||
|
- **No 'any' type allowed**
|
||||||
|
- **Event handlers over useEffect** for state updates
|
||||||
@ -1,259 +0,0 @@
|
|||||||
# 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
103
.cursor/rules/code-style.mdc
Normal file
103
.cursor/rules/code-style.mdc
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# Code Style Guidelines
|
||||||
|
|
||||||
|
## Formatting Standards
|
||||||
|
- **Prettier**: 2-space indentation, single quotes, trailing commas, semicolons
|
||||||
|
- **Print width**: 80 characters
|
||||||
|
- **ESLint**: No unused imports, consistent import ordering, prefer const over let
|
||||||
|
|
||||||
|
## Naming Conventions
|
||||||
|
```typescript
|
||||||
|
// ✅ Variables and functions - camelCase
|
||||||
|
const userAccountBalance = 1000;
|
||||||
|
const calculateMonthlyPayment = () => {};
|
||||||
|
|
||||||
|
// ✅ Constants - SCREAMING_SNAKE_CASE
|
||||||
|
const API_ENDPOINTS = {
|
||||||
|
USERS: '/api/users',
|
||||||
|
ORDERS: '/api/orders',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// ✅ Types and Classes - PascalCase
|
||||||
|
class UserService {}
|
||||||
|
type UserAccountData = {};
|
||||||
|
type ButtonProps = {}; // Component props suffix with 'Props'
|
||||||
|
|
||||||
|
// ✅ Files and directories - kebab-case
|
||||||
|
// user-profile.component.tsx
|
||||||
|
// user-profile.styles.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Import Organization
|
||||||
|
```typescript
|
||||||
|
// ✅ Correct import order
|
||||||
|
// 1. External libraries
|
||||||
|
import React from 'react';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
// 2. Internal modules (absolute paths)
|
||||||
|
import { Button } from '@/components/ui';
|
||||||
|
import { UserService } from '@/services';
|
||||||
|
|
||||||
|
// 3. Relative imports
|
||||||
|
import { UserCardProps } from './types';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Function Structure
|
||||||
|
```typescript
|
||||||
|
// ✅ Small, focused functions
|
||||||
|
// ✅ Required parameters first, optional last
|
||||||
|
const processUserData = (
|
||||||
|
user: User,
|
||||||
|
options: ProcessingOptions,
|
||||||
|
callback?: (result: ProcessedUser) => void
|
||||||
|
): ProcessedUser => {
|
||||||
|
const processedUser = transformUserData(user);
|
||||||
|
applyOptions(processedUser, options);
|
||||||
|
|
||||||
|
if (callback) {
|
||||||
|
callback(processedUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
return processedUser;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
```typescript
|
||||||
|
// ✅ Explain business logic and non-obvious intentions
|
||||||
|
// Apply 15% discount for premium users with orders > $100
|
||||||
|
const discount = isPremiumUser && orderTotal > 100 ? 0.15 : 0;
|
||||||
|
|
||||||
|
// TODO: Replace with proper authentication service
|
||||||
|
const isAuthenticated = localStorage.getItem('token') !== null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSDoc for public APIs
|
||||||
|
* @param basePrice - The base price before modifications
|
||||||
|
* @returns The final price after tax and discount
|
||||||
|
*/
|
||||||
|
const calculateTotalPrice = (basePrice: number): number => {
|
||||||
|
// Implementation
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
```typescript
|
||||||
|
// ✅ Proper error types and meaningful messages
|
||||||
|
try {
|
||||||
|
const user = await userService.findById(userId);
|
||||||
|
if (!user) {
|
||||||
|
throw new UserNotFoundError(`User with ID ${userId} not found`);
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to fetch user', { userId, error });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
```
|
||||||
@ -1,207 +0,0 @@
|
|||||||
# 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
|
|
||||||
```
|
|
||||||
107
.cursor/rules/file-structure.mdc
Normal file
107
.cursor/rules/file-structure.mdc
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# File Structure Guidelines
|
||||||
|
|
||||||
|
## Directory Organization
|
||||||
|
```
|
||||||
|
packages/twenty-front/src/
|
||||||
|
├── components/ # Reusable UI components
|
||||||
|
├── pages/ # Route components
|
||||||
|
├── modules/ # Feature modules
|
||||||
|
├── hooks/ # Custom hooks
|
||||||
|
├── services/ # API services
|
||||||
|
└── types/ # Type definitions
|
||||||
|
|
||||||
|
packages/twenty-server/src/
|
||||||
|
├── modules/ # Feature modules
|
||||||
|
├── entities/ # Database entities
|
||||||
|
├── dto/ # Data transfer objects
|
||||||
|
└── utils/ # Helper functions
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Naming
|
||||||
|
- **kebab-case** for all files and directories
|
||||||
|
- **Descriptive suffixes** for clarity
|
||||||
|
```
|
||||||
|
// ✅ Correct naming
|
||||||
|
user-profile.component.tsx
|
||||||
|
user-profile.styles.ts
|
||||||
|
user-profile.test.tsx
|
||||||
|
user.service.ts
|
||||||
|
user.entity.ts
|
||||||
|
create-user.dto.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Index Files & Barrel Exports
|
||||||
|
```typescript
|
||||||
|
// ✅ Clean barrel exports in index.ts
|
||||||
|
export { UserCard } from './user-card.component';
|
||||||
|
export { UserList } from './user-list.component';
|
||||||
|
export type { UserCardProps, UserListProps } from './types';
|
||||||
|
|
||||||
|
// ✅ Usage - clean imports
|
||||||
|
import { UserCard, UserList } from '@/components/user';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Module Structure
|
||||||
|
```
|
||||||
|
src/modules/user/
|
||||||
|
├── components/ # Module-specific components
|
||||||
|
├── hooks/ # Module hooks
|
||||||
|
├── services/ # API services
|
||||||
|
├── types/ # Type definitions
|
||||||
|
└── index.ts # Module exports
|
||||||
|
```
|
||||||
|
|
||||||
|
## Import/Export Patterns
|
||||||
|
```typescript
|
||||||
|
// ✅ Import organization
|
||||||
|
// 1. External libraries
|
||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
// 2. Internal modules (absolute paths)
|
||||||
|
import { Button } from '@/components/ui';
|
||||||
|
import { UserService } from '@/services';
|
||||||
|
|
||||||
|
// 3. Relative imports
|
||||||
|
import { UserCardProps } from './types';
|
||||||
|
|
||||||
|
// ✅ Named exports only (no default exports)
|
||||||
|
export const UserComponent = ({ user }: UserProps) => {
|
||||||
|
// Component implementation
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Size Guidelines
|
||||||
|
- **Components**: Under 300 lines
|
||||||
|
- **Services**: Under 500 lines
|
||||||
|
- **Extract logic** into hooks/utilities when files grow large
|
||||||
|
- **Use composition** over large monolithic components
|
||||||
|
|
||||||
|
## Configuration Files
|
||||||
|
|
||||||
|
### Project Configuration
|
||||||
|
```
|
||||||
|
.vscode/ # VSCode settings
|
||||||
|
├── settings.json
|
||||||
|
├── extensions.json
|
||||||
|
└── launch.json
|
||||||
|
|
||||||
|
.github/ # GitHub workflows
|
||||||
|
├── workflows/
|
||||||
|
└── templates/
|
||||||
|
|
||||||
|
.cursor/ # Cursor rules
|
||||||
|
├── rules/
|
||||||
|
└── environment.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Configuration
|
||||||
|
- Keep build configs in root or package directories
|
||||||
|
- Use consistent naming for config files
|
||||||
|
- Comment complex configurations
|
||||||
|
- Version control all configuration files
|
||||||
@ -3,31 +3,81 @@ description: Guidelines and best practices for working with Nx in the Twenty wor
|
|||||||
globs:
|
globs:
|
||||||
alwaysApply: false
|
alwaysApply: false
|
||||||
---
|
---
|
||||||
|
---
|
||||||
|
description: Guidelines and best practices for working with Nx in the Twenty workspace, including workspace architecture understanding, configuration management, and generator usage.
|
||||||
|
globs: ["**/nx.json", "**/project.json", "**/workspace.json"]
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
// This file is automatically generated by Nx Console
|
# Nx Guidelines
|
||||||
|
|
||||||
You are in an nx workspace using Nx 18.3.3 and yarn as the package manager.
|
## Core Commands
|
||||||
|
```bash
|
||||||
|
# Run target for specific project
|
||||||
|
npx nx run twenty-front:build
|
||||||
|
npx nx run twenty-server:test
|
||||||
|
|
||||||
You have access to the Nx MCP server and the tools it provides. Use them. Follow these guidelines in order to best help the user:
|
# Run target for all projects
|
||||||
|
npx nx run-many --target=build --all
|
||||||
|
npx nx run-many --target=test --projects=twenty-front,twenty-server
|
||||||
|
|
||||||
# General Guidelines
|
# Generate/modify projects
|
||||||
- When answering questions, use the nx_workspace tool first to gain an understanding of the workspace architecture
|
npx nx g @nx/react:app my-app
|
||||||
- For questions around nx configuration, best practices or if you're unsure, use the nx_docs tool to get relevant, up-to-date docs!! Always use this instead of assuming things about nx configuration
|
npx nx g @nx/react:component my-component
|
||||||
- If the user needs help with an Nx configuration or project graph error, use the 'nx_workspace' tool to get any errors
|
```
|
||||||
- To help answer questions about the workspace structure or simply help with demonstrating how tasks depend on each other, use the 'nx_visualize_graph' tool
|
|
||||||
|
## Project Structure
|
||||||
|
- Each package has a `project.json` with targets
|
||||||
|
- Dependencies managed through `tsconfig.json` path mappings
|
||||||
|
- Shared libraries in `packages/` directory
|
||||||
|
|
||||||
|
## Build Targets
|
||||||
|
```json
|
||||||
|
// project.json
|
||||||
|
{
|
||||||
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"executor": "@nx/vite:build",
|
||||||
|
"options": { "outputPath": "dist/packages/twenty-front" }
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"executor": "@nx/jest:jest",
|
||||||
|
"options": { "jestConfig": "packages/twenty-front/jest.config.ts" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependency Graph
|
||||||
|
```bash
|
||||||
|
# View project dependencies
|
||||||
|
npx nx graph
|
||||||
|
|
||||||
|
# Check what's affected by changes
|
||||||
|
npx nx affected --target=test
|
||||||
|
npx nx affected --target=build --base=main
|
||||||
|
```
|
||||||
|
|
||||||
|
## Library Management
|
||||||
|
- Use `npx nx g @nx/workspace:library` generator for shared libs
|
||||||
|
- Internal imports use `@/` path mapping
|
||||||
|
- Libraries must export through index.ts barrel files
|
||||||
|
|
||||||
|
## Cache Configuration
|
||||||
|
- Nx caches build outputs and test results
|
||||||
|
- Configure `outputs` in project.json targets
|
||||||
|
- Use `inputs` to define what invalidates cache
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"build": {
|
||||||
|
"outputs": ["dist/packages/my-app"],
|
||||||
|
"inputs": ["source", "^source"],
|
||||||
|
"cache": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
# Generation Guidelines
|
|
||||||
If the user wants to generate something, use the following flow:
|
|
||||||
|
|
||||||
- learn about the nx workspace and any specifics the user needs by using the 'nx_workspace' tool and the 'nx_project_details' tool if applicable
|
|
||||||
- get the available generators using the 'nx_generators' tool
|
|
||||||
- decide which generator to use. If no generators seem relevant, check the 'nx_available_plugins' tool to see if the user could install a plugin to help them
|
|
||||||
- get generator details using the 'nx_generator_schema' tool
|
|
||||||
- you may use the 'nx_docs' tool to learn more about a specific generator or technology if you're unsure
|
|
||||||
- decide which options to provide in order to best complete the user's request. Don't make any assumptions and keep the options minimalistic
|
|
||||||
- open the generator UI using the 'nx_open_generate_ui' tool
|
|
||||||
- wait for the user to finish the generator
|
|
||||||
- read the generator log file using the 'nx_read_generator_log' tool
|
|
||||||
- use the information provided in the log file to answer the user's question or continue with what they were doing
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,220 +0,0 @@
|
|||||||
# 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} />;
|
|
||||||
};
|
|
||||||
```
|
|
||||||
86
.cursor/rules/react-general-guidelines.mdc
Normal file
86
.cursor/rules/react-general-guidelines.mdc
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# React Guidelines
|
||||||
|
|
||||||
|
## Core Rules
|
||||||
|
- **Functional components only** (no classes)
|
||||||
|
- **Named exports only** (no default exports)
|
||||||
|
- **Event handlers over useEffect** for state updates
|
||||||
|
|
||||||
|
## Component Structure
|
||||||
|
```typescript
|
||||||
|
// ✅ Correct
|
||||||
|
export const UserProfile = ({ user, onEdit }: UserProfileProps) => {
|
||||||
|
const handleEdit = () => onEdit(user.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
<h1>{user.name}</h1>
|
||||||
|
<Button onClick={handleEdit}>Edit</Button>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props & Event Handlers
|
||||||
|
```typescript
|
||||||
|
// ✅ Correct - Destructure props
|
||||||
|
const Button = ({ onClick, isDisabled, children }: ButtonProps) => (
|
||||||
|
<button onClick={onClick} disabled={isDisabled}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
// ✅ Correct - Event handlers over useEffect
|
||||||
|
const UserForm = ({ onSubmit }: UserFormProps) => {
|
||||||
|
const handleSubmit = async (data: FormData) => {
|
||||||
|
await onSubmit(data);
|
||||||
|
// Direct event handling, not useEffect
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Form onSubmit={handleSubmit} />;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Design
|
||||||
|
- **Small, focused components** - Single responsibility
|
||||||
|
- **Composition over inheritance** - Combine simple components
|
||||||
|
- **Extract complex logic** into custom hooks
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Good - Composed from smaller components
|
||||||
|
const UserCard = ({ user }: UserCardProps) => (
|
||||||
|
<StyledCard>
|
||||||
|
<UserAvatar user={user} />
|
||||||
|
<UserInfo user={user} />
|
||||||
|
<UserActions user={user} />
|
||||||
|
</StyledCard>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
```typescript
|
||||||
|
// ✅ Use memo for expensive components only
|
||||||
|
const ExpensiveChart = memo(({ data }: ChartProps) => {
|
||||||
|
// Complex rendering logic
|
||||||
|
return <ComplexChart data={data} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ Memoize callbacks when needed
|
||||||
|
const UserList = ({ users, onUserSelect }: UserListProps) => {
|
||||||
|
const handleUserSelect = useCallback((user: User) => {
|
||||||
|
onUserSelect(user);
|
||||||
|
}, [onUserSelect]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{users.map(user => (
|
||||||
|
<UserItem key={user.id} user={user} onSelect={handleUserSelect} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
@ -1,219 +0,0 @@
|
|||||||
# 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: UUID!) {
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
```
|
|
||||||
81
.cursor/rules/react-state-management.mdc
Normal file
81
.cursor/rules/react-state-management.mdc
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# React State Management
|
||||||
|
|
||||||
|
## Recoil Patterns
|
||||||
|
```typescript
|
||||||
|
// ✅ Atoms for primitive state
|
||||||
|
export const currentUserState = atom<User | null>({
|
||||||
|
key: 'currentUserState',
|
||||||
|
default: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ Selectors for derived state
|
||||||
|
export const userDisplayNameSelector = selector({
|
||||||
|
key: 'userDisplayNameSelector',
|
||||||
|
get: ({ get }) => {
|
||||||
|
const user = get(currentUserState);
|
||||||
|
return user ? `${user.firstName} ${user.lastName}` : 'Guest';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ Atom families for dynamic atoms
|
||||||
|
export const userByIdState = atomFamily<User | null, string>({
|
||||||
|
key: 'userByIdState',
|
||||||
|
default: null,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Local State Guidelines
|
||||||
|
```typescript
|
||||||
|
// ✅ Multiple useState for unrelated state
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [data, setData] = useState<User[]>([]);
|
||||||
|
|
||||||
|
// ✅ useReducer for complex state logic
|
||||||
|
type FormAction =
|
||||||
|
| { type: 'SET_FIELD'; field: string; value: string }
|
||||||
|
| { type: 'SET_ERRORS'; errors: Record<string, string> }
|
||||||
|
| { type: 'RESET' };
|
||||||
|
|
||||||
|
const formReducer = (state: FormState, action: FormAction): FormState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'SET_FIELD':
|
||||||
|
return { ...state, [action.field]: action.value };
|
||||||
|
case 'SET_ERRORS':
|
||||||
|
return { ...state, errors: action.errors };
|
||||||
|
case 'RESET':
|
||||||
|
return initialFormState;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Flow Rules
|
||||||
|
- **Props down, events up** - Unidirectional data flow
|
||||||
|
- **Avoid bidirectional binding** - Use callback functions
|
||||||
|
- **Normalize complex data** - Use lookup tables over nested objects
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Normalized state structure
|
||||||
|
type UsersState = {
|
||||||
|
byId: Record<string, User>;
|
||||||
|
allIds: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// ✅ Functional state updates
|
||||||
|
const increment = useCallback(() => {
|
||||||
|
setCount(prev => prev + 1);
|
||||||
|
}, []);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Tips
|
||||||
|
- Use atom families for dynamic data collections
|
||||||
|
- Implement proper selector caching
|
||||||
|
- Avoid heavy computations in selectors
|
||||||
|
- Batch state updates when possible
|
||||||
@ -1,253 +0,0 @@
|
|||||||
# 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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
```
|
|
||||||
89
.cursor/rules/testing-guidelines.mdc
Normal file
89
.cursor/rules/testing-guidelines.mdc
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# Testing Guidelines
|
||||||
|
|
||||||
|
## Test Structure (AAA Pattern)
|
||||||
|
```typescript
|
||||||
|
describe('UserService', () => {
|
||||||
|
describe('when getting user by ID', () => {
|
||||||
|
it('should return user data for valid ID', async () => {
|
||||||
|
// Arrange
|
||||||
|
const userId = '123';
|
||||||
|
const expectedUser = { id: '123', name: 'John' };
|
||||||
|
mockUserRepository.findById.mockResolvedValue(expectedUser);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await userService.getUserById(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(expectedUser);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## React Component Testing
|
||||||
|
```typescript
|
||||||
|
// ✅ Test user behavior, not implementation
|
||||||
|
describe('LoginForm', () => {
|
||||||
|
it('should display error message for invalid credentials', async () => {
|
||||||
|
const mockOnSubmit = jest.fn().mockRejectedValue(new Error('Invalid credentials'));
|
||||||
|
render(<LoginForm onSubmit={mockOnSubmit} />);
|
||||||
|
|
||||||
|
await user.type(screen.getByLabelText(/email/i), 'invalid@example.com');
|
||||||
|
await user.type(screen.getByLabelText(/password/i), 'wrongpassword');
|
||||||
|
await user.click(screen.getByRole('button', { name: /sign in/i }));
|
||||||
|
|
||||||
|
expect(await screen.findByText(/invalid credentials/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mocking Patterns
|
||||||
|
```typescript
|
||||||
|
// ✅ Service mocking
|
||||||
|
const mockEmailService = {
|
||||||
|
sendEmail: jest.fn().mockResolvedValue({ success: true }),
|
||||||
|
validateEmail: jest.fn().mockReturnValue(true),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ✅ Test data factories
|
||||||
|
const createTestUser = (overrides = {}) => ({
|
||||||
|
id: uuid(),
|
||||||
|
email: 'test@example.com',
|
||||||
|
name: 'Test User',
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Principles
|
||||||
|
- **Test behavior, not implementation** - Focus on what users see/do
|
||||||
|
- **Use descriptive test names** - "should [behavior] when [condition]"
|
||||||
|
- **Query by user-visible elements** - text, roles, labels over test IDs
|
||||||
|
- **Keep tests isolated** - Independent and repeatable
|
||||||
|
- **70% unit, 20% integration, 10% E2E** - Test pyramid
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
```typescript
|
||||||
|
// Async testing
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Loading...')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// User interactions
|
||||||
|
await user.click(screen.getByRole('button'));
|
||||||
|
await user.type(screen.getByLabelText(/search/i), 'query');
|
||||||
|
|
||||||
|
// API integration tests
|
||||||
|
const response = await request(app)
|
||||||
|
.post('/api/users')
|
||||||
|
.send(userData)
|
||||||
|
.expect(201);
|
||||||
|
```
|
||||||
@ -1,162 +0,0 @@
|
|||||||
# 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
|
|
||||||
291
.cursor/rules/translations.mdc
Normal file
291
.cursor/rules/translations.mdc
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# Translation Guidelines
|
||||||
|
|
||||||
|
## Internationalization (i18n) Overview
|
||||||
|
|
||||||
|
### Supported Languages
|
||||||
|
- English (en) - Primary language
|
||||||
|
- French (fr) - Secondary language
|
||||||
|
- German (de) - Planned
|
||||||
|
- Spanish (es) - Planned
|
||||||
|
- Additional languages based on community contributions
|
||||||
|
|
||||||
|
### i18n Architecture
|
||||||
|
- Use react-i18next for React components
|
||||||
|
- Store translations in JSON files
|
||||||
|
- Implement namespace-based organization
|
||||||
|
- Support for interpolation and pluralization
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
### Translation Files
|
||||||
|
```
|
||||||
|
src/locales/
|
||||||
|
├── en/ # English translations
|
||||||
|
│ ├── common.json # Common UI strings
|
||||||
|
│ ├── auth.json # Authentication strings
|
||||||
|
│ ├── dashboard.json # Dashboard specific
|
||||||
|
│ ├── forms.json # Form labels and validation
|
||||||
|
│ └── errors.json # Error messages
|
||||||
|
├── fr/ # French translations
|
||||||
|
│ ├── common.json
|
||||||
|
│ ├── auth.json
|
||||||
|
│ └── ...
|
||||||
|
└── index.ts # i18n configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
### Translation Keys
|
||||||
|
- Use nested objects for organization
|
||||||
|
- Follow consistent naming patterns
|
||||||
|
- Include context in key names
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"auth": {
|
||||||
|
"login": {
|
||||||
|
"title": "Sign In",
|
||||||
|
"email": "Email Address",
|
||||||
|
"password": "Password",
|
||||||
|
"submit": "Sign In",
|
||||||
|
"forgotPassword": "Forgot Password?"
|
||||||
|
},
|
||||||
|
"register": {
|
||||||
|
"title": "Create Account",
|
||||||
|
"confirmPassword": "Confirm Password"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Translation Implementation
|
||||||
|
|
||||||
|
### React Components
|
||||||
|
- Use useTranslation hook
|
||||||
|
- Specify namespaces for better organization
|
||||||
|
- Handle loading states properly
|
||||||
|
```typescript
|
||||||
|
// ✅ Correct
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
const LoginForm = () => {
|
||||||
|
const { t } = useTranslation('auth');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form>
|
||||||
|
<h1>{t('login.title')}</h1>
|
||||||
|
<input
|
||||||
|
placeholder={t('login.email')}
|
||||||
|
type="email"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
placeholder={t('login.password')}
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
<button type="submit">
|
||||||
|
{t('login.submit')}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Interpolation
|
||||||
|
- Use interpolation for dynamic content
|
||||||
|
- Pass variables through t() function
|
||||||
|
- Keep interpolation simple and readable
|
||||||
|
```typescript
|
||||||
|
// ✅ Correct
|
||||||
|
const WelcomeMessage = ({ userName }: { userName: string }) => {
|
||||||
|
const { t } = useTranslation('common');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<h1>{t('welcome.message', { name: userName })}</h1>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Translation file
|
||||||
|
{
|
||||||
|
"welcome": {
|
||||||
|
"message": "Welcome back, {{name}}!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pluralization
|
||||||
|
- Handle singular/plural forms correctly
|
||||||
|
- Use count-based pluralization
|
||||||
|
- Support different plural rules per language
|
||||||
|
```typescript
|
||||||
|
// ✅ Correct
|
||||||
|
const ItemCount = ({ count }: { count: number }) => {
|
||||||
|
const { t } = useTranslation('common');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span>{t('items.count', { count })}</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Translation file
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"count_one": "{{count}} item",
|
||||||
|
"count_other": "{{count}} items"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Translation Management
|
||||||
|
|
||||||
|
### Adding New Strings
|
||||||
|
1. Add English translation first
|
||||||
|
2. Use descriptive keys that indicate context
|
||||||
|
3. Include comments for translators when needed
|
||||||
|
4. Test with long translations to ensure UI flexibility
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"user": {
|
||||||
|
"profile": {
|
||||||
|
// Displayed in user profile header
|
||||||
|
"displayName": "Display Name",
|
||||||
|
// Used in forms when editing profile
|
||||||
|
"editDisplayName": "Edit Display Name",
|
||||||
|
// Confirmation message after profile update
|
||||||
|
"updateSuccess": "Profile updated successfully"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Translation Validation
|
||||||
|
- Use TypeScript for translation key validation
|
||||||
|
- Implement automated checks for missing translations
|
||||||
|
- Validate interpolation parameters
|
||||||
|
```typescript
|
||||||
|
// ✅ Correct - Type-safe translations
|
||||||
|
type TranslationKey =
|
||||||
|
| 'auth.login.title'
|
||||||
|
| 'auth.login.email'
|
||||||
|
| 'auth.login.password'
|
||||||
|
| 'common.welcome.message';
|
||||||
|
|
||||||
|
const t = (key: TranslationKey, options?: any) => {
|
||||||
|
// Translation implementation
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Key Naming
|
||||||
|
- Use descriptive, hierarchical keys
|
||||||
|
- Avoid abbreviations
|
||||||
|
- Group related translations
|
||||||
|
- Keep keys consistent across languages
|
||||||
|
```json
|
||||||
|
// ✅ Correct
|
||||||
|
{
|
||||||
|
"dashboard": {
|
||||||
|
"header": {
|
||||||
|
"title": "Dashboard",
|
||||||
|
"subtitle": "Welcome to your workspace"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"createNew": "Create New",
|
||||||
|
"refresh": "Refresh Data",
|
||||||
|
"export": "Export"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ Incorrect
|
||||||
|
{
|
||||||
|
"dash_title": "Dashboard",
|
||||||
|
"newBtn": "New",
|
||||||
|
"refreshData": "Refresh"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### String Guidelines
|
||||||
|
- Write clear, concise text
|
||||||
|
- Use consistent terminology
|
||||||
|
- Consider character limits for UI elements
|
||||||
|
- Avoid concatenating translated strings
|
||||||
|
```json
|
||||||
|
// ✅ Correct
|
||||||
|
{
|
||||||
|
"user": {
|
||||||
|
"status": {
|
||||||
|
"online": "Online",
|
||||||
|
"offline": "Offline",
|
||||||
|
"away": "Away"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ Incorrect - Don't concatenate
|
||||||
|
{
|
||||||
|
"user": {
|
||||||
|
"statusPrefix": "User is ",
|
||||||
|
"statusOnline": "online"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Context Information
|
||||||
|
- Provide context for translators
|
||||||
|
- Include character limits when relevant
|
||||||
|
- Explain when/where text appears
|
||||||
|
- Note any technical constraints
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"button": {
|
||||||
|
// Primary action button, max 20 characters
|
||||||
|
"save": "Save Changes",
|
||||||
|
// Secondary button in modal footer
|
||||||
|
"cancel": "Cancel",
|
||||||
|
// Destructive action, should sound cautious
|
||||||
|
"delete": "Delete Permanently"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### Development Process
|
||||||
|
1. Develop features with English translations
|
||||||
|
2. Use placeholder keys during development
|
||||||
|
3. Finalize translation keys before feature completion
|
||||||
|
4. Add translations to all supported languages
|
||||||
|
5. Test with different language strings
|
||||||
|
|
||||||
|
### Translation Updates
|
||||||
|
1. Create translation tasks for new features
|
||||||
|
2. Provide context and screenshots to translators
|
||||||
|
3. Review translations for consistency
|
||||||
|
4. Test UI with translated strings
|
||||||
|
5. Update documentation when needed
|
||||||
|
|
||||||
|
### Quality Assurance
|
||||||
|
- Review translations in context
|
||||||
|
- Test with longest expected translations
|
||||||
|
- Verify formatting with interpolation
|
||||||
|
- Check for cultural appropriateness
|
||||||
|
- Ensure accessibility with screen readers
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
|
||||||
|
### Regular Tasks
|
||||||
|
- Review and update outdated translations
|
||||||
|
- Check for unused translation keys
|
||||||
|
- Maintain consistency across languages
|
||||||
|
- Update translation documentation
|
||||||
|
- Monitor for missing translations in new features
|
||||||
|
|
||||||
|
### Tools and Automation
|
||||||
|
- Use automated translation validation
|
||||||
|
- Implement missing translation detection
|
||||||
|
- Set up continuous integration checks
|
||||||
|
- Maintain translation coverage reports
|
||||||
|
- Use translation management platforms when needed
|
||||||
@ -1,3 +1,14 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
---
|
||||||
|
description: TypeScript best practices and conventions for the Twenty codebase, including strict typing, naming conventions, and type safety guidelines.
|
||||||
|
globs: ["**/*.ts", "**/*.tsx"]
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
# TypeScript Guidelines
|
# TypeScript Guidelines
|
||||||
|
|
||||||
## Core TypeScript Principles
|
## Core TypeScript Principles
|
||||||
@ -169,4 +180,4 @@ Twenty enforces strict TypeScript usage to ensure type safety and maintainable c
|
|||||||
type NonNullableProperties<T> = {
|
type NonNullableProperties<T> = {
|
||||||
[P in keyof T]: NonNullable<T[P]>;
|
[P in keyof T]: NonNullable<T[P]>;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
Reference in New Issue
Block a user