Optimize merge queue to only run E2E and integrate prettier into lint (#18459)

## Summary

- **Merge queue optimization**: Created a dedicated
`ci-merge-queue.yaml` workflow that only runs Playwright E2E tests on
`ubuntu-latest-8-cores`. Removed `merge_group` trigger from all 7
existing CI workflows (front, server, shared, website, sdk, zapier,
docker-compose). The merge queue goes from ~30+ parallel jobs to a
single focused E2E job.
- **Label-based merge queue simulation**: Added `run-merge-queue` label
support so developers can trigger the exact merge queue E2E pipeline on
any open PR before it enters the queue.
- **Prettier in lint**: Chained `prettier --check` into `lint` and
`prettier --write` into `lint --configuration=fix` across `nx.json`
defaults, `twenty-front`, and `twenty-server`. Prettier formatting
errors are now caught by `lint` and fixed by `lint:fix` /
`lint:diff-with-main --configuration=fix`.

## After merge (manual repo settings)

Update GitHub branch protection required status checks:
1. Remove old per-workflow merge queue checks (`ci-front-status-check`,
`ci-e2e-status-check`, `ci-server-status-check`, etc.)
2. Add `ci-merge-queue-status-check` as the required check for the merge
queue
This commit is contained in:
Charles Bochet 2026-03-06 13:20:57 +01:00 committed by GitHub
parent d825ac06dd
commit d37ed7e07c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
82 changed files with 673 additions and 616 deletions

View file

@ -1,10 +1,8 @@
name: CI Front and E2E
name: CI Front
on:
pull_request:
merge_group:
permissions:
contents: read
@ -29,15 +27,6 @@ jobs:
packages/twenty-shared/**
packages/twenty-sdk/**
!packages/twenty-sdk/package.json
changed-files-check-e2e:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/**
!packages/create-twenty-app/package.json
!packages/twenty-sdk/package.json
playwright.config.ts
.github/workflows/ci-front.yaml
front-sb-build:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
@ -257,117 +246,6 @@ jobs:
# name: frontend-build
# path: packages/twenty-front/build
# retention-days: 1
e2e-test:
runs-on: ubuntu-latest
needs: [changed-files-check-e2e, front-build]
if: |
always() &&
needs.changed-files-check-e2e.outputs.any_changed == 'true' &&
(needs.front-build.result == 'success' || needs.front-build.result == 'skipped') &&
(github.event_name == 'push' || github.event_name == 'merge_group' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'run-e2e')))
timeout-minutes: 30
env:
NODE_OPTIONS: "--max-old-space-size=10240"
services:
postgres:
image: twentycrm/twenty-postgres-spilo
env:
PGUSER_SUPERUSER: postgres
PGPASSWORD_SUPERUSER: postgres
ALLOW_NOSSL: "true"
SPILO_PROVIDER: "local"
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 10
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Check system resources
run: |
echo "Available memory:"
free -h
echo "Available disk space:"
df -h
echo "CPU info:"
lscpu
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build twenty-shared
run: npx nx build twenty-shared
- name: Install Playwright Browsers
run: npx nx setup twenty-e2e-testing
- name: Setup environment files
run: |
cp packages/twenty-front/.env.example packages/twenty-front/.env
npx nx reset:env:e2e-testing-server twenty-server
# - name: Download frontend build artifact
# if: needs.front-build.result == 'success'
# uses: actions/download-artifact@v4
# with:
# name: frontend-build
# path: packages/twenty-front/build
# - name: Build frontend (if not available from front-build)
# if: needs.front-build.result == 'skipped'
# run: NODE_ENV=production NODE_OPTIONS="--max-old-space-size=10240" npx nx build twenty-front
- name: Build frontend
run: NODE_ENV=production NODE_OPTIONS="--max-old-space-size=10240" npx nx build twenty-front
- name: Build server
run: npx nx build twenty-server
- name: Create and setup database
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
npx nx run twenty-server:database:reset
- name: Start server
run: |
npx nx start twenty-server &
echo "Waiting for server to be ready..."
timeout 60 bash -c 'until curl -s http://localhost:3000/health; do sleep 2; done'
- name: Start frontend
run: |
npm_config_yes=true npx serve -s packages/twenty-front/build -l 3001 &
echo "Waiting for frontend to be ready..."
timeout 60 bash -c 'until curl -s http://localhost:3001; do sleep 2; done'
- name: Start worker
run: |
npx nx run twenty-server:worker &
echo "Worker started"
- name: Run Playwright tests
run: npx nx test twenty-e2e-testing
# - uses: actions/upload-artifact@v4
# if: always()
# with:
# name: playwright-report
# path: packages/twenty-e2e-testing/run_results/
# retention-days: 30
ci-front-status-check:
if: always() && !cancelled()
timeout-minutes: 5
@ -385,12 +263,3 @@ jobs:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1
ci-e2e-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check-e2e, e2e-test]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1

118
.github/workflows/ci-merge-queue.yaml vendored Normal file
View file

@ -0,0 +1,118 @@
name: CI Merge Queue
on:
merge_group:
pull_request:
types: [labeled, synchronize, opened, reopened]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
e2e-test:
if: >
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' &&
contains(github.event.pull_request.labels.*.name, 'run-merge-queue'))
runs-on: ubuntu-latest-8-cores
timeout-minutes: 30
env:
NODE_OPTIONS: "--max-old-space-size=10240"
services:
postgres:
image: twentycrm/twenty-postgres-spilo
env:
PGUSER_SUPERUSER: postgres
PGPASSWORD_SUPERUSER: postgres
ALLOW_NOSSL: "true"
SPILO_PROVIDER: "local"
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 10
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build twenty-shared
run: npx nx build twenty-shared
- name: Install Playwright Browsers
run: npx nx setup twenty-e2e-testing
- name: Setup environment files
run: |
cp packages/twenty-front/.env.example packages/twenty-front/.env
npx nx reset:env:e2e-testing-server twenty-server
- name: Build frontend
run: NODE_ENV=production npx nx build twenty-front
- name: Build server
run: npx nx build twenty-server
- name: Create and setup database
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
npx nx run twenty-server:database:reset
- name: Start server
run: |
npx nx start twenty-server &
echo "Waiting for server to be ready..."
timeout 60 bash -c 'until curl -s http://localhost:3000/health; do sleep 2; done'
- name: Start frontend
run: |
npm_config_yes=true npx serve -s packages/twenty-front/build -l 3001 &
echo "Waiting for frontend to be ready..."
timeout 60 bash -c 'until curl -s http://localhost:3001; do sleep 2; done'
- name: Start worker
run: |
npx nx run twenty-server:worker &
echo "Worker started"
- name: Run Playwright tests
run: npx nx test twenty-e2e-testing
- name: Upload Playwright results
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-results
path: |
packages/twenty-e2e-testing/run_results/
packages/twenty-e2e-testing/test-results/
retention-days: 7
ci-merge-queue-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [e2e-test]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1

View file

@ -1,8 +1,6 @@
name: CI SDK
on:
merge_group:
pull_request:
permissions:

View file

@ -3,8 +3,6 @@ name: CI Server
on:
pull_request:
merge_group:
permissions:
contents: read

View file

@ -1,8 +1,6 @@
name: CI Shared
on:
merge_group:
pull_request:
permissions:

View file

@ -4,8 +4,6 @@ permissions:
contents: read
on:
merge_group:
pull_request:
concurrency:

View file

@ -4,8 +4,6 @@ permissions:
contents: read
on:
merge_group:
pull_request:
concurrency:

View file

@ -3,8 +3,6 @@ name: CI Zapier
on:
pull_request:
merge_group:
permissions:
contents: read

View file

@ -44,12 +44,12 @@
"cache": true,
"options": {
"cwd": "{projectRoot}",
"command": "npx oxlint -c .oxlintrc.json ."
"command": "npx oxlint -c .oxlintrc.json . && (prettier . --check --cache --cache-location ../../.cache/prettier/{projectRoot} --cache-strategy metadata || (echo 'ERROR: Prettier formatting check failed! Fix with: npx nx lint --configuration=fix' && false))"
},
"configurations": {
"ci": {},
"fix": {
"command": "npx oxlint --fix -c .oxlintrc.json ."
"command": "npx oxlint --fix -c .oxlintrc.json . && prettier . --write --cache --cache-location ../../.cache/prettier/{projectRoot} --cache-strategy metadata"
}
},
"dependsOn": ["^build", "twenty-oxlint-rules:build"]
@ -58,12 +58,12 @@
"executor": "nx:run-commands",
"cache": false,
"options": {
"command": "FILES=$(git diff --name-only --diff-filter=d main -- {projectRoot}/ | grep -E '{args.pattern}'); [ -z \"$FILES\" ] && echo 'No changed files.' || npx oxlint -c {projectRoot}/.oxlintrc.json $FILES",
"command": "FILES=$(git diff --name-only --diff-filter=d main -- {projectRoot}/ | grep -E '{args.pattern}'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint -c {projectRoot}/.oxlintrc.json $FILES && (prettier --check $FILES || (echo 'ERROR: Prettier formatting check failed! Fix with: npx nx lint:diff-with-main --configuration=fix' && false)))",
"pattern": "\\.(ts|tsx|js|jsx)$"
},
"configurations": {
"fix": {
"command": "FILES=$(git diff --name-only --diff-filter=d main -- {projectRoot}/ | grep -E '{args.pattern}'); [ -z \"$FILES\" ] && echo 'No changed files.' || npx oxlint --fix -c {projectRoot}/.oxlintrc.json $FILES"
"command": "FILES=$(git diff --name-only --diff-filter=d main -- {projectRoot}/ | grep -E '{args.pattern}'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint --fix -c {projectRoot}/.oxlintrc.json $FILES && prettier --write $FILES)"
}
}
},

View file

@ -17,22 +17,31 @@
"import/no-duplicates": "error",
"typescript/no-redeclare": "error",
"typescript/ban-ts-comment": "error",
"typescript/consistent-type-imports": ["error", {
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}],
"typescript/consistent-type-imports": [
"error",
{
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}
],
"typescript/explicit-function-return-type": "off",
"typescript/explicit-module-boundary-types": "off",
"typescript/no-empty-object-type": ["error", {
"allowInterfaces": "with-single-extends"
}],
"typescript/no-empty-object-type": [
"error",
{
"allowInterfaces": "with-single-extends"
}
],
"typescript/no-empty-function": "off",
"typescript/no-explicit-any": "off",
"typescript/no-unused-vars": ["warn", {
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}]
"typescript/no-unused-vars": [
"warn",
{
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}
]
}
}

View file

@ -0,0 +1 @@
dist

View file

@ -19,9 +19,11 @@ Create Twenty App is the official scaffolding CLI for building apps on top of [T
- Strong TypeScript support and typed client generation
## Documentation
See Twenty application documentation https://docs.twenty.com/developers/extend/capabilities/apps
## 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)
@ -64,11 +66,11 @@ yarn twenty app:uninstall
Control which example files are included when creating a new app:
| Flag | Behavior |
|------|----------|
| `-e, --exhaustive` | **(default)** Creates all example files without prompting |
| `-m, --minimal` | Creates only core files (`application-config.ts` and `default-role.ts`) |
| `-i, --interactive` | Prompts you to select which examples to include |
| Flag | Behavior |
| ------------------- | ----------------------------------------------------------------------- |
| `-e, --exhaustive` | **(default)** Creates all example files without prompting |
| `-m, --minimal` | Creates only core files (`application-config.ts` and `default-role.ts`) |
| `-i, --interactive` | Prompts you to select which examples to include |
```bash
# Default: all examples included
@ -82,6 +84,7 @@ npx create-twenty-app@latest my-app -i
```
In interactive mode, you can pick from:
- **Example object** — a custom CRM object definition (`objects/example-object.ts`)
- **Example field** — a custom field on the example object (`fields/example-field.ts`)
- **Example logic function** — a server-side handler with HTTP trigger (`logic-functions/hello-world.ts`)
@ -94,6 +97,7 @@ In interactive mode, you can pick from:
## What gets scaffolded
**Core files (always created):**
- `application-config.ts` — Application metadata configuration
- `roles/default-role.ts` — Default role for logic functions
- `logic-functions/pre-install.ts` — Pre-install logic function (runs before app installation)
@ -102,6 +106,7 @@ In interactive mode, you can pick from:
- A prewired `twenty` script that delegates to the `twenty` CLI from twenty-sdk
**Example files (controlled by scaffolding mode):**
- `objects/example-object.ts` — Example custom object with a text field
- `fields/example-field.ts` — Example standalone field extending the example object
- `logic-functions/hello-world.ts` — Example logic function with HTTP trigger
@ -112,14 +117,15 @@ In interactive mode, you can pick from:
- `__tests__/app-install.integration-test.ts` — Integration test that builds, installs, and verifies the app (includes `vitest.config.ts`, `tsconfig.spec.json`, and a setup file)
## Next steps
- Run `yarn twenty help` to see all available commands.
- Use `yarn twenty auth:login` to authenticate with your Twenty workspace.
- Explore the generated project and add your first entity with `yarn twenty entity:add` (logic functions, front components, objects, roles, views, navigation menu items, skills).
- Use `yarn twenty app:dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
- Two typed API clients are autogenerated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated`: `CoreApiClient` (for workspace data via `/graphql`) and `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`).
## Publish your application
Applications are currently stored in `twenty/packages/twenty-apps`.
You can share your application with all Twenty users:
@ -144,9 +150,11 @@ git push
Our team reviews contributions for quality, security, and reusability before merging.
## Troubleshooting
- Auth prompts not appearing: run `yarn twenty auth:login` again and verify the API key permissions.
- Types not generated: ensure `yarn twenty app:dev` is running — it autogenerates the typed client.
## Contributing
- See our [GitHub](https://github.com/twentyhq/twenty)
- Join our [Discord](https://discord.gg/cx5n4Jzs57)

View file

@ -8,9 +8,12 @@
"rules": {
"no-unused-vars": "off",
"typescript/no-unused-vars": ["warn", {
"argsIgnorePattern": "^_"
}],
"typescript/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_"
}
],
"typescript/no-explicit-any": "off"
}
}

View file

@ -4,6 +4,7 @@
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-sdk/src/cli/__tests__/apps/rich-app
## UUID requirement
- All generated UUIDs must be valid UUID v4.
## Common Pitfalls

View file

@ -27,5 +27,11 @@
"~/*": ["./*"]
}
},
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts", "**/*.integration-test.ts"]
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.spec.ts",
"**/*.integration-test.ts"
]
}

