Compare commits

..
18 Commits
Author SHA1 Message Date
Félix Malfait 14b83aa3d0 Bump version 2024-12-01 11:14:34 +01:00
Félix MalfaitandFélix Malfait 922ad2e1d7 Fix 1-click install tag version (#8709)
Followup of #8689
2024-12-01 11:10:02 +01:00
Charles Bochet 2746670ff3 Fix activity target picker 2024-11-29 15:34:11 +01:00
Charles Bochet 65263b85c2 Bump version 2024-11-29 15:33:49 +01:00
Weiko 95815a417b Bump 0.33.4 2024-11-26 15:45:14 +01:00
Raphaël BosiandWeiko bc5c5f3fb9 Gmail error handling fixes (#8732)
Gmail error handling fixes
2024-11-26 15:33:41 +01:00
Weiko 7b7dab66ed Fix labelIdentifierFieldMetadata creation for custom objects (#8729) 2024-11-26 15:33:35 +01:00
Weiko d3ffbaba30 Set missing labelIdentifier to custom objects (#8750)
## Context
Following https://github.com/twentyhq/twenty/pull/8729

This command backfills missing labelIdentifier for custom objects
2024-11-26 15:33:29 +01:00
MarieandWeiko 59b7f1ba68 Fix custom object renaming (#8746)
Currently when renaming an object, we execute
```
await this.fieldMetadataRepository
                    .findOneByOrFail({
                      name: existingObjectMetadata.nameSingular,
                      label: existingObjectMetadata.labelSingular,
                      objectMetadataId: relatedObject.id,
                      workspaceId: workspaceId,
                    })
```
to find the standard relation fields. 
This would throw an error if the label solely was update beforehand
without updating the name too: in that case we will not have migrated
the label of the standard relation fields (which is maybe a mistake?
@Weiko wdyt?).
Let's remove it.
2024-11-26 15:33:22 +01:00
MarieandWeiko fd3e80aaa1 Fix update of custom object icon (#8730)
Icon update was not triggering a save due to missing onBlur prop drill
2024-11-26 15:33:14 +01:00
212d3dc59d Fix: open filter from column (#8747)
Column filter button (image below) was broken for all filter types, this
PR fixes it.

<img width="1053" alt="broken-filter-button"
src="https://github.com/user-attachments/assets/febd10a8-f360-4245-ba06-ef847c79fde1">

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
2024-11-26 15:33:07 +01:00
Weiko 18a13d2175 Fix mutations with camelCase table names (#8740)
## Context
Some mutations are not working properly, workspaceMember soft deletion
for example. workspaceMember being a camelCase table name, it's probably
not propagated properly to pgql (which needs double quote for the table
name to keep it as camelCase)

I didn't have time to dig too much but if the `where` is before
`softDelete`, the query is `WHERE workspaceMember.id = $1` while if it's
after, the query becomes `WHERE id = $1`.
Probably due to the fact that once you call delete/softDelete/update,
the standard builder (SelectQueryBuilder) becomes a
DeleteQueryBuilder/etc... and filters are not handled the same way.
2024-11-26 15:33:01 +01:00
Weiko 726bb56bb6 add delete view fields without views command (#8728)
## Context
We recently added a command to ensure uniqueness on the viewId column in
the viewField table. This created some issues for some old workspaces
that had viewFields with an empty viewId.
This command should get rid of those and set the column as non-nullable.
Also updating the onDelete action accordingly and set one missing for
FavoriteFolder
2024-11-26 15:32:55 +01:00
Félix MalfaitandWeiko de46f5440a Fix Error field type rich text (#8739)
fix #8445

It seems linked to commandBar search where we filter tasks/notes by body
with ilike.
2024-11-26 15:32:43 +01:00
nitinandWeiko c266ce4859 Email invite design improvements (#8681)
closes #7140 

![image](https://github.com/user-attachments/assets/d3a31a49-8b37-4456-98e3-a16fbccb3786)
2024-11-26 15:32:32 +01:00
Weiko 2e8fd6cd99 Bump 0.33.3 2024-11-22 18:32:13 +01:00
Weiko 9f55ce1889 Fix mutations with custom objects (#8688) 2024-11-22 18:18:33 +01:00
Weiko c94b79a8a7 Bump 0.33.2 2024-11-22 15:34:38 +01:00
12795 changed files with 223401 additions and 1186853 deletions
-18
View File
@@ -1,18 +0,0 @@
{
"install": "curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash - && sudo apt-get install -y nodejs && node --version && yarn install && echo 'Setting up Docker Compose environment...' && cd packages/twenty-docker && cp -n docker-compose.yml docker-compose.dev.yml || true && echo 'Dependencies installed and docker-compose prepared'",
"start": "sudo service docker start && echo 'Docker service started' && cd packages/twenty-docker && echo 'Installing yq for YAML processing...' && sudo apt-get update -qq && sudo apt-get install -y wget && wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 && sudo chmod +x /usr/local/bin/yq && echo 'Patching docker-compose for local development...' && yq eval 'del(.services.server.image)' -i docker-compose.dev.yml && yq eval '.services.server.build.context = \"../../\"' -i docker-compose.dev.yml && yq eval '.services.server.build.dockerfile = \"./packages/twenty-docker/twenty/Dockerfile\"' -i docker-compose.dev.yml && yq eval 'del(.services.worker.image)' -i docker-compose.dev.yml && yq eval '.services.worker.build.context = \"../../\"' -i docker-compose.dev.yml && yq eval '.services.worker.build.dockerfile = \"./packages/twenty-docker/twenty/Dockerfile\"' -i docker-compose.dev.yml && echo 'Setting up .env file with database configuration...' && echo 'SERVER_URL=http://localhost:3000' > .env && echo 'APP_SECRET='$(openssl rand -base64 32) >> .env && echo 'PG_DATABASE_PASSWORD='$(openssl rand -hex 16) >> .env && echo 'PG_DATABASE_URL=postgres://postgres:password@localhost:5432/postgres' >> .env && echo 'SIGN_IN_PREFILLED=true' >> .env && echo 'Building and starting services...' && docker-compose -f docker-compose.dev.yml up -d --build && echo 'Waiting for services to initialize...' && sleep 30 && echo 'Checking service health...' && docker-compose -f docker-compose.dev.yml ps && echo 'Environment setup complete!'",
"terminals": [
{
"name": "Database Setup & Seed",
"command": "sleep 40 && cd packages/twenty-docker && echo 'Waiting for PostgreSQL to be ready...' && until docker-compose -f docker-compose.dev.yml exec -T db pg_isready -U postgres; do echo 'Waiting for PostgreSQL...'; sleep 5; done && echo 'PostgreSQL is ready!' && echo 'Waiting for Twenty server to be healthy...' && until docker-compose -f docker-compose.dev.yml exec -T server curl --fail http://localhost:3000/healthz 2>/dev/null; do echo 'Waiting for server...'; sleep 5; done && echo 'Server is healthy!' && echo 'Running database setup and seeding...' && docker-compose -f docker-compose.dev.yml exec -T server npx nx database:reset twenty-server && echo 'Database seeded successfully!' && bash"
},
{
"name": "Application Logs",
"command": "sleep 35 && cd packages/twenty-docker && echo 'Following application logs...' && docker-compose -f docker-compose.dev.yml logs -f server worker"
},
{
"name": "Service Monitor",
"command": "sleep 15 && cd packages/twenty-docker && echo '=== Service Status Monitor ===' && while true; do clear; echo '=== Service Status at $(date) ===' && docker-compose -f docker-compose.dev.yml ps && echo '\\n=== Health Status ===' && (docker-compose -f docker-compose.dev.yml exec -T server curl -s http://localhost:3000/healthz 2>/dev/null && echo '✅ Twenty Server: Healthy') || echo '❌ Twenty Server: Not Ready' && (docker-compose -f docker-compose.dev.yml exec -T db pg_isready -U postgres 2>/dev/null && echo '✅ PostgreSQL: Ready') || echo '❌ PostgreSQL: Not Ready' && echo '\\n=== Database Connection Test ===' && docker-compose -f docker-compose.dev.yml exec -T server node -e \"const { Client } = require('pg'); const client = new Client({connectionString: process.env.PG_DATABASE_URL}); client.connect().then(() => {console.log('✅ Database Connection: OK'); client.end();}).catch(e => console.log('❌ Database Connection: Failed -', e.message));\" || echo 'Connection test failed' && sleep 45; done"
}
]
}
-18
View File
@@ -1,18 +0,0 @@
{
"install": "curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash - && sudo apt-get install -y nodejs && node --version && yarn install && echo 'Installing dependencies complete'",
"start": "sudo service docker start && echo 'Docker service started' && sleep 3 && echo 'Starting PostgreSQL and Redis containers...' && make postgres-on-docker && make redis-on-docker && echo 'Waiting for containers to initialize...' && sleep 20 && echo 'Checking container status...' && docker ps --filter name=twenty_ && echo 'Waiting for PostgreSQL to be ready...' && until docker exec twenty_pg pg_isready -U postgres -h localhost; do echo 'PostgreSQL not ready yet, waiting...'; sleep 3; done && echo 'PostgreSQL is ready!' && echo 'Setting up database...' && cd packages/twenty-server && npx nx database:reset twenty-server || echo 'Database already initialized' && echo 'Environment setup complete!'",
"terminals": [
{
"name": "Development Server",
"command": "echo 'Waiting for database to be fully ready...' && sleep 30 && until docker exec twenty_pg pg_isready -U postgres -h localhost; do echo 'Waiting for PostgreSQL...'; sleep 2; done && echo 'Starting Twenty development server...' && export SERVER_URL=http://localhost:3000 && export PG_DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres && yarn start"
},
{
"name": "Database Management",
"command": "sleep 25 && echo 'Database management terminal ready' && echo 'Waiting for PostgreSQL to be available...' && until docker exec twenty_pg pg_isready -U postgres -h localhost; do echo 'Waiting for PostgreSQL...'; sleep 2; done && echo 'PostgreSQL is ready for database operations!' && echo 'You can now run database commands like:' && echo ' npx nx database:reset twenty-server' && echo ' npx nx database:migrate twenty-server' && bash"
},
{
"name": "Container Logs & Status",
"command": "sleep 10 && echo '=== Container Status Monitor ===' && while true; do echo '\\n=== Container Status at $(date) ===' && docker ps --filter name=twenty_ --format 'table {{.Names}}\\t{{.Status}}\\t{{.Ports}}' && echo '\\n=== PostgreSQL Status ===' && (docker exec twenty_pg pg_isready -U postgres -h localhost && echo 'PostgreSQL: ✅ Ready') || echo 'PostgreSQL: ❌ Not Ready' && echo '\\n=== Redis Status ===' && (docker exec twenty_redis redis-cli ping && echo 'Redis: ✅ Ready') || echo 'Redis: ❌ Not Ready' && sleep 30; done"
}
]
}
-137
View File
@@ -1,137 +0,0 @@
---
description: Twenty CRM development rules and best practices
globs: []
alwaysApply: true
---
# 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.
-30
View File
@@ -1,30 +0,0 @@
---
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
-117
View File
@@ -1,117 +0,0 @@
---
description: Code style guidelines for Twenty CRM
globs: []
alwaysApply: true
---
# 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
// ✅ Use short-form comments, NOT JSDoc blocks
// ✅ 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;
// ✅ Multi-line comments use multiple // lines (NOT /** */ blocks)
// Calculates the total price after applying tax and discount
// Returns the final price that should be charged to the customer
const calculateTotalPrice = (basePrice: number): number => {
// Implementation
};
// ❌ AVOID JSDoc blocks - use short comments instead
/**
* This style is NOT preferred in this codebase
*/
```
## Security Patterns
```typescript
// ✅ CSV Export: Always apply security first, then formatting
const safeValue = formatValueForCSV(sanitizeValueForCSVExport(userInput));
// ✅ Input validation before processing
const sanitizedInput = validateAndSanitize(userInput);
const result = processData(sanitizedInput);
```
## 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;
}
```
-100
View File
@@ -1,100 +0,0 @@
---
description: Guidelines for incorporating user feedback and improving cursor rules
globs: []
alwaysApply: true
---
# Feedback Incorporation Guidelines
## Post-Interaction Reflection
After each coding session or significant interaction, the AI should:
### 1. Reflect on User Feedback
- **Identify patterns** in user corrections or suggestions
- **Note recurring issues** that could be prevented with better rules
- **Recognize gaps** in current cursor rules or guidelines
### 2. Suggest Rule Improvements
When user provides feedback that reveals a pattern or preference:
```typescript
// Example feedback patterns to watch for:
// - "We don't use useEffect, handle state changes in event callbacks"
// - "We don't use JSDoc blocks, prefer // comments"
// - "Always use named exports, never default exports"
// - "We prefer functional components over class components"
// - "Use event handlers over useEffect for state updates"
```
### 3. Proactive Rule Suggestions
At the end of interactions, suggest:
```markdown
## 💡 Suggested Cursor Rule Updates
Based on your feedback today, I recommend adding/updating these rules:
**Code Style Rule Update:**
- Add preference for // comments over JSDoc blocks
- Enforce named exports only (no default exports)
**React Guidelines Update:**
- Document preference for event handlers over useEffect
- Add functional components only rule
Would you like me to help incorporate these into your cursor rules?
```
## Implementation Process
### When to Suggest Updates
- User corrects the same type of mistake multiple times
- User explains a codebase-specific preference
- User points out missing functionality or incomplete implementations
- User provides context about existing patterns not captured in rules
### How to Present Suggestions
1. **Summarize the pattern** observed from feedback
2. **Propose specific rule language** that would prevent the issue
3. **Explain the benefit** of codifying this knowledge
4. **Ask for confirmation** before implementing
### Rule Categories to Consider
- **Code Style**: Formatting, naming, comment styles, export patterns
- **React Patterns**: Hook usage, component structure, state management
- **Architecture**: File organization, import patterns, component composition
- **Testing**: Test structure, naming, coverage expectations
- **Performance**: Optimization patterns, anti-patterns to avoid
## Example Feedback Integration
```markdown
## Today's Learning: React State Management Patterns
**User Feedback Received:**
- "We don't use useEffect, handle state changes in event callbacks"
- "We don't use JSDoc blocks, prefer // comments"
- "Always use named exports, never default exports"
**Proposed Rule Additions:**
```typescript
// ✅ React State Updates - Use event handlers, not useEffect
const handleButtonClick = () => {
setData(newData); // Direct state update in event handler
};
// ❌ Avoid useEffect for state updates
// useEffect(() => { setData(newData); }, [trigger]);
// ✅ Named exports only
export const UserComponent = () => {};
export const useUserData = () => {};
```
**Benefits:**
- Prevents useEffect overuse and related bugs
- Ensures consistent export patterns across codebase
- Documents preferred React patterns for the team
```
This approach helps the AI learn from each interaction and continuously improve the development experience.
-107
View File
@@ -1,107 +0,0 @@
---
description: File structure guidelines for Twenty CRM
globs: []
alwaysApply: true
---
# 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
-78
View File
@@ -1,78 +0,0 @@
---
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: true
---
# Nx Guidelines
## Core Commands
```bash
# Run target for specific project
npx nx run twenty-front:build
npx nx run twenty-server:test
# Run target for all projects
npx nx run-many --target=build --all
npx nx run-many --target=test --projects=twenty-front,twenty-server
# Generate/modify projects
npx nx g @nx/react:app my-app
npx nx g @nx/react:component my-component
```
## 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.mjs" }
}
}
}
```
## 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
}
}
```
@@ -1,85 +0,0 @@
---
description: React general guidelines for Twenty CRM
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>
);
};
```
-80
View File
@@ -1,80 +0,0 @@
---
description: React state management guidelines for Twenty CRM
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
-127
View File
@@ -1,127 +0,0 @@
---
description: Testing guidelines for Twenty CRM
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
## Running Tests
### Single Test File Execution
```bash
# ✅ Run a specific test file (PREFERRED - Fast & Efficient)
npx jest path/to/test.test.ts --config=packages/PROJECT/jest.config.mjs
# Key Benefits:
# - Only runs the specific test file (fast)
# - No dependency resolution overhead
# - Immediate feedback for test development
# ✅ Examples:
# Frontend tests (use .test.ts extension)
npx jest packages/twenty-front/src/modules/localization/utils/detection/detectNumberFormat.test.ts --config=packages/twenty-front/jest.config.mjs
# Server tests (use .spec.ts extension)
npx jest packages/twenty-server/src/utils/__test__/is-work-email.spec.ts --config=packages/twenty-server/jest.config.mjs
# ❌ AVOID - This runs ALL tests (slow):
npx nx test twenty-front --testPathPattern=detectNumberFormat.test.ts
# ✅ Run tests in watch mode for development:
npx jest path/to/test.test.ts --config=packages/twenty-front/jest.config.mjs --watch
# ✅ Run with coverage for single file:
npx jest path/to/test.test.ts --config=packages/twenty-front/jest.config.mjs --coverage
```
### Test Suite Execution
```bash
# Run all tests for a project (use sparingly)
npx nx test twenty-front
npx nx test twenty-server
# Run tests matching a pattern
npx jest --testNamePattern="UserService" --config=packages/twenty-front/jest.config.mjs
```
## 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);
```
-290
View File
@@ -1,290 +0,0 @@
---
description: Translation guidelines for Twenty CRM
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
-182
View File
@@ -1,182 +0,0 @@
---
description: TypeScript best practices and conventions for the Twenty codebase, including strict typing, naming conventions, and type safety guidelines.
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
## 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]>;
};
```
-1
View File
@@ -2,4 +2,3 @@
.env
node_modules
.nx/cache
packages/twenty-server/.env
+133
View File
@@ -0,0 +1,133 @@
module.exports = {
root: true,
extends: ['plugin:prettier/recommended'],
plugins: ['@nx', 'prefer-arrow', 'import', 'unused-imports', 'unicorn'],
rules: {
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
'no-console': ['warn', { allow: ['group', 'groupCollapsed', 'groupEnd'] }],
'no-control-regex': 0,
'no-debugger': 'error',
'no-duplicate-imports': 'error',
'no-undef': 'off',
'no-unused-vars': 'off',
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{
sourceTag: 'scope:shared',
onlyDependOnLibsWithTags: ['scope:shared'],
},
{
sourceTag: 'scope:backend',
onlyDependOnLibsWithTags: ['scope:shared', 'scope:backend'],
},
{
sourceTag: 'scope:frontend',
onlyDependOnLibsWithTags: ['scope:shared', 'scope:frontend'],
},
],
},
],
'import/no-relative-packages': 'error',
'import/no-useless-path-segments': 'error',
'import/no-duplicates': ['error', { considerQueryString: true }],
'prefer-arrow/prefer-arrow-functions': [
'error',
{
disallowPrototype: true,
singleReturnOnly: false,
classPropertiesAllowed: false,
},
],
'unused-imports/no-unused-imports': 'warn',
'unused-imports/no-unused-vars': [
'warn',
{
vars: 'all',
varsIgnorePattern: '^_',
args: 'after-used',
argsIgnorePattern: '^_',
},
],
},
overrides: [
{
files: ['**/*.ts', '**/*.tsx'],
extends: ['plugin:@nx/typescript'],
rules: {
'@typescript-eslint/ban-ts-comment': 'error',
'@typescript-eslint/consistent-type-imports': [
'error',
{ prefer: 'no-type-imports' },
],
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-empty-interface': [
'error',
{
allowSingleExtends: true,
},
],
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-unused-vars': [
'warn',
{
vars: 'all',
varsIgnorePattern: '^_',
args: 'after-used',
argsIgnorePattern: '^_',
},
],
},
},
{
files: ['*.js', '*.jsx'],
extends: ['plugin:@nx/javascript'],
rules: {},
},
{
files: ['*.spec.@(ts|tsx|js|jsx)', '*.test.@(ts|tsx|js|jsx)'],
env: {
jest: true,
},
rules: {
'@typescript-eslint/no-non-null-assertion': 'off',
},
},
{
files: ['**/constants/*.ts', '**/*.constants.ts'],
rules: {
'@typescript-eslint/naming-convention': [
'error',
{
selector: 'variable',
format: ['UPPER_CASE'],
},
],
'unicorn/filename-case': [
'warn',
{
cases: {
pascalCase: true,
},
},
],
'@nx/workspace-max-consts-per-file': ['error', { max: 1 }],
},
},
{
files: ['*.json'],
parser: 'jsonc-eslint-parser',
},
],
};
+90
View File
@@ -0,0 +1,90 @@
var path = require('path');
module.exports = {
extends: [
'plugin:@nx/react',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:storybook/recommended',
],
plugins: ['react-hooks', 'react-refresh'],
overrides: [
{
files: ['*.ts', '*.tsx'],
parserOptions: {
project: ['./tsconfig.base.{json,*.json}'],
},
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['@tabler/icons-react'],
message: 'Please import icons from `twenty-ui`',
},
{
group: ['react-hotkeys-web-hook'],
importNames: ['useHotkeys'],
message:
'Please use the custom wrapper: `useScopedHotkeys` from `twenty-ui`',
},
{
group: ['lodash'],
message:
"Please use the standalone lodash package (for instance: `import groupBy from 'lodash.groupby'` instead of `import { groupBy } from 'lodash'`)",
},
],
},
],
'@nx/workspace-effect-components': 'error',
'@nx/workspace-no-hardcoded-colors': 'error',
'@nx/workspace-matching-state-variable': 'error',
'@nx/workspace-sort-css-properties-alphabetically': 'error',
'@nx/workspace-styled-components-prefixed-with-styled': 'error',
'@nx/workspace-no-state-useref': 'error',
'@nx/workspace-component-props-naming': 'error',
'@nx/workspace-explicit-boolean-predicates-in-if': 'error',
'@nx/workspace-use-getLoadable-and-getValue-to-get-atoms': 'error',
'@nx/workspace-useRecoilCallback-has-dependency-array': 'error',
'@nx/workspace-no-navigate-prefer-link': 'error',
'react/no-unescaped-entities': 'off',
'react/prop-types': 'off',
'react/jsx-key': 'off',
'react/display-name': 'off',
'react/jsx-uses-react': 'off',
'react/react-in-jsx-scope': 'off',
'react/jsx-no-useless-fragment': 'off',
'react/jsx-props-no-spreading': [
'error',
{
explicitSpread: 'ignore',
},
],
'react-hooks/exhaustive-deps': [
'warn',
{
additionalHooks: 'useRecoilCallback',
},
],
},
},
{
files: ['*.stories.@(ts|tsx|js|jsx)'],
rules: {
'@typescript-eslint/no-non-null-assertion': 'off',
},
},
{
files: ['.storybook/main.@(js|cjs|mjs|ts)'],
rules: {
'storybook/no-uninstalled-addons': [
'error',
{
packageJsonLocation: path.resolve(__dirname, './package.json'),
},
],
},
},
],
};
-28
View File
@@ -1,28 +0,0 @@
* text=auto eol=lf
*.ts text eol=lf
*.tsx text eol=lf
*.js text eol=lf
*.jsx text eol=lf
*.json text eol=lf
*.md text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.sh text eol=lf
*.mjs text eol=lf
*.cjs text eol=lf
# Patch files may have mixed line endings by design
*.patch -text
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.svg binary
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary
+8 -4
View File
@@ -13,10 +13,14 @@ Good first issues are a great way to start contributing and get familiar with th
## Issue assignment
To avoid conflicts, we follow these guidelines:
Having multiple contributors address the same issue can cause frustration.
1. For `Good First Issue` and `Experienced Contributor` issues without `size: long` labels, we'll merge the first PRs that meet our [code quality standards](https://twenty.com/developers). **We don't assign contributors to these issues**. For `priority: high` issues, our core team will step in within days if no adequate contributions are received.
2. For `size: long` Issues, assigned contributors have one week to submit their first draft PR.
To avoid conflicts, we follow these guidelines:
1. If a core team member assigned you the issue within the last three days, your PR takes priority.
2. Otherwise, the first submitted PR is prioritized.
3. For "size: long" PRs, the assignment period extends to one week.
Please ensure you're assigned to an issue before starting work.
## How to Contribute
@@ -63,4 +67,4 @@ git push origin your-branch-name
## Reporting Issues
If you face any issues or have suggestions, please feel free to (create an issue on Twenty's GitHub repository)[https://github.com/twentyhq/twenty/issues/new]. Please provide as much detail as possible.
If you face any issues or have suggestions, please feel free to (create an issue on Twenty's GitHub repository)[https://github.com/twentyhq/twenty/issues/new]. Please provide as much detail as possible.
@@ -0,0 +1,33 @@
name: oss.gg hack submission 🕹️
description: "Submit your contribution for the for the oss.gg hackathon"
title: "[🕹️]"
labels: 🕹️ oss.gg, player submission, hacktoberfest
assignees: []
body:
- type: textarea
id: contribution-name
attributes:
label: What side quest or challenge are you solving?
description: Add the name of the side quest or challenge.
validations:
required: true
- type: textarea
id: points
attributes:
label: Points
description: How many points are assigned to this contribution?
validations:
required: true
- type: textarea
id: description
attributes:
label: Description
description: What's the task your performed?
validations:
- type: textarea
id: proof
attributes:
label: Provide proof that you've completed the task
description: Screenshots, loom recordings, links to the content you shared or interacted with.
validations:
required: true
@@ -27,7 +27,7 @@ Having a list that is draggable will be useful, not only in dropdown.
Create a folder @/ui/draggable-list with a DraggableList component
This component should take as prop: itemsComponents, onDragEnd((previousIndex, nextIndex) => {})
Use this component in ObjectOptionsDropdownHiddenFieldsContent (move the logic from ObjectOptionsDropdownHiddenFieldsContent to DraggableList) by passing a list of DraggableMenuItems
Use this component in ViewFieldsVisibilityDropdownSection (move the logic from ViewFieldsVisibilityDropdownSection to DraggableList) by passing a list of DraggableMenuItems
Add a storybook test on this list (we don't know how to actually test the draggable behavior, but we can at least make sure the component renders correctly a list of items)
```
@@ -2,16 +2,15 @@ name: Nx Affected CI
inputs:
parallel:
required: false
default: '3'
types: [number]
default: 3
tag:
required: false
types: [string]
tasks:
required: true
configuration:
required: false
default: 'ci'
args:
required: false
types: [string]
runs:
using: "composite"
steps:
@@ -19,4 +18,4 @@ runs:
uses: nrwl/nx-set-shas@v4
- name: Run affected command
shell: bash
run: npx nx affected --nxBail --configuration=${{ inputs.configuration }} -t=${{ inputs.tasks }} --parallel=${{ inputs.parallel }} --exclude='*,!tag:${{ inputs.tag }}' ${{ inputs.args }}
run: npx nx affected --nxBail --configuration=ci -t=${{ inputs.tasks }} --parallel=${{ inputs.parallel }} --exclude='*,!tag:${{ inputs.tag }}'
@@ -1,35 +0,0 @@
name: Restore cache
inputs:
key:
required: true
description: Prefix to the cache key
additional-paths:
required: false
outputs:
cache-primary-key:
description: actions/cache/restore cache-primary-key outputs proxy
value: ${{ steps.restore-cache.outputs.cache-primary-key }}
cache-hit:
description: String bool indicating whether cache has been directly or indirectly hit
value: ${{ steps.restore-cache.outputs.cache-hit == 'true' || steps.restore-cache.outputs.cache-matched-key != '' }}
runs:
using: composite
steps:
- name: Cache primary key builder
id: cache-primary-key-builder
shell: bash
run: |
echo "CACHE_PRIMARY_KEY_PREFIX=v3-${{ inputs.key }}-${{ github.ref_name }}" >> "${GITHUB_OUTPUT}"
- name: Restore cache
uses: actions/cache/restore@v4
id: restore-cache
with:
key: ${{ steps.cache-primary-key-builder.outputs.CACHE_PRIMARY_KEY_PREFIX }}-${{ github.sha }}
restore-keys: ${{ steps.cache-primary-key-builder.outputs.CACHE_PRIMARY_KEY_PREFIX }}-
path: |
.cache
.nx
node_modules/.cache
packages/*/node_modules/.cache
${{ inputs.additional-paths }}
@@ -1,21 +0,0 @@
name: Save cache
inputs:
key:
required: true
description: Primary key to the cache, should be retrieved from `cache-restore` composite action outputs.
additional-paths:
required: false
runs:
using: "composite"
steps:
- name: Save cache
uses: actions/cache/save@v4
with:
key: ${{ inputs.key }}
path: |
.cache
.nx
node_modules/.cache
packages/*/node_modules/.cache
${{ inputs.additional-paths }}
@@ -0,0 +1,31 @@
name: Restore Tasks Cache CI
inputs:
tag:
required: false
types: [string]
tasks:
required: false
types: [string]
default: all
suffix:
required: false
types: [string]
runs:
using: "composite"
steps:
- name: Compute tasks key
id: tasks-key
shell: bash
run: echo "key=${{ inputs.tasks }}" | tr , - >> $GITHUB_OUTPUT
- name: Restore tasks cache
uses: actions/cache@v3
with:
path: |
.cache
.nx/cache
node_modules/.cache
packages/*/node_modules/.cache
key: tasks-cache-${{ github.ref_name }}-${{ inputs.tag }}-${{ steps.tasks-key.outputs.key }}${{ inputs.suffix }}-${{ github.sha }}
restore-keys: |
tasks-cache-${{ github.ref_name }}-${{ inputs.tag }}-${{ steps.tasks-key.outputs.key }}${{ inputs.suffix }}-
@@ -1,42 +1,23 @@
name: Yarn Install
inputs:
node-version:
required: false
default: '24'
runs:
using: 'composite'
using: "composite"
steps:
- name: Cache primary key builder
id: globals
shell: bash
run: |
echo "ACTION_SHELL=bash" >> "${GITHUB_OUTPUT}"
echo "CACHE_KEY_PREFIX=node_modules-cache-node-${{ inputs.node-version }}-${{ hashFiles('yarn.lock') }}" >> "${GITHUB_OUTPUT}"
echo 'PATH_TO_CACHE<<EOF' >> $GITHUB_OUTPUT
echo "node_modules" >> $GITHUB_OUTPUT
echo "packages/*/node_modules" >> $GITHUB_OUTPUT
echo 'EOF' >> $GITHUB_OUTPUT
- name: Setup Node.js and get yarn cache
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version: ${{ inputs.node-version }}
- name: Restore node_modules
id: cache-node-modules
uses: actions/cache/restore@v4
node-version: "18"
cache: yarn
- name: Cache node modules
id: node-modules-cache
uses: actions/cache@v3
with:
key: v4-${{ steps.globals.outputs.CACHE_KEY_PREFIX }}-${{github.sha}}
restore-keys: v4-${{ steps.globals.outputs.CACHE_KEY_PREFIX }}-
path: ${{ steps.globals.outputs.PATH_TO_CACHE }}
path: |
node_modules
packages/*/node_modules
key: root-node_modules-${{ hashFiles('yarn.lock') }}
restore-keys: root-node_modules-
- name: Install Dependencies
if: ${{ steps.cache-node-modules.outputs.cache-hit != 'true' && steps.cache-node-modules.outputs.cache-matched-key == '' }}
shell: ${{ steps.globals.outputs.ACTION_SHELL }}
run: |
yarn config set enableHardenedMode true
yarn --immutable --check-cache
- name: Save cache
if: ${{ steps.cache-node-modules.outputs.cache-hit != 'true' && steps.cache-node-modules.outputs.cache-matched-key == '' }}
uses: actions/cache/save@v4
with:
key: ${{ steps.cache-node-modules.outputs.cache-primary-key }}
path: ${{ steps.globals.outputs.PATH_TO_CACHE }}
shell: bash
run: yarn --immutable --check-cache
if: steps.node-modules-cache.outputs.cache-hit != 'true'
-5
View File
@@ -1,13 +1,8 @@
name: CD deploy main
permissions:
contents: read
on:
push:
branches:
- main
jobs:
deploy-main:
timeout-minutes: 3
-5
View File
@@ -1,13 +1,8 @@
name: CD deploy tag
permissions:
contents: read
on:
push:
tags:
- 'v*'
jobs:
deploy-tag:
timeout-minutes: 3
-31
View File
@@ -1,31 +0,0 @@
name: Changed files reusable workflow
on:
workflow_call:
inputs:
files:
required: true
type: string
outputs:
any_changed:
value: ${{ jobs.changed-files.outputs.any_changed }}
permissions:
contents: read
jobs:
changed-files:
timeout-minutes: 5
runs-on: ubuntu-latest
outputs:
any_changed: ${{ steps.changed-files.outputs.any_changed }}
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@v45
with:
files: ${{ inputs.files }}
-786
View File
@@ -1,786 +0,0 @@
name: GraphQL and OpenAPI Breaking Changes Detection
on:
pull_request:
types: [opened, synchronize, edited]
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
env:
MAIN_SERVER_PORT: 3000
CURRENT_SERVER_PORT: 3002
permissions:
contents: read
pull-requests: write
checks: write
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
package.json
packages/twenty-server/**
packages/twenty-emails/**
packages/twenty-shared/**
.github/workflows/ci-breaking-changes.yaml
api-breaking-changes:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 45
runs-on: ubuntu-latest
services:
postgres:
image: twentycrm/twenty-postgres-spilo
env:
PGUSER_SUPERUSER: postgres
PGPASSWORD_SUPERUSER: postgres
ALLOW_NOSSL: 'true'
SPILO_PROVIDER: 'local'
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
clickhouse:
image: clickhouse/clickhouse-server:25.8.8
env:
CLICKHOUSE_PASSWORD: clickhousePassword
CLICKHOUSE_URL: "http://default:clickhousePassword@localhost:8123/twenty"
ports:
- 8123:8123
- 9000:9000
options: >-
--health-cmd "clickhouse-client --host=localhost --port=9000 --user=default --password=clickhousePassword --query='SELECT 1'"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout current branch
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Try to merge main into current branch
id: merge_attempt
run: |
echo "Attempting to merge main into current branch..."
git fetch origin main
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
echo "Current branch: $CURRENT_BRANCH"
if git merge origin/main --no-edit; then
echo "✅ Successfully merged main into current branch"
echo "merged=true" >> $GITHUB_OUTPUT
echo "BRANCH_STATE=merged" >> $GITHUB_ENV
else
echo "❌ Merge failed due to conflicts"
echo "⚠️ Falling back to comparing current branch against main without merge"
# Abort the failed merge
git merge --abort
echo "merged=false" >> $GITHUB_OUTPUT
echo "BRANCH_STATE=conflicts" >> $GITHUB_ENV
fi
- name: Install dependencies
uses: ./.github/workflows/actions/yarn-install
- name: Build shared dependencies
run: |
npx nx build twenty-shared
npx nx build twenty-emails
- name: Build current branch server
run: npx nx build twenty-server
- name: Setup databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "current_branch";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "main_branch";'
- name: Run ClickHouse migrations
run: npx nx clickhouse:migrate twenty-server
env:
CLICKHOUSE_URL: http://default:clickhousePassword@localhost:8123/twenty
CLICKHOUSE_PASSWORD: clickhousePassword
- name: Setup current branch database
run: |
npx nx reset:env twenty-server
set_env_var() {
local var_name="$1"
local var_value="$2"
local env_file="packages/twenty-server/.env"
echo "" >> "$env_file"
if grep -q "^${var_name}=" "$env_file" 2>/dev/null; then
sed -i "s|^${var_name}=.*|${var_name}=${var_value}|" "$env_file"
else
echo "${var_name}=${var_value}" >> "$env_file"
fi
}
set_env_var "PG_DATABASE_URL" "postgres://postgres:postgres@localhost:5432/current_branch"
set_env_var "NODE_PORT" "${{ env.CURRENT_SERVER_PORT }}"
set_env_var "REDIS_URL" "redis://localhost:6379"
set_env_var "CLICKHOUSE_URL" "http://default:clickhousePassword@localhost:8123/twenty"
set_env_var "CLICKHOUSE_PASSWORD" "clickhousePassword"
npx nx run twenty-server:database:init:prod
npx nx run twenty-server:database:migrate:prod
- name: Seed current branch database with test data
run: |
npx nx command-no-deps twenty-server -- workspace:seed:dev
- name: Start current branch server in background
run: |
echo "=== Current branch .env file contents ==="
cat packages/twenty-server/.env
echo "=== Starting current branch server ==="
nohup npx nx run twenty-server:start:prod > /tmp/current-server.log 2>&1 &
echo $! > /tmp/current-server.pid
echo "Current server PID: $(cat /tmp/current-server.pid)"
- name: Wait for current branch server to be ready
run: |
echo "Waiting for current branch server to start..."
timeout=300
interval=5
elapsed=0
while [ $elapsed -lt $timeout ]; do
if curl -s "http://localhost:${{ env.CURRENT_SERVER_PORT }}/graphql" > /dev/null 2>&1 && \
curl -s "http://localhost:${{ env.CURRENT_SERVER_PORT }}/rest/open-api/core" > /dev/null 2>&1; then
echo "Current branch server is ready!"
break
fi
echo "Current branch server not ready yet, waiting ${interval}s..."
sleep $interval
elapsed=$((elapsed + interval))
done
if [ $elapsed -ge $timeout ]; then
echo "Timeout waiting for current branch server to start"
echo "Current server log:"
cat /tmp/current-server.log || echo "No current server log found"
exit 1
fi
- name: Download GraphQL and REST responses from current branch
run: |
# Admin token from jest-integration.config.ts
ADMIN_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtOWUzYi00NmQ0LWE1NTYtODhiOWRkYzJiMDM1IiwiaWF0IjoxNzM5NTQ3NjYxLCJleHAiOjMzMjk3MTQ3NjYxfQ.fbOM9yhr3jWDicPZ1n771usUURiPGmNdeFApsgrbxOw"
# Load introspection query from file
INTROSPECTION_QUERY=$(cat packages/twenty-utils/graphql-introspection-query.graphql)
# Prepare the query payload
QUERY_PAYLOAD=$(echo "$INTROSPECTION_QUERY" | tr '\n' ' ' | sed 's/"/\\"/g')
echo "Downloading GraphQL schema from current server..."
curl -X POST "http://localhost:${{ env.CURRENT_SERVER_PORT }}/graphql" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-d "{\"query\":\"${QUERY_PAYLOAD}\"}" \
-o current-schema-introspection.json \
-w "HTTP Status: %{http_code}\n" \
-s
echo "Downloading GraphQL metadata schema from current server..."
curl -X POST "http://localhost:${{ env.CURRENT_SERVER_PORT }}/metadata" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-d "{\"query\":\"${QUERY_PAYLOAD}\"}" \
-o current-metadata-schema-introspection.json \
-w "HTTP Status: %{http_code}\n" \
-s
# Download current branch OpenAPI specs
echo "Downloading OpenAPI specifications from current server..."
curl -s "http://localhost:${{ env.CURRENT_SERVER_PORT }}/rest/open-api/core" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-o current-rest-api.json \
-w "HTTP Status: %{http_code}\n"
curl -s "http://localhost:${{ env.CURRENT_SERVER_PORT }}/rest/open-api/metadata" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-o current-rest-metadata-api.json \
-w "HTTP Status: %{http_code}\n"
# Verify the downloads
echo "Current branch files downloaded:"
ls -la current-*
- name: Preserve current branch files
run: |
# Create a temp directory to store current branch files
mkdir -p /tmp/current-branch-files
# Move current branch files to temp directory
mv current-* /tmp/current-branch-files/ 2>/dev/null || echo "No current-* files to preserve"
echo "Preserved current branch files for later restoration"
- name: Stop current branch server
run: |
if [ -f /tmp/current-server.pid ]; then
echo "Stopping current branch server..."
kill $(cat /tmp/current-server.pid) || true
# Wait a bit for graceful shutdown
sleep 5
# Force kill if still running
kill -9 $(cat /tmp/current-server.pid) 2>/dev/null || true
rm -f /tmp/current-server.pid
fi
- name: Checkout main branch
run: |
git stash
git checkout origin/main
git reset --hard
git clean -xfd -ff
rm -rf node_modules packages/*/node_modules packages/*/dist dist .nx/cache
- name: Install dependencies for main branch
uses: ./.github/workflows/actions/yarn-install
- name: Build main branch dependencies
run: |
npx nx reset
npx nx build twenty-shared
npx nx build twenty-emails
- name: Build main branch server
run: npx nx build twenty-server
- name: Setup main branch database
run: |
npx nx reset:env twenty-server
set_env_var() {
local var_name="$1"
local var_value="$2"
local env_file="packages/twenty-server/.env"
echo "" >> "$env_file"
if grep -q "^${var_name}=" "$env_file" 2>/dev/null; then
sed -i "s|^${var_name}=.*|${var_name}=${var_value}|" "$env_file"
else
echo "${var_name}=${var_value}" >> "$env_file"
fi
}
set_env_var "PG_DATABASE_URL" "postgres://postgres:postgres@localhost:5432/main_branch"
set_env_var "NODE_PORT" "${{ env.MAIN_SERVER_PORT }}"
set_env_var "REDIS_URL" "redis://localhost:6379"
set_env_var "CLICKHOUSE_URL" "http://default:clickhousePassword@localhost:8123/twenty"
set_env_var "CLICKHOUSE_PASSWORD" "clickhousePassword"
npx nx run twenty-server:database:init:prod
npx nx run twenty-server:database:migrate:prod
- name: Seed main branch database with test data
run: |
npx nx command-no-deps twenty-server -- workspace:seed:dev
- name: Start main branch server in background
run: |
echo "=== Main branch .env file contents ==="
cat packages/twenty-server/.env
echo "=== Starting main branch server ==="
nohup npx nx run twenty-server:start:prod > /tmp/main-server.log 2>&1 &
echo $! > /tmp/main-server.pid
echo "Main server PID: $(cat /tmp/main-server.pid)"
- name: Wait for main branch server to be ready
run: |
echo "Waiting for main branch server to start..."
timeout=300
interval=5
elapsed=0
while [ $elapsed -lt $timeout ]; do
if curl -s "http://localhost:${{ env.MAIN_SERVER_PORT }}/graphql" > /dev/null 2>&1 && \
curl -s "http://localhost:${{ env.MAIN_SERVER_PORT }}/rest/open-api/core" > /dev/null 2>&1; then
echo "Main branch server is ready!"
break
fi
echo "Main branch server not ready yet, waiting ${interval}s..."
sleep $interval
elapsed=$((elapsed + interval))
done
if [ $elapsed -ge $timeout ]; then
echo "Timeout waiting for main branch server to start"
echo "Main server log:"
cat /tmp/main-server.log || echo "No main server log found"
exit 1
fi
- name: Download GraphQL and REST responses from main branch
run: |
# Admin token from jest-integration.config.ts
ADMIN_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtOWUzYi00NmQ0LWE1NTYtODhiOWRkYzJiMDM1IiwiaWF0IjoxNzM5NTQ3NjYxLCJleHAiOjMzMjk3MTQ3NjYxfQ.fbOM9yhr3jWDicPZ1n771usUURiPGmNdeFApsgrbxOw"
# Load introspection query from file
INTROSPECTION_QUERY=$(cat packages/twenty-utils/graphql-introspection-query.graphql)
# Prepare the query payload
QUERY_PAYLOAD=$(echo "$INTROSPECTION_QUERY" | tr '\n' ' ' | sed 's/"/\\"/g')
echo "Downloading GraphQL schema from main server..."
curl -X POST "http://localhost:${{ env.MAIN_SERVER_PORT }}/graphql" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-d "{\"query\":\"${QUERY_PAYLOAD}\"}" \
-o main-schema-introspection.json \
-w "HTTP Status: %{http_code}\n" \
-s
echo "Downloading GraphQL metadata schema from main server..."
curl -X POST "http://localhost:${{ env.MAIN_SERVER_PORT }}/metadata" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-d "{\"query\":\"${QUERY_PAYLOAD}\"}" \
-o main-metadata-schema-introspection.json \
-w "HTTP Status: %{http_code}\n" \
-s
# Download main branch OpenAPI specs
echo "Downloading OpenAPI specifications from main server..."
curl -s "http://localhost:${{ env.MAIN_SERVER_PORT }}/rest/open-api/core" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-o main-rest-api.json \
-w "HTTP Status: %{http_code}\n"
curl -s "http://localhost:${{ env.MAIN_SERVER_PORT }}/rest/open-api/metadata" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-o main-rest-metadata-api.json \
-w "HTTP Status: %{http_code}\n"
# Verify the downloads
echo "Main branch files downloaded:"
ls -la main-*
- name: Restore current branch files
run: |
# Move current branch files back to working directory
mv /tmp/current-branch-files/* . 2>/dev/null || echo "No files to restore"
# Verify all files are present
echo "All API files restored:"
ls -la current-* main-* 2>/dev/null || echo "Some files may be missing"
# Clean up temp directory
rm -rf /tmp/current-branch-files
- name: Install OpenAPI Diff Tool
run: |
# Using the Java-based OpenAPITools/openapi-diff via Docker
echo "Using OpenAPITools/openapi-diff via Docker"
- name: Generate GraphQL Schema Diff Reports
run: |
echo "=== INSTALLING GRAPHQL INSPECTOR CLI ==="
npm install -g @graphql-inspector/cli
echo "=== GENERATING GRAPHQL DIFF REPORTS ==="
# Check if GraphQL schema has changes
echo "Checking GraphQL schema for changes..."
if graphql-inspector diff main-schema-introspection.json current-schema-introspection.json >/dev/null 2>&1; then
echo "✅ No changes in GraphQL schema"
# Don't create a diff file for no changes
else
echo "⚠️ Changes detected in GraphQL schema, generating report..."
echo "# GraphQL Schema Changes" > graphql-schema-diff.md
echo "" >> graphql-schema-diff.md
graphql-inspector diff main-schema-introspection.json current-schema-introspection.json >> graphql-schema-diff.md 2>&1 || {
echo "⚠️ **Breaking changes or errors detected in GraphQL schema**" >> graphql-schema-diff.md
echo "" >> graphql-schema-diff.md
echo "\`\`\`" >> graphql-schema-diff.md
graphql-inspector diff main-schema-introspection.json current-schema-introspection.json 2>&1 >> graphql-schema-diff.md || echo "Error generating diff" >> graphql-schema-diff.md
echo "\`\`\`" >> graphql-schema-diff.md
}
fi
# Check if GraphQL metadata schema has changes
echo "Checking GraphQL metadata schema for changes..."
if graphql-inspector diff main-metadata-schema-introspection.json current-metadata-schema-introspection.json >/dev/null 2>&1; then
echo "✅ No changes in GraphQL metadata schema"
# Don't create a diff file for no changes
else
echo "⚠️ Changes detected in GraphQL metadata schema, generating report..."
echo "# GraphQL Metadata Schema Changes" > graphql-metadata-diff.md
echo "" >> graphql-metadata-diff.md
graphql-inspector diff main-metadata-schema-introspection.json current-metadata-schema-introspection.json >> graphql-metadata-diff.md 2>&1 || {
echo "⚠️ **Breaking changes or errors detected in GraphQL metadata schema**" >> graphql-metadata-diff.md
echo "" >> graphql-metadata-diff.md
echo "\`\`\`" >> graphql-metadata-diff.md
graphql-inspector diff main-metadata-schema-introspection.json current-metadata-schema-introspection.json 2>&1 >> graphql-metadata-diff.md || echo "Error generating diff" >> graphql-metadata-diff.md
echo "\`\`\`" >> graphql-metadata-diff.md
}
fi
# Show summary
echo "Generated diff files:"
ls -la *-diff.md 2>/dev/null || echo "No diff files generated (no changes detected)"
- name: Check REST API Breaking Changes
run: |
echo "=== CHECKING REST API FOR BREAKING CHANGES ==="
# Use the Java-based openapi-diff via Docker
docker run --rm -v "$(pwd):/specs" openapitools/openapi-diff:latest \
--json /specs/rest-api-diff.json \
/specs/main-rest-api.json /specs/current-rest-api.json || echo "OpenAPI diff completed with exit code $?"
# Check if the output file was created and is valid JSON
if [ -f "rest-api-diff.json" ] && jq empty rest-api-diff.json 2>/dev/null; then
# Check for breaking changes using Java openapi-diff JSON structure
incompatible=$(jq -r '.incompatible // false' rest-api-diff.json)
different=$(jq -r '.different // false' rest-api-diff.json)
# Count changes
new_endpoints=$(jq -r '.newEndpoints | length' rest-api-diff.json 2>/dev/null || echo "0")
missing_endpoints=$(jq -r '.missingEndpoints | length' rest-api-diff.json 2>/dev/null || echo "0")
changed_operations=$(jq -r '.changedOperations | length' rest-api-diff.json 2>/dev/null || echo "0")
if [ "$incompatible" = "true" ]; then
echo "❌ Breaking changes detected in REST API"
# Generate breaking changes report
echo "# REST API Breaking Changes" > rest-api-diff.md
echo "" >> rest-api-diff.md
echo "⚠️ **Breaking changes detected that may affect existing API consumers**" >> rest-api-diff.md
echo "" >> rest-api-diff.md
# Parse and format the changes from Java openapi-diff
jq -r '
if (.missingEndpoints | length) > 0 then
"## 🚨 Removed Endpoints (" + (.missingEndpoints | length | tostring) + ")\n" +
(.missingEndpoints | map("- **" + .method + " " + .pathUrl + "**: " + (.summary // "")) | join("\n"))
else "" end,
if (.changedOperations | length) > 0 then
"\n## ⚠️ Changed Operations (" + (.changedOperations | length | tostring) + ")\n" +
(.changedOperations | map("- **" + .method + " " + .pathUrl + "**: " + (.summary // "Modified operation")) | join("\n"))
else "" end,
if (.newEndpoints | length) > 0 then
"\n## ✅ New Endpoints (" + (.newEndpoints | length | tostring) + ")\n" +
(.newEndpoints | map("- " + .method + " " + .pathUrl + ": " + (.summary // "")) | join("\n"))
else "" end
' rest-api-diff.json >> rest-api-diff.md
elif [ "$different" = "true" ]; then
echo "📝 Non-breaking changes detected ($new_endpoints new endpoints, $missing_endpoints removed, $changed_operations changed) - no PR comment will be posted"
# Don't create markdown file for non-breaking changes to avoid PR comments
else
echo "✅ No changes detected in REST API"
# Don't create diff file for no changes
fi
else
echo "⚠️ OpenAPI diff tool could not process the files"
echo "# REST API Analysis Error" > rest-api-diff.md
echo "" >> rest-api-diff.md
echo "⚠️ **Error occurred while analyzing REST API changes**" >> rest-api-diff.md
echo "" >> rest-api-diff.md
echo "## Error Output" >> rest-api-diff.md
echo "\`\`\`" >> rest-api-diff.md
docker run --rm -v "$(pwd):/specs" openapitools/openapi-diff:latest /specs/main-rest-api.json /specs/current-rest-api.json 2>&1 >> rest-api-diff.md || echo "Could not capture error output"
echo "\`\`\`" >> rest-api-diff.md
# Don't fail the workflow for tool errors
echo "::warning::REST API analysis tool error - continuing workflow"
fi
- name: Check REST Metadata API Breaking Changes
run: |
echo "=== CHECKING REST METADATA API FOR BREAKING CHANGES ==="
# Use the Java-based openapi-diff for metadata API as well
docker run --rm -v "$(pwd):/specs" openapitools/openapi-diff:latest \
--json /specs/rest-metadata-api-diff.json \
/specs/main-rest-metadata-api.json /specs/current-rest-metadata-api.json || echo "OpenAPI diff completed with exit code $?"
# Check if the output file was created and is valid JSON
if [ -f "rest-metadata-api-diff.json" ] && jq empty rest-metadata-api-diff.json 2>/dev/null; then
# Check for breaking changes using Java openapi-diff JSON structure
incompatible=$(jq -r '.incompatible // false' rest-metadata-api-diff.json)
different=$(jq -r '.different // false' rest-metadata-api-diff.json)
# Count changes
new_endpoints=$(jq -r '.newEndpoints | length' rest-metadata-api-diff.json 2>/dev/null || echo "0")
missing_endpoints=$(jq -r '.missingEndpoints | length' rest-metadata-api-diff.json 2>/dev/null || echo "0")
changed_operations=$(jq -r '.changedOperations | length' rest-metadata-api-diff.json 2>/dev/null || echo "0")
if [ "$incompatible" = "true" ]; then
echo "❌ Breaking changes detected in REST Metadata API"
# Generate breaking changes report (only for breaking changes)
echo "# REST Metadata API Breaking Changes" > rest-metadata-api-diff.md
echo "" >> rest-metadata-api-diff.md
echo "⚠️ **Breaking changes detected that may affect existing API consumers**" >> rest-metadata-api-diff.md
echo "" >> rest-metadata-api-diff.md
# Parse and format the changes from Java openapi-diff
jq -r '
if (.missingEndpoints | length) > 0 then
"## 🚨 Removed Endpoints (" + (.missingEndpoints | length | tostring) + ")\n" +
(.missingEndpoints | map("- **" + .method + " " + .pathUrl + "**: " + (.summary // "")) | join("\n"))
else "" end,
if (.changedOperations | length) > 0 then
"\n## ⚠️ Changed Operations (" + (.changedOperations | length | tostring) + ")\n" +
(.changedOperations | map("- **" + .method + " " + .pathUrl + "**: " + (.summary // "Modified operation")) | join("\n"))
else "" end,
if (.newEndpoints | length) > 0 then
"\n## ✅ New Endpoints (" + (.newEndpoints | length | tostring) + ")\n" +
(.newEndpoints | map("- " + .method + " " + .pathUrl + ": " + (.summary // "")) | join("\n"))
else "" end
' rest-metadata-api-diff.json >> rest-metadata-api-diff.md
elif [ "$different" = "true" ]; then
echo "📝 Non-breaking changes detected ($new_endpoints new endpoints, $missing_endpoints removed, $changed_operations changed) - no PR comment will be posted"
# Don't create markdown file for non-breaking changes to avoid PR comments
else
echo "✅ No changes detected in REST Metadata API"
fi
else
echo "⚠️ OpenAPI diff tool could not process the metadata API files"
echo "# REST Metadata API Analysis Error" > rest-metadata-api-diff.md
echo "" >> rest-metadata-api-diff.md
echo "⚠️ **Error occurred while analyzing REST Metadata API changes**" >> rest-metadata-api-diff.md
echo "" >> rest-metadata-api-diff.md
echo "## Error Output" >> rest-metadata-api-diff.md
echo "\`\`\`" >> rest-metadata-api-diff.md
docker run --rm -v "$(pwd):/specs" openapitools/openapi-diff:latest /specs/main-rest-metadata-api.json /specs/current-rest-metadata-api.json 2>&1 >> rest-metadata-api-diff.md || echo "Could not capture error output"
echo "\`\`\`" >> rest-metadata-api-diff.md
# Don't fail the workflow for tool errors
echo "::warning::REST Metadata API analysis tool error - continuing workflow"
fi
- name: Comment API Changes on PR
if: always()
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
let hasChanges = false;
let comment = '';
try {
if (fs.existsSync('graphql-schema-diff.md')) {
const graphqlDiff = fs.readFileSync('graphql-schema-diff.md', 'utf8');
if (graphqlDiff.trim()) {
if (!hasChanges) {
comment = '## 📊 API Changes Report\n\n';
hasChanges = true;
}
comment += '### GraphQL Schema Changes\n' + graphqlDiff + '\n\n';
}
}
if (fs.existsSync('graphql-metadata-diff.md')) {
const graphqlMetadataDiff = fs.readFileSync('graphql-metadata-diff.md', 'utf8');
if (graphqlMetadataDiff.trim()) {
if (!hasChanges) {
comment = '## 📊 API Changes Report\n\n';
hasChanges = true;
}
comment += '### GraphQL Metadata Schema Changes\n' + graphqlMetadataDiff + '\n\n';
}
}
if (fs.existsSync('rest-api-diff.md')) {
const restDiff = fs.readFileSync('rest-api-diff.md', 'utf8');
if (restDiff.trim()) {
if (!hasChanges) {
comment = '## 📊 API Changes Report\n\n';
hasChanges = true;
}
comment += restDiff + '\n\n';
}
}
if (fs.existsSync('rest-metadata-api-diff.md')) {
const metadataDiff = fs.readFileSync('rest-metadata-api-diff.md', 'utf8');
if (metadataDiff.trim()) {
if (!hasChanges) {
comment = '## 📊 API Changes Report\n\n';
hasChanges = true;
}
comment += metadataDiff + '\n\n';
}
}
// Only post comment if there are changes
if (hasChanges) {
// Add branch state information only if there were conflicts
const branchState = process.env.BRANCH_STATE || 'unknown';
let branchStateNote = '';
if (branchState === 'conflicts') {
branchStateNote = '\n\n⚠️ **Note**: Could not merge with `main` due to conflicts. This comparison shows changes between the current branch and `main` as separate states.\n';
}
// Check if there are any breaking changes detected
let hasBreakingChanges = false;
let breakingChangeNote = '';
// Check for breaking changes in any of the diff files
if (fs.existsSync('rest-api-diff.md')) {
const restDiff = fs.readFileSync('rest-api-diff.md', 'utf8');
if (restDiff.includes('Breaking Changes') || restDiff.includes('🚨') ||
restDiff.includes('Removed Endpoints') || restDiff.includes('Changed Operations')) {
hasBreakingChanges = true;
}
}
if (fs.existsSync('rest-metadata-api-diff.md')) {
const metadataDiff = fs.readFileSync('rest-metadata-api-diff.md', 'utf8');
if (metadataDiff.includes('Breaking Changes') || metadataDiff.includes('🚨') ||
metadataDiff.includes('Removed Endpoints') || metadataDiff.includes('Changed Operations')) {
hasBreakingChanges = true;
}
}
// Also check GraphQL changes for breaking changes indicators
if (fs.existsSync('graphql-schema-diff.md')) {
const graphqlDiff = fs.readFileSync('graphql-schema-diff.md', 'utf8');
if (graphqlDiff.includes('Breaking changes') || graphqlDiff.includes('BREAKING')) {
hasBreakingChanges = true;
}
}
if (fs.existsSync('graphql-metadata-diff.md')) {
const graphqlMetadataDiff = fs.readFileSync('graphql-metadata-diff.md', 'utf8');
if (graphqlMetadataDiff.includes('Breaking changes') || graphqlMetadataDiff.includes('BREAKING')) {
hasBreakingChanges = true;
}
}
// Check PR title for "breaking"
const prTitle = ${{ toJSON(github.event.pull_request.title) }};
const titleContainsBreaking = prTitle.toLowerCase().includes('breaking');
if (hasBreakingChanges) {
if (titleContainsBreaking) {
breakingChangeNote = '\n\n## ✅ Breaking Change Protocol\n\n' +
'**This PR title contains "breaking" and breaking changes were detected - the CI will fail as expected.**\n\n' +
'📝 **Action Required**: Please add `BREAKING CHANGE:` to your commit message to trigger a major version bump.\n\n' +
'Example:\n```\nfeat: add new API endpoint\n\nBREAKING CHANGE: removed deprecated field from User schema\n```';
} else {
breakingChangeNote = '\n\n## ⚠️ Breaking Change Protocol\n\n' +
'**Breaking changes detected but PR title does not contain "breaking" - CI will pass but action needed.**\n\n' +
'🔄 **Options**:\n' +
'1. **If this IS a breaking change**: Add "breaking" to your PR title and add `BREAKING CHANGE:` to your commit message\n' +
'2. **If this is NOT a breaking change**: The API diff tool may have false positives - please review carefully\n\n' +
'For breaking changes, add to commit message:\n```\nfeat: add new API endpoint\n\nBREAKING CHANGE: removed deprecated field from User schema\n```';
}
}
const COMMENT_MARKER = '<!-- API_CHANGES_REPORT -->';
const commentBody = COMMENT_MARKER + '\n' + comment + branchStateNote + '\n⚠️ **Please review these API changes carefully before merging.**' + breakingChangeNote;
// Get all comments to find existing API changes comment
const {data: comments} = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
// Find our existing comment
const botComment = comments.find(comment => comment.body.includes(COMMENT_MARKER));
if (botComment) {
// Update existing comment
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: commentBody
});
console.log('Updated existing API changes comment');
} else {
// Create new comment
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody
});
console.log('Created new API changes comment');
}
} else {
console.log('No API changes detected - skipping PR comment');
// Check if there's an existing comment to remove
const {data: comments} = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const COMMENT_MARKER = '<!-- API_CHANGES_REPORT -->';
const botComment = comments.find(comment => comment.body.includes(COMMENT_MARKER));
if (botComment) {
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
});
console.log('Deleted existing API changes comment (no changes detected)');
}
}
} catch (error) {
console.log('Could not post comment:', error);
}
- name: Cleanup servers
if: always()
run: |
if [ -f /tmp/current-server.pid ]; then
kill $(cat /tmp/current-server.pid) || true
fi
if [ -f /tmp/main-server.pid ]; then
kill $(cat /tmp/main-server.pid) || true
fi
- name: Upload API specifications and diffs
if: always()
uses: actions/upload-artifact@v4
with:
name: api-specifications-and-diffs
path: |
/tmp/main-server.log
/tmp/current-server.log
*-api.json
*-schema-introspection.json
*-diff.md
*-diff.json
@@ -0,0 +1,47 @@
name: CI Chrome Extension
on:
push:
branches:
- main
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
chrome-extension-build:
timeout-minutes: 15
runs-on: ubuntu-latest
env:
VITE_SERVER_BASE_URL: http://localhost:3000
VITE_FRONT_BASE_URL: http://localhost:3001
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@v11
with:
files: |
package.json
packages/twenty-chrome-extension/**
- name: Install dependencies
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/yarn-install
- name: Chrome Extension / Run build
if: steps.changed-files.outputs.any_changed == 'true'
run: npx nx build twenty-chrome-extension
- name: Mark as Valid if No Changes
if: steps.changed-files.outputs.changed != 'true'
run: |
echo "No relevant changes detected. Marking as valid."
-103
View File
@@ -1,103 +0,0 @@
name: CI CLI
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/twenty-cli/**
packages/twenty-server/**
cli-test:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
matrix:
task: [lint, typecheck, test, build]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/workflows/actions/yarn-install
- name: Run ${{ matrix.task }} task
uses: ./.github/workflows/actions/nx-affected
with:
tag: scope:cli
tasks: ${{ matrix.task }}
cli-e2e-test:
timeout-minutes: 30
runs-on: depot-ubuntu-24.04-8
needs: [changed-files-check, cli-test]
if: needs.changed-files-check.outputs.any_changed == 'true'
services:
postgres:
image: twentycrm/twenty-postgres-spilo
env:
PGUSER_SUPERUSER: postgres
PGPASSWORD_SUPERUSER: postgres
ALLOW_NOSSL: 'true'
SPILO_PROVIDER: 'local'
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
env:
NODE_ENV: test
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/workflows/actions/yarn-install
- name: Server / Append billing config to .env.test
working-directory: packages/twenty-server
run: |
echo "" >> .env.test
echo "IS_BILLING_ENABLED=true" >> .env.test
echo "BILLING_STRIPE_API_KEY=test-api-key" >> .env.test
echo "BILLING_STRIPE_BASE_PLAN_PRODUCT_ID=test-base-plan-product-id" >> .env.test
echo "BILLING_STRIPE_WEBHOOK_SECRET=test-webhook-secret" >> .env.test
echo "BILLING_PLAN_REQUIRED_LINK=http://localhost:3001/stripe-redirection" >> .env.test
- name: Server / Create Test DB
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: CLI / Run E2E Tests
run: npx nx test:e2e twenty-cli
ci-cli-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, cli-test, cli-e2e-test]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1
-136
View File
@@ -1,136 +0,0 @@
name: CI E2E Playwright Tests
on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened, labeled]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/**
playwright.config.ts
.github/workflows/ci-e2e.yaml
test:
runs-on: ubuntu-latest
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true' && ( github.event_name == 'push' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'run-e2e')))
timeout-minutes: 30
env:
# https://github.com/actions/runner-images/issues/70#issuecomment-589562148
NODE_OPTIONS: "--max-old-space-size=10240"
services:
postgres:
image: twentycrm/twenty-postgres-spilo
env:
PGUSER_SUPERUSER: postgres
PGPASSWORD_SUPERUSER: postgres
ALLOW_NOSSL: "true"
SPILO_PROVIDER: "local"
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Check system resources
run: |
echo "Available memory:"
free -h
echo "Available disk space:"
df -h
echo "CPU info:"
lscpu
- name: Install dependencies
uses: ./.github/workflows/actions/yarn-install
- name: Build twenty-shared
run: npx nx build twenty-shared
- name: Install Playwright Browsers
run: npx nx setup twenty-e2e-testing
- name: Setup environment files
run: |
cp packages/twenty-front/.env.example packages/twenty-front/.env
npx nx reset:env twenty-server
- name: Build frontend
run: NODE_ENV=production NODE_OPTIONS="--max-old-space-size=10240" npx nx build twenty-front
- name: Build server
run: npx nx build twenty-server
- name: Create and setup database
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
npx nx run twenty-server:database:reset
- name: Start server
run: |
npx nx start twenty-server &
echo "Waiting for server to be ready..."
timeout 60 bash -c 'until curl -s http://localhost:3000/health; do sleep 2; done'
- name: Start frontend
run: |
npm_config_yes=true npx serve -s packages/twenty-front/build -l 3001 &
echo "Waiting for frontend to be ready..."
timeout 60 bash -c 'until curl -s http://localhost:3001; do sleep 2; done'
- name: Start worker
run: |
npx nx run twenty-server:worker &
echo "Worker started"
- name: Run Playwright tests
run: npx nx test twenty-e2e-testing
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: packages/twenty-e2e-testing/run_results/
retention-days: 30
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: packages/twenty-e2e-testing/playwright-report/
retention-days: 30
ci-e2e-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, test]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1
+52
View File
@@ -0,0 +1,52 @@
name: CI E2E Tests
on:
push:
branches:
- main
pull_request:
branches:
- '**'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@v11
with:
files: |
packages/**
playwright.config.ts
- name: Skip if no relevant changes
if: steps.changed-files.outputs.any_changed == 'false'
run: echo "No relevant changes detected. Marking as valid."
- name: Install dependencies
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/yarn-install
- name: Install Playwright Browsers
if: steps.changed-files.outputs.any_changed == 'true'
run: yarn playwright install --with-deps
- name: Run Playwright tests
if: steps.changed-files.outputs.any_changed == 'true'
run: yarn test:e2e companies
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
-64
View File
@@ -1,64 +0,0 @@
name: CI Emails
permissions:
contents: read
on:
push:
branches:
- main
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
package.json
packages/twenty-emails/**
emails-test:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 10
runs-on: depot-ubuntu-24.04-8
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/workflows/actions/yarn-install
- name: Build twenty-emails
run: npx nx build twenty-emails
- name: Run email tests
run: |
# Start the email server in the background
npx nx run twenty-emails:start &
SERVER_PID=$!
# Wait for server to start
sleep 20
# Check if server is running
if ! curl -s http://localhost:4001/preview/test.email > /dev/null; then
echo "Email server failed to start"
kill $SERVER_PID
exit 1
fi
# Kill the server
kill $SERVER_PID
ci-emails-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, emails-test]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1
+126 -100
View File
@@ -1,39 +1,22 @@
name: CI Front
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
env:
STORYBOOK_BUILD_CACHE_KEY_FOR_RESTORE_ACTION: storybook-build-depot-ubuntu-24.04-8-runner
STORYBOOK_BUILD_CACHE_KEY_FOR_SAVE_ACTION: v3-storybook-build-depot-ubuntu-24.04-8-runner-${{ github.ref_name }}-${{ github.sha }}
cancel-in-progress: true
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
package.json
packages/twenty-front/**
packages/twenty-ui/**
packages/twenty-shared/**
front-sb-build:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: depot-ubuntu-24.04-8
runs-on: ubuntu-latest
env:
REACT_APP_SERVER_BASE_URL: http://localhost:3000
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
@@ -43,114 +26,163 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@v11
with:
files: |
package.json
packages/twenty-front/**
packages/twenty-ui/**
- name: Skip if no relevant changes
if: steps.changed-files.outputs.any_changed == 'false'
run: echo "No relevant changes. Skipping CI."
- name: Install dependencies
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/yarn-install
- name: Diagnostic disk space issue
if: steps.changed-files.outputs.any_changed == 'true'
run: df -h
- name: Front / Restore Storybook Task Cache
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/task-cache
with:
tag: scope:frontend
tasks: storybook:build
- name: Front / Write .env
if: steps.changed-files.outputs.any_changed == 'true'
run: npx nx reset:env twenty-front
- name: Front / Build storybook
if: steps.changed-files.outputs.any_changed == 'true'
run: npx nx storybook:build twenty-front
- name: Save storybook build cache
uses: ./.github/workflows/actions/save-cache
with:
key: ${{ env.STORYBOOK_BUILD_CACHE_KEY_FOR_SAVE_ACTION }}
front-sb-test:
timeout-minutes: 30
runs-on: depot-ubuntu-24.04-8
runs-on: shipfox-8vcpu-ubuntu-2204
needs: front-sb-build
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
storybook_scope: [modules, pages, performance]
storybook_scope: [pages, modules]
env:
SHARD_COUNTER: 4
REACT_APP_SERVER_BASE_URL: http://localhost:3000
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
steps:
- name: Fetch local actions
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@v11
with:
files: |
packages/twenty-front/**
- name: Skip if no relevant changes
if: steps.changed-files.outputs.any_changed == 'false'
run: echo "No relevant changes. Skipping CI."
- name: Install dependencies
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/yarn-install
- name: Install Playwright
if: steps.changed-files.outputs.any_changed == 'true'
run: cd packages/twenty-front && npx playwright install
- name: Restore storybook build cache
uses: ./.github/workflows/actions/restore-cache
- name: Front / Restore Storybook Task Cache
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/task-cache
with:
key: ${{ env.STORYBOOK_BUILD_CACHE_KEY_FOR_RESTORE_ACTION }}
tag: scope:frontend
tasks: storybook:build
- name: Front / Write .env
if: steps.changed-files.outputs.any_changed == 'true'
run: npx nx reset:env twenty-front
- name: Run storybook tests
run: npx nx storybook:serve-and-test:static twenty-front --configuration=${{ matrix.storybook_scope }} --shard=${{ matrix.shard }}/${{ env.SHARD_COUNTER }} --checkCoverage=false
- name: Rename coverage file
run: mv packages/twenty-front/coverage/storybook/coverage-storybook.json packages/twenty-front/coverage/storybook/coverage-shard-${{matrix.shard}}.json
- name: Upload coverage artifact
uses: actions/upload-artifact@v4
with:
retention-days: 1
name: coverage-artifacts-${{ matrix.storybook_scope }}-${{ github.run_id }}-${{ matrix.shard }}
path: packages/twenty-front/coverage/storybook/coverage-shard-${{matrix.shard}}.json
merge-reports-and-check-coverage:
if: steps.changed-files.outputs.any_changed == 'true'
run: npx nx storybook:serve-and-test:static twenty-front --configuration=${{ matrix.storybook_scope }}
front-sb-test-performance:
timeout-minutes: 30
runs-on: ubuntu-latest
needs: front-sb-test
runs-on: shipfox-8vcpu-ubuntu-2204
env:
PATH_TO_COVERAGE: packages/twenty-front/coverage/storybook
strategy:
matrix:
storybook_scope: [modules, pages, performance]
REACT_APP_SERVER_BASE_URL: http://localhost:3000
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
steps:
- uses: actions/checkout@v4
- name: Fetch local actions
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/workflows/actions/yarn-install
- uses: actions/download-artifact@v4
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@v11
with:
pattern: coverage-artifacts-${{ matrix.storybook_scope }}-${{ github.run_id }}-*
merge-multiple: true
path: coverage-artifacts
- name: Merge coverage reports
run: |
mkdir -p ${{ env.PATH_TO_COVERAGE }}
npx nyc merge coverage-artifacts ${{ env.PATH_TO_COVERAGE }}/coverage-storybook.json
- name: Checking coverage
run: npx nx storybook:coverage twenty-front --checkCoverage=true --configuration=${{ matrix.storybook_scope }}
files: |
packages/twenty-front/**
- name: Skip if no relevant changes
if: steps.changed-files.outputs.any_changed == 'false'
run: echo "No relevant changes. Skipping CI."
- name: Install dependencies
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/yarn-install
- name: Install Playwright
if: steps.changed-files.outputs.any_changed == 'true'
run: cd packages/twenty-front && npx playwright install
- name: Front / Write .env
if: steps.changed-files.outputs.any_changed == 'true'
run: npx nx reset:env twenty-front
- name: Run storybook tests
if: steps.changed-files.outputs.any_changed == 'true'
run: npx nx run twenty-front:storybook:serve-and-test:static:performance
front-chromatic-deployment:
timeout-minutes: 30
if: contains(github.event.pull_request.labels.*.name, 'run-chromatic') || github.event_name == 'push'
needs: front-sb-build
runs-on: depot-ubuntu-24.04-8
runs-on: ubuntu-latest
env:
REACT_APP_SERVER_BASE_URL: http://127.0.0.1:3000
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/workflows/actions/yarn-install
- name: Restore storybook build cache
uses: ./.github/workflows/actions/restore-cache
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@v11
with:
key: ${{ env.STORYBOOK_BUILD_CACHE_KEY }}
files: |
packages/twenty-front/**
- name: Skip if no relevant changes
if: steps.changed-files.outputs.any_changed == 'false'
run: echo "No relevant changes. Skipping CI."
- name: Install dependencies
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/yarn-install
- name: Front / Restore Storybook Task Cache
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/task-cache
with:
tag: scope:frontend
tasks: storybook:build
- name: Front / Write .env
if: steps.changed-files.outputs.any_changed == 'true'
run: |
cd packages/twenty-front
touch .env
echo "" >> .env
echo "REACT_APP_SERVER_BASE_URL=$REACT_APP_SERVER_BASE_URL" >> .env
echo "REACT_APP_SERVER_BASE_URL: $REACT_APP_SERVER_BASE_URL" >> .env
- name: Publish to Chromatic
if: steps.changed-files.outputs.any_changed == 'true'
run: npx nx run twenty-front:chromatic:ci
front-task:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
env:
TASK_CACHE_KEY: front-task-${{ matrix.task }}
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
strategy:
matrix:
task: [lint, typecheck, test]
@@ -163,41 +195,35 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/workflows/actions/yarn-install
- name: Restore ${{ matrix.task }} cache
id: restore-task-cache
uses: ./.github/workflows/actions/restore-cache
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@v11
with:
key: ${{ env.TASK_CACHE_KEY }}
files: |
packages/twenty-front/**
- name: Skip if no relevant changes
if: steps.changed-files.outputs.any_changed == 'false'
run: echo "No relevant changes. Skipping CI."
- name: Install dependencies
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/yarn-install
- name: Front / Restore ${{ matrix.task }} task cache
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/task-cache
with:
tag: scope:frontend
tasks: ${{ matrix.task }}
- name: Reset .env
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/nx-affected
with:
tag: scope:frontend
tasks: reset:env
- name: Run ${{ matrix.task }} task
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/nx-affected
with:
tag: scope:frontend
tasks: ${{ matrix.task }}
- name: Save ${{ matrix.task }} cache
uses: ./.github/workflows/actions/save-cache
with:
key: ${{ steps.restore-task-cache.outputs.cache-primary-key }}
ci-front-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs:
[
changed-files-check,
front-task,
front-chromatic-deployment,
merge-reports-and-check-coverage,
front-sb-test,
front-sb-build,
]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1
+11 -25
View File
@@ -1,9 +1,4 @@
name: "Release: create"
permissions:
contents: read
pull-requests: write
on:
workflow_dispatch:
inputs:
@@ -18,42 +13,33 @@ on:
default: true
description: Create a release after merging the PR
defaults:
run:
shell: bash --noprofile --norc -euo pipefail {0}
jobs:
create_pr:
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v2
with:
ref: ${{ github.event.inputs.ref }}
- name: Sanitize version
id: sanitize
env:
RAW_VERSION: ${{ github.event.inputs.version }}
run: |
VERSION="${RAW_VERSION#v}"
printf 'version=%s\n' "$VERSION" >> "$GITHUB_OUTPUT"
echo version=$(echo ${{ github.event.inputs.version }} | sed 's/^v//') >> $GITHUB_OUTPUT
- name: Update versions
env:
VERSION: ${{ steps.sanitize.outputs.version }}
run: |
printf '%s\n' "$VERSION" > version.txt
echo ${{ steps.sanitize.outputs.version }} > version.txt
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
branch: release/${{ steps.sanitize.outputs.version }}
commit-message: "chore: release v${{ steps.sanitize.outputs.version }}"
committer: Github Action Deploy <github-action-deploy@twenty.com>
author: Github Action Deploy <github-action-deploy@twenty.com>
title: Release v${{ steps.sanitize.outputs.version }}
labels: |
release
${{ github.event.inputs.create_release == true && 'create_release' || '' }}
branch: release/${{ steps.sanitize.outputs.version }}
commit-message: "chore: release v${{ steps.sanitize.outputs.version }}"
committer: Github Action Deploy <github-action-deploy@twenty.com>
author: Github Action Deploy <github-action-deploy@twenty.com>
title: Release v${{ steps.sanitize.outputs.version }}
labels: |
release
${{ github.event.inputs.create_release == true && 'create_release' || '' }}
+7 -22
View File
@@ -1,17 +1,9 @@
name: "Release: on merge"
permissions:
contents: write
on:
pull_request:
types:
- closed
defaults:
run:
shell: bash --noprofile --norc -euo pipefail {0}
jobs:
tag_and_release:
timeout-minutes: 10
@@ -20,40 +12,33 @@ jobs:
steps:
- name: Check PR Author
id: check_author
env:
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
set -euo pipefail
if [[ "$PR_AUTHOR" != "github-actions[bot]" ]]; then
echo "PR author ($PR_AUTHOR) is not trusted. Exiting."
if [[ "${{ github.event.pull_request.user.login }}" != "github-actions[bot]" ]]; then
echo "PR author (${AUTHOR}) is not trusted. Exiting."
exit 1
fi
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v2
with:
ref: main
- name: Get version from PR title
id: extract_version
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
set -euo pipefail
VERSION=$(printf '%s' "$PR_TITLE" | sed -n 's/.*Release v\([0-9][0-9.]*\).*/\1/p')
VERSION=$(echo "${{ github.event.pull_request.title }}" | sed -n 's/.*Release v\([0-9.]*\).*/\1/p')
if [ -z "$VERSION" ]; then
echo "No valid version found in PR title. Exiting."
exit 1
fi
printf 'VERSION=%s\n' "$VERSION" >> "$GITHUB_ENV"
echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: Push new tag
run: |
set -euo pipefail
git config --global user.name 'Github Action Deploy'
git config --global user.email 'github-action-deploy@twenty.com'
git tag "v${{ env.VERSION }}"
git push origin "v${{ env.VERSION }}"
git tag v${{ env.VERSION }}
git push origin v${{ env.VERSION }}
- uses: release-drafter/release-drafter@v5
if: contains(github.event.pull_request.labels.*.name, 'create_release')
+87 -169
View File
@@ -1,46 +1,29 @@
name: CI Server
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
env:
SERVER_SETUP_CACHE_KEY: server-setup
cancel-in-progress: true
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
package.json
packages/twenty-server/**
packages/twenty-front/src/generated/**
packages/twenty-front/src/generated-metadata/**
packages/twenty-emails/**
packages/twenty-shared/**
server-setup:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: depot-ubuntu-24.04-8
runs-on: ubuntu-latest
env:
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
services:
postgres:
image: twentycrm/twenty-postgres-spilo
env:
PGUSER_SUPERUSER: postgres
PGPASSWORD_SUPERUSER: postgres
ALLOW_NOSSL: 'true'
SPILO_PROVIDER: 'local'
PGPASSWORD_SUPERUSER: twenty
ALLOW_NOSSL: "true"
SPILO_PROVIDER: "local"
ports:
- 5432:5432
options: >-
@@ -57,107 +40,76 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/workflows/actions/yarn-install
- name: Restore server setup
id: restore-server-setup-cache
uses: ./.github/workflows/actions/restore-cache
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@v11
with:
key: ${{ env.SERVER_SETUP_CACHE_KEY }}
- name: Build twenty-shared
run: npx nx build twenty-shared
files: |
package.json
packages/twenty-server/**
packages/twenty-emails/**
- name: Install dependencies
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/yarn-install
- name: Server / Restore Task Cache
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/task-cache
with:
tag: scope:backend
- name: Server / Run lint & typecheck
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/nx-affected
with:
tag: scope:backend
tasks: lint,typecheck
- name: Server / Write .env
run: npx nx reset:env twenty-server
- name: Server / Build
if: steps.changed-files.outputs.any_changed == 'true'
run: npx nx build twenty-server
- name: Server / Write .env
if: steps.changed-files.outputs.any_changed == 'true'
run: npx nx reset:env twenty-server
- name: Server / Create DB
if: steps.changed-files.outputs.any_changed == 'true'
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
npx nx run twenty-server:database:init:prod
npx nx run twenty-server:database:migrate:prod
PGPASSWORD=twenty psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=twenty psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: Worker / Run
run: |
timeout 30s npx nx run twenty-server:worker || exit_code=$?
if [ $exit_code -eq 124 ]; then
# If timeout was reached (exit code 124), consider it a success
exit 0
elif [ $exit_code -ne 0 ]; then
# If worker failed for other reasons, fail the build
exit $exit_code
fi
- name: Server / Start
run: npx nx start:ci twenty-server &
- name: Waiting for server starting...
run: |
for i in {1..10}; do
if curl -f http://localhost:3000/healthz; then
echo "Server ready!"
exit 0
fi
echo "Waiting..."
sleep 2
done
echo "Server did not become healthy in time" >&2
exit 1
- name: Server / Check for Pending Migrations
run: |
CORE_MIGRATION_OUTPUT=$(npx nx run twenty-server:typeorm migration:generate core-migration-check -d src/database/typeorm/core/core.datasource.ts || true)
if: steps.changed-files.outputs.any_changed == 'true'
run: npx nx run twenty-server:worker:ci
CORE_MIGRATION_FILE=$(ls packages/twenty-server/*core-migration-check.ts 2>/dev/null || echo "")
if [ -n "$CORE_MIGRATION_FILE" ]; then
echo "::error::Unexpected migration files were generated. Please create a proper migration manually."
echo "$CORE_MIGRATION_OUTPUT"
rm -f packages/twenty-server/*core-migration-check.ts
exit 1
fi
- name: GraphQL / Check for Pending Generation
run: |
# Run GraphQL generation commands
npx nx run twenty-front:graphql:generate
npx nx run twenty-front:graphql:generate --configuration=metadata
# Check if any files were modified
if ! git diff --quiet; then
echo "::error::GraphQL schema changes detected. Please run 'npx nx run twenty-front:graphql:generate' and 'npx nx run twenty-front:graphql:generate --configuration=metadata' and commit the changes."
echo ""
echo "The following GraphQL schema changes were detected:"
echo "==================================================="
git diff
echo "==================================================="
echo ""
echo "Please run 'npx nx run twenty-front:graphql:generate' and 'npx nx run twenty-front:graphql:generate --configuration=metadata' and commit the changes."
echo ""
exit 1
fi
- name: Save server setup
uses: ./.github/workflows/actions/save-cache
with:
key: ${{ steps.restore-server-setup-cache.outputs.cache-primary-key }}
server-test:
timeout-minutes: 30
runs-on: depot-ubuntu-24.04-8
runs-on: ubuntu-latest
needs: server-setup
env:
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/workflows/actions/yarn-install
- name: Restore server setup
uses: ./.github/workflows/actions/restore-cache
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@v11
with:
key: ${{ env.SERVER_SETUP_CACHE_KEY }}
- name: Server / Run Tests
files: |
package.json
packages/twenty-server/**
packages/twenty-emails/**
- name: Install dependencies
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/yarn-install
- name: Server / Restore Task Cache
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/task-cache
with:
tag: scope:backend
- name: Server / Run Tests
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/nx-affected
with:
tag: scope:backend
@@ -165,20 +117,16 @@ jobs:
server-integration-test:
timeout-minutes: 30
runs-on: depot-ubuntu-24.04-8
runs-on: ubuntu-latest
needs: server-setup
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5]
services:
postgres:
image: twentycrm/twenty-postgres-spilo
env:
PGUSER_SUPERUSER: postgres
PGPASSWORD_SUPERUSER: postgres
ALLOW_NOSSL: 'true'
SPILO_PROVIDER: 'local'
PGPASSWORD_SUPERUSER: twenty
ALLOW_NOSSL: "true"
SPILO_PROVIDER: "local"
ports:
- 5432:5432
options: >-
@@ -190,70 +138,40 @@ jobs:
image: redis
ports:
- 6379:6379
clickhouse:
image: clickhouse/clickhouse-server:25.8.8
env:
CLICKHOUSE_PASSWORD: clickhousePassword
CLICKHOUSE_URL: "http://default:clickhousePassword@localhost:8123/twenty"
ports:
- 8123:8123
- 9000:9000
options: >-
--health-cmd "clickhouse-client --host=localhost --port=9000 --user=default --password=clickhousePassword --query='SELECT 1'"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
NODE_ENV: test
ANALYTICS_ENABLED: true
CLICKHOUSE_URL: "http://default:clickhousePassword@localhost:8123/twenty"
CLICKHOUSE_PASSWORD: clickhousePassword
SHARD_COUNTER: 5
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/workflows/actions/yarn-install
- name: Update .env.test for integrations tests
run: |
echo "" >> .env.test
echo "IS_BILLING_ENABLED=true" >> .env.test
echo "BILLING_STRIPE_API_KEY=test-api-key" >> .env.test
echo "BILLING_STRIPE_BASE_PLAN_PRODUCT_ID=test-base-plan-product-id" >> .env.test
echo "BILLING_STRIPE_WEBHOOK_SECRET=test-webhook-secret" >> .env.test
echo "BILLING_PLAN_REQUIRED_LINK=http://localhost:3001/stripe-redirection" >> .env.test
- name: Restore server setup
uses: ./.github/workflows/actions/restore-cache
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@v11
with:
key: ${{ env.SERVER_SETUP_CACHE_KEY }}
- name: Server / Build
run: npx nx build twenty-server
- name: Build dependencies
run: |
npx nx build twenty-shared
npx nx build twenty-emails
- name: Server / Create Test DB
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: Run ClickHouse migrations
run: npx nx clickhouse:migrate twenty-server
- name: Run ClickHouse seeds
run: npx nx clickhouse:seed twenty-server
files: |
package.json
packages/twenty-server/**
packages/twenty-emails/**
- name: Install dependencies
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/yarn-install
- name: Server / Restore Task Cache
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/task-cache
with:
tag: scope:backend
- name: Server / Run Integration Tests
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/nx-affected
with:
tag: scope:backend
tasks: 'test:integration'
configuration: 'with-db-reset'
args: --shard=${{ matrix.shard }}/${{ env.SHARD_COUNTER }}
ci-server-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, server-setup, server-test, server-integration-test]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1
tasks: "test:integration:with-db-reset"
- name: Server / Upload reset-logs file
if: always()
uses: actions/upload-artifact@v4
with:
name: reset-logs
path: reset-logs.log
-55
View File
@@ -1,55 +0,0 @@
name: CI Shared
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/twenty-shared/**
shared-test:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
matrix:
task: [lint, typecheck, test]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/workflows/actions/yarn-install
- name: Run ${{ matrix.task }} task
uses: ./.github/workflows/actions/nx-affected
with:
tag: scope:frontend
tasks: ${{ matrix.task }}
ci-shared-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, shared-test]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1
+77 -79
View File
@@ -1,94 +1,92 @@
name: 'Test Docker Compose'
on:
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
cancel-in-progress: true
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/twenty-docker/**
docker-compose.yml
test:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run compose
run: |
echo "Patching docker-compose.yml..."
# change image to localbuild using yq
yq eval 'del(.services.server.image)' -i docker-compose.yml
yq eval '.services.server.build.context = "../../"' -i docker-compose.yml
yq eval '.services.server.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i docker-compose.yml
yq eval '.services.server.restart = "no"' -i docker-compose.yml
- name: Checkout
uses: actions/checkout@v2
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@v11
with:
files: |
packages/twenty-docker/**
docker-compose.yml
- name: Skip if no relevant changes
if: steps.changed-files.outputs.any_changed != 'true'
run: echo "No relevant changes detected. Marking as valid."
echo "Setting up .env file..."
cp .env.example .env
echo "Generating secrets..."
echo "" >> .env
echo "# === Randomly generated secrets ===" >>.env
echo "APP_SECRET=$(openssl rand -base64 32)" >>.env
echo "PGPASSWORD_SUPERUSER=$(openssl rand -hex 16)" >>.env
- name: Run compose
if: steps.changed-files.outputs.any_changed == 'true'
run: |
echo "Patching docker-compose.yml..."
# change image to localbuild using yq
yq eval 'del(.services.server.image)' -i docker-compose.yml
yq eval '.services.server.build.context = "../../"' -i docker-compose.yml
yq eval '.services.server.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i docker-compose.yml
yq eval '.services.server.restart = "no"' -i docker-compose.yml
echo "Docker compose up..."
docker compose up -d || {
echo "Docker compose failed to start"
docker compose logs
exit 1
}
docker compose logs db server -f &
pid=$!
yq eval 'del(.services.db.image)' -i docker-compose.yml
yq eval '.services.db.build.context = "../../"' -i docker-compose.yml
yq eval '.services.db.build.dockerfile = "./packages/twenty-docker/twenty-postgres-spilo/Dockerfile"' -i docker-compose.yml
echo "Waiting for database to start..."
count=0
while [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-db-1) = "healthy" ]; do
sleep 1;
count=$((count+1));
if [ $(docker inspect --format='{{.State.Status}}' twenty-db-1) = "exited" ]; then
echo "Database exited"
docker compose logs db
exit 1
fi
if [ $count -gt 300 ]; then
echo "Failed to start database after 5 minutes"
docker compose logs db
exit 1
fi
echo "Still waiting for database... (${count}/60)"
done
echo "Setting up .env file..."
cp .env.example .env
echo "Generating secrets..."
echo "# === Randomly generated secrets ===" >>.env
echo "APP_SECRET=$(openssl rand -base64 32)" >>.env
echo "PGPASSWORD_SUPERUSER=$(openssl rand -base64 32)" >>.env
echo "Waiting for server to start..."
count=0
while [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-server-1) = "healthy" ]; do
sleep 1;
count=$((count+1));
if [ $(docker inspect --format='{{.State.Status}}' twenty-server-1) = "exited" ]; then
echo "Server exited"
docker compose logs server
exit 1
fi
if [ $count -gt 300 ]; then
echo "Failed to start server after 5 minutes"
docker compose logs server
exit 1
fi
echo "Still waiting for server... (${count}/300s)"
done
working-directory: ./packages/twenty-docker/
ci-test-docker-compose-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, test]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1
echo "Docker compose up..."
docker compose up -d || {
echo "Docker compose failed to start"
docker compose logs
exit 1
}
docker compose logs db server -f &
pid=$!
echo "Waiting for database to start..."
count=0
while [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-db-1) = "healthy" ]; do
sleep 1;
count=$((count+1));
if [ $(docker inspect --format='{{.State.Status}}' twenty-db-1) = "exited" ]; then
echo "Database exited"
docker compose logs db
exit 1
fi
if [ $count -gt 300 ]; then
echo "Failed to start database after 5 minutes"
docker compose logs db
exit 1
fi
echo "Still waiting for database... (${count}/60)"
done
echo "Waiting for server to start..."
count=0
while [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-server-1) = "healthy" ]; do
sleep 1;
count=$((count+1));
if [ $(docker inspect --format='{{.State.Status}}' twenty-server-1) = "exited" ]; then
echo "Server exited"
docker compose logs server
exit 1
fi
if [ $count -gt 300 ]; then
echo "Failed to start server after 5 minutes"
docker compose logs server
exit 1
fi
echo "Still waiting for server... (${count}/300s)"
done
working-directory: ./packages/twenty-docker/
+26
View File
@@ -0,0 +1,26 @@
name: CI Tinybird
on:
push:
branches:
- main
paths:
- 'package.json'
- 'packages/twenty-tinybird/**'
pull_request:
paths:
- 'package.json'
- 'packages/twenty-tinybird/**'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ci:
uses: tinybirdco/ci/.github/workflows/ci.yml@main
with:
data_project_dir: packages/twenty-tinybird
secrets:
tb_admin_token: ${{ secrets.TB_ADMIN_TOKEN }}
tb_host: https://api.eu-central-1.aws.tinybird.co
+1 -4
View File
@@ -1,5 +1,4 @@
name: CI Utils
on:
# it's usually not recommended to use pull_request_target
# but we consider it's safe here if we keep the same steps
@@ -7,7 +6,6 @@ on:
# and: https://github.com/facebook/react-native/pull/34370/files
pull_request_target:
types: [opened, synchronize, reopened, closed]
permissions:
actions: write
checks: write
@@ -15,7 +13,6 @@ permissions:
issues: write
pull-requests: write
statuses: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
# We don't cancel in-progress because this workflow is triggered on
@@ -24,7 +21,7 @@ concurrency:
jobs:
danger-js:
timeout-minutes: 5
timeout-minutes: 3
runs-on: ubuntu-latest
if: github.event.action != 'closed'
steps:
+23 -34
View File
@@ -1,39 +1,28 @@
name: CI Website
permissions:
contents: read
on:
push:
branches:
- main
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
cancel-in-progress: true
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
package.json
packages/twenty-website/**
website-build:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 10
timeout-minutes: 3
runs-on: ubuntu-latest
services:
postgres:
image: twentycrm/twenty-postgres-spilo
env:
PGUSER_SUPERUSER: postgres
PGPASSWORD_SUPERUSER: postgres
ALLOW_NOSSL: 'true'
SPILO_PROVIDER: 'local'
PGPASSWORD_SUPERUSER: twenty
ALLOW_NOSSL: "true"
SPILO_PROVIDER: "local"
ports:
- 5432:5432
options: >-
@@ -45,31 +34,31 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@v11
with:
files: 'package.json, packages/twenty-website/**'
- name: Install dependencies
if: steps.changed-files.outputs.changed == 'true'
uses: ./.github/workflows/actions/yarn-install
- name: Server / Create DB
run: PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
if: steps.changed-files.outputs.any_changed == 'true'
run: PGPASSWORD=twenty psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
- name: Website / Run migrations
run: npx nx database:migrate twenty-website
if: steps.changed-files.outputs.changed == 'true'
run: npx nx database:migrate twenty-website
env:
DATABASE_PG_URL: postgres://postgres:postgres@localhost:5432/default
DATABASE_PG_URL: postgres://postgres:twenty@localhost:5432/default
- name: Website / Build Website
if: steps.changed-files.outputs.changed == 'true'
run: npx nx build twenty-website
env:
DATABASE_PG_URL: postgres://postgres:postgres@localhost:5432/default
KEYSTATIC_GITHUB_CLIENT_ID: xxx
KEYSTATIC_GITHUB_CLIENT_SECRET: xxx
KEYSTATIC_SECRET: xxx
NEXT_PUBLIC_KEYSTATIC_GITHUB_APP_SLUG: xxx
ci-website-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, website-build]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1
DATABASE_PG_URL: postgres://postgres:twenty@localhost:5432/default
- name: Mark as VALID
if: steps.changed-files.outputs.changed != 'true' # If no changes, mark as valid
run: echo "No relevant changes detected. CI is valid."
-132
View File
@@ -1,132 +0,0 @@
# Pull down translations from Crowdin every two hours or when triggered manually.
# When force_pull input is true, translations will be pulled regardless of compilation status.
name: 'Pull translations from Crowdin'
permissions:
contents: write
pull-requests: write
on:
schedule:
- cron: '0 */2 * * *' # Every two hours.
workflow_dispatch:
inputs:
force_pull:
description: 'Force pull translations regardless of compilation status'
required: false
type: boolean
default: false
workflow_call:
inputs:
force_pull:
description: 'Force pull translations regardless of compilation status'
required: false
type: boolean
default: false
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
pull_translations:
name: Pull translations
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ github.token }}
ref: ${{ github.head_ref || github.ref_name }}
- name: Setup i18n branch
run: |
git fetch origin i18n || true
git checkout -B i18n origin/i18n || git checkout -b i18n
- name: Install dependencies
uses: ./.github/workflows/actions/yarn-install
- name: Build twenty-shared
run: npx nx build twenty-shared
# Strict mode fails if there are missing translations.
- name: Compile translations
id: compile_translations_strict
run: |
npx nx run twenty-server:lingui:compile --strict
npx nx run twenty-emails:lingui:compile --strict
npx nx run twenty-front:lingui:compile --strict
continue-on-error: true
- name: Stash any changes before pulling translations
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@twenty.com'
git add .
git stash
- name: Pull translations from Crowdin
if: inputs.force_pull || steps.compile_translations_strict.outcome == 'failure'
uses: crowdin/github-action@v2
with:
upload_sources: false
upload_translations: false
download_translations: true
export_only_approved: false
localization_branch_name: i18n
base_url: 'https://twenty.api.crowdin.com'
auto_approve_imported: false
import_eq_suggestions: false
download_sources: false
push_sources: false
skip_untranslated_strings: false
skip_untranslated_files: false
push_translations: true
create_pull_request: false
skip_ref_checkout: true
dryrun_action: false
env:
GITHUB_TOKEN: ${{ github.token }}
CROWDIN_PROJECT_ID: '1'
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
# As the files are extracted from a Docker container, they belong to root:root
# We need to fix this before the next steps
- name: Fix file permissions
run: sudo chown -R runner:docker .
- name: Compile translations
id: compile_translations
# Because we have set English as a fallback locale, this condition does not work anymore
# if: inputs.force_pull || steps.compile_translations_strict.outcome == 'failure'
run: |
npx nx run twenty-server:lingui:compile
npx nx run twenty-emails:lingui:compile
npx nx run twenty-front:lingui:compile
git status
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@twenty.com'
git add .
if ! git diff --staged --quiet --exit-code; then
git commit -m "chore: compile translations"
echo "changes_detected=true" >> $GITHUB_OUTPUT
else
echo "changes_detected=false" >> $GITHUB_OUTPUT
fi
- name: Push changes
if: steps.compile_translations.outputs.changes_detected == 'true'
run: git push origin HEAD:i18n
- name: Create pull request
if: steps.compile_translations.outputs.changes_detected == 'true'
run: |
if git diff --name-only origin/main..HEAD | grep -q .; then
gh pr create -B main -H i18n --title 'i18n - translations' --body 'Created by Github action' || true
else
echo "No file differences between branches, skipping PR creation"
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-105
View File
@@ -1,105 +0,0 @@
name: 'Push translations to Crowdin'
permissions:
contents: write
pull-requests: write
on:
workflow_dispatch:
workflow_call:
push:
branches: ['main']
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
extract_translations:
name: Extract and upload translations
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ github.token }}
ref: main
- name: Setup i18n branch
run: |
git fetch origin i18n || true
git checkout -B i18n origin/i18n || git checkout -b i18n
- name: Install dependencies
uses: ./.github/workflows/actions/yarn-install
- name: Build dependencies
run: npx nx build twenty-shared
- name: Extract translations
run: |
npx nx run twenty-server:lingui:extract
npx nx run twenty-emails:lingui:extract
npx nx run twenty-front:lingui:extract
- name: Check and commit extracted files
id: check_extract_changes
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@twenty.com'
git add .
if ! git diff --staged --quiet --exit-code; then
git commit -m "chore: extract translations"
echo "changes_detected=true" >> $GITHUB_OUTPUT
else
echo "changes_detected=false" >> $GITHUB_OUTPUT
fi
- name: Compile translations
run: |
npx nx run twenty-server:lingui:compile
npx nx run twenty-emails:lingui:compile
npx nx run twenty-front:lingui:compile
- name: Check and commit compiled files
id: check_compile_changes
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@twenty.com'
git add .
if ! git diff --staged --quiet --exit-code; then
git commit -m "chore: compile translations"
echo "changes_detected=true" >> $GITHUB_OUTPUT
else
echo "changes_detected=false" >> $GITHUB_OUTPUT
fi
- name: Push changes and create remote branch if needed
if: steps.check_extract_changes.outputs.changes_detected == 'true' || steps.check_compile_changes.outputs.changes_detected == 'true'
run: git push origin HEAD:i18n
- name: Upload missing translations
if: steps.check_extract_changes.outputs.changes_detected == 'true'
uses: crowdin/github-action@v2
with:
upload_sources: true
upload_translations: true
download_translations: false
localization_branch_name: i18n
base_url: 'https://twenty.api.crowdin.com'
env:
# A numeric ID, found at https://crowdin.com/project/<projectName>/tools/api
CROWDIN_PROJECT_ID: 1
# Visit https://crowdin.com/settings#api-key to create this token
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Create a pull request
if: steps.check_extract_changes.outputs.changes_detected == 'true' || steps.check_compile_changes.outputs.changes_detected == 'true'
run: |
if git diff --name-only origin/main..HEAD | grep -q .; then
gh pr create -B main -H i18n --title 'i18n - translations' --body 'Created by Github action' || true
else
echo "No file differences between branches, skipping PR creation"
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1,37 +0,0 @@
name: 'Preview Environment Dispatch'
permissions:
contents: write
actions: write
pull-requests: read
on:
# Using pull_request_target instead of pull_request to have access to secrets for external contributors
# Security note: This is safe because we're only using the repository-dispatch action with limited scope
# and not checking out or running any code from the external contributor's PR
pull_request_target:
types: [opened, synchronize, reopened, labeled]
paths:
- packages/twenty-docker/**
- packages/twenty-server/**
- packages/twenty-front/**
- .github/workflows/preview-env-dispatch.yaml
- .github/workflows/preview-env-keepalive.yaml
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
trigger-preview:
if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.action == 'reopened' || (github.event.action == 'labeled' && github.event.label.name == 'preview-app')
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Trigger preview environment workflow
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
repository: ${{ github.repository }}
event-type: preview-environment
client-payload: '{"pr_number": "${{ github.event.pull_request.number }}", "pr_head_sha": "${{ github.event.pull_request.head.sha }}", "repo_full_name": "${{ github.repository }}"}'
@@ -1,159 +0,0 @@
name: 'Preview Environment Keep Alive'
permissions:
contents: read
pull-requests: write
on:
repository_dispatch:
types: [preview-environment]
jobs:
preview-environment:
timeout-minutes: 310
runs-on: ubuntu-latest
steps:
- name: Checkout PR
uses: actions/checkout@v4
with:
ref: ${{ github.event.client_payload.pr_head_sha }}
- name: Run compose setup
run: |
echo "Patching docker-compose.yml..."
# change image to localbuild using yq
yq eval 'del(.services.server.image)' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.server.build.context = "../../"' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.server.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i packages/twenty-docker/docker-compose.yml
yq eval 'del(.services.worker.image)' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.worker.build.context = "../../"' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.worker.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i packages/twenty-docker/docker-compose.yml
echo "Adding SIGN_IN_PREFILLED environment variable to server service..."
yq eval '.services.server.environment.SIGN_IN_PREFILLED = "${SIGN_IN_PREFILLED}"' -i packages/twenty-docker/docker-compose.yml
echo "Setting up .env file..."
cp packages/twenty-docker/.env.example packages/twenty-docker/.env
echo "Generating secrets..."
echo "" >> packages/twenty-docker/.env
echo "# === Randomly generated secrets ===" >> packages/twenty-docker/.env
echo "APP_SECRET=$(openssl rand -base64 32)" >> packages/twenty-docker/.env
echo "PG_DATABASE_PASSWORD=$(openssl rand -hex 16)" >> packages/twenty-docker/.env
echo "SIGN_IN_PREFILLED=true" >> packages/twenty-docker/.env
echo "Docker compose build..."
cd packages/twenty-docker/
docker compose build
working-directory: ./
- name: Create Tunnel
id: expose-tunnel
uses: codetalkio/expose-tunnel@v1.5.0
with:
service: bore.pub
port: 3000
- name: Start services with correct SERVER_URL
run: |
cd packages/twenty-docker/
# Update the SERVER_URL with the tunnel URL
echo "Setting SERVER_URL to ${{ steps.expose-tunnel.outputs.tunnel-url }}"
sed -i '/SERVER_URL=/d' .env
echo "" >> .env
echo "SERVER_URL=${{ steps.expose-tunnel.outputs.tunnel-url }}" >> .env
# Start the services
echo "Docker compose up..."
docker compose up -d || {
echo "Docker compose failed to start"
docker compose logs
exit 1
}
echo "Waiting for services to be ready..."
count=0
while [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-db-1) = "healthy" ] || [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-server-1) = "healthy" ]; do
sleep 5
count=$((count+1))
if [ $count -gt 60 ]; then
echo "Timeout waiting for services to be ready"
docker compose logs
exit 1
fi
echo "Still waiting for services... ($count/60)"
done
echo "All services are up and running!"
working-directory: ./
- name: Seed Dev Workspace
run: |
cd packages/twenty-docker/
echo "Seeding full dev workspace..."
if ! docker compose exec -T server yarn command:prod -- workspace:seed:dev; then
echo "❌ Seeding full dev workspace failed. Dumping server logs..."
docker compose logs server
exit 1
fi
working-directory: ./
- name: Output tunnel URL to logs
run: |
echo "✅ Preview Environment Ready!"
echo "🔗 Preview URL: ${{ steps.expose-tunnel.outputs.tunnel-url }}"
echo "⏱️ This environment will be available for 5 hours"
- name: Post comment on PR
uses: actions/github-script@v6
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const COMMENT_MARKER = '<!-- PR_PREVIEW_ENV -->';
const commentBody = `${COMMENT_MARKER}
🚀 **Preview Environment Ready!**
Your preview environment is available at: ${{ steps.expose-tunnel.outputs.tunnel-url }}
This environment will automatically shut down when the PR is closed or after 5 hours.`;
// Get all comments
const {data: comments} = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: ${{ github.event.client_payload.pr_number }},
});
// Find our comment
const botComment = comments.find(comment => comment.body.includes(COMMENT_MARKER));
if (botComment) {
// Update existing comment
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: commentBody
});
console.log('Updated existing comment');
} else {
// Create new comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: ${{ github.event.client_payload.pr_number }},
body: commentBody
});
console.log('Created new comment');
}
- name: Keep tunnel alive for 5 hours
run: timeout 300m sleep 18000 # Stop on whichever we reach first (300m or 5hour sleep)
- name: Cleanup
if: always()
run: |
cd packages/twenty-docker/
docker compose down -v
working-directory: ./
-18
View File
@@ -9,7 +9,6 @@
.nx/installation
.nx/cache
.nx/workspace-data
.pnp.*
.yarn/*
@@ -19,7 +18,6 @@
!.yarn/sdks
!.yarn/versions
.vercel
.swc
**/**/logs/**
@@ -32,19 +30,3 @@ storybook-static
test-results/
dump.rdb
.tinyb
.notes
/data/
/.devenv/
/.direnv/
/.pre-commit-config.yaml
/.envrc
/devenv.nix
/flake.lock
/flake.nix
.crowdin.yml
.react-email/
mcp.json
/.junie/
-24
View File
@@ -1,24 +0,0 @@
{
"mcpServers": {
"postgres": {
"type": "stdio",
"command": "uv",
"args": ["run", "postgres-mcp", "--access-mode=unrestricted"],
"env": {
"DATABASE_URI": "${PG_DATABASE_URL}"
}
},
"playwright": {
"type": "stdio",
"command": "npx",
"args": ["@playwright/mcp@latest", "--no-sandbox", "--headless"],
"env": {}
},
"context7": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@upstash/context7-mcp"],
"env": {}
}
}
}
+1 -1
View File
@@ -1 +1 @@
24.5.0
18.17.1
+2 -2
View File
@@ -1,5 +1,5 @@
{
"singleQuote": true,
"trailingComma": "all",
"endOfLine": "lf"
}
"endOfLine": "auto"
}
+1 -2
View File
@@ -14,7 +14,6 @@
"styled-components.vscode-styled-components",
"unifiedjs.vscode-mdx",
"xyc.vscode-mdx-preview",
"yoavbls.pretty-ts-errors",
"ms-playwright.playwright"
"yoavbls.pretty-ts-errors"
]
}
+17 -62
View File
@@ -5,10 +5,14 @@
{
"name": "twenty-server - start debug",
"type": "node",
"runtimeVersion": "24.5",
"request": "launch",
"runtimeExecutable": "npx",
"runtimeArgs": ["nx", "run", "twenty-server:start"],
"runtimeExecutable": "yarn",
"runtimeVersion": "18",
"runtimeArgs": [
"nx",
"run",
"twenty-server:start",
],
"outputCapture": "std",
"internalConsoleOptions": "openOnSessionStart",
"console": "internalConsole",
@@ -18,8 +22,13 @@
"name": "twenty-server - worker debug",
"type": "node",
"request": "launch",
"runtimeExecutable": "npx",
"runtimeArgs": ["nx", "run", "twenty-server:worker"],
"runtimeExecutable": "yarn",
"runtimeVersion": "18",
"runtimeArgs": [
"nx",
"run",
"twenty-server:worker",
],
"outputCapture": "std",
"internalConsoleOptions": "openOnSessionStart",
"console": "internalConsole",
@@ -30,72 +39,18 @@
"type": "node",
"request": "launch",
"runtimeExecutable": "npx",
"runtimeVersion": "24.5",
"runtimeVersion": "18",
"runtimeArgs": [
"nx",
"run",
"twenty-server:command",
"my-command",
"--my-parameter value"
"--my-parameter value",
],
"outputCapture": "std",
"internalConsoleOptions": "openOnSessionStart",
"console": "internalConsole",
"cwd": "${workspaceFolder}/packages/twenty-server/"
},
{
"name": "Playwright Test current file",
"type": "node",
"request": "launch",
"runtimeExecutable": "npx",
"runtimeArgs": ["nx", "test", "twenty-e2e-testing", "${file}"],
"console": "integratedTerminal",
"cwd": "${workspaceFolder}",
"internalConsoleOptions": "neverOpen",
"envFile": "${workspaceFolder}/packages/twenty-e2e-testing/.env"
},
{
"type": "node",
"request": "launch",
"name": "twenty-server - debug integration test file (to launch with test file open)",
"runtimeExecutable": "npx",
"runtimeArgs": [
"nx",
"run",
"twenty-server:jest",
"--",
"--config",
"./jest-integration.config.ts",
"${relativeFile}",
"--silent=false"
],
"cwd": "${workspaceFolder}/packages/twenty-server",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"env": {
"NODE_ENV": "test"
}
},
{
"type": "node",
"request": "launch",
"name": "twenty-server - debug unit test file (to launch with test file open)",
"runtimeExecutable": "npx",
"runtimeArgs": [
"nx",
"run",
"twenty-server:jest",
"--",
"--config",
"./jest.config.mjs",
"${relativeFile}"
],
"cwd": "${workspaceFolder}/packages/twenty-server",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"env": {
"NODE_ENV": "test"
}
}
]
}
}
+2 -9
View File
@@ -1,8 +1,6 @@
{
"editor.formatOnSave": false,
"files.eol": "\n",
"files.insertFinalNewline": true,
"files.trimTrailingWhitespace": true,
"files.eol": "auto",
"[typescript]": {
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
@@ -45,12 +43,7 @@
],
"typescript.preferences.importModuleSpecifier": "non-relative",
"search.exclude": {
"**/.yarn": true
"**/.yarn": true,
},
"eslint.debug": true,
"files.associations": {
".cursorrules": "markdown"
},
"jestrunner.codeLensSelector": "**/*.{test,spec,integration-spec}.{js,jsx,ts,tsx}",
"typescript.tsdk": "node_modules/typescript/lib"
}
+4 -8
View File
@@ -4,6 +4,10 @@
"name": "ROOT",
"path": "../"
},
{
"name": "packages/twenty-chrome-extension",
"path": "../packages/twenty-chrome-extension"
},
{
"name": "packages/twenty-docker",
"path": "../packages/twenty-docker"
@@ -20,10 +24,6 @@
"name": "packages/twenty-emails",
"path": "../packages/twenty-emails"
},
{
"name": "packages/twenty-shared",
"path": "../packages/twenty-shared"
},
{
"name": "packages/twenty-server",
"path": "../packages/twenty-server"
@@ -40,10 +40,6 @@
"name": "tools/eslint-rules",
"path": "../tools/eslint-rules"
},
{
"name": "packages/twenty-e2e-testing",
"path": "../packages/twenty-e2e-testing"
}
],
"settings": {
"editor.formatOnSave": false,
@@ -1,45 +0,0 @@
diff --git a/commonjs/helpers/phoneInputHelpers.js b/commonjs/helpers/phoneInputHelpers.js
index 8fd1b16ec670ab7d0abcb800a2edd7848e86ce1e..ab6e0dd90def507629c2ead59fba929a739f0759 100644
--- a/commonjs/helpers/phoneInputHelpers.js
+++ b/commonjs/helpers/phoneInputHelpers.js
@@ -574,7 +574,9 @@ function onPhoneDigitsChange(phoneDigits, _ref5) {
// Generate the new `value` property.
var value;
if (phoneDigits) {
- if (phoneDigits[0] === '+') {
+ if(phoneDigits === prevPhoneDigits) {
+ value = e164(phoneDigits, country, metadata)
+ } else if (phoneDigits[0] === '+') {
if (phoneDigits === '+') {
value = undefined;
} else if (country && (0, _getInternationalPhoneNumberPrefix["default"])(country, metadata).indexOf(phoneDigits) === 0) {
diff --git a/modules/helpers/phoneInputHelpers.js b/modules/helpers/phoneInputHelpers.js
index 75e8463f6b5d32c560d1dec2776671bee2af1d07..99af2904807cc3c23aea566bbeebda2a5bc9afa4 100644
--- a/modules/helpers/phoneInputHelpers.js
+++ b/modules/helpers/phoneInputHelpers.js
@@ -551,7 +551,9 @@ export function onPhoneDigitsChange(phoneDigits, _ref5) {
// Generate the new `value` property.
var value;
if (phoneDigits) {
- if (phoneDigits[0] === '+') {
+ if(phoneDigits === prevPhoneDigits) {
+ value = e164(phoneDigits, country, metadata)
+ } else if (phoneDigits[0] === '+') {
if (phoneDigits === '+') {
value = undefined;
} else if (country && getInternationalPhoneNumberPrefix(country, metadata).indexOf(phoneDigits) === 0) {
diff --git a/react-styleguidist/project/source/helpers/phoneInputHelpers.js b/react-styleguidist/project/source/helpers/phoneInputHelpers.js
index b844ef0fd183fe4f45e436bdb2382fc2987dcef2..8960834900e59a57e130521fd369e82fe05f5090 100644
--- a/react-styleguidist/project/source/helpers/phoneInputHelpers.js
+++ b/react-styleguidist/project/source/helpers/phoneInputHelpers.js
@@ -505,7 +505,9 @@ export function onPhoneDigitsChange(phoneDigits, {
// Generate the new `value` property.
let value
if (phoneDigits) {
- if (phoneDigits[0] === '+') {
+ if(phoneDigits === prevPhoneDigits) {
+ value = e164(phoneDigits, country, metadata)
+ } else if (phoneDigits[0] === '+') {
if (phoneDigits === '+') {
value = undefined
} else if (country && getInternationalPhoneNumberPrefix(country, metadata).indexOf(phoneDigits) !== 0) {
+925
View File
File diff suppressed because one or more lines are too long
-942
View File
File diff suppressed because one or more lines are too long
+1 -5
View File
@@ -1,9 +1,5 @@
enableConstraintsChecks: true
enableHardenedMode: true
enableInlineHunks: true
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.9.2.cjs
yarnPath: .yarn/releases/yarn-4.4.0.cjs
-152
View File
@@ -1,152 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Twenty is an open-source CRM built with modern technologies in a monorepo structure. The codebase is organized as an Nx workspace with multiple packages.
## Key Commands
### Development
```bash
# Start development environment (frontend + backend + worker)
yarn start
# Individual package development
npx nx start twenty-front # Start frontend dev server
npx nx start twenty-server # Start backend server
npx nx run twenty-server:worker # Start background worker
```
### Testing
```bash
# Run tests
npx nx test twenty-front # Frontend unit tests
npx nx test twenty-server # Backend unit tests
npx nx run twenty-server:test:integration:with-db-reset # Integration tests with DB reset
# Storybook
npx nx storybook:build twenty-front # Build Storybook
npx nx storybook:serve-and-test:static twenty-front # Run Storybook tests
When testing the UI end to end, click on "Continue with Email" and use the prefilled credentials.
```
### Code Quality
```bash
# Linting
npx nx lint twenty-front # Frontend linting
npx nx lint twenty-server # Backend linting
npx nx lint twenty-front --fix # Auto-fix linting issues
# Type checking
npx nx typecheck twenty-front
npx nx typecheck twenty-server
# Format code
npx nx fmt twenty-front
npx nx fmt twenty-server
```
### Build
```bash
# Build packages
npx nx build twenty-front
npx nx build twenty-server
```
### Database Operations
```bash
# Database management
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
# Generate migration
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/[name] -d src/database/typeorm/core/core.datasource.ts
# Sync metadata
npx nx run twenty-server:command workspace:sync-metadata
```
### GraphQL
```bash
# Generate GraphQL types
npx nx run twenty-front:graphql:generate
```
## Architecture Overview
### Tech Stack
- **Frontend**: React 18, TypeScript, Recoil (state management), Emotion (styling), Vite
- **Backend**: NestJS, TypeORM, PostgreSQL, Redis, GraphQL (with GraphQL Yoga)
- **Monorepo**: Nx workspace managed with Yarn 4
### Package Structure
```
packages/
├── twenty-front/ # React frontend application
├── twenty-server/ # NestJS backend API
├── twenty-ui/ # Shared UI components library
├── twenty-shared/ # Common types and utilities
├── twenty-emails/ # Email templates with React Email
├── twenty-website/ # Next.js documentation website
├── twenty-zapier/ # Zapier integration
└── twenty-e2e-testing/ # Playwright E2E tests
```
### Key Development Principles
- **Functional components only** (no class components)
- **Named exports only** (no default exports)
- **Types over interfaces** (except when extending third-party interfaces)
- **String literals over enums** (except for GraphQL enums)
- **No 'any' type allowed**
- **Event handlers preferred over useEffect** for state updates
### State Management
- **Recoil** for global state management
- Component-specific state with React hooks
- GraphQL cache managed by Apollo Client
### Backend Architecture
- **NestJS modules** for feature organization
- **TypeORM** for database ORM with PostgreSQL
- **GraphQL** API with code-first approach
- **Redis** for caching and session management
- **BullMQ** for background job processing
### Database
- **PostgreSQL** as primary database
- **Redis** for caching and sessions
- **TypeORM migrations** for schema management
- **ClickHouse** for analytics (when enabled)
## Development Workflow
IMPORTANT: Use Context7 for code generation, setup or configuration steps, or library/API documentation. Automatically use the Context7 MCP tools to resolve library IDs and get library docs without waiting for explicit requests.
### Before Making Changes
1. Always run linting and type checking after code changes
2. Test changes with relevant test suites
3. Ensure database migrations are properly structured
4. Check that GraphQL schema changes are backward compatible
### Code Style Notes
- Use **Emotion** for styling with styled-components pattern
- Follow **Nx** workspace conventions for imports
- Use **Lingui** for internationalization
- Components should be in their own directories with tests and stories
### Testing Strategy
- **Unit tests** with Jest for both frontend and backend
- **Integration tests** for critical backend workflows
- **Storybook** for component development and testing
- **E2E tests** with Playwright for critical user flows
## Important Files
- `nx.json` - Nx workspace configuration with task definitions
- `tsconfig.base.json` - Base TypeScript configuration
- `package.json` - Root package with workspace definitions
- `.cursor/rules/` - Development guidelines and best practices
+10 -38
View File
@@ -1,48 +1,20 @@
DOCKER_NETWORK=twenty_network
ensure-docker-network:
docker network inspect $(DOCKER_NETWORK) >/dev/null 2>&1 || docker network create $(DOCKER_NETWORK)
postgres-on-docker: ensure-docker-network
docker run -d --network $(DOCKER_NETWORK) \
postgres-on-docker:
docker run -d \
--name twenty_pg \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-e PGUSER_SUPERUSER=postgres \
-e PGPASSWORD_SUPERUSER=twenty \
-e ALLOW_NOSSL=true \
-v twenty_db_data:/var/lib/postgresql/data \
-v twenty_db_data:/home/postgres/pgdata \
-p 5432:5432 \
postgres:16
twentycrm/twenty-postgres-spilo:latest
@echo "Waiting for PostgreSQL to be ready..."
@until docker exec twenty_pg psql -U postgres -d postgres \
@until PGPASSWORD=twenty psql -h localhost -p 5432 -U postgres -d postgres \
-c 'SELECT pg_is_in_recovery();' 2>/dev/null | grep -q 'f'; do \
sleep 1; \
done
docker exec twenty_pg psql -U postgres -d postgres \
PGPASSWORD=twenty psql -h localhost -p 5432 -U postgres -d postgres \
-c "CREATE DATABASE \"default\" WITH OWNER postgres;" \
-c "CREATE DATABASE \"test\" WITH OWNER postgres;"
redis-on-docker: ensure-docker-network
docker run -d --network $(DOCKER_NETWORK) --name twenty_redis -p 6379:6379 redis/redis-stack-server:latest
clickhouse-on-docker: ensure-docker-network
docker run -d --network $(DOCKER_NETWORK) --name twenty_clickhouse -p 8123:8123 -p 9000:9000 -e CLICKHOUSE_PASSWORD=devPassword clickhouse/clickhouse-server:latest \
grafana-on-docker: ensure-docker-network
docker run -d --network $(DOCKER_NETWORK) \
--name twenty_grafana \
-p 4000:3000 \
-e GF_SECURITY_ADMIN_USER=admin \
-e GF_SECURITY_ADMIN_PASSWORD=admin \
-e GF_INSTALL_PLUGINS=grafana-clickhouse-datasource \
-v $(PWD)/packages/twenty-docker/grafana/provisioning/datasources:/etc/grafana/provisioning/datasources \
grafana/grafana-oss:latest
opentelemetry-collector-on-docker: ensure-docker-network
docker run -d --network $(DOCKER_NETWORK) \
--name twenty_otlp_collector \
-p 4317:4317 \
-p 4318:4318 \
-p 13133:13133 \
-v $(PWD)/packages/twenty-docker/otel-collector/otel-collector-config.yaml:/etc/otel-collector-config.yaml \
otel/opentelemetry-collector-contrib:latest \
--config /etc/otel-collector-config.yaml
redis-on-docker:
docker run -d --name twenty_redis -p 6379:6379 redis/redis-stack-server:latest
+94 -59
View File
@@ -1,137 +1,172 @@
[![Hacktoberfest 2025](packages/twenty-website/public/images/readme/hacktoberfest2025.png)](https://twentycrm.notion.site/)
<br>
<p align="center">
<a href="https://www.twenty.com">
<img src="./packages/twenty-website/public/images/core/logo.svg" width="100px" alt="Twenty logo" />
</a>
</p>
<h2 align="center" >The #1 Open-Source CRM </h2>
<h2 align="center" >The #1 Open-Source CRM </h3>
<p align="center">Tailored to your unique business needs</p>
<p align="center"><a href="https://twenty.com">🌐 Website</a> · <a href="https://twenty.com/developers">📚 Documentation</a> · <a href="https://github.com/orgs/twentyhq/projects/1"><img src="./packages/twenty-website/public/images/readme/planner-icon.svg" width="12" height="12"/> Roadmap </a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty"><img src="./packages/twenty-website/public/images/readme/figma-icon.png" width="12" height="12"/> Figma</a></p>
<p align="center"><a href="https://twenty.com">🌐 Website</a> · <a href="https://twenty.com/developers">📚 Documentation</a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty"><img src="./packages/twenty-website/public/images/readme/figma-icon.png" width="12" height="12"/> Figma</a><p>
<br />
<p align="center">
<a href="https://www.twenty.com">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/github-cover-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/github-cover-light.png" />
<img src="./packages/twenty-website/public/images/readme/github-cover-light.png" alt="Cover" />
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/preview-dark.png">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/preview-light.png">
<img src="./packages/twenty-docs/static/img/preview-light.png" alt="Companies view" />
</picture>
</a>
</p>
<br>
<br />
Weve spent thousands of hours grappling with traditional CRMs like Pipedrive and Salesforce to align them with our business needs, only to end up frustrated — customizations are complex and the closed ecosystems of these platforms can feel restrictive.
# Installation
We felt the need for a CRM platform that empowers rather than constrains. We believe the next great CRM will come from the open-source community. Weve packed Twenty with powerful features to give you full control and help you run your business efficiently.
See:
<br>
# Demo
Go to <a href="https://demo.twenty.com/">demo.twenty.com</a> and login with the following credentials:
```
email: tim@apple.dev
password: Applecar2025
```
See also:
🚀 [Self-hosting](https://twenty.com/developers/section/self-hosting)
🖥️ [Local Setup](https://twenty.com/developers/local-setup)
# Does the world need another CRM?
# Why Choose Twenty?
We understand that the CRM landscape is vast. So why should you choose us?
We built Twenty for three reasons:
⛓️ **Full control, Full Freedom:** Contribute, self-host, fork. Break free from vendor lock-in and join us in shaping the open future of CRM.
**CRMs are too expensive, and users are trapped.** Companies use locked-in customer data to hike prices. It shouldn't be that way.
📊 **Data, Your Way:** The days when the role of CRM platforms was to shift manual data entries to a database are over. Now, the data is already there. CRM 2.0 should be built around your data, allowing you to access and visualize any existing sources, not forcing you to retrofit your data into predefined objects on a remote cloud.
**A fresh start is required to build a better experience.** We can learn from past mistakes and craft a cohesive experience inspired by new UX patterns from tools like Notion, Airtable or Linear.
🎨 **Effortlessly Intuitive:** We set out to create something that we ourselves would always enjoy using. The main application draws inspiration from Notion, a tool known for its user-friendly interface and customization capabilities.
<br>
**We believe in Open-source and community.** Hundreds of developers are already building Twenty together. Once we have plugin capabilities, a whole ecosystem will grow around it.
<br />
<br>
# What You Can Do With Twenty
We're currently in the development phase of Twenty's alpha version.
Please feel free to flag any specific needs you have by creating an issue.
Below are a few features we have implemented to date:
Below are some features we have implemented to date:
+ [Personalize layouts with filters, sort, group by, kanban and table views](#personalize-layouts-with-filters-sort-group-by-kanban-and-table-views)
+ [Customize your objects and fields](#customize-your-objects-and-fields)
+ [Create and manage permissions with custom roles](#create-and-manage-permissions-with-custom-roles)
+ [Automate workflow with triggers and actions](#automate-workflow-with-triggers-and-actions)
+ [Emails, calendar events, files, and more](#emails-calendar-events-files-and-more)
+ [Add, filter, sort, edit, and track customers](#add-filter-sort-edit-and-track-customers)
+ [Create one or several opportunities for each company](#create-one-or-several-opportunities-for-each-company)
+ [See rich notes tasks displayed in a timeline](#see-rich-notes-tasks-displayed-in-a-timeline)
+ [Create tasks on records](#create-tasks-on-records)
+ [Navigate quickly through the app using keyboard shortcuts and search](#navigate-quickly-through-the-app-using-keyboard-shortcuts-and-search)
## Personalize layouts with filters, sort, group by, kanban and table views
## Add, filter, sort, edit, and track customers:
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/views-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/views-light.png" />
<img src="./packages/twenty-website/public/images/readme/views-light.png" alt="Companies Kanban Views" />
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/index-dark.png">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/index-light.png">
<img src="./packages/twenty-docs/static/img/visualise-customer-light.png" alt="Companies view" />
</picture>
</p>
## Customize your objects and fields
## Create one or several opportunities for each company:
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/data-model-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/data-model-light.png" />
<img src="./packages/twenty-website/public/images/readme/data-model-light.png" alt="Setting Custom Objects" />
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/kanban-dark.png">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/kanban-light.png">
<img src="./packages/twenty-docs/static/img/follow-your-deals-light.png" alt="Companies view" />
</picture>
</p>
## Create and manage permissions with custom roles
## Track deals effortlessly with the email integration:
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/permissions-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/permissions-light.png" />
<img src="./packages/twenty-website/public/images/readme/permissions-light.png" alt="Permissions" />
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/emails-dark.png">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/emails-light.png">
<img src="./packages/twenty-docs/static/img/rich-notes-light.png" alt="Companies view" />
</picture>
</p>
## Automate workflow with triggers and actions
## Tailor your data model to meet business needs:
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/workflows-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/workflows-light.png" />
<img src="./packages/twenty-website/public/images/readme/workflows-light.png" alt="Workflows" />
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/data-dark.png">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/data-light.png">
<img src="./packages/twenty-docs/static/img/rich-notes-light.png" alt="Companies view" />
</picture>
</p>
## Emails, calendar events, files, and more
## See rich notes displayed in a timeline:
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/plus-other-features-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/plus-other-features-light.png" />
<img src="./packages/twenty-website/public/images/readme/plus-other-features-light.png" alt="Other Features" />
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/notes-dark.png">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/notes-light.png">
<img src="./packages/twenty-docs/static/img/rich-notes-light.png" alt="Companies view" />
</picture>
</p>
<br />
# Stack
- [TypeScript](https://www.typescriptlang.org/)
- [Nx](https://nx.dev/)
- [NestJS](https://nestjs.com/), with [BullMQ](https://bullmq.io/), [PostgreSQL](https://www.postgresql.org/), [Redis](https://redis.io/)
- [React](https://reactjs.org/), with [Recoil](https://recoiljs.org/), [Emotion](https://emotion.sh/) and [Lingui](https://lingui.dev/)
# Thanks
## Create tasks on records
<p align="center">
<a href="https://www.chromatic.com/"><img src="./packages/twenty-website/public/images/readme/chromatic.png" height="30" alt="Chromatic" /></a>
<a href="https://greptile.com"><img src="./packages/twenty-website/public/images/readme/greptile.png" height="30" alt="Greptile" /></a>
<a href="https://sentry.io/"><img src="./packages/twenty-website/public/images/readme/sentry.png" height="30" alt="Sentry" /></a>
<a href="https://crowdin.com/"><img src="./packages/twenty-website/public/images/readme/crowdin.png" height="30" alt="Crowdin" /></a>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/tasks-dark.png">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/tasks-light.png">
<img src="./packages/twenty-docs/static/img/create-tasks-light.png" alt="Companies view" />
</picture>
</p>
Thanks to these amazing services that we use and recommend for UI testing (Chromatic), code review (Greptile), catching bugs (Sentry) and translating (Crowdin).
## Navigate quickly through the app using keyboard shortcuts and search:
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/keyboard-dark.png">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/keyboard-light.png">
<img src="./packages/twenty-docs/static/img/shortcut-navigation-light.png" alt="Companies view" />
</picture>
</p>
## Connect your CRM to all your tools through our APIs and Webhooks.
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/api-dark.png">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/api-light.png">
<img src="./packages/twenty-docs/static/img/shortcut-navigation-light.png" alt="Companies view" />
</picture>
</p>
<br>
# What's In Store
Heres what you can look forward to:
**Frequent updates:** Were shipping fast! Expect regular updates and new features that enhance your experience.
🔗 **Extensibility:** Were putting the power in your hands. Soon, youll have the tools to extend and customize Twenty with plugins and more.
<br>
# Join the Community
- Star the repo
- Subscribe to releases (watch -> custom -> releases)
- Join [discussions](https://github.com/twentyhq/twenty/discussions) and track [issues](https://github.com/twentyhq/twenty/issues)
- Follow us on [Twitter](https://twitter.com/twentycrm) or [LinkedIn](https://www.linkedin.com/company/twenty/)
- Join our [Discord](https://discord.gg/cx5n4Jzs57)
- Improve translations on [Crowdin](https://twenty.crowdin.com/twenty)
- [Contributions](https://github.com/twentyhq/twenty/contribute) are, of course, most welcome!
-31
View File
@@ -1,31 +0,0 @@
#
# Basic Crowdin CLI configuration
# See https://crowdin.github.io/crowdin-cli/configuration for more information
# See https://support.crowdin.com/developer/configuration-file/ for all available options
#
#
# Defines whether to preserve the original directory structure in the Crowdin project
# Recommended to set to true
#
"preserve_hierarchy": true
#
# Files configuration.
# See https://support.crowdin.com/developer/configuration-file/ for all available options
#
files: [
{
#
# Source files filter
# e.g. "/resources/en/*.json"
#
"source": "**/en.po",
#
# Translation files filter
# e.g. "/resources/%two_letters_code%/%original_file_name%"
#
"translation": "%original_path%/%locale%.po",
}
]
-189
View File
@@ -1,189 +0,0 @@
import js from '@eslint/js';
import nxPlugin from '@nx/eslint-plugin';
import typescriptEslint from '@typescript-eslint/eslint-plugin';
import typescriptParser from '@typescript-eslint/parser';
import importPlugin from 'eslint-plugin-import';
import linguiPlugin from 'eslint-plugin-lingui';
import preferArrowPlugin from 'eslint-plugin-prefer-arrow';
import prettierPlugin from 'eslint-plugin-prettier';
import unicornPlugin from 'eslint-plugin-unicorn';
import unusedImportsPlugin from 'eslint-plugin-unused-imports';
import jsoncParser from 'jsonc-eslint-parser';
export default [
// Base JavaScript configuration
js.configs.recommended,
// Lingui recommended rules
linguiPlugin.configs['flat/recommended'],
// Global ignores
{
ignores: [
'**/node_modules/**',
],
},
// Base configuration for all files
{
files: ['**/*.{js,jsx,ts,tsx}'],
plugins: {
'prettier': prettierPlugin,
'lingui': linguiPlugin,
'@nx': nxPlugin,
'prefer-arrow': preferArrowPlugin,
'import': importPlugin,
'unused-imports': unusedImportsPlugin,
'unicorn': unicornPlugin,
},
rules: {
// General rules
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
'no-console': ['warn', { allow: ['group', 'groupCollapsed', 'groupEnd'] }],
'no-control-regex': 0,
'no-debugger': 'error',
'no-duplicate-imports': 'error',
'no-undef': 'off',
'no-unused-vars': 'off',
// Nx rules
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{
sourceTag: 'scope:sdk',
onlyDependOnLibsWithTags: ['scope:sdk'],
},
{
sourceTag: 'scope:shared',
onlyDependOnLibsWithTags: ['scope:shared'],
},
{
sourceTag: 'scope:backend',
onlyDependOnLibsWithTags: ['scope:shared', 'scope:backend'],
},
{
sourceTag: 'scope:frontend',
onlyDependOnLibsWithTags: ['scope:shared', 'scope:frontend'],
},
{
sourceTag: 'scope:zapier',
onlyDependOnLibsWithTags: ['scope:shared'],
},
],
},
],
// Import rules
'import/no-relative-packages': 'error',
'import/no-useless-path-segments': 'error',
'import/no-duplicates': ['error', { considerQueryString: true }],
// Prefer arrow functions
'prefer-arrow/prefer-arrow-functions': [
'error',
{
disallowPrototype: true,
singleReturnOnly: false,
classPropertiesAllowed: false,
},
],
// Unused imports
'unused-imports/no-unused-imports': 'warn',
'unused-imports/no-unused-vars': [
'warn',
{
vars: 'all',
varsIgnorePattern: '^_',
args: 'after-used',
argsIgnorePattern: '^_',
},
],
},
},
// TypeScript specific configuration
{
files: ['**/*.{ts,tsx}'],
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
plugins: {
'@typescript-eslint': typescriptEslint,
},
rules: {
// TypeScript rules
'no-redeclare': 'off', // Turn off base rule for TypeScript
'@typescript-eslint/no-redeclare': 'error', // Use TypeScript-aware version
'@typescript-eslint/ban-ts-comment': 'error',
'@typescript-eslint/consistent-type-imports': [
'error',
{
prefer: 'type-imports',
fixStyle: 'inline-type-imports'
},
],
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/no-empty-interface': [
'error',
{
allowSingleExtends: true,
},
],
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-unused-vars': 'off',
},
},
// JavaScript specific configuration
{
files: ['*.{js,jsx}'],
rules: {
// JavaScript-specific rules if needed
},
},
// Test files
{
files: [
'*.spec.@(ts|tsx|js|jsx)',
'*.integration-spec.@(ts|tsx|js|jsx)',
'*.test.@(ts|tsx|js|jsx)',
],
languageOptions: {
globals: {
jest: true,
describe: true,
it: true,
expect: true,
beforeEach: true,
afterEach: true,
beforeAll: true,
afterAll: true,
},
},
rules: {
'@typescript-eslint/no-non-null-assertion': 'off',
},
},
// JSON files
{
files: ['**/*.json'],
languageOptions: {
parser: jsoncParser,
},
},
];
-302
View File
@@ -1,302 +0,0 @@
import js from '@eslint/js';
import nxPlugin from '@nx/eslint-plugin';
import typescriptEslint from '@typescript-eslint/eslint-plugin';
import typescriptParser from '@typescript-eslint/parser';
import importPlugin from 'eslint-plugin-import';
import linguiPlugin from 'eslint-plugin-lingui';
import preferArrowPlugin from 'eslint-plugin-prefer-arrow';
import prettierPlugin from 'eslint-plugin-prettier';
import reactPlugin from 'eslint-plugin-react';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
import reactRefreshPlugin from 'eslint-plugin-react-refresh';
import unicornPlugin from 'eslint-plugin-unicorn';
import unusedImportsPlugin from 'eslint-plugin-unused-imports';
import jsoncParser from 'jsonc-eslint-parser';
export default [
// Base JavaScript configuration
js.configs.recommended,
// Lingui recommended rules
linguiPlugin.configs['flat/recommended'],
// Base configuration for all files
{
files: ['**/*.{js,jsx,ts,tsx}'],
plugins: {
'react': reactPlugin,
'react-hooks': reactHooksPlugin,
'react-refresh': reactRefreshPlugin,
'prettier': prettierPlugin,
'lingui': linguiPlugin,
'@nx': nxPlugin,
'prefer-arrow': preferArrowPlugin,
'import': importPlugin,
'unused-imports': unusedImportsPlugin,
'unicorn': unicornPlugin,
},
settings: {
react: {
version: 'detect',
},
},
rules: {
// General rules
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
'no-console': ['warn', { allow: ['group', 'groupCollapsed', 'groupEnd'] }],
'no-control-regex': 0,
'no-debugger': 'error',
'no-duplicate-imports': 'error',
'no-undef': 'off',
'no-unused-vars': 'off',
'prettier/prettier': 'error',
// Nx rules
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{
sourceTag: 'scope:shared',
onlyDependOnLibsWithTags: ['scope:shared'],
},
{
sourceTag: 'scope:backend',
onlyDependOnLibsWithTags: ['scope:shared', 'scope:backend'],
},
{
sourceTag: 'scope:frontend',
onlyDependOnLibsWithTags: ['scope:shared', 'scope:frontend'],
},
{
sourceTag: 'scope:zapier',
onlyDependOnLibsWithTags: ['scope:shared'],
},
],
},
],
// Import rules
'import/no-relative-packages': 'error',
'import/no-useless-path-segments': 'error',
'import/no-duplicates': ['error', { considerQueryString: true }],
// Prefer arrow functions
'prefer-arrow/prefer-arrow-functions': [
'error',
{
disallowPrototype: true,
singleReturnOnly: false,
classPropertiesAllowed: false,
},
],
// Unused imports
'unused-imports/no-unused-imports': 'warn',
'unused-imports/no-unused-vars': [
'warn',
{
vars: 'all',
varsIgnorePattern: '^_',
args: 'after-used',
argsIgnorePattern: '^_',
},
],
// React rules
'react/no-unescaped-entities': 'off',
'react/prop-types': 'off',
'react/jsx-key': 'off',
'react/display-name': 'off',
'react/jsx-uses-react': 'off',
'react/react-in-jsx-scope': 'off',
'react/jsx-no-useless-fragment': 'off',
'react/jsx-props-no-spreading': [
'error',
{
explicitSpread: 'ignore',
},
],
// React hooks rules
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': [
'warn',
{
additionalHooks: 'useRecoilCallback',
},
],
},
},
// TypeScript specific configuration
{
files: ['**/*.{ts,tsx}'],
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaFeatures: {
jsx: true,
},
// Note: project path should be specified by each package individually
},
},
plugins: {
'@typescript-eslint': typescriptEslint,
},
rules: {
// Import restrictions
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['@tabler/icons-react'],
message: 'Please import icons from `twenty-ui`',
},
{
group: ['react-hotkeys-web-hook'],
importNames: ['useHotkeys'],
message: 'Please use the custom wrapper: `useScopedHotkeys` from `twenty-ui`',
},
{
group: ['lodash'],
message: "Please use the standalone lodash package (for instance: `import groupBy from 'lodash.groupby'` instead of `import { groupBy } from 'lodash'`)",
},
],
},
],
// TypeScript rules
'no-redeclare': 'off', // Turn off base rule for TypeScript
'@typescript-eslint/no-redeclare': 'error', // Use TypeScript-aware version
'@typescript-eslint/ban-ts-comment': 'error',
'@typescript-eslint/consistent-type-imports': [
'error',
{
prefer: 'type-imports',
fixStyle: 'inline-type-imports'
},
],
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/no-empty-interface': [
'error',
{
allowSingleExtends: true,
},
],
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
// Custom workspace rules
'@nx/workspace-effect-components': 'error',
'@nx/workspace-no-hardcoded-colors': 'error',
'@nx/workspace-matching-state-variable': 'error',
'@nx/workspace-sort-css-properties-alphabetically': 'error',
'@nx/workspace-styled-components-prefixed-with-styled': 'error',
'@nx/workspace-no-state-useref': 'error',
'@nx/workspace-component-props-naming': 'error',
'@nx/workspace-explicit-boolean-predicates-in-if': 'error',
'@nx/workspace-use-getLoadable-and-getValue-to-get-atoms': 'error',
'@nx/workspace-useRecoilCallback-has-dependency-array': 'error',
'@nx/workspace-no-navigate-prefer-link': 'error',
},
},
// Storybook files
{
files: ['*.stories.@(ts|tsx|js|jsx)'],
rules: {
'@typescript-eslint/no-non-null-assertion': 'off',
},
},
// JavaScript specific configuration
{
files: ['*.{js,jsx}'],
rules: {
// JavaScript-specific rules if needed
},
},
// Constants files
{
files: ['**/constants/*.ts', '**/*.constants.ts'],
rules: {
'@typescript-eslint/naming-convention': [
'error',
{
selector: 'variable',
format: ['UPPER_CASE'],
},
],
'unicorn/filename-case': [
'warn',
{
cases: {
pascalCase: true,
},
},
],
'@nx/workspace-max-consts-per-file': ['error', { max: 1 }],
},
},
// Test files
{
files: [
'*.test.@(ts|tsx|js|jsx)',
],
languageOptions: {
globals: {
jest: true,
describe: true,
it: true,
expect: true,
beforeEach: true,
afterEach: true,
beforeAll: true,
afterAll: true,
},
},
rules: {
'@typescript-eslint/no-non-null-assertion': 'off',
},
},
// Constants files
{
files: ['**/*.constants.ts'],
rules: {
'@typescript-eslint/naming-convention': [
'error',
{
selector: 'variable',
format: ['UPPER_CASE'],
},
],
'unicorn/filename-case': [
'warn',
{
cases: {
pascalCase: true,
},
},
],
'@nx/workspace-max-consts-per-file': ['error', { max: 1 }],
},
},
// JSON files
{
files: ['**/*.json'],
languageOptions: {
parser: jsoncParser,
},
},
];
View File
+26 -58
View File
@@ -1,8 +1,4 @@
{
"workspaceLayout": {
"appsDir": "packages",
"libsDir": "packages"
},
"namedInputs": {
"default": ["{projectRoot}/**/*"],
"excludeStories": [
@@ -18,7 +14,6 @@
"!{projectRoot}/**/tsconfig.spec.json",
"!{projectRoot}/**/*.test.(ts|tsx)",
"!{projectRoot}/**/*.spec.(ts|tsx)",
"!{projectRoot}/**/*.integration-spec.ts",
"!{projectRoot}/**/__tests__/*"
],
"production": [
@@ -44,19 +39,15 @@
"cache": true,
"outputs": ["{options.outputFile}"],
"options": {
"eslintConfig": "{projectRoot}/eslint.config.mjs",
"eslintConfig": "{projectRoot}/.eslintrc.cjs",
"cache": true,
"cacheLocation": "{workspaceRoot}/.cache/eslint"
"cacheLocation": "{workspaceRoot}/.cache/eslint",
"ignorePath": "{workspaceRoot}/.gitignore"
},
"configurations": {
"ci": {
"cacheStrategy": "content"
},
"fix": {
"fix": true
}
},
"dependsOn": ["^build"]
"ci": { "cacheStrategy": "content" },
"fix": { "fix": true }
}
},
"fmt": {
"executor": "nx:run-commands",
@@ -70,14 +61,9 @@
"write": false
},
"configurations": {
"ci": {
"cacheStrategy": "content"
},
"fix": {
"write": true
}
},
"dependsOn": ["^build"]
"ci": { "cacheStrategy": "content" },
"fix": { "write": true }
}
},
"typecheck": {
"executor": "nx:run-commands",
@@ -87,11 +73,8 @@
"command": "tsc -b tsconfig.json --incremental"
},
"configurations": {
"watch": {
"watch": true
}
},
"dependsOn": ["^build"]
"watch": { "watch": true }
}
},
"test": {
"executor": "@nx/jest:jest",
@@ -104,7 +87,7 @@
],
"outputs": ["{projectRoot}/coverage"],
"options": {
"jestConfig": "{projectRoot}/jest.config.mjs",
"jestConfig": "{projectRoot}/jest.config.ts",
"coverage": true,
"coverageReporters": ["text-summary"],
"cacheDirectory": "../../.cache/jest/{projectRoot}"
@@ -114,12 +97,8 @@
"ci": true,
"maxWorkers": 3
},
"coverage": {
"coverageReporters": ["lcov", "text"]
},
"watch": {
"watch": true
}
"coverage": { "coverageReporters": ["lcov", "text"] },
"watch": { "watch": true }
}
},
"test:e2e": {
@@ -133,11 +112,10 @@
"outputs": ["{projectRoot}/{options.output-dir}"],
"options": {
"cwd": "{projectRoot}",
"command": "VITE_DISABLE_TYPESCRIPT_CHECKER=true VITE_DISABLE_ESLINT_CHECKER=true storybook build --test",
"command": "VITE_DISABLE_ESLINT_CHECKER=true storybook build",
"output-dir": "storybook-static",
"config-dir": ".storybook"
},
"dependsOn": ["^build"]
}
},
"storybook:serve:dev": {
"executor": "nx:run-commands",
@@ -169,14 +147,12 @@
"options": {
"cwd": "{projectRoot}",
"commands": [
"test-storybook --url http://localhost:{args.port} --maxWorkers=3 --coverage --coverageDirectory={args.coverageDir} --shard={args.shard}",
"nx storybook:coverage {projectName} --coverageDir={args.coverageDir} --checkCoverage={args.checkCoverage}"
"test-storybook --url http://localhost:{args.port} --maxWorkers=3 --coverage --coverageDirectory={args.coverageDir}",
"nx storybook:coverage {projectName} --coverageDir={args.coverageDir}"
],
"shard": "1/1",
"parallel": false,
"coverageDir": "coverage/storybook",
"port": 6006,
"checkCoverage": true
"port": 6006
}
},
"storybook:test:no-coverage": {
@@ -203,25 +179,20 @@
"!{projectRoot}/coverage/storybook/coverage-storybook.json"
],
"options": {
"command": "npx nyc report --reporter={args.reporter} --reporter=text-summary -t {args.coverageDir} --report-dir {args.coverageDir} --check-coverage={args.checkCoverage} --cwd={projectRoot}",
"command": "npx nyc report --reporter={args.reporter} --reporter=text-summary -t {args.coverageDir} --report-dir {args.coverageDir} --check-coverage --cwd={projectRoot}",
"coverageDir": "coverage/storybook",
"reporter": "lcov",
"checkCoverage": true
"reporter": "lcov"
},
"configurations": {
"text": {
"reporter": "text"
}
"text": { "reporter": "text" }
}
},
"storybook:serve-and-test:static": {
"executor": "nx:run-commands",
"options": {
"commands": [
"npx concurrently --kill-others --success=first -n SB,TEST 'nx storybook:serve:static {projectName} --port={args.port}' 'npx wait-on tcp:{args.port} && nx storybook:test {projectName} --shard={args.shard} --checkCoverage={args.checkCoverage} --port={args.port} --configuration={args.scope}'"
"npx concurrently --kill-others --success=first -n SB,TEST 'nx storybook:serve:static {projectName} --port={args.port} --configuration={args.performance}' 'npx wait-on tcp:{args.port} && nx storybook:test {projectName} --port={args.port} --configuration={args.scope}'"
],
"shard": "1/1",
"checkCoverage": true,
"port": 6006
}
},
@@ -265,7 +236,7 @@
"cache": true,
"inputs": [
"default",
"{workspaceRoot}/eslint.config.mjs",
"{workspaceRoot}/.eslintrc.js",
"{workspaceRoot}/tools/eslint-rules/**/*"
]
},
@@ -280,7 +251,7 @@
}
},
"installation": {
"version": "21.3.11"
"version": "18.3.3"
},
"generators": {
"@nx/react": {
@@ -313,8 +284,5 @@
}
},
"useInferencePlugins": false,
"defaultBase": "main",
"tui": {
"enabled": false
}
"defaultBase": "main"
}
+212 -79
View File
@@ -1,33 +1,119 @@
{
"private": true,
"dependencies": {
"@air/react-drag-to-select": "^5.0.8",
"@apollo/client": "^3.7.17",
"@apollo/server": "^4.7.3",
"@aws-sdk/client-lambda": "^3.614.0",
"@aws-sdk/client-s3": "^3.363.0",
"@aws-sdk/credential-providers": "^3.363.0",
"@blocknote/mantine": "^0.15.3",
"@blocknote/react": "^0.15.3",
"@codesandbox/sandpack-react": "^2.13.5",
"@dagrejs/dagre": "^1.1.2",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@envelop/on-resolve": "^4.1.0",
"@floating-ui/react": "^0.24.3",
"@google-cloud/local-auth": "2.1.0",
"@graphiql/plugin-explorer": "^1.0.2",
"@graphql-tools/schema": "^10.0.0",
"@hello-pangea/dnd": "^16.2.0",
"@hookform/resolvers": "^3.1.1",
"@jsdevtools/rehype-toc": "^3.0.2",
"@linaria/core": "^6.2.0",
"@linaria/react": "^6.2.1",
"@sentry/profiling-node": "^9.26.0",
"@sentry/react": "^9.26.0",
"@mdx-js/react": "^3.0.0",
"@microsoft/microsoft-graph-client": "^3.0.7",
"@nestjs/apollo": "^11.0.5",
"@nestjs/axios": "^3.0.1",
"@nestjs/cli": "^9.0.0",
"@nestjs/common": "^9.0.0",
"@nestjs/config": "^2.3.2",
"@nestjs/core": "^9.0.0",
"@nestjs/event-emitter": "^2.0.3",
"@nestjs/jwt": "^10.0.3",
"@nestjs/passport": "^9.0.3",
"@nestjs/platform-express": "^9.0.0",
"@nestjs/serve-static": "^4.0.1",
"@nestjs/terminus": "^9.2.2",
"@nestjs/typeorm": "^10.0.0",
"@nx/eslint-plugin": "^17.2.8",
"@octokit/graphql": "^7.0.2",
"@ptc-org/nestjs-query-core": "^4.2.0",
"@ptc-org/nestjs-query-typeorm": "4.2.1-alpha.2",
"@react-email/components": "0.0.12",
"@react-email/render": "0.0.10",
"@sentry/node": "^8",
"@sentry/profiling-node": "^8",
"@sentry/react": "^8",
"@sniptt/guards": "^0.2.0",
"@tabler/icons-react": "^3.31.0",
"@wyw-in-js/vite": "^0.7.0",
"@stoplight/elements": "^8.0.5",
"@swc/jest": "^0.2.29",
"@tabler/icons-react": "^2.44.0",
"@tiptap/extension-hard-break": "^2.9.1",
"@types/dompurify": "^3.0.5",
"@types/facepaint": "^1.2.5",
"@types/lodash.camelcase": "^4.3.7",
"@types/lodash.chunk": "^4.2.9",
"@types/lodash.merge": "^4.6.7",
"@types/lodash.pick": "^4.3.7",
"@types/nodemailer": "^6.4.14",
"@types/passport-microsoft": "^1.0.3",
"@wyw-in-js/vite": "^0.5.3",
"add": "^2.0.6",
"addressparser": "^1.0.1",
"afterframe": "^1.0.2",
"apollo-server-express": "^3.12.0",
"apollo-upload-client": "^17.0.0",
"archiver": "^7.0.1",
"axios": "^1.6.2",
"bcrypt": "^5.1.1",
"better-sqlite3": "^9.2.2",
"body-parser": "^1.20.2",
"bullmq": "^4.14.0",
"bytes": "^3.1.2",
"class-transformer": "^0.5.1",
"clsx": "^2.1.1",
"cross-env": "^7.0.3",
"css-loader": "^7.1.2",
"danger-plugin-todos": "^1.3.1",
"dataloader": "^2.2.2",
"date-fns": "^2.30.0",
"date-fns-tz": "^2.0.0",
"debounce": "^2.0.0",
"deep-equal": "^2.2.2",
"dompurify": "^3.0.11",
"dotenv-cli": "^7.2.1",
"drizzle-orm": "^0.29.3",
"esbuild-plugin-svgr": "^2.1.0",
"facepaint": "^1.2.1",
"file-type": "16.5.4",
"framer-motion": "^11.18.0",
"fuse.js": "^7.1.0",
"framer-motion": "^10.12.17",
"googleapis": "105",
"graphiql": "^3.1.1",
"graphql": "16.8.0",
"graphql-fields": "^2.0.3",
"graphql-middleware": "^6.1.35",
"graphql-rate-limit": "^3.3.0",
"graphql-scalars": "^1.23.0",
"graphql-subscriptions": "2.0.0",
"graphql-tag": "^2.12.6",
"graphql-type-json": "^0.3.2",
"graphql-upload": "^13.0.0",
"graphql-yoga": "^4.0.4",
"hex-rgb": "^5.0.0",
"immer": "^10.1.1",
"iframe-resizer-react": "^1.1.0",
"immer": "^10.0.2",
"jest-mock-extended": "^3.0.4",
"js-cookie": "^3.0.5",
"js-levenshtein": "^1.1.6",
"json-2-csv": "^5.4.0",
"jsonwebtoken": "^9.0.0",
"libphonenumber-js": "^1.10.26",
"lodash.camelcase": "^4.3.0",
"lodash.chunk": "^4.2.0",
"lodash.compact": "^3.0.1",
"lodash.escaperegexp": "^4.1.2",
"lodash.debounce": "^4.0.8",
"lodash.groupby": "^4.6.0",
"lodash.identity": "^3.0.0",
"lodash.isempty": "^4.4.0",
@@ -37,87 +123,138 @@
"lodash.mapvalues": "^4.6.0",
"lodash.merge": "^4.6.2",
"lodash.omit": "^4.5.0",
"lodash.pick": "^4.4.0",
"lodash.pickby": "^4.6.0",
"lodash.snakecase": "^4.1.1",
"lodash.upperfirst": "^4.3.1",
"luxon": "^3.3.0",
"microdiff": "^1.3.2",
"moize": "^6.1.6",
"nest-commander": "^3.12.0",
"next": "14.0.4",
"next-mdx-remote": "^4.4.1",
"nodemailer": "^6.9.8",
"openapi-types": "^12.1.3",
"overlayscrollbars": "^2.6.1",
"overlayscrollbars-react": "^0.5.4",
"passport": "^0.7.0",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"passport-microsoft": "^2.0.0",
"patch-package": "^8.0.0",
"pg": "^8.11.3",
"pg-boss": "^9.0.3",
"planer": "^1.2.0",
"pluralize": "^8.0.0",
"prettier": "^3.0.3",
"prism-react-renderer": "^2.1.0",
"qs": "^6.11.2",
"react": "^18.2.0",
"react-data-grid": "7.0.0-beta.13",
"react-datepicker": "^6.7.1",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-error-boundary": "^4.0.11",
"react-helmet-async": "^1.3.0",
"react-hook-form": "^7.45.1",
"react-hotkeys-hook": "^4.4.4",
"react-icons": "^4.12.0",
"react-imask": "^7.6.0",
"react-intersection-observer": "^9.5.2",
"react-loading-skeleton": "^3.3.1",
"react-phone-number-input": "^3.3.4",
"react-responsive": "^9.0.2",
"react-router-dom": "^6.4.4",
"react-textarea-autosize": "^8.4.1",
"react-tooltip": "^5.13.1",
"reactflow": "^11.11.3",
"recoil": "^0.7.7",
"rehype-slug": "^6.0.0",
"remark-behead": "^3.1.0",
"remark-gfm": "^3.0.1",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0",
"scroll-into-view": "^1.16.2",
"semver": "^7.5.4",
"sharp": "^0.32.1",
"slash": "^5.1.0",
"storybook-addon-mock-date": "^0.6.0",
"stripe": "^14.17.0",
"ts-key-enum": "^2.0.12",
"tslib": "^2.8.1",
"tslib": "^2.3.0",
"tsup": "^8.2.4",
"type-fest": "4.10.1",
"typescript": "5.9.2",
"typescript": "5.3.3",
"use-context-selector": "^2.0.0",
"use-debounce": "^10.0.0",
"uuid": "^9.0.0",
"vite-tsconfig-paths": "^4.2.1",
"xlsx-ugnis": "^0.19.3",
"zod": "^4.1.11"
"zod": "3.23.8"
},
"devDependencies": {
"@babel/core": "^7.14.5",
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.24.6",
"@chromatic-com/storybook": "^3",
"@crxjs/vite-plugin": "^1.0.14",
"@graphql-codegen/cli": "^3.3.1",
"@graphql-codegen/client-preset": "^4.1.0",
"@graphql-codegen/typescript": "^3.0.4",
"@graphql-codegen/typescript-operations": "^3.0.4",
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
"@nx/eslint": "21.3.11",
"@nx/eslint-plugin": "21.3.11",
"@nx/jest": "21.3.11",
"@nx/js": "21.3.11",
"@nx/react": "21.3.11",
"@nx/storybook": "21.3.11",
"@nx/vite": "21.3.11",
"@nx/web": "21.3.11",
"@microsoft/microsoft-graph-types": "^2.40.0",
"@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.0",
"@nestjs/testing": "^9.0.0",
"@next/eslint-plugin-next": "^14.1.4",
"@nx/eslint": "18.3.3",
"@nx/eslint-plugin": "18.3.3",
"@nx/jest": "18.3.3",
"@nx/js": "18.3.3",
"@nx/react": "18.3.3",
"@nx/storybook": "18.3.3",
"@nx/vite": "18.3.3",
"@nx/web": "18.3.3",
"@playwright/test": "^1.46.0",
"@sentry/types": "^8",
"@storybook/addon-actions": "8.6.14",
"@sentry/types": "^7.109.0",
"@storybook/addon-actions": "^7.6.3",
"@storybook/addon-coverage": "^1.0.0",
"@storybook/addon-essentials": "8.6.14",
"@storybook/addon-interactions": "8.6.14",
"@storybook/addon-links": "8.6.14",
"@storybook/blocks": "8.6.14",
"@storybook/core-server": "8.6.14",
"@storybook/addon-essentials": "^7.6.7",
"@storybook/addon-interactions": "^7.6.7",
"@storybook/addon-links": "^7.6.7",
"@storybook/addon-onboarding": "^1.0.10",
"@storybook/blocks": "^7.6.3",
"@storybook/core-server": "7.6.3",
"@storybook/icons": "^1.2.9",
"@storybook/preview-api": "8.6.14",
"@storybook/react": "8.6.14",
"@storybook/react-vite": "8.6.14",
"@storybook/test": "8.6.14",
"@storybook/test-runner": "^0.23.0",
"@storybook/types": "8.6.14",
"@storybook/jest": "^0.2.3",
"@storybook/react": "^7.6.3",
"@storybook/react-vite": "^7.6.3",
"@storybook/test": "^7.6.3",
"@storybook/test-runner": "^0.16.0",
"@storybook/testing-library": "^0.2.2",
"@stylistic/eslint-plugin": "^1.5.0",
"@swc-node/register": "1.8.0",
"@swc/cli": "^0.3.12",
"@swc/core": "1.13.3",
"@swc/core": "~1.3.100",
"@swc/helpers": "~0.5.2",
"@swc/jest": "^0.2.39",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "14.0.0",
"@types/addressparser": "^1.0.3",
"@types/apollo-upload-client": "^17.0.2",
"@types/bcrypt": "^5.0.0",
"@types/better-sqlite3": "^7.6.8",
"@types/bytes": "^3.1.1",
"@types/chrome": "^0.0.267",
"@types/deep-equal": "^1.0.1",
"@types/express": "^4.17.13",
"@types/fs-extra": "^11.0.4",
"@types/graphql-fields": "^1.3.6",
"@types/inquirer": "^9.0.9",
"@types/jest": "^30.0.0",
"@types/graphql-upload": "^8.0.12",
"@types/jest": "^29.5.11",
"@types/js-cookie": "^3.0.3",
"@types/js-levenshtein": "^1.1.3",
"@types/lodash.camelcase": "^4.3.7",
"@types/lodash.compact": "^3.0.9",
"@types/lodash.escaperegexp": "^4.1.9",
"@types/lodash.debounce": "^4.0.7",
"@types/lodash.groupby": "^4.6.9",
"@types/lodash.identity": "^3.0.9",
"@types/lodash.isempty": "^4.4.7",
@@ -129,89 +266,88 @@
"@types/lodash.pickby": "^4.6.9",
"@types/lodash.snakecase": "^4.1.7",
"@types/lodash.upperfirst": "^4.3.7",
"@types/luxon": "^3.3.0",
"@types/ms": "^0.7.31",
"@types/node": "^24.0.0",
"@types/node": "18.19.26",
"@types/passport-google-oauth20": "^2.0.11",
"@types/passport-jwt": "^3.0.8",
"@types/passport-microsoft": "^2.1.0",
"@types/pluralize": "^0.0.33",
"@types/react": "^18.2.39",
"@types/react-datepicker": "^6.2.0",
"@types/react-dom": "^18.2.15",
"@types/scroll-into-view": "^1.16.0",
"@types/supertest": "^2.0.11",
"@types/uuid": "^9.0.2",
"@typescript-eslint/eslint-plugin": "^8.39.0",
"@typescript-eslint/parser": "^8.39.0",
"@typescript-eslint/utils": "^8.39.0",
"@vitejs/plugin-react-swc": "3.11.0",
"@typescript-eslint/eslint-plugin": "6.21.0",
"@typescript-eslint/experimental-utils": "^5.62.0",
"@typescript-eslint/parser": "6.21.0",
"@typescript-eslint/utils": "6.21.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"@vitest/ui": "1.4.0",
"@yarnpkg/types": "^4.0.0",
"chromatic": "^6.18.0",
"concurrently": "^8.2.2",
"cross-var": "^1.1.0",
"danger": "^13.0.4",
"dotenv-cli": "^7.4.4",
"esbuild": "^0.25.10",
"eslint": "^9.32.0",
"danger": "^11.3.0",
"dotenv-cli": "^7.2.1",
"drizzle-kit": "^0.20.14",
"esbuild": "^0.23.0",
"eslint": "^8.53.0",
"eslint-config-next": "14.0.4",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-lingui": "^0.9.0",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-prefer-arrow": "^1.2.3",
"eslint-plugin-prettier": "^5.1.2",
"eslint-plugin-project-structure": "^3.9.1",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.4",
"eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-storybook": "^0.9.0",
"eslint-plugin-unicorn": "^56.0.1",
"eslint-plugin-storybook": "^0.6.15",
"eslint-plugin-unicorn": "^51.0.1",
"eslint-plugin-unused-imports": "^3.0.0",
"http-server": "^14.1.1",
"jest": "29.7.0",
"jest-environment-jsdom": "30.0.0-beta.3",
"jest-environment-jsdom": "29.7.0",
"jest-environment-node": "^29.4.1",
"jest-fetch-mock": "^3.0.3",
"jsdom": "~22.1.0",
"msw": "^2.0.11",
"msw-storybook-addon": "^2.0.5",
"nx": "21.3.11",
"msw-storybook-addon": "2.0.0--canary.122.b3ed3b1.0",
"nx": "18.3.3",
"playwright": "^1.46.0",
"prettier": "^3.1.1",
"raw-loader": "^4.0.2",
"rimraf": "^5.0.5",
"source-map-support": "^0.5.20",
"storybook": "8.6.14",
"storybook": "^7.6.3",
"storybook-addon-cookie": "^3.2.0",
"storybook-addon-pseudo-states": "^2.1.2",
"storybook-dark-mode": "^3.0.3",
"supertest": "^6.1.3",
"ts-jest": "^29.1.1",
"ts-loader": "^9.2.3",
"ts-node": "10.9.1",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.17.0",
"vite": "^7.0.0",
"vite-plugin-checker": "^0.10.2",
"vite-plugin-cjs-interop": "^2.2.0",
"vite": "^5.4.0",
"vite-plugin-checker": "^0.6.2",
"vite-plugin-dts": "3.8.1",
"vite-plugin-svgr": "^4.2.0",
"vitest": "1.4.0"
},
"engines": {
"node": "^24.5.0",
"node": "^18.17.1",
"npm": "please-use-yarn",
"yarn": ">=4.0.2"
},
"license": "AGPL-3.0",
"name": "twenty",
"packageManager": "yarn@4.9.2",
"packageManager": "yarn@4.4.0",
"resolutions": {
"graphql": "16.8.1",
"graphql": "16.8.0",
"type-fest": "4.10.1",
"typescript": "5.9.2",
"graphql-redis-subscriptions/ioredis": "^5.6.0",
"prosemirror-view": "1.40.0",
"prosemirror-transform": "1.10.4"
"typescript": "5.3.3"
},
"version": "0.2.1",
"nx": {},
@@ -220,6 +356,7 @@
},
"workspaces": {
"packages": [
"packages/twenty-chrome-extension",
"packages/twenty-front",
"packages/twenty-server",
"packages/twenty-emails",
@@ -228,10 +365,6 @@
"packages/twenty-zapier",
"packages/twenty-website",
"packages/twenty-e2e-testing",
"packages/twenty-shared",
"packages/twenty-sdk",
"packages/twenty-apps",
"packages/twenty-cli",
"tools/eslint-rules"
]
}
@@ -1 +0,0 @@
TWENTY_API_KEY=<SET_YOUR_TWENTY_API>
@@ -1,2 +0,0 @@
.yarn/install-state.gz
.env
File diff suppressed because one or more lines are too long
@@ -1,3 +0,0 @@
yarnPath: .yarn/releases/yarn-4.9.2.cjs
nodeLinker: node-modules
@@ -1,41 +0,0 @@
# Hello world
A minimal hello-world application built with an alpha version of the twenty-cli.
⚠️ Since this project uses an early alpha release, expect breaking changes and evolving features.
This example will gradually gain complexity and capabilities as the twenty-cli matures with future updates.
## Requirements
- twenty-cli `npm install -g twenty-cli`
- an `apiKey`. Go to `/settings/api-webhooks` to generate one
## Install to your Twenty workspace
```bash
cp .env.example .env
```
- replace `<SET_YOUR_TWENTY_API>` with your apiKey
```bash
twenty auth login
twenty app sync
```
## What it does
- creates a new object `postCard` with a `name` field
- creates a serverless function `create-new-post-card`
- two triggers on `create-new-post-card`:
- one route trigger on `GET` `/post-card/create?recipient=RecipientName`
- one databaseEvent trigger on `people.created` events
## Development
Run dev mode and see application updates on your workspace instantly
```bash
twenty app dev
```
@@ -1,28 +0,0 @@
{
"version": "0.0.1",
"license": "MIT",
"engines": {
"node": "^24.5.0",
"npm": "please-use-yarn",
"yarn": ">=4.0.2"
},
"packageManager": "yarn@4.9.2",
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/appManifest.schema.json",
"universalIdentifier": "4ec0391d-18d5-411c-b2f3-266ddc1c3ef7",
"name": "Hello world",
"description": "A hello-world application example",
"env": {
"TWENTY_API_KEY": {
"isSecret": true,
"value": "",
"description": "Twenty api key"
}
},
"dependencies": {
"axios": "^1.12.2",
"twenty-sdk": "^0.0.2"
},
"devDependencies": {
"@types/node": "^24.7.2"
}
}
@@ -1,19 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/serverlessFunction.schema.json",
"universalIdentifier": "e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf",
"name": "create-new-post-card",
"triggers": [
{
"universalIdentifier": "203f1df3-4a82-4d06-a001-b8cf22a31156",
"type": "databaseEvent",
"eventName": "person.created"
},
{
"universalIdentifier": "c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6",
"type": "route",
"path": "/post-card/create",
"httpMethod": "GET",
"isAuthRequired": false
}
]
}
@@ -1,26 +0,0 @@
import axios from 'axios';
export const main = async (params: { recipient: string }): Promise<object> => {
const { recipient } = params;
const options = {
method: 'POST',
url: 'http://localhost:3000/rest/postCards',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.TWENTY_API_KEY}`,
},
data: { name: recipient ?? 'Unknown' },
};
try {
const { data } = await axios.request(options);
console.log(`New post card to "${recipient}" created`);
return data;
} catch (error) {
console.error(error);
throw error;
}
};
@@ -1,10 +0,0 @@
import { ObjectMetadata } from 'twenty-sdk';
@ObjectMetadata({
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
nameSingular: 'postCard',
namePlural: 'postCards',
labelSingular: 'Post card',
labelPlural: 'Post cards',
})
export class PostCard {}
@@ -1,22 +0,0 @@
{
"compileOnSave": false,
"compilerOptions": {
"sourceMap": true,
"declaration": true,
"outDir": "./dist",
"rootDir": ".",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"importHelpers": true,
"strict": true,
"target": "es2018",
"module": "esnext",
"lib": ["es2020", "dom"],
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"resolveJsonModule": true
},
"exclude": ["node_modules", "dist"],
"include": ["**/*.ts"]
}
-273
View File
@@ -1,273 +0,0 @@
# This file is generated by running "yarn install" inside your project.
# Manual changes might be lost - proceed with caution!
__metadata:
version: 8
cacheKey: 10c0
"@types/node@npm:^24.7.2":
version: 24.9.1
resolution: "@types/node@npm:24.9.1"
dependencies:
undici-types: "npm:~7.16.0"
checksum: 10c0/c52f8168080ef9a7c3dc23d8ac6061fab5371aad89231a0f6f4c075869bc3de7e89b075b1f3e3171d9e5143d0dda1807c3dab8e32eac6d68f02e7480e7e78576
languageName: node
linkType: hard
"Hello world@workspace:.":
version: 0.0.0-use.local
resolution: "Hello world@workspace:."
dependencies:
"@types/node": "npm:^24.7.2"
axios: "npm:^1.12.2"
twenty-sdk: "npm:^0.0.2"
languageName: unknown
linkType: soft
"async-function@npm:^1.0.0":
version: 1.0.0
resolution: "async-function@npm:1.0.0"
checksum: 10c0/669a32c2cb7e45091330c680e92eaeb791bc1d4132d827591e499cd1f776ff5a873e77e5f92d0ce795a8d60f10761dec9ddfe7225a5de680f5d357f67b1aac73
languageName: node
linkType: hard
"async-generator-function@npm:^1.0.0":
version: 1.0.0
resolution: "async-generator-function@npm:1.0.0"
checksum: 10c0/2c50ef856c543ad500d8d8777d347e3c1ba623b93e99c9263ecc5f965c1b12d2a140e2ab6e43c3d0b85366110696f28114649411cbcd10b452a92a2318394186
languageName: node
linkType: hard
"asynckit@npm:^0.4.0":
version: 0.4.0
resolution: "asynckit@npm:0.4.0"
checksum: 10c0/d73e2ddf20c4eb9337e1b3df1a0f6159481050a5de457c55b14ea2e5cb6d90bb69e004c9af54737a5ee0917fcf2c9e25de67777bbe58261847846066ba75bc9d
languageName: node
linkType: hard
"axios@npm:^1.12.2":
version: 1.12.2
resolution: "axios@npm:1.12.2"
dependencies:
follow-redirects: "npm:^1.15.6"
form-data: "npm:^4.0.4"
proxy-from-env: "npm:^1.1.0"
checksum: 10c0/80b063e318cf05cd33a4d991cea0162f3573481946f9129efb7766f38fde4c061c34f41a93a9f9521f02b7c9565ccbc197c099b0186543ac84a24580017adfed
languageName: node
linkType: hard
"call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2":
version: 1.0.2
resolution: "call-bind-apply-helpers@npm:1.0.2"
dependencies:
es-errors: "npm:^1.3.0"
function-bind: "npm:^1.1.2"
checksum: 10c0/47bd9901d57b857590431243fea704ff18078b16890a6b3e021e12d279bbf211d039155e27d7566b374d49ee1f8189344bac9833dec7a20cdec370506361c938
languageName: node
linkType: hard
"combined-stream@npm:^1.0.8":
version: 1.0.8
resolution: "combined-stream@npm:1.0.8"
dependencies:
delayed-stream: "npm:~1.0.0"
checksum: 10c0/0dbb829577e1b1e839fa82b40c07ffaf7de8a09b935cadd355a73652ae70a88b4320db322f6634a4ad93424292fa80973ac6480986247f1734a1137debf271d5
languageName: node
linkType: hard
"delayed-stream@npm:~1.0.0":
version: 1.0.0
resolution: "delayed-stream@npm:1.0.0"
checksum: 10c0/d758899da03392e6712f042bec80aa293bbe9e9ff1b2634baae6a360113e708b91326594c8a486d475c69d6259afb7efacdc3537bfcda1c6c648e390ce601b19
languageName: node
linkType: hard
"dunder-proto@npm:^1.0.1":
version: 1.0.1
resolution: "dunder-proto@npm:1.0.1"
dependencies:
call-bind-apply-helpers: "npm:^1.0.1"
es-errors: "npm:^1.3.0"
gopd: "npm:^1.2.0"
checksum: 10c0/199f2a0c1c16593ca0a145dbf76a962f8033ce3129f01284d48c45ed4e14fea9bbacd7b3610b6cdc33486cef20385ac054948fefc6272fcce645c09468f93031
languageName: node
linkType: hard
"es-define-property@npm:^1.0.1":
version: 1.0.1
resolution: "es-define-property@npm:1.0.1"
checksum: 10c0/3f54eb49c16c18707949ff25a1456728c883e81259f045003499efba399c08bad00deebf65cccde8c0e07908c1a225c9d472b7107e558f2a48e28d530e34527c
languageName: node
linkType: hard
"es-errors@npm:^1.3.0":
version: 1.3.0
resolution: "es-errors@npm:1.3.0"
checksum: 10c0/0a61325670072f98d8ae3b914edab3559b6caa980f08054a3b872052640d91da01d38df55df797fcc916389d77fc92b8d5906cf028f4db46d7e3003abecbca85
languageName: node
linkType: hard
"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1":
version: 1.1.1
resolution: "es-object-atoms@npm:1.1.1"
dependencies:
es-errors: "npm:^1.3.0"
checksum: 10c0/65364812ca4daf48eb76e2a3b7a89b3f6a2e62a1c420766ce9f692665a29d94fe41fe88b65f24106f449859549711e4b40d9fb8002d862dfd7eb1c512d10be0c
languageName: node
linkType: hard
"es-set-tostringtag@npm:^2.1.0":
version: 2.1.0
resolution: "es-set-tostringtag@npm:2.1.0"
dependencies:
es-errors: "npm:^1.3.0"
get-intrinsic: "npm:^1.2.6"
has-tostringtag: "npm:^1.0.2"
hasown: "npm:^2.0.2"
checksum: 10c0/ef2ca9ce49afe3931cb32e35da4dcb6d86ab02592cfc2ce3e49ced199d9d0bb5085fc7e73e06312213765f5efa47cc1df553a6a5154584b21448e9fb8355b1af
languageName: node
linkType: hard
"follow-redirects@npm:^1.15.6":
version: 1.15.11
resolution: "follow-redirects@npm:1.15.11"
peerDependenciesMeta:
debug:
optional: true
checksum: 10c0/d301f430542520a54058d4aeeb453233c564aaccac835d29d15e050beb33f339ad67d9bddbce01739c5dc46a6716dbe3d9d0d5134b1ca203effa11a7ef092343
languageName: node
linkType: hard
"form-data@npm:^4.0.4":
version: 4.0.4
resolution: "form-data@npm:4.0.4"
dependencies:
asynckit: "npm:^0.4.0"
combined-stream: "npm:^1.0.8"
es-set-tostringtag: "npm:^2.1.0"
hasown: "npm:^2.0.2"
mime-types: "npm:^2.1.12"
checksum: 10c0/373525a9a034b9d57073e55eab79e501a714ffac02e7a9b01be1c820780652b16e4101819785e1e18f8d98f0aee866cc654d660a435c378e16a72f2e7cac9695
languageName: node
linkType: hard
"function-bind@npm:^1.1.2":
version: 1.1.2
resolution: "function-bind@npm:1.1.2"
checksum: 10c0/d8680ee1e5fcd4c197e4ac33b2b4dce03c71f4d91717292785703db200f5c21f977c568d28061226f9b5900cbcd2c84463646134fd5337e7925e0942bc3f46d5
languageName: node
linkType: hard
"generator-function@npm:^2.0.0":
version: 2.0.1
resolution: "generator-function@npm:2.0.1"
checksum: 10c0/8a9f59df0f01cfefafdb3b451b80555e5cf6d76487095db91ac461a0e682e4ff7a9dbce15f4ecec191e53586d59eece01949e05a4b4492879600bbbe8e28d6b8
languageName: node
linkType: hard
"get-intrinsic@npm:^1.2.6":
version: 1.3.1
resolution: "get-intrinsic@npm:1.3.1"
dependencies:
async-function: "npm:^1.0.0"
async-generator-function: "npm:^1.0.0"
call-bind-apply-helpers: "npm:^1.0.2"
es-define-property: "npm:^1.0.1"
es-errors: "npm:^1.3.0"
es-object-atoms: "npm:^1.1.1"
function-bind: "npm:^1.1.2"
generator-function: "npm:^2.0.0"
get-proto: "npm:^1.0.1"
gopd: "npm:^1.2.0"
has-symbols: "npm:^1.1.0"
hasown: "npm:^2.0.2"
math-intrinsics: "npm:^1.1.0"
checksum: 10c0/9f4ab0cf7efe0fd2c8185f52e6f637e708f3a112610c88869f8f041bb9ecc2ce44bf285dfdbdc6f4f7c277a5b88d8e94a432374d97cca22f3de7fc63795deb5d
languageName: node
linkType: hard
"get-proto@npm:^1.0.1":
version: 1.0.1
resolution: "get-proto@npm:1.0.1"
dependencies:
dunder-proto: "npm:^1.0.1"
es-object-atoms: "npm:^1.0.0"
checksum: 10c0/9224acb44603c5526955e83510b9da41baf6ae73f7398875fba50edc5e944223a89c4a72b070fcd78beb5f7bdda58ecb6294adc28f7acfc0da05f76a2399643c
languageName: node
linkType: hard
"gopd@npm:^1.2.0":
version: 1.2.0
resolution: "gopd@npm:1.2.0"
checksum: 10c0/50fff1e04ba2b7737c097358534eacadad1e68d24cccee3272e04e007bed008e68d2614f3987788428fd192a5ae3889d08fb2331417e4fc4a9ab366b2043cead
languageName: node
linkType: hard
"has-symbols@npm:^1.0.3, has-symbols@npm:^1.1.0":
version: 1.1.0
resolution: "has-symbols@npm:1.1.0"
checksum: 10c0/dde0a734b17ae51e84b10986e651c664379018d10b91b6b0e9b293eddb32f0f069688c841fb40f19e9611546130153e0a2a48fd7f512891fb000ddfa36f5a20e
languageName: node
linkType: hard
"has-tostringtag@npm:^1.0.2":
version: 1.0.2
resolution: "has-tostringtag@npm:1.0.2"
dependencies:
has-symbols: "npm:^1.0.3"
checksum: 10c0/a8b166462192bafe3d9b6e420a1d581d93dd867adb61be223a17a8d6dad147aa77a8be32c961bb2f27b3ef893cae8d36f564ab651f5e9b7938ae86f74027c48c
languageName: node
linkType: hard
"hasown@npm:^2.0.2":
version: 2.0.2
resolution: "hasown@npm:2.0.2"
dependencies:
function-bind: "npm:^1.1.2"
checksum: 10c0/3769d434703b8ac66b209a4cca0737519925bbdb61dd887f93a16372b14694c63ff4e797686d87c90f08168e81082248b9b028bad60d4da9e0d1148766f56eb9
languageName: node
linkType: hard
"math-intrinsics@npm:^1.1.0":
version: 1.1.0
resolution: "math-intrinsics@npm:1.1.0"
checksum: 10c0/7579ff94e899e2f76ab64491d76cf606274c874d8f2af4a442c016bd85688927fcfca157ba6bf74b08e9439dc010b248ce05b96cc7c126a354c3bae7fcb48b7f
languageName: node
linkType: hard
"mime-db@npm:1.52.0":
version: 1.52.0
resolution: "mime-db@npm:1.52.0"
checksum: 10c0/0557a01deebf45ac5f5777fe7740b2a5c309c6d62d40ceab4e23da9f821899ce7a900b7ac8157d4548ddbb7beffe9abc621250e6d182b0397ec7f10c7b91a5aa
languageName: node
linkType: hard
"mime-types@npm:^2.1.12":
version: 2.1.35
resolution: "mime-types@npm:2.1.35"
dependencies:
mime-db: "npm:1.52.0"
checksum: 10c0/82fb07ec56d8ff1fc999a84f2f217aa46cb6ed1033fefaabd5785b9a974ed225c90dc72fff460259e66b95b73648596dbcc50d51ed69cdf464af2d237d3149b2
languageName: node
linkType: hard
"proxy-from-env@npm:^1.1.0":
version: 1.1.0
resolution: "proxy-from-env@npm:1.1.0"
checksum: 10c0/fe7dd8b1bdbbbea18d1459107729c3e4a2243ca870d26d34c2c1bcd3e4425b7bcc5112362df2d93cc7fb9746f6142b5e272fd1cc5c86ddf8580175186f6ad42b
languageName: node
linkType: hard
"twenty-sdk@npm:^0.0.2":
version: 0.0.2
resolution: "twenty-sdk@npm:0.0.2"
checksum: 10c0/99e6fe86059d847b548c1f03e0f0c59a4d540caf1d28dd4500f1f5f0094196985ded955801274de9e72ff03e3d1f41e9a509b4c2c5a02ffc8a027277b1e35d8e
languageName: node
linkType: hard
"undici-types@npm:~7.16.0":
version: 7.16.0
resolution: "undici-types@npm:7.16.0"
checksum: 10c0/3033e2f2b5c9f1504bdc5934646cb54e37ecaca0f9249c983f7b1fc2e87c6d18399ebb05dc7fd5419e02b2e915f734d872a65da2e3eeed1813951c427d33cc9a
languageName: node
linkType: hard
@@ -0,0 +1,6 @@
VITE_SERVER_BASE_URL=https://api.twenty.com
VITE_FRONT_BASE_URL=https://app.twenty.com
VITE_MODE=production
# Used to generate packages/twenty-chrome-extension/src/generated/graphql.tsx
AUTH_TOKEN=<YOUR-TOKEN-HERE>
@@ -0,0 +1,6 @@
module.exports = {
extends: ['./.eslintrc.cjs'],
rules: {
'no-console': 'error',
},
};
@@ -0,0 +1,15 @@
module.exports = {
extends: ['../../.eslintrc.cjs', '../../.eslintrc.react.cjs'],
ignorePatterns: ['!**/*', 'node_modules', 'dist', '**/generated/*'],
overrides: [
{
files: ['*.ts', '*.tsx'],
parserOptions: {
project: ['packages/twenty-chrome-extension/tsconfig.{json,*.json}'],
},
rules: {
'@nx/workspace-explicit-boolean-predicates-in-if': 'warn',
},
},
],
};
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
@@ -0,0 +1 @@
src/generated
@@ -0,0 +1,60 @@
# Twenty Chrome Extension.
This extension allows you to save `company` and `people` information to your twenty workspace directly from LinkedIn.
To install the extension in development mode with hmr (hot module reload), follow these steps.
- STEP 1: Clone the repository and run `yarn install` in the root directory.
- STEP 2: Once the dependencies installation succeeds, create a file with env variables by executing the following command in the root directory.
```
cp ./packages/twenty-chrome-extension/.env.example ./packages/twenty-chrome-extension/.env
```
- STEP 3 (optional): Update values of the environment variables to match those of your instance for `twenty-front` and `twenty-server`. If you want to work on your local machine with the default setup from `Twenty Docs`, replace everything in the .env file with the following.
```
VITE_SERVER_BASE_URL=http://localhost:3000
VITE_FRONT_BASE_URL=http://localhost:3001
```
- STEP 4: Now, execute the following command in the root directory to start up the development server on Port 3002. This will create a `dist` folder in `twenty-chrome-extension`.
```
npx nx start twenty-chrome-extension
```
- STEP 5: Open Google Chrome and head to the extensions page by typing `chrome://extensions` in the address bar.
<p align="center">
<img src="../twenty-chrome-extension/public/readme-images/01-img-one.png" width="600" />
</p>
- STEP 6: Turn on the `Developer mode` from the top-right corner and click `Load unpacked`.
<p align="center">
<img src="../twenty-chrome-extension/public/readme-images/02-img-two.png" width="600" />
</p>
- STEP 7: Select the `dist` folder from `twenty-chrome-extension`.
<p align="center">
<img src="../twenty-chrome-extension/public/readme-images/03-img-three.png" width="600" />
</p>
- STEP 8: This opens up the `options` page, where you must enter your API key.
<p align="center">
<img src="../twenty-chrome-extension/public/readme-images/04-img-four.png" width="600" />
</p>
- STEP 9: Reload any LinkedIn page that you opened before installing the extension for seamless experience.
- STEP 10: Visit any individual or company profile on LinkedIn and click the `Add to Twenty` button to test.
<p align="center">
<img src="../twenty-chrome-extension/public/readme-images/05-img-five.png" width="600" />
</p>
To install the extension in production mode without hmr (hot module reload), replace the command in STEP FOUR with `npx nx build twenty-chrome-extension`. You may or may not want to execute STEP THREE based on your requirements.
@@ -0,0 +1,34 @@
import { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: [
{
[`${import.meta.env.VITE_SERVER_BASE_URL}/graphql`]: {
// some of the mutations and queries require authorization (people or companies)
// so to regenerate the schema with types we need to pass an auth token
headers: {
Authorization: `Bearer ${import.meta.env.AUTH_TOKEN}`,
},
},
},
],
overwrite: true,
documents: ['./src/**/*.ts', '!src/generated/**/*.*'],
generates: {
'./src/generated/graphql.tsx': {
plugins: [
'typescript',
'typescript-operations',
'typescript-react-apollo',
],
config: {
skipTypename: true,
withHooks: true,
withHOC: false,
withComponent: false,
},
},
},
};
export default config;
@@ -0,0 +1,12 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/icons/android/android-launchericon-48-48.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Twenty</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/options/loading-index.tsx"></script>
</body>
</html>
@@ -0,0 +1,10 @@
{
"name": "twenty-chrome-extension",
"description": "",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"build": "npx vite build"
}
}
@@ -0,0 +1,12 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/icons/android/android-launchericon-48-48.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Twenty</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/options/page-inaccessible-index.tsx"></script>
</body>
</html>
@@ -0,0 +1,70 @@
{
"name": "twenty-chrome-extension",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"tags": ["scope:frontend"],
"targets": {
"build": {
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "{projectRoot}/dist"
}
},
"start": {
"executor": "nx:run-commands",
"dependsOn": ["build"],
"options": {
"cwd": "packages/twenty-chrome-extension",
"command": "VITE_MODE=development vite"
}
},
"preview": {
"executor": "@nx/vite:preview-server",
"options": {
"buildTarget": "twenty-chrome-extension:build",
"port": 3002,
"open": true
}
},
"reset:env": {
"executor": "nx:run-commands",
"inputs": ["{projectRoot}/.env.example"],
"outputs": ["{projectRoot}/.env"],
"cache": true,
"options": {
"cwd": "{projectRoot}",
"command": "cp .env.example .env"
}
},
"typecheck": {},
"lint": {
"options": {
"lintFilePatterns": [
"{projectRoot}/src/**/*.{ts,tsx,json}",
"{projectRoot}/package.json"
],
"maxWarnings": 0,
"reportUnusedDisableDirectives": "error"
},
"configurations": {
"ci": { "eslintConfig": "{projectRoot}/.eslintrc-ci.cjs" },
"fix": {}
}
},
"fmt": {
"options": {
"files": "src"
},
"configurations": {
"fix": {}
}
},
"graphql:generate": {
"executor": "nx:run-commands",
"options": {
"cwd": "{projectRoot}",
"command": "graphql-codegen"
}
}
}
}

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 B

@@ -0,0 +1,12 @@
<svg width="32" height="32" viewBox="0 0 136 136" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2343_96406)">
<path d="M136 2.28882e-05H0L0.000144482 136H136V2.28882e-05ZM27.27 50.6401C27.27 43.2101 33.3 37.1801 40.73 37.1801H66.64C67.02 37.1801 67.37 37.4101 67.53 37.7601C67.69 38.1101 67.62 38.5201 67.36 38.8101L61.68 44.9801C60.69 46.0501 59.3 46.6701 57.84 46.6701H40.8C38.57 46.6701 36.76 48.4801 36.76 50.7101V60.8901C36.76 62.2001 35.7 63.2601 34.39 63.2601H29.65C28.34 63.2601 27.28 62.2001 27.28 60.8901V50.6401H27.27ZM107.88 85.3601C107.88 92.7901 101.85 98.82 94.42 98.82H83.41C75.98 98.82 69.95 92.7901 69.95 85.3601V66.0901C69.95 64.7801 70.44 63.5201 71.33 62.5501L77.75 55.5801C78.02 55.2901 78.44 55.1901 78.82 55.3301C79.19 55.4801 79.44 55.83 79.44 56.23V85.3001C79.44 87.5301 81.25 89.3401 83.48 89.3401H94.36C96.59 89.3401 98.4 87.5301 98.4 85.3001V50.7101C98.4 48.4801 96.59 46.6701 94.36 46.6701H81.71C80.26 46.6701 78.88 47.2801 77.89 48.3401L40.16 89.3401H62.83C64.14 89.3401 65.2 90.4001 65.2 91.7101V96.4501C65.2 97.7601 64.14 98.82 62.83 98.82H32.28C29.51 98.82 27.26 96.5701 27.26 93.8001V91.29C27.26 90.03 27.73 88.8201 28.59 87.8901L70.89 41.9401C73.69 38.9001 77.62 37.1801 81.75 37.1801H94.41C101.84 37.1801 107.87 43.2101 107.87 50.6401V85.3601H107.88Z" fill="black"/>
<path d="M27.27 50.6401C27.27 43.2101 33.3 37.1801 40.73 37.1801H66.64C67.02 37.1801 67.37 37.4101 67.53 37.7601C67.69 38.1101 67.62 38.5201 67.36 38.8101L61.68 44.9801C60.69 46.0501 59.3 46.6701 57.84 46.6701H40.8C38.57 46.6701 36.76 48.4801 36.76 50.7101V60.8901C36.76 62.2001 35.7 63.2601 34.39 63.2601H29.65C28.34 63.2601 27.28 62.2001 27.28 60.8901V50.6401H27.27Z" fill="white"/>
<path d="M107.88 85.3601C107.88 92.7901 101.85 98.82 94.42 98.82H83.41C75.98 98.82 69.95 92.7901 69.95 85.3601V66.0901C69.95 64.7801 70.44 63.5201 71.33 62.5501L77.75 55.5801C78.02 55.2901 78.44 55.1901 78.82 55.3301C79.19 55.4801 79.44 55.83 79.44 56.23V85.3001C79.44 87.5301 81.25 89.3401 83.48 89.3401H94.36C96.59 89.3401 98.4 87.5301 98.4 85.3001V50.7101C98.4 48.4801 96.59 46.6701 94.36 46.6701H81.71C80.26 46.6701 78.88 47.2801 77.89 48.3401L40.16 89.3401H62.83C64.14 89.3401 65.2 90.4001 65.2 91.7101V96.4501C65.2 97.7601 64.14 98.82 62.83 98.82H32.28C29.51 98.82 27.26 96.5701 27.26 93.8001V91.29C27.26 90.03 27.73 88.8201 28.59 87.8901L70.89 41.9401C73.69 38.9001 77.62 37.1801 81.75 37.1801H94.41C101.84 37.1801 107.87 43.2101 107.87 50.6401V85.3601H107.88Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_2343_96406">
<rect width="136" height="136" rx="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,22 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/icons/android/android-launchericon-48-48.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Twenty</title>
<style>
/* Reset margin and padding */
html, body {
margin: 0;
padding: 0;
height: 100%; /* Ensure body takes full viewport height */
overflow: hidden; /* Prevents scrollbars from appearing */
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/options/index.tsx"></script>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More