Compare commits
39 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7947b1b843 | ||
|
|
2b399fc94e | ||
|
|
44ba7725ae | ||
|
|
65e01400c0 | ||
|
|
62ea14a072 | ||
|
|
8cdd2a3319 | ||
|
|
1db242d399 | ||
|
|
a583ee405b | ||
|
|
cd73088be6 | ||
|
|
15938c1fca | ||
|
|
071980d511 | ||
|
|
e3d7d0199d | ||
|
|
30b8663a74 | ||
|
|
69868a0ab6 | ||
|
|
4f88aab57f | ||
|
|
5d438bb70c | ||
|
|
a1de37e424 | ||
|
|
a8d4039629 | ||
|
|
4fd80ee470 | ||
|
|
8d24551e71 | ||
|
|
34e3b1b90b | ||
|
|
e1c200527d | ||
|
|
3ee1b528a1 | ||
|
|
6ef15713b1 | ||
|
|
dc50dbdb20 | ||
|
|
0adaf8aa7b | ||
|
|
41ee6eac7a | ||
|
|
b4f996e0c4 | ||
|
|
5c58254eb4 | ||
|
|
192a842f57 | ||
|
|
1b469168c8 | ||
|
|
57de05ea74 | ||
|
|
a174aff5c8 | ||
|
|
96fc98e710 | ||
|
|
9a963ddeca | ||
|
|
13afef5d1d | ||
|
|
83bc6d1a1b | ||
|
|
755f1c92d1 | ||
|
|
6d3ff4c9ce |
18
.github/workflows/ci-cross-version-upgrade.yaml
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
name: CI Cross-Version Upgrade
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
cross-version-upgrade:
|
||||
timeout-minutes: 45
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Placeholder
|
||||
run: echo "Cross-version upgrade CI — not yet implemented"
|
||||
203
README.md
|
|
@ -1,126 +1,170 @@
|
|||
<p align="center">
|
||||
<a href="https://www.producthunt.com/products/twenty-crm?launch=twenty-2-0">
|
||||
<img src="./packages/twenty-website/public/images/readme/product-hunt-banner.png" alt="We're live on Product Hunt — Support us" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.twenty.com">
|
||||
<img src="./packages/twenty-website/public/images/core/logo.svg" width="100px" alt="Twenty logo" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h2 align="center" >The #1 Open-Source CRM </h2>
|
||||
|
||||
<p align="center"><a href="https://twenty.com">🌐 Website</a> · <a href="https://docs.twenty.com">📚 Documentation</a> · <a href="https://github.com/orgs/twentyhq/projects/1"><img src="./packages/twenty-website/public/images/readme/planner-icon.svg" width="12" height="12"/> Roadmap </a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty"><img src="./packages/twenty-website/public/images/readme/figma-icon.png" width="12" height="12"/> Figma</a></p>
|
||||
<br />
|
||||
<h2 align="center" >The #1 Open-Source CRM</h2>
|
||||
|
||||
<p align="center"><a href="https://twenty.com"><img src="./packages/twenty-website/public/images/readme/globe-icon.svg" width="12" height="12"/> Website</a> · <a href="https://docs.twenty.com"><img src="./packages/twenty-website/public/images/readme/book-icon.svg" width="12" height="12"/> Documentation</a> · <a href="https://github.com/orgs/twentyhq/projects/1"><img src="./packages/twenty-website/public/images/readme/map-icon.svg" width="12" height="12"/> Roadmap </a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty"><img src="./packages/twenty-website/public/images/readme/figma-icon.png" width="12" height="12"/> Figma</a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.twenty.com">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/github-cover-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/github-cover-light.png" />
|
||||
<img src="./packages/twenty-website/public/images/readme/github-cover-light.png" alt="Cover" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/github-cover-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/github-cover-light.png" />
|
||||
<img src="./packages/twenty-website/public/images/readme/github-cover-light.png" alt="Twenty banner" />
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
||||
# Installation
|
||||
|
||||
See:
|
||||
🚀 [Self-hosting](https://docs.twenty.com/developers/self-host/capabilities/docker-compose)
|
||||
🖥️ [Local Setup](https://docs.twenty.com/developers/contribute/capabilities/local-setup)
|
||||
|
||||
# Why Twenty
|
||||
|
||||
We built Twenty for three reasons:
|
||||
Twenty gives technical teams the building blocks for a custom CRM that meets complex business needs and quickly adapts as the business evolves. Twenty is the CRM you build, ship, and version like the rest of your stack.
|
||||
|
||||
**CRMs are too expensive, and users are trapped.** Companies use locked-in customer data to hike prices. It shouldn't be that way.
|
||||
|
||||
**A fresh start is required to build a better experience.** We can learn from past mistakes and craft a cohesive experience inspired by new UX patterns from tools like Notion, Airtable or Linear.
|
||||
|
||||
**We believe in open-source and community.** Hundreds of developers are already building Twenty together. Once we have plugin capabilities, a whole ecosystem will grow around it.
|
||||
<a href="https://twenty.com/why-twenty"><img src="./packages/twenty-website/public/images/readme/star-icon.svg" width="14" height="14"/> Learn more about why we built Twenty</a>
|
||||
|
||||
<br />
|
||||
|
||||
# What You Can Do With Twenty
|
||||
# Installation
|
||||
|
||||
Please feel free to flag any specific needs you have by creating an issue.
|
||||
### <img src="./packages/twenty-website/public/images/readme/globe-icon.svg" width="14" height="14"/> Cloud
|
||||
|
||||
Below are a few features we have implemented to date:
|
||||
The fastest way to get started. Sign up at [twenty.com](https://twenty.com) and spin up a workspace in under a minute, with no infrastructure to manage and always up to date.
|
||||
|
||||
+ [Personalize layouts with filters, sort, group by, kanban and table views](#personalize-layouts-with-filters-sort-group-by-kanban-and-table-views)
|
||||
+ [Customize your objects and fields](#customize-your-objects-and-fields)
|
||||
+ [Create and manage permissions with custom roles](#create-and-manage-permissions-with-custom-roles)
|
||||
+ [Automate workflow with triggers and actions](#automate-workflow-with-triggers-and-actions)
|
||||
+ [Emails, calendar events, files, and more](#emails-calendar-events-files-and-more)
|
||||
### <img src="./packages/twenty-website/public/images/readme/book-icon.svg" width="14" height="14"/> Build an app
|
||||
|
||||
Scaffold a new app with the Twenty CLI:
|
||||
|
||||
## Personalize layouts with filters, sort, group by, kanban and table views
|
||||
```bash
|
||||
npx create-twenty-app my-app
|
||||
```
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/views-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/views-light.png" />
|
||||
<img src="./packages/twenty-website/public/images/readme/views-light.png" alt="Companies Kanban Views" />
|
||||
</picture>
|
||||
</p>
|
||||
Define objects, fields, and views as code:
|
||||
|
||||
## Customize your objects and fields
|
||||
```ts
|
||||
import { defineObject, FieldType } from 'twenty-sdk/define';
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/data-model-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/data-model-light.png" />
|
||||
<img src="./packages/twenty-website/public/images/readme/data-model-light.png" alt="Setting Custom Objects" />
|
||||
</picture>
|
||||
</p>
|
||||
export default defineObject({
|
||||
nameSingular: 'deal',
|
||||
namePlural: 'deals',
|
||||
labelSingular: 'Deal',
|
||||
labelPlural: 'Deals',
|
||||
fields: [
|
||||
{ name: 'name', label: 'Name', type: FieldType.TEXT },
|
||||
{ name: 'amount', label: 'Amount', type: FieldType.CURRENCY },
|
||||
{ name: 'closeDate', label: 'Close Date', type: FieldType.DATE_TIME },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Create and manage permissions with custom roles
|
||||
Then ship it to your workspace:
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/permissions-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/permissions-light.png" />
|
||||
<img src="./packages/twenty-website/public/images/readme/permissions-light.png" alt="Permissions" />
|
||||
</picture>
|
||||
</p>
|
||||
```bash
|
||||
npx twenty deploy
|
||||
```
|
||||
|
||||
## Automate workflow with triggers and actions
|
||||
See the [app development guide](https://docs.twenty.com/developers/extend/apps/getting-started) for objects, views, agents, and logic functions.
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/workflows-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/workflows-light.png" />
|
||||
<img src="./packages/twenty-website/public/images/readme/workflows-light.png" alt="Workflows" />
|
||||
</picture>
|
||||
</p>
|
||||
### <img src="./packages/twenty-website/public/images/readme/rocket-icon.svg" width="14" height="14"/> Self-hosting
|
||||
|
||||
## Emails, calendar events, files, and more
|
||||
Run Twenty on your own infrastructure with [Docker Compose](https://docs.twenty.com/developers/self-host/capabilities/docker-compose), or contribute locally via the [local setup guide](https://docs.twenty.com/developers/contribute/capabilities/local-setup).
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/plus-other-features-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-website/public/images/readme/plus-other-features-light.png" />
|
||||
<img src="./packages/twenty-website/public/images/readme/plus-other-features-light.png" alt="Other Features" />
|
||||
</picture>
|
||||
</p>
|
||||
<br />
|
||||
<br />
|
||||
|
||||
# Everything you need
|
||||
|
||||
Twenty gives you the building blocks of a modern CRM (objects, views, workflows, and agents) and lets you extend them as code. Here's a tour of what's in the box.
|
||||
|
||||
Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduction"><img src="./packages/twenty-website/public/images/readme/planner-icon.svg" width="14" height="14"/> User Guide</a> for product walkthroughs, or the <a href="https://docs.twenty.com"><img src="./packages/twenty-website/public/images/readme/book-icon.svg" width="14" height="14"/> Documentation</a> for developer reference.
|
||||
|
||||
<table align="center">
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-build-apps-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-build-apps-light.png" />
|
||||
<img src="./packages/twenty-website/public/images/readme/v2-build-apps-light.png" alt="Create your apps" />
|
||||
</picture>
|
||||
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/getting-started"><img src="./packages/twenty-website/public/images/readme/code-icon.svg" width="16" height="16"/> Learn more about apps in doc</a></p>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-version-control-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-version-control-light.png" />
|
||||
<img src="./packages/twenty-website/public/images/readme/v2-version-control-light.png" alt="Stay on top with version control" />
|
||||
</picture>
|
||||
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/publishing"><img src="./packages/twenty-website/public/images/readme/monitor-icon.svg" width="16" height="16"/> Learn more about version control in doc</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-all-tools-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-all-tools-light.png" />
|
||||
<img src="./packages/twenty-website/public/images/readme/v2-all-tools-light.png" alt="All the tools you need to build anything" />
|
||||
</picture>
|
||||
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/building"><img src="./packages/twenty-website/public/images/readme/rocket-icon.svg" width="16" height="16"/> Learn more about primitives in doc</a></p>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-tools-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-tools-light.png" />
|
||||
<img src="./packages/twenty-website/public/images/readme/v2-tools-light.png" alt="Customize your layouts" />
|
||||
</picture>
|
||||
<p align="center"><a href="https://docs.twenty.com/user-guide/layout/overview"><img src="./packages/twenty-website/public/images/readme/planner-icon.svg" width="16" height="16"/> Learn more about layouts in doc</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-ai-agents-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-ai-agents-light.png" />
|
||||
<img src="./packages/twenty-website/public/images/readme/v2-ai-agents-light.png" alt="AI agents and chats" />
|
||||
</picture>
|
||||
<p align="center"><a href="https://docs.twenty.com/user-guide/ai/overview"><img src="./packages/twenty-website/public/images/readme/message-icon.svg" width="16" height="16"/> Learn more about AI in doc</a></p>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-crm-tools-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-crm-tools-light.png" />
|
||||
<img src="./packages/twenty-website/public/images/readme/v2-crm-tools-light.png" alt="Plus all the tools of a good CRM" />
|
||||
</picture>
|
||||
<p align="center"><a href="https://docs.twenty.com/user-guide/introduction"><img src="./packages/twenty-website/public/images/readme/star-icon.svg" width="16" height="16"/> Learn more about CRM features in doc</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
# Stack
|
||||
- [TypeScript](https://www.typescriptlang.org/)
|
||||
- [Nx](https://nx.dev/)
|
||||
- [NestJS](https://nestjs.com/), with [BullMQ](https://bullmq.io/), [PostgreSQL](https://www.postgresql.org/), [Redis](https://redis.io/)
|
||||
- [React](https://reactjs.org/), with [Jotai](https://jotai.org/), [Linaria](https://linaria.dev/) and [Lingui](https://lingui.dev/)
|
||||
|
||||
- <a href="https://www.typescriptlang.org/"><img src="./packages/twenty-website/public/images/readme/stack-typescript.svg" width="14" height="14"/> TypeScript</a>
|
||||
- <a href="https://nx.dev/"><img src="./packages/twenty-website/public/images/readme/stack-nx.svg" width="14" height="14"/> Nx</a>
|
||||
- <a href="https://nestjs.com/"><img src="./packages/twenty-website/public/images/readme/stack-nestjs.svg" width="14" height="14"/> NestJS</a>, with <a href="https://bullmq.io/">BullMQ</a>, <a href="https://www.postgresql.org/"><img src="./packages/twenty-website/public/images/readme/stack-postgresql.svg" width="14" height="14"/> PostgreSQL</a>, <a href="https://redis.io/"><img src="./packages/twenty-website/public/images/readme/stack-redis.svg" width="14" height="14"/> Redis</a>
|
||||
- <a href="https://reactjs.org/"><img src="./packages/twenty-website/public/images/readme/stack-react.svg" width="14" height="14"/> React</a>, with <a href="https://jotai.org/">Jotai</a>, <a href="https://linaria.dev/">Linaria</a> and <a href="https://lingui.dev/">Lingui</a>
|
||||
|
||||
|
||||
|
||||
# Thanks
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.chromatic.com/"><img src="./packages/twenty-website/public/images/readme/chromatic.png" height="30" alt="Chromatic" /></a>
|
||||
<a href="https://greptile.com"><img src="./packages/twenty-website/public/images/readme/greptile.png" height="30" alt="Greptile" /></a>
|
||||
<a href="https://sentry.io/"><img src="./packages/twenty-website/public/images/readme/sentry.png" height="30" alt="Sentry" /></a>
|
||||
<a href="https://crowdin.com/"><img src="./packages/twenty-website/public/images/readme/crowdin.png" height="30" alt="Crowdin" /></a>
|
||||
<a href="https://e2b.dev/"><img src="./packages/twenty-website/public/images/readme/e2b.svg" height="30" alt="E2B" /></a>
|
||||
<a href="https://www.chromatic.com/"><img src="./packages/twenty-website/public/images/readme/chromatic.png" height="28" alt="Chromatic" /></a>
|
||||
|
||||
<a href="https://greptile.com"><img src="./packages/twenty-website/public/images/readme/greptile.png" height="28" alt="Greptile" /></a>
|
||||
|
||||
<a href="https://sentry.io/"><img src="./packages/twenty-website/public/images/readme/sentry.png" height="28" alt="Sentry" /></a>
|
||||
|
||||
<a href="https://crowdin.com/"><img src="./packages/twenty-website/public/images/readme/crowdin.png" height="28" alt="Crowdin" /></a>
|
||||
</p>
|
||||
|
||||
Thanks to these amazing services that we use and recommend for UI testing (Chromatic), code review (Greptile), catching bugs (Sentry) and translating (Crowdin).
|
||||
|
|
@ -128,9 +172,4 @@ Below are a few features we have implemented to date:
|
|||
|
||||
# Join the Community
|
||||
|
||||
- Star the repo
|
||||
- Subscribe to releases (watch -> custom -> releases)
|
||||
- Follow us on [Twitter](https://twitter.com/twentycrm) or [LinkedIn](https://www.linkedin.com/company/twenty/)
|
||||
- Join our [Discord](https://discord.gg/cx5n4Jzs57)
|
||||
- Improve translations on [Crowdin](https://twenty.crowdin.com/twenty)
|
||||
- [Contributions](https://github.com/twentyhq/twenty/contribute) are, of course, most welcome!
|
||||
<p><a href="https://github.com/twentyhq/twenty"><img src="./packages/twenty-website/public/images/readme/star-icon.svg" width="12" height="12"/> Star the repo</a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://github.com/twentyhq/twenty/discussions"><img src="./packages/twenty-website/public/images/readme/message-icon.svg" width="12" height="12"/> Feature requests</a> · <a href="https://github.com/orgs/twentyhq/projects/1/views/35"><img src="./packages/twenty-website/public/images/readme/rocket-icon.svg" width="12" height="12"/> Releases</a> · <a href="https://twitter.com/twentycrm"><img src="./packages/twenty-website/public/images/readme/x-icon.svg" width="12" height="12"/> X</a> · <a href="https://www.linkedin.com/company/twenty/"><img src="./packages/twenty-website/public/images/readme/linkedin-icon.svg" width="12" height="12"/> LinkedIn</a> · <a href="https://twenty.crowdin.com/twenty"><img src="./packages/twenty-website/public/images/readme/language-icon.svg" width="12" height="12"/> Crowdin</a> · <a href="https://github.com/twentyhq/twenty/contribute"><img src="./packages/twenty-website/public/images/readme/code-icon.svg" width="12" height="12"/> Contribute</a></p>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "create-twenty-app",
|
||||
"version": "1.23.0-canary.9",
|
||||
"version": "2.0.0",
|
||||
"description": "Command-line interface to create Twenty application",
|
||||
"main": "dist/cli.cjs",
|
||||
"bin": "dist/cli.cjs",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "twenty-client-sdk",
|
||||
"version": "1.23.0-canary.9",
|
||||
"version": "2.0.0",
|
||||
"sideEffects": false,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -830,6 +830,7 @@ type Workspace {
|
|||
workspaceCustomApplication: Application
|
||||
featureFlags: [FeatureFlag!]
|
||||
billingSubscriptions: [BillingSubscription!]!
|
||||
installedApplications: [Application!]!
|
||||
currentBillingSubscription: BillingSubscription
|
||||
billingEntitlements: [BillingEntitlement!]!
|
||||
hasValidEnterpriseKey: Boolean!
|
||||
|
|
@ -1617,7 +1618,6 @@ type FeatureFlag {
|
|||
enum FeatureFlagKey {
|
||||
IS_UNIQUE_INDEXES_ENABLED
|
||||
IS_JSON_FILTER_ENABLED
|
||||
IS_AI_ENABLED
|
||||
IS_COMMAND_MENU_ITEM_ENABLED
|
||||
IS_MARKETPLACE_SETTING_TAB_VISIBLE
|
||||
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED
|
||||
|
|
|
|||
|
|
@ -619,6 +619,7 @@ export interface Workspace {
|
|||
workspaceCustomApplication?: Application
|
||||
featureFlags?: FeatureFlag[]
|
||||
billingSubscriptions: BillingSubscription[]
|
||||
installedApplications: Application[]
|
||||
currentBillingSubscription?: BillingSubscription
|
||||
billingEntitlements: BillingEntitlement[]
|
||||
hasValidEnterpriseKey: Scalars['Boolean']
|
||||
|
|
@ -1325,7 +1326,7 @@ export interface FeatureFlag {
|
|||
__typename: 'FeatureFlag'
|
||||
}
|
||||
|
||||
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_AI_ENABLED' | 'IS_COMMAND_MENU_ITEM_ENABLED' | 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_CONNECTED_ACCOUNT_MIGRATED' | 'IS_RICH_TEXT_V1_MIGRATED' | 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' | 'IS_DATASOURCE_MIGRATED'
|
||||
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_COMMAND_MENU_ITEM_ENABLED' | 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_CONNECTED_ACCOUNT_MIGRATED' | 'IS_RICH_TEXT_V1_MIGRATED' | 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' | 'IS_DATASOURCE_MIGRATED'
|
||||
|
||||
export interface WorkspaceUrls {
|
||||
customUrl?: Scalars['String']
|
||||
|
|
@ -3504,6 +3505,7 @@ export interface WorkspaceGenqlSelection{
|
|||
workspaceCustomApplication?: ApplicationGenqlSelection
|
||||
featureFlags?: FeatureFlagGenqlSelection
|
||||
billingSubscriptions?: BillingSubscriptionGenqlSelection
|
||||
installedApplications?: ApplicationGenqlSelection
|
||||
currentBillingSubscription?: BillingSubscriptionGenqlSelection
|
||||
billingEntitlements?: BillingEntitlementGenqlSelection
|
||||
hasValidEnterpriseKey?: boolean | number
|
||||
|
|
@ -8561,7 +8563,6 @@ export const enumLogicFunctionExecutionStatus = {
|
|||
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_SETTING_TAB_VISIBLE: 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' as const,
|
||||
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED: 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' as const,
|
||||
|
|
|
|||
|
|
@ -1765,6 +1765,9 @@ export default {
|
|||
"billingSubscriptions": [
|
||||
138
|
||||
],
|
||||
"installedApplications": [
|
||||
52
|
||||
],
|
||||
"currentBillingSubscription": [
|
||||
138
|
||||
],
|
||||
|
|
|
|||
37
packages/twenty-docker/twenty-website-new/Dockerfile
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
FROM node:24-alpine AS twenty-website-new-build
|
||||
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./package.json .
|
||||
COPY ./yarn.lock .
|
||||
COPY ./.yarnrc.yml .
|
||||
COPY ./tsconfig.base.json .
|
||||
COPY ./nx.json .
|
||||
COPY ./.yarn/releases /app/.yarn/releases
|
||||
COPY ./.yarn/patches /app/.yarn/patches
|
||||
COPY ./packages/twenty-oxlint-rules /app/packages/twenty-oxlint-rules
|
||||
COPY ./packages/twenty-website-new/package.json /app/packages/twenty-website-new/package.json
|
||||
|
||||
RUN yarn
|
||||
|
||||
COPY ./packages/twenty-website-new /app/packages/twenty-website-new
|
||||
RUN npx nx build twenty-website-new
|
||||
|
||||
FROM node:24-alpine AS twenty-website-new
|
||||
|
||||
WORKDIR /app/packages/twenty-website-new
|
||||
|
||||
COPY --from=twenty-website-new-build /app /app
|
||||
|
||||
WORKDIR /app/packages/twenty-website-new
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/twentyhq/twenty
|
||||
LABEL org.opencontainers.image.description="This image provides a consistent and reproducible environment for the new marketing website."
|
||||
|
||||
RUN chown -R 1000 /app
|
||||
|
||||
# Use non root user with uid 1000
|
||||
USER 1000
|
||||
|
||||
CMD ["/bin/sh", "-c", "npx nx start twenty-website-new"]
|
||||
47
packages/twenty-docs/custom.css
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/* CTA button — solid dark pill in light mode, inverted in dark mode */
|
||||
#topbar-cta-button a {
|
||||
background-color: #141414 !important;
|
||||
color: #ffffff !important;
|
||||
padding: 0.5rem 1.25rem !important;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
#topbar-cta-button a:hover {
|
||||
opacity: 0.85 !important;
|
||||
}
|
||||
|
||||
:is(.dark, [data-theme="dark"]) #topbar-cta-button a {
|
||||
background-color: #ffffff !important;
|
||||
color: #141414 !important;
|
||||
}
|
||||
|
||||
/* Center the tab links */
|
||||
.nav-tabs {
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* HACK: Hide the "Overview" sidebar group header in the Developers tab.
|
||||
*
|
||||
* Mintlify requires every page to be inside a named group, and the tab's
|
||||
* landing page is always the first page of the first group. We want the
|
||||
* Developer Overview page to be the tab landing without showing a lonely
|
||||
* "Overview" group header above a single item.
|
||||
*
|
||||
* We use :has() to scope the rule to only the sidebar group that contains
|
||||
* the developers/introduction link, so other tabs are unaffected.
|
||||
*/
|
||||
div:has(> .sidebar-group a[href="/developers/introduction"]),
|
||||
div:has(> .sidebar-group a[href="/user-guide/introduction"]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Remove the top margin on the group that follows the hidden overview group,
|
||||
so it sits flush at the top of the sidebar without a gap. */
|
||||
div:has(> .sidebar-group a[href="/developers/introduction"]) + div,
|
||||
div:has(> .sidebar-group a[href="/user-guide/introduction"]) + div {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
title: Backend Commands
|
||||
icon: "terminal"
|
||||
---
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
title: Bugs, Requests & Pull Requests
|
||||
icon: "bug"
|
||||
info: Report issues, request features, and contribute code
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
title: Best Practices
|
||||
icon: "star"
|
||||
---
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
title: Folder Architecture
|
||||
icon: "folder-tree"
|
||||
info: A detailed look into our folder architecture
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
title: Frontend Commands
|
||||
icon: "terminal"
|
||||
---
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
title: Style Guide
|
||||
icon: "paintbrush"
|
||||
---
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
title: Local Setup
|
||||
icon: "laptop-code"
|
||||
description: "The guide for contributors (or curious developers) who want to run Twenty locally."
|
||||
---
|
||||
|
||||
|
|
|
|||
77
packages/twenty-docs/developers/contribute/commands.mdx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
---
|
||||
title: Commands
|
||||
icon: "terminal"
|
||||
description: Useful commands for developing Twenty.
|
||||
---
|
||||
|
||||
Commands can be run from the repository root using `npx nx`. Use `npx nx run {project}:{command}` for explicit targeting.
|
||||
|
||||
## Starting the App
|
||||
|
||||
```bash
|
||||
npx nx start twenty-front # Frontend dev server (http://localhost:3001)
|
||||
npx nx start twenty-server # Backend server (http://localhost:3000)
|
||||
npx nx run twenty-server:worker # Background worker
|
||||
```
|
||||
|
||||
## Database
|
||||
|
||||
```bash
|
||||
npx nx database:reset twenty-server # Reset and seed database
|
||||
npx nx run twenty-server:database:migrate:prod # Run migrations
|
||||
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow> # Generate a migration
|
||||
```
|
||||
|
||||
## Linting
|
||||
|
||||
```bash
|
||||
npx nx lint:diff-with-main twenty-front # Lint changed files (fastest)
|
||||
npx nx lint:diff-with-main twenty-server
|
||||
npx nx lint twenty-front --configuration=fix # Auto-fix
|
||||
```
|
||||
|
||||
## Type Checking
|
||||
|
||||
```bash
|
||||
npx nx typecheck twenty-front
|
||||
npx nx typecheck twenty-server
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Frontend
|
||||
npx nx test twenty-front # Jest unit tests
|
||||
npx nx storybook:build twenty-front # Build Storybook
|
||||
npx nx storybook:test twenty-front # Storybook tests
|
||||
|
||||
# Backend
|
||||
npx nx run twenty-server:test:unit # Unit tests
|
||||
npx nx run twenty-server:test:integration # Integration tests
|
||||
npx nx run twenty-server:test:integration:with-db-reset # Integration with DB reset
|
||||
|
||||
# Single file (fastest)
|
||||
npx jest path/to/test.test.ts --config=packages/{project}/jest.config.mjs
|
||||
```
|
||||
|
||||
## GraphQL
|
||||
|
||||
```bash
|
||||
npx nx run twenty-front:graphql:generate # Regenerate types
|
||||
npx nx run twenty-front:graphql:generate --configuration=metadata # Metadata schema
|
||||
```
|
||||
|
||||
## Translations
|
||||
|
||||
```bash
|
||||
npx nx run twenty-front:lingui:extract # Extract strings
|
||||
npx nx run twenty-front:lingui:compile # Compile translations
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
npx nx build twenty-shared # Must be built first
|
||||
npx nx build twenty-front
|
||||
npx nx build twenty-server
|
||||
```
|
||||
176
packages/twenty-docs/developers/contribute/style-guide.mdx
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
---
|
||||
title: Style Guide
|
||||
icon: "paintbrush"
|
||||
description: Code conventions and best practices for contributing to Twenty.
|
||||
---
|
||||
|
||||
## React
|
||||
|
||||
### Functional components only
|
||||
|
||||
Always use TSX functional components with named exports.
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
const MyComponent = () => {
|
||||
return <div>Hello World</div>;
|
||||
};
|
||||
export default MyComponent;
|
||||
|
||||
// ✅ Good
|
||||
export function MyComponent() {
|
||||
return <div>Hello World</div>;
|
||||
};
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
Create a type named `{ComponentName}Props`. Use destructuring. Don't use `React.FC`.
|
||||
|
||||
```tsx
|
||||
type MyComponentProps = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const MyComponent = ({ name }: MyComponentProps) => <div>Hello {name}</div>;
|
||||
```
|
||||
|
||||
### No single-variable prop spreading
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
const MyComponent = (props: MyComponentProps) => <Other {...props} />;
|
||||
|
||||
// ✅ Good
|
||||
const MyComponent = ({ prop1, prop2 }: MyComponentProps) => <Other {...{ prop1, prop2 }} />;
|
||||
```
|
||||
|
||||
## State Management
|
||||
|
||||
### Jotai atoms for global state
|
||||
|
||||
```tsx
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
|
||||
export const myAtomState = createAtomState<string>({
|
||||
key: 'myAtomState',
|
||||
defaultValue: 'default value',
|
||||
});
|
||||
```
|
||||
|
||||
- Prefer atoms over prop drilling
|
||||
- Don't use `useRef` for state — use `useState` or atoms
|
||||
- Use atom families and selectors for lists
|
||||
|
||||
### Avoid unnecessary re-renders
|
||||
|
||||
- Extract `useEffect` and data fetching into sibling sidecar components
|
||||
- Prefer event handlers (`handleClick`, `handleChange`) over `useEffect`
|
||||
- Don't use `React.memo()` — fix the root cause instead
|
||||
- Limit `useCallback` / `useMemo` usage
|
||||
|
||||
```tsx
|
||||
// ❌ Bad — useEffect in the same component causes re-renders
|
||||
export const Page = () => {
|
||||
const [data, setData] = useAtomState(dataState);
|
||||
const [dep] = useAtomState(depState);
|
||||
useEffect(() => { setData(dep); }, [dep]);
|
||||
return <div>{data}</div>;
|
||||
};
|
||||
|
||||
// ✅ Good — extract into sibling
|
||||
export const PageData = () => {
|
||||
const [data, setData] = useAtomState(dataState);
|
||||
const [dep] = useAtomState(depState);
|
||||
useEffect(() => { setData(dep); }, [dep]);
|
||||
return <></>;
|
||||
};
|
||||
export const Page = () => {
|
||||
const [data] = useAtomState(dataState);
|
||||
return <div>{data}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
## TypeScript
|
||||
|
||||
- **`type` over `interface`** — more flexible, easier to compose
|
||||
- **String literals over enums** — except for GraphQL codegen enums and internal library APIs
|
||||
- **No `any`** — strict TypeScript enforced
|
||||
- **No type imports** — use regular imports (enforced by Oxlint `typescript/consistent-type-imports`)
|
||||
- **Use [Zod](https://github.com/colinhacks/zod)** for runtime validation of untyped objects
|
||||
|
||||
## JavaScript
|
||||
|
||||
```tsx
|
||||
// Use nullish-coalescing (??) instead of ||
|
||||
const value = process.env.MY_VALUE ?? 'default';
|
||||
|
||||
// Use optional chaining
|
||||
onClick?.();
|
||||
```
|
||||
|
||||
## Naming
|
||||
|
||||
- **Variables**: camelCase, descriptive (`email` not `value`, `fieldMetadata` not `fm`)
|
||||
- **Constants**: SCREAMING_SNAKE_CASE
|
||||
- **Types/Classes**: PascalCase
|
||||
- **Files/directories**: kebab-case (`.component.tsx`, `.service.ts`, `.entity.ts`)
|
||||
- **Event handlers**: `handleClick` (not `onClick` for the handler function)
|
||||
- **Component props**: prefix with component name (`ButtonProps`)
|
||||
- **Styled components**: prefix with `Styled` (`StyledTitle`)
|
||||
|
||||
## Styling
|
||||
|
||||
Use [Linaria](https://github.com/callstack/linaria) styled components. Use theme values — avoid hardcoded `px`, `rem`, or colors.
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
const StyledButton = styled.button`
|
||||
color: #333333;
|
||||
font-size: 1rem;
|
||||
margin-left: 4px;
|
||||
`;
|
||||
|
||||
// ✅ Good
|
||||
const StyledButton = styled.button`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
```
|
||||
|
||||
## Imports
|
||||
|
||||
Use aliases instead of relative paths:
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
import { Foo } from '../../../../../testing/decorators/Foo';
|
||||
|
||||
// ✅ Good
|
||||
import { Foo } from '~/testing/decorators/Foo';
|
||||
import { Bar } from '@/modules/bar/components/Bar';
|
||||
```
|
||||
|
||||
## Folder Structure
|
||||
|
||||
```
|
||||
front
|
||||
└── modules/ # Feature modules
|
||||
│ └── module1/
|
||||
│ ├── components/
|
||||
│ ├── constants/
|
||||
│ ├── contexts/
|
||||
│ ├── graphql/ (fragments, queries, mutations)
|
||||
│ ├── hooks/
|
||||
│ ├── states/ (atoms, selectors)
|
||||
│ ├── types/
|
||||
│ └── utils/
|
||||
└── pages/ # Route-level components
|
||||
└── ui/ # Reusable UI components (display, input, feedback, ...)
|
||||
```
|
||||
|
||||
- Modules can import from other modules, but `ui/` should stay dependency-free
|
||||
- Use `internal/` subfolders for module-private code
|
||||
- Components under 300 lines, services under 500 lines
|
||||
|
|
@ -1,140 +1,55 @@
|
|||
---
|
||||
title: APIs
|
||||
description: Query and modify your CRM data programmatically using REST or GraphQL.
|
||||
icon: "plug"
|
||||
description: REST and GraphQL APIs generated from your workspace schema.
|
||||
---
|
||||
|
||||
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
|
||||
|
||||
Twenty was built to be developer-friendly, offering powerful APIs that adapt to your custom data model. We provide four distinct API types to meet different integration needs.
|
||||
## Schema-per-tenant APIs
|
||||
|
||||
## Developer-First Approach
|
||||
There is no static API reference for Twenty. Each workspace has its own schema — when you add a custom object (say `Invoice`), it immediately gets REST and GraphQL endpoints identical to built-in objects like `Company` or `Person`. The API is generated from the schema, so endpoints use your object and field names directly — no opaque IDs.
|
||||
|
||||
Twenty generates APIs specifically for your data model:
|
||||
- **No long IDs required**: Use your object and field names directly in endpoints
|
||||
- **Standard and custom objects treated equally**: Your custom objects get the same API treatment as built-in ones
|
||||
- **Dedicated endpoints**: Each object and field gets its own API endpoint
|
||||
- **Custom documentation**: Generated specifically for your workspace's data model
|
||||
Your workspace-specific API documentation is available under **Settings → API & Webhooks** after creating an API key. It includes an interactive playground where you can execute real calls against your data.
|
||||
|
||||
<Note>
|
||||
Your personalized API documentation is available under **Settings → API & Webhooks** after creating an API key. Since Twenty generates APIs that match your custom data model, the documentation is unique to your workspace.
|
||||
</Note>
|
||||
## Two APIs
|
||||
|
||||
## The Two API Types
|
||||
**Core API** — `/rest/` and `/graphql/`
|
||||
|
||||
### Core API
|
||||
Accessed on `/rest/` or `/graphql/`
|
||||
CRUD on records: People, Companies, Opportunities, your custom objects. Query, filter, traverse relations.
|
||||
|
||||
Work with your actual **records** (the data):
|
||||
- Create, read, update, delete People, Companies, Opportunities, etc.
|
||||
- Query and filter data
|
||||
- Manage record relationships
|
||||
**Metadata API** — `/rest/metadata/` and `/metadata/`
|
||||
|
||||
### Metadata API
|
||||
Accessed on `/rest/metadata/` or `/metadata/`
|
||||
Schema management: create/modify/delete objects, fields, and relations. This is how you programmatically change your data model.
|
||||
|
||||
Manage your **workspace and data model**:
|
||||
- Create, modify, or delete objects and fields
|
||||
- Configure workspace settings
|
||||
- Define relationships between objects
|
||||
Both are available as REST and GraphQL. GraphQL adds batch upserts and the ability to traverse relations in a single query. Same underlying data either way.
|
||||
|
||||
## REST vs GraphQL
|
||||
|
||||
Both Core and Metadata APIs are available in REST and GraphQL formats:
|
||||
|
||||
| Format | Available Operations |
|
||||
|--------|---------------------|
|
||||
| **REST** | CRUD, batch operations, upserts |
|
||||
| **GraphQL** | Same + **batch upserts**, relationship queries in one call |
|
||||
|
||||
Choose based on your needs — both formats access the same data.
|
||||
|
||||
## API Endpoints
|
||||
## Base URLs
|
||||
|
||||
| Environment | Base URL |
|
||||
|-------------|----------|
|
||||
| **Cloud** | `https://api.twenty.com/` |
|
||||
| **Self-Hosted** | `https://{your-domain}/` |
|
||||
| Cloud | `https://api.twenty.com/` |
|
||||
| Self-Hosted | `https://{your-domain}/` |
|
||||
|
||||
## Authentication
|
||||
|
||||
Every API request requires an API key in the header:
|
||||
|
||||
```
|
||||
Authorization: Bearer YOUR_API_KEY
|
||||
```
|
||||
|
||||
### Create an API Key
|
||||
|
||||
1. Go to **Settings → APIs & Webhooks**
|
||||
2. Click **+ Create key**
|
||||
3. Configure:
|
||||
- **Name**: Descriptive name for the key
|
||||
- **Expiration Date**: When the key expires
|
||||
4. Click **Save**
|
||||
5. **Copy immediately** — the key is only shown once
|
||||
Create an API key in **Settings → API & Webhooks → + Create key**. Copy it immediately — it's shown once. Keys can be scoped to a specific role under **Settings → Roles → Assignment tab** to limit what they can access.
|
||||
|
||||
<VimeoEmbed videoId="928786722" title="Creating API key" />
|
||||
|
||||
<Warning>
|
||||
Your API key grants access to sensitive data. Don't share it with untrusted services. If compromised, disable it immediately and generate a new one.
|
||||
</Warning>
|
||||
For OAuth-based access (external apps acting on behalf of users), see [OAuth](/developers/extend/oauth).
|
||||
|
||||
### Assign a Role to an API Key
|
||||
## Batch operations
|
||||
|
||||
For better security, assign a specific role to limit access:
|
||||
Both REST and GraphQL support batching up to 60 records per request — create, update, or delete. GraphQL also supports batch upsert (create-or-update in one call) using plural names like `CreateCompanies`.
|
||||
|
||||
1. Go to **Settings → Roles**
|
||||
2. Click on the role to assign
|
||||
3. Open the **Assignment** tab
|
||||
4. Under **API Keys**, click **+ Assign to API key**
|
||||
5. Select the API key
|
||||
|
||||
The key will inherit that role's permissions. See [Permissions](/user-guide/permissions-access/capabilities/permissions) for details.
|
||||
|
||||
### Manage API Keys
|
||||
|
||||
**Regenerate**: Settings → APIs & Webhooks → Click key → **Regenerate**
|
||||
|
||||
**Delete**: Settings → APIs & Webhooks → Click key → **Delete**
|
||||
|
||||
## API Playground
|
||||
|
||||
Test your APIs directly in the browser with our built-in playground — available for both **REST** and **GraphQL**.
|
||||
|
||||
### Access the Playground
|
||||
|
||||
1. Go to **Settings → APIs & Webhooks**
|
||||
2. Create an API key (required)
|
||||
3. Click on **REST API** or **GraphQL API** to open the playground
|
||||
|
||||
### What You Get
|
||||
|
||||
- **Interactive documentation**: Generated for your specific data model
|
||||
- **Live testing**: Execute real API calls against your workspace
|
||||
- **Schema explorer**: Browse available objects, fields, and relationships
|
||||
- **Request builder**: Construct queries with autocomplete
|
||||
|
||||
The playground reflects your custom objects and fields, so documentation is always accurate for your workspace.
|
||||
|
||||
## Batch Operations
|
||||
|
||||
Both REST and GraphQL support batch operations:
|
||||
- **Batch size**: Up to 60 records per request
|
||||
- **Operations**: Create, update, delete multiple records
|
||||
|
||||
**GraphQL-only features:**
|
||||
- **Batch Upsert**: Create or update in one call
|
||||
- Use plural object names (e.g., `CreateCompanies` instead of `CreateCompany`)
|
||||
|
||||
## Rate Limits
|
||||
|
||||
API requests are throttled to ensure platform stability:
|
||||
## Rate limits
|
||||
|
||||
| Limit | Value |
|
||||
|-------|-------|
|
||||
| **Requests** | 100 calls per minute |
|
||||
| **Batch size** | 60 records per call |
|
||||
|
||||
<Tip>
|
||||
Use batch operations to maximize throughput — process up to 60 records in a single API call instead of making individual requests.
|
||||
</Tip>
|
||||
| Requests | 100 per minute |
|
||||
| Batch size | 60 records per call |
|
||||
|
|
|
|||
434
packages/twenty-docs/developers/extend/apps/cli-and-testing.mdx
Normal file
|
|
@ -0,0 +1,434 @@
|
|||
---
|
||||
title: CLI & Testing
|
||||
description: CLI commands, testing setup, public assets, npm packages, remotes, and CI configuration.
|
||||
icon: "terminal"
|
||||
---
|
||||
|
||||
## Public assets (`public/` folder)
|
||||
|
||||
The `public/` folder at the root of your app holds static files — images, icons, fonts, or any other assets your app needs at runtime. These files are automatically included in builds, synced during dev mode, and uploaded to the server.
|
||||
|
||||
Files placed in `public/` are:
|
||||
|
||||
- **Publicly accessible** — once synced to the server, assets are served at a public URL. No authentication is needed to access them.
|
||||
- **Available in front components** — use asset URLs to display images, icons, or any media inside your React components.
|
||||
- **Available in logic functions** — reference asset URLs in emails, API responses, or any server-side logic.
|
||||
- **Used for marketplace metadata** — the `logoUrl` and `screenshots` fields in `defineApplication()` reference files from this folder (e.g., `public/logo.png`). These are displayed in the marketplace when your app is published.
|
||||
- **Auto-synced in dev mode** — when you add, update, or delete a file in `public/`, it is synced to the server automatically. No restart needed.
|
||||
- **Included in builds** — `yarn twenty build` bundles all public assets into the distribution output.
|
||||
|
||||
### Accessing public assets with `getPublicAssetUrl`
|
||||
|
||||
Use the `getPublicAssetUrl` helper from `twenty-sdk` to get the full URL of a file in your `public/` directory. It works in both **logic functions** and **front components**.
|
||||
|
||||
**In a logic function:**
|
||||
|
||||
```ts src/logic-functions/send-invoice.ts
|
||||
import { defineLogicFunction, getPublicAssetUrl } from 'twenty-sdk/define';
|
||||
|
||||
const handler = async (): Promise<any> => {
|
||||
const logoUrl = getPublicAssetUrl('logo.png');
|
||||
const invoiceUrl = getPublicAssetUrl('templates/invoice.png');
|
||||
|
||||
// Fetch the file content (no auth required — public endpoint)
|
||||
const response = await fetch(invoiceUrl);
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
return { logoUrl, size: buffer.byteLength };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'a1b2c3d4-...',
|
||||
name: 'send-invoice',
|
||||
description: 'Sends an invoice with the app logo',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**In a front component:**
|
||||
|
||||
```tsx src/front-components/company-card.tsx
|
||||
import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define';
|
||||
|
||||
export default defineFrontComponent(() => {
|
||||
const logoUrl = getPublicAssetUrl('logo.png');
|
||||
|
||||
return <img src={logoUrl} alt="App logo" />;
|
||||
});
|
||||
```
|
||||
|
||||
The `path` argument is relative to your app's `public/` folder. Both `getPublicAssetUrl('logo.png')` and `getPublicAssetUrl('public/logo.png')` resolve to the same URL — the `public/` prefix is stripped automatically if present.
|
||||
|
||||
## Using npm packages
|
||||
|
||||
You can install and use any npm package in your app. Both logic functions and front components are bundled with [esbuild](https://esbuild.github.io/), which inlines all dependencies into the output — no `node_modules` are needed at runtime.
|
||||
|
||||
### Installing a package
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add axios
|
||||
```
|
||||
|
||||
Then import it in your code:
|
||||
|
||||
```ts src/logic-functions/fetch-data.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import axios from 'axios';
|
||||
|
||||
const handler = async (): Promise<any> => {
|
||||
const { data } = await axios.get('https://api.example.com/data');
|
||||
|
||||
return { data };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '...',
|
||||
name: 'fetch-data',
|
||||
description: 'Fetches data from an external API',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
The same works for front components:
|
||||
|
||||
```tsx src/front-components/chart.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
const DateWidget = () => {
|
||||
return <p>Today is {format(new Date(), 'MMMM do, yyyy')}</p>;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'date-widget',
|
||||
component: DateWidget,
|
||||
});
|
||||
```
|
||||
|
||||
### How bundling works
|
||||
|
||||
The build step uses esbuild to produce a single self-contained file per logic function and per front component. All imported packages are inlined into the bundle.
|
||||
|
||||
**Logic functions** run in a Node.js environment. Node built-in modules (`fs`, `path`, `crypto`, `http`, etc.) are available and do not need to be installed.
|
||||
|
||||
**Front components** run in a Web Worker. Node built-in modules are **not** available — only browser APIs and npm packages that work in a browser environment.
|
||||
|
||||
Both environments have `twenty-client-sdk/core` and `twenty-client-sdk/metadata` available as pre-provided modules — these are not bundled but resolved at runtime by the server.
|
||||
|
||||
## Testing your app
|
||||
|
||||
The SDK provides programmatic APIs that let you build, deploy, install, and uninstall your app from test code. Combined with [Vitest](https://vitest.dev/) and the typed API clients, you can write integration tests that verify your app works end-to-end against a real Twenty server.
|
||||
|
||||
### Setup
|
||||
|
||||
The scaffolded app already includes Vitest. If you set it up manually, install the dependencies:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add -D vitest vite-tsconfig-paths
|
||||
```
|
||||
|
||||
Create a `vitest.config.ts` at the root of your app:
|
||||
|
||||
```ts vitest.config.ts
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tsconfigPaths({
|
||||
projects: ['tsconfig.spec.json'],
|
||||
ignoreConfigErrors: true,
|
||||
}),
|
||||
],
|
||||
test: {
|
||||
testTimeout: 120_000,
|
||||
hookTimeout: 120_000,
|
||||
include: ['src/**/*.integration-test.ts'],
|
||||
setupFiles: ['src/__tests__/setup-test.ts'],
|
||||
env: {
|
||||
TWENTY_API_URL: 'http://localhost:2020',
|
||||
TWENTY_API_KEY: 'your-api-key',
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Create a setup file that verifies the server is reachable before tests run:
|
||||
|
||||
```ts src/__tests__/setup-test.ts
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { beforeAll } from 'vitest';
|
||||
|
||||
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
|
||||
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
|
||||
|
||||
beforeAll(async () => {
|
||||
// Verify the server is running
|
||||
const response = await fetch(`${TWENTY_API_URL}/healthz`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Twenty server is not reachable at ${TWENTY_API_URL}. ` +
|
||||
'Start the server before running integration tests.',
|
||||
);
|
||||
}
|
||||
|
||||
// Write a temporary config for the SDK
|
||||
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(TEST_CONFIG_DIR, 'config.json'),
|
||||
JSON.stringify({
|
||||
remotes: {
|
||||
local: {
|
||||
apiUrl: process.env.TWENTY_API_URL,
|
||||
apiKey: process.env.TWENTY_API_KEY,
|
||||
},
|
||||
},
|
||||
defaultRemote: 'local',
|
||||
}, null, 2),
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
### Programmatic SDK APIs
|
||||
|
||||
The `twenty-sdk/cli` subpath exports functions you can call directly from test code:
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `appBuild` | Build the app and optionally pack a tarball |
|
||||
| `appDeploy` | Upload a tarball to the server |
|
||||
| `appInstall` | Install the app on the active workspace |
|
||||
| `appUninstall` | Uninstall the app from the active workspace |
|
||||
|
||||
Each function returns a result object with `success: boolean` and either `data` or `error`.
|
||||
|
||||
### Writing an integration test
|
||||
|
||||
Here is a full example that builds, deploys, and installs the app, then verifies it appears in the workspace:
|
||||
|
||||
```ts src/__tests__/app-install.integration-test.ts
|
||||
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
|
||||
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
const APP_PATH = process.cwd();
|
||||
|
||||
describe('App installation', () => {
|
||||
beforeAll(async () => {
|
||||
const buildResult = await appBuild({
|
||||
appPath: APP_PATH,
|
||||
tarball: true,
|
||||
onProgress: (message: string) => console.log(`[build] ${message}`),
|
||||
});
|
||||
|
||||
if (!buildResult.success) {
|
||||
throw new Error(`Build failed: ${buildResult.error?.message}`);
|
||||
}
|
||||
|
||||
const deployResult = await appDeploy({
|
||||
tarballPath: buildResult.data.tarballPath!,
|
||||
onProgress: (message: string) => console.log(`[deploy] ${message}`),
|
||||
});
|
||||
|
||||
if (!deployResult.success) {
|
||||
throw new Error(`Deploy failed: ${deployResult.error?.message}`);
|
||||
}
|
||||
|
||||
const installResult = await appInstall({ appPath: APP_PATH });
|
||||
|
||||
if (!installResult.success) {
|
||||
throw new Error(`Install failed: ${installResult.error?.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await appUninstall({ appPath: APP_PATH });
|
||||
});
|
||||
|
||||
it('should find the installed app in the workspace', async () => {
|
||||
const metadataClient = new MetadataApiClient();
|
||||
|
||||
const result = await metadataClient.query({
|
||||
findManyApplications: {
|
||||
id: true,
|
||||
name: true,
|
||||
universalIdentifier: true,
|
||||
},
|
||||
});
|
||||
|
||||
const installedApp = result.findManyApplications.find(
|
||||
(app: { universalIdentifier: string }) =>
|
||||
app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
|
||||
expect(installedApp).toBeDefined();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Running tests
|
||||
|
||||
Make sure your local Twenty server is running, then:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn test
|
||||
```
|
||||
|
||||
Or in watch mode during development:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn test:watch
|
||||
```
|
||||
|
||||
### Type checking
|
||||
|
||||
You can also run type checking on your app without running tests:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty typecheck
|
||||
```
|
||||
|
||||
This runs `tsc --noEmit` and reports any type errors.
|
||||
|
||||
## CLI reference
|
||||
|
||||
Beyond `dev`, `build`, `add`, and `typecheck`, the CLI provides commands for executing functions, viewing logs, and managing app installations.
|
||||
|
||||
### Executing functions (`yarn twenty exec`)
|
||||
|
||||
Run a logic function manually without triggering it via HTTP, cron, or database event:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Execute by function name
|
||||
yarn twenty exec -n create-new-post-card
|
||||
|
||||
# Execute by universalIdentifier
|
||||
yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
||||
|
||||
# Pass a JSON payload
|
||||
yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}'
|
||||
|
||||
# Execute the post-install function
|
||||
yarn twenty exec --postInstall
|
||||
```
|
||||
|
||||
### Viewing function logs (`yarn twenty logs`)
|
||||
|
||||
Stream execution logs for your app's logic functions:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Stream all function logs
|
||||
yarn twenty logs
|
||||
|
||||
# Filter by function name
|
||||
yarn twenty logs -n create-new-post-card
|
||||
|
||||
# Filter by universalIdentifier
|
||||
yarn twenty logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
||||
```
|
||||
|
||||
<Note>
|
||||
This is different from `yarn twenty server logs`, which shows the Docker container logs. `yarn twenty logs` shows your app's function execution logs from the Twenty server.
|
||||
</Note>
|
||||
|
||||
### Uninstalling an app (`yarn twenty uninstall`)
|
||||
|
||||
Remove your app from the active workspace:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty uninstall
|
||||
|
||||
# Skip the confirmation prompt
|
||||
yarn twenty uninstall --yes
|
||||
```
|
||||
|
||||
## Managing remotes
|
||||
|
||||
A **remote** is a Twenty server that your app connects to. During setup, the scaffolder creates one for you automatically. You can add more remotes or switch between them at any time.
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Add a new remote (opens a browser for OAuth login)
|
||||
yarn twenty remote add
|
||||
|
||||
# Connect to a local Twenty server (auto-detects port 2020 or 3000)
|
||||
yarn twenty remote add --local
|
||||
|
||||
# Add a remote non-interactively (useful for CI)
|
||||
yarn twenty remote add --api-url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote
|
||||
|
||||
# List all configured remotes
|
||||
yarn twenty remote list
|
||||
|
||||
# Switch the active remote
|
||||
yarn twenty remote switch <name>
|
||||
```
|
||||
|
||||
Your credentials are stored in `~/.twenty/config.json`.
|
||||
|
||||
## CI with GitHub Actions
|
||||
|
||||
The scaffolder generates a ready-to-use GitHub Actions workflow at `.github/workflows/ci.yml`. It runs your integration tests automatically on every push to `main` and on pull requests.
|
||||
|
||||
The workflow:
|
||||
|
||||
1. Checks out your code
|
||||
2. Spins up a temporary Twenty server using the `twentyhq/twenty/.github/actions/spawn-twenty-docker-image` action
|
||||
3. Installs dependencies with `yarn install --immutable`
|
||||
4. Runs `yarn test` with `TWENTY_API_URL` and `TWENTY_API_KEY` injected from the action outputs
|
||||
|
||||
```yaml .github/workflows/ci.yml
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request: {}
|
||||
|
||||
env:
|
||||
TWENTY_VERSION: latest
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Spawn Twenty instance
|
||||
id: twenty
|
||||
uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
|
||||
with:
|
||||
twenty-version: ${{ env.TWENTY_VERSION }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Run integration tests
|
||||
run: yarn test
|
||||
env:
|
||||
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
|
||||
TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
|
||||
```
|
||||
|
||||
You don't need to configure any secrets — the `spawn-twenty-docker-image` action starts an ephemeral Twenty server directly in the runner and outputs the connection details. The `GITHUB_TOKEN` secret is provided automatically by GitHub.
|
||||
|
||||
To pin a specific Twenty version instead of `latest`, change the `TWENTY_VERSION` environment variable at the top of the workflow.
|
||||
494
packages/twenty-docs/developers/extend/apps/data-model.mdx
Normal file
|
|
@ -0,0 +1,494 @@
|
|||
---
|
||||
title: Data Model
|
||||
description: Define objects, fields, roles, and application metadata with the Twenty SDK.
|
||||
icon: "database"
|
||||
---
|
||||
|
||||
The `twenty-sdk` package provides `defineEntity` functions to declare your app's data model. You must use `export default defineEntity({...})` for the SDK to detect your entities. These functions validate your configuration at build time and provide IDE autocompletion and type safety.
|
||||
|
||||
<Note>
|
||||
**File organization is up to you.**
|
||||
Entity detection is AST-based — the SDK finds `export default defineEntity(...)` calls regardless of where the file lives. Grouping files by type (e.g., `logic-functions/`, `roles/`) is just a convention, not a requirement.
|
||||
</Note>
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="defineRole" description="Configure role permissions and object access">
|
||||
|
||||
Roles encapsulate permissions on your workspace's objects and actions.
|
||||
|
||||
```ts restricted-company-role.ts
|
||||
import {
|
||||
defineRole,
|
||||
PermissionFlag,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk/define';
|
||||
|
||||
export default defineRole({
|
||||
universalIdentifier: '2c80f640-2083-4803-bb49-003e38279de6',
|
||||
label: 'My new role',
|
||||
description: 'A role that can be used in your workspace',
|
||||
canReadAllObjectRecords: false,
|
||||
canUpdateAllObjectRecords: false,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
canUpdateAllSettings: false,
|
||||
canBeAssignedToAgents: false,
|
||||
canBeAssignedToUsers: false,
|
||||
canBeAssignedToApiKeys: false,
|
||||
objectPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
},
|
||||
],
|
||||
fieldPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
fieldUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier,
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
],
|
||||
permissionFlags: [PermissionFlag.APPLICATIONS],
|
||||
});
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineApplication" description="Configure application metadata (required, one per app)">
|
||||
|
||||
Every app must have exactly one `defineApplication` call that describes:
|
||||
|
||||
- **Identity**: identifiers, display name, and description.
|
||||
- **Permissions**: which role its functions and front components use.
|
||||
- **(Optional) Variables**: key–value pairs exposed to your functions as environment variables.
|
||||
- **(Optional) Pre-install / post-install functions**: logic functions that run before or after installation.
|
||||
|
||||
```ts src/application-config.ts
|
||||
import { defineApplication } from 'twenty-sdk/define';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
|
||||
displayName: 'My Twenty App',
|
||||
description: 'My first Twenty app',
|
||||
icon: 'IconWorld',
|
||||
applicationVariables: {
|
||||
DEFAULT_RECIPIENT_NAME: {
|
||||
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
|
||||
description: 'Default recipient name for postcards',
|
||||
value: 'Jane Doe',
|
||||
isSecret: false,
|
||||
},
|
||||
},
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `universalIdentifier` fields are deterministic IDs you own. Generate them once and keep them stable across syncs.
|
||||
- `applicationVariables` become environment variables for your functions and front components (e.g., `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`).
|
||||
- `defaultRoleUniversalIdentifier` must reference a role defined with `defineRole()` (see above).
|
||||
- Pre-install and post-install functions are detected automatically during the manifest build — you do not need to reference them in `defineApplication()`.
|
||||
|
||||
#### Marketplace metadata
|
||||
|
||||
If you plan to [publish your app](/developers/extend/apps/publishing), these optional fields control how it appears in the marketplace:
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `author` | Author or company name |
|
||||
| `category` | App category for marketplace filtering |
|
||||
| `logoUrl` | Path to your app logo (e.g., `public/logo.png`) |
|
||||
| `screenshots` | Array of screenshot paths (e.g., `public/screenshot-1.png`) |
|
||||
| `aboutDescription` | Longer markdown description for the "About" tab. If omitted, the marketplace uses the package's `README.md` from npm |
|
||||
| `websiteUrl` | Link to your website |
|
||||
| `termsUrl` | Link to terms of service |
|
||||
| `emailSupport` | Support email address |
|
||||
| `issueReportUrl` | Link to issue tracker |
|
||||
|
||||
#### Roles and permissions
|
||||
|
||||
The `defaultRoleUniversalIdentifier` in `application-config.ts` designates the default role used by your app's logic functions and front components. See `defineRole` above for details.
|
||||
|
||||
- The runtime token injected as `TWENTY_APP_ACCESS_TOKEN` is derived from this role.
|
||||
- The typed client is restricted to the permissions granted to that role.
|
||||
- Follow least-privilege: create a dedicated role with only the permissions your functions need.
|
||||
|
||||
##### Default function role
|
||||
|
||||
When you scaffold a new app, the CLI creates a default role file:
|
||||
|
||||
```ts src/roles/default-role.ts
|
||||
import { defineRole, PermissionFlag } from 'twenty-sdk/define';
|
||||
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
'b648f87b-1d26-4961-b974-0908fd991061';
|
||||
|
||||
export default defineRole({
|
||||
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
label: 'Default function role',
|
||||
description: 'Default role for function Twenty client',
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: false,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
canUpdateAllSettings: false,
|
||||
canBeAssignedToAgents: false,
|
||||
canBeAssignedToUsers: false,
|
||||
canBeAssignedToApiKeys: false,
|
||||
objectPermissions: [],
|
||||
fieldPermissions: [],
|
||||
permissionFlags: [],
|
||||
});
|
||||
```
|
||||
|
||||
This role's `universalIdentifier` is referenced in `application-config.ts` as `defaultRoleUniversalIdentifier`:
|
||||
|
||||
- **\*.role.ts** defines what the role can do.
|
||||
- **application-config.ts** points to that role so your functions inherit its permissions.
|
||||
|
||||
Notes:
|
||||
- Start from the scaffolded role, then progressively restrict it following least-privilege.
|
||||
- Replace `objectPermissions` and `fieldPermissions` with the objects and fields your functions actually need.
|
||||
- `permissionFlags` control access to platform-level capabilities. Keep them minimal.
|
||||
- See a working example: [`hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts).
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineObject" description="Define custom objects with fields">
|
||||
|
||||
Custom objects describe both schema and behavior for records in your workspace. Use `defineObject()` to define objects with built-in validation:
|
||||
|
||||
```ts postCard.object.ts
|
||||
import { defineObject, FieldType } from 'twenty-sdk/define';
|
||||
|
||||
enum PostCardStatus {
|
||||
DRAFT = 'DRAFT',
|
||||
SENT = 'SENT',
|
||||
DELIVERED = 'DELIVERED',
|
||||
RETURNED = 'RETURNED',
|
||||
}
|
||||
|
||||
export default defineObject({
|
||||
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
|
||||
nameSingular: 'postCard',
|
||||
namePlural: 'postCards',
|
||||
labelSingular: 'Post Card',
|
||||
labelPlural: 'Post Cards',
|
||||
description: 'A post card object',
|
||||
icon: 'IconMail',
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
|
||||
name: 'content',
|
||||
type: FieldType.TEXT,
|
||||
label: 'Content',
|
||||
description: "Postcard's content",
|
||||
icon: 'IconAbc',
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
|
||||
name: 'recipientName',
|
||||
type: FieldType.FULL_NAME,
|
||||
label: 'Recipient name',
|
||||
icon: 'IconUser',
|
||||
},
|
||||
{
|
||||
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
|
||||
name: 'recipientAddress',
|
||||
type: FieldType.ADDRESS,
|
||||
label: 'Recipient address',
|
||||
icon: 'IconHome',
|
||||
},
|
||||
{
|
||||
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
|
||||
name: 'status',
|
||||
type: FieldType.SELECT,
|
||||
label: 'Status',
|
||||
icon: 'IconSend',
|
||||
defaultValue: `'${PostCardStatus.DRAFT}'`,
|
||||
options: [
|
||||
{ value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' },
|
||||
{ value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' },
|
||||
{ value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' },
|
||||
{ value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' },
|
||||
],
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
|
||||
name: 'deliveredAt',
|
||||
type: FieldType.DATE_TIME,
|
||||
label: 'Delivered at',
|
||||
icon: 'IconCheck',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
Key points:
|
||||
|
||||
- Use `defineObject()` for built-in validation and better IDE support.
|
||||
- The `universalIdentifier` must be unique and stable across deployments.
|
||||
- Each field requires a `name`, `type`, `label`, and its own stable `universalIdentifier`.
|
||||
- The `fields` array is optional — you can define objects without custom fields.
|
||||
- You can scaffold new objects using `yarn twenty add`, which guides you through naming, fields, and relationships.
|
||||
|
||||
<Note>
|
||||
**Base fields are created automatically.** When you define a custom object, Twenty automatically adds standard fields
|
||||
such as `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` and `deletedAt`.
|
||||
You don't need to define these in your `fields` array — only add your custom fields.
|
||||
You can override default fields by defining a field with the same name in your `fields` array,
|
||||
but this is not recommended.
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineField — Standard fields" description="Extend existing objects with additional fields">
|
||||
|
||||
Use `defineField()` to add fields to objects you don't own — such as standard Twenty objects (Person, Company, etc.) or objects from other apps. Unlike inline fields in `defineObject()`, standalone fields require an `objectUniversalIdentifier` to specify which object they extend:
|
||||
|
||||
```ts src/fields/company-loyalty-tier.field.ts
|
||||
import { defineField, FieldType } from 'twenty-sdk/define';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890',
|
||||
objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object
|
||||
name: 'loyaltyTier',
|
||||
type: FieldType.SELECT,
|
||||
label: 'Loyalty Tier',
|
||||
icon: 'IconStar',
|
||||
options: [
|
||||
{ value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' },
|
||||
{ value: 'SILVER', label: 'Silver', position: 1, color: 'gray' },
|
||||
{ value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
Key points:
|
||||
- `objectUniversalIdentifier` identifies the target object. For standard objects, use `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` exported from `twenty-sdk`.
|
||||
- When defining fields inline in `defineObject()`, you do **not** need `objectUniversalIdentifier` — it's inherited from the parent object.
|
||||
- `defineField()` is the only way to add fields to objects you didn't create with `defineObject()`.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineField — Relation fields" description="Connect objects together with bidirectional relations">
|
||||
|
||||
Relations connect objects together. In Twenty, relations are always **bidirectional** — you define both sides, and each side references the other.
|
||||
|
||||
There are two relation types:
|
||||
|
||||
| Relation type | Description | Has foreign key? |
|
||||
|---------------|-------------|-----------------|
|
||||
| `MANY_TO_ONE` | Many records of this object point to one record of the target | Yes (`joinColumnName`) |
|
||||
| `ONE_TO_MANY` | One record of this object has many records of the target | No (inverse side) |
|
||||
|
||||
#### How relations work
|
||||
|
||||
Every relation requires **two fields** that reference each other:
|
||||
|
||||
1. The **MANY_TO_ONE** side — lives on the object that holds the foreign key
|
||||
2. The **ONE_TO_MANY** side — lives on the object that owns the collection
|
||||
|
||||
Both fields use `FieldType.RELATION` and cross-reference each other via `relationTargetFieldMetadataUniversalIdentifier`.
|
||||
|
||||
#### Example: Post Card has many Recipients
|
||||
|
||||
Suppose a `PostCard` can be sent to many `PostCardRecipient` records. Each recipient belongs to exactly one post card.
|
||||
|
||||
**Step 1: Define the ONE_TO_MANY side on PostCard** (the "one" side):
|
||||
|
||||
```ts src/fields/post-card-recipients-on-post-card.field.ts
|
||||
import { defineField, FieldType, RelationType } from 'twenty-sdk/define';
|
||||
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
|
||||
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
|
||||
|
||||
// Export so the other side can reference it
|
||||
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
|
||||
// Import from the other side
|
||||
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
|
||||
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'postCardRecipients',
|
||||
label: 'Post Card Recipients',
|
||||
icon: 'IconUsers',
|
||||
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Step 2: Define the MANY_TO_ONE side on PostCardRecipient** (the "many" side — holds the foreign key):
|
||||
|
||||
```ts src/fields/post-card-on-post-card-recipient.field.ts
|
||||
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define';
|
||||
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
|
||||
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
|
||||
|
||||
// Export so the other side can reference it
|
||||
export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222';
|
||||
// Import from the other side
|
||||
import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: POST_CARD_FIELD_ID,
|
||||
objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'postCard',
|
||||
label: 'Post Card',
|
||||
icon: 'IconMail',
|
||||
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.CASCADE,
|
||||
joinColumnName: 'postCardId',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
**Circular imports:** Both relation fields reference each other's `universalIdentifier`. To avoid circular import issues, export your field IDs as named constants from each file, and import them in the other file. The build system resolves these at compile time.
|
||||
</Note>
|
||||
|
||||
#### Relating to standard objects
|
||||
|
||||
To create a relation with a built-in Twenty object (Person, Company, etc.), use `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`:
|
||||
|
||||
```ts src/fields/person-on-self-hosting-user.field.ts
|
||||
import {
|
||||
defineField,
|
||||
FieldType,
|
||||
RelationType,
|
||||
OnDeleteAction,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk/define';
|
||||
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';
|
||||
|
||||
export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
|
||||
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PERSON_FIELD_ID,
|
||||
objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'person',
|
||||
label: 'Person',
|
||||
description: 'Person matching with the self hosting user',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||||
relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'personId',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Relation field properties
|
||||
|
||||
| Property | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| `type` | Yes | Must be `FieldType.RELATION` |
|
||||
| `relationTargetObjectMetadataUniversalIdentifier` | Yes | The `universalIdentifier` of the target object |
|
||||
| `relationTargetFieldMetadataUniversalIdentifier` | Yes | The `universalIdentifier` of the matching field on the target object |
|
||||
| `universalSettings.relationType` | Yes | `RelationType.MANY_TO_ONE` or `RelationType.ONE_TO_MANY` |
|
||||
| `universalSettings.onDelete` | MANY_TO_ONE only | What happens when the referenced record is deleted: `CASCADE`, `SET_NULL`, `RESTRICT`, or `NO_ACTION` |
|
||||
| `universalSettings.joinColumnName` | MANY_TO_ONE only | Database column name for the foreign key (e.g., `postCardId`) |
|
||||
|
||||
#### Inline relation fields in defineObject
|
||||
|
||||
You can also define relation fields directly inside `defineObject()`. In that case, omit `objectUniversalIdentifier` — it's inherited from the parent object:
|
||||
|
||||
```ts
|
||||
export default defineObject({
|
||||
universalIdentifier: '...',
|
||||
nameSingular: 'postCardRecipient',
|
||||
// ...
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: POST_CARD_FIELD_ID,
|
||||
type: FieldType.RELATION,
|
||||
name: 'postCard',
|
||||
label: 'Post Card',
|
||||
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.CASCADE,
|
||||
joinColumnName: 'postCardId',
|
||||
},
|
||||
},
|
||||
// ... other fields
|
||||
],
|
||||
});
|
||||
```
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Scaffolding entities with `yarn twenty add`
|
||||
|
||||
Instead of creating entity files by hand, you can use the interactive scaffolder:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty add
|
||||
```
|
||||
|
||||
This prompts you to pick an entity type and walks you through the required fields. It generates a ready-to-use file with a stable `universalIdentifier` and the correct `defineEntity()` call.
|
||||
|
||||
You can also pass the entity type directly to skip the first prompt:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty add object
|
||||
yarn twenty add logicFunction
|
||||
yarn twenty add frontComponent
|
||||
```
|
||||
|
||||
### Available entity types
|
||||
|
||||
| Entity type | Command | Generated file |
|
||||
|-------------|---------|----------------|
|
||||
| Object | `yarn twenty add object` | `src/objects/<name>.ts` |
|
||||
| Field | `yarn twenty add field` | `src/fields/<name>.ts` |
|
||||
| Logic function | `yarn twenty add logicFunction` | `src/logic-functions/<name>.ts` |
|
||||
| Front component | `yarn twenty add frontComponent` | `src/front-components/<name>.tsx` |
|
||||
| Role | `yarn twenty add role` | `src/roles/<name>.ts` |
|
||||
| Skill | `yarn twenty add skill` | `src/skills/<name>.ts` |
|
||||
| Agent | `yarn twenty add agent` | `src/agents/<name>.ts` |
|
||||
| View | `yarn twenty add view` | `src/views/<name>.ts` |
|
||||
| Navigation menu item | `yarn twenty add navigationMenuItem` | `src/navigation-menu-items/<name>.ts` |
|
||||
| Page layout | `yarn twenty add pageLayout` | `src/page-layouts/<name>.ts` |
|
||||
|
||||
### What the scaffolder generates
|
||||
|
||||
Each entity type has its own template. For example, `yarn twenty add object` asks for:
|
||||
|
||||
1. **Name (singular)** — e.g., `invoice`
|
||||
2. **Name (plural)** — e.g., `invoices`
|
||||
3. **Label (singular)** — auto-populated from the name (e.g., `Invoice`)
|
||||
4. **Label (plural)** — auto-populated (e.g., `Invoices`)
|
||||
5. **Create a view and navigation item?** — if you answer yes, the scaffolder also generates a matching view and sidebar link for the new object.
|
||||
|
||||
Other entity types have simpler prompts — most only ask for a name.
|
||||
|
||||
The `field` entity type is more detailed: it asks for the field name, label, type (from a list of all available field types like `TEXT`, `NUMBER`, `SELECT`, `RELATION`, etc.), and the target object's `universalIdentifier`.
|
||||
|
||||
### Custom output path
|
||||
|
||||
Use the `--path` flag to place the generated file in a custom location:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty add logicFunction --path src/custom-folder
|
||||
```
|
||||
419
packages/twenty-docs/developers/extend/apps/front-components.mdx
Normal file
|
|
@ -0,0 +1,419 @@
|
|||
---
|
||||
title: Front Components
|
||||
description: Build React components that render inside Twenty's UI with sandboxed isolation.
|
||||
icon: "window-maximize"
|
||||
---
|
||||
|
||||
Front components are React components that render directly inside Twenty's UI. They run in an **isolated Web Worker** using Remote DOM — your code is sandboxed but renders natively in the page, not in an iframe.
|
||||
|
||||
## Where front components can be used
|
||||
|
||||
Front components can render in two locations within Twenty:
|
||||
|
||||
- **Side panel** — Non-headless front components open in the right-hand side panel. This is the default behavior when a front component is triggered from the command menu.
|
||||
- **Widgets (dashboards and record pages)** — Front components can be embedded as widgets inside page layouts. When configuring a dashboard or a record page layout, users can add a front component widget.
|
||||
|
||||
## Basic example
|
||||
|
||||
The quickest way to see a front component in action is to register it as a **command**. Adding a `command` field with `isPinned: true` makes it appear as a quick-action button in the top-right corner of the page — no page layout needed:
|
||||
|
||||
```tsx src/front-components/hello-world.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
|
||||
const HelloWorld = () => {
|
||||
return (
|
||||
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
|
||||
<h1>Hello from my app!</h1>
|
||||
<p>This component renders inside Twenty.</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
|
||||
name: 'hello-world',
|
||||
description: 'A simple front component',
|
||||
component: HelloWorld,
|
||||
command: {
|
||||
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
|
||||
shortLabel: 'Hello',
|
||||
label: 'Hello World',
|
||||
icon: 'IconBolt',
|
||||
isPinned: true,
|
||||
availabilityType: 'GLOBAL',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
After syncing with `yarn twenty dev` (or running a one-shot `yarn twenty dev --once`), the quick action appears in the top-right corner of the page:
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Quick action button in the top-right corner" />
|
||||
</div>
|
||||
|
||||
Click it to render the component inline.
|
||||
|
||||
## Configuration fields
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `universalIdentifier` | Yes | Stable unique ID for this component |
|
||||
| `component` | Yes | A React component function |
|
||||
| `name` | No | Display name |
|
||||
| `description` | No | Description of what the component does |
|
||||
| `isHeadless` | No | Set to `true` if the component has no visible UI (see below) |
|
||||
| `command` | No | Register the component as a command (see [command options](#command-options) below) |
|
||||
|
||||
## Placing a front component on a page
|
||||
|
||||
Beyond commands, you can embed a front component directly into a record page by adding it as a widget in a **page layout**. See the [definePageLayout](/developers/extend/apps/skills-and-agents#definepagelayout) section for details.
|
||||
|
||||
## Headless vs non-headless
|
||||
|
||||
Front components come in two rendering modes controlled by the `isHeadless` option:
|
||||
|
||||
**Non-headless (default)** — The component renders a visible UI. When triggered from the command menu it opens in the side panel. This is the default behavior when `isHeadless` is `false` or omitted.
|
||||
|
||||
**Headless (`isHeadless: true`)** — The component mounts invisibly in the background. It does not open the side panel. Headless components are designed for actions that execute logic and then unmount themselves — for example, running an async task, navigating to a page, or showing a confirmation modal. They pair naturally with the SDK Command components described below.
|
||||
|
||||
```tsx src/front-components/sync-tracker.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { useRecordId, enqueueSnackbar } from 'twenty-sdk/front-component';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const SyncTracker = () => {
|
||||
const recordId = useRecordId();
|
||||
|
||||
useEffect(() => {
|
||||
enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' });
|
||||
}, [recordId]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'sync-tracker',
|
||||
description: 'Tracks record views silently',
|
||||
isHeadless: true,
|
||||
component: SyncTracker,
|
||||
});
|
||||
```
|
||||
|
||||
Because the component returns `null`, Twenty skips rendering a container for it — no empty space appears in the layout. The component still has access to all hooks and the host communication API.
|
||||
|
||||
## SDK Command components
|
||||
|
||||
The `twenty-sdk` package provides four Command helper components designed for headless front components. Each component executes an action on mount, handles errors by showing a snackbar notification, and automatically unmounts the front component when done.
|
||||
|
||||
Import them from `twenty-sdk/command`:
|
||||
|
||||
- **`Command`** — Runs an async callback via the `execute` prop.
|
||||
- **`CommandLink`** — Navigates to an app path. Props: `to`, `params`, `queryParams`, `options`.
|
||||
- **`CommandModal`** — Opens a confirmation modal. If the user confirms, executes the `execute` callback. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`.
|
||||
- **`CommandOpenSidePanelPage`** — Opens a specific side panel page. Props: `page`, `pageTitle`, `pageIcon`.
|
||||
|
||||
Here is a full example of a headless front component using `Command` to run an action from the command menu:
|
||||
|
||||
```tsx src/front-components/run-action.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Command } from 'twenty-sdk/command';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
const RunAction = () => {
|
||||
const execute = async () => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
await client.mutation({
|
||||
createTask: {
|
||||
__args: { data: { title: 'Created by my app' } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return <Command execute={execute} />;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
|
||||
name: 'run-action',
|
||||
description: 'Creates a task from the command menu',
|
||||
component: RunAction,
|
||||
isHeadless: true,
|
||||
command: {
|
||||
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
|
||||
label: 'Run my action',
|
||||
icon: 'IconPlayerPlay',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
And an example using `CommandModal` to ask for confirmation before executing:
|
||||
|
||||
```tsx src/front-components/delete-draft.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { CommandModal } from 'twenty-sdk/command';
|
||||
|
||||
const DeleteDraft = () => {
|
||||
const execute = async () => {
|
||||
// perform the deletion
|
||||
};
|
||||
|
||||
return (
|
||||
<CommandModal
|
||||
title="Delete draft?"
|
||||
subtitle="This action cannot be undone."
|
||||
execute={execute}
|
||||
confirmButtonText="Delete"
|
||||
confirmButtonAccent="danger"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
|
||||
name: 'delete-draft',
|
||||
description: 'Deletes a draft with confirmation',
|
||||
component: DeleteDraft,
|
||||
isHeadless: true,
|
||||
command: {
|
||||
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
|
||||
label: 'Delete draft',
|
||||
icon: 'IconTrash',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Accessing runtime context
|
||||
|
||||
Inside your component, use SDK hooks to access the current user, record, and component instance:
|
||||
|
||||
```tsx src/front-components/record-info.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
useUserId,
|
||||
useRecordId,
|
||||
useFrontComponentId,
|
||||
} from 'twenty-sdk/front-component';
|
||||
|
||||
const RecordInfo = () => {
|
||||
const userId = useUserId();
|
||||
const recordId = useRecordId();
|
||||
const componentId = useFrontComponentId();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>User: {userId}</p>
|
||||
<p>Record: {recordId ?? 'No record context'}</p>
|
||||
<p>Component: {componentId}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012',
|
||||
name: 'record-info',
|
||||
component: RecordInfo,
|
||||
});
|
||||
```
|
||||
|
||||
Available hooks:
|
||||
|
||||
| Hook | Returns | Description |
|
||||
|------|---------|-------------|
|
||||
| `useUserId()` | `string` or `null` | The current user's ID |
|
||||
| `useRecordId()` | `string` or `null` | The current record's ID (when placed on a record page) |
|
||||
| `useFrontComponentId()` | `string` | This component instance's ID |
|
||||
| `useFrontComponentExecutionContext(selector)` | varies | Access the full execution context with a selector function |
|
||||
|
||||
## Host communication API
|
||||
|
||||
Front components can trigger navigation, modals, and notifications using functions from `twenty-sdk`:
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `navigate(to, params?, queryParams?, options?)` | Navigate to a page in the app |
|
||||
| `openSidePanelPage(params)` | Open a side panel |
|
||||
| `closeSidePanel()` | Close the side panel |
|
||||
| `openCommandConfirmationModal(params)` | Show a confirmation dialog |
|
||||
| `enqueueSnackbar(params)` | Show a toast notification |
|
||||
| `unmountFrontComponent()` | Unmount the component |
|
||||
| `updateProgress(progress)` | Update a progress indicator |
|
||||
|
||||
Here is an example that uses the host API to show a snackbar and close the side panel after an action completes:
|
||||
|
||||
```tsx src/front-components/archive-record.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { useRecordId } from 'twenty-sdk/front-component';
|
||||
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
const ArchiveRecord = () => {
|
||||
const recordId = useRecordId();
|
||||
|
||||
const handleArchive = async () => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
await client.mutation({
|
||||
updateTask: {
|
||||
__args: { id: recordId, data: { status: 'ARCHIVED' } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
await enqueueSnackbar({
|
||||
message: 'Record archived',
|
||||
variant: 'success',
|
||||
});
|
||||
|
||||
await closeSidePanel();
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<p>Archive this record?</p>
|
||||
<button onClick={handleArchive}>Archive</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
|
||||
name: 'archive-record',
|
||||
description: 'Archives the current record',
|
||||
component: ArchiveRecord,
|
||||
});
|
||||
```
|
||||
|
||||
## Command options
|
||||
|
||||
Adding a `command` field to `defineFrontComponent` registers the component in the command menu (Cmd+K). If `isPinned` is `true`, it also appears as a quick-action button in the top-right corner of the page.
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `universalIdentifier` | Yes | Stable unique ID for the command |
|
||||
| `label` | Yes | Full label shown in the command menu (Cmd+K) |
|
||||
| `shortLabel` | No | Shorter label displayed on the pinned quick-action button |
|
||||
| `icon` | No | Icon name displayed next to the label (e.g. `'IconBolt'`, `'IconSend'`) |
|
||||
| `isPinned` | No | When `true`, shows the command as a quick-action button in the top-right corner of the page |
|
||||
| `availabilityType` | No | Controls where the command appears: `'GLOBAL'` (always available), `'RECORD_SELECTION'` (only when records are selected), or `'FALLBACK'` (shown when no other commands match) |
|
||||
| `availabilityObjectUniversalIdentifier` | No | Restrict the command to pages of a specific object type (e.g. only on Company records) |
|
||||
| `conditionalAvailabilityExpression` | No | A boolean expression to dynamically control whether the command is visible (see below) |
|
||||
|
||||
## Conditional availability expressions
|
||||
|
||||
The `conditionalAvailabilityExpression` field lets you control when a command is visible based on the current page context. Import typed variables and operators from `twenty-sdk` to build expressions:
|
||||
|
||||
```tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
pageType,
|
||||
numberOfSelectedRecords,
|
||||
objectPermissions,
|
||||
everyEquals,
|
||||
isDefined,
|
||||
} from 'twenty-sdk/front-component';
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'bulk-action',
|
||||
component: BulkAction,
|
||||
command: {
|
||||
universalIdentifier: '...',
|
||||
label: 'Bulk Update',
|
||||
availabilityType: 'RECORD_SELECTION',
|
||||
conditionalAvailabilityExpression: everyEquals(
|
||||
objectPermissions,
|
||||
'canUpdateObjectRecords',
|
||||
true,
|
||||
),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Context variables** — these represent the current state of the page:
|
||||
|
||||
| Variable | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `pageType` | `string` | Current page type (e.g. `'RecordIndexPage'`, `'RecordShowPage'`) |
|
||||
| `isInSidePanel` | `boolean` | Whether the component is rendered in a side panel |
|
||||
| `numberOfSelectedRecords` | `number` | Number of currently selected records |
|
||||
| `isSelectAll` | `boolean` | Whether "select all" is active |
|
||||
| `selectedRecords` | `array` | The selected record objects |
|
||||
| `favoriteRecordIds` | `array` | IDs of favorited records |
|
||||
| `objectPermissions` | `object` | Permissions for the current object type |
|
||||
| `targetObjectReadPermissions` | `object` | Read permissions for the target object |
|
||||
| `targetObjectWritePermissions` | `object` | Write permissions for the target object |
|
||||
| `featureFlags` | `object` | Active feature flags |
|
||||
| `objectMetadataItem` | `object` | Metadata of the current object type |
|
||||
| `hasAnySoftDeleteFilterOnView` | `boolean` | Whether the current view has a soft-delete filter |
|
||||
|
||||
**Operators** — combine variables into boolean expressions:
|
||||
|
||||
| Operator | Description |
|
||||
|----------|-------------|
|
||||
| `isDefined(value)` | `true` if the value is not null/undefined |
|
||||
| `isNonEmptyString(value)` | `true` if the value is a non-empty string |
|
||||
| `includes(array, value)` | `true` if the array contains the value |
|
||||
| `includesEvery(array, prop, value)` | `true` if every item's property includes the value |
|
||||
| `every(array, prop)` | `true` if the property is truthy on every item |
|
||||
| `everyDefined(array, prop)` | `true` if the property is defined on every item |
|
||||
| `everyEquals(array, prop, value)` | `true` if the property equals the value on every item |
|
||||
| `some(array, prop)` | `true` if the property is truthy on at least one item |
|
||||
| `someDefined(array, prop)` | `true` if the property is defined on at least one item |
|
||||
| `someEquals(array, prop, value)` | `true` if the property equals the value on at least one item |
|
||||
| `someNonEmptyString(array, prop)` | `true` if the property is a non-empty string on at least one item |
|
||||
| `none(array, prop)` | `true` if the property is falsy on every item |
|
||||
| `noneDefined(array, prop)` | `true` if the property is undefined on every item |
|
||||
| `noneEquals(array, prop, value)` | `true` if the property does not equal the value on any item |
|
||||
|
||||
## Public assets
|
||||
|
||||
Front components can access files from the app's `public/` directory using `getPublicAssetUrl`:
|
||||
|
||||
```tsx
|
||||
import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define';
|
||||
|
||||
const Logo = () => <img src={getPublicAssetUrl('logo.png')} alt="Logo" />;
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'logo',
|
||||
component: Logo,
|
||||
});
|
||||
```
|
||||
|
||||
See the [public assets section](/developers/extend/apps/cli-and-testing#public-assets-public-folder) for details.
|
||||
|
||||
## Styling
|
||||
|
||||
Front components support multiple styling approaches. You can use:
|
||||
|
||||
- **Inline styles** — `style={{ color: 'red' }}`
|
||||
- **Twenty UI components** — import from `twenty-sdk/ui` (Button, Tag, Status, Chip, Avatar, and more)
|
||||
- **Emotion** — CSS-in-JS with `@emotion/react`
|
||||
- **Styled-components** — `styled.div` patterns
|
||||
- **Tailwind CSS** — utility classes
|
||||
- **Any CSS-in-JS library** compatible with React
|
||||
|
||||
```tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Button, Tag, Status } from 'twenty-sdk/ui';
|
||||
|
||||
const StyledWidget = () => {
|
||||
return (
|
||||
<div style={{ padding: '16px', display: 'flex', gap: '8px' }}>
|
||||
<Button title="Click me" onClick={() => alert('Clicked!')} />
|
||||
<Tag text="Active" color="green" />
|
||||
<Status color="green" text="Online" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456',
|
||||
name: 'styled-widget',
|
||||
component: StyledWidget,
|
||||
});
|
||||
```
|
||||
|
|
@ -1,12 +1,9 @@
|
|||
---
|
||||
title: Getting Started
|
||||
icon: "rocket"
|
||||
description: Create your first Twenty app in minutes.
|
||||
---
|
||||
|
||||
<Warning>
|
||||
Apps are currently in alpha. The feature works but is still evolving.
|
||||
</Warning>
|
||||
|
||||
## What are apps?
|
||||
|
||||
Apps let you extend Twenty with custom objects, fields, logic functions, front components, AI skills, and more — all managed as code. Instead of configuring everything through the UI, you define your data model and logic in TypeScript and deploy it to one or more workspaces.
|
||||
|
|
|
|||
131
packages/twenty-docs/developers/extend/apps/layout.mdx
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
---
|
||||
title: Layout
|
||||
description: Define views, navigation menu items, and page layouts to shape how your app appears in Twenty.
|
||||
icon: "table-columns"
|
||||
---
|
||||
|
||||
Layout entities control how your app surfaces inside Twenty's UI — what lives in the sidebar, which saved views ship with the app, and how a record detail page is arranged.
|
||||
|
||||
## Layout concepts
|
||||
|
||||
| Concept | What it controls | Entity |
|
||||
|---------|------------------|--------|
|
||||
| **View** | A saved list configuration for an object — visible fields, order, filters, groups | `defineView` |
|
||||
| **Navigation Menu Item** | An entry in the left sidebar that links to a view or an external URL | `defineNavigationMenuItem` |
|
||||
| **Page Layout** | The tabs and widgets that make up a record's detail page | `definePageLayout` |
|
||||
|
||||
Views, navigation items, and page layouts reference each other by `universalIdentifier`:
|
||||
|
||||
- A **navigation menu item** of type `VIEW` points at a `defineView` identifier, so the sidebar link opens that saved view.
|
||||
- A **page layout** of type `RECORD_PAGE` targets an object and can embed [front components](/developers/extend/apps/front-components) inside its tabs as widgets.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="defineView" description="Define saved views for objects">
|
||||
|
||||
Views are saved configurations for how records of an object are displayed — including which fields are visible, their order, and any filters or groups applied. Use `defineView()` to ship pre-configured views with your app:
|
||||
|
||||
```ts src/views/example-view.ts
|
||||
import { defineView, ViewKey } from 'twenty-sdk/define';
|
||||
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
|
||||
import { NAME_FIELD_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
|
||||
|
||||
export default defineView({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'All example items',
|
||||
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
icon: 'IconList',
|
||||
key: ViewKey.INDEX,
|
||||
position: 0,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: 'f926bdb7-6af7-4683-9a09-adbca56c29f0',
|
||||
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
Key points:
|
||||
- `objectUniversalIdentifier` specifies which object this view applies to.
|
||||
- `key` determines the view type (e.g., `ViewKey.INDEX` for the main list view).
|
||||
- `fields` controls which columns appear and their order. Each field references a `fieldMetadataUniversalIdentifier`.
|
||||
- You can also define `filters`, `filterGroups`, `groups`, and `fieldGroups` for more advanced configurations.
|
||||
- `position` controls the ordering when multiple views exist for the same object.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineNavigationMenuItem" description="Define sidebar navigation links">
|
||||
|
||||
Navigation menu items add custom entries to the workspace sidebar. Use `defineNavigationMenuItem()` to link to views, external URLs, or objects:
|
||||
|
||||
```ts src/navigation-menu-items/example-navigation-menu-item.ts
|
||||
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define';
|
||||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from '../views/example-view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: '9327db91-afa1-41b6-bd9d-2b51a26efb4c',
|
||||
name: 'example-navigation-menu-item',
|
||||
icon: 'IconList',
|
||||
color: 'blue',
|
||||
position: 0,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
Key points:
|
||||
- `type` determines what the menu item links to: `NavigationMenuItemType.VIEW` for a saved view, or `NavigationMenuItemType.LINK` for an external URL.
|
||||
- For view links, set `viewUniversalIdentifier`. For external links, set `link`.
|
||||
- `position` controls the ordering in the sidebar.
|
||||
- `icon` and `color` (optional) customize the appearance.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePageLayout" description="Define custom page layouts for record views">
|
||||
|
||||
Page layouts let you customize how a record detail page looks — which tabs appear, what widgets are inside each tab, and how they are arranged. Use `definePageLayout()` to ship custom layouts with your app:
|
||||
|
||||
```ts src/page-layouts/example-record-page-layout.ts
|
||||
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
|
||||
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
|
||||
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
|
||||
|
||||
export default definePageLayout({
|
||||
universalIdentifier: '203aeb94-6701-46d6-9af1-be2bbcc9e134',
|
||||
name: 'Example Record Page',
|
||||
type: 'RECORD_PAGE',
|
||||
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
tabs: [
|
||||
{
|
||||
universalIdentifier: '6ed26b60-a51d-4ad7-86dd-1c04c7f3cac5',
|
||||
title: 'Hello World',
|
||||
position: 50,
|
||||
icon: 'IconWorld',
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||||
widgets: [
|
||||
{
|
||||
universalIdentifier: 'aa4234e0-2e5f-4c02-a96a-573449e2351d',
|
||||
title: 'Hello World',
|
||||
type: 'FRONT_COMPONENT',
|
||||
configuration: {
|
||||
configurationType: 'FRONT_COMPONENT',
|
||||
frontComponentUniversalIdentifier:
|
||||
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
Key points:
|
||||
- `type` is typically `'RECORD_PAGE'` to customize the detail view of a specific object.
|
||||
- `objectUniversalIdentifier` specifies which object this layout applies to.
|
||||
- Each `tab` defines a section of the page with a `title`, `position`, and `layoutMode` (`CANVAS` for free-form layout).
|
||||
- Each `widget` inside a tab can render a front component, a relation list, or other built-in widget types.
|
||||
- `position` on tabs controls their order. Use higher values (e.g., 50) to place custom tabs after built-in ones.
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
561
packages/twenty-docs/developers/extend/apps/logic-functions.mdx
Normal file
|
|
@ -0,0 +1,561 @@
|
|||
---
|
||||
title: Logic Functions
|
||||
description: Define server-side TypeScript functions with HTTP, cron, and database event triggers.
|
||||
icon: "bolt"
|
||||
---
|
||||
|
||||
Logic functions are server-side TypeScript functions that run on the Twenty platform. They can be triggered by HTTP requests, cron schedules, or database events — and can also be exposed as tools for AI agents.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="defineLogicFunction" description="Define logic functions and their triggers">
|
||||
|
||||
Each function file uses `defineLogicFunction()` to export a configuration with a handler and optional triggers.
|
||||
|
||||
```ts src/logic-functions/createPostCard.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk/define';
|
||||
import { CoreApiClient, type Person } from 'twenty-client-sdk/core';
|
||||
|
||||
const handler = async (params: RoutePayload) => {
|
||||
const client = new CoreApiClient();
|
||||
const name = 'name' in params.queryStringParameters
|
||||
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
|
||||
: 'Hello world';
|
||||
|
||||
const result = await client.mutation({
|
||||
createPostCard: {
|
||||
__args: { data: { name } },
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'create-new-post-card',
|
||||
timeoutSeconds: 2,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/post-card/create',
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
/*databaseEventTriggerSettings: {
|
||||
eventName: 'people.created',
|
||||
},*/
|
||||
/*cronTriggerSettings: {
|
||||
pattern: '0 0 1 1 *',
|
||||
},*/
|
||||
});
|
||||
```
|
||||
|
||||
Available trigger types:
|
||||
- **httpRoute**: Exposes your function on an HTTP path and method **under the `/s/` endpoint**:
|
||||
> e.g. `path: '/post-card/create'` is callable at `https://your-twenty-server.com/s/post-card/create`
|
||||
- **cron**: Runs your function on a schedule using a CRON expression.
|
||||
- **databaseEvent**: Runs on workspace object lifecycle events. When the event operation is `updated`, specific fields to listen to can be specified in the `updatedFields` array. If left undefined or empty, any update will trigger the function.
|
||||
> e.g. `person.updated`, `*.created`, `company.*`
|
||||
|
||||
<Note>
|
||||
You can also manually execute a function using the CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty exec -n create-new-post-card -p '{"key": "value"}'
|
||||
```
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty exec -y e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
||||
```
|
||||
|
||||
You can watch logs with:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty logs
|
||||
```
|
||||
</Note>
|
||||
|
||||
#### Route trigger payload
|
||||
|
||||
When a route trigger invokes your logic function, it receives a `RoutePayload` object that follows the
|
||||
[AWS HTTP API v2 format](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html).
|
||||
Import the `RoutePayload` type from `twenty-sdk`:
|
||||
|
||||
```ts
|
||||
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const { headers, queryStringParameters, pathParameters, body } = event;
|
||||
const { method, path } = event.requestContext.http;
|
||||
|
||||
return { message: 'Success' };
|
||||
};
|
||||
```
|
||||
|
||||
The `RoutePayload` type has the following structure:
|
||||
|
||||
| Property | Type | Description | Example |
|
||||
|----------|------|-------------|---------|
|
||||
| `headers` | `Record<string, string \| undefined>` | HTTP headers (only those listed in `forwardedRequestHeaders`) | see section below |
|
||||
| `queryStringParameters` | `Record<string, string \| undefined>` | Query string parameters (multiple values joined with commas) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }`|
|
||||
| `pathParameters` | `Record<string, string \| undefined>` | Path parameters extracted from the route pattern | `/users/:id`, `/users/123` -> `{ id: '123' }` |
|
||||
| `body` | `object \| null` | Parsed request body (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
|
||||
| `isBase64Encoded` | `boolean` | Whether the body is base64 encoded | |
|
||||
| `requestContext.http.method` | `string` | HTTP method (GET, POST, PUT, PATCH, DELETE) | |
|
||||
| `requestContext.http.path` | `string` | Raw request path | |
|
||||
|
||||
|
||||
#### forwardedRequestHeaders
|
||||
|
||||
By default, HTTP headers from incoming requests are **not** passed to your logic function for security reasons.
|
||||
To access specific headers, list them in the `forwardedRequestHeaders` array:
|
||||
|
||||
```ts
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'webhook-handler',
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/webhook',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: false,
|
||||
forwardedRequestHeaders: ['x-webhook-signature', 'content-type'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
In your handler, access the forwarded headers like this:
|
||||
|
||||
```ts
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const signature = event.headers['x-webhook-signature'];
|
||||
const contentType = event.headers['content-type'];
|
||||
|
||||
// Validate webhook signature...
|
||||
return { received: true };
|
||||
};
|
||||
```
|
||||
|
||||
<Note>
|
||||
Header names are normalized to lowercase. Access them using lowercase keys (e.g., `event.headers['content-type']`).
|
||||
</Note>
|
||||
|
||||
#### Exposing a function as a tool
|
||||
|
||||
Logic functions can be exposed as **tools** for AI agents and workflows. When marked as a tool, a function becomes discoverable by Twenty's AI features and can be used in workflow automations.
|
||||
|
||||
To mark a logic function as a tool, set `isTool: true`:
|
||||
|
||||
```ts src/logic-functions/enrich-company.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
const handler = async (params: { companyName: string; domain?: string }) => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
const result = await client.mutation({
|
||||
createTask: {
|
||||
__args: {
|
||||
data: {
|
||||
title: `Enrich data for ${params.companyName}`,
|
||||
body: `Domain: ${params.domain ?? 'unknown'}`,
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
return { taskId: result.createTask.id };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
|
||||
name: 'enrich-company',
|
||||
description: 'Enrich a company record with external data',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
isTool: true,
|
||||
});
|
||||
```
|
||||
|
||||
Key points:
|
||||
|
||||
- You can combine `isTool` with triggers — a function can be both a tool (callable by AI agents) and triggered by events at the same time.
|
||||
- **`toolInputSchema`** (optional): A JSON Schema object describing the parameters your function accepts. The schema is computed automatically from source code static analysis, but you can set it explicitly:
|
||||
|
||||
```ts
|
||||
export default defineLogicFunction({
|
||||
...,
|
||||
toolInputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
companyName: {
|
||||
type: 'string',
|
||||
description: 'The name of the company to enrich',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
description: 'The company website domain (optional)',
|
||||
},
|
||||
},
|
||||
required: ['companyName'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
**Write a good `description`.** AI agents rely on the function's `description` field to decide when to use the tool. Be specific about what the tool does and when it should be called.
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePostInstallLogicFunction" description="Define a post-install logic function (one per app)">
|
||||
|
||||
A post-install function is a logic function that runs automatically once your app has finished installing on a workspace. The server executes it **after** the app's metadata has been synchronized and the SDK client has been generated, so the workspace is fully ready to use and the new schema is in place. Typical use cases include seeding default data, creating initial records, configuring workspace settings, or provisioning resources on third-party services.
|
||||
|
||||
```ts src/logic-functions/post-install.ts
|
||||
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
|
||||
const handler = async (payload: InstallPayload): Promise<void> => {
|
||||
console.log('Post install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
|
||||
name: 'post-install',
|
||||
description: 'Runs after installation to set up the application.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: false,
|
||||
shouldRunSynchronously: false,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
You can also manually execute the post-install function at any time using the CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty exec --postInstall
|
||||
```
|
||||
|
||||
Key points:
|
||||
- Post-install functions use `definePostInstallLogicFunction()` — a specialized variant that omits trigger settings (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
|
||||
- The handler receives an `InstallPayload` with `{ previousVersion?: string; newVersion: string }` — `newVersion` is the version being installed, and `previousVersion` is the version that was previously installed (or `undefined` on a fresh install). Use these values to distinguish fresh installs from upgrades and to run version-specific migration logic.
|
||||
- **When the hook runs**: on fresh installs only, by default. Pass `shouldRunOnVersionUpgrade: true` if you also want it to run when the app is upgraded from a previous version. When omitted, the flag defaults to `false` and upgrades skip the hook.
|
||||
- **Execution model — async by default, sync opt-in**: the `shouldRunSynchronously` flag controls *how* post-install is executed.
|
||||
- `shouldRunSynchronously: false` *(default)* — the hook is **enqueued on the message queue** with `retryLimit: 3` and runs asynchronously in a worker. The install response returns as soon as the job is enqueued, so a slow or failing handler does not block the caller. The worker will retry up to three times. **Use this for long-running jobs** — seeding large datasets, calling slow third-party APIs, provisioning external resources, anything that might exceed a reasonable HTTP response window.
|
||||
- `shouldRunSynchronously: true` — the hook is executed **inline during the install flow** (same executor as pre-install). The install request blocks until the handler finishes, and if it throws, the install caller receives a `POST_INSTALL_ERROR`. No automatic retries. **Use this for fast, must-complete-before-response work** — for example, emitting a validation error to the user, or quick setup that the client will rely on immediately after the install call returns. Keep in mind the metadata migration has already been applied by the time post-install runs, so a sync-mode failure does **not** roll back the schema changes — it only surfaces the error.
|
||||
- Make sure your handler is idempotent. In async mode the queue may retry up to three times; in either mode the hook may run again on upgrades when `shouldRunOnVersionUpgrade: true`.
|
||||
- The environment variables `APPLICATION_ID`, `APP_ACCESS_TOKEN`, and `API_URL` are available inside the handler (same as any other logic function), so you can call the Twenty API with an application access token scoped to your app.
|
||||
- Only one post-install function is allowed per application. The manifest build will error if more than one is detected.
|
||||
- The function's `universalIdentifier`, `shouldRunOnVersionUpgrade`, and `shouldRunSynchronously` are automatically attached to the application manifest under the `postInstallLogicFunction` field during the build — you do not need to reference them in `defineApplication()`.
|
||||
- The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
|
||||
- **Not executed in dev mode**: when an app is registered locally (via `yarn twenty dev`), the server skips the install flow entirely and syncs files directly through the CLI watcher — so post-install never runs in dev mode, regardless of `shouldRunSynchronously`. Use `yarn twenty exec --postInstall` to trigger it manually against a running workspace.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePreInstallLogicFunction" description="Define a pre-install logic function (one per app)">
|
||||
|
||||
A pre-install function is a logic function that runs automatically during installation, **before the workspace metadata migration is applied**. It shares the same payload shape as post-install (`InstallPayload`), but it is positioned earlier in the install flow so it can prepare state that the upcoming migration depends on — typical uses include backing up data, validating compatibility with the new schema, or archiving records that are about to be restructured or dropped.
|
||||
|
||||
```ts src/logic-functions/pre-install.ts
|
||||
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
|
||||
const handler = async (payload: InstallPayload): Promise<void> => {
|
||||
console.log('Pre install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
|
||||
name: 'pre-install',
|
||||
description: 'Runs before installation to prepare the application.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: true,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
You can also manually execute the pre-install function at any time using the CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty exec --preInstall
|
||||
```
|
||||
|
||||
Key points:
|
||||
- Pre-install functions use `definePreInstallLogicFunction()` — same specialized config as post-install, just attached to a different lifecycle slot.
|
||||
- Both pre- and post-install handlers receive the same `InstallPayload` type: `{ previousVersion?: string; newVersion: string }`. Import it once and reuse it for both hooks.
|
||||
- **When the hook runs**: positioned just before the workspace metadata migration (`synchronizeFromManifest`). Before executing, the server runs a purely additive "pared-down sync" that registers the **new** version's pre-install function in the workspace metadata — nothing else is touched — and then executes it. Because this sync is additive-only, the previous version's objects, fields, and data are still intact when your handler runs: you can safely read and back up pre-migration state.
|
||||
- **Execution model**: pre-install is executed **synchronously** and **blocks the install**. If the handler throws, the install is aborted before any schema changes are applied — the workspace stays on the previous version in a consistent state. This is intentional: pre-install is your last chance to refuse a risky upgrade.
|
||||
- As with post-install, only one pre-install function is allowed per application. It is attached to the application manifest under `preInstallLogicFunction` automatically during the build.
|
||||
- **Not executed in dev mode**: same as post-install — the install flow is skipped entirely for locally-registered apps, so pre-install never runs under `yarn twenty dev`. Use `yarn twenty exec --preInstall` to trigger it manually.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Pre-install vs post-install: when to use which" description="Choosing the right install hook">
|
||||
|
||||
Both hooks are part of the same install flow and receive the same `InstallPayload`. The difference is **when** they run relative to the workspace metadata migration, and that changes what data they can safely touch.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ install flow │
|
||||
│ │
|
||||
│ upload package → [pre-install] → metadata migration → │
|
||||
│ generate SDK → [post-install] │
|
||||
│ │
|
||||
│ old schema visible new schema visible │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Pre-install is always **synchronous** (it blocks the install and can abort it). Post-install is **asynchronous by default** — enqueued on a worker with automatic retries — but can opt into synchronous execution with `shouldRunSynchronously: true`. See the `definePostInstallLogicFunction` accordion above for when to use each mode.
|
||||
|
||||
**Use `post-install` for anything that needs the new schema to exist.** This is the common case:
|
||||
|
||||
- Seeding default data (creating initial records, default views, demo content) against newly-added objects and fields.
|
||||
- Registering webhooks with third-party services now that the app has its credentials.
|
||||
- Calling your own API to finish setup that depends on the synchronized metadata.
|
||||
- Idempotent "ensure this exists" logic that should reconcile state on every upgrade — combine with `shouldRunOnVersionUpgrade: true`.
|
||||
|
||||
Example — seed a default `PostCard` record after install:
|
||||
|
||||
```ts src/logic-functions/post-install.ts
|
||||
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
import { createClient } from './generated/client';
|
||||
|
||||
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
|
||||
if (previousVersion) return; // fresh installs only
|
||||
|
||||
const client = createClient();
|
||||
await client.postCard.create({
|
||||
data: { title: 'Welcome to Postcard', content: 'Your first card!' },
|
||||
});
|
||||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
|
||||
name: 'post-install',
|
||||
description: 'Seeds a welcome post card after install.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: false,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**Use `pre-install` when a migration would otherwise destroy or corrupt existing data.** Because pre-install runs against the *previous* schema and its failure rolls back the upgrade, it is the right place for anything risky:
|
||||
|
||||
- **Backing up data that is about to be dropped or restructured** — e.g. you are removing a field in v2 and need to copy its values into another field or export them to storage before the migration runs.
|
||||
- **Archiving records that a new constraint would invalidate** — e.g. a field is becoming `NOT NULL` and you need to delete or fix rows with null values first.
|
||||
- **Validating compatibility and refusing the upgrade if the current data cannot be migrated cleanly** — throw from the handler and the install aborts with no changes applied. This is safer than discovering the incompatibility mid-migration.
|
||||
- **Renaming or rekeying data** ahead of a schema change that would lose the association.
|
||||
|
||||
Example — archive records before a destructive migration:
|
||||
|
||||
```ts src/logic-functions/pre-install.ts
|
||||
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
import { createClient } from './generated/client';
|
||||
|
||||
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
|
||||
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
|
||||
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const client = createClient();
|
||||
const legacyRecords = await client.postCard.findMany({
|
||||
where: { notes: { isNotNull: true } },
|
||||
});
|
||||
|
||||
if (legacyRecords.length === 0) return;
|
||||
|
||||
// Copy legacy `notes` into the new `description` field before the migration
|
||||
// drops the `notes` column. If this fails, the upgrade is aborted and the
|
||||
// workspace stays on v1 with all data intact.
|
||||
await Promise.all(
|
||||
legacyRecords.map((record) =>
|
||||
client.postCard.update({
|
||||
where: { id: record.id },
|
||||
data: { description: record.notes },
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
|
||||
name: 'pre-install',
|
||||
description: 'Backs up legacy notes into description before the v2 migration.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: true,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**Rule of thumb:**
|
||||
|
||||
| You want to... | Use |
|
||||
|---|---|
|
||||
| Seed default data, configure the workspace, register external resources | `post-install` |
|
||||
| Run long-running seeding or third-party calls that shouldn't block the install response | `post-install` (default — `shouldRunSynchronously: false`, with worker retries) |
|
||||
| Run fast setup that the caller will rely on immediately after the install call returns | `post-install` with `shouldRunSynchronously: true` |
|
||||
| Read or back up data that the upcoming migration would lose | `pre-install` |
|
||||
| Reject an upgrade that would corrupt existing data | `pre-install` (throw from the handler) |
|
||||
| Run reconciliation on every upgrade | `post-install` with `shouldRunOnVersionUpgrade: true` |
|
||||
| Do one-off setup on the first install only | `post-install` with `shouldRunOnVersionUpgrade: false` (default) |
|
||||
|
||||
<Note>
|
||||
If in doubt, default to **post-install**. Only reach for pre-install when the migration itself is destructive and you need to intercept the previous state before it is gone.
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Typed API clients (twenty-client-sdk)
|
||||
|
||||
The `twenty-client-sdk` package provides two typed GraphQL clients for interacting with the Twenty API from your logic functions and front components.
|
||||
|
||||
| Client | Import | Endpoint | Generated? |
|
||||
|--------|--------|----------|------------|
|
||||
| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — workspace data (records, objects) | Yes, at dev/build time |
|
||||
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — workspace config, file uploads | No, ships pre-built |
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="CoreApiClient" description="Query and mutate workspace data (records, objects)">
|
||||
|
||||
`CoreApiClient` is the main client for querying and mutating workspace data. It is **generated from your workspace schema** during `yarn twenty dev` or `yarn twenty build`, so it is fully typed to match your objects and fields.
|
||||
|
||||
```ts
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
const client = new CoreApiClient();
|
||||
|
||||
// Query records
|
||||
const { companies } = await client.query({
|
||||
companies: {
|
||||
edges: {
|
||||
node: {
|
||||
id: true,
|
||||
name: true,
|
||||
domainName: {
|
||||
primaryLinkLabel: true,
|
||||
primaryLinkUrl: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Create a record
|
||||
const { createCompany } = await client.mutation({
|
||||
createCompany: {
|
||||
__args: {
|
||||
data: {
|
||||
name: 'Acme Corp',
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The client uses a selection-set syntax: pass `true` to include a field, use `__args` for arguments, and nest objects for relations. You get full autocompletion and type checking based on your workspace schema.
|
||||
|
||||
<Note>
|
||||
**CoreApiClient is generated at dev/build time.** If you use it without running `yarn twenty dev` or `yarn twenty build` first, it throws an error. The generation happens automatically — the CLI introspects your workspace's GraphQL schema and generates a typed client using `@genql/cli`.
|
||||
</Note>
|
||||
|
||||
#### Using CoreSchema for type annotations
|
||||
|
||||
`CoreSchema` provides TypeScript types matching your workspace objects — useful for typing component state or function parameters:
|
||||
|
||||
```ts
|
||||
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
|
||||
import { useState } from 'react';
|
||||
|
||||
const [company, setCompany] = useState<
|
||||
Pick<CoreSchema.Company, 'id' | 'name'> | undefined
|
||||
>(undefined);
|
||||
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.query({
|
||||
company: {
|
||||
__args: { filter: { position: { eq: 1 } } },
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
setCompany(result.company);
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="MetadataApiClient" description="Workspace config, applications, and file uploads">
|
||||
|
||||
`MetadataApiClient` ships pre-built with the SDK (no generation required). It queries the `/metadata` endpoint for workspace configuration, applications, and file uploads.
|
||||
|
||||
```ts
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
|
||||
const metadataClient = new MetadataApiClient();
|
||||
|
||||
// List first 10 objects in the workspace
|
||||
const { objects } = await metadataClient.query({
|
||||
objects: {
|
||||
edges: {
|
||||
node: {
|
||||
id: true,
|
||||
nameSingular: true,
|
||||
namePlural: true,
|
||||
labelSingular: true,
|
||||
isCustom: true,
|
||||
},
|
||||
},
|
||||
__args: {
|
||||
filter: {},
|
||||
paging: { first: 10 },
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Uploading files
|
||||
|
||||
`MetadataApiClient` includes an `uploadFile` method for attaching files to file-type fields:
|
||||
|
||||
```ts
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
import * as fs from 'fs';
|
||||
|
||||
const metadataClient = new MetadataApiClient();
|
||||
|
||||
const fileBuffer = fs.readFileSync('./invoice.pdf');
|
||||
|
||||
const uploadedFile = await metadataClient.uploadFile(
|
||||
fileBuffer, // file contents as a Buffer
|
||||
'invoice.pdf', // filename
|
||||
'application/pdf', // MIME type
|
||||
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier
|
||||
);
|
||||
|
||||
console.log(uploadedFile);
|
||||
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
|
||||
```
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `fileBuffer` | `Buffer` | The raw file contents |
|
||||
| `filename` | `string` | The name of the file (used for storage and display) |
|
||||
| `contentType` | `string` | MIME type (defaults to `application/octet-stream` if omitted) |
|
||||
| `fieldMetadataUniversalIdentifier` | `string` | The `universalIdentifier` of the file-type field on your object |
|
||||
|
||||
Key points:
|
||||
- Uses the field's `universalIdentifier` (not its workspace-specific ID), so your upload code works across any workspace where your app is installed.
|
||||
- The returned `url` is a signed URL you can use to access the uploaded file.
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
|
||||
<Note>
|
||||
When your code runs on Twenty (logic functions or front components), the platform injects credentials as environment variables:
|
||||
|
||||
- `TWENTY_API_URL` — Base URL of the Twenty API
|
||||
- `TWENTY_APP_ACCESS_TOKEN` — Short-lived key scoped to your application's default function role
|
||||
|
||||
You do **not** need to pass these to the clients — they read from `process.env` automatically. The API key's permissions are determined by the role referenced in `defaultRoleUniversalIdentifier` in your `application-config.ts`.
|
||||
</Note>
|
||||
|
|
@ -1,12 +1,9 @@
|
|||
---
|
||||
title: Publishing
|
||||
icon: "upload"
|
||||
description: Distribute your Twenty app to the marketplace or deploy it internally.
|
||||
---
|
||||
|
||||
<Warning>
|
||||
Apps are currently in alpha. The feature works but is still evolving.
|
||||
</Warning>
|
||||
|
||||
## Overview
|
||||
|
||||
Once your app is [built and tested locally](/developers/extend/apps/building), you have two paths for distributing it:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
---
|
||||
title: Skills & Agents
|
||||
description: Define AI skills and agents for your app.
|
||||
icon: "robot"
|
||||
---
|
||||
|
||||
<Warning>
|
||||
Skills and agents are currently in alpha. The feature works but is still evolving.
|
||||
</Warning>
|
||||
|
||||
Apps can define AI capabilities that live inside the workspace — reusable skill instructions and agents with custom system prompts.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="defineSkill" description="Define AI agent skills">
|
||||
|
||||
Skills define reusable instructions and capabilities that AI agents can use within your workspace. Use `defineSkill()` to define skills with built-in validation:
|
||||
|
||||
```ts src/skills/example-skill.ts
|
||||
import { defineSkill } from 'twenty-sdk/define';
|
||||
|
||||
export default defineSkill({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'sales-outreach',
|
||||
label: 'Sales Outreach',
|
||||
description: 'Guides the AI agent through a structured sales outreach process',
|
||||
icon: 'IconBrain',
|
||||
content: `You are a sales outreach assistant. When reaching out to a prospect:
|
||||
1. Research the company and recent news
|
||||
2. Identify the prospect's role and likely pain points
|
||||
3. Draft a personalized message referencing specific details
|
||||
4. Keep the tone professional but conversational`,
|
||||
});
|
||||
```
|
||||
|
||||
Key points:
|
||||
- `name` is a unique identifier string for the skill (kebab-case recommended).
|
||||
- `label` is the human-readable display name shown in the UI.
|
||||
- `content` contains the skill instructions — this is the text the AI agent uses.
|
||||
- `icon` (optional) sets the icon displayed in the UI.
|
||||
- `description` (optional) provides additional context about the skill's purpose.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineAgent" description="Define AI agents with custom prompts">
|
||||
|
||||
Agents are AI assistants that live inside your workspace. Use `defineAgent()` to create agents with a custom system prompt:
|
||||
|
||||
```ts src/agents/example-agent.ts
|
||||
import { defineAgent } from 'twenty-sdk/define';
|
||||
|
||||
export default defineAgent({
|
||||
universalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
|
||||
name: 'sales-assistant',
|
||||
label: 'Sales Assistant',
|
||||
description: 'Helps the sales team draft outreach emails and research prospects',
|
||||
icon: 'IconRobot',
|
||||
prompt: 'You are a helpful sales assistant. Help users with their questions and tasks.',
|
||||
});
|
||||
```
|
||||
|
||||
Key points:
|
||||
- `name` is the unique identifier string for the agent (kebab-case recommended).
|
||||
- `label` is the display name shown in the UI.
|
||||
- `prompt` is the system prompt that defines the agent's behavior.
|
||||
- `description` (optional) provides context about what the agent does.
|
||||
- `icon` (optional) sets the icon displayed in the UI.
|
||||
- `modelId` (optional) overrides the default AI model used by the agent.
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
189
packages/twenty-docs/developers/extend/oauth.mdx
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
---
|
||||
title: OAuth
|
||||
icon: "key"
|
||||
description: Authorization code flow with PKCE and client credentials for server-to-server access.
|
||||
---
|
||||
|
||||
Twenty implements OAuth 2.0 with authorization code + PKCE for user-facing apps and client credentials for server-to-server access. Clients are registered dynamically via [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591) — no manual setup in a dashboard.
|
||||
|
||||
## When to Use OAuth
|
||||
|
||||
| Scenario | Auth Method |
|
||||
|----------|-------------|
|
||||
| Internal scripts, automation | [API Key](/developers/extend/api#authentication) |
|
||||
| External app acting on behalf of a user | **OAuth — Authorization Code** |
|
||||
| Server-to-server, no user context | **OAuth — Client Credentials** |
|
||||
| Twenty App with UI extensions | [Apps](/developers/extend/apps/getting-started) (OAuth is handled automatically) |
|
||||
|
||||
## Register a Client
|
||||
|
||||
Twenty supports **dynamic client registration** per [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591). No manual setup needed — register programmatically:
|
||||
|
||||
```bash
|
||||
POST /oauth/register
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"client_name": "My Integration",
|
||||
"redirect_uris": ["https://myapp.com/callback"],
|
||||
"grant_types": ["authorization_code"],
|
||||
"token_endpoint_auth_method": "client_secret_post"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": "abc123",
|
||||
"client_secret": "secret456",
|
||||
"client_name": "My Integration",
|
||||
"redirect_uris": ["https://myapp.com/callback"]
|
||||
}
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Store the `client_secret` securely — it cannot be retrieved later.
|
||||
</Warning>
|
||||
|
||||
## Scopes
|
||||
|
||||
| Scope | Access |
|
||||
|-------|--------|
|
||||
| `api` | Full read/write access to the Core and Metadata APIs |
|
||||
| `profile` | Read the authenticated user's profile information |
|
||||
|
||||
Request scopes as a space-separated string: `scope=api profile`
|
||||
|
||||
## Authorization Code Flow
|
||||
|
||||
Use this flow when your app acts on behalf of a Twenty user.
|
||||
|
||||
### 1. Redirect the user to authorize
|
||||
|
||||
```
|
||||
GET /oauth/authorize?
|
||||
client_id=YOUR_CLIENT_ID&
|
||||
response_type=code&
|
||||
redirect_uri=https://myapp.com/callback&
|
||||
scope=api&
|
||||
state=random_state_value&
|
||||
code_challenge=CHALLENGE&
|
||||
code_challenge_method=S256
|
||||
```
|
||||
|
||||
| Parameter | Required | Description |
|
||||
|-----------|----------|-------------|
|
||||
| `client_id` | Yes | Your registered client ID |
|
||||
| `response_type` | Yes | Must be `code` |
|
||||
| `redirect_uri` | Yes | Must match a registered redirect URI |
|
||||
| `scope` | No | Space-separated scopes (defaults to `api`) |
|
||||
| `state` | Recommended | Random string to prevent CSRF attacks |
|
||||
| `code_challenge` | Recommended | PKCE challenge (SHA-256 hash of verifier, base64url-encoded) |
|
||||
| `code_challenge_method` | Recommended | Must be `S256` when using PKCE |
|
||||
|
||||
The user sees a consent screen and approves or denies access.
|
||||
|
||||
### 2. Handle the callback
|
||||
|
||||
After authorization, Twenty redirects back to your `redirect_uri`:
|
||||
|
||||
```
|
||||
https://myapp.com/callback?code=AUTH_CODE&state=random_state_value
|
||||
```
|
||||
|
||||
Verify that `state` matches what you sent.
|
||||
|
||||
### 3. Exchange the code for tokens
|
||||
|
||||
```bash
|
||||
POST /oauth/token
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
grant_type=authorization_code&
|
||||
code=AUTH_CODE&
|
||||
redirect_uri=https://myapp.com/callback&
|
||||
client_id=YOUR_CLIENT_ID&
|
||||
client_secret=YOUR_CLIENT_SECRET&
|
||||
code_verifier=YOUR_PKCE_VERIFIER
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"access_token": "eyJhbG...",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
"refresh_token": "dGhpcyBpcyBh..."
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Use the access token
|
||||
|
||||
```bash
|
||||
GET /rest/companies
|
||||
Authorization: Bearer ACCESS_TOKEN
|
||||
```
|
||||
|
||||
### 5. Refresh when expired
|
||||
|
||||
```bash
|
||||
POST /oauth/token
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
grant_type=refresh_token&
|
||||
refresh_token=YOUR_REFRESH_TOKEN&
|
||||
client_id=YOUR_CLIENT_ID&
|
||||
client_secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
## Client Credentials Flow
|
||||
|
||||
For server-to-server integrations with no user interaction:
|
||||
|
||||
```bash
|
||||
POST /oauth/token
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
grant_type=client_credentials&
|
||||
client_id=YOUR_CLIENT_ID&
|
||||
client_secret=YOUR_CLIENT_SECRET&
|
||||
scope=api
|
||||
```
|
||||
|
||||
The returned token has workspace-level access, not tied to any specific user.
|
||||
|
||||
## Server Discovery
|
||||
|
||||
Twenty publishes its OAuth configuration at a standard discovery endpoint:
|
||||
|
||||
```
|
||||
GET /.well-known/oauth-authorization-server
|
||||
```
|
||||
|
||||
This returns all endpoints, supported grant types, scopes, and capabilities — useful for building generic OAuth clients.
|
||||
|
||||
## API Endpoints Summary
|
||||
|
||||
| Endpoint | Purpose |
|
||||
|----------|---------|
|
||||
| `/.well-known/oauth-authorization-server` | Server metadata discovery |
|
||||
| `/oauth/register` | Dynamic client registration |
|
||||
| `/oauth/authorize` | User authorization |
|
||||
| `/oauth/token` | Token exchange and refresh |
|
||||
|
||||
| Environment | Base URL |
|
||||
|-------------|----------|
|
||||
| **Cloud** | `https://api.twenty.com` |
|
||||
| **Self-Hosted** | `https://{your-domain}` |
|
||||
|
||||
## OAuth vs API Keys
|
||||
|
||||
| | API Keys | OAuth |
|
||||
|-|----------|-------|
|
||||
| **Setup** | Generate in Settings | Register a client, implement flow |
|
||||
| **User context** | None (workspace-level) | Specific user's permissions |
|
||||
| **Best for** | Scripts, internal tools | External apps, multi-user integrations |
|
||||
| **Token rotation** | Manual | Automatic via refresh tokens |
|
||||
| **Scoped access** | Full API access | Granular via scopes |
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
---
|
||||
title: Webhooks
|
||||
description: Receive real-time notifications when events occur in your CRM.
|
||||
icon: "satellite-dish"
|
||||
description: Get notified when records change — HTTP POST to your endpoint on every create, update, or delete.
|
||||
---
|
||||
|
||||
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
|
||||
|
||||
|
||||
Webhooks push data to your systems in real-time when events occur in Twenty — no polling required. Use them to keep external systems in sync, trigger automations, or send alerts.
|
||||
Twenty sends an HTTP POST to your URL whenever a record is created, updated, or deleted. All object types are covered, including custom objects.
|
||||
|
||||
## Create a Webhook
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,28 @@
|
|||
---
|
||||
title: Getting Started
|
||||
description: Welcome to Twenty Developer Documentation, your resources for extending, self-hosting, and contributing to Twenty.
|
||||
title: Developers
|
||||
description: Build apps, use the API, self-host, or contribute to the codebase.
|
||||
---
|
||||
|
||||
import { CardTitle } from "/snippets/card-title.mdx"
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card href="/developers/extend/extend" img="/images/user-guide/integrations/plug.png">
|
||||
<CardTitle>Extend</CardTitle>
|
||||
Build integrations with APIs, webhooks, and custom apps.
|
||||
<Card href="/developers/extend/apps/getting-started" img="/images/user-guide/halftone/dev-apps.png">
|
||||
<CardTitle>Apps</CardTitle>
|
||||
Extend Twenty with custom objects, server-side logic, UI components, and AI agents — all as TypeScript packages.
|
||||
</Card>
|
||||
|
||||
<Card href="/developers/self-host/self-host" img="/images/user-guide/what-is-twenty/20.png">
|
||||
<Card href="/developers/extend/api" img="/images/user-guide/halftone/dev-api.png">
|
||||
<CardTitle>API</CardTitle>
|
||||
REST and GraphQL APIs, webhooks, and OAuth.
|
||||
</Card>
|
||||
|
||||
<Card href="/developers/self-host/capabilities/docker-compose" img="/images/user-guide/halftone/dev-self-host.png">
|
||||
<CardTitle>Self-Host</CardTitle>
|
||||
Deploy and manage Twenty on your own infrastructure.
|
||||
Run Twenty on your own infrastructure.
|
||||
</Card>
|
||||
|
||||
<Card href="/developers/contribute/contribute" img="/images/user-guide/github/github-header.png">
|
||||
<Card href="/developers/contribute/capabilities/local-setup" img="/images/user-guide/halftone/dev-contribute.png">
|
||||
<CardTitle>Contribute</CardTitle>
|
||||
Join our open-source community and contribute to Twenty.
|
||||
Set up the monorepo locally and submit PRs.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
title: Other methods
|
||||
icon: "cloud"
|
||||
---
|
||||
|
||||
<Warning>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
title: 1-Click w/ Docker Compose
|
||||
title: Docker Compose
|
||||
icon: "docker"
|
||||
---
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
title: Setup
|
||||
icon: "gear"
|
||||
---
|
||||
|
||||
# Configuration Management
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
title: Troubleshooting
|
||||
icon: "wrench"
|
||||
---
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,385 +1,90 @@
|
|||
---
|
||||
title: Upgrade guide
|
||||
icon: "arrow-up-right-dots"
|
||||
---
|
||||
|
||||
## General guidelines
|
||||
|
||||
**Always make sure to back up your database before starting the upgrade process** by running `docker exec -it {db_container_name_or_id} pg_dumpall -U {postgres_user} > databases_backup.sql`.
|
||||
|
||||
To restore backup, run `cat databases_backup.sql | docker exec -i {db_container_name_or_id} psql -U {postgres_user}`.
|
||||
|
||||
If you used Docker Compose, follow these steps:
|
||||
|
||||
1. In a terminal, on the host where Twenty is running, turn off Twenty: `docker compose down`
|
||||
|
||||
2. Upgrade the version by changing the `TAG` value in the .env file near your docker-compose. ( We recommend consuming `major.minor` version such as `v0.53` )
|
||||
|
||||
3. Bring Twenty back online with `docker compose up -d`
|
||||
|
||||
If you want to upgrade your instance by few versions, e.g. from v0.33.0 to v0.35.0, you have to upgrade your instance sequentially, in this example from v0.33.0 to v0.34.0, then from v0.34.0 to v0.35.0.
|
||||
|
||||
**Make sure that after each upgraded version you have non-corrupted backup.**
|
||||
|
||||
## Version-specific upgrade steps
|
||||
|
||||
## v1.0
|
||||
|
||||
Hello Twenty v1.0! 🎉
|
||||
|
||||
## v0.60
|
||||
|
||||
### Performance Enhancements
|
||||
All interactions with the metadata API have been optimized for better performance, particularly for object metadata manipulation and workspace creation operations.
|
||||
|
||||
We've refactored our caching strategy to prioritize cache hits over database queries when possible, significantly improving the performance of metadata API operations.
|
||||
|
||||
If you encounter any runtime issues after upgrading, you may need to flush your cache to ensure it's synchronized with the latest changes. Run this command in your twenty-server container:
|
||||
**Always back up your database before starting the upgrade process** by running:
|
||||
|
||||
```bash
|
||||
yarn command:prod cache:flush
|
||||
docker exec -it {db_container_name_or_id} pg_dumpall -U {postgres_user} > databases_backup.sql
|
||||
```
|
||||
|
||||
### v0.55
|
||||
To restore from backup:
|
||||
|
||||
Upgrade your Twenty instance to use v0.55 image
|
||||
|
||||
You don't need to run any command anymore, the new image will automatically care about running all required migrations.
|
||||
|
||||
|
||||
### `User does not have permission` error
|
||||
|
||||
If you encounter authorization errors on most requests after upgrading, you may need to flush your cache to recompute the latest permissions.
|
||||
|
||||
In your `twenty-server` container, run:
|
||||
```bash
|
||||
yarn command:prod cache:flush
|
||||
cat databases_backup.sql | docker exec -i {db_container_name_or_id} psql -U {postgres_user}
|
||||
```
|
||||
|
||||
This issue is specific to this Twenty version and should not be required for future upgrades.
|
||||
If you use Docker Compose, follow these steps:
|
||||
|
||||
### v0.54
|
||||
1. Stop Twenty: `docker compose down`
|
||||
2. Change the `TAG` value in the `.env` file next to your `docker-compose.yml`
|
||||
3. Start Twenty: `docker compose up -d`
|
||||
|
||||
Since version `0.53`, no manual actions needed.
|
||||
The server runs all required upgrade migrations automatically on startup. No manual command is needed.
|
||||
|
||||
#### Metadata schema deprecation
|
||||
## Cross-version upgrades (v1.22+)
|
||||
|
||||
We've merged the `metadata` schema into the `core` one to simplify data retrieval from `TypeORM`.
|
||||
We have merged the `migrate` command step within the `upgrade` command. We do not recommend running `migrate` manually within any of your server/worker containers.
|
||||
Starting from **v1.22**, Twenty supports cross-version upgrades. You can jump directly from any supported version to the latest release without stepping through each intermediate version.
|
||||
|
||||
### Since v0.53
|
||||
For example, upgrading from v1.22 straight to v2.0 is fully supported.
|
||||
|
||||
Starting from `0.53`, upgrade is programmatically done within the `DockerFile`, this means from now on, you shouldn't have to run any command manually anymore.
|
||||
## Checking upgrade status
|
||||
|
||||
Make sure to keep upgrading your instance sequentially, without skipping any major version (e.g. `0.43.3` to `0.44.0` is allowed, but `0.43.1` to `0.45.0` isn't), else could lead to workspace version desynchronization that could result in runtime error and missing functionality.
|
||||
The `upgrade:status` command lets you inspect the current state of your instance and workspace migrations. It is useful for debugging upgrade issues or when filing a support request.
|
||||
|
||||
To check if a workspace has been correctly migrated you can review its version in database in `core.workspace` table.
|
||||
Run it from the server container:
|
||||
|
||||
It should always be in the range of your current Twenty's instance `major.minor` version, you can view your instance version in the admin panel (at `/settings/admin-panel`, accessible if your user has `canAccessFullAdminPanel` property set to true in the database) or by running `echo $APP_VERSION` in your `twenty-server` container.
|
||||
|
||||
|
||||
To fix a desynchronized workspace version, you will have to upgrade from the corresponding twenty's version following related upgrade guide sequentially and so on until it reaches desired version.
|
||||
|
||||
#### `auditLog` removal
|
||||
|
||||
We've removed the auditLog standard object, which means your backup size might be significantly reduced after this migration.
|
||||
|
||||
### v0.51 to v0.52
|
||||
|
||||
Upgrade your Twenty instance to use v0.52 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade
|
||||
```bash
|
||||
docker exec -it {server_container_name_or_id} yarn command:prod upgrade:status
|
||||
```
|
||||
|
||||
#### I have a workspace blocked in version between `0.52.0` and `0.52.6`
|
||||
Example output:
|
||||
|
||||
Unfortunately `0.52.0` and `0.52.6` have been completely removed from dockerHub.
|
||||
You will have to manually update your workspace version to `0.51.0` in database and upgrade using twenty version `0.52.11` following its just above upgrade guide.
|
||||
```sh
|
||||
APP_VERSION: v1.23.0
|
||||
|
||||
Instance
|
||||
Inferred version: 1.23.0
|
||||
Latest command: 1.23.0_DropWorkspaceVersionColumnFastInstanceCommand_1785000000000
|
||||
Status: Up to date
|
||||
Executed by: v1.23.0
|
||||
At: 2026-04-16T11:43:58.823Z
|
||||
|
||||
### v0.50 to v0.51
|
||||
Workspace
|
||||
Apple (20202020-1c25-4d02-bf25-6aeccf7ea419)
|
||||
Inferred version: 1.23.0
|
||||
Latest command: 1.23.0_UpdateGlobalObjectContextCommandMenuItemsCommand_1780000005000
|
||||
Status: Up to date
|
||||
Executed by: v1.23.0
|
||||
At: 2026-04-16T11:44:09.361Z
|
||||
|
||||
Upgrade your Twenty instance to use v0.51 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade
|
||||
Summary
|
||||
Instance: Up to date
|
||||
Workspaces: 1 up to date, 0 behind, 0 failed (1 total)
|
||||
```
|
||||
|
||||
### v0.44.0 to v0.50.0
|
||||
### Options
|
||||
|
||||
Upgrade your Twenty instance to use v0.50.0 image
|
||||
| Flag | Description |
|
||||
| --- | --- |
|
||||
| `-w, --workspace-id <id>` | Filter to a specific workspace. Can be passed multiple times. |
|
||||
| `-f, --failed-only` | Hide up-to-date workspaces, only show behind and failed entries. |
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade
|
||||
## Troubleshooting
|
||||
|
||||
If the upgrade fails on some workspaces, the server will not advance past the failing step. Restarting the server (`docker compose up -d`) will retry the upgrade from where it left off.
|
||||
|
||||
To quickly identify problems, run:
|
||||
|
||||
```bash
|
||||
docker exec -it {server_container_name_or_id} yarn command:prod upgrade:status --failed-only
|
||||
```
|
||||
|
||||
#### Docker-compose.yml mutation
|
||||
|
||||
This version includes a `docker-compose.yml` mutation to give `worker` service access to the `server-local-data` volume.
|
||||
Please update your local `docker-compose.yml` with [v0.50.0 docker-compose.yml](https://github.com/twentyhq/twenty/blob/v0.50.0/packages/twenty-docker/docker-compose.yml)
|
||||
|
||||
### v0.43.0 to v0.44.0
|
||||
|
||||
Upgrade your Twenty instance to use v0.44.0 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade
|
||||
```
|
||||
|
||||
### v0.42.0 to v0.43.0
|
||||
|
||||
Upgrade your Twenty instance to use v0.43.0 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade
|
||||
```
|
||||
|
||||
In this version, we have also switched to postgres:16 image in docker-compose.yml.
|
||||
|
||||
#### (Option 1) Database migration
|
||||
|
||||
Keeping the existing postgres-spilo image is fine, but you will have to freeze the version in your docker-compose.yml to be 0.43.0.
|
||||
|
||||
#### (Option 2) Database migration
|
||||
|
||||
If you want to migrate your database to the new postgres:16 image, please follow these steps:
|
||||
|
||||
1. Dump your database from the old postgres-spilo container
|
||||
|
||||
```
|
||||
docker exec -it twenty-db-1 sh
|
||||
pg_dump -U {YOUR_POSTGRES_USER} -d {YOUR_POSTGRES_DB} > databases_backup.sql
|
||||
exit
|
||||
docker cp twenty-db-1:/home/postgres/databases_backup.sql .
|
||||
```
|
||||
|
||||
Make sure your dump file is not empty.
|
||||
|
||||
2. Upgrade your docker-compose.yml to use postgres:16 image as in the [docker-compose.yml](https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/docker-compose.yml) file.
|
||||
|
||||
3. Restore the database to the new postgres:16 container
|
||||
|
||||
```
|
||||
docker cp databases_backup.sql twenty-db-1:/databases_backup.sql
|
||||
docker exec -it twenty-db-1 sh
|
||||
psql -U {YOUR_POSTGRES_USER} -d {YOUR_POSTGRES_DB} -f databases_backup.sql
|
||||
exit
|
||||
```
|
||||
|
||||
### v0.41.0 to v0.42.0
|
||||
|
||||
Upgrade your Twenty instance to use v0.42.0 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.42
|
||||
```
|
||||
|
||||
**Environment Variables**
|
||||
|
||||
- Removed: `FRONT_PORT`, `FRONT_PROTOCOL`, `FRONT_DOMAIN`, `PORT`
|
||||
- Added: `FRONTEND_URL`, `NODE_PORT`, `MAX_NUMBER_OF_WORKSPACES_DELETED_PER_EXECUTION`, `MESSAGING_PROVIDER_MICROSOFT_ENABLED`, `CALENDAR_PROVIDER_MICROSOFT_ENABLED`, `IS_MICROSOFT_SYNC_ENABLED`
|
||||
|
||||
### v0.40.0 to v0.41.0
|
||||
|
||||
Upgrade your Twenty instance to use v0.41.0 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.41
|
||||
```
|
||||
|
||||
**Environment Variables**
|
||||
|
||||
- Removed: `AUTH_MICROSOFT_TENANT_ID`
|
||||
|
||||
|
||||
### v0.35.0 to v0.40.0
|
||||
|
||||
Upgrade your Twenty instance to use v0.40.0 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.40
|
||||
```
|
||||
|
||||
**Environment Variables**
|
||||
|
||||
- Added: `IS_EMAIL_VERIFICATION_REQUIRED`, `EMAIL_VERIFICATION_TOKEN_EXPIRES_IN`, `WORKFLOW_EXEC_THROTTLE_LIMIT`, `WORKFLOW_EXEC_THROTTLE_TTL`
|
||||
|
||||
### v0.34.0 to v0.35.0
|
||||
|
||||
Upgrade your Twenty instance to use v0.35.0 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.35
|
||||
```
|
||||
|
||||
The `yarn database:migrate:prod` command will apply the migrations to the database structure (core and metadata schemas)
|
||||
The `yarn command:prod upgrade-0.35` takes care of the data migration of all workspaces.
|
||||
|
||||
**Environment Variables**
|
||||
|
||||
- We replaced `ENABLE_DB_MIGRATIONS` with `DISABLE_DB_MIGRATIONS` (default value is now `false`, you probably don't have to set anything)
|
||||
|
||||
### v0.33.0 to v0.34.0
|
||||
|
||||
Upgrade your Twenty instance to use v0.34.0 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.34
|
||||
```
|
||||
|
||||
The `yarn database:migrate:prod` command will apply the migrations to the database structure (core and metadata schemas)
|
||||
The `yarn command:prod upgrade-0.34` takes care of the data migration of all workspaces.
|
||||
|
||||
**Environment Variables**
|
||||
|
||||
- Removed: `FRONT_BASE_URL`
|
||||
- Added: `FRONT_DOMAIN`, `FRONT_PROTOCOL`, `FRONT_PORT`
|
||||
|
||||
We have updated the way we handle the frontend URL.
|
||||
You can now set the frontend URL using the `FRONT_DOMAIN`, `FRONT_PROTOCOL` and `FRONT_PORT` variables.
|
||||
If FRONT_DOMAIN is not set, the frontend URL will fall back to `SERVER_URL`.
|
||||
|
||||
### v0.32.0 to v0.33.0
|
||||
|
||||
Upgrade your Twenty instance to use v0.33.0 image
|
||||
|
||||
```
|
||||
yarn command:prod cache:flush
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.33
|
||||
```
|
||||
|
||||
The `yarn command:prod cache:flush` command will flush the Redis cache.
|
||||
The `yarn database:migrate:prod` command will apply the migrations to the database structure (core and metadata schemas)
|
||||
The `yarn command:prod upgrade-0.33` takes care of the data migration of all workspaces.
|
||||
|
||||
Starting from this version, twenty-postgres image for DB became deprecated and twenty-postgres-spilo is used instead.
|
||||
If you want to keep using twenty-postgres image, simply replace `twentycrm/twenty-postgres:${TAG}` with `twentycrm/twenty-postgres` in docker-compose.yml.
|
||||
|
||||
### v0.31.0 to v0.32.0
|
||||
|
||||
Upgrade your Twenty instance to use v0.32.0 image
|
||||
|
||||
**Schema and data migration**
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.32
|
||||
```
|
||||
|
||||
The `yarn database:migrate:prod` command will apply the migrations to the database structure (core and metadata schemas)
|
||||
The `yarn command:prod upgrade-0.32` takes care of the data migration of all workspaces.
|
||||
|
||||
**Environment Variables**
|
||||
|
||||
We have updated the way we handle the Redis connection.
|
||||
|
||||
- Removed: `REDIS_HOST`, `REDIS_PORT`, `REDIS_USERNAME`, `REDIS_PASSWORD`
|
||||
- Added: `REDIS_URL`
|
||||
|
||||
Update your `.env` file to use the new `REDIS_URL` variable instead of the individual Redis connection parameters.
|
||||
|
||||
We have also simplified the way we handle the JWT tokens.
|
||||
|
||||
- Removed: `ACCESS_TOKEN_SECRET`, `LOGIN_TOKEN_SECRET`, `REFRESH_TOKEN_SECRET`, `FILE_TOKEN_SECRET`
|
||||
- Added: `APP_SECRET`
|
||||
|
||||
Update your `.env` file to use the new `APP_SECRET` variable instead of the individual tokens secrets (you can use the same secret as before or generate a new random string)
|
||||
|
||||
**Connected Account**
|
||||
|
||||
If you are using connected account to synchronize your Google emails and calendars, you will need to activate the [People API](https://developers.google.com/people) on your Google Admin console.
|
||||
|
||||
|
||||
### v0.30.0 to v0.31.0
|
||||
|
||||
Upgrade your Twenty instance to use v0.31.0 image
|
||||
|
||||
**Schema and data migration**:
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.31
|
||||
```
|
||||
|
||||
The `yarn database:migrate:prod` command will apply the migrations to the database structure (core and metadata schemas)
|
||||
The `yarn command:prod upgrade-0.31` takes care of the data migration of all workspaces.
|
||||
|
||||
### v0.24.0 to v0.30.0
|
||||
|
||||
Upgrade your Twenty instance to use v0.30.0 image
|
||||
|
||||
**Breaking change**:
|
||||
To enhance performances, Twenty now requires redis cache to be configured. We have updated our [docker-compose.yml](https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/docker-compose.yml) to reflect this.
|
||||
Make sure to update your configuration and to update your environment variables accordingly:
|
||||
```
|
||||
REDIS_HOST={your-redis-host}
|
||||
REDIS_PORT={your-redis-port}
|
||||
CACHE_STORAGE_TYPE=redis
|
||||
```
|
||||
|
||||
**Schema and data migration**:
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.30
|
||||
```
|
||||
|
||||
The `yarn database:migrate:prod` command will apply the migrations to the database structure (core and metadata schemas)
|
||||
The `yarn command:prod upgrade-0.30` takes care of the data migration of all workspaces.
|
||||
|
||||
### v0.23.0 to v0.24.0
|
||||
|
||||
Upgrade your Twenty instance to use v0.24.0 image
|
||||
|
||||
Run the following commands:
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.24
|
||||
```
|
||||
|
||||
The `yarn database:migrate:prod` command will apply the migrations to the database structure (core and metadata schemas)
|
||||
The `yarn command:prod upgrade-0.24` takes care of the data migration of all workspaces.
|
||||
|
||||
### v0.22.0 to v0.23.0
|
||||
|
||||
Upgrade your Twenty instance to use v0.23.0 image
|
||||
|
||||
Run the following commands:
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.23
|
||||
```
|
||||
|
||||
The `yarn database:migrate:prod` command will apply the migrations to the Database.
|
||||
The `yarn command:prod upgrade-0.23` takes care of the data migration, including transferring activities to tasks/notes.
|
||||
|
||||
### v0.21.0 to v0.22.0
|
||||
|
||||
Upgrade your Twenty instance to use v0.22.0 image
|
||||
|
||||
Run the following commands:
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod workspace:sync-metadata -f
|
||||
yarn command:prod upgrade-0.22
|
||||
```
|
||||
|
||||
The `yarn database:migrate:prod` command will apply the migrations to the Database.
|
||||
The `yarn command:prod workspace:sync-metadata -f` command will sync the definition of standard objects to the metadata tables and apply to required migrations to existing workspaces.
|
||||
The `yarn command:prod upgrade-0.22` command will apply specific data transformations to adapt to the new object defaultRequestInstrumentationOptions.
|
||||
|
||||
|
||||
|
||||
This shows only workspaces that are behind or have failed, along with the error message for each failure.
|
||||
|
||||
## Before v1.22
|
||||
|
||||
If your instance is older than v1.22, you must upgrade incrementally through each major tagged version (v1.6 to v1.7, then v1.7 to v1.8, and so on) until you reach v1.22. From there, you can jump directly to the latest version.
|
||||
|
|
|
|||
30
packages/twenty-docs/getting-started/core-concepts/ai.mdx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
title: AI
|
||||
description: How Twenty uses AI to enhance your CRM experience.
|
||||
icon: "robot"
|
||||
---
|
||||
|
||||
Twenty integrates AI directly into your CRM — not as a gimmick, but as a tool that works within your data model and permission system.
|
||||
|
||||
## AI Chatbot
|
||||
|
||||
Ask questions about your data in natural language. The AI chatbot can query your CRM records, summarize information, and help you find what you're looking for without building complex filters.
|
||||
|
||||
## AI Agents
|
||||
|
||||
AI agents go beyond chat — they can execute multi-step tasks autonomously:
|
||||
|
||||
- Enrich records with data from external sources
|
||||
- Draft and send follow-up emails
|
||||
- Analyze pipeline health and flag at-risk deals
|
||||
- Process incoming data and route it to the right team
|
||||
|
||||
Agents work within your existing workflows, so you can combine AI with manual approvals, conditional logic, and external API calls.
|
||||
|
||||
## Permissions & safety
|
||||
|
||||
AI in Twenty respects your permission model. Agents can only access objects and fields that the user (or role) has permission to view. Sensitive data stays protected even when AI is involved.
|
||||
|
||||
<Card title="Full AI guide" icon="arrow-right" href="/user-guide/ai/overview">
|
||||
Detailed reference on the chatbot, agents, and permission controls.
|
||||
</Card>
|
||||
47
packages/twenty-docs/getting-started/core-concepts/apps.mdx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
title: Apps
|
||||
icon: "cube"
|
||||
description: Extend Twenty with code — custom objects, server-side logic, UI components, and AI agents, all as TypeScript packages.
|
||||
---
|
||||
|
||||
Most CRMs give you a config panel. Twenty gives you a platform. Apps are how developers extend Twenty beyond what the UI offers — defining data models, server-side logic, UI components, and AI capabilities as code, then deploying them to one or more workspaces.
|
||||
|
||||
## Why apps exist
|
||||
|
||||
Workflows cover no-code automation. But some things need code: a custom pricing engine, a proprietary enrichment pipeline, a compliance check that runs on every record update, a custom UI panel that pulls data from an internal tool.
|
||||
|
||||
Apps let you build these as first-class extensions — not brittle scripts talking to an API from outside, but code that runs on the platform with full access to the type system, permission model, and UI.
|
||||
|
||||
## What an app can define
|
||||
|
||||
An app is a TypeScript package that declares **entities** using the `twenty-sdk`:
|
||||
|
||||
| Entity | What it does |
|
||||
|--------|-------------|
|
||||
| **Objects & Fields** | New data tables and fields on existing objects — same treatment as built-in ones |
|
||||
| **Logic Functions** | Server-side TypeScript triggered by HTTP routes, cron schedules, or database events |
|
||||
| **Front Components** | Sandboxed React components that render inside Twenty's UI (side panel, widgets, command menu) |
|
||||
| **Skills & Agents** | AI capabilities — reusable instructions and autonomous assistants |
|
||||
| **Views & Navigation** | Pre-configured list views and sidebar menu items |
|
||||
|
||||
Everything is detected via AST analysis at build time — no config files, no registration boilerplate. Put a `export default defineObject(...)` in any `.ts` file and the SDK picks it up.
|
||||
|
||||
## How they run
|
||||
|
||||
- **Logic functions** execute in isolated Node.js processes, sandboxed from the host. They access data through a typed API client scoped to the app's role permissions.
|
||||
- **Front components** run in Web Workers using Remote DOM — sandboxed from the main page but rendering native DOM elements (not iframes).
|
||||
- **Permissions** are enforced at the API level. An app only sees what its role allows.
|
||||
|
||||
## The developer experience
|
||||
|
||||
```bash
|
||||
npx create-twenty-app@latest my-app
|
||||
cd my-app
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
`yarn twenty dev` watches your source files, rebuilds on change, and live-syncs to a local Twenty instance. The typed API client regenerates automatically when the schema changes. When you're ready, `yarn twenty deploy` pushes to production. Apps can also be published to npm and listed in the Twenty marketplace.
|
||||
|
||||
<Card title="Build your first app" icon="arrow-right" href="/developers/extend/apps/getting-started">
|
||||
Full walkthrough — scaffold, develop, deploy.
|
||||
</Card>
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
---
|
||||
title: Calendar & Email
|
||||
description: Sync your email and calendar with Twenty.
|
||||
icon: "envelope"
|
||||
---
|
||||
|
||||
Twenty connects to your existing tools so your CRM stays up to date without manual data entry.
|
||||
|
||||
## Email sync
|
||||
|
||||
Connect your **Google Workspace** or **Microsoft 365** mailbox. Once connected:
|
||||
|
||||
- Emails are automatically linked to the matching Company and People records
|
||||
- Full email threads are visible on each record's timeline
|
||||
- You can send emails directly from Twenty
|
||||
- Multiple mailboxes per user are supported
|
||||
|
||||
You control what gets imported — filter by date range or sender to avoid pulling in irrelevant emails.
|
||||
|
||||
## Calendar sync
|
||||
|
||||
Calendar events sync automatically from your connected account. Events appear on the relevant CRM records, giving you a complete picture of your interactions with each contact or company.
|
||||
|
||||
## Integrations
|
||||
|
||||
Beyond email and calendar, Twenty connects to external tools through:
|
||||
|
||||
| Method | Use case |
|
||||
|--------|----------|
|
||||
| **API** | Build custom integrations with the GraphQL or REST API |
|
||||
| **Webhooks** | Push real-time notifications to external systems when records change |
|
||||
| **Zapier** | Connect to 5,000+ apps without code |
|
||||
| **Workflow HTTP actions** | Call any external API as part of an automated workflow |
|
||||
|
||||
## Custom apps
|
||||
|
||||
Developers can build full-featured apps on top of Twenty — adding custom UI, server-side logic, and deep integrations. Apps can be published for the community or kept private.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Calendar & Email guide" icon="envelope" href="/user-guide/calendar-emails/overview">
|
||||
Set up email sync, calendar sync, and troubleshoot issues.
|
||||
</Card>
|
||||
<Card title="API & Extensions" icon="plug" href="/developers/extend/api">
|
||||
Build custom integrations with the Twenty API.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
title: Dashboards
|
||||
description: Track performance and visualize your CRM data with custom dashboards.
|
||||
icon: "chart-bar"
|
||||
---
|
||||
|
||||
Dashboards give you real-time visibility into your business metrics — pipeline health, team performance, revenue trends, and anything else you want to track.
|
||||
|
||||
## Widgets
|
||||
|
||||
Each dashboard is made up of widgets. A widget is a single chart or metric tied to your CRM data. You can configure:
|
||||
|
||||
- **Chart type** — Bar, line, pie, number, and more
|
||||
- **Data source** — Any object in your data model (standard or custom)
|
||||
- **Filters** — Narrow down to specific records, date ranges, or segments
|
||||
- **Aggregation** — Count, sum, average, min, max on any numeric field
|
||||
- **Grouping** — Break down by select fields, dates, or relations
|
||||
|
||||
## What you can track
|
||||
|
||||
- Pipeline value by stage
|
||||
- Deals closed over time
|
||||
- Average deal size by source
|
||||
- Task completion rates
|
||||
- Custom metrics on any object
|
||||
|
||||
## Sharing
|
||||
|
||||
Dashboards are workspace-level — everyone on your team can see them. Arrange widgets in a grid layout and resize them to build the view that works for your team.
|
||||
|
||||
<Card title="Full Dashboards guide" icon="arrow-right" href="/user-guide/dashboards/overview">
|
||||
Detailed reference on creating dashboards, configuring widgets, and chart settings.
|
||||
</Card>
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
title: Data Model
|
||||
description: Understand how Twenty structures your data with objects, fields, and relations.
|
||||
icon: "database"
|
||||
---
|
||||
|
||||
Everything in Twenty is built around **objects** and **fields** — the building blocks of your data model.
|
||||
|
||||
## Objects
|
||||
|
||||
Objects are the tables that hold your data. Twenty comes with standard objects out of the box:
|
||||
|
||||
- **Companies** — Organizations you do business with
|
||||
- **People** — Individual contacts
|
||||
- **Opportunities** — Deals in your pipeline
|
||||
- **Tasks** — Action items for your team
|
||||
- **Notes** — Free-form text linked to records
|
||||
|
||||
You can also create **custom objects** for anything your business needs — projects, support tickets, products, contracts, or anything else.
|
||||
|
||||
## Fields
|
||||
|
||||
Fields are the properties on each object. Twenty supports a wide range of field types:
|
||||
|
||||
| Category | Types |
|
||||
|----------|-------|
|
||||
| **Basic** | Text, Number, Boolean, Date, Currency, Rating, Select |
|
||||
| **Composite** | Address (street, city, state, zip), Full Name, Links, Phones, Emails |
|
||||
| **Special** | Relation, File Attachment, JSON, Actor (who created/modified) |
|
||||
|
||||
Every object also gets automatic system fields: `id`, `createdAt`, `updatedAt`, `createdBy`, and `position`.
|
||||
|
||||
## Relations
|
||||
|
||||
Objects connect to each other through relations. A Company has many People, an Opportunity belongs to a Company, and so on. You can create custom relations between any objects, including many-to-many relationships.
|
||||
|
||||
## What makes this powerful
|
||||
|
||||
Unlike traditional CRMs where you're limited to pre-defined fields on pre-defined objects, Twenty lets you model your data exactly the way your business works. Custom objects get the same first-class treatment as built-in ones — including API endpoints, views, permissions, and workflow triggers.
|
||||
|
||||
<Card title="Deep dive into the Data Model" icon="arrow-right" href="/user-guide/data-model/overview">
|
||||
Full reference on objects, fields, relations, and how to configure them.
|
||||
</Card>
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
---
|
||||
title: Glossary
|
||||
icon: "book"
|
||||
description: Key terms used throughout Twenty.
|
||||
---
|
||||
|
||||
## API
|
||||
API (Application Programming Interface) allows you to connect Twenty with other software systems and build custom integrations.
|
||||
|
||||
## Apps
|
||||
Apps are custom extensions built as code that can define data models and logic functions. They enable developers to create reusable customizations that can be deployed across multiple workspaces.
|
||||
|
||||
## Code Actions
|
||||
Code Actions are workflow steps that let you write custom JavaScript to transform data, make calculations, or perform complex logic that isn't possible with built-in actions.
|
||||
|
||||
## Command Menu
|
||||
The Command Menu is a quick-access interface (opened with `Cmd + K` on Mac and `Ctrl + K` on Windows) that lets you perform actions, create records, and navigate your workspace efficiently.
|
||||
|
||||
## Company & People
|
||||
The CRM has two fundamental types of records:
|
||||
- A `Company` represents a business or organization.
|
||||
- `People` represent your company's current and prospective customers or clients.
|
||||
|
||||
## Custom Fields
|
||||
Custom Fields are data fields you create to capture information specific to your business needs and processes.
|
||||
|
||||
## Data Model
|
||||
A Data Model is the structure that defines how information is organized in your CRM, including what objects exist, their properties (fields), and how they relate to each other.
|
||||
|
||||
## Favorites
|
||||
Favorites are records you've marked for quick access, appearing in your sidebar for instant navigation to important data.
|
||||
|
||||
## Field
|
||||
A field refers to a specific area where particular data is stored for an entity.
|
||||
|
||||
## Iterator
|
||||
An Iterator is a workflow action that loops through an array of items, executing subsequent actions for each item in the list.
|
||||
|
||||
## Kanban
|
||||
A `Kanban` is a visual way to track your business processes using cards and columns. Each column represents a stage in your process (for example: new, ongoing, won, lost), and you move records through these stages as they progress.
|
||||
|
||||
## Object
|
||||
An Object is a data structure that represents a specific type of entity in your CRM (like People, Companies, or Opportunities). Objects can be standard (built-in) or custom (created by you).
|
||||
|
||||
## Opportunities
|
||||
Opportunities in Twenty CRM are potential deals or sales with accounts or contacts.
|
||||
|
||||
## Record
|
||||
A Record indicates an instance of an object, like a specific account or contact.
|
||||
|
||||
## Relation Fields
|
||||
Relation Fields create connections between different objects, allowing you to link records together (like connecting a Person to a Company).
|
||||
|
||||
## Standard Fields
|
||||
Standard Fields are pre-built data fields that come with objects by default and provide common functionality across all workspaces.
|
||||
|
||||
## Tasks
|
||||
Tasks in Twenty CRM are assigned activities relating to contacts, accounts, or opportunities.
|
||||
|
||||
## Triggers
|
||||
Triggers are the starting point of a workflow — the event or condition that initiates the automation. Examples include record creation, record updates, webhooks, or scheduled times.
|
||||
|
||||
## Views
|
||||
You can customize the display of your records using views, setting different filters, layouts and sorting options for each view.
|
||||
|
||||
## Upsert
|
||||
Upsert is an operation that combines "update" and "insert" — it updates an existing record if a match is found, or creates a new record if no match exists.
|
||||
|
||||
## Webhooks
|
||||
Webhooks are automated messages sent from Twenty to other applications when specific events occur, enabling real-time data synchronization.
|
||||
|
||||
## Workflows
|
||||
Workflows are automated processes that trigger actions based on specific conditions, helping you automate repetitive tasks and business processes.
|
||||
|
||||
## Workspace
|
||||
A `Workspace` typically represents a company using Twenty. It holds all the records and data that you and your team members add to Twenty.
|
||||
It has a single domain name, which is typically the domain name your company uses for employee email addresses.
|
||||
|
||||
## Workspace Members
|
||||
Workspace Members are the Twenty users from your team who have access to your workspace. They can be assigned as owners or assignees for records.
|
||||
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
---
|
||||
title: Layout
|
||||
description: How to navigate, browse, and view records in Twenty.
|
||||
icon: "table-columns"
|
||||
---
|
||||
|
||||
## The main layout
|
||||
|
||||
The center of the screen is where your records live — people, companies, opportunities, tasks, notes, dashboards, workflows, and any custom objects. You view, edit, and delete records here, and create new views.
|
||||
|
||||
<img src="/images/user-guide/home/main-layout.png" style={{width:'100%'}}/>
|
||||
|
||||
## Navigation bar
|
||||
|
||||
The left sidebar gives you:
|
||||
- **Workspace switcher** — switch between workspaces or create a new one (top dropdown)
|
||||
- **Search** — press `/` to focus instantly, searches across all objects
|
||||
- **Settings** — access from the top left
|
||||
- **Favorites** — pinned views, unique per user
|
||||
- **Object shortcuts** — quick access to People, Companies, Opportunities, etc.
|
||||
- **Workflows** — create automations
|
||||
|
||||
Drag items to reorder, create folders to group related objects, hide what you don't use.
|
||||
|
||||
<img src="/images/user-guide/home/navigation-bar.png" style={{width:'100%'}}/>
|
||||
|
||||
## Command menu
|
||||
|
||||
Press `Cmd+K` (Mac) or `Ctrl+K` (Windows) — or click the three dots in the top right. From here you can:
|
||||
- Create new records
|
||||
- Import and export data via CSV
|
||||
- Create new views
|
||||
- Access deleted records (Twenty supports soft and hard deletes)
|
||||
- See keyboard shortcuts for navigating your workspace
|
||||
|
||||
<img src="/images/user-guide/home/command-menu.png" style={{width:'100%'}}/>
|
||||
|
||||
## Search
|
||||
|
||||
Accessible via the Command Menu, the top of the navigation bar, or by pressing `/`. Search works across all objects.
|
||||
|
||||
<img src="/images/user-guide/home/search-bar.png" style={{width:'100%'}}/>
|
||||
|
||||
## Side panel
|
||||
|
||||
Click a record to open the side panel on the right — a quick overview of the record's key information without leaving the current page. Click **Open** to go to the full record page.
|
||||
|
||||
<img src="/images/user-guide/home/side-panel.png" style={{width:'100%'}}/>
|
||||
|
||||
## Views
|
||||
|
||||
Every object supports multiple views — unlimited per object. Use the dropdown at the top left to switch between them.
|
||||
|
||||
- **Table** — spreadsheet-style rows and columns, with grouping, inline editing, and column customization
|
||||
- **Kanban** — drag-and-drop cards organized by a select field, ideal for pipelines
|
||||
- **Calendar** — records plotted by a date field for time-based planning
|
||||
|
||||
Each view saves its own filters, sorting, and field visibility. Share views with your workspace or keep them private. Favorite views for fast access from the sidebar.
|
||||
|
||||
<img src="/images/user-guide/home/view-menu.png" style={{width:'100%'}}/>
|
||||
|
||||
## Record pages
|
||||
|
||||
When you open a record, the detail page is built from configurable **tabs** and **widgets**. Add, remove, reorder, and resize widgets on a grid — fields, related records, emails, timeline, tasks, notes, files, charts, iframes, and more. Each object type has its own layout.
|
||||
|
||||
<Card title="Full Layout guide" icon="arrow-right" href="/user-guide/layout/overview">
|
||||
Navigation, views, record pages — detailed reference and how-tos.
|
||||
</Card>
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
title: Workflows
|
||||
description: Automate your business processes with Twenty's visual workflow builder.
|
||||
icon: "bolt"
|
||||
---
|
||||
|
||||
Workflows let you automate repetitive tasks and connect Twenty to external tools — without writing code (though you can if you want to).
|
||||
|
||||
## How workflows work
|
||||
|
||||
Every workflow has three parts:
|
||||
|
||||
1. **Trigger** — What starts the workflow
|
||||
2. **Steps** — What happens next (one or more actions in sequence)
|
||||
3. **Variables** — Data that flows between steps
|
||||
|
||||
## Triggers
|
||||
|
||||
| Trigger | When it fires |
|
||||
|---------|--------------|
|
||||
| **Record event** | A record is created, updated, deleted, or upserted |
|
||||
| **Manual** | A user clicks a button (on a single record, multiple records, or globally) |
|
||||
| **Schedule** | On a recurring interval (cron syntax) |
|
||||
| **Webhook** | An external system sends an HTTP POST |
|
||||
|
||||
## Actions
|
||||
|
||||
Workflows can chain any combination of:
|
||||
|
||||
- **Record operations** — Create, update, find, delete, or upsert records
|
||||
- **Send email** — Send or draft emails from connected accounts
|
||||
- **HTTP request** — Call any external API
|
||||
- **Code** — Run custom JavaScript for complex logic
|
||||
- **Branches** — If/else conditions to split the workflow path
|
||||
- **Iterator** — Loop over arrays of data
|
||||
- **AI Agent** — Let an AI agent process data autonomously
|
||||
- **Delay** — Wait before continuing
|
||||
- **Form** — Collect user input mid-workflow
|
||||
|
||||
## What you can build
|
||||
|
||||
- Send Slack alerts when a deal reaches a certain stage
|
||||
- Auto-enrich new contacts with data from external APIs
|
||||
- Detect stale opportunities and notify the owner
|
||||
- Sync data between Twenty and your billing system
|
||||
- Generate PDFs or invoices from record data
|
||||
- Auto-reply to inbound emails matching certain criteria
|
||||
|
||||
<Card title="Full Workflows guide" icon="arrow-right" href="/user-guide/workflows/overview">
|
||||
Detailed reference on triggers, actions, variables, and real-world automation recipes.
|
||||
</Card>
|
||||
72
packages/twenty-docs/getting-started/introduction.mdx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
---
|
||||
title: Why Twenty
|
||||
icon: "heart"
|
||||
---
|
||||
|
||||
|
||||
You've been choosing between software that's easy to start but impossible to change, and software that's flexible but takes months to set up. Twenty is the third option: **a production-ready CRM you can reshape as you go.**
|
||||
|
||||
|
||||
## What makes Twenty different
|
||||
|
||||
Twenty is a platform you can build on, not a product you configure.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Built for Agents" icon="robot">
|
||||
Agents operate inside your data model with real permissions. Skills, Tools, MCP.
|
||||
</Card>
|
||||
<Card title="Secured Extensibility" icon="shield">
|
||||
The flexibility of vibe-coded tools on secured foundations.
|
||||
</Card>
|
||||
<Card title="Modern Stack" icon="code">
|
||||
React, TypeScript. Your team already knows how to extend Twenty. No proprietary languages, no gatekeeping.
|
||||
</Card>
|
||||
|
||||
<Card title="No Lock-In" icon="lock-open">
|
||||
Open-source core, self-hostable, export your data anytime.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Who is Twenty for
|
||||
|
||||
- **Large enterprises replacing Salesforce** — your team
|
||||
spends more time fighting the tool than using it.
|
||||
You feel locked-in and the costs keep rising.
|
||||
- **Startups with technical founders** — you've outgrown
|
||||
spreadsheets and Notion. You have big ambitions and want
|
||||
a CRM that scales with you.
|
||||
- **GTM teams looking for an edge** — you want to build
|
||||
your own lead scoring, your own enrichment, your own
|
||||
outbound workflows.
|
||||
- **Privacy-conscious organizations** — you need to
|
||||
self-host and own your data end to end. Regulatory,
|
||||
contractual, or just principle.
|
||||
- **Salesforce partners** —
|
||||
your developers are tired of presenting license cost increases to your clients.
|
||||
You want a better DX, and to deliver projects faster at a lower license cost for your clients.
|
||||
- **Web development agencies** — your team knows TypeScript, React,
|
||||
and PostgreSQL. Twenty opens the CRM market
|
||||
to you without learning APEX or getting proprietary certifications.
|
||||
|
||||
## Who Twenty is not for
|
||||
|
||||
|
||||
- **Teams that want a CRM they never have to think
|
||||
about.** Twenty rewards teams that want to stay close to
|
||||
their tools and shape them over time. If you want
|
||||
something fully managed, Pipedrive or
|
||||
HubSpot will serve you well.
|
||||
- **Companies that need hundreds of pre-built integrations
|
||||
today.** Our ecosystem is growing fast, but it's not yet
|
||||
as broad as Salesforce or HubSpot. If you're comfortable
|
||||
building what's missing, you'll love it. If not, give us
|
||||
another year.
|
||||
- **Organizations where tools are chosen in boardrooms,
|
||||
not by the people using them.** We don't do steak dinners
|
||||
and executive briefings. We win with teams that have the
|
||||
authority to pick their own tools.
|
||||
|
||||
|
||||
<Card title="Ready to get started?" icon="rocket" href="/getting-started/quickstart">
|
||||
Set up Twenty in under 5 minutes — cloud or self-hosted.
|
||||
</Card>
|
||||
87
packages/twenty-docs/getting-started/key-features.mdx
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
---
|
||||
title: Key Features
|
||||
description: A tour of everything Twenty can do — from custom data models to AI-powered automation.
|
||||
icon: "star"
|
||||
---
|
||||
|
||||
Twenty is a full-featured CRM platform. Here's what you can build with it.
|
||||
|
||||
## Custom Data Model
|
||||
|
||||
Define the exact data structure your business needs. Create custom objects (beyond the standard Companies, People, and Opportunities), add custom fields with 20+ field types, and build relationships between any objects.
|
||||
|
||||
<Card title="Learn more about the Data Model" icon="database" href="/user-guide/data-model/overview">
|
||||
Objects, fields, relations — fully customizable to your business.
|
||||
</Card>
|
||||
|
||||
## Views & Pipelines
|
||||
|
||||
See your data the way you want. Switch between table views, kanban boards, and calendar views. Filter with AND/OR logic, sort by multiple fields, group records, and save custom views for your team.
|
||||
|
||||
<Card title="Explore Views & Pipelines" icon="table" href="/user-guide/views-pipelines/overview">
|
||||
Table, kanban, calendar — all with powerful filtering and sorting.
|
||||
</Card>
|
||||
|
||||
## Workflows & Automation
|
||||
|
||||
Automate any business process without writing code. Trigger workflows on record changes, schedules, manual actions, or incoming webhooks. Chain together actions like creating records, sending emails, calling APIs, running custom JavaScript, and branching with conditions.
|
||||
|
||||
<Card title="Build Workflows" icon="bolt" href="/user-guide/workflows/overview">
|
||||
Triggers, actions, branches, and integrations — all visual.
|
||||
</Card>
|
||||
|
||||
## Calendar & Email Sync
|
||||
|
||||
Connect your Google Workspace or Microsoft 365 account. Emails and calendar events automatically appear on the relevant CRM records. Send emails directly from Twenty and track all communication history.
|
||||
|
||||
<Card title="Set up Calendar & Emails" icon="envelope" href="/user-guide/calendar-emails/overview">
|
||||
Sync mailboxes, track activity, send from Twenty.
|
||||
</Card>
|
||||
|
||||
## AI
|
||||
|
||||
Twenty integrates AI agents that can work autonomously within your CRM — answering questions about your data, enriching records, and executing multi-step tasks. AI works within your permission model so it only accesses what it should.
|
||||
|
||||
<Card title="Explore AI" icon="robot" href="/user-guide/ai/overview">
|
||||
AI chatbot, autonomous agents, and smart workflows.
|
||||
</Card>
|
||||
|
||||
## Dashboards & Reporting
|
||||
|
||||
Build custom dashboards with real-time widgets. Track pipeline metrics, team performance, and business KPIs. Configure chart types, date ranges, and filters to get the exact view you need.
|
||||
|
||||
<Card title="Build Dashboards" icon="chart-bar" href="/user-guide/dashboards/overview">
|
||||
Widgets, charts, and real-time analytics.
|
||||
</Card>
|
||||
|
||||
## Permissions & Access Control
|
||||
|
||||
Role-based access control at every level — objects, fields, and individual records. Configure SSO with SAML or OIDC. Audit logs track who did what.
|
||||
|
||||
<Card title="Configure Permissions" icon="lock" href="/user-guide/permissions-access/overview">
|
||||
Roles, SSO, object/field/row-level security.
|
||||
</Card>
|
||||
|
||||
## API & Extensibility
|
||||
|
||||
A developer-first API that adapts to your custom data model. Both GraphQL and REST endpoints, with auto-generated documentation per workspace. Build custom apps, connect via webhooks, or use Zapier.
|
||||
|
||||
<Card title="Explore the API" icon="plug" href="/developers/extend/api">
|
||||
GraphQL, REST, webhooks, and custom apps.
|
||||
</Card>
|
||||
|
||||
## Data Import & Export
|
||||
|
||||
Import data from CSV files or via API. Field mapping, duplicate detection, and error handling built in. Export your data anytime — no lock-in.
|
||||
|
||||
<Card title="Migrate Your Data" icon="cloud-arrow-up" href="/user-guide/data-migration/overview">
|
||||
CSV import, API import, and migration from other CRMs.
|
||||
</Card>
|
||||
|
||||
## Self-Hosting
|
||||
|
||||
Run Twenty on your own infrastructure with a single Docker Compose command. Full control over your data, updates on your schedule, and no per-seat cloud fees.
|
||||
|
||||
<Card title="Self-Host Twenty" icon="server" href="/developers/self-host/self-host">
|
||||
Docker Compose, configuration, and upgrade guides.
|
||||
</Card>
|
||||
67
packages/twenty-docs/getting-started/quickstart.mdx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
---
|
||||
title: Quickstart
|
||||
description: Get Twenty up and running in under 5 minutes — on the cloud or self-hosted.
|
||||
icon: "play"
|
||||
---
|
||||
|
||||
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
|
||||
|
||||
## Signup
|
||||
|
||||
<Steps>
|
||||
<Step title="Create your account">
|
||||
Go to [app.twenty.com](https://app.twenty.com) and sign up with Google, Microsoft, or email.
|
||||
</Step>
|
||||
<Step title="Choose a trial">
|
||||
Pick **30 days** (with card) or **7 days** (without card). Both include full access — unlimited contacts, email integration, custom objects, API. You can change plan or billing interval anytime.
|
||||
</Step>
|
||||
<Step title="Create your workspace">
|
||||
After payment confirmation via Stripe, you'll set up your workspace name and user profile. You can cancel anytime.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<VimeoEmbed videoId="927066829" title="Creating a workspace" />
|
||||
|
||||
|
||||
## Configure your workspace
|
||||
|
||||
Once you're in, three steps to make Twenty yours:
|
||||
|
||||
### 1. Connect your mailbox
|
||||
|
||||
Go to **Settings → Accounts** and connect your Google or Microsoft account. Twenty will import your emails and calendar events, and auto-create contacts from interactions. Using another provider? You can add mailboxes via SMTP or calendars via CalDAV from the same page.
|
||||
|
||||
<Note>Start here — connecting a mailbox gives your team immediate value with real data before you customize anything else.</Note>
|
||||
|
||||
### 2. Shape your data model
|
||||
|
||||
Go to **Settings → Data Model** to create custom objects and fields. A few things to know:
|
||||
- Custom objects and fields are **unlimited on all plans** — no upsell.
|
||||
- **People, Companies, and Opportunities** are the objects that show synced emails and meetings. Use them as your base and add fields to categorize (e.g., a `Person Type` field) rather than creating separate objects that won't have email history.
|
||||
- Two People can't share the same email. Two Companies can't share the same domain.
|
||||
- You can deactivate standard fields/objects you don't need, and hide fields from views without deleting them.
|
||||
|
||||
[Data Model reference →](/user-guide/data-model/overview)
|
||||
|
||||
### 3. Import your data
|
||||
|
||||
Use the Command Menu (`Cmd+K` / `Ctrl+K`) to import People, Companies, Opportunities, or any custom object via CSV. Download the sample file first to see the expected format. Limit files to 10k records and deduplicate emails/domains before importing.
|
||||
|
||||
[Data Migration guide →](/user-guide/data-migration/overview)
|
||||
|
||||
## Next steps
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Learn the layout" icon="table-columns" href="/getting-started/core-concepts/layout">
|
||||
Navigation, views, command menu, side panel.
|
||||
</Card>
|
||||
<Card title="Build Workflows" icon="bolt" href="/user-guide/workflows/overview">
|
||||
Automate your business processes.
|
||||
</Card>
|
||||
<Card title="Create Views" icon="table" href="/user-guide/layout/overview">
|
||||
Table, kanban, calendar — filter and sort your data.
|
||||
</Card>
|
||||
<Card title="Explore the API" icon="plug" href="/developers/extend/api">
|
||||
Schema-per-tenant REST and GraphQL.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
BIN
packages/twenty-docs/images/user-guide/halftone/ai.png
Normal file
|
After Width: | Height: | Size: 210 KiB |
BIN
packages/twenty-docs/images/user-guide/halftone/billing.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 265 KiB |
BIN
packages/twenty-docs/images/user-guide/halftone/dashboards.png
Normal file
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 166 KiB |
BIN
packages/twenty-docs/images/user-guide/halftone/data-model.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
packages/twenty-docs/images/user-guide/halftone/dev-api.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
packages/twenty-docs/images/user-guide/halftone/dev-apps.png
Normal file
|
After Width: | Height: | Size: 237 KiB |
|
After Width: | Height: | Size: 255 KiB |
|
After Width: | Height: | Size: 44 KiB |
BIN
packages/twenty-docs/images/user-guide/halftone/layout.png
Normal file
|
After Width: | Height: | Size: 194 KiB |
BIN
packages/twenty-docs/images/user-guide/halftone/permissions.png
Normal file
|
After Width: | Height: | Size: 862 KiB |
BIN
packages/twenty-docs/images/user-guide/halftone/settings.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
packages/twenty-docs/images/user-guide/halftone/workflows.png
Normal file
|
After Width: | Height: | Size: 852 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
title: الأوامر الخلفية
|
||||
icon: terminal
|
||||
---
|
||||
|
||||
## الأوامر المفيدة
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
title: الأخطاء والطلبات وطلبات السحب
|
||||
icon: خلل
|
||||
info: أبلغ عن المشكلات، واطلب الميزات، وساهم بالشفرة البرمجية
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
title: أفضل الممارسات
|
||||
icon: star
|
||||
---
|
||||
|
||||
تحدد هذه الوثيقة أفضل الممارسات التي يجب اتباعها عند العمل في الواجهة الأمامية.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
title: هيكلية المجلدات
|
||||
icon: folder-tree
|
||||
info: نظرة مفصلة على هيكل المجلدات الخاصة بنا
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
title: أوامر الواجهة الأمامية
|
||||
icon: terminal
|
||||
---
|
||||
|
||||
## الأوامر المفيدة
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
title: دليل الأسلوب
|
||||
icon: paintbrush
|
||||
---
|
||||
|
||||
تشمل هذه الوثيقة القواعد التي يجب اتباعها عند كتابة التعليمات البرمجية.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
title: الإعداد المحلي
|
||||
icon: laptop-code
|
||||
description: الدليل للمساهمين (أو المطورين الفضوليين) الذين يرغبون في تشغيل Twenty محلياً.
|
||||
---
|
||||
|
||||
|
|
|
|||
77
packages/twenty-docs/l/ar/developers/contribute/commands.mdx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
---
|
||||
title: Commands
|
||||
icon: terminal
|
||||
description: Useful commands for developing Twenty.
|
||||
---
|
||||
|
||||
Commands can be run from the repository root using `npx nx`. Use `npx nx run {project}:{command}` for explicit targeting.
|
||||
|
||||
## Starting the App
|
||||
|
||||
```bash
|
||||
npx nx start twenty-front # Frontend dev server (http://localhost:3001)
|
||||
npx nx start twenty-server # Backend server (http://localhost:3000)
|
||||
npx nx run twenty-server:worker # Background worker
|
||||
```
|
||||
|
||||
## Database
|
||||
|
||||
```bash
|
||||
npx nx database:reset twenty-server # Reset and seed database
|
||||
npx nx run twenty-server:database:migrate:prod # Run migrations
|
||||
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow> # Generate a migration
|
||||
```
|
||||
|
||||
## Linting
|
||||
|
||||
```bash
|
||||
npx nx lint:diff-with-main twenty-front # Lint changed files (fastest)
|
||||
npx nx lint:diff-with-main twenty-server
|
||||
npx nx lint twenty-front --configuration=fix # Auto-fix
|
||||
```
|
||||
|
||||
## Type Checking
|
||||
|
||||
```bash
|
||||
npx nx typecheck twenty-front
|
||||
npx nx typecheck twenty-server
|
||||
```
|
||||
|
||||
## الاختبار
|
||||
|
||||
```bash
|
||||
# Frontend
|
||||
npx nx test twenty-front # Jest unit tests
|
||||
npx nx storybook:build twenty-front # Build Storybook
|
||||
npx nx storybook:test twenty-front # Storybook tests
|
||||
|
||||
# Backend
|
||||
npx nx run twenty-server:test:unit # Unit tests
|
||||
npx nx run twenty-server:test:integration # Integration tests
|
||||
npx nx run twenty-server:test:integration:with-db-reset # Integration with DB reset
|
||||
|
||||
# Single file (fastest)
|
||||
npx jest path/to/test.test.ts --config=packages/{project}/jest.config.mjs
|
||||
```
|
||||
|
||||
## جراف كيو إل
|
||||
|
||||
```bash
|
||||
npx nx run twenty-front:graphql:generate # Regenerate types
|
||||
npx nx run twenty-front:graphql:generate --configuration=metadata # Metadata schema
|
||||
```
|
||||
|
||||
## الترجمات
|
||||
|
||||
```bash
|
||||
npx nx run twenty-front:lingui:extract # Extract strings
|
||||
npx nx run twenty-front:lingui:compile # Compile translations
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
npx nx build twenty-shared # Must be built first
|
||||
npx nx build twenty-front
|
||||
npx nx build twenty-server
|
||||
```
|
||||
176
packages/twenty-docs/l/ar/developers/contribute/style-guide.mdx
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
---
|
||||
title: دليل الأسلوب
|
||||
icon: فرشاة الرسم},{
|
||||
description: اتفاقيات الشيفرة وأفضل الممارسات للمساهمة في Twenty.
|
||||
---
|
||||
|
||||
## React
|
||||
|
||||
### المكوّنات الوظيفية فقط
|
||||
|
||||
استخدم دائمًا مكوّنات TSX الوظيفية مع تصديرات مسمّاة.
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
const MyComponent = () => {
|
||||
return <div>Hello World</div>;
|
||||
};
|
||||
export default MyComponent;
|
||||
|
||||
// ✅ Good
|
||||
export function MyComponent() {
|
||||
return <div>Hello World</div>;
|
||||
};
|
||||
```
|
||||
|
||||
### الخصائص
|
||||
|
||||
أنشئ نوعًا باسم `{ComponentName}Props`. استخدم التفكيك. لا تستخدم `React.FC`.
|
||||
|
||||
```tsx
|
||||
type MyComponentProps = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const MyComponent = ({ name }: MyComponentProps) => <div>Hello {name}</div>;
|
||||
```
|
||||
|
||||
### لا تستخدم نشر الخصائص من متغير واحد
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
const MyComponent = (props: MyComponentProps) => <Other {...props} />;
|
||||
|
||||
// ✅ Good
|
||||
const MyComponent = ({ prop1, prop2 }: MyComponentProps) => <Other {...{ prop1, prop2 }} />;
|
||||
```
|
||||
|
||||
## إدارة الحالة
|
||||
|
||||
### ذرات Jotai للحالة العامة
|
||||
|
||||
```tsx
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
|
||||
export const myAtomState = createAtomState<string>({
|
||||
key: 'myAtomState',
|
||||
defaultValue: 'default value',
|
||||
});
|
||||
```
|
||||
|
||||
* فضّل الذرات على تمرير الخصائص عبر المستويات (prop drilling)
|
||||
* لا تستخدم `useRef` للحالة — استخدم `useState` أو الذرات
|
||||
* استخدم عائلات الذرات والمحددات للقوائم
|
||||
|
||||
### تجنّب عمليات إعادة التصيير غير الضرورية
|
||||
|
||||
* انقل `useEffect` وجلب البيانات إلى مكوّنات جانبية شقيقة (sidecar)
|
||||
* فضّل معالِجات الأحداث (`handleClick`, `handleChange`) على `useEffect`
|
||||
* لا تستخدم `React.memo()` — أصلِح السبب الجذري بدلًا من ذلك
|
||||
* حدّد استخدام `useCallback` / `useMemo`
|
||||
|
||||
```tsx
|
||||
// ❌ Bad — useEffect in the same component causes re-renders
|
||||
export const Page = () => {
|
||||
const [data, setData] = useAtomState(dataState);
|
||||
const [dep] = useAtomState(depState);
|
||||
useEffect(() => { setData(dep); }, [dep]);
|
||||
return <div>{data}</div>;
|
||||
};
|
||||
|
||||
// ✅ Good — extract into sibling
|
||||
export const PageData = () => {
|
||||
const [data, setData] = useAtomState(dataState);
|
||||
const [dep] = useAtomState(depState);
|
||||
useEffect(() => { setData(dep); }, [dep]);
|
||||
return <></>;
|
||||
};
|
||||
export const Page = () => {
|
||||
const [data] = useAtomState(dataState);
|
||||
return <div>{data}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
## TypeScript
|
||||
|
||||
* **`type` بدلًا من `interface`** — أكثر مرونة وأسهل في التركيب
|
||||
* **النصوص الحرفية بدل التعدادات** — باستثناء تعدادات codegen الخاصة بـ GraphQL وواجهات برمجة تطبيقات المكتبة الداخلية
|
||||
* **بدون `any`** — يتم فرض TypeScript الصارم
|
||||
* **عدم استيراد الأنواع** — استخدم استيرادات عادية (مفروض بواسطة Oxlint `typescript/consistent-type-imports`)
|
||||
* **استخدم [Zod](https://github.com/colinhacks/zod)** للتحقق وقت التشغيل من الكائنات غير محددة النوع
|
||||
|
||||
## JavaScript
|
||||
|
||||
```tsx
|
||||
// Use nullish-coalescing (??) instead of ||
|
||||
const value = process.env.MY_VALUE ?? 'default';
|
||||
|
||||
// Use optional chaining
|
||||
onClick?.();
|
||||
```
|
||||
|
||||
## التسمية
|
||||
|
||||
* **المتغيرات**: camelCase، وصفية (`email` وليس `value`، `fieldMetadata` وليس `fm`)
|
||||
* **الثوابت**: SCREAMING_SNAKE_CASE
|
||||
* **الأنواع/الفئات**: PascalCase
|
||||
* **الملفات/المجلدات**: kebab-case (`.component.tsx`, `.service.ts`, `.entity.ts`)
|
||||
* **معالجات الأحداث**: `handleClick` (وليس `onClick` لدالة المعالج)
|
||||
* **خصائص المكوّن**: ابدأ باسم المكوّن (`ButtonProps`)
|
||||
* **مكوّنات Styled**: ابدأ بـ `Styled` (`StyledTitle`)
|
||||
|
||||
## التنسيق
|
||||
|
||||
استخدم مكوّنات [Linaria](https://github.com/callstack/linaria) المنسقة. استخدم قيم السمة — وتجنّب القيم المضمّنة صراحة مثل `px` و`rem` أو الألوان.
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
const StyledButton = styled.button`
|
||||
color: #333333;
|
||||
font-size: 1rem;
|
||||
margin-left: 4px;
|
||||
`;
|
||||
|
||||
// ✅ Good
|
||||
const StyledButton = styled.button`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
```
|
||||
|
||||
## استيرادات
|
||||
|
||||
استخدم الأسماء المستعارة بدل المسارات النسبية:
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
import { Foo } from '../../../../../testing/decorators/Foo';
|
||||
|
||||
// ✅ Good
|
||||
import { Foo } from '~/testing/decorators/Foo';
|
||||
import { Bar } from '@/modules/bar/components/Bar';
|
||||
```
|
||||
|
||||
## هيكلية المجلدات
|
||||
|
||||
```
|
||||
front
|
||||
└── modules/ # Feature modules
|
||||
│ └── module1/
|
||||
│ ├── components/
|
||||
│ ├── constants/
|
||||
│ ├── contexts/
|
||||
│ ├── graphql/ (fragments, queries, mutations)
|
||||
│ ├── hooks/
|
||||
│ ├── states/ (atoms, selectors)
|
||||
│ ├── types/
|
||||
│ └── utils/
|
||||
└── pages/ # Route-level components
|
||||
└── ui/ # Reusable UI components (display, input, feedback, ...)
|
||||
```
|
||||
|
||||
* يمكن للوحدات الاستيراد من وحدات أخرى، لكن يجب أن يبقى `ui/` خاليًا من التبعيات
|
||||
* استخدم المجلدات الفرعية `internal/` للشيفرة الخاصة بالوحدة
|
||||
* المكوّنات أقل من 300 سطر، والخدمات أقل من 500 سطر
|
||||
|
|
@ -1,147 +1,55 @@
|
|||
---
|
||||
title: واجهات برمجة التطبيقات
|
||||
description: استعلم وعدّل بيانات إدارة علاقات العملاء (CRM) لديك برمجياً باستخدام REST أو GraphQL.
|
||||
icon: plug
|
||||
description: REST and GraphQL APIs generated from your workspace schema.
|
||||
---
|
||||
|
||||
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
|
||||
|
||||
تم تصميم Twenty ليكون صديقًا للمطورين، حيث يوفر واجهات برمجة قوية تتكيف مع نموذج البيانات المخصص. نحن نوفر أربعة أنواع متميزة من واجهات برمجة التطبيقات لتلبية احتياجات التكامل المختلفة.
|
||||
## Schema-per-tenant APIs
|
||||
|
||||
## نهج المطوّر أولاً
|
||||
There is no static API reference for Twenty. Each workspace has its own schema — when you add a custom object (say `Invoice`), it immediately gets REST and GraphQL endpoints identical to built-in objects like `Company` or `Person`. The API is generated from the schema, so endpoints use your object and field names directly — no opaque IDs.
|
||||
|
||||
تقوم Twenty بإنشاء واجهات برمجة التطبيقات خصيصاً لنموذج بياناتك:
|
||||
Your workspace-specific API documentation is available under **Settings → API & Webhooks** after creating an API key. It includes an interactive playground where you can execute real calls against your data.
|
||||
|
||||
* **لا حاجة إلى معرفات طويلة**: استخدم أسماء الكائنات والحقول مباشرة في نقاط النهاية
|
||||
* **معاملة متساوية للكائنات القياسية والمخصصة**: تحصل كائناتك المخصصة على نفس معاملة واجهة برمجة التطبيقات كما هو الحال مع الكائنات المضمنة
|
||||
* **نقاط نهاية مخصصة**: يحصل كل كائن وحقل على نقطة نهاية API الخاصة به
|
||||
* **وثائق مخصصة**: يتم إنشاؤها خصيصًا لنموذج بيانات مساحة عملك
|
||||
## Two APIs
|
||||
|
||||
<Note>
|
||||
وثائق واجهة برمجة التطبيقات المخصصة لك متاحة ضمن **الإعدادات → واجهات برمجة التطبيقات وخطافات الويب** بعد إنشاء مفتاح API. نظرًا لأن Twenty تُنشئ واجهات برمجة تطبيقات تتطابق مع نموذج البيانات المخصص لديك، فإن الوثائق فريدة لمساحة عملك.
|
||||
</Note>
|
||||
**Core API** — `/rest/` and `/graphql/`
|
||||
|
||||
## نوعا واجهات برمجة التطبيقات
|
||||
CRUD on records: People, Companies, Opportunities, your custom objects. Query, filter, traverse relations.
|
||||
|
||||
### واجهة برمجة التطبيقات الأساسية
|
||||
**Metadata API** — `/rest/metadata/` and `/metadata/`
|
||||
|
||||
يتم الوصول إليها عبر `/rest/` أو `/graphql/`
|
||||
Schema management: create/modify/delete objects, fields, and relations. This is how you programmatically change your data model.
|
||||
|
||||
تعامَل مع **السجلات** الفعلية لديك (البيانات):
|
||||
Both are available as REST and GraphQL. GraphQL adds batch upserts and the ability to traverse relations in a single query. Same underlying data either way.
|
||||
|
||||
* إنشاء وقراءة وتحديث وحذف الأشخاص والشركات والفرص، إلخ.
|
||||
* استعلام وتصفية البيانات
|
||||
* إدارة العلاقات بين السجلات
|
||||
## Base URLs
|
||||
|
||||
### واجهة برمجة البيانات الوصفية
|
||||
|
||||
يتم الوصول إليها عبر `/rest/metadata/` أو `/metadata/`
|
||||
|
||||
إدارة **مساحة العمل ونموذج البيانات** لديك:
|
||||
|
||||
* إنشاء أو تعديل أو حذف الكائنات والحقول
|
||||
* تكوين إعدادات مساحة العمل
|
||||
* تعريف العلاقات بين الكائنات
|
||||
|
||||
## REST مقابل GraphQL
|
||||
|
||||
تتوفر واجهات برمجة التطبيقات الأساسية وواجهات البيانات الوصفية بصيغتي REST وGraphQL:
|
||||
|
||||
| التنسيق | العمليات المتاحة |
|
||||
| ----------- | ----------------------------------------------------------------------------- |
|
||||
| **REST** | CRUD، عمليات الدفعات، إدراج/تحديث |
|
||||
| **GraphQL** | نفس الشيء + **عمليات إدراج/تحديث مجمعة**، واستعلامات العلاقات في استدعاء واحد |
|
||||
|
||||
اختر بناءً على احتياجاتك — كلا الصيغتين تصلان إلى البيانات نفسها.
|
||||
|
||||
## نقاط نهاية API
|
||||
|
||||
| البيئة | عنوان URL الأساسي |
|
||||
| --------------------- | ------------------------- |
|
||||
| **السحابة** | `https://api.twenty.com/` |
|
||||
| **الاستضافة الذاتية** | `https://{your-domain}/` |
|
||||
| البيئة | عنوان URL الأساسي |
|
||||
| ----------- | ------------------------- |
|
||||
| Cloud | `https://api.twenty.com/` |
|
||||
| Self-Hosted | `https://{your-domain}/` |
|
||||
|
||||
## المصادقة
|
||||
|
||||
كل طلب API يتطلب تضمين مفتاح API في رأس الطلب:
|
||||
|
||||
```
|
||||
Authorization: Bearer YOUR_API_KEY
|
||||
```
|
||||
|
||||
### قم بإنشاء مفتاح API
|
||||
|
||||
1. انتقل إلى **الإعدادات → واجهات برمجة التطبيقات وخطافات الويب**
|
||||
2. انقر على **+ إنشاء مفتاح**
|
||||
3. التكوين:
|
||||
* **الاسم**: اسم وصفي للمفتاح
|
||||
* **تاريخ الانتهاء**: متى تنتهي صلاحية المفتاح
|
||||
4. انقر على **حفظ**
|
||||
5. **انسخه فوراً** — يظهر المفتاح مرة واحدة فقط
|
||||
Create an API key in **Settings → API & Webhooks → + Create key**. Copy it immediately — it's shown once. Keys can be scoped to a specific role under **Settings → Roles → Assignment tab** to limit what they can access.
|
||||
|
||||
<VimeoEmbed videoId="928786722" title="إنشاء مفتاح API" />
|
||||
|
||||
<Warning>
|
||||
يمنح مفتاح API الخاص بك الوصول إلى بيانات حساسة. لا تشاركه مع خدمات غير موثوقة. إذا تم اختراقه، عطّلْه فوراً وأنشئ مفتاحاً جديداً.
|
||||
</Warning>
|
||||
For OAuth-based access (external apps acting on behalf of users), see [OAuth](/l/ar/developers/extend/oauth).
|
||||
|
||||
### تعيين دور لمفتاح API
|
||||
## Batch operations
|
||||
|
||||
لتحسين الأمان، عيّن دوراً محدداً لتقييد الوصول:
|
||||
Both REST and GraphQL support batching up to 60 records per request — create, update, or delete. GraphQL also supports batch upsert (create-or-update in one call) using plural names like `CreateCompanies`.
|
||||
|
||||
1. اذهب إلى **الإعدادات → الأدوار**
|
||||
2. انقر على الدور الذي ترغب في تعيينه
|
||||
3. افتح علامة التبويب **التعيين**
|
||||
4. ضمن **مفاتيح API**، انقر على **+ تعيين إلى مفتاح API**
|
||||
5. حدد مفتاح API
|
||||
## Rate limits
|
||||
|
||||
سيرث المفتاح أذونات ذلك الدور. راجع [الأذونات](/l/ar/user-guide/permissions-access/capabilities/permissions) للحصول على التفاصيل.
|
||||
|
||||
### إدارة مفاتيح API
|
||||
|
||||
**إعادة التوليد**: الإعدادات → واجهات برمجة التطبيقات وخطافات الويب → انقر على المفتاح → **إعادة التوليد**
|
||||
|
||||
**حذف**: الإعدادات → واجهات برمجة التطبيقات وخطافات الويب → انقر على المفتاح → **حذف**
|
||||
|
||||
## ملعب واجهة برمجة التطبيقات
|
||||
|
||||
اختبر واجهات برمجة التطبيقات لديك مباشرة في المتصفح باستخدام الملعب المدمج لدينا — متاح لكلٍ من **REST** و**GraphQL**.
|
||||
|
||||
### الوصول إلى الملعب
|
||||
|
||||
1. انتقل إلى **الإعدادات → واجهات برمجة التطبيقات وخطافات الويب**
|
||||
2. أنشئ مفتاح API (مطلوب)
|
||||
3. انقر على **REST API** أو **GraphQL API** لفتح الملعب
|
||||
|
||||
### ما الذي ستحصل عليه
|
||||
|
||||
* **وثائق تفاعلية**: يتم إنشاؤها لنموذج البيانات المحدد لديك
|
||||
* **اختبارات حيّة**: تنفيذ استدعاءات API فعلية على مساحة عملك
|
||||
* **مستكشف المخطط**: تصفح الكائنات والحقول والعلاقات المتاحة
|
||||
* **منشئ الطلبات**: أنشئ الاستعلامات مع الإكمال التلقائي
|
||||
|
||||
يعكس الملعب الكائنات والحقول المخصصة لديك، لذا تكون الوثائق دائماً دقيقة لمساحة عملك.
|
||||
|
||||
## عمليات الدفعات
|
||||
|
||||
كلٌ من REST وGraphQL يدعمان عمليات الدفعات:
|
||||
|
||||
* **حجم الدفعة**: حتى 60 سجل لكل طلب
|
||||
* **العمليات**: إنشاء وتحديث وحذف سجلات متعددة
|
||||
|
||||
**ميزات خاصة بـ GraphQL:**
|
||||
|
||||
* **إدراج/تحديث دفعي**: إنشاء أو تحديث في استدعاء واحد
|
||||
* استخدم الأسماء الجمع للكائنات (على سبيل المثال، `CreateCompanies` بدلاً من `CreateCompany`)
|
||||
|
||||
## حدود المعدل
|
||||
|
||||
يتم تنظيم طلبات API لضمان استقرار المنصة:
|
||||
|
||||
| الحد | القيمة |
|
||||
| -------------- | ---------------------- |
|
||||
| **الطلبات** | 100 استدعاء في الدقيقة |
|
||||
| **حجم الدفعة** | 60 سجل لكل استدعاء |
|
||||
|
||||
<Tip>
|
||||
استخدم عمليات الدفعات لزيادة الإنتاجية — عالج ما يصل إلى 60 سجلًا في استدعاء API واحد بدلاً من إجراء طلبات فردية.
|
||||
</Tip>
|
||||
| الحد | القيمة |
|
||||
| ---------- | ------------------ |
|
||||
| Requests | 100 per minute |
|
||||
| Batch size | 60 سجل لكل استدعاء |
|
||||
|
|
|
|||
|
|
@ -0,0 +1,434 @@
|
|||
---
|
||||
title: CLI والاختبار
|
||||
description: أوامر CLI، إعداد الاختبار، الأصول العامة، حزم npm، المستودعات البعيدة، وتهيئة CI.
|
||||
icon: terminal
|
||||
---
|
||||
|
||||
## الأصول العامة (مجلد `public/`)
|
||||
|
||||
يحتوي مجلد `public/` في جذر تطبيقك على ملفات ثابتة — صور وأيقونات وخطوط وأي أصول أخرى يحتاجها تطبيقك وقت التشغيل. تُدرج هذه الملفات تلقائيًا في عمليات البناء، وتُزامَن أثناء وضع التطوير، وتُرفَع إلى الخادم.
|
||||
|
||||
الملفات الموضوعة في `public/` هي:
|
||||
|
||||
* **متاحة للعامة** — بمجرد مزامنتها إلى الخادم، تُقدَّم الأصول عبر عنوان URL عام. لا حاجة إلى مصادقة للوصول إليها.
|
||||
* **متاحة في المكوّنات الأمامية** — استخدم عناوين الأصول لعرض الصور أو الأيقونات أو أي وسائط داخل مكوّنات React لديك.
|
||||
* **متاحة في الدوال المنطقية** — أشِر إلى عناوين الأصول في رسائل البريد الإلكتروني أو استجابات واجهات البرمجة أو أي منطق على جهة الخادم.
|
||||
* **مستخدمة لبيانات تعريف السوق** — يشير حقلا `logoUrl` و`screenshots` في `defineApplication()` إلى ملفات من هذا المجلد (مثل `public/logo.png`). تُعرَض هذه عند نشر تطبيقك في السوق.
|
||||
* **تُزامَن تلقائيًا في وضع التطوير** — عند إضافة ملف في `public/` أو تحديثه أو حذفه، تتم مزامنته إلى الخادم تلقائيًا. لا حاجة لإعادة التشغيل.
|
||||
* **مضمَّنة في عمليات البناء** — يقوم `yarn twenty build` بتجميع جميع الأصول العامة ضمن مخرجات التوزيع.
|
||||
|
||||
### الوصول إلى الأصول العامة باستخدام `getPublicAssetUrl`
|
||||
|
||||
استخدم المساعد `getPublicAssetUrl` من `twenty-sdk` للحصول على العنوان الكامل لملف في دليل `public/` لديك. يعمل ذلك في كلٍ من الدوال المنطقية والمكوّنات الأمامية.
|
||||
|
||||
**في دالة منطقية:**
|
||||
|
||||
```ts src/logic-functions/send-invoice.ts
|
||||
import { defineLogicFunction, getPublicAssetUrl } from 'twenty-sdk/define';
|
||||
|
||||
const handler = async (): Promise<any> => {
|
||||
const logoUrl = getPublicAssetUrl('logo.png');
|
||||
const invoiceUrl = getPublicAssetUrl('templates/invoice.png');
|
||||
|
||||
// Fetch the file content (no auth required — public endpoint)
|
||||
const response = await fetch(invoiceUrl);
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
return { logoUrl, size: buffer.byteLength };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'a1b2c3d4-...',
|
||||
name: 'send-invoice',
|
||||
description: 'Sends an invoice with the app logo',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**في مكوّن أمامي:**
|
||||
|
||||
```tsx src/front-components/company-card.tsx
|
||||
import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define';
|
||||
|
||||
export default defineFrontComponent(() => {
|
||||
const logoUrl = getPublicAssetUrl('logo.png');
|
||||
|
||||
return <img src={logoUrl} alt="App logo" />;
|
||||
});
|
||||
```
|
||||
|
||||
وسيطة `path` نسبية إلى مجلد `public/` الخاص بتطبيقك. كلٌّ من `getPublicAssetUrl('logo.png')` و`getPublicAssetUrl('public/logo.png')` يُحلاّن إلى العنوان نفسه — تتم إزالة بادئة `public/` تلقائيًا إن وُجدت.
|
||||
|
||||
## استخدام حِزَم npm
|
||||
|
||||
يمكنك تثبيت واستخدام أي حزمة npm في تطبيقك. يتم تجميع كلٍ من الدوال المنطقية والمكوّنات الأمامية باستخدام [esbuild](https://esbuild.github.io/)، والذي يُضمّن جميع التبعيات ضمن المخرجات — لا حاجة إلى `node_modules` وقت التشغيل.
|
||||
|
||||
### تثبيت حزمة
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add axios
|
||||
```
|
||||
|
||||
ثم استوردها في شيفرتك:
|
||||
|
||||
```ts src/logic-functions/fetch-data.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import axios from 'axios';
|
||||
|
||||
const handler = async (): Promise<any> => {
|
||||
const { data } = await axios.get('https://api.example.com/data');
|
||||
|
||||
return { data };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '...',
|
||||
name: 'fetch-data',
|
||||
description: 'Fetches data from an external API',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
وينطبق الأمر نفسه على المكوّنات الأمامية:
|
||||
|
||||
```tsx src/front-components/chart.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
const DateWidget = () => {
|
||||
return <p>Today is {format(new Date(), 'MMMM do, yyyy')}</p>;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'date-widget',
|
||||
component: DateWidget,
|
||||
});
|
||||
```
|
||||
|
||||
### كيف يعمل التجميع
|
||||
|
||||
تستخدم خطوة البناء أداة esbuild لإنتاج ملف واحد مستقل لكل دالة منطقية ولكل مكوّن أمامي. تُضمَّن جميع الحزم المستوردة داخل الحزمة.
|
||||
|
||||
**الدوال المنطقية** تعمل في بيئة Node.js. الوحدات المدمجة في Node (`fs` و`path` و`crypto` و`http` وغيرها) متاحة ولا تحتاج إلى تثبيت.
|
||||
|
||||
**المكوّنات الأمامية** تعمل ضمن Web Worker. وحدات Node المدمجة غير متاحة — المتاح فقط واجهات برمجة المتصفّح وحِزَم npm التي تعمل في بيئة المتصفّح.
|
||||
|
||||
كلتا البيئتين تحتويان على `twenty-client-sdk/core` و`twenty-client-sdk/metadata` كوحدات متاحة مُسبقًا — لا تُضمَّن هذه ضمن الحزم بل تُحلّ وقت التشغيل بواسطة الخادم.
|
||||
|
||||
## اختبار تطبيقك
|
||||
|
||||
يوفّر SDK واجهات برمجة قابلة للتنفيذ برمجيًا تمكّنك من بناء تطبيقك ونشره وتثبيته وإلغاء تثبيته من شيفرة الاختبار. بالاقتران مع [Vitest](https://vitest.dev/) وعملاء واجهة البرمجة مضبوطي الأنواع، يمكنك كتابة اختبارات تكامل تتحقّق من أن تطبيقك يعمل من البداية إلى النهاية مقابل خادم Twenty حقيقي.
|
||||
|
||||
### إعداد
|
||||
|
||||
يتضمّن التطبيق المُولَّد بالقالب بالفعل Vitest. إذا أعددته يدويًا، فثبّت التبعيات:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add -D vitest vite-tsconfig-paths
|
||||
```
|
||||
|
||||
أنشئ `vitest.config.ts` في جذر تطبيقك:
|
||||
|
||||
```ts vitest.config.ts
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tsconfigPaths({
|
||||
projects: ['tsconfig.spec.json'],
|
||||
ignoreConfigErrors: true,
|
||||
}),
|
||||
],
|
||||
test: {
|
||||
testTimeout: 120_000,
|
||||
hookTimeout: 120_000,
|
||||
include: ['src/**/*.integration-test.ts'],
|
||||
setupFiles: ['src/__tests__/setup-test.ts'],
|
||||
env: {
|
||||
TWENTY_API_URL: 'http://localhost:2020',
|
||||
TWENTY_API_KEY: 'your-api-key',
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
أنشئ ملف إعداد يتحقّق من إمكانية الوصول إلى الخادم قبل تشغيل الاختبارات:
|
||||
|
||||
```ts src/__tests__/setup-test.ts
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { beforeAll } from 'vitest';
|
||||
|
||||
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
|
||||
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
|
||||
|
||||
beforeAll(async () => {
|
||||
// Verify the server is running
|
||||
const response = await fetch(`${TWENTY_API_URL}/healthz`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Twenty server is not reachable at ${TWENTY_API_URL}. ` +
|
||||
'Start the server before running integration tests.',
|
||||
);
|
||||
}
|
||||
|
||||
// Write a temporary config for the SDK
|
||||
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(TEST_CONFIG_DIR, 'config.json'),
|
||||
JSON.stringify({
|
||||
remotes: {
|
||||
local: {
|
||||
apiUrl: process.env.TWENTY_API_URL,
|
||||
apiKey: process.env.TWENTY_API_KEY,
|
||||
},
|
||||
},
|
||||
defaultRemote: 'local',
|
||||
}, null, 2),
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
### واجهات SDK البرمجية
|
||||
|
||||
يُصدِّر المسار الفرعي `twenty-sdk/cli` دوالًا يمكنك استدعاؤها مباشرةً من شيفرة الاختبار:
|
||||
|
||||
| دالة | الوصف |
|
||||
| -------------- | ----------------------------------------- |
|
||||
| `appBuild` | بناء التطبيق واختياريًا حزم ملف tarball |
|
||||
| `appDeploy` | رفع ملف tarball إلى الخادم |
|
||||
| `appInstall` | تثبيت التطبيق على مساحة العمل النشطة |
|
||||
| `appUninstall` | إلغاء تثبيت التطبيق من مساحة العمل النشطة |
|
||||
|
||||
تُرجع كل دالة كائن نتيجة يحتوي على `success: boolean` وعلى إمّا `data` أو `error`.
|
||||
|
||||
### كتابة اختبار تكامل
|
||||
|
||||
إليك مثالًا كاملًا يبني التطبيق وينشره ويثبّته، ثم يتحقّق من ظهوره في مساحة العمل:
|
||||
|
||||
```ts src/__tests__/app-install.integration-test.ts
|
||||
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
|
||||
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
const APP_PATH = process.cwd();
|
||||
|
||||
describe('App installation', () => {
|
||||
beforeAll(async () => {
|
||||
const buildResult = await appBuild({
|
||||
appPath: APP_PATH,
|
||||
tarball: true,
|
||||
onProgress: (message: string) => console.log(`[build] ${message}`),
|
||||
});
|
||||
|
||||
if (!buildResult.success) {
|
||||
throw new Error(`Build failed: ${buildResult.error?.message}`);
|
||||
}
|
||||
|
||||
const deployResult = await appDeploy({
|
||||
tarballPath: buildResult.data.tarballPath!,
|
||||
onProgress: (message: string) => console.log(`[deploy] ${message}`),
|
||||
});
|
||||
|
||||
if (!deployResult.success) {
|
||||
throw new Error(`Deploy failed: ${deployResult.error?.message}`);
|
||||
}
|
||||
|
||||
const installResult = await appInstall({ appPath: APP_PATH });
|
||||
|
||||
if (!installResult.success) {
|
||||
throw new Error(`Install failed: ${installResult.error?.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await appUninstall({ appPath: APP_PATH });
|
||||
});
|
||||
|
||||
it('should find the installed app in the workspace', async () => {
|
||||
const metadataClient = new MetadataApiClient();
|
||||
|
||||
const result = await metadataClient.query({
|
||||
findManyApplications: {
|
||||
id: true,
|
||||
name: true,
|
||||
universalIdentifier: true,
|
||||
},
|
||||
});
|
||||
|
||||
const installedApp = result.findManyApplications.find(
|
||||
(app: { universalIdentifier: string }) =>
|
||||
app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
|
||||
expect(installedApp).toBeDefined();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### تشغيل الاختبارات
|
||||
|
||||
تأكّد من تشغيل خادم Twenty المحلي لديك، ثم:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn test
|
||||
```
|
||||
|
||||
أو في وضع المراقبة أثناء التطوير:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn test:watch
|
||||
```
|
||||
|
||||
### التحقق من الأنواع
|
||||
|
||||
يمكنك أيضًا تشغيل التحقق من الأنواع على تطبيقك دون تشغيل الاختبارات:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty typecheck
|
||||
```
|
||||
|
||||
يشغِّل هذا الأمر `tsc --noEmit` ويبلغ عن أي أخطاء في الأنواع.
|
||||
|
||||
## مرجع CLI
|
||||
|
||||
بالإضافة إلى `dev` و`build` و`add` و`typecheck`، يوفّر CLI أوامر لتنفيذ الدوال وعرض السجلات وإدارة تثبيتات التطبيقات.
|
||||
|
||||
### تنفيذ الدوال (`yarn twenty exec`)
|
||||
|
||||
تشغيل دالة منطقية يدويًا دون تشغيلها عبر HTTP أو cron أو حدث قاعدة بيانات:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Execute by function name
|
||||
yarn twenty exec -n create-new-post-card
|
||||
|
||||
# Execute by universalIdentifier
|
||||
yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
||||
|
||||
# Pass a JSON payload
|
||||
yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}'
|
||||
|
||||
# Execute the post-install function
|
||||
yarn twenty exec --postInstall
|
||||
```
|
||||
|
||||
### عرض سجلات الدوال (`yarn twenty logs`)
|
||||
|
||||
بثّ سجلات التنفيذ لدوال تطبيقك المنطقية:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Stream all function logs
|
||||
yarn twenty logs
|
||||
|
||||
# Filter by function name
|
||||
yarn twenty logs -n create-new-post-card
|
||||
|
||||
# Filter by universalIdentifier
|
||||
yarn twenty logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
||||
```
|
||||
|
||||
<Note>
|
||||
يختلف هذا عن `yarn twenty server logs`، الذي يعرض سجلات حاوية Docker. يعرض `yarn twenty logs` سجلات تنفيذ دوال تطبيقك من خادم Twenty.
|
||||
</Note>
|
||||
|
||||
### إلغاء تثبيت تطبيق (`yarn twenty uninstall`)
|
||||
|
||||
أزل تطبيقك من مساحة العمل النشطة:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty uninstall
|
||||
|
||||
# Skip the confirmation prompt
|
||||
yarn twenty uninstall --yes
|
||||
```
|
||||
|
||||
## إدارة الريموتات
|
||||
|
||||
**الريموت** هو خادم Twenty يتصل به تطبيقك. أثناء الإعداد، تُنشئ أداة إنشاء الهيكل واحدًا لك تلقائيًا. يمكنك إضافة ريموتات أخرى أو التبديل بينها في أي وقت.
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Add a new remote (opens a browser for OAuth login)
|
||||
yarn twenty remote add
|
||||
|
||||
# Connect to a local Twenty server (auto-detects port 2020 or 3000)
|
||||
yarn twenty remote add --local
|
||||
|
||||
# Add a remote non-interactively (useful for CI)
|
||||
yarn twenty remote add --api-url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote
|
||||
|
||||
# List all configured remotes
|
||||
yarn twenty remote list
|
||||
|
||||
# Switch the active remote
|
||||
yarn twenty remote switch <name>
|
||||
```
|
||||
|
||||
تُخزَّن بيانات اعتمادك في `~/.twenty/config.json`.
|
||||
|
||||
## التكامل المستمر (CI) باستخدام GitHub Actions
|
||||
|
||||
تولّد أداة إنشاء الهيكل سير عمل GitHub Actions جاهزًا للاستخدام في `.github/workflows/ci.yml`. يشغّل اختبارات التكامل لديك تلقائيًا عند كل دفع إلى `main` وعلى طلبات السحب.
|
||||
|
||||
سير العمل:
|
||||
|
||||
1. يجلب الشيفرة الخاصة بك
|
||||
2. يشغّل خادم Twenty مؤقتًا باستخدام الإجراء `twentyhq/twenty/.github/actions/spawn-twenty-docker-image`
|
||||
3. يثبّت التبعيات باستخدام `yarn install --immutable`
|
||||
4. يشغّل `yarn test` مع حقن `TWENTY_API_URL` و`TWENTY_API_KEY` من مخرجات الإجراء
|
||||
|
||||
```yaml .github/workflows/ci.yml
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request: {}
|
||||
|
||||
env:
|
||||
TWENTY_VERSION: latest
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Spawn Twenty instance
|
||||
id: twenty
|
||||
uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
|
||||
with:
|
||||
twenty-version: ${{ env.TWENTY_VERSION }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Run integration tests
|
||||
run: yarn test
|
||||
env:
|
||||
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
|
||||
TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
|
||||
```
|
||||
|
||||
لا تحتاج إلى تهيئة أي أسرار — إذ يبدأ إجراء `spawn-twenty-docker-image` خادم Twenty عابرًا مباشرة في المشغّل ويُخرِج تفاصيل الاتصال. يتم توفير السر `GITHUB_TOKEN` تلقائيًا من قِبل GitHub.
|
||||
|
||||
لتثبيت إصدار محدّد من Twenty بدلًا من `latest`، غيّر متغير البيئة `TWENTY_VERSION` في أعلى سير العمل.
|
||||
494
packages/twenty-docs/l/ar/developers/extend/apps/data-model.mdx
Normal file
|
|
@ -0,0 +1,494 @@
|
|||
---
|
||||
title: نموذج البيانات
|
||||
description: Define objects, fields, roles, and application metadata with the Twenty SDK.
|
||||
icon: database
|
||||
---
|
||||
|
||||
The `twenty-sdk` package provides `defineEntity` functions to declare your app's data model. يجب عليك استخدام `export default defineEntity({...})` لكي يكتشف SDK الكيانات الخاصة بك. تتحقق هذه الدوال من تكوينك وقت البناء وتوفّر إكمالًا تلقائيًا في بيئة التطوير وأمان الأنواع.
|
||||
|
||||
<Note>
|
||||
**تنظيم الملفات يعود إليك.**
|
||||
يعتمد اكتشاف الكيانات على AST — حيث يعثر SDK على استدعاءات `export default defineEntity(...)` بغض النظر عن مكان وجود الملف. تجميع الملفات حسب النوع (مثلًا، `logic-functions/` و`roles/`) هو مجرّد عرف، وليس متطلبًا.
|
||||
</Note>
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="defineRole" description="تهيئة صلاحيات الدور والوصول إلى الكائنات">
|
||||
|
||||
تُغلّف الأدوار الصلاحيات على كائنات وإجراءات مساحة العمل لديك.
|
||||
|
||||
```ts restricted-company-role.ts
|
||||
import {
|
||||
defineRole,
|
||||
PermissionFlag,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk/define';
|
||||
|
||||
export default defineRole({
|
||||
universalIdentifier: '2c80f640-2083-4803-bb49-003e38279de6',
|
||||
label: 'My new role',
|
||||
description: 'A role that can be used in your workspace',
|
||||
canReadAllObjectRecords: false,
|
||||
canUpdateAllObjectRecords: false,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
canUpdateAllSettings: false,
|
||||
canBeAssignedToAgents: false,
|
||||
canBeAssignedToUsers: false,
|
||||
canBeAssignedToApiKeys: false,
|
||||
objectPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
},
|
||||
],
|
||||
fieldPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
fieldUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier,
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
],
|
||||
permissionFlags: [PermissionFlag.APPLICATIONS],
|
||||
});
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineApplication" description="تهيئة بيانات التعريف للتطبيق (مطلوب، واحد لكل تطبيق)">
|
||||
|
||||
يجب أن يحتوي كل تطبيق على استدعاء واحد فقط لـ `defineApplication` يصف:
|
||||
|
||||
* **الهوية**: المعرّفات، اسم العرض، والوصف.
|
||||
* **الأذونات**: أيُّ دورٍ تستخدمه وظائفه ومكوّناته الأمامية.
|
||||
* **(اختياري) المتغيرات**: أزواج مفتاح-قيمة تُعرض لوظائفك كمتغيرات بيئة.
|
||||
* **(اختياري) دوال ما قبل التثبيت/ما بعد التثبيت**: دوال منطقية تعمل قبل التثبيت أو بعده.
|
||||
|
||||
```ts src/application-config.ts
|
||||
import { defineApplication } from 'twenty-sdk/define';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
|
||||
displayName: 'My Twenty App',
|
||||
description: 'My first Twenty app',
|
||||
icon: 'IconWorld',
|
||||
applicationVariables: {
|
||||
DEFAULT_RECIPIENT_NAME: {
|
||||
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
|
||||
description: 'Default recipient name for postcards',
|
||||
value: 'Jane Doe',
|
||||
isSecret: false,
|
||||
},
|
||||
},
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
الملاحظات:
|
||||
* حقول `universalIdentifier` هي معرّفات حتمية تملكها أنت. أنشِئها مرة واحدة واحتفظ بها ثابتة عبر عمليات المزامنة.
|
||||
* `applicationVariables` تصبح متغيرات بيئة لوظائفك ومكوّناتك الأمامية (على سبيل المثال، `DEFAULT_RECIPIENT_NAME` متاح كـ `process.env.DEFAULT_RECIPIENT_NAME`).
|
||||
* `defaultRoleUniversalIdentifier` يجب أن يُشير إلى دور مُعرَّف باستخدام `defineRole()` (انظر أعلاه).
|
||||
* يتم اكتشاف دوال ما قبل التثبيت وما بعده تلقائيًا أثناء بناء البيان — لا حاجة للإشارة إليها في `defineApplication()`.
|
||||
|
||||
#### بيانات التعريف لسوق التطبيقات
|
||||
|
||||
إذا كنت تخطط لـ [نشر تطبيقك](/l/ar/developers/extend/apps/publishing)، فإن هذه الحقول الاختيارية تتحكّم في كيفية ظهوره في السوق:
|
||||
|
||||
| الحقل | الوصف |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------ |
|
||||
| `author` | اسم المؤلف أو الشركة |
|
||||
| `category` | فئة التطبيق لتصفية سوق التطبيقات |
|
||||
| `logoUrl` | مسار شعار تطبيقك (مثلًا، `public/logo.png`) |
|
||||
| `screenshots` | مصفوفة لمسارات لقطات الشاشة (مثلًا، `public/screenshot-1.png`) |
|
||||
| `aboutDescription` | وصف ماركداون أطول لعلامة التبويب "حول". إذا لم يتم تضمينه، يستخدم السوق ملف `README.md` الخاص بالحزمة من npm |
|
||||
| `websiteUrl` | رابط إلى موقعك الإلكتروني |
|
||||
| `termsUrl` | رابط إلى شروط الخدمة |
|
||||
| `emailSupport` | عنوان البريد الإلكتروني للدعم |
|
||||
| `issueReportUrl` | رابط إلى متتبّع المشاكل |
|
||||
|
||||
#### الأدوار والصلاحيات
|
||||
|
||||
يُحدّد الحقل `defaultRoleUniversalIdentifier` في `application-config.ts` الدور الافتراضي الذي تستخدمه وظائف المنطق والمكوّنات الأمامية في تطبيقك. راجع `defineRole` أعلاه للحصول على التفاصيل.
|
||||
|
||||
* رمز وقت التشغيل المحقون باسم `TWENTY_APP_ACCESS_TOKEN` مستمد من هذا الدور.
|
||||
* العميل مضبوط الأنواع مقيَّد بالأذونات الممنوحة لذلك الدور.
|
||||
* اتبع مبدأ أقل الامتياز: أنشئ دورًا مخصصًا يضم فقط الأذونات التي تحتاجها وظائفك.
|
||||
|
||||
##### الدور الافتراضي للوظيفة
|
||||
|
||||
عند توليد تطبيق جديد بالقالب، ينشئ CLI ملفّ دور افتراضي:
|
||||
|
||||
```ts src/roles/default-role.ts
|
||||
import { defineRole, PermissionFlag } from 'twenty-sdk/define';
|
||||
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
'b648f87b-1d26-4961-b974-0908fd991061';
|
||||
|
||||
export default defineRole({
|
||||
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
label: 'Default function role',
|
||||
description: 'Default role for function Twenty client',
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: false,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
canUpdateAllSettings: false,
|
||||
canBeAssignedToAgents: false,
|
||||
canBeAssignedToUsers: false,
|
||||
canBeAssignedToApiKeys: false,
|
||||
objectPermissions: [],
|
||||
fieldPermissions: [],
|
||||
permissionFlags: [],
|
||||
});
|
||||
```
|
||||
|
||||
يُشار إلى `universalIdentifier` لهذا الدور في `application-config.ts` باسم `defaultRoleUniversalIdentifier`:
|
||||
|
||||
* **\*.role.ts** يحدد ما يمكن أن يفعله الدور.
|
||||
* **application-config.ts** يشير إلى ذلك الدور بحيث ترث وظائفك أذوناته.
|
||||
|
||||
الملاحظات:
|
||||
* ابدأ من الدور المُنشأ بالقالب، ثم قيّده تدريجيًا باتباع مبدأ أقل الامتياز.
|
||||
* استبدل `objectPermissions` و`fieldPermissions` بالكائنات والحقول التي تحتاجها وظائفك فعليًا.
|
||||
* `permissionFlags` تتحكم في الوصول إلى القدرات على مستوى المنصة. اجعلها في حدّها الأدنى.
|
||||
* اطّلع على مثال عملي: [`hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts).
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineObject" description="تعريف كائنات مخصصة مع حقول">
|
||||
|
||||
تصف الكائنات المخصصة كلًا من المخطط والسلوك للسجلات في مساحة عملك. استخدم `defineObject()` لتعريف كائنات مع تحقق مدمج:
|
||||
|
||||
```ts postCard.object.ts
|
||||
import { defineObject, FieldType } from 'twenty-sdk/define';
|
||||
|
||||
enum PostCardStatus {
|
||||
DRAFT = 'DRAFT',
|
||||
SENT = 'SENT',
|
||||
DELIVERED = 'DELIVERED',
|
||||
RETURNED = 'RETURNED',
|
||||
}
|
||||
|
||||
export default defineObject({
|
||||
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
|
||||
nameSingular: 'postCard',
|
||||
namePlural: 'postCards',
|
||||
labelSingular: 'Post Card',
|
||||
labelPlural: 'Post Cards',
|
||||
description: 'A post card object',
|
||||
icon: 'IconMail',
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
|
||||
name: 'content',
|
||||
type: FieldType.TEXT,
|
||||
label: 'Content',
|
||||
description: "Postcard's content",
|
||||
icon: 'IconAbc',
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
|
||||
name: 'recipientName',
|
||||
type: FieldType.FULL_NAME,
|
||||
label: 'Recipient name',
|
||||
icon: 'IconUser',
|
||||
},
|
||||
{
|
||||
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
|
||||
name: 'recipientAddress',
|
||||
type: FieldType.ADDRESS,
|
||||
label: 'Recipient address',
|
||||
icon: 'IconHome',
|
||||
},
|
||||
{
|
||||
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
|
||||
name: 'status',
|
||||
type: FieldType.SELECT,
|
||||
label: 'Status',
|
||||
icon: 'IconSend',
|
||||
defaultValue: `'${PostCardStatus.DRAFT}'`,
|
||||
options: [
|
||||
{ value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' },
|
||||
{ value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' },
|
||||
{ value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' },
|
||||
{ value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' },
|
||||
],
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
|
||||
name: 'deliveredAt',
|
||||
type: FieldType.DATE_TIME,
|
||||
label: 'Delivered at',
|
||||
icon: 'IconCheck',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
النقاط الرئيسية:
|
||||
|
||||
* استخدم `defineObject()` للحصول على تحقق مدمج ودعم أفضل من IDE.
|
||||
* `universalIdentifier` يجب أن يكون فريدًا وثابتًا عبر عمليات النشر.
|
||||
* يتطلب كل حقل `name` و`type` و`label` ومعرّف `universalIdentifier` ثابتًا خاصًا به.
|
||||
* المصفوفة `fields` اختيارية — يمكنك تعريف كائنات بدون حقول مخصصة.
|
||||
* يمكنك إنشاء كائنات جديدة باستخدام `yarn twenty add`، والذي يرشدك خلال التسمية والحقول والعلاقات.
|
||||
|
||||
<Note>
|
||||
**يتم إنشاء الحقول الأساسية تلقائيًا.** عند تعريف كائن مخصص، يضيف Twenty تلقائيًا حقولًا قياسية
|
||||
مثل `id` و`name` و`createdAt` و`updatedAt` و`createdBy` و`updatedBy` و`deletedAt`.
|
||||
لا تحتاج إلى تعريف هذه في مصفوفة `fields` — أضف فقط حقولك المخصصة.
|
||||
يمكنك تجاوز الحقول الافتراضية من خلال تعريف حقل بالاسم نفسه في مصفوفة `fields` الخاصة بك،
|
||||
لكن هذا غير مستحسن.
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineField — الحقول القياسية" description="وسّع الكائنات الموجودة بحقول إضافية">
|
||||
|
||||
استخدم `defineField()` لإضافة حقول إلى كائنات لا تملكها — مثل كائنات Twenty القياسية (Person, Company, etc.) أو كائنات من تطبيقات أخرى. على خلاف الحقول المضمّنة في `defineObject()`، تتطلّب الحقول المستقلة `objectUniversalIdentifier` لتحديد الكائن الذي تقوم بتوسيعه:
|
||||
|
||||
```ts src/fields/company-loyalty-tier.field.ts
|
||||
import { defineField, FieldType } from 'twenty-sdk/define';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890',
|
||||
objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object
|
||||
name: 'loyaltyTier',
|
||||
type: FieldType.SELECT,
|
||||
label: 'Loyalty Tier',
|
||||
icon: 'IconStar',
|
||||
options: [
|
||||
{ value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' },
|
||||
{ value: 'SILVER', label: 'Silver', position: 1, color: 'gray' },
|
||||
{ value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
النقاط الرئيسية:
|
||||
* `objectUniversalIdentifier` يحدّد الكائن الهدف. بالنسبة للكائنات القياسية، استخدم `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` المُصدَّر من `twenty-sdk`.
|
||||
* عند تعريف الحقول بشكل مضمّن في `defineObject()`، **لا** تحتاج إلى `objectUniversalIdentifier` — إذ يُورَّث من الكائن الأب.
|
||||
* `defineField()` هي الطريقة الوحيدة لإضافة حقول إلى كائنات لم تُنشئها باستخدام `defineObject()`.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineField — حقول العلاقات" description="وصِل الكائنات معًا بعلاقات ثنائية الاتجاه">
|
||||
|
||||
تربط العلاقات الكائنات معًا. في Twenty، تكون العلاقات دائمًا **ثنائية الاتجاه** — حيث تعرّف الجانبين، ويشير كل جانب إلى الآخر.
|
||||
|
||||
هناك نوعان من العلاقات:
|
||||
|
||||
| نوع العلاقة | الوصف | هل لديه مفتاح خارجي؟ |
|
||||
| ------------- | ------------------------------------------------------ | ---------------------- |
|
||||
| `MANY_TO_ONE` | تشير العديد من سجلات هذا الكائن إلى سجل واحد من الهدف | نعم (`joinColumnName`) |
|
||||
| `ONE_TO_MANY` | يحتوي سجل واحد من هذا الكائن على العديد من سجلات الهدف | لا (الجانب العكسي) |
|
||||
|
||||
#### كيف تعمل العلاقات
|
||||
|
||||
تتطلّب كل علاقة **حقلين** يشيران إلى بعضهما البعض:
|
||||
|
||||
1. جانب **MANY_TO_ONE** — يوجد على الكائن الذي يحمل المفتاح الخارجي
|
||||
2. جانب **ONE_TO_MANY** — يوجد على الكائن الذي يملك المجموعة
|
||||
|
||||
يستخدم كلا الحقلين `FieldType.RELATION` ويُحيل كلٌ منهما إلى الآخر عبر `relationTargetFieldMetadataUniversalIdentifier`.
|
||||
|
||||
#### مثال: البطاقة البريدية لديها العديد من المستلمين
|
||||
|
||||
افترض أن `PostCard` يمكن إرسالها إلى العديد من سجلات `PostCardRecipient`. ينتمي كل مستلم إلى بطاقة بريدية واحدة بالضبط.
|
||||
|
||||
**الخطوة 1: عرّف جانب ONE_TO_MANY على PostCard** (جانب "الواحد"):
|
||||
|
||||
```ts src/fields/post-card-recipients-on-post-card.field.ts
|
||||
import { defineField, FieldType, RelationType } from 'twenty-sdk/define';
|
||||
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
|
||||
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
|
||||
|
||||
// Export so the other side can reference it
|
||||
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
|
||||
// Import from the other side
|
||||
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
|
||||
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'postCardRecipients',
|
||||
label: 'Post Card Recipients',
|
||||
icon: 'IconUsers',
|
||||
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**الخطوة 2: عرّف جانب MANY_TO_ONE على PostCardRecipient** (جانب "العديد" — يحمل المفتاح الخارجي):
|
||||
|
||||
```ts src/fields/post-card-on-post-card-recipient.field.ts
|
||||
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define';
|
||||
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
|
||||
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
|
||||
|
||||
// Export so the other side can reference it
|
||||
export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222';
|
||||
// Import from the other side
|
||||
import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: POST_CARD_FIELD_ID,
|
||||
objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'postCard',
|
||||
label: 'Post Card',
|
||||
icon: 'IconMail',
|
||||
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.CASCADE,
|
||||
joinColumnName: 'postCardId',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
**الاستيرادات الدائرية:** كلا حقلي العلاقة يُحيل كلٌ منهما إلى `universalIdentifier` الخاص بالآخر. لتجنّب مشكلات الاستيراد الدائري، صدّر معرّفات الحقول كثوابت مسمّاة من كل ملف، واستوردها في الملف الآخر. يقوم نظام البناء بحلّها في وقت التجميع.
|
||||
</Note>
|
||||
|
||||
#### الربط مع الكائنات القياسية
|
||||
|
||||
لإنشاء علاقة مع كائن Twenty مضمّن (Person, Company, etc.)، استخدم `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`:
|
||||
|
||||
```ts src/fields/person-on-self-hosting-user.field.ts
|
||||
import {
|
||||
defineField,
|
||||
FieldType,
|
||||
RelationType,
|
||||
OnDeleteAction,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk/define';
|
||||
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';
|
||||
|
||||
export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
|
||||
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PERSON_FIELD_ID,
|
||||
objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'person',
|
||||
label: 'Person',
|
||||
description: 'Person matching with the self hosting user',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||||
relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'personId',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### خصائص حقل العلاقة
|
||||
|
||||
| الخاصية | مطلوب | الوصف |
|
||||
| ------------------------------------------------- | --------------- | -------------------------------------------------------------------------------------- |
|
||||
| `type` | نعم | يجب أن يكون `FieldType.RELATION` |
|
||||
| `relationTargetObjectMetadataUniversalIdentifier` | نعم | قيمة `universalIdentifier` للكائن الهدف |
|
||||
| `relationTargetFieldMetadataUniversalIdentifier` | نعم | قيمة `universalIdentifier` للحقل المطابق على الكائن الهدف |
|
||||
| `universalSettings.relationType` | نعم | `RelationType.MANY_TO_ONE` أو `RelationType.ONE_TO_MANY` |
|
||||
| `universalSettings.onDelete` | MANY_TO_ONE فقط | ماذا يحدث عند حذف السجل المشار إليه: `CASCADE`، `SET_NULL`، `RESTRICT`، أو `NO_ACTION` |
|
||||
| `universalSettings.joinColumnName` | MANY_TO_ONE فقط | اسم عمود قاعدة البيانات للمفتاح الخارجي (مثل `postCardId`) |
|
||||
|
||||
#### حقول العلاقات المضمّنة في defineObject
|
||||
|
||||
يمكنك أيضًا تعريف حقول العلاقات مباشرةً داخل `defineObject()`. في هذه الحالة، احذف `objectUniversalIdentifier` — إذ يُورَّث من الكائن الأب:
|
||||
|
||||
```ts
|
||||
export default defineObject({
|
||||
universalIdentifier: '...',
|
||||
nameSingular: 'postCardRecipient',
|
||||
// ...
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: POST_CARD_FIELD_ID,
|
||||
type: FieldType.RELATION,
|
||||
name: 'postCard',
|
||||
label: 'Post Card',
|
||||
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.CASCADE,
|
||||
joinColumnName: 'postCardId',
|
||||
},
|
||||
},
|
||||
// ... other fields
|
||||
],
|
||||
});
|
||||
```
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## توليد قوالب الكيانات باستخدام `yarn twenty add`
|
||||
|
||||
بدلًا من إنشاء ملفات الكيانات يدويًا، يمكنك استخدام أداة القوالب التفاعلية:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty add
|
||||
```
|
||||
|
||||
ستطالبك باختيار نوع الكيان وتُرشدك خلال الحقول المطلوبة. تُولّد ملفًا جاهزًا للاستخدام مع `universalIdentifier` ثابت واستدعاء `defineEntity()` الصحيح.
|
||||
|
||||
يمكنك أيضًا تمرير نوع الكيان مباشرة لتخطي المطالبة الأولى:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty add object
|
||||
yarn twenty add logicFunction
|
||||
yarn twenty add frontComponent
|
||||
```
|
||||
|
||||
### أنواع الكيانات المتاحة
|
||||
|
||||
| نوع الكيان | أمر | الملف المُولَّد |
|
||||
| ------------------ | ------------------------------------ | ------------------------------------------------------- |
|
||||
| كائن | `yarn twenty add object` | `src/objects/\<name>.ts` |
|
||||
| الحقل | `yarn twenty add field` | `src/fields/\<name>.ts` |
|
||||
| دالة منطقية | `yarn twenty add logicFunction` | `src/logic-functions/\<name>.ts` |
|
||||
| مكوّن أمامي | `yarn twenty add frontComponent` | `src/front-components/\<name>.tsx` |
|
||||
| دور | `yarn twenty add role` | `src/roles/\<name>.ts` |
|
||||
| مهارة | `yarn twenty add skill` | `src/skills/\<name>.ts` |
|
||||
| وكيل | `yarn twenty add agent` | `src/agents/\<name>.ts` |
|
||||
| عرض | `yarn twenty add view` | `src/views/\<name>.ts` |
|
||||
| عنصر قائمة التنقّل | `yarn twenty add navigationMenuItem` | `src/navigation-menu-items/\<name>.ts` |
|
||||
| تخطيط الصفحة | `yarn twenty add pageLayout` | `src/page-layouts/\<name>.ts` |
|
||||
|
||||
### ما الذي تُنشئه أداة القوالب
|
||||
|
||||
لكل نوع كيان قالب خاص به. على سبيل المثال، يسأل `yarn twenty add object` عن:
|
||||
|
||||
1. **الاسم (مفرد)** — مثل `invoice`
|
||||
2. **الاسم (جمع)** — مثل `invoices`
|
||||
3. **التسمية (مفرد)** — تُستمد تلقائيًا من الاسم (مثل `Invoice`)
|
||||
4. **التسمية (جمع)** — تُملأ تلقائيًا (مثل `Invoices`)
|
||||
5. **إنشاء عرض وعنصر تنقّل؟** — إذا أجبت بنعم، فستُنشئ أداة القوالب أيضًا عرضًا مطابقًا ورابط شريط جانبي للكائن الجديد.
|
||||
|
||||
أنواع الكيانات الأخرى لها مطالبات أبسط — فمعظمها يطلب اسمًا فقط.
|
||||
|
||||
نوع الكيان `field` أكثر تفصيلاً: يطلب اسم الحقل وتسمية الحقل ونوعه (من قائمة بكل أنواع الحقول المتاحة مثل `TEXT` و`NUMBER` و`SELECT` و`RELATION` وغيرها)، ومعرّف `universalIdentifier` للكائن الهدف.
|
||||
|
||||
### مسار خرج مخصّص
|
||||
|
||||
استخدم العلم `--path` لوضع الملف المُولَّد في موقع مخصّص:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty add logicFunction --path src/custom-folder
|
||||
```
|
||||
|
|
@ -0,0 +1,419 @@
|
|||
---
|
||||
title: المكوّنات الأمامية
|
||||
description: Build React components that render inside Twenty's UI with sandboxed isolation.
|
||||
icon: window-maximize
|
||||
---
|
||||
|
||||
المكوّنات الأمامية هي مكوّنات React تُعرَض مباشرة داخل واجهة مستخدم Twenty. تعمل ضمن **Web Worker** معزول باستخدام Remote DOM — تكون شيفرتك في صندوق عزل لكنها تُعرَض أصيلًا داخل الصفحة، وليس ضمن iframe.
|
||||
|
||||
## أين يمكن استخدام مكوّنات الواجهة الأمامية
|
||||
|
||||
يمكن عرض مكوّنات الواجهة الأمامية في موقعين داخل Twenty:
|
||||
|
||||
* **اللوحة الجانبية** — المكوّنات غير عديمة الرأس تفتح في اللوحة الجانبية اليمنى. هذا هو السلوك الافتراضي عندما يتم تشغيل مكوّن واجهة أمامية من قائمة الأوامر.
|
||||
* **الويدجت (لوحات المعلومات وصفحات السجلات)** — يمكن تضمين مكوّنات الواجهة الأمامية كويدجت داخل تخطيطات الصفحات. عند تكوين لوحة معلومات أو تخطيط صفحة سجل، يمكن للمستخدمين إضافة ويدجت لمكوّن واجهة أمامية.
|
||||
|
||||
## مثال أساسي
|
||||
|
||||
أسرع طريقة لرؤية مكوّن أمامي قيد العمل هي تسجيله كأمر. إضافة حقل `command` مع `isPinned: true` يجعلُه يظهر كزر إجراء سريع في الزاوية العلوية اليمنى من الصفحة — دون الحاجة إلى تخطيط صفحة:
|
||||
|
||||
```tsx src/front-components/hello-world.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
|
||||
const HelloWorld = () => {
|
||||
return (
|
||||
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
|
||||
<h1>Hello from my app!</h1>
|
||||
<p>This component renders inside Twenty.</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
|
||||
name: 'hello-world',
|
||||
description: 'A simple front component',
|
||||
component: HelloWorld,
|
||||
command: {
|
||||
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
|
||||
shortLabel: 'Hello',
|
||||
label: 'Hello World',
|
||||
icon: 'IconBolt',
|
||||
isPinned: true,
|
||||
availabilityType: 'GLOBAL',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
بعد المزامنة باستخدام `yarn twenty dev` (أو تشغيل الأمر لمرة واحدة `yarn twenty dev --once`)، يظهر الإجراء السريع في الزاوية العلوية اليمنى من الصفحة:
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="زر إجراء سريع في الزاوية العلوية اليمنى" />
|
||||
</div>
|
||||
|
||||
انقره لعرض المكوّن مضمنًا داخل الصفحة.
|
||||
|
||||
## حقول التكوين
|
||||
|
||||
| الحقل | مطلوب | الوصف |
|
||||
| --------------------- | ----- | ----------------------------------------------------------------- |
|
||||
| `universalIdentifier` | نعم | معرّف فريد ثابت لهذا المكوّن |
|
||||
| `component` | نعم | دالة مكوّن React |
|
||||
| `name` | لا | اسم العرض |
|
||||
| `description` | لا | وصف لما يفعله المكوّن |
|
||||
| `isHeadless` | لا | عيِّنه إلى `true` إذا كان المكوّن بلا واجهة مرئية (انظر أدناه) |
|
||||
| `command` | لا | سجّل المكوّن كأمر (انظر [خيارات الأوامر](#command-options) أدناه) |
|
||||
|
||||
## وضع مكوّن أمامي على صفحة
|
||||
|
||||
إضافةً إلى الأوامر، يمكنك تضمين مكوّن أمامي مباشرةً في صفحة سجل عبر إضافته كودجت في **تخطيط صفحة**. راجع قسم [definePageLayout](/l/ar/developers/extend/apps/skills-and-agents#definepagelayout) للتفاصيل.
|
||||
|
||||
## عديم الرأس مقابل غير عديم الرأس
|
||||
|
||||
تأتي مكوّنات الواجهة الأمامية بوضعَي عرض يتحكّم بهما الخيار `isHeadless`:
|
||||
|
||||
**غير عديم الرأس (افتراضي)** — يعرض المكوّن واجهة مستخدم مرئية. عند تشغيله من قائمة الأوامر يفتح في اللوحة الجانبية. هذا هو السلوك الافتراضي عندما تكون `isHeadless` تساوي `false` أو يتم تجاهلها.
|
||||
|
||||
**عديم الرأس (`isHeadless: true`)** — يتم تركيب المكوّن بشكل غير مرئي في الخلفية. لا يفتح اللوحة الجانبية. تم تصميم المكوّنات عديمة الرأس لإجراءات تنفّذ منطقًا ثم تُزيل تركيبها ذاتيًا — على سبيل المثال، تشغيل مهمة غير متزامنة، أو الانتقال إلى صفحة، أو إظهار نافذة تأكيد منبثقة. تتوافق بشكل طبيعي مع مكوّنات Command في SDK الموصوفة أدناه.
|
||||
|
||||
```tsx src/front-components/sync-tracker.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { useRecordId, enqueueSnackbar } from 'twenty-sdk/front-component';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const SyncTracker = () => {
|
||||
const recordId = useRecordId();
|
||||
|
||||
useEffect(() => {
|
||||
enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' });
|
||||
}, [recordId]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'sync-tracker',
|
||||
description: 'Tracks record views silently',
|
||||
isHeadless: true,
|
||||
component: SyncTracker,
|
||||
});
|
||||
```
|
||||
|
||||
نظرًا لأن المكوّن يُرجع `null`، فإن Twenty يتخطّى عرض حاوية له — ولن تظهر مساحة فارغة في التخطيط. لا يزال لدى المكوّن إمكانية الوصول إلى جميع الخطافات وواجهة برمجة الاتصال مع المضيف.
|
||||
|
||||
## مكوّنات Command في SDK
|
||||
|
||||
توفر حزمة `twenty-sdk` أربعة مكوّنات مساعدة من نوع Command مصممة للمكوّنات عديمة الرأس في الواجهة الأمامية. كل مكوّن ينفّذ إجراءً عند التركيب، ويتعامل مع الأخطاء بعرض إشعار Snackbar، ويزيل تركيب مكوّن الواجهة الأمامية تلقائيًا عند الانتهاء.
|
||||
|
||||
استوردها من `twenty-sdk/command`:
|
||||
|
||||
* **`Command`** — يشغّل رد نداء غير متزامن عبر الخاصية `execute`.
|
||||
* **`CommandLink`** — ينتقل إلى مسار في التطبيق. الخصائص: `to`، `params`، `queryParams`، `options`.
|
||||
* **`CommandModal`** — يفتح نافذة تأكيد منبثقة. إذا أكّد المستخدم، ينفّذ رد النداء `execute`. الخصائص: `title`، `subtitle`، `execute`، `confirmButtonText`، `confirmButtonAccent`.
|
||||
* **`CommandOpenSidePanelPage`** — يفتح صفحة محدّدة في اللوحة الجانبية. الخصائص: `page`، `pageTitle`، `pageIcon`.
|
||||
|
||||
فيما يلي مثال كامل لمكوّن واجهة أمامية عديم الرأس يستخدم `Command` لتشغيل إجراء من قائمة الأوامر:
|
||||
|
||||
```tsx src/front-components/run-action.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Command } from 'twenty-sdk/command';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
const RunAction = () => {
|
||||
const execute = async () => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
await client.mutation({
|
||||
createTask: {
|
||||
__args: { data: { title: 'Created by my app' } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return <Command execute={execute} />;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
|
||||
name: 'run-action',
|
||||
description: 'Creates a task from the command menu',
|
||||
component: RunAction,
|
||||
isHeadless: true,
|
||||
command: {
|
||||
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
|
||||
label: 'Run my action',
|
||||
icon: 'IconPlayerPlay',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
ومثال يستخدم `CommandModal` لطلب التأكيد قبل التنفيذ:
|
||||
|
||||
```tsx src/front-components/delete-draft.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { CommandModal } from 'twenty-sdk/command';
|
||||
|
||||
const DeleteDraft = () => {
|
||||
const execute = async () => {
|
||||
// perform the deletion
|
||||
};
|
||||
|
||||
return (
|
||||
<CommandModal
|
||||
title="Delete draft?"
|
||||
subtitle="This action cannot be undone."
|
||||
execute={execute}
|
||||
confirmButtonText="Delete"
|
||||
confirmButtonAccent="danger"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
|
||||
name: 'delete-draft',
|
||||
description: 'Deletes a draft with confirmation',
|
||||
component: DeleteDraft,
|
||||
isHeadless: true,
|
||||
command: {
|
||||
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
|
||||
label: 'Delete draft',
|
||||
icon: 'IconTrash',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## الوصول إلى سياق وقت التشغيل
|
||||
|
||||
داخل مكوّنك، استخدم خطافات SDK للوصول إلى المستخدم الحالي، والسجل، ومثيل المكوّن:
|
||||
|
||||
```tsx src/front-components/record-info.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
useUserId,
|
||||
useRecordId,
|
||||
useFrontComponentId,
|
||||
} from 'twenty-sdk/front-component';
|
||||
|
||||
const RecordInfo = () => {
|
||||
const userId = useUserId();
|
||||
const recordId = useRecordId();
|
||||
const componentId = useFrontComponentId();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>User: {userId}</p>
|
||||
<p>Record: {recordId ?? 'No record context'}</p>
|
||||
<p>Component: {componentId}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012',
|
||||
name: 'record-info',
|
||||
component: RecordInfo,
|
||||
});
|
||||
```
|
||||
|
||||
الخطافات المتاحة:
|
||||
|
||||
| الخطّاف | القيم المعادة | الوصف |
|
||||
| --------------------------------------------- | ------------------ | ---------------------------------------------- |
|
||||
| `useUserId()` | `string` أو `null` | معرّف المستخدم الحالي |
|
||||
| `useRecordId()` | `string` أو `null` | معرّف السجل الحالي (عند وضعه على صفحة سجل) |
|
||||
| `useFrontComponentId()` | `string` | معرّف مثيل هذا المكوّن |
|
||||
| `useFrontComponentExecutionContext(selector)` | يختلف | الوصول إلى سياق التنفيذ الكامل عبر دالة محدِّد |
|
||||
|
||||
## واجهة الاتصال مع المضيف
|
||||
|
||||
يمكن للمكوّنات الأمامية تشغيل التنقّل والنوافذ المنبثقة والإشعارات باستخدام دوال من `twenty-sdk`:
|
||||
|
||||
| دالة | الوصف |
|
||||
| ----------------------------------------------- | ------------------------------ |
|
||||
| `navigate(to, params?, queryParams?, options?)` | الانتقال إلى صفحة داخل التطبيق |
|
||||
| `openSidePanelPage(params)` | فتح لوحة جانبية |
|
||||
| `closeSidePanel()` | إغلاق اللوحة الجانبية |
|
||||
| `openCommandConfirmationModal(params)` | عرض مربع حوار تأكيد |
|
||||
| `enqueueSnackbar(params)` | عرض إشعار توست |
|
||||
| `unmountFrontComponent()` | إلغاء تركيب المكوّن |
|
||||
| `updateProgress(progress)` | تحديث مؤشّر التقدّم |
|
||||
|
||||
فيما يلي مثال يستخدم واجهة برمجة تطبيقات المضيف لعرض Snackbar وإغلاق اللوحة الجانبية بعد اكتمال الإجراء:
|
||||
|
||||
```tsx src/front-components/archive-record.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { useRecordId } from 'twenty-sdk/front-component';
|
||||
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
const ArchiveRecord = () => {
|
||||
const recordId = useRecordId();
|
||||
|
||||
const handleArchive = async () => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
await client.mutation({
|
||||
updateTask: {
|
||||
__args: { id: recordId, data: { status: 'ARCHIVED' } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
await enqueueSnackbar({
|
||||
message: 'Record archived',
|
||||
variant: 'success',
|
||||
});
|
||||
|
||||
await closeSidePanel();
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<p>Archive this record?</p>
|
||||
<button onClick={handleArchive}>Archive</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
|
||||
name: 'archive-record',
|
||||
description: 'Archives the current record',
|
||||
component: ArchiveRecord,
|
||||
});
|
||||
```
|
||||
|
||||
## خيارات الأوامر
|
||||
|
||||
إضافة حقل `command` إلى `defineFrontComponent` تُسجِّل المكوّن في قائمة الأوامر (Cmd+K). إذا كانت قيمة `isPinned` هي `true`، فسيظهر أيضًا كزر إجراء سريع في الزاوية العلوية اليمنى من الصفحة.
|
||||
|
||||
| الحقل | مطلوب | الوصف |
|
||||
| --------------------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `universalIdentifier` | نعم | معرّف فريد ثابت للأمر |
|
||||
| `label` | نعم | التسمية الكاملة المعروضة في قائمة الأوامر (Cmd+K) |
|
||||
| `shortLabel` | لا | تسمية أقصر تُعرَض على زر الإجراء السريع المثبّت |
|
||||
| `icon` | لا | اسم الأيقونة المعروض بجانب التسمية (مثل `'IconBolt'` و`'IconSend'`) |
|
||||
| `isPinned` | لا | عند كونها `true`، يعرض الأمر كزر إجراء سريع في الزاوية العلوية اليمنى من الصفحة |
|
||||
| `availabilityType` | لا | تتحكّم في مكان ظهور الأمر: `'GLOBAL'` (متاح دائمًا)، و`'RECORD_SELECTION'` (فقط عند تحديد سجلات)، أو `'FALLBACK'` (يُعرَض عند عدم تطابق أي أوامر أخرى) |
|
||||
| `availabilityObjectUniversalIdentifier` | لا | تقييد الأمر بصفحات نوع كائن معيّن (مثل سجلات Company فقط) |
|
||||
| `conditionalAvailabilityExpression` | لا | تعبير منطقي للتحكم ديناميكيًا في ما إذا كان الأمر مرئيًا (انظر أدناه) |
|
||||
|
||||
## تعابير الإتاحة الشرطية
|
||||
|
||||
يتيح لك الحقل `conditionalAvailabilityExpression` التحكّم في وقت ظهور الأمر بناءً على سياق الصفحة الحالي. استورد متغيّرات ومشغّلات مضبوطة الأنواع من `twenty-sdk` لبناء التعابير:
|
||||
|
||||
```tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
pageType,
|
||||
numberOfSelectedRecords,
|
||||
objectPermissions,
|
||||
everyEquals,
|
||||
isDefined,
|
||||
} from 'twenty-sdk/front-component';
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'bulk-action',
|
||||
component: BulkAction,
|
||||
command: {
|
||||
universalIdentifier: '...',
|
||||
label: 'Bulk Update',
|
||||
availabilityType: 'RECORD_SELECTION',
|
||||
conditionalAvailabilityExpression: everyEquals(
|
||||
objectPermissions,
|
||||
'canUpdateObjectRecords',
|
||||
true,
|
||||
),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**متغيّرات السياق** — تُمثّل الحالة الحالية للصفحة:
|
||||
|
||||
| المتغيّر | النوع | الوصف |
|
||||
| ------------------------------ | ------------- | --------------------------------------------------------------- |
|
||||
| `pageType` | `string` | نوع الصفحة الحالي (مثل `'RecordIndexPage'` و`'RecordShowPage'`) |
|
||||
| `isInSidePanel` | `boolean` | ما إذا كان المكوّن معروضًا في لوحة جانبية |
|
||||
| `numberOfSelectedRecords` | `number` | عدد السجلات المحدّدة حاليًا |
|
||||
| `isSelectAll` | `boolean` | ما إذا كان "تحديد الكل" مفعّلًا |
|
||||
| `selectedRecords` | `array` | كائنات السجلات المحدّدة |
|
||||
| `favoriteRecordIds` | `array` | معرّفات السجلات المفضّلة |
|
||||
| `objectPermissions` | `object` | الأذونات الخاصة بنوع الكائن الحالي |
|
||||
| `targetObjectReadPermissions` | `object` | أذونات القراءة للكائن الهدف |
|
||||
| `targetObjectWritePermissions` | `object` | أذونات الكتابة للكائن الهدف |
|
||||
| `featureFlags` | `object` | أعلام الميزات المفعَّلة |
|
||||
| `objectMetadataItem` | `object` | بيانات التعريف لنوع الكائن الحالي |
|
||||
| `hasAnySoftDeleteFilterOnView` | `قيمة منطقية` | ما إذا كان العرض الحالي يحتوي على مرشّح حذف منطقي |
|
||||
|
||||
**المُشغِّلات** — جمّع المتغيّرات في تعابير منطقية:
|
||||
|
||||
| المُشغِّل | الوصف |
|
||||
| ----------------------------------- | -------------------------------------------------------------- |
|
||||
| `isDefined(value)` | `true` إذا لم تكن القيمة null/undefined |
|
||||
| `isNonEmptyString(value)` | `true` إذا كانت القيمة سلسلة غير فارغة |
|
||||
| `includes(array, value)` | `true` إذا كانت المصفوفة تحتوي على القيمة |
|
||||
| `includesEvery(array, prop, value)` | `true` إذا كانت خاصية كل عنصر تتضمن القيمة |
|
||||
| `every(array, prop)` | `true` إذا كانت الخاصية تُقيَّم بصحّة في كل عنصر |
|
||||
| `everyDefined(array, prop)` | `true` إذا كانت الخاصية معرّفة في كل عنصر |
|
||||
| `everyEquals(array, prop, value)` | `true` إذا كانت الخاصية تساوي القيمة في كل عنصر |
|
||||
| `some(array, prop)` | `true` إذا كانت الخاصية تُقيَّم بصحّة في عنصر واحد على الأقل |
|
||||
| `someDefined(array, prop)` | `true` إذا كانت الخاصية معرّفة في عنصر واحد على الأقل |
|
||||
| `someEquals(array, prop, value)` | `true` إذا كانت الخاصية تساوي القيمة في عنصر واحد على الأقل |
|
||||
| `someNonEmptyString(array, prop)` | `true` إذا كانت الخاصية سلسلة غير فارغة في عنصر واحد على الأقل |
|
||||
| `none(array, prop)` | `true` إذا كانت الخاصية تُقيَّم بخطأ في كل عنصر |
|
||||
| `noneDefined(array, prop)` | `true` إذا كانت الخاصية غير معرّفة في كل عنصر |
|
||||
| `noneEquals(array, prop, value)` | `true` إذا لم تكن الخاصية تساوي القيمة في أي عنصر |
|
||||
|
||||
## الأصول العامة
|
||||
|
||||
يمكن للمكوّنات الأمامية الوصول إلى ملفات من دليل `public/` للتطبيق باستخدام `getPublicAssetUrl`:
|
||||
|
||||
```tsx
|
||||
import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define';
|
||||
|
||||
const Logo = () => <img src={getPublicAssetUrl('logo.png')} alt="Logo" />;
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'logo',
|
||||
component: Logo,
|
||||
});
|
||||
```
|
||||
|
||||
راجع [قسم الأصول العامة](/l/ar/developers/extend/apps/cli-and-testing#public-assets-public-folder) للتفاصيل.
|
||||
|
||||
## التنسيق
|
||||
|
||||
تدعم المكوّنات الأمامية عدة أساليب للتنسيق. يمكنك استخدام:
|
||||
|
||||
* **أنماط مضمنة** — `style={{ color: 'red' }}`
|
||||
* **مكوّنات Twenty لواجهة المستخدم** — استورد من `twenty-sdk/ui` (Button وTag وStatus وChip وAvatar وغيرها)
|
||||
* **Emotion** — CSS-in-JS مع `@emotion/react`
|
||||
* **Styled-components** — أنماط `styled.div`
|
||||
* **Tailwind CSS** — أصناف مساعدة
|
||||
* **أي مكتبة CSS-in-JS** متوافقة مع React
|
||||
|
||||
```tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Button, Tag, Status } from 'twenty-sdk/ui';
|
||||
|
||||
const StyledWidget = () => {
|
||||
return (
|
||||
<div style={{ padding: '16px', display: 'flex', gap: '8px' }}>
|
||||
<Button title="Click me" onClick={() => alert('Clicked!')} />
|
||||
<Tag text="Active" color="green" />
|
||||
<Status color="green" text="Online" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456',
|
||||
name: 'styled-widget',
|
||||
component: StyledWidget,
|
||||
});
|
||||
```
|
||||
|
|
@ -1,12 +1,9 @@
|
|||
---
|
||||
title: البدء
|
||||
icon: rocket
|
||||
description: أنشئ أول تطبيق Twenty خلال دقائق.
|
||||
---
|
||||
|
||||
<Warning>
|
||||
التطبيقات حاليًا في مرحلة الألفا. الميزة تعمل لكنها لا تزال قيد التطور.
|
||||
</Warning>
|
||||
|
||||
## ما هي التطبيقات؟
|
||||
|
||||
تتيح لك التطبيقات توسيع Twenty باستخدام كائنات وحقول مخصّصة ووظائف منطقية ومكوّنات الواجهة الأمامية ومهارات الذكاء الاصطناعي وغير ذلك — جميعها تُدار ككود. بدلًا من تكوين كل شيء عبر واجهة المستخدم، تعرّف نموذج بياناتك ومنطقك في TypeScript وتقوم بنشره إلى مساحة عمل واحدة أو أكثر.
|
||||
|
|
|
|||
131
packages/twenty-docs/l/ar/developers/extend/apps/layout.mdx
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
---
|
||||
title: التخطيط
|
||||
description: Define views, navigation menu items, and page layouts to shape how your app appears in Twenty.
|
||||
icon: table-columns
|
||||
---
|
||||
|
||||
Layout entities control how your app surfaces inside Twenty's UI — what lives in the sidebar, which saved views ship with the app, and how a record detail page is arranged.
|
||||
|
||||
## Layout concepts
|
||||
|
||||
| Concept | What it controls | كيان |
|
||||
| ------------------------ | --------------------------------------------------------------------------------- | -------------------------- |
|
||||
| **View** | A saved list configuration for an object — visible fields, order, filters, groups | `defineView` |
|
||||
| **Navigation Menu Item** | An entry in the left sidebar that links to a view or an external URL | `defineNavigationMenuItem` |
|
||||
| **Page Layout** | The tabs and widgets that make up a record's detail page | `definePageLayout` |
|
||||
|
||||
Views, navigation items, and page layouts reference each other by `universalIdentifier`:
|
||||
|
||||
* A **navigation menu item** of type `VIEW` points at a `defineView` identifier, so the sidebar link opens that saved view.
|
||||
* A **page layout** of type `RECORD_PAGE` targets an object and can embed [front components](/l/ar/developers/extend/apps/front-components) inside its tabs as widgets.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="defineView" description="تعريف العروض المحفوظة للكائنات">
|
||||
|
||||
العروض هي تكوينات محفوظة لكيفية عرض سجلات كائن ما — بما في ذلك الحقول المرئية وترتيبها وأي مرشّحات أو مجموعات مُطبَّقة. استخدم `defineView()` لتضمين عروض مُهيّأة مسبقًا مع تطبيقك:
|
||||
|
||||
```ts src/views/example-view.ts
|
||||
import { defineView, ViewKey } from 'twenty-sdk/define';
|
||||
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
|
||||
import { NAME_FIELD_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
|
||||
|
||||
export default defineView({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'All example items',
|
||||
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
icon: 'IconList',
|
||||
key: ViewKey.INDEX,
|
||||
position: 0,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: 'f926bdb7-6af7-4683-9a09-adbca56c29f0',
|
||||
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
النقاط الرئيسية:
|
||||
* `objectUniversalIdentifier` يحدّد الكائن الذي ينطبق عليه هذا العرض.
|
||||
* `key` يحدّد نوع العرض (مثل `ViewKey.INDEX` لعرض القائمة الرئيسي).
|
||||
* `fields` يتحكّم في الأعمدة الظاهرة وترتيبها. يشير كل حقل إلى `fieldMetadataUniversalIdentifier`.
|
||||
* يمكنك أيضًا تعريف `filters` و`filterGroups` و`groups` و`fieldGroups` لمزيد من التكوينات المتقدمة.
|
||||
* `position` يتحكّم في الترتيب عند وجود عدة عروض لنفس الكائن.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineNavigationMenuItem" description="تعريف روابط التنقل في الشريط الجانبي">
|
||||
|
||||
تضيف عناصر قائمة التنقل إدخالات مخصّصة إلى الشريط الجانبي لمساحة العمل. استخدم `defineNavigationMenuItem()` للارتباط بالعروض أو عناوين URL خارجية أو الكائنات:
|
||||
|
||||
```ts src/navigation-menu-items/example-navigation-menu-item.ts
|
||||
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define';
|
||||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from '../views/example-view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: '9327db91-afa1-41b6-bd9d-2b51a26efb4c',
|
||||
name: 'example-navigation-menu-item',
|
||||
icon: 'IconList',
|
||||
color: 'blue',
|
||||
position: 0,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
النقاط الرئيسية:
|
||||
* `type` يحدّد إلى ماذا يرتبط عنصر القائمة: `NavigationMenuItemType.VIEW` لعرض محفوظ، أو `NavigationMenuItemType.LINK` لعنوان URL خارجي.
|
||||
* لروابط العروض، عيِّن `viewUniversalIdentifier`. لروابط خارجية، عيِّن `link`.
|
||||
* `position` يتحكّم في الترتيب ضمن الشريط الجانبي.
|
||||
* `icon` و`color` (اختياريان) يخصّصان المظهر.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePageLayout" description="عرّف تخطيطات صفحات مخصّصة لعرض السجلات">
|
||||
|
||||
تتيح لك تخطيطات الصفحات تخصيص مظهر صفحة تفاصيل السجل — ما الألسنة التي تظهر، وما الويدجتات داخل كل لسان، وكيف يتم ترتيبها. استخدم `definePageLayout()` لتضمين تخطيطات مخصّصة مع تطبيقك:
|
||||
|
||||
```ts src/page-layouts/example-record-page-layout.ts
|
||||
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
|
||||
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
|
||||
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
|
||||
|
||||
export default definePageLayout({
|
||||
universalIdentifier: '203aeb94-6701-46d6-9af1-be2bbcc9e134',
|
||||
name: 'Example Record Page',
|
||||
type: 'RECORD_PAGE',
|
||||
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
tabs: [
|
||||
{
|
||||
universalIdentifier: '6ed26b60-a51d-4ad7-86dd-1c04c7f3cac5',
|
||||
title: 'Hello World',
|
||||
position: 50,
|
||||
icon: 'IconWorld',
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||||
widgets: [
|
||||
{
|
||||
universalIdentifier: 'aa4234e0-2e5f-4c02-a96a-573449e2351d',
|
||||
title: 'Hello World',
|
||||
type: 'FRONT_COMPONENT',
|
||||
configuration: {
|
||||
configurationType: 'FRONT_COMPONENT',
|
||||
frontComponentUniversalIdentifier:
|
||||
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
النقاط الرئيسية:
|
||||
* `type` يكون عادة `'RECORD_PAGE'` لتخصيص عرض التفاصيل لكائن محدّد.
|
||||
* `objectUniversalIdentifier` يحدّد الكائن الذي ينطبق عليه هذا التخطيط.
|
||||
* يُعرّف كل `tab` قسمًا من الصفحة مع `title` و`position` و`layoutMode` (`CANVAS` لتخطيط حرّ).
|
||||
* يمكن لكل `widget` داخل لسان أن يعرض مكوّنًا أماميًا أو قائمة علاقات أو أنواع ويدجت مدمجة أخرى.
|
||||
* `position` على الألسنة يتحكّم في ترتيبها. استخدم قيمًا أعلى (مثل 50) لوضع الألسنة المخصّصة بعد الألسنة المدمجة.
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
|
@ -0,0 +1,559 @@
|
|||
---
|
||||
title: الوظائف المنطقية
|
||||
description: Define server-side TypeScript functions with HTTP, cron, and database event triggers.
|
||||
icon: bolt
|
||||
---
|
||||
|
||||
Logic functions are server-side TypeScript functions that run on the Twenty platform. They can be triggered by HTTP requests, cron schedules, or database events — and can also be exposed as tools for AI agents.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="defineLogicFunction" description="عرّف الدوال المنطقية ومشغّلاتها">
|
||||
|
||||
كل ملف وظيفة يستخدم `defineLogicFunction()` لتصدير تكوين مع معالج ومشغّلات اختيارية.
|
||||
|
||||
```ts src/logic-functions/createPostCard.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk/define';
|
||||
import { CoreApiClient, type Person } from 'twenty-client-sdk/core';
|
||||
|
||||
const handler = async (params: RoutePayload) => {
|
||||
const client = new CoreApiClient();
|
||||
const name = 'name' in params.queryStringParameters
|
||||
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
|
||||
: 'Hello world';
|
||||
|
||||
const result = await client.mutation({
|
||||
createPostCard: {
|
||||
__args: { data: { name } },
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'create-new-post-card',
|
||||
timeoutSeconds: 2,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/post-card/create',
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
/*databaseEventTriggerSettings: {
|
||||
eventName: 'people.created',
|
||||
},*/
|
||||
/*cronTriggerSettings: {
|
||||
pattern: '0 0 1 1 *',
|
||||
},*/
|
||||
});
|
||||
```
|
||||
|
||||
أنواع المشغّلات المتاحة:
|
||||
* **httpRoute**: يعرِض وظيفتك على مسار وطريقة HTTP **تحت نقطة النهاية `/s/`**:
|
||||
> مثال: `path: '/post-card/create'` يمكن استدعاؤه عبر `https://your-twenty-server.com/s/post-card/create`
|
||||
* **cron**: يشغّل وظيفتك على جدول باستخدام تعبير CRON.
|
||||
* **databaseEvent**: يعمل على أحداث دورة حياة كائنات مساحة العمل. عندما تكون عملية الحدث هي `updated`، يمكن تحديد الحقول المحددة المراد الاستماع إليها في مصفوفة `updatedFields`. إذا تُركت غير معرّفة أو فارغة، فسيؤدي أي تحديث إلى تشغيل الدالة.
|
||||
> مثال: `person.updated`، `*.created`، `company.*`
|
||||
|
||||
<Note>
|
||||
يمكنك أيضًا تنفيذ دالة يدويًا باستخدام CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty exec -n create-new-post-card -p '{"key": "value"}'
|
||||
```
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty exec -y e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
||||
```
|
||||
|
||||
يمكنك متابعة السجلات باستخدام:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty logs
|
||||
```
|
||||
</Note>
|
||||
|
||||
#### حمولة مشغل المسار
|
||||
|
||||
عندما يستدعي مُشغِّل المسار وظيفتك المنطقية، فإنها تتلقّى كائن `RoutePayload` الذي يتبع [صيغة AWS HTTP API v2](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html).
|
||||
استورد نوع `RoutePayload` من `twenty-sdk`:
|
||||
|
||||
```ts
|
||||
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const { headers, queryStringParameters, pathParameters, body } = event;
|
||||
const { method, path } = event.requestContext.http;
|
||||
|
||||
return { message: 'Success' };
|
||||
};
|
||||
```
|
||||
|
||||
يحتوي نوع `RoutePayload` على البنية التالية:
|
||||
|
||||
| الخاصية | النوع | الوصف | مثال |
|
||||
| ---------------------------- | ------------------------------------------------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------- |
|
||||
| `headers` | `Record\<string, string \| undefined>` | رؤوس HTTP (فقط تلك المدرجة في `forwardedRequestHeaders`) | انظر القسم أدناه |
|
||||
| `queryStringParameters` | `Record\<string, string \| undefined>` | معلمات سلسلة الاستعلام (تُضمّ القيم المتعددة باستخدام فواصل) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
|
||||
| `pathParameters` | `Record\<string, string \| undefined>` | معلمات المسار المستخرجة من نمط المسار | `/users/:id`, `/users/123` -> `{ id: '123' }` |
|
||||
| `body` | `object \| null` | جسم الطلب المُحلَّل (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
|
||||
| `isBase64Encoded` | `boolean` | ما إذا كان جسم الطلب مُرمَّزًا بترميز base64 | |
|
||||
| `requestContext.http.method` | `string` | طريقة HTTP (GET, POST, PUT, PATCH, DELETE) | |
|
||||
| `requestContext.http.path` | `string` | المسار الخام للطلب | |
|
||||
|
||||
|
||||
#### forwardedRequestHeaders
|
||||
|
||||
افتراضيًا، **لا** تُمرَّر رؤوس HTTP من الطلبات الواردة إلى دالتك المنطقية لأسباب أمنية.
|
||||
للوصول إلى رؤوس محددة، أدرِجها في مصفوفة `forwardedRequestHeaders`:
|
||||
|
||||
```ts
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'webhook-handler',
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/webhook',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: false,
|
||||
forwardedRequestHeaders: ['x-webhook-signature', 'content-type'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
في معالجك، يمكنك الوصول إلى الرؤوس المُمرَّرة بهذه الطريقة:
|
||||
|
||||
```ts
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const signature = event.headers['x-webhook-signature'];
|
||||
const contentType = event.headers['content-type'];
|
||||
|
||||
// Validate webhook signature...
|
||||
return { received: true };
|
||||
};
|
||||
```
|
||||
|
||||
<Note>
|
||||
تُحوَّل أسماء الرؤوس إلى أحرف صغيرة. يمكنك الوصول إليها باستخدام مفاتيح بأحرف صغيرة (على سبيل المثال، `event.headers['content-type']`).
|
||||
</Note>
|
||||
|
||||
#### إتاحة دالة كأداة
|
||||
|
||||
يمكن إتاحة الدوال المنطقية بوصفها **أدوات** لوكلاء الذكاء الاصطناعي وسير العمل. عند تمييز دالة كأداة، تصبح قابلة للاكتشاف بواسطة ميزات الذكاء الاصطناعي في Twenty ويمكن استخدامها في أتمتة سير العمل.
|
||||
|
||||
لتمييز دالة منطقية كأداة، عيِّن `isTool: true`:
|
||||
|
||||
```ts src/logic-functions/enrich-company.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
const handler = async (params: { companyName: string; domain?: string }) => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
const result = await client.mutation({
|
||||
createTask: {
|
||||
__args: {
|
||||
data: {
|
||||
title: `Enrich data for ${params.companyName}`,
|
||||
body: `Domain: ${params.domain ?? 'unknown'}`,
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
return { taskId: result.createTask.id };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
|
||||
name: 'enrich-company',
|
||||
description: 'Enrich a company record with external data',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
isTool: true,
|
||||
});
|
||||
```
|
||||
|
||||
النقاط الرئيسية:
|
||||
|
||||
* يمكنك دمج `isTool` مع المشغِّلات — إذ يمكن للدالة أن تكون أداة (قابلة للاستدعاء من قِبل وكلاء الذكاء الاصطناعي) وأن تُشغَّل بواسطة الأحداث في الوقت نفسه.
|
||||
* **`toolInputSchema`** (اختياري): كائن JSON Schema يصف المعلمات التي تقبلها دالتك. يُحسَب المخطط تلقائيًا من خلال تحليل ساكن للشيفرة المصدرية، ولكن يمكنك تعيينه صراحةً:
|
||||
|
||||
```ts
|
||||
export default defineLogicFunction({
|
||||
...,
|
||||
toolInputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
companyName: {
|
||||
type: 'string',
|
||||
description: 'The name of the company to enrich',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
description: 'The company website domain (optional)',
|
||||
},
|
||||
},
|
||||
required: ['companyName'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
**اكتب `description` جيدًا.** يعتمد وكلاء الذكاء الاصطناعي على حقل `description` الخاص بالدالة لتحديد وقت استخدام الأداة. كن محددًا بشأن ما تفعله الأداة ومتى ينبغي استدعاؤها.
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePostInstallLogicFunction" description="تعريف دالة منطقية لما بعد التثبيت (واحدة لكل تطبيق)">
|
||||
|
||||
دالة ما بعد التثبيت هي دالة منطقية تعمل تلقائيًا بعد تثبيت تطبيقك على مساحة عمل. ينفّذه الخادم **بعد** مزامنة البيانات الوصفية للتطبيق وإنشاء عميل SDK، بحيث تكون مساحة العمل جاهزة تمامًا للاستخدام ويكون المخطط الجديد مطبَّقًا. تشمل حالات الاستخدام النموذجية تهيئة البيانات الافتراضية، وإنشاء السجلات الأولية، وتكوين إعدادات مساحة العمل، أو توفير الموارد على خدمات جهات خارجية.
|
||||
|
||||
```ts src/logic-functions/post-install.ts
|
||||
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
|
||||
const handler = async (payload: InstallPayload): Promise<void> => {
|
||||
console.log('Post install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
|
||||
name: 'post-install',
|
||||
description: 'Runs after installation to set up the application.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: false,
|
||||
shouldRunSynchronously: false,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
يمكنك أيضًا تنفيذ دالة ما بعد التثبيت يدويًا في أي وقت باستخدام CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty exec --postInstall
|
||||
```
|
||||
|
||||
النقاط الرئيسية:
|
||||
* تستخدم دوال ما بعد التثبيت `definePostInstallLogicFunction()` — وهو إصدار متخصص يستبعد إعدادات المُشغِّل (`cronTriggerSettings` و`databaseEventTriggerSettings` و`httpRouteTriggerSettings` و`isTool`).
|
||||
* يتلقى المعالج `InstallPayload` يحتوي على `{ previousVersion?: string; newVersion: string }` — حيث إن `newVersion` هو الإصدار الجاري تثبيته، و`previousVersion` هو الإصدار الذي كان مُثبّتًا سابقًا (أو `undefined` عند التثبيت الأولي). استخدم هذه القيم للتمييز بين عمليات التثبيت الجديدة والترقيات ولتشغيل منطق الترحيل الخاص بالإصدار.
|
||||
* **موعد تشغيل الخطاف**: في عمليات التثبيت الجديدة فقط، افتراضيًا. مرّر `shouldRunOnVersionUpgrade: true` إذا كنت تريد تشغيله أيضًا عند ترقية التطبيق من إصدار سابق. عند إغفاله، تكون القيمة الافتراضية للعلم `false`، وتتجاوز الترقيات هذا الخطاف.
|
||||
* **نموذج التنفيذ — غير متزامن افتراضيًا، والتزامني اختياري**: يتحكّم العلم `shouldRunSynchronously` في كيفية تنفيذ ما بعد التثبيت.
|
||||
* `shouldRunSynchronously: false` *(الإعداد الافتراضي)* — يتم **إدراج الخطاف في قائمة الرسائل** مع `retryLimit: 3` ويعمل بشكل غير متزامن داخل عامل عمل. يعود ردّ التثبيت بمجرد وضع المهمة في الطابور، لذا فإن معالجًا بطيئًا أو متعطلًا لا يحجب المستدعي. سيُجرِّب العامل إعادة المحاولة حتى ثلاث مرات. **استخدم هذا للمهام طويلة التشغيل** — بَذر مجموعات بيانات كبيرة، استدعاء واجهات برمجة تطبيقات خارجية بطيئة، تهيئة موارد خارجية، أو أي شيء قد يتجاوز نافذة استجابة HTTP المعقولة.
|
||||
* `shouldRunSynchronously: true` — يُنفّذ الخطاف **ضمن تدفّق التثبيت مباشرةً** (نفس المنفِّذ كما قبل التثبيت). يَحجُب طلب التثبيت حتى ينتهي المعالج، وإذا رمى استثناءً، سيتلقى مستدعي التثبيت `POST_INSTALL_ERROR`. لا توجد محاولات إعادة تلقائية. **استخدم هذا للمهام السريعة التي يجب إكمالها قبل الاستجابة** — مثل إظهار خطأ تحقق للمستخدم، أو إعداد سريع سيعتمد عليه العميل مباشرةً بعد عودة نداء التثبيت. ضع في اعتبارك أن ترحيل البيانات الوصفية يكون قد طُبِّق بالفعل عند تشغيل ما بعد التثبيت، لذلك فإن فشل الوضع المتزامن **لا** يعيد التغييرات على المخطط إلى الوراء — بل يكتفي بإبراز الخطأ.
|
||||
* تأكّد من أن معالجك قابل للتنفيذ المتكرر دون آثار جانبية. في الوضع غير المتزامن قد تُعيد قائمة الانتظار المحاولة حتى ثلاث مرات؛ وفي أي من الوضعين قد يعمل الخطاف مجددًا أثناء الترقيات عند ضبط `shouldRunOnVersionUpgrade: true`.
|
||||
* متغيرات البيئة `APPLICATION_ID` و`APP_ACCESS_TOKEN` و`API_URL` متاحة داخل المعالج (كما في أي دالة منطق أخرى)، لذا يمكنك استدعاء واجهة Twenty API باستخدام رمز وصول للتطبيق مقيّد بنطاق تطبيقك.
|
||||
* يُسمح بدالة ما بعد التثبيت واحدة فقط لكل تطبيق. سيُنتج إنشاء ملف البيان خطأً إذا تم اكتشاف أكثر من واحدة.
|
||||
* تُرفَق خصائص الدالة `universalIdentifier` و`shouldRunOnVersionUpgrade` و`shouldRunSynchronously` تلقائيًا ببيان التطبيق ضمن الحقل `postInstallLogicFunction` أثناء عملية البناء — ولا تحتاج إلى الإشارة إليها في `defineApplication()`.
|
||||
* تم تعيين مهلة افتراضية إلى 300 ثانية (5 دقائق) للسماح بمهام الإعداد الأطول مثل تهيئة البيانات.
|
||||
* **لا يُنفَّذ في وضع التطوير**: عند تسجيل تطبيق محليًا (عبر `yarn twenty dev`)، يتجاوز الخادم تدفّق التثبيت بالكامل ويُزامن الملفات مباشرةً عبر مراقِب CLI — لذا لن يعمل ما بعد التثبيت في وضع التطوير مطلقًا، بغضّ النظر عن `shouldRunSynchronously`. استخدم `yarn twenty exec --postInstall` لتشغيله يدويًا على مساحة عمل قيد التشغيل.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePreInstallLogicFunction" description="تعريف دالة منطقية لما قبل التثبيت (واحدة لكل تطبيق)">
|
||||
|
||||
دالة ما قبل التثبيت هي دالة منطقية تعمل تلقائيًا أثناء التثبيت، **قبل تطبيق ترحيل البيانات الوصفية لمساحة العمل**. تتشارك نفس بنية الحمولة مع ما بعد التثبيت (`InstallPayload`)، لكنها موضوعة أبكر في تدفّق التثبيت كي تجهّز حالة يعتمد عليها الترحيل القادم — ومن الاستخدامات الشائعة: نسخ البيانات احتياطيًا، التحقق من التوافق مع المخطط الجديد، أو أرشفة السجلات التي ستُعاد هيكلتها أو ستُحذف.
|
||||
|
||||
```ts src/logic-functions/pre-install.ts
|
||||
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
|
||||
const handler = async (payload: InstallPayload): Promise<void> => {
|
||||
console.log('Pre install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
|
||||
name: 'pre-install',
|
||||
description: 'Runs before installation to prepare the application.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: true,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
يمكنك أيضًا تنفيذ دالة ما قبل التثبيت يدويًا في أي وقت باستخدام CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty exec --preInstall
|
||||
```
|
||||
|
||||
النقاط الرئيسية:
|
||||
* تستخدم دوال ما قبل التثبيت `definePreInstallLogicFunction()` — نفس الإعدادات المتخصصة كما في ما بعد التثبيت، لكنها مرتبطة بموضع مختلف ضمن دورة الحياة.
|
||||
* يتلقّى كلٌّ من معالجي ما قبل التثبيت وما بعد التثبيت النوع نفسه `InstallPayload`: `{ previousVersion?: string; newVersion: string }`. استورده مرة واحدة وأعد استخدامه لكلا الخطافين.
|
||||
* **موعد تشغيل الخطاف**: موضوع مباشرةً قبل ترحيل البيانات الوصفية لمساحة العمل (`synchronizeFromManifest`). قبل التنفيذ، يُشغِّل الخادم مزامنة "pared-down sync" ذات طابع إضافي فقط تقوم بتسجيل دالة ما قبل التثبيت للإصدار **الجديد** في البيانات الوصفية لمساحة العمل — دون لمس أي شيء آخر — ثم يُنفّذها. لأن هذه المزامنة «إضافية فقط»، تبقى كائنات وحقول وبيانات الإصدار السابق سليمة عند تشغيل معالجك: يمكنك قراءة حالة ما قبل الترحيل ونسخها احتياطيًا بأمان.
|
||||
* **نموذج التنفيذ**: يُنفَّذ ما قبل التثبيت **بشكل متزامن** و**يحجب عملية التثبيت**. إذا رمى المعالج استثناءً، تُلغى عملية التثبيت قبل تطبيق أي تغييرات على المخطط — وتبقى مساحة العمل على الإصدار السابق بحالة متّسقة. هذا مقصود: ما قبل التثبيت هو فرصتك الأخيرة لرفض ترقية تنطوي على مخاطر.
|
||||
* كما هو الحال مع ما بعد التثبيت، يُسمح بدالة ما قبل التثبيت واحدة فقط لكل تطبيق. تُربَط تلقائيًا ببيان التطبيق تحت `preInstallLogicFunction` أثناء عملية البناء.
|
||||
* **لا يُنفَّذ في وضع التطوير**: كما في ما بعد التثبيت — يتم تجاوز تدفّق التثبيت بالكامل للتطبيقات المسجّلة محليًا، لذا لن يعمل ما قبل التثبيت مطلقًا عند `yarn twenty dev`. استخدم `yarn twenty exec --preInstall` لتشغيله يدويًا.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="ما قبل التثبيت مقابل ما بعد التثبيت: متى تستخدم أيّهما" description="اختيار خطاف التثبيت المناسب">
|
||||
|
||||
كلا الخطافين جزء من تدفّق التثبيت نفسه ويتلقّيان نفس `InstallPayload`. الاختلاف يكمن في **موعد** تشغيلهما نسبةً إلى ترحيل البيانات الوصفية لمساحة العمل، وهذا يغيّر البيانات التي يمكنهما التعامل معها بأمان.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ install flow │
|
||||
│ │
|
||||
│ upload package → [pre-install] → metadata migration → │
|
||||
│ generate SDK → [post-install] │
|
||||
│ │
|
||||
│ old schema visible new schema visible │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
ما قبل التثبيت دائمًا **متزامن** (يحجب التثبيت ويمكنه إحباطه). ما بعد التثبيت **غير متزامن افتراضيًا** — يُدرج على عامل مع محاولات إعادة تلقائية — لكن يمكن التبديل إلى تنفيذ متزامن عبر `shouldRunSynchronously: true`. راجع الأكورديون `definePostInstallLogicFunction` أعلاه لمعرفة متى تستخدم كل وضع.
|
||||
|
||||
**استخدم `post-install` لأي شيء يتطلّب وجود المخطط الجديد.** وهذا هو السيناريو الشائع:
|
||||
|
||||
* بَذر بيانات افتراضية (إنشاء سجلات أولية وعروض افتراضية ومحتوى تجريبي) للكائنات والحقول المضافة حديثًا.
|
||||
* تسجيل خطافات الويب مع خدمات أطراف ثالثة بعد أن حصل التطبيق على بيانات الاعتماد الخاصة به.
|
||||
* استدعاء واجهة برمجة التطبيقات الخاصة بك لإكمال إعداد يعتمد على البيانات الوصفية المتزامنة.
|
||||
* منطق idempotent لتحقيق "تأكّد من وجود هذا" والذي ينبغي مواءمة الحالة في كل ترقية — بالاقتران مع `shouldRunOnVersionUpgrade: true`.
|
||||
|
||||
مثال — بَذر سجل `PostCard` افتراضي بعد التثبيت:
|
||||
|
||||
```ts src/logic-functions/post-install.ts
|
||||
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
import { createClient } from './generated/client';
|
||||
|
||||
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
|
||||
if (previousVersion) return; // fresh installs only
|
||||
|
||||
const client = createClient();
|
||||
await client.postCard.create({
|
||||
data: { title: 'Welcome to Postcard', content: 'Your first card!' },
|
||||
});
|
||||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
|
||||
name: 'post-install',
|
||||
description: 'Seeds a welcome post card after install.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: false,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**استخدم `pre-install` عندما قد يُتلف الترحيل أو يدمّر البيانات الحالية.** لأن ما قبل التثبيت يعمل مقابل المخطط *السابق* وفشله يُرجِع الترقية إلى الوراء، فهو المكان المناسب لأي شيء محفوف بالمخاطر:
|
||||
|
||||
* **نسخ البيانات احتياطيًا قبل حذفها أو إعادة هيكلتها** — مثل إزالة حقل في v2 وتحتاج إلى نسخ قيمه إلى حقل آخر أو تصديرها إلى التخزين قبل تشغيل الترحيل.
|
||||
* **أرشفة السجلات التي سيبطلها قيد جديد** — مثل أن يصبح حقل ما `NOT NULL` وتحتاج أولًا إلى حذف الصفوف ذات القيم الفارغة أو إصلاحها.
|
||||
* **التحقق من التوافق ورفض الترقية إذا تعذّر ترحيل البيانات الحالية بسلاسة** — ارمِ من داخل المعالج وسيُلغى التثبيت دون تطبيق أي تغييرات. هذا أكثر أمانًا من اكتشاف عدم التوافق في منتصف الترحيل.
|
||||
* **إعادة تسمية البيانات أو إعادة تعيين مفاتيحها** قبل تغيير في المخطط قد يؤدي إلى فقدان الارتباط.
|
||||
|
||||
مثال — أرشف السجلات قبل ترحيل هدّام:
|
||||
|
||||
```ts src/logic-functions/pre-install.ts
|
||||
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
import { createClient } from './generated/client';
|
||||
|
||||
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
|
||||
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
|
||||
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const client = createClient();
|
||||
const legacyRecords = await client.postCard.findMany({
|
||||
where: { notes: { isNotNull: true } },
|
||||
});
|
||||
|
||||
if (legacyRecords.length === 0) return;
|
||||
|
||||
// Copy legacy `notes` into the new `description` field before the migration
|
||||
// drops the `notes` column. If this fails, the upgrade is aborted and the
|
||||
// workspace stays on v1 with all data intact.
|
||||
await Promise.all(
|
||||
legacyRecords.map((record) =>
|
||||
client.postCard.update({
|
||||
where: { id: record.id },
|
||||
data: { description: record.notes },
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
|
||||
name: 'pre-install',
|
||||
description: 'Backs up legacy notes into description before the v2 migration.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: true,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**قاعدة عامة:**
|
||||
|
||||
| You want to... | استخدام |
|
||||
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
|
||||
| بذر بيانات افتراضية، تهيئة مساحة العمل، تسجيل موارد خارجية | `post-install` |
|
||||
| تشغيل بذر طويل الأمد أو استدعاءات أطراف ثالثة لا ينبغي أن تحجب استجابة التثبيت | `post-install` (الإعداد الافتراضي — `shouldRunSynchronously: false`، مع محاولات إعادة من العامل) |
|
||||
| تشغيل إعداد سريع سيعتمد عليه المستدعي مباشرةً بعد عودة نداء التثبيت | `post-install` مع `shouldRunSynchronously: true` |
|
||||
| قراءة البيانات أو نسخها احتياطيًا والتي قد يفقدها الترحيل القادم | `pre-install` |
|
||||
| رفض ترقية قد تُفسد البيانات الحالية | `pre-install` (ارمِ من المعالج) |
|
||||
| تنفيذ مواءمة في كل ترقية | `post-install` مع `shouldRunOnVersionUpgrade: true` |
|
||||
| تنفيذ إعداد لمرة واحدة في التثبيت الأول فقط | `post-install` مع `shouldRunOnVersionUpgrade: false` (الإعداد الافتراضي) |
|
||||
|
||||
<Note>
|
||||
إذا ساورك الشك، فاجعل الافتراضي هو **post-install**. الجأ إلى ما قبل التثبيت فقط عندما يكون الترحيل نفسه هدّامًا وتحتاج إلى التقاط الحالة السابقة قبل أن تزول.
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## عملاء واجهة برمجة تطبيقات مضبوطة الأنواع (`twenty-client-sdk`)
|
||||
|
||||
توفر حزمة `twenty-client-sdk` عميلين لـ GraphQL ذوي أنواع ثابتة للتفاعل مع واجهة Twenty البرمجية من وظائفك المنطقية ومكوّنات الواجهة الأمامية.
|
||||
|
||||
| العميل | استيراد | نقطة النهاية | مُولَّد؟ |
|
||||
| ------------------- | ---------------------------- | --------------------------------------------------- | -------------------------- |
|
||||
| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — بيانات مساحة العمل (السجلات، الكائنات) | نعم، في وقت التطوير/البناء |
|
||||
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — تكوين مساحة العمل، رفع الملفات | لا، يأتي مُجهزًا مسبقًا |
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="CoreApiClient" description="استعلام وتعديل بيانات مساحة العمل (السجلات، الكائنات)">
|
||||
|
||||
`CoreApiClient` هو العميل الرئيسي للاستعلام وتعديل بيانات مساحة العمل. يُولَّد من مخطط مساحة العمل لديك أثناء `yarn twenty dev` أو `yarn twenty build`، لذا فهو مضبوط الأنواع بالكامل ليتوافق مع كائناتك وحقولك.
|
||||
|
||||
```ts
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
const client = new CoreApiClient();
|
||||
|
||||
// Query records
|
||||
const { companies } = await client.query({
|
||||
companies: {
|
||||
edges: {
|
||||
node: {
|
||||
id: true,
|
||||
name: true,
|
||||
domainName: {
|
||||
primaryLinkLabel: true,
|
||||
primaryLinkUrl: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Create a record
|
||||
const { createCompany } = await client.mutation({
|
||||
createCompany: {
|
||||
__args: {
|
||||
data: {
|
||||
name: 'Acme Corp',
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
يستخدم العميل صياغة مجموعة اختيار: مرِّر `true` لتضمين حقل، واستخدم `__args` للوسيطات، وعشّش الكائنات للعلاقات. ستحصل على إكمال تلقائي كامل وفحص للأنواع يعتمد على مخطط مساحة العمل لديك.
|
||||
|
||||
<Note>
|
||||
**يتم توليد CoreApiClient في وقت التطوير/البناء.** إذا استخدمته دون تشغيل `yarn twenty dev` أو `yarn twenty build` أولًا، فسيؤدي ذلك إلى خطأ. تحدث عملية التوليد تلقائيًا — إذ يستطلع CLI مخطط GraphQL لمساحة عملك وينشئ عميلًا مضبوط الأنواع باستخدام `@genql/cli`.
|
||||
</Note>
|
||||
|
||||
#### استخدام CoreSchema للتعليقات التوضيحية للأنواع
|
||||
|
||||
`CoreSchema` يوفّر أنواع TypeScript المطابقة لكائنات مساحة العمل لديك — مفيد لتعيين أنواع حالة المكوّن أو معاملات الدوال:
|
||||
|
||||
```ts
|
||||
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
|
||||
import { useState } from 'react';
|
||||
|
||||
const [company, setCompany] = useState<
|
||||
Pick<CoreSchema.Company, 'id' | 'name'> | undefined
|
||||
>(undefined);
|
||||
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.query({
|
||||
company: {
|
||||
__args: { filter: { position: { eq: 1 } } },
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
setCompany(result.company);
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="MetadataApiClient" description="إعدادات مساحة العمل، والتطبيقات، ورفع الملفات">
|
||||
|
||||
يأتي `MetadataApiClient` مُجهّزًا مسبقًا مع SDK (لا حاجة للتوليد). يستعلم عن نقطة النهاية `/metadata` للحصول على تكوين مساحة العمل والتطبيقات ورفع الملفات.
|
||||
|
||||
```ts
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
|
||||
const metadataClient = new MetadataApiClient();
|
||||
|
||||
// List first 10 objects in the workspace
|
||||
const { objects } = await metadataClient.query({
|
||||
objects: {
|
||||
edges: {
|
||||
node: {
|
||||
id: true,
|
||||
nameSingular: true,
|
||||
namePlural: true,
|
||||
labelSingular: true,
|
||||
isCustom: true,
|
||||
},
|
||||
},
|
||||
__args: {
|
||||
filter: {},
|
||||
paging: { first: 10 },
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### رفع الملفات
|
||||
|
||||
يتضمن `MetadataApiClient` طريقة `uploadFile` لإرفاق الملفات بالحقول من نوع الملف:
|
||||
|
||||
```ts
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
import * as fs from 'fs';
|
||||
|
||||
const metadataClient = new MetadataApiClient();
|
||||
|
||||
const fileBuffer = fs.readFileSync('./invoice.pdf');
|
||||
|
||||
const uploadedFile = await metadataClient.uploadFile(
|
||||
fileBuffer, // file contents as a Buffer
|
||||
'invoice.pdf', // filename
|
||||
'application/pdf', // MIME type
|
||||
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier
|
||||
);
|
||||
|
||||
console.log(uploadedFile);
|
||||
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
|
||||
```
|
||||
|
||||
| المعلمة | النوع | الوصف |
|
||||
| ---------------------------------- | -------- | ---------------------------------------------------------------------- |
|
||||
| `fileBuffer` | `Buffer` | المحتوى الخام للملف |
|
||||
| `filename` | `string` | اسم الملف (يُستخدم للتخزين والعرض) |
|
||||
| `contentType` | `string` | نوع MIME (القيمة الافتراضية `application/octet-stream` إذا لم يُحدَّد) |
|
||||
| `fieldMetadataUniversalIdentifier` | `string` | قيمة `universalIdentifier` لحقل نوع الملف في كائنك |
|
||||
|
||||
النقاط الرئيسية:
|
||||
* يستخدم `universalIdentifier` الخاص بالحقل (وليس معرّفه الخاص بمساحة العمل)، بحيث يعمل كود الرفع لديك عبر أي مساحة عمل مُثبَّت فيها تطبيقك.
|
||||
* العنوان `url` المُعاد هو عنوان URL موقّع يمكنك استخدامه للوصول إلى الملف المرفوع.
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
<Note>
|
||||
عند تشغيل كودك على Twenty (وظائف منطقية أو مكوّنات أمامية)، يقوم النظام الأساسي بحقن بيانات الاعتماد كمتغيرات بيئية:
|
||||
|
||||
* `TWENTY_API_URL` — عنوان URL الأساسي لواجهة Twenty البرمجية
|
||||
* `TWENTY_APP_ACCESS_TOKEN` — مفتاح قصير العمر ذو نطاق يقتصر على الدور الافتراضي لوظيفة تطبيقك
|
||||
|
||||
لست **بحاجة** إلى تمرير هذه القيم إلى العملاء — فهي تُقرأ تلقائيًا من `process.env`. تُحدَّد أذونات مفتاح واجهة برمجة التطبيقات بواسطة الدور المشار إليه في `defaultRoleUniversalIdentifier` ضمن `application-config.ts`.
|
||||
</Note>
|
||||
|
|
@ -1,12 +1,9 @@
|
|||
---
|
||||
title: النشر
|
||||
icon: رفع
|
||||
description: وزّع تطبيق Twenty الخاص بك على سوق Twenty أو انشره داخليًا.
|
||||
---
|
||||
|
||||
<Warning>
|
||||
التطبيقات حاليًا في مرحلة الألفا. الميزة تعمل لكنها لا تزال قيد التطور.
|
||||
</Warning>
|
||||
|
||||
## نظرة عامة
|
||||
|
||||
بمجرد أن يكون تطبيقك [مبنيًا ومختبرًا محليًا](/l/ar/developers/extend/apps/building)، لديك مساران لتوزيعه:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
---
|
||||
title: المهارات والوكلاء
|
||||
description: Define AI skills and agents for your app.
|
||||
icon: robot
|
||||
---
|
||||
|
||||
<Warning>
|
||||
Skills and agents are currently in alpha. الميزة تعمل لكنها لا تزال قيد التطور.
|
||||
</Warning>
|
||||
|
||||
Apps can define AI capabilities that live inside the workspace — reusable skill instructions and agents with custom system prompts.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="defineSkill" description="عرّف مهارات وكلاء الذكاء الاصطناعي">
|
||||
|
||||
تُحدِّد المهارات تعليمات وإمكانات قابلة لإعادة الاستخدام يمكن لوكلاء الذكاء الاصطناعي استخدامها داخل مساحة العمل لديك. استخدم `defineSkill()` لتعريف مهارات مع تحقّق مدمج:
|
||||
|
||||
```ts src/skills/example-skill.ts
|
||||
import { defineSkill } from 'twenty-sdk/define';
|
||||
|
||||
export default defineSkill({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'sales-outreach',
|
||||
label: 'Sales Outreach',
|
||||
description: 'Guides the AI agent through a structured sales outreach process',
|
||||
icon: 'IconBrain',
|
||||
content: `You are a sales outreach assistant. When reaching out to a prospect:
|
||||
1. Research the company and recent news
|
||||
2. Identify the prospect's role and likely pain points
|
||||
3. Draft a personalized message referencing specific details
|
||||
4. Keep the tone professional but conversational`,
|
||||
});
|
||||
```
|
||||
|
||||
النقاط الرئيسية:
|
||||
* `name` هي سلسلة معرّف فريدة للمهارة (يُنصَح باستخدام kebab-case).
|
||||
* `label` هو اسم العرض المقروء للبشر الظاهر في واجهة المستخدم.
|
||||
* `content` يحتوي على تعليمات المهارة — وهو النص الذي يستخدمه وكيل الذكاء الاصطناعي.
|
||||
* `icon` (اختياري) يحدّد الأيقونة المعروضة في واجهة المستخدم.
|
||||
* `description` (اختياري) يوفّر سياقًا إضافيًا حول غرض المهارة.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineAgent" description="عرِّف وكلاء الذكاء الاصطناعي باستخدام موجهات مخصّصة">
|
||||
|
||||
الوكلاء هم مساعدون ذكاء اصطناعي يعيشون داخل مساحة العمل لديك. استخدم `defineAgent()` لإنشاء وكلاء بموجه نظام مخصّص:
|
||||
|
||||
```ts src/agents/example-agent.ts
|
||||
import { defineAgent } from 'twenty-sdk/define';
|
||||
|
||||
export default defineAgent({
|
||||
universalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
|
||||
name: 'sales-assistant',
|
||||
label: 'Sales Assistant',
|
||||
description: 'Helps the sales team draft outreach emails and research prospects',
|
||||
icon: 'IconRobot',
|
||||
prompt: 'You are a helpful sales assistant. Help users with their questions and tasks.',
|
||||
});
|
||||
```
|
||||
|
||||
النقاط الرئيسية:
|
||||
* `name` هي سلسلة معرّف فريدة للوكيل (يُنصح باستخدام kebab-case).
|
||||
* `label` هو اسم العرض الظاهر في واجهة المستخدم.
|
||||
* `prompt` هو موجه النظام الذي يحدّد سلوك الوكيل.
|
||||
* `description` (اختياري) يوفّر سياقًا حول ما يفعله الوكيل.
|
||||
* `icon` (اختياري) يحدّد الأيقونة المعروضة في واجهة المستخدم.
|
||||
* `modelId` (اختياري) يتجاوز نموذج الذكاء الاصطناعي الافتراضي الذي يستخدمه الوكيل.
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
189
packages/twenty-docs/l/ar/developers/extend/oauth.mdx
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
---
|
||||
title: OAuth
|
||||
icon: المفتاح
|
||||
description: تدفق رمز التفويض مع PKCE وبيانات اعتماد العميل للوصول من خادم إلى خادم.
|
||||
---
|
||||
|
||||
تُطبِّق Twenty بروتوكول OAuth 2.0 باستخدام رمز التفويض + PKCE للتطبيقات المواجهة للمستخدم، وبيانات اعتماد العميل للوصول من خادم إلى خادم. يُجرى تسجيل العملاء ديناميكيًا عبر [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591) — دون إعداد يدوي في لوحة التحكم.
|
||||
|
||||
## متى تستخدم OAuth
|
||||
|
||||
| السيناريو | طريقة المصادقة |
|
||||
| -------------------------------------------- | ------------------------------------------------------------------------------------ |
|
||||
| البرامج النصية الداخلية، والأتمتة | [مفتاح API](/l/ar/developers/extend/api#authentication) |
|
||||
| تطبيق خارجي يعمل نيابةً عن مستخدم | **OAuth — رمز التفويض** |
|
||||
| من خادم إلى خادم، دون سياق مستخدم | **OAuth — بيانات اعتماد العميل** |
|
||||
| تطبيق Twenty مع امتدادات واجهة المستخدم (UI) | [التطبيقات](/l/ar/developers/extend/apps/getting-started) (يتم التعامل مع OAuth تلقائيًا) |
|
||||
|
||||
## تسجيل عميل
|
||||
|
||||
تدعم Twenty **التسجيل الديناميكي للعملاء** وفقًا لـ[RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591). لا حاجة إلى إعداد يدوي — سجّل برمجيًا:
|
||||
|
||||
```bash
|
||||
POST /oauth/register
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"client_name": "My Integration",
|
||||
"redirect_uris": ["https://myapp.com/callback"],
|
||||
"grant_types": ["authorization_code"],
|
||||
"token_endpoint_auth_method": "client_secret_post"
|
||||
}
|
||||
```
|
||||
|
||||
**الاستجابة:**
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": "abc123",
|
||||
"client_secret": "secret456",
|
||||
"client_name": "My Integration",
|
||||
"redirect_uris": ["https://myapp.com/callback"]
|
||||
}
|
||||
```
|
||||
|
||||
<Warning>
|
||||
احفظ `client_secret` بأمان — لا يمكن استرجاعه لاحقًا.
|
||||
</Warning>
|
||||
|
||||
## النطاقات
|
||||
|
||||
| النطاق | الوصول |
|
||||
| --------- | -------------------------------------------------------------- |
|
||||
| `api` | إمكانية قراءة/كتابة كاملة لواجهات برمجة تطبيقات Core وMetadata |
|
||||
| `profile` | قراءة معلومات ملف تعريف المستخدم المُصادَق عليه |
|
||||
|
||||
اطلب النطاقات كسلسلة مفصولة بمسافات: `scope=api profile`
|
||||
|
||||
## تدفق رمز التفويض
|
||||
|
||||
استخدم هذا التدفق عندما يعمل تطبيقك نيابةً عن مستخدم Twenty.
|
||||
|
||||
### 1. أعد توجيه المستخدم للتفويض
|
||||
|
||||
```
|
||||
GET /oauth/authorize?
|
||||
client_id=YOUR_CLIENT_ID&
|
||||
response_type=code&
|
||||
redirect_uri=https://myapp.com/callback&
|
||||
scope=api&
|
||||
state=random_state_value&
|
||||
code_challenge=CHALLENGE&
|
||||
code_challenge_method=S256
|
||||
```
|
||||
|
||||
| المعلمة | مطلوب | الوصف |
|
||||
| ----------------------- | -------- | -------------------------------------------------------- |
|
||||
| `client_id` | نعم | معرّف العميل المسجّل الخاص بك |
|
||||
| `response_type` | نعم | يجب أن يكون `code` |
|
||||
| `redirect_uri` | نعم | يجب أن يطابق عنوان URI لإعادة التوجيه المسجّل |
|
||||
| `scope` | لا | نطاقات مفصولة بمسافات (القيمة الافتراضية هي `api`) |
|
||||
| `state` | مُوصى به | سلسلة عشوائية لمنع هجمات CSRF |
|
||||
| `code_challenge` | مُوصى به | تحدّي PKCE (تجزئة SHA-256 لـ verifier، بترميز base64url) |
|
||||
| `code_challenge_method` | مُوصى به | يجب أن تكون `S256` عند استخدام PKCE |
|
||||
|
||||
يرى المستخدم شاشة موافقة ويوافق على الوصول أو يرفضه.
|
||||
|
||||
### ٢. معالجة الاستدعاء المرتجع
|
||||
|
||||
بعد التفويض، تعيد Twenty التوجيه إلى `redirect_uri` الخاص بك:
|
||||
|
||||
```
|
||||
https://myapp.com/callback?code=AUTH_CODE&state=random_state_value
|
||||
```
|
||||
|
||||
تحقّق من أن قيمة `state` تطابق ما أرسلته.
|
||||
|
||||
### ٣. استبدِل الرمز بالرموز
|
||||
|
||||
```bash
|
||||
POST /oauth/token
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
grant_type=authorization_code&
|
||||
code=AUTH_CODE&
|
||||
redirect_uri=https://myapp.com/callback&
|
||||
client_id=YOUR_CLIENT_ID&
|
||||
client_secret=YOUR_CLIENT_SECRET&
|
||||
code_verifier=YOUR_PKCE_VERIFIER
|
||||
```
|
||||
|
||||
**الاستجابة:**
|
||||
|
||||
```json
|
||||
{
|
||||
"access_token": "eyJhbG...",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
"refresh_token": "dGhpcyBpcyBh..."
|
||||
}
|
||||
```
|
||||
|
||||
### 4. استخدم رمز الوصول
|
||||
|
||||
```bash
|
||||
GET /rest/companies
|
||||
Authorization: Bearer ACCESS_TOKEN
|
||||
```
|
||||
|
||||
### 5. حدِّث عند انتهاء الصلاحية
|
||||
|
||||
```bash
|
||||
POST /oauth/token
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
grant_type=refresh_token&
|
||||
refresh_token=YOUR_REFRESH_TOKEN&
|
||||
client_id=YOUR_CLIENT_ID&
|
||||
client_secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
## تدفق بيانات اعتماد العميل
|
||||
|
||||
لعمليات التكامل من خادم إلى خادم دون تفاعل مستخدم:
|
||||
|
||||
```bash
|
||||
POST /oauth/token
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
grant_type=client_credentials&
|
||||
client_id=YOUR_CLIENT_ID&
|
||||
client_secret=YOUR_CLIENT_SECRET&
|
||||
scope=api
|
||||
```
|
||||
|
||||
الرمز المُعاد يمتلك وصولًا على مستوى مساحة العمل، وغير مرتبط بأي مستخدم محدّد.
|
||||
|
||||
## اكتشاف الخادم
|
||||
|
||||
تنشر Twenty إعدادات OAuth الخاصة بها عند نقطة اكتشاف قياسية:
|
||||
|
||||
```
|
||||
GET /.well-known/oauth-authorization-server
|
||||
```
|
||||
|
||||
يعيد هذا جميع نقاط النهاية وأنواع المنح المدعومة والنطاقات والقدرات — وهو مفيد لبناء عملاء OAuth عامّين.
|
||||
|
||||
## ملخص نقاط نهاية API
|
||||
|
||||
| نقطة النهاية | الغرض |
|
||||
| ----------------------------------------- | -------------------------- |
|
||||
| `/.well-known/oauth-authorization-server` | اكتشاف بيانات تعريف الخادم |
|
||||
| `/oauth/register` | التسجيل الديناميكي للعميل |
|
||||
| `/oauth/authorize` | تفويض المستخدم |
|
||||
| `/oauth/token` | مبادلة الرموز وتحديثها |
|
||||
|
||||
| البيئة | عنوان URL الأساسي |
|
||||
| --------------------- | ------------------------ |
|
||||
| **السحابة** | `https://api.twenty.com` |
|
||||
| **الاستضافة الذاتية** | `https://{your-domain}` |
|
||||
|
||||
## OAuth مقابل مفاتيح API
|
||||
|
||||
| | مفاتيح واجهة برمجة التطبيقات | OAuth |
|
||||
| ------------------------ | -------------------------------- | ------------------------------------------ |
|
||||
| **الإعداد** | إنشاء من الإعدادات | تسجيل عميل، وتنفيذ التدفق |
|
||||
| **سياق المستخدم** | لا يوجد (على مستوى مساحة العمل) | أذونات مستخدم محدّد |
|
||||
| **الأفضل لـ** | البرامج النصية، الأدوات الداخلية | تطبيقات خارجية، وتكاملات متعددة المستخدمين |
|
||||
| **تدوير الرموز** | يدوي | تلقائي عبر رموز التحديث |
|
||||
| **وصول محدود بالنطاقات** | وصول كامل إلى API | تفصيلي عبر النطاقات |
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
---
|
||||
title: خطافات الويب
|
||||
description: استقبل إشعارات في الوقت الفعلي عند وقوع أحداث في نظام إدارة علاقات العملاء (CRM) الخاص بك.
|
||||
icon: satellite-dish
|
||||
description: احصل على إشعار عند تغيّر السجلات — سيتم إرسال طلب HTTP POST إلى endpoint الخاص بك عند كل عملية إنشاء أو تحديث أو حذف.
|
||||
---
|
||||
|
||||
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
|
||||
|
||||
تدفع خطافات الويب البيانات إلى أنظمتك في الوقت الفعلي عند وقوع أحداث في Twenty — دون الحاجة إلى الاستطلاع الدوري. استخدمها للحفاظ على تزامن الأنظمة الخارجية، وتشغيل الأتمتة، أو إرسال التنبيهات.
|
||||
يقوم Twenty بإرسال طلب HTTP POST إلى URL الخاص بك كلما تم إنشاء سجل أو تحديثه أو حذفه. جميع أنواع الكائنات مشمولة، بما في ذلك الكائنات المخصصة.
|
||||
|
||||
## إنشاء خطاف ويب
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,28 @@
|
|||
---
|
||||
title: البدء
|
||||
description: مرحبًا بك في وثائق المطوّرين الخاصة بـ Twenty، مرجعك للتوسيع والاستضافة الذاتية والمساهمة في Twenty.
|
||||
title: المطورون
|
||||
description: Build apps, use the API, self-host, or contribute to the codebase.
|
||||
---
|
||||
|
||||
import { CardTitle } from "/snippets/card-title.mdx"
|
||||
|
||||
<CardGroup cols={٣}>
|
||||
<Card href="/l/ar/developers/extend/extend" img="/images/user-guide/integrations/plug.png">
|
||||
<CardTitle>التوسيع</CardTitle>
|
||||
أنشئ عمليات تكامل مع واجهات برمجة التطبيقات وخطافات الويب والتطبيقات المخصصة.
|
||||
<Card href="/l/ar/developers/extend/apps/getting-started" img="/images/user-guide/halftone/dev-apps.png">
|
||||
<CardTitle>Apps</CardTitle>
|
||||
Extend Twenty with custom objects, server-side logic, UI components, and AI agents — all as TypeScript packages.
|
||||
</Card>
|
||||
|
||||
<Card href="/l/ar/developers/self-host/self-host" img="/images/user-guide/what-is-twenty/20.png">
|
||||
<CardTitle>الاستضافة الذاتية</CardTitle>
|
||||
قم بنشر Twenty وإدارته على البنية التحتية الخاصة بك.
|
||||
<Card href="/l/ar/developers/extend/api" img="/images/user-guide/halftone/dev-api.png">
|
||||
<CardTitle>API</CardTitle>
|
||||
REST and GraphQL APIs, webhooks, and OAuth.
|
||||
</Card>
|
||||
|
||||
<Card href="/l/ar/developers/contribute/contribute" img="/images/user-guide/github/github-header.png">
|
||||
<CardTitle>المساهمة</CardTitle>
|
||||
انضم إلى مجتمعنا مفتوح المصدر وساهم في Twenty.
|
||||
<Card href="/l/ar/developers/self-host/capabilities/docker-compose" img="/images/user-guide/halftone/dev-self-host.png">
|
||||
<CardTitle>Self-Host</CardTitle>
|
||||
Run Twenty on your own infrastructure.
|
||||
</Card>
|
||||
|
||||
<Card href="/l/ar/developers/contribute/capabilities/local-setup" img="/images/user-guide/halftone/dev-contribute.png">
|
||||
<CardTitle>Contribute</CardTitle>
|
||||
Set up the monorepo locally and submit PRs.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
title: طرق أخرى
|
||||
icon: cloud
|
||||
---
|
||||
|
||||
<Warning>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
title: بنقرة واحدة مع Docker Compose
|
||||
title: Docker Compose
|
||||
icon: docker
|
||||
---
|
||||
|
||||
<Warning>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
title: إعداد
|
||||
icon: gear
|
||||
---
|
||||
|
||||
# إدارة الإعدادات
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
title: استكشاف الأخطاء وإصلاحها
|
||||
icon: wrench
|
||||
---
|
||||
|
||||
## استكشاف الأخطاء وإصلاحها
|
||||
|
|
|
|||
|
|
@ -1,381 +1,90 @@
|
|||
---
|
||||
title: دليل الترقية
|
||||
icon: arrow-up-right-dots
|
||||
---
|
||||
|
||||
## إرشادات عامة
|
||||
|
||||
**تأكد دائماً من عمل نسخة احتياطية لقاعدة بياناتك قبل بدء عملية الترقية** عن طريق تشغيل `docker exec -it {db_container_name_or_id} pg_dumpall -U {postgres_user} > databases_backup.sql`.
|
||||
|
||||
لاستعادة النسخة الاحتياطية، قم بتشغيل `cat databases_backup.sql | docker exec -i {db_container_name_or_id} psql -U {postgres_user}`.
|
||||
|
||||
إذا كنت تستخدم Docker Compose، اتبع الخطوات التالية:
|
||||
|
||||
1. في الطرفية، على الجهاز الذي يعمل فيه Twenty، قم بإيقاف Twenty: `docker compose down`
|
||||
|
||||
2. قم بترقية الإصدار عن طريق تغيير قيمة `TAG` في ملف .env بجانب docker-compose. ( نوصي باستخدام إصدار `major.minor` مثل `v0.53` )
|
||||
|
||||
3. قم بإعادة تشغيل Twenty باستخدام `docker compose up -d`
|
||||
|
||||
إذا كنت ترغب في ترقية مثيلك بزيادة بعض الإصدارات، مثل الانتقال من v0.33.0 إلى v0.35.0، يجب أن تقوم بترقية مثيلك بشكل تسلسلي، في هذا المثال من v0.33.0 إلى v0.34.0، ثم من v0.34.0 إلى v0.35.0.
|
||||
|
||||
**تأكد من أن لديك نسخة احتياطية غير تالفة بعد كل إصدار تمت ترقيته.**
|
||||
|
||||
## خطوات الترقية الخاصة بالإصدار
|
||||
|
||||
## v1.0
|
||||
|
||||
مرحباً Twenty v1.0! 🎉
|
||||
|
||||
## v0.60
|
||||
|
||||
### تحسين الأداء
|
||||
|
||||
تم تحسين جميع التفاعلات مع واجهة برمجة التطبيقات للبيانات الوصفية للحصول على أداء أفضل، خاصة فيما يتعلق بمعالجة بيانات الكائن وإنشاء المساحات.
|
||||
|
||||
أعدنا تصميم استراتيجيتنا للتخزين المؤقت لإعطاء الأولوية للوصول عبر التخزين المؤقت على استعلامات قاعدة البيانات قدر الإمكان، مما أدى إلى تحسين كبير في أداء عمليات واجهة برمجة التطبيقات للبيانات الوصفية.
|
||||
|
||||
إذا واجهت أي مشاكل في وقت التشغيل بعد الترقية، قد تحتاج إلى مسح التخزين المؤقت لضمان تزامنه مع أحدث التغييرات. قم بتشغيل هذا الأمر في حاوية خادم twenty الخاص بك:
|
||||
**Always back up your database before starting the upgrade process** by running:
|
||||
|
||||
```bash
|
||||
yarn command:prod cache:flush
|
||||
docker exec -it {db_container_name_or_id} pg_dumpall -U {postgres_user} > databases_backup.sql
|
||||
```
|
||||
|
||||
### v0.55
|
||||
|
||||
قم بترقية مثيل Twenty الخاص بك لاستخدام صورة v0.55
|
||||
|
||||
لم تعد بحاجة إلى تشغيل أي أمر، الصورة الجديدة ستعتني بتشغيل جميع الترحيلات المطلوبة تلقائيًا.
|
||||
|
||||
### خطأ: `User does not have permission`
|
||||
|
||||
إذا واجهت أخطاء في الأذونات في معظم الطلبات بعد الترقية، فقد تحتاج إلى مسح التخزين المؤقت لإعادة حساب أحدث الأذونات.
|
||||
|
||||
في حاوية خادم `twenty` الخاص بك، قم بتشغيل:
|
||||
To restore from backup:
|
||||
|
||||
```bash
|
||||
yarn command:prod cache:flush
|
||||
cat databases_backup.sql | docker exec -i {db_container_name_or_id} psql -U {postgres_user}
|
||||
```
|
||||
|
||||
هذه المشكلة خاصة بهذا الإصدار من Twenty ولا يجب أن تكون ضرورية في الترقيات المستقبلية.
|
||||
If you use Docker Compose, follow these steps:
|
||||
|
||||
### v0.54
|
||||
1. Stop Twenty: `docker compose down`
|
||||
2. Change the `TAG` value in the `.env` file next to your `docker-compose.yml`
|
||||
3. Start Twenty: `docker compose up -d`
|
||||
|
||||
منذ الإصدار `0.53`، لا حاجة لأي إجراءات يدوية.
|
||||
The server runs all required upgrade migrations automatically on startup. No manual command is needed.
|
||||
|
||||
#### إيقاف تشغيل مخطط البيانات الوصفية
|
||||
## Cross-version upgrades (v1.22+)
|
||||
|
||||
قمنا بدمج مخطط `metadata` مع مخطط `core` لتبسيط استرجاع البيانات من `TypeORM`.
|
||||
قمنا بدمج خطوة تنفيذ الأمر `migrate` مع الأمر `upgrade`. لا ننصح بتشغيل `migrate` يدويًا داخل أي من حاويات الخادم/العمل الخاصة بك.
|
||||
Starting from **v1.22**, Twenty supports cross-version upgrades. You can jump directly from any supported version to the latest release without stepping through each intermediate version.
|
||||
|
||||
### منذ v0.53
|
||||
For example, upgrading from v1.22 straight to v2.0 is fully supported.
|
||||
|
||||
بدءًا من الإصدار `0.53`، تتم الترقية بشكل برمجي داخل `DockerFile`، مما يعني أنه من الآن فصاعدًا، لن تحتاج إلى تشغيل أي أوامر يدويًا بعد الآن.
|
||||
## Checking upgrade status
|
||||
|
||||
تأكد من متابعة الترقية الخاصة بك تسلسليًا، دون تخطي أي إصدار رئيسي (على سبيل المثال `0.43.3` إلى `0.44.0` مسموح، ولكن `0.43.1` إلى `0.45.0` غير مسموح)، قد يؤدي بخلاف ذلك إلى عدم تزامن إصدار مساحة العمل مما قد يؤدي إلى خطأ في وقت التشغيل وفقدان الوظائف.
|
||||
The `upgrade:status` command lets you inspect the current state of your instance and workspace migrations. It is useful for debugging upgrade issues or when filing a support request.
|
||||
|
||||
للتحقق مما إذا كانت مساحة العمل قد تمت ترقيتها بشكل صحيح ، يمكنك مراجعة نسختها في قاعدة البيانات في جدول `core.workspace`.
|
||||
Run it from the server container:
|
||||
|
||||
يجب أن تكون دائمًا في نطاق إصدار `major.minor` لحساب Twenty الحالي الخاص بك ، ويمكنك مشاهدة نسخة حسابك في لوحة المدير (في `/settings/admin-panel`، يمكن الوصول إليها إذا كانت خاصية `canAccessFullAdminPanel` الخاصة بالمستخدم مصفوفة إلى true في قاعدة البيانات) أو عن طريق تشغيل `echo $APP_VERSION` في حاوية `twenty-server` الخاصة بك.
|
||||
|
||||
لإصلاح إصدار مساحة العمل غير المتزامن ، سيتعين عليك الترقية من الإصدار المعني لـ Twenty باتباع دليل الترقية الخاص ذو الصلة تسلسليًا وهكذا حتى يصل إلى الإصدار المطلوب.
|
||||
|
||||
#### إزالة `auditLog`
|
||||
|
||||
لقد قمنا بإزالة كائن المعيار auditLog، مما يعني أن حجم النسخة الاحتياطية الخاصة بك قد يقل بشكل كبير بعد هذه الترقية.
|
||||
|
||||
### من v0.51 إلى v0.52
|
||||
|
||||
قم بترقية مثيل Twenty الخاص بك لاستخدام صورة v0.52
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade
|
||||
```bash
|
||||
docker exec -it {server_container_name_or_id} yarn command:prod upgrade:status
|
||||
```
|
||||
|
||||
#### لدي مساحة عمل محظورة في الإصدار بين `0.52.0` و`0.52.6`
|
||||
مثال على المخرجات:
|
||||
|
||||
لسوء الحظ، تم إزالة `0.52.0` و`0.52.6` بالكامل من dockerHub.
|
||||
سيتعين عليك تحديث نسخة مساحة العمل يدويًا إلى `0.51.0` في قاعدة البيانات والترقية باستخدام إصدار twenty عند `0.52.11` باتباع دليل الترقية الخاص به أعلاه.
|
||||
```sh
|
||||
APP_VERSION: v1.23.0
|
||||
|
||||
### من v0.50 إلى v0.51
|
||||
Instance
|
||||
Inferred version: 1.23.0
|
||||
Latest command: 1.23.0_DropWorkspaceVersionColumnFastInstanceCommand_1785000000000
|
||||
Status: Up to date
|
||||
Executed by: v1.23.0
|
||||
At: 2026-04-16T11:43:58.823Z
|
||||
|
||||
قم بترقية مثيل Twenty الخاص بك لاستخدام صورة v0.51
|
||||
Workspace
|
||||
Apple (20202020-1c25-4d02-bf25-6aeccf7ea419)
|
||||
Inferred version: 1.23.0
|
||||
Latest command: 1.23.0_UpdateGlobalObjectContextCommandMenuItemsCommand_1780000005000
|
||||
Status: Up to date
|
||||
Executed by: v1.23.0
|
||||
At: 2026-04-16T11:44:09.361Z
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade
|
||||
Summary
|
||||
Instance: Up to date
|
||||
Workspaces: 1 up to date, 0 behind, 0 failed (1 total)
|
||||
```
|
||||
|
||||
### من v0.44.0 إلى v0.50.0
|
||||
### خيارات
|
||||
|
||||
قم بترقية مثيل Twenty الخاص بك لاستخدام صورة v0.50.0
|
||||
| Flag | الوصف |
|
||||
| ------------------------- | ---------------------------------------------------------------- |
|
||||
| `-w, --workspace-id <id>` | Filter to a specific workspace. Can be passed multiple times. |
|
||||
| `-f, --failed-only` | Hide up-to-date workspaces, only show behind and failed entries. |
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade
|
||||
## استكشاف الأخطاء وإصلاحها
|
||||
|
||||
If the upgrade fails on some workspaces, the server will not advance past the failing step. Restarting the server (`docker compose up -d`) will retry the upgrade from where it left off.
|
||||
|
||||
To quickly identify problems, run:
|
||||
|
||||
```bash
|
||||
docker exec -it {server_container_name_or_id} yarn command:prod upgrade:status --failed-only
|
||||
```
|
||||
|
||||
#### تغيير ملف docker-compose.yml
|
||||
This shows only workspaces that are behind or have failed, along with the error message for each failure.
|
||||
|
||||
يتضمن هذا الإصدار تغييرًا في `docker-compose.yml` لمنح خدمة `worker` إمكانية الوصول إلى وحدة التخزين `server-local-data`.
|
||||
يرجى تحديث `docker-compose.yml` المحلي الخاص بك بـ [docker-compose.yml v0.50.0](https://github.com/twentyhq/twenty/blob/v0.50.0/packages/twenty-docker/docker-compose.yml)
|
||||
## Before v1.22
|
||||
|
||||
### من v0.43.0 إلى v0.44.0
|
||||
|
||||
قم بترقية مثيل Twenty الخاص بك لاستخدام صورة v0.44.0
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade
|
||||
```
|
||||
|
||||
### من v0.42.0 إلى v0.43.0
|
||||
|
||||
قم بترقية مثيل Twenty الخاص بك لاستخدام صورة v0.43.0
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade
|
||||
```
|
||||
|
||||
في هذا الإصدار، قمنا أيضًا بالتحول إلى صورة postgres:16 في docker-compose.yml.
|
||||
|
||||
#### (الخيار 1) ترحيل قاعدة البيانات
|
||||
|
||||
احتفاظ بصورة postgres-spilo الحالية مقبول، ولكن سيتعين عليك تجميد الإصدار في docker-compose.yml ليكون 0.43.0.
|
||||
|
||||
#### (الخيار 2) ترحيل قاعدة البيانات
|
||||
|
||||
إذا كنت تريد ترحيل قاعدة بياناتك إلى الصورة الجديدة postgres:16، يرجى اتباع هذه الخطوات:
|
||||
|
||||
1. نسخ قاعدة البيانات الخاصة بك من حاوية postgres-spilo القديمة
|
||||
|
||||
```
|
||||
docker exec -it twenty-db-1 sh
|
||||
pg_dump -U {YOUR_POSTGRES_USER} -d {YOUR_POSTGRES_DB} > databases_backup.sql
|
||||
exit
|
||||
docker cp twenty-db-1:/home/postgres/databases_backup.sql .
|
||||
```
|
||||
|
||||
تأكد من أن ملف النسخ الاحتياطي ليس فارغًا.
|
||||
|
||||
2. قم بترقية docker-compose.yml الخاص بك لاستخدام صورة postgres:16 كما هو في الملف [docker-compose.yml](https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/docker-compose.yml).
|
||||
|
||||
3. استعادة قاعدة البيانات إلى الحاوية الجديدة postgres:16
|
||||
|
||||
```
|
||||
docker cp databases_backup.sql twenty-db-1:/databases_backup.sql
|
||||
docker exec -it twenty-db-1 sh
|
||||
psql -U {YOUR_POSTGRES_USER} -d {YOUR_POSTGRES_DB} -f databases_backup.sql
|
||||
exit
|
||||
```
|
||||
|
||||
### من v0.41.0 إلى v0.42.0
|
||||
|
||||
قم بترقية مثيل Twenty الخاص بك لاستخدام صورة v0.42.0
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.42
|
||||
```
|
||||
|
||||
**متغيرات البيئة**
|
||||
|
||||
* تمت الإزالة: `FRONT_PORT`, `FRONT_PROTOCOL`, `FRONT_DOMAIN`, `PORT`
|
||||
* تمت الإضافة: `FRONTEND_URL`, `NODE_PORT`, `MAX_NUMBER_OF_WORKSPACES_DELETED_PER_EXECUTION`, `MESSAGING_PROVIDER_MICROSOFT_ENABLED`, `CALENDAR_PROVIDER_MICROSOFT_ENABLED`, `IS_MICROSOFT_SYNC_ENABLED`
|
||||
|
||||
### من v0.40.0 إلى v0.41.0
|
||||
|
||||
قم بترقية مثيل Twenty الخاص بك لاستخدام صورة v0.41.0
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.41
|
||||
```
|
||||
|
||||
**متغيرات البيئة**
|
||||
|
||||
* تمت الإزالة: `AUTH_MICROSOFT_TENANT_ID`
|
||||
|
||||
### من v0.35.0 إلى v0.40.0
|
||||
|
||||
قم بترقية مثيل Twenty الخاص بك لاستخدام صورة v0.40.0
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.40
|
||||
```
|
||||
|
||||
**متغيرات البيئة**
|
||||
|
||||
* تمت الإضافة: `IS_EMAIL_VERIFICATION_REQUIRED`, `EMAIL_VERIFICATION_TOKEN_EXPIRES_IN`, `WORKFLOW_EXEC_THROTTLE_LIMIT`, `WORKFLOW_EXEC_THROTTLE_TTL`
|
||||
|
||||
### من v0.34.0 إلى v0.35.0
|
||||
|
||||
قم بترقية مثيل Twenty الخاص بك لاستخدام صورة v0.35.0
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.35
|
||||
```
|
||||
|
||||
أمر `yarn database:migrate:prod` سيقوم بتطبيق الترقيات على هيكل قاعدة البيانات (مخططات core وmetadata)
|
||||
أمر `yarn command:prod upgrade-0.35` يتولى ترقية البيانات إلى جميع المساحات.
|
||||
|
||||
**متغيرات البيئة**
|
||||
|
||||
* قمنا باستبدال `ENABLE_DB_MIGRATIONS` بـ `DISABLE_DB_MIGRATIONS` (القيمة الافتراضية الآن `false`, على الأرجح لن تحتاج إلى تعيين أي شيء)
|
||||
|
||||
### من v0.33.0 إلى v0.34.0
|
||||
|
||||
قم بترقية مثيل Twenty الخاص بك لاستخدام صورة v0.34.0
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.34
|
||||
```
|
||||
|
||||
أمر `yarn database:migrate:prod` سيقوم بتطبيق الترقيات على هيكل قاعدة البيانات (مخططات core وmetadata)
|
||||
أمر `yarn command:prod upgrade-0.34` يتولى ترقية البيانات إلى جميع المساحات.
|
||||
|
||||
**متغيرات البيئة**
|
||||
|
||||
* تمت الإزالة: `FRONT_BASE_URL`
|
||||
* تمت الإضافة: `FRONT_DOMAIN`, `FRONT_PROTOCOL`, `FRONT_PORT`
|
||||
|
||||
لقد قمنا بتحديث الطريقة التي نتعامل بها مع عنوان URL الخاص بالواجهة الأمامية.
|
||||
يمكنك الآن تعيين عنوان URL الخاص بالواجهة الأمامية باستخدام متغيرات `FRONT_DOMAIN`, `FRONT_PROTOCOL` و`FRONT_PORT`.
|
||||
إذا لم يتم تعيين FRONT_DOMAIN، فسوف يتراجع عنوان URL للواجهة الأمامية إلى `SERVER_URL`.
|
||||
|
||||
### من v0.32.0 إلى v0.33.0
|
||||
|
||||
قم بترقية مثيل Twenty الخاص بك لاستخدام صورة v0.33.0
|
||||
|
||||
```
|
||||
yarn command:prod cache:flush
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.33
|
||||
```
|
||||
|
||||
أمر `yarn command:prod cache:flush` سيقوم بمسح ذاكرة تخزين Redis المؤقتة.
|
||||
أمر `yarn database:migrate:prod` سيقوم بتطبيق الترقيات على هيكل قاعدة البيانات (مخططات core وmetadata)
|
||||
أمر `yarn command:prod upgrade-0.33` يتولى ترقية البيانات إلى جميع المساحات.
|
||||
|
||||
بدءًا من هذا الإصدار، أصبحت صورة twenty-postgres للقاعدة غير نشطة وتم استخدام twenty-postgres-spilo بدلاً منها.
|
||||
إذا كنت ترغب في الاستمرار باستخدام صورة twenty-postgres، فما عليك سوى استبدال `twentycrm/twenty-postgres:${TAG}` بـ `twentycrm/twenty-postgres` في docker-compose.yml.
|
||||
|
||||
### من v0.31.0 إلى v0.32.0
|
||||
|
||||
قم بترقية مثيل Twenty الخاص بك لاستخدام صورة v0.32.0
|
||||
|
||||
**ترقية المخطط والبيانات**
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.32
|
||||
```
|
||||
|
||||
أمر `yarn database:migrate:prod` سيقوم بتطبيق الترقيات على هيكل قاعدة البيانات (مخططات core وmetadata)
|
||||
أمر `yarn command:prod upgrade-0.32` يتولى ترقية البيانات إلى جميع المساحات.
|
||||
|
||||
**متغيرات البيئة**
|
||||
|
||||
لقد قمنا بتحديث الطريقة التي نتعامل بها مع اتصال Redis.
|
||||
|
||||
* تمت الإزالة: `REDIS_HOST`, `REDIS_PORT`, `REDIS_USERNAME`, `REDIS_PASSWORD`
|
||||
* تمت الإضافة: `REDIS_URL`
|
||||
|
||||
قم بتحديث ملفك `.env` لاستخدام المتغير الجديد `REDIS_URL` بدلاً من معلمات اتصال Redis الفردية.
|
||||
|
||||
قمنا أيضًا بتبسيط الطريقة التي نتعامل بها مع رموز JWT.
|
||||
|
||||
* تمت الإزالة: `ACCESS_TOKEN_SECRET`, `LOGIN_TOKEN_SECRET`, `REFRESH_TOKEN_SECRET`, `FILE_TOKEN_SECRET`
|
||||
* تمت الإضافة: `APP_SECRET`
|
||||
|
||||
قم بتحديث ملفك `.env` لاستخدام المتغير الجديد `APP_SECRET` بدلاً من الأسرار الفردية للرموز (يمكنك استخدام نفس السر كما كان من قبل أو توليد سلسلة عشوائية جديدة)
|
||||
|
||||
**الحساب المتصل**
|
||||
|
||||
إذا كنت تستخدم حسابًا متصلًا لمزامنة رسائل بريدك الإلكتروني في جوجل والتقويمات، فستحتاج إلى تفعيل [People API](https://developers.google.com/people) في وحدة تحكم مشرف جوجل لديك.
|
||||
|
||||
### من v0.30.0 إلى v0.31.0
|
||||
|
||||
قم بترقية مثيل Twenty الخاص بك لاستخدام صورة v0.31.0
|
||||
|
||||
**ترقية المخطط والبيانات**:
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.31
|
||||
```
|
||||
|
||||
أمر `yarn database:migrate:prod` سيقوم بتطبيق الترقيات على هيكل قاعدة البيانات (مخططات core وmetadata)
|
||||
أمر `yarn command:prod upgrade-0.31` يتولى ترقية البيانات إلى جميع المساحات.
|
||||
|
||||
### من v0.24.0 إلى v0.30.0
|
||||
|
||||
قم بترقية مثيل Twenty الخاص بك لاستخدام صورة v0.30.0
|
||||
|
||||
**تغيير كبير**:
|
||||
لتحسين الأداء، يتطلب Twenty الآن تكوين Redis للتخزين المؤقت. قمنا بتحديث [docker-compose.yml](https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/docker-compose.yml) لتعكس ذلك.
|
||||
تأكد من تحديث إعدادات التكوين الخاصة بك وتحديث المتغيرات البيئية الخاصة بك وفقًا لذلك:
|
||||
|
||||
```
|
||||
REDIS_HOST={your-redis-host}
|
||||
REDIS_PORT={your-redis-port}
|
||||
CACHE_STORAGE_TYPE=redis
|
||||
```
|
||||
|
||||
**ترقية المخطط والبيانات**:
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.30
|
||||
```
|
||||
|
||||
أمر `yarn database:migrate:prod` سيقوم بتطبيق الترقيات على هيكل قاعدة البيانات (مخططات core وmetadata)
|
||||
أمر `yarn command:prod upgrade-0.30` يتولى ترقية البيانات إلى جميع المساحات.
|
||||
|
||||
### من v0.23.0 إلى v0.24.0
|
||||
|
||||
قم بترقية مثيل Twenty الخاص بك لاستخدام صورة v0.24.0
|
||||
|
||||
قم بتشغيل الأوامر التالية:
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.24
|
||||
```
|
||||
|
||||
أمر `yarn database:migrate:prod` سيقوم بتطبيق الترقيات على هيكل قاعدة البيانات (مخططات core وmetadata)
|
||||
أمر `yarn command:prod upgrade-0.24` يتولى ترقية البيانات إلى جميع المساحات.
|
||||
|
||||
### من v0.22.0 إلى v0.23.0
|
||||
|
||||
قم بترقية مثيل Twenty الخاص بك لاستخدام صورة v0.23.0
|
||||
|
||||
قم بتشغيل الأوامر التالية:
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.23
|
||||
```
|
||||
|
||||
أمر `yarn database:migrate:prod` سيقوم بتطبيق الترقيات على قاعدة البيانات.
|
||||
أمر `yarn command:prod upgrade-0.23` يتولى ترقية البيانات، بما في ذلك نقل الأنشطة إلى المهام/الملاحظات.
|
||||
|
||||
### من v0.21.0 إلى v0.22.0
|
||||
|
||||
قم بترقية مثيل Twenty الخاص بك لاستخدام صورة v0.22.0
|
||||
|
||||
قم بتشغيل الأوامر التالية:
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod workspace:sync-metadata -f
|
||||
yarn command:prod upgrade-0.22
|
||||
```
|
||||
|
||||
أمر `yarn database:migrate:prod` سيقوم بتطبيق الترقيات على قاعدة البيانات.
|
||||
الأمر `yarn command:prod workspace:sync-metadata -f` سيزامن تعريف الكائنات القياسية مع جداول البيانات الوصفية ويطبق الترقيات المطلوبة على مساحات العمل الموجودة.
|
||||
الأمر `yarn command:prod upgrade-0.22` سيقوم بتطبيق تحويلات بيانات محددة للتكيف مع الخيارات الافتراضية الجديدة لتوثيق الطلبات في الكائنات.
|
||||
If your instance is older than v1.22, you must upgrade incrementally through each major tagged version (v1.6 to v1.7, then v1.7 to v1.8, and so on) until you reach v1.22. From there, you can jump directly to the latest version.
|
||||
|
|
|
|||
|
|
@ -1,24 +1,27 @@
|
|||
{
|
||||
"tabs": {
|
||||
"gettingStarted": {
|
||||
"label": "البدء",
|
||||
"groups": {
|
||||
"welcome": {
|
||||
"label": "Welcome"
|
||||
},
|
||||
"coreConcepts": {
|
||||
"label": "Core Concepts"
|
||||
}
|
||||
}
|
||||
},
|
||||
"userGuide": {
|
||||
"label": "دليل المستخدم",
|
||||
"groups": {
|
||||
"discoverTwenty": {
|
||||
"label": "اكتشف Twenty",
|
||||
"groups": {
|
||||
"gettingStartedCapabilities": {
|
||||
"label": "القدرات"
|
||||
},
|
||||
"gettingStartedHowTos": {
|
||||
"label": "الإرشادات"
|
||||
}
|
||||
}
|
||||
"userGuideOverview": {
|
||||
"label": "نظرة عامة"
|
||||
},
|
||||
"dataModel": {
|
||||
"label": "نموذج البيانات",
|
||||
"groups": {
|
||||
"dataModelCapabilities": {
|
||||
"label": "القدرات"
|
||||
"dataModelReference": {
|
||||
"label": "Reference"
|
||||
},
|
||||
"dataModelHowTos": {
|
||||
"label": "الإرشادات"
|
||||
|
|
@ -28,8 +31,8 @@
|
|||
"dataMigration": {
|
||||
"label": "ترحيل البيانات",
|
||||
"groups": {
|
||||
"dataMigrationCapabilities": {
|
||||
"label": "القدرات"
|
||||
"dataMigrationReference": {
|
||||
"label": "Reference"
|
||||
},
|
||||
"dataMigrationHowTos": {
|
||||
"label": "الإرشادات"
|
||||
|
|
@ -39,8 +42,8 @@
|
|||
"calendarEmails": {
|
||||
"label": "التقويم والبريد الإلكتروني",
|
||||
"groups": {
|
||||
"calendarEmailsCapabilities": {
|
||||
"label": "القدرات"
|
||||
"calendarEmailsReference": {
|
||||
"label": "Reference"
|
||||
},
|
||||
"calendarEmailsHowTos": {
|
||||
"label": "الإرشادات"
|
||||
|
|
@ -50,8 +53,8 @@
|
|||
"workflows": {
|
||||
"label": "سير العمل",
|
||||
"groups": {
|
||||
"workflowsCapabilities": {
|
||||
"label": "القدرات"
|
||||
"workflowsReference": {
|
||||
"label": "Reference"
|
||||
},
|
||||
"workflowsHowTos": {
|
||||
"label": "الإرشادات",
|
||||
|
|
@ -75,21 +78,26 @@
|
|||
"ai": {
|
||||
"label": "الذكاء الاصطناعي",
|
||||
"groups": {
|
||||
"aiCapabilities": {
|
||||
"label": "القدرات"
|
||||
"aiReference": {
|
||||
"label": "Reference"
|
||||
},
|
||||
"aiHowTos": {
|
||||
"label": "الإرشادات"
|
||||
}
|
||||
}
|
||||
},
|
||||
"viewsPipelines": {
|
||||
"label": "طرق العرض والمسارات",
|
||||
"layout": {
|
||||
"label": "التخطيط",
|
||||
"groups": {
|
||||
"viewsPipelinesCapabilities": {
|
||||
"label": "القدرات"
|
||||
"layoutReference": {
|
||||
"label": "Reference",
|
||||
"groups": {
|
||||
"layoutViews": {
|
||||
"label": "العروض"
|
||||
}
|
||||
}
|
||||
},
|
||||
"viewsPipelinesHowTos": {
|
||||
"layoutHowTos": {
|
||||
"label": "الإرشادات"
|
||||
}
|
||||
}
|
||||
|
|
@ -97,8 +105,8 @@
|
|||
"dashboards": {
|
||||
"label": "لوحات القيادة",
|
||||
"groups": {
|
||||
"dashboardsCapabilities": {
|
||||
"label": "القدرات"
|
||||
"dashboardsReference": {
|
||||
"label": "Reference"
|
||||
},
|
||||
"dashboardsHowTos": {
|
||||
"label": "الإرشادات"
|
||||
|
|
@ -108,8 +116,8 @@
|
|||
"permissionsAccess": {
|
||||
"label": "الصلاحيات والوصول",
|
||||
"groups": {
|
||||
"permissionsAccessCapabilities": {
|
||||
"label": "القدرات"
|
||||
"permissionsAccessReference": {
|
||||
"label": "Reference"
|
||||
},
|
||||
"permissionsAccessHowTos": {
|
||||
"label": "الإرشادات"
|
||||
|
|
@ -119,8 +127,8 @@
|
|||
"billing": {
|
||||
"label": "الفوترة",
|
||||
"groups": {
|
||||
"billingCapabilities": {
|
||||
"label": "القدرات"
|
||||
"billingReference": {
|
||||
"label": "Reference"
|
||||
},
|
||||
"billingHowTos": {
|
||||
"label": "الإرشادات"
|
||||
|
|
@ -130,8 +138,8 @@
|
|||
"settings": {
|
||||
"label": "\\ا\\ل\\إ\\ع\\د\\ا\\د\\ا\\ت",
|
||||
"groups": {
|
||||
"settingsCapabilities": {
|
||||
"label": "القدرات"
|
||||
"settingsReference": {
|
||||
"label": "Reference"
|
||||
},
|
||||
"settingsHowTos": {
|
||||
"label": "الإرشادات"
|
||||
|
|
@ -143,59 +151,20 @@
|
|||
"developers": {
|
||||
"label": "المطورون",
|
||||
"groups": {
|
||||
"developersGroup": {
|
||||
"label": "المطورون"
|
||||
"developersOverview": {
|
||||
"label": "نظرة عامة"
|
||||
},
|
||||
"extend": {
|
||||
"label": "التوسيع",
|
||||
"groups": {
|
||||
"apps": {
|
||||
"label": "التطبيقات"
|
||||
}
|
||||
}
|
||||
"apps": {
|
||||
"label": "التطبيقات"
|
||||
},
|
||||
"api": {
|
||||
"label": "واجهة برمجة التطبيقات"
|
||||
},
|
||||
"selfHost": {
|
||||
"label": "الاستضافة الذاتية",
|
||||
"groups": {
|
||||
"selfHostCapabilities": {
|
||||
"label": "القدرات"
|
||||
}
|
||||
}
|
||||
"label": "الاستضافة الذاتية"
|
||||
},
|
||||
"contribute": {
|
||||
"label": "المساهمة",
|
||||
"groups": {
|
||||
"contributeCapabilities": {
|
||||
"label": "القدرات",
|
||||
"groups": {
|
||||
"frontendDevelopment": {
|
||||
"label": "تطوير الواجهة الأمامية",
|
||||
"groups": {
|
||||
"twentyUi": {
|
||||
"label": "Twenty UI",
|
||||
"groups": {
|
||||
"display": {
|
||||
"label": "عرض"
|
||||
},
|
||||
"feedback": {
|
||||
"label": "التغذية الراجعة"
|
||||
},
|
||||
"input": {
|
||||
"label": "إدخال"
|
||||
},
|
||||
"navigation": {
|
||||
"label": "التنقل"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"backendDevelopment": {
|
||||
"label": "تطوير الواجهة الخلفية"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"label": "المساهمة"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: تلميح التطبيق
|
||||
image: /images/user-guide/tips/light-bulb.png
|
||||
icon: رسالة
|
||||
---
|
||||
|
||||
<Frame>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: علامة صحيح
|
||||
image: /images/user-guide/tasks/tasks_header.png
|
||||
icon: circle-check
|
||||
---
|
||||
|
||||
<Frame>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
---
|
||||
title: رقاقة
|
||||
image: /images/user-guide/github/github-header.png
|
||||
---
|
||||
|
||||
<Frame>
|
||||
|
|
@ -39,7 +38,7 @@ export const MyComponent = () => {
|
|||
<Tab title="المحددات">
|
||||
|
||||
|
||||
| المحددات | النوع | الوصف |
|
||||
| الخصائص | النوع | الوصف |
|
||||
| ------------------ | ----------------------- | ---------------------------------------------------------------------- |
|
||||
| linkToEntity | نص | الرابط إلى الكيان |
|
||||
| معرف الكيان | نص | المعرف الفريد للكيان |
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: الأيقونات
|
||||
image: /images/user-guide/objects/objects.png
|
||||
icon: الأيقونات
|
||||
---
|
||||
|
||||
<Frame>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,7 @@
|
|||
---
|
||||
title: شارة قريبًا
|
||||
image: /images/user-guide/kanban-views/kanban.png
|
||||
---
|
||||
|
||||
<Frame>
|
||||
<img src="/images/user-guide/kanban-views/kanban.png" alt="رأس الصفحة" />
|
||||
</Frame>
|
||||
|
||||
شارة صغيرة أو "كبسولة" للإشارة إلى أن شيئًا ما قادم قريبًا.
|
||||
|
||||
```jsx
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
---
|
||||
title: علامة
|
||||
image: /images/user-guide/table-views/table.png
|
||||
icon: علامة
|
||||
---
|
||||
|
||||
<Frame>
|
||||
<img src="/images/user-guide/table-views/table.png" alt="رأس الصفحة" />
|
||||
</Frame>
|
||||
|
||||
مكوّن لتصنيف المحتوى أو وسمه بصريًا.
|
||||
|
||||
<Tabs>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
---
|
||||
title: إدخال
|
||||
image: /images/user-guide/tips/light-bulb.png
|
||||
---
|
||||
|
||||
<Frame>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
---
|
||||
title: محرر الكتل
|
||||
image: /images/user-guide/api/api.png
|
||||
---
|
||||
|
||||
<Frame>
|
||||
|
|
@ -27,9 +26,9 @@ export const MyComponent = () => {
|
|||
<Tab title="المحددات">
|
||||
|
||||
|
||||
| المحددات | النوع | الوصف |
|
||||
| -------- | ----------------- | ------------------------ |
|
||||
| محرر | `BlockNoteEditor` | مثيل أو تكوين محرر الكتل |
|
||||
| الخصائص | النوع | الوصف |
|
||||
| ------- | ----------------- | ------------------------ |
|
||||
| محرر | `BlockNoteEditor` | مثيل أو تكوين محرر الكتل |
|
||||
|
||||
|
||||
|
||||
|
|
|
|||