View file

@ -24,7 +24,5 @@
"vite.config.ts",
"jest.config.mjs"
],
"exclude": [
"src/constants/base-application/vitest.config.ts"
]
"exclude": ["src/constants/base-application/vitest.config.ts"]
}

View file

@ -87,7 +87,7 @@ export class LoginPage {
async clickLoginWithEmailIfVisible() {
try {
await this.loginWithEmailButton.click();
await this.loginWithEmailButton.click({ timeout: 3000 });
} catch {
// Button not found - email field might already be visible (SSO-only or different auth flow)
}

View file

@ -34,7 +34,7 @@ test('Sign up with invite link via email', async ({
});
await test.step('Create new account', async () => {
await loginPage.clickLoginWithEmail();
await loginPage.clickLoginWithEmailIfVisible();
await loginPage.typeEmail(email);
await loginPage.clickContinueButton();
await loginPage.typePassword(process.env.DEFAULT_PASSWORD);

View file

@ -34,10 +34,7 @@ test('Create workflow', async ({ page }) => {
const recordName = page.getByTestId('top-bar-title').getByPlaceholder('Name');
await expect(recordName).toBeVisible();
await recordName.click();
const nameInput = page.getByTestId('top-bar-title').getByRole('textbox');
await nameInput.fill(NEW_WORKFLOW_NAME);
await recordName.fill(NEW_WORKFLOW_NAME);
const workflowDiagramContainer = page.locator('.react-flow__renderer');
await workflowDiagramContainer.click();

View file

@ -5,12 +5,13 @@
"categories": {
"correctness": "off"
},
"ignorePatterns": [
"node_modules"
],
"ignorePatterns": ["node_modules"],
"rules": {
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
"no-console": ["warn", { "allow": ["group", "groupCollapsed", "groupEnd"] }],
"no-console": [
"warn",
{ "allow": ["group", "groupCollapsed", "groupEnd"] }
],
"no-control-regex": "off",
"no-debugger": "error",
"no-duplicate-imports": "error",
@ -34,31 +35,43 @@
"typescript/no-redeclare": "error",
"typescript/ban-ts-comment": "error",
"typescript/consistent-type-imports": ["error", {
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}],
"typescript/consistent-type-imports": [
"error",
{
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}
],
"typescript/explicit-function-return-type": "off",
"typescript/explicit-module-boundary-types": "off",
"typescript/no-empty-object-type": ["error", {
"allowInterfaces": "with-single-extends"
}],
"typescript/no-empty-object-type": [
"error",
{
"allowInterfaces": "with-single-extends"
}
],
"typescript/no-empty-function": "off",
"typescript/no-explicit-any": "off",
"typescript/no-unused-vars": ["warn", {
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}],
"typescript/no-unused-vars": [
"warn",
{
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}
],
"twenty/enforce-module-boundaries": ["error", {
"depConstraints": [
{
"sourceTag": "scope:backend",
"onlyDependOnLibsWithTags": ["scope:shared", "scope:backend"]
}
]
}]
"twenty/enforce-module-boundaries": [
"error",
{
"depConstraints": [
{
"sourceTag": "scope:backend",
"onlyDependOnLibsWithTags": ["scope:shared", "scope:backend"]
}
]
}
]
}
}

View file

@ -0,0 +1 @@
src/locales/generated

View file

@ -5,12 +5,12 @@
"plugins": [
[
"@lingui/swc-plugin",
{
"runtimeModules": {
"i18n": ["@lingui/core", "i18n"],
"trans": ["@lingui/react", "Trans"]
},
"stripNonEssentialFields": false
{
"runtimeModules": {
"i18n": ["@lingui/core", "i18n"],
"trans": ["@lingui/react", "Trans"]
},
"stripNonEssentialFields": false
}
]
]

View file

@ -13,9 +13,5 @@
"src/*": ["./src/*"]
}
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"vite.config.ts"
]
"include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts"]
}

View file

@ -7,10 +7,5 @@
"types": ["node", "@nx/react/typings/image.d.ts", "vite/client"]
},
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"],
"exclude": [
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx"
]
"exclude": ["**/*.spec.ts", "**/*.test.ts", "**/*.spec.tsx", "**/*.test.tsx"]
}

View file

@ -1,3 +1,4 @@
src/generated
src/generated-metadata
src/locales/generated
src/testing/mock-data/generated

View file

@ -77,11 +77,11 @@
],
"options": {
"cwd": "{projectRoot}",
"command": "npx oxlint --type-aware -c .oxlintrc.json src/"
"command": "npx oxlint --type-aware -c .oxlintrc.json src/ && (prettier src/ --check --cache --cache-location ../../.cache/prettier/{projectRoot} --cache-strategy metadata || (echo 'ERROR: Prettier formatting check failed! Fix with: npx nx lint twenty-front --configuration=fix' && false))"
},
"configurations": {
"fix": {
"command": "npx oxlint --type-aware --fix -c .oxlintrc.json src/"
"command": "npx oxlint --type-aware --fix -c .oxlintrc.json src/ && prettier src/ --write --cache --cache-location ../../.cache/prettier/{projectRoot} --cache-strategy metadata"
}
}
},
@ -89,11 +89,11 @@
"executor": "nx:run-commands",
"options": {
"cwd": "{projectRoot}",
"command": "FILES=$(git diff --name-only --diff-filter=d main...HEAD -- src/ | grep -E '\\.(ts|tsx)$'); [ -z \"$FILES\" ] && echo 'No changed files.' || npx oxlint --type-aware -c .oxlintrc.json $FILES"
"command": "FILES=$(git diff --name-only --diff-filter=d main...HEAD -- src/ | grep -E '\\.(ts|tsx)$'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint --type-aware -c .oxlintrc.json $FILES && (prettier --check $FILES || (echo 'ERROR: Prettier formatting check failed! Fix with: npx nx lint:diff-with-main twenty-front --configuration=fix' && false)))"
},
"configurations": {
"fix": {
"command": "FILES=$(git diff --name-only --diff-filter=d main...HEAD -- src/ | grep -E '\\.(ts|tsx)$'); [ -z \"$FILES\" ] && echo 'No changed files.' || npx oxlint --type-aware --fix -c .oxlintrc.json $FILES"
"command": "FILES=$(git diff --name-only --diff-filter=d main...HEAD -- src/ | grep -E '\\.(ts|tsx)$'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint --type-aware --fix -c .oxlintrc.json $FILES && prettier --write $FILES)"
}
}
},

