Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
224e3b2b1f | ||
|
|
4cc3deb937 | ||
|
|
e15feda3c3 | ||
|
|
9f95c4763c | ||
|
|
e6fe48b66d | ||
|
|
20ac5b7e84 | ||
|
|
136f362b24 | ||
|
|
291792f864 | ||
|
|
9054d3aef6 | ||
|
|
19dd4d6c1b | ||
|
|
a0c6727a61 | ||
|
|
b002930554 | ||
|
|
aa0ea96582 | ||
|
|
36dece43c7 | ||
|
|
ac8e0d4217 | ||
|
|
ee3ebd0ca0 | ||
|
|
c11e4ece39 | ||
|
|
5bbfce7789 | ||
|
|
887e0283c5 | ||
|
|
94e019f012 | ||
|
|
c23961fa81 | ||
|
|
2612145436 | ||
|
|
4c97642258 | ||
|
|
08f019e9c9 | ||
|
|
888fa271f0 | ||
|
|
bb5c64952c | ||
|
|
6dedb35a1f | ||
|
|
436333f110 | ||
|
|
ec283b8f2d | ||
|
|
c3b969ab74 | ||
|
|
287fe90ce9 | ||
|
|
d10c0a6439 | ||
|
|
176e81cd76 | ||
|
|
1ce4da5b67 | ||
|
|
ef66d6b337 | ||
|
|
bcca5d0002 | ||
|
|
e0630b8653 | ||
|
|
61a27984e8 | ||
|
|
fd21d0c6ca | ||
|
|
8d539f0e49 | ||
|
|
a6cecdbd49 | ||
|
|
383935d0d9 | ||
|
|
37908114fc | ||
|
|
369ae2862f | ||
|
|
5bde41ebbb | ||
|
|
8fa3962e1c | ||
|
|
ca00e8dece | ||
|
|
2263e14394 |
@@ -35,15 +35,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
credentials:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
image: postgres:18
|
||||
env:
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: postgres
|
||||
ALLOW_NOSSL: 'true'
|
||||
SPILO_PROVIDER: 'local'
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
@@ -53,16 +48,10 @@ jobs:
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis
|
||||
credentials:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
ports:
|
||||
- 6379:6379
|
||||
clickhouse:
|
||||
image: clickhouse/clickhouse-server:25.8.8
|
||||
credentials:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
env:
|
||||
CLICKHOUSE_PASSWORD: clickhousePassword
|
||||
CLICKHOUSE_URL: "http://default:clickhousePassword@localhost:8123/twenty"
|
||||
@@ -86,6 +75,8 @@ jobs:
|
||||
run: |
|
||||
echo "Attempting to merge main into current branch..."
|
||||
|
||||
git config user.email "[email protected]"
|
||||
git config user.name "CI"
|
||||
git fetch origin main
|
||||
|
||||
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
@@ -99,8 +90,8 @@ jobs:
|
||||
echo "❌ Merge failed due to conflicts"
|
||||
echo "⚠️ Falling back to comparing current branch against main without merge"
|
||||
|
||||
# Abort the failed merge
|
||||
git merge --abort
|
||||
# Abort the failed merge (may not exist if merge never started)
|
||||
git merge --abort 2>/dev/null || true
|
||||
|
||||
echo "merged=false" >> $GITHUB_OUTPUT
|
||||
echo "BRANCH_STATE=conflicts" >> $GITHUB_ENV
|
||||
@@ -402,11 +393,22 @@ jobs:
|
||||
# Clean up temp directory
|
||||
rm -rf /tmp/current-branch-files
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
- name: Validate downloaded schema files
|
||||
id: validate-schemas
|
||||
run: |
|
||||
valid=true
|
||||
|
||||
for file in main-schema-introspection.json current-schema-introspection.json \
|
||||
main-metadata-schema-introspection.json current-metadata-schema-introspection.json \
|
||||
main-rest-api.json current-rest-api.json \
|
||||
main-rest-metadata-api.json current-rest-metadata-api.json; do
|
||||
if [ ! -f "$file" ] || ! jq empty "$file" 2>/dev/null; then
|
||||
echo "::warning::Invalid or missing schema file: $file"
|
||||
valid=false
|
||||
fi
|
||||
done
|
||||
|
||||
echo "valid=$valid" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Install OpenAPI Diff Tool
|
||||
run: |
|
||||
@@ -414,6 +416,7 @@ jobs:
|
||||
echo "Using OpenAPITools/openapi-diff via Docker"
|
||||
|
||||
- name: Generate GraphQL Schema Diff Reports
|
||||
if: steps.validate-schemas.outputs.valid == 'true'
|
||||
run: |
|
||||
echo "=== INSTALLING GRAPHQL INSPECTOR CLI ==="
|
||||
npm install -g @graphql-inspector/cli
|
||||
@@ -424,7 +427,6 @@ jobs:
|
||||
echo "Checking GraphQL schema for changes..."
|
||||
if graphql-inspector diff main-schema-introspection.json current-schema-introspection.json >/dev/null 2>&1; then
|
||||
echo "✅ No changes in GraphQL schema"
|
||||
# Don't create a diff file for no changes
|
||||
else
|
||||
echo "⚠️ Changes detected in GraphQL schema, generating report..."
|
||||
echo "# GraphQL Schema Changes" > graphql-schema-diff.md
|
||||
@@ -442,7 +444,6 @@ jobs:
|
||||
echo "Checking GraphQL metadata schema for changes..."
|
||||
if graphql-inspector diff main-metadata-schema-introspection.json current-metadata-schema-introspection.json >/dev/null 2>&1; then
|
||||
echo "✅ No changes in GraphQL metadata schema"
|
||||
# Don't create a diff file for no changes
|
||||
else
|
||||
echo "⚠️ Changes detected in GraphQL metadata schema, generating report..."
|
||||
echo "# GraphQL Metadata Schema Changes" > graphql-metadata-diff.md
|
||||
@@ -461,6 +462,7 @@ jobs:
|
||||
ls -la *-diff.md 2>/dev/null || echo "No diff files generated (no changes detected)"
|
||||
|
||||
- name: Check REST API Breaking Changes
|
||||
if: steps.validate-schemas.outputs.valid == 'true'
|
||||
run: |
|
||||
echo "=== CHECKING REST API FOR BREAKING CHANGES ==="
|
||||
|
||||
@@ -529,6 +531,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Check REST Metadata API Breaking Changes
|
||||
if: steps.validate-schemas.outputs.valid == 'true'
|
||||
run: |
|
||||
echo "=== CHECKING REST METADATA API FOR BREAKING CHANGES ==="
|
||||
|
||||
|
||||
@@ -36,15 +36,10 @@ jobs:
|
||||
runs-on: ubuntu-latest-4-cores
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
credentials:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
image: postgres:18
|
||||
env:
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: postgres
|
||||
ALLOW_NOSSL: 'true'
|
||||
SPILO_PROVIDER: 'local'
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
@@ -54,9 +49,6 @@ jobs:
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis
|
||||
credentials:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
ports:
|
||||
- 6379:6379
|
||||
env:
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
name: CI Front Component Renderer
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
merge_group:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
if: github.event_name != 'merge_group'
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
package.json
|
||||
yarn.lock
|
||||
packages/twenty-front-component-renderer/**
|
||||
packages/twenty-sdk/**
|
||||
packages/twenty-shared/**
|
||||
!packages/twenty-sdk/package.json
|
||||
renderer-task:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
task: [build, typecheck, lint]
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Run ${{ matrix.task }}
|
||||
run: npx nx ${{ matrix.task }} twenty-front-component-renderer
|
||||
renderer-sb-build:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build storybook
|
||||
run: npx nx storybook:build twenty-front-component-renderer
|
||||
- name: Upload storybook build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: storybook-twenty-front-component-renderer
|
||||
path: packages/twenty-front-component-renderer/storybook-static
|
||||
retention-days: 1
|
||||
renderer-sb-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
needs: renderer-sb-build
|
||||
env:
|
||||
STORYBOOK_URL: http://localhost:6008
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build dependencies
|
||||
run: npx nx build twenty-sdk
|
||||
- name: Download storybook build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-twenty-front-component-renderer
|
||||
path: packages/twenty-front-component-renderer/storybook-static
|
||||
- name: Install Playwright
|
||||
run: |
|
||||
cd packages/twenty-front-component-renderer
|
||||
npx playwright install
|
||||
- name: Serve storybook & run tests
|
||||
run: |
|
||||
npx http-server packages/twenty-front-component-renderer/storybook-static --port 6008 --silent &
|
||||
timeout 30 bash -c 'until curl -sf http://localhost:6008 > /dev/null 2>&1; do sleep 1; done'
|
||||
npx nx storybook:test twenty-front-component-renderer
|
||||
ci-front-component-renderer-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
[
|
||||
changed-files-check,
|
||||
renderer-task,
|
||||
renderer-sb-build,
|
||||
renderer-sb-test,
|
||||
]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
@@ -25,6 +25,7 @@ jobs:
|
||||
package.json
|
||||
yarn.lock
|
||||
packages/twenty-front/**
|
||||
packages/twenty-front-component-renderer/**
|
||||
packages/twenty-ui/**
|
||||
packages/twenty-shared/**
|
||||
packages/twenty-sdk/**
|
||||
@@ -93,7 +94,7 @@ jobs:
|
||||
run: |
|
||||
npx nx build twenty-shared
|
||||
npx nx build twenty-ui
|
||||
npx nx build twenty-sdk
|
||||
npx nx build twenty-front-component-renderer
|
||||
- name: Download storybook build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
|
||||
@@ -25,15 +25,10 @@ jobs:
|
||||
NODE_OPTIONS: "--max-old-space-size=10240"
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
credentials:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
image: postgres:18
|
||||
env:
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: postgres
|
||||
ALLOW_NOSSL: "true"
|
||||
SPILO_PROVIDER: "local"
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
@@ -43,9 +38,6 @@ jobs:
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis
|
||||
credentials:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
ports:
|
||||
- 6379:6379
|
||||
steps:
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
task: [lint, typecheck, test:unit, storybook:build, storybook:test, test:integration]
|
||||
task: [lint, typecheck, test:unit, test:integration]
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
@@ -41,9 +41,6 @@ jobs:
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build
|
||||
run: npx nx build twenty-sdk
|
||||
- name: Install Playwright
|
||||
if: contains(matrix.task, 'storybook')
|
||||
run: npx playwright install chromium
|
||||
- name: Run ${{ matrix.task }} task
|
||||
uses: ./.github/actions/nx-affected
|
||||
with:
|
||||
@@ -56,15 +53,10 @@ jobs:
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
credentials:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
image: postgres:18
|
||||
env:
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: postgres
|
||||
ALLOW_NOSSL: 'true'
|
||||
SPILO_PROVIDER: 'local'
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
@@ -74,9 +66,6 @@ jobs:
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis
|
||||
credentials:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
ports:
|
||||
- 6379:6379
|
||||
env:
|
||||
|
||||
@@ -83,15 +83,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
credentials:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
image: postgres:18
|
||||
env:
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: postgres
|
||||
ALLOW_NOSSL: 'true'
|
||||
SPILO_PROVIDER: 'local'
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
@@ -101,9 +96,6 @@ jobs:
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis
|
||||
credentials:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
ports:
|
||||
- 6379:6379
|
||||
steps:
|
||||
@@ -231,15 +223,10 @@ jobs:
|
||||
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
credentials:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
image: postgres:18
|
||||
env:
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: postgres
|
||||
ALLOW_NOSSL: 'true'
|
||||
SPILO_PROVIDER: 'local'
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
@@ -249,16 +236,10 @@ jobs:
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis
|
||||
credentials:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
ports:
|
||||
- 6379:6379
|
||||
clickhouse:
|
||||
image: clickhouse/clickhouse-server:25.8.8
|
||||
credentials:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
env:
|
||||
CLICKHOUSE_PASSWORD: clickhousePassword
|
||||
CLICKHOUSE_URL: "http://default:clickhousePassword@localhost:8123/twenty"
|
||||
|
||||
@@ -26,15 +26,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
credentials:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
image: postgres:18
|
||||
env:
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: postgres
|
||||
ALLOW_NOSSL: 'true'
|
||||
SPILO_PROVIDER: 'local'
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
|
||||
@@ -29,15 +29,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
credentials:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
image: postgres:18
|
||||
env:
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: postgres
|
||||
ALLOW_NOSSL: 'true'
|
||||
SPILO_PROVIDER: 'local'
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
@@ -47,9 +42,6 @@ jobs:
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis
|
||||
credentials:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
ports:
|
||||
- 6379:6379
|
||||
steps:
|
||||
|
||||
@@ -180,6 +180,7 @@
|
||||
"graphql": "16.8.1",
|
||||
"type-fest": "4.10.1",
|
||||
"typescript": "5.9.2",
|
||||
"nodemailer": "8.0.4",
|
||||
"graphql-redis-subscriptions/ioredis": "^5.6.0",
|
||||
"@lingui/core": "5.1.2",
|
||||
"@types/qs": "6.9.16",
|
||||
@@ -208,6 +209,7 @@
|
||||
"packages/twenty-e2e-testing",
|
||||
"packages/twenty-shared",
|
||||
"packages/twenty-sdk",
|
||||
"packages/twenty-front-component-renderer",
|
||||
"packages/twenty-client-sdk",
|
||||
"packages/twenty-apps",
|
||||
"packages/twenty-cli",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-twenty-app",
|
||||
"version": "0.8.0-canary.6",
|
||||
"version": "0.8.0-canary.7",
|
||||
"description": "Command-line interface to create Twenty application",
|
||||
"main": "dist/cli.cjs",
|
||||
"bin": "dist/cli.cjs",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
## Base documentation
|
||||
|
||||
- Documentation: https://docs.twenty.com/developers/extend/capabilities/apps
|
||||
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-sdk/src/app-seeds/rich-app
|
||||
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/fixtures/postcard-app
|
||||
|
||||
## UUID requirement
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
## Base documentation
|
||||
|
||||
- Documentation: https://docs.twenty.com/developers/extend/capabilities/apps
|
||||
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-sdk/src/app-seeds/rich-app
|
||||
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/fixtures/postcard-app
|
||||
|
||||
## UUID requirement
|
||||
- All generated UUIDs must be valid UUID v4.
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
defineField,
|
||||
FieldType,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk';
|
||||
|
||||
// Field on existing company object
|
||||
export default defineField({
|
||||
universalIdentifier: 'f922fdb8-10a9-4f11-a1d0-992a779f6dff',
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
type: FieldType.BOOLEAN,
|
||||
name: 'canReceivePostcards',
|
||||
label: 'Can Receive Postcards',
|
||||
description: 'Whether the company can receive postcards',
|
||||
icon: 'IconMailbox',
|
||||
defaultValue: false,
|
||||
});
|
||||
@@ -6,6 +6,9 @@ export const RECIPIENT_UNIVERSAL_IDENTIFIER =
|
||||
export const RECIPIENT_EMAIL_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'd2a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5d';
|
||||
|
||||
export const RECIPIENT_ADDRESS_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'd3a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5d';
|
||||
|
||||
export default defineObject({
|
||||
universalIdentifier: RECIPIENT_UNIVERSAL_IDENTIFIER,
|
||||
nameSingular: 'recipient',
|
||||
@@ -23,7 +26,7 @@ export default defineObject({
|
||||
icon: 'IconMail',
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'd3a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
universalIdentifier: RECIPIENT_ADDRESS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.ADDRESS,
|
||||
label: 'Mailing Address',
|
||||
name: 'mailingAddress',
|
||||
|
||||
+7
-3
@@ -1,13 +1,16 @@
|
||||
import { defineView } from 'twenty-sdk';
|
||||
import { ViewType } from 'twenty-shared/types';
|
||||
import {
|
||||
POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
|
||||
SENT_AT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
} from '../objects/post-card-recipient.object';
|
||||
import { defineView } from 'twenty-sdk';
|
||||
import { ViewType } from 'twenty-shared/types';
|
||||
|
||||
export const ALL_POST_CARD_RECIPIENTS_VIEW_ID =
|
||||
'b1a2b3c4-0003-4a7b-8c9d-0e1f2a3b4c5d';
|
||||
|
||||
export const POST_CARD_RECIPIENT_SENT_AT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'fd959c6f-3465-4a3a-b7ad-3f4004fffc9a';
|
||||
|
||||
export default defineView({
|
||||
universalIdentifier: ALL_POST_CARD_RECIPIENTS_VIEW_ID,
|
||||
name: 'All Post Card Recipients',
|
||||
@@ -17,7 +20,8 @@ export default defineView({
|
||||
position: 2,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: 'bf1a2b3c-0004-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
universalIdentifier:
|
||||
POST_CARD_RECIPIENT_SENT_AT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
fieldMetadataUniversalIdentifier: SENT_AT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { defineView } from 'twenty-sdk';
|
||||
import { ViewType } from 'twenty-shared/types';
|
||||
import {
|
||||
RECIPIENT_ADDRESS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
RECIPIENT_EMAIL_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
RECIPIENT_UNIVERSAL_IDENTIFIER,
|
||||
} from '../objects/recipient.object';
|
||||
import { defineView } from 'twenty-sdk';
|
||||
import { ViewType } from 'twenty-shared/types';
|
||||
|
||||
export const ALL_RECIPIENTS_VIEW_ID = 'b1a2b3c4-0002-4a7b-8c9d-0e1f2a3b4c5d';
|
||||
|
||||
@@ -23,5 +24,13 @@ export default defineView({
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'bf1a2b3c-0004-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
RECIPIENT_ADDRESS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 1,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
## Base documentation
|
||||
|
||||
- Documentation: https://docs.twenty.com/developers/extend/capabilities/apps
|
||||
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-sdk/src/app-seeds/rich-app
|
||||
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/fixtures/postcard-app
|
||||
|
||||
## UUID requirement
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ export default defineObject({
|
||||
labelPlural: 'Example items',
|
||||
description: 'A sample custom object',
|
||||
icon: 'IconBox',
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
## Base documentation
|
||||
|
||||
- Documentation: https://docs.twenty.com/developers/extend/capabilities/apps
|
||||
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-sdk/src/app-seeds/rich-app
|
||||
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/fixtures/postcard-app
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-client-sdk",
|
||||
"version": "0.8.0-canary.6",
|
||||
"version": "0.8.0-canary.7",
|
||||
"sideEffects": false,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
|
||||
@@ -1720,12 +1720,12 @@ enum FeatureFlagKey {
|
||||
IS_UNIQUE_INDEXES_ENABLED
|
||||
IS_JSON_FILTER_ENABLED
|
||||
IS_AI_ENABLED
|
||||
IS_COMMAND_MENU_ITEM_ENABLED
|
||||
IS_MARKETPLACE_ENABLED
|
||||
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED
|
||||
IS_PUBLIC_DOMAIN_ENABLED
|
||||
IS_EMAILING_DOMAIN_ENABLED
|
||||
IS_JUNCTION_RELATIONS_ENABLED
|
||||
IS_COMMAND_MENU_ITEM_ENABLED
|
||||
IS_DRAFT_EMAIL_ENABLED
|
||||
IS_USAGE_ANALYTICS_ENABLED
|
||||
IS_RICH_TEXT_V1_MIGRATED
|
||||
@@ -3766,8 +3766,13 @@ input UpsertFieldsWidgetGroupInput {
|
||||
}
|
||||
|
||||
input UpsertFieldsWidgetFieldInput {
|
||||
"""The id of the view field"""
|
||||
viewFieldId: UUID!
|
||||
"""The id of the view field. Required if fieldMetadataId is not provided."""
|
||||
viewFieldId: UUID
|
||||
|
||||
"""
|
||||
The id of the field metadata. Used to create a new view field when viewFieldId is not provided.
|
||||
"""
|
||||
fieldMetadataId: UUID
|
||||
isVisible: Boolean!
|
||||
position: Float!
|
||||
}
|
||||
|
||||
@@ -1422,7 +1422,7 @@ export interface PublicFeatureFlag {
|
||||
__typename: 'PublicFeatureFlag'
|
||||
}
|
||||
|
||||
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_AI_ENABLED' | 'IS_MARKETPLACE_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_COMMAND_MENU_ITEM_ENABLED' | 'IS_DRAFT_EMAIL_ENABLED' | 'IS_USAGE_ANALYTICS_ENABLED' | 'IS_RICH_TEXT_V1_MIGRATED' | 'IS_DIRECT_GRAPHQL_EXECUTION_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' | 'IS_CONNECTED_ACCOUNT_MIGRATED' | 'IS_GRAPHQL_QUERY_TIMING_ENABLED' | 'IS_RECORD_TABLE_WIDGET_ENABLED' | 'IS_DATASOURCE_MIGRATED'
|
||||
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_AI_ENABLED' | 'IS_COMMAND_MENU_ITEM_ENABLED' | 'IS_MARKETPLACE_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_DRAFT_EMAIL_ENABLED' | 'IS_USAGE_ANALYTICS_ENABLED' | 'IS_RICH_TEXT_V1_MIGRATED' | 'IS_DIRECT_GRAPHQL_EXECUTION_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' | 'IS_CONNECTED_ACCOUNT_MIGRATED' | 'IS_GRAPHQL_QUERY_TIMING_ENABLED' | 'IS_RECORD_TABLE_WIDGET_ENABLED' | 'IS_DATASOURCE_MIGRATED'
|
||||
|
||||
export interface ClientConfig {
|
||||
appVersion?: Scalars['String']
|
||||
@@ -6258,8 +6258,10 @@ fields?: (UpsertFieldsWidgetFieldInput[] | null)}
|
||||
export interface UpsertFieldsWidgetGroupInput {id: Scalars['UUID'],name: Scalars['String'],position: Scalars['Float'],isVisible: Scalars['Boolean'],fields: UpsertFieldsWidgetFieldInput[]}
|
||||
|
||||
export interface UpsertFieldsWidgetFieldInput {
|
||||
/** The id of the view field */
|
||||
viewFieldId: Scalars['UUID'],isVisible: Scalars['Boolean'],position: Scalars['Float']}
|
||||
/** The id of the view field. Required if fieldMetadataId is not provided. */
|
||||
viewFieldId?: (Scalars['UUID'] | null),
|
||||
/** The id of the field metadata. Used to create a new view field when viewFieldId is not provided. */
|
||||
fieldMetadataId?: (Scalars['UUID'] | null),isVisible: Scalars['Boolean'],position: Scalars['Float']}
|
||||
|
||||
export interface CreateApiKeyInput {name: Scalars['String'],expiresAt: Scalars['String'],revokedAt?: (Scalars['String'] | null),roleId: Scalars['UUID']}
|
||||
|
||||
@@ -8947,12 +8949,12 @@ export const enumFeatureFlagKey = {
|
||||
IS_UNIQUE_INDEXES_ENABLED: 'IS_UNIQUE_INDEXES_ENABLED' as const,
|
||||
IS_JSON_FILTER_ENABLED: 'IS_JSON_FILTER_ENABLED' as const,
|
||||
IS_AI_ENABLED: 'IS_AI_ENABLED' as const,
|
||||
IS_COMMAND_MENU_ITEM_ENABLED: 'IS_COMMAND_MENU_ITEM_ENABLED' as const,
|
||||
IS_MARKETPLACE_ENABLED: 'IS_MARKETPLACE_ENABLED' as const,
|
||||
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED: 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' as const,
|
||||
IS_PUBLIC_DOMAIN_ENABLED: 'IS_PUBLIC_DOMAIN_ENABLED' as const,
|
||||
IS_EMAILING_DOMAIN_ENABLED: 'IS_EMAILING_DOMAIN_ENABLED' as const,
|
||||
IS_JUNCTION_RELATIONS_ENABLED: 'IS_JUNCTION_RELATIONS_ENABLED' as const,
|
||||
IS_COMMAND_MENU_ITEM_ENABLED: 'IS_COMMAND_MENU_ITEM_ENABLED' as const,
|
||||
IS_DRAFT_EMAIL_ENABLED: 'IS_DRAFT_EMAIL_ENABLED' as const,
|
||||
IS_USAGE_ANALYTICS_ENABLED: 'IS_USAGE_ANALYTICS_ENABLED' as const,
|
||||
IS_RICH_TEXT_V1_MIGRATED: 'IS_RICH_TEXT_V1_MIGRATED' as const,
|
||||
|
||||
@@ -9556,6 +9556,9 @@ export default {
|
||||
"viewFieldId": [
|
||||
3
|
||||
],
|
||||
"fieldMetadataId": [
|
||||
3
|
||||
],
|
||||
"isVisible": [
|
||||
6
|
||||
],
|
||||
|
||||
@@ -16,6 +16,7 @@ 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/
|
||||
|
||||
@@ -51,6 +52,7 @@ FROM common-deps AS twenty-front-build
|
||||
ARG REACT_APP_SERVER_BASE_URL
|
||||
|
||||
COPY ./packages/twenty-front /app/packages/twenty-front
|
||||
COPY ./packages/twenty-front-component-renderer /app/packages/twenty-front-component-renderer
|
||||
COPY ./packages/twenty-ui /app/packages/twenty-ui
|
||||
COPY ./packages/twenty-shared /app/packages/twenty-shared
|
||||
COPY ./packages/twenty-sdk /app/packages/twenty-sdk
|
||||
|
||||
@@ -358,17 +358,17 @@ export default defineApplication({
|
||||
|
||||
إذا كنت تخطط لـ [نشر تطبيقك](/l/ar/developers/extend/apps/publishing)، فإن هذه الحقول الاختيارية تتحكّم في كيفية ظهور تطبيقك في السوق:
|
||||
|
||||
| الحقل | الوصف |
|
||||
| ------------------ | ---------------------------------------------------- |
|
||||
| `author` | اسم المؤلف أو الشركة |
|
||||
| `category` | فئة التطبيق لتصفية سوق التطبيقات |
|
||||
| `logoUrl` | المسار إلى شعار تطبيقك (نسبيًا إلى `./assets/`) |
|
||||
| `screenshots` | مصفوفة لمسارات لقطات الشاشة (نسبيًا إلى `./assets/`) |
|
||||
| `aboutDescription` | وصف ماركداون أطول لعلامة التبويب "حول" |
|
||||
| `websiteUrl` | رابط إلى موقعك الإلكتروني |
|
||||
| `termsUrl` | رابط إلى شروط الخدمة |
|
||||
| `emailSupport` | عنوان البريد الإلكتروني للدعم |
|
||||
| `issueReportUrl` | رابط إلى متتبّع المشاكل |
|
||||
| الحقل | الوصف |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------ |
|
||||
| `author` | اسم المؤلف أو الشركة |
|
||||
| `category` | فئة التطبيق لتصفية سوق التطبيقات |
|
||||
| `logoUrl` | المسار إلى شعار تطبيقك (نسبيًا إلى `./assets/`) |
|
||||
| `screenshots` | مصفوفة لمسارات لقطات الشاشة (نسبيًا إلى `./assets/`) |
|
||||
| `aboutDescription` | وصف ماركداون أطول لعلامة التبويب "حول". إذا لم يتم تضمينه، يستخدم السوق ملف `README.md` الخاص بالحزمة من npm |
|
||||
| `websiteUrl` | رابط إلى موقعك الإلكتروني |
|
||||
| `termsUrl` | رابط إلى شروط الخدمة |
|
||||
| `emailSupport` | عنوان البريد الإلكتروني للدعم |
|
||||
| `issueReportUrl` | رابط إلى متتبّع المشاكل |
|
||||
|
||||
#### الأدوار والصلاحيات
|
||||
|
||||
|
||||
@@ -9,18 +9,19 @@ description: أنشئ أول تطبيق Twenty خلال دقائق.
|
||||
|
||||
تتيح لك التطبيقات توسيع Twenty باستخدام كائنات وحقول ووظائف منطقية ومهارات ذكاء اصطناعي ومكونات واجهة مستخدم مخصصة — جميعها تُدار ككود.
|
||||
|
||||
**ما الذي يمكنك فعله اليوم:**
|
||||
**ما الذي يمكنك بناؤه:**
|
||||
|
||||
* عرِّف كائنات وحقولًا مخصصة على شكل كود (نموذج بيانات مُدار)
|
||||
* أنشئ وظائف منطقية مع مشغلات مخصصة (مسارات HTTP، cron، أحداث قاعدة البيانات)
|
||||
* حدد المهارات لوكلاء الذكاء الاصطناعي
|
||||
* أنشئ مكونات واجهية تُعرَض داخل واجهة مستخدم Twenty
|
||||
* انشر التطبيق نفسه عبر مساحات عمل متعددة
|
||||
* كائنات مخصّصة، وحقول، وطرق عرض، وعناصر تنقّل لتشكيل نموذج بياناتك
|
||||
* دوال منطقية يتم تشغيلها عبر مسارات HTTP، وجداول cron، أو أحداث قاعدة البيانات
|
||||
* مكوّنات واجهة أمامية تُعرَض مباشرة داخل واجهة مستخدم Twenty
|
||||
* مهارات توسّع قدرات وكلاء الذكاء الاصطناعي في Twenty
|
||||
* انشر تطبيقاً عبر مساحات عمل متعددة
|
||||
|
||||
## المتطلبات الأساسية
|
||||
|
||||
* Node.js 24+ وYarn 4
|
||||
* Docker (لخادم تطوير Twenty المحلي)
|
||||
* Node.js 24+
|
||||
* Yarn 4
|
||||
* Docker (أو مثيل Twenty محلي قيد التشغيل)
|
||||
|
||||
## البدء
|
||||
|
||||
@@ -29,21 +30,9 @@ description: أنشئ أول تطبيق Twenty خلال دقائق.
|
||||
```bash filename="Terminal"
|
||||
# Scaffold a new app (includes all examples by default)
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# Start dev mode: automatically syncs local changes to your workspace
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
يدعم المُهيئ وضعين للتحكم في ملفات الأمثلة التي سيتم تضمينها:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimal: only core files (application-config.ts and default-role.ts)
|
||||
npx create-twenty-app@latest my-app --minimal
|
||||
```
|
||||
> استخدم الخيار `--minimal` لتهيئة تثبيت مصغّر
|
||||
|
||||
من هنا يمكنك:
|
||||
|
||||
|
||||
@@ -102,6 +102,10 @@ yarn twenty catalog-sync -r production
|
||||
|
||||
تأتي بيانات التعريف المعروضة في السوق من استدعائك لـ `defineApplication()` في الشيفرة المصدرية لتطبيقك — حقول مثل `displayName` و`description` و`author` و`category` و`logoUrl` و`screenshots` و`aboutDescription` و`websiteUrl` و`termsUrl`.
|
||||
|
||||
<Note>
|
||||
إذا لم يحدد تطبيقك `aboutDescription` في `defineApplication()`، فسيستخدم السوق تلقائيًا ملف `README.md` الخاص بحزمتك من npm كمحتوى لصفحة حول. هذا يعني أنه يمكنك الاحتفاظ بملف README واحد لكل من npm وسوق Twenty. إذا كنت تريد وصفًا مختلفًا في السوق، فقم بتعيين `aboutDescription` بشكل صريح.
|
||||
</Note>
|
||||
|
||||
### النشر عبر CI
|
||||
|
||||
يتضمن المشروع المُولَّد سير عمل GitHub Actions يقوم بالنشر عند كل إصدار:
|
||||
|
||||
@@ -38,7 +38,7 @@ yarn twenty dev
|
||||
|
||||
### إدارة الخادم المحلي
|
||||
|
||||
يتضمن SDK أوامر لإدارة خادم تطوير Twenty محلي (صورة Docker متكاملة تتضمن PostgreSQL وRedis والخادم والعامل):
|
||||
يتضمن SDK أوامر لإدارة خادم تطوير Twenty محلي (صورة Docker متكاملة تتضمن PostgreSQL وRedis والخادم والعامل على المنفذ 2020). تنطبق هذه الأوامر فقط على خادم التطوير المستند إلى Docker — ولا تُدير مثيل Twenty المُشغَّل من المصدر (مثل `npx nx start twenty-server` على المنفذ 3000):
|
||||
|
||||
```bash filename="Terminal"
|
||||
# ابدأ الخادم المحلي (يسحب الصورة إذا لزم الأمر)
|
||||
|
||||
+59
-24
@@ -55,11 +55,12 @@ export const main = async (
|
||||
params: { companyId: string },
|
||||
) => {
|
||||
const { companyId } = params;
|
||||
|
||||
// Replace with your Twenty GraphQL endpoint
|
||||
// Replace with your Twenty GraphQL endpoints (/metadata for metadata and files or /graphql for your records)
|
||||
// Cloud: https://api.twenty.com/graphql
|
||||
// Self-hosted: https://your-domain.com/graphql
|
||||
const graphqlEndpoint = 'https://api.twenty.com/graphql';
|
||||
|
||||
const metadataGraphqlEndpoint = 'https://api.twenty.com/metadata';
|
||||
const dataGraphqlEndpoint = 'https://api.twenty.com/graphql';
|
||||
|
||||
// Replace with your API key from Settings → APIs
|
||||
const authToken = 'YOUR_API_KEY';
|
||||
@@ -79,11 +80,40 @@ export const main = async (
|
||||
const pdfBlob = await pdfResponse.blob();
|
||||
const pdfFile = new File([pdfBlob], filename, { type: 'application/pdf' });
|
||||
|
||||
// Step 2: Upload the file via GraphQL multipart upload
|
||||
const fieldMetadataIdQuery = `
|
||||
query FindUploadFileFieldMetadataId {
|
||||
objects {
|
||||
edges {
|
||||
node {
|
||||
nameSingular
|
||||
fieldsList {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Step 2: Find a fieldMetadataId of "Attachment file" field in Attachments object with GraphQL API
|
||||
const response = await fetch(metadataGraphqlEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: {
|
||||
query: fieldMetadataIdQuery,
|
||||
}
|
||||
});
|
||||
const result = await response.json();
|
||||
const uploadFileFieldMetadataId = result.data.objects.edges.find(object => object.node.nameSingular === 'attachment').node.fieldsList.find(field => field.name === 'file').id;
|
||||
|
||||
// Step 3: Upload the file via GraphQL multipart upload
|
||||
const uploadMutation = `
|
||||
mutation UploadFile($file: Upload!, $fileFolder: FileFolder) {
|
||||
uploadFile(file: $file, fileFolder: $fileFolder) {
|
||||
path
|
||||
mutation UploadFilesFieldFile($file: Upload!, $fieldMetadataId: String!) {
|
||||
uploadFilesFieldFile(file: $file, fieldMetadataId: $fieldMetadataId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -91,12 +121,12 @@ export const main = async (
|
||||
const uploadForm = new FormData();
|
||||
uploadForm.append('operations', JSON.stringify({
|
||||
query: uploadMutation,
|
||||
variables: { file: null, fileFolder: 'Attachment' },
|
||||
variables: { file: null, fieldMetadataId: uploadFileFieldMetadataId },
|
||||
}));
|
||||
uploadForm.append('map', JSON.stringify({ '0': ['variables.file'] }));
|
||||
uploadForm.append('0', pdfFile);
|
||||
|
||||
const uploadResponse = await fetch(graphqlEndpoint, {
|
||||
const uploadResponse = await fetch(metadataGraphqlEndpoint, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${authToken}` },
|
||||
body: uploadForm,
|
||||
@@ -108,15 +138,15 @@ export const main = async (
|
||||
throw new Error(`Upload failed: ${uploadResult.errors[0].message}`);
|
||||
}
|
||||
|
||||
const filePath = uploadResult.data?.uploadFile?.path;
|
||||
const fileId = uploadResult.data?.uploadFilesFieldFile?.id;
|
||||
|
||||
if (!filePath) {
|
||||
throw new Error('No file path returned from upload');
|
||||
if (!fileId) {
|
||||
throw new Error('No file id returned from upload');
|
||||
}
|
||||
|
||||
// Step 3: Create the attachment linked to the company
|
||||
// Step 4: Create the attachment linked to the company
|
||||
const attachmentMutation = `
|
||||
mutation CreateAttachment($data: AttachmentCreateInput!) {
|
||||
mutation CreateOneAttachment($data: AttachmentCreateInput!) {
|
||||
createAttachment(data: $data) {
|
||||
id
|
||||
name
|
||||
@@ -124,7 +154,7 @@ export const main = async (
|
||||
}
|
||||
`;
|
||||
|
||||
const attachmentResponse = await fetch(graphqlEndpoint, {
|
||||
const attachmentResponse = await fetch(dataGraphqlEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
@@ -135,8 +165,13 @@ export const main = async (
|
||||
variables: {
|
||||
data: {
|
||||
name: filename,
|
||||
fullPath: filePath,
|
||||
companyId,
|
||||
targetCompanyId: companyId,
|
||||
file: [
|
||||
{
|
||||
fileId: fileId,
|
||||
label: filename
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -156,14 +191,14 @@ export const main = async (
|
||||
|
||||
#### لإرفاقه بكائن مختلف
|
||||
|
||||
استبدل `companyId` بالحقل المناسب:
|
||||
استبدل `targetCompanyId` بالحقل المناسب:
|
||||
|
||||
| كائن | اسم الحقل |
|
||||
| ---------- | -------------------- |
|
||||
| الشركة | `companyId` |
|
||||
| شخص | `personId` |
|
||||
| الفرصة | `opportunityId` |
|
||||
| كائن مخصّص | `yourCustomObjectId` |
|
||||
| كائن | اسم الحقل |
|
||||
| ---------- | -------------------------- |
|
||||
| الشركة | `targetCompanyId` |
|
||||
| شخص | `targetPersonId` |
|
||||
| الفرصة | `targetOpportunityId` |
|
||||
| كائن مخصّص | `targetYourCustomObjectId` |
|
||||
|
||||
حدّث كل من معلمة الوظيفة وكائن `variables.data` في عملية الـ mutation الخاصة بالمرفق.
|
||||
|
||||
|
||||
@@ -358,17 +358,17 @@ Poznámky:
|
||||
|
||||
Pokud plánujete [zveřejnit svou aplikaci](/l/cs/developers/extend/apps/publishing), tato volitelná pole určují, jak se vaše aplikace zobrazuje na Marketplace:
|
||||
|
||||
| Pole | Popis |
|
||||
| ------------------ | -------------------------------------------------------- |
|
||||
| `author` | Jméno autora nebo název společnosti |
|
||||
| `category` | Kategorie aplikace pro filtrování na Marketplace |
|
||||
| `logoUrl` | Cesta k logu vaší aplikace (relativně k `./assets/`) |
|
||||
| `screenshots` | Pole cest ke snímkům obrazovky (relativně k `./assets/`) |
|
||||
| `aboutDescription` | Delší popis v Markdownu pro kartu "O aplikaci" |
|
||||
| `websiteUrl` | Odkaz na váš web |
|
||||
| `termsUrl` | Odkaz na Podmínky služby |
|
||||
| `emailSupport` | E-mailová adresa podpory |
|
||||
| `issueReportUrl` | Odkaz na nástroj pro sledování problémů |
|
||||
| Pole | Popis |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------- |
|
||||
| `author` | Jméno autora nebo název společnosti |
|
||||
| `category` | Kategorie aplikace pro filtrování na Marketplace |
|
||||
| `logoUrl` | Cesta k logu vaší aplikace (relativně k `./assets/`) |
|
||||
| `screenshots` | Pole cest ke snímkům obrazovky (relativně k `./assets/`) |
|
||||
| `aboutDescription` | Delší popis v Markdownu pro kartu "O aplikaci". Pokud je vynecháno, tržiště použije `README.md` balíčku z npm |
|
||||
| `websiteUrl` | Odkaz na váš web |
|
||||
| `termsUrl` | Odkaz na Podmínky služby |
|
||||
| `emailSupport` | E-mailová adresa podpory |
|
||||
| `issueReportUrl` | Odkaz na nástroj pro sledování problémů |
|
||||
|
||||
#### Role a oprávnění
|
||||
|
||||
|
||||
@@ -9,18 +9,19 @@ Aplikace jsou aktuálně v alfa testování. Tato funkce je funkční, ale stál
|
||||
|
||||
Aplikace vám umožňují rozšířit Twenty o vlastní objekty, pole, logické funkce, AI schopnosti a komponenty uživatelského rozhraní — vše je spravováno jako kód.
|
||||
|
||||
**Co můžete dělat už dnes:**
|
||||
**Co můžete vytvořit:**
|
||||
|
||||
* Definujte vlastní objekty a pole jako kód (spravovaný datový model)
|
||||
* Vytvářejte logické funkce s vlastními spouštěči (HTTP routy, cron, databázové události)
|
||||
* Definujte dovednosti agentů AI
|
||||
* Vytvářejte frontendové komponenty, které se vykreslují uvnitř uživatelského rozhraní Twenty
|
||||
* Nasazujte stejnou aplikaci do více pracovních prostorů
|
||||
* Vlastní objekty, pole, zobrazení a položky navigace pro utváření vašeho datového modelu
|
||||
* Logické funkce spouštěné trasami HTTP, plánovačem cron nebo událostmi databáze
|
||||
* Frontendové komponenty, které se vykreslují přímo uvnitř uživatelského rozhraní Twenty
|
||||
* Dovednosti, které rozšiřují možnosti AI agentů Twenty
|
||||
* Nasazení aplikace napříč více pracovními prostory
|
||||
|
||||
## Předpoklady
|
||||
|
||||
* Node.js 24+ a Yarn 4
|
||||
* Docker (pro místní vývojový server Twenty)
|
||||
* Node.js 24+
|
||||
* Yarn 4
|
||||
* Docker (nebo běžící lokální instance Twenty)
|
||||
|
||||
## Začínáme
|
||||
|
||||
@@ -29,21 +30,9 @@ Vytvořte novou aplikaci pomocí oficiálního scaffolderu, poté se ověřte a
|
||||
```bash filename="Terminal"
|
||||
# Scaffold a new app (includes all examples by default)
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# Start dev mode: automatically syncs local changes to your workspace
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
Nástroj pro generování kostry podporuje dva režimy pro řízení toho, které ukázkové soubory jsou zahrnuty:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimal: only core files (application-config.ts and default-role.ts)
|
||||
npx create-twenty-app@latest my-app --minimal
|
||||
```
|
||||
> Použijte volbu `--minimal` k vygenerování minimální instalace
|
||||
|
||||
Odtud můžete:
|
||||
|
||||
|
||||
@@ -102,6 +102,10 @@ yarn twenty catalog-sync -r production
|
||||
|
||||
Metadata zobrazená v tržišti pocházejí z volání `defineApplication()` ve zdrojovém kódu vaší aplikace — z polí jako `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl` a `termsUrl`.
|
||||
|
||||
<Note>
|
||||
Pokud vaše aplikace nedefinuje `aboutDescription` v `defineApplication()`, tržiště automaticky použije soubor `README.md` vašeho balíčku z npm jako obsah stránky O aplikaci. To znamená, že můžete spravovat jediný soubor README jak pro npm, tak pro tržiště Twenty. Pokud chcete v tržišti jiný popis, explicitně nastavte `aboutDescription`.
|
||||
</Note>
|
||||
|
||||
### Publikování pomocí CI
|
||||
|
||||
Vygenerovaný projekt obsahuje pracovní postup GitHub Actions, který publikuje při každém vydání:
|
||||
@@ -238,8 +242,8 @@ Začněte v režimu **Development** při sestavování své aplikace. Až bude p
|
||||
| --------------------------- | ----------------------------------------------------- | --------------------------------------------------- |
|
||||
| `yarn twenty build` | Sestaví aplikaci a vygeneruje manifest | `--tarball` — také vytvoří balíček `.tgz` |
|
||||
| `yarn twenty publish` | Sestaví a publikuje na npm | `--tag <tag>` — npm dist-tag (např. `beta`, `next`) |
|
||||
| `yarn twenty deploy` | Sestaví a nahraje tarball na server | `-r, --remote <name>` — cílový vzdálený server |
|
||||
| `yarn twenty catalog-sync` | Spustí synchronizaci katalogu tržiště na serveru | `-r, --remote <name>` — cílový vzdálený server |
|
||||
| `yarn twenty deploy` | Sestaví a nahraje tarball na server | `-r, --remote <name>` — cílový vzdálený repozitář |
|
||||
| `yarn twenty catalog-sync` | Spustí synchronizaci katalogu tržiště na serveru | `-r, --remote <name>` — cílový vzdálený repozitář |
|
||||
| `yarn twenty install` | Nainstaluje nasazenou aplikaci do pracovního prostoru | `-r, --remote <name>` — cílový vzdálený server |
|
||||
| `yarn twenty dev` | Sleduje a synchronizuje lokální změny | Používá výchozí vzdálený server |
|
||||
| `yarn twenty remote add` | Přidá připojení k serveru | `--url`, `--token`, `--as`, `--local`, `--port` |
|
||||
|
||||
@@ -38,7 +38,7 @@ yarn twenty dev
|
||||
|
||||
### Správa místního serveru
|
||||
|
||||
SDK obsahuje příkazy ke správě místního vývojového serveru Twenty (all-in-one obraz Dockeru s PostgreSQL, Redisem, serverem a workerem):
|
||||
SDK obsahuje příkazy ke správě místního vývojového serveru Twenty (all-in-one obraz Dockeru s PostgreSQL, Redisem, serverem a workerem na portu 2020). Tyto příkazy se vztahují pouze na vývojový server založený na Dockeru — nespravují instanci Twenty spuštěnou ze zdrojového kódu (např. `npx nx start twenty-server` na portu 3000):
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Spusťte místní server (v případě potřeby stáhne obraz)
|
||||
|
||||
+59
-24
@@ -55,11 +55,12 @@ export const main = async (
|
||||
params: { companyId: string },
|
||||
) => {
|
||||
const { companyId } = params;
|
||||
|
||||
// Replace with your Twenty GraphQL endpoint
|
||||
// Replace with your Twenty GraphQL endpoints (/metadata for metadata and files or /graphql for your records)
|
||||
// Cloud: https://api.twenty.com/graphql
|
||||
// Self-hosted: https://your-domain.com/graphql
|
||||
const graphqlEndpoint = 'https://api.twenty.com/graphql';
|
||||
|
||||
const metadataGraphqlEndpoint = 'https://api.twenty.com/metadata';
|
||||
const dataGraphqlEndpoint = 'https://api.twenty.com/graphql';
|
||||
|
||||
// Replace with your API key from Settings → APIs
|
||||
const authToken = 'YOUR_API_KEY';
|
||||
@@ -79,11 +80,40 @@ export const main = async (
|
||||
const pdfBlob = await pdfResponse.blob();
|
||||
const pdfFile = new File([pdfBlob], filename, { type: 'application/pdf' });
|
||||
|
||||
// Step 2: Upload the file via GraphQL multipart upload
|
||||
const fieldMetadataIdQuery = `
|
||||
query FindUploadFileFieldMetadataId {
|
||||
objects {
|
||||
edges {
|
||||
node {
|
||||
nameSingular
|
||||
fieldsList {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Step 2: Find a fieldMetadataId of "Attachment file" field in Attachments object with GraphQL API
|
||||
const response = await fetch(metadataGraphqlEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: {
|
||||
query: fieldMetadataIdQuery,
|
||||
}
|
||||
});
|
||||
const result = await response.json();
|
||||
const uploadFileFieldMetadataId = result.data.objects.edges.find(object => object.node.nameSingular === 'attachment').node.fieldsList.find(field => field.name === 'file').id;
|
||||
|
||||
// Step 3: Upload the file via GraphQL multipart upload
|
||||
const uploadMutation = `
|
||||
mutation UploadFile($file: Upload!, $fileFolder: FileFolder) {
|
||||
uploadFile(file: $file, fileFolder: $fileFolder) {
|
||||
path
|
||||
mutation UploadFilesFieldFile($file: Upload!, $fieldMetadataId: String!) {
|
||||
uploadFilesFieldFile(file: $file, fieldMetadataId: $fieldMetadataId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -91,12 +121,12 @@ export const main = async (
|
||||
const uploadForm = new FormData();
|
||||
uploadForm.append('operations', JSON.stringify({
|
||||
query: uploadMutation,
|
||||
variables: { file: null, fileFolder: 'Attachment' },
|
||||
variables: { file: null, fieldMetadataId: uploadFileFieldMetadataId },
|
||||
}));
|
||||
uploadForm.append('map', JSON.stringify({ '0': ['variables.file'] }));
|
||||
uploadForm.append('0', pdfFile);
|
||||
|
||||
const uploadResponse = await fetch(graphqlEndpoint, {
|
||||
const uploadResponse = await fetch(metadataGraphqlEndpoint, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${authToken}` },
|
||||
body: uploadForm,
|
||||
@@ -108,15 +138,15 @@ export const main = async (
|
||||
throw new Error(`Upload failed: ${uploadResult.errors[0].message}`);
|
||||
}
|
||||
|
||||
const filePath = uploadResult.data?.uploadFile?.path;
|
||||
const fileId = uploadResult.data?.uploadFilesFieldFile?.id;
|
||||
|
||||
if (!filePath) {
|
||||
throw new Error('No file path returned from upload');
|
||||
if (!fileId) {
|
||||
throw new Error('No file id returned from upload');
|
||||
}
|
||||
|
||||
// Step 3: Create the attachment linked to the company
|
||||
// Step 4: Create the attachment linked to the company
|
||||
const attachmentMutation = `
|
||||
mutation CreateAttachment($data: AttachmentCreateInput!) {
|
||||
mutation CreateOneAttachment($data: AttachmentCreateInput!) {
|
||||
createAttachment(data: $data) {
|
||||
id
|
||||
name
|
||||
@@ -124,7 +154,7 @@ export const main = async (
|
||||
}
|
||||
`;
|
||||
|
||||
const attachmentResponse = await fetch(graphqlEndpoint, {
|
||||
const attachmentResponse = await fetch(dataGraphqlEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
@@ -135,8 +165,13 @@ export const main = async (
|
||||
variables: {
|
||||
data: {
|
||||
name: filename,
|
||||
fullPath: filePath,
|
||||
companyId,
|
||||
targetCompanyId: companyId,
|
||||
file: [
|
||||
{
|
||||
fileId: fileId,
|
||||
label: filename
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -156,14 +191,14 @@ export const main = async (
|
||||
|
||||
#### Chcete-li připojit k jinému objektu
|
||||
|
||||
Nahraďte `companyId` příslušným polem:
|
||||
Nahraďte `targetCompanyId` příslušným polem:
|
||||
|
||||
| Objekt | Název pole |
|
||||
| -------------- | -------------------- |
|
||||
| Společnost | `companyId` |
|
||||
| Osoba | `personId` |
|
||||
| Příležitost | `opportunityId` |
|
||||
| Vlastní objekt | `yourCustomObjectId` |
|
||||
| Objekt | Název pole |
|
||||
| -------------- | -------------------------- |
|
||||
| Společnost | `targetCompanyId` |
|
||||
| Osoba | `targetPersonId` |
|
||||
| Příležitost | `targetOpportunityId` |
|
||||
| Vlastní objekt | `targetYourCustomObjectId` |
|
||||
|
||||
Aktualizujte jak parametr funkce, tak objekt `variables.data` v mutaci přílohy.
|
||||
|
||||
|
||||
@@ -358,17 +358,17 @@ Notizen:
|
||||
|
||||
Wenn Sie planen, [Ihre App zu veröffentlichen](/l/de/developers/extend/apps/publishing), steuern diese optionalen Felder, wie Ihre App im Marktplatz erscheint:
|
||||
|
||||
| Feld | Beschreibung |
|
||||
| ------------------ | ---------------------------------------------------- |
|
||||
| `author` | Name des Autors oder des Unternehmens |
|
||||
| `category` | App-Kategorie für die Filterung im Marktplatz |
|
||||
| `logoUrl` | Pfad zu Ihrem App-Logo (relativ zu `./assets/`) |
|
||||
| `screenshots` | Array von Screenshot-Pfaden (relativ zu `./assets/`) |
|
||||
| `aboutDescription` | Längere Markdown-Beschreibung für den Tab "Info" |
|
||||
| `websiteUrl` | Link zu Ihrer Website |
|
||||
| `termsUrl` | Link zu den Nutzungsbedingungen |
|
||||
| `emailSupport` | Support-E-Mail-Adresse |
|
||||
| `issueReportUrl` | Link zum Issue-Tracker |
|
||||
| Feld | Beschreibung |
|
||||
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `author` | Name des Autors oder des Unternehmens |
|
||||
| `category` | App-Kategorie für die Filterung im Marktplatz |
|
||||
| `logoUrl` | Pfad zu Ihrem App-Logo (relativ zu `./assets/`) |
|
||||
| `screenshots` | Array von Screenshot-Pfaden (relativ zu `./assets/`) |
|
||||
| `aboutDescription` | Längere Markdown-Beschreibung für den Tab "Info". Wenn weggelassen, verwendet der Marketplace die `README.md` des Pakets von npm |
|
||||
| `websiteUrl` | Link zu Ihrer Website |
|
||||
| `termsUrl` | Link zu den Nutzungsbedingungen |
|
||||
| `emailSupport` | Support-E-Mail-Adresse |
|
||||
| `issueReportUrl` | Link zum Issue-Tracker |
|
||||
|
||||
#### Rollen und Berechtigungen
|
||||
|
||||
|
||||
@@ -9,18 +9,19 @@ Apps befinden sich derzeit in der Alpha-Testphase. Die Funktion ist funktionsfä
|
||||
|
||||
Apps ermöglichen es Ihnen, Twenty mit benutzerdefinierten Objekten, Feldern, Logikfunktionen, KI-Fähigkeiten und UI-Komponenten zu erweitern — alles als Code verwaltet.
|
||||
|
||||
**Was Sie heute tun können:**
|
||||
**Was Sie erstellen können:**
|
||||
|
||||
* Benutzerdefinierte Objekte und Felder als Code definieren (verwaltetes Datenmodell)
|
||||
* Erstellen Sie Logikfunktionen mit benutzerdefinierten Triggern (HTTP-Routen, cron, Datenbankereignisse)
|
||||
* Fähigkeiten für KI-Agenten definieren
|
||||
* Erstellen Sie Frontend-Komponenten, die in der Twenty-UI gerendert werden
|
||||
* Dieselbe App in mehreren Workspaces bereitstellen
|
||||
* Benutzerdefinierte Objekte, Felder, Ansichten und Navigationselemente, um Ihr Datenmodell zu gestalten
|
||||
* Logikfunktionen, die durch HTTP-Routen, Cron-Zeitpläne oder Datenbankereignisse ausgelöst werden
|
||||
* Frontend-Komponenten, die direkt innerhalb der Twenty-UI gerendert werden
|
||||
* Skills, die die KI-Agenten von Twenty erweitern
|
||||
* Eine App in mehreren Workspaces bereitstellen
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
* Node.js 24+ und Yarn 4
|
||||
* Docker (für den lokalen Twenty-Dev-Server)
|
||||
* Node.js 24+
|
||||
* Yarn 4
|
||||
* Docker (oder eine lokal laufende Twenty-Instanz)
|
||||
|
||||
## Erste Schritte
|
||||
|
||||
@@ -29,21 +30,9 @@ Erstellen Sie mit dem offiziellen Scaffolder eine neue App, authentifizieren Sie
|
||||
```bash filename="Terminal"
|
||||
# Scaffold a new app (includes all examples by default)
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# Start dev mode: automatically syncs local changes to your workspace
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
Das Scaffolding-Tool unterstützt zwei Modi, um zu steuern, welche Beispieldateien enthalten sind:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimal: only core files (application-config.ts and default-role.ts)
|
||||
npx create-twenty-app@latest my-app --minimal
|
||||
```
|
||||
> Verwenden Sie die Option `--minimal`, um eine minimale Installation zu erstellen
|
||||
|
||||
Von hier aus können Sie:
|
||||
|
||||
|
||||
@@ -102,6 +102,10 @@ yarn twenty catalog-sync -r production
|
||||
|
||||
Die im Marktplatz angezeigten Metadaten stammen aus Ihrem `defineApplication()`-Aufruf im Quellcode Ihrer App — Felder wie `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl` und `termsUrl`.
|
||||
|
||||
<Note>
|
||||
Wenn deine App keine `aboutDescription` in `defineApplication()` definiert, verwendet der Marktplatz automatisch die `README.md` deines Pakets von npm als Inhalt der Über-uns-Seite. Das bedeutet, dass du eine einzige README sowohl für npm als auch für den Twenty-Marktplatz pflegen kannst. Wenn du im Marktplatz eine andere Beschreibung möchtest, setze `aboutDescription` explizit.
|
||||
</Note>
|
||||
|
||||
### CI-Veröffentlichung
|
||||
|
||||
Das vorgefertigte Projekt enthält einen GitHub-Actions-Workflow, der bei jedem Release eine Veröffentlichung durchführt:
|
||||
|
||||
@@ -38,7 +38,7 @@ yarn twenty dev
|
||||
|
||||
### Lokale Serververwaltung
|
||||
|
||||
Das SDK enthält Befehle zur Verwaltung eines lokalen Twenty-Dev-Servers (All-in-One-Docker-Image mit PostgreSQL, Redis, Server und Worker):
|
||||
Das SDK enthält Befehle zur Verwaltung eines lokalen Twenty-Dev-Servers (All-in-One-Docker-Image mit PostgreSQL, Redis, Server und Worker auf Port 2020). Diese Befehle gelten nur für den Docker-basierten Dev-Server — sie verwalten keine aus dem Quellcode gestartete Twenty-Instanz (z. B. `npx nx start twenty-server` auf Port 3000):
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Den lokalen Server starten (lädt das Image bei Bedarf herunter)
|
||||
|
||||
+59
-24
@@ -55,11 +55,12 @@ export const main = async (
|
||||
params: { companyId: string },
|
||||
) => {
|
||||
const { companyId } = params;
|
||||
|
||||
// Replace with your Twenty GraphQL endpoint
|
||||
// Replace with your Twenty GraphQL endpoints (/metadata for metadata and files or /graphql for your records)
|
||||
// Cloud: https://api.twenty.com/graphql
|
||||
// Self-hosted: https://your-domain.com/graphql
|
||||
const graphqlEndpoint = 'https://api.twenty.com/graphql';
|
||||
|
||||
const metadataGraphqlEndpoint = 'https://api.twenty.com/metadata';
|
||||
const dataGraphqlEndpoint = 'https://api.twenty.com/graphql';
|
||||
|
||||
// Replace with your API key from Settings → APIs
|
||||
const authToken = 'YOUR_API_KEY';
|
||||
@@ -79,11 +80,40 @@ export const main = async (
|
||||
const pdfBlob = await pdfResponse.blob();
|
||||
const pdfFile = new File([pdfBlob], filename, { type: 'application/pdf' });
|
||||
|
||||
// Step 2: Upload the file via GraphQL multipart upload
|
||||
const fieldMetadataIdQuery = `
|
||||
query FindUploadFileFieldMetadataId {
|
||||
objects {
|
||||
edges {
|
||||
node {
|
||||
nameSingular
|
||||
fieldsList {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Step 2: Find a fieldMetadataId of "Attachment file" field in Attachments object with GraphQL API
|
||||
const response = await fetch(metadataGraphqlEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: {
|
||||
query: fieldMetadataIdQuery,
|
||||
}
|
||||
});
|
||||
const result = await response.json();
|
||||
const uploadFileFieldMetadataId = result.data.objects.edges.find(object => object.node.nameSingular === 'attachment').node.fieldsList.find(field => field.name === 'file').id;
|
||||
|
||||
// Step 3: Upload the file via GraphQL multipart upload
|
||||
const uploadMutation = `
|
||||
mutation UploadFile($file: Upload!, $fileFolder: FileFolder) {
|
||||
uploadFile(file: $file, fileFolder: $fileFolder) {
|
||||
path
|
||||
mutation UploadFilesFieldFile($file: Upload!, $fieldMetadataId: String!) {
|
||||
uploadFilesFieldFile(file: $file, fieldMetadataId: $fieldMetadataId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -91,12 +121,12 @@ export const main = async (
|
||||
const uploadForm = new FormData();
|
||||
uploadForm.append('operations', JSON.stringify({
|
||||
query: uploadMutation,
|
||||
variables: { file: null, fileFolder: 'Attachment' },
|
||||
variables: { file: null, fieldMetadataId: uploadFileFieldMetadataId },
|
||||
}));
|
||||
uploadForm.append('map', JSON.stringify({ '0': ['variables.file'] }));
|
||||
uploadForm.append('0', pdfFile);
|
||||
|
||||
const uploadResponse = await fetch(graphqlEndpoint, {
|
||||
const uploadResponse = await fetch(metadataGraphqlEndpoint, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${authToken}` },
|
||||
body: uploadForm,
|
||||
@@ -108,15 +138,15 @@ export const main = async (
|
||||
throw new Error(`Upload failed: ${uploadResult.errors[0].message}`);
|
||||
}
|
||||
|
||||
const filePath = uploadResult.data?.uploadFile?.path;
|
||||
const fileId = uploadResult.data?.uploadFilesFieldFile?.id;
|
||||
|
||||
if (!filePath) {
|
||||
throw new Error('No file path returned from upload');
|
||||
if (!fileId) {
|
||||
throw new Error('No file id returned from upload');
|
||||
}
|
||||
|
||||
// Step 3: Create the attachment linked to the company
|
||||
// Step 4: Create the attachment linked to the company
|
||||
const attachmentMutation = `
|
||||
mutation CreateAttachment($data: AttachmentCreateInput!) {
|
||||
mutation CreateOneAttachment($data: AttachmentCreateInput!) {
|
||||
createAttachment(data: $data) {
|
||||
id
|
||||
name
|
||||
@@ -124,7 +154,7 @@ export const main = async (
|
||||
}
|
||||
`;
|
||||
|
||||
const attachmentResponse = await fetch(graphqlEndpoint, {
|
||||
const attachmentResponse = await fetch(dataGraphqlEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
@@ -135,8 +165,13 @@ export const main = async (
|
||||
variables: {
|
||||
data: {
|
||||
name: filename,
|
||||
fullPath: filePath,
|
||||
companyId,
|
||||
targetCompanyId: companyId,
|
||||
file: [
|
||||
{
|
||||
fileId: fileId,
|
||||
label: filename
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -156,14 +191,14 @@ export const main = async (
|
||||
|
||||
#### An ein anderes Objekt anhängen
|
||||
|
||||
Ersetzen Sie `companyId` durch das entsprechende Feld:
|
||||
Ersetzen Sie `targetCompanyId` durch das entsprechende Feld:
|
||||
|
||||
| Objekt | Feldname |
|
||||
| -------------------------- | -------------------- |
|
||||
| Unternehmen | `companyId` |
|
||||
| Person | `personId` |
|
||||
| Opportunity | `opportunityId` |
|
||||
| Benutzerdefiniertes Objekt | `yourCustomObjectId` |
|
||||
| Objekt | Feldname |
|
||||
| -------------------------- | -------------------------- |
|
||||
| Unternehmen | `targetCompanyId` |
|
||||
| Person | `targetPersonId` |
|
||||
| Opportunity | `targetOpportunityId` |
|
||||
| Benutzerdefiniertes Objekt | `targetYourCustomObjectId` |
|
||||
|
||||
Aktualisieren Sie sowohl den Funktionsparameter als auch das Objekt `variables.data` in der Attachment-Mutation.
|
||||
|
||||
|
||||
@@ -358,17 +358,17 @@ Note:
|
||||
|
||||
Se prevedi di [pubblicare la tua app](/l/it/developers/extend/apps/publishing), questi campi opzionali controllano come la tua app appare nel marketplace:
|
||||
|
||||
| Campo | Descrizione |
|
||||
| ------------------ | ----------------------------------------------------------- |
|
||||
| `autore` | Nome dell'autore o dell'azienda |
|
||||
| `categoria` | Categoria dell'app per il filtraggio nel marketplace |
|
||||
| `logoUrl` | Percorso del logo della tua app (relativo a `./assets/`) |
|
||||
| `screenshots` | Array di percorsi degli screenshot (relativi a `./assets/`) |
|
||||
| `aboutDescription` | Descrizione markdown più lunga per la scheda "Informazioni" |
|
||||
| `websiteUrl` | Link al tuo sito web |
|
||||
| `termsUrl` | Link ai Termini di servizio |
|
||||
| `emailSupport` | Indirizzo email di supporto |
|
||||
| `issueReportUrl` | Link al sistema di tracciamento dei problemi |
|
||||
| Campo | Descrizione |
|
||||
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `autore` | Nome dell'autore o dell'azienda |
|
||||
| `categoria` | Categoria dell'app per il filtraggio nel marketplace |
|
||||
| `logoUrl` | Percorso del logo della tua app (relativo a `./assets/`) |
|
||||
| `screenshots` | Array di percorsi degli screenshot (relativi a `./assets/`) |
|
||||
| `aboutDescription` | Descrizione markdown più lunga per la scheda "Informazioni". Se omesso, il marketplace utilizza il `README.md` del pacchetto da npm |
|
||||
| `websiteUrl` | Link al tuo sito web |
|
||||
| `termsUrl` | Link ai Termini di servizio |
|
||||
| `emailSupport` | Indirizzo email di supporto |
|
||||
| `issueReportUrl` | Link al sistema di tracciamento dei problemi |
|
||||
|
||||
#### Ruoli e permessi
|
||||
|
||||
|
||||
@@ -9,18 +9,19 @@ Le app sono attualmente in fase alfa. La funzionalità è funzionante ma ancora
|
||||
|
||||
Le app ti permettono di estendere Twenty con oggetti, campi, funzioni logiche, competenze IA e componenti UI personalizzati — il tutto gestito come codice.
|
||||
|
||||
**Cosa puoi fare oggi:**
|
||||
**Cosa puoi creare:**
|
||||
|
||||
* Definisci oggetti e campi personalizzati come codice (modello dati gestito)
|
||||
* Crea funzioni logiche con trigger personalizzati (route HTTP, cron, eventi del database)
|
||||
* Definisci le abilità per gli agenti IA
|
||||
* Crea componenti front-end che vengono renderizzati all'interno della UI di Twenty
|
||||
* Distribuisci la stessa app su più spazi di lavoro
|
||||
* Oggetti, campi, viste ed elementi di navigazione personalizzati per definire il tuo modello di dati
|
||||
* Funzioni logiche attivate da route HTTP, pianificazioni cron o eventi del database
|
||||
* Componenti front-end che vengono renderizzati direttamente all'interno della UI di Twenty
|
||||
* Abilità che estendono gli agenti IA di Twenty
|
||||
* Distribuisci un'app su più spazi di lavoro
|
||||
|
||||
## Prerequisiti
|
||||
|
||||
* Node.js 24+ e Yarn 4
|
||||
* Docker (per il server di sviluppo locale di Twenty)
|
||||
* Node.js 24+
|
||||
* Yarn 4
|
||||
* Docker (o un'istanza locale di Twenty in esecuzione)
|
||||
|
||||
## Per iniziare
|
||||
|
||||
@@ -29,21 +30,9 @@ Crea una nuova app utilizzando lo scaffolder ufficiale, quindi autenticati e ini
|
||||
```bash filename="Terminal"
|
||||
# Scaffold a new app (includes all examples by default)
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# Start dev mode: automatically syncs local changes to your workspace
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
Lo strumento di scaffolding supporta due modalità per controllare quali file di esempio vengono inclusi:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimal: only core files (application-config.ts and default-role.ts)
|
||||
npx create-twenty-app@latest my-app --minimal
|
||||
```
|
||||
> Usa l'opzione `--minimal` per creare un'installazione minima
|
||||
|
||||
Da qui puoi:
|
||||
|
||||
|
||||
@@ -102,6 +102,10 @@ yarn twenty catalog-sync -r production
|
||||
|
||||
I metadati mostrati nel marketplace provengono dalla chiamata a `defineApplication()` nel codice sorgente della tua app — campi come `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl` e `termsUrl`.
|
||||
|
||||
<Note>
|
||||
Se la tua app non definisce un `aboutDescription` in `defineApplication()`, il marketplace userà automaticamente il `README.md` del tuo pacchetto su npm come contenuto della pagina Informazioni. Questo significa che puoi mantenere un unico README sia per npm sia per il marketplace di Twenty. Se desideri una descrizione diversa nel marketplace, imposta esplicitamente `aboutDescription`.
|
||||
</Note>
|
||||
|
||||
### Pubblicazione con CI
|
||||
|
||||
Il progetto generato include un workflow di GitHub Actions che pubblica a ogni release:
|
||||
|
||||
@@ -38,7 +38,7 @@ yarn twenty dev
|
||||
|
||||
### Gestione del server locale
|
||||
|
||||
L'SDK include comandi per gestire un server di sviluppo locale di Twenty (immagine Docker all-in-one con PostgreSQL, Redis, server e worker):
|
||||
L'SDK include comandi per gestire un server di sviluppo locale di Twenty (immagine Docker all-in-one con PostgreSQL, Redis, server e worker sulla porta 2020). Questi comandi si applicano solo al server di sviluppo basato su Docker — non gestiscono un'istanza di Twenty avviata dal sorgente (ad es. `npx nx start twenty-server` sulla porta 3000):
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Avvia il server locale (scarica l'immagine se necessario)
|
||||
|
||||
+59
-24
@@ -55,11 +55,12 @@ export const main = async (
|
||||
params: { companyId: string },
|
||||
) => {
|
||||
const { companyId } = params;
|
||||
|
||||
// Replace with your Twenty GraphQL endpoint
|
||||
// Replace with your Twenty GraphQL endpoints (/metadata for metadata and files or /graphql for your records)
|
||||
// Cloud: https://api.twenty.com/graphql
|
||||
// Self-hosted: https://your-domain.com/graphql
|
||||
const graphqlEndpoint = 'https://api.twenty.com/graphql';
|
||||
|
||||
const metadataGraphqlEndpoint = 'https://api.twenty.com/metadata';
|
||||
const dataGraphqlEndpoint = 'https://api.twenty.com/graphql';
|
||||
|
||||
// Replace with your API key from Settings → APIs
|
||||
const authToken = 'YOUR_API_KEY';
|
||||
@@ -79,11 +80,40 @@ export const main = async (
|
||||
const pdfBlob = await pdfResponse.blob();
|
||||
const pdfFile = new File([pdfBlob], filename, { type: 'application/pdf' });
|
||||
|
||||
// Step 2: Upload the file via GraphQL multipart upload
|
||||
const fieldMetadataIdQuery = `
|
||||
query FindUploadFileFieldMetadataId {
|
||||
objects {
|
||||
edges {
|
||||
node {
|
||||
nameSingular
|
||||
fieldsList {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Step 2: Find a fieldMetadataId of "Attachment file" field in Attachments object with GraphQL API
|
||||
const response = await fetch(metadataGraphqlEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: {
|
||||
query: fieldMetadataIdQuery,
|
||||
}
|
||||
});
|
||||
const result = await response.json();
|
||||
const uploadFileFieldMetadataId = result.data.objects.edges.find(object => object.node.nameSingular === 'attachment').node.fieldsList.find(field => field.name === 'file').id;
|
||||
|
||||
// Step 3: Upload the file via GraphQL multipart upload
|
||||
const uploadMutation = `
|
||||
mutation UploadFile($file: Upload!, $fileFolder: FileFolder) {
|
||||
uploadFile(file: $file, fileFolder: $fileFolder) {
|
||||
path
|
||||
mutation UploadFilesFieldFile($file: Upload!, $fieldMetadataId: String!) {
|
||||
uploadFilesFieldFile(file: $file, fieldMetadataId: $fieldMetadataId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -91,12 +121,12 @@ export const main = async (
|
||||
const uploadForm = new FormData();
|
||||
uploadForm.append('operations', JSON.stringify({
|
||||
query: uploadMutation,
|
||||
variables: { file: null, fileFolder: 'Attachment' },
|
||||
variables: { file: null, fieldMetadataId: uploadFileFieldMetadataId },
|
||||
}));
|
||||
uploadForm.append('map', JSON.stringify({ '0': ['variables.file'] }));
|
||||
uploadForm.append('0', pdfFile);
|
||||
|
||||
const uploadResponse = await fetch(graphqlEndpoint, {
|
||||
const uploadResponse = await fetch(metadataGraphqlEndpoint, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${authToken}` },
|
||||
body: uploadForm,
|
||||
@@ -108,15 +138,15 @@ export const main = async (
|
||||
throw new Error(`Upload failed: ${uploadResult.errors[0].message}`);
|
||||
}
|
||||
|
||||
const filePath = uploadResult.data?.uploadFile?.path;
|
||||
const fileId = uploadResult.data?.uploadFilesFieldFile?.id;
|
||||
|
||||
if (!filePath) {
|
||||
throw new Error('No file path returned from upload');
|
||||
if (!fileId) {
|
||||
throw new Error('No file id returned from upload');
|
||||
}
|
||||
|
||||
// Step 3: Create the attachment linked to the company
|
||||
// Step 4: Create the attachment linked to the company
|
||||
const attachmentMutation = `
|
||||
mutation CreateAttachment($data: AttachmentCreateInput!) {
|
||||
mutation CreateOneAttachment($data: AttachmentCreateInput!) {
|
||||
createAttachment(data: $data) {
|
||||
id
|
||||
name
|
||||
@@ -124,7 +154,7 @@ export const main = async (
|
||||
}
|
||||
`;
|
||||
|
||||
const attachmentResponse = await fetch(graphqlEndpoint, {
|
||||
const attachmentResponse = await fetch(dataGraphqlEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
@@ -135,8 +165,13 @@ export const main = async (
|
||||
variables: {
|
||||
data: {
|
||||
name: filename,
|
||||
fullPath: filePath,
|
||||
companyId,
|
||||
targetCompanyId: companyId,
|
||||
file: [
|
||||
{
|
||||
fileId: fileId,
|
||||
label: filename
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -156,14 +191,14 @@ export const main = async (
|
||||
|
||||
#### Per allegare a un oggetto diverso
|
||||
|
||||
Sostituisci `companyId` con il campo appropriato:
|
||||
Sostituisci `targetCompanyId` con il campo appropriato:
|
||||
|
||||
| Oggetto | Nome del campo |
|
||||
| ---------------------- | -------------------- |
|
||||
| Azienda | `companyId` |
|
||||
| Persona | `personId` |
|
||||
| Opportunità | `opportunityId` |
|
||||
| Oggetto personalizzato | `yourCustomObjectId` |
|
||||
| Oggetto | Nome del campo |
|
||||
| ---------------------- | -------------------------- |
|
||||
| Azienda | `targetCompanyId` |
|
||||
| Persona | `targetPersonId` |
|
||||
| Opportunità | `targetOpportunityId` |
|
||||
| Oggetto personalizzato | `targetYourCustomObjectId` |
|
||||
|
||||
Aggiorna sia il parametro della funzione sia l'oggetto `variables.data` nella mutation dell'allegato.
|
||||
|
||||
|
||||
@@ -358,17 +358,17 @@ Notițe:
|
||||
|
||||
Dacă intenționați să [publicați aplicația](/l/ro/developers/extend/apps/publishing), aceste câmpuri opționale controlează modul în care aplicația apare în marketplace:
|
||||
|
||||
| Câmp | Descriere |
|
||||
| ------------------ | ------------------------------------------------------------- |
|
||||
| `autor` | Numele autorului sau al companiei |
|
||||
| `categorie` | Categoria aplicației pentru filtrarea în marketplace |
|
||||
| `logoUrl` | Calea către logo-ul aplicației (relativă la `./assets/`) |
|
||||
| `screenshots` | Listă de căi către capturi de ecran (relative la `./assets/`) |
|
||||
| `aboutDescription` | Descriere markdown mai lungă pentru fila "About" |
|
||||
| `websiteUrl` | Link către site-ul dvs. |
|
||||
| `termsUrl` | Link către termenii de serviciu |
|
||||
| `emailSupport` | Adresă de e-mail pentru suport |
|
||||
| `issueReportUrl` | Link către sistemul de urmărire a problemelor |
|
||||
| Câmp | Descriere |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `autor` | Numele autorului sau al companiei |
|
||||
| `categorie` | Categoria aplicației pentru filtrarea în marketplace |
|
||||
| `logoUrl` | Calea către logo-ul aplicației (relativă la `./assets/`) |
|
||||
| `screenshots` | Listă de căi către capturi de ecran (relative la `./assets/`) |
|
||||
| `aboutDescription` | Descriere markdown mai lungă pentru fila "About". Dacă este omis, marketplace-ul folosește `README.md` al pachetului de pe npm |
|
||||
| `websiteUrl` | Link către site-ul dvs. |
|
||||
| `termsUrl` | Link către termenii de serviciu |
|
||||
| `emailSupport` | Adresă de e-mail pentru suport |
|
||||
| `issueReportUrl` | Link către sistemul de urmărire a problemelor |
|
||||
|
||||
#### Roluri și permisiuni
|
||||
|
||||
|
||||
@@ -9,18 +9,19 @@ Aplicațiile sunt în prezent în testare alfa. Caracteristica funcționează, d
|
||||
|
||||
Aplicațiile vă permit să extindeți Twenty cu obiecte personalizate, câmpuri, funcții logice, abilități IA și componente UI — toate gestionate ca cod.
|
||||
|
||||
**Ce puteți face astăzi:**
|
||||
**Ce puteți construi:**
|
||||
|
||||
* Definiți obiecte și câmpuri personalizate sub formă de cod (model de date gestionat)
|
||||
* Construiți funcții logice cu declanșatoare personalizate (rute HTTP, cron, evenimente ale bazei de date)
|
||||
* Definiți abilități pentru agenți de IA
|
||||
* Construiți componente front-end care se afișează în interfața Twenty
|
||||
* Implementați aceeași aplicație în mai multe spații de lucru
|
||||
* Obiecte, câmpuri, vizualizări și elemente de navigare personalizate pentru a defini modelul dumneavoastră de date
|
||||
* Funcții logice declanșate de rute HTTP, programări cron sau evenimente din baza de date
|
||||
* Componente front-end care se afișează direct în interfața Twenty
|
||||
* Abilități care extind capabilitățile agenților AI ai Twenty
|
||||
* Implementați o aplicație în mai multe spații de lucru
|
||||
|
||||
## Cerințe
|
||||
|
||||
* Node.js 24+ și Yarn 4
|
||||
* Docker (pentru serverul local de dezvoltare Twenty)
|
||||
* Node.js 24+
|
||||
* Yarn 4
|
||||
* Docker (sau o instanță Twenty locală în execuție)
|
||||
|
||||
## Începeți
|
||||
|
||||
@@ -29,21 +30,9 @@ Creați o aplicație nouă folosind generatorul oficial, apoi autentificați-vă
|
||||
```bash filename="Terminal"
|
||||
# Scaffold a new app (includes all examples by default)
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# Start dev mode: automatically syncs local changes to your workspace
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
Generatorul de schelet acceptă două moduri pentru a controla ce fișiere de exemplu sunt incluse:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimal: only core files (application-config.ts and default-role.ts)
|
||||
npx create-twenty-app@latest my-app --minimal
|
||||
```
|
||||
> Folosiți opțiunea `--minimal` pentru a genera o instalare minimă
|
||||
|
||||
De aici puteți:
|
||||
|
||||
|
||||
@@ -102,6 +102,10 @@ yarn twenty catalog-sync -r production
|
||||
|
||||
Metadatele afișate în marketplace provin din apelul tău `defineApplication()` din codul sursă al aplicației — câmpuri precum `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl` și `termsUrl`.
|
||||
|
||||
<Note>
|
||||
Dacă aplicația ta nu definește un `aboutDescription` în `defineApplication()`, piața va folosi automat fișierul `README.md` al pachetului tău de pe npm drept conținut pentru pagina Despre. Acest lucru înseamnă că poți menține un singur README atât pentru npm, cât și pentru piața Twenty. Dacă vrei o descriere diferită în piață, setează explicit `aboutDescription`.
|
||||
</Note>
|
||||
|
||||
### Publicare CI
|
||||
|
||||
Proiectul generat include un workflow GitHub Actions care publică la fiecare lansare:
|
||||
|
||||
@@ -38,7 +38,7 @@ yarn twenty dev
|
||||
|
||||
### Gestionarea serverului local
|
||||
|
||||
SDK-ul include comenzi pentru a gestiona un server local de dezvoltare Twenty (imagine Docker all-in-one cu PostgreSQL, Redis, server și worker):
|
||||
SDK-ul include comenzi pentru a gestiona un server local de dezvoltare Twenty (imagine Docker all-in-one cu PostgreSQL, Redis, server și worker pe portul 2020). Aceste comenzi se aplică doar serverului de dezvoltare bazat pe Docker — nu gestionează o instanță Twenty pornită din sursă (de exemplu, `npx nx start twenty-server` pe portul 3000):
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Pornește serverul local (descarcă imaginea dacă este necesar)
|
||||
|
||||
+59
-24
@@ -55,11 +55,12 @@ export const main = async (
|
||||
params: { companyId: string },
|
||||
) => {
|
||||
const { companyId } = params;
|
||||
|
||||
// Replace with your Twenty GraphQL endpoint
|
||||
// Replace with your Twenty GraphQL endpoints (/metadata for metadata and files or /graphql for your records)
|
||||
// Cloud: https://api.twenty.com/graphql
|
||||
// Self-hosted: https://your-domain.com/graphql
|
||||
const graphqlEndpoint = 'https://api.twenty.com/graphql';
|
||||
|
||||
const metadataGraphqlEndpoint = 'https://api.twenty.com/metadata';
|
||||
const dataGraphqlEndpoint = 'https://api.twenty.com/graphql';
|
||||
|
||||
// Replace with your API key from Settings → APIs
|
||||
const authToken = 'YOUR_API_KEY';
|
||||
@@ -79,11 +80,40 @@ export const main = async (
|
||||
const pdfBlob = await pdfResponse.blob();
|
||||
const pdfFile = new File([pdfBlob], filename, { type: 'application/pdf' });
|
||||
|
||||
// Step 2: Upload the file via GraphQL multipart upload
|
||||
const fieldMetadataIdQuery = `
|
||||
query FindUploadFileFieldMetadataId {
|
||||
objects {
|
||||
edges {
|
||||
node {
|
||||
nameSingular
|
||||
fieldsList {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Step 2: Find a fieldMetadataId of "Attachment file" field in Attachments object with GraphQL API
|
||||
const response = await fetch(metadataGraphqlEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: {
|
||||
query: fieldMetadataIdQuery,
|
||||
}
|
||||
});
|
||||
const result = await response.json();
|
||||
const uploadFileFieldMetadataId = result.data.objects.edges.find(object => object.node.nameSingular === 'attachment').node.fieldsList.find(field => field.name === 'file').id;
|
||||
|
||||
// Step 3: Upload the file via GraphQL multipart upload
|
||||
const uploadMutation = `
|
||||
mutation UploadFile($file: Upload!, $fileFolder: FileFolder) {
|
||||
uploadFile(file: $file, fileFolder: $fileFolder) {
|
||||
path
|
||||
mutation UploadFilesFieldFile($file: Upload!, $fieldMetadataId: String!) {
|
||||
uploadFilesFieldFile(file: $file, fieldMetadataId: $fieldMetadataId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -91,12 +121,12 @@ export const main = async (
|
||||
const uploadForm = new FormData();
|
||||
uploadForm.append('operations', JSON.stringify({
|
||||
query: uploadMutation,
|
||||
variables: { file: null, fileFolder: 'Attachment' },
|
||||
variables: { file: null, fieldMetadataId: uploadFileFieldMetadataId },
|
||||
}));
|
||||
uploadForm.append('map', JSON.stringify({ '0': ['variables.file'] }));
|
||||
uploadForm.append('0', pdfFile);
|
||||
|
||||
const uploadResponse = await fetch(graphqlEndpoint, {
|
||||
const uploadResponse = await fetch(metadataGraphqlEndpoint, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${authToken}` },
|
||||
body: uploadForm,
|
||||
@@ -108,15 +138,15 @@ export const main = async (
|
||||
throw new Error(`Upload failed: ${uploadResult.errors[0].message}`);
|
||||
}
|
||||
|
||||
const filePath = uploadResult.data?.uploadFile?.path;
|
||||
const fileId = uploadResult.data?.uploadFilesFieldFile?.id;
|
||||
|
||||
if (!filePath) {
|
||||
throw new Error('No file path returned from upload');
|
||||
if (!fileId) {
|
||||
throw new Error('No file id returned from upload');
|
||||
}
|
||||
|
||||
// Step 3: Create the attachment linked to the company
|
||||
// Step 4: Create the attachment linked to the company
|
||||
const attachmentMutation = `
|
||||
mutation CreateAttachment($data: AttachmentCreateInput!) {
|
||||
mutation CreateOneAttachment($data: AttachmentCreateInput!) {
|
||||
createAttachment(data: $data) {
|
||||
id
|
||||
name
|
||||
@@ -124,7 +154,7 @@ export const main = async (
|
||||
}
|
||||
`;
|
||||
|
||||
const attachmentResponse = await fetch(graphqlEndpoint, {
|
||||
const attachmentResponse = await fetch(dataGraphqlEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
@@ -135,8 +165,13 @@ export const main = async (
|
||||
variables: {
|
||||
data: {
|
||||
name: filename,
|
||||
fullPath: filePath,
|
||||
companyId,
|
||||
targetCompanyId: companyId,
|
||||
file: [
|
||||
{
|
||||
fileId: fileId,
|
||||
label: filename
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -156,14 +191,14 @@ export const main = async (
|
||||
|
||||
#### Pentru a atașa la un alt obiect
|
||||
|
||||
Înlocuiți `companyId` cu câmpul corespunzător:
|
||||
Înlocuiți `targetCompanyId` cu câmpul corespunzător:
|
||||
|
||||
| Obiect | Nume câmp |
|
||||
| ------------------- | -------------------- |
|
||||
| Companie | `companyId` |
|
||||
| Persoană | `personId` |
|
||||
| Oportunitate | `opportunityId` |
|
||||
| Obiect personalizat | `yourCustomObjectId` |
|
||||
| Obiect | Nume câmp |
|
||||
| ------------------- | -------------------------- |
|
||||
| Companie | `targetCompanyId` |
|
||||
| Persoană | `targetPersonId` |
|
||||
| Oportunitate | `targetOpportunityId` |
|
||||
| Obiect personalizat | `targetYourCustomObjectId` |
|
||||
|
||||
Actualizați atât parametrul funcției, cât și obiectul `variables.data` în mutația de atașare.
|
||||
|
||||
|
||||
@@ -358,17 +358,17 @@ export default defineApplication({
|
||||
|
||||
Если вы планируете [опубликовать приложение](/l/ru/developers/extend/apps/publishing), эти необязательные поля определяют, как ваше приложение отображается в маркетплейсе:
|
||||
|
||||
| Поле | Описание |
|
||||
| ------------------ | ------------------------------------------------------------ |
|
||||
| `author` | Имя автора или название компании |
|
||||
| `category` | Категория приложения для фильтрации в маркетплейсе |
|
||||
| `logoUrl` | Путь к логотипу вашего приложения (относительно `./assets/`) |
|
||||
| `screenshots` | Массив путей к скриншотам (относительно `./assets/`) |
|
||||
| `aboutDescription` | Расширенное описание в Markdown для вкладки "About" |
|
||||
| `websiteUrl` | Ссылка на ваш сайт |
|
||||
| `termsUrl` | Ссылка на условия предоставления услуг |
|
||||
| `emailSupport` | Адрес электронной почты поддержки |
|
||||
| `issueReportUrl` | Ссылка на систему отслеживания проблем |
|
||||
| Поле | Описание |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| `author` | Имя автора или название компании |
|
||||
| `category` | Категория приложения для фильтрации в маркетплейсе |
|
||||
| `logoUrl` | Путь к логотипу вашего приложения (относительно `./assets/`) |
|
||||
| `screenshots` | Массив путей к скриншотам (относительно `./assets/`) |
|
||||
| `aboutDescription` | Расширенное описание в Markdown для вкладки "About". Если опущено, маркетплейс использует `README.md` пакета из npm |
|
||||
| `websiteUrl` | Ссылка на ваш сайт |
|
||||
| `termsUrl` | Ссылка на условия предоставления услуг |
|
||||
| `emailSupport` | Адрес электронной почты поддержки |
|
||||
| `issueReportUrl` | Ссылка на систему отслеживания проблем |
|
||||
|
||||
#### Роли и разрешения
|
||||
|
||||
|
||||
@@ -9,18 +9,19 @@ description: Создайте своё первое приложение Twenty
|
||||
|
||||
Приложения позволяют расширять Twenty с помощью пользовательских объектов, полей, логических функций, навыков ИИ и UI-компонентов — всё это управляется как код.
|
||||
|
||||
**Что вы можете делать уже сегодня:**
|
||||
**Что вы можете создать:**
|
||||
|
||||
* Определяйте пользовательские объекты и поля в виде кода (управляемая модель данных)
|
||||
* Создавайте логические функции с пользовательскими триггерами (HTTP-маршруты, cron, события базы данных)
|
||||
* Определяйте навыки для ИИ-агентов
|
||||
* Создавайте фронтенд-компоненты, которые отображаются внутри интерфейса Twenty
|
||||
* Развёртывайте одно и то же приложение в нескольких рабочих пространствах
|
||||
* Пользовательские объекты, поля, представления и элементы навигации для формирования вашей модели данных
|
||||
* Логические функции, запускаемые маршрутами HTTP, расписаниями cron или событиями базы данных
|
||||
* Фронтенд-компоненты, которые непосредственно отображаются внутри интерфейса Twenty
|
||||
* Навыки, расширяющие возможности ИИ-агентов Twenty
|
||||
* Разверните приложение в нескольких рабочих пространствах
|
||||
|
||||
## Требования
|
||||
|
||||
* Node.js 24+ и Yarn 4
|
||||
* Docker (для локального сервера разработки Twenty)
|
||||
* Node.js 24+
|
||||
* Yarn 4
|
||||
* Docker (или запущенный локальный экземпляр Twenty)
|
||||
|
||||
## Начало работы
|
||||
|
||||
@@ -29,21 +30,9 @@ description: Создайте своё первое приложение Twenty
|
||||
```bash filename="Terminal"
|
||||
# Scaffold a new app (includes all examples by default)
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# Start dev mode: automatically syncs local changes to your workspace
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
Генератор каркаса поддерживает два режима для управления тем, какие файлы-примеры включаются:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimal: only core files (application-config.ts and default-role.ts)
|
||||
npx create-twenty-app@latest my-app --minimal
|
||||
```
|
||||
> Используйте параметр `--minimal`, чтобы создать минимальную установку
|
||||
|
||||
Отсюда вы можете:
|
||||
|
||||
|
||||
@@ -102,6 +102,10 @@ yarn twenty catalog-sync -r production
|
||||
|
||||
Метаданные, отображаемые в маркетплейсе, берутся из вызова `defineApplication()` в исходном коде вашего приложения — из таких полей, как `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl` и `termsUrl`.
|
||||
|
||||
<Note>
|
||||
Если ваше приложение не определяет `aboutDescription` в `defineApplication()`, маркетплейс автоматически использует `README.md` вашего пакета из npm в качестве содержимого страницы «О приложении». Это означает, что вы можете поддерживать единый README как для npm, так и для маркетплейса Twenty. Если вы хотите другое описание в маркетплейсе, явно задайте `aboutDescription`.
|
||||
</Note>
|
||||
|
||||
### Публикация через CI
|
||||
|
||||
Сгенерированный шаблоном проект включает рабочий процесс GitHub Actions, который выполняет публикацию при каждом релизе:
|
||||
@@ -238,8 +242,8 @@ Twenty группирует приложения в три категории в
|
||||
| --------------------------- | -------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| `yarn twenty build` | Скомпилировать приложение и сгенерировать манифест | `--tarball` — также создать пакет `.tgz` |
|
||||
| `yarn twenty publish` | Собрать и опубликовать в npm | `--tag <tag>` — dist-tag npm (например, `beta`, `next`) |
|
||||
| `yarn twenty deploy` | Собрать и загрузить tarball на сервер | `-r, --remote <name>` — целевой remote |
|
||||
| `yarn twenty catalog-sync` | Запустить на сервере синхронизацию каталога маркетплейса | `-r, --remote <name>` — целевой remote |
|
||||
| `yarn twenty deploy` | Собрать и загрузить tarball на сервер | `-r, --remote <name>` — целевой удалённый репозиторий |
|
||||
| `yarn twenty catalog-sync` | Запустить на сервере синхронизацию каталога маркетплейса | `-r, --remote <name>` — целевой удалённый репозиторий |
|
||||
| `yarn twenty install` | Установить развернутое приложение в рабочем пространстве | `-r, --remote <name>` — целевой remote |
|
||||
| `yarn twenty dev` | Отслеживать и синхронизировать локальные изменения | Использует remote по умолчанию |
|
||||
| `yarn twenty remote add` | Добавить подключение к серверу | `--url`, `--token`, `--as`, `--local`, `--port` |
|
||||
|
||||
@@ -38,7 +38,7 @@ yarn twenty dev
|
||||
|
||||
### Управление локальным сервером
|
||||
|
||||
SDK включает команды для управления локальным сервером разработки Twenty (универсальный образ Docker с PostgreSQL, Redis, сервером и воркером):
|
||||
SDK включает команды для управления локальным сервером разработки Twenty (универсальный образ Docker с PostgreSQL, Redis, сервером и воркером на порту 2020). Эти команды применимы только к серверу разработки на базе Docker — они не управляют экземпляром Twenty, запущенным из исходного кода (например, `npx nx start twenty-server` на порту 3000):
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Запустить локальный сервер (при необходимости будет загружен образ)
|
||||
|
||||
+59
-24
@@ -55,11 +55,12 @@ export const main = async (
|
||||
params: { companyId: string },
|
||||
) => {
|
||||
const { companyId } = params;
|
||||
|
||||
// Replace with your Twenty GraphQL endpoint
|
||||
// Replace with your Twenty GraphQL endpoints (/metadata for metadata and files or /graphql for your records)
|
||||
// Cloud: https://api.twenty.com/graphql
|
||||
// Self-hosted: https://your-domain.com/graphql
|
||||
const graphqlEndpoint = 'https://api.twenty.com/graphql';
|
||||
|
||||
const metadataGraphqlEndpoint = 'https://api.twenty.com/metadata';
|
||||
const dataGraphqlEndpoint = 'https://api.twenty.com/graphql';
|
||||
|
||||
// Replace with your API key from Settings → APIs
|
||||
const authToken = 'YOUR_API_KEY';
|
||||
@@ -79,11 +80,40 @@ export const main = async (
|
||||
const pdfBlob = await pdfResponse.blob();
|
||||
const pdfFile = new File([pdfBlob], filename, { type: 'application/pdf' });
|
||||
|
||||
// Step 2: Upload the file via GraphQL multipart upload
|
||||
const fieldMetadataIdQuery = `
|
||||
query FindUploadFileFieldMetadataId {
|
||||
objects {
|
||||
edges {
|
||||
node {
|
||||
nameSingular
|
||||
fieldsList {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Step 2: Find a fieldMetadataId of "Attachment file" field in Attachments object with GraphQL API
|
||||
const response = await fetch(metadataGraphqlEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: {
|
||||
query: fieldMetadataIdQuery,
|
||||
}
|
||||
});
|
||||
const result = await response.json();
|
||||
const uploadFileFieldMetadataId = result.data.objects.edges.find(object => object.node.nameSingular === 'attachment').node.fieldsList.find(field => field.name === 'file').id;
|
||||
|
||||
// Step 3: Upload the file via GraphQL multipart upload
|
||||
const uploadMutation = `
|
||||
mutation UploadFile($file: Upload!, $fileFolder: FileFolder) {
|
||||
uploadFile(file: $file, fileFolder: $fileFolder) {
|
||||
path
|
||||
mutation UploadFilesFieldFile($file: Upload!, $fieldMetadataId: String!) {
|
||||
uploadFilesFieldFile(file: $file, fieldMetadataId: $fieldMetadataId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -91,12 +121,12 @@ export const main = async (
|
||||
const uploadForm = new FormData();
|
||||
uploadForm.append('operations', JSON.stringify({
|
||||
query: uploadMutation,
|
||||
variables: { file: null, fileFolder: 'Attachment' },
|
||||
variables: { file: null, fieldMetadataId: uploadFileFieldMetadataId },
|
||||
}));
|
||||
uploadForm.append('map', JSON.stringify({ '0': ['variables.file'] }));
|
||||
uploadForm.append('0', pdfFile);
|
||||
|
||||
const uploadResponse = await fetch(graphqlEndpoint, {
|
||||
const uploadResponse = await fetch(metadataGraphqlEndpoint, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${authToken}` },
|
||||
body: uploadForm,
|
||||
@@ -108,15 +138,15 @@ export const main = async (
|
||||
throw new Error(`Upload failed: ${uploadResult.errors[0].message}`);
|
||||
}
|
||||
|
||||
const filePath = uploadResult.data?.uploadFile?.path;
|
||||
const fileId = uploadResult.data?.uploadFilesFieldFile?.id;
|
||||
|
||||
if (!filePath) {
|
||||
throw new Error('No file path returned from upload');
|
||||
if (!fileId) {
|
||||
throw new Error('No file id returned from upload');
|
||||
}
|
||||
|
||||
// Step 3: Create the attachment linked to the company
|
||||
// Step 4: Create the attachment linked to the company
|
||||
const attachmentMutation = `
|
||||
mutation CreateAttachment($data: AttachmentCreateInput!) {
|
||||
mutation CreateOneAttachment($data: AttachmentCreateInput!) {
|
||||
createAttachment(data: $data) {
|
||||
id
|
||||
name
|
||||
@@ -124,7 +154,7 @@ export const main = async (
|
||||
}
|
||||
`;
|
||||
|
||||
const attachmentResponse = await fetch(graphqlEndpoint, {
|
||||
const attachmentResponse = await fetch(dataGraphqlEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
@@ -135,8 +165,13 @@ export const main = async (
|
||||
variables: {
|
||||
data: {
|
||||
name: filename,
|
||||
fullPath: filePath,
|
||||
companyId,
|
||||
targetCompanyId: companyId,
|
||||
file: [
|
||||
{
|
||||
fileId: fileId,
|
||||
label: filename
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -156,14 +191,14 @@ export const main = async (
|
||||
|
||||
#### Чтобы прикреплять к другому объекту
|
||||
|
||||
Замените `companyId` на соответствующее поле:
|
||||
Замените `targetCompanyId` на соответствующее поле:
|
||||
|
||||
| Объект | Имя поля |
|
||||
| ----------------------- | -------------------- |
|
||||
| Компания | `companyId` |
|
||||
| Контакт | `personId` |
|
||||
| Сделка | `opportunityId` |
|
||||
| Пользовательский объект | `yourCustomObjectId` |
|
||||
| Объект | Имя поля |
|
||||
| ----------------------- | -------------------------- |
|
||||
| Компания | `targetCompanyId` |
|
||||
| Контакт | `targetPersonId` |
|
||||
| Сделка | `targetOpportunityId` |
|
||||
| Пользовательский объект | `targetYourCustomObjectId` |
|
||||
|
||||
Обновите и параметр функции, и объект `variables.data` в мутации вложения.
|
||||
|
||||
|
||||
@@ -358,17 +358,17 @@ Notlar:
|
||||
|
||||
Eğer [uygulamanızı yayımlamayı](/l/tr/developers/extend/apps/publishing) planlıyorsanız, bu isteğe bağlı alanlar uygulamanızın pazaryerinde nasıl görüneceğini kontrol eder:
|
||||
|
||||
| Alan | Açıklama |
|
||||
| ------------------ | ------------------------------------------------------------- |
|
||||
| `author` | Yazar veya şirket adı |
|
||||
| `category` | Pazaryerinde filtreleme için uygulama kategorisi |
|
||||
| `logoUrl` | Uygulama logonuzun yolu (`./assets/` dizinine göre) |
|
||||
| `screenshots` | Ekran görüntüsü yollarının dizisi (`./assets/` dizinine göre) |
|
||||
| `aboutDescription` | "Hakkında" sekmesi için daha uzun bir markdown açıklaması |
|
||||
| `websiteUrl` | Web sitenize bağlantı |
|
||||
| `termsUrl` | Hizmet Koşulları'na bağlantı |
|
||||
| `emailSupport` | Destek e-posta adresi |
|
||||
| `issueReportUrl` | Sorun izleyicisine bağlantı |
|
||||
| Alan | Açıklama |
|
||||
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `author` | Yazar veya şirket adı |
|
||||
| `category` | Pazaryerinde filtreleme için uygulama kategorisi |
|
||||
| `logoUrl` | Uygulama logonuzun yolu (`./assets/` dizinine göre) |
|
||||
| `screenshots` | Ekran görüntüsü yollarının dizisi (`./assets/` dizinine göre) |
|
||||
| `aboutDescription` | "Hakkında" sekmesi için daha uzun bir markdown açıklaması. Belirtilmezse, pazaryeri npm'deki paketin `README.md` dosyasını kullanır |
|
||||
| `websiteUrl` | Web sitenize bağlantı |
|
||||
| `termsUrl` | Hizmet Koşulları'na bağlantı |
|
||||
| `emailSupport` | Destek e-posta adresi |
|
||||
| `issueReportUrl` | Sorun izleyicisine bağlantı |
|
||||
|
||||
#### Roller ve izinler
|
||||
|
||||
|
||||
@@ -9,18 +9,19 @@ Uygulamalar şu anda alfa testinde. Özellik işlevsel ancak hâlâ gelişmekte.
|
||||
|
||||
Uygulamalar, Twenty'yi özel nesneler, alanlar, mantık işlevleri, Yapay Zeka yetenekleri ve UI bileşenleriyle genişletmenizi sağlar — tümü kod olarak yönetilir.
|
||||
|
||||
**Bugün Yapabilecekleriniz:**
|
||||
**Oluşturabilecekleriniz:**
|
||||
|
||||
* Özel nesneleri ve alanları kod olarak tanımlayın (yönetilen veri modeli)
|
||||
* Özel tetikleyicilerle mantık işlevleri oluşturun (HTTP routes, cron, database events)
|
||||
* Yapay zekâ ajanları için becerileri tanımlayın
|
||||
* Twenty'nin UI'si içinde görüntülenen ön bileşenler oluşturun
|
||||
* Aynı uygulamayı birden çok çalışma alanına dağıtın
|
||||
* Veri modelinizi şekillendirmek için özel nesneler, alanlar, görünümler ve gezinti öğeleri
|
||||
* HTTP rotaları, cron zamanlamaları veya veritabanı olayları tarafından tetiklenen mantık işlevleri
|
||||
* Twenty'nin UI'si içinde doğrudan görüntülenen ön uç bileşenleri
|
||||
* Twenty'nin yapay zeka ajanlarını genişleten beceriler
|
||||
* Bir uygulamayı birden çok çalışma alanına dağıtın
|
||||
|
||||
## Ön Gereksinimler
|
||||
|
||||
* Node.js 24+ ve Yarn 4
|
||||
* Docker (yerel Twenty geliştirme sunucusu için)
|
||||
* Node.js 24+
|
||||
* Yarn 4
|
||||
* Docker (veya çalışan yerel bir Twenty örneği)
|
||||
|
||||
## Başlarken
|
||||
|
||||
@@ -29,21 +30,9 @@ Resmi iskelet oluşturucu aracını kullanarak yeni bir uygulama oluşturun, ard
|
||||
```bash filename="Terminal"
|
||||
# Scaffold a new app (includes all examples by default)
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# Start dev mode: automatically syncs local changes to your workspace
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
İskelet oluşturucu, hangi örnek dosyaların dahil edileceğini kontrol etmek için iki modu destekler:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimal: only core files (application-config.ts and default-role.ts)
|
||||
npx create-twenty-app@latest my-app --minimal
|
||||
```
|
||||
> Minimal bir kurulum iskeleti oluşturmak için `--minimal` seçeneğini kullanın
|
||||
|
||||
Buradan şunları yapabilirsiniz:
|
||||
|
||||
|
||||
@@ -102,6 +102,10 @@ yarn twenty catalog-sync -r production
|
||||
|
||||
Pazar yerinde gösterilen meta veriler, uygulamanızın kaynak kodundaki `defineApplication()` çağrısından gelir — `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl` ve `termsUrl` gibi alanlar.
|
||||
|
||||
<Note>
|
||||
Uygulamanız `defineApplication()` içinde bir `aboutDescription` tanımlamıyorsa, pazaryeri, hakkında sayfasının içeriği olarak paketinizin npm'deki `README.md` dosyasını otomatik olarak kullanır. Bu, hem npm hem de Twenty pazaryeri için tek bir README dosyası kullanabileceğiniz anlamına gelir. Pazaryerinde farklı bir açıklama istiyorsanız, `aboutDescription` değerini açıkça ayarlayın.
|
||||
</Note>
|
||||
|
||||
### CI üzerinden yayımlama
|
||||
|
||||
İskelet proje, her sürümde yayımlayan bir GitHub Actions iş akışını içerir:
|
||||
|
||||
@@ -38,7 +38,7 @@ yarn twenty dev
|
||||
|
||||
### Yerel Sunucu Yönetimi
|
||||
|
||||
SDK, yerel bir Twenty geliştirme sunucusunu yönetmek için komutlar içerir (PostgreSQL, Redis, sunucu ve worker içeren hepsi bir arada Docker imajı):
|
||||
SDK, yerel bir Twenty geliştirme sunucusunu yönetmek için komutlar içerir (PostgreSQL, Redis, sunucu ve worker içeren hepsi bir arada Docker imajı, 2020 numaralı portta). Bu komutlar yalnızca Docker tabanlı geliştirme sunucusu için geçerlidir — kaynak koddan başlatılan bir Twenty örneğini yönetmezler (örn. 3000 numaralı portta `npx nx start twenty-server`):
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Yerel sunucuyu başlatın (gerekirse imajı indirir)
|
||||
|
||||
+59
-24
@@ -55,11 +55,12 @@ export const main = async (
|
||||
params: { companyId: string },
|
||||
) => {
|
||||
const { companyId } = params;
|
||||
|
||||
// Replace with your Twenty GraphQL endpoint
|
||||
// Replace with your Twenty GraphQL endpoints (/metadata for metadata and files or /graphql for your records)
|
||||
// Cloud: https://api.twenty.com/graphql
|
||||
// Self-hosted: https://your-domain.com/graphql
|
||||
const graphqlEndpoint = 'https://api.twenty.com/graphql';
|
||||
|
||||
const metadataGraphqlEndpoint = 'https://api.twenty.com/metadata';
|
||||
const dataGraphqlEndpoint = 'https://api.twenty.com/graphql';
|
||||
|
||||
// Replace with your API key from Settings → APIs
|
||||
const authToken = 'YOUR_API_KEY';
|
||||
@@ -79,11 +80,40 @@ export const main = async (
|
||||
const pdfBlob = await pdfResponse.blob();
|
||||
const pdfFile = new File([pdfBlob], filename, { type: 'application/pdf' });
|
||||
|
||||
// Step 2: Upload the file via GraphQL multipart upload
|
||||
const fieldMetadataIdQuery = `
|
||||
query FindUploadFileFieldMetadataId {
|
||||
objects {
|
||||
edges {
|
||||
node {
|
||||
nameSingular
|
||||
fieldsList {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Step 2: Find a fieldMetadataId of "Attachment file" field in Attachments object with GraphQL API
|
||||
const response = await fetch(metadataGraphqlEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: {
|
||||
query: fieldMetadataIdQuery,
|
||||
}
|
||||
});
|
||||
const result = await response.json();
|
||||
const uploadFileFieldMetadataId = result.data.objects.edges.find(object => object.node.nameSingular === 'attachment').node.fieldsList.find(field => field.name === 'file').id;
|
||||
|
||||
// Step 3: Upload the file via GraphQL multipart upload
|
||||
const uploadMutation = `
|
||||
mutation UploadFile($file: Upload!, $fileFolder: FileFolder) {
|
||||
uploadFile(file: $file, fileFolder: $fileFolder) {
|
||||
path
|
||||
mutation UploadFilesFieldFile($file: Upload!, $fieldMetadataId: String!) {
|
||||
uploadFilesFieldFile(file: $file, fieldMetadataId: $fieldMetadataId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -91,12 +121,12 @@ export const main = async (
|
||||
const uploadForm = new FormData();
|
||||
uploadForm.append('operations', JSON.stringify({
|
||||
query: uploadMutation,
|
||||
variables: { file: null, fileFolder: 'Attachment' },
|
||||
variables: { file: null, fieldMetadataId: uploadFileFieldMetadataId },
|
||||
}));
|
||||
uploadForm.append('map', JSON.stringify({ '0': ['variables.file'] }));
|
||||
uploadForm.append('0', pdfFile);
|
||||
|
||||
const uploadResponse = await fetch(graphqlEndpoint, {
|
||||
const uploadResponse = await fetch(metadataGraphqlEndpoint, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${authToken}` },
|
||||
body: uploadForm,
|
||||
@@ -108,15 +138,15 @@ export const main = async (
|
||||
throw new Error(`Upload failed: ${uploadResult.errors[0].message}`);
|
||||
}
|
||||
|
||||
const filePath = uploadResult.data?.uploadFile?.path;
|
||||
const fileId = uploadResult.data?.uploadFilesFieldFile?.id;
|
||||
|
||||
if (!filePath) {
|
||||
throw new Error('No file path returned from upload');
|
||||
if (!fileId) {
|
||||
throw new Error('No file id returned from upload');
|
||||
}
|
||||
|
||||
// Step 3: Create the attachment linked to the company
|
||||
// Step 4: Create the attachment linked to the company
|
||||
const attachmentMutation = `
|
||||
mutation CreateAttachment($data: AttachmentCreateInput!) {
|
||||
mutation CreateOneAttachment($data: AttachmentCreateInput!) {
|
||||
createAttachment(data: $data) {
|
||||
id
|
||||
name
|
||||
@@ -124,7 +154,7 @@ export const main = async (
|
||||
}
|
||||
`;
|
||||
|
||||
const attachmentResponse = await fetch(graphqlEndpoint, {
|
||||
const attachmentResponse = await fetch(dataGraphqlEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
@@ -135,8 +165,13 @@ export const main = async (
|
||||
variables: {
|
||||
data: {
|
||||
name: filename,
|
||||
fullPath: filePath,
|
||||
companyId,
|
||||
targetCompanyId: companyId,
|
||||
file: [
|
||||
{
|
||||
fileId: fileId,
|
||||
label: filename
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -156,14 +191,14 @@ export const main = async (
|
||||
|
||||
#### Farklı bir nesneye eklemek için
|
||||
|
||||
`companyId` değerini uygun alanla değiştirin:
|
||||
`targetCompanyId` değerini uygun alanla değiştirin:
|
||||
|
||||
| Nesne | Alan Adı |
|
||||
| ---------- | -------------------- |
|
||||
| Şirket | `companyId` |
|
||||
| Kişi | `personId` |
|
||||
| Fırsat | `opportunityId` |
|
||||
| Özel Nesne | `yourCustomObjectId` |
|
||||
| Nesne | Alan Adı |
|
||||
| ---------- | -------------------------- |
|
||||
| Şirket | `targetCompanyId` |
|
||||
| Kişi | `targetPersonId` |
|
||||
| Fırsat | `targetOpportunityId` |
|
||||
| Özel Nesne | `targetYourCustomObjectId` |
|
||||
|
||||
Ek oluşturma mutasyonunda hem işlev parametresini hem de `variables.data` nesnesini güncelleyin.
|
||||
|
||||
|
||||
@@ -358,17 +358,17 @@ export default defineApplication({
|
||||
|
||||
如果你计划[发布你的应用](/l/zh/developers/extend/apps/publishing),这些可选字段将控制你的应用在应用市场中的展示:
|
||||
|
||||
| 字段 | 描述 |
|
||||
| ------------------ | -------------------------- |
|
||||
| `作者` | 作者或公司名称 |
|
||||
| `类别` | 用于应用市场筛选的应用类别 |
|
||||
| `logoUrl` | 你的应用徽标的路径(相对于 `./assets/`) |
|
||||
| `screenshots` | 屏幕截图路径数组(相对于 `./assets/`) |
|
||||
| `aboutDescription` | 用于“关于”选项卡的更长 Markdown 描述 |
|
||||
| `websiteUrl` | 你的网站链接 |
|
||||
| `termsUrl` | 服务条款链接 |
|
||||
| `emailSupport` | 支持电子邮件地址 |
|
||||
| `issueReportUrl` | 问题跟踪器链接 |
|
||||
| 字段 | 描述 |
|
||||
| ------------------ | -------------------------------------------------------------- |
|
||||
| `作者` | 作者或公司名称 |
|
||||
| `类别` | 用于应用市场筛选的应用类别 |
|
||||
| `logoUrl` | 你的应用徽标的路径(相对于 `./assets/`) |
|
||||
| `screenshots` | 屏幕截图路径数组(相对于 `./assets/`) |
|
||||
| `aboutDescription` | 用于“关于”选项卡的更长的 Markdown 描述。 如果省略,市场将使用该软件包在 npm 上的 `README.md`。 |
|
||||
| `websiteUrl` | 你的网站链接 |
|
||||
| `termsUrl` | 服务条款链接 |
|
||||
| `emailSupport` | 支持电子邮件地址 |
|
||||
| `issueReportUrl` | 问题跟踪器链接 |
|
||||
|
||||
#### 角色和权限
|
||||
|
||||
|
||||
@@ -9,18 +9,19 @@ description: 几分钟内创建你的第一个 Twenty 应用。
|
||||
|
||||
应用可通过自定义对象、字段、逻辑函数、AI 技能和 UI 组件来扩展 Twenty——全部以代码进行管理。
|
||||
|
||||
**你现在可以做什么:**
|
||||
**你可以构建的内容:**
|
||||
|
||||
* 以代码定义自定义对象和字段(受管理的数据模型)
|
||||
* 使用自定义触发器(HTTP 路由、cron、数据库事件)构建逻辑函数
|
||||
* 为 AI 智能体定义技能
|
||||
* 构建在 Twenty 的 UI 中渲染的前端组件
|
||||
* 自定义对象、字段、视图和导航项,以塑造你的数据模型
|
||||
* 由 HTTP 路由、cron 调度或数据库事件触发的逻辑函数
|
||||
* 在 Twenty 的 UI 中直接渲染的前端组件
|
||||
* 用于扩展 Twenty 的 AI 代理的技能
|
||||
* 将同一个应用部署到多个工作空间
|
||||
|
||||
## 先决条件
|
||||
|
||||
* Node.js 24+ 和 Yarn 4
|
||||
* Docker (用于本地 Twenty 开发服务器)
|
||||
* Node.js 24+
|
||||
* Yarn 4
|
||||
* Docker (或正在运行的本地 Twenty 实例)
|
||||
|
||||
## 开始使用
|
||||
|
||||
@@ -29,21 +30,9 @@ description: 几分钟内创建你的第一个 Twenty 应用。
|
||||
```bash filename="Terminal"
|
||||
# Scaffold a new app (includes all examples by default)
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# Start dev mode: automatically syncs local changes to your workspace
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
脚手架工具支持两种模式,用于控制包含哪些示例文件:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimal: only core files (application-config.ts and default-role.ts)
|
||||
npx create-twenty-app@latest my-app --minimal
|
||||
```
|
||||
> 使用 `--minimal` 选项生成最简安装脚手架
|
||||
|
||||
从这里您可以:
|
||||
|
||||
|
||||
@@ -102,6 +102,10 @@ yarn twenty catalog-sync -r production
|
||||
|
||||
市场中显示的元数据来自你在应用源代码中调用的 `defineApplication()` —— 诸如 `displayName`、`description`、`author`、`category`、`logoUrl`、`screenshots`、`aboutDescription`、`websiteUrl` 和 `termsUrl` 等字段。
|
||||
|
||||
<Note>
|
||||
如果您的应用未在 `defineApplication()` 中定义 `aboutDescription`,市场将自动使用 npm 上您的软件包的 `README.md` 作为关于页面内容。 这意味着您可以为 npm 和 Twenty 市场维护同一个 README。 如果您希望在市场中使用不同的描述,请显式设置 `aboutDescription`。
|
||||
</Note>
|
||||
|
||||
### CI 发布
|
||||
|
||||
脚手架项目包含一个 GitHub Actions 工作流,会在每次发版时自动发布:
|
||||
|
||||
@@ -38,7 +38,7 @@ yarn twenty dev
|
||||
|
||||
### 本地服务器管理
|
||||
|
||||
该 SDK 包含用于管理本地 Twenty 开发服务器的命令(该服务器是一体化 Docker 镜像,内含 PostgreSQL、Redis、服务器和工作进程):
|
||||
该 SDK 包含用于管理本地 Twenty 开发服务器的命令(该服务器是一体化 Docker 镜像,内含 PostgreSQL、Redis、服务器和工作进程,监听 2020 端口)。 这些命令仅适用于基于 Docker 的开发服务器——它们不会管理从源代码启动的 Twenty 实例(例如通过 `npx nx start twenty-server` 启动的实例,运行在 3000 端口):
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Start the local server (pulls the image if needed)
|
||||
|
||||
+59
-24
@@ -55,11 +55,12 @@ export const main = async (
|
||||
params: { companyId: string },
|
||||
) => {
|
||||
const { companyId } = params;
|
||||
|
||||
// Replace with your Twenty GraphQL endpoint
|
||||
// Replace with your Twenty GraphQL endpoints (/metadata for metadata and files or /graphql for your records)
|
||||
// Cloud: https://api.twenty.com/graphql
|
||||
// Self-hosted: https://your-domain.com/graphql
|
||||
const graphqlEndpoint = 'https://api.twenty.com/graphql';
|
||||
|
||||
const metadataGraphqlEndpoint = 'https://api.twenty.com/metadata';
|
||||
const dataGraphqlEndpoint = 'https://api.twenty.com/graphql';
|
||||
|
||||
// Replace with your API key from Settings → APIs
|
||||
const authToken = 'YOUR_API_KEY';
|
||||
@@ -79,11 +80,40 @@ export const main = async (
|
||||
const pdfBlob = await pdfResponse.blob();
|
||||
const pdfFile = new File([pdfBlob], filename, { type: 'application/pdf' });
|
||||
|
||||
// Step 2: Upload the file via GraphQL multipart upload
|
||||
const fieldMetadataIdQuery = `
|
||||
query FindUploadFileFieldMetadataId {
|
||||
objects {
|
||||
edges {
|
||||
node {
|
||||
nameSingular
|
||||
fieldsList {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Step 2: Find a fieldMetadataId of "Attachment file" field in Attachments object with GraphQL API
|
||||
const response = await fetch(metadataGraphqlEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: {
|
||||
query: fieldMetadataIdQuery,
|
||||
}
|
||||
});
|
||||
const result = await response.json();
|
||||
const uploadFileFieldMetadataId = result.data.objects.edges.find(object => object.node.nameSingular === 'attachment').node.fieldsList.find(field => field.name === 'file').id;
|
||||
|
||||
// Step 3: Upload the file via GraphQL multipart upload
|
||||
const uploadMutation = `
|
||||
mutation UploadFile($file: Upload!, $fileFolder: FileFolder) {
|
||||
uploadFile(file: $file, fileFolder: $fileFolder) {
|
||||
path
|
||||
mutation UploadFilesFieldFile($file: Upload!, $fieldMetadataId: String!) {
|
||||
uploadFilesFieldFile(file: $file, fieldMetadataId: $fieldMetadataId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -91,12 +121,12 @@ export const main = async (
|
||||
const uploadForm = new FormData();
|
||||
uploadForm.append('operations', JSON.stringify({
|
||||
query: uploadMutation,
|
||||
variables: { file: null, fileFolder: 'Attachment' },
|
||||
variables: { file: null, fieldMetadataId: uploadFileFieldMetadataId },
|
||||
}));
|
||||
uploadForm.append('map', JSON.stringify({ '0': ['variables.file'] }));
|
||||
uploadForm.append('0', pdfFile);
|
||||
|
||||
const uploadResponse = await fetch(graphqlEndpoint, {
|
||||
const uploadResponse = await fetch(metadataGraphqlEndpoint, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${authToken}` },
|
||||
body: uploadForm,
|
||||
@@ -108,15 +138,15 @@ export const main = async (
|
||||
throw new Error(`Upload failed: ${uploadResult.errors[0].message}`);
|
||||
}
|
||||
|
||||
const filePath = uploadResult.data?.uploadFile?.path;
|
||||
const fileId = uploadResult.data?.uploadFilesFieldFile?.id;
|
||||
|
||||
if (!filePath) {
|
||||
throw new Error('No file path returned from upload');
|
||||
if (!fileId) {
|
||||
throw new Error('No file id returned from upload');
|
||||
}
|
||||
|
||||
// Step 3: Create the attachment linked to the company
|
||||
// Step 4: Create the attachment linked to the company
|
||||
const attachmentMutation = `
|
||||
mutation CreateAttachment($data: AttachmentCreateInput!) {
|
||||
mutation CreateOneAttachment($data: AttachmentCreateInput!) {
|
||||
createAttachment(data: $data) {
|
||||
id
|
||||
name
|
||||
@@ -124,7 +154,7 @@ export const main = async (
|
||||
}
|
||||
`;
|
||||
|
||||
const attachmentResponse = await fetch(graphqlEndpoint, {
|
||||
const attachmentResponse = await fetch(dataGraphqlEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
@@ -135,8 +165,13 @@ export const main = async (
|
||||
variables: {
|
||||
data: {
|
||||
name: filename,
|
||||
fullPath: filePath,
|
||||
companyId,
|
||||
targetCompanyId: companyId,
|
||||
file: [
|
||||
{
|
||||
fileId: fileId,
|
||||
label: filename
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -156,14 +191,14 @@ export const main = async (
|
||||
|
||||
#### 要附加到其他对象
|
||||
|
||||
将 `companyId` 替换为相应的字段:
|
||||
将 `targetCompanyId` 替换为相应的字段:
|
||||
|
||||
| 对象 | 字段名称 |
|
||||
| ----- | -------------------- |
|
||||
| 公司 | `companyId` |
|
||||
| 人员 | `personId` |
|
||||
| 机会 | `opportunityId` |
|
||||
| 自定义对象 | `yourCustomObjectId` |
|
||||
| 对象 | 字段名称 |
|
||||
| ----- | -------------------------- |
|
||||
| 公司 | `targetCompanyId` |
|
||||
| 人员 | `targetPersonId` |
|
||||
| 机会 | `targetOpportunityId` |
|
||||
| 自定义对象 | `targetYourCustomObjectId` |
|
||||
|
||||
同时更新函数参数以及附件的 mutation 中的 `variables.data` 对象。
|
||||
|
||||
|
||||
+57
-22
@@ -54,11 +54,12 @@ export const main = async (
|
||||
params: { companyId: string },
|
||||
) => {
|
||||
const { companyId } = params;
|
||||
|
||||
// Replace with your Twenty GraphQL endpoint
|
||||
// Replace with your Twenty GraphQL endpoints (/metadata for metadata and files or /graphql for your records)
|
||||
// Cloud: https://api.twenty.com/graphql
|
||||
// Self-hosted: https://your-domain.com/graphql
|
||||
const graphqlEndpoint = 'https://api.twenty.com/graphql';
|
||||
|
||||
const metadataGraphqlEndpoint = 'https://api.twenty.com/metadata';
|
||||
const dataGraphqlEndpoint = 'https://api.twenty.com/graphql';
|
||||
|
||||
// Replace with your API key from Settings → APIs
|
||||
const authToken = 'YOUR_API_KEY';
|
||||
@@ -78,11 +79,40 @@ export const main = async (
|
||||
const pdfBlob = await pdfResponse.blob();
|
||||
const pdfFile = new File([pdfBlob], filename, { type: 'application/pdf' });
|
||||
|
||||
// Step 2: Upload the file via GraphQL multipart upload
|
||||
const fieldMetadataIdQuery = `
|
||||
query FindUploadFileFieldMetadataId {
|
||||
objects {
|
||||
edges {
|
||||
node {
|
||||
nameSingular
|
||||
fieldsList {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Step 2: Find a fieldMetadataId of "Attachment file" field in Attachments object with GraphQL API
|
||||
const response = await fetch(metadataGraphqlEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: {
|
||||
query: fieldMetadataIdQuery,
|
||||
}
|
||||
});
|
||||
const result = await response.json();
|
||||
const uploadFileFieldMetadataId = result.data.objects.edges.find(object => object.node.nameSingular === 'attachment').node.fieldsList.find(field => field.name === 'file').id;
|
||||
|
||||
// Step 3: Upload the file via GraphQL multipart upload
|
||||
const uploadMutation = `
|
||||
mutation UploadFile($file: Upload!, $fileFolder: FileFolder) {
|
||||
uploadFile(file: $file, fileFolder: $fileFolder) {
|
||||
path
|
||||
mutation UploadFilesFieldFile($file: Upload!, $fieldMetadataId: String!) {
|
||||
uploadFilesFieldFile(file: $file, fieldMetadataId: $fieldMetadataId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -90,12 +120,12 @@ export const main = async (
|
||||
const uploadForm = new FormData();
|
||||
uploadForm.append('operations', JSON.stringify({
|
||||
query: uploadMutation,
|
||||
variables: { file: null, fileFolder: 'Attachment' },
|
||||
variables: { file: null, fieldMetadataId: uploadFileFieldMetadataId },
|
||||
}));
|
||||
uploadForm.append('map', JSON.stringify({ '0': ['variables.file'] }));
|
||||
uploadForm.append('0', pdfFile);
|
||||
|
||||
const uploadResponse = await fetch(graphqlEndpoint, {
|
||||
const uploadResponse = await fetch(metadataGraphqlEndpoint, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${authToken}` },
|
||||
body: uploadForm,
|
||||
@@ -107,15 +137,15 @@ export const main = async (
|
||||
throw new Error(`Upload failed: ${uploadResult.errors[0].message}`);
|
||||
}
|
||||
|
||||
const filePath = uploadResult.data?.uploadFile?.path;
|
||||
const fileId = uploadResult.data?.uploadFilesFieldFile?.id;
|
||||
|
||||
if (!filePath) {
|
||||
throw new Error('No file path returned from upload');
|
||||
if (!fileId) {
|
||||
throw new Error('No file id returned from upload');
|
||||
}
|
||||
|
||||
// Step 3: Create the attachment linked to the company
|
||||
// Step 4: Create the attachment linked to the company
|
||||
const attachmentMutation = `
|
||||
mutation CreateAttachment($data: AttachmentCreateInput!) {
|
||||
mutation CreateOneAttachment($data: AttachmentCreateInput!) {
|
||||
createAttachment(data: $data) {
|
||||
id
|
||||
name
|
||||
@@ -123,7 +153,7 @@ export const main = async (
|
||||
}
|
||||
`;
|
||||
|
||||
const attachmentResponse = await fetch(graphqlEndpoint, {
|
||||
const attachmentResponse = await fetch(dataGraphqlEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
@@ -134,8 +164,13 @@ export const main = async (
|
||||
variables: {
|
||||
data: {
|
||||
name: filename,
|
||||
fullPath: filePath,
|
||||
companyId,
|
||||
targetCompanyId: companyId,
|
||||
file: [
|
||||
{
|
||||
fileId: fileId,
|
||||
label: filename
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -155,14 +190,14 @@ export const main = async (
|
||||
|
||||
#### To attach to a different object
|
||||
|
||||
Replace `companyId` with the appropriate field:
|
||||
Replace `targetCompanyId` with the appropriate field:
|
||||
|
||||
| Object | Field Name |
|
||||
|--------|------------|
|
||||
| Company | `companyId` |
|
||||
| Person | `personId` |
|
||||
| Opportunity | `opportunityId` |
|
||||
| Custom Object | `yourCustomObjectId` |
|
||||
| Company | `targetCompanyId` |
|
||||
| Person | `targetPersonId` |
|
||||
| Opportunity | `targetOpportunityId` |
|
||||
| Custom Object | `targetYourCustomObjectId` |
|
||||
|
||||
Update both the function parameter and the `variables.data` object in the attachment mutation.
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
storybook-static
|
||||
src/__stories__/example-sources-built
|
||||
src/__stories__/example-sources-built-preact
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["react", "typescript", "import", "unicorn"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
},
|
||||
"ignorePatterns": ["node_modules", "dist"],
|
||||
"rules": {
|
||||
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
|
||||
"no-console": "off",
|
||||
"no-control-regex": "off",
|
||||
"no-debugger": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"no-undef": "off",
|
||||
"no-unused-vars": "off",
|
||||
"no-redeclare": "off",
|
||||
"import/no-duplicates": "error",
|
||||
"typescript/no-redeclare": "error",
|
||||
"typescript/ban-ts-comment": "error",
|
||||
"typescript/consistent-type-imports": [
|
||||
"error",
|
||||
{
|
||||
"prefer": "type-imports",
|
||||
"fixStyle": "inline-type-imports"
|
||||
}
|
||||
],
|
||||
"typescript/explicit-function-return-type": "off",
|
||||
"typescript/explicit-module-boundary-types": "off",
|
||||
"typescript/no-empty-object-type": [
|
||||
"error",
|
||||
{
|
||||
"allowInterfaces": "with-single-extends"
|
||||
}
|
||||
],
|
||||
"typescript/no-empty-function": "off",
|
||||
"typescript/no-explicit-any": "off",
|
||||
"typescript/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"vars": "all",
|
||||
"varsIgnorePattern": "^_",
|
||||
"args": "after-used",
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"react/no-unescaped-entities": "off",
|
||||
"react/prop-types": "off",
|
||||
"react/jsx-key": "off",
|
||||
"react/display-name": "off",
|
||||
"react/jsx-uses-react": "off",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/jsx-no-useless-fragment": "off",
|
||||
"react/jsx-props-no-spreading": ["error", { "explicitSpread": "ignore" }],
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "warn"
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -8,10 +8,10 @@ const dirname =
|
||||
? __dirname
|
||||
: path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const sdkRoot = path.resolve(dirname, '..');
|
||||
const packageRoot = path.resolve(dirname, '..');
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/front-component-renderer/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||
|
||||
addons: ['@storybook/addon-vitest'],
|
||||
|
||||
@@ -23,11 +23,11 @@ const config: StorybookConfig = {
|
||||
|
||||
staticDirs: [
|
||||
{
|
||||
from: '../src/front-component-renderer/__stories__/example-sources-built',
|
||||
from: '../src/__stories__/example-sources-built',
|
||||
to: '/built',
|
||||
},
|
||||
{
|
||||
from: '../src/front-component-renderer/__stories__/example-sources-built-preact',
|
||||
from: '../src/__stories__/example-sources-built-preact',
|
||||
to: '/built-preact',
|
||||
},
|
||||
],
|
||||
@@ -57,7 +57,7 @@ const config: StorybookConfig = {
|
||||
},
|
||||
plugins: [
|
||||
...(viteConfig.plugins ?? []),
|
||||
tsconfigPaths({ root: sdkRoot }),
|
||||
tsconfigPaths({ root: packageRoot }),
|
||||
],
|
||||
optimizeDeps: {
|
||||
...viteConfig.optimizeDeps,
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "twenty-front-component-renderer",
|
||||
"version": "0.8.0-canary.5",
|
||||
"private": true,
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.mjs",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"package.json"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "npx rimraf dist && npx vite build"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.cjs"
|
||||
}
|
||||
},
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@quilted/threads": "^4.0.1",
|
||||
"@remote-dom/core": "^1.10.1",
|
||||
"@remote-dom/react": "^1.2.2",
|
||||
"@sniptt/guards": "^0.2.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chakra-ui/react": "^3.33.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@mui/material": "^7.3.8",
|
||||
"@storybook/addon-vitest": "^10.2.13",
|
||||
"@storybook/react-vite": "^10.2.13",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitest/browser-playwright": "^4.0.18",
|
||||
"playwright": "^1.56.1",
|
||||
"storybook": "^10.2.13",
|
||||
"styled-components": "^6.1.0",
|
||||
"ts-morph": "^25.0.0",
|
||||
"tsx": "^4.7.0",
|
||||
"twenty-sdk": "workspace:*",
|
||||
"twenty-shared": "workspace:*",
|
||||
"twenty-ui": "workspace:*",
|
||||
"vite-plugin-dts": "^4.5.4",
|
||||
"vite-tsconfig-paths": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
"yarn": "^4.0.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"name": "twenty-front-component-renderer",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "packages/twenty-front-component-renderer/src",
|
||||
"projectType": "library",
|
||||
"tags": ["scope:frontend"],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"inputs": ["production", "^production"],
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["{projectRoot}/dist"],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"commands": [
|
||||
"npx rimraf dist && npx vite build -c vite.config.ts",
|
||||
"tsgo -p tsconfig.lib.json --declaration --emitDeclarationOnly --noEmit false --outDir dist --rootDir src && npx tsc-alias -p tsconfig.lib.json --outDir dist"
|
||||
],
|
||||
"parallel": false
|
||||
}
|
||||
},
|
||||
"typecheck": {},
|
||||
"lint": {},
|
||||
"generate-remote-dom-elements": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"dependsOn": ["^build"],
|
||||
"inputs": [
|
||||
"{projectRoot}/scripts/remote-dom/**/*",
|
||||
"{projectRoot}/src/constants/**/*",
|
||||
"{workspaceRoot}/packages/twenty-ui/src/**/index.ts",
|
||||
"{workspaceRoot}/packages/twenty-ui/src/**/*.tsx"
|
||||
],
|
||||
"outputs": [
|
||||
"{projectRoot}/src/host/generated/*",
|
||||
"{projectRoot}/src/remote/generated/*"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "packages/twenty-front-component-renderer",
|
||||
"command": "tsx -r tsconfig-paths/register scripts/remote-dom/generate-remote-dom-elements.ts"
|
||||
},
|
||||
"configurations": {
|
||||
"verbose": {
|
||||
"command": "tsx -r tsconfig-paths/register scripts/remote-dom/generate-remote-dom-elements.ts --verbose"
|
||||
}
|
||||
}
|
||||
},
|
||||
"storybook:prebuild": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"dependsOn": [
|
||||
"generate-remote-dom-elements",
|
||||
{
|
||||
"target": "build:sdk",
|
||||
"projects": "twenty-sdk"
|
||||
},
|
||||
{
|
||||
"target": "build:individual",
|
||||
"projects": "twenty-ui"
|
||||
},
|
||||
{
|
||||
"target": "build:individual",
|
||||
"projects": "twenty-shared"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
"{projectRoot}/scripts/front-component-stories/**/*",
|
||||
"{projectRoot}/src/__stories__/example-sources/*",
|
||||
"{workspaceRoot}/packages/twenty-sdk/src/cli/utilities/build/**/*"
|
||||
],
|
||||
"outputs": ["{projectRoot}/src/__stories__/example-sources-built/*"],
|
||||
"options": {
|
||||
"command": "tsx {projectRoot}/scripts/front-component-stories/build-source-examples.ts"
|
||||
}
|
||||
},
|
||||
"storybook:build": {
|
||||
"dependsOn": ["storybook:prebuild"],
|
||||
"configurations": {
|
||||
"test": {}
|
||||
}
|
||||
},
|
||||
"storybook:serve:dev": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"port": 6008
|
||||
}
|
||||
},
|
||||
"storybook:serve:static": {
|
||||
"options": {
|
||||
"buildTarget": "twenty-front-component-renderer:storybook:build",
|
||||
"port": 6008
|
||||
},
|
||||
"configurations": {
|
||||
"test": {}
|
||||
}
|
||||
},
|
||||
"storybook:test": {
|
||||
"dependsOn": ["storybook:prebuild"],
|
||||
"options": {
|
||||
"command": "vitest run --coverage --config vitest.storybook.config.ts --shard={args.shard}"
|
||||
}
|
||||
},
|
||||
"storybook:test:no-coverage": {
|
||||
"dependsOn": ["storybook:prebuild"],
|
||||
"options": {
|
||||
"command": "vitest run --config vitest.storybook.config.ts --shard={args.shard}"
|
||||
}
|
||||
},
|
||||
"storybook:coverage": {}
|
||||
}
|
||||
}
|
||||
+8
-5
@@ -3,20 +3,20 @@ import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { getFrontComponentBuildPlugins } from '../../src/cli/utilities/build/common/front-component-build/utils/get-front-component-build-plugins';
|
||||
import { getFrontComponentBuildPlugins } from 'twenty-sdk/front-component-renderer/build';
|
||||
|
||||
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const exampleSourcesDir = path.resolve(
|
||||
dirname,
|
||||
'../../src/front-component-renderer/__stories__/example-sources',
|
||||
'../../src/__stories__/example-sources',
|
||||
);
|
||||
const exampleSourcesBuiltDir = path.resolve(
|
||||
dirname,
|
||||
'../../src/front-component-renderer/__stories__/example-sources-built',
|
||||
'../../src/__stories__/example-sources-built',
|
||||
);
|
||||
const exampleSourcesBuiltPreactDir = path.resolve(
|
||||
dirname,
|
||||
'../../src/front-component-renderer/__stories__/example-sources-built-preact',
|
||||
'../../src/__stories__/example-sources-built-preact',
|
||||
);
|
||||
|
||||
const rootNodeModules = path.resolve(dirname, '../../../../node_modules');
|
||||
@@ -26,7 +26,10 @@ const twentyUiIndividualIndex = path.resolve(
|
||||
'../../../twenty-ui/dist/individual/individual-entry.js',
|
||||
);
|
||||
|
||||
const sdkIndividualIndex = path.resolve(dirname, '../../dist/sdk/index.js');
|
||||
const sdkIndividualIndex = path.resolve(
|
||||
dirname,
|
||||
'../../../twenty-sdk/dist/sdk/index.js',
|
||||
);
|
||||
|
||||
const twentySharedIndividualDir = path.resolve(
|
||||
dirname,
|
||||
+6
-12
@@ -4,9 +4,9 @@ import * as path from 'path';
|
||||
import { IndentationText, Project, QuoteKind } from 'ts-morph';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { ALLOWED_HTML_ELEMENTS } from '../../src/sdk/front-component-api/constants/AllowedHtmlElements';
|
||||
import { COMMON_HTML_EVENTS } from '../../src/sdk/front-component-api/constants/CommonHtmlEvents';
|
||||
import { HTML_COMMON_PROPERTIES } from '../../src/sdk/front-component-api/constants/HtmlCommonProperties';
|
||||
import { ALLOWED_HTML_ELEMENTS } from '../../src/constants/AllowedHtmlElements';
|
||||
import { COMMON_HTML_EVENTS } from '../../src/constants/CommonHtmlEvents';
|
||||
import { HTML_COMMON_PROPERTIES } from '../../src/constants/HtmlCommonProperties';
|
||||
|
||||
import {
|
||||
type ComponentSchema,
|
||||
@@ -19,15 +19,9 @@ import {
|
||||
|
||||
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
||||
const PACKAGE_PATH = path.resolve(SCRIPT_DIR, '../..');
|
||||
const FRONT_COMPONENT_PATH = path.join(
|
||||
PACKAGE_PATH,
|
||||
'src/front-component-renderer',
|
||||
);
|
||||
const HOST_GENERATED_DIR = path.join(FRONT_COMPONENT_PATH, 'host/generated');
|
||||
const REMOTE_GENERATED_DIR = path.join(
|
||||
FRONT_COMPONENT_PATH,
|
||||
'remote/generated',
|
||||
);
|
||||
const SRC_PATH = path.join(PACKAGE_PATH, 'src');
|
||||
const HOST_GENERATED_DIR = path.join(SRC_PATH, 'host/generated');
|
||||
const REMOTE_GENERATED_DIR = path.join(SRC_PATH, 'remote/generated');
|
||||
|
||||
const extractHtmlTag = (tag: string): string => tag.slice(5);
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import type { Project, SourceFile } from 'ts-morph';
|
||||
|
||||
import { EVENT_TO_REACT } from '../../../src/sdk/front-component-api/constants/EventToReact';
|
||||
import { EVENT_TO_REACT } from '../../../src/constants/EventToReact';
|
||||
import { type ComponentSchema } from './schemas';
|
||||
import { addExportedConst } from './utils';
|
||||
|
||||
+1
-2
@@ -336,8 +336,7 @@ export const generateRemoteElements = (
|
||||
});
|
||||
|
||||
sourceFile.addImportDeclaration({
|
||||
moduleSpecifier:
|
||||
'../../../sdk/front-component-api/constants/SerializedEventData',
|
||||
moduleSpecifier: '@/constants/SerializedEventData',
|
||||
namedImports: [{ name: 'SerializedEventData', isTypeOnly: true }],
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user