Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29893cf994 | ||
|
|
fe68dc2642 |
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"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'",
|
||||
"install": "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": [
|
||||
{
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"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!'",
|
||||
"install": "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 && npm run database:init || 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"
|
||||
"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:password@localhost:5432/postgres && yarn start"
|
||||
},
|
||||
{
|
||||
"name": "Database Management",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
description: Twenty CRM development rules and best practices
|
||||
globs: []
|
||||
alwaysApply: true
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# Twenty Development Rules
|
||||
|
||||
@@ -12,7 +12,6 @@ This directory contains Twenty's development guidelines and best practices in th
|
||||
### 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)
|
||||
- **server-migrations.mdc** - Backend migration and TypeORM guidelines for `twenty-server` (Auto-attached to server entities and migration files)
|
||||
|
||||
### Code Quality
|
||||
- **typescript-guidelines.mdc** - TypeScript best practices and conventions (Auto-attached to .ts/.tsx files)
|
||||
@@ -41,7 +40,7 @@ You can manually reference any rule using the `@ruleName` syntax:
|
||||
- `@testing-guidelines` - Get testing recommendations
|
||||
|
||||
### Rule Types Used
|
||||
- **Always Applied** - Loaded in every context (architecture.mdc, README.mdc)
|
||||
- **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
|
||||
@@ -56,9 +55,7 @@ npx nx storybook:build twenty-front # Build Storybook
|
||||
npx nx storybook:serve-and-test:static # Run Storybook tests
|
||||
|
||||
# Development
|
||||
npx nx lint:changed twenty-front # Lint changed files only (fastest)
|
||||
npx nx lint:changed twenty-front --configuration=fix # Auto-fix changed files
|
||||
npx nx lint twenty-front # Lint all files (slower)
|
||||
npx nx lint twenty-front # Run linter
|
||||
npx nx typecheck twenty-front # Type checking
|
||||
npx nx run twenty-front:graphql:generate # Generate GraphQL types
|
||||
```
|
||||
@@ -72,9 +69,7 @@ npx nx run twenty-server:database:migrate:prod # Run migrations
|
||||
|
||||
# Development
|
||||
npx nx run twenty-server:start # Start the server
|
||||
npx nx lint:changed twenty-server # Lint changed files only (fastest)
|
||||
npx nx lint:changed twenty-server --configuration=fix # Auto-fix changed files
|
||||
npx nx run twenty-server:lint # Lint all files (slower)
|
||||
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
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
---
|
||||
description: Twenty CRM architecture overview - monorepo structure, tech stack, and development principles
|
||||
globs: []
|
||||
alwaysApply: true
|
||||
|
||||
@@ -1,324 +0,0 @@
|
||||
---
|
||||
description: Process and guidelines for creating release changelogs for Twenty CRM
|
||||
globs: ["**/releases/*.mdx", "**/releases/**"]
|
||||
alwaysApply: false
|
||||
---
|
||||
# Twenty Release Changelog Process
|
||||
|
||||
Complete guide for creating release changelogs, including codebase research, file structure, and content guidelines.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting, gather the following information:
|
||||
|
||||
### Required Information
|
||||
|
||||
**Version Number**: `{VERSION}` (e.g., 1.9.0, 2.0.0, 2.1.0)
|
||||
|
||||
**Release Date**: Use today's date in format: YYYY-MM-DD
|
||||
|
||||
### Changes/Features to Document
|
||||
|
||||
List the features and changes to include in this release:
|
||||
|
||||
1. **Feature Name**: ______________________________
|
||||
- Brief description: ______________________________
|
||||
- Related area (workflow, UI, backend, etc.): ______________________________
|
||||
|
||||
2. **Feature Name**: ______________________________
|
||||
- Brief description: ______________________________
|
||||
- Related area: ______________________________
|
||||
|
||||
3. **Feature Name**: ______________________________
|
||||
- Brief description: ______________________________
|
||||
- Related area: ______________________________
|
||||
|
||||
## Codebase Research Guide
|
||||
|
||||
If feature descriptions are not provided or need enhancement, research the codebase:
|
||||
|
||||
### Where to Look
|
||||
|
||||
**For Workflow Features:**
|
||||
- Frontend: `packages/twenty-front/src/modules/workflow/`
|
||||
- Backend: `packages/twenty-server/src/modules/workflow/`
|
||||
- Components: `packages/twenty-front/src/modules/workflow/components/`
|
||||
|
||||
**For UI/UX Changes:**
|
||||
- Components: `packages/twenty-front/src/modules/ui/`
|
||||
- Layout: `packages/twenty-front/src/modules/layout/`
|
||||
- Design system: `packages/twenty-ui/src/`
|
||||
|
||||
**For Backend/API Features:**
|
||||
- Server modules: `packages/twenty-server/src/modules/`
|
||||
- Entities: `packages/twenty-server/src/entities/`
|
||||
- Services: Look for `*.service.ts` files
|
||||
|
||||
**For Database/ORM Changes:**
|
||||
- Migrations: `packages/twenty-server/src/database/typeorm/`
|
||||
- Entities: `packages/twenty-server/src/entities/`
|
||||
|
||||
### Research Commands
|
||||
|
||||
```bash
|
||||
# Find recent merged PRs (adjust date as needed)
|
||||
gh pr list --search "merged:>2025-10-01" --limit 50 --state merged
|
||||
|
||||
# View recent commits
|
||||
git log --since="2 weeks ago" --oneline --no-merges
|
||||
|
||||
# View commits between releases (replace with actual release tags)
|
||||
git log v1.7.0..v1.8.0 --oneline
|
||||
|
||||
# Search for specific feature keywords in code
|
||||
grep -r "iterator" packages/twenty-front/src/modules/workflow/
|
||||
grep -r "bulk select" packages/twenty-front/src/modules/workflow/
|
||||
|
||||
# Find recent changes in specific directory
|
||||
git log --since="2 weeks ago" --oneline -- packages/twenty-front/src/modules/workflow/
|
||||
```
|
||||
|
||||
### Using Codebase Search
|
||||
|
||||
Use the AI codebase search to find:
|
||||
- "How does the workflow iterator node work?"
|
||||
- "Where is bulk select implemented for workflows?"
|
||||
- "What changes were made to the search node limit?"
|
||||
|
||||
## Step-by-Step Process
|
||||
|
||||
### 1. Setup Git Branch
|
||||
|
||||
**IMPORTANT**: Always start from an up-to-date main branch to avoid merge conflicts and ensure the changelog is based on the latest code.
|
||||
|
||||
```bash
|
||||
cd /Users/thomascolasdesfrancs/code/twenty
|
||||
git checkout main
|
||||
git pull origin main
|
||||
git checkout -b {VERSION}
|
||||
```
|
||||
|
||||
Replace `{VERSION}` with the actual version number (e.g., `1.9.0`)
|
||||
|
||||
⚠️ **Do this first** before making any file changes. This ensures your branch is based on the latest main.
|
||||
|
||||
### 2. Create File Structure
|
||||
|
||||
**Create changelog file:**
|
||||
- Path: `packages/twenty-website/src/content/releases/{VERSION}.mdx`
|
||||
- Example: `packages/twenty-website/src/content/releases/1.9.0.mdx`
|
||||
|
||||
**Create image folder:**
|
||||
- Path: `packages/twenty-website/public/images/releases/{MINOR_VERSION}/`
|
||||
- Example for version 1.9.0: `packages/twenty-website/public/images/releases/1.9/`
|
||||
- Example for version 2.0.0: `packages/twenty-website/public/images/releases/2.0/`
|
||||
|
||||
```bash
|
||||
# Create the image folder
|
||||
mkdir -p packages/twenty-website/public/images/releases/{MINOR_VERSION}
|
||||
```
|
||||
|
||||
### 3. Move Illustration Files
|
||||
|
||||
**Source:** `/Users/thomascolasdesfrancs/Downloads/🆕`
|
||||
|
||||
**Destination:** `packages/twenty-website/public/images/releases/{MINOR_VERSION}/`
|
||||
|
||||
**Naming Convention:** `{VERSION}-descriptive-name.png`
|
||||
|
||||
Examples:
|
||||
- `1.9.0-feature-name.png`
|
||||
- `1.9.0-another-feature.png`
|
||||
|
||||
```bash
|
||||
# Move and rename files
|
||||
cp ~/Downloads/🆕/source-file.png packages/twenty-website/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-name.png
|
||||
```
|
||||
|
||||
### 4. Research Features (if needed)
|
||||
|
||||
If descriptions are not provided:
|
||||
1. Use the research commands above to find recent PRs and commits
|
||||
2. Search the codebase for feature-related code
|
||||
3. Read PR descriptions for context
|
||||
4. Check component comments and documentation
|
||||
|
||||
### 5. Write Changelog Content
|
||||
|
||||
Create the MDX file with this structure:
|
||||
|
||||
```markdown
|
||||
---
|
||||
release: {VERSION}
|
||||
Date: {YYYY-MM-DD}
|
||||
---
|
||||
|
||||
# Feature 1 Name
|
||||
|
||||
Short description explaining what the feature does and why it's useful. Keep it user-focused and concise (1-2 sentences).
|
||||
|
||||

|
||||
|
||||
# Feature 2 Name
|
||||
|
||||
Another short description of the second feature.
|
||||
|
||||

|
||||
|
||||
# Feature 3 Name
|
||||
|
||||
Description of the third feature.
|
||||
|
||||

|
||||
```
|
||||
|
||||
**Style Guidelines:**
|
||||
- Use H1 (`#`) for feature names
|
||||
- Keep descriptions to 1-2 sentences
|
||||
- Focus on user benefits, not technical implementation
|
||||
- Use active voice
|
||||
- Start with what the user can now do
|
||||
- **NEVER mention the brand name "Twenty"** in changelog text - use "your workspace", "the platform", or similar neutral references instead
|
||||
|
||||
**Reference Previous Changelogs:**
|
||||
- Check `packages/twenty-website/src/content/releases/` for examples
|
||||
- Recent releases: 1.7.0.mdx, 1.6.0.mdx, 1.5.0.mdx
|
||||
|
||||
### 6. Review
|
||||
|
||||
Open the changelog file for review:
|
||||
```bash
|
||||
# Open in Cursor
|
||||
cursor packages/twenty-website/src/content/releases/{VERSION}.mdx
|
||||
|
||||
# Open image folder to verify illustrations
|
||||
open packages/twenty-website/public/images/releases/{MINOR_VERSION}
|
||||
```
|
||||
|
||||
Review checklist:
|
||||
- [ ] Version number is correct in frontmatter
|
||||
- [ ] Date is today's date
|
||||
- [ ] All features are documented
|
||||
- [ ] Image paths are correct
|
||||
- [ ] Image files exist in the folder
|
||||
- [ ] Descriptions are clear and user-focused
|
||||
- [ ] Spelling and grammar are correct
|
||||
|
||||
### 7. Present Changelog for User Approval
|
||||
|
||||
**IMPORTANT**: Before committing and creating the PR, always show the complete changelog content to the user and wait for explicit approval.
|
||||
|
||||
**What to show:**
|
||||
1. Display the full MDX content of the changelog file
|
||||
2. Confirm that illustration files were moved to the correct location
|
||||
3. List the image file names and paths
|
||||
|
||||
**What to say:**
|
||||
```
|
||||
I've created the changelog for version {VERSION}. Here's the content for your review:
|
||||
|
||||
[Show full MDX content]
|
||||
|
||||
Images moved to:
|
||||
- packages/twenty-website/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-1.png
|
||||
- packages/twenty-website/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-2.png
|
||||
|
||||
Please review the content. Once you approve, I'll commit the changes and create the pull request.
|
||||
```
|
||||
|
||||
**Wait for user approval before proceeding to step 8.**
|
||||
|
||||
Possible user responses:
|
||||
- "Looks good" / "Approve" / "Create the PR" → Proceed to step 8
|
||||
- Requests changes → Make the requested edits, then show content again
|
||||
- Asks questions → Answer them, then wait for approval
|
||||
|
||||
### 8. Commit Changes
|
||||
|
||||
```bash
|
||||
# Check status
|
||||
git status
|
||||
|
||||
# Add files
|
||||
git add packages/twenty-website/src/content/releases/{VERSION}.mdx
|
||||
git add packages/twenty-website/public/images/releases/{MINOR_VERSION}/
|
||||
|
||||
# Commit
|
||||
git commit -m "Add {VERSION} release changelog"
|
||||
|
||||
# Push branch
|
||||
git push -u origin {VERSION}
|
||||
```
|
||||
|
||||
### 9. Create Pull Request
|
||||
|
||||
```bash
|
||||
# Create PR using GitHub CLI
|
||||
gh pr create \
|
||||
--title "Release {VERSION}" \
|
||||
--body "## Release {VERSION}
|
||||
|
||||
This release includes:
|
||||
|
||||
- Feature 1
|
||||
- Feature 2
|
||||
- Feature 3
|
||||
|
||||
Changelog file: \`packages/twenty-website/src/content/releases/{VERSION}.mdx\`
|
||||
Release date: {DATE}" \
|
||||
--base main \
|
||||
--head {VERSION}
|
||||
```
|
||||
|
||||
Or visit: `https://github.com/twentyhq/twenty/pull/new/{VERSION}`
|
||||
|
||||
## File Naming Conventions
|
||||
|
||||
### Changelog Files
|
||||
- **Format**: `{MAJOR}.{MINOR}.{PATCH}.mdx`
|
||||
- **Convention**: One file per complete version
|
||||
- **Examples**: `1.6.0.mdx`, `1.7.0.mdx`, `2.0.0.mdx`
|
||||
- **Location**: `packages/twenty-website/src/content/releases/`
|
||||
|
||||
### Image Folders
|
||||
- **Format**: `{MAJOR}.{MINOR}/`
|
||||
- **Convention**: One folder per minor version (shared across patches)
|
||||
- **Examples**: `1.6/`, `1.7/`, `2.0/`
|
||||
- **Location**: `packages/twenty-website/public/images/releases/`
|
||||
|
||||
### Image Files
|
||||
- **Format**: `{VERSION}-descriptive-name.png`
|
||||
- **Convention**: Kebab-case descriptive names
|
||||
- **Examples**:
|
||||
- `1.8.0-workflow-iterator.png`
|
||||
- `1.8.0-bulk-select.png`
|
||||
- `1.9.0-new-feature.png`
|
||||
|
||||
## Quick Reference Template
|
||||
|
||||
Copy and fill this for each release:
|
||||
|
||||
```
|
||||
VERSION: ___________
|
||||
DATE: ___________
|
||||
MINOR_VERSION: ___________
|
||||
|
||||
Features to document:
|
||||
1. ___________________________
|
||||
2. ___________________________
|
||||
3. ___________________________
|
||||
|
||||
Branch name: {VERSION}
|
||||
Changelog path: packages/twenty-website/src/content/releases/{VERSION}.mdx
|
||||
Images path: packages/twenty-website/public/images/releases/{MINOR_VERSION}/
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- **Start early**: Begin documenting features as they're developed
|
||||
- **User perspective**: Write for users, not developers
|
||||
- **Be concise**: 1-2 sentences per feature
|
||||
- **Visual first**: Illustrations should showcase the feature clearly
|
||||
- **Consistent style**: Match tone and structure of previous changelogs
|
||||
- **Test links**: Verify all image paths work before committing
|
||||
- **Research thoroughly**: Use codebase search to understand features deeply
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
description: Code style guidelines for Twenty CRM
|
||||
globs: []
|
||||
alwaysApply: true
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# Code Style Guidelines
|
||||
|
||||
@@ -30,18 +30,6 @@ type ButtonProps = {}; // Component props suffix with 'Props'
|
||||
// ✅ Files and directories - kebab-case
|
||||
// user-profile.component.tsx
|
||||
// user-profile.styles.ts
|
||||
|
||||
// ❌ NEVER use abbreviations in variable names
|
||||
// Bad
|
||||
const users = data.map((u) => u.name);
|
||||
const field = items.find((f) => f.id === id);
|
||||
|
||||
// Good
|
||||
const users = data.map((user) => user.name);
|
||||
const field = items.find((item) => item.id === id);
|
||||
const fieldMetadata = inlineFields.find(
|
||||
(fieldMetadataItem) => fieldMetadataItem.name === fieldName,
|
||||
);
|
||||
```
|
||||
|
||||
## Import Organization
|
||||
@@ -71,89 +59,32 @@ const processUserData = (
|
||||
): 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 (WHY, not WHAT)
|
||||
// ✅ 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
|
||||
/**
|
||||
* JSDoc for public APIs
|
||||
* @param basePrice - The base price before modifications
|
||||
* @returns The final price after tax and discount
|
||||
*/
|
||||
const calculateTotalPrice = (basePrice: number): number => {
|
||||
// Implementation
|
||||
};
|
||||
|
||||
// ❌ AVOID obvious comments that just describe what code does
|
||||
// Bad: Get all inline fields dynamically
|
||||
const { inlineFieldMetadataItems } = useFieldListFieldMetadataItems({...});
|
||||
|
||||
// Bad: Define standard fields in display order
|
||||
const standardFieldOrder = ['startsAt', 'endsAt', 'conferenceLink'];
|
||||
|
||||
// Bad: Split fields into standard and custom
|
||||
const standardFields = standardFieldOrder.map(...)
|
||||
|
||||
// ✅ GOOD: Only comment if explaining non-obvious business logic
|
||||
// Calendar events display standard fields first, then custom fields after participants
|
||||
// to maintain consistency with the legacy UI behavior
|
||||
const standardFields = standardFieldOrder.map(...)
|
||||
|
||||
// ❌ AVOID JSDoc blocks - use short comments instead
|
||||
/**
|
||||
* This style is NOT preferred in this codebase
|
||||
*/
|
||||
```
|
||||
|
||||
**Comment Guidelines:**
|
||||
- **DO** comment complex business rules or domain-specific logic
|
||||
- **DO** comment non-obvious algorithmic decisions
|
||||
- **DO** add TODOs for future improvements
|
||||
- **DON'T** comment obvious variable declarations or function calls
|
||||
- **DON'T** comment what is already clear from well-named variables/functions
|
||||
- **DON'T** add comments that just repeat what the code says
|
||||
|
||||
## Utility Helpers
|
||||
```typescript
|
||||
// ✅ Use existing utility helpers instead of manual checks
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isNonEmptyString, isNonEmptyArray } from '@sniptt/guards';
|
||||
|
||||
// ❌ Manual type guards
|
||||
const validItems = items.filter((item): item is Item => item !== undefined);
|
||||
const hasValue = value !== null && value !== undefined;
|
||||
|
||||
// ✅ Use utility helpers
|
||||
const validItems = items.filter(isDefined);
|
||||
const hasValue = isDefined(value);
|
||||
|
||||
// Other useful helpers:
|
||||
// - isDefined(value) - checks !== null && !== undefined
|
||||
// - isNonEmptyString(value) - checks string is defined and not empty
|
||||
// - isNonEmptyArray(value) - checks array is defined and has items
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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.
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
description: File structure guidelines for Twenty CRM
|
||||
globs: []
|
||||
alwaysApply: true
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# File Structure Guidelines
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
---
|
||||
description: Guidelines and best practices for working with Nx in the Twenty workspace, including workspace architecture understanding, configuration management, and generator usage.
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
---
|
||||
description: Guidelines and best practices for working with Nx in the Twenty workspace, including workspace architecture understanding, configuration management, and generator usage.
|
||||
globs: ["**/nx.json", "**/project.json", "**/workspace.json"]
|
||||
alwaysApply: true
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Nx Guidelines
|
||||
@@ -12,13 +17,7 @@ alwaysApply: true
|
||||
npx nx run twenty-front:build
|
||||
npx nx run twenty-server:test
|
||||
|
||||
# Lint changed files only (recommended - much faster!)
|
||||
npx nx lint:changed twenty-front # Lint only files changed vs main
|
||||
npx nx lint:changed twenty-server
|
||||
npx nx lint:changed twenty-front --configuration=fix # Auto-fix changed files
|
||||
npx nx lint:changed twenty-front --configuration=staged # Lint staged files
|
||||
|
||||
# Run target for all projects (slower)
|
||||
# Run target for all projects
|
||||
npx nx run-many --target=build --all
|
||||
npx nx run-many --target=test --projects=twenty-front,twenty-server
|
||||
|
||||
@@ -43,25 +42,18 @@ npx nx g @nx/react:component my-component
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"options": { "jestConfig": "packages/twenty-front/jest.config.mjs" }
|
||||
"options": { "jestConfig": "packages/twenty-front/jest.config.ts" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Linting Strategy
|
||||
For faster development, always prefer linting only changed files:
|
||||
- Use `npx nx lint:changed <project>` to lint only files changed vs main branch
|
||||
- Use `--configuration=fix` to auto-fix issues in changed files
|
||||
- Use `--configuration=staged` to lint only staged files (useful for pre-commit hooks)
|
||||
- Only use `npx nx lint <project>` when you need to lint the entire project
|
||||
|
||||
## Dependency Graph
|
||||
```bash
|
||||
# View project dependencies
|
||||
npx nx graph
|
||||
|
||||
# Check what's affected by changes (runs target on affected projects)
|
||||
# Check what's affected by changes
|
||||
npx nx affected --target=test
|
||||
npx nx affected --target=build --base=main
|
||||
```
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
description: React general guidelines for Twenty CRM
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# React Guidelines
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
description: React state management guidelines for Twenty CRM
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# React State Management
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
description: Guidelines for generating and managing TypeORM migrations in twenty-server
|
||||
globs: [
|
||||
"packages/twenty-server/src/**/*.entity.ts",
|
||||
"packages/twenty-server/src/database/typeorm/**/*.ts"
|
||||
]
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
## Server Migrations (twenty-server)
|
||||
|
||||
- **When changing an entity, always generate a migration**
|
||||
- If you modify a `*.entity.ts` file in `packages/twenty-server/src`, you **must** generate a corresponding TypeORM migration instead of manually editing the database schema.
|
||||
- Use the Nx + TypeORM command from the project root:
|
||||
|
||||
```bash
|
||||
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/common/[name] -d src/database/typeorm/core/core.datasource.ts
|
||||
```
|
||||
|
||||
- Replace `[name]` with a descriptive, kebab-case migration name that reflects the change (for example, `add-agent-turn-evaluation`).
|
||||
|
||||
- **Prefer generated migrations over manual edits**
|
||||
- Let TypeORM infer schema changes from the updated entities; only adjust the generated migration file manually if absolutely necessary (for example, for data backfills or complex constraints).
|
||||
- Keep schema changes (DDL) in these generated migrations and avoid mixing in heavy data migrations unless there is a strong reason and clear comments.
|
||||
|
||||
- **Keep migrations consistent and reversible**
|
||||
- Ensure the generated migration includes both `up` and `down` logic that correctly applies and reverts the entity change when possible.
|
||||
- Do not delete or rewrite existing, committed migrations unless you are explicitly working on a pre-release branch where history rewrites are allowed by team conventions.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
description: Testing guidelines for Twenty CRM
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# Testing Guidelines
|
||||
@@ -69,45 +70,6 @@ beforeEach(() => {
|
||||
- **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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
description: Translation guidelines for Twenty CRM
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# Translation Guidelines
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
description: TypeScript best practices and conventions for the Twenty codebase, including strict typing, naming conventions, and type safety guidelines.
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
---
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"setup-worktree": [
|
||||
"nvm use",
|
||||
"yarn",
|
||||
"cp $ROOT_WORKTREE_PATH/packages/twenty-server/.env packages/twenty-server/.env || true",
|
||||
"cp $ROOT_WORKTREE_PATH/packages/twenty-front/.env packages/twenty-front/.env || true"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['plugin:prettier/recommended', 'plugin:lingui/recommended'],
|
||||
ignorePatterns: ['node_modules'],
|
||||
plugins: [
|
||||
'@nx',
|
||||
'prefer-arrow',
|
||||
'import',
|
||||
'unused-imports',
|
||||
'unicorn',
|
||||
'lingui',
|
||||
],
|
||||
rules: {
|
||||
'lingui/no-single-variables-to-translate': 'off',
|
||||
'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'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:zapier',
|
||||
onlyDependOnLibsWithTags: ['scope:shared'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
'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-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)',
|
||||
'*.integration-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',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,199 @@
|
||||
var path = require('path');
|
||||
|
||||
module.exports = {
|
||||
extends: [
|
||||
'plugin:@nx/react',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:storybook/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'plugin:lingui/recommended',
|
||||
'plugin:@nx/typescript'
|
||||
],
|
||||
plugins: ['react-hooks', 'react-refresh', '@nx', 'prefer-arrow', 'import', 'unused-imports', 'unicorn', 'lingui'],
|
||||
rules: {
|
||||
'lingui/no-single-variables-to-translate': 'off',
|
||||
'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'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:zapier',
|
||||
onlyDependOnLibsWithTags: ['scope:shared'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
'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'],
|
||||
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'`)",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-empty-interface': [
|
||||
'error',
|
||||
{
|
||||
allowSingleExtends: true,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@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'),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.js', '*.jsx'],
|
||||
extends: ['plugin:@nx/javascript'],
|
||||
rules: {},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'*.test.@(ts|tsx|js|jsx)',
|
||||
],
|
||||
env: {
|
||||
jest: true,
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
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 }],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.json'],
|
||||
parser: 'jsonc-eslint-parser',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Contributing to Twenty
|
||||
|
||||
Thanks for considering contributing to Twenty!
|
||||
Thanks for considering contributing to Twenty!
|
||||
|
||||
Please make sure to go through the [documentation](https://docs.twenty.com) before.
|
||||
Please make sure to go through the [documentation](https://docs.twenty.com) before.
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
## Good first issues
|
||||
|
||||
Good first issues are a great way to start contributing and get familiar with the codebase. You can find them on by filtering on the [good first issue](https://github.com/twentyhq/twenty/labels/good%20first%20issue) label.
|
||||
Good first issues are a great way to start contributing and get familiar with the codebase. You can find them on by filtering on the [good first issue](https://github.com/twentyhq/twenty/labels/good%20first%20issue) label.
|
||||
|
||||
## Issue assignment
|
||||
|
||||
To avoid conflicts, we follow these guidelines:
|
||||
|
||||
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://docs.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.
|
||||
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.
|
||||
|
||||
## How to Contribute
|
||||
@@ -63,4 +63,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.
|
||||
|
||||
@@ -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)
|
||||
```
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 3
|
||||
versioning-strategy: "lockfile-only"
|
||||
assignees:
|
||||
- "mabdullahabaid"
|
||||
ignore:
|
||||
- dependency-name: "@graphql-yoga/nestjs"
|
||||
- dependency-name: "@nestjs/graphql"
|
||||
- dependency-name: "@ptc-org/nestjs-query-graphql"
|
||||
- dependency-name: "typeorm"
|
||||
@@ -25,11 +25,11 @@ runs:
|
||||
uses: actions/cache/restore@v4
|
||||
id: restore-cache
|
||||
with:
|
||||
key: ${{ steps.cache-primary-key-builder.outputs.CACHE_PRIMARY_KEY_PREFIX }}-${{ github.sha }}
|
||||
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
|
||||
.nx/cache
|
||||
node_modules/.cache
|
||||
packages/*/node_modules/.cache
|
||||
${{ inputs.additional-paths }}
|
||||
@@ -15,7 +15,7 @@ runs:
|
||||
key: ${{ inputs.key }}
|
||||
path: |
|
||||
.cache
|
||||
.nx
|
||||
.nx/cache
|
||||
node_modules/.cache
|
||||
packages/*/node_modules/.cache
|
||||
${{ inputs.additional-paths }}
|
||||
@@ -2,10 +2,10 @@ name: Yarn Install
|
||||
inputs:
|
||||
node-version:
|
||||
required: false
|
||||
default: '24'
|
||||
default: '22'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Cache primary key builder
|
||||
id: globals
|
||||
@@ -25,8 +25,8 @@ runs:
|
||||
id: cache-node-modules
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
key: v4-${{ steps.globals.outputs.CACHE_KEY_PREFIX }}-${{github.sha}}
|
||||
restore-keys: v4-${{ steps.globals.outputs.CACHE_KEY_PREFIX }}-
|
||||
key: v3-${{ steps.globals.outputs.CACHE_KEY_PREFIX }}-${{github.sha}}
|
||||
restore-keys: v3-${{ steps.globals.outputs.CACHE_KEY_PREFIX }}-
|
||||
path: ${{ steps.globals.outputs.PATH_TO_CACHE }}
|
||||
- name: Install Dependencies
|
||||
if: ${{ steps.cache-node-modules.outputs.cache-hit != 'true' && steps.cache-node-modules.outputs.cache-matched-key == '' }}
|
||||
@@ -40,3 +40,4 @@ runs:
|
||||
with:
|
||||
key: ${{ steps.cache-node-modules.outputs.cache-primary-key }}
|
||||
path: ${{ steps.globals.outputs.PATH_TO_CACHE }}
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
name: CD deploy main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy-main:
|
||||
timeout-minutes: 3
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
name: CD deploy tag
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
deploy-tag:
|
||||
timeout-minutes: 3
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
name: Changed files reusable workflow
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
@@ -10,9 +9,6 @@ on:
|
||||
any_changed:
|
||||
value: ${{ jobs.changed-files.outputs.any_changed }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
changed-files:
|
||||
timeout-minutes: 5
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
name: GraphQL and OpenAPI Breaking Changes Detection
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
pull_request:
|
||||
types: [opened, synchronize, edited]
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
MAIN_SERVER_PORT: 3000
|
||||
@@ -34,7 +34,9 @@ jobs:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 45
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
env:
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
@@ -55,7 +57,7 @@ jobs:
|
||||
ports:
|
||||
- 6379:6379
|
||||
clickhouse:
|
||||
image: clickhouse/clickhouse-server:25.8.8
|
||||
image: clickhouse/clickhouse-server:latest
|
||||
env:
|
||||
CLICKHOUSE_PASSWORD: clickhousePassword
|
||||
CLICKHOUSE_URL: "http://default:clickhousePassword@localhost:8123/twenty"
|
||||
@@ -124,14 +126,13 @@ jobs:
|
||||
- name: Setup current branch database
|
||||
run: |
|
||||
npx nx reset:env twenty-server
|
||||
# Function to set or update environment variable
|
||||
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
|
||||
if grep -q "^${var_name}=" "$env_file"; then
|
||||
sed -i "s|^${var_name}=.*|${var_name}=${var_value}|" "$env_file"
|
||||
else
|
||||
echo "${var_name}=${var_value}" >> "$env_file"
|
||||
@@ -258,16 +259,13 @@ jobs:
|
||||
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
|
||||
git clean -fd
|
||||
|
||||
- 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
|
||||
|
||||
@@ -276,15 +274,13 @@ jobs:
|
||||
|
||||
- name: Setup main branch database
|
||||
run: |
|
||||
npx nx reset:env twenty-server
|
||||
# Function to set or update environment variable
|
||||
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
|
||||
if grep -q "^${var_name}=" "$env_file"; then
|
||||
sed -i "s|^${var_name}=.*|${var_name}=${var_value}|" "$env_file"
|
||||
else
|
||||
echo "${var_name}=${var_value}" >> "$env_file"
|
||||
@@ -293,9 +289,6 @@ jobs:
|
||||
|
||||
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
|
||||
@@ -684,7 +677,7 @@ jobs:
|
||||
}
|
||||
|
||||
// Check PR title for "breaking"
|
||||
const prTitle = ${{ toJSON(github.event.pull_request.title) }};
|
||||
const prTitle = `${{ github.event.pull_request.title }}`;
|
||||
const titleContainsBreaking = prTitle.toLowerCase().includes('breaking');
|
||||
|
||||
if (hasBreakingChanges) {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
name: CI Chrome Extension
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
package.json
|
||||
packages/twenty-chrome-extension/**
|
||||
chrome-extension-build:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
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: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Chrome Extension / Run build
|
||||
run: npx nx build twenty-chrome-extension
|
||||
ci-chrome-extension-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, chrome-extension-build]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
@@ -1,57 +0,0 @@
|
||||
name: CI Create App
|
||||
|
||||
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/create-twenty-app/**
|
||||
create-app-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: Build
|
||||
run: npx nx build create-twenty-app
|
||||
- name: Run ${{ matrix.task }} task
|
||||
uses: ./.github/workflows/actions/nx-affected
|
||||
with:
|
||||
tag: scope:create-app
|
||||
tasks: ${{ matrix.task }}
|
||||
ci-create-app-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, create-app-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
@@ -0,0 +1,63 @@
|
||||
name: CI Demo check
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 7,19 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./packages/twenty-e2e-testing
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: yarn playwright install --with-deps
|
||||
|
||||
- name: Run Playwright tests
|
||||
id: test
|
||||
run: yarn playwright test --grep "@demo-only"
|
||||
|
||||
- name: Upload report after tests
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 90
|
||||
|
||||
- name: Send Discord notification
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
uses: Ilshidur/action-discord@0.3.2
|
||||
with:
|
||||
args: 'Demo check ${{ steps.test.outcome }} - check ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
|
||||
- name: Send email if demo is not working
|
||||
if: steps.test.outcome == 'failure'
|
||||
uses: dawidd6/action-send-mail@v3.12.0
|
||||
with:
|
||||
connection_url: ${{ secrets.MAIL_CONNECTION }}
|
||||
server_address: smtp.gmail.com
|
||||
server_port: 465
|
||||
secure: true
|
||||
username: ${{ secrets.MAIL_USERNAME }}
|
||||
subject: 'Demo is not working'
|
||||
from: 'Github CI Demo check'
|
||||
to: ${{ secrets.RECIPIENTS }}
|
||||
body: '<a href="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}">Link</a>'
|
||||
priority: high
|
||||
@@ -1,47 +0,0 @@
|
||||
name: CI Docs
|
||||
|
||||
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: |
|
||||
package.json
|
||||
packages/twenty-docs/**
|
||||
eslint.config.mjs
|
||||
|
||||
docs-lint:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/cancel-workflow-action@0.11.0
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
|
||||
- name: Fetch local actions
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
|
||||
- name: Docs / Lint English MDX files
|
||||
run: npx eslint "packages/twenty-docs/{developers,user-guide,twenty-ui,getting-started,snippets}/**/*.mdx" --max-warnings 0
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
name: CI E2E Playwright Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -7,12 +6,9 @@ on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
@@ -28,6 +24,7 @@ jobs:
|
||||
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:
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
# https://github.com/actions/runner-images/issues/70#issuecomment-589562148
|
||||
NODE_OPTIONS: "--max-old-space-size=10240"
|
||||
services:
|
||||
@@ -78,7 +75,7 @@ jobs:
|
||||
- name: Setup environment files
|
||||
run: |
|
||||
cp packages/twenty-front/.env.example packages/twenty-front/.env
|
||||
npx nx reset:env:e2e-testing-server twenty-server
|
||||
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
|
||||
@@ -103,7 +100,7 @@ jobs:
|
||||
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 &
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
name: CI Emails
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -12,7 +8,7 @@ on:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
@@ -26,6 +22,8 @@ jobs:
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 10
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
env:
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
@@ -56,7 +54,7 @@ jobs:
|
||||
ci-emails-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
needs: [changed-files-check, emails-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
name: CI Front
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -7,16 +6,12 @@ on:
|
||||
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
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 }}
|
||||
STORYBOOK_BUILD_CACHE_KEY: storybook-build-depot-ubuntu-24.04-8-runner
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
@@ -24,17 +19,17 @@ jobs:
|
||||
with:
|
||||
files: |
|
||||
package.json
|
||||
yarn.lock
|
||||
packages/twenty-front/**
|
||||
packages/twenty-ui/**
|
||||
packages/twenty-shared/**
|
||||
front-sb-build:
|
||||
needs: changed-files-check
|
||||
needs: [changed-files-check]
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
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
|
||||
@@ -48,6 +43,11 @@ jobs:
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Diagnostic disk space issue
|
||||
run: df -h
|
||||
- name: Restore storybook build cache
|
||||
id: restore-storybook-build-cache
|
||||
uses: ./.github/workflows/actions/restore-cache
|
||||
with:
|
||||
key: ${{ env.STORYBOOK_BUILD_CACHE_KEY }}
|
||||
- name: Front / Write .env
|
||||
run: npx nx reset:env twenty-front
|
||||
- name: Front / Build storybook
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
- name: Save storybook build cache
|
||||
uses: ./.github/workflows/actions/save-cache
|
||||
with:
|
||||
key: ${{ env.STORYBOOK_BUILD_CACHE_KEY_FOR_SAVE_ACTION }}
|
||||
key: ${{ steps.restore-storybook-build-cache.outputs.cache-primary-key }}
|
||||
front-sb-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
@@ -68,6 +68,7 @@ jobs:
|
||||
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
|
||||
@@ -80,7 +81,7 @@ jobs:
|
||||
- name: Restore storybook build cache
|
||||
uses: ./.github/workflows/actions/restore-cache
|
||||
with:
|
||||
key: ${{ env.STORYBOOK_BUILD_CACHE_KEY_FOR_RESTORE_ACTION }}
|
||||
key: ${{ env.STORYBOOK_BUILD_CACHE_KEY }}
|
||||
- name: Front / Write .env
|
||||
run: npx nx reset:env twenty-front
|
||||
- name: Run storybook tests
|
||||
@@ -95,7 +96,7 @@ jobs:
|
||||
path: packages/twenty-front/coverage/storybook/coverage-shard-${{matrix.shard}}.json
|
||||
merge-reports-and-check-coverage:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
needs: front-sb-test
|
||||
env:
|
||||
PATH_TO_COVERAGE: packages/twenty-front/coverage/storybook
|
||||
@@ -127,6 +128,7 @@ jobs:
|
||||
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:
|
||||
@@ -141,16 +143,16 @@ jobs:
|
||||
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
|
||||
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
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
env:
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
TASK_CACHE_KEY: front-task-${{ matrix.task }}
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -185,37 +187,14 @@ jobs:
|
||||
uses: ./.github/workflows/actions/save-cache
|
||||
with:
|
||||
key: ${{ steps.restore-task-cache.outputs.cache-primary-key }}
|
||||
front-build:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_OPTIONS: "--max-old-space-size=10240"
|
||||
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: Front / Write .env
|
||||
run: npx nx reset:env twenty-front
|
||||
- name: Build frontend
|
||||
run: npx nx build twenty-front
|
||||
ci-front-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
needs:
|
||||
[
|
||||
changed-files-check,
|
||||
front-task,
|
||||
front-build,
|
||||
front-chromatic-deployment,
|
||||
merge-reports-and-check-coverage,
|
||||
front-sb-test,
|
||||
|
||||
@@ -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' || '' }}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
name: CI SDK
|
||||
|
||||
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-sdk/**
|
||||
sdk-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: Build
|
||||
run: npx nx build twenty-sdk
|
||||
- name: Run ${{ matrix.task }} task
|
||||
uses: ./.github/workflows/actions/nx-affected
|
||||
with:
|
||||
tag: scope:sdk
|
||||
tasks: ${{ matrix.task }}
|
||||
sdk-e2e-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
needs: [changed-files-check, sdk-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: SDK / Run E2E Tests
|
||||
run: npx nx test:e2e twenty-sdk
|
||||
ci-sdk-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, sdk-test, sdk-e2e-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
@@ -1,5 +1,4 @@
|
||||
name: CI Server
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -7,12 +6,9 @@ on:
|
||||
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
SERVER_SETUP_CACHE_KEY: server-setup
|
||||
@@ -23,10 +19,7 @@ jobs:
|
||||
with:
|
||||
files: |
|
||||
package.json
|
||||
yarn.lock
|
||||
packages/twenty-server/**
|
||||
packages/twenty-front/src/generated/**
|
||||
packages/twenty-front/src/generated-metadata/**
|
||||
packages/twenty-emails/**
|
||||
packages/twenty-shared/**
|
||||
server-setup:
|
||||
@@ -34,6 +27,8 @@ jobs:
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
env:
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
@@ -72,10 +67,10 @@ jobs:
|
||||
with:
|
||||
tag: scope:backend
|
||||
tasks: lint,typecheck
|
||||
- name: Server / Write .env
|
||||
run: npx nx reset:env twenty-server
|
||||
- name: Server / Build
|
||||
run: npx nx build twenty-server
|
||||
- name: Server / Write .env
|
||||
run: npx nx reset:env twenty-server
|
||||
- name: Server / Create DB
|
||||
run: |
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||
@@ -92,20 +87,6 @@ jobs:
|
||||
# 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)
|
||||
@@ -117,36 +98,32 @@ jobs:
|
||||
echo "$CORE_MIGRATION_OUTPUT"
|
||||
|
||||
rm -f packages/twenty-server/*core-migration-check.ts
|
||||
|
||||
|
||||
exit 1
|
||||
fi
|
||||
- name: GraphQL / Check for Pending Generation
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: |
|
||||
# Run GraphQL generation commands
|
||||
npx nx run twenty-front:graphql:generate
|
||||
npx nx run twenty-front:graphql:generate --configuration=metadata
|
||||
|
||||
# Check if GraphQL generated files were modified
|
||||
if ! git diff --quiet -- packages/twenty-front/src/generated packages/twenty-front/src/generated-metadata; then
|
||||
# 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 -- packages/twenty-front/src/generated packages/twenty-front/src/generated-metadata
|
||||
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
|
||||
needs: server-setup
|
||||
env:
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
@@ -171,7 +148,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4, 5, 6]
|
||||
shard: [1, 2, 3, 4]
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
@@ -192,7 +169,7 @@ jobs:
|
||||
ports:
|
||||
- 6379:6379
|
||||
clickhouse:
|
||||
image: clickhouse/clickhouse-server:25.8.8
|
||||
image: clickhouse/clickhouse-server:latest
|
||||
env:
|
||||
CLICKHOUSE_PASSWORD: clickhousePassword
|
||||
CLICKHOUSE_URL: "http://default:clickhousePassword@localhost:8123/twenty"
|
||||
@@ -200,16 +177,17 @@ jobs:
|
||||
- 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-cmd "clickhouse-client --host=localhost --port=9000 --user=default --password=clickhousePassword --query='SELECT 1'"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
env:
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
NODE_ENV: test
|
||||
ANALYTICS_ENABLED: true
|
||||
CLICKHOUSE_URL: "http://default:clickhousePassword@localhost:8123/twenty"
|
||||
CLICKHOUSE_PASSWORD: clickhousePassword
|
||||
SHARD_COUNTER: 6
|
||||
SHARD_COUNTER: 4
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
@@ -219,7 +197,6 @@ jobs:
|
||||
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
|
||||
@@ -252,9 +229,9 @@ jobs:
|
||||
ci-server-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
needs: [changed-files-check, server-setup, server-test, server-integration-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
name: CI Shared
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -7,12 +6,9 @@ on:
|
||||
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
@@ -25,6 +21,8 @@ jobs:
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
strategy:
|
||||
matrix:
|
||||
task: [lint, typecheck, test]
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
name: 'Test Docker Compose'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
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:
|
||||
@@ -37,7 +33,6 @@ jobs:
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
name: CI Website
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -12,7 +8,7 @@ on:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
name: 'Pull docs 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 status'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
workflow_call:
|
||||
inputs:
|
||||
force_pull:
|
||||
description: 'Force pull translations regardless of status'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
pull_request:
|
||||
paths:
|
||||
- 'packages/twenty-docs/**'
|
||||
- 'crowdin.yml'
|
||||
- '.github/workflows/docs-i18n-pull.yaml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
pull_docs_translations:
|
||||
name: Pull docs translations
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
ref: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: 'yarn.lock'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: 'yarn.lock'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Setup i18n branch
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
git fetch origin i18n || true
|
||||
git checkout -B i18n origin/i18n || git checkout -b i18n
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@twenty.com'
|
||||
|
||||
- name: Stash any changes before pulling translations
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
git add .
|
||||
git stash || true
|
||||
|
||||
- name: Pull translated docs from Crowdin
|
||||
if: github.event_name != 'pull_request' && (inputs.force_pull == true || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
download_translations: true
|
||||
source: 'packages/twenty-docs/**/*.mdx'
|
||||
translation: 'packages/twenty-docs/l/%two_letters_code%/**/%original_file_name%'
|
||||
export_only_approved: false
|
||||
localization_branch_name: i18n
|
||||
base_url: 'https://twenty.api.crowdin.com'
|
||||
skip_untranslated_files: true
|
||||
push_translations: false
|
||||
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 }}
|
||||
|
||||
- name: Fix file permissions
|
||||
if: github.event_name != 'pull_request'
|
||||
run: sudo chown -R runner:docker . || true
|
||||
|
||||
- name: Fix translated documentation links
|
||||
run: bash packages/twenty-docs/scripts/fix-translated-links.sh
|
||||
|
||||
- name: Regenerate navigation template
|
||||
if: github.event_name == 'pull_request'
|
||||
run: yarn docs:generate-navigation-template
|
||||
|
||||
- name: Regenerate docs.json
|
||||
run: yarn docs:generate
|
||||
|
||||
- name: Commit artifacts to pull request branch
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
git add packages/twenty-docs/docs.json packages/twenty-docs/navigation/navigation.template.json
|
||||
if git diff --staged --quiet --exit-code; then
|
||||
echo "No navigation/doc changes to commit."
|
||||
exit 0
|
||||
fi
|
||||
git commit -m "chore: sync docs artifacts"
|
||||
git push origin "HEAD:$HEAD_REF"
|
||||
env:
|
||||
HEAD_REF: ${{ github.head_ref }}
|
||||
|
||||
- name: Check for changes and commit
|
||||
if: github.event_name != 'pull_request'
|
||||
id: check_changes
|
||||
run: |
|
||||
git add .
|
||||
if ! git diff --staged --quiet --exit-code; then
|
||||
git commit -m "chore: update docs translations from Crowdin and fix internal links"
|
||||
echo "changes_detected=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "changes_detected=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Push changes
|
||||
if: github.event_name != 'pull_request' && steps.check_changes.outputs.changes_detected == 'true'
|
||||
run: git push origin HEAD:i18n
|
||||
|
||||
- name: Create pull request
|
||||
if: github.event_name != 'pull_request' && steps.check_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 - docs 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,68 +0,0 @@
|
||||
name: 'Push docs to Crowdin'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
push:
|
||||
branches: ['main', 'docs-localized-navigation']
|
||||
paths:
|
||||
- 'packages/twenty-docs/**/*.mdx'
|
||||
- '!packages/twenty-docs/fr/**'
|
||||
- 'crowdin.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
push_docs:
|
||||
name: Push documentation to Crowdin
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: 'yarn.lock'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Generate navigation template for Crowdin
|
||||
run: yarn docs:generate-navigation-template
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: 'yarn.lock'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Generate navigation template for Crowdin
|
||||
run: yarn docs:generate-navigation-template
|
||||
|
||||
- name: Upload docs to Crowdin
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
download_translations: false
|
||||
localization_branch_name: i18n
|
||||
base_url: 'https://twenty.api.crowdin.com'
|
||||
env:
|
||||
CROWDIN_PROJECT_ID: 1
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
|
||||
name: 'Pull translations from Crowdin'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 */2 * * *' # Every two hours.
|
||||
@@ -27,12 +23,16 @@ on:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
pull_translations:
|
||||
name: Pull translations
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -74,8 +74,6 @@ jobs:
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
download_translations: true
|
||||
source: '**/en.po'
|
||||
translation: '%original_path%/%locale%.po'
|
||||
export_only_approved: false
|
||||
localization_branch_name: i18n
|
||||
base_url: 'https://twenty.api.crowdin.com'
|
||||
@@ -93,7 +91,7 @@ jobs:
|
||||
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
|
||||
@@ -101,11 +99,10 @@ jobs:
|
||||
|
||||
- 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'
|
||||
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-emails:lingui:compile
|
||||
npx nx run twenty-front:lingui:compile
|
||||
git status
|
||||
git config --global user.name 'github-actions'
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
name: 'Push translations to Crowdin'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
@@ -12,12 +8,15 @@ on:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
extract_translations:
|
||||
name: Extract and upload translations
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
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
|
||||
@@ -24,6 +19,10 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
trigger-preview:
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
pull-requests: read
|
||||
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
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
name: 'Preview Environment Keep Alive'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [preview-environment]
|
||||
@@ -30,14 +26,10 @@ jobs:
|
||||
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
|
||||
@@ -61,7 +53,6 @@ jobs:
|
||||
# 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
|
||||
|
||||
+1
-3
@@ -9,7 +9,6 @@
|
||||
|
||||
.nx/installation
|
||||
.nx/cache
|
||||
.nx/workspace-data
|
||||
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
@@ -46,5 +45,4 @@ dump.rdb
|
||||
.crowdin.yml
|
||||
.react-email/
|
||||
|
||||
mcp.json
|
||||
/.junie/
|
||||
mcp.json
|
||||
@@ -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": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
Vendored
+17
-10
@@ -5,10 +5,14 @@
|
||||
{
|
||||
"name": "twenty-server - start debug",
|
||||
"type": "node",
|
||||
"runtimeVersion": "24.5",
|
||||
"runtimeVersion": "22.12",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeArgs": ["nx", "run", "twenty-server:start"],
|
||||
"runtimeArgs": [
|
||||
"nx",
|
||||
"run",
|
||||
"twenty-server:start"
|
||||
],
|
||||
"outputCapture": "std",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"console": "internalConsole",
|
||||
@@ -19,7 +23,11 @@
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeArgs": ["nx", "run", "twenty-server:worker"],
|
||||
"runtimeArgs": [
|
||||
"nx",
|
||||
"run",
|
||||
"twenty-server:worker"
|
||||
],
|
||||
"outputCapture": "std",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"console": "internalConsole",
|
||||
@@ -30,7 +38,7 @@
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeVersion": "24.5",
|
||||
"runtimeVersion": "22.12",
|
||||
"runtimeArgs": [
|
||||
"nx",
|
||||
"run",
|
||||
@@ -66,15 +74,14 @@
|
||||
"--",
|
||||
"--config",
|
||||
"./jest-integration.config.ts",
|
||||
"${relativeFile}",
|
||||
"--silent=false",
|
||||
"${relativeFile}"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/packages/twenty-server",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"env": {
|
||||
"NODE_ENV": "test"
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
@@ -87,7 +94,7 @@
|
||||
"twenty-server:jest",
|
||||
"--",
|
||||
"--config",
|
||||
"./jest.config.mjs",
|
||||
"./jest.config.ts",
|
||||
"${relativeFile}"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/packages/twenty-server",
|
||||
@@ -95,7 +102,7 @@
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"env": {
|
||||
"NODE_ENV": "test"
|
||||
}
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Vendored
+2
-5
@@ -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": {
|
||||
@@ -51,6 +49,5 @@
|
||||
"files.associations": {
|
||||
".cursorrules": "markdown"
|
||||
},
|
||||
"jestrunner.codeLensSelector": "**/*.{test,spec,integration-spec}.{js,jsx,ts,tsx}",
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
"jestrunner.codeLensSelector": "**/*.{test,spec,integration-spec}.{js,jsx,ts,tsx}"
|
||||
}
|
||||
|
||||
Vendored
+4
@@ -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"
|
||||
|
||||
+925
File diff suppressed because one or more lines are too long
Vendored
-942
File diff suppressed because one or more lines are too long
+1
-1
@@ -6,4 +6,4 @@ enableInlineHunks: true
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.9.2.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.4.0.cjs
|
||||
|
||||
@@ -1,158 +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 (changed files only - fastest)
|
||||
npx nx lint:changed twenty-front # Lint only changed files vs main
|
||||
npx nx lint:changed twenty-server # Lint only changed files vs main
|
||||
npx nx lint:changed twenty-front --configuration=fix # Auto-fix changed files
|
||||
npx nx lint:changed twenty-front --configuration=staged # Lint staged files only
|
||||
|
||||
# Linting (full project)
|
||||
npx nx lint twenty-front # Lint all files in frontend
|
||||
npx nx lint twenty-server # Lint all files in backend
|
||||
npx nx lint twenty-front --fix # Auto-fix all 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/common/[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
|
||||
@@ -3,7 +3,8 @@ 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
|
||||
postgres-on-docker:
|
||||
make ensure-docker-network
|
||||
docker run -d --network $(DOCKER_NETWORK) \
|
||||
--name twenty_pg \
|
||||
-e POSTGRES_USER=postgres \
|
||||
@@ -21,13 +22,16 @@ postgres-on-docker: ensure-docker-network
|
||||
-c "CREATE DATABASE \"default\" WITH OWNER postgres;" \
|
||||
-c "CREATE DATABASE \"test\" WITH OWNER postgres;"
|
||||
|
||||
redis-on-docker: ensure-docker-network
|
||||
redis-on-docker:
|
||||
make 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
|
||||
clickhouse-on-docker:
|
||||
make 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
|
||||
grafana-on-docker:
|
||||
make ensure-docker-network
|
||||
docker run -d --network $(DOCKER_NETWORK) \
|
||||
--name twenty_grafana \
|
||||
-p 4000:3000 \
|
||||
@@ -37,7 +41,8 @@ grafana-on-docker: ensure-docker-network
|
||||
-v $(PWD)/packages/twenty-docker/grafana/provisioning/datasources:/etc/grafana/provisioning/datasources \
|
||||
grafana/grafana-oss:latest
|
||||
|
||||
opentelemetry-collector-on-docker: ensure-docker-network
|
||||
opentelemetry-collector-on-docker:
|
||||
make ensure-docker-network
|
||||
docker run -d --network $(DOCKER_NETWORK) \
|
||||
--name twenty_otlp_collector \
|
||||
-p 4317:4317 \
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
<br />
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.producthunt.com/products/twenty-crm">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/product-hunt-banner-light.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/product-hunt-banner-light.png" />
|
||||
<img src="./packages/twenty-website/public/images/readme/product-hunt-banner-light.png" alt="Product Hunt Banner" />
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
<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" />
|
||||
@@ -6,7 +19,7 @@
|
||||
|
||||
<h2 align="center" >The #1 Open-Source CRM </h2>
|
||||
|
||||
<p align="center"><a href="https://twenty.com">🌐 Website</a> · <a href="https://docs.twenty.com">📚 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://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>
|
||||
<br />
|
||||
|
||||
|
||||
@@ -22,11 +35,11 @@
|
||||
|
||||
<br />
|
||||
|
||||
# Installation
|
||||
# Installation
|
||||
|
||||
See:
|
||||
🚀 [Self-hosting](https://docs.twenty.com/developers/self-hosting/docker-compose)
|
||||
🖥️ [Local Setup](https://docs.twenty.com/developers/local-setup)
|
||||
See:
|
||||
🚀 [Self-hosting](https://twenty.com/developers/section/self-hosting)
|
||||
🖥️ [Local Setup](https://twenty.com/developers/local-setup)
|
||||
|
||||
# Does the world need another CRM?
|
||||
|
||||
@@ -42,7 +55,7 @@ We built Twenty for three reasons:
|
||||
|
||||
# What You Can Do With Twenty
|
||||
|
||||
Please feel free to flag any specific needs you have by creating an issue.
|
||||
Please feel free to flag any specific needs you have by creating an issue.
|
||||
|
||||
Below are a few features we have implemented to date:
|
||||
|
||||
@@ -129,7 +142,7 @@ Below are a few features we have implemented to date:
|
||||
|
||||
- Star the repo
|
||||
- Subscribe to releases (watch -> custom -> releases)
|
||||
- Follow us on [Twitter](https://twitter.com/twentycrm) or [LinkedIn](https://www.linkedin.com/company/twenty/)
|
||||
- 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!
|
||||
- Improve translations on [Crowdin](https://twenty.crowdin.com/twenty)
|
||||
- [Contributions](https://github.com/twentyhq/twenty/contribute) are, of course, most welcome!
|
||||
|
||||
+1
-33
@@ -27,37 +27,5 @@ files: [
|
||||
# e.g. "/resources/%two_letters_code%/%original_file_name%"
|
||||
#
|
||||
"translation": "%original_path%/%locale%.po",
|
||||
},
|
||||
{
|
||||
#
|
||||
# MDX documentation files - user-guide
|
||||
# Using md type to preserve JSX component structure
|
||||
# This prevents Crowdin from reformatting <Warning>, <Accordion>, etc.
|
||||
#
|
||||
"source": "packages/twenty-docs/user-guide/**/*.mdx",
|
||||
"translation": "packages/twenty-docs/l/%two_letters_code%/user-guide/**/%original_file_name%",
|
||||
},
|
||||
{
|
||||
#
|
||||
# MDX documentation files - developers
|
||||
# Using md type to preserve JSX component structure
|
||||
#
|
||||
"source": "packages/twenty-docs/developers/**/*.mdx",
|
||||
"translation": "packages/twenty-docs/l/%two_letters_code%/developers/**/%original_file_name%",
|
||||
},
|
||||
{
|
||||
#
|
||||
# MDX documentation files - twenty-ui
|
||||
# Using md type to preserve JSX component structure
|
||||
#
|
||||
"source": "packages/twenty-docs/twenty-ui/**/*.mdx",
|
||||
"translation": "packages/twenty-docs/l/%two_letters_code%/twenty-ui/**/%original_file_name%",
|
||||
},
|
||||
{
|
||||
#
|
||||
# Navigation labels template - translated into per-locale navigation.json
|
||||
#
|
||||
"source": "packages/twenty-docs/navigation/navigation.template.json",
|
||||
"translation": "packages/twenty-docs/l/%two_letters_code%/navigation.json",
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -1,213 +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 * as mdxPlugin from 'eslint-plugin-mdx';
|
||||
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:apps',
|
||||
onlyDependOnLibsWithTags: ['scope:apps', 'scope:sdk'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:sdk',
|
||||
onlyDependOnLibsWithTags: ['scope:sdk', 'scope:shared'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:shared',
|
||||
onlyDependOnLibsWithTags: ['scope:shared'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:backend',
|
||||
onlyDependOnLibsWithTags: ['scope:shared', 'scope:backend'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:frontend',
|
||||
onlyDependOnLibsWithTags: ['scope:shared', 'scope:frontend'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
// 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,
|
||||
},
|
||||
},
|
||||
|
||||
// MDX files
|
||||
{
|
||||
...mdxPlugin.flat,
|
||||
plugins: {
|
||||
...mdxPlugin.flat.plugins,
|
||||
'@nx': nxPlugin,
|
||||
},
|
||||
},
|
||||
mdxPlugin.flatCodeBlocks,
|
||||
{
|
||||
files: ['**/*.mdx'],
|
||||
rules: {
|
||||
'no-unused-vars': 'off',
|
||||
'unused-imports/no-unused-imports': 'off',
|
||||
'unused-imports/no-unused-vars': 'off',
|
||||
// Enforce JSX tags on separate lines to prevent Crowdin translation issues
|
||||
'@nx/workspace-mdx-component-newlines': 'error',
|
||||
// Disallow angle bracket placeholders to prevent Crowdin translation errors
|
||||
'@nx/workspace-no-angle-bracket-placeholders': 'error',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,787 +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',
|
||||
},
|
||||
],
|
||||
// Lingui - detect untranslated strings
|
||||
'lingui/no-unlocalized-strings': [
|
||||
'error',
|
||||
{
|
||||
ignore: [
|
||||
// Ignore strings which are a single "word" (no spaces) and don't start with uppercase
|
||||
'^(?![A-Z])\\S+$',
|
||||
// Ignore UPPERCASE literals (constants, env vars)
|
||||
'^[A-Z0-9_-]+$',
|
||||
// Ignore strings that look like code/technical (contain special chars)
|
||||
'^[\\s]*$', // whitespace only
|
||||
'.*[{}/<>].*', // contains code-like characters
|
||||
'^\\d+(\\.\\d+)?(px|rem|em|%|vh|vw|s|ms)?$', // CSS units
|
||||
'^[\\d.]+(px|rem|em|%|vh|vw|fr|s|ms)?(\\s+[\\d.]+(px|rem|em|%|vh|vw|fr|s|ms)?)*$', // CSS values like "200px 1fr 20px"
|
||||
'^#[0-9a-fA-F]{3,8}$', // hex colors
|
||||
'^rgba?\\(.*\\)$', // rgb/rgba colors
|
||||
'^(auto|none|inherit|initial|unset|flex|grid|block|inline|inline-block|relative|absolute|fixed|sticky)$', // CSS keywords
|
||||
'^color:.*$', // CSS color declarations
|
||||
'^font-.*$', // CSS font declarations
|
||||
'^\\d+$', // numbers only
|
||||
'^https?:\\/\\/.*', // URLs
|
||||
'^@.*', // @ mentions or decorators
|
||||
'^\\/.*', // paths starting with /
|
||||
'^[HhMmSsYyDdAaPp:.,\\s-]+$', // date format patterns (HH:mm, yyyy-MM-dd, etc.)
|
||||
'^Arrow(Up|Down|Left|Right)$', // keyboard keys
|
||||
'^(Enter|Escape|Tab|Space|Backspace|Delete)$', // keyboard keys
|
||||
'^Text$', // clipboard data type
|
||||
'^(allow-|sandbox)', // iframe sandbox values
|
||||
'^Id$', // technical identifier suffix (e.g., fieldNameId)
|
||||
'^(string|number|boolean|void|any|unknown|never|object)$', // TypeScript type keywords
|
||||
'^(Dark|Light)$', // color schemes
|
||||
'^translate\\(.*\\)$', // CSS transform strings
|
||||
'^svg .*$', // CSS selectors
|
||||
'^Icon[A-Z]\\w*$', // Icon names like IconDefault, IconTable, IconSettings
|
||||
'^\\w*Icon$', // Icon names that end with Icon like FieldIcon
|
||||
'^%c.*$', // Console format strings
|
||||
// Common item IDs for selectable lists
|
||||
'^(Group|CalendarView|CalendarDateField|Compact view)$',
|
||||
'^(Layout|Visibility|Fields|Delete view|Copy link to view|Create custom view)$',
|
||||
'^(GroupBy|Sort|HideEmptyGroups|HiddenGroups)$',
|
||||
// HTTP headers and auth (technical, not user-facing)
|
||||
'^Authorization$',
|
||||
'^Bearer .*',
|
||||
// Allow object keys that are technical identifiers
|
||||
'^(topLeft|topRight|bottomLeft|bottomRight)$',
|
||||
// Color schemes and CSS media queries
|
||||
'^System$',
|
||||
'^\\(prefers-color-scheme:',
|
||||
// GraphQL query names (used in refetchQueries)
|
||||
'^Get[A-Z]\\w*$',
|
||||
// React Context names (technical identifiers)
|
||||
'.*Context$',
|
||||
// SVG paths (geometric coordinates, not translatable)
|
||||
'^M[0-9 LML]+$',
|
||||
'^[ML][0-9 ]+$',
|
||||
// Database ordering values (technical, backend API)
|
||||
'^(Asc|Desc)Nulls(First|Last)$',
|
||||
// Calendar response status values (backend enum values, not user-facing)
|
||||
'^(Yes|No|Maybe)$',
|
||||
// Email validation error prefixes (combined with dynamic content)
|
||||
'^Invalid email(s)?:',
|
||||
// GraphQL type construction patterns
|
||||
'.*FilterInput$',
|
||||
'.*OrderByInput.*',
|
||||
'^\\$filter.*',
|
||||
'^\\$orderBy.*',
|
||||
'^\\$after.*',
|
||||
'^\\$before.*',
|
||||
'^\\$first.*',
|
||||
'^\\$last.*',
|
||||
// Logger names (technical identifiers)
|
||||
'^Twenty(-\\w+)?$',
|
||||
// Cookie names and cookie string patterns
|
||||
'^twenty_session_id$',
|
||||
'^; domain=',
|
||||
// Context names for createRequiredContext
|
||||
'^[A-Z][a-zA-Z]+$',
|
||||
// JSON-like filter patterns
|
||||
'^%"type":',
|
||||
'^%"objectNameSingular":',
|
||||
],
|
||||
ignoreNames: [
|
||||
// HTML/React attributes that shouldn't be translated
|
||||
{ regex: { pattern: 'className', flags: 'i' } },
|
||||
{ regex: { pattern: 'styleName', flags: 'i' } },
|
||||
{ regex: { pattern: 'testId', flags: 'i' } },
|
||||
'data-testid',
|
||||
'dataTestId',
|
||||
'src',
|
||||
'srcSet',
|
||||
'href',
|
||||
'target',
|
||||
'rel',
|
||||
'type',
|
||||
'id',
|
||||
'key',
|
||||
'name',
|
||||
'htmlFor',
|
||||
'width',
|
||||
'height',
|
||||
'fill',
|
||||
'stroke',
|
||||
'viewBox',
|
||||
'clipPath',
|
||||
'd', // SVG path
|
||||
'transform',
|
||||
'displayName',
|
||||
'defaultValue',
|
||||
'to', // router links
|
||||
'path',
|
||||
'pathname',
|
||||
'hash',
|
||||
'componentInstanceId',
|
||||
'hotkeyScope',
|
||||
'dropdownId',
|
||||
'recoilScopeId',
|
||||
'modalId',
|
||||
'dialogId',
|
||||
'itemId',
|
||||
'selectableItemIdArray',
|
||||
'listenerId',
|
||||
'focusId',
|
||||
'color', // color prop values
|
||||
'variant', // component variants
|
||||
'size', // size prop values
|
||||
'position', // position values
|
||||
'align', // alignment values
|
||||
'justify', // justification values
|
||||
'direction', // direction values
|
||||
'orientation', // orientation values
|
||||
'status', // status values
|
||||
'state', // state values
|
||||
'mode', // mode values
|
||||
'accent', // accent values
|
||||
|
||||
// CSS-related props
|
||||
'gridAutoColumns',
|
||||
'gridAutoRows',
|
||||
'gridTemplateColumns',
|
||||
'gridTemplateRows',
|
||||
'gridColumn',
|
||||
'gridRow',
|
||||
'gap',
|
||||
'margin',
|
||||
'padding',
|
||||
'border',
|
||||
'borderRadius',
|
||||
'boxShadow',
|
||||
'flex',
|
||||
'flexDirection',
|
||||
'flexWrap',
|
||||
'justifyContent',
|
||||
'alignItems',
|
||||
'alignContent',
|
||||
'overflow',
|
||||
'display',
|
||||
'cursor',
|
||||
'zIndex',
|
||||
'opacity',
|
||||
'fontWeight',
|
||||
'fontSize',
|
||||
'lineHeight',
|
||||
'textAlign',
|
||||
'textDecoration',
|
||||
'whiteSpace',
|
||||
'wordBreak',
|
||||
'objectFit',
|
||||
'backgroundSize',
|
||||
'backgroundPosition',
|
||||
'minWidth',
|
||||
'maxWidth',
|
||||
'minHeight',
|
||||
'maxHeight',
|
||||
'mobileGridAutoColumns',
|
||||
'tabletGridAutoColumns',
|
||||
|
||||
// Styled components
|
||||
'css',
|
||||
'theme',
|
||||
'animation',
|
||||
'transition',
|
||||
|
||||
// GraphQL
|
||||
'query',
|
||||
'mutation',
|
||||
'subscription',
|
||||
'fragment',
|
||||
'operationName',
|
||||
'variables',
|
||||
'__typename',
|
||||
|
||||
// Technical identifiers
|
||||
'fieldName',
|
||||
'columnName',
|
||||
'objectNameSingular',
|
||||
'objectNamePlural',
|
||||
'metadataId',
|
||||
'nameSingular',
|
||||
'namePlural',
|
||||
|
||||
// Event types
|
||||
'eventName',
|
||||
'event',
|
||||
'action',
|
||||
'actionType',
|
||||
|
||||
// Icon names
|
||||
'iconName',
|
||||
{ regex: { pattern: '^Icon[A-Z]' } },
|
||||
|
||||
// UPPER_CASE names (constants)
|
||||
{ regex: { pattern: '^[A-Z][A-Z0-9_]*$' } },
|
||||
|
||||
// Sort direction values (backend API)
|
||||
'orderBy',
|
||||
{ regex: { pattern: '^(Asc|Desc)(NullsFirst|NullsLast)?$' } },
|
||||
|
||||
// HTTP headers (technical, not user-facing)
|
||||
'Authorization',
|
||||
],
|
||||
ignoreFunctions: [
|
||||
// Console and logging
|
||||
'console.*',
|
||||
'*.log',
|
||||
'*.warn',
|
||||
'*.error',
|
||||
'*.debug',
|
||||
'*.info',
|
||||
'*.trace',
|
||||
'logDebug',
|
||||
'formatTitle',
|
||||
|
||||
// Error handling (technical messages, not user-facing)
|
||||
'Error',
|
||||
'TypeError',
|
||||
'RangeError',
|
||||
'SyntaxError',
|
||||
'throw',
|
||||
'assertUnreachable',
|
||||
'CustomError',
|
||||
'parseInitialBlocknote',
|
||||
|
||||
// Testing
|
||||
'describe',
|
||||
'it',
|
||||
'test',
|
||||
'expect',
|
||||
'jest.*',
|
||||
'*.toBe',
|
||||
'*.toEqual',
|
||||
'*.toContain',
|
||||
'*.toMatch',
|
||||
'*.toThrow',
|
||||
|
||||
// React/Libraries internals
|
||||
'require',
|
||||
'import',
|
||||
'styled',
|
||||
'styled.*',
|
||||
'css',
|
||||
'keyframes',
|
||||
'createGlobalStyle',
|
||||
|
||||
// Router
|
||||
'useNavigate',
|
||||
'navigate',
|
||||
'useLocation',
|
||||
'useParams',
|
||||
|
||||
// Date formatting (patterns are not translatable)
|
||||
'format',
|
||||
'formatDate',
|
||||
'formatDateTime',
|
||||
'formatTime',
|
||||
'parseISO',
|
||||
'parse',
|
||||
|
||||
// Navigation
|
||||
'useNavigationSection',
|
||||
|
||||
// Recoil
|
||||
'atom',
|
||||
'atomFamily',
|
||||
'selector',
|
||||
'selectorFamily',
|
||||
'useSetRecoilState',
|
||||
'useRecoilState',
|
||||
'useRecoilValue',
|
||||
|
||||
// GraphQL operations
|
||||
'gql',
|
||||
'useQuery',
|
||||
'useMutation',
|
||||
'useLazyQuery',
|
||||
'useSubscription',
|
||||
|
||||
// Type checking and validation
|
||||
'*.includes',
|
||||
'*.indexOf',
|
||||
'*.startsWith',
|
||||
'*.endsWith',
|
||||
'*.split',
|
||||
'*.join',
|
||||
'*.match',
|
||||
'*.replace',
|
||||
'*.test',
|
||||
'Object.keys',
|
||||
'Object.values',
|
||||
'Object.entries',
|
||||
'Array.isArray',
|
||||
|
||||
// DOM operations
|
||||
'*.getElementById',
|
||||
'*.getElementsByClassName',
|
||||
'*.querySelector',
|
||||
'*.querySelectorAll',
|
||||
'*.getAttribute',
|
||||
'*.setAttribute',
|
||||
'*.addEventListener',
|
||||
'*.removeEventListener',
|
||||
'*.dispatchEvent',
|
||||
'*.createElement',
|
||||
|
||||
// Storage
|
||||
'localStorage.*',
|
||||
'sessionStorage.*',
|
||||
'searchParams.*',
|
||||
'*.get',
|
||||
'*.set',
|
||||
'*.has',
|
||||
'*.delete',
|
||||
|
||||
// Misc utilities
|
||||
'cva',
|
||||
'cn',
|
||||
'clsx',
|
||||
'classNames',
|
||||
'track',
|
||||
'*.postMessage',
|
||||
'*.dispatch',
|
||||
'*.commit',
|
||||
|
||||
// Event handlers (typically receive enum values, not user-facing text)
|
||||
'onChange',
|
||||
'onClick',
|
||||
'onSelect',
|
||||
'onSubmit',
|
||||
'onFocus',
|
||||
'onBlur',
|
||||
'onKeyDown',
|
||||
'onKeyUp',
|
||||
'onMouseEnter',
|
||||
'onMouseLeave',
|
||||
|
||||
// Logging functions (technical messages, not user-facing)
|
||||
'logError',
|
||||
'logDebug',
|
||||
'logInfo',
|
||||
'logWarn',
|
||||
'loggerLink',
|
||||
|
||||
// Context creation (technical names)
|
||||
'createRequiredContext',
|
||||
|
||||
// GraphQL refetch queries (technical identifiers)
|
||||
'refetchQueries',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
// 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 and story-related files
|
||||
{
|
||||
files: [
|
||||
'**/*.stories.ts',
|
||||
'**/*.stories.tsx',
|
||||
'**/*.stories.js',
|
||||
'**/*.stories.jsx',
|
||||
'**/__stories__/**/*',
|
||||
],
|
||||
rules: {
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'lingui/no-unlocalized-strings': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
// Debug files - development only, not user-facing
|
||||
{
|
||||
files: [
|
||||
'**/Debug*.tsx',
|
||||
'**/*Debug*.tsx',
|
||||
'**/*DebugDisplay*.tsx',
|
||||
'**/*DebugHelper*.tsx',
|
||||
'**/*DebugObserver*.tsx',
|
||||
],
|
||||
rules: {
|
||||
'lingui/no-unlocalized-strings': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
// Testing utilities and mock data - not user-facing
|
||||
{
|
||||
files: [
|
||||
'**/testing/**/*.tsx',
|
||||
'**/testing/**/*.ts',
|
||||
'**/__mocks__/**/*',
|
||||
'**/*mock*.ts',
|
||||
'**/*Mock*.ts',
|
||||
'**/perf/**/*',
|
||||
],
|
||||
rules: {
|
||||
'lingui/no-unlocalized-strings': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
// Constants files - technical values, not user-facing
|
||||
{
|
||||
files: [
|
||||
'**/constants/**/*.ts',
|
||||
'**/*.constants.ts',
|
||||
'**/validation-schemas/**/*.ts',
|
||||
'**/*Schema.ts',
|
||||
'**/*-schema.ts',
|
||||
],
|
||||
rules: {
|
||||
'lingui/no-unlocalized-strings': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
// Layout configuration files - titles are translated at consumption time
|
||||
{
|
||||
files: ['**/layouts/**/*.ts'],
|
||||
rules: {
|
||||
'lingui/no-unlocalized-strings': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
// Service files - contain technical strings (logger names, HTTP headers, etc.)
|
||||
{
|
||||
files: ['**/services/**/*.ts'],
|
||||
rules: {
|
||||
'lingui/no-unlocalized-strings': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
// State files - contain technical default values
|
||||
{
|
||||
files: ['**/states/**/*.ts'],
|
||||
rules: {
|
||||
'lingui/no-unlocalized-strings': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
// Utility files - technical/developer-facing
|
||||
{
|
||||
files: [
|
||||
'**/utils/**/*.ts',
|
||||
'**/*Utils.ts',
|
||||
'**/*-utils.ts',
|
||||
'**/*Util.ts',
|
||||
'**/*-util.ts',
|
||||
'**/errors/**/*.ts',
|
||||
'**/*Error.ts',
|
||||
'**/*-error.ts',
|
||||
],
|
||||
rules: {
|
||||
'lingui/no-unlocalized-strings': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
// Config and setup files - not user-facing
|
||||
{
|
||||
files: [
|
||||
'**/*.config.ts',
|
||||
'**/*.config.js',
|
||||
'**/vite.config.ts',
|
||||
'**/.storybook/**/*',
|
||||
],
|
||||
rules: {
|
||||
'lingui/no-unlocalized-strings': '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',
|
||||
'**/*.test.tsx',
|
||||
'**/*.test.js',
|
||||
'**/*.test.jsx',
|
||||
'**/*.spec.ts',
|
||||
'**/*.spec.tsx',
|
||||
'**/*.spec.js',
|
||||
'**/*.spec.jsx',
|
||||
'**/__tests__/**/*.ts',
|
||||
'**/__tests__/**/*.tsx',
|
||||
'**/__mocks__/**/*.ts',
|
||||
'**/__mocks__/**/*.tsx',
|
||||
],
|
||||
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',
|
||||
'lingui/no-unlocalized-strings': '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,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,8 +1,4 @@
|
||||
{
|
||||
"workspaceLayout": {
|
||||
"appsDir": "packages",
|
||||
"libsDir": "packages"
|
||||
},
|
||||
"namedInputs": {
|
||||
"default": ["{projectRoot}/**/*"],
|
||||
"excludeStories": [
|
||||
@@ -36,7 +32,7 @@
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"start": {
|
||||
"cache": false,
|
||||
"cache": true,
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"lint": {
|
||||
@@ -44,17 +40,14 @@
|
||||
"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
|
||||
}
|
||||
"ci": { "cacheStrategy": "content" },
|
||||
"fix": { "fix": true }
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
@@ -70,12 +63,8 @@
|
||||
"write": false
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"cacheStrategy": "content"
|
||||
},
|
||||
"fix": {
|
||||
"write": true
|
||||
}
|
||||
"ci": { "cacheStrategy": "content" },
|
||||
"fix": { "write": true }
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
@@ -87,9 +76,7 @@
|
||||
"command": "tsc -b tsconfig.json --incremental"
|
||||
},
|
||||
"configurations": {
|
||||
"watch": {
|
||||
"watch": true
|
||||
}
|
||||
"watch": { "watch": true }
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
@@ -104,7 +91,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 +101,8 @@
|
||||
"ci": true,
|
||||
"maxWorkers": 3
|
||||
},
|
||||
"coverage": {
|
||||
"coverageReporters": ["lcov", "text"]
|
||||
},
|
||||
"watch": {
|
||||
"watch": true
|
||||
}
|
||||
"coverage": { "coverageReporters": ["lcov", "text"] },
|
||||
"watch": { "watch": true }
|
||||
}
|
||||
},
|
||||
"test:e2e": {
|
||||
@@ -133,7 +116,7 @@
|
||||
"outputs": ["{projectRoot}/{options.output-dir}"],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "VITE_DISABLE_TYPESCRIPT_CHECKER=true storybook build --test",
|
||||
"command": "VITE_DISABLE_TYPESCRIPT_CHECKER=true VITE_DISABLE_ESLINT_CHECKER=true storybook build --test",
|
||||
"output-dir": "storybook-static",
|
||||
"config-dir": ".storybook"
|
||||
},
|
||||
@@ -209,9 +192,7 @@
|
||||
"checkCoverage": true
|
||||
},
|
||||
"configurations": {
|
||||
"text": {
|
||||
"reporter": "text"
|
||||
}
|
||||
"text": { "reporter": "text" }
|
||||
}
|
||||
},
|
||||
"storybook:serve-and-test:static": {
|
||||
@@ -234,7 +215,7 @@
|
||||
"command": "nx storybook:build {projectName}",
|
||||
"forwardAllArgs": false
|
||||
},
|
||||
"chromatic --storybook-build-dir=storybook-static {args.ci}"
|
||||
"cross-var chromatic --project-token=$CHROMATIC_PROJECT_TOKEN --storybook-build-dir=storybook-static {args.ci}"
|
||||
],
|
||||
"parallel": false
|
||||
},
|
||||
@@ -265,7 +246,7 @@
|
||||
"cache": true,
|
||||
"inputs": [
|
||||
"default",
|
||||
"{workspaceRoot}/eslint.config.mjs",
|
||||
"{workspaceRoot}/.eslintrc.js",
|
||||
"{workspaceRoot}/tools/eslint-rules/**/*"
|
||||
]
|
||||
},
|
||||
@@ -280,7 +261,7 @@
|
||||
}
|
||||
},
|
||||
"installation": {
|
||||
"version": "22.0.3"
|
||||
"version": "18.3.3"
|
||||
},
|
||||
"generators": {
|
||||
"@nx/react": {
|
||||
@@ -313,8 +294,5 @@
|
||||
}
|
||||
},
|
||||
"useInferencePlugins": false,
|
||||
"defaultBase": "main",
|
||||
"tui": {
|
||||
"enabled": false
|
||||
}
|
||||
"defaultBase": "main"
|
||||
}
|
||||
|
||||
+215
-65
@@ -2,31 +2,123 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.7.17",
|
||||
"@date-fns/tz": "^1.4.1",
|
||||
"@apollo/server": "^4.7.3",
|
||||
"@aws-sdk/client-lambda": "^3.700.0",
|
||||
"@aws-sdk/client-s3": "^3.700.0",
|
||||
"@aws-sdk/client-sts": "^3.700.0",
|
||||
"@aws-sdk/credential-providers": "^3.700.0",
|
||||
"@calcom/embed-react": "^1.5.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",
|
||||
"@radix-ui/colors": "^3.0.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": "^11.0.0",
|
||||
"@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.35",
|
||||
"@react-email/render": "0.0.17",
|
||||
"@sentry/node": "^9.26.0",
|
||||
"@sentry/profiling-node": "^9.26.0",
|
||||
"@sentry/react": "^9.26.0",
|
||||
"@sniptt/guards": "^0.2.0",
|
||||
"@stoplight/elements": "^8.0.5",
|
||||
"@swc/jest": "^0.2.29",
|
||||
"@tabler/icons-react": "^3.31.0",
|
||||
"@wyw-in-js/vite": "^0.7.0",
|
||||
"@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",
|
||||
"@xyflow/react": "^12.4.2",
|
||||
"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",
|
||||
"body-parser": "^1.20.2",
|
||||
"bullmq": "^5.40.0",
|
||||
"bytes": "^3.1.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cron-parser": "^5.1.1",
|
||||
"cron-validate": "^1.4.5",
|
||||
"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",
|
||||
"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-redis-subscriptions": "^2.7.0",
|
||||
"graphql-scalars": "^1.23.0",
|
||||
"graphql-sse": "^2.5.4",
|
||||
"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",
|
||||
"imapflow": "^1.0.186",
|
||||
"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.debounce": "^4.0.8",
|
||||
"lodash.escaperegexp": "^4.1.2",
|
||||
"lodash.groupby": "^4.6.0",
|
||||
"lodash.identity": "^3.0.0",
|
||||
@@ -37,85 +129,136 @@
|
||||
"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",
|
||||
"mailparser": "^3.7.3",
|
||||
"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",
|
||||
"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.15.1",
|
||||
"react-loading-skeleton": "^3.3.1",
|
||||
"react-phone-number-input": "patch:react-phone-number-input@npm%3A3.4.5#~/.yarn/patches/react-phone-number-input-npm-3.4.5-dc2895c306.patch",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react-router-dom": "^6.4.4",
|
||||
"react-textarea-autosize": "^8.4.1",
|
||||
"react-tooltip": "^5.13.1",
|
||||
"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": "^17.3.1",
|
||||
"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-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": "22.0.3",
|
||||
"@nx/eslint-plugin": "22.0.3",
|
||||
"@nx/jest": "22.0.3",
|
||||
"@nx/js": "22.0.3",
|
||||
"@nx/react": "22.0.3",
|
||||
"@nx/storybook": "22.0.3",
|
||||
"@nx/vite": "22.0.3",
|
||||
"@nx/web": "22.0.3",
|
||||
"@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",
|
||||
"@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.7.42",
|
||||
"@swc/helpers": "~0.5.2",
|
||||
"@swc/jest": "^0.2.39",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@types/addressparser": "^1.0.3",
|
||||
"@types/apollo-upload-client": "^17.0.2",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@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/file-saver": "^2.0.7",
|
||||
"@types/graphql-fields": "^1.3.6",
|
||||
"@types/inquirer": "^9.0.9",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/graphql-upload": "^8.0.12",
|
||||
"@types/imapflow": "^1.0.21",
|
||||
"@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.debounce": "^4.0.7",
|
||||
"@types/lodash.escaperegexp": "^4.1.9",
|
||||
"@types/lodash.groupby": "^4.6.9",
|
||||
"@types/lodash.identity": "^3.0.9",
|
||||
@@ -128,42 +271,48 @@
|
||||
"@types/lodash.pickby": "^4.6.9",
|
||||
"@types/lodash.snakecase": "^4.1.7",
|
||||
"@types/lodash.upperfirst": "^4.3.7",
|
||||
"@types/luxon": "^3.3.0",
|
||||
"@types/mailparser": "^3.4.6",
|
||||
"@types/ms": "^0.7.31",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"@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",
|
||||
"danger": "^13.0.4",
|
||||
"dotenv-cli": "^7.4.4",
|
||||
"esbuild": "^0.25.10",
|
||||
"eslint": "^9.32.0",
|
||||
"cross-var": "^1.1.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-import": "2.29.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||
"eslint-plugin-lingui": "^0.9.0",
|
||||
"eslint-plugin-mdx": "^3.6.2",
|
||||
"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",
|
||||
@@ -172,35 +321,42 @@
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"jsdom": "~22.1.0",
|
||||
"msw": "^2.0.11",
|
||||
"msw-storybook-addon": "^2.0.5",
|
||||
"nx": "22.0.3",
|
||||
"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": "^6.3.5",
|
||||
"vite-plugin-checker": "^0.6.2",
|
||||
"vite-plugin-cjs-interop": "^2.2.0",
|
||||
"vite-plugin-dts": "3.8.1",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vitest": "1.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
"node": "^22.12.0",
|
||||
"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",
|
||||
"typescript": "5.3.3",
|
||||
"graphql-redis-subscriptions/ioredis": "^5.6.0",
|
||||
"prosemirror-view": "1.40.0",
|
||||
"prosemirror-transform": "1.10.4"
|
||||
@@ -208,12 +364,11 @@
|
||||
"version": "0.2.1",
|
||||
"nx": {},
|
||||
"scripts": {
|
||||
"docs:generate": "tsx packages/twenty-docs/scripts/generate-docs-json.ts",
|
||||
"docs:generate-navigation-template": "tsx packages/twenty-docs/scripts/generate-navigation-template.ts",
|
||||
"start": "npx concurrently --kill-others 'npx nx run-many -t start -p twenty-server twenty-front' 'npx wait-on tcp:3000 && npx nx run twenty-server:worker'"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"packages/twenty-chrome-extension",
|
||||
"packages/twenty-front",
|
||||
"packages/twenty-server",
|
||||
"packages/twenty-emails",
|
||||
@@ -221,13 +376,8 @@
|
||||
"packages/twenty-utils",
|
||||
"packages/twenty-zapier",
|
||||
"packages/twenty-website",
|
||||
"packages/twenty-docs",
|
||||
"packages/twenty-e2e-testing",
|
||||
"packages/twenty-shared",
|
||||
"packages/twenty-sdk",
|
||||
"packages/twenty-apps",
|
||||
"packages/twenty-cli",
|
||||
"packages/create-twenty-app",
|
||||
"tools/eslint-rules"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
<div align="center">
|
||||
<a href="https://twenty.com">
|
||||
<picture>
|
||||
<img alt="Twenty logo" src="https://raw.githubusercontent.com/twentyhq/twenty/2f25922f4cd5bd61e1427c57c4f8ea224e1d552c/packages/twenty-website/public/images/core/logo.svg" height="128">
|
||||
</picture>
|
||||
</a>
|
||||
<h1>Create Twenty App</h1>
|
||||
|
||||
<a href="https://www.npmjs.com/package/create-twenty-app"><img alt="NPM version" src="https://img.shields.io/npm/v/create-twenty-app.svg?style=for-the-badge&labelColor=000000"></a>
|
||||
<a href="https://github.com/twentyhq/twenty/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/npm/l/next.svg?style=for-the-badge&labelColor=000000"></a>
|
||||
<a href="https://discord.gg/cx5n4Jzs57"><img alt="Join the community on Discord" src="https://img.shields.io/badge/Join%20the%20community-blueviolet.svg?style=for-the-badge&logo=Twenty&labelColor=000000&logoWidth=20"></a>
|
||||
|
||||
</div>
|
||||
|
||||
Create Twenty App is the official scaffolding CLI for building apps on top of [Twenty CRM](https://twenty.com). It sets up a ready‑to‑run project that works seamlessly with the [twenty-sdk](https://www.npmjs.com/package/twenty-sdk).
|
||||
|
||||
- Zero‑config project bootstrap
|
||||
- Preconfigured scripts for auth, generate, dev sync, one‑off sync, uninstall
|
||||
- Strong TypeScript support and typed client generation
|
||||
|
||||
## Prerequisites
|
||||
- Node.js 24+ (recommended) and Yarn 4
|
||||
- A Twenty workspace and an API key (create one at https://app.twenty.com/settings/api-webhooks)
|
||||
|
||||
## Quick start
|
||||
```bash
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# Authenticate using your API key (you'll be prompted)
|
||||
yarn auth
|
||||
|
||||
# Add a new entity to your application (guided)
|
||||
yarn create-entity
|
||||
|
||||
# Generate a typed Twenty client and workspace entity types
|
||||
yarn generate
|
||||
|
||||
# Start dev mode: automatically syncs local changes to your workspace
|
||||
yarn dev
|
||||
|
||||
# Or run a one‑time sync
|
||||
yarn sync
|
||||
|
||||
# Watch your application's functions logs
|
||||
yarn logs
|
||||
|
||||
# Uninstall the application from the current workspace
|
||||
yarn uninstall
|
||||
|
||||
# Display commands' help
|
||||
yarn help
|
||||
```
|
||||
|
||||
## What gets scaffolded
|
||||
- A minimal app structure ready for Twenty
|
||||
- TypeScript configuration
|
||||
- Prewired scripts that wrap the `twenty` CLI from twenty-sdk
|
||||
- Example placeholders to help you add entities, actions, and sync logic
|
||||
|
||||
## Next steps
|
||||
- Explore the generated project and add your first entity with `yarn create-entity`.
|
||||
- Keep your types up‑to‑date using `yarn generate`.
|
||||
- Use `yarn dev` while you iterate to see changes instantly in your workspace.
|
||||
|
||||
|
||||
## Publish your application
|
||||
Applications are currently stored in `twenty/packages/twenty-apps`.
|
||||
|
||||
You can share your application with all Twenty users:
|
||||
|
||||
```bash
|
||||
# pull the Twenty project
|
||||
git clone https://github.com/twentyhq/twenty.git
|
||||
cd twenty
|
||||
|
||||
# create a new branch
|
||||
git checkout -b feature/my-awesome-app
|
||||
```
|
||||
|
||||
- Copy your app folder into `twenty/packages/twenty-apps`.
|
||||
- Commit your changes and open a pull request on https://github.com/twentyhq/twenty
|
||||
|
||||
```bash
|
||||
git commit -m "Add new application"
|
||||
git push
|
||||
```
|
||||
|
||||
Our team reviews contributions for quality, security, and reusability before merging.
|
||||
|
||||
## Troubleshooting
|
||||
- Auth prompts not appearing: run `yarn auth` again and verify the API key permissions.
|
||||
- Types not generated: ensure `yarn generate` runs without errors, then re‑start `yarn dev`.
|
||||
|
||||
## Contributing
|
||||
- See our [GitHub](https://github.com/twentyhq/twenty)
|
||||
- Join our [Discord](https://discord.gg/cx5n4Jzs57)
|
||||
@@ -1,111 +0,0 @@
|
||||
import js from '@eslint/js';
|
||||
import typescriptEslint from '@typescript-eslint/eslint-plugin';
|
||||
import typescriptParser from '@typescript-eslint/parser';
|
||||
import prettierPlugin from 'eslint-plugin-prettier';
|
||||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
languageOptions: {
|
||||
parser: typescriptParser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
},
|
||||
globals: {
|
||||
// Node.js globals
|
||||
process: 'readonly',
|
||||
console: 'readonly',
|
||||
Buffer: 'readonly',
|
||||
__dirname: 'readonly',
|
||||
__filename: 'readonly',
|
||||
global: 'readonly',
|
||||
setTimeout: 'readonly',
|
||||
clearTimeout: 'readonly',
|
||||
setInterval: 'readonly',
|
||||
clearInterval: 'readonly',
|
||||
// Browser globals that Node.js also has
|
||||
URL: 'readonly',
|
||||
URLSearchParams: 'readonly',
|
||||
// Node.js types
|
||||
NodeJS: 'readonly',
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': typescriptEslint,
|
||||
prettier: prettierPlugin,
|
||||
},
|
||||
rules: {
|
||||
...typescriptEslint.configs.recommended.rules,
|
||||
'prettier/prettier': 'error',
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'no-useless-escape': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.js'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
globals: {
|
||||
process: 'readonly',
|
||||
console: 'readonly',
|
||||
Buffer: 'readonly',
|
||||
__dirname: 'readonly',
|
||||
__filename: 'readonly',
|
||||
global: 'readonly',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.test.ts', '**/*.spec.ts', '**/__tests__/**/*.ts'],
|
||||
languageOptions: {
|
||||
parser: typescriptParser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
},
|
||||
globals: {
|
||||
// Node.js globals
|
||||
process: 'readonly',
|
||||
console: 'readonly',
|
||||
Buffer: 'readonly',
|
||||
__dirname: 'readonly',
|
||||
__filename: 'readonly',
|
||||
global: 'readonly',
|
||||
// Jest globals
|
||||
describe: 'readonly',
|
||||
it: 'readonly',
|
||||
test: 'readonly',
|
||||
expect: 'readonly',
|
||||
jest: 'readonly',
|
||||
beforeEach: 'readonly',
|
||||
afterEach: 'readonly',
|
||||
beforeAll: 'readonly',
|
||||
afterAll: 'readonly',
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': typescriptEslint,
|
||||
prettier: prettierPlugin,
|
||||
},
|
||||
rules: {
|
||||
...typescriptEslint.configs.recommended.rules,
|
||||
'prettier/prettier': 'error',
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'no-useless-escape': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ['dist/**', 'node_modules/**'],
|
||||
},
|
||||
];
|
||||
@@ -1,40 +0,0 @@
|
||||
const jestConfig = {
|
||||
displayName: 'twenty-cli',
|
||||
preset: '../../jest.preset.js',
|
||||
testEnvironment: 'node',
|
||||
transformIgnorePatterns: ['../../node_modules/'],
|
||||
transform: {
|
||||
'^.+\\.[tj]sx?$': [
|
||||
'@swc/jest',
|
||||
{
|
||||
jsc: {
|
||||
parser: { syntax: 'typescript', tsx: false },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/src/$1',
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'js'],
|
||||
extensionsToTreatAsEsm: ['.ts'],
|
||||
coverageDirectory: './coverage',
|
||||
testMatch: [
|
||||
'<rootDir>/src/**/__tests__/**/*.(test|spec).{js,ts}',
|
||||
'<rootDir>/src/**/?(*.)(test|spec).{js,ts}',
|
||||
],
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.{ts,js}',
|
||||
'!src/**/*.d.ts',
|
||||
'!src/cli.ts', // Exclude CLI entry point from coverage
|
||||
],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
statements: 1,
|
||||
lines: 1,
|
||||
functions: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default jestConfig;
|
||||
@@ -1,54 +0,0 @@
|
||||
{
|
||||
"name": "create-twenty-app",
|
||||
"version": "0.2.2",
|
||||
"description": "Command-line interface to create Twenty application",
|
||||
"main": "dist/cli.cjs",
|
||||
"bin": "dist/cli.cjs",
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "npx rimraf dist && npx vite build"
|
||||
},
|
||||
"keywords": [
|
||||
"twenty",
|
||||
"cli",
|
||||
"crm",
|
||||
"application",
|
||||
"development"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/cli.d.ts",
|
||||
"import": "./dist/cli.mjs",
|
||||
"require": "./dist/cli.cjs"
|
||||
}
|
||||
},
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@genql/cli": "^3.0.3",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.0.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"inquirer": "^10.0.0",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"lodash.kebabcase": "^4.1.1",
|
||||
"lodash.startcase": "^4.4.0",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^11.0.0",
|
||||
"@types/inquirer": "^9.0.0",
|
||||
"@types/lodash.camelcase": "^4.3.7",
|
||||
"@types/lodash.kebabcase": "^4.1.7",
|
||||
"@types/lodash.startcase": "^4",
|
||||
"@types/node": "^20.0.0",
|
||||
"vite": "^7.0.0",
|
||||
"vite-plugin-dts": "^4.5.4",
|
||||
"vite-tsconfig-paths": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
"yarn": "^4.0.2"
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
{
|
||||
"name": "create-twenty-app",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "library",
|
||||
"tags": ["scope:create-app"],
|
||||
"targets": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["{projectRoot}/dist"]
|
||||
},
|
||||
"dev": {
|
||||
"executor": "nx:run-commands",
|
||||
"dependsOn": ["build"],
|
||||
"options": {
|
||||
"cwd": "packages/create-twenty-app",
|
||||
"command": "tsx src/cli.ts"
|
||||
}
|
||||
},
|
||||
"start": {
|
||||
"executor": "nx:run-commands",
|
||||
"dependsOn": ["build"],
|
||||
"options": {
|
||||
"cwd": "packages/create-twenty-app",
|
||||
"command": "node dist/cli.cjs"
|
||||
}
|
||||
},
|
||||
"typecheck": {},
|
||||
"lint": {
|
||||
"options": {
|
||||
"lintFilePatterns": ["{projectRoot}/src/**/*.{ts,json}"],
|
||||
"maxWarnings": 0
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"lintFilePatterns": ["{projectRoot}/src/**/*.{ts,json}"],
|
||||
"maxWarnings": 0
|
||||
},
|
||||
"fix": {}
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "{projectRoot}/jest.config.mjs"
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"ci": true,
|
||||
"coverage": true,
|
||||
"watchAll": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import chalk from 'chalk';
|
||||
import { Command, CommanderError } from 'commander';
|
||||
import { CreateAppCommand } from '@/create-app.command';
|
||||
import packageJson from '../package.json';
|
||||
|
||||
const program = new Command(packageJson.name)
|
||||
.description('CLI tool to initialize a new Twenty application')
|
||||
.version(
|
||||
packageJson.version,
|
||||
'-v, --version',
|
||||
'Output the current version of create-twenty-app.',
|
||||
)
|
||||
.argument('[directory]')
|
||||
.helpOption('-h, --help', 'Display this help message.')
|
||||
.action(async (directory?: string) => {
|
||||
if (directory && !/^[a-z0-9-]+$/.test(directory)) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
`Invalid directory "${directory}". Must contain only lowercase letters, numbers, and hyphens`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
await new CreateAppCommand().execute(directory);
|
||||
});
|
||||
|
||||
program.exitOverride();
|
||||
|
||||
try {
|
||||
program.parse();
|
||||
} catch (error) {
|
||||
if (error instanceof CommanderError) {
|
||||
process.exit(error.exitCode);
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
console.error(chalk.red('Error:'), error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
24.5.0
|
||||
Binary file not shown.
-942
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,27 +0,0 @@
|
||||
This is a [Twenty](https://twenty.com) application project bootstrapped with [`create-twenty-app`](https://www.npmjs.com/package/create-twenty-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, authenticate to your workspace:
|
||||
|
||||
```bash
|
||||
yarn auth
|
||||
```
|
||||
|
||||
Then, install this app to your workspace:
|
||||
|
||||
```bash
|
||||
yarn sync
|
||||
```
|
||||
|
||||
Open your Twenty instance and go to `/settings/applications` section to see the result.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Twenty applications, take a look at the following resources:
|
||||
|
||||
- [twenty-sdk](https://www.npmjs.com/package/twenty-sdk) - learn about `twenty-sdk` tool.
|
||||
- [Twenty doc](https://docs.twenty.com/) - Twenty's documentation.
|
||||
- Join our [Discord](https://discord.gg/cx5n4Jzs57)
|
||||
|
||||
You can check out [the Twenty GitHub repository](https://github.com/twentyhq/twenty) - your feedback and contributions are welcome!
|
||||
@@ -1,137 +0,0 @@
|
||||
import js from '@eslint/js';
|
||||
import typescriptEslint from '@typescript-eslint/eslint-plugin';
|
||||
import typescriptParser from '@typescript-eslint/parser';
|
||||
import importPlugin from 'eslint-plugin-import';
|
||||
import preferArrowPlugin from 'eslint-plugin-prefer-arrow';
|
||||
import prettierPlugin from 'eslint-plugin-prettier';
|
||||
import unusedImportsPlugin from 'eslint-plugin-unused-imports';
|
||||
|
||||
export default [
|
||||
// Base JS rules
|
||||
js.configs.recommended,
|
||||
|
||||
// Global ignores
|
||||
{
|
||||
ignores: ['**/node_modules/**', '**/dist/**', '**/coverage/**'],
|
||||
},
|
||||
|
||||
// Base config for TS/JS files
|
||||
{
|
||||
files: ['**/*.{js,jsx,ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: {
|
||||
prettier: prettierPlugin,
|
||||
import: importPlugin,
|
||||
'prefer-arrow': preferArrowPlugin,
|
||||
'unused-imports': unusedImportsPlugin,
|
||||
},
|
||||
rules: {
|
||||
// General rules (aligned with main project)
|
||||
'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',
|
||||
|
||||
// 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: '^_',
|
||||
},
|
||||
],
|
||||
|
||||
// Prettier (formatting as lint errors if you want)
|
||||
'prettier/prettier': 'error',
|
||||
},
|
||||
},
|
||||
|
||||
// TypeScript-specific configuration
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
parser: typescriptParser,
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': typescriptEslint,
|
||||
},
|
||||
rules: {
|
||||
// Turn off base rule and use TS-aware versions
|
||||
'no-redeclare': 'off',
|
||||
'@typescript-eslint/no-redeclare': 'error',
|
||||
|
||||
'@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',
|
||||
},
|
||||
},
|
||||
|
||||
// Test files (Jest)
|
||||
{
|
||||
files: ['**/*.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',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"allowUnreachableCode": false,
|
||||
"strict": true,
|
||||
"alwaysStrict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictBindCallApply": false,
|
||||
"target": "es2018",
|
||||
"module": "esnext",
|
||||
"lib": ["es2020", "dom"],
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
import chalk from 'chalk';
|
||||
import * as fs from 'fs-extra';
|
||||
import inquirer from 'inquirer';
|
||||
import * as path from 'path';
|
||||
import { copyBaseApplicationProject } from '@/utils/app-template';
|
||||
import kebabCase from 'lodash.kebabcase';
|
||||
import { convertToLabel } from '@/utils/convert-to-label';
|
||||
import { tryGitInit } from '@/utils/try-git-init';
|
||||
import { install } from '@/utils/install';
|
||||
|
||||
const CURRENT_EXECUTION_DIRECTORY = process.env.INIT_CWD || process.cwd();
|
||||
|
||||
export class CreateAppCommand {
|
||||
async execute(directory?: string): Promise<void> {
|
||||
try {
|
||||
const { appName, appDisplayName, appDirectory, appDescription } =
|
||||
await this.getAppInfos(directory);
|
||||
|
||||
await this.validateDirectory(appDirectory);
|
||||
|
||||
this.logCreationInfo({ appDirectory, appName });
|
||||
|
||||
await fs.ensureDir(appDirectory);
|
||||
|
||||
await copyBaseApplicationProject({
|
||||
appName,
|
||||
appDisplayName,
|
||||
appDescription,
|
||||
appDirectory,
|
||||
});
|
||||
|
||||
await install(appDirectory);
|
||||
|
||||
await tryGitInit(appDirectory);
|
||||
|
||||
this.logSuccess(appDirectory);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('Initialization failed:'),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private async getAppInfos(directory?: string): Promise<{
|
||||
appName: string;
|
||||
appDisplayName: string;
|
||||
appDescription: string;
|
||||
appDirectory: string;
|
||||
}> {
|
||||
const { name, displayName, description } = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'name',
|
||||
message: 'Application name:',
|
||||
when: () => !directory,
|
||||
default: 'my-awesome-app',
|
||||
validate: (input) => {
|
||||
if (input.length === 0) return 'Application name is required';
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'displayName',
|
||||
message: 'Application display name:',
|
||||
default: (answers: any) => {
|
||||
return convertToLabel(answers?.name ?? directory);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'description',
|
||||
message: 'Application description (optional):',
|
||||
default: '',
|
||||
},
|
||||
]);
|
||||
|
||||
const computedName = name ?? directory;
|
||||
|
||||
const appName = computedName.trim();
|
||||
|
||||
const appDisplayName = displayName.trim();
|
||||
|
||||
const appDescription = description.trim();
|
||||
|
||||
const appDirectory = directory
|
||||
? path.join(CURRENT_EXECUTION_DIRECTORY, directory)
|
||||
: path.join(CURRENT_EXECUTION_DIRECTORY, kebabCase(appName));
|
||||
|
||||
return { appName, appDisplayName, appDirectory, appDescription };
|
||||
}
|
||||
|
||||
private async validateDirectory(appDirectory: string): Promise<void> {
|
||||
if (!(await fs.pathExists(appDirectory))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = await fs.readdir(appDirectory);
|
||||
if (files.length > 0) {
|
||||
throw new Error(
|
||||
`Directory ${appDirectory} already exists and is not empty`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private logCreationInfo({
|
||||
appDirectory,
|
||||
appName,
|
||||
}: {
|
||||
appDirectory: string;
|
||||
appName: string;
|
||||
}): void {
|
||||
console.log(chalk.blue('🎯 Creating Twenty Application'));
|
||||
console.log(chalk.gray(`📁 Directory: ${appDirectory}`));
|
||||
console.log(chalk.gray(`📝 Name: ${appName}`));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
private logSuccess(appDirectory: string): void {
|
||||
console.log(chalk.green('✅ Application created!'));
|
||||
console.log('');
|
||||
console.log(chalk.blue('Next steps:'));
|
||||
console.log(`cd ${appDirectory.split('/').reverse()[0] ?? ''}`);
|
||||
console.log('yarn auth');
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { convertToLabel } from '@/utils/convert-to-label';
|
||||
|
||||
describe('convertToLabel', () => {
|
||||
it('should convert to label', () => {
|
||||
expect(convertToLabel('toto')).toBe('Toto');
|
||||
expect(convertToLabel('totoTata')).toBe('Toto tata');
|
||||
expect(convertToLabel('totoTataTiti')).toBe('Toto tata titi');
|
||||
expect(convertToLabel('toto-tata-titi')).toBe('Toto tata titi');
|
||||
});
|
||||
});
|
||||
@@ -1,143 +0,0 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export const copyBaseApplicationProject = async ({
|
||||
appName,
|
||||
appDisplayName,
|
||||
appDescription,
|
||||
appDirectory,
|
||||
}: {
|
||||
appName: string;
|
||||
appDisplayName: string;
|
||||
appDescription: string;
|
||||
appDirectory: string;
|
||||
}) => {
|
||||
await fs.copy(join(__dirname, './constants/base-application'), appDirectory);
|
||||
|
||||
await createPackageJson({ appName, appDirectory });
|
||||
|
||||
await createGitignore(appDirectory);
|
||||
|
||||
await createYarnLock(appDirectory);
|
||||
|
||||
await createApplicationConfig({
|
||||
displayName: appDisplayName,
|
||||
description: appDescription,
|
||||
appDirectory,
|
||||
});
|
||||
};
|
||||
|
||||
const createYarnLock = async (appDirectory: string) => {
|
||||
const yarnLockContent = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
`;
|
||||
|
||||
await fs.writeFile(join(appDirectory, 'yarn.lock'), yarnLockContent);
|
||||
};
|
||||
const createGitignore = async (appDirectory: string) => {
|
||||
const gitignoreContent = `# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn
|
||||
|
||||
# codegen
|
||||
generated
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# dev
|
||||
/dist/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
`;
|
||||
|
||||
await fs.writeFile(join(appDirectory, '.gitignore'), gitignoreContent);
|
||||
};
|
||||
|
||||
const createApplicationConfig = async ({
|
||||
displayName,
|
||||
description,
|
||||
appDirectory,
|
||||
}: {
|
||||
displayName: string;
|
||||
description?: string;
|
||||
appDirectory: string;
|
||||
}) => {
|
||||
const content = `import { type ApplicationConfig } from 'twenty-sdk';
|
||||
|
||||
const config: ApplicationConfig = {
|
||||
universalIdentifier: '${v4()}',
|
||||
displayName: '${displayName}',
|
||||
description: '${description ?? ''}',
|
||||
};
|
||||
|
||||
export default config;
|
||||
`;
|
||||
|
||||
await fs.writeFile(join(appDirectory, 'application.config.ts'), content);
|
||||
};
|
||||
|
||||
const createPackageJson = async ({
|
||||
appName,
|
||||
appDirectory,
|
||||
}: {
|
||||
appName: string;
|
||||
appDirectory: string;
|
||||
}) => {
|
||||
const packageJson = {
|
||||
name: appName,
|
||||
version: '0.1.0',
|
||||
license: 'MIT',
|
||||
engines: {
|
||||
node: '^24.5.0',
|
||||
npm: 'please-use-yarn',
|
||||
yarn: '>=4.0.2',
|
||||
},
|
||||
packageManager: 'yarn@4.9.2',
|
||||
scripts: {
|
||||
'create-entity': 'twenty app add',
|
||||
dev: 'twenty app dev',
|
||||
generate: 'twenty app generate',
|
||||
sync: 'twenty app sync',
|
||||
logs: 'twenty app logs',
|
||||
uninstall: 'twenty app uninstall',
|
||||
help: 'twenty help',
|
||||
auth: 'twenty auth login',
|
||||
},
|
||||
dependencies: {
|
||||
'twenty-sdk': '0.2.2',
|
||||
},
|
||||
devDependencies: {
|
||||
'@types/node': '^24.7.2',
|
||||
typescript: '^5.9.3',
|
||||
},
|
||||
};
|
||||
|
||||
await fs.writeFile(
|
||||
join(appDirectory, 'package.json'),
|
||||
JSON.stringify(packageJson, null, 2),
|
||||
'utf8',
|
||||
);
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
import { startCase } from 'lodash';
|
||||
|
||||
export const convertToLabel = (str: string) => {
|
||||
const s = startCase(str).toLowerCase();
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
import chalk from 'chalk';
|
||||
import { promisify } from 'util';
|
||||
import { exec } from 'child_process';
|
||||
|
||||
const execPromise = promisify(exec);
|
||||
|
||||
export const install = async (root: string) => {
|
||||
try {
|
||||
await execPromise('yarn', { cwd: root });
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red('yarn install failed:'), error.stdout);
|
||||
}
|
||||
};
|
||||
@@ -1,70 +0,0 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
import { promisify } from 'util';
|
||||
import { exec } from 'child_process';
|
||||
|
||||
const execPromise = promisify(exec);
|
||||
|
||||
const isInGitRepository = async (root: string): Promise<boolean> => {
|
||||
try {
|
||||
await execPromise('git rev-parse --is-inside-work-tree', { cwd: root });
|
||||
return true;
|
||||
} catch {
|
||||
// Empty catch block
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const isInMercurialRepository = async (root: string): Promise<boolean> => {
|
||||
try {
|
||||
await execPromise('hg --cwd . root', { cwd: root });
|
||||
return true;
|
||||
} catch {
|
||||
// Empty catch block
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const isDefaultBranchSet = async (root: string): Promise<boolean> => {
|
||||
try {
|
||||
await execPromise('git config init.defaultBranch', { cwd: root });
|
||||
return true;
|
||||
} catch {
|
||||
// Empty catch block
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const tryGitInit = async (root: string): Promise<boolean> => {
|
||||
try {
|
||||
await execPromise('git --version', { cwd: root });
|
||||
|
||||
if (
|
||||
(await isInGitRepository(root)) ||
|
||||
(await isInMercurialRepository(root))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
await execPromise('git init', { cwd: root });
|
||||
|
||||
try {
|
||||
if (!(await isDefaultBranchSet(root))) {
|
||||
await execPromise('git checkout -b main', { cwd: root });
|
||||
}
|
||||
|
||||
await execPromise('git add -A', { cwd: root });
|
||||
await execPromise(
|
||||
'git commit -m "Initial commit from Create Twenty App"',
|
||||
{
|
||||
cwd: root,
|
||||
},
|
||||
);
|
||||
return true;
|
||||
} catch {
|
||||
fs.rm(join(root, '.git'), { recursive: true, force: true });
|
||||
return false;
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": false,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strictNullChecks": true,
|
||||
"alwaysStrict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictBindCallApply": false,
|
||||
"noEmit": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
],
|
||||
"extends": "../../tsconfig.base.json"
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": false,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/__tests__/**"
|
||||
]
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": [
|
||||
"**/__mocks__/**/*",
|
||||
"vite.config.ts",
|
||||
"jest.config.mjs",
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.test.tsx"
|
||||
]
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { defineConfig } from 'vite';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import packageJson from './package.json';
|
||||
|
||||
const moduleEntries = Object.keys((packageJson as any).exports || {})
|
||||
.filter(
|
||||
(key) => key !== './style.css' && key !== '.' && !key.startsWith('./src/'),
|
||||
)
|
||||
.map((module) => `src/${module.replace(/^\.\//, '')}/index.ts`);
|
||||
|
||||
const entries = ['src/cli.ts', ...moduleEntries];
|
||||
|
||||
const entryFileNames = (chunk: any, extension: 'cjs' | 'mjs') => {
|
||||
if (!chunk.isEntry) {
|
||||
throw new Error(
|
||||
`Should never occurs, encountered a non entry chunk ${chunk.facadeModuleId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const splitFaceModuleId = chunk.facadeModuleId?.split('/');
|
||||
if (splitFaceModuleId === undefined) {
|
||||
throw new Error(
|
||||
`Should never occurs splitFaceModuleId is undefined ${chunk.facadeModuleId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const moduleDirectory = splitFaceModuleId[splitFaceModuleId?.length - 2];
|
||||
if (moduleDirectory === 'src') {
|
||||
return `${chunk.name}.${extension}`;
|
||||
}
|
||||
return `${moduleDirectory}.${extension}`;
|
||||
};
|
||||
|
||||
const copyAssetPlugin = (targets: { src: string; dest: string }[]) => {
|
||||
return {
|
||||
name: 'copy-assets',
|
||||
closeBundle: async () => {
|
||||
for (const target of targets) {
|
||||
await fs.copy(
|
||||
path.resolve(__dirname, target.src),
|
||||
path.resolve(__dirname, target.dest),
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default defineConfig(() => {
|
||||
const tsConfigPath = path.resolve(__dirname, './tsconfig.lib.json');
|
||||
|
||||
return {
|
||||
root: __dirname,
|
||||
cacheDir: '../../node_modules/.vite/packages/create-twenty-app',
|
||||
plugins: [
|
||||
tsconfigPaths({
|
||||
root: __dirname,
|
||||
}),
|
||||
dts({ entryRoot: './src', tsconfigPath: tsConfigPath }),
|
||||
copyAssetPlugin([
|
||||
{
|
||||
src: 'src/constants/base-application',
|
||||
dest: 'dist/constants/base-application',
|
||||
},
|
||||
]),
|
||||
],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
lib: { entry: entries, name: 'create-twenty-app' },
|
||||
rollupOptions: {
|
||||
external: [
|
||||
...Object.keys((packageJson as any).dependencies || {}),
|
||||
'path',
|
||||
'fs',
|
||||
'child_process',
|
||||
],
|
||||
output: [
|
||||
{
|
||||
format: 'es',
|
||||
entryFileNames: (chunk) => entryFileNames(chunk, 'mjs'),
|
||||
},
|
||||
{
|
||||
format: 'cjs',
|
||||
interop: 'auto',
|
||||
esModule: true,
|
||||
exports: 'named',
|
||||
entryFileNames: (chunk) => entryFileNames(chunk, 'cjs'),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
logLevel: 'warn',
|
||||
};
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
generated
|
||||
@@ -1,6 +0,0 @@
|
||||
TWENTY_API_KEY=<SET_YOUR_TWENTY_API>
|
||||
DAYS_AGO=<SET_YOUR_DAYS_AGO>
|
||||
DISCORD_WEBHOOK_URL=<SET_YOUR_DISCORD_WEBHOOK_URL>
|
||||
FB_GRAPH_TOKEN=<SET_YOUR_FB_GRAPH_TOKEN>
|
||||
WHATSAPP_RECIPIENT_PHONE_NUMBER=<SET_YOUR_WHATSAPP_RECIPIENT_PHONE_NUMBER>
|
||||
SLACK_HOOK_URL=<SET_YOUR_SLACK_HOOK_URL>
|
||||
@@ -1,2 +0,0 @@
|
||||
.yarn/install-state.gz
|
||||
.env
|
||||
-942
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,108 +0,0 @@
|
||||
# Twenty CRM Activity Reporter ��
|
||||
|
||||
A TypeScript-based reporting bot that summarizes activity from your Twenty CRM workspace and sends daily/periodic reports to Slack, Discord, and WhatsApp. Meet Kylian Mbaguette, your friendly CRM activity reporter!
|
||||
|
||||
## Features
|
||||
|
||||
- 🧑💻 **People & Company Tracking**: Summarizes newly created people and companies
|
||||
- 🎯 **Opportunity Monitoring**: Reports on new opportunities created, broken down by stage
|
||||
- ✅ **Task Analytics**:
|
||||
- Tracks task creation
|
||||
- Calculates on-time completion rates
|
||||
- Identifies team members with the most overdue tasks (the "slackers")
|
||||
- 🔔 **Multi-Platform Notifications**: Send reports to Slack, Discord, and/or WhatsApp
|
||||
- ⏰ **Configurable Time Range**: Look back any number of days
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js (v14 or higher recommended)
|
||||
- TypeScript
|
||||
- A [Twenty CRM](https://twenty.com) account with API access
|
||||
- Optional: Slack webhook, Discord webhook, and/or WhatsApp Business API access
|
||||
|
||||
## Installing dependencies
|
||||
```bash
|
||||
# Install dependencies
|
||||
yarn install
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| `TWENTY_API_KEY` | ✅ Yes | Your Twenty CRM API key |
|
||||
| `DAYS_AGO` | ✅ Yes | Number of days to look back for the report |
|
||||
| `SLACK_HOOK_URL` | ❌ No | Slack incoming webhook URL |
|
||||
| `DISCORD_WEBHOOK_URL` | ❌ No | Discord webhook URL |
|
||||
| `FB_GRAPH_TOKEN` | ❌ No | Facebook Graph API token for WhatsApp |
|
||||
| `WHATSAPP_RECIPIENT_PHONE_NUMBER` | ❌ No | WhatsApp recipient phone number (with country code) |
|
||||
|
||||
## Project Structure
|
||||
```
|
||||
.
|
||||
├── index.ts # Main entry point
|
||||
├── people-creation-summariser.ts # Summarizes people/company creation
|
||||
├── opportunity-creation-summariser.ts # Summarizes opportunity creation
|
||||
├── task-creation-summariser.ts # Summarizes task creation & completion
|
||||
├── senders.ts # Handles sending to Slack/Discord/WhatsApp
|
||||
├── utils.ts # API request utility
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Data Collection**: The bot queries the Twenty CRM API for activities within the specified time range
|
||||
2. **Analysis**:
|
||||
- Counts new people and companies
|
||||
- Categorizes opportunities by stage
|
||||
- Calculates task completion rates and identifies overdue tasks
|
||||
3. **Reporting**: Formats the data into friendly messages
|
||||
4. **Distribution**: Sends reports to configured platforms (Slack, Discord, WhatsApp)
|
||||
|
||||
## Report Format
|
||||
|
||||
Each report includes:
|
||||
```
|
||||
Bonjour! 🥖 Je m'appelle Kylian Mbaguette. Over the last X days:
|
||||
|
||||
🧑💻 People & Companies
|
||||
- X People and Y Companies were added
|
||||
|
||||
🎯 Opportunities
|
||||
- X Opportunities were added: Y in NEW, Z in PROPOSAL
|
||||
|
||||
📋 Tasks
|
||||
- X Tasks were created
|
||||
- Y% Tasks were completed on time
|
||||
- [Name] slacked the most with Z Tasks overdue
|
||||
```
|
||||
|
||||
## API Integration
|
||||
|
||||
This bot uses the [Twenty CRM REST API](https://api.twenty.com/rest/). The following endpoints are used:
|
||||
|
||||
- `GET /people` - Fetch people data
|
||||
- `GET /opportunities` - Fetch opportunity data
|
||||
- `GET /tasks` - Fetch task data
|
||||
- `GET /workspaceMembers/{id}` - Fetch workspace member details
|
||||
|
||||
## Notes
|
||||
|
||||
- The "slacker" detection is lighthearted and identifies team members with the most overdue tasks
|
||||
- At least one messaging platform must be configured for the bot to send reports
|
||||
- The bot uses ISO date format (YYYY-MM-DD) for date filtering
|
||||
- Task completion percentage only considers incomplete tasks (excludes already completed tasks from the calculation)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Issue**: No messages being sent
|
||||
- **Solution**: Ensure at least one messaging platform is configured with valid credentials
|
||||
|
||||
**Issue**: API authentication errors
|
||||
- **Solution**: Verify your `TWENTY_API_KEY` is correct and has necessary permissions
|
||||
|
||||
**Issue**: WhatsApp messages not sending
|
||||
- **Solution**: Ensure both `FB_GRAPH_TOKEN` and `WHATSAPP_RECIPIENT_PHONE_NUMBER` are set correctly
|
||||
|
||||
## Contributing
|
||||
Built with ❤️ and 🥖 by Azmat, Ali and Mike from 9dots
|
||||
@@ -1,45 +0,0 @@
|
||||
import { type ApplicationConfig } from 'twenty-sdk/application';
|
||||
|
||||
const config: ApplicationConfig = {
|
||||
universalIdentifier: 'b53627f5-ca60-478c-bc43-c7ab4904e34a',
|
||||
displayName: 'Activity Summary',
|
||||
description:
|
||||
'A TypeScript-based reporting bot that summarizes activity from your Twenty CRM workspace and sends daily/periodic reports to Slack, Discord, and WhatsApp. Meet Kylian Mbaguette, your friendly CRM activity reporter!',
|
||||
applicationVariables: {
|
||||
TWENTY_API_KEY: {
|
||||
universalIdentifier: '304b7d5d-e2bb-4444-9b04-6b3ae8b73730',
|
||||
description: 'Twenty API Key',
|
||||
isSecret: true,
|
||||
},
|
||||
DAYS_AGO: {
|
||||
universalIdentifier: '040a3097-9cee-4f74-b957-c2f9bf636c3f',
|
||||
description:
|
||||
'How far back into the past we want to summarise – defaults to the past 7 days',
|
||||
value: '7',
|
||||
isSecret: false,
|
||||
},
|
||||
SLACK_HOOK_URL: {
|
||||
universalIdentifier: 'fd16e370-934c-4267-83b4-7d88259bf7e1',
|
||||
description: 'Slack hook URL for sending message to channel',
|
||||
isSecret: true,
|
||||
},
|
||||
DISCORD_WEBHOOK_URL: {
|
||||
universalIdentifier: 'f3741075-d525-4988-ba42-55d519c6fd76',
|
||||
description:
|
||||
'Discord webhook URL for sending message to channel of a server',
|
||||
isSecret: true,
|
||||
},
|
||||
FB_GRAPH_TOKEN: {
|
||||
universalIdentifier: 'fb907f49-74ac-4aa5-ba45-cfc9250ecc44',
|
||||
description: 'For Facebook auth',
|
||||
isSecret: true,
|
||||
},
|
||||
WHATSAPP_RECIPIENT_PHONE_NUMBER: {
|
||||
universalIdentifier: 'c856ee5d-44bf-42f4-9a39-2553a94af518',
|
||||
description: 'Phone number for receiving WhatsApp message',
|
||||
isSecret: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"name": "activity-summary",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
"npm": "please-use-yarn",
|
||||
"yarn": ">=4.0.2"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"dependencies": {
|
||||
"twenty-sdk": "0.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user