View file

@ -51,8 +51,7 @@ export const TimelineCard = () => {
const { timelineActivities, loading, fetchMoreRecords } =
useTimelineActivities(targetRecord);
const isTimelineActivitiesEmpty =
timelineActivities.length === 0;
const isTimelineActivitiesEmpty = timelineActivities.length === 0;
if (loading === true) {
return <SkeletonLoader withSubSections />;

View file

@ -31,11 +31,9 @@ export const getActivityTargetObjectRecords = ({
const targets = activityTargets
? activityTargets
: 'noteTargets' in activityRecord &&
isDefined(activityRecord.noteTargets)
: 'noteTargets' in activityRecord && isDefined(activityRecord.noteTargets)
? activityRecord.noteTargets
: 'taskTargets' in activityRecord &&
isDefined(activityRecord.taskTargets)
: 'taskTargets' in activityRecord && isDefined(activityRecord.taskTargets)
? activityRecord.taskTargets
: [];

View file

@ -66,7 +66,8 @@ export const RoutingStatusDisplay = ({
const [isExpanded, setIsExpanded] = useState(false);
const isLoading = data.state === 'loading';
const isDebugMode = process.env.IS_DEBUG_MODE === 'true';
const isExpandable = isDebugMode && data.state === 'routed' && isDefined(data.debug);
const isExpandable =
isDebugMode && data.state === 'routed' && isDefined(data.debug);
if (data.state === 'error') {
return null;

View file

@ -117,56 +117,108 @@ export const useCreateAppRouter = (
<Route path={AppPath.VerifyEmail} element={<VerifyEmailEffect />} />
<Route
path={AppPath.SignInUp}
element={<LazyRoute><SignInUp /></LazyRoute>}
element={
<LazyRoute>
<SignInUp />
</LazyRoute>
}
/>
<Route
path={AppPath.Invite}
element={<LazyRoute><SignInUp /></LazyRoute>}
element={
<LazyRoute>
<SignInUp />
</LazyRoute>
}
/>
<Route
path={AppPath.ResetPassword}
element={<LazyRoute><PasswordReset /></LazyRoute>}
element={
<LazyRoute>
<PasswordReset />
</LazyRoute>
}
/>
<Route
path={AppPath.CreateWorkspace}
element={<LazyRoute><CreateWorkspace /></LazyRoute>}
element={
<LazyRoute>
<CreateWorkspace />
</LazyRoute>
}
/>
<Route
path={AppPath.CreateProfile}
element={<LazyRoute><CreateProfile /></LazyRoute>}
element={
<LazyRoute>
<CreateProfile />
</LazyRoute>
}
/>
<Route
path={AppPath.SyncEmails}
element={<LazyRoute><SyncEmails /></LazyRoute>}
element={
<LazyRoute>
<SyncEmails />
</LazyRoute>
}
/>
<Route
path={AppPath.InviteTeam}
element={<LazyRoute><InviteTeam /></LazyRoute>}
element={
<LazyRoute>
<InviteTeam />
</LazyRoute>
}
/>
<Route
path={AppPath.PlanRequired}
element={<LazyRoute><ChooseYourPlan /></LazyRoute>}
element={
<LazyRoute>
<ChooseYourPlan />
</LazyRoute>
}
/>
<Route
path={AppPath.PlanRequiredSuccess}
element={<LazyRoute><PaymentSuccess /></LazyRoute>}
element={
<LazyRoute>
<PaymentSuccess />
</LazyRoute>
}
/>
<Route
path={AppPath.BookCallDecision}
element={<LazyRoute><BookCallDecision /></LazyRoute>}
element={
<LazyRoute>
<BookCallDecision />
</LazyRoute>
}
/>
<Route
path={AppPath.BookCall}
element={<LazyRoute><BookCall /></LazyRoute>}
element={
<LazyRoute>
<BookCall />
</LazyRoute>
}
/>
<Route path={indexAppPath.getIndexAppPath()} element={<></>} />
<Route
path={AppPath.RecordIndexPage}
element={<LazyRoute><RecordIndexPage /></LazyRoute>}
element={
<LazyRoute>
<RecordIndexPage />
</LazyRoute>
}
/>
<Route
path={AppPath.RecordShowPage}
element={<LazyRoute><RecordShowPage /></LazyRoute>}
element={
<LazyRoute>
<RecordShowPage />
</LazyRoute>
}
/>
<Route
path={AppPath.SettingsCatchAll}
@ -179,13 +231,21 @@ export const useCreateAppRouter = (
/>
<Route
path={AppPath.NotFoundWildcard}
element={<LazyRoute><NotFound /></LazyRoute>}
element={
<LazyRoute>
<NotFound />
</LazyRoute>
}
/>
</Route>
<Route element={<BlankLayout />}>
<Route
path={AppPath.Authorize}
element={<LazyRoute><Authorize /></LazyRoute>}
element={
<LazyRoute>
<Authorize />
</LazyRoute>
}
/>
</Route>
</Route>,

View file

@ -97,7 +97,12 @@ export const MeteredPriceSelector = ({
selectedPriceId !== currentMeteredPrice?.stripePriceId;
const isUpgrade = () => {
if (!isChanged || !isDefined(selectedPrice) || !isDefined(currentMeteredPrice)) return false;
if (
!isChanged ||
!isDefined(selectedPrice) ||
!isDefined(currentMeteredPrice)
)
return false;
return (
(selectedPrice.tiers as BillingPriceTiers)[0].flatAmount >
(currentMeteredPrice.tiers as BillingPriceTiers)[0].flatAmount

View file

@ -72,7 +72,7 @@ export const FavoriteFolderPickerEffect = ({
.filter(
(favorite) =>
favorite.recordId === targetId &&
isDefined(favorite.forWorkspaceMemberId),
isDefined(favorite.forWorkspaceMemberId),
)
.map((favorite) => favorite.favoriteFolderId ?? 'no-folder');
setFavoriteFolderPickerChecked(checkedFolderIds);

View file

@ -50,8 +50,7 @@ export const useMentionSearch = () => {
},
});
const searchRecords =
data?.search.edges.map((edge) => edge.node) ?? [];
const searchRecords = data?.search.edges.map((edge) => edge.node) ?? [];
return searchRecords.map((searchRecord) => ({
recordId: searchRecord.recordId,

View file

@ -56,7 +56,11 @@ export const useFindOneRecord = <T extends ObjectRecord = ObjectRecord>({
const { data, loading, error, refetch } = useQuery<{
[nameSingular: string]: RecordGqlNode;
}>(findOneRecordQuery, {
skip: !isDefined(objectMetadataItem) || !objectRecordId || skip || !hasReadPermission,
skip:
!isDefined(objectMetadataItem) ||
!objectRecordId ||
skip ||
!hasReadPermission,
variables: { objectRecordId },
client: apolloCoreClient,
onCompleted: (data) => {

View file

@ -93,8 +93,7 @@ export const RecordTableVirtualizedInitialDataLoadEffect = () => {
JSON.stringify(lastContextStoreVirtualizedVisibleRecordFields) !==
JSON.stringify(visibleRecordFields)
) {
const lastFields =
lastContextStoreVirtualizedVisibleRecordFields ?? [];
const lastFields = lastContextStoreVirtualizedVisibleRecordFields ?? [];
const currentFields = visibleRecordFields ?? [];
setLastContextStoreVirtualizedVisibleRecordFields(visibleRecordFields);

View file

@ -48,7 +48,9 @@ export const SettingsOptionCardContentButton = ({
</StyledSettingsCardDescription>
)}
</StyledSettingsCardTextContainer>
{isDefined(Button) && <StyledButtonContainer>{Button}</StyledButtonContainer>}
{isDefined(Button) && (
<StyledButtonContainer>{Button}</StyledButtonContainer>
)}
</StyledSettingsCardContent>
);
};

View file

@ -110,7 +110,9 @@ export const SettingsRolePermissionsObjectLevelRecordLevelPermissionMeValueSelec
}
}
const compatibleWorkspaceMemberFields = !isDefined(workspaceMemberMetadataItem)
const compatibleWorkspaceMemberFields = !isDefined(
workspaceMemberMetadataItem,
)
? []
: workspaceMemberMetadataItem.fields.filter((field) => {
if (

View file

@ -28,14 +28,12 @@ export const SubMatchingSelectInput = ({
const optionsToSelect = useMemo(() => {
const searchTerm = normalizeSearchText(searchFilter);
return (
options.filter((option) => {
return (
option.value !== selectedOption?.value &&
normalizeSearchText(option.label).includes(searchTerm)
);
})
);
return options.filter((option) => {
return (
option.value !== selectedOption?.value &&
normalizeSearchText(option.label).includes(searchTerm)
);
});
}, [options, searchFilter, selectedOption?.value]);
const optionsInDropDown = useMemo(

View file

@ -206,11 +206,11 @@ export const ValidationStep = ({
if (filterByErrors) {
return data.filter((value) => {
if (isDefined(value?.__errors)) {
return (
(Object.values(value.__errors)?.filter(
(err) => err.level === 'error',
).length ?? 0) > 0
);
return (
(Object.values(value.__errors)?.filter(
(err) => err.level === 'error',
).length ?? 0) > 0
);
}
return false;
});

View file

@ -58,14 +58,12 @@ export const SelectInput = ({
const optionsToSelect = useMemo(() => {
const searchTerm = normalizeSearchText(searchFilter);
return (
options.filter((option) => {
return (
option.value !== selectedOption?.value &&
normalizeSearchText(option.label).includes(searchTerm)
);
})
);
return options.filter((option) => {
return (
option.value !== selectedOption?.value &&
normalizeSearchText(option.label).includes(searchTerm)
);
});
}, [options, searchFilter, selectedOption?.value]);
const optionsInDropDown = useMemo(

View file

@ -10,7 +10,9 @@ export const useSystemColorScheme = (): ColorScheme => {
);
const [preferredColorScheme, setPreferredColorScheme] = useState<ColorScheme>(
isUndefinedOrNull(window.matchMedia) || !mediaQuery.matches ? 'Light' : 'Dark',
isUndefinedOrNull(window.matchMedia) || !mediaQuery.matches
? 'Light'
: 'Dark',
);
useEffect(() => {

View file

@ -83,7 +83,7 @@ export const ViewPickerOptionDropdown = ({
: favorites.some(
(favorite) =>
favorite.recordId === view.id &&
isDefined(favorite.forWorkspaceMemberId),
isDefined(favorite.forWorkspaceMemberId),
);
const handleDelete = () => {

View file

@ -116,7 +116,8 @@ export const WorkflowStepFilterValueInput = ({
const isDisabled = !stepFilter.operand;
const operandHasNoInput =
(isDefined(stepFilter) && !configurableViewFilterOperands.has(stepFilter.operand)) ??
(isDefined(stepFilter) &&
!configurableViewFilterOperands.has(stepFilter.operand)) ??
true;
const {

View file

@ -144,7 +144,11 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
...trigger,
settings: {
...trigger.settings,
fields: isDefined(fields) ? (Array.isArray(fields) ? fields : [fields]) : null,
fields: isDefined(fields)
? Array.isArray(fields)
? fields
: [fields]
: null,
},
});
};

View file

@ -121,7 +121,10 @@ export const WorkflowEditTriggerManual = ({
options={availableMetadata}
disabled={triggerOptions.readonly}
onChange={(objectNameSingular) => {
if (triggerOptions.readonly === true || !isDefined(availability)) {
if (
triggerOptions.readonly === true ||
!isDefined(availability)
) {
return;
}

View file

@ -39,8 +39,8 @@ export const useMapFieldMetadataItemToSettingsObjectDetailTableItem = (
fieldMetadataItem,
fieldType: fieldType ?? '',
dataType:
(isDefined(relationObjectMetadataItem?.labelPlural) ||
isFieldTypeSupportedInSettings(fieldMetadataType))
isDefined(relationObjectMetadataItem?.labelPlural) ||
isFieldTypeSupportedInSettings(fieldMetadataType)
? getSettingsFieldTypeConfig(fieldMetadataType as SettingsFieldType)
?.label
: '',

View file

@ -0,0 +1,5 @@
{
"name": "twenty-oxlint-rules",
"private": true,
"type": "module"
}

View file

@ -3,8 +3,8 @@
"compilerOptions": {
"outDir": "../../.cache/tsc",
"esModuleInterop": true,
"moduleResolution": "node16",
"module": "node16",
"moduleResolution": "bundler",
"module": "esnext",
"types": ["node"]
},
"include": ["**/*.ts"]

View file

@ -17,23 +17,32 @@
"import/no-duplicates": "error",
"typescript/no-redeclare": "error",
"typescript/ban-ts-comment": "error",
"typescript/consistent-type-imports": ["error", {
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}],
"typescript/consistent-type-imports": [
"error",
{
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}
],
"typescript/explicit-function-return-type": "off",
"typescript/explicit-module-boundary-types": "off",
"typescript/no-empty-object-type": ["error", {
"allowInterfaces": "with-single-extends"
}],
"typescript/no-empty-object-type": [
"error",
{
"allowInterfaces": "with-single-extends"
}
],
"typescript/no-empty-function": "off",
"typescript/no-explicit-any": "off",
"typescript/no-unused-vars": ["warn", {
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}],
"typescript/no-unused-vars": [
"warn",
{
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}
],
"react/no-unescaped-entities": "off",
"react/prop-types": "off",
"react/jsx-key": "off",

View file

@ -0,0 +1,3 @@
dist
storybook-static
coverage

View file

@ -11,9 +11,7 @@ const dirname =
const sdkRoot = path.resolve(dirname, '..');
const config: StorybookConfig = {
stories: [
'../src/front-component-renderer/**/*.stories.@(js|jsx|ts|tsx)',
],
stories: ['../src/front-component-renderer/**/*.stories.@(js|jsx|ts|tsx)'],
addons: ['@storybook/addon-vitest'],

View file

@ -19,9 +19,11 @@ A CLI and SDK to develop, build, and publish applications that extend [Twenty CR
- Works great with the scaffolder: [create-twenty-app](https://www.npmjs.com/package/create-twenty-app)
## Documentation
See Twenty application documentation https://docs.twenty.com/developers/extend/capabilities/apps
## Prerequisites
- Node.js 24+ (recommended) and Yarn 4
- A Twenty workspace and an API key. Generate one at https://app.twenty.com/settings/api-webhooks
@ -73,6 +75,7 @@ In a scaffolded project (via `create-twenty-app`), use `yarn twenty <command>` i
Authenticate the CLI against your Twenty workspace.
- `twenty auth:login` — Authenticate with Twenty.
- Options:
- `--api-key <key>`: API key for authentication.
- `--api-url <url>`: Twenty API URL (defaults to your current profile's value or `http://localhost:3000`).
@ -83,6 +86,7 @@ Authenticate the CLI against your Twenty workspace.
- `twenty auth:status` — Print the current authentication status (API URL, masked API key, validity).
- `twenty auth:list` — List all configured workspaces.
- Behavior: Displays all available workspaces with their authentication status and API URLs. Shows which workspace is the current default.
- `twenty auth:switch [workspace]` — Switch the default workspace for authentication.
@ -123,6 +127,7 @@ twenty auth:switch production
Application development commands.
- `twenty app:dev [appPath]` — Start development mode: watch and sync local application changes.
- Behavior: Builds your application (functions and front components), computes the manifest, syncs everything to your workspace, then watches the directory for changes and re-syncs automatically. Displays an interactive UI showing build and sync status in real time. Press Ctrl+C to stop.
- `twenty app:typecheck [appPath]` — Run TypeScript type checking on the application (runs `tsc --noEmit`). Exits with code 1 if type errors are found.
@ -149,6 +154,7 @@ Application development commands.
### Function
- `twenty function:logs [appPath]` — Stream application function logs.
- Options:
- `-u, --functionUniversalIdentifier <id>`: Only show logs for a specific function universal ID.
- `-n, --functionName <name>`: Only show logs for a specific function name.
@ -249,8 +255,8 @@ Notes:
- `twenty auth:switch` sets the `defaultWorkspace` field, which is used when `--workspace` is not specified.
- `twenty auth:list` shows all configured workspaces and their authentication status.
## Troubleshooting
- Auth errors: run `twenty auth:login` again and ensure the API key has the required permissions.
- Typings out of date: restart `twenty app:dev` to refresh the client and types.
- Not seeing changes in dev: make sure dev mode is running (`twenty app:dev`).
@ -301,5 +307,6 @@ node packages/twenty-sdk/dist/cli.cjs <command>
```
### Resources
- See our [GitHub](https://github.com/twentyhq/twenty)
- Join our [Discord](https://discord.gg/cx5n4Jzs57)

View file

@ -3,24 +3,14 @@
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/twenty-sdk/src",
"projectType": "library",
"tags": [
"scope:sdk",
"scope:shared"
],
"tags": ["scope:sdk"],
"targets": {
"build": {
"executor": "nx:run-commands",
"cache": true,
"inputs": [
"production",
"^production"
],
"dependsOn": [
"^build"
],
"outputs": [
"{projectRoot}/dist"
],
"inputs": ["production", "^production"],
"dependsOn": ["^build"],
"outputs": ["{projectRoot}/dist"],
"options": {
"cwd": "{projectRoot}",
"commands": [
@ -32,9 +22,7 @@
},
"dev": {
"executor": "nx:run-commands",
"dependsOn": [
"^build"
],
"dependsOn": ["^build"],
"options": {
"cwd": "packages/twenty-sdk",
"command": "npx rimraf dist && npx vite build -c vite.config.node.ts && npx vite build -c vite.config.browser.ts && npx vite build -c vite.config.sdk.ts && tsgo -p tsconfig.lib.json --declaration --emitDeclarationOnly --noEmit false --outDir dist --rootDir src && npx tsc-alias -p tsconfig.lib.json --outDir dist && npx vite build -c vite.config.node.ts --watch & npx vite build -c vite.config.browser.ts --watch & npx vite build -c vite.config.sdk.ts --watch"
@ -42,9 +30,7 @@
},
"start": {
"executor": "nx:run-commands",
"dependsOn": [
"build"
],
"dependsOn": ["build"],
"options": {
"cwd": "packages/twenty-sdk",
"command": "node dist/cli.cjs"
@ -54,9 +40,7 @@
"lint": {},
"test": {
"executor": "@nx/vitest:test",
"outputs": [
"{workspaceRoot}/coverage/{projectRoot}"
],
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"config": "{projectRoot}/vitest.config.ts"
},
@ -103,15 +87,9 @@
"build:sdk": {
"executor": "nx:run-commands",
"cache": true,
"dependsOn": [
"^build"
],
"inputs": [
"{projectRoot}/src/sdk/**/*"
],
"outputs": [
"{projectRoot}/dist/sdk"
],
"dependsOn": ["^build"],
"inputs": ["{projectRoot}/src/sdk/**/*"],
"outputs": ["{projectRoot}/dist/sdk"],
"options": {
"cwd": "{projectRoot}",
"command": "npx vite build -c vite.config.sdk.ts"
@ -120,9 +98,7 @@
"generate-remote-dom-elements": {
"executor": "nx:run-commands",
"cache": true,
"dependsOn": [
"^build"
],
"dependsOn": ["^build"],
"inputs": [
"{projectRoot}/scripts/remote-dom/**/*",
"{projectRoot}/src/sdk/front-component-api/**/*",
@ -171,9 +147,7 @@
}
},
"storybook:build": {
"dependsOn": [
"storybook:prebuild"
],
"dependsOn": ["storybook:prebuild"],
"configurations": {
"test": {}
}
@ -194,17 +168,13 @@
}
},
"storybook:test": {
"dependsOn": [
"storybook:prebuild"
],
"dependsOn": ["storybook:prebuild"],
"options": {
"command": "vitest run --coverage --config vitest.storybook.config.ts --shard={args.shard}"
}
},
"storybook:test:no-coverage": {
"dependsOn": [
"storybook:prebuild"
],
"dependsOn": ["storybook:prebuild"],
"options": {
"command": "vitest run --config vitest.storybook.config.ts --shard={args.shard}"
}

View file

@ -6,7 +6,9 @@ export const CardDisplay = ({
content: string;
}) => {
return (
<div style={{ border: '1px solid #ccc', padding: '16px', borderRadius: '8px' }}>
<div
style={{ border: '1px solid #ccc', padding: '16px', borderRadius: '8px' }}
>
<h3>{title}</h3>
<p>{content}</p>
</div>

View file

@ -114,11 +114,7 @@ export const DevUiApplicationPanel = ({
const entities = groupedEntities.get(type) ?? [];
return (
<DevUiEntitySection
key={type}
type={type}
entities={entities}
/>
<DevUiEntitySection key={type} type={type} entities={entities} />
);
})}
</Box>

View file

@ -1,6 +1,4 @@
import {
type OrchestratorStateEntityInfo,
} from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state';
import { type OrchestratorStateEntityInfo } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state';
import {
type DevUiStatus,
DEV_UI_STATUS_CONFIG,
@ -37,9 +35,7 @@ export const DevUiEntityRow = ({
return (
<Box>
<DevUiStatusIcon
uiStatus={mapFileStatusToDevUiStatus(entity.status)}
/>
<DevUiStatusIcon uiStatus={mapFileStatusToDevUiStatus(entity.status)} />
<Text>{entity.name}</Text>
{entity.path !== entity.name && (
<Text dimColor> ({shortenPath(entity.path)})</Text>

View file

@ -74,10 +74,8 @@ const BundleSizesTable = () => {
</thead>
<tbody>
{entries.map((entry) => {
const reactWidth =
(entry.reactBytes / maxBytes) * BAR_MAX_WIDTH;
const preactWidth =
(entry.preactBytes / maxBytes) * BAR_MAX_WIDTH;
const reactWidth = (entry.reactBytes / maxBytes) * BAR_MAX_WIDTH;
const preactWidth = (entry.preactBytes / maxBytes) * BAR_MAX_WIDTH;
const saving =
entry.reactBytes > 0
? (
@ -102,7 +100,9 @@ const BundleSizesTable = () => {
{formatLabel(entry.name)}
</td>
<td style={{ padding: '10px 12px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
<div
style={{ display: 'flex', flexDirection: 'column', gap: 3 }}
>
<div
style={{
height: 14,

View file

@ -62,11 +62,7 @@ const ChakraComponent = () => {
>
Increment
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setCount(0)}
>
<Button variant="outline" size="sm" onClick={() => setCount(0)}>
Reset
</Button>
</HStack>

View file

@ -37,9 +37,7 @@ const SdkContextComponent = () => {
const userId = useUserId();
const fullContext = useFrontComponentExecutionContext(
(context) => context,
);
const fullContext = useFrontComponentExecutionContext((context) => context);
return (
<div data-testid="sdk-context-component" style={CARD_STYLE}>
@ -78,9 +76,7 @@ const SdkContextComponent = () => {
</pre>
</div>
<div
style={{ display: 'flex', alignItems: 'center', gap: 12 }}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<button
data-testid="sdk-context-button"
onClick={() => setRenderCount((previous) => previous + 1)}

View file

@ -13,14 +13,18 @@ export default defineConfig({
environment: 'node',
include: [
'src/**/__tests__/**/*.{test,spec}.{ts,tsx}',
'src/**/__integration__/**/*.{test,spec}.{ts,tsx}',
'src/**/*.{test,spec}.{ts,tsx}',
],
exclude: ['**/node_modules/**', '**/.git/**', '**/__e2e__/**'],
exclude: [
'**/node_modules/**',
'**/.git/**',
'**/__e2e__/**',
'**/__integration__/**',
],
coverage: {
provider: 'v8',
include: ['src/**/*.{ts,tsx}'],
exclude: ['src/**/*.d.ts', 'src/cli/cli.ts'],
exclude: ['src/**/*.d.ts', 'src/cli/cli.ts', 'src/**/__stories__/**'],
thresholds: {
statements: 1,
lines: 1,

View file

@ -162,12 +162,12 @@
],
"options": {
"cwd": "{projectRoot}",
"command": "npx oxlint --type-aware -c .oxlintrc.json src/"
"command": "npx oxlint --type-aware -c .oxlintrc.json src/ && (prettier src/ --check --cache --cache-location ../../.cache/prettier/{projectRoot} --cache-strategy metadata || (echo 'ERROR: Prettier formatting check failed! Fix with: npx nx lint twenty-server --configuration=fix' && false))"
},
"configurations": {
"ci": {},
"fix": {
"command": "npx oxlint --type-aware --fix -c .oxlintrc.json src/"
"command": "npx oxlint --type-aware --fix -c .oxlintrc.json src/ && prettier src/ --write --cache --cache-location ../../.cache/prettier/{projectRoot} --cache-strategy metadata"
}
}
},

View file

@ -30,14 +30,14 @@ export class MyService {
// Track a workspace event
auditService.insertWorkspaceEvent(CUSTOM_DOMAIN_ACTIVATED_EVENT, {});
// Track an object event
auditService.createObjectEvent(OBJECT_RECORD_CREATED_EVENT, {
recordId: 'record-id',
objectMetadataId: 'object-metadata-id',
// other properties
});
// Track a pageview
auditService.createPageviewEvent('page-name', {
href: '/path',
@ -82,12 +82,14 @@ Then update the `events.type.ts` file:
```typescript
// src/engine/core-modules/analytics/types/events.type.ts
import { MY_EVENT, MyEventTrackEvent } from '../utils/events/track/my-feature/my-event';
import {
MY_EVENT,
MyEventTrackEvent,
} from '../utils/events/track/my-feature/my-event';
// Add to the union type
export type TrackEventName =
| typeof MY_EVENT
// ... other event names;
export type TrackEventName = typeof MY_EVENT;
// ... other event names;
// Add to the TrackEvents interface
export interface TrackEvents {
@ -96,9 +98,8 @@ export interface TrackEvents {
}
// The TrackEventProperties type will automatically use the new event
export type TrackEventProperties<T extends TrackEventName> = T extends keyof TrackEvents
? TrackEvents[T]['properties']
: object;
export type TrackEventProperties<T extends TrackEventName> =
T extends keyof TrackEvents ? TrackEvents[T]['properties'] : object;
```
## API
@ -136,9 +137,8 @@ export interface TrackEvents {
}
// Use the mapping to extract properties for each event type
export type TrackEventProperties<T extends TrackEventName> = T extends keyof TrackEvents
? TrackEvents[T]['properties']
: object;
export type TrackEventProperties<T extends TrackEventName> =
T extends keyof TrackEvents ? TrackEvents[T]['properties'] : object;
```
This approach makes it easier to add new events without having to modify a complex nested conditional type.

View file

@ -3,6 +3,4 @@ var main = async (params) => {
const message = `Hello, input: ${a} and ${b}`;
return { message };
};
export {
main
};
export { main };

View file

@ -8,7 +8,10 @@
"ignorePatterns": ["node_modules", "dist"],
"rules": {
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
"no-console": ["warn", { "allow": ["group", "groupCollapsed", "groupEnd"] }],
"no-console": [
"warn",
{ "allow": ["group", "groupCollapsed", "groupEnd"] }
],
"no-control-regex": "off",
"no-debugger": "error",
"no-duplicate-imports": "error",
@ -18,28 +21,43 @@
"import/no-duplicates": "error",
"typescript/no-redeclare": "error",
"typescript/ban-ts-comment": "error",
"typescript/consistent-type-imports": ["error", {
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}],
"typescript/consistent-type-imports": [
"error",
{
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}
],
"typescript/explicit-function-return-type": "off",
"typescript/explicit-module-boundary-types": "off",
"typescript/no-empty-object-type": ["error", {
"allowInterfaces": "with-single-extends"
}],
"typescript/no-empty-object-type": [
"error",
{
"allowInterfaces": "with-single-extends"
}
],
"typescript/no-empty-function": "off",
"typescript/no-explicit-any": "off",
"typescript/no-unused-vars": ["warn", {
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}],
"twenty/enforce-module-boundaries": ["error", {
"depConstraints": [
{ "sourceTag": "scope:shared", "onlyDependOnLibsWithTags": ["scope:shared"] }
]
}]
"typescript/no-unused-vars": [
"warn",
{
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}
],
"twenty/enforce-module-boundaries": [
"error",
{
"depConstraints": [
{
"sourceTag": "scope:shared",
"onlyDependOnLibsWithTags": ["scope:shared"]
}
]
}
]
},
"overrides": [
{

View file

@ -3,21 +3,13 @@
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/twenty-shared/src",
"projectType": "library",
"tags": [
"scope:shared"
],
"tags": ["scope:shared"],
"targets": {
"build": {
"executor": "nx:run-commands",
"cache": true,
"inputs": [
"production",
"^production"
],
"dependsOn": [
"generateBarrels",
"^build"
],
"inputs": ["production", "^production"],
"dependsOn": ["generateBarrels", "^build"],
"outputs": [
"{projectRoot}/dist",
"{projectRoot}/ai/package.json",
@ -59,16 +51,9 @@
"build:individual": {
"executor": "nx:run-commands",
"cache": true,
"dependsOn": [
"build"
],
"inputs": [
"production",
"^production"
],
"outputs": [
"{projectRoot}/dist/individual"
],
"dependsOn": ["build"],
"inputs": ["production", "^production"],
"outputs": ["{projectRoot}/dist/individual"],
"options": {
"cwd": "{projectRoot}",
"command": "npx vite build -c vite.config.individual.ts"
@ -77,34 +62,17 @@
"generateBarrels": {
"executor": "nx:run-commands",
"cache": true,
"inputs": [
"production",
"{projectRoot}/scripts/generateBarrels.ts"
],
"inputs": ["production", "{projectRoot}/scripts/generateBarrels.ts"],
"outputs": [
"{projectRoot}/src/index.ts",
"{projectRoot}/src/*/index.ts",
"{projectRoot}/package.json"
],
"options": {
"command": "tsx {projectRoot}/scripts/generateBarrels.ts"
}
"options": { "command": "tsx {projectRoot}/scripts/generateBarrels.ts" }
},
"typecheck": {},
"test": {},
"lint": {
"options": {},
"configurations": {
"fix": {}
}
},
"fmt": {
"options": {
"files": "src"
},
"configurations": {
"fix": {}
}
}
"lint": { "options": {}, "configurations": { "fix": {} } },
"fmt": { "options": { "files": "src" }, "configurations": { "fix": {} } }
}
}

View file

@ -151,7 +151,10 @@ type WriteInJsonFileArgs = {
};
const updateJsonFile = ({ content, file }: WriteInJsonFileArgs) => {
const updatedJsonFile = JSON.stringify(content);
const formattedContent = prettierFormat(updatedJsonFile, 'json-stringify');
const formattedContent = prettier.format(updatedJsonFile, {
...prettierConfiguration,
filepath: file,
});
fs.writeFileSync(file, formattedContent, 'utf-8');
};

View file

@ -9,4 +9,3 @@ export * from './types';
export * from './utils';
export * from './workflow';
export * from './workspace';

View file

@ -21,7 +21,5 @@
"jest.config.mjs",
"vite.config.*.ts"
],
"exclude": [
"src/individual-entry.ts"
]
"exclude": ["src/individual-entry.ts"]
}

View file

@ -5,13 +5,13 @@
"categories": {
"correctness": "off"
},
"ignorePatterns": [
"node_modules",
"generated"
],
"ignorePatterns": ["node_modules", "generated"],
"rules": {
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
"no-console": ["warn", { "allow": ["group", "groupCollapsed", "groupEnd"] }],
"no-console": [
"warn",
{ "allow": ["group", "groupCollapsed", "groupEnd"] }
],
"no-control-regex": "off",
"no-debugger": "error",
"no-duplicate-imports": "error",
@ -35,31 +35,43 @@
"typescript/no-redeclare": "error",
"typescript/ban-ts-comment": "error",
"typescript/consistent-type-imports": ["error", {
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}],
"typescript/consistent-type-imports": [
"error",
{
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}
],
"typescript/explicit-function-return-type": "off",
"typescript/explicit-module-boundary-types": "off",
"typescript/no-empty-object-type": ["error", {
"allowInterfaces": "with-single-extends"
}],
"typescript/no-empty-object-type": [
"error",
{
"allowInterfaces": "with-single-extends"
}
],
"typescript/no-empty-function": "off",
"typescript/no-explicit-any": "off",
"typescript/no-unused-vars": ["warn", {
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}],
"typescript/no-unused-vars": [
"warn",
{
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}
],
"twenty/enforce-module-boundaries": ["error", {
"depConstraints": [
{
"sourceTag": "scope:shared",
"onlyDependOnLibsWithTags": ["scope:shared"]
}
]
}]
"twenty/enforce-module-boundaries": [
"error",
{
"depConstraints": [
{
"sourceTag": "scope:shared",
"onlyDependOnLibsWithTags": ["scope:shared"]
}
]
}
]
}
}

View file

@ -0,0 +1,3 @@
dist
storybook-static
coverage

View file

@ -43,8 +43,7 @@ const config: StorybookConfig = {
...viteConfig.resolve,
alias: {
...(viteConfig.resolve?.alias ?? {}),
'@tabler/icons-react':
'@tabler/icons-react/dist/esm/icons/index.mjs',
'@tabler/icons-react': '@tabler/icons-react/dist/esm/icons/index.mjs',
},
},
};

View file

@ -1,5 +1,5 @@
<style>
[data-is-storybook="true"] {
background-color: transparent!important;
}
</style>
[data-is-storybook='true'] {
background-color: transparent !important;
}
</style>

View file

@ -6,29 +6,29 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.3.7/iframeResizer.contentWindow.min.js"></script>
<style type="text/css">
body {
margin: 0;
font-family: 'Inter', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html {
font-size: 13px;
}
.sbdocs-wrapper {
padding: 0 !important;
}
*::-webkit-scrollbar {
height: 4px;
width: 4px;
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html {
font-size: 13px;
}
.sbdocs-wrapper {
padding: 0 !important;
}
*::-webkit-scrollbar {
height: 4px;
width: 4px;
}
*::-webkit-scrollbar-corner {
background-color: transparent;
}
*::-webkit-scrollbar-corner {
background-color: transparent;
}
*::-webkit-scrollbar-thumb {
background-color: transparent;
border-radius: 2px;
}
</style>
*::-webkit-scrollbar-thumb {
background-color: transparent;
border-radius: 2px;
}
</style>

View file

@ -3,15 +3,10 @@
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/twenty-ui/src",
"projectType": "library",
"tags": [
"scope:shared"
],
"tags": ["scope:shared"],
"targets": {
"build": {
"dependsOn": [
"^build",
"generateBarrels"
],
"dependsOn": ["^build", "generateBarrels"],
"outputs": [
"{projectRoot}/dist",
"{projectRoot}/accessibility/package.json",
@ -44,16 +39,10 @@
},
"generateThemeConstants": {
"executor": "nx:run-commands",
"dependsOn": [
"build"
],
"dependsOn": ["build"],
"cache": true,
"inputs": [
"{projectRoot}/scripts/generateThemeConstants.ts"
],
"outputs": [
"{projectRoot}/src/theme-constants/generated"
],
"inputs": ["{projectRoot}/scripts/generateThemeConstants.ts"],
"outputs": ["{projectRoot}/src/theme-constants/generated"],
"options": {
"command": "tsx {projectRoot}/scripts/generateThemeConstants.ts"
}
@ -61,31 +50,19 @@
"generateBarrels": {
"executor": "nx:run-commands",
"cache": true,
"inputs": [
"production",
"{projectRoot}/scripts/generateBarrels.ts"
],
"inputs": ["production", "{projectRoot}/scripts/generateBarrels.ts"],
"outputs": [
"{projectRoot}/src/**/*/index.ts",
"{projectRoot}/package.json"
],
"options": {
"command": "tsx {projectRoot}/scripts/generateBarrels.ts"
}
"options": { "command": "tsx {projectRoot}/scripts/generateBarrels.ts" }
},
"build:individual": {
"executor": "nx:run-commands",
"cache": true,
"dependsOn": [
"build"
],
"inputs": [
"production",
"^production"
],
"outputs": [
"{projectRoot}/dist/individual"
],
"dependsOn": ["build"],
"inputs": ["production", "^production"],
"outputs": ["{projectRoot}/dist/individual"],
"options": {
"cwd": "{projectRoot}",
"command": "npx vite build -c vite.config.individual.ts"
@ -93,39 +70,17 @@
},
"clean": {
"executor": "nx:run-commands",
"options": {
"command": "rimraf {projectRoot}/dist"
}
"options": { "command": "rimraf {projectRoot}/dist" }
},
"lint": {},
"fmt": {
"options": {
"files": "src"
},
"configurations": {
"fix": {}
}
},
"fmt": { "options": { "files": "src" }, "configurations": { "fix": {} } },
"test": {},
"typecheck": {},
"storybook:build": {
"configurations": {
"test": {}
}
},
"storybook:serve:dev": {
"options": {
"port": 6007
}
},
"storybook:build": { "configurations": { "test": {} } },
"storybook:serve:dev": { "options": { "port": 6007 } },
"storybook:serve:static": {
"options": {
"buildTarget": "twenty-ui:storybook:build",
"port": 6007
},
"configurations": {
"test": {}
}
"options": { "buildTarget": "twenty-ui:storybook:build", "port": 6007 },
"configurations": { "test": {} }
},
"storybook:test": {},
"storybook:test:no-coverage": {},

View file

@ -151,7 +151,10 @@ type WriteInJsonFileArgs = {
};
const updateJsonFile = ({ content, file }: WriteInJsonFileArgs) => {
const updatedJsonFile = JSON.stringify(content);
const formattedContent = prettierFormat(updatedJsonFile, 'json-stringify');
const formattedContent = prettier.format(updatedJsonFile, {
...prettierConfiguration,
filepath: file,
});
fs.writeFileSync(file, formattedContent, 'utf-8');
};

View file

@ -110,8 +110,16 @@
--t-background-overlay-primary: #000000b8;
--t-background-overlay-secondary: #0000005c;
--t-background-overlay-tertiary: #0000005c;
--t-background-radial-gradient: radial-gradient(50% 62.62% at 50% 0%, color(display-p3 0.506 0.506 0.506) 0%, color(display-p3 0.482 0.482 0.482) 100%);
--t-background-radial-gradient-hover: radial-gradient(76.32% 95.59% at 50% 0%, color(display-p3 0.482 0.482 0.482) 0%, color(display-p3 0.702 0.702 0.702) 100%);
--t-background-radial-gradient: radial-gradient(
50% 62.62% at 50% 0%,
color(display-p3 0.506 0.506 0.506) 0%,
color(display-p3 0.482 0.482 0.482) 100%
);
--t-background-radial-gradient-hover: radial-gradient(
76.32% 95.59% at 50% 0%,
color(display-p3 0.482 0.482 0.482) 0%,
color(display-p3 0.702 0.702 0.702) 100%
);
--t-background-primary-inverted: color(display-p3 0.922 0.922 0.922);
--t-background-primary-inverted-hover: color(display-p3 0.702 0.702 0.702);
--t-blur-light: blur(6px) saturate(200%) contrast(100%) brightness(130%);
@ -132,11 +140,14 @@
--t-border-radius-xxl: 40px;
--t-border-radius-pill: 999px;
--t-border-radius-rounded: 100%;
--t-box-shadow-color: rgba(0,0,0,0.6);
--t-box-shadow-light: 0px 2px 4px 0px rgba(0,0,0,0.04), 0px 0px 4px 0px rgba(0,0,0,0.08);
--t-box-shadow-strong: 2px 4px 16px 0px rgba(0,0,0,0.16), 0px 2px 4px 0px rgba(0,0,0,0.08);
--t-box-shadow-underline: 0px 1px 0px 0px rgba(0,0,0,0.32);
--t-box-shadow-super-heavy: 2px 4px 16px 0px rgba(0,0,0,0.12), 0px 2px 4px 0px rgba(0,0,0,0.04);
--t-box-shadow-color: rgba(0, 0, 0, 0.6);
--t-box-shadow-light: 0px 2px 4px 0px rgba(0, 0, 0, 0.04),
0px 0px 4px 0px rgba(0, 0, 0, 0.08);
--t-box-shadow-strong: 2px 4px 16px 0px rgba(0, 0, 0, 0.16),
0px 2px 4px 0px rgba(0, 0, 0, 0.08);
--t-box-shadow-underline: 0px 1px 0px 0px rgba(0, 0, 0, 0.32);
--t-box-shadow-super-heavy: 2px 4px 16px 0px rgba(0, 0, 0, 0.12),
0px 2px 4px 0px rgba(0, 0, 0, 0.04);
--t-font-color-primary: color(display-p3 0.922 0.922 0.922);
--t-font-color-secondary: color(display-p3 0.702 0.702 0.702);
--t-font-color-tertiary: color(display-p3 0.506 0.506 0.506);

View file

@ -110,8 +110,16 @@
--t-background-overlay-primary: color(display-p3 0 0 0 / 0.722);
--t-background-overlay-secondary: color(display-p3 0 0 0 / 0.361);
--t-background-overlay-tertiary: color(display-p3 0 0 0 / 0.071);
--t-background-radial-gradient: radial-gradient(50% 62.62% at 50% 0%, color(display-p3 0.6 0.6 0.6) 0%, color(display-p3 0.514 0.514 0.514) 100%);
--t-background-radial-gradient-hover: radial-gradient(76.32% 95.59% at 50% 0%, color(display-p3 0.514 0.514 0.514) 0%, color(display-p3 0.4 0.4 0.4) 100%);
--t-background-radial-gradient: radial-gradient(
50% 62.62% at 50% 0%,
color(display-p3 0.6 0.6 0.6) 0%,
color(display-p3 0.514 0.514 0.514) 100%
);
--t-background-radial-gradient-hover: radial-gradient(
76.32% 95.59% at 50% 0%,
color(display-p3 0.514 0.514 0.514) 0%,
color(display-p3 0.4 0.4 0.4) 100%
);
--t-background-primary-inverted: color(display-p3 0.2 0.2 0.2);
--t-background-primary-inverted-hover: color(display-p3 0.4 0.4 0.4);
--t-blur-light: blur(6px) saturate(200%) contrast(50%) brightness(130%);
@ -133,10 +141,14 @@
--t-border-radius-pill: 999px;
--t-border-radius-rounded: 100%;
--t-box-shadow-color: color(display-p3 0 0 0 / 0.039);
--t-box-shadow-light: 0px 2px 4px 0px color(display-p3 0 0 0 / 0.039), 0px 0px 4px 0px color(display-p3 0 0 0 / 0.078);
--t-box-shadow-strong: 2px 4px 16px 0px color(display-p3 0 0 0 / 0.161), 0px 2px 4px 0px color(display-p3 0 0 0 / 0.078);
--t-box-shadow-light: 0px 2px 4px 0px color(display-p3 0 0 0 / 0.039),
0px 0px 4px 0px color(display-p3 0 0 0 / 0.078);
--t-box-shadow-strong: 2px 4px 16px 0px color(display-p3 0 0 0 / 0.161),
0px 2px 4px 0px color(display-p3 0 0 0 / 0.078);
--t-box-shadow-underline: 0px 1px 0px 0px color(display-p3 0 0 0 / 0.361);
--t-box-shadow-super-heavy: 0px 0px 8px 0px color(display-p3 0 0 0 / 0.161), 0px 8px 64px -16px color(display-p3 0 0 0 / 0.478), 0px 24px 56px -16px color(display-p3 0 0 0 / 0.078);
--t-box-shadow-super-heavy: 0px 0px 8px 0px color(display-p3 0 0 0 / 0.161),
0px 8px 64px -16px color(display-p3 0 0 0 / 0.478),
0px 24px 56px -16px color(display-p3 0 0 0 / 0.078);
--t-font-color-primary: color(display-p3 0.2 0.2 0.2);
--t-font-color-secondary: color(display-p3 0.4 0.4 0.4);
--t-font-color-tertiary: color(display-p3 0.6 0.6 0.6);

View file

@ -63,8 +63,7 @@ export default defineConfig(({ command }) => {
alias: {
'@ui/': path.resolve(__dirname, 'src') + '/',
'@assets/': path.resolve(__dirname, 'src/assets') + '/',
'@tabler/icons-react':
'@tabler/icons-react/dist/esm/icons/index.mjs',
'@tabler/icons-react': '@tabler/icons-react/dist/esm/icons/index.mjs',
},
},
css: {

View file

@ -17,23 +17,32 @@
"import/no-duplicates": "error",
"typescript/no-redeclare": "error",
"typescript/ban-ts-comment": "error",
"typescript/consistent-type-imports": ["error", {
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}],
"typescript/consistent-type-imports": [
"error",
{
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}
],
"typescript/explicit-function-return-type": "off",
"typescript/explicit-module-boundary-types": "off",
"typescript/no-empty-object-type": ["error", {
"allowInterfaces": "with-single-extends"
}],
"typescript/no-empty-object-type": [
"error",
{
"allowInterfaces": "with-single-extends"
}
],
"typescript/no-empty-function": "off",
"typescript/no-explicit-any": "off",
"typescript/no-unused-vars": ["warn", {
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}],
"typescript/no-unused-vars": [
"warn",
{
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}
],
"react/no-unescaped-entities": "off",
"react/prop-types": "off",
"react/jsx-key": "off",

View file

@ -1,16 +1,21 @@
## 2.2.0
* Fix authentication issue
- Fix authentication issue
## 2.1.1
* Add changelog
- Add changelog
## 2.1.0
* Fix some authentication issues
* Remove position from input fields
* Fix required boolean fields that should not be
- Fix some authentication issues
- Remove position from input fields
- Fix required boolean fields that should not be
## 2.0.0
* First release
- First release
## 1.0.0
Initial release to public.

View file

@ -23,9 +23,7 @@
"executor": "nx:run-commands",
"options": {
"cwd": "{projectRoot}",
"commands": [
"NODE_ENV=test jest --testTimeout 10000"
]
"commands": ["NODE_ENV=test jest --testTimeout 10000"]
}
},
"lint": {},

View file

@ -55779,6 +55779,12 @@ __metadata:
languageName: unknown
linkType: soft
"twenty-oxlint-rules@workspace:packages/twenty-oxlint-rules":
version: 0.0.0-use.local
resolution: "twenty-oxlint-rules@workspace:packages/twenty-oxlint-rules"
languageName: unknown
linkType: soft
"twenty-sdk@workspace:*, twenty-sdk@workspace:packages/twenty-sdk":
version: 0.0.0-use.local
resolution: "twenty-sdk@workspace:packages/twenty-sdk"