Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ad4387f96 | ||
|
|
e6399b180e | ||
|
|
8c2885f9ed | ||
|
|
a76047f28b | ||
|
|
281eaa3721 | ||
|
|
c804f27846 | ||
|
|
54e22423df | ||
|
|
a0dd7d9e22 | ||
|
|
97ec720d2c | ||
|
|
95a34d8517 | ||
|
|
4852ac401a | ||
|
|
1cd983a330 | ||
|
|
37ca09e8f9 | ||
|
|
91124a3cb8 | ||
|
|
41ad63a8ab | ||
|
|
9ddf9af4c4 | ||
|
|
3ffda0a29e | ||
|
|
0bb345b75f | ||
|
|
3c7c62c79f | ||
|
|
596ce32bd6 | ||
|
|
d2cfbf319b | ||
|
|
a3cf565ba3 | ||
|
|
fa8304d323 | ||
|
|
467ddaa27a | ||
|
|
fc4cf7fe09 | ||
|
|
9e94045fa5 | ||
|
|
ff22988caf | ||
|
|
74cc2b4c87 | ||
|
|
7aa2afc67e | ||
|
|
a025dc368b | ||
|
|
5bf7e8b101 | ||
|
|
276d4f6e84 | ||
|
|
4f439bbe43 | ||
|
|
4fbd8b207d | ||
|
|
f7d812fca6 | ||
|
|
c3a320c27b | ||
|
|
85752f8a61 | ||
|
|
8a0225e974 | ||
|
|
abc67efd40 | ||
|
|
636deffb93 | ||
|
|
e1828b6f41 | ||
|
|
4b76457217 | ||
|
|
d8bd717f1f | ||
|
|
b44fb1ad23 | ||
|
|
f32b03a3ec | ||
|
|
c8e405cb4e | ||
|
|
83db37d33f | ||
|
|
5bdbfe651e | ||
|
|
3fbb70c13f | ||
|
|
c6b6c824d4 | ||
|
|
b21bf66b38 | ||
|
|
22838ac6de | ||
|
|
bddd23fd9c | ||
|
|
842e679cc6 | ||
|
|
a713f8d87f | ||
|
|
fd6d5f895d | ||
|
|
11628d19a3 | ||
|
|
46ba5fd16c | ||
|
|
4649736d49 | ||
|
|
51384bc085 | ||
|
|
abfa6200dd | ||
|
|
480e5796ec | ||
|
|
d8e2de48e6 | ||
|
|
b49e58dfc6 | ||
|
|
a6118b7dc3 | ||
|
|
19ee9444ed | ||
|
|
b92617d46e | ||
|
|
c476c6c80b | ||
|
|
d2dda67596 | ||
|
|
6aec449a56 | ||
|
|
7ea1dfdd49 | ||
|
|
3290bf3ab1 | ||
|
|
fbaea0639a | ||
|
|
2a3b8adcfb | ||
|
|
8f362186ce | ||
|
|
8998009805 | ||
|
|
a710c105cf | ||
|
|
a5cd64daf5 | ||
|
|
df3e217d64 | ||
|
|
1c06506256 | ||
|
|
d271ba06f1 | ||
|
|
7d35979fcf | ||
|
|
8d3047c1fc | ||
|
|
e632b7dbb9 | ||
|
|
5b3ee3f1a7 | ||
|
|
875795cc30 | ||
|
|
350b835fbc | ||
|
|
ef49dad0bc | ||
|
|
e3a668a054 | ||
|
|
74536e6f98 | ||
|
|
b6ec2acb56 | ||
|
|
bb5f294c5a | ||
|
|
13aaea597e | ||
|
|
ec893c2767 | ||
|
|
f8c1ad5b3c | ||
|
|
a90895e167 | ||
|
|
9b7f7d059a | ||
|
|
95fee18126 | ||
|
|
5b8804ff06 | ||
|
|
6545ca274c | ||
|
|
15c52d3a39 | ||
|
|
3db1af9a17 | ||
|
|
1e7c1691f4 | ||
|
|
577312f121 | ||
|
|
fb8b9cb86c |
@@ -7,6 +7,17 @@ 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,20 +4,19 @@
|
||||
# See https://crowdin.github.io/crowdin-cli/configuration for more information
|
||||
#
|
||||
|
||||
"preserve_hierarchy": true
|
||||
"base_path": ".."
|
||||
|
||||
files: [
|
||||
{
|
||||
#
|
||||
# Source files filter - PO files for Lingui
|
||||
#
|
||||
"source": "**/en.po",
|
||||
preserve_hierarchy: true
|
||||
base_path: ..
|
||||
|
||||
files:
|
||||
#
|
||||
# Source files filter - PO files for Lingui
|
||||
#
|
||||
- source: packages/twenty-front/src/locales/en.po
|
||||
#
|
||||
# Translation files path
|
||||
#
|
||||
"translation": "%original_path%/%locale%.po",
|
||||
}
|
||||
]
|
||||
|
||||
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'
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
#
|
||||
# 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'
|
||||
@@ -78,6 +78,83 @@ jobs:
|
||||
tag: scope:backend
|
||||
tasks: lint,typecheck
|
||||
|
||||
server-previous-version-upgrade-mutation-guard:
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Get changed upgrade-version-command files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files: |
|
||||
packages/twenty-server/src/database/commands/upgrade-version-command/**
|
||||
- name: Check upgrade version commands are in current version only
|
||||
if: >
|
||||
steps.changed-files.outputs.any_changed == 'true' &&
|
||||
!contains(github.event.pull_request.labels.*.name, 'ci:allow-previous-version-upgrade-mutation')
|
||||
run: |
|
||||
VERSION_CONSTANT_FILE="packages/twenty-server/src/engine/core-modules/upgrade/constants/twenty-current-version.constant.ts"
|
||||
|
||||
CURRENT_VERSION=$(sed -n "s/.*TWENTY_CURRENT_VERSION = '\([0-9.]*\)'.*/\1/p" "$VERSION_CONSTANT_FILE")
|
||||
|
||||
if [ -z "$CURRENT_VERSION" ]; then
|
||||
echo "::error::Could not extract TWENTY_CURRENT_VERSION from $VERSION_CONSTANT_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CURRENT_DIR=$(echo "$CURRENT_VERSION" | sed -E 's/^([0-9]+)\.([0-9]+)\..*/\1-\2/')
|
||||
|
||||
echo "Current version: $CURRENT_VERSION (directory: $CURRENT_DIR)"
|
||||
|
||||
ADDED_OFFENDERS=""
|
||||
MODIFIED_OFFENDERS=""
|
||||
|
||||
check_files() {
|
||||
local category="$1"
|
||||
shift
|
||||
for file in "$@"; do
|
||||
VERSION_DIR=$(echo "$file" | sed -n 's|.*upgrade-version-command/\([0-9]*-[0-9]*\)/.*|\1|p')
|
||||
|
||||
if [ -n "$VERSION_DIR" ] && [ "$VERSION_DIR" != "$CURRENT_DIR" ]; then
|
||||
if [ "$category" = "added" ]; then
|
||||
ADDED_OFFENDERS="$ADDED_OFFENDERS\n - $file (version directory: $VERSION_DIR)"
|
||||
else
|
||||
MODIFIED_OFFENDERS="$MODIFIED_OFFENDERS\n - $file (version directory: $VERSION_DIR)"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
check_files "added" ${{ steps.changed-files.outputs.added_files }}
|
||||
check_files "modified" ${{ steps.changed-files.outputs.modified_files }}
|
||||
|
||||
if [ -n "$ADDED_OFFENDERS" ] || [ -n "$MODIFIED_OFFENDERS" ]; then
|
||||
echo "This PR touches upgrade command files outside the current version directory ($CURRENT_DIR / $CURRENT_VERSION)."
|
||||
|
||||
if [ -n "$ADDED_OFFENDERS" ]; then
|
||||
echo ""
|
||||
echo "New files added to non-current version directories:"
|
||||
echo -e "$ADDED_OFFENDERS"
|
||||
fi
|
||||
|
||||
if [ -n "$MODIFIED_OFFENDERS" ]; then
|
||||
echo ""
|
||||
echo "Existing files modified in non-current version directories:"
|
||||
echo -e "$MODIFIED_OFFENDERS"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "If this is intentional, add the label 'ci:allow-previous-version-upgrade-mutation' to this PR and re-run CI."
|
||||
echo "Otherwise, please move your changes to the current version directory ($CURRENT_DIR)."
|
||||
|
||||
echo "::error::Upgrade commands were added or modified in non-current version directories."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
server-validation:
|
||||
needs: server-build
|
||||
timeout-minutes: 30
|
||||
@@ -311,6 +388,7 @@ jobs:
|
||||
changed-files-check,
|
||||
server-build,
|
||||
server-lint-typecheck,
|
||||
server-previous-version-upgrade-mutation-guard,
|
||||
server-validation,
|
||||
server-test,
|
||||
server-integration-test,
|
||||
|
||||
@@ -74,8 +74,6 @@ jobs:
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
download_translations: true
|
||||
source: '**/en.po'
|
||||
translation: '%original_path%/%locale%.po'
|
||||
export_only_approved: false
|
||||
localization_branch_name: i18n
|
||||
base_url: 'https://twenty.api.crowdin.com'
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
# 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
|
||||
@@ -0,0 +1,111 @@
|
||||
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
|
||||
@@ -1,172 +1,19 @@
|
||||
{
|
||||
"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",
|
||||
"vite": "^7.0.0",
|
||||
"vitest": "^4.0.18"
|
||||
"verdaccio": "^6.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
@@ -185,7 +32,8 @@
|
||||
"@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"
|
||||
"@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"
|
||||
},
|
||||
"version": "0.2.1",
|
||||
"nx": {},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-twenty-app",
|
||||
"version": "2.1.0",
|
||||
"version": "2.3.0",
|
||||
"description": "Command-line interface to create Twenty application",
|
||||
"main": "dist/cli.cjs",
|
||||
"bin": "dist/cli.cjs",
|
||||
@@ -40,12 +40,17 @@
|
||||
"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,6 +26,7 @@ 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 (
|
||||
@@ -36,6 +37,7 @@ const program = new Command(packageJson.name)
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
skipLocalInstance?: boolean;
|
||||
yes?: boolean;
|
||||
},
|
||||
) => {
|
||||
if (directory && !/^[a-z0-9-]+$/.test(directory)) {
|
||||
@@ -59,6 +61,7 @@ const program = new Command(packageJson.name)
|
||||
displayName: options?.displayName,
|
||||
description: options?.description,
|
||||
skipLocalInstance: options?.skipLocalInstance,
|
||||
yes: options?.yes,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -11,7 +11,9 @@ import * as path from 'path';
|
||||
import { basename } from 'path';
|
||||
import {
|
||||
authLoginOAuth,
|
||||
checkDockerRunning,
|
||||
ConfigService,
|
||||
containerExists,
|
||||
detectLocalServer,
|
||||
serverStart,
|
||||
type ServerStartResult,
|
||||
@@ -27,6 +29,7 @@ type CreateAppOptions = {
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
skipLocalInstance?: boolean;
|
||||
yes?: boolean;
|
||||
};
|
||||
|
||||
export class CreateAppCommand {
|
||||
@@ -71,7 +74,7 @@ export class CreateAppCommand {
|
||||
let serverResult: ServerStartResult | undefined;
|
||||
|
||||
if (!options.skipLocalInstance) {
|
||||
const shouldStartServer = await this.shouldStartServer();
|
||||
const shouldStartServer = await this.shouldStartServer(options.yes);
|
||||
|
||||
if (shouldStartServer) {
|
||||
const startResult = await serverStart({
|
||||
@@ -223,13 +226,35 @@ export class CreateAppCommand {
|
||||
);
|
||||
}
|
||||
|
||||
private async shouldStartServer(): Promise<boolean> {
|
||||
private async shouldStartServer(autoConfirm?: boolean): 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',
|
||||
|
||||
@@ -82,7 +82,17 @@ describe('verifyGitHubSignature', () => {
|
||||
});
|
||||
|
||||
describe('getRawBodyForSignature', () => {
|
||||
it('returns the string as-is for string body', () => {
|
||||
it('prefers event.rawBody when the runtime forwarded it', () => {
|
||||
const original = '{ "action": "opened", "number": 42 }';
|
||||
expect(
|
||||
getRawBodyForSignature({
|
||||
body: { action: 'opened', number: 42 },
|
||||
rawBody: original,
|
||||
}),
|
||||
).toBe(original);
|
||||
});
|
||||
|
||||
it('falls back to string body when rawBody is not provided', () => {
|
||||
expect(
|
||||
getRawBodyForSignature({ body: '{"a":1}', isBase64Encoded: false }),
|
||||
).toBe('{"a":1}');
|
||||
@@ -119,3 +129,22 @@ describe('verifyGitHubSignature with parsed body', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('end-to-end: server forwards rawBody, signature verifies', () => {
|
||||
it('verifies a signature when rawBody is present alongside parsed body', () => {
|
||||
const original = '{ "action": "opened", "number": 42 }';
|
||||
const event = {
|
||||
body: { action: 'opened', number: 42 },
|
||||
rawBody: original,
|
||||
isBase64Encoded: false,
|
||||
};
|
||||
|
||||
const rawBody = getRawBodyForSignature(event);
|
||||
const result = verifyGitHubSignature({
|
||||
rawBody,
|
||||
signatureHeader: sign(original),
|
||||
secret: SECRET,
|
||||
});
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,11 @@ export type SignatureVerificationResult =
|
||||
export function getRawBodyForSignature(event: {
|
||||
body: unknown;
|
||||
isBase64Encoded?: boolean;
|
||||
rawBody?: string;
|
||||
}): string | null {
|
||||
if (typeof event.rawBody === 'string') {
|
||||
return event.rawBody;
|
||||
}
|
||||
const raw = event.body;
|
||||
if (raw == null) return '';
|
||||
if (typeof raw === 'string') {
|
||||
|
||||
@@ -8,7 +8,6 @@ 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',
|
||||
|
||||
@@ -4,6 +4,5 @@ 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,7 +5,6 @@ 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',
|
||||
|
||||
@@ -30,31 +30,31 @@ export default defineView({
|
||||
],
|
||||
groups: [
|
||||
{
|
||||
universalIdentifier: 'bg1a2b3c-0001-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
universalIdentifier: 'e9ed34f1-3c3d-41b1-869b-00aae0033d9c',
|
||||
fieldValue: 'DRAFT',
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'bg1a2b3c-0002-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
universalIdentifier: '19b1a3c1-53f0-4d32-b072-d645dac98e38',
|
||||
fieldValue: 'SENT',
|
||||
position: 1,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'bg1a2b3c-0003-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
universalIdentifier: 'f545cb5a-370d-423f-9b4e-278a9a465bdf',
|
||||
fieldValue: 'DELIVERED',
|
||||
position: 2,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'bg1a2b3c-0004-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
universalIdentifier: '5d4c6d5f-af53-4cd0-a843-df38915561b2',
|
||||
fieldValue: 'RETURNED',
|
||||
position: 3,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'bg1a2b3c-0005-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
universalIdentifier: '5ebbd7dc-9939-4594-b2a0-519269b4531f',
|
||||
fieldValue: 'LOST',
|
||||
position: 4,
|
||||
isVisible: true,
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["typescript"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
},
|
||||
"ignorePatterns": ["node_modules", "dist"],
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
|
||||
"typescript/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"typescript/no-explicit-any": "off"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 469 B |
@@ -0,0 +1,32 @@
|
||||
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,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
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,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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';
|
||||
@@ -0,0 +1,126 @@
|
||||
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' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
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' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
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;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
export const ISSUE_CREATE_MUTATION = `
|
||||
mutation IssueCreate($input: IssueCreateInput!) {
|
||||
issueCreate(input: $input) {
|
||||
success
|
||||
issue { id identifier title url }
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,33 @@
|
||||
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,
|
||||
isTool: true,
|
||||
toolInputSchema: {
|
||||
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'],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
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 };
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
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 };
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
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,
|
||||
isTool: true,
|
||||
toolInputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
export type CreateIssueInput = {
|
||||
teamId?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
export type CreateIssueMutationResult = {
|
||||
issueCreate: {
|
||||
success: boolean;
|
||||
issue: {
|
||||
id: string;
|
||||
identifier: string;
|
||||
title: string;
|
||||
url: string;
|
||||
} | null;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
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}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export type LinearGraphQLResult<TData> = {
|
||||
data?: TData;
|
||||
errors?: Array<{ message: string }>;
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
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: [],
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"types": ["vitest/globals", "node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
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.1.0",
|
||||
"version": "2.3.0",
|
||||
"sideEffects": false,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@@ -46,6 +46,8 @@
|
||||
"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",
|
||||
|
||||
@@ -580,6 +580,7 @@ type Application {
|
||||
id: UUID!
|
||||
name: String!
|
||||
description: String
|
||||
logo: String
|
||||
version: String
|
||||
universalIdentifier: String!
|
||||
packageJsonChecksum: String
|
||||
@@ -862,7 +863,6 @@ type User {
|
||||
firstName: String!
|
||||
lastName: String!
|
||||
email: String!
|
||||
defaultAvatarUrl: String
|
||||
isEmailVerified: Boolean!
|
||||
disabled: Boolean
|
||||
canImpersonate: Boolean!
|
||||
@@ -1292,6 +1292,20 @@ 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!
|
||||
@@ -2202,7 +2216,6 @@ type MarketplaceApp {
|
||||
id: String!
|
||||
name: String!
|
||||
description: String!
|
||||
icon: String!
|
||||
author: String!
|
||||
category: String!
|
||||
logo: String
|
||||
@@ -2537,6 +2550,10 @@ type ConnectedAccountDTO {
|
||||
connectionParameters: ImapSmtpCaldavConnectionParameters
|
||||
lastSignedInAt: DateTime
|
||||
userWorkspaceId: UUID!
|
||||
applicationConnectionProviderId: UUID
|
||||
applicationId: UUID
|
||||
name: String
|
||||
visibility: String!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
@@ -2564,6 +2581,10 @@ type ConnectedAccountPublicDTO {
|
||||
scopes: [String!]
|
||||
lastSignedInAt: DateTime
|
||||
userWorkspaceId: UUID!
|
||||
applicationConnectionProviderId: UUID
|
||||
applicationId: UUID
|
||||
name: String
|
||||
visibility: String!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
connectionParameters: PublicImapSmtpCaldavConnectionParameters
|
||||
@@ -2609,19 +2630,6 @@ 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!
|
||||
@@ -2634,6 +2642,21 @@ 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!
|
||||
@@ -2661,22 +2684,6 @@ 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!
|
||||
@@ -2933,6 +2940,7 @@ 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!
|
||||
@@ -2993,22 +3001,13 @@ 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!
|
||||
@@ -3057,56 +3056,6 @@ 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
|
||||
@@ -3193,6 +3142,7 @@ 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!
|
||||
@@ -3242,6 +3192,7 @@ 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!
|
||||
@@ -3291,6 +3242,10 @@ 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!
|
||||
@@ -3340,7 +3295,6 @@ 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!
|
||||
@@ -3510,6 +3464,59 @@ 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!
|
||||
|
||||
@@ -414,6 +414,7 @@ export interface Application {
|
||||
id: Scalars['UUID']
|
||||
name: Scalars['String']
|
||||
description?: Scalars['String']
|
||||
logo?: Scalars['String']
|
||||
version?: Scalars['String']
|
||||
universalIdentifier: Scalars['String']
|
||||
packageJsonChecksum?: Scalars['String']
|
||||
@@ -647,7 +648,6 @@ export interface User {
|
||||
firstName: Scalars['String']
|
||||
lastName: Scalars['String']
|
||||
email: Scalars['String']
|
||||
defaultAvatarUrl?: Scalars['String']
|
||||
isEmailVerified: Scalars['Boolean']
|
||||
disabled?: Scalars['Boolean']
|
||||
canImpersonate: Scalars['Boolean']
|
||||
@@ -1018,6 +1018,22 @@ export interface PageLayout {
|
||||
|
||||
export type PageLayoutType = 'RECORD_INDEX' | 'RECORD_PAGE' | 'DASHBOARD' | 'STANDALONE_PAGE'
|
||||
|
||||
export interface ApplicationConnectionProviderOAuthConfig {
|
||||
scopes: Scalars['String'][]
|
||||
isClientCredentialsConfigured: Scalars['Boolean']
|
||||
__typename: 'ApplicationConnectionProviderOAuthConfig'
|
||||
}
|
||||
|
||||
export interface ApplicationConnectionProvider {
|
||||
id: Scalars['UUID']
|
||||
applicationId: Scalars['String']
|
||||
type: Scalars['String']
|
||||
name: Scalars['String']
|
||||
displayName: Scalars['String']
|
||||
oauth?: ApplicationConnectionProviderOAuthConfig
|
||||
__typename: 'ApplicationConnectionProvider'
|
||||
}
|
||||
|
||||
export interface Analytics {
|
||||
/** Boolean that confirms query was dispatched */
|
||||
success: Scalars['Boolean']
|
||||
@@ -1940,7 +1956,6 @@ export interface MarketplaceApp {
|
||||
id: Scalars['String']
|
||||
name: Scalars['String']
|
||||
description: Scalars['String']
|
||||
icon: Scalars['String']
|
||||
author: Scalars['String']
|
||||
category: Scalars['String']
|
||||
logo?: Scalars['String']
|
||||
@@ -2223,6 +2238,10 @@ export interface ConnectedAccountDTO {
|
||||
connectionParameters?: ImapSmtpCaldavConnectionParameters
|
||||
lastSignedInAt?: Scalars['DateTime']
|
||||
userWorkspaceId: Scalars['UUID']
|
||||
applicationConnectionProviderId?: Scalars['UUID']
|
||||
applicationId?: Scalars['UUID']
|
||||
name?: Scalars['String']
|
||||
visibility: Scalars['String']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
__typename: 'ConnectedAccountDTO'
|
||||
@@ -2253,6 +2272,10 @@ export interface ConnectedAccountPublicDTO {
|
||||
scopes?: Scalars['String'][]
|
||||
lastSignedInAt?: Scalars['DateTime']
|
||||
userWorkspaceId: Scalars['UUID']
|
||||
applicationConnectionProviderId?: Scalars['UUID']
|
||||
applicationId?: Scalars['UUID']
|
||||
name?: Scalars['String']
|
||||
visibility: Scalars['String']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
connectionParameters?: PublicImapSmtpCaldavConnectionParameters
|
||||
@@ -2304,20 +2327,6 @@ export interface Skill {
|
||||
__typename: 'Skill'
|
||||
}
|
||||
|
||||
export interface AgentChatThread {
|
||||
id: Scalars['UUID']
|
||||
title?: Scalars['String']
|
||||
totalInputTokens: Scalars['Int']
|
||||
totalOutputTokens: Scalars['Int']
|
||||
contextWindowTokens?: Scalars['Int']
|
||||
conversationSize: Scalars['Int']
|
||||
totalInputCredits: Scalars['Float']
|
||||
totalOutputCredits: Scalars['Float']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
__typename: 'AgentChatThread'
|
||||
}
|
||||
|
||||
export interface AgentMessage {
|
||||
id: Scalars['UUID']
|
||||
threadId: Scalars['UUID']
|
||||
@@ -2331,6 +2340,22 @@ export interface AgentMessage {
|
||||
__typename: 'AgentMessage'
|
||||
}
|
||||
|
||||
export interface AgentChatThread {
|
||||
id: Scalars['ID']
|
||||
title?: Scalars['String']
|
||||
totalInputTokens: Scalars['Int']
|
||||
totalOutputTokens: Scalars['Int']
|
||||
contextWindowTokens?: Scalars['Int']
|
||||
conversationSize: Scalars['Int']
|
||||
totalInputCredits: Scalars['Float']
|
||||
totalOutputCredits: Scalars['Float']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
deletedAt?: Scalars['DateTime']
|
||||
lastMessageAt?: Scalars['DateTime']
|
||||
__typename: 'AgentChatThread'
|
||||
}
|
||||
|
||||
export interface AiSystemPromptSection {
|
||||
title: Scalars['String']
|
||||
content: Scalars['String']
|
||||
@@ -2363,22 +2388,6 @@ export interface AgentChatEvent {
|
||||
__typename: 'AgentChatEvent'
|
||||
}
|
||||
|
||||
export interface AgentChatThreadEdge {
|
||||
/** The node containing the AgentChatThread */
|
||||
node: AgentChatThread
|
||||
/** Cursor for this node. */
|
||||
cursor: Scalars['ConnectionCursor']
|
||||
__typename: 'AgentChatThreadEdge'
|
||||
}
|
||||
|
||||
export interface AgentChatThreadConnection {
|
||||
/** Paging information */
|
||||
pageInfo: PageInfo
|
||||
/** Array of edges. */
|
||||
edges: AgentChatThreadEdge[]
|
||||
__typename: 'AgentChatThreadConnection'
|
||||
}
|
||||
|
||||
export interface AgentTurnEvaluation {
|
||||
id: Scalars['UUID']
|
||||
turnId: Scalars['UUID']
|
||||
@@ -2558,6 +2567,7 @@ export interface Query {
|
||||
getPageLayoutTab: PageLayoutTab
|
||||
getPageLayouts: PageLayout[]
|
||||
getPageLayout?: PageLayout
|
||||
applicationConnectionProviders: ApplicationConnectionProvider[]
|
||||
getPageLayoutWidgets: PageLayoutWidget[]
|
||||
getPageLayoutWidget: PageLayoutWidget
|
||||
findOneLogicFunction: LogicFunction
|
||||
@@ -2591,13 +2601,13 @@ export interface Query {
|
||||
webhooks: Webhook[]
|
||||
webhook?: Webhook
|
||||
minimalMetadata: MinimalMetadata
|
||||
chatThreads: AgentChatThread[]
|
||||
chatThread: AgentChatThread
|
||||
chatMessages: AgentMessage[]
|
||||
chatStreamCatchupChunks: ChatStreamCatchupChunks
|
||||
getAiSystemPromptPreview: AiSystemPromptPreview
|
||||
skills: Skill[]
|
||||
skill?: Skill
|
||||
chatThreads: AgentChatThreadConnection
|
||||
agentTurns: AgentTurn[]
|
||||
checkUserExists: CheckUserExist
|
||||
checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid
|
||||
@@ -2633,16 +2643,6 @@ export interface Query {
|
||||
__typename: 'Query'
|
||||
}
|
||||
|
||||
export type AgentChatThreadSortFields = 'id' | 'updatedAt'
|
||||
|
||||
|
||||
/** Sort Directions */
|
||||
export type SortDirection = 'ASC' | 'DESC'
|
||||
|
||||
|
||||
/** Sort Nulls Options */
|
||||
export type SortNulls = 'NULLS_FIRST' | 'NULLS_LAST'
|
||||
|
||||
export type EventLogTable = 'WORKSPACE_EVENT' | 'PAGEVIEW' | 'OBJECT_EVENT' | 'USAGE_EVENT' | 'APPLICATION_LOG'
|
||||
|
||||
export type UsageOperationType = 'AI_CHAT_TOKEN' | 'AI_WORKFLOW_TOKEN' | 'WORKFLOW_EXECUTION' | 'CODE_EXECUTION' | 'WEB_SEARCH'
|
||||
@@ -2675,6 +2675,7 @@ export interface Mutation {
|
||||
updateView: View
|
||||
deleteView: Scalars['Boolean']
|
||||
destroyView: Scalars['Boolean']
|
||||
upsertViewWidget: View
|
||||
createViewSort: ViewSort
|
||||
updateViewSort: ViewSort
|
||||
deleteViewSort: Scalars['Boolean']
|
||||
@@ -2724,6 +2725,7 @@ export interface Mutation {
|
||||
resetPageLayoutToDefault: PageLayout
|
||||
resetPageLayoutWidgetToDefault: PageLayoutWidget
|
||||
resetPageLayoutTabToDefault: PageLayoutTab
|
||||
updateOneApplicationVariable: Scalars['Boolean']
|
||||
createPageLayoutWidget: PageLayoutWidget
|
||||
updatePageLayoutWidget: PageLayoutWidget
|
||||
destroyPageLayoutWidget: Scalars['Boolean']
|
||||
@@ -2773,6 +2775,10 @@ export interface Mutation {
|
||||
createChatThread: AgentChatThread
|
||||
sendChatMessage: SendChatMessageResult
|
||||
stopAgentChatStream: Scalars['Boolean']
|
||||
renameChatThread: AgentChatThread
|
||||
archiveChatThread: AgentChatThread
|
||||
unarchiveChatThread: AgentChatThread
|
||||
deleteChatThread: Scalars['Boolean']
|
||||
deleteQueuedChatMessage: Scalars['Boolean']
|
||||
createSkill: Skill
|
||||
updateSkill: Skill
|
||||
@@ -2822,7 +2828,6 @@ export interface Mutation {
|
||||
installApplication: Scalars['Boolean']
|
||||
runWorkspaceMigration: Scalars['Boolean']
|
||||
uninstallApplication: Scalars['Boolean']
|
||||
updateOneApplicationVariable: Scalars['Boolean']
|
||||
createOIDCIdentityProvider: SetupSso
|
||||
createSAMLIdentityProvider: SetupSso
|
||||
deleteSSOIdentityProvider: DeleteSso
|
||||
@@ -3313,6 +3318,7 @@ export interface ApplicationGenqlSelection{
|
||||
id?: boolean | number
|
||||
name?: boolean | number
|
||||
description?: boolean | number
|
||||
logo?: boolean | number
|
||||
version?: boolean | number
|
||||
universalIdentifier?: boolean | number
|
||||
packageJsonChecksum?: boolean | number
|
||||
@@ -3536,7 +3542,6 @@ export interface UserGenqlSelection{
|
||||
firstName?: boolean | number
|
||||
lastName?: boolean | number
|
||||
email?: boolean | number
|
||||
defaultAvatarUrl?: boolean | number
|
||||
isEmailVerified?: boolean | number
|
||||
disabled?: boolean | number
|
||||
canImpersonate?: boolean | number
|
||||
@@ -3934,6 +3939,24 @@ export interface PageLayoutGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ApplicationConnectionProviderOAuthConfigGenqlSelection{
|
||||
scopes?: boolean | number
|
||||
isClientCredentialsConfigured?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ApplicationConnectionProviderGenqlSelection{
|
||||
id?: boolean | number
|
||||
applicationId?: boolean | number
|
||||
type?: boolean | number
|
||||
name?: boolean | number
|
||||
displayName?: boolean | number
|
||||
oauth?: ApplicationConnectionProviderOAuthConfigGenqlSelection
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface AnalyticsGenqlSelection{
|
||||
/** Boolean that confirms query was dispatched */
|
||||
success?: boolean | number
|
||||
@@ -4922,7 +4945,6 @@ export interface MarketplaceAppGenqlSelection{
|
||||
id?: boolean | number
|
||||
name?: boolean | number
|
||||
description?: boolean | number
|
||||
icon?: boolean | number
|
||||
author?: boolean | number
|
||||
category?: boolean | number
|
||||
logo?: boolean | number
|
||||
@@ -5228,6 +5250,10 @@ export interface ConnectedAccountDTOGenqlSelection{
|
||||
connectionParameters?: ImapSmtpCaldavConnectionParametersGenqlSelection
|
||||
lastSignedInAt?: boolean | number
|
||||
userWorkspaceId?: boolean | number
|
||||
applicationConnectionProviderId?: boolean | number
|
||||
applicationId?: boolean | number
|
||||
name?: boolean | number
|
||||
visibility?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
__typename?: boolean | number
|
||||
@@ -5261,6 +5287,10 @@ export interface ConnectedAccountPublicDTOGenqlSelection{
|
||||
scopes?: boolean | number
|
||||
lastSignedInAt?: boolean | number
|
||||
userWorkspaceId?: boolean | number
|
||||
applicationConnectionProviderId?: boolean | number
|
||||
applicationId?: boolean | number
|
||||
name?: boolean | number
|
||||
visibility?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
connectionParameters?: PublicImapSmtpCaldavConnectionParametersGenqlSelection
|
||||
@@ -5318,6 +5348,20 @@ export interface SkillGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface AgentMessageGenqlSelection{
|
||||
id?: boolean | number
|
||||
threadId?: boolean | number
|
||||
turnId?: boolean | number
|
||||
agentId?: boolean | number
|
||||
role?: boolean | number
|
||||
status?: boolean | number
|
||||
parts?: AgentMessagePartGenqlSelection
|
||||
processedAt?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface AgentChatThreadGenqlSelection{
|
||||
id?: boolean | number
|
||||
title?: boolean | number
|
||||
@@ -5329,20 +5373,8 @@ export interface AgentChatThreadGenqlSelection{
|
||||
totalOutputCredits?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface AgentMessageGenqlSelection{
|
||||
id?: boolean | number
|
||||
threadId?: boolean | number
|
||||
turnId?: boolean | number
|
||||
agentId?: boolean | number
|
||||
role?: boolean | number
|
||||
status?: boolean | number
|
||||
parts?: AgentMessagePartGenqlSelection
|
||||
processedAt?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
deletedAt?: boolean | number
|
||||
lastMessageAt?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
@@ -5384,24 +5416,6 @@ export interface AgentChatEventGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface AgentChatThreadEdgeGenqlSelection{
|
||||
/** The node containing the AgentChatThread */
|
||||
node?: AgentChatThreadGenqlSelection
|
||||
/** Cursor for this node. */
|
||||
cursor?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface AgentChatThreadConnectionGenqlSelection{
|
||||
/** Paging information */
|
||||
pageInfo?: PageInfoGenqlSelection
|
||||
/** Array of edges. */
|
||||
edges?: AgentChatThreadEdgeGenqlSelection
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface AgentTurnEvaluationGenqlSelection{
|
||||
id?: boolean | number
|
||||
turnId?: boolean | number
|
||||
@@ -5565,6 +5579,7 @@ export interface QueryGenqlSelection{
|
||||
getPageLayoutTab?: (PageLayoutTabGenqlSelection & { __args: {id: Scalars['String']} })
|
||||
getPageLayouts?: (PageLayoutGenqlSelection & { __args?: {objectMetadataId?: (Scalars['String'] | null), pageLayoutType?: (PageLayoutType | null)} })
|
||||
getPageLayout?: (PageLayoutGenqlSelection & { __args: {id: Scalars['String']} })
|
||||
applicationConnectionProviders?: (ApplicationConnectionProviderGenqlSelection & { __args: {applicationId: Scalars['UUID']} })
|
||||
getPageLayoutWidgets?: (PageLayoutWidgetGenqlSelection & { __args: {pageLayoutTabId: Scalars['String']} })
|
||||
getPageLayoutWidget?: (PageLayoutWidgetGenqlSelection & { __args: {id: Scalars['String']} })
|
||||
findOneLogicFunction?: (LogicFunctionGenqlSelection & { __args: {input: LogicFunctionIdInput} })
|
||||
@@ -5616,19 +5631,13 @@ export interface QueryGenqlSelection{
|
||||
webhooks?: WebhookGenqlSelection
|
||||
webhook?: (WebhookGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
minimalMetadata?: MinimalMetadataGenqlSelection
|
||||
chatThreads?: AgentChatThreadGenqlSelection
|
||||
chatThread?: (AgentChatThreadGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
chatMessages?: (AgentMessageGenqlSelection & { __args: {threadId: Scalars['UUID']} })
|
||||
chatStreamCatchupChunks?: (ChatStreamCatchupChunksGenqlSelection & { __args: {threadId: Scalars['UUID']} })
|
||||
getAiSystemPromptPreview?: AiSystemPromptPreviewGenqlSelection
|
||||
skills?: SkillGenqlSelection
|
||||
skill?: (SkillGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
chatThreads?: (AgentChatThreadConnectionGenqlSelection & { __args: {
|
||||
/** Limit or page results. */
|
||||
paging: CursorPaging,
|
||||
/** Specify to filter the records returned. */
|
||||
filter: AgentChatThreadFilter,
|
||||
/** Specify to sort results. */
|
||||
sorting: AgentChatThreadSort[]} })
|
||||
agentTurns?: (AgentTurnGenqlSelection & { __args: {agentId: Scalars['UUID']} })
|
||||
checkUserExists?: (CheckUserExistGenqlSelection & { __args: {email: Scalars['String'], captchaToken?: (Scalars['String'] | null)} })
|
||||
checkWorkspaceInviteHashIsValid?: (WorkspaceInviteHashValidGenqlSelection & { __args: {inviteHash: Scalars['String']} })
|
||||
@@ -5675,14 +5684,6 @@ export interface AgentIdInput {
|
||||
/** The id of the agent. */
|
||||
id: Scalars['UUID']}
|
||||
|
||||
export interface AgentChatThreadFilter {and?: (AgentChatThreadFilter[] | null),or?: (AgentChatThreadFilter[] | null),id?: (UUIDFilterComparison | null),updatedAt?: (DateFieldComparison | null)}
|
||||
|
||||
export interface DateFieldComparison {is?: (Scalars['Boolean'] | null),isNot?: (Scalars['Boolean'] | null),eq?: (Scalars['DateTime'] | null),neq?: (Scalars['DateTime'] | null),gt?: (Scalars['DateTime'] | null),gte?: (Scalars['DateTime'] | null),lt?: (Scalars['DateTime'] | null),lte?: (Scalars['DateTime'] | null),in?: (Scalars['DateTime'][] | null),notIn?: (Scalars['DateTime'][] | null),between?: (DateFieldComparisonBetween | null),notBetween?: (DateFieldComparisonBetween | null)}
|
||||
|
||||
export interface DateFieldComparisonBetween {lower: Scalars['DateTime'],upper: Scalars['DateTime']}
|
||||
|
||||
export interface AgentChatThreadSort {field: AgentChatThreadSortFields,direction: SortDirection,nulls?: (SortNulls | null)}
|
||||
|
||||
export interface EventLogQueryInput {table: EventLogTable,filters?: (EventLogFiltersInput | null),first?: (Scalars['Int'] | null),after?: (Scalars['String'] | null)}
|
||||
|
||||
export interface EventLogFiltersInput {eventType?: (Scalars['String'] | null),userWorkspaceId?: (Scalars['String'] | null),dateRange?: (EventLogDateRangeInput | null),recordId?: (Scalars['String'] | null),objectMetadataId?: (Scalars['String'] | null)}
|
||||
@@ -5725,6 +5726,7 @@ export interface MutationGenqlSelection{
|
||||
updateView?: (ViewGenqlSelection & { __args: {id: Scalars['String'], input: UpdateViewInput} })
|
||||
deleteView?: { __args: {id: Scalars['String']} }
|
||||
destroyView?: { __args: {id: Scalars['String']} }
|
||||
upsertViewWidget?: (ViewGenqlSelection & { __args: {input: UpsertViewWidgetInput} })
|
||||
createViewSort?: (ViewSortGenqlSelection & { __args: {input: CreateViewSortInput} })
|
||||
updateViewSort?: (ViewSortGenqlSelection & { __args: {input: UpdateViewSortInput} })
|
||||
deleteViewSort?: { __args: {input: DeleteViewSortInput} }
|
||||
@@ -5774,6 +5776,7 @@ export interface MutationGenqlSelection{
|
||||
resetPageLayoutToDefault?: (PageLayoutGenqlSelection & { __args: {id: Scalars['String']} })
|
||||
resetPageLayoutWidgetToDefault?: (PageLayoutWidgetGenqlSelection & { __args: {id: Scalars['String']} })
|
||||
resetPageLayoutTabToDefault?: (PageLayoutTabGenqlSelection & { __args: {id: Scalars['String']} })
|
||||
updateOneApplicationVariable?: { __args: {key: Scalars['String'], value: Scalars['String'], applicationId: Scalars['UUID']} }
|
||||
createPageLayoutWidget?: (PageLayoutWidgetGenqlSelection & { __args: {input: CreatePageLayoutWidgetInput} })
|
||||
updatePageLayoutWidget?: (PageLayoutWidgetGenqlSelection & { __args: {id: Scalars['String'], input: UpdatePageLayoutWidgetInput} })
|
||||
destroyPageLayoutWidget?: { __args: {id: Scalars['String']} }
|
||||
@@ -5823,6 +5826,10 @@ export interface MutationGenqlSelection{
|
||||
createChatThread?: AgentChatThreadGenqlSelection
|
||||
sendChatMessage?: (SendChatMessageResultGenqlSelection & { __args: {threadId: Scalars['UUID'], text: Scalars['String'], messageId: Scalars['UUID'], browsingContext?: (Scalars['JSON'] | null), modelId?: (Scalars['String'] | null), fileIds?: (Scalars['UUID'][] | null)} })
|
||||
stopAgentChatStream?: { __args: {threadId: Scalars['UUID']} }
|
||||
renameChatThread?: (AgentChatThreadGenqlSelection & { __args: {id: Scalars['UUID'], title: Scalars['String']} })
|
||||
archiveChatThread?: (AgentChatThreadGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
unarchiveChatThread?: (AgentChatThreadGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
deleteChatThread?: { __args: {id: Scalars['UUID']} }
|
||||
deleteQueuedChatMessage?: { __args: {messageId: Scalars['UUID']} }
|
||||
createSkill?: (SkillGenqlSelection & { __args: {input: CreateSkillInput} })
|
||||
updateSkill?: (SkillGenqlSelection & { __args: {input: UpdateSkillInput} })
|
||||
@@ -5872,7 +5879,6 @@ export interface MutationGenqlSelection{
|
||||
installApplication?: { __args: {appRegistrationId: Scalars['String'], version?: (Scalars['String'] | null)} }
|
||||
runWorkspaceMigration?: { __args: {workspaceMigration: WorkspaceMigrationInput} }
|
||||
uninstallApplication?: { __args: {universalIdentifier: Scalars['String']} }
|
||||
updateOneApplicationVariable?: { __args: {key: Scalars['String'], value: Scalars['String'], applicationId: Scalars['UUID']} }
|
||||
createOIDCIdentityProvider?: (SetupSsoGenqlSelection & { __args: {input: SetupOIDCSsoInput} })
|
||||
createSAMLIdentityProvider?: (SetupSsoGenqlSelection & { __args: {input: SetupSAMLSsoInput} })
|
||||
deleteSSOIdentityProvider?: (DeleteSsoGenqlSelection & { __args: {input: DeleteSsoInput} })
|
||||
@@ -5944,6 +5950,30 @@ export interface CreateViewInput {id?: (Scalars['UUID'] | null),name: Scalars['S
|
||||
|
||||
export interface UpdateViewInput {id?: (Scalars['UUID'] | null),name?: (Scalars['String'] | null),type?: (ViewType | null),icon?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isCompact?: (Scalars['Boolean'] | null),openRecordIn?: (ViewOpenRecordIn | null),kanbanAggregateOperation?: (AggregateOperations | null),kanbanAggregateOperationFieldMetadataId?: (Scalars['UUID'] | null),anyFieldFilterValue?: (Scalars['String'] | null),calendarLayout?: (ViewCalendarLayout | null),calendarFieldMetadataId?: (Scalars['UUID'] | null),visibility?: (ViewVisibility | null),mainGroupByFieldMetadataId?: (Scalars['UUID'] | null),shouldHideEmptyGroups?: (Scalars['Boolean'] | null)}
|
||||
|
||||
export interface UpsertViewWidgetInput {
|
||||
/** The id of the view widget (page layout widget). */
|
||||
widgetId: Scalars['UUID'],
|
||||
/** The view fields to upsert. */
|
||||
viewFields?: (UpsertViewWidgetViewFieldInput[] | null),
|
||||
/** The view filters to upsert. */
|
||||
viewFilters?: (UpsertViewWidgetViewFilterInput[] | null),
|
||||
/** The view filter groups to upsert. */
|
||||
viewFilterGroups?: (UpsertViewWidgetViewFilterGroupInput[] | null),
|
||||
/** The view sorts to upsert. */
|
||||
viewSorts?: (UpsertViewWidgetViewSortInput[] | null)}
|
||||
|
||||
export interface UpsertViewWidgetViewFieldInput {
|
||||
/** The id of an existing view field to update. */
|
||||
viewFieldId?: (Scalars['UUID'] | null),
|
||||
/** The field metadata id. Used to create a new view field when viewFieldId is not provided. */
|
||||
fieldMetadataId?: (Scalars['UUID'] | null),isVisible: Scalars['Boolean'],position: Scalars['Float'],size?: (Scalars['Float'] | null)}
|
||||
|
||||
export interface UpsertViewWidgetViewFilterInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],operand?: (ViewFilterOperand | null),value: Scalars['JSON'],viewFilterGroupId?: (Scalars['UUID'] | null),positionInViewFilterGroup?: (Scalars['Float'] | null),subFieldName?: (Scalars['String'] | null)}
|
||||
|
||||
export interface UpsertViewWidgetViewFilterGroupInput {id?: (Scalars['UUID'] | null),parentViewFilterGroupId?: (Scalars['UUID'] | null),logicalOperator?: (ViewFilterGroupLogicalOperator | null),positionInViewFilterGroup?: (Scalars['Float'] | null)}
|
||||
|
||||
export interface UpsertViewWidgetViewSortInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],direction?: (ViewSortDirection | null)}
|
||||
|
||||
export interface CreateViewSortInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],direction?: (ViewSortDirection | null),viewId: Scalars['UUID']}
|
||||
|
||||
export interface UpdateViewSortInput {
|
||||
@@ -6827,6 +6857,22 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const ApplicationConnectionProviderOAuthConfig_possibleTypes: string[] = ['ApplicationConnectionProviderOAuthConfig']
|
||||
export const isApplicationConnectionProviderOAuthConfig = (obj?: { __typename?: any } | null): obj is ApplicationConnectionProviderOAuthConfig => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isApplicationConnectionProviderOAuthConfig"')
|
||||
return ApplicationConnectionProviderOAuthConfig_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ApplicationConnectionProvider_possibleTypes: string[] = ['ApplicationConnectionProvider']
|
||||
export const isApplicationConnectionProvider = (obj?: { __typename?: any } | null): obj is ApplicationConnectionProvider => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isApplicationConnectionProvider"')
|
||||
return ApplicationConnectionProvider_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const Analytics_possibleTypes: string[] = ['Analytics']
|
||||
export const isAnalytics = (obj?: { __typename?: any } | null): obj is Analytics => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isAnalytics"')
|
||||
@@ -8019,14 +8065,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const AgentChatThread_possibleTypes: string[] = ['AgentChatThread']
|
||||
export const isAgentChatThread = (obj?: { __typename?: any } | null): obj is AgentChatThread => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isAgentChatThread"')
|
||||
return AgentChatThread_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const AgentMessage_possibleTypes: string[] = ['AgentMessage']
|
||||
export const isAgentMessage = (obj?: { __typename?: any } | null): obj is AgentMessage => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isAgentMessage"')
|
||||
@@ -8035,6 +8073,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const AgentChatThread_possibleTypes: string[] = ['AgentChatThread']
|
||||
export const isAgentChatThread = (obj?: { __typename?: any } | null): obj is AgentChatThread => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isAgentChatThread"')
|
||||
return AgentChatThread_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const AiSystemPromptSection_possibleTypes: string[] = ['AiSystemPromptSection']
|
||||
export const isAiSystemPromptSection = (obj?: { __typename?: any } | null): obj is AiSystemPromptSection => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isAiSystemPromptSection"')
|
||||
@@ -8075,22 +8121,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const AgentChatThreadEdge_possibleTypes: string[] = ['AgentChatThreadEdge']
|
||||
export const isAgentChatThreadEdge = (obj?: { __typename?: any } | null): obj is AgentChatThreadEdge => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isAgentChatThreadEdge"')
|
||||
return AgentChatThreadEdge_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const AgentChatThreadConnection_possibleTypes: string[] = ['AgentChatThreadConnection']
|
||||
export const isAgentChatThreadConnection = (obj?: { __typename?: any } | null): obj is AgentChatThreadConnection => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isAgentChatThreadConnection"')
|
||||
return AgentChatThreadConnection_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const AgentTurnEvaluation_possibleTypes: string[] = ['AgentTurnEvaluation']
|
||||
export const isAgentTurnEvaluation = (obj?: { __typename?: any } | null): obj is AgentTurnEvaluation => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isAgentTurnEvaluation"')
|
||||
@@ -8822,21 +8852,6 @@ export const enumAllMetadataName = {
|
||||
webhook: 'webhook' as const
|
||||
}
|
||||
|
||||
export const enumAgentChatThreadSortFields = {
|
||||
id: 'id' as const,
|
||||
updatedAt: 'updatedAt' as const
|
||||
}
|
||||
|
||||
export const enumSortDirection = {
|
||||
ASC: 'ASC' as const,
|
||||
DESC: 'DESC' as const
|
||||
}
|
||||
|
||||
export const enumSortNulls = {
|
||||
NULLS_FIRST: 'NULLS_FIRST' as const,
|
||||
NULLS_LAST: 'NULLS_LAST' as const
|
||||
}
|
||||
|
||||
export const enumEventLogTable = {
|
||||
WORKSPACE_EVENT: 'WORKSPACE_EVENT' as const,
|
||||
PAGEVIEW: 'PAGEVIEW' as const,
|
||||
|
||||
@@ -4,6 +4,6 @@ set -e
|
||||
echo "==> START Registering cron jobs"
|
||||
|
||||
cd /app/packages/twenty-server
|
||||
yarn command:prod cron:register:all --dev-mode
|
||||
yarn command:prod cron:register:all
|
||||
|
||||
echo "==> DONE"
|
||||
|
||||
@@ -11,10 +11,12 @@ 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
|
||||
|
||||
|
||||
@@ -1,8 +1,26 @@
|
||||
# ===========================================================================
|
||||
# Shared build stages (used by both targets)
|
||||
# Dependency stages
|
||||
# ===========================================================================
|
||||
|
||||
FROM node:24-alpine AS common-deps
|
||||
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
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -13,22 +31,16 @@ 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 && yarn cache clean && npx nx reset
|
||||
RUN yarn workspaces focus twenty twenty-server twenty-emails twenty-shared twenty-client-sdk && yarn cache clean && npx nx reset
|
||||
|
||||
|
||||
FROM common-deps AS twenty-server-build
|
||||
FROM server-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
|
||||
|
||||
@@ -44,10 +56,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-sdk twenty-client-sdk twenty-server
|
||||
RUN yarn workspaces focus --production twenty-emails twenty-shared twenty-client-sdk twenty-server
|
||||
|
||||
|
||||
FROM common-deps AS twenty-front-build
|
||||
FROM front-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
|
||||
@@ -99,11 +111,8 @@ 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)."
|
||||
@@ -208,11 +217,8 @@ 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
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
---
|
||||
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,7 +77,6 @@ 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',
|
||||
|
||||
@@ -88,11 +88,11 @@ 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.
|
||||
Dev mode is only available on Twenty instances running in development (`NODE_ENV=development`). Production instances reject dev sync requests. Use `yarn twenty deploy` followed by `yarn twenty install` to publish and install on production servers — `deploy` publishes to the application registry, while `install` installs it on a given workspace. See [Publishing Apps](/developers/extend/apps/publishing) for details.
|
||||
</Warning>
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Dev mode terminal output" />
|
||||
<img src="/images/docs/developers/extends/apps/dev.png" alt="Dev mode terminal output" />
|
||||
</div>
|
||||
|
||||
#### One-shot sync with `yarn twenty dev --once`
|
||||
@@ -127,13 +127,7 @@ Click on **My twenty app** to open its **application registration**. A registrat
|
||||
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" />
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Installed app" />
|
||||
</div>
|
||||
|
||||
You are all set! Edit any file in `src/` and the changes will be picked up automatically.
|
||||
@@ -219,13 +213,31 @@ The scaffolder already started a local Twenty server for you. To manage it later
|
||||
| `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 server status, URL, and credentials |
|
||||
| `yarn twenty server status` | Show server status, URL, version, and credentials |
|
||||
| `yarn twenty server logs` | Stream server logs |
|
||||
| `yarn twenty server logs --lines 100` | Show the last 100 log lines |
|
||||
| `yarn twenty server reset` | Delete all data and start fresh |
|
||||
| `yarn twenty server upgrade` | Pull the latest `twenty-app-dev` image and recreate the container |
|
||||
| `yarn twenty server upgrade 2.2.0` | Upgrade to a specific version |
|
||||
|
||||
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
|
||||
|
||||
Use `yarn twenty server upgrade` to check for a newer `twenty-app-dev` Docker image and update the container. The command pulls the image, compares it against the one the container was created from, and only recreates the container if the image actually changed. Your data volumes are preserved — only the container is replaced.
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Upgrade to the latest version (skips recreation if already up to date)
|
||||
yarn twenty server upgrade
|
||||
|
||||
# Upgrade to a specific version
|
||||
yarn twenty server upgrade 2.2.0
|
||||
```
|
||||
|
||||
If a newer image is available and the container was running, the upgrade command automatically starts a new container with the updated image. Run `yarn twenty server start` afterward to wait for it to become healthy. If the image hasn't changed, the container is left untouched.
|
||||
|
||||
You can verify the running version with `yarn twenty server status`, which displays the `APP_VERSION` from the container.
|
||||
|
||||
### Running a test instance
|
||||
|
||||
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.
|
||||
@@ -234,9 +246,10 @@ Pass `--test` to any `server` command to manage a second, fully isolated instanc
|
||||
|---------|-------------|
|
||||
| `yarn twenty server start --test` | Start the test instance (defaults to port 2021) |
|
||||
| `yarn twenty server stop --test` | Stop the test instance |
|
||||
| `yarn twenty server status --test` | Show test instance status, URL, and credentials |
|
||||
| `yarn twenty server status --test` | Show test instance status, URL, version, and credentials |
|
||||
| `yarn twenty server logs --test` | Stream test instance logs |
|
||||
| `yarn twenty server reset --test` | Wipe test data and start fresh |
|
||||
| `yarn twenty server upgrade --test` | Upgrade the test instance image |
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -101,6 +101,7 @@ The `RoutePayload` type has the following structure:
|
||||
| `queryStringParameters` | `Record<string, string \| undefined>` | Query string parameters (multiple values joined with commas) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }`|
|
||||
| `pathParameters` | `Record<string, string \| undefined>` | Path parameters extracted from the route pattern | `/users/:id`, `/users/123` -> `{ id: '123' }` |
|
||||
| `body` | `object \| null` | Parsed request body (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
|
||||
| `rawBody` | `string \| undefined` | Original UTF-8 request body, before JSON parsing. Useful for verifying HMAC-style webhook signatures (e.g. GitHub's `X-Hub-Signature-256`, Stripe). `undefined` when the runtime did not preserve it. | |
|
||||
| `isBase64Encoded` | `boolean` | Whether the body is base64 encoded | |
|
||||
| `requestContext.http.method` | `string` | HTTP method (GET, POST, PUT, PATCH, DELETE) | |
|
||||
| `requestContext.http.path` | `string` | Raw request path | |
|
||||
|
||||
@@ -77,6 +77,39 @@ 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.
|
||||
|
||||
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 773 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 724 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 662 KiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 241 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 245 KiB |
@@ -94,15 +94,16 @@ const handler = async (event: RoutePayload) => {
|
||||
|
||||
يحتوي نوع `RoutePayload` على البنية التالية:
|
||||
|
||||
| الخاصية | النوع | الوصف | مثال |
|
||||
| ---------------------------- | ------------------------------------------------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------- |
|
||||
| `headers` | `Record\<string, string \| undefined>` | رؤوس HTTP (فقط تلك المدرجة في `forwardedRequestHeaders`) | انظر القسم أدناه |
|
||||
| `queryStringParameters` | `Record\<string, string \| undefined>` | معلمات سلسلة الاستعلام (تُضمّ القيم المتعددة باستخدام فواصل) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
|
||||
| `pathParameters` | `Record\<string, string \| undefined>` | معلمات المسار المستخرجة من نمط المسار | `/users/:id`, `/users/123` -> `{ id: '123' }` |
|
||||
| `body` | `object \| null` | جسم الطلب المُحلَّل (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
|
||||
| `isBase64Encoded` | `boolean` | ما إذا كان جسم الطلب مُرمَّزًا بترميز base64 | |
|
||||
| `requestContext.http.method` | `string` | طريقة HTTP (GET, POST, PUT, PATCH, DELETE) | |
|
||||
| `requestContext.http.path` | `string` | المسار الخام للطلب | |
|
||||
| الخاصية | النوع | الوصف | مثال |
|
||||
| ---------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| `headers` | `Record\<string, string \| undefined>` | رؤوس HTTP (فقط تلك المدرجة في `forwardedRequestHeaders`) | انظر القسم أدناه |
|
||||
| `queryStringParameters` | `Record\<string, string \| undefined>` | معلمات سلسلة الاستعلام (تُضمّ القيم المتعددة باستخدام فواصل) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
|
||||
| `pathParameters` | `Record\<string, string \| undefined>` | معلمات المسار المستخرجة من نمط المسار | `/users/:id`, `/users/123` -> `{ id: '123' }` |
|
||||
| `body` | `object \| null` | جسم الطلب المُحلَّل (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
|
||||
| `rawBody` | `string \| undefined` | Original UTF-8 request body, before JSON parsing. Useful for verifying HMAC-style webhook signatures (e.g. GitHub's `X-Hub-Signature-256`, Stripe). `undefined` when the runtime did not preserve it. | |
|
||||
| `isBase64Encoded` | `boolean` | ما إذا كان جسم الطلب مُرمَّزًا بترميز base64 | |
|
||||
| `requestContext.http.method` | `string` | طريقة HTTP (GET, POST, PUT, PATCH, DELETE) | |
|
||||
| `requestContext.http.path` | `string` | المسار الخام للطلب | |
|
||||
|
||||
|
||||
#### forwardedRequestHeaders
|
||||
|
||||
@@ -19,7 +19,7 @@ description: تعرّف على خطط تسعير Twenty وكيفية التبد
|
||||
* دعم قياسي
|
||||
|
||||
<Note>
|
||||
الميزات المتميزة (SSO وأذونات على مستوى الصف) غير مشمولة في خطة Pro.
|
||||
Premium features (SSO, row-level permissions and AI usage data) are not included in the Pro plan.
|
||||
</Note>
|
||||
|
||||
### المؤسسة (سحابي)
|
||||
@@ -27,7 +27,7 @@ description: تعرّف على خطط تسعير Twenty وكيفية التبد
|
||||
للفرق الأكبر ذات الاحتياجات المتقدّمة:
|
||||
|
||||
* كل ما في Pro
|
||||
* **ميزات متميزة**: تكامل SSO وأذونات على مستوى الصف
|
||||
* **Premium features**: SSO integration, row-level permissions and AI usage data
|
||||
* دعم متميز
|
||||
|
||||
## خطط الاستضافة الذاتية
|
||||
@@ -45,7 +45,7 @@ description: تعرّف على خطط تسعير Twenty وكيفية التبد
|
||||
للفرق التي تحتاج إلى ميزات متميزة أثناء الاستضافة الذاتية:
|
||||
|
||||
* جميع ميزات Pro
|
||||
* **ميزات متميزة**: تكامل SSO وأذونات على مستوى الصف
|
||||
* **Premium features**: SSO integration, row-level permissions and AI usage data
|
||||
* دعم فريق Twenty
|
||||
* لا يُشترط نشر الشيفرة المخصّصة كمفتوح المصدر قبل التوزيع
|
||||
|
||||
@@ -55,6 +55,7 @@ description: تعرّف على خطط تسعير Twenty وكيفية التبد
|
||||
|
||||
* **تكامل SSO**: تسجيل دخول أحادي مع موفّر الهوية لديك
|
||||
* **أذونات على مستوى الصف**: تحكّم دقيق في الوصول على مستوى السجل
|
||||
* **AI usage data**: Track AI consumption across the workspace
|
||||
|
||||
## التبديل بين الخطط
|
||||
|
||||
@@ -77,3 +78,15 @@ description: تعرّف على خطط تسعير Twenty وكيفية التبد
|
||||
### التبديل إلى الفوترة الشهرية
|
||||
|
||||
تواصل مع الدعم للعودة إلى الفوترة الشهرية.
|
||||
|
||||
## Obtain an Enterprise Key for Organization (Self-Hosted)
|
||||
|
||||
To use the Organization (Self-Hosted) plan, you need to obtain an Enterprise key:
|
||||
|
||||
1. Go to **Settings → Admin Panel → Enterprise**
|
||||
|
||||
<img src="/images/user-guide/billing/enterprise-key.png" alt="Enterprise key" />
|
||||
|
||||
2. Click **Get Enterprise Key**
|
||||
3. When you are redirected to Stripe, enter your payment details and confirm
|
||||
4. When your Enterprise key is displayed, paste it into the Enterprise settings page and activate the Organization license
|
||||
|
||||
@@ -16,6 +16,11 @@ description: الأسئلة الشائعة حول تسعير Twenty والفوت
|
||||
لا تتوفر الميزات المتميزة إلا في خطط Organization (السحابة أو الاستضافة الذاتية):
|
||||
* **تكامل SSO**: تسجيل الدخول الأحادي مع موفر الهوية لديك
|
||||
* **أذونات على مستوى السجل**: تحكم دقيق في الوصول على مستوى السجل
|
||||
* **بيانات استخدام الذكاء الاصطناعي**: تتبع استهلاك الذكاء الاصطناعي عبر مساحة العمل
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="هل خطة Organization على السحابة وخطة Organization ذاتية الاستضافة متماثلتان؟">
|
||||
كلتاهما تقدمان الميزات المتميزة نفسها. ومع ذلك، لا يمكن استخدام اشتراك السحابة لعمليات النشر ذاتية الاستضافة. يتطلب النشر ذاتي الاستضافة مفتاح Enterprise.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="هل تقدمون مقاعد مجانية للمستخدمين العارضين فقط؟">
|
||||
|
||||
@@ -53,38 +53,45 @@ People ←→ Project Assignments ←→ Projects
|
||||
1. اذهب إلى **الإعدادات → نموذج البيانات**
|
||||
2. انقر **+ كائن جديد**
|
||||
3. سمِّه تسمية وصفية (مثلًا: "تعيين مشروع"، "عضو فريق"، "طلب منتج")
|
||||
4. انقر على **حفظ**
|
||||
4. فعِّل خيار "تخطي إنشاء حقل الاسم"
|
||||
|
||||
<img src="/images/user-guide/fields/new-pivot-object.png" alt="كائن ربط جديد" />
|
||||
|
||||
5. انقر على **حفظ**
|
||||
|
||||
<Tip>
|
||||
**اتفاقية التسمية**: استخدم اسمًا يصف العلاقة، مثل "تعيين مشروع" أو "عضوية الفريق". هذا يجعل نموذج البيانات أسهل في الفهم.
|
||||
</Tip>
|
||||
|
||||
## الخطوة 2: إنشاء علاقات من كائن الربط
|
||||
## الخطوة 2: إنشاء علاقات بين الكائنات وكائن الربط
|
||||
|
||||
أضِف حقول علاقة من كائن الربط إلى كلا الكائنين اللذين تريد ربطهما.
|
||||
أضف حقول العلاقة من كلٍ من الكائنين لديك إلى كائن الربط.
|
||||
|
||||
### العلاقة الأولى (كائن الربط → الكائن A)
|
||||
### العلاقة الأولى (الكائن A → كائن الربط)
|
||||
|
||||
1. حدِّد كائن الربط في **الإعدادات → نموذج البيانات**
|
||||
2. انقر **+ إضافة حقل**
|
||||
3. اختر **العلاقة** كنوع الحقل
|
||||
4. اختر الكائن الأول (مثلًا، "الأشخاص")
|
||||
5. عيِّن نوع العلاقة إلى **متعدد-إلى-واحد** (يمكن لعديد من التعيينات الارتباط بشخص واحد)
|
||||
6. قم بتسمية الحقول:
|
||||
* الحقل على كائن الربط: مثلًا، "شخص"
|
||||
* الحقل على الأشخاص: مثلًا، "تعيينات المشروع"
|
||||
7. انقر على **حفظ**
|
||||
|
||||
### العلاقة الثانية (كائن الربط → الكائن B)
|
||||
|
||||
1. وأنت ما زلت في كائن الربط، انقر **+ إضافة حقل**
|
||||
2. اختر **العلاقة** كنوع الحقل
|
||||
3. اختر الكائن الثاني (مثلًا، "المشاريع")
|
||||
4. عيِّن نوع العلاقة إلى **متعدد-إلى-واحد**
|
||||
1. حدِّد الكائن الأول لديك في **الإعدادات → نموذج البيانات**
|
||||
2. انقر **+ إضافة علاقة**
|
||||
3. اختر كائن الربط (مثلًا، "تعيينات المشروع")
|
||||
4. عيِّن نوع العلاقة إلى **واحد-إلى-متعدد** (يمكن لشخص واحد الارتباط بالعديد من التعيينات)
|
||||
5. قم بتسمية الحقول:
|
||||
* الحقل على الأشخاص: مثلًا، "تعيينات المشروع"
|
||||
* الحقل على كائن الربط: مثلًا، "شخص"
|
||||
6. انقر على **حفظ**
|
||||
|
||||
### العلاقة الثانية (الكائن B → كائن الربط)
|
||||
|
||||
1. حدِّد الكائن الثاني لديك في **الإعدادات → نموذج البيانات**
|
||||
2. انقر **+ إضافة علاقة**
|
||||
3. اختر كائن الربط (مثلًا، "تعيينات المشروع")
|
||||
4. عيِّن نوع العلاقة إلى **واحد-إلى-متعدد** (يمكن لمشروع واحد الارتباط بالعديد من التعيينات)
|
||||
5. فعّل **"هذه علاقة بكائن ربط"**
|
||||
|
||||
<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}} />
|
||||
|
||||
6. قم بتسمية الحقول:
|
||||
* الحقل على كائن الربط: مثلًا، "مشروع"
|
||||
* الحقل على المشاريع: مثلًا، "أعضاء الفريق"
|
||||
6. انقر على **حفظ**
|
||||
7. انقر على **حفظ**
|
||||
|
||||
## الخطوة 3: ضبط عرض علاقة الربط
|
||||
|
||||
@@ -98,18 +105,6 @@ People ←→ Project Assignments ←→ Projects
|
||||
6. حدِّد **العلاقة الهدف** (مثلًا، "مشروع" — الحقل على كائن الربط الذي يشير إلى الجانب الآخر)
|
||||
7. انقر على **حفظ**
|
||||
|
||||
{/* TODO: Add image
|
||||
<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}}/>
|
||||
*/}
|
||||
|
||||
كرِّر على الكائن الآخر:
|
||||
|
||||
1. اختر "المشاريع" في نموذج البيانات
|
||||
2. حرِّر حقل العلاقة "أعضاء الفريق"
|
||||
3. فعّل مفتاح الربط
|
||||
4. حدِّد "شخص" كالعلاقة الهدف
|
||||
5. حفظ
|
||||
|
||||
## النتيجة
|
||||
|
||||
بعد التكوين:
|
||||
@@ -130,15 +125,15 @@ People ←→ Project Assignments ←→ Projects
|
||||
|
||||
### إضافة علاقات
|
||||
|
||||
1. **تعيين مشروع → الأشخاص**
|
||||
* النوع: متعدد-إلى-واحد
|
||||
* الحقل على التعيين: "شخص"
|
||||
1. **الأشخاص → تعيين مشروع**
|
||||
* النوع: واحد-إلى-متعدد
|
||||
* الحقل على الأشخاص: "تعيينات المشروع"
|
||||
* الحقل على التعيين: "شخص"
|
||||
|
||||
2. **تعيين مشروع → المشاريع**
|
||||
* النوع: متعدد-إلى-واحد
|
||||
* الحقل على التعيين: "مشروع"
|
||||
2. **المشاريع → تعيين مشروع**
|
||||
* النوع: واحد-إلى-متعدد
|
||||
* الحقل على المشاريع: "أعضاء الفريق"
|
||||
* الحقل على التعيين: "مشروع"
|
||||
|
||||
### ضبط عرض علاقة الربط
|
||||
|
||||
|
||||
@@ -30,3 +30,9 @@ description: خصص الشريط الجانبي الأيسر ليتوافق مع
|
||||
## قائمة الأوامر
|
||||
|
||||
اضغط `Cmd+K` (أو `Ctrl+K`) لفتح قائمة الأوامر — شريط بحث للوصول السريع يتيح لك الانتقال إلى أي سجل أو طريقة عرض أو إجراء دون التنقل عبر الشريط الجانبي.
|
||||
|
||||
## تخصيص الشريط الجانبي
|
||||
|
||||
لتخصيص الشريط الجانبي، مرّر المؤشر فوق قسم "مساحة العمل" في الشريط الجانبي وانقر على أيقونة مفتاح الربط.
|
||||
|
||||
<img src="/images/user-guide/layout/navigation-edit-icon.png" alt="أيقونة تحرير التنقّل" />
|
||||
|
||||
@@ -3,6 +3,8 @@ title: صفحات السجل
|
||||
description: خصص تخطيط صفحات تفاصيل السجلات باستخدام علامات تبويب وأدوات.
|
||||
---
|
||||
|
||||
## نظرة عامة
|
||||
|
||||
عند فتح سجل في Twenty، تتكون صفحة التفاصيل من **علامات تبويب** و**أدوات**. كلاهما قابل للتخصيص بالكامل حسب نوع الكائن.
|
||||
|
||||
## علامات التبويب
|
||||
@@ -38,12 +40,20 @@ description: خصص تخطيط صفحات تفاصيل السجلات باستخ
|
||||
|
||||
1. افتح أي سجل
|
||||
2. اضغط على `Cmd+K` وابحث عن "تحرير تخطيط صفحة السجل"
|
||||
|
||||
أو
|
||||
|
||||
1. انتقل إلى الإعدادات > نموذج البيانات > الكائن الذي تختاره > التخطيط
|
||||
|
||||
2. انقر على الزر "تخصيص صفحة السجل" لذلك الكائن
|
||||
|
||||
3. أنت الآن في وضع التخصيص:
|
||||
* **أضف أدوات** من منتقي الأدوات
|
||||
* **اسحب الأدوات** لإعادة وضعها على الشبكة
|
||||
* **غيّر حجم الأدوات** بسحب حوافها
|
||||
* **اضبط الحقول** المعروضة داخل كل أداة
|
||||
* **أدِر علامات التبويب** — أضف، أزل، أعد التسمية، وأعد الترتيب
|
||||
|
||||
4. احفظ تغييراتك — سيتم تطبيقها على جميع السجلات لذلك النوع من الكائنات
|
||||
|
||||
## ظهور الحقول
|
||||
|
||||
@@ -95,15 +95,16 @@ const handler = async (event: RoutePayload) => {
|
||||
|
||||
Typ `RoutePayload` má následující strukturu:
|
||||
|
||||
| Vlastnost | Typ | Popis | Příklad |
|
||||
| ---------------------------- | ------------------------------------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| `headers` | `Record\<string, string \| undefined>` | Záhlaví HTTP (pouze ta uvedená v `forwardedRequestHeaders`) | viz sekci níže |
|
||||
| `queryStringParameters` | `Record\<string, string \| undefined>` | Parametry query stringu (více hodnot spojených čárkami) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
|
||||
| `pathParameters` | `Record\<string, string \| undefined>` | Parametry cesty extrahované ze vzoru trasy | `/users/:id`, `/users/123` -> `{ id: '123' }` |
|
||||
| `body` | `object \| null` | Parsované tělo požadavku (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
|
||||
| `isBase64Encoded` | `boolean` | Zda je tělo kódováno base64 | |
|
||||
| `requestContext.http.method` | `string` | Metoda HTTP (GET, POST, PUT, PATCH, DELETE) | |
|
||||
| `requestContext.http.path` | `string` | Nezpracovaná cesta požadavku | |
|
||||
| Vlastnost | Typ | Popis | Příklad |
|
||||
| ---------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| `headers` | `Record\<string, string \| undefined>` | Záhlaví HTTP (pouze ta uvedená v `forwardedRequestHeaders`) | viz sekci níže |
|
||||
| `queryStringParameters` | `Record\<string, string \| undefined>` | Parametry query stringu (více hodnot spojených čárkami) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
|
||||
| `pathParameters` | `Record\<string, string \| undefined>` | Parametry cesty extrahované ze vzoru trasy | `/users/:id`, `/users/123` -> `{ id: '123' }` |
|
||||
| `body` | `object \| null` | Parsované tělo požadavku (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
|
||||
| `rawBody` | `string \| undefined` | Original UTF-8 request body, before JSON parsing. Useful for verifying HMAC-style webhook signatures (e.g. GitHub's `X-Hub-Signature-256`, Stripe). `undefined` when the runtime did not preserve it. | |
|
||||
| `isBase64Encoded` | `boolean` | Zda je tělo kódováno base64 | |
|
||||
| `requestContext.http.method` | `string` | Metoda HTTP (GET, POST, PUT, PATCH, DELETE) | |
|
||||
| `requestContext.http.path` | `string` | Nezpracovaná cesta požadavku | |
|
||||
|
||||
|
||||
#### forwardedRequestHeaders
|
||||
|
||||
@@ -19,7 +19,7 @@ Pro týmy připravené škálovat:
|
||||
* Standardní podpora
|
||||
|
||||
<Note>
|
||||
Prémiové funkce (SSO a oprávnění na úrovni řádků) nejsou součástí plánu Pro.
|
||||
Premium features (SSO, row-level permissions and AI usage data) are not included in the Pro plan.
|
||||
</Note>
|
||||
|
||||
### Organizace (Cloud)
|
||||
@@ -27,7 +27,7 @@ Prémiové funkce (SSO a oprávnění na úrovni řádků) nejsou součástí pl
|
||||
Pro větší týmy s pokročilými potřebami:
|
||||
|
||||
* Vše z plánu Pro
|
||||
* **Prémiové funkce**: integrace SSO a oprávnění na úrovni řádků
|
||||
* **Premium features**: SSO integration, row-level permissions and AI usage data
|
||||
* Prioritní podpora
|
||||
|
||||
## Plány pro self‑hosting
|
||||
@@ -45,7 +45,7 @@ Hostujte Twenty na vlastní infrastruktuře bez poplatků:
|
||||
Pro týmy, které při self‑hostingu potřebují prémiové funkce:
|
||||
|
||||
* Všechny funkce plánu Pro
|
||||
* **Prémiové funkce**: integrace SSO a oprávnění na úrovni řádků
|
||||
* **Premium features**: SSO integration, row-level permissions and AI usage data
|
||||
* Podpora týmu Twenty
|
||||
* Není vyžadováno zveřejnit vlastní kód jako open‑source před distribucí
|
||||
|
||||
@@ -55,6 +55,7 @@ Prémiové funkce jsou dostupné pouze v plánech Organizace (Cloud nebo self‑
|
||||
|
||||
* **Integrace SSO**: jednotné přihlášení s vaším poskytovatelem identity
|
||||
* **Oprávnění na úrovni řádků**: jemně odstupňované řízení přístupu na úrovni záznamu
|
||||
* **AI usage data**: Track AI consumption across the workspace
|
||||
|
||||
## Přepínání plánů
|
||||
|
||||
@@ -77,3 +78,15 @@ Chcete-li přejít na nižší plán, kontaktujte podporu.
|
||||
### Přepnout na měsíční fakturaci
|
||||
|
||||
Chcete-li přepnout zpět na měsíční fakturaci, kontaktujte podporu.
|
||||
|
||||
## Obtain an Enterprise Key for Organization (Self-Hosted)
|
||||
|
||||
To use the Organization (Self-Hosted) plan, you need to obtain an Enterprise key:
|
||||
|
||||
1. Go to **Settings → Admin Panel → Enterprise**
|
||||
|
||||
<img src="/images/user-guide/billing/enterprise-key.png" alt="Enterprise key" />
|
||||
|
||||
2. Click **Get Enterprise Key**
|
||||
3. When you are redirected to Stripe, enter your payment details and confirm
|
||||
4. When your Enterprise key is displayed, paste it into the Enterprise settings page and activate the Organization license
|
||||
|
||||
@@ -16,6 +16,11 @@ Pokud chcete hostovat sami a potřebujete prémiové funkce (SSO a oprávnění
|
||||
Prémiové funkce jsou dostupné pouze v plánech Organization (Cloud nebo Self-Hosted):
|
||||
* **Integrace SSO**: Jednotné přihlášení s vaším poskytovatelem identity
|
||||
* **Oprávnění na úrovni řádků**: Jemně granulované řízení přístupu na úrovni záznamu
|
||||
* **AI usage data**: Track AI consumption across the workspace
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Is Organization plan on cloud and Organization plan on self-hosted the same?">
|
||||
They offer the same premium features. However, a cloud subscription cannot be used for self-hosted deployments. Self-hosted requires an Enterprise key.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Nabízíte volná místa pro uživatele pouze s přístupem ke čtení?">
|
||||
|
||||
@@ -53,38 +53,45 @@ Nejprve vytvořte mezilehlý objekt, který bude uchovávat propojení.
|
||||
1. Přejděte do **Nastavení → Datový model**
|
||||
2. Klikněte na **+ New object**
|
||||
3. Pojmenujte jej výstižně (např. "Project Assignment", "Team Member", "Product Order")
|
||||
4. Klikněte na **Uložit**
|
||||
4. Přepněte "Přeskočit vytvoření pole Název" na zapnuto
|
||||
|
||||
<img src="/images/user-guide/fields/new-pivot-object.png" alt="Nový spojovací objekt" />
|
||||
|
||||
5. Klikněte na **Uložit**
|
||||
|
||||
<Tip>
|
||||
**Konvence pojmenování**: Použijte název, který popisuje vztah, například "Project Assignment" nebo "Team Membership". Tím bude datový model srozumitelnější.
|
||||
</Tip>
|
||||
|
||||
## Krok 2: Vytvořte vztahy ze spojovacího objektu
|
||||
## Krok 2: Vytvořte vztahy mezi objekty a spojovacím objektem
|
||||
|
||||
Přidejte relační pole ze spojovacího objektu do obou objektů, které chcete propojit.
|
||||
Přidejte vztahová pole z obou objektů do spojovacího objektu.
|
||||
|
||||
### První vztah (Junction → Objekt A)
|
||||
### První vztah (Objekt A → Junction)
|
||||
|
||||
1. Vyberte svůj spojovací objekt v **Settings → Data Model**
|
||||
2. Klikněte na **+ Add Field**
|
||||
3. Zvolte **Relation** jako typ pole
|
||||
4. Vyberte první objekt (např. "People")
|
||||
5. Nastavte typ vztahu na **Many-to-One** (mnoho přiřazení může odkazovat na jednu osobu)
|
||||
6. Pojmenujte pole:
|
||||
* Pole na spojovacím objektu: např. "Person"
|
||||
* Pole na People: např. "Project Assignments"
|
||||
7. Klikněte na **Uložit**
|
||||
|
||||
### Druhý vztah (Junction → Objekt B)
|
||||
|
||||
1. Stále na spojovacím objektu klikněte na **+ Add Field**
|
||||
2. Zvolte **Relation** jako typ pole
|
||||
3. Vyberte druhý objekt (např. "Projects")
|
||||
4. Nastavte typ vztahu na **Many-to-One**
|
||||
1. Vyberte svůj první objekt v **Settings → Data Model**
|
||||
2. Klikněte na **+ Přidat vztah**
|
||||
3. Vyberte spojovací objekt (např. "Project Assignments")
|
||||
4. Nastavte typ vztahu na **One-To-Many** (jedna osoba může být propojena s mnoha přiřazeními)
|
||||
5. Pojmenujte pole:
|
||||
* Pole na People: např. "Project Assignments"
|
||||
* Pole na spojovacím objektu: např. "Person"
|
||||
6. Klikněte na **Uložit**
|
||||
|
||||
### Druhý vztah (Objekt B → Junction)
|
||||
|
||||
1. Vyberte svůj druhý objekt v **Settings → Data Model**
|
||||
2. Klikněte na **+ Přidat vztah**
|
||||
3. Vyberte spojovací objekt (např. "Project Assignments")
|
||||
4. Nastavte typ vztahu na **One-To-Many** (jeden projekt může být propojen s mnoha přiřazeními)
|
||||
5. Povolte **"This is a relation to a Junction Object"**
|
||||
|
||||
<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}} />
|
||||
|
||||
6. Pojmenujte pole:
|
||||
* Pole na spojovacím objektu: např. "Project"
|
||||
* Pole na Projects: např. "Team Members"
|
||||
6. Klikněte na **Uložit**
|
||||
7. Klikněte na **Uložit**
|
||||
|
||||
## Krok 3: Nakonfigurujte zobrazení vztahu přes spojovací objekt
|
||||
|
||||
@@ -98,18 +105,6 @@ Nyní nakonfigurujte zdrojové objekty tak, aby zobrazovaly propojené záznamy
|
||||
6. Vyberte **Target relation** (např. "Project" — pole na spojovacím objektu, které ukazuje na druhou stranu)
|
||||
7. Klikněte na **Uložit**
|
||||
|
||||
{/* TODO: Add image
|
||||
<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}}/>
|
||||
*/}
|
||||
|
||||
Opakujte pro druhý objekt:
|
||||
|
||||
1. Vyberte "Projects" v Data Model
|
||||
2. Upravte relační pole "Team Members"
|
||||
3. Povolte přepínač pro vztah přes spojovací objekt
|
||||
4. Vyberte "Person" jako cílový vztah
|
||||
5. Uložit
|
||||
|
||||
## Výsledek
|
||||
|
||||
Po konfiguraci:
|
||||
@@ -130,15 +125,15 @@ Zde je kompletní postup:
|
||||
|
||||
### Přidejte vztahy
|
||||
|
||||
1. **Project Assignment → People**
|
||||
* Typ: Many-to-One
|
||||
* Pole na Assignment: "Person"
|
||||
1. **People → Project Assignment**
|
||||
* Typ: One-to-Many
|
||||
* Pole na People: "Project Assignments"
|
||||
* Pole na Assignment: "Person"
|
||||
|
||||
2. **Project Assignment → Projects**
|
||||
* Typ: Many-to-One
|
||||
* Pole na Assignment: "Project"
|
||||
2. **Projects → Project Assignment**
|
||||
* Typ: One-to-Many
|
||||
* Pole na Projects: "Team Members"
|
||||
* Pole na Assignment: "Project"
|
||||
|
||||
### Nakonfigurujte zobrazení spojovacího objektu
|
||||
|
||||
|
||||
@@ -30,3 +30,9 @@ Přidejte odkazy na externí nástroje přímo do postranního panelu. Užitečn
|
||||
## Nabídka příkazů
|
||||
|
||||
Stiskněte `Cmd+K` (nebo `Ctrl+K`) pro otevření nabídky příkazů — rychlá vyhledávací lišta pro přechod na libovolný záznam, zobrazení nebo akci bez procházení postranního panelu.
|
||||
|
||||
## Přizpůsobení postranního panelu
|
||||
|
||||
Chcete-li přizpůsobit postranní panel, najeďte myší na sekci "Pracovní prostor" v postranním panelu a klikněte na ikonu klíče.
|
||||
|
||||
<img src="/images/user-guide/layout/navigation-edit-icon.png" alt="Ikona pro úpravu navigace" />
|
||||
|
||||
@@ -3,6 +3,8 @@ title: Stránky záznamů
|
||||
description: Přizpůsobte rozvržení jednotlivých stránek detailu záznamu pomocí karet a widgetů.
|
||||
---
|
||||
|
||||
## Přehled
|
||||
|
||||
Když v Twenty otevřete záznam, stránka detailu se skládá z **karet** a **widgetů**. Obojí lze pro každý typ objektu plně přizpůsobit.
|
||||
|
||||
## Karty
|
||||
@@ -38,12 +40,20 @@ Widgety jsou stavební prvky uvnitř každé karty. Mezi dostupné typy widgetů
|
||||
|
||||
1. Otevřete libovolný záznam
|
||||
2. Stiskněte `Cmd+K` a vyhledejte "Upravit rozložení stránky záznamu"
|
||||
|
||||
nebo
|
||||
|
||||
1. Přejděte do Nastavení > Datový model > objekt dle vašeho výběru > Rozložení
|
||||
|
||||
2. Klikněte na tlačítko "Přizpůsobit stránku záznamu" pro daný objekt
|
||||
|
||||
3. Nyní jste v režimu přizpůsobení:
|
||||
* **Přidávejte widgety** z výběru widgetů
|
||||
* **Přetahujte widgety** pro jejich přemístění v mřížce
|
||||
* **Změňte velikost widgetů** tažením jejich okrajů
|
||||
* **Nakonfigurujte pole** zobrazovaná v jednotlivých widgetech
|
||||
* **Spravujte karty** — přidávejte, odebírejte, přejmenovávejte, měňte pořadí
|
||||
|
||||
4. Uložte změny — budou platit pro všechny záznamy daného typu objektu
|
||||
|
||||
## Viditelnost polí
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
---
|
||||
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,7 +77,6 @@ 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',
|
||||
|
||||
@@ -88,11 +88,11 @@ 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).
|
||||
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`, gefolgt von `yarn twenty install`, um auf Produktionsservern zu veröffentlichen und zu installieren — `deploy` veröffentlicht im Anwendungsregister, während `install` es in einem angegebenen Arbeitsbereich installiert. 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.jpg" alt="Terminalausgabe im Dev-Modus" />
|
||||
<img src="/images/docs/developers/extends/apps/dev.png" alt="Terminalausgabe im Dev-Modus" />
|
||||
</div>
|
||||
|
||||
#### Einmalige Synchronisierung mit `yarn twenty dev --once`
|
||||
@@ -127,13 +127,7 @@ Klicken Sie auf **My twenty app**, um die **Anwendungsregistrierung** zu öffnen
|
||||
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" />
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Installierte App" />
|
||||
</div>
|
||||
|
||||
Alles erledigt! Bearbeiten Sie eine beliebige Datei in `src/`, und die Änderungen werden automatisch übernommen.
|
||||
@@ -213,30 +207,49 @@ Die Beispiele stammen aus dem Verzeichnis [twenty-apps/examples](https://github.
|
||||
|
||||
Der Scaffolder hat bereits einen lokalen Twenty-Server für Sie gestartet. Um ihn später zu verwalten, verwenden Sie `yarn twenty server`:
|
||||
|
||||
| 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 |
|
||||
| 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, Version 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 |
|
||||
| `yarn twenty server upgrade` | Das neueste `twenty-app-dev`-Image herunterladen und den Container neu erstellen |
|
||||
| `yarn twenty server upgrade 2.2.0` | Auf eine bestimmte Version aktualisieren |
|
||||
|
||||
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.
|
||||
|
||||
### Aktualisieren des Server-Images
|
||||
|
||||
Verwenden Sie `yarn twenty server upgrade`, um nach einem neueren `twenty-app-dev`-Docker-Image zu suchen und den Container zu aktualisieren. Der Befehl lädt das Image herunter, vergleicht es mit dem, aus dem der Container erstellt wurde, und erstellt den Container nur neu, wenn sich das Image tatsächlich geändert hat. Ihre Daten-Volumes bleiben erhalten — nur der Container wird ersetzt.
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Upgrade to the latest version (skips recreation if already up to date)
|
||||
yarn twenty server upgrade
|
||||
|
||||
# Upgrade to a specific version
|
||||
yarn twenty server upgrade 2.2.0
|
||||
```
|
||||
|
||||
Wenn ein neueres Image verfügbar ist und der Container lief, startet der Upgrade-Befehl automatisch einen neuen Container mit dem aktualisierten Image. Führen Sie anschließend `yarn twenty server start` aus, um zu warten, bis er betriebsbereit ist. Wenn sich das Image nicht geändert hat, bleibt der Container unverändert.
|
||||
|
||||
Sie können die laufende Version mit `yarn twenty server status` überprüfen; dieser Befehl zeigt die `APP_VERSION` des Containers an.
|
||||
|
||||
### Eine Testinstanz ausführen
|
||||
|
||||
Ü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.
|
||||
|
||||
| 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 |
|
||||
| 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, Version 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 |
|
||||
| `yarn twenty server upgrade --test` | Das Image der Testinstanz aktualisieren |
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -94,15 +94,16 @@ const handler = async (event: RoutePayload) => {
|
||||
|
||||
Der Typ `RoutePayload` hat die folgende Struktur:
|
||||
|
||||
| Eigenschaft | Typ | Beschreibung | Beispiel |
|
||||
| ---------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| `headers` | `Record\<string, string \| undefined>` | HTTP-Header (nur die in `forwardedRequestHeaders` aufgelisteten) | siehe Abschnitt unten |
|
||||
| `queryStringParameters` | `Record\<string, string \| undefined>` | Query-String-Parameter (mehrere Werte mit Kommas verbunden) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
|
||||
| `pathParameters` | `Record\<string, string \| undefined>` | Aus dem Routenmuster extrahierte Pfadparameter | `/users/:id`, `/users/123` -> `{ id: '123' }` |
|
||||
| `body` | `object \| null` | Geparster Request-Body (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
|
||||
| `isBase64Encoded` | `boolean` | Gibt an, ob der Body Base64-codiert ist | |
|
||||
| `requestContext.http.method` | `Zeichenkette` | HTTP-Methode (GET, POST, PUT, PATCH, DELETE) | |
|
||||
| `requestContext.http.path` | `Zeichenkette` | Rohpfad der Anfrage | |
|
||||
| Eigenschaft | Typ | Beschreibung | Beispiel |
|
||||
| ---------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| `headers` | `Record\<string, string \| undefined>` | HTTP-Header (nur die in `forwardedRequestHeaders` aufgelisteten) | siehe Abschnitt unten |
|
||||
| `queryStringParameters` | `Record\<string, string \| undefined>` | Query-String-Parameter (mehrere Werte mit Kommas verbunden) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
|
||||
| `pathParameters` | `Record\<string, string \| undefined>` | Aus dem Routenmuster extrahierte Pfadparameter | `/users/:id`, `/users/123` -> `{ id: '123' }` |
|
||||
| `body` | `object \| null` | Geparster Request-Body (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
|
||||
| `rawBody` | `string \| undefined` | Ursprünglicher UTF-8-Request-Body vor dem JSON-Parsing. Nützlich zur Verifizierung von Webhook-Signaturen im HMAC-Stil (z. B. GitHubs `X-Hub-Signature-256`, Stripe). `undefined`, wenn die Laufzeitumgebung es nicht beibehalten hat. | |
|
||||
| `isBase64Encoded` | `boolean` | Gibt an, ob der Body Base64-codiert ist | |
|
||||
| `requestContext.http.method` | `Zeichenkette` | HTTP-Methode (GET, POST, PUT, PATCH, DELETE) | |
|
||||
| `requestContext.http.path` | `Zeichenkette` | Rohpfad der Anfrage | |
|
||||
|
||||
|
||||
#### forwardedRequestHeaders
|
||||
|
||||
@@ -77,6 +77,39 @@ 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.
|
||||
|
||||
@@ -19,7 +19,7 @@ Für Teams, die bereit sind zu skalieren:
|
||||
* Standard-Support
|
||||
|
||||
<Note>
|
||||
Premiumfunktionen (SSO und Berechtigungen auf Zeilenebene) sind im Pro-Plan nicht enthalten.
|
||||
Premiumfunktionen (SSO, Berechtigungen auf Zeilenebene und KI-Nutzungsdaten) sind im Pro-Tarif nicht enthalten.
|
||||
</Note>
|
||||
|
||||
### Organisation (Cloud)
|
||||
@@ -27,7 +27,7 @@ Premiumfunktionen (SSO und Berechtigungen auf Zeilenebene) sind im Pro-Plan nich
|
||||
Für größere Teams mit erweiterten Anforderungen:
|
||||
|
||||
* Alles aus Pro
|
||||
* **Premiumfunktionen**: SSO-Integration und Berechtigungen auf Zeilenebene
|
||||
* **Premiumfunktionen**: SSO-Integration, Berechtigungen auf Zeilenebene und KI-Nutzungsdaten
|
||||
* Vorrangiger Support
|
||||
|
||||
## Selbstgehostete Pläne
|
||||
@@ -45,7 +45,7 @@ Hosten Sie Twenty auf Ihrer eigenen Infrastruktur kostenlos:
|
||||
Für Teams, die beim Selbsthosting Premiumfunktionen benötigen:
|
||||
|
||||
* Alle Pro-Funktionen
|
||||
* **Premiumfunktionen**: SSO-Integration und Berechtigungen auf Zeilenebene
|
||||
* **Premiumfunktionen**: SSO-Integration, Berechtigungen auf Zeilenebene und KI-Nutzungsdaten
|
||||
* Support durch das Twenty-Team
|
||||
* Keine Verpflichtung, benutzerdefinierten Code vor der Weitergabe als Open Source zu veröffentlichen
|
||||
|
||||
@@ -55,6 +55,7 @@ Premiumfunktionen sind nur in den Organisation-Plänen (Cloud oder Selbstgehoste
|
||||
|
||||
* **SSO-Integration**: Single Sign-On mit Ihrem Identitätsanbieter
|
||||
* **Berechtigungen auf Zeilenebene**: Feingranulare Zugriffskontrolle auf Datensatzebene
|
||||
* **KI-Nutzungsdaten**: Verfolgen Sie den KI-Verbrauch in Ihrem Arbeitsbereich
|
||||
|
||||
## Pläne wechseln
|
||||
|
||||
@@ -77,3 +78,15 @@ Wenden Sie sich an den Support, um Ihren Plan herabzustufen.
|
||||
### Zur monatlichen Abrechnung wechseln
|
||||
|
||||
Wenden Sie sich an den Support, um wieder zur monatlichen Abrechnung zu wechseln.
|
||||
|
||||
## Enterprise-Schlüssel für Organization (Self-Hosted) abrufen
|
||||
|
||||
Um den Tarif Organization (Self-Hosted) zu verwenden, müssen Sie einen Enterprise-Schlüssel abrufen:
|
||||
|
||||
1. Gehen Sie zu **Einstellungen → Admin-Panel → Enterprise**
|
||||
|
||||
<img src="/images/user-guide/billing/enterprise-key.png" alt="Enterprise-Schlüssel" />
|
||||
|
||||
2. Klicken Sie auf **Enterprise-Schlüssel abrufen**
|
||||
3. Wenn Sie zu Stripe weitergeleitet werden, geben Sie Ihre Zahlungsdaten ein und bestätigen Sie
|
||||
4. Wenn Ihr Enterprise-Schlüssel angezeigt wird, fügen Sie ihn auf der Seite Enterprise-Einstellungen ein und aktivieren Sie die Organization-Lizenz
|
||||
|
||||
@@ -16,6 +16,11 @@ Wenn Sie selbst hosten möchten und die Premium-Funktionen (SSO und Berechtigung
|
||||
Premium-Funktionen sind nur in den Organization-Tarifen (Cloud oder Self-Hosted) verfügbar:
|
||||
* **SSO-Integration**: Single Sign-On mit Ihrem Identitätsanbieter
|
||||
* **Berechtigungen auf Zeilenebene**: feingranulare Zugriffskontrolle auf Datensatzebene
|
||||
* **KI-Nutzungsdaten**: Verfolgen Sie den KI-Verbrauch in Ihrem Arbeitsbereich
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Sind der Organisationsplan in der Cloud und der Organisationsplan bei selbst gehosteter Bereitstellung gleich?">
|
||||
Sie bieten dieselben Premium-Funktionen. Ein Cloud-Abonnement kann jedoch nicht für selbst gehostete Bereitstellungen verwendet werden. Für selbst gehostete Bereitstellungen ist ein Enterprise-Schlüssel erforderlich.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Bieten Sie kostenlose Plätze für Nur-Ansicht-Benutzer an?">
|
||||
|
||||
@@ -53,38 +53,45 @@ Erstellen Sie zunächst das Zwischenobjekt, das die Verknüpfungen speichert.
|
||||
1. Gehen Sie zu **Einstellungen → Datenmodell**
|
||||
2. Klicken Sie auf **+ Neues Objekt**
|
||||
3. Benennen Sie es aussagekräftig (z. B. "Projektzuweisung", "Teammitglied", "Produktbestellung")
|
||||
4. Klicken Sie auf **Speichern**
|
||||
4. Schalten Sie "Erstellen eines Namensfelds überspringen" ein
|
||||
|
||||
<img src="/images/user-guide/fields/new-pivot-object.png" alt="Neues Verknüpfungsobjekt" />
|
||||
|
||||
5. Klicken Sie auf **Speichern**
|
||||
|
||||
<Tip>
|
||||
**Namenskonvention**: Verwenden Sie einen Namen, der die Beziehung beschreibt, z. B. "Projektzuweisung" oder "Teammitgliedschaft". Das macht das Datenmodell leichter verständlich.
|
||||
</Tip>
|
||||
|
||||
## Schritt 2: Beziehungen vom Verknüpfungsobjekt erstellen
|
||||
## Schritt 2: Beziehungen zwischen Objekten und dem Verknüpfungsobjekt erstellen
|
||||
|
||||
Fügen Sie vom Verknüpfungsobjekt aus Beziehungsfelder zu beiden Objekten hinzu, die Sie verbinden möchten.
|
||||
Fügen Sie für jedes Ihrer beiden Objekte Beziehungsfelder zum Verknüpfungsobjekt hinzu.
|
||||
|
||||
### Erste Beziehung (Verknüpfung → Objekt A)
|
||||
### Erste Beziehung (Objekt A → Verknüpfung)
|
||||
|
||||
1. Wählen Sie Ihr Verknüpfungsobjekt unter **Einstellungen → Datenmodell** aus
|
||||
2. Klicken Sie auf **+ Feld hinzufügen**
|
||||
3. Wählen Sie **Relation** als Feldtyp
|
||||
4. Wählen Sie das erste Objekt aus (z. B. "Personen")
|
||||
5. Legen Sie den Beziehungstyp auf **Viele-zu-Eins** fest (viele Zuweisungen können mit einer Person verknüpft werden)
|
||||
6. Benennen Sie die Felder:
|
||||
* Feld auf der Verknüpfung: z. B. "Person"
|
||||
* Feld bei Personen: z. B. "Projektzuweisungen"
|
||||
7. Klicken Sie auf **Speichern**
|
||||
|
||||
### Zweite Beziehung (Verknüpfung → Objekt B)
|
||||
|
||||
1. Bleiben Sie im Verknüpfungsobjekt und klicken Sie auf **+ Feld hinzufügen**
|
||||
2. Wählen Sie **Relation** als Feldtyp
|
||||
3. Wählen Sie das zweite Objekt aus (z. B. "Projekte")
|
||||
4. Legen Sie den Beziehungstyp auf **Viele-zu-Eins** fest
|
||||
1. Wählen Sie Ihr erstes Objekt unter **Einstellungen → Datenmodell** aus
|
||||
2. Klicken Sie auf **+ Beziehung hinzufügen**
|
||||
3. Wählen Sie das Verknüpfungsobjekt aus (z. B. "Projektzuweisungen")
|
||||
4. Legen Sie den Beziehungstyp auf **Eins-zu-Viele** fest (eine Person kann mit vielen Zuweisungen verknüpft werden)
|
||||
5. Benennen Sie die Felder:
|
||||
* Feld bei Personen: z. B. "Projektzuweisungen"
|
||||
* Feld auf der Verknüpfung: z. B. "Person"
|
||||
6. Klicken Sie auf **Speichern**
|
||||
|
||||
### Zweite Beziehung (Objekt B → Verknüpfung)
|
||||
|
||||
1. Wählen Sie Ihr zweites Objekt unter **Einstellungen → Datenmodell** aus
|
||||
2. Klicken Sie auf **+ Beziehung hinzufügen**
|
||||
3. Wählen Sie das Verknüpfungsobjekt aus (z. B. "Projektzuweisungen")
|
||||
4. Legen Sie den Beziehungstyp auf **Eins-zu-Viele** fest (ein Projekt kann mit vielen Zuweisungen verknüpft werden)
|
||||
5. Aktivieren Sie **"Dies ist eine Beziehung zu einem Verknüpfungsobjekt"**
|
||||
|
||||
<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}} />
|
||||
|
||||
6. Benennen Sie die Felder:
|
||||
* Feld auf der Verknüpfung: z. B. "Projekt"
|
||||
* Feld bei Projekten: z. B. "Teammitglieder"
|
||||
6. Klicken Sie auf **Speichern**
|
||||
7. Klicken Sie auf **Speichern**
|
||||
|
||||
## Schritt 3: Anzeige der Verknüpfungsbeziehung konfigurieren
|
||||
|
||||
@@ -98,18 +105,6 @@ Konfigurieren Sie nun die Quellobjekte so, dass verknüpfte Datensätze direkt a
|
||||
6. Wählen Sie die **Zielbeziehung** aus (z. B. "Projekt" — das Feld an der Verknüpfung, das auf die andere Seite zeigt)
|
||||
7. Klicken Sie auf **Speichern**
|
||||
|
||||
{/* TODO: Add image
|
||||
<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}}/>
|
||||
*/}
|
||||
|
||||
Wiederholen Sie dies für das andere Objekt:
|
||||
|
||||
1. Wählen Sie "Projekte" im Datenmodell aus
|
||||
2. Bearbeiten Sie das Beziehungsfeld "Teammitglieder"
|
||||
3. Aktivieren Sie den Verknüpfungsschalter
|
||||
4. Wählen Sie "Person" als Zielbeziehung aus
|
||||
5. Speichern
|
||||
|
||||
## Ergebnis
|
||||
|
||||
Nach der Konfiguration:
|
||||
@@ -130,15 +125,15 @@ Hier ist eine vollständige Schritt-für-Schritt-Anleitung:
|
||||
|
||||
### Beziehungen hinzufügen
|
||||
|
||||
1. **Projektzuweisung → Personen**
|
||||
* Typ: Viele-zu-Eins
|
||||
* Feld bei Zuweisung: "Person"
|
||||
1. **Personen → Projektzuweisung**
|
||||
* Typ: Eins-zu-Viele
|
||||
* Feld bei Personen: "Projektzuweisungen"
|
||||
* Feld bei Zuweisung: "Person"
|
||||
|
||||
2. **Projektzuweisung → Projekte**
|
||||
* Typ: Viele-zu-Eins
|
||||
* Feld bei Zuweisung: "Projekt"
|
||||
2. **Projekte → Projektzuweisung**
|
||||
* Typ: Eins-zu-Viele
|
||||
* Feld bei Projekten: "Teammitglieder"
|
||||
* Feld bei Zuweisung: "Projekt"
|
||||
|
||||
### Verknüpfungsanzeige konfigurieren
|
||||
|
||||
|
||||
@@ -30,3 +30,9 @@ Fügen Sie Links zu externen Tools direkt in der Seitenleiste hinzu. Nützlich,
|
||||
## Befehlsmenü
|
||||
|
||||
Drücken Sie `Cmd+K` (oder `Ctrl+K`), um das Befehlsmenü zu öffnen — eine Suchleiste für den Schnellzugriff, mit der Sie zu jedem Datensatz, jeder Ansicht oder Aktion springen können, ohne durch die Seitenleiste zu navigieren.
|
||||
|
||||
## Anpassung der Seitenleiste
|
||||
|
||||
Um die Seitenleiste anzupassen, bewegen Sie den Mauszeiger über den Abschnitt "Arbeitsbereich" in der Seitenleiste und klicken Sie auf das Schraubenschlüssel-Symbol.
|
||||
|
||||
<img src="/images/user-guide/layout/navigation-edit-icon.png" alt="Symbol zum Bearbeiten der Navigation" />
|
||||
|
||||
@@ -3,6 +3,8 @@ title: Datensatzseiten
|
||||
description: Passen Sie das Layout einzelner Datensatz-Detailseiten mit Tabs und Widgets an.
|
||||
---
|
||||
|
||||
## Übersicht
|
||||
|
||||
Wenn Sie in Twenty einen Datensatz öffnen, besteht die Detailseite aus **Tabs** und **Widgets**. Beide lassen sich je Objekttyp vollständig anpassen.
|
||||
|
||||
## Registerkarten
|
||||
@@ -38,12 +40,20 @@ Widgets sind die Bausteine in jedem Tab. Zu den verfügbaren Widget-Typen gehör
|
||||
|
||||
1. Öffnen Sie einen beliebigen Datensatz
|
||||
2. Drücken Sie `Cmd+K` und suchen Sie nach "Layout der Datensatzseite bearbeiten"
|
||||
|
||||
oder
|
||||
|
||||
1. Gehen Sie zu Einstellungen > Datenmodell > Objekt Ihrer Wahl > Layout
|
||||
|
||||
2. Klicken Sie für dieses Objekt auf die Schaltfläche "Datensatzseite anpassen"
|
||||
|
||||
3. Sie befinden sich jetzt im Anpassungsmodus:
|
||||
* **Widgets hinzufügen** aus der Widget-Auswahl
|
||||
* **Widgets ziehen**, um sie im Raster neu zu positionieren
|
||||
* **Widgets in der Größe ändern**, indem Sie ihre Ränder ziehen
|
||||
* **Felder konfigurieren**, die in jedem Widget angezeigt werden
|
||||
* **Tabs verwalten** — hinzufügen, entfernen, umbenennen, neu anordnen
|
||||
|
||||
4. Speichern Sie Ihre Änderungen — sie gelten für alle Datensätze dieses Objekttyps
|
||||
|
||||
## Feldsichtbarkeit
|
||||
|
||||
@@ -94,15 +94,16 @@ const handler = async (event: RoutePayload) => {
|
||||
|
||||
Il tipo `RoutePayload` ha la seguente struttura:
|
||||
|
||||
| Proprietà | Tipo | Descrizione | Esempio |
|
||||
| ---------------------------- | ------------------------------------------------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| `headers` | `Record\<string, string \| undefined>` | Intestazioni HTTP (solo quelle elencate in `forwardedRequestHeaders`) | vedi la sezione sotto |
|
||||
| `queryStringParameters` | `Record\<string, string \| undefined>` | Parametri della query string (valori multipli uniti da virgole) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
|
||||
| `pathParameters` | `Record\<string, string \| undefined>` | Parametri di percorso estratti dal pattern della route | `/users/:id`, `/users/123` -> `{ id: '123' }` |
|
||||
| `body` | `object \| null` | Corpo della richiesta analizzato (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
|
||||
| `isBase64Encoded` | `boolean` | Indica se il corpo è codificato in base64 | |
|
||||
| `requestContext.http.method` | `string` | Metodo HTTP (GET, POST, PUT, PATCH, DELETE) | |
|
||||
| `requestContext.http.path` | `string` | Percorso della richiesta non elaborato | |
|
||||
| Proprietà | Tipo | Descrizione | Esempio |
|
||||
| ---------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| `headers` | `Record\<string, string \| undefined>` | Intestazioni HTTP (solo quelle elencate in `forwardedRequestHeaders`) | vedi la sezione sotto |
|
||||
| `queryStringParameters` | `Record\<string, string \| undefined>` | Parametri della query string (valori multipli uniti da virgole) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
|
||||
| `pathParameters` | `Record\<string, string \| undefined>` | Parametri di percorso estratti dal pattern della route | `/users/:id`, `/users/123` -> `{ id: '123' }` |
|
||||
| `body` | `object \| null` | Corpo della richiesta analizzato (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
|
||||
| `rawBody` | `string \| undefined` | Original UTF-8 request body, before JSON parsing. Useful for verifying HMAC-style webhook signatures (e.g. GitHub's `X-Hub-Signature-256`, Stripe). `undefined` when the runtime did not preserve it. | |
|
||||
| `isBase64Encoded` | `boolean` | Indica se il corpo è codificato in base64 | |
|
||||
| `requestContext.http.method` | `string` | Metodo HTTP (GET, POST, PUT, PATCH, DELETE) | |
|
||||
| `requestContext.http.path` | `string` | Percorso della richiesta non elaborato | |
|
||||
|
||||
|
||||
#### forwardedRequestHeaders
|
||||
|
||||
@@ -19,7 +19,7 @@ Per i team pronti a scalare:
|
||||
* Supporto standard
|
||||
|
||||
<Note>
|
||||
Le funzionalità premium (SSO e autorizzazioni a livello di riga) non sono incluse nel piano Pro.
|
||||
Premium features (SSO, row-level permissions and AI usage data) are not included in the Pro plan.
|
||||
</Note>
|
||||
|
||||
### Organizzazione (Cloud)
|
||||
@@ -27,7 +27,7 @@ Le funzionalità premium (SSO e autorizzazioni a livello di riga) non sono inclu
|
||||
Per i team più numerosi con esigenze avanzate:
|
||||
|
||||
* Tutto ciò che è incluso in Pro
|
||||
* **Funzionalità premium**: integrazione SSO e autorizzazioni a livello di riga
|
||||
* **Premium features**: SSO integration, row-level permissions and AI usage data
|
||||
* Supporto prioritario
|
||||
|
||||
## Piani self‑hosted
|
||||
@@ -45,7 +45,7 @@ Esegui Twenty sulla tua infrastruttura senza costi:
|
||||
Per i team che necessitano di funzionalità premium con il self‑hosting:
|
||||
|
||||
* Tutte le funzionalità di Pro
|
||||
* **Funzionalità premium**: integrazione SSO e autorizzazioni a livello di riga
|
||||
* **Premium features**: SSO integration, row-level permissions and AI usage data
|
||||
* Supporto del team di Twenty
|
||||
* Nessun requisito di pubblicare il codice personalizzato come open source prima della distribuzione
|
||||
|
||||
@@ -55,6 +55,7 @@ Le funzionalità premium sono disponibili solo nei piani Organizzazione (Cloud o
|
||||
|
||||
* **Integrazione SSO**: Single Sign‑On con il tuo provider di identità
|
||||
* **Autorizzazioni a livello di riga**: controllo degli accessi granulare a livello di record
|
||||
* **AI usage data**: Track AI consumption across the workspace
|
||||
|
||||
## Cambio piano
|
||||
|
||||
@@ -77,3 +78,15 @@ Contatta il supporto per eseguire il downgrade del tuo piano.
|
||||
### Passa alla fatturazione mensile
|
||||
|
||||
Contatta il supporto per tornare alla fatturazione mensile.
|
||||
|
||||
## Obtain an Enterprise Key for Organization (Self-Hosted)
|
||||
|
||||
To use the Organization (Self-Hosted) plan, you need to obtain an Enterprise key:
|
||||
|
||||
1. Go to **Settings → Admin Panel → Enterprise**
|
||||
|
||||
<img src="/images/user-guide/billing/enterprise-key.png" alt="Enterprise key" />
|
||||
|
||||
2. Click **Get Enterprise Key**
|
||||
3. When you are redirected to Stripe, enter your payment details and confirm
|
||||
4. When your Enterprise key is displayed, paste it into the Enterprise settings page and activate the Organization license
|
||||
|
||||
@@ -16,6 +16,11 @@ Se vuoi effettuare il self-hosting e hai bisogno delle funzionalità Premium (SS
|
||||
Le funzionalità Premium sono disponibili solo nei piani Organization (Cloud o Self-Hosted):
|
||||
* **Integrazione SSO**: Single Sign-On con il tuo provider di identità
|
||||
* **Autorizzazioni a livello di riga**: controllo degli accessi granulare a livello di record
|
||||
* **Dati di utilizzo dell'IA**: Tieni traccia del consumo dell'IA nel tuo spazio di lavoro
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Il piano Organization su cloud e il piano Organization in modalità self-hosted sono la stessa cosa?">
|
||||
Offrono le stesse funzionalità Premium. Tuttavia, un abbonamento cloud non può essere utilizzato per distribuzioni self-hosted. La modalità self-hosted richiede una chiave Enterprise.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Offrite posti gratuiti per utenti solo visualizzazione?">
|
||||
|
||||
@@ -53,38 +53,45 @@ Per prima cosa, crea l'oggetto intermedio che conterrà i collegamenti.
|
||||
1. Vai a **Impostazioni → Modello dati**
|
||||
2. Fai clic su **+ Nuovo oggetto**
|
||||
3. Assegnagli un nome descrittivo (ad es., "Assegnazione al progetto", "Membro del team", "Ordine del prodotto")
|
||||
4. Clicca su **Salva**
|
||||
4. Attiva l'opzione "Salta la creazione di un campo Nome"
|
||||
|
||||
<img src="/images/user-guide/fields/new-pivot-object.png" alt="Nuovo oggetto pivot" />
|
||||
|
||||
5. Clicca su **Salva**
|
||||
|
||||
<Tip>
|
||||
**Convenzione di denominazione**: Usa un nome che descriva la relazione, come "Assegnazione al progetto" o "Appartenenza al team". Questo rende il modello di dati più facile da comprendere.
|
||||
</Tip>
|
||||
|
||||
## Passaggio 2: Crea le relazioni dall'oggetto di giunzione
|
||||
## Passaggio 2: Crea relazioni tra gli oggetti e l'oggetto di giunzione
|
||||
|
||||
Aggiungi campi di relazione dall'oggetto di giunzione a entrambi gli oggetti che desideri collegare.
|
||||
Aggiungi campi di relazione da ciascuno dei due oggetti all'oggetto di giunzione.
|
||||
|
||||
### Prima relazione (Giunzione → Oggetto A)
|
||||
### Prima relazione (Oggetto A → Oggetto di giunzione)
|
||||
|
||||
1. Seleziona il tuo oggetto di giunzione in **Impostazioni → Modello dati**
|
||||
2. Fai clic su **+ Aggiungi campo**
|
||||
3. Scegli **Relazione** come tipo di campo
|
||||
4. Seleziona il primo oggetto (ad es., "Persone")
|
||||
5. Imposta il tipo di relazione su **Molti-a-uno** (molte assegnazioni possono collegarsi a una persona)
|
||||
6. Assegna un nome ai campi:
|
||||
* Campo sull'oggetto di giunzione: ad es., "Persona"
|
||||
* Campo su Persone: ad es., "Assegnazioni ai progetti"
|
||||
7. Clicca su **Salva**
|
||||
|
||||
### Seconda relazione (Giunzione → Oggetto B)
|
||||
|
||||
1. Sempre sull'oggetto di giunzione, fai clic su **+ Aggiungi campo**
|
||||
2. Scegli **Relazione** come tipo di campo
|
||||
3. Seleziona il secondo oggetto (ad es., "Progetti")
|
||||
4. Imposta il tipo di relazione su **Molti-a-uno**
|
||||
1. Seleziona il tuo primo oggetto in **Impostazioni → Modello dati**
|
||||
2. Fai clic su **+ Aggiungi relazione**
|
||||
3. Seleziona l'oggetto di giunzione (ad es., "Assegnazioni al progetto")
|
||||
4. Imposta il tipo di relazione su **Uno-a-molti** (una persona può collegarsi a molte assegnazioni)
|
||||
5. Assegna un nome ai campi:
|
||||
* Campo su Persone: ad es., "Assegnazioni ai progetti"
|
||||
* Campo sull'oggetto di giunzione: ad es., "Persona"
|
||||
6. Clicca su **Salva**
|
||||
|
||||
### Seconda relazione (Oggetto B → Oggetto di giunzione)
|
||||
|
||||
1. Seleziona il tuo secondo oggetto in **Impostazioni → Modello dati**
|
||||
2. Fai clic su **+ Aggiungi relazione**
|
||||
3. Seleziona l'oggetto di giunzione (ad es., "Assegnazioni al progetto")
|
||||
4. Imposta il tipo di relazione su **Uno-a-molti** (un progetto può collegarsi a molte assegnazioni)
|
||||
5. Abilita **"Questa è una relazione con un oggetto di giunzione"**
|
||||
|
||||
<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}} />
|
||||
|
||||
6. Assegna un nome ai campi:
|
||||
* Campo sull'oggetto di giunzione: ad es., "Progetto"
|
||||
* Campo su Progetti: ad es., "Membri del team"
|
||||
6. Clicca su **Salva**
|
||||
7. Clicca su **Salva**
|
||||
|
||||
## Passaggio 3: Configura la visualizzazione della relazione di giunzione
|
||||
|
||||
@@ -98,18 +105,6 @@ Ora configura gli oggetti sorgente per visualizzare direttamente i record colleg
|
||||
6. Seleziona la **Relazione di destinazione** (ad es., "Progetto" — il campo sull'oggetto di giunzione che punta all'altro lato)
|
||||
7. Clicca su **Salva**
|
||||
|
||||
{/* TODO: Add image
|
||||
<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}}/>
|
||||
*/}
|
||||
|
||||
Ripeti per l'altro oggetto:
|
||||
|
||||
1. Seleziona "Progetti" in Modello dati
|
||||
2. Modifica il campo di relazione "Membri del team"
|
||||
3. Abilita l'interruttore di giunzione
|
||||
4. Seleziona "Persona" come relazione di destinazione
|
||||
5. Salva
|
||||
|
||||
## Risultato
|
||||
|
||||
Dopo la configurazione:
|
||||
@@ -130,15 +125,15 @@ Ecco una procedura completa:
|
||||
|
||||
### Aggiungi relazioni
|
||||
|
||||
1. **Assegnazione al progetto → Persone**
|
||||
* Tipo: Molti-a-uno
|
||||
* Campo su Assegnazione: "Persona"
|
||||
1. **Persone → Assegnazione al progetto**
|
||||
* Tipo: Uno-a-molti
|
||||
* Campo su Persone: "Assegnazioni ai progetti"
|
||||
* Campo su Assegnazione: "Persona"
|
||||
|
||||
2. **Assegnazione al progetto → Progetti**
|
||||
* Tipo: Molti-a-uno
|
||||
* Campo su Assegnazione: "Progetto"
|
||||
2. **Progetti → Assegnazione al progetto**
|
||||
* Tipo: Uno-a-molti
|
||||
* Campo su Progetti: "Membri del team"
|
||||
* Campo su Assegnazione: "Progetto"
|
||||
|
||||
### Configura la visualizzazione della giunzione
|
||||
|
||||
|
||||
@@ -30,3 +30,9 @@ Aggiungi collegamenti a strumenti esterni direttamente nella barra laterale. Uti
|
||||
## Menu Comandi
|
||||
|
||||
Premi `Cmd+K` (o `Ctrl+K`) per aprire il menu comandi — una barra di ricerca ad accesso rapido per passare a qualsiasi record, vista o azione senza usare la barra laterale.
|
||||
|
||||
## Personalizzare la barra laterale
|
||||
|
||||
Per personalizzare la barra laterale, passa il mouse sulla sezione "Workspace" nella barra laterale e fai clic sull'icona a forma di chiave inglese.
|
||||
|
||||
<img src="/images/user-guide/layout/navigation-edit-icon.png" alt="Icona di modifica della navigazione" />
|
||||
|
||||
@@ -3,6 +3,8 @@ title: Pagine dei record
|
||||
description: Personalizza il layout delle singole pagine di dettaglio dei record con schede e widget.
|
||||
---
|
||||
|
||||
## Panoramica
|
||||
|
||||
Quando apri un record in Twenty, la pagina di dettaglio è composta da **schede** e **widget**. Entrambi sono completamente personalizzabili per ciascun tipo di oggetto.
|
||||
|
||||
## Schede
|
||||
@@ -38,12 +40,20 @@ I widget sono gli elementi costitutivi all'interno di ogni scheda. Tipi di widge
|
||||
|
||||
1. Apri un record qualsiasi
|
||||
2. Premi `Cmd+K` e cerca "Modifica il layout della pagina del record"
|
||||
|
||||
o
|
||||
|
||||
1. Vai a Impostazioni > Modello dati > oggetto di tua scelta > Layout
|
||||
|
||||
2. Fai clic sul pulsante "Personalizza pagina del record" per quell'oggetto
|
||||
|
||||
3. Ora sei in modalità di personalizzazione:
|
||||
* **Aggiungi widget** dal selettore di widget
|
||||
* **Trascina i widget** per riposizionarli sulla griglia
|
||||
* **Ridimensiona i widget** trascinando i bordi
|
||||
* **Configura i campi** mostrati all'interno di ciascun widget
|
||||
* **Gestisci le schede** — aggiungi, rimuovi, rinomina, riordina
|
||||
|
||||
4. Salva le modifiche — si applicano a tutti i record di quel tipo di oggetto
|
||||
|
||||
## Visibilità dei campi
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
---
|
||||
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,7 +77,6 @@ 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',
|
||||
|
||||
@@ -88,11 +88,11 @@ 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.
|
||||
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` seguido de `yarn twenty install` para publicar e instalar em servidores de produção — `deploy` publica no registro de aplicativos, enquanto `install` o instala em um determinado workspace. Veja [Publicação de aplicativos](/l/pt/developers/extend/apps/publishing) para detalhes.
|
||||
</Warning>
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Saída do terminal no modo de desenvolvimento" />
|
||||
<img src="/images/docs/developers/extends/apps/dev.png" alt="Saída do terminal no modo de desenvolvimento" />
|
||||
</div>
|
||||
|
||||
#### Sincronização única com `yarn twenty dev --once`
|
||||
@@ -127,13 +127,7 @@ Clique em **My twenty app** para abrir o seu **registro do aplicativo**. Um regi
|
||||
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" />
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Aplicação instalada" />
|
||||
</div>
|
||||
|
||||
Tudo pronto! Edite qualquer arquivo em `src/` e as alterações serão detectadas automaticamente.
|
||||
@@ -213,30 +207,49 @@ Os exemplos são obtidos do diretório [twenty-apps/examples](https://github.com
|
||||
|
||||
A ferramenta de scaffolding já iniciou um servidor local do Twenty para você. Para gerenciá-lo depois, use `yarn twenty server`:
|
||||
|
||||
| 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 |
|
||||
| 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, a versão 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 |
|
||||
| `yarn twenty server upgrade` | Baixe a imagem mais recente `twenty-app-dev` e recrie o contêiner |
|
||||
| `yarn twenty server upgrade 2.2.0` | Atualizar para uma versão específica |
|
||||
|
||||
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.
|
||||
|
||||
### Atualizando a imagem do servidor
|
||||
|
||||
Use `yarn twenty server upgrade` para verificar se há uma imagem do Docker `twenty-app-dev` mais recente e atualizar o container. O comando baixa a imagem, a compara com aquela a partir da qual o container foi criado e só recria o container se a imagem realmente tiver sido alterada. Seus volumes de dados são preservados — apenas o container é substituído.
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Upgrade to the latest version (skips recreation if already up to date)
|
||||
yarn twenty server upgrade
|
||||
|
||||
# Upgrade to a specific version
|
||||
yarn twenty server upgrade 2.2.0
|
||||
```
|
||||
|
||||
Se uma imagem mais recente estiver disponível e o container estiver em execução, o comando de atualização iniciará automaticamente um novo container com a imagem atualizada. Em seguida, execute `yarn twenty server start` para aguardar até que ele fique saudável. Se a imagem não tiver mudado, o container permanece inalterado.
|
||||
|
||||
Você pode verificar a versão em execução com `yarn twenty server status`, que exibe o `APP_VERSION` do container.
|
||||
|
||||
### Executando uma instância de teste
|
||||
|
||||
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.
|
||||
|
||||
| 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 |
|
||||
| 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, a versão 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 |
|
||||
| `yarn twenty server upgrade --test` | Atualiza a imagem da instância de teste |
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -94,15 +94,16 @@ const handler = async (event: RoutePayload) => {
|
||||
|
||||
O tipo `RoutePayload` tem a seguinte estrutura:
|
||||
|
||||
| Propriedade | Tipo | Descrição | Exemplo |
|
||||
| ---------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| `headers` | `Record\<string, string \| undefined>` | Cabeçalhos HTTP (apenas aqueles listados em `forwardedRequestHeaders`) | veja a seção abaixo |
|
||||
| `queryStringParameters` | `Record\<string, string \| undefined>` | Parâmetros de query string (valores múltiplos unidos por vírgulas) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
|
||||
| `pathParameters` | `Record\<string, string \| undefined>` | Parâmetros de caminho extraídos do padrão de rota | `/users/:id`, `/users/123` -> `{ id: '123' }` |
|
||||
| `body` | `object \| null` | Corpo da requisição analisado (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
|
||||
| `isBase64Encoded` | `boolean` | Se o corpo está codificado em base64 | |
|
||||
| `requestContext.http.method` | `string` | Método HTTP (GET, POST, PUT, PATCH, DELETE) | |
|
||||
| `requestContext.http.path` | `string` | Caminho bruto da requisição | |
|
||||
| Propriedade | Tipo | Descrição | Exemplo |
|
||||
| ---------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| `headers` | `Record\<string, string \| undefined>` | Cabeçalhos HTTP (apenas aqueles listados em `forwardedRequestHeaders`) | veja a seção abaixo |
|
||||
| `queryStringParameters` | `Record\<string, string \| undefined>` | Parâmetros de query string (valores múltiplos unidos por vírgulas) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
|
||||
| `pathParameters` | `Record\<string, string \| undefined>` | Parâmetros de caminho extraídos do padrão de rota | `/users/:id`, `/users/123` -> `{ id: '123' }` |
|
||||
| `body` | `object \| null` | Corpo da requisição analisado (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
|
||||
| `rawBody` | `string \| undefined` | Corpo da solicitação UTF-8 original, antes da análise de JSON. Útil para verificar assinaturas de webhook no estilo HMAC (por exemplo, `X-Hub-Signature-256` do GitHub, Stripe). `undefined` quando o ambiente de execução não o preservou. | |
|
||||
| `isBase64Encoded` | `boolean` | Se o corpo está codificado em base64 | |
|
||||
| `requestContext.http.method` | `string` | Método HTTP (GET, POST, PUT, PATCH, DELETE) | |
|
||||
| `requestContext.http.path` | `string` | Caminho bruto da requisição | |
|
||||
|
||||
|
||||
#### forwardedRequestHeaders
|
||||
|
||||
@@ -77,6 +77,39 @@ 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.
|
||||
|
||||
@@ -19,7 +19,7 @@ Para equipes prontas para escalar:
|
||||
* Suporte padrão
|
||||
|
||||
<Note>
|
||||
Recursos premium (SSO e permissões em nível de linha) não estão incluídos no plano Pro.
|
||||
Os recursos Premium (SSO, permissões em nível de linha e dados de uso de IA) não estão incluídos no plano Pro.
|
||||
</Note>
|
||||
|
||||
### Organização (Nuvem)
|
||||
@@ -27,7 +27,7 @@ Recursos premium (SSO e permissões em nível de linha) não estão incluídos n
|
||||
Para equipes maiores com necessidades avançadas:
|
||||
|
||||
* Tudo do Pro
|
||||
* **Recursos premium**: integração SSO e permissões em nível de linha
|
||||
* **Recursos Premium**: integração com SSO, permissões em nível de linha e dados de uso de IA
|
||||
* Suporte prioritário
|
||||
|
||||
## Planos auto-hospedados
|
||||
@@ -45,7 +45,7 @@ Hospede o Twenty na sua própria infraestrutura sem custo:
|
||||
Para equipes que precisam de recursos premium enquanto se auto-hospedam:
|
||||
|
||||
* Todos os recursos do Pro
|
||||
* **Recursos premium**: integração SSO e permissões em nível de linha
|
||||
* **Recursos Premium**: integração com SSO, permissões em nível de linha e dados de uso de IA
|
||||
* Suporte da equipe Twenty
|
||||
* Sem necessidade de publicar código personalizado como código aberto antes de distribuir
|
||||
|
||||
@@ -55,6 +55,7 @@ Os recursos premium estão disponíveis apenas nos planos Organização (Nuvem o
|
||||
|
||||
* **Integração SSO**: Single Sign-On com seu provedor de identidade
|
||||
* **Permissões em nível de linha**: controle de acesso granular no nível de registro
|
||||
* **Dados de uso de IA**: Acompanhe o consumo de IA em todo o espaço de trabalho
|
||||
|
||||
## Mudança de planos
|
||||
|
||||
@@ -77,3 +78,15 @@ Entre em contato com o suporte para fazer downgrade do seu plano.
|
||||
### Mudar para faturamento mensal
|
||||
|
||||
Entre em contato com o suporte para voltar ao faturamento mensal.
|
||||
|
||||
## Obtenha uma chave Enterprise para Organization (Self-Hosted)
|
||||
|
||||
Para usar o plano Organization (Self-Hosted), você precisa obter uma chave Enterprise:
|
||||
|
||||
1. Vá para **Configurações → Painel de Administração → Enterprise**
|
||||
|
||||
<img src="/images/user-guide/billing/enterprise-key.png" alt="Chave Enterprise" />
|
||||
|
||||
2. Clique em **Obter chave Enterprise**
|
||||
3. Quando você for redirecionado ao Stripe, insira seus dados de pagamento e confirme
|
||||
4. Quando sua chave Enterprise for exibida, cole-a na página de configurações do Enterprise e ative a licença do Organization
|
||||
|
||||
@@ -16,6 +16,11 @@ Se você quiser hospedar por conta própria e precisar dos recursos Premium (SSO
|
||||
Os recursos Premium estão disponíveis apenas nos planos Organization (Cloud ou Self-Hosted):
|
||||
* **Integração de SSO**: Single Sign-On com seu provedor de identidade
|
||||
* **Permissões em nível de linha**: Controle de acesso granular no nível de registro
|
||||
* **Dados de uso de IA**: Acompanhe o consumo de IA em todo o espaço de trabalho
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="O plano Organization na nuvem e o plano Organization em ambiente autohospedado são iguais?">
|
||||
Ambos oferecem os mesmos recursos Premium. No entanto, uma assinatura na nuvem não pode ser usada para implantações autohospedadas. O ambiente autohospedado requer uma chave Enterprise.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Você oferece assentos gratuitos para usuários apenas visualizadores?">
|
||||
|
||||
@@ -53,38 +53,45 @@ Primeiro, crie o objeto intermediário que manterá as conexões.
|
||||
1. Vá a **Definições → Modelo de Dados**
|
||||
2. Clique em **+ Novo objeto**
|
||||
3. Dê um nome descritivo (por exemplo, "Atribuição de Projeto", "Membro da Equipe", "Pedido de Produto")
|
||||
4. Clique em **Salvar**
|
||||
4. Ative a opção "Ignorar a criação de um campo Nome"
|
||||
|
||||
<img src="/images/user-guide/fields/new-pivot-object.png" alt="Novo objeto pivô" />
|
||||
|
||||
5. Clique em **Salvar**
|
||||
|
||||
<Tip>
|
||||
**Convenção de nomenclatura**: Use um nome que descreva a relação, como "Atribuição de Projeto" ou "Participação na Equipe". Isso torna o modelo de dados mais fácil de entender.
|
||||
</Tip>
|
||||
|
||||
## Etapa 2: Criar Relações a partir do Objeto de Junção
|
||||
## Etapa 2: Crie relações entre os objetos e o objeto de junção
|
||||
|
||||
Adicione campos de relação do objeto de junção para ambos os objetos que deseja conectar.
|
||||
Adicione campos de relação de cada um dos seus dois objetos ao objeto de junção.
|
||||
|
||||
### Primeira Relação (Junção → Objeto A)
|
||||
### Primeira Relação (Objeto A → Junção)
|
||||
|
||||
1. Selecione seu objeto de junção em **Settings → Data Model**
|
||||
2. Clique em **+ Adicionar campo**
|
||||
3. Escolha **Relação** como o tipo de campo
|
||||
4. Selecione o primeiro objeto (por exemplo, "Pessoas")
|
||||
5. Defina o tipo de relação como **Muitos-para-um** (muitas atribuições podem se vincular a uma pessoa)
|
||||
6. Nomeie os campos:
|
||||
* Campo na junção: por exemplo, "Pessoa"
|
||||
* Campo em Pessoas: por exemplo, "Atribuições de Projeto"
|
||||
7. Clique em **Salvar**
|
||||
|
||||
### Segunda Relação (Junção → Objeto B)
|
||||
|
||||
1. Ainda no objeto de junção, clique em **+ Add Field**
|
||||
2. Escolha **Relação** como o tipo de campo
|
||||
3. Selecione o segundo objeto (por exemplo, "Projetos")
|
||||
4. Defina o tipo de relação como **Muitos-para-um**
|
||||
1. Selecione seu primeiro objeto em **Settings → Data Model**
|
||||
2. Clique em **+ Adicionar relação**
|
||||
3. Selecione o objeto de junção (por exemplo, "Atribuições de Projeto")
|
||||
4. Defina o tipo de relação como **Um-para-muitos** (uma pessoa pode se vincular a muitas atribuições)
|
||||
5. Nomeie os campos:
|
||||
* Campo em Pessoas: por exemplo, "Atribuições de Projeto"
|
||||
* Campo na junção: por exemplo, "Pessoa"
|
||||
6. Clique em **Salvar**
|
||||
|
||||
### Segunda Relação (Objeto B → Junção)
|
||||
|
||||
1. Selecione seu segundo objeto em **Settings → Data Model**
|
||||
2. Clique em **+ Adicionar relação**
|
||||
3. Selecione o objeto de junção (por exemplo, "Atribuições de Projeto")
|
||||
4. Defina o tipo de relação como **Um-para-muitos** (um projeto pode se vincular a muitas atribuições)
|
||||
5. Ative **"Esta é uma relação com um objeto de junção"**
|
||||
|
||||
<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}} />
|
||||
|
||||
6. Nomeie os campos:
|
||||
* Campo na junção: por exemplo, "Projeto"
|
||||
* Campo em Projetos: por exemplo, "Membros da Equipe"
|
||||
6. Clique em **Salvar**
|
||||
7. Clique em **Salvar**
|
||||
|
||||
## Etapa 3: Configurar a Exibição da Relação de Junção
|
||||
|
||||
@@ -98,18 +105,6 @@ Agora configure os objetos de origem para exibir diretamente os registros vincul
|
||||
6. Selecione a **Relação de destino** (por exemplo, "Projeto" — o campo na junção que aponta para o outro lado)
|
||||
7. Clique em **Salvar**
|
||||
|
||||
{/* TODO: Add image
|
||||
<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}}/>
|
||||
*/}
|
||||
|
||||
Repita para o outro objeto:
|
||||
|
||||
1. Selecione "Projetos" em Data Model
|
||||
2. Edite o campo de relação "Membros da Equipe"
|
||||
3. Ative o alternador de junção
|
||||
4. Selecione "Pessoa" como a relação de destino
|
||||
5. Salvar
|
||||
|
||||
## Resultado
|
||||
|
||||
Após a configuração:
|
||||
@@ -130,15 +125,15 @@ Aqui está um passo a passo completo:
|
||||
|
||||
### Adicionar Relações
|
||||
|
||||
1. **Atribuição de Projeto → Pessoas**
|
||||
* Tipo: Muitos-para-um
|
||||
* Campo em Atribuição: "Pessoa"
|
||||
1. **Pessoas → Atribuição de Projeto**
|
||||
* Tipo: Um-para-muitos
|
||||
* Campo em Pessoas: "Atribuições de Projeto"
|
||||
* Campo em Atribuição: "Pessoa"
|
||||
|
||||
2. **Atribuição de Projeto → Projetos**
|
||||
* Tipo: Muitos-para-um
|
||||
* Campo em Atribuição: "Projeto"
|
||||
2. **Projetos → Atribuição de Projeto**
|
||||
* Tipo: Um-para-muitos
|
||||
* Campo em Projetos: "Membros da Equipe"
|
||||
* Campo em Atribuição: "Projeto"
|
||||
|
||||
### Configurar Exibição de Junção
|
||||
|
||||
|
||||
@@ -30,3 +30,9 @@ Adicione links para ferramentas externas diretamente na barra lateral. Útil par
|
||||
## Menu de Comandos
|
||||
|
||||
Pressione `Cmd+K` (ou `Ctrl+K`) para abrir o menu de comandos — uma barra de pesquisa de acesso rápido para ir a qualquer registro, visualização ou ação sem navegar pela barra lateral.
|
||||
|
||||
## Personalizando a barra lateral
|
||||
|
||||
Para personalizar a barra lateral, passe o cursor sobre a seção "Workspace" na barra lateral e clique no ícone de chave inglesa.
|
||||
|
||||
<img src="/images/user-guide/layout/navigation-edit-icon.png" alt="Ícone de edição da navegação" />
|
||||
|
||||
@@ -3,6 +3,8 @@ title: Páginas de Registos
|
||||
description: Personalize o layout das páginas de detalhe de registos individuais com abas e widgets.
|
||||
---
|
||||
|
||||
## Visão Geral
|
||||
|
||||
Ao abrir um registo no Twenty, a página de detalhe é composta por **abas** e **widgets**. Ambos são totalmente personalizáveis por tipo de objeto.
|
||||
|
||||
## Abas
|
||||
@@ -38,12 +40,20 @@ Os widgets são os componentes básicos dentro de cada aba. Tipos de widget disp
|
||||
|
||||
1. Abra qualquer registro
|
||||
2. Pressione `Cmd+K` e pesquise por "Edit record page layout"
|
||||
|
||||
ou
|
||||
|
||||
1. Vá para Configurações > Modelo de dados > objeto de sua escolha > Layout
|
||||
|
||||
2. Clique no botão "Personalizar página de registro" para esse objeto
|
||||
|
||||
3. Agora você está no modo de personalização:
|
||||
* **Adicionar widgets** no seletor de widgets
|
||||
* **Arraste widgets** para reposicioná-los na grade
|
||||
* **Redimensione os widgets** arrastando suas bordas
|
||||
* **Configure campos** exibidos em cada widget
|
||||
* **Gerencie abas** — adicione, remova, renomeie, reordene
|
||||
|
||||
4. Salve suas alterações — elas se aplicam a todos os registros desse tipo de objeto
|
||||
|
||||
## Visibilidade de campos
|
||||
|
||||