Compare commits
112
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80e675f430 | ||
|
|
29091ca369 | ||
|
|
5e71b7cb9c | ||
|
|
a641fbe804 | ||
|
|
f536c4d8b3 | ||
|
|
5ab3eeb830 | ||
|
|
75bb3a904d | ||
|
|
82a1179e23 | ||
|
|
662de17644 | ||
|
|
1993614637 | ||
|
|
a3aae5c857 | ||
|
|
22a203680e | ||
|
|
36bcc71f3d | ||
|
|
06bdb5ad6a | ||
|
|
a9696705c1 | ||
|
|
0acc08c333 | ||
|
|
c2a79fc0c2 | ||
|
|
5b28e59ca7 | ||
|
|
66d93c4d28 | ||
|
|
2c69102f15 | ||
|
|
fea47aa9f8 | ||
|
|
73268535dc | ||
|
|
02bd71052b | ||
|
|
faee5ee63d | ||
|
|
06451407ce | ||
|
|
59029a0035 | ||
|
|
cac4999e9f | ||
|
|
403db7ad3f | ||
|
|
ef499b6d47 | ||
|
|
9f9a6a45dd | ||
|
|
1f1da901ea | ||
|
|
f65aafe96b | ||
|
|
a79b816117 | ||
|
|
d37ed7e07c | ||
|
|
d825ac06dd | ||
|
|
1c898f36d6 | ||
|
|
364c944ca6 | ||
|
|
aa062644c8 | ||
|
|
3d7cb4499f | ||
|
|
9d808302aa | ||
|
|
514d0017ea | ||
|
|
90cced0e74 | ||
|
|
4d0b8a8644 | ||
|
|
9d57bc39e5 | ||
|
|
b421efbff7 | ||
|
|
e27a8b5107 | ||
|
|
4797f97a95 | ||
|
|
2f3399fd5f | ||
|
|
61f9cf9260 | ||
|
|
ef003fb929 | ||
|
|
62a634831d | ||
|
|
1ec7244d1b | ||
|
|
13e569eaac | ||
|
|
1affa1e004 | ||
|
|
c53a13417e | ||
|
|
e5e3132ddd | ||
|
|
4965790ecc | ||
|
|
fc2b1de860 | ||
|
|
12257f4cc7 | ||
|
|
57d8954973 | ||
|
|
cfeea43eaf | ||
|
|
5853891b02 | ||
|
|
38ad0820c0 | ||
|
|
647c32ff3e | ||
|
|
7293d4c1f8 | ||
|
|
1acbf28316 | ||
|
|
1decd40eea | ||
|
|
1b9d188e4a | ||
|
|
57499342f1 | ||
|
|
ecbc0ac013 | ||
|
|
c571473d67 | ||
|
|
2a82df7073 | ||
|
|
a2f80d882b | ||
|
|
abd9709291 | ||
|
|
9d4ff7820d | ||
|
|
4cfd738312 | ||
|
|
0e89c96170 | ||
|
|
bfa50f566e | ||
|
|
338a38682d | ||
|
|
6a2e0182ab | ||
|
|
72086fe111 | ||
|
|
228865bd94 | ||
|
|
2493adbb87 | ||
|
|
26f0a416a1 | ||
|
|
c41a8e2b23 | ||
|
|
4c001778c2 | ||
|
|
911a46aa45 | ||
|
|
c53d281960 | ||
|
|
7f6b270a76 | ||
|
|
c94657dc0a | ||
|
|
aeedcf3353 | ||
|
|
eda905f271 | ||
|
|
be01a85d67 | ||
|
|
999dcd4468 | ||
|
|
b11f77df2a | ||
|
|
f09a9cc25a | ||
|
|
d59d2efb8b | ||
|
|
07803f232f | ||
|
|
aaa483c020 | ||
|
|
845a1934d3 | ||
|
|
c97d872b9f | ||
|
|
906a0aed38 | ||
|
|
80d054563e | ||
|
|
5b544809f7 | ||
|
|
3b2bf39565 | ||
|
|
995793c0ac | ||
|
|
225f185278 | ||
|
|
ca1d49c6cd | ||
|
|
2f9c94d9a0 | ||
|
|
a9c4920fcc | ||
|
|
07dd27f6c4 | ||
|
|
7a2e397ad1 |
@@ -8,7 +8,7 @@ alwaysApply: true
|
||||
## Formatting Standards
|
||||
- **Prettier**: 2-space indentation, single quotes, trailing commas, semicolons
|
||||
- **Print width**: 80 characters
|
||||
- **ESLint**: No unused imports, consistent import ordering, prefer const over let
|
||||
- **Oxlint**: No unused imports, consistent import ordering, prefer const over let
|
||||
|
||||
## Naming Conventions
|
||||
```typescript
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
description: ESM dependency guidelines for twenty-sdk and create-twenty-app packages
|
||||
globs: ["packages/twenty-sdk/**", "packages/create-twenty-app/**"]
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# ESM Dependency Guidelines
|
||||
|
||||
## Context
|
||||
|
||||
`twenty-sdk` and `create-twenty-app` are published as dual-format npm packages (ESM `.mjs` + CJS `.cjs`). Dependencies listed in `dependencies` are **externalized** by the Vite/Rollup build — they are not bundled, and consumers resolve them from `node_modules` at runtime.
|
||||
|
||||
This means **CJS-only dependencies break the ESM output**. When Rollup emits `import { foo } from 'cjs-package'`, Node.js ESM cannot resolve named exports from CommonJS modules, causing `SyntaxError: Named export 'foo' not found`.
|
||||
|
||||
## Rules
|
||||
|
||||
### Only add ESM-compatible dependencies
|
||||
|
||||
Before adding a new dependency to `package.json`, verify it supports ESM:
|
||||
- Check for `"type": "module"` in its `package.json`
|
||||
- Or check for an `"exports"` map with ESM entries
|
||||
- Or check for a `"module"` field pointing to an ESM build
|
||||
|
||||
### Use native `node:fs/promises` for standard fs operations
|
||||
|
||||
```typescript
|
||||
// ✅ Import native fs functions directly
|
||||
import { readFile, writeFile, mkdir, rm, cp } from 'node:fs/promises';
|
||||
import { createWriteStream, existsSync } from 'node:fs';
|
||||
|
||||
// ✅ Import only custom helpers from fs-utils (no native re-exports)
|
||||
import { pathExists, ensureDir, emptyDir, copy, move, remove, readJson, writeJson, ensureFile } from '@/cli/utilities/file/fs-utils';
|
||||
|
||||
// ❌ Don't use fs-extra (CJS-only, breaks ESM bundle)
|
||||
import * as fs from 'fs-extra';
|
||||
|
||||
// ❌ Don't use import * as fs from fs-utils (it doesn't re-export native fs)
|
||||
import * as fs from '@/cli/utilities/file/fs-utils';
|
||||
```
|
||||
|
||||
### Use `@/cli/utilities/string/kebab-case` instead of lodash
|
||||
|
||||
```typescript
|
||||
// ✅ Use internal utility
|
||||
import { kebabCase } from '@/cli/utilities/string/kebab-case';
|
||||
|
||||
// ❌ Don't use lodash single-function packages (CJS-only, unmaintained)
|
||||
import kebabCase from 'lodash.kebabcase';
|
||||
```
|
||||
|
||||
### When no ESM alternative exists
|
||||
|
||||
If a CJS-only package has no ESM replacement (e.g. `archiver`), add it to the `cjsOnlyPackages` list in `vite.config.node.ts` so it gets inlined into the bundle instead of externalized.
|
||||
@@ -15,8 +15,16 @@ inputs:
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Fetch main branch for diff
|
||||
shell: bash
|
||||
run: git fetch origin main --depth=1
|
||||
- name: Get last successful commit
|
||||
if: env.NX_BASE == ''
|
||||
uses: nrwl/nx-set-shas@v4
|
||||
- name: Fallback to origin/main if no base found
|
||||
if: env.NX_BASE == ''
|
||||
shell: bash
|
||||
run: echo "NX_BASE=$(git rev-parse origin/main)" >> $GITHUB_ENV
|
||||
- name: Run affected command
|
||||
shell: bash
|
||||
env:
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-depth: 10
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v45
|
||||
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
- name: Checkout current branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-depth: 10
|
||||
|
||||
- name: Try to merge main into current branch
|
||||
id: merge_attempt
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build
|
||||
|
||||
@@ -21,7 +21,6 @@ jobs:
|
||||
files: |
|
||||
package.json
|
||||
packages/twenty-docs/**
|
||||
eslint.config.mjs
|
||||
|
||||
docs-lint:
|
||||
needs: changed-files-check
|
||||
@@ -37,11 +36,11 @@ jobs:
|
||||
- name: Fetch local actions
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-depth: 10
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/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
|
||||
- name: Docs / Lint
|
||||
run: npx nx lint twenty-docs
|
||||
|
||||
|
||||
@@ -25,12 +25,12 @@ jobs:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build twenty-emails
|
||||
|
||||
+32
-165
@@ -1,8 +1,7 @@
|
||||
name: CI Front and E2E
|
||||
name: CI Front
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
merge_group:
|
||||
|
||||
permissions:
|
||||
@@ -19,6 +18,7 @@ env:
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
if: github.event_name != 'merge_group'
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
@@ -29,15 +29,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'
|
||||
@@ -53,7 +44,7 @@ jobs:
|
||||
- name: Fetch local actions
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Diagnostic disk space issue
|
||||
@@ -62,13 +53,19 @@ jobs:
|
||||
run: npx nx reset:env twenty-front
|
||||
- name: Front / Build storybook
|
||||
run: npx nx storybook:build twenty-front
|
||||
- name: Upload storybook build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: storybook-static
|
||||
path: packages/twenty-front/storybook-static
|
||||
retention-days: 1
|
||||
- name: Save storybook build cache
|
||||
uses: ./.github/actions/save-cache
|
||||
with:
|
||||
key: ${{ env.STORYBOOK_BUILD_CACHE_KEY_FOR_SAVE_ACTION }}
|
||||
front-sb-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
runs-on: ubuntu-latest
|
||||
needs: front-sb-build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -78,26 +75,41 @@ jobs:
|
||||
env:
|
||||
SHARD_COUNTER: 4
|
||||
REACT_APP_SERVER_BASE_URL: http://localhost:3000
|
||||
STORYBOOK_URL: http://localhost:6006
|
||||
steps:
|
||||
- name: Fetch local actions
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Restore storybook build cache
|
||||
uses: ./.github/actions/restore-cache
|
||||
with:
|
||||
key: ${{ env.STORYBOOK_BUILD_CACHE_KEY_FOR_RESTORE_ACTION }}
|
||||
- name: Clean stale storybook vitest cache
|
||||
run: rm -rf packages/twenty-front/node_modules/.cache/storybook
|
||||
- name: Build dependencies
|
||||
run: |
|
||||
npx nx build twenty-shared
|
||||
npx nx build twenty-ui
|
||||
npx nx build twenty-sdk
|
||||
- name: Download storybook build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-static
|
||||
path: packages/twenty-front/storybook-static
|
||||
- name: Install Playwright
|
||||
run: |
|
||||
cd packages/twenty-front
|
||||
npx playwright install
|
||||
- name: Front / Write .env
|
||||
run: npx nx reset:env twenty-front
|
||||
- name: Run storybook tests
|
||||
run: npx nx storybook:test twenty-front --configuration=${{ matrix.storybook_scope }} --shard=${{ matrix.shard }}/${{ env.SHARD_COUNTER }}
|
||||
- name: Serve storybook & run tests
|
||||
run: |
|
||||
npx http-server packages/twenty-front/storybook-static --port 6006 --silent &
|
||||
timeout 30 bash -c 'until curl -sf http://localhost:6006 > /dev/null 2>&1; do sleep 1; done'
|
||||
npx nx storybook:test twenty-front --configuration=${{ matrix.storybook_scope }} --shard=${{ matrix.shard }}/${{ env.SHARD_COUNTER }}
|
||||
# - name: Rename coverage file
|
||||
# run: |
|
||||
# if [ -f "packages/twenty-front/coverage/storybook/coverage-final.json" ]; then
|
||||
@@ -125,7 +137,7 @@ jobs:
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# with:
|
||||
# fetch-depth: 0
|
||||
# fetch-depth: 10
|
||||
# - name: Install dependencies
|
||||
# uses: ./.github/actions/yarn-install
|
||||
# - uses: actions/download-artifact@v4
|
||||
@@ -139,32 +151,6 @@ jobs:
|
||||
# npx nyc merge coverage-artifacts ${{ env.PATH_TO_COVERAGE }}/coverage-storybook.json
|
||||
# - name: Checking coverage
|
||||
# run: npx nx storybook:coverage twenty-front --checkCoverage=true --configuration=${{ matrix.storybook_scope }}
|
||||
front-chromatic-deployment:
|
||||
timeout-minutes: 30
|
||||
if: false
|
||||
needs: front-sb-build
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
env:
|
||||
REACT_APP_SERVER_BASE_URL: http://127.0.0.1:3000
|
||||
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Restore storybook build cache
|
||||
uses: ./.github/actions/restore-cache
|
||||
with:
|
||||
key: ${{ env.STORYBOOK_BUILD_CACHE_KEY_FOR_RESTORE_ACTION }}
|
||||
- name: Front / Write .env
|
||||
run: |
|
||||
cd packages/twenty-front
|
||||
touch .env
|
||||
echo "" >> .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'
|
||||
@@ -184,7 +170,7 @@ jobs:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Restore ${{ matrix.task }} cache
|
||||
@@ -214,6 +200,7 @@ jobs:
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
env:
|
||||
NODE_OPTIONS: "--max-old-space-size=10240"
|
||||
ANALYZE: "true"
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/cancel-workflow-action@0.11.0
|
||||
@@ -222,7 +209,7 @@ jobs:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Front / Write .env
|
||||
@@ -235,117 +222,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: 0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: Check system resources
|
||||
run: |
|
||||
echo "Available memory:"
|
||||
free -h
|
||||
echo "Available disk space:"
|
||||
df -h
|
||||
echo "CPU info:"
|
||||
lscpu
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/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
|
||||
@@ -363,12 +239,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
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
name: CI Merge Queue
|
||||
|
||||
on:
|
||||
merge_group:
|
||||
|
||||
pull_request:
|
||||
types: [labeled, synchronize, opened, reopened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'merge_group' && github.event.merge_group.base_ref || 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: Restore Nx build cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
key: v4-e2e-build-${{ github.ref_name }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
v4-e2e-build-${{ github.ref_name }}-
|
||||
v4-e2e-build-main-
|
||||
path: |
|
||||
.nx
|
||||
node_modules/.cache
|
||||
packages/*/node_modules/.cache
|
||||
|
||||
- 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: Save Nx build cache
|
||||
if: always()
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
key: v4-e2e-build-${{ github.ref_name }}-${{ github.sha }}
|
||||
path: |
|
||||
.nx
|
||||
node_modules/.cache
|
||||
packages/*/node_modules/.cache
|
||||
|
||||
- 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
|
||||
@@ -1,9 +1,8 @@
|
||||
name: CI SDK
|
||||
|
||||
on:
|
||||
merge_group:
|
||||
|
||||
pull_request:
|
||||
merge_group:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -14,10 +13,12 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
if: github.event_name != 'merge_group'
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
packages/twenty-sdk/**
|
||||
packages/twenty-server/**
|
||||
!packages/twenty-sdk/package.json
|
||||
sdk-test:
|
||||
needs: changed-files-check
|
||||
@@ -35,7 +36,7 @@ jobs:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build
|
||||
@@ -50,7 +51,7 @@ jobs:
|
||||
tasks: ${{ matrix.task }}
|
||||
sdk-e2e-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, sdk-test]
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
services:
|
||||
@@ -78,7 +79,7 @@ jobs:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build
|
||||
|
||||
@@ -2,7 +2,6 @@ name: CI Server
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
merge_group:
|
||||
|
||||
permissions:
|
||||
@@ -13,10 +12,11 @@ concurrency:
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
env:
|
||||
SERVER_SETUP_CACHE_KEY: server-setup
|
||||
SERVER_BUILD_CACHE_KEY: server-build
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
if: github.event_name != 'merge_group'
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
@@ -25,13 +25,62 @@ jobs:
|
||||
packages/twenty-server/**
|
||||
packages/twenty-front/src/generated/**
|
||||
packages/twenty-front/src/generated-metadata/**
|
||||
packages/twenty-sdk/src/clients/generated/metadata/**
|
||||
packages/twenty-emails/**
|
||||
packages/twenty-shared/**
|
||||
server-setup:
|
||||
|
||||
server-build:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Restore server build cache
|
||||
id: restore-server-build-cache
|
||||
uses: ./.github/actions/restore-cache
|
||||
with:
|
||||
key: ${{ env.SERVER_BUILD_CACHE_KEY }}
|
||||
- name: Build twenty-shared
|
||||
run: npx nx build twenty-shared
|
||||
- name: Server / Write .env
|
||||
run: npx nx reset:env twenty-server
|
||||
- name: Server / Build
|
||||
run: npx nx build twenty-server
|
||||
- name: Save server build cache
|
||||
uses: ./.github/actions/save-cache
|
||||
with:
|
||||
key: ${{ steps.restore-server-build-cache.outputs.cache-primary-key }}
|
||||
|
||||
server-lint-typecheck:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build twenty-shared
|
||||
run: npx nx build twenty-shared
|
||||
- name: Server / Run lint & typecheck
|
||||
uses: ./.github/actions/nx-affected
|
||||
with:
|
||||
tag: scope:backend
|
||||
tasks: lint,typecheck
|
||||
|
||||
server-validation:
|
||||
needs: server-build
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
@@ -55,21 +104,15 @@ jobs:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Restore server setup
|
||||
id: restore-server-setup-cache
|
||||
- name: Restore server build cache
|
||||
uses: ./.github/actions/restore-cache
|
||||
with:
|
||||
key: ${{ env.SERVER_SETUP_CACHE_KEY }}
|
||||
key: ${{ env.SERVER_BUILD_CACHE_KEY }}
|
||||
- name: Build twenty-shared
|
||||
run: npx nx build twenty-shared
|
||||
- name: Server / Run lint & typecheck
|
||||
uses: ./.github/actions/nx-affected
|
||||
with:
|
||||
tag: scope:backend
|
||||
tasks: lint,typecheck
|
||||
- name: Server / Write .env
|
||||
run: npx nx reset:env twenty-server
|
||||
- name: Server / Build
|
||||
@@ -84,10 +127,8 @@ jobs:
|
||||
run: |
|
||||
timeout 30s npx nx run twenty-server:worker || exit_code=$?
|
||||
if [ $exit_code -eq 124 ]; then
|
||||
# If timeout was reached (exit code 124), consider it a success
|
||||
exit 0
|
||||
elif [ $exit_code -ne 0 ]; then
|
||||
# If worker failed for other reasons, fail the build
|
||||
exit $exit_code
|
||||
fi
|
||||
- name: Server / Start
|
||||
@@ -118,13 +159,13 @@ jobs:
|
||||
|
||||
exit 1
|
||||
fi
|
||||
- name: GraphQL / Check for Pending Generation
|
||||
- name: Check for Pending Code Generation
|
||||
run: |
|
||||
# Run GraphQL generation commands
|
||||
HAS_ERRORS=false
|
||||
|
||||
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
|
||||
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 ""
|
||||
@@ -133,29 +174,42 @@ jobs:
|
||||
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."
|
||||
HAS_ERRORS=true
|
||||
fi
|
||||
|
||||
npx nx run twenty-sdk:generate-metadata-client
|
||||
|
||||
if ! git diff --quiet -- packages/twenty-sdk/src/clients/generated/metadata; then
|
||||
echo "::error::SDK metadata client changes detected. Please run 'npx nx run twenty-sdk:generate-metadata-client' and commit the changes."
|
||||
echo ""
|
||||
echo "The following SDK metadata client changes were detected:"
|
||||
echo "==================================================="
|
||||
git diff -- packages/twenty-sdk/src/clients/generated/metadata
|
||||
echo "==================================================="
|
||||
echo ""
|
||||
HAS_ERRORS=true
|
||||
fi
|
||||
|
||||
if [ "$HAS_ERRORS" = true ]; then
|
||||
exit 1
|
||||
fi
|
||||
- name: Save server setup
|
||||
uses: ./.github/actions/save-cache
|
||||
with:
|
||||
key: ${{ steps.restore-server-setup-cache.outputs.cache-primary-key }}
|
||||
server-test:
|
||||
needs: server-build
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
needs: server-setup
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Restore server setup
|
||||
- name: Restore server build cache
|
||||
uses: ./.github/actions/restore-cache
|
||||
with:
|
||||
key: ${{ env.SERVER_SETUP_CACHE_KEY }}
|
||||
key: ${{ env.SERVER_BUILD_CACHE_KEY }}
|
||||
- name: Build twenty-shared
|
||||
run: npx nx build twenty-shared
|
||||
- name: Server / Run Tests
|
||||
uses: ./.github/actions/nx-affected
|
||||
with:
|
||||
@@ -164,12 +218,12 @@ jobs:
|
||||
|
||||
server-integration-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
needs: server-setup
|
||||
runs-on: ubuntu-latest
|
||||
needs: server-build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4, 5, 6, 7, 8]
|
||||
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
@@ -207,12 +261,12 @@ jobs:
|
||||
ANALYTICS_ENABLED: true
|
||||
CLICKHOUSE_URL: "http://default:clickhousePassword@localhost:8123/twenty"
|
||||
CLICKHOUSE_PASSWORD: clickhousePassword
|
||||
SHARD_COUNTER: 8
|
||||
SHARD_COUNTER: 10
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Update .env.test for integrations tests
|
||||
@@ -223,10 +277,10 @@ jobs:
|
||||
echo "BILLING_STRIPE_BASE_PLAN_PRODUCT_ID=test-base-plan-product-id" >> .env.test
|
||||
echo "BILLING_STRIPE_WEBHOOK_SECRET=test-webhook-secret" >> .env.test
|
||||
echo "BILLING_PLAN_REQUIRED_LINK=http://localhost:3001/stripe-redirection" >> .env.test
|
||||
- name: Restore server setup
|
||||
- name: Restore server build cache
|
||||
uses: ./.github/actions/restore-cache
|
||||
with:
|
||||
key: ${{ env.SERVER_SETUP_CACHE_KEY }}
|
||||
key: ${{ env.SERVER_BUILD_CACHE_KEY }}
|
||||
- name: Server / Build
|
||||
run: npx nx build twenty-server
|
||||
- name: Build dependencies
|
||||
@@ -247,11 +301,20 @@ jobs:
|
||||
tasks: 'test:integration'
|
||||
configuration: 'with-db-reset'
|
||||
args: --shard=${{ matrix.shard }}/${{ env.SHARD_COUNTER }}
|
||||
|
||||
ci-server-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, server-setup, server-test, server-integration-test]
|
||||
needs:
|
||||
[
|
||||
changed-files-check,
|
||||
server-build,
|
||||
server-lint-typecheck,
|
||||
server-validation,
|
||||
server-test,
|
||||
server-integration-test,
|
||||
]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
name: CI Shared
|
||||
|
||||
on:
|
||||
merge_group:
|
||||
|
||||
pull_request:
|
||||
merge_group:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -14,6 +13,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
if: github.event_name != 'merge_group'
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
@@ -36,13 +36,13 @@ jobs:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Run ${{ matrix.task }} task
|
||||
uses: ./.github/actions/nx-affected
|
||||
with:
|
||||
tag: scope:frontend
|
||||
tag: scope:shared
|
||||
tasks: ${{ matrix.task }}
|
||||
ci-shared-status-check:
|
||||
if: always() && !cancelled()
|
||||
|
||||
@@ -4,9 +4,8 @@ permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
merge_group:
|
||||
|
||||
pull_request:
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -14,6 +13,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
if: github.event_name != 'merge_group'
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
|
||||
@@ -4,9 +4,8 @@ permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
merge_group:
|
||||
|
||||
pull_request:
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -14,6 +13,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
if: github.event_name != 'merge_group'
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-depth: 10
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
|
||||
@@ -3,8 +3,6 @@ name: CI Zapier
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
merge_group:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -28,7 +26,7 @@ jobs:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
@@ -52,7 +50,7 @@ jobs:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
|
||||
@@ -107,7 +105,7 @@ jobs:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build
|
||||
|
||||
@@ -164,4 +164,4 @@ jobs:
|
||||
token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
|
||||
repository: twentyhq/ci-privileged
|
||||
event-type: claude-cross-repo-response
|
||||
client-payload: '{"repo": ${{ toJSON(steps.prompt.outputs.repo) }}, "issue_number": ${{ toJSON(steps.prompt.outputs.issue_number) }}, "run_id": ${{ toJSON(github.run_id) }}, "run_url": ${{ toJSON(format('{0}/{1}/actions/runs/{2}', github.server_url, github.repository, github.run_id)) }}}'
|
||||
client-payload: '{"repo": ${{ toJSON(steps.prompt.outputs.repo) }}, "issue_number": ${{ toJSON(steps.prompt.outputs.issue_number) }}, "run_id": ${{ toJSON(github.run_id) }}, "run_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}'
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ coverage
|
||||
dist
|
||||
storybook-static
|
||||
*.tsbuildinfo
|
||||
.eslintcache
|
||||
.oxlintcache
|
||||
.nyc_output
|
||||
test-results/
|
||||
dump.rdb
|
||||
|
||||
Vendored
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"arcanis.vscode-zipfs",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"oxc.oxc-vscode",
|
||||
"esbenp.prettier-vscode",
|
||||
"figma.figma-vscode-extension",
|
||||
"firsttris.vscode-jest-runner",
|
||||
|
||||
Vendored
+10
-7
@@ -4,25 +4,28 @@
|
||||
"files.insertFinalNewline": true,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"[typescript]": {
|
||||
"editor.formatOnSave": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.fixAll.oxc": "explicit",
|
||||
"source.addMissingImports": "always",
|
||||
"source.organizeImports": "always"
|
||||
}
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.formatOnSave": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.fixAll.oxc": "explicit",
|
||||
"source.addMissingImports": "always",
|
||||
"source.organizeImports": "always"
|
||||
}
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.formatOnSave": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.fixAll.oxc": "explicit",
|
||||
"source.addMissingImports": "always",
|
||||
"source.organizeImports": "always"
|
||||
}
|
||||
@@ -48,7 +51,7 @@
|
||||
"search.exclude": {
|
||||
"**/.yarn": true
|
||||
},
|
||||
"eslint.debug": true,
|
||||
"oxc.lint.enable": true,
|
||||
"files.associations": {
|
||||
".cursorrules": "markdown"
|
||||
},
|
||||
|
||||
Vendored
+13
-9
@@ -37,8 +37,8 @@
|
||||
"path": "../packages/twenty-zapier"
|
||||
},
|
||||
{
|
||||
"name": "tools/eslint-rules",
|
||||
"path": "../tools/eslint-rules"
|
||||
"name": "packages/twenty-oxlint-rules",
|
||||
"path": "../packages/twenty-oxlint-rules"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-e2e-testing",
|
||||
@@ -49,23 +49,26 @@
|
||||
"editor.formatOnSave": false,
|
||||
"files.eol": "auto",
|
||||
"[typescript]": {
|
||||
"editor.formatOnSave": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.fixAll.oxc": "explicit",
|
||||
"source.addMissingImports": "always"
|
||||
}
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.formatOnSave": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.fixAll.oxc": "explicit",
|
||||
"source.addMissingImports": "always"
|
||||
}
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.formatOnSave": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.fixAll.oxc": "explicit",
|
||||
"source.addMissingImports": "always"
|
||||
}
|
||||
},
|
||||
@@ -88,7 +91,7 @@
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"[javascript][typescript][typescriptreact]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.fixAll.oxc": "explicit",
|
||||
"source.addMissingImports": "always"
|
||||
}
|
||||
},
|
||||
@@ -98,6 +101,7 @@
|
||||
"files.exclude": {
|
||||
"packages/": true
|
||||
},
|
||||
"oxc.lint.enable": true,
|
||||
"jest.runMode": "on-demand",
|
||||
"jest.disabledWorkspaceFolders": [
|
||||
"ROOT",
|
||||
|
||||
@@ -90,7 +90,7 @@ npx nx run twenty-front:graphql:generate --configuration=metadata
|
||||
## Architecture Overview
|
||||
|
||||
### Tech Stack
|
||||
- **Frontend**: React 18, TypeScript, Jotai (state management), Emotion (styling), Vite
|
||||
- **Frontend**: React 18, TypeScript, Jotai (state management), Linaria (styling), Vite
|
||||
- **Backend**: NestJS, TypeORM, PostgreSQL, Redis, GraphQL (with GraphQL Yoga)
|
||||
- **Monorepo**: Nx workspace managed with Yarn 4
|
||||
|
||||
@@ -175,7 +175,7 @@ IMPORTANT: Use Context7 for code generation, setup or configuration steps, or li
|
||||
5. Run `graphql:generate` after any GraphQL schema changes
|
||||
|
||||
### Code Style Notes
|
||||
- Use **Emotion** for styling with styled-components pattern
|
||||
- Use **Linaria** for styling with zero-runtime CSS-in-JS (styled-components pattern)
|
||||
- Follow **Nx** workspace conventions for imports
|
||||
- Use **Lingui** for internationalization
|
||||
- Apply security first, then formatting (sanitize before format)
|
||||
|
||||
@@ -109,7 +109,7 @@ Below are a few features we have implemented to date:
|
||||
- [TypeScript](https://www.typescriptlang.org/)
|
||||
- [Nx](https://nx.dev/)
|
||||
- [NestJS](https://nestjs.com/), with [BullMQ](https://bullmq.io/), [PostgreSQL](https://www.postgresql.org/), [Redis](https://redis.io/)
|
||||
- [React](https://reactjs.org/), with [Jotai](https://jotai.org/), [Emotion](https://emotion.sh/) and [Lingui](https://lingui.dev/)
|
||||
- [React](https://reactjs.org/), with [Jotai](https://jotai.org/), [Linaria](https://linaria.dev/) and [Lingui](https://lingui.dev/)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,423 +0,0 @@
|
||||
# Emotion → Linaria Migration Plan: twenty-front
|
||||
|
||||
## Overview
|
||||
|
||||
Migrate all Emotion (`@emotion/styled`, `@emotion/react`) usages in
|
||||
`packages/twenty-front/src` to Linaria (`@linaria/react`, `@linaria/core`),
|
||||
following the same patterns already established in the `twenty-ui` package.
|
||||
|
||||
Linaria is a **zero-runtime** CSS-in-JS library. Styles are extracted at
|
||||
build time by [wyw-in-js](https://wyw-in-js.dev/) (the Vite plugin is
|
||||
`@wyw-in-js/vite`, already configured in `twenty-front/vite.config.ts`).
|
||||
This means every expression inside a `styled` or `css` template literal
|
||||
must be statically evaluable at build time — no runtime theme objects,
|
||||
no closures over component state, no side-effects.
|
||||
|
||||
**Total files to migrate: ~998**
|
||||
|
||||
| Category | Files | Description |
|
||||
|---|---|---|
|
||||
| styled-only | 694 | Import `@emotion/styled` but not `useTheme` |
|
||||
| styled + useTheme | 224 | Import both `@emotion/styled` and `useTheme` |
|
||||
| useTheme-only | 79 | Import `useTheme` but not `@emotion/styled` |
|
||||
| css / Global only | 1 | Import `css` or `Global` from `@emotion/react` only |
|
||||
|
||||
## Theme Architecture
|
||||
|
||||
Two build-time utilities produce the theme system:
|
||||
|
||||
- **`buildThemeReferencingRootCssVariables`** — walks the theme object and
|
||||
builds a nested mirror where every leaf is a `var(--t-xxx)` string
|
||||
(evaluated at build time by wyw-in-js)
|
||||
- **`prepareThemeForRootCssVariableInjection`** — walks the runtime theme
|
||||
and collects flat `[--css-variable-name, value]` pairs, injected onto
|
||||
`document.documentElement` by `ThemeCssVariableInjectorEffect`
|
||||
|
||||
`themeCssVariables` is the build-time object; every leaf resolves to a CSS
|
||||
`var()` reference. It is safe to use inside `styled` and `css` templates
|
||||
because wyw-in-js can evaluate it statically.
|
||||
|
||||
## Migration Patterns
|
||||
|
||||
### 1. `styled` import
|
||||
|
||||
```diff
|
||||
- import styled from '@emotion/styled';
|
||||
+ import { styled } from '@linaria/react';
|
||||
```
|
||||
|
||||
### 2. Theme access in styled components
|
||||
|
||||
Replace Emotion's `({ theme }) =>` prop-function pattern with static
|
||||
`themeCssVariables` references:
|
||||
|
||||
```diff
|
||||
+ import { themeCssVariables } from 'twenty-ui/theme';
|
||||
|
||||
const StyledTitle = styled.span`
|
||||
- color: ${({ theme }) => theme.font.color.primary};
|
||||
- font-size: ${({ theme }) => theme.font.size.lg};
|
||||
+ color: ${themeCssVariables.font.color.primary};
|
||||
+ font-size: ${themeCssVariables.font.size.lg};
|
||||
`;
|
||||
```
|
||||
|
||||
### 3. Spacing
|
||||
|
||||
`theme.spacing(N)` is a function; in Linaria it becomes an indexed lookup:
|
||||
|
||||
```diff
|
||||
- margin-top: ${({ theme }) => theme.spacing(3)};
|
||||
+ margin-top: ${themeCssVariables.spacing[3]};
|
||||
```
|
||||
|
||||
For multi-arg spacing like `theme.spacing(2, 4)` → `"8px 16px"`:
|
||||
|
||||
```diff
|
||||
- padding: ${({ theme }) => theme.spacing(2, 4)};
|
||||
+ padding: ${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[4]};
|
||||
```
|
||||
|
||||
The spacing scale covers integers 0–32 plus `0.5` and `1.5`. Any other
|
||||
fractional values (`0.25`, `0.75`, `1.25`, `2.5`, `3.5`) must be replaced
|
||||
with literal pixel values (e.g. `theme.spacing(2.5)` → `10px`).
|
||||
|
||||
### 4. `useTheme` → `useContext(ThemeContext)`
|
||||
|
||||
For runtime theme access (icon sizes, animation durations, conditional logic
|
||||
outside of styled components):
|
||||
|
||||
```diff
|
||||
- import { useTheme } from '@emotion/react';
|
||||
+ import { useContext } from 'react';
|
||||
+ import { ThemeContext } from 'twenty-ui/theme';
|
||||
|
||||
const MyComponent = () => {
|
||||
- const theme = useTheme();
|
||||
+ const { theme } = useContext(ThemeContext);
|
||||
return <Icon size={theme.icon.size.sm} />;
|
||||
};
|
||||
```
|
||||
|
||||
### 5. `css` template literal
|
||||
|
||||
Linaria's `css` (from `@linaria/core`) returns a **class name string**, not
|
||||
a serialized style object like Emotion's `css`. This has two consequences:
|
||||
|
||||
**Standalone usage** — apply via `className`, not the `css` prop:
|
||||
|
||||
```diff
|
||||
- import { css } from '@emotion/react';
|
||||
+ import { css } from '@linaria/core';
|
||||
|
||||
const myClass = css`
|
||||
text-decoration: none;
|
||||
`;
|
||||
|
||||
- <Link css={myClass} />
|
||||
+ <Link className={myClass} />
|
||||
```
|
||||
|
||||
**Inside `styled` templates** — do NOT nest `css` tags. Linaria's `css`
|
||||
returns a class name, not raw CSS text, so interpolating it inside `styled`
|
||||
produces broken output. Use plain strings instead:
|
||||
|
||||
```diff
|
||||
// WRONG — css`` returns a class name, not CSS text
|
||||
${({ handle }) =>
|
||||
handle === 'left'
|
||||
- ? css`left: ${themeCssVariables.spacing[1]};`
|
||||
- : css`right: ${themeCssVariables.spacing[1]};`}
|
||||
+ ? `left: ${themeCssVariables.spacing[1]};`
|
||||
+ : `right: ${themeCssVariables.spacing[1]};`}
|
||||
```
|
||||
|
||||
### 6. Interpolation return types
|
||||
|
||||
wyw-in-js requires prop interpolation functions to return `string | number`.
|
||||
They must **never** return `false`, `undefined`, or `null`. Replace
|
||||
short-circuit `&&` with ternary expressions:
|
||||
|
||||
```diff
|
||||
// WRONG — returns false when condition is false
|
||||
- ${({ isActive }) => isActive && `background: ${themeCssVariables.color.blue};`}
|
||||
// CORRECT
|
||||
+ ${({ isActive }) => isActive ? `background: ${themeCssVariables.color.blue};` : ''}
|
||||
```
|
||||
|
||||
### 7. Block interpolations (multi-declaration returns)
|
||||
|
||||
Linaria wraps each interpolation result in a single CSS custom property
|
||||
(`var(--xxx)`). An interpolation that returns **multiple CSS declarations**
|
||||
produces invalid CSS. Split into one interpolation per property:
|
||||
|
||||
```diff
|
||||
// WRONG — single interpolation returning multiple declarations
|
||||
- ${({ divider, theme }) => {
|
||||
- const border = `1px solid ${theme.border.color.light}`;
|
||||
- return divider === 'left' ? `border-left: ${border}` : `border-right: ${border}`;
|
||||
- }}
|
||||
|
||||
// CORRECT — one interpolation per property
|
||||
+ border-left: ${({ divider }) =>
|
||||
+ divider === 'left' ? `1px solid ${themeCssVariables.border.color.light}` : 'none'};
|
||||
+ border-right: ${({ divider }) =>
|
||||
+ divider === 'right' ? `1px solid ${themeCssVariables.border.color.light}` : 'none'};
|
||||
```
|
||||
|
||||
### 8. CSS var + unit concatenation
|
||||
|
||||
CSS custom properties can't be concatenated with unit suffixes directly
|
||||
(`var(--x)px` is invalid). Use `calc()` to attach units:
|
||||
|
||||
```diff
|
||||
- transition: background ${themeCssVariables.animation.duration.instant}s ease;
|
||||
+ transition: background calc(${themeCssVariables.animation.duration.instant} * 1s) ease;
|
||||
```
|
||||
|
||||
### 9. `styled(Component)` requires `className`
|
||||
|
||||
Linaria's `styled(Component)` works by passing a generated `className` to
|
||||
the wrapped component. The component **must** accept and forward a
|
||||
`className` prop — otherwise the styles are silently lost. If the component
|
||||
doesn't support it, either add `className` support or use a wrapper div.
|
||||
|
||||
Linaria also does **not** support Emotion's `shouldForwardProp` option.
|
||||
Custom props on HTML elements are automatically filtered by Linaria's
|
||||
runtime (via `@emotion/is-prop-valid`). For custom components, all props are
|
||||
forwarded — ensure the wrapped component ignores unknown props gracefully.
|
||||
|
||||
### 10. `type Theme` → `type ThemeType`
|
||||
|
||||
```diff
|
||||
- import { type Theme } from '@emotion/react';
|
||||
+ import { type ThemeType } from 'twenty-ui/theme';
|
||||
```
|
||||
|
||||
### 11. Framer Motion integration
|
||||
|
||||
Linaria doesn't support `styled(motion.div)` — wrapping a motion element
|
||||
with `styled()` causes the component body to be stripped at build time by
|
||||
wyw-in-js. Define the styled component first, then wrap with
|
||||
`motion.create()`:
|
||||
|
||||
```tsx
|
||||
const StyledBarBase = styled.div`
|
||||
background-color: ${themeCssVariables.font.color.primary};
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const StyledBar = motion.create(StyledBarBase);
|
||||
```
|
||||
|
||||
### 12. Dynamic styles via CSS variables
|
||||
|
||||
When a component needs to compute styles from multiple props with complex
|
||||
branching logic (e.g. combining `variant`, `accent`, `disabled`, `focus`),
|
||||
Linaria's prop interpolations become unwieldy. Use a `computeDynamicStyles`
|
||||
helper that returns a `CSSProperties` object injected via `style={}`,
|
||||
referenced from the static CSS with `var()`:
|
||||
|
||||
```tsx
|
||||
const StyledButton = styled.button`
|
||||
background: var(--btn-bg);
|
||||
border-color: var(--btn-border-color);
|
||||
&:hover { background: var(--btn-hover-bg); }
|
||||
`;
|
||||
|
||||
const dynamicStyles = useMemo(() => {
|
||||
const s = computeButtonDynamicStyles(variant, accent, ...);
|
||||
return {
|
||||
'--btn-bg': s.background,
|
||||
'--btn-hover-bg': s.hoverBackground,
|
||||
} as CSSProperties;
|
||||
}, [variant, accent, ...]);
|
||||
|
||||
return <StyledButton style={dynamicStyles} />;
|
||||
```
|
||||
|
||||
### 13. `Global` component
|
||||
|
||||
Replace Emotion's `<Global styles={...} />` with standard CSS or the
|
||||
`ThemeCssVariableInjectorEffect` pattern from twenty-ui.
|
||||
|
||||
### 14. `ThemeProvider`
|
||||
|
||||
The `BaseThemeProvider` already wraps children with both Emotion's
|
||||
`ThemeProvider` and Linaria's `ThemeContextProvider`. Once all Emotion usages
|
||||
are gone, the Emotion `ThemeProvider` wrapper can be removed.
|
||||
|
||||
---
|
||||
|
||||
## PR Breakdown
|
||||
|
||||
Files are grouped to keep each PR around ~100 files with consistent review
|
||||
surface. We start with the simplest, lowest-risk modules.
|
||||
|
||||
### PR 1 (~97 files) — Small standalone modules
|
||||
|
||||
Low-risk modules with mostly simple `styled`-only patterns.
|
||||
|
||||
| Module | Files |
|
||||
|---|---|
|
||||
| spreadsheet-import | 28 |
|
||||
| billing | 10 |
|
||||
| views | 14 |
|
||||
| navigation-menu-item | 14 |
|
||||
| blocknote-editor | 7 |
|
||||
| advanced-text-editor | 7 |
|
||||
| favorites | 7 |
|
||||
| navigation | 4 |
|
||||
| information-banner | 3 |
|
||||
| sign-in-background-mock | 3 |
|
||||
|
||||
### PR 2 (~100 files) — Auth, tiny modules, loading, testing, pages (part 1)
|
||||
|
||||
| Module | Files |
|
||||
|---|---|
|
||||
| auth | 19 |
|
||||
| action-menu | 3 |
|
||||
| object-metadata | 3 |
|
||||
| onboarding | 2 |
|
||||
| workspace | 2 |
|
||||
| file | 2 |
|
||||
| error-handler | 2 |
|
||||
| front-components | 1 |
|
||||
| geo-map | 1 |
|
||||
| hooks | 1 |
|
||||
| loading | 5 |
|
||||
| testing | 5 |
|
||||
| pages (first ~55 files) | ~55 |
|
||||
|
||||
### PR 3 (~97 files) — Pages (remaining) + activities + AI
|
||||
|
||||
| Module | Files |
|
||||
|---|---|
|
||||
| pages (remaining ~16 files) | ~16 |
|
||||
| activities | 53 |
|
||||
| ai | 28 |
|
||||
|
||||
### PR 4 (~100 files) — Command-menu + workflow (part 1)
|
||||
|
||||
| Module | Files |
|
||||
|---|---|
|
||||
| command-menu | 53 |
|
||||
| workflow (first ~47 files) | ~47 |
|
||||
|
||||
### PR 5 (~115 files) — Workflow (remaining) + page-layout
|
||||
|
||||
| Module | Files |
|
||||
|---|---|
|
||||
| workflow (remaining ~32 files) | ~32 |
|
||||
| page-layout | 83 |
|
||||
|
||||
### PR 6 (~100 files) — UI module (part 1)
|
||||
|
||||
| Module | Files |
|
||||
|---|---|
|
||||
| ui (first ~100 files) | ~100 |
|
||||
|
||||
### PR 7 (~85 files) — UI module (remaining) + object-record (start)
|
||||
|
||||
| Module | Files |
|
||||
|---|---|
|
||||
| ui (remaining ~23 files) | ~23 |
|
||||
| object-record (first ~62 files) | ~62 |
|
||||
|
||||
### PR 8 (~100 files) — Object-record (continued)
|
||||
|
||||
| Module | Files |
|
||||
|---|---|
|
||||
| object-record (next ~100 files) | ~100 |
|
||||
|
||||
### PR 9 (~100 files) — Settings (part 1)
|
||||
|
||||
| Module | Files |
|
||||
|---|---|
|
||||
| settings (first ~100 files) | ~100 |
|
||||
|
||||
### PR 10 (~102 files) — Settings (part 2) + final cleanup
|
||||
|
||||
| Module | Files |
|
||||
|---|---|
|
||||
| settings (remaining ~102 files) | ~102 |
|
||||
| css/Global-only file | 1 |
|
||||
|
||||
### Post-migration PR — Remove Emotion
|
||||
|
||||
Once all PRs are merged:
|
||||
|
||||
- Remove `ThemeProvider` from `@emotion/react` in `BaseThemeProvider`
|
||||
- Remove `@emotion/styled` and `@emotion/react` dependencies
|
||||
- Remove `@styled/typescript-styled-plugin` from tsconfig
|
||||
- Clean up any remaining Emotion-related configuration
|
||||
|
||||
---
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Mitigation |
|
||||
|---|---|
|
||||
| wyw-in-js evaluates at build time; dynamic expressions may fail | Use `themeCssVariables` for static theme values; pass dynamic values as component props or via `style={}` CSS variables |
|
||||
| `theme.spacing(N)` function → `themeCssVariables.spacing[N]` index | Pre-computed for integers 0–32 plus 0.5 and 1.5; other fractional values → literal pixel values |
|
||||
| `useTheme` used for runtime logic (not just styles) | Replace with `useContext(ThemeContext)`, destructure `{ theme }` |
|
||||
| Multi-arg `theme.spacing(a, b, c)` | Split into individual `themeCssVariables.spacing[N]` references |
|
||||
| `css` tag inside `styled` templates | Linaria `css` returns class name, not CSS text; use plain strings inside `styled` |
|
||||
| Interpolation returns `false` / `undefined` | wyw-in-js requires `string \| number`; use ternary `? : ''` instead of `&&` |
|
||||
| Block interpolations (multiple declarations) | Split into one interpolation per CSS property |
|
||||
| `var(--x)px` concatenation | Use `calc(var(--x) * 1px)` |
|
||||
| `styled(motion.div)` stripped by wyw-in-js | Use `motion.create(StyledBase)` pattern |
|
||||
| `styled(Component)` with no `className` prop | Add `className` support to wrapped component or use wrapper div |
|
||||
| Complex multi-prop style branching | Use `computeDynamicStyles` + `style={}` + `var()` references |
|
||||
|
||||
---
|
||||
|
||||
## Known Issues & TODOs
|
||||
|
||||
### `styled(Component)` from `twenty-ui` — style overrides silently dropped
|
||||
|
||||
When `twenty-front` uses `styled(SomeComponent)` to extend a pre-built
|
||||
component from `twenty-ui` (e.g. `Card`, `CardContent`, `PropertyBox`,
|
||||
`Chip`, `Pill`, `Avatar`, `StyledHoverableMenuItemBase`, `MenuItemLeftContent`,
|
||||
`TabList`, `NavigationDrawerSection`), the style overrides are silently
|
||||
ignored at runtime. This happens because `wyw-in-js` in `twenty-front`
|
||||
cannot resolve the base class name of a component that was already compiled
|
||||
in another package.
|
||||
|
||||
**Current workaround:** Replace `styled(Component)` with a plain
|
||||
`styled.div` (or wrapper div) that duplicates the needed base styles.
|
||||
This fixes the visual regression but loses the component's built-in
|
||||
behavior (e.g. `CardContent`'s framer-motion animation, `PropertyBox`'s
|
||||
layout-context padding logic, `Card`'s border/overflow).
|
||||
|
||||
**Proper fix (TODO):** Add customization props directly to the base
|
||||
components so consumers can override styles without wrapping:
|
||||
|
||||
- `Card` / `CardContent` — add `padding`, `backgroundColor`, `borderColor` props
|
||||
- `PropertyBox` — add `padding`, `height`, `noPadding` props
|
||||
- Other frequently extended components — audit and add props as needed
|
||||
|
||||
**Affected files (using workaround today):**
|
||||
|
||||
- `CalendarEventDetails.tsx` — `styled(PropertyBox)` → `styled.div`, `styled(Chip)` → wrapper
|
||||
- `CalendarEventParticipantsResponseStatusField.tsx` — `styled(PropertyBox)` → `styled.div`
|
||||
- `CalendarEventNotSharedContent.tsx` — `styled(Card)` / `styled(CardContent)` → `styled.div`
|
||||
- `CalendarDayCardContent.tsx` — `styled(CardContent)` → `styled(motion.div)`
|
||||
- `ActivityRow.tsx` — `styled(CardContent)` → `styled.div`
|
||||
- `EmailThreadPreview.tsx` — `styled(Avatar)` → wrapper div
|
||||
- `SignInAppNavigationDrawerMock.tsx` — `styled(NavigationDrawerSection)` → wrapper div
|
||||
- `DefaultLayout.tsx` — `styled(SignInAppNavigationDrawerMock)` → wrapper div
|
||||
- `LastUsedPill.tsx` — `styled(Pill)` → wrapper span
|
||||
- `Logo.tsx` — dynamic `background-image` via inline `style` instead of prop
|
||||
- `DatePicker.tsx` / `DateTimePicker.tsx` — `styled(StyledHoverableMenuItemBase)` / `styled(MenuItemLeftContent)` → `styled.div`
|
||||
- `SelectControl.tsx` / `MultiSelectControl.tsx` — `styled(IconChevronDown)` → wrapper div
|
||||
|
||||
---
|
||||
|
||||
## Validation Checklist (per PR)
|
||||
|
||||
- [ ] `npx nx lint:diff-with-main twenty-front` passes
|
||||
- [ ] `npx nx typecheck twenty-front` passes
|
||||
- [ ] `npx nx test twenty-front` passes
|
||||
- [ ] Visual spot-check of affected components in the app
|
||||
- [ ] No remaining `@emotion/styled` or `@emotion/react` imports in migrated files
|
||||
@@ -1,226 +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';
|
||||
|
||||
const twentyRules = await nxPlugin.loadWorkspaceRules(
|
||||
'packages/twenty-eslint-rules',
|
||||
);
|
||||
|
||||
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:create-app',
|
||||
onlyDependOnLibsWithTags: ['scope:create-app', 'scope:shared'],
|
||||
},
|
||||
{
|
||||
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', 'scope:zapier'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
// 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-object-type': [
|
||||
'error',
|
||||
{
|
||||
allowInterfaces: 'with-single-extends',
|
||||
},
|
||||
],
|
||||
'@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,
|
||||
twenty: { rules: twentyRules },
|
||||
},
|
||||
},
|
||||
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
|
||||
'twenty/mdx-component-newlines': 'error',
|
||||
// Disallow angle bracket placeholders to prevent Crowdin translation errors
|
||||
'twenty/no-angle-bracket-placeholders': 'error',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -40,34 +40,30 @@
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint",
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"outputs": ["{options.outputFile}"],
|
||||
"options": {
|
||||
"eslintConfig": "{projectRoot}/eslint.config.mjs",
|
||||
"cache": true,
|
||||
"cacheLocation": "{workspaceRoot}/.cache/eslint"
|
||||
"cwd": "{projectRoot}",
|
||||
"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": {
|
||||
"cacheStrategy": "content"
|
||||
},
|
||||
"ci": {},
|
||||
"fix": {
|
||||
"fix": true
|
||||
"command": "npx oxlint --fix -c .oxlintrc.json . && prettier . --write --cache --cache-location ../../.cache/prettier/{projectRoot} --cache-strategy metadata"
|
||||
}
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
"dependsOn": ["^build", "twenty-oxlint-rules:build"]
|
||||
},
|
||||
"lint:diff-with-main": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": false,
|
||||
"options": {
|
||||
"command": "git diff --name-only --diff-filter=d main | grep -E '{args.pattern}' | grep '^{projectRoot}/' | xargs sh -c 'if [ $# -gt 0 ]; then npx eslint --config {projectRoot}/eslint.config.mjs \"$@\"; fi' _",
|
||||
"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": "git diff --name-only --diff-filter=d main | grep -E '{args.pattern}' | grep '^{projectRoot}/' | xargs sh -c 'if [ $# -gt 0 ]; then npx eslint --config {projectRoot}/eslint.config.mjs --fix \"$@\"; fi' _"
|
||||
"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)"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -147,7 +143,7 @@
|
||||
"outputs": ["{projectRoot}/{options.output-dir}"],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "NODE_OPTIONS='--max-old-space-size=10240' VITE_DISABLE_TYPESCRIPT_CHECKER=true storybook build --test",
|
||||
"command": "NODE_OPTIONS='--max-old-space-size=10240' storybook build --test",
|
||||
"output-dir": "storybook-static",
|
||||
"config-dir": ".storybook"
|
||||
},
|
||||
@@ -255,14 +251,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@nx/eslint:lint": {
|
||||
"cache": true,
|
||||
"inputs": [
|
||||
"default",
|
||||
"{workspaceRoot}/eslint.config.mjs",
|
||||
"{workspaceRoot}/packages/twenty-eslint-rules/**/*"
|
||||
]
|
||||
},
|
||||
"@nx/vite:build": {
|
||||
"cache": true,
|
||||
"dependsOn": ["^build"],
|
||||
@@ -276,23 +264,21 @@
|
||||
"generators": {
|
||||
"@nx/react": {
|
||||
"application": {
|
||||
"style": "@emotion/styled",
|
||||
"linter": "eslint",
|
||||
"style": "@linaria/react",
|
||||
"bundler": "vite",
|
||||
"compiler": "swc",
|
||||
"unitTestRunner": "jest",
|
||||
"projectNameAndRootFormat": "derived"
|
||||
},
|
||||
"library": {
|
||||
"style": "@emotion/styled",
|
||||
"linter": "eslint",
|
||||
"style": "@linaria/react",
|
||||
"bundler": "vite",
|
||||
"compiler": "swc",
|
||||
"unitTestRunner": "jest",
|
||||
"projectNameAndRootFormat": "derived"
|
||||
},
|
||||
"component": {
|
||||
"style": "@emotion/styled"
|
||||
"style": "@linaria/react"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+16
-37
@@ -2,14 +2,13 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.7.17",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@floating-ui/react": "^0.24.3",
|
||||
"@linaria/core": "^6.2.0",
|
||||
"@linaria/react": "^6.2.1",
|
||||
"@radix-ui/colors": "^3.0.0",
|
||||
"@sniptt/guards": "^0.2.0",
|
||||
"@tabler/icons-react": "^3.31.0",
|
||||
"@wyw-in-js/babel-preset": "^1.0.6",
|
||||
"@wyw-in-js/vite": "^0.7.0",
|
||||
"archiver": "^7.0.1",
|
||||
"danger-plugin-todos": "^1.3.1",
|
||||
@@ -41,12 +40,13 @@
|
||||
"lodash.snakecase": "^4.1.1",
|
||||
"lodash.upperfirst": "^4.3.1",
|
||||
"microdiff": "^1.3.2",
|
||||
"next-with-linaria": "^1.3.0",
|
||||
"planer": "^1.2.0",
|
||||
"pluralize": "^8.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react-router-dom": "^6.4.4",
|
||||
"react-router-dom": "^6.30.3",
|
||||
"react-tooltip": "^5.13.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"rxjs": "^7.2.0",
|
||||
@@ -72,14 +72,13 @@
|
||||
"@graphql-codegen/typescript": "^3.0.4",
|
||||
"@graphql-codegen/typescript-operations": "^3.0.4",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
|
||||
"@nx/eslint": "22.3.3",
|
||||
"@nx/eslint-plugin": "22.3.3",
|
||||
"@nx/jest": "22.3.3",
|
||||
"@nx/js": "22.3.3",
|
||||
"@nx/react": "22.3.3",
|
||||
"@nx/storybook": "22.3.3",
|
||||
"@nx/vite": "22.3.3",
|
||||
"@nx/web": "22.3.3",
|
||||
"@nx/jest": "22.5.4",
|
||||
"@nx/js": "22.5.4",
|
||||
"@nx/react": "22.5.4",
|
||||
"@nx/storybook": "22.5.4",
|
||||
"@nx/vite": "22.5.4",
|
||||
"@nx/web": "22.5.4",
|
||||
"@oxlint/plugins": "^1.51.0",
|
||||
"@sentry/types": "^8",
|
||||
"@storybook-community/storybook-addon-cookie": "^5.0.0",
|
||||
"@storybook/addon-coverage": "^3.0.0",
|
||||
@@ -89,11 +88,10 @@
|
||||
"@storybook/icons": "^2.0.1",
|
||||
"@storybook/react-vite": "^10.2.13",
|
||||
"@storybook/test-runner": "^0.24.2",
|
||||
"@stylistic/eslint-plugin": "^1.5.0",
|
||||
"@swc-node/register": "1.11.1",
|
||||
"@swc/cli": "^0.3.12",
|
||||
"@swc/core": "1.15.11",
|
||||
"@swc/helpers": "~0.5.18",
|
||||
"@swc-node/register": "^1.11.1",
|
||||
"@swc/cli": "^0.7.10",
|
||||
"@swc/core": "^1.15.11",
|
||||
"@swc/helpers": "~0.5.19",
|
||||
"@swc/jest": "^0.2.39",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
@@ -132,9 +130,6 @@
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"@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",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260116.1",
|
||||
"@vitejs/plugin-react-swc": "4.2.3",
|
||||
"@vitest/browser-playwright": "^4.0.18",
|
||||
@@ -146,22 +141,6 @@
|
||||
"danger": "^13.0.4",
|
||||
"dotenv-cli": "^7.4.4",
|
||||
"esbuild": "^0.25.10",
|
||||
"eslint": "^9.32.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-lingui": "^0.9.0",
|
||||
"eslint-plugin-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-refresh": "^0.4.4",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"eslint-plugin-storybook": "^10.2.13",
|
||||
"eslint-plugin-unicorn": "^56.0.1",
|
||||
"eslint-plugin-unused-imports": "^3.0.0",
|
||||
"http-server": "^14.1.1",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "30.0.0-beta.3",
|
||||
@@ -170,7 +149,7 @@
|
||||
"jsdom": "~22.1.0",
|
||||
"msw": "^2.12.7",
|
||||
"msw-storybook-addon": "^2.0.6",
|
||||
"nx": "22.3.3",
|
||||
"nx": "22.5.4",
|
||||
"prettier": "^3.1.1",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^5.0.5",
|
||||
@@ -230,7 +209,7 @@
|
||||
"packages/twenty-apps",
|
||||
"packages/twenty-cli",
|
||||
"packages/create-twenty-app",
|
||||
"packages/twenty-eslint-rules"
|
||||
"packages/twenty-oxlint-rules"
|
||||
]
|
||||
},
|
||||
"prettier": {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["typescript", "import", "unicorn"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
},
|
||||
"ignorePatterns": ["node_modules", "dist"],
|
||||
"rules": {
|
||||
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
|
||||
"no-console": "off",
|
||||
"no-control-regex": "off",
|
||||
"no-debugger": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"no-undef": "off",
|
||||
"no-unused-vars": "off",
|
||||
"no-redeclare": "off",
|
||||
"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/explicit-function-return-type": "off",
|
||||
"typescript/explicit-module-boundary-types": "off",
|
||||
"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": "^_"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
dist
|
||||
@@ -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)
|
||||
|
||||
@@ -41,7 +43,7 @@ yarn twenty auth:login
|
||||
yarn twenty entity:add
|
||||
|
||||
# Start dev mode: watches, builds, and syncs local changes to your workspace
|
||||
# (also auto-generates typed API clients — CoreApiClient and MetadataApiClient — in node_modules/twenty-sdk/generated)
|
||||
# (also auto-generates typed CoreApiClient — MetadataApiClient ships pre-built with the SDK — both available via `twenty-sdk/clients`)
|
||||
yarn twenty app:dev
|
||||
|
||||
# Watch your application's function logs
|
||||
@@ -64,11 +66,10 @@ 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 |
|
||||
| `-m, --minimal` | Creates only core files (`application-config.ts` and `default-role.ts`) |
|
||||
|
||||
```bash
|
||||
# Default: all examples included
|
||||
@@ -76,31 +77,21 @@ npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimal: only core files
|
||||
npx create-twenty-app@latest my-app -m
|
||||
|
||||
# Interactive: choose which examples to include
|
||||
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`)
|
||||
- **Example front component** — a React UI component (`front-components/hello-world.tsx`)
|
||||
- **Example view** — a saved view for the example object (`views/example-view.ts`)
|
||||
- **Example navigation menu item** — a sidebar link (`navigation-menu-items/example-navigation-menu-item.ts`)
|
||||
- **Example skill** — an AI agent skill definition (`skills/example-skill.ts`)
|
||||
|
||||
## 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)
|
||||
- `logic-functions/post-install.ts` — Post-install logic function (runs after app installation)
|
||||
- TypeScript configuration, ESLint, package.json, .gitignore
|
||||
- TypeScript configuration, Oxlint, package.json, .gitignore
|
||||
- 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
|
||||
@@ -108,16 +99,18 @@ In interactive mode, you can pick from:
|
||||
- `views/example-view.ts` — Example saved view for the example object
|
||||
- `navigation-menu-items/example-navigation-menu-item.ts` — Example sidebar navigation link
|
||||
- `skills/example-skill.ts` — Example AI agent skill definition
|
||||
- `__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 auto‑generated 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`).
|
||||
|
||||
- `CoreApiClient` (for workspace data via `/graphql`) is auto-generated by `yarn twenty app:dev`. `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`) ships pre-built with the SDK. Both are available via `import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/clients'`.
|
||||
|
||||
## Publish your application
|
||||
|
||||
Applications are currently stored in `twenty/packages/twenty-apps`.
|
||||
|
||||
You can share your application with all Twenty users:
|
||||
@@ -142,9 +135,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 auto‑generates the typed client.
|
||||
|
||||
## Contributing
|
||||
|
||||
- See our [GitHub](https://github.com/twentyhq/twenty)
|
||||
- Join our [Discord](https://discord.gg/cx5n4Jzs57)
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import baseConfig from '../../eslint.config.mjs';
|
||||
|
||||
export default [
|
||||
...baseConfig,
|
||||
{
|
||||
ignores: ['**/dist/**'],
|
||||
},
|
||||
{
|
||||
files: ['**/*.{js,jsx,ts,tsx}'],
|
||||
rules: {
|
||||
'prettier/prettier': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
},
|
||||
ignores: ['src/**/*.ts', '!src/cli/**/*.ts'],
|
||||
},
|
||||
];
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-twenty-app",
|
||||
"version": "0.6.3",
|
||||
"version": "0.7.0-canary.0",
|
||||
"description": "Command-line interface to create Twenty application",
|
||||
"main": "dist/cli.cjs",
|
||||
"bin": "dist/cli.cjs",
|
||||
|
||||
@@ -25,19 +25,7 @@
|
||||
}
|
||||
},
|
||||
"typecheck": {},
|
||||
"lint": {
|
||||
"options": {
|
||||
"lintFilePatterns": ["{projectRoot}/src/**/*.{ts,json}"],
|
||||
"maxWarnings": 0
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"lintFilePatterns": ["{projectRoot}/src/**/*.{ts,json}"],
|
||||
"maxWarnings": 0
|
||||
},
|
||||
"fix": {}
|
||||
}
|
||||
},
|
||||
"lint": {},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
|
||||
@@ -18,10 +18,6 @@ const program = new Command(packageJson.name)
|
||||
'-m, --minimal',
|
||||
'Create only core entities (application-config and default-role)',
|
||||
)
|
||||
.option(
|
||||
'-i, --interactive',
|
||||
'Interactively choose which entity examples to include',
|
||||
)
|
||||
.helpOption('-h, --help', 'Display this help message.')
|
||||
.action(
|
||||
async (
|
||||
@@ -29,19 +25,14 @@ const program = new Command(packageJson.name)
|
||||
options?: {
|
||||
exhaustive?: boolean;
|
||||
minimal?: boolean;
|
||||
interactive?: boolean;
|
||||
},
|
||||
) => {
|
||||
const modeFlags = [
|
||||
options?.exhaustive,
|
||||
options?.minimal,
|
||||
options?.interactive,
|
||||
].filter(Boolean);
|
||||
const modeFlags = [options?.exhaustive, options?.minimal].filter(Boolean);
|
||||
|
||||
if (modeFlags.length > 1) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
'Error: --exhaustive, --minimal, and --interactive are mutually exclusive.',
|
||||
'Error: --exhaustive and --minimal are mutually exclusive.',
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
@@ -56,11 +47,7 @@ const program = new Command(packageJson.name)
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const mode: ScaffoldingMode = options?.minimal
|
||||
? 'minimal'
|
||||
: options?.interactive
|
||||
? 'interactive'
|
||||
: 'exhaustive';
|
||||
const mode: ScaffoldingMode = options?.minimal ? 'minimal' : 'exhaustive';
|
||||
|
||||
await new CreateAppCommand().execute(directory, mode);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["typescript"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
},
|
||||
"ignorePatterns": ["node_modules", "dist"],
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
|
||||
"typescript/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"typescript/no-explicit-any": "off"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -36,6 +36,17 @@ yarn twenty function:execute # Execute a function with JSON payload
|
||||
yarn twenty app:uninstall # Uninstall app from workspace
|
||||
```
|
||||
|
||||
## Integration Tests
|
||||
|
||||
If your project includes the example integration test (`src/__tests__/app-install.integration-test.ts`), you can run it with:
|
||||
|
||||
```bash
|
||||
# Make sure a Twenty server is running at http://localhost:3000
|
||||
yarn test
|
||||
```
|
||||
|
||||
The test builds and installs the app, then verifies it appears in the applications list. Test configuration (API URL and API key) is defined in `vitest.config.ts`.
|
||||
|
||||
## LLMs instructions
|
||||
|
||||
Main docs and pitfalls are available in LLMS.md file.
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import js from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default [
|
||||
// Base JS recommended rules
|
||||
js.configs.recommended,
|
||||
|
||||
// TypeScript recommended rules
|
||||
...tseslint.configs.recommended,
|
||||
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// Common TypeScript-friendly tweaks
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{ argsIgnorePattern: '^_' },
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'no-unused-vars': 'off', // handled by TS rule
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -27,5 +27,11 @@
|
||||
"~/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/*.integration-test.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export class CreateAppCommand {
|
||||
const { appName, appDisplayName, appDirectory, appDescription } =
|
||||
await this.getAppInfos(directory);
|
||||
|
||||
const exampleOptions = await this.resolveExampleOptions(mode);
|
||||
const exampleOptions = this.resolveExampleOptions(mode);
|
||||
|
||||
await this.validateDirectory(appDirectory);
|
||||
|
||||
@@ -103,9 +103,7 @@ export class CreateAppCommand {
|
||||
return { appName, appDisplayName, appDirectory, appDescription };
|
||||
}
|
||||
|
||||
private async resolveExampleOptions(
|
||||
mode: ScaffoldingMode,
|
||||
): Promise<ExampleOptions> {
|
||||
private resolveExampleOptions(mode: ScaffoldingMode): ExampleOptions {
|
||||
if (mode === 'minimal') {
|
||||
return {
|
||||
includeExampleObject: false,
|
||||
@@ -115,88 +113,21 @@ export class CreateAppCommand {
|
||||
includeExampleView: false,
|
||||
includeExampleNavigationMenuItem: false,
|
||||
includeExampleSkill: false,
|
||||
includeExampleAgent: false,
|
||||
includeExampleIntegrationTest: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (mode === 'exhaustive') {
|
||||
return {
|
||||
includeExampleObject: true,
|
||||
includeExampleField: true,
|
||||
includeExampleLogicFunction: true,
|
||||
includeExampleFrontComponent: true,
|
||||
includeExampleView: true,
|
||||
includeExampleNavigationMenuItem: true,
|
||||
includeExampleSkill: true,
|
||||
};
|
||||
}
|
||||
|
||||
const { selectedExamples } = await inquirer.prompt([
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'selectedExamples',
|
||||
message: 'Select which example files to include:',
|
||||
choices: [
|
||||
{
|
||||
name: 'Example object (custom object definition)',
|
||||
value: 'object',
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
name: 'Example field (custom field on the example object)',
|
||||
value: 'field',
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
name: 'Example logic function (server-side handler)',
|
||||
value: 'logicFunction',
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
name: 'Example front component (React UI component)',
|
||||
value: 'frontComponent',
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
name: 'Example view (saved view for the example object)',
|
||||
value: 'view',
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
name: 'Example navigation menu item (sidebar link)',
|
||||
value: 'navigationMenuItem',
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
name: 'Example skill (AI agent skill definition)',
|
||||
value: 'skill',
|
||||
checked: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const includeField = selectedExamples.includes('field');
|
||||
const includeView = selectedExamples.includes('view');
|
||||
const includeObject =
|
||||
selectedExamples.includes('object') || includeField || includeView;
|
||||
|
||||
if ((includeField || includeView) && !selectedExamples.includes('object')) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Note: Example object auto-included because example field/view depends on it.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
includeExampleObject: includeObject,
|
||||
includeExampleField: includeField,
|
||||
includeExampleLogicFunction: selectedExamples.includes('logicFunction'),
|
||||
includeExampleFrontComponent: selectedExamples.includes('frontComponent'),
|
||||
includeExampleView: includeView,
|
||||
includeExampleNavigationMenuItem:
|
||||
selectedExamples.includes('navigationMenuItem'),
|
||||
includeExampleSkill: selectedExamples.includes('skill'),
|
||||
includeExampleObject: true,
|
||||
includeExampleField: true,
|
||||
includeExampleLogicFunction: true,
|
||||
includeExampleFrontComponent: true,
|
||||
includeExampleView: true,
|
||||
includeExampleNavigationMenuItem: true,
|
||||
includeExampleSkill: true,
|
||||
includeExampleIntegrationTest: true,
|
||||
includeExampleAgent: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type ScaffoldingMode = 'exhaustive' | 'minimal' | 'interactive';
|
||||
export type ScaffoldingMode = 'exhaustive' | 'minimal';
|
||||
|
||||
export type ExampleOptions = {
|
||||
includeExampleObject: boolean;
|
||||
@@ -8,4 +8,6 @@ export type ExampleOptions = {
|
||||
includeExampleView: boolean;
|
||||
includeExampleNavigationMenuItem: boolean;
|
||||
includeExampleSkill: boolean;
|
||||
includeExampleAgent: boolean;
|
||||
includeExampleIntegrationTest: boolean;
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { type ExampleOptions } from '@/types/scaffolding-options';
|
||||
import { GENERATED_DIR } from 'twenty-shared/application';
|
||||
import { copyBaseApplicationProject } from '@/utils/app-template';
|
||||
import * as fs from 'fs-extra';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import createTwentyAppPackageJson from 'package.json';
|
||||
import { join } from 'path';
|
||||
import { GENERATED_DIR } from 'twenty-shared/application';
|
||||
|
||||
jest.mock('fs-extra', () => {
|
||||
const actual = jest.requireActual('fs-extra');
|
||||
@@ -25,16 +25,29 @@ const ALL_EXAMPLES: ExampleOptions = {
|
||||
includeExampleView: true,
|
||||
includeExampleNavigationMenuItem: true,
|
||||
includeExampleSkill: true,
|
||||
includeExampleAgent: true,
|
||||
includeExampleIntegrationTest: true,
|
||||
};
|
||||
|
||||
const SEED_TSCONFIG = {
|
||||
compilerOptions: { paths: { 'src/*': ['./src/*'] } },
|
||||
exclude: ['node_modules', 'dist', '**/*.integration-test.ts'],
|
||||
};
|
||||
|
||||
const seedTsconfig = async (directory: string) => {
|
||||
await fs.writeJson(join(directory, 'tsconfig.json'), SEED_TSCONFIG);
|
||||
};
|
||||
|
||||
const NO_EXAMPLES: ExampleOptions = {
|
||||
includeExampleObject: false,
|
||||
includeExampleField: false,
|
||||
includeExampleSkill: false,
|
||||
includeExampleAgent: false,
|
||||
includeExampleLogicFunction: false,
|
||||
includeExampleFrontComponent: false,
|
||||
includeExampleView: false,
|
||||
includeExampleNavigationMenuItem: false,
|
||||
includeExampleIntegrationTest: false,
|
||||
};
|
||||
|
||||
describe('copyBaseApplicationProject', () => {
|
||||
@@ -46,6 +59,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
`test-twenty-app-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||
);
|
||||
await fs.ensureDir(testAppDirectory);
|
||||
await seedTsconfig(testAppDirectory);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
@@ -149,11 +163,18 @@ describe('copyBaseApplicationProject', () => {
|
||||
"import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role'",
|
||||
);
|
||||
|
||||
expect(appConfigContent).toContain(
|
||||
'export const APPLICATION_UNIVERSAL_IDENTIFIER',
|
||||
);
|
||||
expect(appConfigContent).toContain(
|
||||
'universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER',
|
||||
);
|
||||
|
||||
expect(appConfigContent).toContain("displayName: 'My Test App'");
|
||||
expect(appConfigContent).toContain("description: 'A test application'");
|
||||
|
||||
expect(appConfigContent).toMatch(
|
||||
/universalIdentifier: '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'/,
|
||||
/APPLICATION_UNIVERSAL_IDENTIFIER =\s*'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'/,
|
||||
);
|
||||
|
||||
expect(appConfigContent).toContain(
|
||||
@@ -235,6 +256,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
it('should generate unique UUIDs for each application', async () => {
|
||||
const firstAppDir = join(testAppDirectory, 'app1');
|
||||
await fs.ensureDir(firstAppDir);
|
||||
await seedTsconfig(firstAppDir);
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'app-one',
|
||||
appDisplayName: 'App One',
|
||||
@@ -245,6 +267,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
|
||||
const secondAppDir = join(testAppDirectory, 'app2');
|
||||
await fs.ensureDir(secondAppDir);
|
||||
await seedTsconfig(secondAppDir);
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'app-two',
|
||||
appDisplayName: 'App Two',
|
||||
@@ -263,7 +286,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
);
|
||||
|
||||
const uuidRegex =
|
||||
/universalIdentifier: '([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'/;
|
||||
/APPLICATION_UNIVERSAL_IDENTIFIER =\s*'([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'/;
|
||||
const firstUuid = firstAppConfig.match(uuidRegex)?.[1];
|
||||
const secondUuid = secondAppConfig.match(uuidRegex)?.[1];
|
||||
|
||||
@@ -275,6 +298,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
it('should generate unique role UUIDs for each application', async () => {
|
||||
const firstAppDir = join(testAppDirectory, 'app1');
|
||||
await fs.ensureDir(firstAppDir);
|
||||
await seedTsconfig(firstAppDir);
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'app-one',
|
||||
appDisplayName: 'App One',
|
||||
@@ -285,6 +309,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
|
||||
const secondAppDir = join(testAppDirectory, 'app2');
|
||||
await fs.ensureDir(secondAppDir);
|
||||
await seedTsconfig(secondAppDir);
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'app-two',
|
||||
appDisplayName: 'App Two',
|
||||
@@ -355,6 +380,12 @@ describe('copyBaseApplicationProject', () => {
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
join(srcPath, '__tests__', 'app-install.integration-test.ts'),
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
// Install functions should always exist
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
@@ -428,6 +459,11 @@ describe('copyBaseApplicationProject', () => {
|
||||
),
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
join(srcPath, '__tests__', 'app-install.integration-test.ts'),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -442,10 +478,12 @@ describe('copyBaseApplicationProject', () => {
|
||||
includeExampleObject: false,
|
||||
includeExampleField: false,
|
||||
includeExampleSkill: false,
|
||||
includeExampleAgent: false,
|
||||
includeExampleLogicFunction: false,
|
||||
includeExampleFrontComponent: true,
|
||||
includeExampleView: false,
|
||||
includeExampleNavigationMenuItem: false,
|
||||
includeExampleIntegrationTest: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -478,11 +516,13 @@ describe('copyBaseApplicationProject', () => {
|
||||
exampleOptions: {
|
||||
includeExampleObject: false,
|
||||
includeExampleSkill: false,
|
||||
includeExampleAgent: false,
|
||||
includeExampleField: false,
|
||||
includeExampleLogicFunction: true,
|
||||
includeExampleFrontComponent: false,
|
||||
includeExampleView: false,
|
||||
includeExampleNavigationMenuItem: false,
|
||||
includeExampleIntegrationTest: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -540,6 +580,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
it('should generate unique UUIDs for example objects across apps', async () => {
|
||||
const firstAppDir = join(testAppDirectory, 'app1');
|
||||
await fs.ensureDir(firstAppDir);
|
||||
await seedTsconfig(firstAppDir);
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'app-one',
|
||||
appDisplayName: 'App One',
|
||||
@@ -550,6 +591,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
|
||||
const secondAppDir = join(testAppDirectory, 'app2');
|
||||
await fs.ensureDir(secondAppDir);
|
||||
await seedTsconfig(secondAppDir);
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'app-two',
|
||||
appDisplayName: 'App Two',
|
||||
@@ -635,15 +677,24 @@ describe('copyBaseApplicationProject', () => {
|
||||
|
||||
const content = await fs.readFile(viewPath, 'utf8');
|
||||
|
||||
expect(content).toContain("import { defineView } from 'twenty-sdk'");
|
||||
expect(content).toContain(
|
||||
"import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/objects/example-object'",
|
||||
"import { defineView, ViewKey } from 'twenty-sdk'",
|
||||
);
|
||||
expect(content).toContain(
|
||||
"import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER, NAME_FIELD_UNIVERSAL_IDENTIFIER } from 'src/objects/example-object'",
|
||||
);
|
||||
expect(content).toContain('export default defineView({');
|
||||
expect(content).toContain(
|
||||
'objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER',
|
||||
);
|
||||
expect(content).toContain("name: 'example-view'");
|
||||
expect(content).toContain("name: 'All example items'");
|
||||
expect(content).toContain('fields: [');
|
||||
expect(content).toContain(
|
||||
'fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER',
|
||||
);
|
||||
expect(content).toContain('isVisible: true');
|
||||
expect(content).toContain('key: ViewKey.INDEX');
|
||||
expect(content).toContain('size: 200');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -674,6 +725,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
expect(content).toContain('export default defineNavigationMenuItem({');
|
||||
expect(content).toContain("name: 'example-navigation-menu-item'");
|
||||
expect(content).toContain("icon: 'IconList'");
|
||||
expect(content).toContain("color: 'blue'");
|
||||
expect(content).toContain('position: 0');
|
||||
});
|
||||
});
|
||||
@@ -738,6 +790,44 @@ describe('copyBaseApplicationProject', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration test', () => {
|
||||
it('should include vitest and test scripts in package.json when enabled', async () => {
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'my-test-app',
|
||||
appDisplayName: 'My Test App',
|
||||
appDescription: 'A test application',
|
||||
appDirectory: testAppDirectory,
|
||||
exampleOptions: ALL_EXAMPLES,
|
||||
});
|
||||
|
||||
const packageJson = await fs.readJson(
|
||||
join(testAppDirectory, 'package.json'),
|
||||
);
|
||||
|
||||
expect(packageJson.scripts.test).toBe('vitest run');
|
||||
expect(packageJson.scripts['test:watch']).toBe('vitest');
|
||||
expect(packageJson.devDependencies.vitest).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not include vitest or test scripts when disabled', async () => {
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'my-test-app',
|
||||
appDisplayName: 'My Test App',
|
||||
appDescription: 'A test application',
|
||||
appDirectory: testAppDirectory,
|
||||
exampleOptions: NO_EXAMPLES,
|
||||
});
|
||||
|
||||
const packageJson = await fs.readJson(
|
||||
join(testAppDirectory, 'package.json'),
|
||||
);
|
||||
|
||||
expect(packageJson.scripts.test).toBeUndefined();
|
||||
expect(packageJson.scripts['test:watch']).toBeUndefined();
|
||||
expect(packageJson.devDependencies.vitest).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('post-install logic function', () => {
|
||||
it('should create post-install.ts with definePostInstallLogicFunction and typed payload', async () => {
|
||||
await copyBaseApplicationProject({
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { scaffoldIntegrationTest } from '@/utils/test-template';
|
||||
import * as fs from 'fs-extra';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
describe('scaffoldIntegrationTest', () => {
|
||||
let testAppDirectory: string;
|
||||
let sourceFolderPath: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
testAppDirectory = join(
|
||||
tmpdir(),
|
||||
`test-twenty-app-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||
);
|
||||
sourceFolderPath = join(testAppDirectory, 'src');
|
||||
await fs.ensureDir(sourceFolderPath);
|
||||
|
||||
await fs.writeJson(join(testAppDirectory, 'tsconfig.json'), {
|
||||
compilerOptions: {
|
||||
paths: { 'src/*': ['./src/*'] },
|
||||
},
|
||||
exclude: ['node_modules', 'dist', '**/*.integration-test.ts'],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (testAppDirectory && (await fs.pathExists(testAppDirectory))) {
|
||||
await fs.remove(testAppDirectory);
|
||||
}
|
||||
});
|
||||
|
||||
describe('integration test file', () => {
|
||||
it('should create app-install.integration-test.ts with correct structure', async () => {
|
||||
await scaffoldIntegrationTest({
|
||||
appDirectory: testAppDirectory,
|
||||
sourceFolderPath,
|
||||
});
|
||||
|
||||
const testPath = join(
|
||||
sourceFolderPath,
|
||||
'__tests__',
|
||||
'app-install.integration-test.ts',
|
||||
);
|
||||
|
||||
expect(await fs.pathExists(testPath)).toBe(true);
|
||||
|
||||
const content = await fs.readFile(testPath, 'utf8');
|
||||
|
||||
expect(content).toContain(
|
||||
"import { appBuild, appUninstall } from 'twenty-sdk/cli'",
|
||||
);
|
||||
expect(content).toContain(
|
||||
"import { MetadataApiClient } from 'twenty-sdk/clients'",
|
||||
);
|
||||
expect(content).toContain(
|
||||
"import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config'",
|
||||
);
|
||||
expect(content).toContain('appBuild');
|
||||
expect(content).toContain('appUninstall');
|
||||
expect(content).toContain('new MetadataApiClient()');
|
||||
expect(content).toContain('findManyApplications');
|
||||
expect(content).toContain('APPLICATION_UNIVERSAL_IDENTIFIER');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setup-test file', () => {
|
||||
it('should create setup-test.ts with SDK config bootstrap', async () => {
|
||||
await scaffoldIntegrationTest({
|
||||
appDirectory: testAppDirectory,
|
||||
sourceFolderPath,
|
||||
});
|
||||
|
||||
const setupTestPath = join(
|
||||
sourceFolderPath,
|
||||
'__tests__',
|
||||
'setup-test.ts',
|
||||
);
|
||||
|
||||
expect(await fs.pathExists(setupTestPath)).toBe(true);
|
||||
|
||||
const content = await fs.readFile(setupTestPath, 'utf8');
|
||||
|
||||
expect(content).toContain('.twenty-sdk-test');
|
||||
expect(content).toContain('config.json');
|
||||
expect(content).toContain('process.env.TWENTY_API_URL');
|
||||
expect(content).toContain('process.env.TWENTY_API_KEY');
|
||||
expect(content).toContain('assertServerIsReachable');
|
||||
});
|
||||
});
|
||||
|
||||
describe('vitest config', () => {
|
||||
it('should create vitest.config.ts with env vars and setup file', async () => {
|
||||
await scaffoldIntegrationTest({
|
||||
appDirectory: testAppDirectory,
|
||||
sourceFolderPath,
|
||||
});
|
||||
|
||||
const vitestConfigPath = join(testAppDirectory, 'vitest.config.ts');
|
||||
|
||||
expect(await fs.pathExists(vitestConfigPath)).toBe(true);
|
||||
|
||||
const content = await fs.readFile(vitestConfigPath, 'utf8');
|
||||
|
||||
expect(content).toContain('TWENTY_API_KEY');
|
||||
expect(content).not.toContain('TWENTY_TEST_API_KEY');
|
||||
expect(content).toContain('TWENTY_API_URL');
|
||||
expect(content).toContain('setup-test.ts');
|
||||
expect(content).toContain('tsconfig.spec.json');
|
||||
expect(content).toContain('integration-test.ts');
|
||||
});
|
||||
});
|
||||
|
||||
describe('tsconfig.spec.json', () => {
|
||||
it('should create tsconfig.spec.json extending the base tsconfig', async () => {
|
||||
await scaffoldIntegrationTest({
|
||||
appDirectory: testAppDirectory,
|
||||
sourceFolderPath,
|
||||
});
|
||||
|
||||
const tsconfigSpecPath = join(testAppDirectory, 'tsconfig.spec.json');
|
||||
|
||||
expect(await fs.pathExists(tsconfigSpecPath)).toBe(true);
|
||||
|
||||
const tsconfigSpec = await fs.readJson(tsconfigSpecPath);
|
||||
|
||||
expect(tsconfigSpec.extends).toBe('./tsconfig.json');
|
||||
expect(tsconfigSpec.compilerOptions.composite).toBe(true);
|
||||
expect(tsconfigSpec.include).toContain('src/**/*.ts');
|
||||
expect(tsconfigSpec.exclude).not.toContain('**/*.integration-test.ts');
|
||||
});
|
||||
|
||||
it('should add a reference to tsconfig.spec.json in tsconfig.json', async () => {
|
||||
await scaffoldIntegrationTest({
|
||||
appDirectory: testAppDirectory,
|
||||
sourceFolderPath,
|
||||
});
|
||||
|
||||
const tsconfig = await fs.readJson(
|
||||
join(testAppDirectory, 'tsconfig.json'),
|
||||
);
|
||||
|
||||
expect(tsconfig.references).toEqual([{ path: './tsconfig.spec.json' }]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,10 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
import { v4 } from 'uuid';
|
||||
import { ASSETS_DIR } from 'twenty-shared/application';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { type ExampleOptions } from '@/types/scaffolding-options';
|
||||
import { scaffoldIntegrationTest } from '@/utils/test-template';
|
||||
import createTwentyAppPackageJson from 'package.json';
|
||||
|
||||
const SRC_FOLDER = 'src';
|
||||
@@ -23,7 +24,11 @@ export const copyBaseApplicationProject = async ({
|
||||
}) => {
|
||||
await fs.copy(join(__dirname, './constants/base-application'), appDirectory);
|
||||
|
||||
await createPackageJson({ appName, appDirectory });
|
||||
await createPackageJson({
|
||||
appName,
|
||||
appDirectory,
|
||||
includeExampleIntegrationTest: exampleOptions.includeExampleIntegrationTest,
|
||||
});
|
||||
|
||||
await createGitignore(appDirectory);
|
||||
|
||||
@@ -98,6 +103,21 @@ export const copyBaseApplicationProject = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (exampleOptions.includeExampleAgent) {
|
||||
await createExampleAgent({
|
||||
appDirectory: sourceFolderPath,
|
||||
fileFolder: 'agents',
|
||||
fileName: 'example-agent.ts',
|
||||
});
|
||||
}
|
||||
|
||||
if (exampleOptions.includeExampleIntegrationTest) {
|
||||
await scaffoldIntegrationTest({
|
||||
appDirectory,
|
||||
sourceFolderPath,
|
||||
});
|
||||
}
|
||||
|
||||
await createDefaultPreInstallFunction({
|
||||
appDirectory: sourceFolderPath,
|
||||
fileFolder: 'logic-functions',
|
||||
@@ -167,6 +187,7 @@ yarn-error.log*
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
*.d.ts
|
||||
`;
|
||||
|
||||
await fs.writeFile(join(appDirectory, '.gitignore'), gitignoreContent);
|
||||
@@ -418,16 +439,29 @@ const createExampleView = async ({
|
||||
fileName: string;
|
||||
}) => {
|
||||
const universalIdentifier = v4();
|
||||
const viewFieldUniversalIdentifier = v4();
|
||||
|
||||
const content = `import { defineView } from 'twenty-sdk';
|
||||
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/objects/example-object';
|
||||
const content = `import { defineView, ViewKey } from 'twenty-sdk';
|
||||
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER, NAME_FIELD_UNIVERSAL_IDENTIFIER } from 'src/objects/example-object';
|
||||
|
||||
export const EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER = '${universalIdentifier}';
|
||||
|
||||
export default defineView({
|
||||
universalIdentifier: '${universalIdentifier}',
|
||||
name: 'example-view',
|
||||
universalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
name: 'All example items',
|
||||
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
icon: 'IconList',
|
||||
key: ViewKey.INDEX,
|
||||
position: 0,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: '${viewFieldUniversalIdentifier}',
|
||||
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
},
|
||||
],
|
||||
});
|
||||
`;
|
||||
|
||||
@@ -447,18 +481,15 @@ const createExampleNavigationMenuItem = async ({
|
||||
const universalIdentifier = v4();
|
||||
|
||||
const content = `import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: '${universalIdentifier}',
|
||||
name: 'example-navigation-menu-item',
|
||||
icon: 'IconList',
|
||||
color: 'blue',
|
||||
position: 0,
|
||||
// Link to a view:
|
||||
// viewUniversalIdentifier: '...',
|
||||
// Or link to an object:
|
||||
// targetObjectUniversalIdentifier: '...',
|
||||
// Or link to an external URL:
|
||||
// link: 'https://example.com',
|
||||
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
`;
|
||||
|
||||
@@ -496,6 +527,36 @@ export default defineSkill({
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
};
|
||||
|
||||
const createExampleAgent = async ({
|
||||
appDirectory,
|
||||
fileFolder,
|
||||
fileName,
|
||||
}: {
|
||||
appDirectory: string;
|
||||
fileFolder?: string;
|
||||
fileName: string;
|
||||
}) => {
|
||||
const universalIdentifier = v4();
|
||||
|
||||
const content = `import { defineAgent } from 'twenty-sdk';
|
||||
|
||||
export const EXAMPLE_AGENT_UNIVERSAL_IDENTIFIER =
|
||||
'${universalIdentifier}';
|
||||
|
||||
export default defineAgent({
|
||||
universalIdentifier: EXAMPLE_AGENT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'example-agent',
|
||||
label: 'Example Agent',
|
||||
description: 'A sample AI agent for your application',
|
||||
icon: 'IconRobot',
|
||||
prompt: 'You are a helpful assistant. Help users with their questions and tasks.',
|
||||
});
|
||||
`;
|
||||
|
||||
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
};
|
||||
|
||||
const createApplicationConfig = async ({
|
||||
displayName,
|
||||
description,
|
||||
@@ -509,11 +570,16 @@ const createApplicationConfig = async ({
|
||||
fileFolder?: string;
|
||||
fileName: string;
|
||||
}) => {
|
||||
const universalIdentifier = v4();
|
||||
|
||||
const content = `import { defineApplication } from 'twenty-sdk';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
|
||||
export const APPLICATION_UNIVERSAL_IDENTIFIER =
|
||||
'${universalIdentifier}';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '${v4()}',
|
||||
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
displayName: '${displayName}',
|
||||
description: '${description ?? ''}',
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
@@ -527,10 +593,34 @@ export default defineApplication({
|
||||
const createPackageJson = async ({
|
||||
appName,
|
||||
appDirectory,
|
||||
includeExampleIntegrationTest,
|
||||
}: {
|
||||
appName: string;
|
||||
appDirectory: string;
|
||||
includeExampleIntegrationTest: boolean;
|
||||
}) => {
|
||||
const scripts: Record<string, string> = {
|
||||
twenty: 'twenty',
|
||||
lint: 'oxlint -c .oxlintrc.json .',
|
||||
'lint:fix': 'oxlint --fix -c .oxlintrc.json .',
|
||||
};
|
||||
|
||||
const devDependencies: Record<string, string> = {
|
||||
typescript: '^5.9.3',
|
||||
'@types/node': '^24.7.2',
|
||||
'@types/react': '^18.2.0',
|
||||
react: '^18.2.0',
|
||||
oxlint: '^0.16.0',
|
||||
'twenty-sdk': createTwentyAppPackageJson.version,
|
||||
};
|
||||
|
||||
if (includeExampleIntegrationTest) {
|
||||
scripts.test = 'vitest run';
|
||||
scripts['test:watch'] = 'vitest';
|
||||
devDependencies.vitest = '^3.1.1';
|
||||
devDependencies['vite-tsconfig-paths'] = '^4.2.1';
|
||||
}
|
||||
|
||||
const packageJson = {
|
||||
name: appName,
|
||||
version: '0.1.0',
|
||||
@@ -541,21 +631,8 @@ const createPackageJson = async ({
|
||||
yarn: '>=4.0.2',
|
||||
},
|
||||
packageManager: 'yarn@4.9.2',
|
||||
scripts: {
|
||||
twenty: 'twenty',
|
||||
lint: 'eslint',
|
||||
'lint:fix': 'eslint --fix',
|
||||
},
|
||||
dependencies: {},
|
||||
devDependencies: {
|
||||
'twenty-sdk': createTwentyAppPackageJson.version,
|
||||
typescript: '^5.9.3',
|
||||
'@types/node': '^24.7.2',
|
||||
'@types/react': '^18.2.0',
|
||||
react: '^18.2.0',
|
||||
eslint: '^9.32.0',
|
||||
'typescript-eslint': '^8.50.0',
|
||||
},
|
||||
scripts,
|
||||
devDependencies,
|
||||
};
|
||||
|
||||
await fs.writeFile(
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
|
||||
const SEED_API_KEY =
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik';
|
||||
|
||||
export const scaffoldIntegrationTest = async ({
|
||||
appDirectory,
|
||||
sourceFolderPath,
|
||||
}: {
|
||||
appDirectory: string;
|
||||
sourceFolderPath: string;
|
||||
}) => {
|
||||
await createIntegrationTest({
|
||||
appDirectory: sourceFolderPath,
|
||||
fileFolder: '__tests__',
|
||||
fileName: 'app-install.integration-test.ts',
|
||||
});
|
||||
|
||||
await createSetupTest({
|
||||
appDirectory: sourceFolderPath,
|
||||
fileFolder: '__tests__',
|
||||
fileName: 'setup-test.ts',
|
||||
});
|
||||
|
||||
await createVitestConfig(appDirectory);
|
||||
await createTsconfigSpec(appDirectory);
|
||||
};
|
||||
|
||||
const createVitestConfig = async (appDirectory: string) => {
|
||||
const content = `import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tsconfigPaths({
|
||||
projects: ['tsconfig.spec.json'],
|
||||
ignoreConfigErrors: true,
|
||||
}),
|
||||
],
|
||||
test: {
|
||||
testTimeout: 120_000,
|
||||
hookTimeout: 120_000,
|
||||
include: ['src/**/*.integration-test.ts'],
|
||||
setupFiles: ['src/__tests__/setup-test.ts'],
|
||||
env: {
|
||||
TWENTY_API_URL: 'http://localhost:3000',
|
||||
TWENTY_API_KEY:
|
||||
'${SEED_API_KEY}',
|
||||
},
|
||||
},
|
||||
});
|
||||
`;
|
||||
|
||||
await fs.writeFile(join(appDirectory, 'vitest.config.ts'), content);
|
||||
};
|
||||
|
||||
const createTsconfigSpec = async (appDirectory: string) => {
|
||||
const tsconfigSpec = {
|
||||
extends: './tsconfig.json',
|
||||
compilerOptions: {
|
||||
composite: true,
|
||||
types: ['vitest/globals'],
|
||||
},
|
||||
include: ['src/**/*.ts', 'src/**/*.tsx'],
|
||||
exclude: ['node_modules', 'dist'],
|
||||
};
|
||||
|
||||
await fs.writeFile(
|
||||
join(appDirectory, 'tsconfig.spec.json'),
|
||||
JSON.stringify(tsconfigSpec, null, 2),
|
||||
);
|
||||
|
||||
const tsconfigPath = join(appDirectory, 'tsconfig.json');
|
||||
const tsconfig = await fs.readJson(tsconfigPath);
|
||||
|
||||
tsconfig.references = [{ path: './tsconfig.spec.json' }];
|
||||
|
||||
await fs.writeFile(tsconfigPath, JSON.stringify(tsconfig, null, 2));
|
||||
};
|
||||
|
||||
const createSetupTest = async ({
|
||||
appDirectory,
|
||||
fileFolder,
|
||||
fileName,
|
||||
}: {
|
||||
appDirectory: string;
|
||||
fileFolder?: string;
|
||||
fileName: string;
|
||||
}) => {
|
||||
const content = `import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { beforeAll } from 'vitest';
|
||||
|
||||
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:3000';
|
||||
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
|
||||
|
||||
const assertServerIsReachable = async () => {
|
||||
let response: Response;
|
||||
|
||||
try {
|
||||
response = await fetch(\`\${TWENTY_API_URL}/healthz\`);
|
||||
} catch {
|
||||
throw new Error(
|
||||
\`Twenty server is not reachable at \${TWENTY_API_URL}. \` +
|
||||
'Make sure the server is running before executing integration tests.',
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(\`Server at \${TWENTY_API_URL} returned \${response.status}\`);
|
||||
}
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
await assertServerIsReachable();
|
||||
|
||||
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
|
||||
|
||||
const configFile = {
|
||||
profiles: {
|
||||
default: {
|
||||
apiUrl: process.env.TWENTY_API_URL,
|
||||
apiKey: process.env.TWENTY_API_KEY,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(TEST_CONFIG_DIR, 'config.json'),
|
||||
JSON.stringify(configFile, null, 2),
|
||||
);
|
||||
});
|
||||
`;
|
||||
|
||||
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
};
|
||||
|
||||
const createIntegrationTest = async ({
|
||||
appDirectory,
|
||||
fileFolder,
|
||||
fileName,
|
||||
}: {
|
||||
appDirectory: string;
|
||||
fileFolder?: string;
|
||||
fileName: string;
|
||||
}) => {
|
||||
const content = `import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
|
||||
import { appBuild, appUninstall } from 'twenty-sdk/cli';
|
||||
import { MetadataApiClient } from 'twenty-sdk/clients';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
const APP_PATH = process.cwd();
|
||||
|
||||
describe('App installation', () => {
|
||||
let appInstalled = false;
|
||||
|
||||
beforeAll(async () => {
|
||||
const buildResult = await appBuild({
|
||||
appPath: APP_PATH,
|
||||
onProgress: (message: string) => console.log(\`[build] \${message}\`),
|
||||
});
|
||||
|
||||
if (!buildResult.success) {
|
||||
throw new Error(
|
||||
\`Build failed: \${buildResult.error?.message ?? 'Unknown error'}\`,
|
||||
);
|
||||
}
|
||||
|
||||
appInstalled = true;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (!appInstalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uninstallResult = await appUninstall({ appPath: APP_PATH });
|
||||
|
||||
if (!uninstallResult.success) {
|
||||
console.warn(
|
||||
\`App uninstall failed: \${uninstallResult.error?.message ?? 'Unknown error'}\`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should find the installed app in the applications list', async () => {
|
||||
const metadataClient = new MetadataApiClient();
|
||||
|
||||
const result = await metadataClient.query({
|
||||
findManyApplications: {
|
||||
id: true,
|
||||
name: true,
|
||||
universalIdentifier: true,
|
||||
},
|
||||
});
|
||||
|
||||
const installedApp = result.findManyApplications.find(
|
||||
(application: { universalIdentifier: string }) =>
|
||||
application.universalIdentifier ===
|
||||
APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
|
||||
expect(installedApp).toBeDefined();
|
||||
});
|
||||
});
|
||||
`;
|
||||
|
||||
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
};
|
||||
@@ -23,5 +23,6 @@
|
||||
"**/__mocks__/**/*",
|
||||
"vite.config.ts",
|
||||
"jest.config.mjs"
|
||||
]
|
||||
],
|
||||
"exclude": ["src/constants/base-application/vitest.config.ts"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["typescript", "import", "unicorn"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
},
|
||||
"ignorePatterns": ["node_modules"],
|
||||
"rules": {
|
||||
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
|
||||
"no-console": ["warn", { "allow": ["group", "groupCollapsed", "groupEnd"] }],
|
||||
"no-control-regex": "off",
|
||||
"no-debugger": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"no-undef": "off",
|
||||
"no-unused-vars": "off",
|
||||
"no-redeclare": "off",
|
||||
"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/explicit-function-return-type": "off",
|
||||
"typescript/explicit-module-boundary-types": "off",
|
||||
"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": "^_"
|
||||
}]
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import js from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default [
|
||||
// Base JS recommended rules
|
||||
js.configs.recommended,
|
||||
|
||||
// TypeScript recommended rules
|
||||
...tseslint.configs.recommended,
|
||||
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// Common TypeScript-friendly tweaks
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{ argsIgnorePattern: '^_' },
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'no-unused-vars': 'off', // handled by TS rule
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -10,8 +10,8 @@
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"scripts": {
|
||||
"twenty": "twenty",
|
||||
"lint": "eslint",
|
||||
"lint:fix": "eslint --fix"
|
||||
"lint": "oxlint -c .oxlintrc.json .",
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-sdk": "latest"
|
||||
@@ -19,9 +19,8 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2",
|
||||
"@types/react": "^18.2.0",
|
||||
"eslint": "^9.32.0",
|
||||
"oxlint": "^0.16.0",
|
||||
"react": "^18.2.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.50.0"
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
-9
@@ -1,7 +1,6 @@
|
||||
import { defineLogicFunction, RoutePayload } from "twenty-sdk";
|
||||
import { MetadataApiClient } from 'twenty-sdk/generated';
|
||||
|
||||
|
||||
export const OAUTH_TOKEN_PAIRS_PATH = '/oauth/token-pairs';
|
||||
|
||||
type ApolloTokenResponse = {
|
||||
@@ -53,16 +52,8 @@ const handler = async (event: RoutePayload): Promise<any> => {
|
||||
const apolloClientSecret = process.env.APOLLO_CLIENT_SECRET ?? '';
|
||||
const applicationId = process.env.APPLICATION_ID ?? '';
|
||||
|
||||
|
||||
const metadataClient = new MetadataApiClient({});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const tokenPairs = await getAuthenticationTokenPairs(
|
||||
code,
|
||||
apolloClientId,
|
||||
|
||||
@@ -5,8 +5,6 @@ import {
|
||||
} from 'twenty-sdk';
|
||||
import { CoreApiClient } from 'twenty-sdk/generated';
|
||||
|
||||
|
||||
|
||||
type CompanyRecord = {
|
||||
id: string;
|
||||
name?: string;
|
||||
@@ -38,8 +36,6 @@ type ApolloEnrichResponse = {
|
||||
organization?: ApolloOrganization;
|
||||
};
|
||||
|
||||
|
||||
|
||||
const extractDomain = (
|
||||
domainName?: CompanyRecord['domainName'],
|
||||
): string | undefined => {
|
||||
@@ -172,7 +168,6 @@ const updateCompanyInTwenty = async (
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
type CompanyUpdateEvent = DatabaseEventPayload<
|
||||
ObjectRecordUpdateEvent<CompanyRecord>
|
||||
>;
|
||||
@@ -181,7 +176,6 @@ const handler = async (
|
||||
event: CompanyUpdateEvent,
|
||||
): Promise<object | undefined> => {
|
||||
|
||||
|
||||
const { recordId, properties } = event;
|
||||
const { after: companyAfter } = properties;
|
||||
|
||||
@@ -206,7 +200,6 @@ const handler = async (
|
||||
return { skipped: true, reason: 'no enrichment data to apply' };
|
||||
}
|
||||
|
||||
|
||||
await updateCompanyInTwenty(recordId, updateData);
|
||||
|
||||
const result = {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["typescript", "import", "unicorn"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
},
|
||||
"ignorePatterns": ["node_modules"],
|
||||
"rules": {
|
||||
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
|
||||
"no-console": ["warn", { "allow": ["group", "groupCollapsed", "groupEnd"] }],
|
||||
"no-control-regex": "off",
|
||||
"no-debugger": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"no-undef": "off",
|
||||
"no-unused-vars": "off",
|
||||
"no-redeclare": "off",
|
||||
|
||||
"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/explicit-function-return-type": "off",
|
||||
"typescript/explicit-module-boundary-types": "off",
|
||||
"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": "^_"
|
||||
}]
|
||||
}
|
||||
}
|
||||
@@ -26,21 +26,6 @@
|
||||
"typecheck": {
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint",
|
||||
"outputs": [
|
||||
"{options.outputFile}"
|
||||
],
|
||||
"options": {
|
||||
"lintFilePatterns": [
|
||||
"packages/twenty-apps/community/fireflies/**/*.{ts,tsx,js,jsx}"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"fix": {
|
||||
"fix": true
|
||||
}
|
||||
}
|
||||
}
|
||||
"lint": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Usage: yarn setup:fields
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
/* oxlint-disable no-console */
|
||||
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable no-console */
|
||||
/* oxlint-disable no-console */
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable no-console */
|
||||
/* oxlint-disable no-console */
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable no-console */
|
||||
/* oxlint-disable no-console */
|
||||
/**
|
||||
* Fetch historical Fireflies meetings and insert into Twenty.
|
||||
*
|
||||
@@ -219,4 +219,3 @@ main().catch((error) => {
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable no-console */
|
||||
/* oxlint-disable no-console */
|
||||
/**
|
||||
* Fetch a Fireflies meeting by ID and insert it into Twenty using the same path
|
||||
* as the webhook handler.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable no-console */
|
||||
/* oxlint-disable no-console */
|
||||
/**
|
||||
* Test script for Fireflies webhook against local Twenty instance
|
||||
*
|
||||
|
||||
@@ -81,4 +81,3 @@ describe('HistoricalImporter', () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -158,4 +158,3 @@ export class HistoricalImporter {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ export class AppLogger {
|
||||
debug(message: string, ...args: unknown[]): void {
|
||||
this.captureLog('debug', message, ...args);
|
||||
if (this.shouldLog('debug')) {
|
||||
// eslint-disable-next-line no-console
|
||||
// oxlint-disable-next-line no-console
|
||||
console.log(`[${this.context}] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ export class AppLogger {
|
||||
info(message: string, ...args: unknown[]): void {
|
||||
this.captureLog('info', message, ...args);
|
||||
if (this.shouldLog('info')) {
|
||||
// eslint-disable-next-line no-console
|
||||
// oxlint-disable-next-line no-console
|
||||
console.log(`[${this.context}] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,7 @@ export class AppLogger {
|
||||
warn(message: string, ...args: unknown[]): void {
|
||||
this.captureLog('warn', message, ...args);
|
||||
if (this.shouldLog('warn')) {
|
||||
// eslint-disable-next-line no-console
|
||||
// oxlint-disable-next-line no-console
|
||||
console.warn(`[${this.context}] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
@@ -96,7 +96,7 @@ export class AppLogger {
|
||||
error(message: string, ...args: unknown[]): void {
|
||||
this.captureLog('error', message, ...args);
|
||||
if (this.shouldLog('error')) {
|
||||
// eslint-disable-next-line no-console
|
||||
// oxlint-disable-next-line no-console
|
||||
console.error(`[${this.context}] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
@@ -104,7 +104,7 @@ export class AppLogger {
|
||||
// For fatal errors, security issues, or data corruption - always visible
|
||||
critical(message: string, ...args: unknown[]): void {
|
||||
this.captureLog('error', `CRITICAL: ${message}`, ...args);
|
||||
// eslint-disable-next-line no-console
|
||||
// oxlint-disable-next-line no-console
|
||||
console.error(`[${this.context}] CRITICAL: ${message}`, ...args);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export { Meeting } from './meeting';
|
||||
|
||||
|
||||
|
||||
@@ -330,7 +330,6 @@ export class WebhookHandler {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async createFailedMeetingRecord(params: unknown, error: string): Promise<void> {
|
||||
try {
|
||||
const twentyApiKey = process.env.TWENTY_API_KEY || '';
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["react", "typescript", "import", "unicorn"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"node_modules"
|
||||
],
|
||||
"rules": {
|
||||
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
|
||||
"no-console": ["warn", { "allow": ["group", "groupCollapsed", "groupEnd"] }],
|
||||
"no-control-regex": "off",
|
||||
"no-debugger": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"no-undef": "off",
|
||||
"no-unused-vars": "off",
|
||||
"no-redeclare": "off",
|
||||
|
||||
"import/no-duplicates": "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/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "warn",
|
||||
|
||||
"typescript/no-redeclare": "error",
|
||||
"typescript/ban-ts-comment": "error",
|
||||
"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-function": "off",
|
||||
"typescript/no-explicit-any": "off",
|
||||
"typescript/no-unused-vars": ["warn", {
|
||||
"vars": "all",
|
||||
"varsIgnorePattern": "^_",
|
||||
"args": "after-used",
|
||||
"argsIgnorePattern": "^_"
|
||||
}]
|
||||
}
|
||||
}
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
import typescriptParser from '@typescript-eslint/parser';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import reactConfig from '../../../../twenty-eslint-rules/eslint.config.react.mjs';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export default [
|
||||
// Extend shared React configuration
|
||||
...reactConfig,
|
||||
|
||||
// Global ignores
|
||||
{
|
||||
ignores: [
|
||||
'**/node_modules/**',
|
||||
],
|
||||
},
|
||||
|
||||
// TypeScript project-specific configuration
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
parser: typescriptParser,
|
||||
parserOptions: {
|
||||
project: [path.resolve(__dirname, 'tsconfig.*.json')],
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'@nx/enforce-module-boundaries': [
|
||||
'error',
|
||||
{
|
||||
enforceBuildableLibDependency: true,
|
||||
allow: [],
|
||||
depConstraints: [
|
||||
{
|
||||
sourceTag: 'scope:sdk',
|
||||
onlyDependOnLibsWithTags: ['scope:sdk'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:shared',
|
||||
onlyDependOnLibsWithTags: ['scope:shared'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:backend',
|
||||
onlyDependOnLibsWithTags: ['scope:shared', 'scope:backend'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:frontend',
|
||||
onlyDependOnLibsWithTags: ['scope:shared', 'scope:frontend'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:zapier',
|
||||
onlyDependOnLibsWithTags: ['scope:shared'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:browser-extension',
|
||||
onlyDependOnLibsWithTags: ['scope:twenty-ui', 'scope:browser-extension']
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
];
|
||||
-1
@@ -43,7 +43,6 @@ export default defineContentScript({
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
ctx.addEventListener(window, 'wxt:locationchange', ({newUrl, }) => {
|
||||
const injectedBtn = document.querySelector('[data-id="twenty-btn"]');
|
||||
if(personPattern.includes(newUrl) && !injectedBtn) ui.mount();
|
||||
|
||||
-2
@@ -8,7 +8,6 @@ const StyledButton = styled.button`
|
||||
--text-color: #0a66c2;
|
||||
--bg-color: #00000000;
|
||||
|
||||
|
||||
font-size: ${({theme}) => theme.spacing(3.5)};
|
||||
font-weight: ${({theme}) => theme.font.weight.semiBold};
|
||||
font-family: ${({theme}) => theme.font.family};
|
||||
@@ -26,7 +25,6 @@ const StyledButton = styled.button`
|
||||
transition-timing-function: cubic-bezier(.4, 0, .2, 1);
|
||||
transition-duration: 167ms;
|
||||
|
||||
|
||||
&:hover {
|
||||
background-color: var(--hover-bg-color);
|
||||
color: var(--hover-color);
|
||||
|
||||
+2
-7
@@ -1,6 +1,5 @@
|
||||
import { ThemeProvider } from '@emotion/react';
|
||||
import type React from 'react';
|
||||
import { THEME_DARK, ThemeContextProvider } from 'twenty-ui/theme';
|
||||
import { ColorSchemeProvider } from 'twenty-ui/theme-constants';
|
||||
|
||||
type ThemeContextProps = {
|
||||
children: React.ReactNode;
|
||||
@@ -8,10 +7,6 @@ type ThemeContextProps = {
|
||||
|
||||
export const ThemeContext = ({ children }: ThemeContextProps) => {
|
||||
return (
|
||||
<ThemeProvider theme={THEME_DARK}>
|
||||
<ThemeContextProvider theme={THEME_DARK}>
|
||||
{children}
|
||||
</ThemeContextProvider>
|
||||
</ThemeProvider>
|
||||
<ColorSchemeProvider colorScheme="dark">{children}</ColorSchemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
import type { ThemeType } from 'twenty-ui/theme';
|
||||
|
||||
declare module '@emotion/react' {
|
||||
export interface Theme extends ThemeType {}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
TWENTY_API_KEY=<SET_YOUR_TWENTY_API_KEY>
|
||||
TWENTY_API_URL=<SET_YOUR_TWENTY_API_URL>
|
||||
@@ -1,3 +1,38 @@
|
||||
.yarn/install-state.gz
|
||||
.env
|
||||
# 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/
|
||||
|
||||
.twenty
|
||||
|
||||
# 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
|
||||
*.d.ts
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
24.5.0
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["typescript"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
},
|
||||
"ignorePatterns": ["node_modules", "dist"],
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
|
||||
"typescript/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"typescript/no-explicit-any": "off"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
## Base documentation
|
||||
|
||||
- Documentation: https://docs.twenty.com/developers/extend/capabilities/apps
|
||||
- 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
|
||||
|
||||
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
|
||||
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
|
||||
@@ -1,41 +1,62 @@
|
||||
# Hello world
|
||||
This is a [Twenty](https://twenty.com) application project bootstrapped with [`create-twenty-app`](https://www.npmjs.com/package/create-twenty-app).
|
||||
|
||||
A minimal hello-world application built with an alpha version of the twenty-cli.
|
||||
## Getting Started
|
||||
|
||||
⚠️ Since this project uses an early alpha release, expect breaking changes and evolving features.
|
||||
|
||||
This example will gradually gain complexity and capabilities as the twenty-cli matures with future updates.
|
||||
|
||||
|
||||
## Requirements
|
||||
- twenty-cli `npm install -g twenty-cli`
|
||||
- an `apiKey`. Go to `/settings/api-webhooks` to generate one
|
||||
|
||||
|
||||
## Install to your Twenty workspace
|
||||
First, authenticate to your workspace:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
yarn twenty auth:login
|
||||
```
|
||||
|
||||
- replace `<SET_YOUR_TWENTY_API_KEY>` and `<SET_YOUR_TWENTY_API_URL>` accordingly
|
||||
Then, start development mode to sync your app and watch for changes:
|
||||
|
||||
```bash
|
||||
twenty auth login
|
||||
twenty app sync
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
## What it does
|
||||
- creates a new object `postCard` with a `name` field
|
||||
- creates a serverless function `create-new-post-card`
|
||||
- two triggers on `create-new-post-card`:
|
||||
- one route trigger on `GET` `/post-card/create?recipient=RecipientName`
|
||||
- one databaseEvent trigger on `people.created` events
|
||||
Open your Twenty instance and go to `/settings/applications` section to see the result.
|
||||
|
||||
## Development
|
||||
## Available Commands
|
||||
|
||||
Run dev mode and see application updates on your workspace instantly
|
||||
Run `yarn twenty help` to list all available commands. Common commands:
|
||||
|
||||
```bash
|
||||
twenty app dev
|
||||
# Authentication
|
||||
yarn twenty auth:login # Authenticate with Twenty
|
||||
yarn twenty auth:logout # Remove credentials
|
||||
yarn twenty auth:status # Check auth status
|
||||
yarn twenty auth:switch # Switch default workspace
|
||||
yarn twenty auth:list # List all configured workspaces
|
||||
|
||||
# Application
|
||||
yarn twenty app:dev # Start dev mode (watch, build, sync, and auto-generate typed client)
|
||||
yarn twenty entity:add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
|
||||
yarn twenty function:logs # Stream function logs
|
||||
yarn twenty function:execute # Execute a function with JSON payload
|
||||
yarn twenty app:uninstall # Uninstall app from workspace
|
||||
```
|
||||
|
||||
## Integration Tests
|
||||
|
||||
If your project includes the example integration test (`src/__tests__/app-install.integration-test.ts`), you can run it with:
|
||||
|
||||
```bash
|
||||
# Make sure a Twenty server is running at http://localhost:3000
|
||||
yarn test
|
||||
```
|
||||
|
||||
The test builds and installs the app, then verifies it appears in the applications list. Test configuration (API URL and API key) is defined in `vitest.config.ts`.
|
||||
|
||||
## LLMs instructions
|
||||
|
||||
Main docs and pitfalls are available in LLMS.md file.
|
||||
|
||||
## 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,6 +1,6 @@
|
||||
{
|
||||
"name": "hello-world",
|
||||
"version": "0.2.2",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
@@ -10,17 +10,19 @@
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"scripts": {
|
||||
"twenty": "twenty",
|
||||
"auth": "twenty auth:login",
|
||||
"dev": "twenty app:dev",
|
||||
"build": "twenty app:build",
|
||||
"typecheck": "twenty app:typecheck",
|
||||
"uninstall": "twenty app:uninstall",
|
||||
"entity:add": "twenty entity:add"
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-sdk": "0.6.3"
|
||||
"lint": "oxlint -c .oxlintrc.json .",
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json .",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2"
|
||||
"@types/node": "^24.7.2",
|
||||
"@types/react": "^18.2.0",
|
||||
"oxlint": "^0.16.0",
|
||||
"react": "^18.2.0",
|
||||
"twenty-sdk": "0.6.4",
|
||||
"typescript": "^5.9.3",
|
||||
"vite-tsconfig-paths": "^4.2.1",
|
||||
"vitest": "^3.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
|
||||
import { appBuild, appUninstall } from 'twenty-sdk/cli';
|
||||
import { MetadataApiClient } from 'twenty-sdk/clients';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
const APP_PATH = process.cwd();
|
||||
|
||||
describe('App installation', () => {
|
||||
let appInstalled = false;
|
||||
|
||||
beforeAll(async () => {
|
||||
const buildResult = await appBuild({
|
||||
appPath: APP_PATH,
|
||||
onProgress: (message: string) => console.log(`[build] ${message}`),
|
||||
});
|
||||
|
||||
if (!buildResult.success) {
|
||||
throw new Error(
|
||||
`Build failed: ${buildResult.error?.message ?? 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
|
||||
appInstalled = true;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (!appInstalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uninstallResult = await appUninstall({ appPath: APP_PATH });
|
||||
|
||||
if (!uninstallResult.success) {
|
||||
console.warn(
|
||||
`App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should find the installed app in the applications list', async () => {
|
||||
const metadataClient = new MetadataApiClient();
|
||||
|
||||
const result = await metadataClient.query({
|
||||
findManyApplications: {
|
||||
id: true,
|
||||
name: true,
|
||||
universalIdentifier: true,
|
||||
},
|
||||
});
|
||||
|
||||
const installedApp = result.findManyApplications.find(
|
||||
(application: { universalIdentifier: string }) =>
|
||||
application.universalIdentifier ===
|
||||
APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
|
||||
expect(installedApp).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { beforeAll } from 'vitest';
|
||||
|
||||
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:3000';
|
||||
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
|
||||
|
||||
const assertServerIsReachable = async () => {
|
||||
let response: Response;
|
||||
|
||||
try {
|
||||
response = await fetch(`${TWENTY_API_URL}/healthz`);
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Twenty server is not reachable at ${TWENTY_API_URL}. ` +
|
||||
'Make sure the server is running before executing integration tests.',
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server at ${TWENTY_API_URL} returned ${response.status}`);
|
||||
}
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
await assertServerIsReachable();
|
||||
|
||||
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
|
||||
|
||||
const configFile = {
|
||||
profiles: {
|
||||
default: {
|
||||
apiUrl: process.env.TWENTY_API_URL,
|
||||
apiKey: process.env.TWENTY_API_KEY,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(TEST_CONFIG_DIR, 'config.json'),
|
||||
JSON.stringify(configFile, null, 2),
|
||||
);
|
||||
});
|
||||
@@ -1,55 +0,0 @@
|
||||
import {
|
||||
defineLogicFunction,
|
||||
type CronPayload,
|
||||
type DatabaseEventPayload,
|
||||
type ObjectRecordCreateEvent,
|
||||
} from 'twenty-sdk';
|
||||
import { CoreApiClient as Twenty, type CoreSchema } from 'twenty-sdk/generated';
|
||||
|
||||
type CreateNewPostCardParams =
|
||||
| { name?: string }
|
||||
| DatabaseEventPayload<ObjectRecordCreateEvent<CoreSchema.Person>>
|
||||
| CronPayload;
|
||||
|
||||
const handler = async (params: CreateNewPostCardParams) => {
|
||||
const client = new Twenty();
|
||||
|
||||
const name =
|
||||
'name' in params
|
||||
? params.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
|
||||
: 'Hello world';
|
||||
|
||||
const createPostCard = await client.mutation({
|
||||
createPostCard: {
|
||||
__args: {
|
||||
data: {
|
||||
name,
|
||||
},
|
||||
},
|
||||
name: true,
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
console.log('createPostCard result', createPostCard);
|
||||
|
||||
return createPostCard;
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'create-new-post-card',
|
||||
timeoutSeconds: 2,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/post-card/create',
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: false,
|
||||
},
|
||||
cronTriggerSettings: {
|
||||
pattern: '0 0 1 1 *',
|
||||
},
|
||||
databaseEventTriggerSettings: {
|
||||
eventName: 'person.created',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
|
||||
export const APPLICATION_UNIVERSAL_IDENTIFIER =
|
||||
'6563e091-9f5b-4026-a3ea-7e3b3d09e218';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
displayName: 'Hello world',
|
||||
description: '',
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||||
displayName: 'Hello World',
|
||||
description: 'A simple hello world app',
|
||||
icon: 'IconWorld',
|
||||
applicationVariables: {
|
||||
DEFAULT_RECIPIENT_NAME: {
|
||||
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
|
||||
description: 'Default recipient name for postcards',
|
||||
value: 'Alex Karp',
|
||||
isSecret: false,
|
||||
},
|
||||
},
|
||||
defaultRoleUniversalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineField, FieldType } from 'twenty-sdk';
|
||||
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/objects/example-object';
|
||||
|
||||
export default defineField({
|
||||
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
universalIdentifier: '770d32c2-cf12-4ab2-b66d-73f92dc239b5',
|
||||
type: FieldType.NUMBER,
|
||||
name: 'priority',
|
||||
label: 'Priority',
|
||||
description: 'Priority level for the example item (1-10)',
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk';
|
||||
|
||||
export const HelloWorld = () => {
|
||||
return (
|
||||
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
|
||||
<h1>Hello, World!</h1>
|
||||
<p>This is your first front component.</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'd371f098-5b2c-42f0-898d-94459f1ee337',
|
||||
name: 'hello-world-front-component',
|
||||
description: 'A sample front component',
|
||||
component: HelloWorld,
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk';
|
||||
|
||||
const handler = async (): Promise<{ message: string }> => {
|
||||
return { message: 'Hello, World!' };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '2baa26eb-9aaf-4856-a4f4-30d6fd6480ee',
|
||||
name: 'hello-world-logic-function',
|
||||
description: 'A simple logic function',
|
||||
timeoutSeconds: 5,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/hello-world-logic-function',
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: false,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
|
||||
|
||||
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
|
||||
console.log('Post install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: '7a3f4684-51db-494d-833b-a747a3b90507',
|
||||
name: 'post-install',
|
||||
description: 'Runs after installation to set up the application.',
|
||||
timeoutSeconds: 300,
|
||||
handler,
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
|
||||
|
||||
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
|
||||
console.log('Pre install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: '1272ffdb-8e2f-492c-ab37-66c2b97e9c23',
|
||||
name: 'pre-install',
|
||||
description: 'Runs before installation to prepare the application.',
|
||||
timeoutSeconds: 300,
|
||||
handler,
|
||||
});
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: '10f90627-e9c2-44b7-9742-bed77e3d1b17',
|
||||
name: 'example-navigation-menu-item',
|
||||
icon: 'IconList',
|
||||
color: 'blue',
|
||||
position: 0,
|
||||
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { defineObject, FieldType } from 'twenty-sdk';
|
||||
|
||||
export const EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER =
|
||||
'dfd43356-39b3-4b55-b4a7-279bec689928';
|
||||
|
||||
export const NAME_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'd2d7f6cd-33f6-456f-bf00-17adeca926ba';
|
||||
|
||||
export default defineObject({
|
||||
universalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
nameSingular: 'exampleItem',
|
||||
namePlural: 'exampleItems',
|
||||
labelSingular: 'Example item',
|
||||
labelPlural: 'Example items',
|
||||
description: 'A sample custom object',
|
||||
icon: 'IconBox',
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.TEXT,
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
description: 'Name of the example item',
|
||||
icon: 'IconAbc',
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -1,89 +0,0 @@
|
||||
import { defineObject, FieldType } from 'twenty-sdk';
|
||||
|
||||
const POST_CARD_STATUS = {
|
||||
DRAFT: 'DRAFT',
|
||||
SENT: 'SENT',
|
||||
DELIVERED: 'DELIVERED',
|
||||
RETURNED: 'RETURNED',
|
||||
} as const;
|
||||
|
||||
export default defineObject({
|
||||
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
|
||||
nameSingular: 'postCard',
|
||||
namePlural: 'postCards',
|
||||
labelSingular: 'Post card',
|
||||
labelPlural: 'Post cards',
|
||||
description: 'A post card object',
|
||||
icon: 'IconMail',
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
|
||||
type: FieldType.TEXT,
|
||||
name: 'content',
|
||||
label: 'Content',
|
||||
description: "Postcard's content",
|
||||
icon: 'IconAbc',
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
|
||||
type: FieldType.FULL_NAME,
|
||||
name: 'recipientName',
|
||||
label: 'Recipient name',
|
||||
icon: 'IconUser',
|
||||
},
|
||||
{
|
||||
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
|
||||
type: FieldType.ADDRESS,
|
||||
name: 'recipientAddress',
|
||||
label: 'Recipient address',
|
||||
icon: 'IconHome',
|
||||
},
|
||||
{
|
||||
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
|
||||
type: FieldType.SELECT,
|
||||
name: 'status',
|
||||
label: 'Status',
|
||||
icon: 'IconSend',
|
||||
defaultValue: `'${POST_CARD_STATUS.DRAFT}'`,
|
||||
options: [
|
||||
{
|
||||
id: 'a1b2c3d4-0001-4000-8000-000000000001',
|
||||
value: POST_CARD_STATUS.DRAFT,
|
||||
label: 'Draft',
|
||||
position: 0,
|
||||
color: 'gray',
|
||||
},
|
||||
{
|
||||
id: 'a1b2c3d4-0002-4000-8000-000000000002',
|
||||
value: POST_CARD_STATUS.SENT,
|
||||
label: 'Sent',
|
||||
position: 1,
|
||||
color: 'orange',
|
||||
},
|
||||
{
|
||||
id: 'a1b2c3d4-0003-4000-8000-000000000003',
|
||||
value: POST_CARD_STATUS.DELIVERED,
|
||||
label: 'Delivered',
|
||||
position: 2,
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
id: 'a1b2c3d4-0004-4000-8000-000000000004',
|
||||
value: POST_CARD_STATUS.RETURNED,
|
||||
label: 'Returned',
|
||||
position: 3,
|
||||
color: 'orange',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
|
||||
type: FieldType.DATE_TIME,
|
||||
name: 'deliveredAt',
|
||||
label: 'Delivered at',
|
||||
icon: 'IconCheck',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { defineRole } from 'twenty-sdk';
|
||||
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
'9238bc7b-d38f-4a1c-9d19-31ab7bc67a2f';
|
||||
|
||||
export default defineRole({
|
||||
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
label: 'Hello world default function role',
|
||||
description: 'Hello world default function role',
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
canSoftDeleteAllObjectRecords: true,
|
||||
canDestroyAllObjectRecords: false,
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
import { defineRole, PermissionFlag } from 'twenty-sdk';
|
||||
|
||||
export default defineRole({
|
||||
universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
|
||||
label: 'Default function role',
|
||||
description: 'Default role for function Twenty client',
|
||||
canReadAllObjectRecords: false,
|
||||
canUpdateAllObjectRecords: false,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
canUpdateAllSettings: false,
|
||||
canBeAssignedToAgents: false,
|
||||
canBeAssignedToUsers: false,
|
||||
canBeAssignedToApiKeys: false,
|
||||
objectPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
},
|
||||
],
|
||||
fieldPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
|
||||
fieldUniversalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
],
|
||||
permissionFlags: [PermissionFlag.APPLICATIONS],
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { defineSkill } from 'twenty-sdk';
|
||||
|
||||
export const EXAMPLE_SKILL_UNIVERSAL_IDENTIFIER =
|
||||
'd0940029-9d3c-40be-903a-52d65393028f';
|
||||
|
||||
export default defineSkill({
|
||||
universalIdentifier: EXAMPLE_SKILL_UNIVERSAL_IDENTIFIER,
|
||||
name: 'example-skill',
|
||||
label: 'Example Skill',
|
||||
description: 'A sample skill for your application',
|
||||
icon: 'IconBrain',
|
||||
content: 'Add your skill instructions here. Skills provide context and capabilities to AI agents.',
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { defineView, ViewKey } from 'twenty-sdk';
|
||||
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER, NAME_FIELD_UNIVERSAL_IDENTIFIER } from 'src/objects/example-object';
|
||||
|
||||
export const EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER = 'e004df40-29f3-47ba-b39d-d3a5c444367a';
|
||||
|
||||
export default defineView({
|
||||
universalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
name: 'All example items',
|
||||
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
icon: 'IconList',
|
||||
key: ViewKey.INDEX,
|
||||
position: 0,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: '496c40c2-5766-419c-93bf-20fdad3f34bb',
|
||||
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -5,21 +5,45 @@
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"allowUnreachableCode": false,
|
||||
"strictNullChecks": true,
|
||||
"strict": true,
|
||||
"alwaysStrict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictBindCallApply": false,
|
||||
"target": "es2018",
|
||||
"module": "esnext",
|
||||
"lib": ["es2020", "dom"],
|
||||
"lib": [
|
||||
"es2020",
|
||||
"dom"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"paths": {
|
||||
"src/*": [
|
||||
"./src/*"
|
||||
],
|
||||
"~/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
|
||||
}
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/*.integration-test.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"types": [
|
||||
"vitest/globals"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tsconfigPaths({
|
||||
projects: ['tsconfig.spec.json'],
|
||||
ignoreConfigErrors: true,
|
||||
}),
|
||||
],
|
||||
test: {
|
||||
testTimeout: 120_000,
|
||||
hookTimeout: 120_000,
|
||||
include: ['src/**/*.integration-test.ts'],
|
||||
setupFiles: ['src/__tests__/setup-test.ts'],
|
||||
env: {
|
||||
TWENTY_API_URL: 'http://localhost:3000',
|
||||
TWENTY_API_KEY:
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik',
|
||||
},
|
||||
},
|
||||
});
|
||||
+1220
-1416
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
# 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/
|
||||
|
||||
.twenty/*
|
||||
!.twenty/output/
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
# Remove once prod ready
|
||||
.twenty
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user