Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d64f23894e |
@@ -106,35 +106,34 @@ Replace `{VERSION}` with the actual version number (e.g., `1.9.0`)
|
||||
### 2. Create File Structure
|
||||
|
||||
**Create changelog file:**
|
||||
- Path: `packages/twenty-website-new/src/content/releases/{VERSION}.mdx`
|
||||
- Example: `packages/twenty-website-new/src/content/releases/1.9.0.mdx`
|
||||
- Path: `packages/twenty-website/src/content/releases/{VERSION}.mdx`
|
||||
- Example: `packages/twenty-website/src/content/releases/1.9.0.mdx`
|
||||
|
||||
**Create image folder:**
|
||||
- Path: `packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/`
|
||||
- Example for version 1.9.0: `packages/twenty-website-new/public/images/releases/1.9/`
|
||||
- Example for version 2.0.0: `packages/twenty-website-new/public/images/releases/2.0/`
|
||||
- Path: `packages/twenty-website/public/images/releases/{MINOR_VERSION}/`
|
||||
- Example for version 1.9.0: `packages/twenty-website/public/images/releases/1.9/`
|
||||
- Example for version 2.0.0: `packages/twenty-website/public/images/releases/2.0/`
|
||||
|
||||
```bash
|
||||
# Create the image folder
|
||||
mkdir -p packages/twenty-website-new/public/images/releases/{MINOR_VERSION}
|
||||
mkdir -p packages/twenty-website/public/images/releases/{MINOR_VERSION}
|
||||
```
|
||||
|
||||
### 3. Move Illustration Files
|
||||
|
||||
**Source:** `/Users/thomascolasdesfrancs/Downloads/🆕`
|
||||
|
||||
**Destination:** `packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/`
|
||||
**Destination:** `packages/twenty-website/public/images/releases/{MINOR_VERSION}/`
|
||||
|
||||
**Naming Convention:** `{VERSION}-descriptive-name.webp`
|
||||
**Naming Convention:** `{VERSION}-descriptive-name.png`
|
||||
|
||||
Examples:
|
||||
- `1.9.0-feature-name.webp`
|
||||
- `1.9.0-another-feature.webp`
|
||||
- `1.9.0-feature-name.png`
|
||||
- `1.9.0-another-feature.png`
|
||||
|
||||
```bash
|
||||
# Move and rename source files, then convert to webp if needed
|
||||
cp ~/Downloads/🆕/source-file.png packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-name.png
|
||||
cd packages/twenty-website-new && node scripts/convert-png-to-webp.mjs
|
||||
# Move and rename files
|
||||
cp ~/Downloads/🆕/source-file.png packages/twenty-website/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-name.png
|
||||
```
|
||||
|
||||
### 4. Research Features (if needed)
|
||||
@@ -159,19 +158,19 @@ Date: {YYYY-MM-DD}
|
||||
|
||||
Short description explaining what the feature does and why it's useful. Keep it user-focused and concise (1-2 sentences).
|
||||
|
||||

|
||||

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

|
||||

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

|
||||

|
||||
```
|
||||
|
||||
**Style Guidelines:**
|
||||
@@ -183,7 +182,7 @@ Description of the third feature.
|
||||
- **NEVER mention the brand name "Twenty"** in changelog text - use "your workspace", "the platform", or similar neutral references instead
|
||||
|
||||
**Reference Previous Changelogs:**
|
||||
- Check `packages/twenty-website-new/src/content/releases/` for examples
|
||||
- Check `packages/twenty-website/src/content/releases/` for examples
|
||||
- Recent releases: 1.7.0.mdx, 1.6.0.mdx, 1.5.0.mdx
|
||||
|
||||
### 6. Review
|
||||
@@ -191,10 +190,10 @@ Description of the third feature.
|
||||
Open the changelog file for review:
|
||||
```bash
|
||||
# Open in Cursor
|
||||
cursor packages/twenty-website-new/src/content/releases/{VERSION}.mdx
|
||||
cursor packages/twenty-website/src/content/releases/{VERSION}.mdx
|
||||
|
||||
# Open image folder to verify illustrations
|
||||
open packages/twenty-website-new/public/images/releases/{MINOR_VERSION}
|
||||
open packages/twenty-website/public/images/releases/{MINOR_VERSION}
|
||||
```
|
||||
|
||||
Review checklist:
|
||||
@@ -222,8 +221,8 @@ I've created the changelog for version {VERSION}. Here's the content for your re
|
||||
[Show full MDX content]
|
||||
|
||||
Images moved to:
|
||||
- packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-1.webp
|
||||
- packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-2.webp
|
||||
- packages/twenty-website/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-1.png
|
||||
- packages/twenty-website/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-2.png
|
||||
|
||||
Please review the content. Once you approve, I'll commit the changes and create the pull request.
|
||||
```
|
||||
@@ -242,8 +241,8 @@ Possible user responses:
|
||||
git status
|
||||
|
||||
# Add files
|
||||
git add packages/twenty-website-new/src/content/releases/{VERSION}.mdx
|
||||
git add packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/
|
||||
git add packages/twenty-website/src/content/releases/{VERSION}.mdx
|
||||
git add packages/twenty-website/public/images/releases/{MINOR_VERSION}/
|
||||
|
||||
# Commit
|
||||
git commit -m "Add {VERSION} release changelog"
|
||||
@@ -266,7 +265,7 @@ This release includes:
|
||||
- Feature 2
|
||||
- Feature 3
|
||||
|
||||
Changelog file: \`packages/twenty-website-new/src/content/releases/{VERSION}.mdx\`
|
||||
Changelog file: \`packages/twenty-website/src/content/releases/{VERSION}.mdx\`
|
||||
Release date: {DATE}" \
|
||||
--base main \
|
||||
--head {VERSION}
|
||||
@@ -280,21 +279,21 @@ Or visit: `https://github.com/twentyhq/twenty/pull/new/{VERSION}`
|
||||
- **Format**: `{MAJOR}.{MINOR}.{PATCH}.mdx`
|
||||
- **Convention**: One file per complete version
|
||||
- **Examples**: `1.6.0.mdx`, `1.7.0.mdx`, `2.0.0.mdx`
|
||||
- **Location**: `packages/twenty-website-new/src/content/releases/`
|
||||
- **Location**: `packages/twenty-website/src/content/releases/`
|
||||
|
||||
### Image Folders
|
||||
- **Format**: `{MAJOR}.{MINOR}/`
|
||||
- **Convention**: One folder per minor version (shared across patches)
|
||||
- **Examples**: `1.6/`, `1.7/`, `2.0/`
|
||||
- **Location**: `packages/twenty-website-new/public/images/releases/`
|
||||
- **Location**: `packages/twenty-website/public/images/releases/`
|
||||
|
||||
### Image Files
|
||||
- **Format**: `{VERSION}-descriptive-name.webp`
|
||||
- **Format**: `{VERSION}-descriptive-name.png`
|
||||
- **Convention**: Kebab-case descriptive names
|
||||
- **Examples**:
|
||||
- `1.8.0-workflow-iterator.webp`
|
||||
- `1.8.0-bulk-select.webp`
|
||||
- `1.9.0-new-feature.webp`
|
||||
- `1.8.0-workflow-iterator.png`
|
||||
- `1.8.0-bulk-select.png`
|
||||
- `1.9.0-new-feature.png`
|
||||
|
||||
## Quick Reference Template
|
||||
|
||||
@@ -311,8 +310,8 @@ Features to document:
|
||||
3. ___________________________
|
||||
|
||||
Branch name: {VERSION}
|
||||
Changelog path: packages/twenty-website-new/src/content/releases/{VERSION}.mdx
|
||||
Images path: packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/
|
||||
Changelog path: packages/twenty-website/src/content/releases/{VERSION}.mdx
|
||||
Images path: packages/twenty-website/public/images/releases/{MINOR_VERSION}/
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
@@ -7,17 +7,6 @@ inputs:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Free disk space for install
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: |
|
||||
# Default GitHub images ship large SDKs this repo does not use; removing
|
||||
# them avoids ENOSPC when restoring or linking a full Yarn node_modules.
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf /opt/hostedtoolcache/CodeQL
|
||||
df -h
|
||||
- name: Cache primary key builder
|
||||
id: globals
|
||||
shell: bash
|
||||
|
||||
@@ -4,19 +4,20 @@
|
||||
# See https://crowdin.github.io/crowdin-cli/configuration for more information
|
||||
#
|
||||
|
||||
preserve_hierarchy: true
|
||||
base_path: ..
|
||||
"preserve_hierarchy": true
|
||||
"base_path": ".."
|
||||
|
||||
files: [
|
||||
{
|
||||
#
|
||||
# Source files filter - PO files for Lingui
|
||||
#
|
||||
"source": "**/en.po",
|
||||
|
||||
files:
|
||||
#
|
||||
# Source files filter - PO files for Lingui
|
||||
#
|
||||
- source: packages/twenty-front/src/locales/en.po
|
||||
#
|
||||
# Translation files path
|
||||
#
|
||||
translation: '%original_path%/%locale%.po'
|
||||
- source: packages/twenty-server/src/engine/core-modules/i18n/locales/en.po
|
||||
translation: '%original_path%/%locale%.po'
|
||||
- source: packages/twenty-emails/src/locales/en.po
|
||||
translation: '%original_path%/%locale%.po'
|
||||
"translation": "%original_path%/%locale%.po",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
#
|
||||
# Crowdin CLI configuration for Website translations (twenty-website-new)
|
||||
# Project ID: 4
|
||||
# See https://crowdin.github.io/crowdin-cli/configuration for more information
|
||||
#
|
||||
|
||||
project_id: 4
|
||||
preserve_hierarchy: true
|
||||
base_url: 'https://twenty.api.crowdin.com'
|
||||
base_path: ..
|
||||
languages_mapping:
|
||||
locale:
|
||||
fr: fr-FR
|
||||
|
||||
files:
|
||||
#
|
||||
# Source file - PO file for Lingui
|
||||
#
|
||||
- source: packages/twenty-website-new/src/locales/en.po
|
||||
#
|
||||
# Translation files path
|
||||
#
|
||||
translation: '%original_path%/%locale%.po'
|
||||
@@ -1,12 +1,12 @@
|
||||
name: CI Website
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
merge_group:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
@@ -18,40 +18,53 @@ jobs:
|
||||
with:
|
||||
files: |
|
||||
package.json
|
||||
yarn.lock
|
||||
packages/twenty-website-new/**
|
||||
packages/twenty-shared/**
|
||||
website-task:
|
||||
packages/twenty-website/**
|
||||
website-build:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=6144'
|
||||
strategy:
|
||||
matrix:
|
||||
task: [lint, typecheck, test]
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/cancel-workflow-action@0.11.0
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Run ${{ matrix.task }} task
|
||||
uses: ./.github/actions/nx-affected
|
||||
with:
|
||||
tag: scope:website
|
||||
tasks: ${{ matrix.task }}
|
||||
|
||||
- name: Server / Create DB
|
||||
run: PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||
|
||||
- name: Website / Run migrations
|
||||
run: npx nx database:migrate twenty-website
|
||||
env:
|
||||
DATABASE_PG_URL: postgres://postgres:postgres@localhost:5432/default
|
||||
- name: Website / Build Website
|
||||
run: npx nx build twenty-website
|
||||
env:
|
||||
DATABASE_PG_URL: postgres://postgres:postgres@localhost:5432/default
|
||||
KEYSTATIC_GITHUB_CLIENT_ID: xxx
|
||||
KEYSTATIC_GITHUB_CLIENT_SECRET: xxx
|
||||
KEYSTATIC_SECRET: xxx
|
||||
NEXT_PUBLIC_KEYSTATIC_GITHUB_APP_SLUG: xxx
|
||||
ci-website-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, website-task]
|
||||
needs: [changed-files-check, website-build]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
|
||||
@@ -58,6 +58,7 @@ jobs:
|
||||
npx nx run twenty-server:lingui:compile --strict
|
||||
npx nx run twenty-emails:lingui:compile --strict
|
||||
npx nx run twenty-front:lingui:compile --strict
|
||||
npx nx run twenty-website-new:lingui:compile --strict
|
||||
continue-on-error: true
|
||||
|
||||
- name: Stash any changes before pulling translations
|
||||
@@ -74,6 +75,8 @@ jobs:
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
download_translations: true
|
||||
source: '**/en.po'
|
||||
translation: '%original_path%/%locale%.po'
|
||||
export_only_approved: false
|
||||
localization_branch_name: i18n
|
||||
base_url: 'https://twenty.api.crowdin.com'
|
||||
@@ -113,6 +116,7 @@ jobs:
|
||||
npx nx run twenty-server:lingui:compile
|
||||
npx nx run twenty-emails:lingui:compile
|
||||
npx nx run twenty-front:lingui:compile
|
||||
npx nx run twenty-website-new:lingui:compile
|
||||
git status
|
||||
git add .
|
||||
if ! git diff --staged --quiet --exit-code; then
|
||||
|
||||
@@ -41,6 +41,7 @@ jobs:
|
||||
npx nx run twenty-server:lingui:extract
|
||||
npx nx run twenty-emails:lingui:extract
|
||||
npx nx run twenty-front:lingui:extract
|
||||
npx nx run twenty-website-new:lingui:extract
|
||||
|
||||
- name: Check and commit extracted files
|
||||
id: check_extract_changes
|
||||
@@ -60,6 +61,7 @@ jobs:
|
||||
npx nx run twenty-server:lingui:compile
|
||||
npx nx run twenty-emails:lingui:compile
|
||||
npx nx run twenty-front:lingui:compile
|
||||
npx nx run twenty-website-new:lingui:compile
|
||||
|
||||
- name: Check and commit compiled files
|
||||
id: check_compile_changes
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
# Pull down website translations from Crowdin every two hours or when triggered manually.
|
||||
# When force_pull input is true, translations will be pulled regardless of compilation status.
|
||||
|
||||
name: 'Pull website translations from Crowdin'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 */2 * * *' # Every two hours.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
force_pull:
|
||||
description: 'Force pull translations regardless of compilation status'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
workflow_call:
|
||||
inputs:
|
||||
force_pull:
|
||||
description: 'Force pull translations regardless of compilation status'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
pull_website_translations:
|
||||
name: Pull website translations
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
ref: ${{ github.head_ref || github.ref_name }}
|
||||
|
||||
- name: Setup website i18n branch
|
||||
run: |
|
||||
git fetch origin i18n-website || true
|
||||
git checkout -B i18n-website origin/i18n-website || git checkout -b i18n-website
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
|
||||
- name: Build twenty-shared
|
||||
run: npx nx build twenty-shared
|
||||
|
||||
# Strict mode fails if there are missing website translations.
|
||||
- name: Compile website translations
|
||||
id: compile_translations_strict
|
||||
run: npx nx run twenty-website-new:lingui:compile --strict
|
||||
continue-on-error: true
|
||||
|
||||
- name: Stash any changes before pulling translations
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@twenty.com'
|
||||
git add .
|
||||
git stash
|
||||
|
||||
- name: Pull website translations from Crowdin
|
||||
if: inputs.force_pull || steps.compile_translations_strict.outcome == 'failure'
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
download_translations: true
|
||||
source: 'packages/twenty-website-new/src/locales/en.po'
|
||||
translation: 'packages/twenty-website-new/src/locales/%locale%.po'
|
||||
export_only_approved: false
|
||||
localization_branch_name: i18n-website
|
||||
base_url: 'https://twenty.api.crowdin.com'
|
||||
auto_approve_imported: false
|
||||
import_eq_suggestions: false
|
||||
download_sources: false
|
||||
push_sources: false
|
||||
skip_untranslated_strings: false
|
||||
skip_untranslated_files: false
|
||||
push_translations: false
|
||||
create_pull_request: false
|
||||
skip_ref_checkout: true
|
||||
dryrun_action: false
|
||||
config: '.github/crowdin-website.yml'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
# Website translations project
|
||||
CROWDIN_PROJECT_ID: '4'
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
|
||||
# As the files are extracted from a Docker container, they belong to root:root.
|
||||
# We need to fix this before the next steps.
|
||||
- name: Fix file permissions
|
||||
run: sudo chown -R runner:docker .
|
||||
|
||||
- name: Compile website translations
|
||||
id: compile_translations
|
||||
run: |
|
||||
npx nx run twenty-website-new:lingui:compile
|
||||
git status
|
||||
git add packages/twenty-website-new/src/locales
|
||||
if ! git diff --staged --quiet --exit-code; then
|
||||
git commit -m "chore: compile website translations"
|
||||
echo "changes_detected=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "changes_detected=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Push changes
|
||||
if: steps.compile_translations.outputs.changes_detected == 'true'
|
||||
run: git push origin HEAD:i18n-website
|
||||
|
||||
- name: Create pull request
|
||||
if: steps.compile_translations.outputs.changes_detected == 'true'
|
||||
run: |
|
||||
if git diff --name-only origin/main..HEAD | grep -q .; then
|
||||
gh pr create -B main -H i18n-website --title 'i18n - website translations' --body 'Created by Github action' || true
|
||||
else
|
||||
echo "No file differences between branches, skipping PR creation"
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Trigger i18n automerge
|
||||
if: steps.compile_translations.outputs.changes_detected == 'true'
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
|
||||
repository: twentyhq/twenty-infra
|
||||
event-type: i18n-pr-ready
|
||||
@@ -1,111 +0,0 @@
|
||||
name: 'Push website translations to Crowdin'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
push:
|
||||
branches: ['main']
|
||||
paths:
|
||||
- 'packages/twenty-website-new/**'
|
||||
- '.github/crowdin-website.yml'
|
||||
- '.github/workflows/website-i18n-push.yaml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
extract_website_translations:
|
||||
name: Extract and upload website translations
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
ref: main
|
||||
|
||||
- name: Setup website i18n branch
|
||||
run: |
|
||||
git fetch origin i18n-website || true
|
||||
git checkout -B i18n-website origin/i18n-website || git checkout -b i18n-website
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
|
||||
- name: Build dependencies
|
||||
run: npx nx build twenty-shared
|
||||
|
||||
- name: Extract website translations
|
||||
run: npx nx run twenty-website-new:lingui:extract
|
||||
|
||||
- name: Check and commit extracted files
|
||||
id: check_extract_changes
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@twenty.com'
|
||||
git add packages/twenty-website-new/src/locales
|
||||
if ! git diff --staged --quiet --exit-code; then
|
||||
git commit -m "chore: extract website translations"
|
||||
echo "changes_detected=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "changes_detected=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Compile website translations
|
||||
run: npx nx run twenty-website-new:lingui:compile
|
||||
|
||||
- name: Check and commit compiled files
|
||||
id: check_compile_changes
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@twenty.com'
|
||||
git add packages/twenty-website-new/src/locales/generated
|
||||
if ! git diff --staged --quiet --exit-code; then
|
||||
git commit -m "chore: compile website translations"
|
||||
echo "changes_detected=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "changes_detected=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Push changes and create remote branch if needed
|
||||
if: steps.check_extract_changes.outputs.changes_detected == 'true' || steps.check_compile_changes.outputs.changes_detected == 'true'
|
||||
run: git push origin HEAD:i18n-website
|
||||
|
||||
- name: Upload missing website translations
|
||||
if: steps.check_extract_changes.outputs.changes_detected == 'true'
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: true
|
||||
download_translations: false
|
||||
localization_branch_name: i18n-website
|
||||
base_url: 'https://twenty.api.crowdin.com'
|
||||
config: '.github/crowdin-website.yml'
|
||||
env:
|
||||
# Website translations project
|
||||
CROWDIN_PROJECT_ID: '4'
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
|
||||
- name: Create a pull request
|
||||
if: steps.check_extract_changes.outputs.changes_detected == 'true' || steps.check_compile_changes.outputs.changes_detected == 'true'
|
||||
run: |
|
||||
if git diff --name-only origin/main..HEAD | grep -q .; then
|
||||
gh pr create -B main -H i18n-website --title 'i18n - website translations' --body 'Created by Github action' || true
|
||||
else
|
||||
echo "No file differences between branches, skipping PR creation"
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Trigger i18n automerge
|
||||
if: steps.check_extract_changes.outputs.changes_detected == 'true' || steps.check_compile_changes.outputs.changes_detected == 'true'
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
|
||||
repository: twentyhq/twenty-infra
|
||||
event-type: i18n-pr-ready
|
||||
@@ -110,8 +110,7 @@ packages/
|
||||
├── twenty-ui/ # Shared UI components library
|
||||
├── twenty-shared/ # Common types and utilities
|
||||
├── twenty-emails/ # Email templates with React Email
|
||||
├── twenty-website-new/ # Next.js marketing website
|
||||
├── twenty-docs/ # Documentation website
|
||||
├── twenty-website/ # Next.js documentation website
|
||||
├── twenty-zapier/ # Zapier integration
|
||||
└── twenty-e2e-testing/ # Playwright E2E tests
|
||||
```
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<p align="center">
|
||||
<a href="https://www.twenty.com">
|
||||
<img src="./packages/twenty-website-new/public/images/core/logo.svg" width="100px" alt="Twenty logo" />
|
||||
<img src="./packages/twenty-website/public/images/core/logo.svg" width="100px" alt="Twenty logo" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h2 align="center" >The #1 Open-Source CRM</h2>
|
||||
|
||||
<p align="center"><a href="https://twenty.com"><img src="./packages/twenty-website-new/public/images/readme/globe-icon.svg" width="12" height="12"/> Website</a> · <a href="https://docs.twenty.com"><img src="./packages/twenty-website-new/public/images/readme/book-icon.svg" width="12" height="12"/> Documentation</a> · <a href="https://github.com/orgs/twentyhq/projects/1"><img src="./packages/twenty-website-new/public/images/readme/map-icon.svg" width="12" height="12"/> Roadmap </a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website-new/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty"><img src="./packages/twenty-website-new/public/images/readme/figma-icon.png" width="12" height="12"/> Figma</a></p>
|
||||
<p align="center"><a href="https://twenty.com"><img src="./packages/twenty-website/public/images/readme/globe-icon.svg" width="12" height="12"/> Website</a> · <a href="https://docs.twenty.com"><img src="./packages/twenty-website/public/images/readme/book-icon.svg" width="12" height="12"/> Documentation</a> · <a href="https://github.com/orgs/twentyhq/projects/1"><img src="./packages/twenty-website/public/images/readme/map-icon.svg" width="12" height="12"/> Roadmap </a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty"><img src="./packages/twenty-website/public/images/readme/figma-icon.png" width="12" height="12"/> Figma</a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.twenty.com">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/github-cover-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/github-cover-light.png" />
|
||||
<img src="./packages/twenty-website-new/public/images/readme/github-cover-light.png" alt="Twenty banner" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/github-cover-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/github-cover-light.png" />
|
||||
<img src="./packages/twenty-website/public/images/readme/github-cover-light.png" alt="Twenty banner" />
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
@@ -24,17 +24,17 @@
|
||||
|
||||
Twenty gives technical teams the building blocks for a custom CRM that meets complex business needs and quickly adapts as the business evolves. Twenty is the CRM you build, ship, and version like the rest of your stack.
|
||||
|
||||
<a href="https://twenty.com/why-twenty"><img src="./packages/twenty-website-new/public/images/readme/star-icon.svg" width="14" height="14"/> Learn more about why we built Twenty</a>
|
||||
<a href="https://twenty.com/why-twenty"><img src="./packages/twenty-website/public/images/readme/star-icon.svg" width="14" height="14"/> Learn more about why we built Twenty</a>
|
||||
|
||||
<br />
|
||||
|
||||
# Installation
|
||||
|
||||
### <img src="./packages/twenty-website-new/public/images/readme/globe-icon.svg" width="14" height="14"/> Cloud
|
||||
### <img src="./packages/twenty-website/public/images/readme/globe-icon.svg" width="14" height="14"/> Cloud
|
||||
|
||||
The fastest way to get started. Sign up at [twenty.com](https://twenty.com) and spin up a workspace in under a minute, with no infrastructure to manage and always up to date.
|
||||
|
||||
### <img src="./packages/twenty-website-new/public/images/readme/book-icon.svg" width="14" height="14"/> Build an app
|
||||
### <img src="./packages/twenty-website/public/images/readme/book-icon.svg" width="14" height="14"/> Build an app
|
||||
|
||||
Scaffold a new app with the Twenty CLI:
|
||||
|
||||
@@ -68,7 +68,7 @@ npx twenty deploy
|
||||
|
||||
See the [app development guide](https://docs.twenty.com/developers/extend/apps/getting-started) for objects, views, agents, and logic functions.
|
||||
|
||||
### <img src="./packages/twenty-website-new/public/images/readme/rocket-icon.svg" width="14" height="14"/> Self-hosting
|
||||
### <img src="./packages/twenty-website/public/images/readme/rocket-icon.svg" width="14" height="14"/> Self-hosting
|
||||
|
||||
Run Twenty on your own infrastructure with [Docker Compose](https://docs.twenty.com/developers/self-host/capabilities/docker-compose), or contribute locally via the [local setup guide](https://docs.twenty.com/developers/contribute/capabilities/local-setup).
|
||||
|
||||
@@ -79,61 +79,61 @@ Run Twenty on your own infrastructure with [Docker Compose](https://docs.twenty.
|
||||
|
||||
Twenty gives you the building blocks of a modern CRM (objects, views, workflows, and agents) and lets you extend them as code. Here's a tour of what's in the box.
|
||||
|
||||
Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduction"><img src="./packages/twenty-website-new/public/images/readme/planner-icon.svg" width="14" height="14"/> User Guide</a> for product walkthroughs, or the <a href="https://docs.twenty.com"><img src="./packages/twenty-website-new/public/images/readme/book-icon.svg" width="14" height="14"/> Documentation</a> for developer reference.
|
||||
Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduction"><img src="./packages/twenty-website/public/images/readme/planner-icon.svg" width="14" height="14"/> User Guide</a> for product walkthroughs, or the <a href="https://docs.twenty.com"><img src="./packages/twenty-website/public/images/readme/book-icon.svg" width="14" height="14"/> Documentation</a> for developer reference.
|
||||
|
||||
<table align="center">
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-build-apps-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-build-apps-light.png" />
|
||||
<img src="./packages/twenty-website-new/public/images/readme/v2-build-apps-light.png" alt="Create your apps" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-build-apps-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-build-apps-light.png" />
|
||||
<img src="./packages/twenty-website/public/images/readme/v2-build-apps-light.png" alt="Create your apps" />
|
||||
</picture>
|
||||
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/getting-started"><img src="./packages/twenty-website-new/public/images/readme/code-icon.svg" width="16" height="16"/> Learn more about apps in doc</a></p>
|
||||
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/getting-started"><img src="./packages/twenty-website/public/images/readme/code-icon.svg" width="16" height="16"/> Learn more about apps in doc</a></p>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-version-control-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-version-control-light.png" />
|
||||
<img src="./packages/twenty-website-new/public/images/readme/v2-version-control-light.png" alt="Stay on top with version control" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-version-control-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-version-control-light.png" />
|
||||
<img src="./packages/twenty-website/public/images/readme/v2-version-control-light.png" alt="Stay on top with version control" />
|
||||
</picture>
|
||||
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/publishing"><img src="./packages/twenty-website-new/public/images/readme/monitor-icon.svg" width="16" height="16"/> Learn more about version control in doc</a></p>
|
||||
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/publishing"><img src="./packages/twenty-website/public/images/readme/monitor-icon.svg" width="16" height="16"/> Learn more about version control in doc</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-all-tools-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-all-tools-light.png" />
|
||||
<img src="./packages/twenty-website-new/public/images/readme/v2-all-tools-light.png" alt="All the tools you need to build anything" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-all-tools-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-all-tools-light.png" />
|
||||
<img src="./packages/twenty-website/public/images/readme/v2-all-tools-light.png" alt="All the tools you need to build anything" />
|
||||
</picture>
|
||||
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/building"><img src="./packages/twenty-website-new/public/images/readme/rocket-icon.svg" width="16" height="16"/> Learn more about primitives in doc</a></p>
|
||||
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/building"><img src="./packages/twenty-website/public/images/readme/rocket-icon.svg" width="16" height="16"/> Learn more about primitives in doc</a></p>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-tools-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-tools-light.png" />
|
||||
<img src="./packages/twenty-website-new/public/images/readme/v2-tools-light.png" alt="Customize your layouts" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-tools-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-tools-light.png" />
|
||||
<img src="./packages/twenty-website/public/images/readme/v2-tools-light.png" alt="Customize your layouts" />
|
||||
</picture>
|
||||
<p align="center"><a href="https://docs.twenty.com/user-guide/layout/overview"><img src="./packages/twenty-website-new/public/images/readme/planner-icon.svg" width="16" height="16"/> Learn more about layouts in doc</a></p>
|
||||
<p align="center"><a href="https://docs.twenty.com/user-guide/layout/overview"><img src="./packages/twenty-website/public/images/readme/planner-icon.svg" width="16" height="16"/> Learn more about layouts in doc</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-ai-agents-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-ai-agents-light.png" />
|
||||
<img src="./packages/twenty-website-new/public/images/readme/v2-ai-agents-light.png" alt="AI agents and chats" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-ai-agents-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-ai-agents-light.png" />
|
||||
<img src="./packages/twenty-website/public/images/readme/v2-ai-agents-light.png" alt="AI agents and chats" />
|
||||
</picture>
|
||||
<p align="center"><a href="https://docs.twenty.com/user-guide/ai/overview"><img src="./packages/twenty-website-new/public/images/readme/message-icon.svg" width="16" height="16"/> Learn more about AI in doc</a></p>
|
||||
<p align="center"><a href="https://docs.twenty.com/user-guide/ai/overview"><img src="./packages/twenty-website/public/images/readme/message-icon.svg" width="16" height="16"/> Learn more about AI in doc</a></p>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-crm-tools-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-crm-tools-light.png" />
|
||||
<img src="./packages/twenty-website-new/public/images/readme/v2-crm-tools-light.png" alt="Plus all the tools of a good CRM" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-crm-tools-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-crm-tools-light.png" />
|
||||
<img src="./packages/twenty-website/public/images/readme/v2-crm-tools-light.png" alt="Plus all the tools of a good CRM" />
|
||||
</picture>
|
||||
<p align="center"><a href="https://docs.twenty.com/user-guide/introduction"><img src="./packages/twenty-website-new/public/images/readme/star-icon.svg" width="16" height="16"/> Learn more about CRM features in doc</a></p>
|
||||
<p align="center"><a href="https://docs.twenty.com/user-guide/introduction"><img src="./packages/twenty-website/public/images/readme/star-icon.svg" width="16" height="16"/> Learn more about CRM features in doc</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -142,23 +142,23 @@ Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduc
|
||||
|
||||
# Stack
|
||||
|
||||
- <a href="https://www.typescriptlang.org/"><img src="./packages/twenty-website-new/public/images/readme/stack-typescript.svg" width="14" height="14"/> TypeScript</a>
|
||||
- <a href="https://nx.dev/"><img src="./packages/twenty-website-new/public/images/readme/stack-nx.svg" width="14" height="14"/> Nx</a>
|
||||
- <a href="https://nestjs.com/"><img src="./packages/twenty-website-new/public/images/readme/stack-nestjs.svg" width="14" height="14"/> NestJS</a>, with <a href="https://bullmq.io/">BullMQ</a>, <a href="https://www.postgresql.org/"><img src="./packages/twenty-website-new/public/images/readme/stack-postgresql.svg" width="14" height="14"/> PostgreSQL</a>, <a href="https://redis.io/"><img src="./packages/twenty-website-new/public/images/readme/stack-redis.svg" width="14" height="14"/> Redis</a>
|
||||
- <a href="https://reactjs.org/"><img src="./packages/twenty-website-new/public/images/readme/stack-react.svg" width="14" height="14"/> React</a>, with <a href="https://jotai.org/">Jotai</a>, <a href="https://linaria.dev/">Linaria</a> and <a href="https://lingui.dev/">Lingui</a>
|
||||
- <a href="https://www.typescriptlang.org/"><img src="./packages/twenty-website/public/images/readme/stack-typescript.svg" width="14" height="14"/> TypeScript</a>
|
||||
- <a href="https://nx.dev/"><img src="./packages/twenty-website/public/images/readme/stack-nx.svg" width="14" height="14"/> Nx</a>
|
||||
- <a href="https://nestjs.com/"><img src="./packages/twenty-website/public/images/readme/stack-nestjs.svg" width="14" height="14"/> NestJS</a>, with <a href="https://bullmq.io/">BullMQ</a>, <a href="https://www.postgresql.org/"><img src="./packages/twenty-website/public/images/readme/stack-postgresql.svg" width="14" height="14"/> PostgreSQL</a>, <a href="https://redis.io/"><img src="./packages/twenty-website/public/images/readme/stack-redis.svg" width="14" height="14"/> Redis</a>
|
||||
- <a href="https://reactjs.org/"><img src="./packages/twenty-website/public/images/readme/stack-react.svg" width="14" height="14"/> React</a>, with <a href="https://jotai.org/">Jotai</a>, <a href="https://linaria.dev/">Linaria</a> and <a href="https://lingui.dev/">Lingui</a>
|
||||
|
||||
|
||||
|
||||
# Thanks
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.chromatic.com/"><img src="./packages/twenty-website-new/public/images/readme/chromatic.png" height="28" alt="Chromatic" /></a>
|
||||
<a href="https://www.chromatic.com/"><img src="./packages/twenty-website/public/images/readme/chromatic.png" height="28" alt="Chromatic" /></a>
|
||||
|
||||
<a href="https://greptile.com"><img src="./packages/twenty-website-new/public/images/readme/greptile.png" height="28" alt="Greptile" /></a>
|
||||
<a href="https://greptile.com"><img src="./packages/twenty-website/public/images/readme/greptile.png" height="28" alt="Greptile" /></a>
|
||||
|
||||
<a href="https://sentry.io/"><img src="./packages/twenty-website-new/public/images/readme/sentry.png" height="28" alt="Sentry" /></a>
|
||||
<a href="https://sentry.io/"><img src="./packages/twenty-website/public/images/readme/sentry.png" height="28" alt="Sentry" /></a>
|
||||
|
||||
<a href="https://crowdin.com/"><img src="./packages/twenty-website-new/public/images/readme/crowdin.png" height="28" alt="Crowdin" /></a>
|
||||
<a href="https://crowdin.com/"><img src="./packages/twenty-website/public/images/readme/crowdin.png" height="28" alt="Crowdin" /></a>
|
||||
</p>
|
||||
|
||||
Thanks to these amazing services that we use and recommend for UI testing (Chromatic), code review (Greptile), catching bugs (Sentry) and translating (Crowdin).
|
||||
@@ -166,4 +166,4 @@ Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduc
|
||||
|
||||
# Join the Community
|
||||
|
||||
<p><a href="https://github.com/twentyhq/twenty"><img src="./packages/twenty-website-new/public/images/readme/star-icon.svg" width="12" height="12"/> Star the repo</a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website-new/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://github.com/twentyhq/twenty/discussions"><img src="./packages/twenty-website-new/public/images/readme/message-icon.svg" width="12" height="12"/> Feature requests</a> · <a href="https://github.com/orgs/twentyhq/projects/1/views/35"><img src="./packages/twenty-website-new/public/images/readme/rocket-icon.svg" width="12" height="12"/> Releases</a> · <a href="https://twitter.com/twentycrm"><img src="./packages/twenty-website-new/public/images/readme/x-icon.svg" width="12" height="12"/> X</a> · <a href="https://www.linkedin.com/company/twenty/"><img src="./packages/twenty-website-new/public/images/readme/linkedin-icon.svg" width="12" height="12"/> LinkedIn</a> · <a href="https://twenty.crowdin.com/twenty"><img src="./packages/twenty-website-new/public/images/readme/language-icon.svg" width="12" height="12"/> Crowdin</a> · <a href="https://github.com/twentyhq/twenty/contribute"><img src="./packages/twenty-website-new/public/images/readme/code-icon.svg" width="12" height="12"/> Contribute</a></p>
|
||||
<p><a href="https://github.com/twentyhq/twenty"><img src="./packages/twenty-website/public/images/readme/star-icon.svg" width="12" height="12"/> Star the repo</a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://github.com/twentyhq/twenty/discussions"><img src="./packages/twenty-website/public/images/readme/message-icon.svg" width="12" height="12"/> Feature requests</a> · <a href="https://github.com/orgs/twentyhq/projects/1/views/35"><img src="./packages/twenty-website/public/images/readme/rocket-icon.svg" width="12" height="12"/> Releases</a> · <a href="https://twitter.com/twentycrm"><img src="./packages/twenty-website/public/images/readme/x-icon.svg" width="12" height="12"/> X</a> · <a href="https://www.linkedin.com/company/twenty/"><img src="./packages/twenty-website/public/images/readme/linkedin-icon.svg" width="12" height="12"/> LinkedIn</a> · <a href="https://twenty.crowdin.com/twenty"><img src="./packages/twenty-website/public/images/readme/language-icon.svg" width="12" height="12"/> Crowdin</a> · <a href="https://github.com/twentyhq/twenty/contribute"><img src="./packages/twenty-website/public/images/readme/code-icon.svg" width="12" height="12"/> Contribute</a></p>
|
||||
|
||||
@@ -1,20 +1,172 @@
|
||||
{
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@apollo/client": "^4.0.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",
|
||||
"date-fns": "^2.30.0",
|
||||
"date-fns-tz": "^2.0.0",
|
||||
"deep-equal": "^2.2.2",
|
||||
"file-type": "16.5.4",
|
||||
"framer-motion": "^11.18.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
"googleapis": "105",
|
||||
"hex-rgb": "^5.0.0",
|
||||
"immer": "^10.1.1",
|
||||
"jotai": "^2.17.1",
|
||||
"libphonenumber-js": "^1.10.26",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"lodash.chunk": "^4.2.0",
|
||||
"lodash.compact": "^3.0.1",
|
||||
"lodash.escaperegexp": "^4.1.2",
|
||||
"lodash.groupby": "^4.6.0",
|
||||
"lodash.identity": "^3.0.0",
|
||||
"lodash.isempty": "^4.4.0",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.isobject": "^3.0.2",
|
||||
"lodash.kebabcase": "^4.1.1",
|
||||
"lodash.mapvalues": "^4.6.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"lodash.omit": "^4.5.0",
|
||||
"lodash.pickby": "^4.6.0",
|
||||
"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.30.3",
|
||||
"react-tooltip": "^5.13.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"rxjs": "^7.2.0",
|
||||
"semver": "^7.5.4",
|
||||
"slash": "^5.1.0",
|
||||
"temporal-polyfill": "^0.3.0",
|
||||
"ts-key-enum": "^2.0.12",
|
||||
"tslib": "^2.8.1",
|
||||
"type-fest": "4.10.1",
|
||||
"typescript": "5.9.2",
|
||||
"uuid": "^9.0.0",
|
||||
"vite-tsconfig-paths": "^4.2.1",
|
||||
"xlsx-ugnis": "^0.19.3",
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.14.5",
|
||||
"@babel/preset-react": "^7.14.5",
|
||||
"@babel/preset-typescript": "^7.24.6",
|
||||
"@chromatic-com/storybook": "^4.1.3",
|
||||
"@graphql-codegen/cli": "^3.3.1",
|
||||
"@graphql-codegen/client-preset": "^4.1.0",
|
||||
"@graphql-codegen/typescript": "^3.0.4",
|
||||
"@graphql-codegen/typescript-operations": "^3.0.4",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
|
||||
"@nx/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",
|
||||
"@storybook/addon-docs": "^10.3.3",
|
||||
"@storybook/addon-links": "^10.3.3",
|
||||
"@storybook/addon-vitest": "^10.3.3",
|
||||
"@storybook/icons": "^2.0.1",
|
||||
"@storybook/react-vite": "^10.3.3",
|
||||
"@storybook/test-runner": "^0.24.2",
|
||||
"@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",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@types/addressparser": "^1.0.3",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/bytes": "^3.1.1",
|
||||
"@types/chrome": "^0.0.267",
|
||||
"@types/deep-equal": "^1.0.1",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/graphql-fields": "^1.3.6",
|
||||
"@types/inquirer": "^9.0.9",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/lodash.camelcase": "^4.3.7",
|
||||
"@types/lodash.compact": "^3.0.9",
|
||||
"@types/lodash.escaperegexp": "^4.1.9",
|
||||
"@types/lodash.groupby": "^4.6.9",
|
||||
"@types/lodash.identity": "^3.0.9",
|
||||
"@types/lodash.isempty": "^4.4.7",
|
||||
"@types/lodash.isequal": "^4.5.7",
|
||||
"@types/lodash.isobject": "^3.0.7",
|
||||
"@types/lodash.kebabcase": "^4.1.7",
|
||||
"@types/lodash.mapvalues": "^4.6.9",
|
||||
"@types/lodash.omit": "^4.5.9",
|
||||
"@types/lodash.pickby": "^4.6.9",
|
||||
"@types/lodash.snakecase": "^4.1.7",
|
||||
"@types/lodash.upperfirst": "^4.3.7",
|
||||
"@types/ms": "^0.7.31",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/passport-google-oauth20": "^2.0.11",
|
||||
"@types/passport-jwt": "^3.0.8",
|
||||
"@types/passport-microsoft": "^2.1.0",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
"@types/react": "^18.2.39",
|
||||
"@types/react-datepicker": "^6.2.0",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260116.1",
|
||||
"@vitejs/plugin-react-swc": "4.2.3",
|
||||
"@vitest/browser-playwright": "^4.0.18",
|
||||
"@vitest/coverage-istanbul": "^4.0.18",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"@yarnpkg/types": "^4.0.0",
|
||||
"chromatic": "^6.18.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"danger": "^13.0.4",
|
||||
"dotenv-cli": "^7.4.4",
|
||||
"esbuild": "^0.25.10",
|
||||
"http-server": "^14.1.1",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "30.0.0-beta.3",
|
||||
"jest-environment-node": "^29.4.1",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"jsdom": "~22.1.0",
|
||||
"msw": "^2.12.7",
|
||||
"msw-storybook-addon": "^2.0.6",
|
||||
"nx": "22.5.4",
|
||||
"prettier": "^3.1.1",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^5.0.5",
|
||||
"source-map-support": "^0.5.20",
|
||||
"storybook": "^10.3.3",
|
||||
"storybook-addon-mock-date": "2.0.0",
|
||||
"storybook-addon-pseudo-states": "^10.3.3",
|
||||
"supertest": "^6.1.3",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-loader": "^9.2.3",
|
||||
"ts-node": "10.9.1",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"tsx": "^4.17.0",
|
||||
"verdaccio": "^6.3.1"
|
||||
"verdaccio": "^6.3.1",
|
||||
"vite": "^7.0.0",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
@@ -33,9 +185,7 @@
|
||||
"@lingui/core": "5.1.2",
|
||||
"@types/qs": "6.9.16",
|
||||
"@wyw-in-js/transform@npm:0.6.0": "patch:@wyw-in-js/transform@npm%3A0.7.0#~/.yarn/patches/@wyw-in-js-transform-npm-0.7.0-ba641dc99f.patch",
|
||||
"@wyw-in-js/transform@npm:0.7.0": "patch:@wyw-in-js/transform@npm%3A0.7.0#~/.yarn/patches/@wyw-in-js-transform-npm-0.7.0-ba641dc99f.patch",
|
||||
"@opentelemetry/api": "1.9.1",
|
||||
"chokidar": "^3.6.0"
|
||||
"@wyw-in-js/transform@npm:0.7.0": "patch:@wyw-in-js/transform@npm%3A0.7.0#~/.yarn/patches/@wyw-in-js-transform-npm-0.7.0-ba641dc99f.patch"
|
||||
},
|
||||
"version": "0.2.1",
|
||||
"nx": {},
|
||||
@@ -53,6 +203,7 @@
|
||||
"packages/twenty-ui",
|
||||
"packages/twenty-utils",
|
||||
"packages/twenty-zapier",
|
||||
"packages/twenty-website",
|
||||
"packages/twenty-website-new",
|
||||
"packages/twenty-docs",
|
||||
"packages/twenty-e2e-testing",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div align="center">
|
||||
<a href="https://twenty.com">
|
||||
<picture>
|
||||
<img alt="Twenty logo" src="https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-website-new/public/images/core/logo.svg" height="128">
|
||||
<img alt="Twenty logo" src="https://raw.githubusercontent.com/twentyhq/twenty/2f25922f4cd5bd61e1427c57c4f8ea224e1d552c/packages/twenty-website/public/images/core/logo.svg" height="128">
|
||||
</picture>
|
||||
</a>
|
||||
<h1>Create Twenty App</h1>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-twenty-app",
|
||||
"version": "2.3.0",
|
||||
"version": "2.1.0",
|
||||
"description": "Command-line interface to create Twenty application",
|
||||
"main": "dist/cli.cjs",
|
||||
"bin": "dist/cli.cjs",
|
||||
@@ -40,17 +40,12 @@
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@swc/core": "^1.15.11",
|
||||
"@swc/jest": "^0.2.39",
|
||||
"@types/fs-extra": "^11.0.0",
|
||||
"@types/inquirer": "^9.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/lodash.camelcase": "^4.3.7",
|
||||
"@types/lodash.kebabcase": "^4.1.7",
|
||||
"@types/lodash.startcase": "^4",
|
||||
"@types/node": "^20.0.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-node": "^29.4.1",
|
||||
"twenty-shared": "workspace:*",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.0.0",
|
||||
|
||||
@@ -26,7 +26,6 @@ const program = new Command(packageJson.name)
|
||||
'--skip-local-instance',
|
||||
'Skip the local Twenty instance setup prompt',
|
||||
)
|
||||
.option('-y, --yes', 'Auto-confirm prompts (e.g. start existing container)')
|
||||
.helpOption('-h, --help', 'Display this help message.')
|
||||
.action(
|
||||
async (
|
||||
@@ -37,7 +36,6 @@ const program = new Command(packageJson.name)
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
skipLocalInstance?: boolean;
|
||||
yes?: boolean;
|
||||
},
|
||||
) => {
|
||||
if (directory && !/^[a-z0-9-]+$/.test(directory)) {
|
||||
@@ -61,7 +59,6 @@ const program = new Command(packageJson.name)
|
||||
displayName: options?.displayName,
|
||||
description: options?.description,
|
||||
skipLocalInstance: options?.skipLocalInstance,
|
||||
yes: options?.yes,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -11,9 +11,7 @@ import * as path from 'path';
|
||||
import { basename } from 'path';
|
||||
import {
|
||||
authLoginOAuth,
|
||||
checkDockerRunning,
|
||||
ConfigService,
|
||||
containerExists,
|
||||
detectLocalServer,
|
||||
serverStart,
|
||||
type ServerStartResult,
|
||||
@@ -29,7 +27,6 @@ type CreateAppOptions = {
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
skipLocalInstance?: boolean;
|
||||
yes?: boolean;
|
||||
};
|
||||
|
||||
export class CreateAppCommand {
|
||||
@@ -74,7 +71,7 @@ export class CreateAppCommand {
|
||||
let serverResult: ServerStartResult | undefined;
|
||||
|
||||
if (!options.skipLocalInstance) {
|
||||
const shouldStartServer = await this.shouldStartServer(options.yes);
|
||||
const shouldStartServer = await this.shouldStartServer();
|
||||
|
||||
if (shouldStartServer) {
|
||||
const startResult = await serverStart({
|
||||
@@ -226,35 +223,13 @@ export class CreateAppCommand {
|
||||
);
|
||||
}
|
||||
|
||||
private async shouldStartServer(autoConfirm?: boolean): Promise<boolean> {
|
||||
private async shouldStartServer(): Promise<boolean> {
|
||||
const existingServerUrl = await detectLocalServer();
|
||||
|
||||
if (existingServerUrl) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (checkDockerRunning() && containerExists()) {
|
||||
if (autoConfirm) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { startExisting } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'startExisting',
|
||||
message:
|
||||
'An existing Twenty server container was found. Would you like to start it?',
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
return startExisting;
|
||||
}
|
||||
|
||||
if (autoConfirm) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { startDocker } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-client-sdk": "2.2.0",
|
||||
"twenty-sdk": "2.2.0"
|
||||
"twenty-client-sdk": "0.9.0",
|
||||
"twenty-sdk": "0.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2",
|
||||
|
||||
@@ -8,6 +8,7 @@ export default defineApplication({
|
||||
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
displayName: 'Postcard App',
|
||||
description: 'Send postcards easily with Twenty',
|
||||
icon: 'IconWorld',
|
||||
applicationVariables: {
|
||||
DEFAULT_RECIPIENT_NAME: {
|
||||
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { useEffect } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
enqueueSnackbar,
|
||||
unmountFrontComponent,
|
||||
updateProgress,
|
||||
useRecordId,
|
||||
} from 'twenty-sdk/front-component';
|
||||
import { useRecordId, updateProgress, enqueueSnackbar, unmountFrontComponent } from 'twenty-sdk/front-component';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
|
||||
|
||||
const SYSTEM_PROMPT =
|
||||
@@ -18,9 +14,9 @@ const GeneratePostCardEffect = () => {
|
||||
const recordId = useRecordId();
|
||||
|
||||
useEffect(() => {
|
||||
if (recordId === null) {
|
||||
if (!isDefined(recordId)) {
|
||||
enqueueSnackbar({
|
||||
message: 'Please select exactly one record',
|
||||
message: 'No record selected',
|
||||
variant: 'error',
|
||||
});
|
||||
unmountFrontComponent();
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { useEffect } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
enqueueSnackbar,
|
||||
unmountFrontComponent,
|
||||
updateProgress,
|
||||
useRecordId,
|
||||
} from 'twenty-sdk/front-component';
|
||||
import { useRecordId, updateProgress, enqueueSnackbar, unmountFrontComponent } from 'twenty-sdk/front-component';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
|
||||
|
||||
const SendPostCardsEffect = () => {
|
||||
@@ -18,25 +14,55 @@ const SendPostCardsEffect = () => {
|
||||
await updateProgress(0.1);
|
||||
const client = new CoreApiClient();
|
||||
|
||||
let idsToSend: string[] = [];
|
||||
|
||||
if (isDefined(recordId)) {
|
||||
idsToSend = [recordId];
|
||||
} else {
|
||||
const { postCards } = await client.query({
|
||||
postCards: {
|
||||
__args: {
|
||||
filter: { status: { eq: 'DRAFT' } },
|
||||
},
|
||||
edges: { node: { id: true } },
|
||||
},
|
||||
});
|
||||
|
||||
idsToSend =
|
||||
postCards?.edges?.map(
|
||||
(edge: { node: { id: string; status: true } }) => edge.node.id,
|
||||
) ?? [];
|
||||
}
|
||||
|
||||
if (idsToSend.length === 0) {
|
||||
await updateProgress(1);
|
||||
await unmountFrontComponent();
|
||||
return;
|
||||
}
|
||||
|
||||
await updateProgress(0.3);
|
||||
|
||||
if (recordId) {
|
||||
for (let i = 0; i < idsToSend.length; i++) {
|
||||
await client.mutation({
|
||||
updatePostCard: {
|
||||
__args: {
|
||||
id: recordId,
|
||||
id: idsToSend[i],
|
||||
data: { status: 'SENT' },
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
await enqueueSnackbar({
|
||||
message: `Postcard sent`,
|
||||
variant: 'success',
|
||||
});
|
||||
await updateProgress(0.3 + (0.7 * (i + 1)) / idsToSend.length);
|
||||
}
|
||||
|
||||
const count = idsToSend.length;
|
||||
|
||||
await enqueueSnackbar({
|
||||
message: `${count} postcard${count > 1 ? 's' : ''} sent`,
|
||||
variant: 'success',
|
||||
});
|
||||
|
||||
await unmountFrontComponent();
|
||||
} catch (error) {
|
||||
const message =
|
||||
|
||||
@@ -3317,8 +3317,8 @@ __metadata:
|
||||
oxlint: "npm:^0.16.0"
|
||||
react: "npm:^19.0.0"
|
||||
react-dom: "npm:^19.0.0"
|
||||
twenty-client-sdk: "npm:2.2.0"
|
||||
twenty-sdk: "npm:2.2.0"
|
||||
twenty-client-sdk: "npm:0.9.0"
|
||||
twenty-sdk: "npm:0.9.0"
|
||||
typescript: "npm:^5.9.3"
|
||||
vite-tsconfig-paths: "npm:^4.2.1"
|
||||
vitest: "npm:^3.1.1"
|
||||
@@ -4059,21 +4059,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"twenty-client-sdk@npm:2.2.0":
|
||||
version: 2.2.0
|
||||
resolution: "twenty-client-sdk@npm:2.2.0"
|
||||
"twenty-client-sdk@npm:0.9.0":
|
||||
version: 0.9.0
|
||||
resolution: "twenty-client-sdk@npm:0.9.0"
|
||||
dependencies:
|
||||
"@genql/cli": "npm:^3.0.3"
|
||||
"@genql/runtime": "npm:^2.10.0"
|
||||
esbuild: "npm:^0.25.0"
|
||||
graphql: "npm:^16.8.1"
|
||||
checksum: 10c0/90122593efa53440ae386960211a2c274b13a410942bf6f9bc35cf952ae55fe83280e29074677fd284fa3c79069fc578980db06a92a143c08e043f865dbbbd3c
|
||||
checksum: 10c0/4b42a6622a9852fc3eca50c1131b116c5602af006ee5f1d3b4f1e97721bbc8ef5c2f3b60d9dee3ca9ca8c3c7ad4b292a7b55007b779b4c042997dbc29940ba54
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"twenty-sdk@npm:2.2.0":
|
||||
version: 2.2.0
|
||||
resolution: "twenty-sdk@npm:2.2.0"
|
||||
"twenty-sdk@npm:0.9.0":
|
||||
version: 0.9.0
|
||||
resolution: "twenty-sdk@npm:0.9.0"
|
||||
dependencies:
|
||||
"@genql/cli": "npm:^3.0.3"
|
||||
"@genql/runtime": "npm:^2.10.0"
|
||||
@@ -4093,7 +4093,7 @@ __metadata:
|
||||
react: "npm:^19.0.0"
|
||||
react-dom: "npm:^19.0.0"
|
||||
tinyglobby: "npm:^0.2.15"
|
||||
twenty-client-sdk: "npm:2.2.0"
|
||||
twenty-client-sdk: "npm:0.9.0"
|
||||
typescript: "npm:^5.9.2"
|
||||
uuid: "npm:^13.0.0"
|
||||
vite: "npm:^7.0.0"
|
||||
@@ -4101,7 +4101,7 @@ __metadata:
|
||||
zod: "npm:^4.1.11"
|
||||
bin:
|
||||
twenty: dist/cli.cjs
|
||||
checksum: 10c0/8978b4b0aa5ea282c8f76799347d9d1812901ec609bc2bd9270261a6bc5589ad361f6102a06900a4511a29a8378cd3811c2c0bad9f01cb8adadabca6d96dfd0b
|
||||
checksum: 10c0/27f93e5edac3265f819abacc853598435718020258a95d55518562e086e42daabae784964b42005d8e4ff30d1d6f13a12a38c656e18c60bad98da3f579a8e1c3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
@@ -4,5 +4,6 @@ export default defineApplication({
|
||||
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000001',
|
||||
displayName: 'Root App',
|
||||
description: 'An app with all entities at root level',
|
||||
icon: 'IconFolder',
|
||||
defaultRoleUniversalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000002',
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ export default defineApplication({
|
||||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||||
displayName: 'Rich App',
|
||||
description: 'A simple rich app',
|
||||
icon: 'IconWorld',
|
||||
applicationVariables: {
|
||||
DEFAULT_RECIPIENT_NAME: {
|
||||
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
|
||||
|
||||
@@ -10,13 +10,5 @@ export default defineLogicFunction({
|
||||
description: 'Look up a recipient by name to find their details',
|
||||
timeoutSeconds: 5,
|
||||
handler,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
recipientName: { type: 'string' },
|
||||
},
|
||||
required: ['recipientName'],
|
||||
},
|
||||
},
|
||||
isTool: true,
|
||||
});
|
||||
|
||||
@@ -30,31 +30,31 @@ export default defineView({
|
||||
],
|
||||
groups: [
|
||||
{
|
||||
universalIdentifier: 'e9ed34f1-3c3d-41b1-869b-00aae0033d9c',
|
||||
universalIdentifier: 'bg1a2b3c-0001-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
fieldValue: 'DRAFT',
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '19b1a3c1-53f0-4d32-b072-d645dac98e38',
|
||||
universalIdentifier: 'bg1a2b3c-0002-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
fieldValue: 'SENT',
|
||||
position: 1,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'f545cb5a-370d-423f-9b4e-278a9a465bdf',
|
||||
universalIdentifier: 'bg1a2b3c-0003-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
fieldValue: 'DELIVERED',
|
||||
position: 2,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '5d4c6d5f-af53-4cd0-a843-df38915561b2',
|
||||
universalIdentifier: 'bg1a2b3c-0004-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
fieldValue: 'RETURNED',
|
||||
position: 3,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '5ebbd7dc-9939-4594-b2a0-519269b4531f',
|
||||
universalIdentifier: 'bg1a2b3c-0005-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
fieldValue: 'LOST',
|
||||
position: 4,
|
||||
isVisible: true,
|
||||
|
||||
@@ -111,8 +111,7 @@ export default defineLogicFunction({
|
||||
description:
|
||||
'Structured web search powered by Exa. Returns entity-aware results with category filtering (companies, people, research papers, news, and other content types). Prefer this when the query benefits from structured data or a specific category. For general real-time web browsing, prefer the native `web_search` tool when it is available.',
|
||||
timeoutSeconds: 30,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: exaWebSearchInputSchema,
|
||||
},
|
||||
isTool: true,
|
||||
toolInputSchema: exaWebSearchInputSchema,
|
||||
handler,
|
||||
});
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"$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"
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
# Linear for Twenty
|
||||
|
||||
Connect your Linear account to Twenty to create issues and look up teams
|
||||
straight from your workflows or the AI chat.
|
||||
|
||||
## What you can do
|
||||
|
||||
Once installed and connected, two tools become available:
|
||||
|
||||
- **Create Linear issue** — from the AI chat, ask something like
|
||||
*"create a Linear issue in the Engineering team titled 'Fix login bug'"*
|
||||
and the AI will file it for you. From a workflow, add it as a step
|
||||
with `teamId` + `title` (and optional `description`).
|
||||
- **List Linear teams** — discovers the teams in your Linear workspace,
|
||||
useful when you need to pick a `teamId` for the create-issue step.
|
||||
|
||||
## Installing
|
||||
|
||||
1. Open **Settings → Applications** in your Twenty workspace.
|
||||
2. Find **Linear** in the available apps and click **Install**.
|
||||
3. Open the app, go to the **Connections** tab, and click **Add connection**.
|
||||
4. Choose **Just for me** (your personal Linear account) or
|
||||
**Workspace shared** (a team-managed Linear account anyone in this
|
||||
workspace can act through), then complete the Linear sign-in.
|
||||
|
||||
That's it — you can now use the tools above.
|
||||
|
||||
> If you see a "Linear OAuth is not yet set up by your server administrator"
|
||||
> notice on the Connections tab, ask your Twenty admin to follow the
|
||||
> **Self-hosting setup** below — they need to provide the OAuth credentials
|
||||
> before connections can be added.
|
||||
|
||||
---
|
||||
|
||||
## Self-hosting setup
|
||||
|
||||
This section is for Twenty server admins. If you're on Twenty Cloud, skip
|
||||
this — the OAuth credentials are already configured.
|
||||
|
||||
### 1. Register an OAuth app in Linear
|
||||
|
||||
1. Visit https://linear.app/settings/api/applications/new.
|
||||
2. Set the **Redirect URI** to `<SERVER_URL>/apps/oauth/callback` (for
|
||||
local dev: `http://localhost:3000/apps/oauth/callback`).
|
||||
3. Copy the generated **Client ID** and **Client Secret**.
|
||||
|
||||
### 2. Wire the credentials into Twenty
|
||||
|
||||
1. In **Settings → Applications**, find **Linear**, click into it, and go
|
||||
to the **Application registration** tab (admin-only).
|
||||
2. Paste your Linear **Client ID** into `LINEAR_CLIENT_ID` and the
|
||||
**Client Secret** into `LINEAR_CLIENT_SECRET`.
|
||||
|
||||
Workspace users will now be able to add Linear connections from the
|
||||
**Connections** tab as described above.
|
||||
|
||||
### 3. (Developers only) Building the app from source
|
||||
|
||||
If you're working on this app rather than installing the published version:
|
||||
|
||||
```bash
|
||||
cd packages/twenty-apps/internal/twenty-linear
|
||||
|
||||
# For day-to-day development (publish + install + watch in one):
|
||||
yarn twenty dev
|
||||
|
||||
# Manual publish flow (deploy registers the app, install activates it):
|
||||
yarn twenty deploy
|
||||
yarn twenty install
|
||||
```
|
||||
|
||||
`twenty dev` is recommended for iteration — it publishes, installs, and
|
||||
watches for changes in one command. Use `twenty deploy` + `twenty install`
|
||||
when you want to control each step separately (e.g. deploying to a
|
||||
production server without auto-installing).
|
||||
|
||||
This serves as the reference implementation for Twenty's
|
||||
`defineConnectionProvider({ type: 'oauth' })` flow — useful as a template
|
||||
when adding OAuth integrations for other providers.
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"name": "twenty-linear",
|
||||
"version": "0.1.5",
|
||||
"description": "Linear integration for Twenty. Connect a user's Linear account and create issues from logic functions.",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
"npm": "please-use-yarn",
|
||||
"yarn": ">=4.0.2"
|
||||
},
|
||||
"keywords": [
|
||||
"twenty-app"
|
||||
],
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"scripts": {
|
||||
"twenty": "twenty",
|
||||
"lint": "oxlint -c .oxlintrc.json .",
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json .",
|
||||
"test": "vitest run --config vitest.unit.config.ts",
|
||||
"test:watch": "vitest --config vitest.unit.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-sdk": "2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2",
|
||||
"oxlint": "^0.16.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"vitest": "^3.1.1"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<svg fill="#5E6AD2" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Linear</title><path d="M2.886 4.18A11.982 11.982 0 0 1 11.99 0C18.624 0 24 5.376 24 12.009c0 3.64-1.62 6.903-4.18 9.105L2.887 4.18ZM1.817 5.626l16.556 16.556c-.524.33-1.075.62-1.65.866L.951 7.277c.247-.575.537-1.126.866-1.65ZM.322 9.163l14.515 14.515c-.71.172-1.443.282-2.195.322L0 11.358a12 12 0 0 1 .322-2.195Zm-.17 4.862 9.823 9.824a12.02 12.02 0 0 1-9.824-9.824Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 469 B |
@@ -1,32 +0,0 @@
|
||||
import { defineApplication } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
displayName: 'Linear',
|
||||
description:
|
||||
'Connect Linear to Twenty. Each workspace member connects their own Linear account; logic functions can then create issues and read team data on their behalf.',
|
||||
logoUrl: 'public/linear-logomark.svg',
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
// OAuth client_id/secret live at the registration level (one OAuth app per
|
||||
// Twenty server, configured by the server admin) — not per-workspace —
|
||||
// so they're declared as serverVariables, not applicationVariables.
|
||||
serverVariables: {
|
||||
LINEAR_CLIENT_ID: {
|
||||
description:
|
||||
'OAuth client ID from your Linear OAuth application (linear.app/settings/api/applications).',
|
||||
isSecret: false,
|
||||
isRequired: true,
|
||||
},
|
||||
LINEAR_CLIENT_SECRET: {
|
||||
description:
|
||||
'OAuth client secret from your Linear OAuth application. Stored encrypted; never exposed in API responses.',
|
||||
isSecret: true,
|
||||
isRequired: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
import { defineConnectionProvider } from 'twenty-sdk/define';
|
||||
|
||||
import { LINEAR_CONNECTION_PROVIDER_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
|
||||
export default defineConnectionProvider({
|
||||
universalIdentifier: LINEAR_CONNECTION_PROVIDER_UNIVERSAL_IDENTIFIER,
|
||||
name: 'linear',
|
||||
displayName: 'Linear',
|
||||
type: 'oauth',
|
||||
oauth: {
|
||||
authorizationEndpoint: 'https://linear.app/oauth/authorize',
|
||||
tokenEndpoint: 'https://api.linear.app/oauth/token',
|
||||
revokeEndpoint: 'https://api.linear.app/oauth/revoke',
|
||||
scopes: ['read', 'write'],
|
||||
clientIdVariable: 'LINEAR_CLIENT_ID',
|
||||
clientSecretVariable: 'LINEAR_CLIENT_SECRET',
|
||||
tokenRequestContentType: 'form-urlencoded',
|
||||
// Linear supports PKCE but doesn't require it for confidential clients.
|
||||
// Disabled to keep the test surface minimal.
|
||||
usePkce: false,
|
||||
},
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
// Group all universal identifiers in a single file. Per the codebase
|
||||
// convention (see twenty-for-twenty), closely-related constants live
|
||||
// together so the rest of the app's source files can stay at one
|
||||
// `export default` per file.
|
||||
|
||||
export const APPLICATION_UNIVERSAL_IDENTIFIER =
|
||||
'6f4e7c2a-3d8e-4a91-b2cf-9e0b8d5f4a2e';
|
||||
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
'b6c33347-e41c-4b90-8a37-7b3c49baa85a';
|
||||
|
||||
export const LINEAR_CONNECTION_PROVIDER_UNIVERSAL_IDENTIFIER =
|
||||
'9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f';
|
||||
|
||||
export const CREATE_LINEAR_ISSUE_UNIVERSAL_IDENTIFIER =
|
||||
'01f829c9-1661-41fa-9ee1-b67e64716c2e';
|
||||
|
||||
export const LIST_LINEAR_TEAMS_UNIVERSAL_IDENTIFIER =
|
||||
'15824bbc-9c64-4f97-b45f-d0a44b402bb8';
|
||||
@@ -1,126 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { createLinearIssueHandler } from '../handlers/create-linear-issue-handler';
|
||||
|
||||
import { buildConnection, stubConnectionsThenLinear } from './test-utils';
|
||||
|
||||
const SAVED_ENV = { ...process.env };
|
||||
|
||||
describe('createLinearIssueHandler', () => {
|
||||
beforeEach(() => {
|
||||
process.env.TWENTY_API_URL = 'http://api.test';
|
||||
process.env.TWENTY_APP_ACCESS_TOKEN = 'app-token';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = { ...SAVED_ENV };
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('returns an error when required input fields are missing', async () => {
|
||||
const result = await createLinearIssueHandler({ title: 'no team' });
|
||||
|
||||
expect(result).toMatchObject({
|
||||
success: false,
|
||||
error: expect.stringContaining('teamId'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when no Linear connection exists', async () => {
|
||||
stubConnectionsThenLinear([], {});
|
||||
|
||||
const result = await createLinearIssueHandler({
|
||||
teamId: 'team_1',
|
||||
title: 'hi',
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
success: false,
|
||||
error: expect.stringContaining('not connected'),
|
||||
});
|
||||
});
|
||||
|
||||
it('calls Linear with the only available connection and returns the issue', async () => {
|
||||
const issue = {
|
||||
id: 'issue_1',
|
||||
identifier: 'TEAM-1',
|
||||
title: 'Hello from Twenty',
|
||||
url: 'https://linear.app/twenty/issue/TEAM-1',
|
||||
};
|
||||
|
||||
const fetchMock = stubConnectionsThenLinear([buildConnection()], {
|
||||
data: { issueCreate: { success: true, issue } },
|
||||
});
|
||||
|
||||
const result = await createLinearIssueHandler({
|
||||
teamId: 'team_1',
|
||||
title: 'Hello from Twenty',
|
||||
description: 'Body',
|
||||
});
|
||||
|
||||
expect(result).toEqual({ success: true, issue });
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[1];
|
||||
|
||||
expect(url).toBe('https://api.linear.app/graphql');
|
||||
expect(init.headers.Authorization).toBe('Bearer lin_test_access_token');
|
||||
expect(JSON.parse(init.body as string).variables.input).toEqual({
|
||||
teamId: 'team_1',
|
||||
title: 'Hello from Twenty',
|
||||
description: 'Body',
|
||||
});
|
||||
});
|
||||
|
||||
it('prefers a workspace-shared connection over a user-visibility one', async () => {
|
||||
const userConnection = buildConnection({
|
||||
id: 'conn_user',
|
||||
accessToken: 'lin_user',
|
||||
});
|
||||
const sharedConnection = buildConnection({
|
||||
id: 'conn_shared',
|
||||
visibility: 'workspace',
|
||||
accessToken: 'lin_shared',
|
||||
});
|
||||
|
||||
const fetchMock = stubConnectionsThenLinear(
|
||||
[userConnection, sharedConnection],
|
||||
{
|
||||
data: {
|
||||
issueCreate: {
|
||||
success: true,
|
||||
issue: {
|
||||
id: 'issue_2',
|
||||
identifier: 'T-2',
|
||||
title: 'Hi',
|
||||
url: 'https://linear.app/x/T-2',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const result = await createLinearIssueHandler({
|
||||
teamId: 'team_1',
|
||||
title: 'Hi',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(fetchMock.mock.calls[1][1].headers.Authorization).toBe(
|
||||
'Bearer lin_shared',
|
||||
);
|
||||
});
|
||||
|
||||
it('surfaces Linear GraphQL errors as the handler error', async () => {
|
||||
stubConnectionsThenLinear([buildConnection()], {
|
||||
errors: [{ message: 'Invalid teamId' }],
|
||||
});
|
||||
|
||||
const result = await createLinearIssueHandler({
|
||||
teamId: 'bogus',
|
||||
title: 'hi',
|
||||
});
|
||||
|
||||
expect(result).toEqual({ success: false, error: 'Invalid teamId' });
|
||||
});
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { listLinearTeamsHandler } from '../handlers/list-linear-teams-handler';
|
||||
|
||||
import { buildConnection, stubConnectionsThenLinear } from './test-utils';
|
||||
|
||||
const SAVED_ENV = { ...process.env };
|
||||
|
||||
describe('listLinearTeamsHandler', () => {
|
||||
beforeEach(() => {
|
||||
process.env.TWENTY_API_URL = 'http://api.test';
|
||||
process.env.TWENTY_APP_ACCESS_TOKEN = 'app-token';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = { ...SAVED_ENV };
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('returns the teams when the Linear query succeeds', async () => {
|
||||
const teams = [
|
||||
{ id: 'team_1', name: 'Engineering', key: 'ENG' },
|
||||
{ id: 'team_2', name: 'Design', key: 'DES' },
|
||||
];
|
||||
|
||||
const fetchMock = stubConnectionsThenLinear([buildConnection()], {
|
||||
data: { teams: { nodes: teams } },
|
||||
});
|
||||
|
||||
const result = await listLinearTeamsHandler();
|
||||
|
||||
expect(result).toEqual({ success: true, teams });
|
||||
expect(fetchMock.mock.calls[1][1].headers.Authorization).toBe(
|
||||
'Bearer lin_test_access_token',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns success=false when no Linear connection exists', async () => {
|
||||
stubConnectionsThenLinear([], { data: { teams: { nodes: [] } } });
|
||||
|
||||
const result = await listLinearTeamsHandler();
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('surfaces Linear errors', async () => {
|
||||
stubConnectionsThenLinear([buildConnection()], {
|
||||
errors: [{ message: 'rate limited' }],
|
||||
});
|
||||
|
||||
const result = await listLinearTeamsHandler();
|
||||
|
||||
expect(result).toEqual({ success: false, error: 'rate limited' });
|
||||
});
|
||||
});
|
||||
@@ -1,48 +0,0 @@
|
||||
import { vi } from 'vitest';
|
||||
|
||||
export const USER_WORKSPACE_ID = '11111111-1111-1111-1111-111111111111';
|
||||
|
||||
export const buildConnection = (
|
||||
overrides: Partial<Record<string, unknown>> = {},
|
||||
) => ({
|
||||
id: 'conn_1',
|
||||
name: 'octocat@example.com',
|
||||
visibility: 'user' as const,
|
||||
providerName: 'linear',
|
||||
userWorkspaceId: USER_WORKSPACE_ID,
|
||||
accessToken: 'lin_test_access_token',
|
||||
scopes: ['read', 'write'],
|
||||
handle: 'octocat@example.com',
|
||||
lastRefreshedAt: '2024-01-01T00:00:00.000Z',
|
||||
authFailedAt: null,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
// Stubs `fetch` to first answer the SDK's `/apps/connections/list` call, then
|
||||
// the handler's downstream Linear GraphQL request.
|
||||
export const stubConnectionsThenLinear = (
|
||||
connections: ReturnType<typeof buildConnection>[],
|
||||
linearJson: unknown,
|
||||
) => {
|
||||
const fetchMock = vi.fn(async (url: string) => {
|
||||
if (url.endsWith('/apps/connections/list')) {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => connections,
|
||||
text: async () => JSON.stringify(connections),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => linearJson,
|
||||
text: async () => JSON.stringify(linearJson),
|
||||
};
|
||||
});
|
||||
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
return fetchMock;
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
export const ISSUE_CREATE_MUTATION = `
|
||||
mutation IssueCreate($input: IssueCreateInput!) {
|
||||
issueCreate(input: $input) {
|
||||
success
|
||||
issue { id identifier title url }
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,34 +0,0 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
|
||||
import { CREATE_LINEAR_ISSUE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { createLinearIssueHandler } from 'src/logic-functions/handlers/create-linear-issue-handler';
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: CREATE_LINEAR_ISSUE_UNIVERSAL_IDENTIFIER,
|
||||
name: 'create-linear-issue',
|
||||
description:
|
||||
'Create a Linear issue on behalf of the connected user. Requires a teamId (call list-linear-teams to discover one) and a title.',
|
||||
timeoutSeconds: 30,
|
||||
handler: createLinearIssueHandler,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
teamId: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The Linear team ID to create the issue in. Use list-linear-teams to discover available teams.',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'The issue title.',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Optional issue description (Markdown supported).',
|
||||
},
|
||||
},
|
||||
required: ['teamId', 'title'],
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,73 +0,0 @@
|
||||
import { listConnections } from 'twenty-sdk/logic-function';
|
||||
|
||||
import { ISSUE_CREATE_MUTATION } from 'src/logic-functions/constants/issue-create-mutation.constant';
|
||||
import { type CreateIssueInput } from 'src/logic-functions/types/create-issue-input.type';
|
||||
import { type CreateIssueMutationResult } from 'src/logic-functions/types/create-issue-mutation-result.type';
|
||||
import { callLinearGraphQL } from 'src/logic-functions/utils/call-linear-graphql';
|
||||
|
||||
type HandlerResult =
|
||||
| {
|
||||
success: true;
|
||||
issue: {
|
||||
id: string;
|
||||
identifier: string;
|
||||
title: string;
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
| { success: false; error: string };
|
||||
|
||||
export const createLinearIssueHandler = async (
|
||||
input: CreateIssueInput,
|
||||
): Promise<HandlerResult> => {
|
||||
if (!input.teamId || !input.title) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Both `teamId` and `title` are required.',
|
||||
};
|
||||
}
|
||||
|
||||
const connections = await listConnections({ providerName: 'linear' });
|
||||
// Workspace-shared credentials win when present (a team-managed service
|
||||
// account); otherwise fall back to the first user-scoped connection.
|
||||
const connection =
|
||||
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
|
||||
|
||||
if (!connection) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
'Linear is not connected. Open the app settings and click "Add connection" first.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await callLinearGraphQL<CreateIssueMutationResult>({
|
||||
accessToken: connection.accessToken,
|
||||
query: ISSUE_CREATE_MUTATION,
|
||||
variables: {
|
||||
input: {
|
||||
teamId: input.teamId,
|
||||
title: input.title,
|
||||
description: input.description,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (result.errors || !result.data) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.errors?.[0]?.message ?? 'Unknown Linear API error',
|
||||
};
|
||||
}
|
||||
|
||||
const { success, issue } = result.data.issueCreate;
|
||||
|
||||
if (!success || !issue) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Linear reported the mutation as unsuccessful.',
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true, issue };
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
import { listConnections } from 'twenty-sdk/logic-function';
|
||||
|
||||
import { callLinearGraphQL } from 'src/logic-functions/utils/call-linear-graphql';
|
||||
|
||||
type LinearTeam = {
|
||||
id: string;
|
||||
name: string;
|
||||
key: string;
|
||||
};
|
||||
|
||||
type TeamsQueryResult = {
|
||||
teams: { nodes: LinearTeam[] };
|
||||
};
|
||||
|
||||
type HandlerResult =
|
||||
| { success: true; teams: LinearTeam[] }
|
||||
| { success: false; error: string };
|
||||
|
||||
export const listLinearTeamsHandler = async (): Promise<HandlerResult> => {
|
||||
const connections = await listConnections({ providerName: 'linear' });
|
||||
const connection =
|
||||
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
|
||||
|
||||
if (!connection) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
'Linear is not connected. Open the app settings and click "Add connection" first.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await callLinearGraphQL<TeamsQueryResult>({
|
||||
accessToken: connection.accessToken,
|
||||
query: `
|
||||
query Teams {
|
||||
teams { nodes { id name key } }
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
if (result.errors || !result.data) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.errors?.[0]?.message ?? 'Unknown Linear API error',
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true, teams: result.data.teams.nodes };
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
|
||||
import { LIST_LINEAR_TEAMS_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { listLinearTeamsHandler } from 'src/logic-functions/handlers/list-linear-teams-handler';
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: LIST_LINEAR_TEAMS_UNIVERSAL_IDENTIFIER,
|
||||
name: 'list-linear-teams',
|
||||
description:
|
||||
"Returns the connected user's Linear teams. Useful for picking a teamId to pass to create-linear-issue.",
|
||||
timeoutSeconds: 15,
|
||||
handler: listLinearTeamsHandler,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
export type CreateIssueInput = {
|
||||
teamId?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
export type CreateIssueMutationResult = {
|
||||
issueCreate: {
|
||||
success: boolean;
|
||||
issue: {
|
||||
id: string;
|
||||
identifier: string;
|
||||
title: string;
|
||||
url: string;
|
||||
} | null;
|
||||
};
|
||||
};
|
||||
@@ -1,58 +0,0 @@
|
||||
import { type LinearGraphQLResult } from 'src/logic-functions/utils/types/linear-graphql-result.type';
|
||||
|
||||
const LINEAR_GRAPHQL_ENDPOINT = 'https://api.linear.app/graphql';
|
||||
|
||||
export const callLinearGraphQL = async <TData>({
|
||||
accessToken,
|
||||
query,
|
||||
variables,
|
||||
}: {
|
||||
accessToken: string;
|
||||
query: string;
|
||||
variables?: Record<string, unknown>;
|
||||
}): Promise<LinearGraphQLResult<TData>> => {
|
||||
let response: Response;
|
||||
|
||||
try {
|
||||
response = await fetch(LINEAR_GRAPHQL_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({ query, variables }),
|
||||
});
|
||||
} catch (error) {
|
||||
return {
|
||||
errors: [
|
||||
{
|
||||
message: `Linear API request failed: ${(error as Error).message}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => '');
|
||||
|
||||
return {
|
||||
errors: [
|
||||
{
|
||||
message: `Linear API responded with ${response.status}: ${text.slice(0, 500)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return (await response.json()) as LinearGraphQLResult<TData>;
|
||||
} catch (error) {
|
||||
return {
|
||||
errors: [
|
||||
{
|
||||
message: `Linear API returned a non-JSON response: ${(error as Error).message}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
export type LinearGraphQLResult<TData> = {
|
||||
data?: TData;
|
||||
errors?: Array<{ message: string }>;
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import { defineRole } from 'twenty-sdk/define';
|
||||
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
|
||||
// The Linear logic functions never read workspace data — they only call
|
||||
// Linear's GraphQL API on behalf of the connected user.
|
||||
export default defineRole({
|
||||
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
label: 'Linear function role',
|
||||
description: 'No-op role for Linear logic functions',
|
||||
canReadAllObjectRecords: false,
|
||||
canUpdateAllObjectRecords: false,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
canUpdateAllSettings: false,
|
||||
canBeAssignedToAgents: false,
|
||||
canBeAssignedToUsers: false,
|
||||
canBeAssignedToApiKeys: false,
|
||||
objectPermissions: [],
|
||||
fieldPermissions: [],
|
||||
permissionFlags: [],
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"allowUnreachableCode": false,
|
||||
"strict": true,
|
||||
"alwaysStrict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictBindCallApply": false,
|
||||
"target": "es2020",
|
||||
"module": "esnext",
|
||||
"lib": ["es2020"],
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"paths": {
|
||||
"src/*": ["./src/*"],
|
||||
"~/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"types": ["vitest/globals", "node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
const TWENTY_SDK_SRC = path.resolve(
|
||||
__dirname,
|
||||
'../../../twenty-sdk/src/sdk',
|
||||
);
|
||||
|
||||
// twenty-sdk's `exports` map points at compiled `./dist/*`. Aliasing the
|
||||
// subpaths to source keeps unit tests self-contained — no `yarn build` in
|
||||
// twenty-sdk required before running them.
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tsconfigPaths({
|
||||
projects: ['tsconfig.spec.json'],
|
||||
ignoreConfigErrors: true,
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: [
|
||||
{
|
||||
find: 'twenty-sdk/logic-function',
|
||||
replacement: path.join(TWENTY_SDK_SRC, 'logic-function/index.ts'),
|
||||
},
|
||||
{
|
||||
find: 'twenty-sdk/define',
|
||||
replacement: path.join(TWENTY_SDK_SRC, 'define/index.ts'),
|
||||
},
|
||||
// The SDK source uses `@/*` to refer to its own `src/`. Vitest
|
||||
// doesn't pick up the SDK's tsconfig path mapping when resolving
|
||||
// a different package, so map the alias here.
|
||||
{
|
||||
find: /^@\/(.*)$/,
|
||||
replacement: path.resolve(__dirname, '../../../twenty-sdk/src/$1'),
|
||||
},
|
||||
],
|
||||
},
|
||||
test: {
|
||||
include: ['src/**/*.test.ts'],
|
||||
},
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-client-sdk",
|
||||
"version": "2.3.0",
|
||||
"version": "2.1.0",
|
||||
"sideEffects": false,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@@ -46,8 +46,6 @@
|
||||
"graphql": "^16.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260116.1",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"twenty-shared": "workspace:*",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.0.0",
|
||||
|
||||
@@ -51,7 +51,6 @@ type ApplicationRegistration {
|
||||
logoUrl: String
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
isConfigured: Boolean!
|
||||
}
|
||||
|
||||
enum ApplicationRegistrationSourceType {
|
||||
@@ -325,115 +324,6 @@ type FrontComponent {
|
||||
applicationTokenPair: ApplicationTokenPair
|
||||
}
|
||||
|
||||
type CommandMenuItem {
|
||||
id: UUID!
|
||||
workflowVersionId: UUID
|
||||
frontComponentId: UUID
|
||||
frontComponent: FrontComponent
|
||||
engineComponentKey: EngineComponentKey!
|
||||
label: String!
|
||||
icon: String
|
||||
shortLabel: String
|
||||
position: Float!
|
||||
isPinned: Boolean!
|
||||
availabilityType: CommandMenuItemAvailabilityType!
|
||||
payload: CommandMenuItemPayload
|
||||
hotKeys: [String!]
|
||||
conditionalAvailabilityExpression: String
|
||||
availabilityObjectMetadataId: UUID
|
||||
pageLayoutId: UUID
|
||||
universalIdentifier: UUID
|
||||
applicationId: UUID
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
enum EngineComponentKey {
|
||||
NAVIGATE_TO_NEXT_RECORD
|
||||
NAVIGATE_TO_PREVIOUS_RECORD
|
||||
CREATE_NEW_RECORD
|
||||
DELETE_RECORDS
|
||||
RESTORE_RECORDS
|
||||
DESTROY_RECORDS
|
||||
ADD_TO_FAVORITES
|
||||
REMOVE_FROM_FAVORITES
|
||||
EXPORT_NOTE_TO_PDF
|
||||
EXPORT_RECORDS
|
||||
UPDATE_MULTIPLE_RECORDS
|
||||
MERGE_MULTIPLE_RECORDS
|
||||
IMPORT_RECORDS
|
||||
EXPORT_VIEW
|
||||
SEE_DELETED_RECORDS
|
||||
CREATE_NEW_VIEW
|
||||
HIDE_DELETED_RECORDS
|
||||
EDIT_RECORD_PAGE_LAYOUT
|
||||
EDIT_DASHBOARD_LAYOUT
|
||||
SAVE_DASHBOARD_LAYOUT
|
||||
CANCEL_DASHBOARD_LAYOUT
|
||||
DUPLICATE_DASHBOARD
|
||||
ACTIVATE_WORKFLOW
|
||||
DEACTIVATE_WORKFLOW
|
||||
DISCARD_DRAFT_WORKFLOW
|
||||
TEST_WORKFLOW
|
||||
SEE_ACTIVE_VERSION_WORKFLOW
|
||||
SEE_RUNS_WORKFLOW
|
||||
SEE_VERSIONS_WORKFLOW
|
||||
ADD_NODE_WORKFLOW
|
||||
TIDY_UP_WORKFLOW
|
||||
DUPLICATE_WORKFLOW
|
||||
SEE_VERSION_WORKFLOW_RUN
|
||||
SEE_WORKFLOW_WORKFLOW_RUN
|
||||
STOP_WORKFLOW_RUN
|
||||
SEE_RUNS_WORKFLOW_VERSION
|
||||
SEE_WORKFLOW_WORKFLOW_VERSION
|
||||
USE_AS_DRAFT_WORKFLOW_VERSION
|
||||
SEE_VERSIONS_WORKFLOW_VERSION
|
||||
SEARCH_RECORDS
|
||||
SEARCH_RECORDS_FALLBACK
|
||||
ASK_AI
|
||||
VIEW_PREVIOUS_AI_CHATS
|
||||
NAVIGATION
|
||||
TRIGGER_WORKFLOW_VERSION
|
||||
FRONT_COMPONENT_RENDERER
|
||||
REPLY_TO_EMAIL_THREAD
|
||||
COMPOSE_EMAIL
|
||||
GO_TO_PEOPLE
|
||||
GO_TO_COMPANIES
|
||||
GO_TO_DASHBOARDS
|
||||
GO_TO_OPPORTUNITIES
|
||||
GO_TO_SETTINGS
|
||||
GO_TO_TASKS
|
||||
GO_TO_NOTES
|
||||
GO_TO_WORKFLOWS
|
||||
GO_TO_RUNS
|
||||
DELETE_SINGLE_RECORD
|
||||
DELETE_MULTIPLE_RECORDS
|
||||
RESTORE_SINGLE_RECORD
|
||||
RESTORE_MULTIPLE_RECORDS
|
||||
DESTROY_SINGLE_RECORD
|
||||
DESTROY_MULTIPLE_RECORDS
|
||||
EXPORT_FROM_RECORD_INDEX
|
||||
EXPORT_FROM_RECORD_SHOW
|
||||
EXPORT_MULTIPLE_RECORDS
|
||||
}
|
||||
|
||||
enum CommandMenuItemAvailabilityType {
|
||||
GLOBAL
|
||||
GLOBAL_OBJECT_CONTEXT
|
||||
RECORD_SELECTION
|
||||
FALLBACK
|
||||
}
|
||||
|
||||
union CommandMenuItemPayload = PathCommandMenuItemPayload | ObjectMetadataCommandMenuItemPayload
|
||||
|
||||
type PathCommandMenuItemPayload {
|
||||
path: String!
|
||||
}
|
||||
|
||||
type ObjectMetadataCommandMenuItemPayload {
|
||||
objectMetadataItemId: UUID!
|
||||
}
|
||||
|
||||
type LogicFunction {
|
||||
id: UUID!
|
||||
name: String!
|
||||
@@ -442,11 +332,11 @@ type LogicFunction {
|
||||
timeoutSeconds: Float!
|
||||
sourceHandlerPath: String!
|
||||
handlerName: String!
|
||||
toolInputSchema: JSON
|
||||
isTool: Boolean!
|
||||
cronTriggerSettings: JSON
|
||||
databaseEventTriggerSettings: JSON
|
||||
httpRouteTriggerSettings: JSON
|
||||
toolTriggerSettings: JSON
|
||||
workflowActionTriggerSettings: JSON
|
||||
applicationId: UUID
|
||||
universalIdentifier: UUID
|
||||
createdAt: DateTime!
|
||||
@@ -690,7 +580,6 @@ type Application {
|
||||
id: UUID!
|
||||
name: String!
|
||||
description: String
|
||||
logo: String
|
||||
version: String
|
||||
universalIdentifier: String!
|
||||
packageJsonChecksum: String
|
||||
@@ -705,7 +594,6 @@ type Application {
|
||||
defaultLogicFunctionRole: Role
|
||||
agents: [Agent!]!
|
||||
frontComponents: [FrontComponent!]!
|
||||
commandMenuItems: [CommandMenuItem!]!
|
||||
logicFunctions: [LogicFunction!]!
|
||||
objects: [Object!]!
|
||||
applicationVariables: [ApplicationVariable!]!
|
||||
@@ -974,6 +862,7 @@ type User {
|
||||
firstName: String!
|
||||
lastName: String!
|
||||
email: String!
|
||||
defaultAvatarUrl: String
|
||||
isEmailVerified: Boolean!
|
||||
disabled: Boolean
|
||||
canImpersonate: Boolean!
|
||||
@@ -1403,20 +1292,6 @@ enum PageLayoutType {
|
||||
STANDALONE_PAGE
|
||||
}
|
||||
|
||||
type ApplicationConnectionProviderOAuthConfig {
|
||||
scopes: [String!]!
|
||||
isClientCredentialsConfigured: Boolean!
|
||||
}
|
||||
|
||||
type ApplicationConnectionProvider {
|
||||
id: UUID!
|
||||
applicationId: String!
|
||||
type: String!
|
||||
name: String!
|
||||
displayName: String!
|
||||
oauth: ApplicationConnectionProviderOAuthConfig
|
||||
}
|
||||
|
||||
type Analytics {
|
||||
"""Boolean that confirms query was dispatched"""
|
||||
success: Boolean!
|
||||
@@ -2327,6 +2202,7 @@ type MarketplaceApp {
|
||||
id: String!
|
||||
name: String!
|
||||
description: String!
|
||||
icon: String!
|
||||
author: String!
|
||||
category: String!
|
||||
logo: String
|
||||
@@ -2435,6 +2311,114 @@ type PostgresCredentials {
|
||||
workspaceId: UUID!
|
||||
}
|
||||
|
||||
type CommandMenuItem {
|
||||
id: UUID!
|
||||
workflowVersionId: UUID
|
||||
frontComponentId: UUID
|
||||
frontComponent: FrontComponent
|
||||
engineComponentKey: EngineComponentKey!
|
||||
label: String!
|
||||
icon: String
|
||||
shortLabel: String
|
||||
position: Float!
|
||||
isPinned: Boolean!
|
||||
availabilityType: CommandMenuItemAvailabilityType!
|
||||
payload: CommandMenuItemPayload
|
||||
hotKeys: [String!]
|
||||
conditionalAvailabilityExpression: String
|
||||
availabilityObjectMetadataId: UUID
|
||||
pageLayoutId: UUID
|
||||
applicationId: UUID
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
enum EngineComponentKey {
|
||||
NAVIGATE_TO_NEXT_RECORD
|
||||
NAVIGATE_TO_PREVIOUS_RECORD
|
||||
CREATE_NEW_RECORD
|
||||
DELETE_RECORDS
|
||||
RESTORE_RECORDS
|
||||
DESTROY_RECORDS
|
||||
ADD_TO_FAVORITES
|
||||
REMOVE_FROM_FAVORITES
|
||||
EXPORT_NOTE_TO_PDF
|
||||
EXPORT_RECORDS
|
||||
UPDATE_MULTIPLE_RECORDS
|
||||
MERGE_MULTIPLE_RECORDS
|
||||
IMPORT_RECORDS
|
||||
EXPORT_VIEW
|
||||
SEE_DELETED_RECORDS
|
||||
CREATE_NEW_VIEW
|
||||
HIDE_DELETED_RECORDS
|
||||
EDIT_RECORD_PAGE_LAYOUT
|
||||
EDIT_DASHBOARD_LAYOUT
|
||||
SAVE_DASHBOARD_LAYOUT
|
||||
CANCEL_DASHBOARD_LAYOUT
|
||||
DUPLICATE_DASHBOARD
|
||||
ACTIVATE_WORKFLOW
|
||||
DEACTIVATE_WORKFLOW
|
||||
DISCARD_DRAFT_WORKFLOW
|
||||
TEST_WORKFLOW
|
||||
SEE_ACTIVE_VERSION_WORKFLOW
|
||||
SEE_RUNS_WORKFLOW
|
||||
SEE_VERSIONS_WORKFLOW
|
||||
ADD_NODE_WORKFLOW
|
||||
TIDY_UP_WORKFLOW
|
||||
DUPLICATE_WORKFLOW
|
||||
SEE_VERSION_WORKFLOW_RUN
|
||||
SEE_WORKFLOW_WORKFLOW_RUN
|
||||
STOP_WORKFLOW_RUN
|
||||
SEE_RUNS_WORKFLOW_VERSION
|
||||
SEE_WORKFLOW_WORKFLOW_VERSION
|
||||
USE_AS_DRAFT_WORKFLOW_VERSION
|
||||
SEE_VERSIONS_WORKFLOW_VERSION
|
||||
SEARCH_RECORDS
|
||||
SEARCH_RECORDS_FALLBACK
|
||||
ASK_AI
|
||||
VIEW_PREVIOUS_AI_CHATS
|
||||
NAVIGATION
|
||||
TRIGGER_WORKFLOW_VERSION
|
||||
FRONT_COMPONENT_RENDERER
|
||||
REPLY_TO_EMAIL_THREAD
|
||||
COMPOSE_EMAIL
|
||||
GO_TO_PEOPLE
|
||||
GO_TO_COMPANIES
|
||||
GO_TO_DASHBOARDS
|
||||
GO_TO_OPPORTUNITIES
|
||||
GO_TO_SETTINGS
|
||||
GO_TO_TASKS
|
||||
GO_TO_NOTES
|
||||
GO_TO_WORKFLOWS
|
||||
GO_TO_RUNS
|
||||
DELETE_SINGLE_RECORD
|
||||
DELETE_MULTIPLE_RECORDS
|
||||
RESTORE_SINGLE_RECORD
|
||||
RESTORE_MULTIPLE_RECORDS
|
||||
DESTROY_SINGLE_RECORD
|
||||
DESTROY_MULTIPLE_RECORDS
|
||||
EXPORT_FROM_RECORD_INDEX
|
||||
EXPORT_FROM_RECORD_SHOW
|
||||
EXPORT_MULTIPLE_RECORDS
|
||||
}
|
||||
|
||||
enum CommandMenuItemAvailabilityType {
|
||||
GLOBAL
|
||||
GLOBAL_OBJECT_CONTEXT
|
||||
RECORD_SELECTION
|
||||
FALLBACK
|
||||
}
|
||||
|
||||
union CommandMenuItemPayload = PathCommandMenuItemPayload | ObjectMetadataCommandMenuItemPayload
|
||||
|
||||
type PathCommandMenuItemPayload {
|
||||
path: String!
|
||||
}
|
||||
|
||||
type ObjectMetadataCommandMenuItemPayload {
|
||||
objectMetadataItemId: UUID!
|
||||
}
|
||||
|
||||
type ToolIndexEntry {
|
||||
name: String!
|
||||
description: String!
|
||||
@@ -2553,10 +2537,6 @@ type ConnectedAccountDTO {
|
||||
connectionParameters: ImapSmtpCaldavConnectionParameters
|
||||
lastSignedInAt: DateTime
|
||||
userWorkspaceId: UUID!
|
||||
connectionProviderId: UUID
|
||||
applicationId: UUID
|
||||
name: String
|
||||
visibility: String!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
@@ -2584,10 +2564,6 @@ type ConnectedAccountPublicDTO {
|
||||
scopes: [String!]
|
||||
lastSignedInAt: DateTime
|
||||
userWorkspaceId: UUID!
|
||||
connectionProviderId: UUID
|
||||
applicationId: UUID
|
||||
name: String
|
||||
visibility: String!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
connectionParameters: PublicImapSmtpCaldavConnectionParameters
|
||||
@@ -2633,6 +2609,19 @@ type Skill {
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type AgentChatThread {
|
||||
id: UUID!
|
||||
title: String
|
||||
totalInputTokens: Int!
|
||||
totalOutputTokens: Int!
|
||||
contextWindowTokens: Int
|
||||
conversationSize: Int!
|
||||
totalInputCredits: Float!
|
||||
totalOutputCredits: Float!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type AgentMessage {
|
||||
id: UUID!
|
||||
threadId: UUID!
|
||||
@@ -2645,21 +2634,6 @@ type AgentMessage {
|
||||
createdAt: DateTime!
|
||||
}
|
||||
|
||||
type AgentChatThread {
|
||||
id: ID!
|
||||
title: String
|
||||
totalInputTokens: Int!
|
||||
totalOutputTokens: Int!
|
||||
contextWindowTokens: Int
|
||||
conversationSize: Int!
|
||||
totalInputCredits: Float!
|
||||
totalOutputCredits: Float!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
deletedAt: DateTime
|
||||
lastMessageAt: DateTime
|
||||
}
|
||||
|
||||
type AiSystemPromptSection {
|
||||
title: String!
|
||||
content: String!
|
||||
@@ -2687,6 +2661,22 @@ type AgentChatEvent {
|
||||
event: JSON!
|
||||
}
|
||||
|
||||
type AgentChatThreadEdge {
|
||||
"""The node containing the AgentChatThread"""
|
||||
node: AgentChatThread!
|
||||
|
||||
"""Cursor for this node."""
|
||||
cursor: ConnectionCursor!
|
||||
}
|
||||
|
||||
type AgentChatThreadConnection {
|
||||
"""Paging information"""
|
||||
pageInfo: PageInfo!
|
||||
|
||||
"""Array of edges."""
|
||||
edges: [AgentChatThreadEdge!]!
|
||||
}
|
||||
|
||||
type AgentTurnEvaluation {
|
||||
id: UUID!
|
||||
turnId: UUID!
|
||||
@@ -2873,8 +2863,6 @@ enum AllMetadataName {
|
||||
fieldPermission
|
||||
frontComponent
|
||||
webhook
|
||||
applicationVariable
|
||||
connectionProvider
|
||||
}
|
||||
|
||||
type MinimalObjectMetadata {
|
||||
@@ -2945,7 +2933,6 @@ type Query {
|
||||
getPageLayoutTab(id: String!): PageLayoutTab!
|
||||
getPageLayouts(objectMetadataId: String, pageLayoutType: PageLayoutType): [PageLayout!]!
|
||||
getPageLayout(id: String!): PageLayout
|
||||
applicationConnectionProviders(applicationId: UUID!): [ApplicationConnectionProvider!]!
|
||||
getPageLayoutWidgets(pageLayoutTabId: String!): [PageLayoutWidget!]!
|
||||
getPageLayoutWidget(id: String!): PageLayoutWidget!
|
||||
findOneLogicFunction(input: LogicFunctionIdInput!): LogicFunction!
|
||||
@@ -3006,13 +2993,22 @@ type Query {
|
||||
webhooks: [Webhook!]!
|
||||
webhook(id: UUID!): Webhook
|
||||
minimalMetadata: MinimalMetadata!
|
||||
chatThreads: [AgentChatThread!]!
|
||||
chatThread(id: UUID!): AgentChatThread!
|
||||
chatMessages(threadId: UUID!): [AgentMessage!]!
|
||||
chatStreamCatchupChunks(threadId: UUID!): ChatStreamCatchupChunks!
|
||||
getAiSystemPromptPreview: AiSystemPromptPreview!
|
||||
skills: [Skill!]!
|
||||
skill(id: UUID!): Skill
|
||||
chatThreads(
|
||||
"""Limit or page results."""
|
||||
paging: CursorPaging! = {first: 10}
|
||||
|
||||
"""Specify to filter the records returned."""
|
||||
filter: AgentChatThreadFilter! = {}
|
||||
|
||||
"""Specify to sort results."""
|
||||
sorting: [AgentChatThreadSort!]! = [{field: updatedAt, direction: DESC}]
|
||||
): AgentChatThreadConnection!
|
||||
agentTurns(agentId: UUID!): [AgentTurn!]!
|
||||
checkUserExists(email: String!, captchaToken: String): CheckUserExist!
|
||||
checkWorkspaceInviteHashIsValid(inviteHash: String!): WorkspaceInviteHashValid!
|
||||
@@ -3061,6 +3057,56 @@ input AgentIdInput {
|
||||
id: UUID!
|
||||
}
|
||||
|
||||
input AgentChatThreadFilter {
|
||||
and: [AgentChatThreadFilter!]
|
||||
or: [AgentChatThreadFilter!]
|
||||
id: UUIDFilterComparison
|
||||
updatedAt: DateFieldComparison
|
||||
}
|
||||
|
||||
input DateFieldComparison {
|
||||
is: Boolean
|
||||
isNot: Boolean
|
||||
eq: DateTime
|
||||
neq: DateTime
|
||||
gt: DateTime
|
||||
gte: DateTime
|
||||
lt: DateTime
|
||||
lte: DateTime
|
||||
in: [DateTime!]
|
||||
notIn: [DateTime!]
|
||||
between: DateFieldComparisonBetween
|
||||
notBetween: DateFieldComparisonBetween
|
||||
}
|
||||
|
||||
input DateFieldComparisonBetween {
|
||||
lower: DateTime!
|
||||
upper: DateTime!
|
||||
}
|
||||
|
||||
input AgentChatThreadSort {
|
||||
field: AgentChatThreadSortFields!
|
||||
direction: SortDirection!
|
||||
nulls: SortNulls
|
||||
}
|
||||
|
||||
enum AgentChatThreadSortFields {
|
||||
id
|
||||
updatedAt
|
||||
}
|
||||
|
||||
"""Sort Directions"""
|
||||
enum SortDirection {
|
||||
ASC
|
||||
DESC
|
||||
}
|
||||
|
||||
"""Sort Nulls Options"""
|
||||
enum SortNulls {
|
||||
NULLS_FIRST
|
||||
NULLS_LAST
|
||||
}
|
||||
|
||||
input EventLogQueryInput {
|
||||
table: EventLogTable!
|
||||
filters: EventLogFiltersInput
|
||||
@@ -3147,7 +3193,6 @@ type Mutation {
|
||||
updateView(id: String!, input: UpdateViewInput!): View!
|
||||
deleteView(id: String!): Boolean!
|
||||
destroyView(id: String!): Boolean!
|
||||
upsertViewWidget(input: UpsertViewWidgetInput!): View!
|
||||
createViewSort(input: CreateViewSortInput!): ViewSort!
|
||||
updateViewSort(input: UpdateViewSortInput!): ViewSort!
|
||||
deleteViewSort(input: DeleteViewSortInput!): Boolean!
|
||||
@@ -3197,7 +3242,6 @@ type Mutation {
|
||||
resetPageLayoutToDefault(id: String!): PageLayout!
|
||||
resetPageLayoutWidgetToDefault(id: String!): PageLayoutWidget!
|
||||
resetPageLayoutTabToDefault(id: String!): PageLayoutTab!
|
||||
updateOneApplicationVariable(key: String!, value: String!, applicationId: UUID!): Boolean!
|
||||
createPageLayoutWidget(input: CreatePageLayoutWidgetInput!): PageLayoutWidget!
|
||||
updatePageLayoutWidget(id: String!, input: UpdatePageLayoutWidgetInput!): PageLayoutWidget!
|
||||
destroyPageLayoutWidget(id: String!): Boolean!
|
||||
@@ -3247,10 +3291,6 @@ type Mutation {
|
||||
createChatThread: AgentChatThread!
|
||||
sendChatMessage(threadId: UUID!, text: String!, messageId: UUID!, browsingContext: JSON, modelId: String, fileIds: [UUID!]): SendChatMessageResult!
|
||||
stopAgentChatStream(threadId: UUID!): Boolean!
|
||||
renameChatThread(id: UUID!, title: String!): AgentChatThread!
|
||||
archiveChatThread(id: UUID!): AgentChatThread!
|
||||
unarchiveChatThread(id: UUID!): AgentChatThread!
|
||||
deleteChatThread(id: UUID!): Boolean!
|
||||
deleteQueuedChatMessage(messageId: UUID!): Boolean!
|
||||
createSkill(input: CreateSkillInput!): Skill!
|
||||
updateSkill(input: UpdateSkillInput!): Skill!
|
||||
@@ -3300,6 +3340,7 @@ type Mutation {
|
||||
installApplication(appRegistrationId: String!, version: String): Boolean!
|
||||
runWorkspaceMigration(workspaceMigration: WorkspaceMigrationInput!): Boolean!
|
||||
uninstallApplication(universalIdentifier: String!): Boolean!
|
||||
updateOneApplicationVariable(key: String!, value: String!, applicationId: UUID!): Boolean!
|
||||
createOIDCIdentityProvider(input: SetupOIDCSsoInput!): SetupSso!
|
||||
createSAMLIdentityProvider(input: SetupSAMLSsoInput!): SetupSso!
|
||||
deleteSSOIdentityProvider(input: DeleteSsoInput!): DeleteSso!
|
||||
@@ -3469,59 +3510,6 @@ input UpdateViewInput {
|
||||
shouldHideEmptyGroups: Boolean
|
||||
}
|
||||
|
||||
input UpsertViewWidgetInput {
|
||||
"""The id of the view widget (page layout widget)."""
|
||||
widgetId: UUID!
|
||||
|
||||
"""The view fields to upsert."""
|
||||
viewFields: [UpsertViewWidgetViewFieldInput!]
|
||||
|
||||
"""The view filters to upsert."""
|
||||
viewFilters: [UpsertViewWidgetViewFilterInput!]
|
||||
|
||||
"""The view filter groups to upsert."""
|
||||
viewFilterGroups: [UpsertViewWidgetViewFilterGroupInput!]
|
||||
|
||||
"""The view sorts to upsert."""
|
||||
viewSorts: [UpsertViewWidgetViewSortInput!]
|
||||
}
|
||||
|
||||
input UpsertViewWidgetViewFieldInput {
|
||||
"""The id of an existing view field to update."""
|
||||
viewFieldId: UUID
|
||||
|
||||
"""
|
||||
The field metadata id. Used to create a new view field when viewFieldId is not provided.
|
||||
"""
|
||||
fieldMetadataId: UUID
|
||||
isVisible: Boolean!
|
||||
position: Float!
|
||||
size: Float
|
||||
}
|
||||
|
||||
input UpsertViewWidgetViewFilterInput {
|
||||
id: UUID
|
||||
fieldMetadataId: UUID!
|
||||
operand: ViewFilterOperand = CONTAINS
|
||||
value: JSON!
|
||||
viewFilterGroupId: UUID
|
||||
positionInViewFilterGroup: Float
|
||||
subFieldName: String
|
||||
}
|
||||
|
||||
input UpsertViewWidgetViewFilterGroupInput {
|
||||
id: UUID
|
||||
parentViewFilterGroupId: UUID
|
||||
logicalOperator: ViewFilterGroupLogicalOperator = AND
|
||||
positionInViewFilterGroup: Float
|
||||
}
|
||||
|
||||
input UpsertViewWidgetViewSortInput {
|
||||
id: UUID
|
||||
fieldMetadataId: UUID!
|
||||
direction: ViewSortDirection = ASC
|
||||
}
|
||||
|
||||
input CreateViewSortInput {
|
||||
id: UUID
|
||||
fieldMetadataId: UUID!
|
||||
@@ -3783,12 +3771,12 @@ input CreateLogicFunctionFromSourceInput {
|
||||
name: String!
|
||||
description: String
|
||||
timeoutSeconds: Float
|
||||
toolInputSchema: JSON
|
||||
isTool: Boolean
|
||||
source: JSON
|
||||
cronTriggerSettings: JSON
|
||||
databaseEventTriggerSettings: JSON
|
||||
httpRouteTriggerSettings: JSON
|
||||
toolTriggerSettings: JSON
|
||||
workflowActionTriggerSettings: JSON
|
||||
}
|
||||
|
||||
input ExecuteOneLogicFunctionInput {
|
||||
@@ -3812,13 +3800,13 @@ input UpdateLogicFunctionFromSourceInputUpdates {
|
||||
description: String
|
||||
timeoutSeconds: Float
|
||||
sourceHandlerCode: String
|
||||
toolInputSchema: JSON
|
||||
handlerName: String
|
||||
sourceHandlerPath: String
|
||||
isTool: Boolean
|
||||
cronTriggerSettings: JSON
|
||||
databaseEventTriggerSettings: JSON
|
||||
httpRouteTriggerSettings: JSON
|
||||
toolTriggerSettings: JSON
|
||||
workflowActionTriggerSettings: JSON
|
||||
}
|
||||
|
||||
input CreateCommandMenuItemInput {
|
||||
|
||||
@@ -26,11 +26,11 @@ prod-run:
|
||||
prod-postgres-run:
|
||||
@docker run -d -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres --name twenty-postgres twenty-postgres:$(TAG)
|
||||
|
||||
prod-website-new-build:
|
||||
@cd ../.. && docker build -f ./packages/twenty-docker/twenty-website-new/Dockerfile --platform $(PLATFORM) --tag twenty-website-new:$(TAG) . && cd -
|
||||
prod-website-build:
|
||||
@cd ../.. && docker build -f ./packages/twenty-docker/twenty-website/Dockerfile --platform $(PLATFORM) --tag twenty-website:$(TAG) . && cd -
|
||||
|
||||
prod-website-new-run:
|
||||
@docker run -d -p 3000:3000 --name twenty-website-new twenty-website-new:$(TAG)
|
||||
prod-website-run:
|
||||
@docker run -d -p 3000:3000 --name twenty-website twenty-website:$(TAG)
|
||||
|
||||
# =============================================================================
|
||||
# Local Development Services
|
||||
|
||||
@@ -34,27 +34,25 @@ has_schema=$(PGPASSWORD=twenty psql -h localhost -U twenty -d default -tAc \
|
||||
"SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'core')")
|
||||
|
||||
if [ "$has_schema" = "f" ]; then
|
||||
step_start "Running initial database setup and migrations"
|
||||
yarn database:init:prod
|
||||
step_start "Running initial database setup"
|
||||
NODE_OPTIONS="--max-old-space-size=1500" node ./dist/database/scripts/setup-db.js
|
||||
step_done
|
||||
fi
|
||||
|
||||
step_start "Running migrations"
|
||||
yarn database:migrate:prod --force
|
||||
step_done
|
||||
|
||||
step_start "Flushing cache"
|
||||
if ! yarn command:prod cache:flush; then
|
||||
echo "Warning: Failed to flush cache before upgrade, but continuing startup..."
|
||||
fi
|
||||
yarn command:prod cache:flush
|
||||
step_done
|
||||
|
||||
step_start "Running upgrade"
|
||||
if ! yarn command:prod upgrade; then
|
||||
echo "Warning: Upgrade completed with errors. Some workspaces may not be fully migrated. Check logs for details."
|
||||
fi
|
||||
yarn command:prod upgrade
|
||||
step_done
|
||||
|
||||
step_start "Flushing cache"
|
||||
if ! yarn command:prod cache:flush; then
|
||||
echo "Warning: Failed to flush cache after upgrade, but continuing startup..."
|
||||
fi
|
||||
yarn command:prod cache:flush
|
||||
step_done
|
||||
|
||||
# Only seed on first boot — check if the dev workspace already exists
|
||||
|
||||
@@ -4,6 +4,6 @@ set -e
|
||||
echo "==> START Registering cron jobs"
|
||||
|
||||
cd /app/packages/twenty-server
|
||||
yarn command:prod cron:register:all
|
||||
yarn command:prod cron:register:all --dev-mode
|
||||
|
||||
echo "==> DONE"
|
||||
|
||||
@@ -11,12 +11,10 @@ COPY ./nx.json .
|
||||
COPY ./.yarn/releases /app/.yarn/releases
|
||||
COPY ./.yarn/patches /app/.yarn/patches
|
||||
COPY ./packages/twenty-oxlint-rules /app/packages/twenty-oxlint-rules
|
||||
COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/package.json
|
||||
COPY ./packages/twenty-website-new/package.json /app/packages/twenty-website-new/package.json
|
||||
|
||||
RUN yarn
|
||||
|
||||
COPY ./packages/twenty-shared /app/packages/twenty-shared
|
||||
COPY ./packages/twenty-website-new /app/packages/twenty-website-new
|
||||
RUN npx nx build twenty-website-new
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
FROM node:24-alpine AS twenty-website-build
|
||||
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./package.json .
|
||||
COPY ./yarn.lock .
|
||||
COPY ./.yarnrc.yml .
|
||||
COPY ./.yarn/releases /app/.yarn/releases
|
||||
COPY ./.yarn/patches /app/.yarn/patches
|
||||
COPY ./packages/twenty-oxlint-rules /app/packages/twenty-oxlint-rules
|
||||
COPY ./packages/twenty-ui/package.json /app/packages/twenty-ui/
|
||||
COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/
|
||||
COPY ./packages/twenty-website/package.json /app/packages/twenty-website/package.json
|
||||
|
||||
RUN yarn
|
||||
|
||||
ENV KEYSTATIC_GITHUB_CLIENT_ID="<fake build value>"
|
||||
ENV KEYSTATIC_GITHUB_CLIENT_SECRET="<fake build value>"
|
||||
ENV KEYSTATIC_SECRET="<fake build value>"
|
||||
ENV NEXT_PUBLIC_KEYSTATIC_GITHUB_APP_SLUG="<fake build value>"
|
||||
|
||||
COPY ./packages/twenty-ui /app/packages/twenty-ui
|
||||
COPY ./packages/twenty-website /app/packages/twenty-website
|
||||
RUN npx nx build twenty-website
|
||||
|
||||
FROM node:24-alpine AS twenty-website
|
||||
|
||||
WORKDIR /app/packages/twenty-website
|
||||
|
||||
COPY --from=twenty-website-build /app /app
|
||||
|
||||
WORKDIR /app/packages/twenty-website
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/twentyhq/twenty
|
||||
LABEL org.opencontainers.image.description="This image provides a consistent and reproducible environment for the website."
|
||||
|
||||
RUN chown -R 1000 /app
|
||||
|
||||
# Use non root user with uid 1000
|
||||
USER 1000
|
||||
|
||||
CMD ["/bin/sh", "-c", "npx nx start"]
|
||||
@@ -1,26 +1,8 @@
|
||||
# ===========================================================================
|
||||
# Dependency stages
|
||||
# Shared build stages (used by both targets)
|
||||
# ===========================================================================
|
||||
|
||||
FROM node:24-alpine AS front-deps
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./package.json ./yarn.lock ./.yarnrc.yml ./tsconfig.base.json ./nx.json /app/
|
||||
COPY ./.yarn/releases /app/.yarn/releases
|
||||
COPY ./.yarn/patches /app/.yarn/patches
|
||||
|
||||
COPY ./packages/twenty-ui/package.json /app/packages/twenty-ui/
|
||||
COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/
|
||||
COPY ./packages/twenty-front/package.json /app/packages/twenty-front/
|
||||
COPY ./packages/twenty-front-component-renderer/package.json /app/packages/twenty-front-component-renderer/
|
||||
COPY ./packages/twenty-sdk/package.json /app/packages/twenty-sdk/
|
||||
COPY ./packages/twenty-client-sdk/package.json /app/packages/twenty-client-sdk/
|
||||
|
||||
RUN yarn workspaces focus twenty twenty-front twenty-front-component-renderer twenty-ui twenty-shared twenty-sdk twenty-client-sdk && yarn cache clean && npx nx reset
|
||||
|
||||
|
||||
FROM node:24-alpine AS server-deps
|
||||
FROM node:24-alpine AS common-deps
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -31,16 +13,22 @@ COPY ./.yarn/patches /app/.yarn/patches
|
||||
COPY ./packages/twenty-emails/package.json /app/packages/twenty-emails/
|
||||
COPY ./packages/twenty-server/package.json /app/packages/twenty-server/
|
||||
COPY ./packages/twenty-server/patches /app/packages/twenty-server/patches
|
||||
COPY ./packages/twenty-ui/package.json /app/packages/twenty-ui/
|
||||
COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/
|
||||
COPY ./packages/twenty-front/package.json /app/packages/twenty-front/
|
||||
COPY ./packages/twenty-front-component-renderer/package.json /app/packages/twenty-front-component-renderer/
|
||||
COPY ./packages/twenty-sdk/package.json /app/packages/twenty-sdk/
|
||||
COPY ./packages/twenty-client-sdk/package.json /app/packages/twenty-client-sdk/
|
||||
|
||||
RUN yarn workspaces focus twenty twenty-server twenty-emails twenty-shared twenty-client-sdk && yarn cache clean && npx nx reset
|
||||
RUN yarn && yarn cache clean && npx nx reset
|
||||
|
||||
|
||||
FROM server-deps AS twenty-server-build
|
||||
FROM common-deps AS twenty-server-build
|
||||
|
||||
COPY ./packages/twenty-emails /app/packages/twenty-emails
|
||||
COPY ./packages/twenty-shared /app/packages/twenty-shared
|
||||
COPY ./packages/twenty-ui /app/packages/twenty-ui
|
||||
COPY ./packages/twenty-sdk /app/packages/twenty-sdk
|
||||
COPY ./packages/twenty-client-sdk /app/packages/twenty-client-sdk
|
||||
COPY ./packages/twenty-server /app/packages/twenty-server
|
||||
|
||||
@@ -56,10 +44,10 @@ RUN npx nx run twenty-server:build
|
||||
RUN find /app/packages/twenty-server/dist -name '*.d.ts' -delete \
|
||||
&& rm -rf /app/packages/twenty-server/dist/packages/twenty-server/test
|
||||
|
||||
RUN yarn workspaces focus --production twenty-emails twenty-shared twenty-client-sdk twenty-server
|
||||
RUN yarn workspaces focus --production twenty-emails twenty-shared twenty-sdk twenty-client-sdk twenty-server
|
||||
|
||||
|
||||
FROM front-deps AS twenty-front-build
|
||||
FROM common-deps AS twenty-front-build
|
||||
|
||||
COPY ./packages/twenty-front /app/packages/twenty-front
|
||||
COPY ./packages/twenty-front-component-renderer /app/packages/twenty-front-component-renderer
|
||||
@@ -111,8 +99,11 @@ COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-shared/package
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-shared/dist /app/packages/twenty-shared/dist
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-emails/package.json /app/packages/twenty-emails/
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-emails/dist /app/packages/twenty-emails/dist
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-sdk/package.json /app/packages/twenty-sdk/
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-client-sdk/package.json /app/packages/twenty-client-sdk/
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-client-sdk/dist /app/packages/twenty-client-sdk/dist
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-ui/package.json /app/packages/twenty-ui/
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-front/package.json /app/packages/twenty-front/
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/twentyhq/twenty
|
||||
LABEL org.opencontainers.image.description="Twenty server image (no frontend)."
|
||||
@@ -217,8 +208,11 @@ COPY --from=twenty-server-build /app/packages/twenty-shared/package.json /app/pa
|
||||
COPY --from=twenty-server-build /app/packages/twenty-shared/dist /app/packages/twenty-shared/dist
|
||||
COPY --from=twenty-server-build /app/packages/twenty-emails/package.json /app/packages/twenty-emails/
|
||||
COPY --from=twenty-server-build /app/packages/twenty-emails/dist /app/packages/twenty-emails/dist
|
||||
COPY --from=twenty-server-build /app/packages/twenty-sdk/package.json /app/packages/twenty-sdk/
|
||||
COPY --from=twenty-server-build /app/packages/twenty-client-sdk/package.json /app/packages/twenty-client-sdk/
|
||||
COPY --from=twenty-server-build /app/packages/twenty-client-sdk/dist /app/packages/twenty-client-sdk/dist
|
||||
COPY --from=twenty-server-build /app/packages/twenty-ui/package.json /app/packages/twenty-ui/
|
||||
COPY --from=twenty-server-build /app/packages/twenty-front/package.json /app/packages/twenty-front/
|
||||
|
||||
# Frontend static build
|
||||
COPY --from=twenty-front-build /app/packages/twenty-front/build /app/packages/twenty-server/dist/front
|
||||
|
||||
@@ -16,17 +16,9 @@ setup_and_migrate_db() {
|
||||
yarn database:init:prod
|
||||
fi
|
||||
|
||||
if ! yarn command:prod cache:flush; then
|
||||
echo "Warning: Failed to flush cache before upgrade, but continuing startup..."
|
||||
fi
|
||||
|
||||
if ! yarn command:prod upgrade; then
|
||||
echo "Warning: Upgrade completed with errors. Some workspaces may not be fully migrated. Check logs for details."
|
||||
fi
|
||||
|
||||
if ! yarn command:prod cache:flush; then
|
||||
echo "Warning: Failed to flush cache after upgrade, but continuing startup..."
|
||||
fi
|
||||
yarn command:prod cache:flush
|
||||
yarn command:prod upgrade
|
||||
yarn command:prod cache:flush
|
||||
|
||||
echo "Successfully migrated DB!"
|
||||
}
|
||||
|
||||
@@ -77,10 +77,14 @@ To deploy to Mintlify:
|
||||
3. Set subdirectory to `packages/twenty-docs`
|
||||
4. Mintlify will auto-deploy and generate search embeddings
|
||||
|
||||
## Status
|
||||
## Next Steps
|
||||
|
||||
This migration has been completed. The legacy `packages/twenty-website` package
|
||||
has been removed, and documentation now lives in `packages/twenty-docs`.
|
||||
1. **Manual Review** - Check for any component conversion issues
|
||||
2. **Fix Image Paths** - Verify all images render correctly
|
||||
3. **Test Navigation** - Ensure all internal links work
|
||||
4. **Deploy** - Push to production Mintlify
|
||||
5. **Update Helper Agent** - Verify searchArticles tool works with full content
|
||||
6. **Deprecate twenty-website docs** - Once migration is confirmed working
|
||||
|
||||
## Known Issues to Review
|
||||
|
||||
@@ -88,3 +92,4 @@ has been removed, and documentation now lives in `packages/twenty-docs`.
|
||||
- Some images may have incorrect paths
|
||||
- Custom styled components may need adjustment
|
||||
- Video embeds might need review
|
||||
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
---
|
||||
title: Connections
|
||||
description: Let your app act on a user's behalf in third-party services via OAuth.
|
||||
icon: 'plug'
|
||||
---
|
||||
|
||||
Connections are credentials a user holds for an external service (Linear, GitHub, Slack, ...). Your app declares **how** those credentials are obtained — a **connection provider** — and consumes them at runtime to make authenticated calls to the third-party API.
|
||||
|
||||
Today only OAuth 2.0 is supported. Future credential types (personal access tokens, API keys, basic auth) will plug into the same surface — apps already using `defineConnectionProvider({ type: 'oauth', ... })` won't need to migrate.
|
||||
|
||||
<AccordionGroup>
|
||||
|
||||
<Accordion title="defineConnectionProvider" description="Declare how your app's connections are obtained">
|
||||
|
||||
A connection provider describes the OAuth handshake your app needs. The user clicks "Add connection" in your app's settings, completes the provider's consent screen, and a `ConnectedAccount` row is created in their workspace.
|
||||
|
||||
A working setup needs **two files** — the connection provider, and a matching `serverVariables` declaration on `defineApplication` that holds the OAuth client credentials.
|
||||
|
||||
```ts src/connection-providers/linear-connection.ts
|
||||
import { defineConnectionProvider } from 'twenty-sdk/define';
|
||||
|
||||
export default defineConnectionProvider({
|
||||
universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
|
||||
name: 'linear',
|
||||
displayName: 'Linear',
|
||||
icon: 'IconBrandLinear',
|
||||
type: 'oauth',
|
||||
oauth: {
|
||||
authorizationEndpoint: 'https://linear.app/oauth/authorize',
|
||||
tokenEndpoint: 'https://api.linear.app/oauth/token',
|
||||
scopes: ['read', 'write'],
|
||||
// These must match keys in `defineApplication.serverVariables` below.
|
||||
clientIdVariable: 'LINEAR_CLIENT_ID',
|
||||
clientSecretVariable: 'LINEAR_CLIENT_SECRET',
|
||||
// Optional: defaults to 'json'. Some providers (Linear, Slack) want
|
||||
// 'form-urlencoded' for the token request.
|
||||
tokenRequestContentType: 'form-urlencoded',
|
||||
// Optional: defaults to true. Disable only if the provider rejects PKCE.
|
||||
usePkce: false,
|
||||
// Optional: extra query params on the authorize URL.
|
||||
// authorizationParams: { prompt: 'consent' },
|
||||
// Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
|
||||
// revokeEndpoint: 'https://example.com/oauth/revoke',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts src/application.config.ts
|
||||
import { defineApplication } from 'twenty-sdk/define';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '...',
|
||||
displayName: 'Linear',
|
||||
description: 'Connect Linear to Twenty.',
|
||||
defaultRoleUniversalIdentifier: '...',
|
||||
// OAuth client credentials live on the app registration (one OAuth app per
|
||||
// Twenty server, configured by the admin) — not per-workspace. Declare them
|
||||
// as serverVariables so the admin can fill them in once for all installs.
|
||||
serverVariables: {
|
||||
LINEAR_CLIENT_ID: {
|
||||
description: 'OAuth client ID from your Linear OAuth application.',
|
||||
isSecret: false,
|
||||
isRequired: true,
|
||||
},
|
||||
LINEAR_CLIENT_SECRET: {
|
||||
description: 'OAuth client secret from your Linear OAuth application.',
|
||||
isSecret: true,
|
||||
isRequired: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Key points:
|
||||
|
||||
- `name` is the unique identifier string used in `listConnections({ providerName })` (kebab-case, must match `^[a-z][a-z0-9-]*$`).
|
||||
- `displayName` shows in the per-app settings tab and in the AI tool list.
|
||||
- `clientIdVariable` / `clientSecretVariable` are **names**, not values — they must match keys declared in `defineApplication.serverVariables`. The actual `client_id` and `client_secret` are entered by the server admin through the app registration UI, never committed to your repo.
|
||||
- Use `serverVariables` (not `applicationVariables`) — OAuth credentials are server-wide and one OAuth app per Twenty server.
|
||||
- Until both `serverVariables` are filled in, the per-app settings tab shows a "needs server admin" hint and the "Add connection" button is disabled.
|
||||
- `type: 'oauth'` is the only supported value today. The discriminator is forward-compatible: future types (`'pat'`, `'api-key'`, ...) will add new sub-config blocks alongside `oauth`.
|
||||
|
||||
The OAuth callback URL your provider needs to whitelist is:
|
||||
|
||||
```
|
||||
https://<your-twenty-server>/apps/oauth/callback
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="listConnections / getConnection" description="Use connections from a logic function">
|
||||
|
||||
Inside a logic function handler, `listConnections({ providerName })` returns this app's `ConnectedAccount` rows for the given provider, with refreshed access tokens.
|
||||
|
||||
```ts src/logic-functions/handlers/create-linear-issue-handler.ts
|
||||
import { listConnections } from 'twenty-sdk/logic-function';
|
||||
|
||||
export const createLinearIssueHandler = async (input: {
|
||||
teamId?: string;
|
||||
title?: string;
|
||||
}) => {
|
||||
if (!input.teamId || !input.title) {
|
||||
return { success: false, error: 'teamId and title are required' };
|
||||
}
|
||||
|
||||
const connections = await listConnections({ providerName: 'linear' });
|
||||
|
||||
// Workspace-shared credentials win when present; fall back to the first
|
||||
// user-visibility one. For HTTP-route triggers you typically pick the
|
||||
// request user's connection via event.userWorkspaceId instead.
|
||||
const connection =
|
||||
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
|
||||
|
||||
if (!connection) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
'Linear is not connected. Open the app settings and click "Add connection".',
|
||||
};
|
||||
}
|
||||
|
||||
// Use connection.accessToken to call the third-party API.
|
||||
const response = await fetch('https://api.linear.app/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${connection.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
|
||||
}),
|
||||
});
|
||||
|
||||
return { success: response.ok };
|
||||
};
|
||||
```
|
||||
|
||||
Each connection has:
|
||||
|
||||
| Field | Description |
|
||||
| ----------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| `id` | Unique row id; pass to `getConnection(id)` to refetch a single one |
|
||||
| `visibility` | `'user'` (private to one workspace member) or `'workspace'` (shared with all members) |
|
||||
| `scopes` | OAuth permissions granted by the upstream provider (distinct from `visibility` — those are unrelated) |
|
||||
| `userWorkspaceId` | The owner's userWorkspace id — useful for picking "the request user's connection" in HTTP-route triggers |
|
||||
| `accessToken` | Fresh OAuth access token (refreshed automatically if expired) |
|
||||
| `name` / `handle` | The connection's display name (auto-derived at OAuth callback, user-renameable) |
|
||||
| `authFailedAt` | Set when the most recent refresh failed; the user must reconnect |
|
||||
|
||||
Key points:
|
||||
|
||||
- Pass `{ providerName }` to filter by provider; omit it to get all connections this app owns across all providers.
|
||||
- The server transparently refreshes the access token before returning. Your handler always sees a usable token (or `authFailedAt` set).
|
||||
- `getConnection(id)` is the single-row equivalent.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Per-user vs workspace-shared visibility" description="How users choose between private and shared credentials">
|
||||
|
||||
When a user clicks "Add connection," they're prompted to pick a visibility:
|
||||
|
||||
- **Just for me** — the credential is private to the connecting user. Any logic function called on their behalf (HTTP-route trigger with `isAuthRequired: true`) sees it; cron triggers and database events do not.
|
||||
- **Workspace shared** — any workspace member can use the credential. Cron / database triggers also see it, since they have no request user.
|
||||
|
||||
Use the right one for each handler:
|
||||
|
||||
```ts
|
||||
// HTTP-route trigger — prefer the request user's own connection.
|
||||
const conn =
|
||||
connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
|
||||
connections.find((c) => c.visibility === 'workspace');
|
||||
|
||||
// Cron trigger — no request user; only shared credentials are sensible.
|
||||
const conn = connections.find((c) => c.visibility === 'workspace');
|
||||
```
|
||||
|
||||
Multiple connections per (user, provider) are allowed, so the same user can hold "Personal Linear" and "Work Linear" side by side.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="One-time provider setup" description="Register your OAuth app with the third-party service">
|
||||
|
||||
For each connection provider, the server admin needs to register an OAuth app at the third party first.
|
||||
|
||||
1. Go to the provider's developer settings (e.g. https://linear.app/settings/api/applications/new).
|
||||
2. Set the **Redirect URI** to `<SERVER_URL>/apps/oauth/callback`.
|
||||
3. Copy the generated **Client ID** and **Client Secret**.
|
||||
4. Open the installed app in Twenty as a server admin → set the values on the corresponding `serverVariables`.
|
||||
5. Workspace members can then add connections from the per-app **Connections** section.
|
||||
|
||||
</Accordion>
|
||||
|
||||
</AccordionGroup>
|
||||
@@ -77,6 +77,7 @@ export default defineApplication({
|
||||
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
|
||||
displayName: 'My Twenty App',
|
||||
description: 'My first Twenty app',
|
||||
icon: 'IconWorld',
|
||||
applicationVariables: {
|
||||
DEFAULT_RECIPIENT_NAME: {
|
||||
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
|
||||
|
||||
@@ -15,7 +15,7 @@ Front components can render in two locations within Twenty:
|
||||
|
||||
## Basic example
|
||||
|
||||
The quickest way to see a front component in action is to register it with a **command menu item**. Use `defineCommandMenuItem` in a separate file to make the component appear as a quick-action button in the top-right corner of the page:
|
||||
The quickest way to see a front component in action is to register it as a **command**. Adding a `command` field with `isPinned: true` makes it appear as a quick-action button in the top-right corner of the page — no page layout needed:
|
||||
|
||||
```tsx src/front-components/hello-world.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
@@ -34,20 +34,14 @@ export default defineFrontComponent({
|
||||
name: 'hello-world',
|
||||
description: 'A simple front component',
|
||||
component: HelloWorld,
|
||||
});
|
||||
```
|
||||
|
||||
```ts src/command-menu-items/hello-world.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
|
||||
shortLabel: 'Hello',
|
||||
label: 'Hello World',
|
||||
icon: 'IconBolt',
|
||||
isPinned: true,
|
||||
availabilityType: 'GLOBAL',
|
||||
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
|
||||
command: {
|
||||
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
|
||||
shortLabel: 'Hello',
|
||||
label: 'Hello World',
|
||||
icon: 'IconBolt',
|
||||
isPinned: true,
|
||||
availabilityType: 'GLOBAL',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -68,6 +62,7 @@ Click it to render the component inline.
|
||||
| `name` | No | Display name |
|
||||
| `description` | No | Description of what the component does |
|
||||
| `isHeadless` | No | Set to `true` if the component has no visible UI (see below) |
|
||||
| `command` | No | Register the component as a command (see [command options](#command-options) below) |
|
||||
|
||||
## Placing a front component on a page
|
||||
|
||||
@@ -146,17 +141,11 @@ export default defineFrontComponent({
|
||||
description: 'Creates a task from the command menu',
|
||||
component: RunAction,
|
||||
isHeadless: true,
|
||||
});
|
||||
```
|
||||
|
||||
```ts src/command-menu-items/run-action.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
|
||||
label: 'Run my action',
|
||||
icon: 'IconPlayerPlay',
|
||||
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
|
||||
command: {
|
||||
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
|
||||
label: 'Run my action',
|
||||
icon: 'IconPlayerPlay',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -188,6 +177,11 @@ export default defineFrontComponent({
|
||||
description: 'Deletes a draft with confirmation',
|
||||
component: DeleteDraft,
|
||||
isHeadless: true,
|
||||
command: {
|
||||
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
|
||||
label: 'Delete draft',
|
||||
icon: 'IconTrash',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -229,8 +223,7 @@ Available hooks:
|
||||
| Hook | Returns | Description |
|
||||
|------|---------|-------------|
|
||||
| `useUserId()` | `string` or `null` | The current user's ID |
|
||||
| `useSelectedRecordIds()` | `string[]` | All selected record IDs (empty array if none selected) |
|
||||
| `useRecordId()` | `string` or `null` | **Deprecated.** Use `useSelectedRecordIds()` instead |
|
||||
| `useRecordId()` | `string` or `null` | The current record's ID (when placed on a record page) |
|
||||
| `useFrontComponentId()` | `string` | This component instance's ID |
|
||||
| `useFrontComponentExecutionContext(selector)` | varies | Access the full execution context with a selector function |
|
||||
|
||||
@@ -293,84 +286,14 @@ export default defineFrontComponent({
|
||||
});
|
||||
```
|
||||
|
||||
### Working with multiple records
|
||||
## Command options
|
||||
|
||||
Use `useSelectedRecordIds()` to handle multiple selected records. This is useful for bulk operations:
|
||||
|
||||
```tsx src/front-components/bulk-export.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { useSelectedRecordIds, numberOfSelectedRecords } from 'twenty-sdk/front-component';
|
||||
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
const BulkExport = () => {
|
||||
const selectedRecordIds = useSelectedRecordIds();
|
||||
|
||||
const handleExport = async () => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
for (const recordId of selectedRecordIds) {
|
||||
await client.mutation({
|
||||
updateTask: {
|
||||
__args: { id: recordId, data: { exported: true } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await enqueueSnackbar({
|
||||
message: `Exported ${selectedRecordIds.length} records`,
|
||||
variant: 'success',
|
||||
});
|
||||
|
||||
await closeSidePanel();
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<p>Export {selectedRecordIds.length} selected record(s)?</p>
|
||||
<button onClick={handleExport}>Export</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
|
||||
name: 'bulk-export',
|
||||
description: 'Export selected records',
|
||||
component: BulkExport,
|
||||
command: {
|
||||
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
|
||||
label: 'Bulk Export',
|
||||
availabilityType: 'RECORD_SELECTION',
|
||||
conditionalAvailabilityExpression: numberOfSelectedRecords > 0,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## defineCommandMenuItem
|
||||
|
||||
Use `defineCommandMenuItem` to register a front component in the command menu (Cmd+K). If `isPinned` is `true`, it also appears as a quick-action button in the top-right corner of the page.
|
||||
|
||||
```ts src/command-menu-items/open-dashboard.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
label: 'Open Dashboard',
|
||||
shortLabel: 'Dashboard',
|
||||
icon: 'IconLayoutDashboard',
|
||||
isPinned: true,
|
||||
availabilityType: 'GLOBAL',
|
||||
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
|
||||
});
|
||||
```
|
||||
Adding a `command` field to `defineFrontComponent` registers the component in the command menu (Cmd+K). If `isPinned` is `true`, it also appears as a quick-action button in the top-right corner of the page.
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `universalIdentifier` | Yes | Stable unique ID for the command |
|
||||
| `label` | Yes | Full label shown in the command menu (Cmd+K) |
|
||||
| `frontComponentUniversalIdentifier` | Yes | The `universalIdentifier` of the front component this command opens |
|
||||
| `shortLabel` | No | Shorter label displayed on the pinned quick-action button |
|
||||
| `icon` | No | Icon name displayed next to the label (e.g. `'IconBolt'`, `'IconSend'`) |
|
||||
| `isPinned` | No | When `true`, shows the command as a quick-action button in the top-right corner of the page |
|
||||
@@ -382,23 +305,30 @@ export default defineCommandMenuItem({
|
||||
|
||||
The `conditionalAvailabilityExpression` field lets you control when a command is visible based on the current page context. Import typed variables and operators from `twenty-sdk` to build expressions:
|
||||
|
||||
```ts src/command-menu-items/bulk-update.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
```tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
pageType,
|
||||
numberOfSelectedRecords,
|
||||
objectPermissions,
|
||||
everyEquals,
|
||||
isDefined,
|
||||
} from 'twenty-sdk/front-component';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
label: 'Bulk Update',
|
||||
availabilityType: 'RECORD_SELECTION',
|
||||
frontComponentUniversalIdentifier: '...',
|
||||
conditionalAvailabilityExpression: everyEquals(
|
||||
objectPermissions,
|
||||
'canUpdateObjectRecords',
|
||||
true,
|
||||
),
|
||||
name: 'bulk-action',
|
||||
component: BulkAction,
|
||||
command: {
|
||||
universalIdentifier: '...',
|
||||
label: 'Bulk Update',
|
||||
availabilityType: 'RECORD_SELECTION',
|
||||
conditionalAvailabilityExpression: everyEquals(
|
||||
objectPermissions,
|
||||
'canUpdateObjectRecords',
|
||||
true,
|
||||
),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -4,52 +4,48 @@ icon: "rocket"
|
||||
description: Create your first Twenty app in minutes.
|
||||
---
|
||||
|
||||
## What are apps?
|
||||
|
||||
Apps let you extend Twenty with custom objects, fields, logic functions, front components, AI skills, and more — all managed as code. Instead of configuring everything through the UI, you define your data model and logic in TypeScript and deploy it to one or more workspaces.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Node.js 24+** — [Download](https://nodejs.org/)
|
||||
- **Yarn 4** — bundled with Node via Corepack. Enable it: `corepack enable`
|
||||
- **Docker** — [Download](https://www.docker.com/products/docker-desktop/). Needed to run a local Twenty server. Skip if you already have Twenty running elsewhere.
|
||||
Before you begin, make sure the following is installed on your machine:
|
||||
|
||||
Building a Twenty app has three phases. The scaffolder collapses them into one happy-path command, but each phase is a separate concept — when something fails, knowing which phase you're in tells you what to fix.
|
||||
- **Node.js 24+** — [Download here](https://nodejs.org/)
|
||||
- **Yarn 4** — Comes with Node.js via Corepack. Enable it by running `corepack enable`
|
||||
- **Docker** — [Download here](https://www.docker.com/products/docker-desktop/). Required to run a local Twenty instance. Not needed if you already have a Twenty server running.
|
||||
|
||||
| Phase | What you do | Tool | Result |
|
||||
|---|---|---|---|
|
||||
| **1. Scaffold** | Generate the app's source code | `npx create-twenty-app` | A TypeScript project on disk |
|
||||
| **2. Run a server** | Start a Twenty server to sync into | Docker + `yarn twenty server` | A running Twenty instance |
|
||||
| **3. Sync** | Live-sync your code to the server | `yarn twenty dev` | Your changes appear in the UI |
|
||||
## Create your first app
|
||||
|
||||
---
|
||||
### Scaffold your app
|
||||
|
||||
## Phase 1 — Scaffold your project
|
||||
|
||||
Create a new app from the template:
|
||||
Open a terminal and run:
|
||||
|
||||
```bash filename="Terminal"
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
```
|
||||
|
||||
You'll be prompted for a name and description — press **Enter** for the defaults. This generates a TypeScript project in `my-twenty-app/` with a starter `application-config.ts`, a default role, a CI workflow, and an integration test.
|
||||
You will be prompted to enter a name and a description for your app. Press **Enter** to accept the defaults.
|
||||
|
||||
**After this phase:** you have an app's source code on your machine. It isn't running yet — that's Phase 2.
|
||||
This creates a new folder called `my-twenty-app` with everything you need.
|
||||
|
||||
---
|
||||
### Set up a local Twenty instance
|
||||
|
||||
## Phase 2 — Run a local Twenty server
|
||||
|
||||
Your app needs a Twenty server to sync into. The server is a full Twenty instance — UI, GraphQL API, PostgreSQL — running locally in Docker. Your local code uploads its definitions to that server, which makes them appear in the UI.
|
||||
|
||||
The scaffolder offers to start one for you:
|
||||
The scaffolder will ask:
|
||||
|
||||
> **Would you like to set up a local Twenty instance?**
|
||||
|
||||
- **Yes (recommended)** — pulls the `twentycrm/twenty-app-dev` Docker image and starts it on port `2020`. Make sure Docker is running first.
|
||||
- **No** — choose this if you already have a Twenty server you want to connect to. You can wire it up later with `yarn twenty remote add`.
|
||||
- **Type `yes`** (recommended) — This pulls the `twenty-app-dev` Docker image and starts a local Twenty server on port `2020`. Make sure Docker is running before you continue.
|
||||
- **Type `no`** — Choose this if you already have a Twenty server running locally.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/start-instance.png" alt="Should start local instance?" />
|
||||
</div>
|
||||
|
||||
Once the server is up, a browser opens for sign-in. Use the pre-seeded demo account:
|
||||
### Sign in to your workspace
|
||||
|
||||
Next, a browser window will open with the Twenty login page. Sign in with the pre-seeded demo account:
|
||||
|
||||
- **Email:** `tim@apple.dev`
|
||||
- **Password:** `tim@apple.dev`
|
||||
@@ -58,66 +54,50 @@ Once the server is up, a browser opens for sign-in. Use the pre-seeded demo acco
|
||||
<img src="/images/docs/developers/extends/apps/login.png" alt="Twenty login screen" />
|
||||
</div>
|
||||
|
||||
Click **Authorize** on the next screen — this gives the CLI access to your workspace.
|
||||
### Authorize the app
|
||||
|
||||
After you sign in, you will see an authorization screen. This lets your app interact with your workspace.
|
||||
|
||||
Click **Authorize** to continue.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/authorize.png" alt="Twenty CLI authorization screen" />
|
||||
</div>
|
||||
|
||||
Your terminal will confirm everything is set up.
|
||||
Once authorized, your terminal will confirm that everything is set up.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/scaffolded.png" alt="App scaffolded successfully" />
|
||||
</div>
|
||||
|
||||
**After this phase:** you have a running Twenty server at [http://localhost:2020](http://localhost:2020) with your CLI authorized to sync to it.
|
||||
### Start developing
|
||||
|
||||
<Note>
|
||||
If Docker isn't installed or running, the scaffolder will tell you the right start command for your OS. Once Docker is up, you can resume with `yarn twenty server start` — no need to re-scaffold.
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Sync your changes
|
||||
|
||||
This is the inner loop you'll spend most of your time in.
|
||||
Go into your new app folder and start the development server:
|
||||
|
||||
```bash filename="Terminal"
|
||||
cd my-twenty-app
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
This watches `src/`, rebuilds on every change, and syncs the result to the server. Edit a file, save, and within a second the server reflects the change. You'll see a live status panel in your terminal.
|
||||
This watches your source files, rebuilds on every change, and syncs your app to the local Twenty server automatically. You should see a live status panel in your terminal.
|
||||
|
||||
For more detailed output (build logs, sync requests, error traces), add `--verbose`.
|
||||
For more detailed output (build logs, sync requests, error traces), use the `--verbose` flag:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev --verbose
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Dev mode is only available on Twenty instances running in development (`NODE_ENV=development`). Production instances reject dev sync requests. Use `yarn twenty deploy` to deploy to production servers — see [Publishing Apps](/developers/extend/apps/publishing) for details.
|
||||
</Warning>
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/dev.png" alt="Dev mode terminal output" />
|
||||
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Dev mode terminal output" />
|
||||
</div>
|
||||
|
||||
Open [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer). You should see your app under **Your Apps**.
|
||||
#### One-shot sync with `yarn twenty dev --once`
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="Your Apps list showing My twenty app" />
|
||||
</div>
|
||||
|
||||
Click **My twenty app** to see its **application registration** — a server-level record describing your app (name, identifier, OAuth credentials, source). One registration can be installed across multiple workspaces on the same server.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="Application registration details" />
|
||||
</div>
|
||||
|
||||
Click **View installed app** to see the workspace install. The **About** tab shows version and management options.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Installed app" />
|
||||
</div>
|
||||
|
||||
**After this phase:** you have a live development loop. Edit any file in `src/` and it appears in the UI.
|
||||
|
||||
### One-shot sync for CI and scripts
|
||||
|
||||
Pass `--once` to run a single build + sync and exit — same pipeline, no watcher:
|
||||
If you do not want a watcher running in the background (for example in a CI pipeline, a git hook, or a scripted workflow), pass the `--once` flag. It runs the same pipeline as `yarn twenty dev` — build manifest, bundle files, upload, sync, regenerate the typed API client — but **exits as soon as the sync completes**:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev --once
|
||||
@@ -125,14 +105,38 @@ yarn twenty dev --once
|
||||
|
||||
| Command | Behavior | When to use |
|
||||
|---------|----------|-------------|
|
||||
| `yarn twenty dev` | Watches and re-syncs on every change. Runs until you stop it. | Interactive local development. |
|
||||
| `yarn twenty dev --once` | Single build + sync, exits `0` on success, `1` on failure. | CI, pre-commit hooks, AI agents, scripted workflows. |
|
||||
| `yarn twenty dev` | Watches your source files and re-syncs on every change. Keeps running until you stop it. | Interactive local development — you want the live status panel and instant feedback loop. |
|
||||
| `yarn twenty dev --once` | Performs a single build + sync, then exits with code `0` on success or `1` on failure. | Scripts, CI, pre-commit hooks, AI agents, and any non-interactive workflow. |
|
||||
|
||||
Both modes need a server in development mode and an authenticated remote.
|
||||
Both modes require a Twenty server running in development mode and an authenticated remote — the same prerequisites apply.
|
||||
|
||||
<Warning>
|
||||
Dev mode is only available on Twenty instances running in development (`NODE_ENV=development`). Production instances reject dev sync requests — use `yarn twenty deploy` to deploy to production servers. See [Publishing Apps](/developers/extend/apps/publishing).
|
||||
</Warning>
|
||||
### See your app in Twenty
|
||||
|
||||
Open [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer) in your browser. Navigate to **Settings > Apps** and select the **Developer** tab. You should see your app listed under **Your Apps**:
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="Your Apps list showing My twenty app" />
|
||||
</div>
|
||||
|
||||
Click on **My twenty app** to open its **application registration**. A registration is a server-level record that describes your app — its name, unique identifier, OAuth credentials, and source (local, npm, or tarball). It lives on the server, not inside any specific workspace. When you install an app into a workspace, Twenty creates a workspace-scoped **application** that points back to this registration. One registration can be installed across multiple workspaces on the same server.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="Application registration details" />
|
||||
</div>
|
||||
|
||||
Click **View installed app** to see the installed app. The **About** tab shows the current version and management options:
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Installed app — About tab" />
|
||||
</div>
|
||||
|
||||
Switch to the **Content** tab to see everything your app provides — objects, fields, logic functions, and agents:
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-4.png" alt="Installed app — Content tab" />
|
||||
</div>
|
||||
|
||||
You are all set! Edit any file in `src/` and the changes will be picked up automatically.
|
||||
|
||||
---
|
||||
|
||||
@@ -142,110 +146,115 @@ Apps are composed of **entities** — each defined as a TypeScript file with a s
|
||||
|
||||
| Entity | What it does |
|
||||
|--------|-------------|
|
||||
| **Objects & Fields** | Custom data models (Post Card, Invoice, etc.) with typed fields |
|
||||
| **Logic functions** | Server-side TypeScript triggered by HTTP routes, cron schedules, or database events |
|
||||
| **Objects & Fields** | Define custom data models (like Post Card, Invoice) with typed fields |
|
||||
| **Logic functions** | Server-side TypeScript functions triggered by HTTP routes, cron schedules, or database events |
|
||||
| **Front components** | React components that render inside Twenty's UI (side panel, widgets, command menu) |
|
||||
| **Skills & Agents** | AI capabilities — reusable instructions and autonomous assistants |
|
||||
| **Views & Navigation** | Pre-configured list views and sidebar menu items |
|
||||
| **Views & Navigation** | Pre-configured list views and sidebar menu items for your objects |
|
||||
| **Page layouts** | Custom record detail pages with tabs and widgets |
|
||||
|
||||
Full reference: [Building Apps](/developers/extend/apps/building).
|
||||
Head over to [Building Apps](/developers/extend/apps/building) for a detailed guide on each entity type.
|
||||
|
||||
---
|
||||
|
||||
## Project structure
|
||||
|
||||
The scaffolder generates the following file structure:
|
||||
|
||||
```text filename="my-twenty-app/"
|
||||
my-twenty-app/
|
||||
package.json
|
||||
yarn.lock
|
||||
.gitignore
|
||||
.nvmrc
|
||||
.yarnrc.yml
|
||||
.oxlintrc.json
|
||||
tsconfig.json
|
||||
tsconfig.spec.json # TypeScript config for tests
|
||||
vitest.config.ts # Vitest test runner configuration
|
||||
LLMS.md
|
||||
README.md
|
||||
.github/
|
||||
└── workflows/
|
||||
└── ci.yml # GitHub Actions CI workflow
|
||||
public/ # Public assets (images, fonts, etc.)
|
||||
src/
|
||||
application-config.ts # Required — your app's entry point
|
||||
default-role.ts # Permissions for logic functions
|
||||
constants/
|
||||
universal-identifiers.ts # Auto-generated UUIDs and metadata
|
||||
__tests__/
|
||||
setup-test.ts
|
||||
app-install.integration-test.ts
|
||||
.github/workflows/ci.yml # GitHub Actions
|
||||
public/ # Static assets
|
||||
vitest.config.ts # Test runner config
|
||||
tsconfig.json, tsconfig.spec.json
|
||||
.nvmrc, .yarnrc.yml, .oxlintrc.json
|
||||
README.md, LLMS.md
|
||||
├── application-config.ts # Required — main application configuration
|
||||
├── default-role.ts # Default role for logic functions
|
||||
├── constants/
|
||||
│ └── universal-identifiers.ts # Auto-generated UUIDs and app metadata
|
||||
└── __tests__/
|
||||
├── setup-test.ts # Test setup (server health check, config)
|
||||
└── app-install.integration-test.ts # Integration test
|
||||
```
|
||||
|
||||
| File / Folder | Purpose |
|
||||
|---|---|
|
||||
| `src/application-config.ts` | **Required.** The main configuration file for your app. |
|
||||
| `src/default-role.ts` | Default role controlling what your logic functions can access. |
|
||||
| `src/constants/universal-identifiers.ts` | Auto-generated UUIDs and metadata (display name, description). |
|
||||
| `src/__tests__/` | Integration tests (setup + example test). |
|
||||
| `public/` | Static assets (images, fonts) served with your app. |
|
||||
|
||||
### Starting from an example
|
||||
|
||||
Use `--example` to start with a more complete project (custom objects, fields, logic functions, front components):
|
||||
To start from a more complete example with custom objects, fields, logic functions, front components, and more, use the `--example` flag:
|
||||
|
||||
```bash filename="Terminal"
|
||||
npx create-twenty-app@latest my-twenty-app --example postcard
|
||||
```
|
||||
|
||||
Examples live in [twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples). You can also scaffold individual entities into an existing project with `yarn twenty add` — see [Building Apps](/developers/extend/apps/building#scaffolding-entities-with-yarn-twenty-add).
|
||||
Examples are sourced from the [twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples) directory on GitHub. You can also scaffold individual entities into an existing project with `yarn twenty add` (see [Building Apps](/developers/extend/apps/building#scaffolding-entities-with-yarn-twenty-add)).
|
||||
|
||||
---
|
||||
### Key files
|
||||
|
||||
## Managing the local server
|
||||
| File / Folder | Purpose |
|
||||
|---|---|
|
||||
| `package.json` | Declares your app name, version, and dependencies. Includes a `twenty` script so you can run `yarn twenty help` to see all commands. |
|
||||
| `src/application-config.ts` | **Required.** The main configuration file for your app. |
|
||||
| `src/default-role.ts` | Default role that controls what your logic functions can access. |
|
||||
| `src/constants/universal-identifiers.ts` | Auto-generated UUIDs and app metadata (display name, description). |
|
||||
| `src/__tests__/` | Integration tests (setup + example test). |
|
||||
| `public/` | Static assets (images, fonts) served with your app. |
|
||||
|
||||
Use `yarn twenty server` to control the local Twenty container:
|
||||
## Local development server
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `yarn twenty server start` | Start the server (pulls the image if needed) |
|
||||
The scaffolder already started a local Twenty server for you. To manage it later, use `yarn twenty server`:
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `yarn twenty server start` | Start the local server (pulls image if needed) |
|
||||
| `yarn twenty server start --port 3030` | Start on a custom port |
|
||||
| `yarn twenty server start --test` | Start a separate test instance on port 2021 |
|
||||
| `yarn twenty server stop` | Stop the server (preserves data) |
|
||||
| `yarn twenty server status` | Show URL, version, and login credentials |
|
||||
| `yarn twenty server status` | Show server status, URL, and credentials |
|
||||
| `yarn twenty server logs` | Stream server logs |
|
||||
| `yarn twenty server reset` | Wipe data and start fresh |
|
||||
| `yarn twenty server upgrade` | Pull the latest `twenty-app-dev` image |
|
||||
| `yarn twenty server upgrade 2.2.0` | Upgrade to a specific version |
|
||||
| `yarn twenty server logs --lines 100` | Show the last 100 log lines |
|
||||
| `yarn twenty server reset` | Delete all data and start fresh |
|
||||
|
||||
Data persists across restarts in two Docker volumes (`twenty-app-dev-data` for PostgreSQL, `twenty-app-dev-storage` for files). Use `reset` to wipe everything.
|
||||
Data is persisted across restarts in two Docker volumes (`twenty-app-dev-data` for PostgreSQL, `twenty-app-dev-storage` for files). Use `reset` to wipe everything and start fresh.
|
||||
|
||||
### Upgrading the server image
|
||||
### Running a test instance
|
||||
|
||||
`yarn twenty server upgrade` pulls the latest image, compares digests, and only recreates the container if anything actually changed. Volumes are preserved — only the container is replaced. If a new image was pulled and the container was running, the upgrade automatically starts a new container; run `yarn twenty server start` afterward to wait for it to become healthy.
|
||||
Pass `--test` to any `server` command to manage a second, fully isolated instance — useful for running integration tests or experimenting without touching your main dev data.
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty server upgrade # Latest
|
||||
yarn twenty server upgrade 2.2.0 # Specific version
|
||||
```
|
||||
|
||||
Verify the running version with `yarn twenty server status` (it shows the `APP_VERSION` baked into the container).
|
||||
|
||||
### Running a parallel test instance
|
||||
|
||||
Pass `--test` to any `server` command to manage a second, fully isolated instance — useful for integration tests or experiments without touching your main dev data:
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `yarn twenty server start --test` | Start the test instance (defaults to port 2021) |
|
||||
| `yarn twenty server stop --test` | Stop it |
|
||||
| `yarn twenty server status --test` | Show its status |
|
||||
| `yarn twenty server logs --test` | Stream its logs |
|
||||
| `yarn twenty server reset --test` | Wipe its data |
|
||||
| `yarn twenty server upgrade --test` | Upgrade its image |
|
||||
| `yarn twenty server stop --test` | Stop the test instance |
|
||||
| `yarn twenty server status --test` | Show test instance status, URL, and credentials |
|
||||
| `yarn twenty server logs --test` | Stream test instance logs |
|
||||
| `yarn twenty server reset --test` | Wipe test data and start fresh |
|
||||
|
||||
The test instance has its own container (`twenty-app-dev-test`), volumes (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`), and config — it runs alongside your main instance without conflicts. Combine `--test` with `--port` to override 2021.
|
||||
The test instance runs in its own Docker container (`twenty-app-dev-test`) with dedicated volumes (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) and config, so it can run in parallel with your main instance without conflicts. Combine `--test` with `--port` to override the default 2021.
|
||||
|
||||
---
|
||||
<Note>
|
||||
The server requires **Docker** to be running. If you see a "Docker not running" error, make sure Docker Desktop (or the Docker daemon) is started.
|
||||
</Note>
|
||||
|
||||
## Manual setup (without the scaffolder)
|
||||
|
||||
Skip the scaffolder if you're adding the SDK to an existing project:
|
||||
If you prefer to set things up yourself instead of using `create-twenty-app`, you can do it in two steps.
|
||||
|
||||
**1. Add `twenty-sdk` and `twenty-client-sdk` as dependencies:**
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add twenty-sdk twenty-client-sdk
|
||||
```
|
||||
|
||||
Add the script to `package.json`:
|
||||
**2. Add a `twenty` script to your `package.json`:**
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
@@ -255,19 +264,19 @@ Add the script to `package.json`:
|
||||
}
|
||||
```
|
||||
|
||||
You can now run `yarn twenty dev`, `yarn twenty server start`, and the rest.
|
||||
You can now run `yarn twenty dev`, `yarn twenty help`, and all other commands.
|
||||
|
||||
<Note>
|
||||
Don't install `twenty-sdk` globally — pin it per project so each app uses its own version.
|
||||
Do not install `twenty-sdk` globally. Always use it as a local project dependency so that each project can pin its own version.
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Docker errors** — Make sure Docker Desktop (or the daemon) is running before `yarn twenty server start`. The error message will show the right start command for your OS.
|
||||
- **Wrong Node version** — Need 24+. Check with `node -v`.
|
||||
- **Yarn 4 missing** — Run `corepack enable`.
|
||||
- **Dependencies broken** — `rm -rf node_modules && yarn install`.
|
||||
If you run into issues:
|
||||
|
||||
Stuck? Ask on the [Twenty Discord](https://discord.com/channels/1130383047699738754/1130386664812982322).
|
||||
- Make sure **Docker is running** before starting the scaffolder with a local instance.
|
||||
- Make sure you are using **Node.js 24+** (`node -v` to check).
|
||||
- Make sure **Corepack is enabled** (`corepack enable`) so Yarn 4 is available.
|
||||
- Try deleting `node_modules` and running `yarn install` again if dependencies seem broken.
|
||||
|
||||
Still stuck? Ask for help on the [Twenty Discord](https://discord.com/channels/1130383047699738754/1130386664812982322).
|
||||
|
||||
@@ -76,28 +76,6 @@ yarn twenty logs
|
||||
```
|
||||
</Note>
|
||||
|
||||
#### enqueueLogicFunctionExecution
|
||||
|
||||
Inside your logic function handler — regardless of trigger (HTTP route, cron, database event, tool, workflow action, or install hook) — you can enqueue **another** function that belongs to the **same application** on the worker queue. Import it from `twenty-sdk/logic-function`:
|
||||
|
||||
```ts
|
||||
import {
|
||||
enqueueLogicFunctionExecution,
|
||||
type RoutePayload,
|
||||
} from 'twenty-sdk/logic-function';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const { jobId, status } = await enqueueLogicFunctionExecution({
|
||||
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
|
||||
payload: { fromWebhook: true },
|
||||
});
|
||||
|
||||
return { accepted: true, jobId, status };
|
||||
};
|
||||
```
|
||||
|
||||
Pass **exactly one** of `name` or `universalIdentifier`.
|
||||
|
||||
#### Route trigger payload
|
||||
|
||||
When a route trigger invokes your logic function, it receives a `RoutePayload` object that follows the
|
||||
@@ -164,14 +142,11 @@ const handler = async (event: RoutePayload) => {
|
||||
Header names are normalized to lowercase. Access them using lowercase keys (e.g., `event.headers['content-type']`).
|
||||
</Note>
|
||||
|
||||
#### Exposing a function as an AI tool or workflow action
|
||||
#### Exposing a function as a tool
|
||||
|
||||
Logic functions can be exposed on two surfaces, each with its own trigger:
|
||||
Logic functions can be exposed as **tools** for AI agents and workflows. When marked as a tool, a function becomes discoverable by Twenty's AI features and can be used in workflow automations.
|
||||
|
||||
- **`toolTriggerSettings`** — makes the function discoverable by Twenty's AI features (chat, MCP, function calling). Uses standard JSON Schema, the format LLMs natively understand.
|
||||
- **`workflowActionTriggerSettings`** — makes the function appear as a step in the visual workflow builder. Uses Twenty's rich `InputSchema` so the builder can render proper field editors, variable pickers, and labels.
|
||||
|
||||
A function can opt into one, the other, or both. They sit alongside `cronTriggerSettings`, `databaseEventTriggerSettings`, and `httpRouteTriggerSettings` — same pattern, same shape.
|
||||
To mark a logic function as a tool, set `isTool: true`:
|
||||
|
||||
```ts src/logic-functions/enrich-company.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
@@ -201,33 +176,31 @@ export default defineLogicFunction({
|
||||
description: 'Enrich a company record with external data',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
toolTriggerSettings: {},
|
||||
isTool: true,
|
||||
});
|
||||
```
|
||||
|
||||
Key points:
|
||||
|
||||
- A function can mix surfaces — declare both `toolTriggerSettings` and `workflowActionTriggerSettings` to expose it in chat AND in the workflow builder.
|
||||
- `toolTriggerSettings.inputSchema` and `workflowActionTriggerSettings.inputSchema` are both optional. When omitted, the manifest builder infers them from the handler source code (JSON Schema for the AI tool, Twenty's `InputSchema` for the workflow action). Provide one explicitly when you want richer typing — for example, with `FieldMetadataType`-aware fields like `CURRENCY` or `RELATION` for the workflow builder, or with `description` fields the AI agent can read:
|
||||
- You can combine `isTool` with triggers — a function can be both a tool (callable by AI agents) and triggered by events at the same time.
|
||||
- **`toolInputSchema`** (optional): A JSON Schema object describing the parameters your function accepts. The schema is computed automatically from source code static analysis, but you can set it explicitly:
|
||||
|
||||
```ts
|
||||
export default defineLogicFunction({
|
||||
...,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
companyName: {
|
||||
type: 'string',
|
||||
description: 'The name of the company to enrich',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
description: 'The company website domain (optional)',
|
||||
},
|
||||
toolInputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
companyName: {
|
||||
type: 'string',
|
||||
description: 'The name of the company to enrich',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
description: 'The company website domain (optional)',
|
||||
},
|
||||
required: ['companyName'],
|
||||
},
|
||||
required: ['companyName'],
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -266,7 +239,7 @@ yarn twenty exec --postInstall
|
||||
```
|
||||
|
||||
Key points:
|
||||
- Post-install functions use `definePostInstallLogicFunction()` — a specialized variant that omits trigger settings (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `toolTriggerSettings`, `workflowActionTriggerSettings`).
|
||||
- Post-install functions use `definePostInstallLogicFunction()` — a specialized variant that omits trigger settings (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
|
||||
- The handler receives an `InstallPayload` with `{ previousVersion?: string; newVersion: string }` — `newVersion` is the version being installed, and `previousVersion` is the version that was previously installed (or `undefined` on a fresh install). Use these values to distinguish fresh installs from upgrades and to run version-specific migration logic.
|
||||
- **When the hook runs**: on fresh installs only, by default. Pass `shouldRunOnVersionUpgrade: true` if you also want it to run when the app is upgraded from a previous version. When omitted, the flag defaults to `false` and upgrades skip the hook.
|
||||
- **Execution model — async by default, sync opt-in**: the `shouldRunSynchronously` flag controls *how* post-install is executed.
|
||||
|
||||
@@ -77,39 +77,6 @@ Pre-release tags work as expected: bumping `1.0.0-rc.1` → `1.0.0-rc.2` is allo
|
||||
|
||||
{/* TODO: add screenshot of the Upgrade button */}
|
||||
|
||||
### Server version compatibility
|
||||
|
||||
If your app uses a feature introduced in a specific Twenty server version (for example, OAuth providers added in v2.3.0), you should declare the minimum server version your app requires using the `engines.twenty` field in `package.json`:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"name": "twenty-my-app",
|
||||
"version": "1.0.0",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
"twenty": ">=2.3.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The value is a standard [semver range](https://github.com/npm/node-semver#ranges). Common patterns:
|
||||
|
||||
| Range | Meaning |
|
||||
|-------|---------|
|
||||
| `>=2.3.0` | Any server from 2.3.0 onward |
|
||||
| `>=2.3.0 <3.0.0` | 2.3.0 or later, but below the next major |
|
||||
| `^2.3.0` | Same as `>=2.3.0 <3.0.0` |
|
||||
|
||||
**What happens at deploy and install time:**
|
||||
|
||||
- If `engines.twenty` is set and the target server's version does not satisfy the range, the deploy (tarball upload) or install is rejected with a `SERVER_VERSION_INCOMPATIBLE` error and a message indicating both the required range and the actual server version.
|
||||
- If `engines.twenty` is **not set**, the app is accepted on any server version (backward-compatible with existing apps).
|
||||
- If the server has no `APP_VERSION` configured, the check is skipped.
|
||||
|
||||
<Note>
|
||||
The server is the authoritative check — it validates `engines.twenty` on both tarball upload and workspace install. If you deploy a tarball out-of-band or install from the marketplace, the server still enforces compatibility.
|
||||
</Note>
|
||||
|
||||
## Automated CI/CD (scaffolded workflows)
|
||||
|
||||
Apps generated with `create-twenty-app` ship with two GitHub Actions workflows out of the box, under `.github/workflows/`. They are ready to run as soon as you push the repo to GitHub — no extra setup is needed for CI, and CD only requires a single secret.
|
||||
@@ -198,14 +165,6 @@ export default defineApplication({
|
||||
|
||||
See the [defineApplication accordion](/developers/extend/apps/building#defineentity-functions) in the Building Apps page for the full list of marketplace fields (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl`, etc.).
|
||||
|
||||
#### Recommended screenshot dimensions
|
||||
|
||||
The marketplace renders `screenshots` in a fixed `8:5` container (for example, `1600×1000 px`).
|
||||
|
||||
<Note>
|
||||
Screenshots of any aspect ratio are displayed in full and are never cropped, but anything significantly taller or narrower than `8:5` will show empty bands on the sides.
|
||||
</Note>
|
||||
|
||||
### Publish
|
||||
|
||||
```bash filename="Terminal"
|
||||
@@ -225,9 +184,9 @@ The Twenty server syncs its marketplace catalog from the npm registry **every ho
|
||||
You can trigger the sync immediately instead of waiting:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty server catalog-sync
|
||||
yarn twenty catalog-sync
|
||||
# To target a specific remote:
|
||||
# yarn twenty server catalog-sync --remote production
|
||||
# yarn twenty catalog-sync --remote production
|
||||
```
|
||||
|
||||
The metadata shown in the marketplace comes from your `defineApplication()` config — fields like `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl`, and `termsUrl`.
|
||||
|
||||
@@ -3866,10 +3866,10 @@
|
||||
"language": "ro",
|
||||
"tabs": [
|
||||
{
|
||||
"tab": "Începeți",
|
||||
"tab": "Getting Started",
|
||||
"groups": [
|
||||
{
|
||||
"group": "Bun venit",
|
||||
"group": "Welcome",
|
||||
"pages": [
|
||||
"l/ro/getting-started/introduction",
|
||||
"l/ro/getting-started/key-features",
|
||||
@@ -3877,7 +3877,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Concepte cheie",
|
||||
"group": "Core Concepts",
|
||||
"pages": [
|
||||
"l/ro/getting-started/core-concepts/data-model",
|
||||
"l/ro/getting-started/core-concepts/layout",
|
||||
@@ -3895,7 +3895,7 @@
|
||||
"tab": "User Guide",
|
||||
"groups": [
|
||||
{
|
||||
"group": "Prezentare generală",
|
||||
"group": "Overview",
|
||||
"pages": [
|
||||
"l/ro/user-guide/introduction"
|
||||
]
|
||||
@@ -3906,7 +3906,7 @@
|
||||
"pages": [
|
||||
"l/ro/user-guide/data-model/overview",
|
||||
{
|
||||
"group": "Referință",
|
||||
"group": "Reference",
|
||||
"pages": [
|
||||
"l/ro/user-guide/data-model/capabilities/objects",
|
||||
"l/ro/user-guide/data-model/capabilities/fields",
|
||||
@@ -3932,7 +3932,7 @@
|
||||
"pages": [
|
||||
"l/ro/user-guide/data-migration/overview",
|
||||
{
|
||||
"group": "Referință",
|
||||
"group": "Reference",
|
||||
"pages": [
|
||||
"l/ro/user-guide/data-migration/capabilities/file-formats",
|
||||
"l/ro/user-guide/data-migration/capabilities/field-mapping",
|
||||
@@ -3964,7 +3964,7 @@
|
||||
"pages": [
|
||||
"l/ro/user-guide/calendar-emails/overview",
|
||||
{
|
||||
"group": "Referință",
|
||||
"group": "Reference",
|
||||
"pages": [
|
||||
"l/ro/user-guide/calendar-emails/capabilities/mailbox",
|
||||
"l/ro/user-guide/calendar-emails/capabilities/calendar"
|
||||
@@ -3989,7 +3989,7 @@
|
||||
"pages": [
|
||||
"l/ro/user-guide/workflows/overview",
|
||||
{
|
||||
"group": "Referință",
|
||||
"group": "Reference",
|
||||
"pages": [
|
||||
"l/ro/user-guide/workflows/capabilities/workflow-triggers",
|
||||
"l/ro/user-guide/workflows/capabilities/workflow-actions",
|
||||
@@ -4052,7 +4052,7 @@
|
||||
"pages": [
|
||||
"l/ro/user-guide/ai/overview",
|
||||
{
|
||||
"group": "Referință",
|
||||
"group": "Reference",
|
||||
"pages": [
|
||||
"l/ro/user-guide/ai/capabilities/ai-chatbot",
|
||||
"l/ro/user-guide/ai/capabilities/ai-agents",
|
||||
@@ -4068,16 +4068,16 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Aspect",
|
||||
"group": "Layout",
|
||||
"icon": "table-columns",
|
||||
"pages": [
|
||||
"l/ro/user-guide/layout/overview",
|
||||
{
|
||||
"group": "Referință",
|
||||
"group": "Reference",
|
||||
"pages": [
|
||||
"l/ro/user-guide/layout/capabilities/navigation",
|
||||
{
|
||||
"group": "Vizualizări",
|
||||
"group": "Views",
|
||||
"pages": [
|
||||
"l/ro/user-guide/views-pipelines/capabilities/table-views",
|
||||
"l/ro/user-guide/views-pipelines/capabilities/kanban-views",
|
||||
@@ -4091,7 +4091,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Ghiduri practice",
|
||||
"group": "How-Tos",
|
||||
"pages": [
|
||||
"l/ro/user-guide/views-pipelines/how-tos/create-a-table-view-with-grouping",
|
||||
"l/ro/user-guide/views-pipelines/how-tos/create-a-kanban-view-for-projects",
|
||||
@@ -4110,7 +4110,7 @@
|
||||
"pages": [
|
||||
"l/ro/user-guide/dashboards/overview",
|
||||
{
|
||||
"group": "Referință",
|
||||
"group": "Reference",
|
||||
"pages": [
|
||||
"l/ro/user-guide/dashboards/capabilities/dashboards",
|
||||
"l/ro/user-guide/dashboards/capabilities/widgets",
|
||||
@@ -4132,7 +4132,7 @@
|
||||
"pages": [
|
||||
"l/ro/user-guide/permissions-access/overview",
|
||||
{
|
||||
"group": "Referință",
|
||||
"group": "Reference",
|
||||
"pages": [
|
||||
"l/ro/user-guide/permissions-access/capabilities/permissions",
|
||||
"l/ro/user-guide/permissions-access/capabilities/sso-configuration"
|
||||
@@ -4152,7 +4152,7 @@
|
||||
"pages": [
|
||||
"l/ro/user-guide/billing/overview",
|
||||
{
|
||||
"group": "Referință",
|
||||
"group": "Reference",
|
||||
"pages": [
|
||||
"l/ro/user-guide/billing/capabilities/pricing-plans",
|
||||
"l/ro/user-guide/billing/capabilities/credits"
|
||||
@@ -4172,7 +4172,7 @@
|
||||
"pages": [
|
||||
"l/ro/user-guide/settings/overview",
|
||||
{
|
||||
"group": "Referință",
|
||||
"group": "Reference",
|
||||
"pages": [
|
||||
"l/ro/user-guide/settings/capabilities/workspace-settings",
|
||||
"l/ro/user-guide/settings/capabilities/member-management",
|
||||
@@ -4196,7 +4196,7 @@
|
||||
"tab": "Dezvoltatori",
|
||||
"groups": [
|
||||
{
|
||||
"group": "Prezentare generală",
|
||||
"group": "Overview",
|
||||
"pages": [
|
||||
"l/ro/developers/introduction"
|
||||
]
|
||||
|
||||
@@ -35,8 +35,14 @@ Everything is detected via AST analysis at build time — no config files, no re
|
||||
|
||||
## The developer experience
|
||||
|
||||
You write your app as a TypeScript project on your machine. The CLI watches your source files and live-syncs them to a running Twenty server — edit a file, see the change in the UI within a second. The typed API client regenerates automatically when the schema changes. When you're ready, `yarn twenty deploy` pushes to a production server, or `yarn twenty publish` lists your app on npm and the Twenty marketplace.
|
||||
```bash
|
||||
npx create-twenty-app@latest my-app
|
||||
cd my-app
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
`yarn twenty dev` watches your source files, rebuilds on change, and live-syncs to a local Twenty instance. The typed API client regenerates automatically when the schema changes. When you're ready, `yarn twenty deploy` pushes to production. Apps can also be published to npm and listed in the Twenty marketplace.
|
||||
|
||||
<Card title="Build your first app" icon="arrow-right" href="/developers/extend/apps/getting-started">
|
||||
Three-phase walkthrough — scaffold, run a local server, sync your changes.
|
||||
Full walkthrough — scaffold, develop, deploy.
|
||||
</Card>
|
||||
|
||||
|
Before Width: | Height: | Size: 773 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 724 KiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 662 KiB After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 241 KiB |
|
Before Width: | Height: | Size: 108 KiB |
@@ -1,193 +0,0 @@
|
||||
---
|
||||
title: Verbindungen
|
||||
description: Ermöglichen Sie Ihrer App, im Namen eines Benutzers über OAuth in Diensten von Drittanbietern zu handeln.
|
||||
icon: plug
|
||||
---
|
||||
|
||||
Verbindungen sind Anmeldedaten, die ein Benutzer für einen externen Dienst besitzt (Linear, GitHub, Slack, ...). Ihre App legt fest, **wie** diese Anmeldedaten bezogen werden — ein **Verbindungsanbieter** — und verwendet sie zur Laufzeit, um authentifizierte Aufrufe an die Drittanbieter-API zu tätigen.
|
||||
|
||||
Derzeit wird nur OAuth 2.0 unterstützt. Zukünftige Anmeldedatentypen (Personal Access Tokens, API-Schlüssel, Basic Auth) werden in dieselbe Oberfläche integriert — Apps, die bereits `defineConnectionProvider({ type: 'oauth', ... })` müssen nicht migriert werden.
|
||||
|
||||
<AccordionGroup>
|
||||
|
||||
<Accordion title="defineConnectionProvider" description="Legen Sie fest, wie die Verbindungen Ihrer App bezogen werden">
|
||||
|
||||
Ein Verbindungsanbieter beschreibt den OAuth-Handshake, den Ihre App benötigt. Der Benutzer klickt in den Einstellungen Ihrer App auf "Verbindung hinzufügen", schließt den Zustimmungsbildschirm des Anbieters ab, und in seinem Arbeitsbereich wird eine `ConnectedAccount`-Zeile erstellt.
|
||||
|
||||
Eine funktionierende Einrichtung benötigt **zwei Dateien** — den Verbindungsanbieter und eine passende `serverVariables`-Deklaration in `defineApplication`, die die OAuth-Client-Anmeldedaten enthält.
|
||||
|
||||
```ts src/connection-providers/linear-connection.ts
|
||||
import { defineConnectionProvider } from 'twenty-sdk/define';
|
||||
|
||||
export default defineConnectionProvider({
|
||||
universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
|
||||
name: 'linear',
|
||||
displayName: 'Linear',
|
||||
icon: 'IconBrandLinear',
|
||||
type: 'oauth',
|
||||
oauth: {
|
||||
authorizationEndpoint: 'https://linear.app/oauth/authorize',
|
||||
tokenEndpoint: 'https://api.linear.app/oauth/token',
|
||||
scopes: ['read', 'write'],
|
||||
// These must match keys in `defineApplication.serverVariables` below.
|
||||
clientIdVariable: 'LINEAR_CLIENT_ID',
|
||||
clientSecretVariable: 'LINEAR_CLIENT_SECRET',
|
||||
// Optional: defaults to 'json'. Some providers (Linear, Slack) want
|
||||
// 'form-urlencoded' for the token request.
|
||||
tokenRequestContentType: 'form-urlencoded',
|
||||
// Optional: defaults to true. Disable only if the provider rejects PKCE.
|
||||
usePkce: false,
|
||||
// Optional: extra query params on the authorize URL.
|
||||
// authorizationParams: { prompt: 'consent' },
|
||||
// Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
|
||||
// revokeEndpoint: 'https://example.com/oauth/revoke',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts src/application.config.ts
|
||||
import { defineApplication } from 'twenty-sdk/define';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '...',
|
||||
displayName: 'Linear',
|
||||
description: 'Connect Linear to Twenty.',
|
||||
defaultRoleUniversalIdentifier: '...',
|
||||
// OAuth client credentials live on the app registration (one OAuth app per
|
||||
// Twenty server, configured by the admin) — not per-workspace. Declare them
|
||||
// as serverVariables so the admin can fill them in once for all installs.
|
||||
serverVariables: {
|
||||
LINEAR_CLIENT_ID: {
|
||||
description: 'OAuth client ID from your Linear OAuth application.',
|
||||
isSecret: false,
|
||||
isRequired: true,
|
||||
},
|
||||
LINEAR_CLIENT_SECRET: {
|
||||
description: 'OAuth client secret from your Linear OAuth application.',
|
||||
isSecret: true,
|
||||
isRequired: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Hauptpunkte:
|
||||
|
||||
* `name` ist die eindeutige Bezeichner-Zeichenfolge, die in `listConnections({ providerName })` verwendet wird (kebab-case, muss `^[a-z][a-z0-9-]*$` entsprechen).
|
||||
* `displayName` wird im Einstellungs-Tab der jeweiligen App und in der KI-Toolliste angezeigt.
|
||||
* `clientIdVariable` / `clientSecretVariable` sind **Namen**, keine Werte — sie müssen den in `defineApplication.serverVariables` deklarierten Schlüsseln entsprechen. Die tatsächlichen `client_id` und `client_secret` werden vom Serveradministrator über die App-Registrierungsoberfläche eingegeben und niemals in Ihr Repository eingecheckt.
|
||||
* Verwenden Sie `serverVariables` (nicht `applicationVariables`) — OAuth-Anmeldedaten gelten serverweit und es gibt eine OAuth-App pro Twenty-Server.
|
||||
* Solange beide `serverVariables` nicht ausgefüllt sind, zeigt der Einstellungs-Tab pro App den Hinweis "Benötigt Server-Admin" an und der Button "Verbindung hinzufügen" ist deaktiviert.
|
||||
* `type: 'oauth'` ist derzeit der einzige unterstützte Wert. Der Diskriminator ist vorwärtskompatibel: zukünftige Typen (`'pat'`, `'api-key'`, ...) werden neue Unterkonfigurationsblöcke neben `oauth` hinzufügen.
|
||||
|
||||
Die OAuth-Callback-URL, die Ihr Anbieter auf die Whitelist setzen muss, lautet:
|
||||
|
||||
```
|
||||
https://<your-twenty-server>/apps/oauth/callback
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="listConnections / getConnection" description="Verbindungen aus einer Logikfunktion verwenden">
|
||||
|
||||
Innerhalb eines Logikfunktions-Handlers gibt `listConnections({ providerName })` die `ConnectedAccount`-Zeilen dieser App für den angegebenen Anbieter zurück, mit aktualisierten Zugriffstoken.
|
||||
|
||||
```ts src/logic-functions/handlers/create-linear-issue-handler.ts
|
||||
import { listConnections } from 'twenty-sdk/logic-function';
|
||||
|
||||
export const createLinearIssueHandler = async (input: {
|
||||
teamId?: string;
|
||||
title?: string;
|
||||
}) => {
|
||||
if (!input.teamId || !input.title) {
|
||||
return { success: false, error: 'teamId and title are required' };
|
||||
}
|
||||
|
||||
const connections = await listConnections({ providerName: 'linear' });
|
||||
|
||||
// Workspace-shared credentials win when present; fall back to the first
|
||||
// user-visibility one. For HTTP-route triggers you typically pick the
|
||||
// request user's connection via event.userWorkspaceId instead.
|
||||
const connection =
|
||||
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
|
||||
|
||||
if (!connection) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
'Linear is not connected. Open the app settings and click "Add connection".',
|
||||
};
|
||||
}
|
||||
|
||||
// Use connection.accessToken to call the third-party API.
|
||||
const response = await fetch('https://api.linear.app/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${connection.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
|
||||
}),
|
||||
});
|
||||
|
||||
return { success: response.ok };
|
||||
};
|
||||
```
|
||||
|
||||
Jede Verbindung hat:
|
||||
|
||||
| Feld | Beschreibung |
|
||||
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `id` | Eindeutige Zeilen-ID; an `getConnection(id)` übergeben, um eine einzelne Verbindung erneut abzurufen |
|
||||
| `sichtbarkeit` | `'user'` (privat für ein Mitglied des Arbeitsbereichs) oder `'workspace'` (mit allen Mitgliedern geteilt) |
|
||||
| `geltungsbereiche` | Vom Upstream-Anbieter gewährte OAuth-Berechtigungen (unabhängig von `visibility` — diese sind nicht miteinander verknüpft) |
|
||||
| `userWorkspaceId` | Die userWorkspace-ID des Eigentümers — nützlich, um "die Verbindung des anfragenden Benutzers" in HTTP-Routen-Triggern auszuwählen |
|
||||
| `accessToken` | Frisches OAuth-Zugriffstoken (wird bei Ablauf automatisch erneuert) |
|
||||
| `name` / `handle` | Anzeigename der Verbindung (automatisch beim OAuth-Callback abgeleitet, vom Benutzer umbenennbar) |
|
||||
| `authFailedAt` | Gesetzt, wenn die jüngste Aktualisierung fehlgeschlagen ist; der Benutzer muss die Verbindung erneut herstellen |
|
||||
|
||||
Hauptpunkte:
|
||||
|
||||
* Übergeben Sie `{ providerName }`, um nach Anbieter zu filtern; lassen Sie es weg, um alle Verbindungen dieser App über alle Anbieter hinweg zu erhalten.
|
||||
* Der Server aktualisiert das Zugriffstoken vor der Rückgabe transparent. Ihr Handler sieht stets ein verwendbares Token (oder `authFailedAt` ist gesetzt).
|
||||
* `getConnection(id)` ist das Pendant für eine einzelne Zeile.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Sichtbarkeit: pro Benutzer vs. im Arbeitsbereich geteilt" description="Wie Benutzer zwischen privaten und geteilten Anmeldedaten wählen">
|
||||
|
||||
Wenn ein Benutzer auf "Verbindung hinzufügen" klickt, wird er aufgefordert, eine Sichtbarkeit auszuwählen:
|
||||
|
||||
* **Nur für mich** — die Anmeldedaten sind für den sich verbindenden Benutzer privat. Jede Logikfunktion, die in seinem/ihrem Auftrag aufgerufen wird (HTTP-Routen-Trigger mit `isAuthRequired: true`), sieht sie; Cron-Trigger und Datenbankereignisse nicht.
|
||||
* **Im Arbeitsbereich geteilt** — jedes Arbeitsbereichsmitglied kann die Anmeldedaten verwenden. Cron-/Datenbank-Trigger sehen sie ebenfalls, da sie keinen anfragenden Benutzer haben.
|
||||
|
||||
Verwenden Sie für jeden Handler die richtige Option:
|
||||
|
||||
```ts
|
||||
// HTTP-route trigger — prefer the request user's own connection.
|
||||
const conn =
|
||||
connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
|
||||
connections.find((c) => c.visibility === 'workspace');
|
||||
|
||||
// Cron trigger — no request user; only shared credentials are sensible.
|
||||
const conn = connections.find((c) => c.visibility === 'workspace');
|
||||
```
|
||||
|
||||
Mehrere Verbindungen pro (Benutzer, Anbieter) sind erlaubt, sodass derselbe Benutzer "Persönliches Linear" und "Arbeits-Linear" nebeneinander haben kann.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Einmalige Anbietereinrichtung" description="Registrieren Sie Ihre OAuth-App beim Drittanbieterdienst">
|
||||
|
||||
Für jeden Verbindungsanbieter muss der Serveradministrator zunächst eine OAuth-App beim Drittanbieter registrieren.
|
||||
|
||||
1. Gehen Sie zu den Entwickler-Einstellungen des Anbieters (z. B. https://linear.app/settings/api/applications/new).
|
||||
2. Setzen Sie die **Redirect-URI** auf `\<SERVER_URL>/apps/oauth/callback`.
|
||||
3. Kopieren Sie die generierte **Client ID** und das **Client Secret**.
|
||||
4. Öffnen Sie die installierte App in Twenty als Serveradministrator → setzen Sie die Werte in den entsprechenden `serverVariables`.
|
||||
5. Mitglieder des Arbeitsbereichs können dann Verbindungen im **Verbindungen**-Abschnitt der jeweiligen App hinzufügen.
|
||||
|
||||
</Accordion>
|
||||
|
||||
</AccordionGroup>
|
||||
@@ -77,6 +77,7 @@ export default defineApplication({
|
||||
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
|
||||
displayName: 'My Twenty App',
|
||||
description: 'My first Twenty app',
|
||||
icon: 'IconWorld',
|
||||
applicationVariables: {
|
||||
DEFAULT_RECIPIENT_NAME: {
|
||||
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
|
||||
|
||||
@@ -15,7 +15,7 @@ Front-Komponenten können an zwei Stellen innerhalb von Twenty gerendert werden:
|
||||
|
||||
## Einfaches Beispiel
|
||||
|
||||
Der schnellste Weg, eine Front-Komponente in Aktion zu sehen, ist, sie als **Befehlsmenüeintrag** zu registrieren. Verwende `defineCommandMenuItem` in einer separaten Datei, damit die Komponente als Schnellaktionsschaltfläche oben rechts auf der Seite erscheint:
|
||||
Der schnellste Weg, eine Front-Komponente in Aktion zu sehen, ist, sie als Befehl zu registrieren. Das Hinzufügen eines `command`-Felds mit `isPinned: true` lässt sie als Schnellaktionsschaltfläche oben rechts auf der Seite erscheinen — kein Seitenlayout erforderlich:
|
||||
|
||||
```tsx src/front-components/hello-world.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
@@ -34,20 +34,14 @@ export default defineFrontComponent({
|
||||
name: 'hello-world',
|
||||
description: 'A simple front component',
|
||||
component: HelloWorld,
|
||||
});
|
||||
```
|
||||
|
||||
```ts src/command-menu-items/hello-world.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
|
||||
shortLabel: 'Hello',
|
||||
label: 'Hello World',
|
||||
icon: 'IconBolt',
|
||||
isPinned: true,
|
||||
availabilityType: 'GLOBAL',
|
||||
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
|
||||
command: {
|
||||
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
|
||||
shortLabel: 'Hello',
|
||||
label: 'Hello World',
|
||||
icon: 'IconBolt',
|
||||
isPinned: true,
|
||||
availabilityType: 'GLOBAL',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -61,13 +55,14 @@ Klicken Sie darauf, um die Komponente inline zu rendern.
|
||||
|
||||
## Konfigurationsfelder
|
||||
|
||||
| Feld | Erforderlich | Beschreibung |
|
||||
| --------------------- | ------------ | --------------------------------------------------------------------------- |
|
||||
| `universalIdentifier` | Ja | Stabile eindeutige ID für diese Komponente |
|
||||
| `component` | Ja | Eine React-Komponentenfunktion |
|
||||
| `name` | Nein | Anzeigename |
|
||||
| `description` | Nein | Beschreibung dessen, was die Komponente macht |
|
||||
| `isHeadless` | Nein | Auf `true` setzen, wenn die Komponente keine sichtbare UI hat (siehe unten) |
|
||||
| Feld | Erforderlich | Beschreibung |
|
||||
| --------------------- | ------------ | ---------------------------------------------------------------------------------------- |
|
||||
| `universalIdentifier` | Ja | Stabile eindeutige ID für diese Komponente |
|
||||
| `component` | Ja | Eine React-Komponentenfunktion |
|
||||
| `name` | Nein | Anzeigename |
|
||||
| `description` | Nein | Beschreibung dessen, was die Komponente macht |
|
||||
| `isHeadless` | Nein | Auf `true` setzen, wenn die Komponente keine sichtbare UI hat (siehe unten) |
|
||||
| `command` | Nein | Die Komponente als Befehl registrieren (siehe unten [Befehlsoptionen](#command-options)) |
|
||||
|
||||
## Eine Front-Komponente auf einer Seite platzieren
|
||||
|
||||
@@ -146,17 +141,11 @@ export default defineFrontComponent({
|
||||
description: 'Creates a task from the command menu',
|
||||
component: RunAction,
|
||||
isHeadless: true,
|
||||
});
|
||||
```
|
||||
|
||||
```ts src/command-menu-items/run-action.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
|
||||
label: 'Run my action',
|
||||
icon: 'IconPlayerPlay',
|
||||
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
|
||||
command: {
|
||||
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
|
||||
label: 'Run my action',
|
||||
icon: 'IconPlayerPlay',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -188,6 +177,11 @@ export default defineFrontComponent({
|
||||
description: 'Deletes a draft with confirmation',
|
||||
component: DeleteDraft,
|
||||
isHeadless: true,
|
||||
command: {
|
||||
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
|
||||
label: 'Delete draft',
|
||||
icon: 'IconTrash',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -229,8 +223,7 @@ Verfügbare Hooks:
|
||||
| Hook | Gibt zurück | Beschreibung |
|
||||
| --------------------------------------------- | -------------------- | --------------------------------------------------------------------------- |
|
||||
| `useUserId()` | `string` oder `null` | Die ID des aktuellen Benutzers |
|
||||
| `useSelectedRecordIds()` | `Zeichenkette[]` | Alle ausgewählten Datensatz-IDs (leeres Array, wenn keine ausgewählt sind) |
|
||||
| `useRecordId()` | `string` oder `null` | **Veraltet.** Verwenden Sie stattdessen `useSelectedRecordIds()` |
|
||||
| `useRecordId()` | `string` oder `null` | Die ID des aktuellen Datensatzes (wenn auf einer Datensatzseite platziert) |
|
||||
| `useFrontComponentId()` | `string` | Die ID dieser Komponenteninstanz |
|
||||
| `useFrontComponentExecutionContext(selector)` | variiert | Zugriff auf den vollständigen Ausführungskontext mit einer Selektorfunktion |
|
||||
|
||||
@@ -293,84 +286,14 @@ export default defineFrontComponent({
|
||||
});
|
||||
```
|
||||
|
||||
### Mit mehreren Datensätzen arbeiten
|
||||
## Befehlsoptionen
|
||||
|
||||
Verwenden Sie `useSelectedRecordIds()`, um mehrere ausgewählte Datensätze zu verwalten. Dies ist nützlich für Stapelvorgänge:
|
||||
|
||||
```tsx src/front-components/bulk-export.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { useSelectedRecordIds, numberOfSelectedRecords } from 'twenty-sdk/front-component';
|
||||
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
const BulkExport = () => {
|
||||
const selectedRecordIds = useSelectedRecordIds();
|
||||
|
||||
const handleExport = async () => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
for (const recordId of selectedRecordIds) {
|
||||
await client.mutation({
|
||||
updateTask: {
|
||||
__args: { id: recordId, data: { exported: true } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await enqueueSnackbar({
|
||||
message: `Exported ${selectedRecordIds.length} records`,
|
||||
variant: 'success',
|
||||
});
|
||||
|
||||
await closeSidePanel();
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<p>Export {selectedRecordIds.length} selected record(s)?</p>
|
||||
<button onClick={handleExport}>Export</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
|
||||
name: 'bulk-export',
|
||||
description: 'Export selected records',
|
||||
component: BulkExport,
|
||||
command: {
|
||||
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
|
||||
label: 'Bulk Export',
|
||||
availabilityType: 'RECORD_SELECTION',
|
||||
conditionalAvailabilityExpression: numberOfSelectedRecords > 0,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## defineCommandMenuItem
|
||||
|
||||
Verwende `defineCommandMenuItem`, um eine Front-Komponente im Befehlsmenü (Cmd+K) zu registrieren. Wenn `isPinned` `true` ist, erscheint sie außerdem als Schnellaktionsschaltfläche oben rechts auf der Seite.
|
||||
|
||||
```ts src/command-menu-items/open-dashboard.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
label: 'Open Dashboard',
|
||||
shortLabel: 'Dashboard',
|
||||
icon: 'IconLayoutDashboard',
|
||||
isPinned: true,
|
||||
availabilityType: 'GLOBAL',
|
||||
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
|
||||
});
|
||||
```
|
||||
Das Hinzufügen eines `command`-Felds zu `defineFrontComponent` registriert die Komponente im Befehlsmenü (Cmd+K). Wenn `isPinned` `true` ist, erscheint sie außerdem als Schnellaktionsschaltfläche oben rechts auf der Seite.
|
||||
|
||||
| Feld | Erforderlich | Beschreibung |
|
||||
| --------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `universalIdentifier` | Ja | Stabile eindeutige ID für den Befehl |
|
||||
| `label` | Ja | Vollständiges Label, das im Befehlsmenü (Cmd+K) angezeigt wird |
|
||||
| `frontComponentUniversalIdentifier` | Ja | Der `universalIdentifier` der Front-Komponente, die dieser Befehl öffnet |
|
||||
| `shortLabel` | Nein | Kürzeres Label, das auf der angehefteten Schnellaktionsschaltfläche angezeigt wird |
|
||||
| `icon` | Nein | Neben dem Label angezeigter Icon-Name (z. B. 'IconBolt', 'IconSend') |
|
||||
| `isPinned` | Nein | Bei `true` wird der Befehl als Schnellaktionsschaltfläche oben rechts auf der Seite angezeigt |
|
||||
@@ -382,23 +305,30 @@ export default defineCommandMenuItem({
|
||||
|
||||
Mit dem Feld `conditionalAvailabilityExpression` können Sie basierend auf dem aktuellen Seitenkontext steuern, wann ein Befehl sichtbar ist. Importieren Sie typisierte Variablen und Operatoren aus `twenty-sdk`, um Ausdrücke zu erstellen:
|
||||
|
||||
```ts src/command-menu-items/bulk-update.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
```tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
pageType,
|
||||
numberOfSelectedRecords,
|
||||
objectPermissions,
|
||||
everyEquals,
|
||||
isDefined,
|
||||
} from 'twenty-sdk/front-component';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
label: 'Bulk Update',
|
||||
availabilityType: 'RECORD_SELECTION',
|
||||
frontComponentUniversalIdentifier: '...',
|
||||
conditionalAvailabilityExpression: everyEquals(
|
||||
objectPermissions,
|
||||
'canUpdateObjectRecords',
|
||||
true,
|
||||
),
|
||||
name: 'bulk-action',
|
||||
component: BulkAction,
|
||||
command: {
|
||||
universalIdentifier: '...',
|
||||
label: 'Bulk Update',
|
||||
availabilityType: 'RECORD_SELECTION',
|
||||
conditionalAvailabilityExpression: everyEquals(
|
||||
objectPermissions,
|
||||
'canUpdateObjectRecords',
|
||||
true,
|
||||
),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -4,52 +4,48 @@ icon: rocket
|
||||
description: Erstellen Sie in wenigen Minuten Ihre erste Twenty-App.
|
||||
---
|
||||
|
||||
## Was sind Apps?
|
||||
|
||||
Apps ermöglichen es Ihnen, Twenty mit benutzerdefinierten Objekten, Feldern, Logikfunktionen, Frontend-Komponenten, KI-Fähigkeiten und mehr zu erweitern — alles als Code verwaltet. Anstatt alles über die UI zu konfigurieren, definieren Sie Ihr Datenmodell und Ihre Logik in TypeScript und stellen es in einem oder mehreren Workspaces bereit.
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
Bevor Sie beginnen, stellen Sie sicher, dass Folgendes auf Ihrem Rechner installiert ist:
|
||||
|
||||
* **Node.js 24+** — [Hier herunterladen](https://nodejs.org/)
|
||||
* **Yarn 4** — Wird mit Node.js über Corepack mitgeliefert. Aktivieren Sie es: `corepack enable`
|
||||
* **Docker** — [Hier herunterladen](https://www.docker.com/products/docker-desktop/). Erforderlich, um einen lokalen Twenty-Server auszuführen. Überspringen Sie dies, wenn Twenty bereits anderswo läuft.
|
||||
* **Yarn 4** — Wird mit Node.js über Corepack mitgeliefert. Aktivieren Sie es, indem Sie `corepack enable` ausführen
|
||||
* **Docker** — [Hier herunterladen](https://www.docker.com/products/docker-desktop/). Erforderlich, um eine lokale Twenty-Instanz auszuführen. Nicht erforderlich, wenn bereits ein Twenty-Server läuft.
|
||||
|
||||
Das Erstellen einer Twenty-App umfasst drei Phasen. Das Scaffolding-Tool fasst sie zu einem einzigen Happy-Path-Befehl zusammen, aber jede Phase ist ein eigenes Konzept — wenn etwas fehlschlägt, hilft Ihnen das Wissen, in welcher Phase Sie sich befinden, zu erkennen, was zu beheben ist.
|
||||
## Erstellen Sie Ihre erste App
|
||||
|
||||
| Phase | Was Sie tun | Tool | Ergebnis |
|
||||
| ----------------------- | ------------------------------------------------------- | ----------------------------- | ---------------------------------------------------- |
|
||||
| **1. Gerüst erstellen** | Den Quellcode der App erzeugen | `npx create-twenty-app` | Ein TypeScript-Projekt auf der Festplatte |
|
||||
| **2. Server starten** | Einen Twenty-Server starten, in den synchronisiert wird | Docker + `yarn twenty server` | Eine laufende Twenty-Instanz |
|
||||
| **3. Synchronisieren** | Ihren Code live mit dem Server synchronisieren | `yarn twenty dev` | Ihre Änderungen erscheinen in der Benutzeroberfläche |
|
||||
### App-Gerüst erstellen
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Projektgerüst erstellen
|
||||
|
||||
Erstellen Sie eine neue App aus der Vorlage:
|
||||
Öffnen Sie ein Terminal und führen Sie Folgendes aus:
|
||||
|
||||
```bash filename="Terminal"
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
```
|
||||
|
||||
Sie werden nach einem Namen und einer Beschreibung gefragt — drücken Sie **Enter** für die Standardwerte. Dadurch wird ein TypeScript-Projekt in `my-twenty-app/` erzeugt, mit einer Startdatei `application-config.ts`, einer Standardrolle, einem CI-Workflow und einem Integrationstest.
|
||||
Sie werden aufgefordert, einen Namen und eine Beschreibung für Ihre App einzugeben. Drücken Sie **Enter**, um die Standardwerte zu übernehmen.
|
||||
|
||||
**Nach dieser Phase:** Sie haben den Quellcode einer App auf Ihrem Rechner. Es läuft noch nicht — das ist Phase 2.
|
||||
Dadurch wird ein neuer Ordner namens `my-twenty-app` mit allem erstellt, was Sie benötigen.
|
||||
|
||||
---
|
||||
### Lokale Twenty-Instanz einrichten
|
||||
|
||||
## Phase 2 — Einen lokalen Twenty-Server starten
|
||||
|
||||
Ihre App benötigt einen Twenty-Server, in den sie synchronisieren kann. Der Server ist eine vollständige Twenty-Instanz — UI, GraphQL-API, PostgreSQL — die lokal in Docker läuft. Ihr lokaler Code lädt seine Definitionen auf diesen Server hoch, wodurch sie in der Benutzeroberfläche erscheinen.
|
||||
|
||||
Das Scaffolding-Tool bietet an, einen für Sie zu starten:
|
||||
Das Scaffolding-Tool fragt:
|
||||
|
||||
> **Möchten Sie eine lokale Twenty-Instanz einrichten?**
|
||||
|
||||
* **Ja (empfohlen)** — lädt das Docker-Image `twentycrm/twenty-app-dev` herunter und startet es auf Port `2020`. Stellen Sie sicher, dass Docker läuft.
|
||||
* **Nein** — wählen Sie dies, wenn Sie bereits einen Twenty-Server haben, mit dem Sie sich verbinden möchten. Sie können die Verbindung später mit `yarn twenty remote add` herstellen.
|
||||
* **Geben Sie `yes` ein** (empfohlen) — Dadurch wird das Docker-Image `twenty-app-dev` heruntergeladen und ein lokaler Twenty-Server auf Port `2020` gestartet. Stellen Sie sicher, dass Docker läuft, bevor Sie fortfahren.
|
||||
* **Geben Sie `no` ein** — Wählen Sie dies, wenn bereits ein Twenty-Server lokal läuft.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/start-instance.png" alt="Soll die lokale Instanz gestartet werden?" />
|
||||
</div>
|
||||
|
||||
Sobald der Server läuft, öffnet sich ein Browser zur Anmeldung. Verwenden Sie das vorab eingerichtete Demo-Konto:
|
||||
### Melden Sie sich bei Ihrem Arbeitsbereich an
|
||||
|
||||
Anschließend öffnet sich ein Browserfenster mit der Twenty-Anmeldeseite. Melden Sie sich mit dem vorab eingerichteten Demo-Konto an:
|
||||
|
||||
* **E-Mail:** `tim@apple.dev`
|
||||
* **Passwort:** `tim@apple.dev`
|
||||
@@ -58,81 +54,89 @@ Sobald der Server läuft, öffnet sich ein Browser zur Anmeldung. Verwenden Sie
|
||||
<img src="/images/docs/developers/extends/apps/login.png" alt="Twenty-Anmeldebildschirm" />
|
||||
</div>
|
||||
|
||||
Klicken Sie auf dem nächsten Bildschirm auf **Authorize** — dadurch erhält die CLI Zugriff auf Ihren Arbeitsbereich.
|
||||
### Autorisieren Sie die App
|
||||
|
||||
Nach der Anmeldung sehen Sie einen Autorisierungsbildschirm. Dadurch kann Ihre App mit Ihrem Arbeitsbereich interagieren.
|
||||
|
||||
Klicken Sie auf **Authorize**, um fortzufahren.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/authorize.png" alt="Twenty-CLI-Autorisierungsbildschirm" />
|
||||
</div>
|
||||
|
||||
Ihr Terminal bestätigt, dass alles eingerichtet ist.
|
||||
Nach der Autorisierung bestätigt Ihr Terminal, dass alles eingerichtet ist.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/scaffolded.png" alt="App-Gerüst erfolgreich erstellt" />
|
||||
</div>
|
||||
|
||||
**Nach dieser Phase:** Sie haben einen laufenden Twenty-Server unter [http://localhost:2020](http://localhost:2020), und Ihre CLI ist autorisiert, mit ihm zu synchronisieren.
|
||||
### Beginnen Sie mit der Entwicklung
|
||||
|
||||
<Note>
|
||||
Wenn Docker nicht installiert ist oder nicht läuft, zeigt das Scaffolding-Tool den richtigen Startbefehl für Ihr Betriebssystem an. Sobald Docker läuft, können Sie mit `yarn twenty server start` fortfahren — ein erneutes Scaffolding ist nicht nötig.
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Ihre Änderungen synchronisieren
|
||||
|
||||
Das ist die innere Schleife, in der Sie die meiste Zeit verbringen werden.
|
||||
Wechseln Sie in Ihren neuen App-Ordner und starten Sie den Entwicklungsserver:
|
||||
|
||||
```bash filename="Terminal"
|
||||
cd my-twenty-app
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
Dies überwacht `src/`, baut bei jeder Änderung neu und synchronisiert das Ergebnis mit dem Server. Bearbeiten Sie eine Datei, speichern Sie, und innerhalb einer Sekunde spiegelt der Server die Änderung wider. Sie sehen eine Live-Statusanzeige in Ihrem Terminal.
|
||||
Dadurch werden Ihre Quelldateien überwacht, bei jeder Änderung neu gebaut und Ihre App automatisch mit dem lokalen Twenty-Server synchronisiert. In Ihrem Terminal sollte eine Live-Statusanzeige angezeigt werden.
|
||||
|
||||
Für ausführlichere Ausgaben (Build-Protokolle, Sync-Anfragen, Fehlerspuren) fügen Sie `--verbose` hinzu.
|
||||
Für ausführlichere Ausgaben (Build-Protokolle, Sync-Anfragen, Fehlerspuren) verwenden Sie das Flag `--verbose`:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev --verbose
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Der Dev-Modus ist nur auf Twenty-Instanzen verfügbar, die im Entwicklungsmodus laufen (`NODE_ENV=development`). Produktionsinstanzen lehnen Dev-Synchronisierungsanfragen ab. Verwenden Sie `yarn twenty deploy`, um auf Produktionsservern bereitzustellen — Details finden Sie unter [Apps veröffentlichen](/l/de/developers/extend/apps/publishing).
|
||||
</Warning>
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/dev.png" alt="Terminalausgabe im Dev-Modus" />
|
||||
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Terminalausgabe im Dev-Modus" />
|
||||
</div>
|
||||
|
||||
Öffnen Sie [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer). Unter **Your Apps** sollte Ihre App angezeigt werden.
|
||||
#### Einmalige Synchronisierung mit `yarn twenty dev --once`
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="Liste "Your Apps", die "My twenty app" anzeigt" />
|
||||
</div>
|
||||
|
||||
Klicken Sie auf **My twenty app**, um die **Anwendungsregistrierung** anzuzeigen — ein serverseitiger Datensatz, der Ihre App beschreibt (Name, Bezeichner, OAuth-Anmeldedaten, Quelle). Eine Registrierung kann in mehreren Arbeitsbereichen auf demselben Server installiert werden.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="Details der Anwendungsregistrierung" />
|
||||
</div>
|
||||
|
||||
Klicken Sie auf **View installed app**, um die Installation im Arbeitsbereich anzuzeigen. Die Registerkarte **About** zeigt die Version und Verwaltungsoptionen.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Installierte App" />
|
||||
</div>
|
||||
|
||||
**Nach dieser Phase:** Sie haben eine Live-Entwicklungsschleife. Bearbeiten Sie eine beliebige Datei in `src/`, und sie erscheint in der Benutzeroberfläche.
|
||||
|
||||
### Einmalige Synchronisierung für CI und Skripte
|
||||
|
||||
Verwenden Sie `--once`, um einen einzelnen Build + Sync auszuführen und zu beenden — gleiche Pipeline, kein Watcher:
|
||||
Wenn Sie keinen Watcher im Hintergrund ausführen lassen möchten (zum Beispiel in einer CI-Pipeline, einem Git-Hook oder einem skriptgesteuerten Workflow), geben Sie das Flag `--once` an. Es führt dieselbe Pipeline aus wie `yarn twenty dev` — Build-Manifest erstellen, Dateien bündeln, hochladen, synchronisieren, den typisierten API-Client neu generieren — beendet sich jedoch, **sobald die Synchronisierung abgeschlossen ist**:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev --once
|
||||
```
|
||||
|
||||
| Befehl | Verhalten | Wann verwenden |
|
||||
| ------------------------ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------- |
|
||||
| `yarn twenty dev` | Überwacht und synchronisiert bei jeder Änderung erneut. Läuft, bis Sie es stoppen. | Interaktive lokale Entwicklung. |
|
||||
| `yarn twenty dev --once` | Einmaliger Build + Sync, beendet sich mit `0` bei Erfolg, mit `1` bei Fehler. | CI, Pre-Commit-Hooks, KI-Agenten, skriptgesteuerte Workflows. |
|
||||
| Befehl | Verhalten | Wann verwenden |
|
||||
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| `yarn twenty dev` | Überwacht Ihre Quelldateien und synchronisiert bei jeder Änderung erneut. Läuft weiter, bis Sie es stoppen. | Interaktive lokale Entwicklung — Sie möchten das Live-Status-Panel und eine sofortige Feedback-Schleife. |
|
||||
| `yarn twenty dev --once` | Führt einen einzelnen Build + Sync aus und beendet sich anschließend mit Code `0` bei Erfolg oder `1` bei einem Fehler. | Skripte, CI, Pre-Commit-Hooks, KI-Agenten und jeder nicht-interaktive Workflow. |
|
||||
|
||||
Beide Modi benötigen einen Server im Entwicklungsmodus und eine authentifizierte Remote-Verbindung.
|
||||
Beide Modi erfordern einen Twenty-Server, der im Entwicklungsmodus läuft, und ein authentifiziertes Remote — es gelten dieselben Voraussetzungen.
|
||||
|
||||
<Warning>
|
||||
Der Dev-Modus ist nur auf Twenty-Instanzen verfügbar, die im Entwicklungsmodus laufen (`NODE_ENV=development`). Produktionsinstanzen lehnen Dev-Sync-Anfragen ab — verwenden Sie `yarn twenty deploy`, um auf Produktionsserver bereitzustellen. Siehe [Apps veröffentlichen](/l/de/developers/extend/apps/publishing).
|
||||
</Warning>
|
||||
### Sehen Sie sich Ihre App in Twenty an
|
||||
|
||||
Öffnen Sie [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer) in Ihrem Browser. Navigieren Sie zu **Settings > Apps** und wählen Sie die Registerkarte **Developer**. Unter **Your Apps** sollte Ihre App aufgeführt sein:
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="Liste "Your Apps", die "My twenty app" anzeigt" />
|
||||
</div>
|
||||
|
||||
Klicken Sie auf **My twenty app**, um die **Anwendungsregistrierung** zu öffnen. Eine Registrierung ist ein Servereintrag, der Ihre App beschreibt — ihren Namen, den eindeutigen Bezeichner, OAuth-Zugangsdaten und die Quelle (lokal, npm oder Tarball). Sie befindet sich auf dem Server, nicht in einem bestimmten Arbeitsbereich. Wenn Sie eine App in einen Arbeitsbereich installieren, erstellt Twenty eine arbeitsbereichsbezogene Anwendung, die auf diese Registrierung verweist. Eine Registrierung kann in mehreren Arbeitsbereichen auf demselben Server installiert werden.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="Details der Anwendungsregistrierung" />
|
||||
</div>
|
||||
|
||||
Klicken Sie auf **View installed app**, um die installierte App anzuzeigen. Die Registerkarte **About** zeigt die aktuelle Version und Verwaltungsoptionen:
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Installierte App — Registerkarte About" />
|
||||
</div>
|
||||
|
||||
Wechseln Sie zur Registerkarte **Content**, um alles zu sehen, was Ihre App bereitstellt — Objekte, Felder, Logikfunktionen und Agenten:
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-4.png" alt="Installierte App — Registerkarte Content" />
|
||||
</div>
|
||||
|
||||
Alles erledigt! Bearbeiten Sie eine beliebige Datei in `src/`, und die Änderungen werden automatisch übernommen.
|
||||
|
||||
---
|
||||
|
||||
@@ -140,112 +144,117 @@ Der Dev-Modus ist nur auf Twenty-Instanzen verfügbar, die im Entwicklungsmodus
|
||||
|
||||
Apps bestehen aus **Entitäten** — jede ist als TypeScript-Datei mit einem einzigen `export default` definiert:
|
||||
|
||||
| Entität | Was sie macht |
|
||||
| -------------------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| **Objekte & Felder** | Benutzerdefinierte Datenmodelle (Postkarte, Rechnung usw.) mit typisierten Feldern |
|
||||
| **Logikfunktionen** | Serverseitiges TypeScript, ausgelöst durch HTTP-Routen, Cron-Zeitpläne oder Datenbankereignisse |
|
||||
| **Frontend-Komponenten** | React-Komponenten, die in der UI von Twenty gerendert werden (Seitenleiste, Widgets, Befehlsmenü) |
|
||||
| **Fähigkeiten & Agenten** | KI-Funktionen — wiederverwendbare Anweisungen und autonome Assistenten |
|
||||
| **Ansichten & Navigation** | Vorkonfigurierte Listenansichten und Seitenleisteneinträge |
|
||||
| **Seitenlayouts** | Benutzerdefinierte Datensatz-Detailseiten mit Tabs und Widgets |
|
||||
| Entität | Was sie macht |
|
||||
| -------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Objekte & Felder** | Definieren Sie benutzerdefinierte Datenmodelle (wie Post Card, Invoice) mit typisierten Feldern |
|
||||
| **Logikfunktionen** | Serverseitige TypeScript-Funktionen, die durch HTTP-Routen, Cron-Zeitpläne oder Datenbankereignisse ausgelöst werden |
|
||||
| **Frontend-Komponenten** | React-Komponenten, die in der UI von Twenty gerendert werden (Seitenleiste, Widgets, Befehlsmenü) |
|
||||
| **Fähigkeiten & Agenten** | KI-Funktionen — wiederverwendbare Anweisungen und autonome Assistenten |
|
||||
| **Ansichten & Navigation** | Vorkonfigurierte Listenansichten und Seitenleisteneinträge für Ihre Objekte |
|
||||
| **Seitenlayouts** | Benutzerdefinierte Datensatz-Detailseiten mit Tabs und Widgets |
|
||||
|
||||
Vollständige Referenz: [Apps entwickeln](/l/de/developers/extend/apps/building).
|
||||
Wechseln Sie zu [Apps erstellen](/l/de/developers/extend/apps/building) für eine ausführliche Anleitung zu jedem Entitätstyp.
|
||||
|
||||
---
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
Der Scaffolder erzeugt die folgende Verzeichnisstruktur:
|
||||
|
||||
```text filename="my-twenty-app/"
|
||||
my-twenty-app/
|
||||
package.json
|
||||
yarn.lock
|
||||
.gitignore
|
||||
.nvmrc
|
||||
.yarnrc.yml
|
||||
.oxlintrc.json
|
||||
tsconfig.json
|
||||
tsconfig.spec.json # TypeScript config for tests
|
||||
vitest.config.ts # Vitest test runner configuration
|
||||
LLMS.md
|
||||
README.md
|
||||
.github/
|
||||
└── workflows/
|
||||
└── ci.yml # GitHub Actions CI workflow
|
||||
public/ # Public assets (images, fonts, etc.)
|
||||
src/
|
||||
application-config.ts # Required — your app's entry point
|
||||
default-role.ts # Permissions for logic functions
|
||||
constants/
|
||||
universal-identifiers.ts # Auto-generated UUIDs and metadata
|
||||
__tests__/
|
||||
setup-test.ts
|
||||
app-install.integration-test.ts
|
||||
.github/workflows/ci.yml # GitHub Actions
|
||||
public/ # Static assets
|
||||
vitest.config.ts # Test runner config
|
||||
tsconfig.json, tsconfig.spec.json
|
||||
.nvmrc, .yarnrc.yml, .oxlintrc.json
|
||||
README.md, LLMS.md
|
||||
├── application-config.ts # Required — main application configuration
|
||||
├── default-role.ts # Default role for logic functions
|
||||
├── constants/
|
||||
│ └── universal-identifiers.ts # Auto-generated UUIDs and app metadata
|
||||
└── __tests__/
|
||||
├── setup-test.ts # Test setup (server health check, config)
|
||||
└── app-install.integration-test.ts # Integration test
|
||||
```
|
||||
|
||||
| Datei / Ordner | Zweck |
|
||||
| ---------------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| `src/application-config.ts` | **Erforderlich.** Die Hauptkonfigurationsdatei für Ihre App. |
|
||||
| `src/default-role.ts` | Standardrolle, die steuert, worauf Ihre Logikfunktionen zugreifen können. |
|
||||
| `src/constants/universal-identifiers.ts` | Automatisch erzeugte UUIDs und Metadaten (Anzeigename, Beschreibung). |
|
||||
| `src/__tests__/` | Integrationstests (Setup + Beispieltest). |
|
||||
| `public/` | Statische Assets (Bilder, Schriftarten), die mit Ihrer App ausgeliefert werden. |
|
||||
|
||||
### Mit einem Beispiel beginnen
|
||||
|
||||
Verwenden Sie `--example`, um mit einem vollständigeren Projekt zu starten (benutzerdefinierte Objekte, Felder, Logikfunktionen, Front-End-Komponenten):
|
||||
Um mit einem umfassenderen Beispiel mit benutzerdefinierten Objekten, Feldern, Logikfunktionen, Frontend-Komponenten und mehr zu starten, verwenden Sie die Option `--example`:
|
||||
|
||||
```bash filename="Terminal"
|
||||
npx create-twenty-app@latest my-twenty-app --example postcard
|
||||
```
|
||||
|
||||
Die Beispiele befinden sich unter [twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples). Sie können auch einzelne Entitäten in einem bestehenden Projekt mit `yarn twenty add` erzeugen — siehe [Apps entwickeln](/l/de/developers/extend/apps/building#scaffolding-entities-with-yarn-twenty-add).
|
||||
Die Beispiele stammen aus dem Verzeichnis [twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples) auf GitHub. Sie können auch einzelne Entitäten in einem bestehenden Projekt mit `yarn twenty add` erzeugen (siehe [Apps erstellen](/l/de/developers/extend/apps/building#scaffolding-entities-with-yarn-twenty-add)).
|
||||
|
||||
---
|
||||
### Wichtige Dateien
|
||||
|
||||
## Lokalen Server verwalten
|
||||
| Datei / Ordner | Zweck |
|
||||
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `package.json` | Deklariert den App-Namen, die Version und Abhängigkeiten. Enthält ein `twenty`-Skript, sodass Sie `yarn twenty help` ausführen können, um alle Befehle anzuzeigen. |
|
||||
| `src/application-config.ts` | **Erforderlich.** Die Hauptkonfigurationsdatei für Ihre App. |
|
||||
| `src/default-role.ts` | Standardrolle, die steuert, worauf Ihre Logikfunktionen zugreifen können. |
|
||||
| `src/constants/universal-identifiers.ts` | Automatisch erzeugte UUIDs und App-Metadaten (Anzeigename, Beschreibung). |
|
||||
| `src/__tests__/` | Integrationstests (Setup + Beispieltest). |
|
||||
| `public/` | Statische Assets (Bilder, Schriftarten), die mit Ihrer App ausgeliefert werden. |
|
||||
|
||||
Verwenden Sie `yarn twenty server`, um den lokalen Twenty-Container zu steuern:
|
||||
## Lokaler Entwicklungsserver
|
||||
|
||||
| Befehl | Was es tut |
|
||||
| -------------------------------------- | --------------------------------------------------- |
|
||||
| `yarn twenty server start` | Server starten (lädt das Image bei Bedarf herunter) |
|
||||
| `yarn twenty server start --port 3030` | Auf einem benutzerdefinierten Port starten |
|
||||
| `yarn twenty server stop` | Server stoppen (Daten bleiben erhalten) |
|
||||
| `yarn twenty server status` | URL, Version und Anmeldedaten anzeigen |
|
||||
| `yarn twenty server logs` | Serverprotokolle streamen |
|
||||
| `yarn twenty server reset` | Alle Daten löschen und neu starten |
|
||||
| `yarn twenty server upgrade` | Das neueste `twenty-app-dev`-Image herunterladen |
|
||||
| `yarn twenty server upgrade 2.2.0` | Auf eine bestimmte Version aktualisieren |
|
||||
Der Scaffolder hat bereits einen lokalen Twenty-Server für Sie gestartet. Um ihn später zu verwalten, verwenden Sie `yarn twenty server`:
|
||||
|
||||
Daten bleiben über Neustarts hinweg in zwei Docker-Volumes bestehen (`twenty-app-dev-data` für PostgreSQL, `twenty-app-dev-storage` für Dateien). Verwenden Sie `reset`, um alles zu löschen.
|
||||
| Befehl | Beschreibung |
|
||||
| -------------------------------------- | ----------------------------------------------------------- |
|
||||
| `yarn twenty server start` | Lokalen Server starten (lädt das Image bei Bedarf herunter) |
|
||||
| `yarn twenty server start --port 3030` | Auf einem benutzerdefinierten Port starten |
|
||||
| `yarn twenty server start --test` | Starten Sie eine separate Testinstanz auf Port 2021 |
|
||||
| `yarn twenty server stop` | Server stoppen (Daten bleiben erhalten) |
|
||||
| `yarn twenty server status` | Serverstatus, URL und Anmeldedaten anzeigen |
|
||||
| `yarn twenty server logs` | Serverprotokolle streamen |
|
||||
| `yarn twenty server logs --lines 100` | Die letzten 100 Protokollzeilen anzeigen |
|
||||
| `yarn twenty server reset` | Alle Daten löschen und neu starten |
|
||||
|
||||
### Aktualisieren des Server-Images
|
||||
Daten bleiben über Neustarts hinweg in zwei Docker-Volumes bestehen (`twenty-app-dev-data` für PostgreSQL, `twenty-app-dev-storage` für Dateien). Verwenden Sie `reset`, um alles zu löschen und neu zu beginnen.
|
||||
|
||||
`yarn twenty server upgrade` lädt das neueste Image herunter, vergleicht die Digests und erstellt den Container nur neu, wenn sich tatsächlich etwas geändert hat. Die Volumes bleiben erhalten — nur der Container wird ersetzt. Wenn ein neues Image heruntergeladen wurde und der Container lief, startet das Upgrade automatisch einen neuen Container; führen Sie anschließend `yarn twenty server start` aus, um zu warten, bis er betriebsbereit ist.
|
||||
### Eine Testinstanz ausführen
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty server upgrade # Latest
|
||||
yarn twenty server upgrade 2.2.0 # Specific version
|
||||
```
|
||||
Übergeben Sie `--test` an jeden `server`-Befehl, um eine zweite, vollständig isolierte Instanz zu verwalten — nützlich, um Integrationstests auszuführen oder zu experimentieren, ohne Ihre Hauptentwicklungsdaten anzutasten.
|
||||
|
||||
Überprüfen Sie die laufende Version mit `yarn twenty server status` (dies zeigt die im Container enthaltene `APP_VERSION` an).
|
||||
| Befehl | Beschreibung |
|
||||
| ---------------------------------- | ----------------------------------------------------- |
|
||||
| `yarn twenty server start --test` | Die Testinstanz starten (standardmäßig Port 2021) |
|
||||
| `yarn twenty server stop --test` | Die Testinstanz stoppen |
|
||||
| `yarn twenty server status --test` | Status, URL und Anmeldedaten der Testinstanz anzeigen |
|
||||
| `yarn twenty server logs --test` | Protokolle der Testinstanz streamen |
|
||||
| `yarn twenty server reset --test` | Alle Testdaten löschen und neu starten |
|
||||
|
||||
### Eine parallele Testinstanz ausführen
|
||||
Die Testinstanz läuft in einem eigenen Docker-Container (`twenty-app-dev-test`) mit dedizierten Volumes (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) und eigener Konfiguration, sodass sie parallel zu Ihrer Hauptinstanz ohne Konflikte ausgeführt werden kann. Kombinieren Sie `--test` mit `--port`, um den Standardport 2021 zu überschreiben.
|
||||
|
||||
Übergeben Sie `--test` an jeden `server`-Befehl, um eine zweite, vollständig isolierte Instanz zu verwalten — nützlich für Integrationstests oder Experimente, ohne Ihre Hauptentwicklungsdaten anzutasten:
|
||||
|
||||
| Befehl | Was es tut |
|
||||
| ----------------------------------- | ------------------------------------------------- |
|
||||
| `yarn twenty server start --test` | Die Testinstanz starten (standardmäßig Port 2021) |
|
||||
| `yarn twenty server stop --test` | Anhalten |
|
||||
| `yarn twenty server status --test` | Status anzeigen |
|
||||
| `yarn twenty server logs --test` | Protokolle streamen |
|
||||
| `yarn twenty server reset --test` | Daten löschen |
|
||||
| `yarn twenty server upgrade --test` | Image aktualisieren |
|
||||
|
||||
Die Testinstanz hat ihren eigenen Container (`twenty-app-dev-test`), eigene Volumes (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) und eine eigene Konfiguration — sie läuft parallel zu Ihrer Hauptinstanz ohne Konflikte. Kombinieren Sie `--test` mit `--port`, um den Port 2021 zu überschreiben.
|
||||
|
||||
---
|
||||
<Note>
|
||||
Der Server erfordert, dass **Docker** läuft. Wenn der Fehler "Docker not running" angezeigt wird, stellen Sie sicher, dass Docker Desktop (oder der Docker-Daemon) gestartet ist.
|
||||
</Note>
|
||||
|
||||
## Manuelle Einrichtung (ohne Scaffolder)
|
||||
|
||||
Überspringen Sie das Scaffolding-Tool, wenn Sie das SDK zu einem bestehenden Projekt hinzufügen:
|
||||
Wenn Sie die Einrichtung lieber selbst vornehmen möchten, anstatt `create-twenty-app` zu verwenden, können Sie dies in zwei Schritten tun.
|
||||
|
||||
**1. Fügen Sie `twenty-sdk` und `twenty-client-sdk` als Abhängigkeiten hinzu:**
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add twenty-sdk twenty-client-sdk
|
||||
```
|
||||
|
||||
Fügen Sie der `package.json` das Skript hinzu:
|
||||
**2. Fügen Sie Ihrer `package.json` ein `twenty`-Skript hinzu:**
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
@@ -255,19 +264,19 @@ Fügen Sie der `package.json` das Skript hinzu:
|
||||
}
|
||||
```
|
||||
|
||||
Sie können jetzt `yarn twenty dev`, `yarn twenty server start` und den Rest ausführen.
|
||||
Sie können jetzt `yarn twenty dev`, `yarn twenty help` und alle anderen Befehle ausführen.
|
||||
|
||||
<Note>
|
||||
Installieren Sie `twenty-sdk` nicht global — fixieren Sie es pro Projekt, damit jede App ihre eigene Version verwendet.
|
||||
Installieren Sie `twenty-sdk` nicht global. Verwenden Sie es immer als lokale Projektabhängigkeit, damit jedes Projekt seine eigene Version festlegen kann.
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Fehlerbehebung
|
||||
|
||||
* **Docker-Fehler** — Stellen Sie sicher, dass Docker Desktop (oder der Daemon) läuft, bevor Sie `yarn twenty server start` ausführen. Die Fehlermeldung zeigt den richtigen Startbefehl für Ihr Betriebssystem an.
|
||||
* **Falsche Node-Version** — 24+ erforderlich. Prüfen Sie mit `node -v`.
|
||||
* **Yarn 4 fehlt** — Führen Sie `corepack enable` aus.
|
||||
* **Abhängigkeiten defekt** — `rm -rf node_modules && yarn install`.
|
||||
Wenn Probleme auftreten:
|
||||
|
||||
Hängen Sie fest? Bitten Sie im [Twenty-Discord](https://discord.com/channels/1130383047699738754/1130386664812982322) um Hilfe.
|
||||
* Stellen Sie sicher, dass **Docker läuft**, bevor Sie das Scaffolding-Tool mit einer lokalen Instanz starten.
|
||||
* Stellen Sie sicher, dass Sie **Node.js 24+** verwenden (`node -v` zur Überprüfung).
|
||||
* Stellen Sie sicher, dass **Corepack aktiviert ist** (`corepack enable`), damit Yarn 4 verfügbar ist.
|
||||
* Versuchen Sie, `node_modules` zu löschen und `yarn install` erneut auszuführen, wenn Abhängigkeiten fehlerhaft erscheinen.
|
||||
|
||||
Hängen Sie immer noch fest? Bitten Sie im [Twenty-Discord](https://discord.com/channels/1130383047699738754/1130386664812982322) um Hilfe.
|
||||
|
||||
@@ -141,14 +141,11 @@ const handler = async (event: RoutePayload) => {
|
||||
Header-Namen werden in Kleinbuchstaben normalisiert. Greifen Sie mit Schlüsseln in Kleinbuchstaben darauf zu (z. B. `event.headers['content-type']`).
|
||||
</Note>
|
||||
|
||||
#### Eine Funktion als KI-Tool oder Workflow-Aktion verfügbar machen
|
||||
#### Eine Funktion als Tool bereitstellen
|
||||
|
||||
Logikfunktionen können auf zwei Oberflächen verfügbar gemacht werden, jeweils mit eigenem Trigger:
|
||||
Logikfunktionen können als **Tools** für KI-Agenten und Workflows verfügbar gemacht werden. Wenn eine Funktion als Tool markiert ist, wird sie von den KI-Funktionen von Twenty auffindbar und kann in Workflow-Automatisierungen verwendet werden.
|
||||
|
||||
* **`toolTriggerSettings`** — macht die Funktion über die KI-Funktionen von Twenty (Chat, MCP, Funktionsaufrufe) auffindbar. Verwendet das standardmäßige JSON Schema, das Format, das LLMs nativ verstehen.
|
||||
* **`workflowActionTriggerSettings`** — lässt die Funktion als Schritt im visuellen Workflow-Builder erscheinen. Verwendet das umfangreiche `InputSchema` von Twenty, sodass der Builder geeignete Feldeditoren, Variablenauswahlen und Beschriftungen rendern kann.
|
||||
|
||||
Eine Funktion kann sich für eine, die andere oder beide entscheiden. Sie stehen neben `cronTriggerSettings`, `databaseEventTriggerSettings` und `httpRouteTriggerSettings` — gleiches Muster, gleiche Struktur.
|
||||
Um eine Logikfunktion als Tool zu markieren, setzen Sie `isTool: true`:
|
||||
|
||||
```ts src/logic-functions/enrich-company.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
@@ -178,33 +175,31 @@ export default defineLogicFunction({
|
||||
description: 'Enrich a company record with external data',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
toolTriggerSettings: {},
|
||||
isTool: true,
|
||||
});
|
||||
```
|
||||
|
||||
Hauptpunkte:
|
||||
|
||||
* Eine Funktion kann Oberflächen mischen — deklarieren Sie sowohl `toolTriggerSettings` als auch `workflowActionTriggerSettings`, um sie im Chat UND im Workflow-Builder bereitzustellen.
|
||||
* `toolTriggerSettings.inputSchema` und `workflowActionTriggerSettings.inputSchema` sind beide optional. Wenn sie weggelassen werden, leitet der Manifest-Builder sie aus dem Handler-Quellcode ab (JSON Schema für das KI-Tool, das `InputSchema` von Twenty für die Workflow-Aktion). Geben Sie eines explizit an, wenn Sie eine reichere Typisierung wünschen — zum Beispiel mit `FieldMetadataType`-fähigen Feldern wie `CURRENCY` oder `RELATION` für den Workflow-Builder oder mit `description`-Feldern, die der KI-Agent lesen kann:
|
||||
* Sie können `isTool` mit Triggern kombinieren — eine Funktion kann gleichzeitig sowohl ein Tool (von KI-Agenten aufrufbar) als auch durch Ereignisse ausgelöst werden.
|
||||
* **`toolInputSchema`** (optional): Ein JSON-Schema-Objekt, das die Parameter beschreibt, die Ihre Funktion akzeptiert. Das Schema wird automatisch durch statische Analyse des Quellcodes ermittelt, Sie können es jedoch auch explizit festlegen:
|
||||
|
||||
```ts
|
||||
export default defineLogicFunction({
|
||||
...,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
companyName: {
|
||||
type: 'string',
|
||||
description: 'The name of the company to enrich',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
description: 'The company website domain (optional)',
|
||||
},
|
||||
toolInputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
companyName: {
|
||||
type: 'string',
|
||||
description: 'The name of the company to enrich',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
description: 'The company website domain (optional)',
|
||||
},
|
||||
required: ['companyName'],
|
||||
},
|
||||
required: ['companyName'],
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -243,7 +238,7 @@ yarn twenty exec --postInstall
|
||||
```
|
||||
|
||||
Hauptpunkte:
|
||||
* Post-Installationsfunktionen verwenden `definePostInstallLogicFunction()` — eine spezialisierte Variante, die Trigger-Einstellungen (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `toolTriggerSettings`, `workflowActionTriggerSettings`) weglässt.
|
||||
* Post-Installationsfunktionen verwenden `definePostInstallLogicFunction()` — eine spezialisierte Variante, die Trigger-Einstellungen (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`) weglässt.
|
||||
* Der Handler erhält ein `InstallPayload` mit `{ previousVersion?: string; newVersion: string }` — `newVersion` ist die zu installierende Version, und `previousVersion` ist die zuvor installierte Version (oder `undefined` bei einer Neuinstallation). Verwenden Sie diese Werte, um Neuinstallationen von Upgrades zu unterscheiden und versionsspezifische Migrationslogik auszuführen.
|
||||
* **Wann der Hook ausgeführt wird**: standardmäßig nur bei Neuinstallationen. Übergeben Sie `shouldRunOnVersionUpgrade: true`, wenn er auch beim Upgrade der App von einer vorherigen Version ausgeführt werden soll. Wenn weggelassen, ist das Flag standardmäßig `false` und Upgrades überspringen den Hook.
|
||||
* **Ausführungsmodell — standardmäßig asynchron, synchron optional**: Das Flag `shouldRunSynchronously` steuert, *wie* Post-Install ausgeführt wird.
|
||||
|
||||
@@ -77,39 +77,6 @@ Pre-Release-Tags funktionieren wie erwartet: Das Erhöhen von `1.0.0-rc.1` → `
|
||||
|
||||
{/* TODO: add screenshot of the Upgrade button */}
|
||||
|
||||
### Kompatibilität der Serverversionen
|
||||
|
||||
Wenn Ihre App eine Funktion verwendet, die in einer bestimmten Twenty-Serverversion eingeführt wurde (z. B. OAuth-Anbieter, die in v2.3.0 hinzugefügt wurden), sollten Sie die minimale Serverversion, die Ihre App benötigt, mithilfe des Felds `engines.twenty` in `package.json` angeben:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"name": "twenty-my-app",
|
||||
"version": "1.0.0",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
"twenty": ">=2.3.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Der Wert ist ein standardmäßiger [semver-Bereich](https://github.com/npm/node-semver#ranges). Häufige Muster:
|
||||
|
||||
| Bereich | Bedeutung |
|
||||
| ---------------------------------- | ------------------------------------------------------ |
|
||||
| `>=2.3.0` | Jeder Server ab 2.3.0 |
|
||||
| `>=2.3.0 \<3.0.0` | 2.3.0 oder höher, aber unter der nächsten Hauptversion |
|
||||
| `^2.3.0` | Entspricht `>=2.3.0 \<3.0.0` |
|
||||
|
||||
**Was bei Bereitstellung und Installation passiert:**
|
||||
|
||||
* Wenn `engines.twenty` gesetzt ist und die Version des Zielservers den Bereich nicht erfüllt, wird die Bereitstellung (Tarball-Upload) oder Installation mit dem Fehler `SERVER_VERSION_INCOMPATIBLE` abgelehnt, zusammen mit einer Meldung, die sowohl den erforderlichen Bereich als auch die tatsächliche Serverversion angibt.
|
||||
* Wenn `engines.twenty` nicht gesetzt ist, wird die App auf jeder Serverversion akzeptiert (abwärtskompatibel mit bestehenden Apps).
|
||||
* Wenn auf dem Server keine `APP_VERSION` konfiguriert ist, wird die Prüfung übersprungen.
|
||||
|
||||
<Note>
|
||||
Der Server ist die maßgebliche Prüfinstanz — er validiert `engines.twenty` sowohl beim Tarball-Upload als auch bei der Workspace-Installation. Auch wenn Sie ein Tarball außerhalb des regulären Prozesses bereitstellen oder aus dem Marketplace installieren, erzwingt der Server weiterhin die Kompatibilität.
|
||||
</Note>
|
||||
|
||||
## Automatisiertes CI/CD (vorgefertigte Workflows)
|
||||
|
||||
Apps, die mit `create-twenty-app` erzeugt wurden, enthalten von Haus aus zwei GitHub-Actions-Workflows unter `.github/workflows/`. Sie sind einsatzbereit, sobald Sie das Repository zu GitHub pushen — für CI ist keine zusätzliche Einrichtung erforderlich, und für CD ist nur ein einziges Secret nötig.
|
||||
@@ -198,14 +165,6 @@ export default defineApplication({
|
||||
|
||||
Siehe das [defineApplication-Akkordeon](/l/de/developers/extend/apps/building#defineentity-functions) auf der Seite Building Apps für die vollständige Liste der Marktplatzfelder (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl` usw.).
|
||||
|
||||
#### Empfohlene Abmessungen für Screenshots
|
||||
|
||||
Der Marktplatz stellt `screenshots` in einem festen `8:5`-Container dar (zum Beispiel `1600×1000 px`).
|
||||
|
||||
<Note>
|
||||
Screenshots mit beliebigem Seitenverhältnis werden vollständig angezeigt und niemals beschnitten, aber alles, was deutlich höher oder schmaler als `8:5` ist, zeigt an den Seiten leere Balken.
|
||||
</Note>
|
||||
|
||||
### Veröffentlichen
|
||||
|
||||
```bash filename="Terminal"
|
||||
@@ -225,9 +184,9 @@ Der Twenty-Server synchronisiert seinen Marktplatzkatalog **stündlich** aus der
|
||||
Sie können die Synchronisierung sofort auslösen, anstatt zu warten:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty server catalog-sync
|
||||
yarn twenty catalog-sync
|
||||
# To target a specific remote:
|
||||
# yarn twenty server catalog-sync --remote production
|
||||
# yarn twenty catalog-sync --remote production
|
||||
```
|
||||
|
||||
Die im Marktplatz angezeigten Metadaten stammen aus Ihrer `defineApplication()`-Konfiguration — Felder wie `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl` und `termsUrl`.
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
---
|
||||
title: Conexões
|
||||
description: Permita que seu aplicativo aja em nome de um usuário em serviços de terceiros via OAuth.
|
||||
icon: plug
|
||||
---
|
||||
|
||||
Conexões são credenciais que um usuário mantém para um serviço externo (Linear, GitHub, Slack, ...). Seu app declara **como** essas credenciais são obtidas — um **provedor de conexão** — e as consome em tempo de execução para fazer chamadas autenticadas à API de terceiros.
|
||||
|
||||
Atualmente, apenas o OAuth 2.0 tem suporte. Tipos de credenciais futuros (tokens de acesso pessoal, chaves de API, autenticação básica) serão conectados à mesma interface — apps que já usam `defineConnectionProvider({ type: 'oauth', ... })` não precisarão migrar.
|
||||
|
||||
<AccordionGroup>
|
||||
|
||||
<Accordion title="defineConnectionProvider" description="Declare como as conexões do seu app são obtidas">
|
||||
|
||||
Um provedor de conexão descreve o handshake OAuth de que seu app precisa. O usuário clica em "Adicionar conexão" nas configurações do seu app, conclui a tela de consentimento do provedor e uma linha `ConnectedAccount` é criada no seu workspace.
|
||||
|
||||
Uma configuração funcional precisa de **dois arquivos** — o provedor de conexão e uma declaração correspondente de `serverVariables` em `defineApplication` que contém as credenciais do cliente OAuth.
|
||||
|
||||
```ts src/connection-providers/linear-connection.ts
|
||||
import { defineConnectionProvider } from 'twenty-sdk/define';
|
||||
|
||||
export default defineConnectionProvider({
|
||||
universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
|
||||
name: 'linear',
|
||||
displayName: 'Linear',
|
||||
icon: 'IconBrandLinear',
|
||||
type: 'oauth',
|
||||
oauth: {
|
||||
authorizationEndpoint: 'https://linear.app/oauth/authorize',
|
||||
tokenEndpoint: 'https://api.linear.app/oauth/token',
|
||||
scopes: ['read', 'write'],
|
||||
// These must match keys in `defineApplication.serverVariables` below.
|
||||
clientIdVariable: 'LINEAR_CLIENT_ID',
|
||||
clientSecretVariable: 'LINEAR_CLIENT_SECRET',
|
||||
// Optional: defaults to 'json'. Some providers (Linear, Slack) want
|
||||
// 'form-urlencoded' for the token request.
|
||||
tokenRequestContentType: 'form-urlencoded',
|
||||
// Optional: defaults to true. Disable only if the provider rejects PKCE.
|
||||
usePkce: false,
|
||||
// Optional: extra query params on the authorize URL.
|
||||
// authorizationParams: { prompt: 'consent' },
|
||||
// Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
|
||||
// revokeEndpoint: 'https://example.com/oauth/revoke',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts src/application.config.ts
|
||||
import { defineApplication } from 'twenty-sdk/define';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '...',
|
||||
displayName: 'Linear',
|
||||
description: 'Connect Linear to Twenty.',
|
||||
defaultRoleUniversalIdentifier: '...',
|
||||
// OAuth client credentials live on the app registration (one OAuth app per
|
||||
// Twenty server, configured by the admin) — not per-workspace. Declare them
|
||||
// as serverVariables so the admin can fill them in once for all installs.
|
||||
serverVariables: {
|
||||
LINEAR_CLIENT_ID: {
|
||||
description: 'OAuth client ID from your Linear OAuth application.',
|
||||
isSecret: false,
|
||||
isRequired: true,
|
||||
},
|
||||
LINEAR_CLIENT_SECRET: {
|
||||
description: 'OAuth client secret from your Linear OAuth application.',
|
||||
isSecret: true,
|
||||
isRequired: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Pontos-chave:
|
||||
|
||||
* `name` é a string de identificador exclusivo usada em `listConnections({ providerName })` (kebab-case, deve corresponder a `^[a-z][a-z0-9-]*$`).
|
||||
* `displayName` aparece na aba de configurações do app e na lista de ferramentas de IA.
|
||||
* `clientIdVariable` / `clientSecretVariable` são **nomes**, não valores — devem corresponder às chaves declaradas em `defineApplication.serverVariables`. Os `client_id` e `client_secret` reais são inseridos pelo administrador do servidor por meio da interface de registro do app e nunca são versionados no seu repositório.
|
||||
* Use `serverVariables` (não `applicationVariables`) — as credenciais OAuth são do servidor como um todo e há um app OAuth por servidor do Twenty.
|
||||
* Até que ambos os `serverVariables` sejam preenchidos, a aba de configurações do app mostra uma dica "precisa de administrador do servidor" e o botão "Adicionar conexão" fica desativado.
|
||||
* `type: 'oauth'` é o único valor compatível atualmente. O discriminador é compatível com versões futuras: tipos futuros (`'pat'`, `'api-key'`, ...) adicionarão novos blocos de subconfiguração ao lado de `oauth`.
|
||||
|
||||
O URL de callback do OAuth que seu provedor precisa adicionar à lista de permissões é:
|
||||
|
||||
```
|
||||
https://<your-twenty-server>/apps/oauth/callback
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="listConnections / getConnection" description="Use conexões a partir de uma função de lógica">
|
||||
|
||||
Dentro de um handler de função de lógica, `listConnections({ providerName })` retorna as linhas `ConnectedAccount` deste app para o provedor fornecido, com tokens de acesso atualizados.
|
||||
|
||||
```ts src/logic-functions/handlers/create-linear-issue-handler.ts
|
||||
import { listConnections } from 'twenty-sdk/logic-function';
|
||||
|
||||
export const createLinearIssueHandler = async (input: {
|
||||
teamId?: string;
|
||||
title?: string;
|
||||
}) => {
|
||||
if (!input.teamId || !input.title) {
|
||||
return { success: false, error: 'teamId and title are required' };
|
||||
}
|
||||
|
||||
const connections = await listConnections({ providerName: 'linear' });
|
||||
|
||||
// Workspace-shared credentials win when present; fall back to the first
|
||||
// user-visibility one. For HTTP-route triggers you typically pick the
|
||||
// request user's connection via event.userWorkspaceId instead.
|
||||
const connection =
|
||||
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
|
||||
|
||||
if (!connection) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
'Linear is not connected. Open the app settings and click "Add connection".',
|
||||
};
|
||||
}
|
||||
|
||||
// Use connection.accessToken to call the third-party API.
|
||||
const response = await fetch('https://api.linear.app/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${connection.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
|
||||
}),
|
||||
});
|
||||
|
||||
return { success: response.ok };
|
||||
};
|
||||
```
|
||||
|
||||
Cada conexão tem:
|
||||
|
||||
| Campo | Descrição |
|
||||
| ----------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `id` | ID de linha exclusivo; passe para `getConnection(id)` para buscar novamente um único registro |
|
||||
| `visibilidade` | `'user'` (privada para um membro do workspace) ou `'workspace'` (compartilhada com todos os membros) |
|
||||
| `escopos` | Permissões OAuth concedidas pelo provedor de origem (distintas de `visibility` — não têm relação) |
|
||||
| `userWorkspaceId` | O id de userWorkspace do proprietário — útil para selecionar "a conexão do usuário da requisição" em gatilhos de rota HTTP |
|
||||
| `accessToken` | Token de acesso OAuth recente (atualizado automaticamente se estiver expirado) |
|
||||
| `name` / `handle` | O nome de exibição da conexão (derivado automaticamente no callback do OAuth, renomeável pelo usuário) |
|
||||
| `authFailedAt` | Definido quando a atualização mais recente falhou; o usuário deve reconectar |
|
||||
|
||||
Pontos-chave:
|
||||
|
||||
* Passe `{ providerName }` para filtrar por provedor; omita para obter todas as conexões que este app possui em todos os provedores.
|
||||
* O servidor atualiza transparentemente o token de acesso antes de retornar. Seu handler sempre vê um token utilizável (ou `authFailedAt` definido).
|
||||
* `getConnection(id)` é o equivalente de uma única linha.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Visibilidade por usuário vs. compartilhada no workspace" description="Como os usuários escolhem entre credenciais privadas e compartilhadas">
|
||||
|
||||
Quando um usuário clica em "Adicionar conexão", é solicitado que escolha uma visibilidade:
|
||||
|
||||
* **Apenas para mim** — a credencial é privada para o usuário que a conectou. Qualquer função de lógica chamada em seu nome (gatilho de rota HTTP com `isAuthRequired: true`) a vê; gatilhos cron e eventos de banco de dados não.
|
||||
* **Compartilhada no workspace** — qualquer membro do workspace pode usar a credencial. Gatilhos de cron / banco de dados também a veem, pois não há um usuário da requisição.
|
||||
|
||||
Use a adequada para cada handler:
|
||||
|
||||
```ts
|
||||
// HTTP-route trigger — prefer the request user's own connection.
|
||||
const conn =
|
||||
connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
|
||||
connections.find((c) => c.visibility === 'workspace');
|
||||
|
||||
// Cron trigger — no request user; only shared credentials are sensible.
|
||||
const conn = connections.find((c) => c.visibility === 'workspace');
|
||||
```
|
||||
|
||||
Várias conexões por (usuário, provedor) são permitidas, então o mesmo usuário pode manter "Linear pessoal" e "Linear de trabalho" lado a lado.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Configuração única do provedor" description="Registre seu app OAuth no serviço de terceiros">
|
||||
|
||||
Para cada provedor de conexão, o administrador do servidor precisa primeiro registrar um app OAuth no serviço de terceiros.
|
||||
|
||||
1. Acesse as configurações de desenvolvedor do provedor (por exemplo, https://linear.app/settings/api/applications/new).
|
||||
2. Defina a **URI de redirecionamento** como `\<SERVER_URL>/apps/oauth/callback`.
|
||||
3. Copie o **ID do cliente** e o **Segredo do cliente** gerados.
|
||||
4. Abra o app instalado no Twenty como administrador do servidor → defina os valores nos `serverVariables` correspondentes.
|
||||
5. Os membros do workspace podem então adicionar conexões na seção **Conexões** de cada app.
|
||||
|
||||
</Accordion>
|
||||
|
||||
</AccordionGroup>
|
||||
@@ -77,6 +77,7 @@ export default defineApplication({
|
||||
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
|
||||
displayName: 'My Twenty App',
|
||||
description: 'My first Twenty app',
|
||||
icon: 'IconWorld',
|
||||
applicationVariables: {
|
||||
DEFAULT_RECIPIENT_NAME: {
|
||||
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
|
||||
|
||||
@@ -15,7 +15,7 @@ Os componentes de front-end podem ser renderizados em dois locais dentro do Twen
|
||||
|
||||
## Exemplo básico
|
||||
|
||||
A maneira mais rápida de ver um componente de front-end em ação é registrá-lo como um **item do menu de comando**. Use `defineCommandMenuItem` em um arquivo separado para fazer o componente aparecer como um botão de ação rápida no canto superior direito da página:
|
||||
A maneira mais rápida de ver um componente de front-end em ação é registrá-lo como um **comando**. Adicionar um campo `command` com `isPinned: true` faz com que ele apareça como um botão de ação rápida no canto superior direito da página — não é necessário layout de página:
|
||||
|
||||
```tsx src/front-components/hello-world.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
@@ -34,20 +34,14 @@ export default defineFrontComponent({
|
||||
name: 'hello-world',
|
||||
description: 'A simple front component',
|
||||
component: HelloWorld,
|
||||
});
|
||||
```
|
||||
|
||||
```ts src/command-menu-items/hello-world.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
|
||||
shortLabel: 'Hello',
|
||||
label: 'Hello World',
|
||||
icon: 'IconBolt',
|
||||
isPinned: true,
|
||||
availabilityType: 'GLOBAL',
|
||||
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
|
||||
command: {
|
||||
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
|
||||
shortLabel: 'Hello',
|
||||
label: 'Hello World',
|
||||
icon: 'IconBolt',
|
||||
isPinned: true,
|
||||
availabilityType: 'GLOBAL',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -61,13 +55,14 @@ Clique nele para renderizar o componente inline.
|
||||
|
||||
## Campos de configuração
|
||||
|
||||
| Campo | Obrigatório | Descrição |
|
||||
| --------------------- | ----------- | ---------------------------------------------------------------------------- |
|
||||
| `universalIdentifier` | Sim | ID único e estável para este componente |
|
||||
| `component` | Sim | Uma função de componente React |
|
||||
| `name` | Não | Nome de Exibição |
|
||||
| `description` | Não | Descrição do que o componente faz |
|
||||
| `isHeadless` | Não | Defina como `true` se o componente não tiver interface visível (veja abaixo) |
|
||||
| Campo | Obrigatório | Descrição |
|
||||
| --------------------- | ----------- | ----------------------------------------------------------------------------------------- |
|
||||
| `universalIdentifier` | Sim | ID único e estável para este componente |
|
||||
| `component` | Sim | Uma função de componente React |
|
||||
| `name` | Não | Nome de Exibição |
|
||||
| `description` | Não | Descrição do que o componente faz |
|
||||
| `isHeadless` | Não | Defina como `true` se o componente não tiver interface visível (veja abaixo) |
|
||||
| `command` | Não | Registre o componente como um comando (veja [opções de comando](#command-options) abaixo) |
|
||||
|
||||
## Colocando um componente de front-end em uma página
|
||||
|
||||
@@ -146,17 +141,11 @@ export default defineFrontComponent({
|
||||
description: 'Creates a task from the command menu',
|
||||
component: RunAction,
|
||||
isHeadless: true,
|
||||
});
|
||||
```
|
||||
|
||||
```ts src/command-menu-items/run-action.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
|
||||
label: 'Run my action',
|
||||
icon: 'IconPlayerPlay',
|
||||
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
|
||||
command: {
|
||||
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
|
||||
label: 'Run my action',
|
||||
icon: 'IconPlayerPlay',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -188,6 +177,11 @@ export default defineFrontComponent({
|
||||
description: 'Deletes a draft with confirmation',
|
||||
component: DeleteDraft,
|
||||
isHeadless: true,
|
||||
command: {
|
||||
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
|
||||
label: 'Delete draft',
|
||||
icon: 'IconTrash',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -226,13 +220,12 @@ export default defineFrontComponent({
|
||||
|
||||
Hooks disponíveis:
|
||||
|
||||
| Hook | Retorna | Descrição |
|
||||
| --------------------------------------------- | ------------------ | ----------------------------------------------------------------------------------- |
|
||||
| `useUserId()` | `string` ou `null` | O ID do usuário atual |
|
||||
| `useSelectedRecordIds()` | `string[]` | Todos os IDs dos registros selecionados (array vazio se nenhum estiver selecionado) |
|
||||
| `useRecordId()` | `string` ou `null` | **Obsoleto.** Use `useSelectedRecordIds()` em vez disso |
|
||||
| `useFrontComponentId()` | `string` | O ID desta instância do componente |
|
||||
| `useFrontComponentExecutionContext(selector)` | varia | Acesse o contexto de execução completo com uma função seletora |
|
||||
| Hook | Retorna | Descrição |
|
||||
| --------------------------------------------- | ------------------ | ------------------------------------------------------------------ |
|
||||
| `useUserId()` | `string` ou `null` | O ID do usuário atual |
|
||||
| `useRecordId()` | `string` ou `null` | O ID do registro atual (quando colocado em uma página de registro) |
|
||||
| `useFrontComponentId()` | `string` | O ID desta instância do componente |
|
||||
| `useFrontComponentExecutionContext(selector)` | varia | Acesse o contexto de execução completo com uma função seletora |
|
||||
|
||||
## API de comunicação do host
|
||||
|
||||
@@ -293,84 +286,14 @@ export default defineFrontComponent({
|
||||
});
|
||||
```
|
||||
|
||||
### Trabalhando com vários registros
|
||||
## Opções de comando
|
||||
|
||||
Use `useSelectedRecordIds()` para lidar com vários registros selecionados. Isso é útil para operações em lote:
|
||||
|
||||
```tsx src/front-components/bulk-export.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { useSelectedRecordIds, numberOfSelectedRecords } from 'twenty-sdk/front-component';
|
||||
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
const BulkExport = () => {
|
||||
const selectedRecordIds = useSelectedRecordIds();
|
||||
|
||||
const handleExport = async () => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
for (const recordId of selectedRecordIds) {
|
||||
await client.mutation({
|
||||
updateTask: {
|
||||
__args: { id: recordId, data: { exported: true } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await enqueueSnackbar({
|
||||
message: `Exported ${selectedRecordIds.length} records`,
|
||||
variant: 'success',
|
||||
});
|
||||
|
||||
await closeSidePanel();
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<p>Export {selectedRecordIds.length} selected record(s)?</p>
|
||||
<button onClick={handleExport}>Export</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
|
||||
name: 'bulk-export',
|
||||
description: 'Export selected records',
|
||||
component: BulkExport,
|
||||
command: {
|
||||
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
|
||||
label: 'Bulk Export',
|
||||
availabilityType: 'RECORD_SELECTION',
|
||||
conditionalAvailabilityExpression: numberOfSelectedRecords > 0,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## defineCommandMenuItem
|
||||
|
||||
Use `defineCommandMenuItem` para registrar um componente de front-end no menu de comando (Cmd+K). Se `isPinned` for `true`, ele também aparece como um botão de ação rápida no canto superior direito da página.
|
||||
|
||||
```ts src/command-menu-items/open-dashboard.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
label: 'Open Dashboard',
|
||||
shortLabel: 'Dashboard',
|
||||
icon: 'IconLayoutDashboard',
|
||||
isPinned: true,
|
||||
availabilityType: 'GLOBAL',
|
||||
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
|
||||
});
|
||||
```
|
||||
Adicionar um campo `command` a `defineFrontComponent` registra o componente no menu de comandos (Cmd+K). Se `isPinned` for `true`, ele também aparece como um botão de ação rápida no canto superior direito da página.
|
||||
|
||||
| Campo | Obrigatório | Descrição |
|
||||
| --------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `universalIdentifier` | Sim | ID exclusivo e estável para o comando |
|
||||
| `label` | Sim | Rótulo completo exibido no menu de comandos (Cmd+K) |
|
||||
| `frontComponentUniversalIdentifier` | Sim | O `universalIdentifier` do componente de front-end que este comando abre |
|
||||
| `shortLabel` | Não | Rótulo mais curto exibido no botão fixado de ação rápida |
|
||||
| `icon` | Não | Nome do ícone exibido ao lado do rótulo (por exemplo, `'IconBolt'`, `'IconSend'`) |
|
||||
| `isPinned` | Não | Quando `true`, mostra o comando como um botão de ação rápida no canto superior direito da página |
|
||||
@@ -382,23 +305,30 @@ export default defineCommandMenuItem({
|
||||
|
||||
O campo `conditionalAvailabilityExpression` permite controlar quando um comando é visível com base no contexto da página atual. Importe variáveis tipadas e operadores de `twenty-sdk` para construir expressões:
|
||||
|
||||
```ts src/command-menu-items/bulk-update.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
```tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
pageType,
|
||||
numberOfSelectedRecords,
|
||||
objectPermissions,
|
||||
everyEquals,
|
||||
isDefined,
|
||||
} from 'twenty-sdk/front-component';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
label: 'Bulk Update',
|
||||
availabilityType: 'RECORD_SELECTION',
|
||||
frontComponentUniversalIdentifier: '...',
|
||||
conditionalAvailabilityExpression: everyEquals(
|
||||
objectPermissions,
|
||||
'canUpdateObjectRecords',
|
||||
true,
|
||||
),
|
||||
name: 'bulk-action',
|
||||
component: BulkAction,
|
||||
command: {
|
||||
universalIdentifier: '...',
|
||||
label: 'Bulk Update',
|
||||
availabilityType: 'RECORD_SELECTION',
|
||||
conditionalAvailabilityExpression: everyEquals(
|
||||
objectPermissions,
|
||||
'canUpdateObjectRecords',
|
||||
true,
|
||||
),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -4,52 +4,48 @@ icon: rocket
|
||||
description: Crie seu primeiro app do Twenty em minutos.
|
||||
---
|
||||
|
||||
## O que são aplicativos?
|
||||
|
||||
Os aplicativos permitem que você estenda o Twenty com objetos e campos personalizados, funções lógicas, componentes de front-end, habilidades de IA e mais — tudo gerenciado como código. Em vez de configurar tudo pela UI, você define seu modelo de dados e a lógica em TypeScript e implanta em um ou mais workspaces.
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
* **Node.js 24+** — [Baixar](https://nodejs.org/)
|
||||
* **Yarn 4** — Vem com o Node.js via Corepack. Ative-o: `corepack enable`
|
||||
* **Docker** — [Baixar](https://www.docker.com/products/docker-desktop/). Necessário para executar um servidor Twenty local. Ignore se você já tiver o Twenty em execução em outro lugar.
|
||||
Antes de começar, verifique se o seguinte está instalado na sua máquina:
|
||||
|
||||
A criação de um aplicativo Twenty tem três fases. A ferramenta de scaffolding as reúne em um único comando do fluxo ideal, mas cada fase é um conceito separado — quando algo falha, saber em que fase você está indica o que corrigir.
|
||||
* **Node.js 24+** — [Baixe aqui](https://nodejs.org/)
|
||||
* **Yarn 4** — Vem com o Node.js via Corepack. Ative-o executando `corepack enable`
|
||||
* **Docker** — [Baixe aqui](https://www.docker.com/products/docker-desktop/). Necessário para executar uma instância local do Twenty. Não é necessário se você já tiver um servidor Twenty em execução.
|
||||
|
||||
| Fase | O que você faz | Ferramenta | Resultado |
|
||||
| --------------------------- | -------------------------------------------------- | ----------------------------- | ------------------------------------- |
|
||||
| **1. Criar scaffolding** | Gerar o código-fonte do aplicativo | `npx create-twenty-app` | Um projeto TypeScript em disco |
|
||||
| **2. Executar um servidor** | Iniciar um servidor Twenty para o qual sincronizar | Docker + `yarn twenty server` | Uma instância Twenty em execução |
|
||||
| **3. Sincronizar** | Sincronize seu código em tempo real com o servidor | `yarn twenty dev` | Suas alterações aparecem na interface |
|
||||
## Crie seu primeiro aplicativo
|
||||
|
||||
---
|
||||
### Gere o scaffold do seu aplicativo
|
||||
|
||||
## Fase 1 — Fazer scaffolding do seu projeto
|
||||
|
||||
Crie um novo aplicativo a partir do modelo:
|
||||
Abra um terminal e execute:
|
||||
|
||||
```bash filename="Terminal"
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
```
|
||||
|
||||
Você será solicitado a informar um nome e uma descrição — pressione **Enter** para aceitar os valores padrão. Isso gera um projeto TypeScript em `my-twenty-app/` com um `application-config.ts` inicial, um papel padrão, um fluxo de trabalho de CI e um teste de integração.
|
||||
Será solicitado que você informe um nome e uma descrição para o seu aplicativo. Pressione **Enter** para aceitar os valores padrão.
|
||||
|
||||
**Após esta fase:** você tem o código-fonte de um aplicativo na sua máquina. Ele ainda não está em execução — isso é a Fase 2.
|
||||
Isso cria uma nova pasta chamada `my-twenty-app` com tudo de que você precisa.
|
||||
|
||||
---
|
||||
### Configure uma instância local do Twenty
|
||||
|
||||
## Fase 2 — Executar um servidor Twenty local
|
||||
|
||||
Seu aplicativo precisa de um servidor Twenty para o qual sincronizar. O servidor é uma instância completa do Twenty — interface, API GraphQL, PostgreSQL — executando localmente no Docker. Seu código local envia suas definições para esse servidor, o que faz com que elas apareçam na interface.
|
||||
|
||||
A ferramenta de scaffolding oferece iniciar um para você:
|
||||
O gerador de scaffold perguntará:
|
||||
|
||||
> **Você gostaria de configurar uma instância local do Twenty?**
|
||||
|
||||
* **Sim (recomendado)** — baixa a imagem Docker `twentycrm/twenty-app-dev` e a inicia na porta `2020`. Certifique-se de que o Docker esteja em execução primeiro.
|
||||
* **Não** — escolha isto se você já tiver um servidor Twenty ao qual deseja se conectar. Você pode conectá-lo depois com `yarn twenty remote add`.
|
||||
* **Digite `yes`** (recomendado) — Isso baixa a imagem Docker `twenty-app-dev` e inicia um servidor Twenty local na porta `2020`. Certifique-se de que o Docker esteja em execução antes de continuar.
|
||||
* **Digite `no`** — Escolha esta opção se você já tiver um servidor Twenty em execução localmente.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/start-instance.png" alt="Deve iniciar instância local?" />
|
||||
</div>
|
||||
|
||||
Quando o servidor estiver ativo, um navegador será aberto para login. Use a conta de demonstração pré-configurada:
|
||||
### Faça login no seu espaço de trabalho
|
||||
|
||||
Em seguida, uma janela do navegador será aberta com a página de login do Twenty. Faça login com a conta de demonstração pré-configurada:
|
||||
|
||||
* **E-mail:** `tim@apple.dev`
|
||||
* **Senha:** `tim@apple.dev`
|
||||
@@ -58,81 +54,89 @@ Quando o servidor estiver ativo, um navegador será aberto para login. Use a con
|
||||
<img src="/images/docs/developers/extends/apps/login.png" alt="Tela de login do Twenty" />
|
||||
</div>
|
||||
|
||||
Clique em **Authorize** na próxima tela — isso dá à CLI acesso ao seu espaço de trabalho.
|
||||
### Autorize o aplicativo
|
||||
|
||||
Após fazer login, você verá uma tela de autorização. Isso permite que seu aplicativo interaja com seu espaço de trabalho.
|
||||
|
||||
Clique em **Authorize** para continuar.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/authorize.png" alt="Tela de autorização da CLI do Twenty" />
|
||||
</div>
|
||||
|
||||
Seu terminal confirmará que tudo está configurado.
|
||||
Depois de autorizado, seu terminal confirmará que tudo está configurado.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/scaffolded.png" alt="Scaffold do aplicativo criado com sucesso" />
|
||||
</div>
|
||||
|
||||
**Após esta fase:** você tem um servidor Twenty em execução em [http://localhost:2020](http://localhost:2020) com sua CLI autorizada a sincronizar com ele.
|
||||
### Comece a desenvolver
|
||||
|
||||
<Note>
|
||||
Se o Docker não estiver instalado ou em execução, a ferramenta de scaffolding informará o comando de inicialização correto para o seu sistema operacional. Quando o Docker estiver ativo, você pode retomar com `yarn twenty server start` — sem necessidade de recriar o scaffolding.
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Fase 3 — Sincronizar suas alterações
|
||||
|
||||
Este é o ciclo interno no qual você passará a maior parte do tempo.
|
||||
Entre na nova pasta do seu aplicativo e inicie o servidor de desenvolvimento:
|
||||
|
||||
```bash filename="Terminal"
|
||||
cd my-twenty-app
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
Isso monitora `src/`, recompila a cada alteração e sincroniza o resultado com o servidor. Edite um arquivo, salve e, em um segundo, o servidor refletirá a alteração. Você verá um painel de status em tempo real no seu terminal.
|
||||
Isso observa seus arquivos-fonte, recompila a cada alteração e sincroniza seu aplicativo com o servidor Twenty local automaticamente. Você deverá ver um painel de status em tempo real no seu terminal.
|
||||
|
||||
Para uma saída mais detalhada (logs de build, solicitações de sincronização, rastros de erro), adicione `--verbose`.
|
||||
Para uma saída mais detalhada (logs de build, solicitações de sincronização, rastros de erro), use a flag `--verbose`:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev --verbose
|
||||
```
|
||||
|
||||
<Warning>
|
||||
O modo de desenvolvimento só está disponível em instâncias do Twenty em modo de desenvolvimento (`NODE_ENV=development`). Instâncias de produção rejeitam solicitações de sincronização de desenvolvimento. Use `yarn twenty deploy` para fazer o deploy em servidores de produção — veja [Publicando aplicativos](/l/pt/developers/extend/apps/publishing) para detalhes.
|
||||
</Warning>
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/dev.png" alt="Saída do terminal no modo de desenvolvimento" />
|
||||
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Saída do terminal no modo de desenvolvimento" />
|
||||
</div>
|
||||
|
||||
Abra [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer). Você deverá ver seu aplicativo em **Your Apps**.
|
||||
#### Sincronização única com `yarn twenty dev --once`
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="Lista Your Apps exibindo My twenty app" />
|
||||
</div>
|
||||
|
||||
Clique em **My twenty app** para ver seu **registro do aplicativo** — um registro em nível de servidor que descreve seu aplicativo (nome, identificador, credenciais OAuth, origem). Um registro pode ser instalado em vários espaços de trabalho no mesmo servidor.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="Detalhes do registro do aplicativo" />
|
||||
</div>
|
||||
|
||||
Clique em **View installed app** para ver a instalação no espaço de trabalho. A aba **About** mostra a versão e as opções de gerenciamento.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Aplicação instalada" />
|
||||
</div>
|
||||
|
||||
**Após esta fase:** você tem um ciclo de desenvolvimento em tempo real. Edite qualquer arquivo em `src/` e ele aparecerá na interface.
|
||||
|
||||
### Sincronização única para CI e scripts
|
||||
|
||||
Passe `--once` para executar uma única compilação + sincronização e sair — mesmo pipeline, sem watcher:
|
||||
Se você não quiser um monitor em execução em segundo plano (por exemplo, em um pipeline de CI, um hook do git ou um fluxo de trabalho com script), passe a flag `--once`. Ele executa o mesmo pipeline que `yarn twenty dev` — gerar o manifesto, empacotar arquivos, fazer upload, sincronizar, regenerar o cliente de API tipado — mas **encerra assim que a sincronização for concluída**:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev --once
|
||||
```
|
||||
|
||||
| Comando | Comportamento | Quando usar |
|
||||
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
||||
| `yarn twenty dev` | Monitora e ressincroniza a cada alteração. Fica em execução até você interrompê-lo. | Desenvolvimento local interativo. |
|
||||
| `yarn twenty dev --once` | Executa uma única compilação + sincronização e, em seguida, encerra com o código `0` em caso de sucesso ou `1` em caso de falha. | Scripts, CI, hooks de pre-commit, agentes de IA e fluxos de trabalho com script. |
|
||||
| Comando | Comportamento | Quando usar |
|
||||
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| `yarn twenty dev` | Monitora seus arquivos-fonte e ressincroniza a cada alteração. Continua em execução até você interrompê-lo. | Desenvolvimento local interativo — você quer o painel de status em tempo real e um ciclo de feedback instantâneo. |
|
||||
| `yarn twenty dev --once` | Executa uma única compilação + sincronização e, em seguida, encerra com o código `0` em caso de sucesso ou `1` em caso de falha. | Scripts, CI, hooks de pre-commit, agentes de IA e qualquer fluxo de trabalho não interativo. |
|
||||
|
||||
Ambos os modos precisam de um servidor em modo de desenvolvimento e de um remoto autenticado.
|
||||
Ambos os modos exigem um servidor Twenty em execução no modo de desenvolvimento e um remoto autenticado — aplicam-se os mesmos pré-requisitos.
|
||||
|
||||
<Warning>
|
||||
O modo de desenvolvimento só está disponível em instâncias do Twenty em modo de desenvolvimento (`NODE_ENV=development`). Instâncias de produção rejeitam solicitações de sincronização de desenvolvimento — use `yarn twenty deploy` para implantar em servidores de produção. Veja [Publicação de aplicativos](/l/pt/developers/extend/apps/publishing).
|
||||
</Warning>
|
||||
### Veja seu aplicativo no Twenty
|
||||
|
||||
Abra [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer) no seu navegador. Navegue até **Settings > Apps** e selecione a aba **Developer**. Você deverá ver seu aplicativo listado em **Your Apps**:
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="Lista Your Apps exibindo My twenty app" />
|
||||
</div>
|
||||
|
||||
Clique em **My twenty app** para abrir o seu **registro do aplicativo**. Um registro é um registro em nível de servidor que descreve seu aplicativo — seu nome, identificador exclusivo, credenciais OAuth e origem (local, npm ou tarball). Ele reside no servidor, não dentro de nenhum espaço de trabalho específico. Quando você instala um aplicativo em um espaço de trabalho, o Twenty cria uma **aplicação** com escopo do espaço de trabalho que aponta para esse registro. Um registro pode ser instalado em vários espaços de trabalho no mesmo servidor.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="Detalhes do registro do aplicativo" />
|
||||
</div>
|
||||
|
||||
Clique em **View installed app** para ver o aplicativo instalado. A aba **About** mostra a versão atual e as opções de gerenciamento:
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Aplicativo instalado — aba About" />
|
||||
</div>
|
||||
|
||||
Altere para a aba **Content** para ver tudo o que seu aplicativo oferece — objetos, campos, funções de lógica e agentes:
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-4.png" alt="Aplicativo instalado — aba Content" />
|
||||
</div>
|
||||
|
||||
Tudo pronto! Edite qualquer arquivo em `src/` e as alterações serão detectadas automaticamente.
|
||||
|
||||
---
|
||||
|
||||
@@ -142,110 +146,115 @@ Os aplicativos são compostos por **entidades** — cada uma definida como um ar
|
||||
|
||||
| Entidade | O que faz |
|
||||
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| **Objetos e campos** | Modelos de dados personalizados (Cartão postal, Fatura etc.) com campos tipados |
|
||||
| **Objetos e campos** | Defina modelos de dados personalizados (como cartão postal, fatura) com campos tipados |
|
||||
| **Funções lógicas** | Funções TypeScript do lado do servidor acionadas por rotas HTTP, agendamentos do cron ou eventos de banco de dados |
|
||||
| **Componentes de front-end** | Componentes React que são renderizados na UI do Twenty (painel lateral, widgets, menu de comandos) |
|
||||
| **Habilidades e agentes** | Recursos de IA — instruções reutilizáveis e assistentes autônomos |
|
||||
| **Exibições e navegação** | Exibições de lista pré-configuradas e itens de menu da barra lateral |
|
||||
| **Exibições e navegação** | Exibições de lista pré-configuradas e itens de menu da barra lateral para seus objetos |
|
||||
| **Layouts de página** | Páginas de detalhes de registros personalizadas com abas e widgets |
|
||||
|
||||
Referência completa: [Criando aplicativos](/l/pt/developers/extend/apps/building).
|
||||
Acesse [Criando aplicativos](/l/pt/developers/extend/apps/building) para um guia detalhado sobre cada tipo de entidade.
|
||||
|
||||
---
|
||||
|
||||
## Estrutura do projeto
|
||||
|
||||
A ferramenta de scaffolding gera a seguinte estrutura de arquivos:
|
||||
|
||||
```text filename="my-twenty-app/"
|
||||
my-twenty-app/
|
||||
package.json
|
||||
yarn.lock
|
||||
.gitignore
|
||||
.nvmrc
|
||||
.yarnrc.yml
|
||||
.oxlintrc.json
|
||||
tsconfig.json
|
||||
tsconfig.spec.json # TypeScript config for tests
|
||||
vitest.config.ts # Vitest test runner configuration
|
||||
LLMS.md
|
||||
README.md
|
||||
.github/
|
||||
└── workflows/
|
||||
└── ci.yml # GitHub Actions CI workflow
|
||||
public/ # Public assets (images, fonts, etc.)
|
||||
src/
|
||||
application-config.ts # Required — your app's entry point
|
||||
default-role.ts # Permissions for logic functions
|
||||
constants/
|
||||
universal-identifiers.ts # Auto-generated UUIDs and metadata
|
||||
__tests__/
|
||||
setup-test.ts
|
||||
app-install.integration-test.ts
|
||||
.github/workflows/ci.yml # GitHub Actions
|
||||
public/ # Static assets
|
||||
vitest.config.ts # Test runner config
|
||||
tsconfig.json, tsconfig.spec.json
|
||||
.nvmrc, .yarnrc.yml, .oxlintrc.json
|
||||
README.md, LLMS.md
|
||||
├── application-config.ts # Required — main application configuration
|
||||
├── default-role.ts # Default role for logic functions
|
||||
├── constants/
|
||||
│ └── universal-identifiers.ts # Auto-generated UUIDs and app metadata
|
||||
└── __tests__/
|
||||
├── setup-test.ts # Test setup (server health check, config)
|
||||
└── app-install.integration-test.ts # Integration test
|
||||
```
|
||||
|
||||
| Arquivo / Pasta | Finalidade |
|
||||
| ---------------------------------------- | ------------------------------------------------------------------------ |
|
||||
| `src/application-config.ts` | **Obrigatório.** O principal arquivo de configuração do seu aplicativo. |
|
||||
| `src/default-role.ts` | Papel padrão que controla o que suas funções de lógica podem acessar. |
|
||||
| `src/constants/universal-identifiers.ts` | UUIDs gerados automaticamente e metadados (nome de exibição, descrição). |
|
||||
| `src/__tests__/` | Testes de integração (configuração + teste de exemplo). |
|
||||
| `public/` | Recursos estáticos (imagens, fontes) servidos com seu aplicativo. |
|
||||
|
||||
### Começando a partir de um exemplo
|
||||
|
||||
Use `--example` para começar com um projeto mais completo (objetos personalizados, campos, funções de lógica, componentes de front-end):
|
||||
Para começar a partir de um exemplo mais completo com objetos, campos, funções de lógica, componentes de front-end e mais, use a opção `--example`:
|
||||
|
||||
```bash filename="Terminal"
|
||||
npx create-twenty-app@latest my-twenty-app --example postcard
|
||||
```
|
||||
|
||||
Os exemplos estão em [twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples). Você também pode criar o scaffolding de entidades individuais em um projeto existente com `yarn twenty add` — veja [Criando aplicativos](/l/pt/developers/extend/apps/building#scaffolding-entities-with-yarn-twenty-add).
|
||||
Os exemplos são obtidos do diretório [twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples) no GitHub. Você também pode criar o scaffolding de entidades individuais em um projeto existente com `yarn twenty add` (veja [Criando aplicativos](/l/pt/developers/extend/apps/building#scaffolding-entities-with-yarn-twenty-add)).
|
||||
|
||||
---
|
||||
### Arquivos principais
|
||||
|
||||
## Gerenciando o servidor local
|
||||
| Arquivo / Pasta | Finalidade |
|
||||
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `package.json` | Declara o nome, a versão e as dependências do seu aplicativo. Inclui um script `twenty` para que você possa executar `yarn twenty help` e ver todos os comandos. |
|
||||
| `src/application-config.ts` | **Obrigatório.** O principal arquivo de configuração do seu aplicativo. |
|
||||
| `src/default-role.ts` | Papel padrão que controla o que suas funções de lógica podem acessar. |
|
||||
| `src/constants/universal-identifiers.ts` | UUIDs gerados automaticamente e metadados do aplicativo (nome de exibição, descrição). |
|
||||
| `src/__tests__/` | Testes de integração (configuração + teste de exemplo). |
|
||||
| `public/` | Recursos estáticos (imagens, fontes) servidos com seu aplicativo. |
|
||||
|
||||
Use `yarn twenty server` para controlar o contêiner Twenty local:
|
||||
## Servidor de desenvolvimento local
|
||||
|
||||
| Comando | O que faz |
|
||||
| -------------------------------------- | ------------------------------------------------ |
|
||||
| `yarn twenty server start` | Inicia o servidor (baixa a imagem se necessário) |
|
||||
| `yarn twenty server start --port 3030` | Iniciar em uma porta personalizada |
|
||||
| `yarn twenty server stop` | Interrompe o servidor (preserva os dados) |
|
||||
| `yarn twenty server status` | Mostra a URL, a versão e as credenciais de login |
|
||||
| `yarn twenty server logs` | Transmite os logs do servidor |
|
||||
| `yarn twenty server reset` | Apaga os dados e começa do zero |
|
||||
| `yarn twenty server upgrade` | Baixa a imagem mais recente `twenty-app-dev` |
|
||||
| `yarn twenty server upgrade 2.2.0` | Atualizar para uma versão específica |
|
||||
A ferramenta de scaffolding já iniciou um servidor local do Twenty para você. Para gerenciá-lo depois, use `yarn twenty server`:
|
||||
|
||||
Os dados são persistidos entre reinicializações em dois volumes do Docker (`twenty-app-dev-data` para PostgreSQL, `twenty-app-dev-storage` para arquivos). Use `reset` para apagar tudo.
|
||||
| Comando | Descrição |
|
||||
| -------------------------------------- | ------------------------------------------------------ |
|
||||
| `yarn twenty server start` | Inicia o servidor local (baixa a imagem se necessário) |
|
||||
| `yarn twenty server start --port 3030` | Iniciar em uma porta personalizada |
|
||||
| `yarn twenty server start --test` | Inicie uma instância de teste separada na porta 2021 |
|
||||
| `yarn twenty server stop` | Interrompe o servidor (preserva os dados) |
|
||||
| `yarn twenty server status` | Mostra o status do servidor, a URL e as credenciais |
|
||||
| `yarn twenty server logs` | Transmite os logs do servidor |
|
||||
| `yarn twenty server logs --lines 100` | Mostra as últimas 100 linhas de log |
|
||||
| `yarn twenty server reset` | Exclui todos os dados e inicia do zero |
|
||||
|
||||
### Atualizando a imagem do servidor
|
||||
Os dados são persistidos entre reinicializações em dois volumes do Docker (`twenty-app-dev-data` para PostgreSQL, `twenty-app-dev-storage` para arquivos). Use `reset` para apagar tudo e começar do zero.
|
||||
|
||||
`yarn twenty server upgrade` baixa a imagem mais recente, compara os digests e só recria o contêiner se algo realmente tiver mudado. Os volumes são preservados — apenas o contêiner é substituído. Se uma nova imagem foi baixada e o contêiner estava em execução, a atualização inicia automaticamente um novo contêiner; execute `yarn twenty server start` depois para aguardar até que ele fique saudável.
|
||||
### Executando uma instância de teste
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty server upgrade # Latest
|
||||
yarn twenty server upgrade 2.2.0 # Specific version
|
||||
```
|
||||
Passe `--test` para qualquer comando de `server` para gerenciar uma segunda instância totalmente isolada — útil para executar testes de integração ou experimentar sem tocar nos seus dados principais de desenvolvimento.
|
||||
|
||||
Verifique a versão em execução com `yarn twenty server status` (ele mostra o `APP_VERSION` incorporado ao contêiner).
|
||||
| Comando | Descrição |
|
||||
| ---------------------------------- | ------------------------------------------------------------- |
|
||||
| `yarn twenty server start --test` | Inicia a instância de teste (padrão: porta 2021) |
|
||||
| `yarn twenty server stop --test` | Interrompe a instância de teste |
|
||||
| `yarn twenty server status --test` | Mostra o status da instância de teste, a URL e as credenciais |
|
||||
| `yarn twenty server logs --test` | Transmite os logs da instância de teste |
|
||||
| `yarn twenty server reset --test` | Exclui os dados de teste e inicia do zero |
|
||||
|
||||
### Executando uma instância de teste paralela
|
||||
A instância de teste é executada em seu próprio contêiner Docker (`twenty-app-dev-test`) com volumes dedicados (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) e configuração própria, para que possa ser executada em paralelo com sua instância principal sem conflitos. Combine `--test` com `--port` para substituir a porta padrão 2021.
|
||||
|
||||
Passe `--test` para qualquer comando de `server` para gerenciar uma segunda instância totalmente isolada — útil para testes de integração ou para experimentar sem tocar nos seus dados principais de desenvolvimento:
|
||||
|
||||
| Comando | O que faz |
|
||||
| ----------------------------------- | ------------------------------------------------ |
|
||||
| `yarn twenty server start --test` | Inicia a instância de teste (padrão: porta 2021) |
|
||||
| `yarn twenty server stop --test` | Parar |
|
||||
| `yarn twenty server status --test` | Mostrar seu status |
|
||||
| `yarn twenty server logs --test` | Transmitir seus logs |
|
||||
| `yarn twenty server reset --test` | Apagar seus dados |
|
||||
| `yarn twenty server upgrade --test` | Atualizar sua imagem |
|
||||
|
||||
A instância de teste tem seu próprio contêiner (`twenty-app-dev-test`), volumes (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) e configuração — ela é executada junto com sua instância principal sem conflitos. Combine `--test` com `--port` para substituir 2021.
|
||||
|
||||
---
|
||||
<Note>
|
||||
O servidor requer que o **Docker** esteja em execução. Se você vir um erro "Docker not running", certifique-se de que o Docker Desktop (ou o daemon do Docker) esteja iniciado.
|
||||
</Note>
|
||||
|
||||
## Configuração manual (sem o gerador)
|
||||
|
||||
Ignore a ferramenta de scaffolding se você estiver adicionando o SDK a um projeto existente:
|
||||
Se preferir configurar tudo por conta própria em vez de usar `create-twenty-app`, você pode fazer isso em duas etapas.
|
||||
|
||||
**1. Adicione `twenty-sdk` e `twenty-client-sdk` como dependências:**
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add twenty-sdk twenty-client-sdk
|
||||
```
|
||||
|
||||
Adicione o script ao `package.json`:
|
||||
**2. Adicione um script `twenty` ao seu `package.json`:**
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
@@ -255,19 +264,19 @@ Adicione o script ao `package.json`:
|
||||
}
|
||||
```
|
||||
|
||||
Agora você pode executar `yarn twenty dev`, `yarn twenty server start` e o restante.
|
||||
Agora você pode executar `yarn twenty dev`, `yarn twenty help` e todos os outros comandos.
|
||||
|
||||
<Note>
|
||||
Não instale `twenty-sdk` globalmente — fixe-o por projeto, para que cada aplicativo use sua própria versão.
|
||||
Não instale o `twenty-sdk` globalmente. Use-o sempre como uma dependência local do projeto para que cada projeto possa fixar sua própria versão.
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Resolução de Problemas
|
||||
|
||||
* **Erros do Docker** — Certifique-se de que o Docker Desktop (ou o daemon) esteja em execução antes de `yarn twenty server start`. A mensagem de erro mostrará o comando de inicialização correto para o seu sistema operacional.
|
||||
* **Versão errada do Node** — É necessário 24 ou superior. Verifique com `node -v`.
|
||||
* **Falta o Yarn 4** — Execute `corepack enable`.
|
||||
* **Dependências com problemas** — `rm -rf node_modules && yarn install`.
|
||||
Se você tiver problemas:
|
||||
|
||||
Travou? Peça ajuda no [Discord da Twenty](https://discord.com/channels/1130383047699738754/1130386664812982322).
|
||||
* Certifique-se de que o **Docker está em execução** antes de iniciar o scaffolder com uma instância local.
|
||||
* Certifique-se de que está usando **Node.js 24+** (`node -v` para verificar).
|
||||
* Certifique-se de que o **Corepack está ativado** (`corepack enable`) para que o Yarn 4 esteja disponível.
|
||||
* Tente excluir `node_modules` e executar `yarn install` novamente se as dependências parecerem corrompidas.
|
||||
|
||||
Ainda com dificuldades? Peça ajuda no [Discord da Twenty](https://discord.com/channels/1130383047699738754/1130386664812982322).
|
||||
|
||||
@@ -141,14 +141,11 @@ const handler = async (event: RoutePayload) => {
|
||||
Os nomes dos cabeçalhos são normalizados para minúsculas. Acesse-os usando chaves em minúsculas (por exemplo, `event.headers['content-type']`).
|
||||
</Note>
|
||||
|
||||
#### Expor uma função como ferramenta de IA ou como ação de fluxo de trabalho
|
||||
#### Expor uma função como ferramenta
|
||||
|
||||
As funções de lógica podem ser expostas em duas superfícies, cada uma com seu próprio gatilho:
|
||||
Funções lógicas podem ser expostas como **ferramentas** para agentes de IA e fluxos de trabalho. Quando marcada como ferramenta, uma função fica detectável pelos recursos de IA do Twenty e pode ser usada em automações de fluxos de trabalho.
|
||||
|
||||
* **`toolTriggerSettings`** — torna a função disponível para os recursos de IA do Twenty (chat, MCP, chamadas de função). Usa o JSON Schema padrão, o formato que os LLMs entendem nativamente.
|
||||
* **`workflowActionTriggerSettings`** — torna a função visível como uma etapa no construtor visual de fluxos de trabalho. Usa o `InputSchema` avançado do Twenty para que o construtor possa renderizar editores de campo adequados, seletores de variáveis e rótulos.
|
||||
|
||||
Uma função pode optar por uma, pela outra ou por ambas. Ficam ao lado de `cronTriggerSettings`, `databaseEventTriggerSettings` e `httpRouteTriggerSettings` — mesmo padrão, mesmo formato.
|
||||
Para marcar uma função de lógica como ferramenta, defina `isTool: true`:
|
||||
|
||||
```ts src/logic-functions/enrich-company.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
@@ -178,33 +175,31 @@ export default defineLogicFunction({
|
||||
description: 'Enrich a company record with external data',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
toolTriggerSettings: {},
|
||||
isTool: true,
|
||||
});
|
||||
```
|
||||
|
||||
Pontos-chave:
|
||||
|
||||
* Uma função pode misturar superfícies — declare tanto `toolTriggerSettings` quanto `workflowActionTriggerSettings` para expô-la no chat E no construtor de fluxos de trabalho.
|
||||
* `toolTriggerSettings.inputSchema` e `workflowActionTriggerSettings.inputSchema` são opcionais. Quando omitidos, o construtor de manifestos os infere a partir do código-fonte do handler (JSON Schema para a ferramenta de IA, `InputSchema` do Twenty para a ação de fluxo de trabalho). Forneça um explicitamente quando quiser uma tipagem mais rica — por exemplo, com campos compatíveis com `FieldMetadataType`, como `CURRENCY` ou `RELATION` para o construtor de fluxos de trabalho, ou com campos `description` que o agente de IA pode ler:
|
||||
* Você pode combinar `isTool` com gatilhos — uma função pode ser ao mesmo tempo uma ferramenta (chamável por agentes de IA) e acionada por eventos.
|
||||
* **`toolInputSchema`** (opcional): Um objeto JSON Schema que descreve os parâmetros que sua função aceita. O schema é calculado automaticamente a partir da análise estática do código-fonte, mas você pode defini-lo explicitamente:
|
||||
|
||||
```ts
|
||||
export default defineLogicFunction({
|
||||
...,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
companyName: {
|
||||
type: 'string',
|
||||
description: 'The name of the company to enrich',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
description: 'The company website domain (optional)',
|
||||
},
|
||||
toolInputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
companyName: {
|
||||
type: 'string',
|
||||
description: 'The name of the company to enrich',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
description: 'The company website domain (optional)',
|
||||
},
|
||||
required: ['companyName'],
|
||||
},
|
||||
required: ['companyName'],
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -243,7 +238,7 @@ yarn twenty exec --postInstall
|
||||
```
|
||||
|
||||
Pontos-chave:
|
||||
* As funções de pós-instalação usam `definePostInstallLogicFunction()` — uma variante especializada que omite as configurações de gatilho (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `toolTriggerSettings`, `workflowActionTriggerSettings`).
|
||||
* As funções de pós-instalação usam `definePostInstallLogicFunction()` — uma variante especializada que omite as configurações de gatilho (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
|
||||
* O manipulador recebe um `InstallPayload` com `{ previousVersion?: string; newVersion: string }` — `newVersion` é a versão que está sendo instalada, e `previousVersion` é a versão que foi instalada anteriormente (ou `undefined` em uma instalação nova). Use esses valores para distinguir instalações novas de atualizações e para executar lógica de migração específica da versão.
|
||||
* **Quando o hook é executado**: apenas em instalações novas, por padrão. Passe `shouldRunOnVersionUpgrade: true` se você também quiser que ele seja executado quando o app for atualizado a partir de uma versão anterior. Quando omitida, a flag tem valor padrão `false` e as atualizações ignoram o hook.
|
||||
* **Modelo de execução — assíncrono por padrão, síncrono opcional**: a flag `shouldRunSynchronously` controla *como* a pós-instalação é executada.
|
||||
|
||||
@@ -77,39 +77,6 @@ Tags de pré-lançamento funcionam como esperado: incrementar `1.0.0-rc.1` → `
|
||||
|
||||
{/* TODO: add screenshot of the Upgrade button */}
|
||||
|
||||
### Compatibilidade da versão do servidor
|
||||
|
||||
Se o seu aplicativo usar um recurso introduzido em uma versão específica do servidor Twenty (por exemplo, provedores OAuth adicionados na v2.3.0), você deve declarar a versão mínima do servidor que seu aplicativo requer usando o campo `engines.twenty` em `package.json`:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"name": "twenty-my-app",
|
||||
"version": "1.0.0",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
"twenty": ">=2.3.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
O valor é um [intervalo semver](https://github.com/npm/node-semver#ranges) padrão. Padrões comuns:
|
||||
|
||||
| Intervalo | Significado |
|
||||
| ---------------------------------- | ---------------------------------------------------------- |
|
||||
| `>=2.3.0` | Qualquer servidor a partir de 2.3.0 |
|
||||
| `>=2.3.0 \<3.0.0` | 2.3.0 ou posterior, mas abaixo da próxima versão principal |
|
||||
| `^2.3.0` | O mesmo que `>=2.3.0 \<3.0.0` |
|
||||
|
||||
**O que acontece no momento da implantação e da instalação:**
|
||||
|
||||
* Se `engines.twenty` estiver definido e a versão do servidor de destino não satisfizer o intervalo, a implantação (upload do tarball) ou a instalação será rejeitada com o erro `SERVER_VERSION_INCOMPATIBLE` e uma mensagem indicando tanto o intervalo exigido quanto a versão real do servidor.
|
||||
* Se `engines.twenty` **não estiver definido**, o aplicativo é aceito em qualquer versão do servidor (retrocompatível com os aplicativos existentes).
|
||||
* Se o servidor não tiver `APP_VERSION` configurado, a verificação será ignorada.
|
||||
|
||||
<Note>
|
||||
O servidor realiza a verificação definitiva — ele valida `engines.twenty` tanto no upload do tarball quanto na instalação no workspace. Se você implantar um tarball fora de banda ou instalar a partir do marketplace, o servidor ainda impõe a compatibilidade.
|
||||
</Note>
|
||||
|
||||
## CI/CD automatizado (fluxos de trabalho pré-configurados)
|
||||
|
||||
Os apps gerados com `create-twenty-app` já vêm com dois fluxos de trabalho do GitHub Actions prontos, em `.github/workflows/`. Eles estão prontos para executar assim que você fizer push do repositório para o GitHub — nenhuma configuração extra é necessária para CI, e CD requer apenas um único segredo.
|
||||
@@ -198,14 +165,6 @@ export default defineApplication({
|
||||
|
||||
Veja o [acordeão de defineApplication](/l/pt/developers/extend/apps/building#defineentity-functions) na página Building Apps para a lista completa de campos do marketplace (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl`, etc.).
|
||||
|
||||
#### Dimensões recomendadas para capturas de tela
|
||||
|
||||
O marketplace renderiza `screenshots` em um contêiner fixo de `8:5` (por exemplo, `1600×1000 px`).
|
||||
|
||||
<Note>
|
||||
Capturas de tela de qualquer proporção são exibidas por completo e nunca são cortadas, mas qualquer coisa significativamente mais alta ou mais estreita que `8:5` exibirá faixas vazias nas laterais.
|
||||
</Note>
|
||||
|
||||
### Publicar
|
||||
|
||||
```bash filename="Terminal"
|
||||
@@ -225,9 +184,9 @@ O servidor Twenty sincroniza seu catálogo do marketplace a partir do registro d
|
||||
Você pode acionar a sincronização imediatamente em vez de esperar:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty server catalog-sync
|
||||
yarn twenty catalog-sync
|
||||
# To target a specific remote:
|
||||
# yarn twenty server catalog-sync --remote production
|
||||
# yarn twenty catalog-sync --remote production
|
||||
```
|
||||
|
||||
Os metadados exibidos no marketplace vêm da sua configuração `defineApplication()` — campos como `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl` e `termsUrl`.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
title: Comenzi Backend
|
||||
icon: terminal
|
||||
---
|
||||
|
||||
## Comenzi utile
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
title: Bug-uri, solicitări și pull request-uri
|
||||
icon: bug
|
||||
info: Raportați probleme, solicitați funcționalități și contribuiți cu cod
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
title: Cele mai bune practici
|
||||
icon: star
|
||||
---
|
||||
|
||||
Acest document prezintă cele mai bune practici pe care ar trebui să le urmați atunci când lucrați la frontend.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
title: Arhitectura Folderului
|
||||
icon: folder-tree
|
||||
info: O privire detaliată asupra arhitecturii folderului nostru
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
title: Comenzi Frontend
|
||||
icon: terminal
|
||||
---
|
||||
|
||||
## Comenzi utile
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
title: Ghid de stil
|
||||
icon: paintbrush
|
||||
---
|
||||
|
||||
Acest document include regulile ce trebuie urmate la scrierea codului.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
title: Configurare locală
|
||||
icon: laptop-code
|
||||
description: Ghidul pentru contribuitori (sau dezvoltatori curioși) care doresc să ruleze Twenty local.
|
||||
---
|
||||
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
---
|
||||
title: Comenzi
|
||||
icon: terminal
|
||||
description: Comenzi utile pentru dezvoltarea Twenty.
|
||||
---
|
||||
|
||||
Comenzile pot fi rulate din rădăcina repozitoriului folosind `npx nx`. Folosiți `npx nx run {project}:{command}` pentru a specifica în mod explicit ținta.
|
||||
|
||||
## Pornirea aplicației
|
||||
|
||||
```bash
|
||||
npx nx start twenty-front # Frontend dev server (http://localhost:3001)
|
||||
npx nx start twenty-server # Backend server (http://localhost:3000)
|
||||
npx nx run twenty-server:worker # Background worker
|
||||
```
|
||||
|
||||
## Bază de date
|
||||
|
||||
```bash
|
||||
npx nx database:reset twenty-server # Reset and seed database
|
||||
npx nx run twenty-server:database:migrate:prod # Run migrations
|
||||
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow> # Generate a migration
|
||||
```
|
||||
|
||||
## Linting
|
||||
|
||||
```bash
|
||||
npx nx lint:diff-with-main twenty-front # Lint changed files (fastest)
|
||||
npx nx lint:diff-with-main twenty-server
|
||||
npx nx lint twenty-front --configuration=fix # Auto-fix
|
||||
```
|
||||
|
||||
## Verificarea tipurilor
|
||||
|
||||
```bash
|
||||
npx nx typecheck twenty-front
|
||||
npx nx typecheck twenty-server
|
||||
```
|
||||
|
||||
## Testare
|
||||
|
||||
```bash
|
||||
# Frontend
|
||||
npx nx test twenty-front # Jest unit tests
|
||||
npx nx storybook:build twenty-front # Build Storybook
|
||||
npx nx storybook:test twenty-front # Storybook tests
|
||||
|
||||
# Backend
|
||||
npx nx run twenty-server:test:unit # Unit tests
|
||||
npx nx run twenty-server:test:integration # Integration tests
|
||||
npx nx run twenty-server:test:integration:with-db-reset # Integration with DB reset
|
||||
|
||||
# Single file (fastest)
|
||||
npx jest path/to/test.test.ts --config=packages/{project}/jest.config.mjs
|
||||
```
|
||||
|
||||
## GraphQL
|
||||
|
||||
```bash
|
||||
npx nx run twenty-front:graphql:generate # Regenerate types
|
||||
npx nx run twenty-front:graphql:generate --configuration=metadata # Metadata schema
|
||||
```
|
||||
|
||||
## Traduceri
|
||||
|
||||
```bash
|
||||
npx nx run twenty-front:lingui:extract # Extract strings
|
||||
npx nx run twenty-front:lingui:compile # Compile translations
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
npx nx build twenty-shared # Must be built first
|
||||
npx nx build twenty-front
|
||||
npx nx build twenty-server
|
||||
```
|
||||
@@ -1,176 +0,0 @@
|
||||
---
|
||||
title: Ghid de stil
|
||||
icon: paintbrush
|
||||
description: Convenții de cod și bune practici pentru a contribui la Twenty.
|
||||
---
|
||||
|
||||
## React
|
||||
|
||||
### Doar componente funcționale
|
||||
|
||||
Folosește întotdeauna componente funcționale TSX cu exporturi denumite.
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
const MyComponent = () => {
|
||||
return <div>Hello World</div>;
|
||||
};
|
||||
export default MyComponent;
|
||||
|
||||
// ✅ Good
|
||||
export function MyComponent() {
|
||||
return <div>Hello World</div>;
|
||||
};
|
||||
```
|
||||
|
||||
### Proprietăți
|
||||
|
||||
Creează un tip numit `{ComponentName}Props`. Folosește destructurarea. Nu folosi `React.FC`.
|
||||
|
||||
```tsx
|
||||
type MyComponentProps = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const MyComponent = ({ name }: MyComponentProps) => <div>Hello {name}</div>;
|
||||
```
|
||||
|
||||
### Fără spread al unei singure variabile de props
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
const MyComponent = (props: MyComponentProps) => <Other {...props} />;
|
||||
|
||||
// ✅ Good
|
||||
const MyComponent = ({ prop1, prop2 }: MyComponentProps) => <Other {...{ prop1, prop2 }} />;
|
||||
```
|
||||
|
||||
## Managementul stării
|
||||
|
||||
### Atomi Jotai pentru starea globală
|
||||
|
||||
```tsx
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
|
||||
export const myAtomState = createAtomState<string>({
|
||||
key: 'myAtomState',
|
||||
defaultValue: 'default value',
|
||||
});
|
||||
```
|
||||
|
||||
* Preferă atomii în locul prop drilling-ului
|
||||
* Nu folosi `useRef` pentru stare — folosește `useState` sau atomi
|
||||
* Folosește familii de atomi și selectori pentru liste
|
||||
|
||||
### Evită re-randări inutile
|
||||
|
||||
* Extrage `useEffect` și preluarea datelor în componente surori de tip sidecar
|
||||
* Preferă handleri de evenimente (`handleClick`, `handleChange`) în locul lui `useEffect`
|
||||
* Nu folosi `React.memo()` — în schimb, rezolvă cauza principală
|
||||
* Limitează utilizarea `useCallback` / `useMemo`
|
||||
|
||||
```tsx
|
||||
// ❌ Bad — useEffect in the same component causes re-renders
|
||||
export const Page = () => {
|
||||
const [data, setData] = useAtomState(dataState);
|
||||
const [dep] = useAtomState(depState);
|
||||
useEffect(() => { setData(dep); }, [dep]);
|
||||
return <div>{data}</div>;
|
||||
};
|
||||
|
||||
// ✅ Good — extract into sibling
|
||||
export const PageData = () => {
|
||||
const [data, setData] = useAtomState(dataState);
|
||||
const [dep] = useAtomState(depState);
|
||||
useEffect(() => { setData(dep); }, [dep]);
|
||||
return <></>;
|
||||
};
|
||||
export const Page = () => {
|
||||
const [data] = useAtomState(dataState);
|
||||
return <div>{data}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
## TypeScript
|
||||
|
||||
* **`type` în loc de `interface`** — mai flexibil, mai ușor de compus
|
||||
* **Șiruri literale în loc de enum-uri** — cu excepția enum-urilor generate de GraphQL codegen și a API-urilor interne ale bibliotecii
|
||||
* **Fără `any`** — TypeScript strict este impus
|
||||
* **Fără importuri de tip** — folosește importuri obișnuite (impus de Oxlint `typescript/consistent-type-imports`)
|
||||
* **Folosește [Zod](https://github.com/colinhacks/zod)** pentru validarea în timp de execuție a obiectelor netipizate
|
||||
|
||||
## JavaScript
|
||||
|
||||
```tsx
|
||||
// Use nullish-coalescing (??) instead of ||
|
||||
const value = process.env.MY_VALUE ?? 'default';
|
||||
|
||||
// Use optional chaining
|
||||
onClick?.();
|
||||
```
|
||||
|
||||
## Denumiri
|
||||
|
||||
* **Variabile**: camelCase, descriptive (`email` nu `value`, `fieldMetadata` nu `fm`)
|
||||
* **Constante**: SCREAMING_SNAKE_CASE
|
||||
* **Tipuri/Clase**: PascalCase
|
||||
* **Fișiere/directoare**: kebab-case (`.component.tsx`, `.service.ts`, `.entity.ts`)
|
||||
* **Handleri de evenimente**: `handleClick` (nu `onClick` pentru funcția handler)
|
||||
* **Props de componentă**: prefixează cu numele componentei (`ButtonProps`)
|
||||
* **Componente stilizate**: prefixează cu `Styled` (`StyledTitle`)
|
||||
|
||||
## Stilizare
|
||||
|
||||
Folosește componente stilizate [Linaria](https://github.com/callstack/linaria). Folosește valorile din temă — evită `px`, `rem` sau culori hardcodate.
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
const StyledButton = styled.button`
|
||||
color: #333333;
|
||||
font-size: 1rem;
|
||||
margin-left: 4px;
|
||||
`;
|
||||
|
||||
// ✅ Good
|
||||
const StyledButton = styled.button`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
```
|
||||
|
||||
## Importuri
|
||||
|
||||
Folosește aliasuri în locul căilor relative:
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
import { Foo } from '../../../../../testing/decorators/Foo';
|
||||
|
||||
// ✅ Good
|
||||
import { Foo } from '~/testing/decorators/Foo';
|
||||
import { Bar } from '@/modules/bar/components/Bar';
|
||||
```
|
||||
|
||||
## Structura folderelor
|
||||
|
||||
```
|
||||
front
|
||||
└── modules/ # Feature modules
|
||||
│ └── module1/
|
||||
│ ├── components/
|
||||
│ ├── constants/
|
||||
│ ├── contexts/
|
||||
│ ├── graphql/ (fragments, queries, mutations)
|
||||
│ ├── hooks/
|
||||
│ ├── states/ (atoms, selectors)
|
||||
│ ├── types/
|
||||
│ └── utils/
|
||||
└── pages/ # Route-level components
|
||||
└── ui/ # Reusable UI components (display, input, feedback, ...)
|
||||
```
|
||||
|
||||
* Modulele pot importa din alte module, dar `ui/` ar trebui să rămână fără dependențe
|
||||
* Folosește subfoldere `internal/` pentru cod privat modulului
|
||||
* Componente sub 300 de linii, servicii sub 500 de linii
|
||||
@@ -1,55 +1,147 @@
|
||||
---
|
||||
title: API-uri
|
||||
icon: plug
|
||||
description: API-urile REST și GraphQL generate din schema spațiului tău de lucru.
|
||||
description: Interogați și modificați programatic datele din CRM folosind REST sau GraphQL.
|
||||
---
|
||||
|
||||
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
|
||||
|
||||
## API-uri cu schemă per-tenant
|
||||
Twenty a fost creat pentru a fi prietenos cu dezvoltatorii, oferind API-uri puternice care se adaptează la modelul dvs. de date personalizat. Oferim patru tipuri distincte de API-uri pentru a satisface diferite nevoi de integrare.
|
||||
|
||||
Nu există o referință API statică pentru Twenty. Fiecare spațiu de lucru are propria schemă — când adaugi un obiect personalizat (de exemplu `Invoice`), acesta primește imediat endpoint-uri REST și GraphQL identice cu cele ale obiectelor încorporate precum `Company` sau `Person`. API-ul este generat din schemă, astfel încât endpoint-urile folosesc direct denumirile obiectelor și câmpurilor tale — fără ID-uri opace.
|
||||
## Abordare orientată către dezvoltatori
|
||||
|
||||
Documentația API specifică spațiului tău de lucru este disponibilă la **Settings → API & Webhooks** după crearea unei chei API. Include un mediu interactiv de testare în care poți executa apeluri reale asupra datelor tale.
|
||||
Twenty generează API-uri special pentru modelul dvs. de date:
|
||||
|
||||
## Două API-uri
|
||||
* **Nu sunt necesare ID-uri lungi**: Utilizați direct numele obiectelor și câmpurilor în punctele finale
|
||||
* **Obiectele standard și personalizate tratate în mod egal**: Obiectele dvs. personalizate primesc același tratament API ca și cele încorporate
|
||||
* **Puncte finale dedicate**: Fiecare obiect și câmp primește propriul său punct final API
|
||||
* **Documentație personalizată**: Generată special pentru modelul de date al spațiului dvs. de lucru
|
||||
|
||||
**API de bază** — `/rest/` și `/graphql/`
|
||||
<Note>
|
||||
Documentația API personalizată este disponibilă la **Settings → API & Webhooks** după crearea unei chei API. Deoarece Twenty generează API-uri care se potrivesc modelului dvs. de date personalizat, documentația este unică pentru spațiul dvs. de lucru.
|
||||
</Note>
|
||||
|
||||
CRUD pe înregistrări: Persoane, Companii, Oportunități, obiectele tale personalizate. Interogare, filtrare, parcurgere a relațiilor.
|
||||
## Cele două tipuri de API-uri
|
||||
|
||||
**API de metadate** — `/rest/metadata/` și `/metadata/`
|
||||
### API Core
|
||||
|
||||
Administrarea schemei: creare/modificare/ștergere de obiecte, câmpuri și relații. Așa îți modifici programatic modelul de date.
|
||||
Accesibil prin `/rest/` sau `/graphql/`
|
||||
|
||||
Ambele sunt disponibile ca REST și GraphQL. GraphQL adaugă upsert-uri în lot și posibilitatea de a parcurge relațiile într-o singură interogare. Aceleași date de bază, indiferent de metodă.
|
||||
Lucrați cu **înregistrările** reale (datele):
|
||||
|
||||
## URL-uri de bază
|
||||
* Creați, citiți, actualizați, ștergeți Persoane, Companii, Oportunități etc.
|
||||
* Interogați și filtrați datele
|
||||
* Gestionați relațiile dintre înregistrări
|
||||
|
||||
| Mediu | URL de bază |
|
||||
| ---------------- | ------------------------- |
|
||||
| Cloud | `https://api.twenty.com/` |
|
||||
| Găzduire proprie | `https://{your-domain}/` |
|
||||
### API Metadata
|
||||
|
||||
Accesibil prin `/rest/metadata/` sau `/metadata/`
|
||||
|
||||
Gestionați-vă **spațiul de lucru și modelul de date**:
|
||||
|
||||
* Creați, modificați sau ștergeți obiecte și câmpuri
|
||||
* Configurați setările spațiului de lucru
|
||||
* Definiți relațiile dintre obiecte
|
||||
|
||||
## REST vs GraphQL
|
||||
|
||||
Atât API-urile Core, cât și API-urile Metadata sunt disponibile în formatele REST și GraphQL:
|
||||
|
||||
| Format | Operațiuni disponibile |
|
||||
| ----------- | -------------------------------------------------------------------------- |
|
||||
| **REST** | CRUD, operațiuni de grup, upsert-uri |
|
||||
| **GraphQL** | La fel + **upsert-uri de grup**, interogări de relații într-un singur apel |
|
||||
|
||||
Alegeți în funcție de nevoi — ambele formate accesează aceleași date.
|
||||
|
||||
## Puncte Finale API
|
||||
|
||||
| Mediu | URL de bază |
|
||||
| -------------------- | ------------------------- |
|
||||
| **Cloud** | `https://api.twenty.com/` |
|
||||
| **Găzduire proprie** | `https://{your-domain}/` |
|
||||
|
||||
## Autentificare
|
||||
|
||||
Fiecare solicitare API necesită o cheie API în antet:
|
||||
|
||||
```
|
||||
Authorization: Bearer YOUR_API_KEY
|
||||
```
|
||||
|
||||
Creează o cheie API în **Settings → API & Webhooks → + Create key**. Copiază-o imediat — este afișată o singură dată. Cheile pot fi limitate la un rol specific în **Settings → Roles → Assignment tab** pentru a restricționa la ce pot avea acces.
|
||||
### Creați o cheie API
|
||||
|
||||
1. Mergeți la **Setări → API-uri & Webhook-uri**
|
||||
2. Faceți clic pe **+ Create key**
|
||||
3. Configurați:
|
||||
* **Name**: Nume descriptiv pentru cheie
|
||||
* **Expiration Date**: Când expiră cheia
|
||||
4. Faceți clic pe **Salvare**
|
||||
5. **Copiați imediat** — cheia este afișată o singură dată
|
||||
|
||||
<VimeoEmbed videoId="928786722" title="Crearea unei chei API" />
|
||||
|
||||
Pentru acces bazat pe OAuth (aplicații externe care acționează în numele utilizatorilor), vezi [OAuth](/l/ro/developers/extend/oauth).
|
||||
<Warning>
|
||||
Cheia dvs. API oferă acces la date sensibile. Nu o partajați cu servicii care nu sunt de încredere. Dacă este compromisă, dezactivați-o imediat și generați una nouă.
|
||||
</Warning>
|
||||
|
||||
### Atribuiți un rol unei chei API
|
||||
|
||||
Pentru o securitate sporită, atribuiți un rol specific pentru a limita accesul:
|
||||
|
||||
1. Accesați **Setări → Roluri**
|
||||
2. Faceți clic pe rolul pe care doriți să-l atribuiți
|
||||
3. Deschideți fila **Atribuire**
|
||||
4. În **API Keys**, faceți clic pe **+ Assign to API key**
|
||||
5. Selectați cheia API
|
||||
|
||||
Cheia va moșteni permisiunile acelui rol. Consultați [Permisiuni](/l/ro/user-guide/permissions-access/capabilities/permissions) pentru detalii.
|
||||
|
||||
### Gestionați cheile API
|
||||
|
||||
**Regenerate**: Settings → APIs & Webhooks → Faceți clic pe cheie → **Regenerate**
|
||||
|
||||
**Delete**: Settings → APIs & Webhooks → Faceți clic pe cheie → **Delete**
|
||||
|
||||
## Platformă de testare API
|
||||
|
||||
Testați API-urile direct în browser cu platforma noastră integrată de testare — disponibilă atât pentru **REST**, cât și pentru **GraphQL**.
|
||||
|
||||
### Accesați platforma de testare
|
||||
|
||||
1. Mergeți la **Setări → API-uri & Webhook-uri**
|
||||
2. Creați o cheie API (obligatoriu)
|
||||
3. Faceți clic pe **REST API** sau **GraphQL API** pentru a deschide platforma de testare
|
||||
|
||||
### Ce obțineți
|
||||
|
||||
* **Documentație interactivă**: Generată pentru modelul dvs. de date specific
|
||||
* **Testare live**: Executați apeluri API reale către spațiul dvs. de lucru
|
||||
* **Explorator de scheme**: Parcurgeți obiectele, câmpurile și relațiile disponibile
|
||||
* **Constructor de cereri**: Construiți interogări cu completare automată
|
||||
|
||||
Platforma de testare reflectă obiectele și câmpurile dvs. personalizate, astfel încât documentația este întotdeauna corectă pentru spațiul dvs. de lucru.
|
||||
|
||||
## Operațiuni de grup
|
||||
|
||||
Atât REST, cât și GraphQL acceptă procesarea în lot de până la 60 de înregistrări per cerere — creare, actualizare sau ștergere. GraphQL acceptă, de asemenea, upsert în lot (creare-sau-actualizare într-un singur apel) folosind nume la plural precum `CreateCompanies`.
|
||||
Atât REST, cât și GraphQL suportă operațiuni de grup:
|
||||
|
||||
* **Dimensiunea grupului**: Până la 60 de înregistrări pe cerere
|
||||
* **Operațiuni**: Creați, actualizați, ștergeți mai multe înregistrări
|
||||
|
||||
**Funcții exclusive GraphQL:**
|
||||
|
||||
* **Upsert de grup**: Creați sau actualizați într-un singur apel
|
||||
* Folosiți nume de obiecte la plural (de exemplu, `CreateCompanies` în loc de `CreateCompany`)
|
||||
|
||||
## Limitări de rată
|
||||
|
||||
| Limită | Valoare |
|
||||
| ------------------- | -------------------------- |
|
||||
| Solicitări | 100 pe minut |
|
||||
| Dimensiunea lotului | 60 de înregistrări pe apel |
|
||||
Solicitările API sunt limitate pentru a asigura stabilitatea platformei:
|
||||
|
||||
| Limită | Valoare |
|
||||
| ----------------------- | -------------------------- |
|
||||
| **Solicitări** | 100 de apeluri pe minut |
|
||||
| **Dimensiunea lotului** | 60 de înregistrări pe apel |
|
||||
|
||||
<Tip>
|
||||
Utilizați operațiunile de grup pentru a maximiza debitul — procesați până la 60 de înregistrări într-un singur apel API în loc să faceți solicitări individuale.
|
||||
</Tip>
|
||||
|
||||
@@ -1,434 +0,0 @@
|
||||
---
|
||||
title: CLI & Testare
|
||||
description: Comenzi CLI, configurare pentru testare, resurse publice, pachete npm, remote-uri și configurare CI.
|
||||
icon: terminal
|
||||
---
|
||||
|
||||
## Resurse publice (folderul `public/`)
|
||||
|
||||
Folderul `public/` din rădăcina aplicației conține fișiere statice — imagini, pictograme, fonturi sau orice alte resurse de care are nevoie aplicația la rulare. Aceste fișiere sunt incluse automat în build-uri, sincronizate în timpul modului de dezvoltare și încărcate pe server.
|
||||
|
||||
Fișierele plasate în `public/` sunt:
|
||||
|
||||
* **Accesibile public** — odată sincronizate pe server, resursele sunt servite la un URL public. Nu este necesară autentificarea pentru a le accesa.
|
||||
* **Disponibile în componentele frontend** — folosiți URL-urile resurselor pentru a afișa imagini, pictograme sau orice media în componentele React.
|
||||
* **Disponibile în funcțiile logice** — referiți URL-urile resurselor în e-mailuri, răspunsuri API sau orice logică pe server.
|
||||
* **Utilizate pentru metadatele marketplace-ului** — câmpurile `logoUrl` și `screenshots` din `defineApplication()` fac referire la fișiere din acest folder (de ex., `public/logo.png`). Acestea sunt afișate în marketplace când aplicația este publicată.
|
||||
* **Sincronizate automat în modul de dezvoltare** — când adăugați, actualizați sau ștergeți un fișier în `public/`, acesta este sincronizat automat cu serverul. Nu este nevoie de repornire.
|
||||
* **Incluse în build-uri** — `yarn twenty build` împachetează toate resursele publice în outputul de distribuție.
|
||||
|
||||
### Accesarea resurselor publice cu `getPublicAssetUrl`
|
||||
|
||||
Utilizați helperul `getPublicAssetUrl` din `twenty-sdk` pentru a obține URL-ul complet al unui fișier din directorul `public/`. Funcționează atât în funcții logice, cât și în componente frontend.
|
||||
|
||||
**Într-o funcție logică:**
|
||||
|
||||
```ts src/logic-functions/send-invoice.ts
|
||||
import { defineLogicFunction, getPublicAssetUrl } from 'twenty-sdk/define';
|
||||
|
||||
const handler = async (): Promise<any> => {
|
||||
const logoUrl = getPublicAssetUrl('logo.png');
|
||||
const invoiceUrl = getPublicAssetUrl('templates/invoice.png');
|
||||
|
||||
// Fetch the file content (no auth required — public endpoint)
|
||||
const response = await fetch(invoiceUrl);
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
return { logoUrl, size: buffer.byteLength };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'a1b2c3d4-...',
|
||||
name: 'send-invoice',
|
||||
description: 'Sends an invoice with the app logo',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**Într-o componentă frontend:**
|
||||
|
||||
```tsx src/front-components/company-card.tsx
|
||||
import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define';
|
||||
|
||||
export default defineFrontComponent(() => {
|
||||
const logoUrl = getPublicAssetUrl('logo.png');
|
||||
|
||||
return <img src={logoUrl} alt="App logo" />;
|
||||
});
|
||||
```
|
||||
|
||||
Argumentul `path` este relativ la folderul `public/` al aplicației. Atât `getPublicAssetUrl('logo.png')`, cât și `getPublicAssetUrl('public/logo.png')` se rezolvă la același URL — prefixul `public/` este eliminat automat dacă este prezent.
|
||||
|
||||
## Utilizarea pachetelor npm
|
||||
|
||||
Puteți instala și utiliza orice pachet npm în aplicația dvs. Atât funcțiile logice, cât și componentele frontend sunt împachetate cu [esbuild](https://esbuild.github.io/), care integrează toate dependențele în output — nu sunt necesare `node_modules` la rulare.
|
||||
|
||||
### Instalarea unui pachet
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add axios
|
||||
```
|
||||
|
||||
Apoi importați-l în codul dvs.:
|
||||
|
||||
```ts src/logic-functions/fetch-data.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import axios from 'axios';
|
||||
|
||||
const handler = async (): Promise<any> => {
|
||||
const { data } = await axios.get('https://api.example.com/data');
|
||||
|
||||
return { data };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '...',
|
||||
name: 'fetch-data',
|
||||
description: 'Fetches data from an external API',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
Același lucru funcționează și pentru componentele frontend:
|
||||
|
||||
```tsx src/front-components/chart.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
const DateWidget = () => {
|
||||
return <p>Today is {format(new Date(), 'MMMM do, yyyy')}</p>;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'date-widget',
|
||||
component: DateWidget,
|
||||
});
|
||||
```
|
||||
|
||||
### Cum funcționează împachetarea
|
||||
|
||||
Pasul de build folosește esbuild pentru a produce un singur fișier autonom pentru fiecare funcție logică și pentru fiecare componentă frontend. Toate pachetele importate sunt integrate în bundle.
|
||||
|
||||
**Funcțiile logice** rulează într-un mediu Node.js. Modulele built-in Node (`fs`, `path`, `crypto`, `http` etc.) sunt disponibile și nu trebuie instalate.
|
||||
|
||||
**Componentele frontend** rulează într-un Web Worker. Modulele built-in Node nu sunt disponibile — doar API-urile de browser și pachetele npm care funcționează într-un mediu de browser.
|
||||
|
||||
Ambele medii au `twenty-client-sdk/core` și `twenty-client-sdk/metadata` disponibile ca module pre-furnizate — acestea nu sunt incluse în bundle, ci sunt rezolvate la rulare de către server.
|
||||
|
||||
## Testarea aplicației
|
||||
|
||||
SDK-ul oferă API-uri programatice care vă permit să construiți, să distribuiți, să instalați și să dezinstalați aplicația din codul de test. Combinat cu [Vitest](https://vitest.dev/) și clienții API tipizați, puteți scrie teste de integrare care verifică faptul că aplicația funcționează cap-coadă împotriva unui server Twenty real.
|
||||
|
||||
### Configurare
|
||||
|
||||
Aplicația generată (scaffolded) include deja Vitest. Dacă o configurați manual, instalați dependențele:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add -D vitest vite-tsconfig-paths
|
||||
```
|
||||
|
||||
Creați un `vitest.config.ts` în rădăcina aplicației:
|
||||
|
||||
```ts vitest.config.ts
|
||||
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:2020',
|
||||
TWENTY_API_KEY: 'your-api-key',
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Creați un fișier de configurare care verifică faptul că serverul este accesibil înainte de rularea testelor:
|
||||
|
||||
```ts src/__tests__/setup-test.ts
|
||||
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:2020';
|
||||
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
|
||||
|
||||
beforeAll(async () => {
|
||||
// Verify the server is running
|
||||
const response = await fetch(`${TWENTY_API_URL}/healthz`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Twenty server is not reachable at ${TWENTY_API_URL}. ` +
|
||||
'Start the server before running integration tests.',
|
||||
);
|
||||
}
|
||||
|
||||
// Write a temporary config for the SDK
|
||||
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(TEST_CONFIG_DIR, 'config.json'),
|
||||
JSON.stringify({
|
||||
remotes: {
|
||||
local: {
|
||||
apiUrl: process.env.TWENTY_API_URL,
|
||||
apiKey: process.env.TWENTY_API_KEY,
|
||||
},
|
||||
},
|
||||
defaultRemote: 'local',
|
||||
}, null, 2),
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
### API-uri SDK programatice
|
||||
|
||||
Subruta `twenty-sdk/cli` exportă funcții pe care le puteți apela direct din codul de test:
|
||||
|
||||
| Funcție | Descriere |
|
||||
| -------------- | --------------------------------------------------------- |
|
||||
| `appBuild` | Construiți aplicația și, opțional, împachetați un tarball |
|
||||
| `appDeploy` | Încărcați un tarball pe server |
|
||||
| `appInstall` | Instalați aplicația în spațiul de lucru activ |
|
||||
| `appUninstall` | Dezinstalați aplicația din spațiul de lucru activ |
|
||||
|
||||
Fiecare funcție returnează un obiect rezultat cu `success: boolean` și fie `data`, fie `error`.
|
||||
|
||||
### Scrierea unui test de integrare
|
||||
|
||||
Iată un exemplu complet care construiește, distribuie și instalează aplicația, apoi verifică faptul că aceasta apare în spațiul de lucru:
|
||||
|
||||
```ts src/__tests__/app-install.integration-test.ts
|
||||
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
|
||||
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
const APP_PATH = process.cwd();
|
||||
|
||||
describe('App installation', () => {
|
||||
beforeAll(async () => {
|
||||
const buildResult = await appBuild({
|
||||
appPath: APP_PATH,
|
||||
tarball: true,
|
||||
onProgress: (message: string) => console.log(`[build] ${message}`),
|
||||
});
|
||||
|
||||
if (!buildResult.success) {
|
||||
throw new Error(`Build failed: ${buildResult.error?.message}`);
|
||||
}
|
||||
|
||||
const deployResult = await appDeploy({
|
||||
tarballPath: buildResult.data.tarballPath!,
|
||||
onProgress: (message: string) => console.log(`[deploy] ${message}`),
|
||||
});
|
||||
|
||||
if (!deployResult.success) {
|
||||
throw new Error(`Deploy failed: ${deployResult.error?.message}`);
|
||||
}
|
||||
|
||||
const installResult = await appInstall({ appPath: APP_PATH });
|
||||
|
||||
if (!installResult.success) {
|
||||
throw new Error(`Install failed: ${installResult.error?.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await appUninstall({ appPath: APP_PATH });
|
||||
});
|
||||
|
||||
it('should find the installed app in the workspace', async () => {
|
||||
const metadataClient = new MetadataApiClient();
|
||||
|
||||
const result = await metadataClient.query({
|
||||
findManyApplications: {
|
||||
id: true,
|
||||
name: true,
|
||||
universalIdentifier: true,
|
||||
},
|
||||
});
|
||||
|
||||
const installedApp = result.findManyApplications.find(
|
||||
(app: { universalIdentifier: string }) =>
|
||||
app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
|
||||
expect(installedApp).toBeDefined();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Rularea testelor
|
||||
|
||||
Asigurați-vă că serverul Twenty local rulează, apoi:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn test
|
||||
```
|
||||
|
||||
Sau în modul watch în timpul dezvoltării:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn test:watch
|
||||
```
|
||||
|
||||
### Verificarea tipurilor
|
||||
|
||||
Puteți rula și verificarea tipurilor pe aplicație fără a rula testele:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty typecheck
|
||||
```
|
||||
|
||||
Aceasta rulează `tsc --noEmit` și raportează orice erori de tip.
|
||||
|
||||
## Referință CLI
|
||||
|
||||
Dincolo de `dev`, `build`, `add` și `typecheck`, CLI oferă comenzi pentru executarea funcțiilor, vizualizarea jurnalelor și gestionarea instalărilor de aplicații.
|
||||
|
||||
### Executarea funcțiilor (`yarn twenty exec`)
|
||||
|
||||
Rulați manual o funcție logică fără a o declanșa prin HTTP, cron sau eveniment de bază de date:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Execute by function name
|
||||
yarn twenty exec -n create-new-post-card
|
||||
|
||||
# Execute by universalIdentifier
|
||||
yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
||||
|
||||
# Pass a JSON payload
|
||||
yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}'
|
||||
|
||||
# Execute the post-install function
|
||||
yarn twenty exec --postInstall
|
||||
```
|
||||
|
||||
### Vizualizarea jurnalelor funcțiilor (`yarn twenty logs`)
|
||||
|
||||
Transmiteți în flux jurnalele de execuție pentru funcțiile logice ale aplicației:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Stream all function logs
|
||||
yarn twenty logs
|
||||
|
||||
# Filter by function name
|
||||
yarn twenty logs -n create-new-post-card
|
||||
|
||||
# Filter by universalIdentifier
|
||||
yarn twenty logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
||||
```
|
||||
|
||||
<Note>
|
||||
Acest lucru este diferit de `yarn twenty server logs`, care afișează jurnalele containerului Docker. `yarn twenty logs` afișează jurnalele de execuție ale funcțiilor aplicației de pe serverul Twenty.
|
||||
</Note>
|
||||
|
||||
### Dezinstalarea unei aplicații (`yarn twenty uninstall`)
|
||||
|
||||
Eliminați aplicația din spațiul de lucru activ:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty uninstall
|
||||
|
||||
# Skip the confirmation prompt
|
||||
yarn twenty uninstall --yes
|
||||
```
|
||||
|
||||
## Gestionarea remote-urilor
|
||||
|
||||
Un „remote” este un server Twenty la care se conectează aplicația. În timpul configurării, Scaffolderul creează automat unul pentru dvs. Puteți adăuga mai multe remote-uri sau comuta între ele oricând.
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Add a new remote (opens a browser for OAuth login)
|
||||
yarn twenty remote add
|
||||
|
||||
# Connect to a local Twenty server (auto-detects port 2020 or 3000)
|
||||
yarn twenty remote add --local
|
||||
|
||||
# Add a remote non-interactively (useful for CI)
|
||||
yarn twenty remote add --api-url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote
|
||||
|
||||
# List all configured remotes
|
||||
yarn twenty remote list
|
||||
|
||||
# Switch the active remote
|
||||
yarn twenty remote switch <name>
|
||||
```
|
||||
|
||||
Acreditările dvs. sunt stocate în `~/.twenty/config.json`.
|
||||
|
||||
## CI cu GitHub Actions
|
||||
|
||||
Scaffolderul generează un workflow GitHub Actions gata de utilizare în `.github/workflows/ci.yml`. Rulează automat testele de integrare la fiecare push pe `main` și la pull request-uri.
|
||||
|
||||
Workflow-ul:
|
||||
|
||||
1. Preia codul
|
||||
2. Pornește un server Twenty temporar folosind acțiunea `twentyhq/twenty/.github/actions/spawn-twenty-docker-image`
|
||||
3. Instalează dependențele cu `yarn install --immutable`
|
||||
4. Rulează `yarn test` cu `TWENTY_API_URL` și `TWENTY_API_KEY` injectate din rezultatele acțiunii
|
||||
|
||||
```yaml .github/workflows/ci.yml
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request: {}
|
||||
|
||||
env:
|
||||
TWENTY_VERSION: latest
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Spawn Twenty instance
|
||||
id: twenty
|
||||
uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
|
||||
with:
|
||||
twenty-version: ${{ env.TWENTY_VERSION }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Run integration tests
|
||||
run: yarn test
|
||||
env:
|
||||
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
|
||||
TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
|
||||
```
|
||||
|
||||
Nu trebuie să configurați niciun secret — acțiunea `spawn-twenty-docker-image` pornește un server Twenty efemer direct în runner și oferă detaliile de conectare. Secretul `GITHUB_TOKEN` este furnizat automat de GitHub.
|
||||
|
||||
Pentru a fixa o versiune Twenty specifică în loc de `latest`, modificați variabila de mediu `TWENTY_VERSION` din partea de sus a workflow-ului.
|
||||
@@ -1,193 +0,0 @@
|
||||
---
|
||||
title: Conexiuni
|
||||
description: Permite aplicației tale să acționeze în numele unui utilizator în servicii ale terților prin OAuth.
|
||||
icon: plug
|
||||
---
|
||||
|
||||
Conexiunile sunt acreditări pe care un utilizator le deține pentru un serviciu extern (Linear, GitHub, Slack, ...). Aplicația ta declară **cum** sunt obținute acele acreditări — un **furnizor de conexiune** — și le folosește în timpul execuției pentru a efectua apeluri autentificate către API-ul terț.
|
||||
|
||||
În prezent este acceptat doar OAuth 2.0. Tipurile viitoare de acreditări (jetoane de acces personale, chei API, autentificare de bază) se vor integra în aceeași interfață — aplicațiile care deja folosesc `defineConnectionProvider({ type: 'oauth', ... })` nu vor trebui să migreze.
|
||||
|
||||
<AccordionGroup>
|
||||
|
||||
<Accordion title="defineConnectionProvider" description="Declară cum sunt obținute conexiunile aplicației tale">
|
||||
|
||||
Un furnizor de conexiune descrie handshake-ul OAuth de care are nevoie aplicația ta. Utilizatorul face clic pe "Adaugă conexiune" în setările aplicației tale, completează ecranul de consimțământ al furnizorului și este creată o înregistrare `ConnectedAccount` în spațiul său de lucru.
|
||||
|
||||
O configurație funcțională are nevoie de **două fișiere** — furnizorul de conexiune și o declarație `serverVariables` corespunzătoare în `defineApplication` care conține acreditările clientului OAuth.
|
||||
|
||||
```ts src/connection-providers/linear-connection.ts
|
||||
import { defineConnectionProvider } from 'twenty-sdk/define';
|
||||
|
||||
export default defineConnectionProvider({
|
||||
universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
|
||||
name: 'linear',
|
||||
displayName: 'Linear',
|
||||
icon: 'IconBrandLinear',
|
||||
type: 'oauth',
|
||||
oauth: {
|
||||
authorizationEndpoint: 'https://linear.app/oauth/authorize',
|
||||
tokenEndpoint: 'https://api.linear.app/oauth/token',
|
||||
scopes: ['read', 'write'],
|
||||
// These must match keys in `defineApplication.serverVariables` below.
|
||||
clientIdVariable: 'LINEAR_CLIENT_ID',
|
||||
clientSecretVariable: 'LINEAR_CLIENT_SECRET',
|
||||
// Optional: defaults to 'json'. Some providers (Linear, Slack) want
|
||||
// 'form-urlencoded' for the token request.
|
||||
tokenRequestContentType: 'form-urlencoded',
|
||||
// Optional: defaults to true. Disable only if the provider rejects PKCE.
|
||||
usePkce: false,
|
||||
// Optional: extra query params on the authorize URL.
|
||||
// authorizationParams: { prompt: 'consent' },
|
||||
// Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
|
||||
// revokeEndpoint: 'https://example.com/oauth/revoke',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts src/application.config.ts
|
||||
import { defineApplication } from 'twenty-sdk/define';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '...',
|
||||
displayName: 'Linear',
|
||||
description: 'Connect Linear to Twenty.',
|
||||
defaultRoleUniversalIdentifier: '...',
|
||||
// OAuth client credentials live on the app registration (one OAuth app per
|
||||
// Twenty server, configured by the admin) — not per-workspace. Declare them
|
||||
// as serverVariables so the admin can fill them in once for all installs.
|
||||
serverVariables: {
|
||||
LINEAR_CLIENT_ID: {
|
||||
description: 'OAuth client ID from your Linear OAuth application.',
|
||||
isSecret: false,
|
||||
isRequired: true,
|
||||
},
|
||||
LINEAR_CLIENT_SECRET: {
|
||||
description: 'OAuth client secret from your Linear OAuth application.',
|
||||
isSecret: true,
|
||||
isRequired: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Puncte cheie:
|
||||
|
||||
* `name` este șirul identificator unic folosit în `listConnections({ providerName })` (kebab-case, trebuie să corespundă `^[a-z][a-z0-9-]*$`).
|
||||
* `displayName` apare în fila de setări a aplicației și în lista de instrumente AI.
|
||||
* `clientIdVariable` / `clientSecretVariable` sunt **nume**, nu valori — trebuie să se potrivească cheilor declarate în `defineApplication.serverVariables`. Valorile reale `client_id` și `client_secret` sunt introduse de administratorul serverului prin interfața de înregistrare a aplicației și nu sunt niciodată comise în repo-ul tău.
|
||||
* Folosește `serverVariables` (nu `applicationVariables`) — acreditările OAuth sunt la nivel de server și există o singură aplicație OAuth pentru fiecare server Twenty.
|
||||
* Până când ambele `serverVariables` sunt completate, fila de setări a aplicației afișează un indiciu "necesită administrator de server" și butonul "Adaugă conexiune" este dezactivat.
|
||||
* `type: 'oauth'` este singura valoare acceptată în prezent. Discriminatorul este compatibil cu versiuni viitoare: tipurile viitoare (`'pat'`, `'api-key'`, ...) vor adăuga blocuri noi de sub-configurație alături de `oauth`.
|
||||
|
||||
URL-ul de callback OAuth pe care furnizorul tău trebuie să îl includă pe lista albă este:
|
||||
|
||||
```
|
||||
https://<your-twenty-server>/apps/oauth/callback
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="listConnections / getConnection" description="Folosește conexiunile dintr-o funcție logică">
|
||||
|
||||
În interiorul unui handler de funcție logică, `listConnections({ providerName })` returnează înregistrările `ConnectedAccount` ale acestei aplicații pentru furnizorul dat, cu tokenuri de acces reîmprospătate.
|
||||
|
||||
```ts src/logic-functions/handlers/create-linear-issue-handler.ts
|
||||
import { listConnections } from 'twenty-sdk/logic-function';
|
||||
|
||||
export const createLinearIssueHandler = async (input: {
|
||||
teamId?: string;
|
||||
title?: string;
|
||||
}) => {
|
||||
if (!input.teamId || !input.title) {
|
||||
return { success: false, error: 'teamId and title are required' };
|
||||
}
|
||||
|
||||
const connections = await listConnections({ providerName: 'linear' });
|
||||
|
||||
// Workspace-shared credentials win when present; fall back to the first
|
||||
// user-visibility one. For HTTP-route triggers you typically pick the
|
||||
// request user's connection via event.userWorkspaceId instead.
|
||||
const connection =
|
||||
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
|
||||
|
||||
if (!connection) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
'Linear is not connected. Open the app settings and click "Add connection".',
|
||||
};
|
||||
}
|
||||
|
||||
// Use connection.accessToken to call the third-party API.
|
||||
const response = await fetch('https://api.linear.app/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${connection.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
|
||||
}),
|
||||
});
|
||||
|
||||
return { success: response.ok };
|
||||
};
|
||||
```
|
||||
|
||||
Fiecare conexiune are:
|
||||
|
||||
| Câmp | Descriere |
|
||||
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `id` | ID unic al înregistrării; pasează-l la `getConnection(id)` pentru a reobține acea înregistrare |
|
||||
| `visibility` | `'user'` (privată pentru un membru al spațiului de lucru) sau `'workspace'` (partajată cu toți membrii) |
|
||||
| `scopes` | Permisiunile OAuth acordate de furnizorul upstream (distincte de `visibility` — nu au legătură) |
|
||||
| `userWorkspaceId` | ID-ul userWorkspace al deținătorului — util pentru a alege "conexiunea utilizatorului care face cererea" în declanșatoarele de rută HTTP |
|
||||
| `accessToken` | Token de acces OAuth proaspăt (reîmprospătat automat dacă a expirat) |
|
||||
| `name` / `handle` | Numele afișat al conexiunii (derivat automat la callback-ul OAuth, poate fi redenumit de utilizator) |
|
||||
| `authFailedAt` | Setat când cea mai recentă reîmprospătare a eșuat; utilizatorul trebuie să se reconecteze |
|
||||
|
||||
Puncte cheie:
|
||||
|
||||
* Pasează `{ providerName }` pentru a filtra după furnizor; omite-l pentru a obține toate conexiunile pe care această aplicație le deține la toți furnizorii.
|
||||
* Serverul reîmprospătează transparent tokenul de acces înainte de a returna. Handlerul tău vede întotdeauna un token utilizabil (sau `authFailedAt` setat).
|
||||
* `getConnection(id)` este echivalentul pentru o singură înregistrare.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Vizibilitate per utilizator vs partajată la nivel de spațiu de lucru" description="Cum aleg utilizatorii între acreditări private și partajate">
|
||||
|
||||
Când un utilizator face clic pe "Adaugă conexiune", i se solicită să aleagă o vizibilitate:
|
||||
|
||||
* **Doar pentru mine** — acreditarea este privată pentru utilizatorul care se conectează. Orice funcție logică apelată în numele lor (declanșator de rută HTTP cu `isAuthRequired: true`) o vede; declanșatoarele cron și evenimentele din bază de date nu.
|
||||
* **Partajată la nivel de spațiu de lucru** — orice membru al spațiului de lucru poate folosi acreditarea. Declanșatoarele cron / din bază de date o văd, de asemenea, deoarece nu au un utilizator al cererii.
|
||||
|
||||
Folosește-o pe cea potrivită pentru fiecare handler:
|
||||
|
||||
```ts
|
||||
// HTTP-route trigger — prefer the request user's own connection.
|
||||
const conn =
|
||||
connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
|
||||
connections.find((c) => c.visibility === 'workspace');
|
||||
|
||||
// Cron trigger — no request user; only shared credentials are sensible.
|
||||
const conn = connections.find((c) => c.visibility === 'workspace');
|
||||
```
|
||||
|
||||
Sunt permise mai multe conexiuni per (utilizator, furnizor), astfel încât același utilizator poate avea "Personal Linear" și "Work Linear" una lângă alta.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Configurare unică a furnizorului" description="Înregistrează-ți aplicația OAuth la serviciul terț">
|
||||
|
||||
Pentru fiecare furnizor de conexiune, administratorul serverului trebuie mai întâi să înregistreze o aplicație OAuth la serviciul terț.
|
||||
|
||||
1. Mergi la setările pentru dezvoltatori ale furnizorului (de ex. https://linear.app/settings/api/applications/new).
|
||||
2. Setează **Redirect URI** la `\<SERVER_URL>/apps/oauth/callback`.
|
||||
3. Copiază **Client ID** și **Client Secret** generate.
|
||||
4. Deschide aplicația instalată în Twenty ca administrator de server → setează valorile pe `serverVariables` corespunzătoare.
|
||||
5. Membrii spațiului de lucru pot apoi să adauge conexiuni din secțiunea **Conexiuni** a fiecărei aplicații.
|
||||
|
||||
</Accordion>
|
||||
|
||||
</AccordionGroup>
|
||||