Compare commits

..
Author SHA1 Message Date
93eeeb2397 i18n - docs translations (#19509)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-09 16:55:20 +02:00
Charles BochetandGitHub 36fbfca069 Add application-logs module with driver pattern for logic function log persistence (#19486)
## Summary

- Introduces a new `application-logs` core module with a driver pattern
(disabled/console/clickhouse) to capture and persist logic function
execution logs
- Adds a ClickHouse `applicationLog` table with per-line log storage,
30-day TTL, and `ORDER BY (workspaceId, timestamp, applicationId,
logicFunctionId)`
- Surfaces application logs in the existing frontend audit logs table as
a new "Application Logs" source with dedicated columns (Function,
Timestamp, Level, Message, Execution ID)

## Details

**Write path**: `LogicFunctionExecutorService.handleExecutionResult()`
parses the multi-line log string from driver output into individual `{
timestamp, level, message }` entries, generates an execution UUID, and
passes them to `ApplicationLogsService.writeLogs()` which delegates to
the configured driver.

**Driver pattern**: Follows the exception-handler module style (Symbol
injection token + `forRootAsync` dynamic module). Three drivers:
- `DISABLED` (default) — no-op, prevents information leaking
- `CONSOLE` — structured stdout logging with level-based `console.*`
calls
- `CLICKHOUSE` — inserts rows into the `applicationLog` ClickHouse table

**Read path**: Extends the existing event-logs module by adding
`APPLICATION_LOG` to the `EventLogTable` enum, table name mapping, and
normalization logic.

**Config**: New `APPLICATION_LOG_DRIVER_TYPE` environment variable
(default: `DISABLED`).
2026-04-09 14:35:24 +00:00
Abdullah.andGitHub 5116002ca2 chore: replace glb files and lottie with optimized variants (#19503)
As per title. Replaced .json file with .lottie file - it is 10x smaller.

Illustrations folder went from 150mb to 5.9mb with compressed variants
of 3D models. The DracoLoader, GLTFLoader and some other logic logic is
repetitive across files, but Thomas is working on the same files at the
moment, so not touching too much to avoid conflicts with his PR. Once
he's done styling, we can extract shared logic into helpers.
2026-04-09 14:27:13 +00:00
EtienneandGitHub caac791421 Cleaning - Remove logs (#19498) 2026-04-09 14:19:53 +00:00
d3a1f45027 i18n - translations (#19506)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-09 16:32:53 +02:00
a5174c0519 i18n - translations (#19504)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-09 16:25:54 +02:00
nitinandGitHub 50c5a1a8de [AI] new model tab design (#19384)
closes
https://discord.com/channels/1130383047699738754/1480979892610007121

<img width="1839" height="1316" alt="CleanShot 2026-04-07 at 15 22 38"
src="https://github.com/user-attachments/assets/d8f6048c-4e0f-425f-b7ea-4913d116020e"
/>
2026-04-09 14:15:43 +00:00
EtienneandGitHub 74e26ae635 Fix Date/DateTime/Relation field forms (#19463)
https://discord.com/channels/1130383047699738754/1486025695481168102

On Date and DateTime, picker doesn't open when clicking.
On Relation, No record option can't be selected. Introduce the "null",
to break relation.
2026-04-09 14:09:44 +00:00
Paul RastoinandGitHub 5e3cf7cd2b Prepare 1.21 (#19501)
Migrating some 1.21 migrations to the new pattern so it writes in the
history
When release in prod we will ahve to manually inject them as they have
already been run
It's mainly for self host so when releasing the 1.22 we will be able to
rely on the 1.21 history
2026-04-09 13:53:21 +00:00
16e145b036 Fix moving a widget to another tab (#19450)
https://github.com/user-attachments/assets/aac81e79-7f2f-4a34-bf68-c76061086821

---------

Co-authored-by: Weiko <corentin@twenty.com>
2026-04-09 15:59:04 +02:00
neo773andGitHub 1e908e5f0f fix: SendEmail workflow ConnectedAccount query (#19484) 2026-04-09 15:58:12 +02:00
WeikoandGitHub d495a9f412 reorganize standard page layouts (#19482)
<details>
<summary>
Reorganizing standard page layouts (see screenshot below)
</summary>
<img width="355" height="617" alt="Screenshot 2026-04-09 at 10 44 02"
src="https://github.com/user-attachments/assets/0b71d607-1d9d-48b6-8612-3de98d12d1fa"
/>
</details>
<details>
<summary>
Note: All standard objects now have a last system section for
createdBy/createdAt however since all new custom fields are added to the
last section they are set there (see 2nd screenshot below) until people
move them to dedicated section which can be counterintuitive. There is
an upcoming feature that allows user to set a default section and
visibility for newly added fields that should solve that
</summary>
<img width="360" height="849" alt="Screenshot 2026-04-09 at 10 43 44"
src="https://github.com/user-attachments/assets/127990d0-66b7-4b1d-ab00-8251e9707a04"
/>
</details>

Another note: Section are not translated at the moment
2026-04-09 15:44:38 +02:00
1df9942416 i18n - translations (#19500)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-09 15:05:50 +02:00
EtienneandGitHub 34972f6167 Record table widget - Follow up (#19479)
closes https://github.com/twentyhq/core-team-issues/issues/2360
2026-04-09 12:50:10 +00:00
Abdullah.andGitHub 37acd15033 More website fixes. (#19489)
This PR fixes most of the visual issues with home page and pricing page.
I think it would be a good checkpoint to merge.
2026-04-09 12:41:14 +00:00
c76824e69f i18n - docs translations (#19497)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-09 14:42:31 +02:00
neo773andGitHub 7bd84a6029 messaging fix relaunch cron jobs (#19492) 2026-04-09 12:25:12 +00:00
WeikoandGitHub 1577a1933f Move page layout backfill command out of 1-21 release (#19483)
Note: The next tag might be 2.0 and this can be changed later, the point
here is to move the command out of 1-21
2026-04-09 12:21:47 +00:00
Raphaël BosiandGitHub 086ea644bb Resolve frontComponent relation on command menu items via selector (#19493)
## Bug description

Headless command menu items were interpreted as non headless command
menu items and opened in the side panel.


https://github.com/user-attachments/assets/44d8b4d3-9ee5-4f12-9ff8-b3ed51e5b3b6

## Fix


https://github.com/user-attachments/assets/9d6eb900-9817-4ceb-87aa-547ad046a5a1

- Command menu items received via SSE lack the nested `frontComponent`
relation (only `frontComponentId` is present), causing
`item.frontComponent?.isHeadless` to always be undefined.
- Add `frontComponents` to the metadata store's stale entity loading
- Rewrite `commandMenuItemsSelector` to join `commandMenuItems` with
`frontComponents` by `frontComponentId`
2026-04-09 12:15:52 +00:00
Raphaël BosiandGitHub 19f11b1216 [COMMAND MENU ITEMS] Add dynamic label and icon to command menu navigation items (#19452) 2026-04-09 12:00:49 +00:00
9b8cb610c3 Flush Redis between server runs in breaking changes CI (#19491)
## Summary
Added a Redis cache flush step in the breaking changes CI workflow to
prevent stale data contamination between consecutive server runs.

## Key Changes
- Added a new workflow step that executes `redis-cli FLUSHALL` between
the current branch server run and the main branch server run
- Includes error handling to log a warning if the Redis flush fails,
without blocking the workflow
- Added explanatory comments documenting why this step is necessary

## Implementation Details
The Redis flush is necessary because both the current branch and main
branch servers share the same Redis instance during CI testing. The
`CoreEntityCacheService` and `WorkspaceCacheService` persist cached
entities across process restarts, which can cause stale data from the
current branch server to contaminate the main branch server comparison.
This step ensures a clean cache state before switching branches.

https://claude.ai/code/session_01BVMacfAXDMNx5WAFtP7GgW

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-09 12:56:24 +02:00
64c7f52f06 i18n - docs translations (#19490)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-09 12:39:31 +02:00
dc6e76b6ab i18n - translations (#19488)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-09 12:26:14 +02:00
martmullGitHubcubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
a78a843419 Fix install and deploy commands (#19481)
- disable publish with existing version
- disable installation of app version already installed

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-04-09 10:10:16 +00:00
martmullandGitHub 5eaabe95e7 Fix role synchronisation (#19469)
As title
solves
https://discord.com/channels/1130383047699738754/1491167098398052503
2026-04-09 09:36:16 +00:00
Thomas TrompetteandGitHub 5edc034f8b Enqueue a snack bar on merge preview errors (#19465)
Fixes https://github.com/twentyhq/twenty/issues/19312

<img width="400" height="500" alt="Capture d’écran 2026-04-08 à 18 17
57"
src="https://github.com/user-attachments/assets/41ac28b2-0208-47b4-bd85-f803c524330a"
/>
2026-04-09 08:43:54 +00:00
Raphaël BosiandGitHub 9080180156 [COMMAND MENU ITEMS] Sync object metadata with navigation command menu items (#19456)
When an object is created or enabled we create a navigation command menu
item to that object. And when the object is disabled or deleted, we
remove this command menu item.
2026-04-09 08:18:49 +00:00
f8dff23a3e Refactor: Extract EventRow shared types and styles to EventRowBase (#19480)
## Summary
This PR refactors the EventRow component structure by extracting shared
types and styled components into a dedicated base file, improving code
organization and reducing duplication.

## Key Changes
- Created new `EventRowBase.tsx` file to centralize shared EventRow
utilities
- Moved `EventRowDynamicComponentProps` interface from
`EventRowDynamicComponent.tsx` to `EventRowBase.tsx`
- Moved styled components `StyledEventRowItemColumn` and
`StyledEventRowItemAction` from `EventRowDynamicComponent.tsx` to
`EventRowBase.tsx`
- Updated all imports across 6 files to reference the new `EventRowBase`
module instead of `EventRowDynamicComponent`
- Simplified `EventRowDynamicComponent.tsx` to re-export types and
styles from `EventRowBase` for backward compatibility

## Files Updated
- `EventRowDynamicComponent.tsx` - Simplified to import and re-export
from EventRowBase
- `EventRowActivity.tsx` - Updated import path
- `EventRowCalendarEvent.tsx` - Updated import path
- `EventRowMainObject.tsx` - Updated import path
- `EventRowMainObjectUpdated.tsx` - Updated import path
- `EventRowMessage.tsx` - Updated import path

## Benefits
- Better separation of concerns with shared base utilities in a
dedicated module
- Reduced circular dependency risks
- Clearer module structure for future EventRow-related components
- Maintains backward compatibility through re-exports

https://claude.ai/code/session_011EyhBJ56RGuZHxBVWwzEQy

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-09 10:14:18 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1b8f26323a Bump @babel/preset-react from 7.26.3 to 7.28.5 (#19475)
Bumps
[@babel/preset-react](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-react)
from 7.26.3 to 7.28.5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/babel/babel/releases"><code>@​babel/preset-react</code>'s
releases</a>.</em></p>
<blockquote>
<h2>v7.28.5 (2025-10-23)</h2>
<p>Thank you <a
href="https://github.com/CO0Ki3"><code>@​CO0Ki3</code></a>, <a
href="https://github.com/Olexandr88"><code>@​Olexandr88</code></a>, and
<a href="https://github.com/youthfulhps"><code>@​youthfulhps</code></a>
for your first PRs!</p>
<h4>👓 Spec Compliance</h4>
<ul>
<li><code>babel-parser</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17446">#17446</a>
Allow <code>Runtime Errors for Function Call Assignment Targets</code>
(<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
<li><code>babel-helper-validator-identifier</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17501">#17501</a>
fix: update identifier to unicode 17 (<a
href="https://github.com/fisker"><code>@​fisker</code></a>)</li>
</ul>
</li>
</ul>
<h4>🐛 Bug Fix</h4>
<ul>
<li><code>babel-plugin-proposal-destructuring-private</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17534">#17534</a>
Allow mixing private destructuring and rest (<a
href="https://github.com/CO0Ki3"><code>@​CO0Ki3</code></a>)</li>
</ul>
</li>
<li><code>babel-parser</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17521">#17521</a>
Improve <code>@babel/parser</code> error typing (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
<li><a
href="https://redirect.github.com/babel/babel/pull/17491">#17491</a>
fix: improve ts-only declaration parsing (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-plugin-proposal-discard-binding</code>,
<code>babel-plugin-transform-destructuring</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17519">#17519</a>
fix: <code>rest</code> correctly returns plain array (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
<li><code>babel-helper-create-class-features-plugin</code>,
<code>babel-helper-member-expression-to-functions</code>,
<code>babel-plugin-transform-block-scoping</code>,
<code>babel-plugin-transform-optional-chaining</code>,
<code>babel-traverse</code>, <code>babel-types</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17503">#17503</a> Fix
<code>JSXIdentifier</code> handling in
<code>isReferencedIdentifier</code> (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-traverse</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17504">#17504</a>
fix: ensure scope.push register in anonymous fn (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
</ul>
<h4>🏠 Internal</h4>
<ul>
<li><code>babel-types</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17494">#17494</a>
Type checking babel-types scripts (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
</ul>
<h4>🏃‍♀️ Performance</h4>
<ul>
<li><code>babel-core</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17490">#17490</a>
Faster finding of locations in <code>buildCodeFrameError</code> (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
</ul>
<h4>Committers: 8</h4>
<ul>
<li>Babel Bot (<a
href="https://github.com/babel-bot"><code>@​babel-bot</code></a>)</li>
<li>Byeongho Yoo (<a
href="https://github.com/youthfulhps"><code>@​youthfulhps</code></a>)</li>
<li>Huáng Jùnliàng (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
<li>Hyeon Dokko (<a
href="https://github.com/CO0Ki3"><code>@​CO0Ki3</code></a>)</li>
<li>Nicolò Ribaudo (<a
href="https://github.com/nicolo-ribaudo"><code>@​nicolo-ribaudo</code></a>)</li>
<li><a
href="https://github.com/Olexandr88"><code>@​Olexandr88</code></a></li>
<li><a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a></li>
<li>fisker Cheung (<a
href="https://github.com/fisker"><code>@​fisker</code></a>)</li>
</ul>
<h2>v7.28.4 (2025-09-05)</h2>
<p>Thanks <a
href="https://github.com/gwillen"><code>@​gwillen</code></a> and <a
href="https://github.com/mrginglymus"><code>@​mrginglymus</code></a> for
your first PRs!</p>
<h4>🏠 Internal</h4>
<ul>
<li><code>babel-core</code>,
<code>babel-helper-check-duplicate-nodes</code>,
<code>babel-traverse</code>, <code>babel-types</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17493">#17493</a>
Update Jest to v30.1.1 (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-plugin-transform-regenerator</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17455">#17455</a>
chore: Clean up <code>transform-regenerator</code> (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/babel/babel/blob/main/CHANGELOG.md"><code>@​babel/preset-react</code>'s
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<blockquote>
<p><strong>Tags:</strong></p>
<ul>
<li>💥 [Breaking Change]</li>
<li>👓 [Spec Compliance]</li>
<li>🚀 [New Feature]</li>
<li>🐛 [Bug Fix]</li>
<li>📝 [Documentation]</li>
<li>🏠 [Internal]</li>
<li>💅 [Polish]</li>
</ul>
</blockquote>
<p><em>Note: Gaps between patch versions are faulty, broken or test
releases.</em></p>
<p>This file contains the changelog starting from v8.0.0-alpha.0.</p>
<ul>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v7.15.0-v7.28.5.md">CHANGELOG
- v7.15.0 to v7.28.5</a> for v7.15.0 to v7.28.5 changes (the last common
release between the v8 and v7 release lines was v7.28.5).</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v7.0.0-v7.14.9.md">CHANGELOG
- v7.0.0 to v7.14.9</a> for v7.0.0 to v7.14.9 changes.</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v7-prereleases.md">CHANGELOG
- v7 prereleases</a> for v7.0.0-alpha.1 to v7.0.0-rc.4 changes.</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v4.md">CHANGELOG
- v4</a>, <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v5.md">CHANGELOG
- v5</a>, and <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v6.md">CHANGELOG
- v6</a> for v4.x-v6.x changes.</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-6to5.md">CHANGELOG
- 6to5</a> for the pre-4.0.0 version changelog.</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/packages/babel-parser/CHANGELOG.md">Babylon's
CHANGELOG</a> for the Babylon pre-7.0.0-beta.29 version changelog.</li>
<li>See <a
href="https://github.com/babel/babel-eslint/releases"><code>babel-eslint</code>'s
releases</a> for the changelog before <code>@babel/eslint-parser</code>
7.8.0.</li>
<li>See <a
href="https://github.com/babel/eslint-plugin-babel/releases"><code>eslint-plugin-babel</code>'s
releases</a> for the changelog before <code>@babel/eslint-plugin</code>
7.8.0.</li>
</ul>
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<h2>v8.0.0-rc.3 (2026-03-16)</h2>
<h4>👓 Spec Compliance</h4>
<ul>
<li><code>babel-parser</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17839">#17839</a>
Fix(parser): async x =&gt; {} must be in leading pos (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
<li><a
href="https://redirect.github.com/babel/babel/pull/17803">#17803</a>
Disallow non-leading solo await within F# pipeline (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
</ul>
<h4>💥 Breaking Change</h4>
<ul>
<li><code>babel-parser</code>,
<code>babel-plugin-proposal-do-expressions</code>,
<code>babel-plugin-proposal-pipeline-operator</code>,
<code>babel-plugin-transform-exponentiation-operator</code>,
<code>babel-plugin-transform-instanceof</code>,
<code>babel-traverse</code>, <code>babel-types</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17867">#17867</a>
[Babel 8] Remove <code>Import</code> from the <code>Expression</code>
alias (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-plugin-transform-react-jsx-development</code>,
<code>babel-plugin-transform-react-jsx</code>,
<code>babel-preset-react</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17845">#17845</a>
Gate jsxDEV source/self with <code>developmentSourceSelf</code> option
(<a
href="https://github.com/rootvector2"><code>@​rootvector2</code></a>)</li>
</ul>
</li>
<li><code>babel-generator</code>, <code>babel-parser</code>,
<code>babel-types</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17835">#17835</a>
fix: Remove <code>decorators</code> from <code>TSDeclareMethod</code>
(<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
<li><code>babel-helper-import-to-platform-api</code>,
<code>babel-plugin-proposal-import-wasm-source</code>,
<code>babel-plugin-transform-json-modules</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17816">#17816</a>
Pass <code>file</code> instead of <code>path</code> to
importToPlatformApi builders (<a
href="https://github.com/nicolo-ribaudo"><code>@​nicolo-ribaudo</code></a>)</li>
</ul>
</li>
</ul>
<h4>🚀 New Feature</h4>
<ul>
<li><code>babel-plugin-transform-react-jsx-development</code>,
<code>babel-plugin-transform-react-jsx</code>,
<code>babel-preset-react</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17862">#17862</a> Add
<code>sourceSelf</code> option to
<code>@babel/plugin-transform-react-jsx-development</code> (<a
href="https://github.com/nicolo-ribaudo"><code>@​nicolo-ribaudo</code></a>)</li>
</ul>
</li>
<li><code>babel-parser</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/16935">#16935</a>
feat: Add <code>locations</code> option to parser (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/babel/babel/commit/61647ae2397c82c3c71f077b5ab109106a5cac0f"><code>61647ae</code></a>
v7.28.5</li>
<li><a
href="https://github.com/babel/babel/commit/42cb285b59fc99a8102d69bef6223b75617e9f46"><code>42cb285</code></a>
Improve <code>@babel/core</code> types (<a
href="https://github.com/babel/babel/tree/HEAD/packages/babel-preset-react/issues/17404">#17404</a>)</li>
<li><a
href="https://github.com/babel/babel/commit/eebd3a06021c13d335b5b0bd79734df3abbea678"><code>eebd3a0</code></a>
v7.27.1</li>
<li><a
href="https://github.com/babel/babel/commit/fdc0fb59e119ee0b38bced63867a344a5b4bc2f3"><code>fdc0fb5</code></a>
[Babel 8] Bump nodejs requirements to <code>^20.19.0 || &gt;=
22.12.0</code> (<a
href="https://github.com/babel/babel/tree/HEAD/packages/babel-preset-react/issues/17204">#17204</a>)</li>
<li><a
href="https://github.com/babel/babel/commit/cd24cc07ef6558b7f6510f9177f6393c91b0549f"><code>cd24cc0</code></a>
chore: Update TS 5.7 (<a
href="https://github.com/babel/babel/tree/HEAD/packages/babel-preset-react/issues/17053">#17053</a>)</li>
<li>See full diff in <a
href="https://github.com/babel/babel/commits/v7.28.5/packages/babel-preset-react">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by [GitHub Actions](<a
href="https://www.npmjs.com/~GitHub">https://www.npmjs.com/~GitHub</a>
Actions), a new releaser for <code>@​babel/preset-react</code> since
your current version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@babel/preset-react&package-manager=npm_and_yarn&previous-version=7.26.3&new-version=7.28.5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-09 06:58:41 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
80337d5c37 Bump tsdav from 2.1.5 to 2.1.8 (#19476)
Bumps [tsdav](https://github.com/natelindev/tsdav) from 2.1.5 to 2.1.8.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/natelindev/tsdav/releases">tsdav's
releases</a>.</em></p>
<blockquote>
<h2>v2.1.8</h2>
<h5>improvements</h5>
<ul>
<li>fixed <a
href="https://redirect.github.com/natelindev/tsdav/issues/272">#272</a>
malformed expand request in fetchCalendarObjects</li>
<li>optimized fetchCalendarObjects to reduce redundant requests when
expand is true</li>
<li>added Bearer auth support for token-based providers (e.g., Nextcloud
OIDC)</li>
<li>added fetch overrides across account, request, and collection
helpers for custom transports</li>
<li>prefer native fetch in Cloudflare Workers to avoid cross-fetch
incompatibilities</li>
<li>collectionQuery now rejects on non-OK responses instead of returning
empty arrays</li>
<li>fetchVCards filters out collection URLs to avoid empty/invalid
entries (Radicale-compatible)</li>
<li>docs updates for iCal feed import, providers, and custom transport
guidance</li>
</ul>
<h2>v2.1.7</h2>
<h2>What's Changed</h2>
<ul>
<li>Fix: Handle empty body responses in davRequest to prevent crash by
<a href="https://github.com/hsvtslv"><code>@​hsvtslv</code></a> in <a
href="https://redirect.github.com/natelindev/tsdav/pull/267">natelindev/tsdav#267</a></li>
<li>Fix npm authentication in auto-release workflow by <a
href="https://github.com/Copilot"><code>@​Copilot</code></a> in <a
href="https://redirect.github.com/natelindev/tsdav/pull/271">natelindev/tsdav#271</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/hsvtslv"><code>@​hsvtslv</code></a> made
their first contribution in <a
href="https://redirect.github.com/natelindev/tsdav/pull/267">natelindev/tsdav#267</a></li>
<li><a href="https://github.com/Copilot"><code>@​Copilot</code></a> made
their first contribution in <a
href="https://redirect.github.com/natelindev/tsdav/pull/271">natelindev/tsdav#271</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/natelindev/tsdav/compare/v2.1.6...v2.1.7">https://github.com/natelindev/tsdav/compare/v2.1.6...v2.1.7</a></p>
<h2>v2.1.6</h2>
<h5>improvements</h5>
<ul>
<li>added AGENTS.md</li>
<li>updated dependencies</li>
<li>fixed docs build issues</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/natelindev/tsdav/blob/main/CHANGELOG.md">tsdav's
changelog</a>.</em></p>
<blockquote>
<h2>v2.1.8</h2>
<h5>improvements</h5>
<ul>
<li>fixed <a
href="https://redirect.github.com/natelindev/tsdav/issues/272">#272</a>
malformed expand request in fetchCalendarObjects</li>
<li>optimized fetchCalendarObjects to reduce redundant requests when
expand is true</li>
<li>added Bearer auth support for token-based providers (e.g., Nextcloud
OIDC)</li>
<li>added fetch overrides across account, request, and collection
helpers for custom transports</li>
<li>prefer native fetch in Cloudflare Workers to avoid cross-fetch
incompatibilities</li>
<li>collectionQuery now rejects on non-OK responses instead of returning
empty arrays</li>
<li>fetchVCards filters out collection URLs to avoid empty/invalid
entries (Radicale-compatible)</li>
<li>docs updates for iCal feed import, providers, and custom transport
guidance</li>
</ul>
<h2>v2.1.7</h2>
<h5>improvements</h5>
<ul>
<li>docs: add browser usage example and clarify class-based login</li>
<li>docs: add Nextcloud connection guidance and Apple password
reference</li>
</ul>
<h2>v2.1.6</h2>
<h5>improvements</h5>
<ul>
<li>added AGENTS.md</li>
<li>updated dependencies</li>
<li>fixed docs build issues</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/natelindev/tsdav/commit/1800c16c8eab089851e64021db33d70413882e3a"><code>1800c16</code></a>
Merge pull request <a
href="https://redirect.github.com/natelindev/tsdav/issues/273">#273</a>
from natelindev/fix/fix-all-issues</li>
<li><a
href="https://github.com/natelindev/tsdav/commit/ad7782b7a0949682283c1ae5f09197b594e62d01"><code>ad7782b</code></a>
fix ci</li>
<li><a
href="https://github.com/natelindev/tsdav/commit/c0275001ba07117092e34b9bb06e7d9ae9b5599e"><code>c027500</code></a>
fix ci</li>
<li><a
href="https://github.com/natelindev/tsdav/commit/2f818c06f871d7467ec38ec50695108a6902f44b"><code>2f818c0</code></a>
chore: prepare v2.1.8 release</li>
<li><a
href="https://github.com/natelindev/tsdav/commit/bfab3ac8eb8ac99eb94f5d7fe01fe681a3140778"><code>bfab3ac</code></a>
fix: prefer global fetch for Cloudflare Workers compatibility (Issue
215)</li>
<li><a
href="https://github.com/natelindev/tsdav/commit/1942d42d9b7d2943b1525713e32ac1a55b0b449d"><code>1942d42</code></a>
fix: support custom fetch override in DAVClient for Electron/CORS (<a
href="https://redirect.github.com/natelindev/tsdav/issues/216">#216</a>)</li>
<li><a
href="https://github.com/natelindev/tsdav/commit/3d52838fcdcccd02ac45605d8ee7626d464e0fee"><code>3d52838</code></a>
docs: add guide for importing iCal feeds (Airbnb, Booking)</li>
<li><a
href="https://github.com/natelindev/tsdav/commit/318b0d34868e56a8031af76b26a8fb7050f74de0"><code>318b0d3</code></a>
fix: ensure collectionQuery throws on gateway errors and status &gt;=
400</li>
<li><a
href="https://github.com/natelindev/tsdav/commit/aa9445a3aa925750bd2db2264bc69e2656a2dbc1"><code>aa9445a</code></a>
feat: support custom fetch override for KaiOS mozSystem support (Issue
220)</li>
<li><a
href="https://github.com/natelindev/tsdav/commit/ce487707805e059b9920bef4767e3788e917cb5d"><code>ce48770</code></a>
fix: address issues with Radicale and CardDAV vCard fetching (Issue
239)</li>
<li>Additional commits viewable in <a
href="https://github.com/natelindev/tsdav/compare/v2.1.5...v2.1.8">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by [GitHub Actions](<a
href="https://www.npmjs.com/~GitHub">https://www.npmjs.com/~GitHub</a>
Actions), a new releaser for tsdav since your current version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tsdav&package-manager=npm_and_yarn&previous-version=2.1.5&new-version=2.1.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-09 06:42:56 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
91b40b391d Bump graphql-sse from 2.5.4 to 2.6.0 (#19477)
Bumps [graphql-sse](https://github.com/enisdenjo/graphql-sse) from 2.5.4
to 2.6.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/enisdenjo/graphql-sse/releases">graphql-sse's
releases</a>.</em></p>
<blockquote>
<h2>v2.6.0</h2>
<h1><a
href="https://github.com/enisdenjo/graphql-sse/compare/v2.5.4...v2.6.0">2.6.0</a>
(2025-10-22)</h1>
<h3>Features</h3>
<ul>
<li><strong>client:</strong> Support dynamic URLs and headers per
request for distinct connection mode (<a
href="https://redirect.github.com/enisdenjo/graphql-sse/issues/124">#124</a>)
(<a
href="https://github.com/enisdenjo/graphql-sse/commit/7be7251b42d31fd55f459ee0b99ee5bfe0d4a1dd">7be7251</a>),
closes <a
href="https://redirect.github.com/enisdenjo/graphql-sse/issues/110">#110</a>
<a
href="https://redirect.github.com/enisdenjo/graphql-sse/issues/113">#113</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/enisdenjo/graphql-sse/blob/master/CHANGELOG.md">graphql-sse's
changelog</a>.</em></p>
<blockquote>
<h1><a
href="https://github.com/enisdenjo/graphql-sse/compare/v2.5.4...v2.6.0">2.6.0</a>
(2025-10-22)</h1>
<h3>Features</h3>
<ul>
<li><strong>client:</strong> Support dynamic URLs and headers per
request for distinct connection mode (<a
href="https://redirect.github.com/enisdenjo/graphql-sse/issues/124">#124</a>)
(<a
href="https://github.com/enisdenjo/graphql-sse/commit/7be7251b42d31fd55f459ee0b99ee5bfe0d4a1dd">7be7251</a>),
closes <a
href="https://redirect.github.com/enisdenjo/graphql-sse/issues/110">#110</a>
<a
href="https://redirect.github.com/enisdenjo/graphql-sse/issues/113">#113</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/enisdenjo/graphql-sse/commit/918b9d414a4f938c715a8f8bc13184c0849eac50"><code>918b9d4</code></a>
chore(release): 🎉 2.6.0 [skip ci]</li>
<li><a
href="https://github.com/enisdenjo/graphql-sse/commit/2320882f6dd8a25d3d36c4218d3bf2215ea0122f"><code>2320882</code></a>
docs(readme): unnecessary badge</li>
<li><a
href="https://github.com/enisdenjo/graphql-sse/commit/d1a1e82aa93ead6267d23aeab7aaf30a02051c01"><code>d1a1e82</code></a>
ci: bump actions</li>
<li><a
href="https://github.com/enisdenjo/graphql-sse/commit/ad13eb1283b353cc11839ef7acc3a16679c4ca55"><code>ad13eb1</code></a>
docs: algolia is not used anymore</li>
<li><a
href="https://github.com/enisdenjo/graphql-sse/commit/7be7251b42d31fd55f459ee0b99ee5bfe0d4a1dd"><code>7be7251</code></a>
feat(client): Support dynamic URLs and headers per request for distinct
conne...</li>
<li>See full diff in <a
href="https://github.com/enisdenjo/graphql-sse/compare/v2.5.4...v2.6.0">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~theguild-bot">theguild-bot</a>, a new
releaser for graphql-sse since your current version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=graphql-sse&package-manager=npm_and_yarn&previous-version=2.5.4&new-version=2.6.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-09 06:40:38 +00:00
c27e3cd1a0 chore: sync AI model catalog from models.dev (#19478)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

This PR updates pricing, context windows, and model availability based
on the latest data.
New models meeting inclusion criteria (tool calling, pricing data,
context limits) are added automatically.
Deprecated models are detected based on cost-efficiency within the same
model family.

**Please review before merging** — verify no critical models were
incorrectly deprecated.

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-04-09 08:28:33 +02:00
ffb6029c2d fix(ai-models): use AWS provider chain for Bedrock IRSA auth (#19470)
## Summary
- `buildBedrockProvider` ignored `authType: "role"` and only honored
static `accessKeyId`/`secretAccessKey` pairs, so deployments relying on
IRSA (e.g. EKS pods with `AWS_WEB_IDENTITY_TOKEN_FILE` + `AWS_ROLE_ARN`)
had no way to authenticate — `@ai-sdk/amazon-bedrock` does not walk the
AWS default credential chain on its own.
- When `authType === 'role'`, wire `fromNodeProviderChain()` from
`@aws-sdk/credential-providers` (already a server dependency, used by
the S3 and logic-function drivers) into `createAmazonBedrock` so IRSA
and other ambient AWS credentials resolve correctly.
- The static-credentials path is unchanged for `authType !== 'role'`.

## Test plan
- [ ] Deploy a server pod with IRSA + an `ai-models-config` entry using
`"authType": "role"` and verify Bedrock calls succeed (no `Could not
load credentials from any providers` error).
- [ ] Verify static-credentials providers (`accessKeyId` +
`secretAccessKey`) still work as before.
- [ ] `npx nx lint:diff-with-main twenty-server` passes.
- [ ] `npx nx typecheck twenty-server` passes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 22:54:35 +02:00
6073bb6706 Fix AI model registry staleness on self-hosted instances (#19427)
## Summary

Fixes #19422.

Self-hosted users hitting "No AI models are available" after configuring
API keys via the admin panel were victims of a stale
`AiModelRegistryService` cache. Only the four `addAiProvider` /
`removeAiProvider` / `addModelToProvider` / `removeModelFromProvider`
mutations called `refreshRegistry()` — setting an API key through
`set/update/deleteDatabaseConfigVariable` left the registry pointing at
the pre-mutation provider state.

Rather than patch each mutation site (and re-introduce the same class of
bug on the next one), the registry now invalidates lazily based on the
LLM config-group hash, mirroring the pattern `WebSearchDriverFactory`
already uses via `DriverFactoryBase`. Any mutation to an LLM-tagged
config variable is picked up automatically on the next read — callers
never have to remember to refresh.

- Extracted `getConfigGroupHash` into a shared util reused by both
`DriverFactoryBase` and `AiModelRegistryService` (and switched the hash
to `JSON.stringify` so object-typed config vars like `AI_PROVIDERS`
actually contribute meaningfully).
- `AiModelRegistryService` gates all internal `Map` access behind
private getters that call `ensureFresh()`, so future read paths can't
accidentally observe stale state. Build path uses underscored backing
fields directly to avoid recursing.
- Dropped the now-redundant `refreshRegistry()` public method and its
four call sites in `admin-panel.resolver.ts`.

## Test plan

- [x] `nx typecheck twenty-server` passes
- [x] Existing admin-panel + ai-models specs pass (79 tests)
- [ ] Manual: on a self-hosted instance, set `OPENAI_API_KEY` (or
another provider key) via the admin panel `setDatabaseConfigVariable`
mutation and confirm AI features work without a server restart
- [ ] Manual: existing flows (`addAiProvider`, `addModelToProvider`,
etc.) still pick up new providers on the next read

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 22:32:05 +02:00
5874fb5f39 i18n - translations (#19467)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-08 18:50:23 +02:00
37dbd94596 i18n - docs translations (#19466)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-08 18:46:25 +02:00
WeikoandGitHub 238018dc7b Reset Tab Page Layout (#19453)
## Context
- Add "Reset to default" for page layout tabs and widgets — backend
mutation resets overrides, reactivates deactivated children, and deletes
custom children
- Fix override write bug where mutating an overridable property (e.g.
widget title) on a standard-app entity incorrectly overwrote the base
column instead of writing to the overrides JSONB —
PageLayoutUpdateService now uses resolveFlatEntityOverridableProperties
for accurate diffing and routes properties through
sanitizeOverridableEntityInput
- Deprecate isOverridden - We need more time to think about this
feature. Currently this adds too much complexity for a very small
benefit



https://github.com/user-attachments/assets/a84546c8-1e15-4d9e-a489-0825cf8b8ed2
2026-04-08 18:43:57 +02:00
Raphaël BosiandGitHub 2267c56f15 [COMMAND MENU ITEMS] Disable hide label toggle for items with no short label (#19464)
<img width="824" height="1140" alt="CleanShot 2026-04-08 at 18 08 31@2x"
src="https://github.com/user-attachments/assets/67989cd7-ac0f-4c28-b128-6db50abecc88"
/>
2026-04-08 16:32:01 +00:00
7aed9291cd fix(ai-chat): preload web_search action tool when driver is enabled (#19461)
## Summary
- When `WEB_SEARCH_DRIVER` is set to a non-native driver (e.g. `EXA`),
the `web_search` action tool was only listed in the tool catalog and had
to be discovered via `learn_tools` before use. Worse,
`system-prompt-builder` explicitly instructed the model **not** to call
`web_search` whenever it wasn't in the preloaded set, so even setting
`EXA_API_KEY` correctly resulted in the model never calling Exa.
- Fix: preload `web_search` from the registry alongside the other action
tools when `shouldUseNativeSearch()` is false, mirroring how native
provider search is already injected into `directTools`. The system
prompt builder picks this up automatically and advertises `web_search`
as a ready-to-use tool.
- Unrelated: pass `isSystemBuild: true` to the messageThread upgrade
command's migration build.

## Test plan
- [ ] With `WEB_SEARCH_DRIVER=EXA` and `EXA_API_KEY` set, ask the chat
agent for current/news information and verify it calls `web_search`
directly (not via `learn_tools` / `execute_tool`) and that Exa is hit.
- [ ] With `WEB_SEARCH_PREFER_NATIVE=true`, verify native provider
search is still used and the action tool is not preloaded.
- [ ] With `WEB_SEARCH_DRIVER=DISABLED`, verify behavior is unchanged
(`shouldUseNativeSearch()` returns true → no Exa preload).
- [ ] Run the 1-21 fix-message-thread-view-and-label-identifier upgrade
command on a workspace and confirm the migration builds/runs as a system
build.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 18:22:27 +02:00
BOHEUSandGitHub 5c867303e0 Fix documentation about importing dates (#19434)
Related to https://github.com/twentyhq/core-team-issues/issues/2364
2026-04-08 14:48:04 +00:00
neo773andGitHub fda3eabb5f introduce MESSAGING_MESSAGES_GET_BATCH_SIZE as config variable (#19455)
makes`MESSAGING_MESSAGES_GET_BATCH_SIZE` configurable for self hosters

/closes #19147
2026-04-08 14:47:04 +00:00
Paul RastoinandGitHub 7d6111b0a5 Fix workspace command name inserted in db (#19458)
# Introduction
Persist in the db the registry computed name that contains
version_classname_timestamp to be consistent with the instance command
pattern too
2026-04-08 14:43:24 +00:00
Thomas TrompetteandGitHub 4df3539ba1 Fix: Staled data on record table after merging records (#19457)
Fix record table not refreshing after merging records

The record table's virtualization state persists across navigations, so
when the table remounts after a merge redirect, none of the existing
reload conditions were satisfied and the table rendered stale data.

Add an isInitializedOnMount local state to
RecordTableVirtualizedInitialDataLoadEffect that guarantees a fresh
triggerInitialRecordTableDataLoad on every mount, acting as a fallback
when no other reload condition matches.
2026-04-08 14:34:09 +00:00
Paul RastoinandGitHub 8905d860c7 Store upgrade commands error message (#19443)
# Introduction
Storing the failing upgrade command formatted message in database.
2026-04-08 13:21:18 +00:00
Charles BochetandGitHub bc7b5aee58 chore: centralize deploy/install CD actions in twentyhq/twenty (#19454)
## Summary

- Adds `deploy-twenty-app` and `install-twenty-app` composite actions to
`.github/actions/` so app repos can reference them remotely — same
pattern as `spawn-twenty-app-dev-test` for CI
- Updates `cd.yml` in template, hello-world, and postcard to use
`twentyhq/twenty/.github/actions/deploy-twenty-app@main` /
`install-twenty-app@main` instead of local `./.github/actions/` copies
- Removes the 6 local action files that were duplicated across template
and example apps

**Before** (each app repo carried its own action copies):
```yaml
uses: ./.github/actions/deploy
```

**After** (centralized, like CI):
```yaml
uses: twentyhq/twenty/.github/actions/deploy-twenty-app@main
```


Made with [Cursor](https://cursor.com)
2026-04-08 15:25:51 +02:00
Raphaël BosiandGitHub 31b6dbc583 [COMMAND MENU ITEMS] Remove object metadata name from search commands (#19435)
Command menu items label become "Search" instead of "Search companies"
or "Search people" since the search is made across all objects.
2026-04-08 13:15:26 +00:00
EtienneandGitHub 3306d66f5b cleaning - remove logs (#19445) 2026-04-08 13:05:47 +00:00
Charles BochetandGitHub 6cd3f2db2b chore: replace spawn-twenty-app-dev-test with native postgres/redis services (#19449)
## Summary

- Replaces the `spawn-twenty-app-dev-test` Docker action with native
GitHub Actions services (`postgres:18` + `redis`) and direct server
startup (`npx nx start:ci twenty-server`)
- Aligns with the pattern already used by `ci-sdk.yaml` for e2e tests
- Removes dependency on the `twenty-app-dev` Docker image for CI

Updated workflows:
- `ci-example-app-postcard`
- `ci-example-app-hello-world`
- `ci-create-app-e2e-minimal`
- `ci-create-app-e2e-hello-world`
- `ci-create-app-e2e-postcard`

Server setup pattern:
1. Postgres 18 + Redis as job services
2. `CREATE DATABASE "test"` via psql
3. `npx nx run twenty-server:database:reset` (migrations + seed)
4. `nohup npx nx start:ci twenty-server &`
5. `npx wait-on http://localhost:3000/healthz`


Made with [Cursor](https://cursor.com)
2026-04-08 13:03:35 +00:00
Thomas des FrancsandGitHub 0e0fb246e6 Refactor the website hero into an interactive multi-view illustration (#19429)
## Summary
- replace the home hero visual with an interactive multi-view experience
for table, kanban, workflow, and dashboard states
- add the supporting hero data model, page normalizers, loaders, chips,
and sales dashboard assets
- update related website illustration components and ignore local Claude
worktrees in `.gitignore`

## Testing
- Not run (not requested)
2026-04-08 13:00:41 +00:00
Abdullah.andGitHub 83917f0dca Isolate illustrations from sections to style them independently. (#19447)
This PR moves illustrations from sections to a separate folder of their
own and accesses which illustration to load on a specific page using a
registry.

The reason for this change is that we want to be able to independently
style each illustration since shared styles being applied to nine
illustrations of ThreeCards, for example, does not get us the eventual
output visually that we're after because the underlying assets have
different lighting, angles etc.

Once we're at a position where we know what can be reused, I will
refactor. For now, Thomas would to take illustration styling to try and
implement what he has in mind, so isolating these files for him to work
without breaking anything.
2026-04-08 12:58:46 +00:00
ac4819758d i18n - translations (#19451)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-08 15:03:32 +02:00
Raphaël BosiandGitHub d07c27a907 [COMMAND MENU ITEMS] Create union type for command menu item payload (#19432)
Replace generic JSON scalar with a typed GraphQL union
CommandMenuItemPayload (PathNavigationPayload |
ObjectMetadataNavigationPayload) for the CommandMenuItem.payload field
2026-04-08 12:47:34 +00:00
265d859c6e i18n - docs translations (#19446)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-08 14:42:19 +02:00
Charles BochetandGitHub 1ae88f4e4f chore: add CD workflow template and point spawn action to main (#19430)
## Summary

- Adds reusable composite GitHub Actions for Twenty app deployment:
- `.github/actions/deploy` — builds and deploys to a remote instance
(`api-url`, `api-key` inputs)
- `.github/actions/install` — installs/upgrades on a specific workspace
(`api-url`, `api-key` inputs)
- Adds a `cd.yml` CD workflow that calls both actions in sequence. The
workflow:
  - Deploys on push to `main`
  - Can be triggered from a PR by adding a `deploy` label
- Configures a named remote via `TWENTY_DEPLOY_URL` env var and
`TWENTY_DEPLOY_API_KEY` secret
- Applied to: `create-twenty-app` template, `postcard` example,
`hello-world` example
- Updates the `spawn-twenty-app-dev-test` action ref from
`@feature/sdk-config-file-source-of-truth` to `@main` in all `ci.yml`
files
2026-04-08 12:31:38 +00:00
Paul RastoinandGitHub 85be463487 Slow instance commands (#19431)
# Introduction
This PR introduces the slow instance commands pattern, that allow
migrating data in prior of the schema migration, that would fail if not.

Slow instance commands runs after the fast instance commands and before
the workspace commands.
On twenty instance that do not has any active or suspended workspace the
data migration part is skipped but the migration still runs, especially
for fresh installs

We were previously hacking through typeorm transaction system to gain
such granularity using save points:
```ts
export class AddPayloadToCommandMenuItem1775129635528
  implements MigrationInterface
{
  name = 'AddPayloadToCommandMenuItem1775129635528';

  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(
      `ALTER TABLE "core"."commandMenuItem" ADD "payload" jsonb`,
    );

    const savepointName =
      'sp_add_payload_check_constraint_to_command_menu_item';

    try {
      await queryRunner.query(`SAVEPOINT ${savepointName}`);

      await addPayloadCheckConstraintToCommandMenuItem(queryRunner);

      await queryRunner.query(`RELEASE SAVEPOINT ${savepointName}`);
    } catch (e) {
      try {
        await queryRunner.query(`ROLLBACK TO SAVEPOINT ${savepointName}`);
        await queryRunner.query(`RELEASE SAVEPOINT ${savepointName}`);
      } catch (rollbackError) {
        // oxlint-disable-next-line no-console
        console.error(
          'Failed to rollback to savepoint in AddPayloadToCommandMenuItem1775129635528',
          rollbackError,
        );
        throw rollbackError;
      }

      // oxlint-disable-next-line no-console
      console.error(
        'Swallowing AddPayloadToCommandMenuItem1775129635528 error',
        e,
      );
    }
  }
```

It was afterwards re-applied within an workspace commands, it was hacky
and missleading for the self host having false positive in logs

## New pattern

Generate the slow instance command
```
npx nx database:migrate:generate twenty-server -- --name add-foo-bar-columns --type slow
```

```ts
import { DataSource, QueryRunner } from 'typeorm';

import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { SlowInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/slow-instance-command.interface';

@RegisteredInstanceCommand('1.21.0', 1775640902366, { type: 'slow' })
export class AddPrastoinColToWorkspaceSlowInstanceCommand implements SlowInstanceCommand {
  async runDataMigration(dataSource: DataSource): Promise<void> {
    // TODO: implement data backfill before the DDL migration
  }

  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query('ALTER TABLE "core"."workspace" ADD "prastoin" character varying');
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query('ALTER TABLE "core"."workspace" DROP COLUMN "prastoin"');
  }
}
```

### Why `run-instance-commands` and `upgrade` remain separate commands

These two commands serve fundamentally different purposes with
incompatible scoping semantics:
The run-instance-commands iterates over all the legacy typeorm and the
instance commands of all versions, used for database init and so on. we
could be centralizing both but the readability tradeoff isn't worth it

In the future thanks to the cross-version pattern we will be able to
centralize them but not right now

Please note that by default the `run-instance-commands` only run the
fast instance commands which is expected for our cloud prod CD
2026-04-08 12:09:29 +00:00
BugIsGodandGitHub 8a84e32cf6 fix(ai): use @ai-sdk/openai-compatible for third-party providers (#19438)
## Summary:

I found a bug: 404 call when I use third-party providers (I used
deepseek). I found the final request url is:
`https://api.deepseek.com/v1/responses' `if we use createOpenAI. But the
correct one should be `https://api.provider.com/v1/chat/completions` for
the thirty provider. So I replace createOpenAI with
createOpenAICompatible in the openai-compatible provider path.

**Reproduction**: Add DeepSeek as a new AI provider (deepseek-chat,
deepseek-reasoner)

Also check the source code in Open Code project, they also use
createOpenAICompatible for the @ai-sdk/openai-compatible scenario
<img width="703" height="369" alt="image"
src="https://github.com/user-attachments/assets/90c4f924-6f1a-4fe7-821b-f13ee86a7a39"
/>


official docs: https://ai-sdk.dev/providers/openai-compatible-providers
https://ai-sdk.dev/providers/ai-sdk-providers/openai

<img width="860" height="293" alt="image"
src="https://github.com/user-attachments/assets/283b9b91-f1d6-4b1c-bb91-16ddccdff8b4"
/>



## Before
<img width="473" height="750" alt="deepseek before"
src="https://github.com/user-attachments/assets/f6b89294-1fa7-4ddc-a6a8-d396070caaca"
/>

## After

<img width="468" height="753" alt="deepseek after"
src="https://github.com/user-attachments/assets/0d170b70-e829-4a4c-abad-38879427c2df"
/>

## Backend error log

```json
[1] [Nest] 50907  - 07/04/2026, 23:48:57     LOG [ChatExecutionService] Starting chat execution with model deepseek/deepseek-chat, 4 active tools
[1] APICallError [AI_APICallError]: Not Found
[1]     at /Users/abel/Documents/Code/twenty/node_modules/@ai-sdk/provider-utils/dist/index.js:2512:14
[1]     at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
[1]     at async postToApi (/Users/abel/Documents/Code/twenty/node_modules/@ai-sdk/provider-utils/dist/index.js:2373:28)
[1]     at async OpenAIResponsesLanguageModel.doStream (/Users/abel/Documents/Code/twenty/node_modules/@ai-sdk/openai/dist/index.js:4925:50)
[1]     at async fn (/Users/abel/Documents/Code/twenty/node_modules/ai/dist/index.js:7106:27)
[1]     at async /Users/abel/Documents/Code/twenty/node_modules/ai/dist/index.js:2340:24
[1]     at async _retryWithExponentialBackoff (/Users/abel/Documents/Code/twenty/node_modules/ai/dist/index.js:2599:12)
[1]     at async streamStep (/Users/abel/Documents/Code/twenty/node_modules/ai/dist/index.js:7063:17)
[1]     at async fn (/Users/abel/Documents/Code/twenty/node_modules/ai/dist/index.js:7455:9)
[1]     at async /Users/abel/Documents/Code/twenty/node_modules/ai/dist/index.js:2340:24 {
[1]   cause: undefined,
[1]   url: 'https://api.deepseek.com/v1/responses',
[1]   requestBodyValues: {
[1]     model: 'deepseek-chat',
[1]     input: [ [Object], [Object] ],
[1]     temperature: undefined,
[1]     top_p: undefined,
[1]     max_output_tokens: undefined,
[1]     conversation: undefined,
[1]     max_tool_calls: undefined,
[1]     metadata: undefined,
[1]     parallel_tool_calls: undefined,
[1]     previous_response_id: undefined,
[1]     store: undefined,
[1]     user: undefined,
[1]     instructions: undefined,
[1]     service_tier: undefined,
[1]     include: undefined,
[1]     prompt_cache_key: undefined,
[1]     prompt_cache_retention: undefined,
[1]     safety_identifier: undefined,
[1]     top_logprobs: undefined,
[1]     truncation: undefined,
[1]     tools: [ [Object], [Object], [Object], [Object] ],
[1]     tool_choice: 'auto',
[1]     stream: true
[1]   },
[1]   statusCode: 404,
[1]   responseHeaders: {
[1]     'access-control-allow-credentials': 'true',
[1]     connection: 'keep-alive',
[1]     'content-length': '0',
[1]     date: 'Tue, 07 Apr 2026 22:48:57 GMT',
[1]     server: 'elb',
[1]     'strict-transport-security': 'max-age=31536000; includeSubDomains; preload',
[1]     vary: 'origin, access-control-request-method, access-control-request-headers',
[1]     via: '1.1 83867089cd39052cd05f9e04909bedde.cloudfront.net (CloudFront)',
[1]     'x-amz-cf-id': 'O4b0VTi9Q1VVmTmq6czGlEWst7IPnAQl544hB7uIvfnSphBvUKbZTw==',
[1]     'x-amz-cf-pop': 'DUB56-P3',
[1]     'x-cache': 'Error from cloudfront',
[1]     'x-content-type-options': 'nosniff',
[1]     'x-ds-trace-id': 'a90e91809d285170338ef077f67ae2be'
[1]   },
[1]   responseBody: '',
[1]   isRetryable: false,
[1]   data: undefined,
[1]   Symbol(vercel.ai.error): true,
[1]   Symbol(vercel.ai.error.AI_APICallError): true
[1] }
[1] Exception Captured
[1]   undefined
[1]   [
[1]     NoOutputGeneratedError [AI_NoOutputGeneratedError]: No output generated. Check the stream for errors.
[1]         at Object.flush (/Users/abel/Documents/Code/twenty/node_modules/ai/dist/index.js:6656:103)
[1]         at invokePromiseCallback (node:internal/webstreams/util:172:10)
[1]         at Object.<anonymous> (node:internal/webstreams/util:177:23)
[1]         at transformStreamDefaultSinkCloseAlgorithm (node:internal/webstreams/transformstream:621:43)
[1]         at node:internal/webstreams/transformstream:379:11
[1]         at writableStreamDefaultControllerProcessClose (node:internal/webstreams/writablestream:1162:28)
[1]         at writableStreamDefaultControllerAdvanceQueueIfNeeded (node:internal/webstreams/writablestream:1253:5)
[1]         at writableStreamDefaultControllerClose (node:internal/webstreams/writablestream:1220:3)
[1]         at writableStreamClose (node:internal/webstreams/writablestream:722:3)
[1]         at writableStreamDefaultWriterClose (node:internal/webstreams/writablestream:1091:10) {
[1]       cause: undefined,
[1]       Symbol(vercel.ai.error): true,
[1]       Symbol(vercel.ai.error.AI_NoOutputGeneratedError): true
[1]     }
[1]   ]
[1] [Nest] 50907  - 07/04/2026, 23:48:59     LOG [BullMQDriver] Job 24 with name StreamAgentChatJob processed on queue ai-stream-queue in 2756.63ms
2026-04-08 12:01:49 +00:00
WeikoandGitHub 8aa208fc93 Fix overridable entities logic for SSE (#19433)
## Context
SSE metadata events for overridable entities (viewField, viewFieldGroup,
pageLayoutWidget, pageLayoutTab) were sending raw flat entity data
without resolving overrides into base properties or filtering isActive:
false entities. This caused the frontend to display stale/incorrect
values (e.g., a hidden viewField still appearing visible).

## Implementation
Add a sanitization step in
MetadataEventPublisher.enrichMetadataEventBatch that resolves overrides
and converts isActive transitions into the appropriate event types
(deactivated -> delete, reactivated -> create), matching what GraphQL
resolvers already do
2026-04-08 11:53:55 +00:00
Thomas TrompetteandGitHub 9a58f3d459 Add a lock on function creation (#19428)
Fixes
https://twenty-v7.sentry.io/issues/7368127776/?environment=prod&project=4507072499810304&query=is%3Aunresolved&referrer=issue-stream

- Add a distributed lock (via CacheLockService) around the Lambda
executor build in LambdaDriver to prevent concurrent workflow runs from
racing on CreateFunctionCommand for the same logic function
- Use double-checked locking: isAlreadyBuilt is checked lock-free first
(fast path), then re-checked inside the lock to avoid redundant rebuilds
2026-04-08 11:50:57 +00:00
Baptiste DevessierandGitHub 9d96e529c8 Fix last widget bottom bar inconsistencies (#19440)
Widgets weren't sorted
2026-04-08 11:29:11 +00:00
Baptiste DevessierandGitHub e9310555fb Fix tab selection in layout edit mode (#19436)
https://github.com/user-attachments/assets/20e4a506-5f22-4017-8300-a494b70e8d51
2026-04-08 11:12:46 +00:00
8090fa4364 i18n - docs translations (#19437)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-08 12:40:41 +02:00
martmullandGitHub 90de1d4a34 Add twenty sync command (#19413)
Add one shot app synchronisation `twenty sync command` command

Complementary with `twenty app dev` command which is watch mode

Fixes
https://discord.com/channels/1130383047699738754/1489644493106839663
2026-04-08 09:26:42 +00:00
8026451220 chore: sync AI model catalog from models.dev (#19426)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

This PR updates pricing, context windows, and model availability based
on the latest data.
New models meeting inclusion criteria (tool calling, pricing data,
context limits) are added automatically.
Deprecated models are detected based on cost-efficiency within the same
model family.

**Please review before merging** — verify no critical models were
incorrectly deprecated.

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-04-08 08:27:14 +02:00
Arturo MantinettiandGitHub b3d46b0fa3 feat: Add support for CLF currency code (#19420)
## Summary
This PR adds support for the **CLF (Unidad de Fomento)** currency code
across the application.

## Changes
- Added `CLF` to the supported currency list
- Updated validation logic to recognize CLF as a valid currency
- Adjusted formatting and handling where applicable

## Motivation
CLF is a widely used unit of account in Chile, commonly used for
financial operations such as real estate, contracts, and indexed
payments.
Supporting CLF improves localization and enables better adoption of
Twenty CRM in the Chilean market.

## Files Modified
- Updated 3 files to integrate CLF support (currency configuration,
validation, and related logic)

## Testing
- Verified that CLF can be selected and processed correctly
- Confirmed no regression in existing currency behavior
2026-04-08 05:10:36 +00:00
a8085db5fd i18n - translations (#19425)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-08 07:09:03 +02:00
b540ae9735 i18n - translations (#19424)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-08 07:05:51 +02:00
neo773andGitHub 20adb86917 fix: IMAP skip no-select flag folders properly (#19402)
Tested with a dovecot server running in Docker with synthetic seed

<img width="546" height="54" alt="image"
src="https://github.com/user-attachments/assets/81cbeae6-9cb6-406b-846a-209af403385f"
/>


/closes #19090
2026-04-08 04:54:01 +00:00
martmullandGitHub c91d642f29 App feedbacks front (#19417)
## Before
<img width="777" height="284" alt="image"
src="https://github.com/user-attachments/assets/22a8c318-ed7c-4ec5-b7a8-a688098ec103"
/>


## After
<img width="897" height="329" alt="image"
src="https://github.com/user-attachments/assets/16ad78bf-a449-4f25-99f6-13c35e448f1b"
/>
2026-04-08 04:51:17 +00:00
Charles BochetandGitHub 15eb3e7edc feat(sdk): use config file as single source of truth, remove env var fallbacks (#19409)
## Summary

- **Config as source of truth**: `~/.twenty/config.json` is now the
single source of truth for SDK authentication — env var fallbacks have
been removed from the config resolution chain.
- **Test instance support**: `twenty server start --test` spins up a
dedicated Docker instance on port 2021 with its own config
(`config.test.json`), so integration tests don't interfere with the dev
environment.
- **API key auth for marketplace**: Removed `UserAuthGuard` from
`MarketplaceResolver` so API key tokens (workspace-scoped) can call
`installMarketplaceApp`.
- **CI for example apps**: Added monorepo CI workflows for `hello-world`
and `postcard` example apps to catch regressions.
- **Simplified CI**: All `ci-create-app-e2e` and example app workflows
now use a shared `spawn-twenty-app-dev-test` action (Docker-based)
instead of building the server from source. Consolidated auth env vars
to `TWENTY_API_URL` + `TWENTY_API_KEY`.
- **Template publishing fix**: `create-twenty-app` template now
correctly preserves `.github/` and `.gitignore` through npm publish
(stored without leading dot, renamed after copy).

## Test plan

- [x] CI SDK (lint, typecheck, unit, integration, e2e) — all green
- [x] CI Example App Hello World — green
- [x] CI Example App Postcard — green
- [x] CI Create App E2E minimal — green
- [x] CI Front, CI Server, CI Shared — green
2026-04-08 06:49:10 +02:00
af3423dc6f i18n - translations (#19421)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-08 03:42:20 +02:00
Abdul RahmanandGitHub a0484f686a Add color to object icon picker in data model (#19368)
Closes [#2291](https://github.com/twentyhq/core-team-issues/issues/2291)
2026-04-08 01:27:08 +00:00
martmullandGitHub 5c8b6e395b App feedbacks front (#19416)
- Removes agents from app settings
https://discord.com/channels/1130383047699738754/1488500960656490618
- fix public assets not properly pack in deploy and build commands
-
2026-04-07 19:49:02 +00:00
9bdef449e6 Fix messageThread view and labelIdentifier on legacy workspaces (#19414)
## Summary
Adds `upgrade:1-21:fix-message-thread-view-and-label-identifier` to
retroactively apply two messageThread changes from #19351 that never
propagated to existing workspaces (because
`synchronizeTwentyStandardApplicationOrThrow` only runs at workspace
creation):

1. **`allMessageThreads` view fields**: delete-and-recreate all view
fields for the standard view from the current twenty-standard
definition, which adds the new `subject` and `updatedAt` columns. Same
pattern as the FIELDS_WIDGET sync in
`1-21-workspace-command-1775500005000-backfill-page-layouts-and-fields-widget-view-fields`.
2. **`messageThread.labelIdentifierFieldMetadataId`**: repointed to the
`subject` field via `validateBuildAndRunWorkspaceMigration`. The
migration runner already resolves
`labelIdentifierFieldMetadataUniversalIdentifier` → id in
`update-object-action-handler`, so this goes through the standard flow
and properly invalidates caches.

Both fixes share a single `validateBuildAndRunWorkspaceMigration` call
(one `viewField` op, one `objectMetadata` update op). Idempotent — skips
if nothing to do.

Depends on `upgrade:1-21:backfill-message-thread-subject` having run
first (the label identifier fix needs the `subject` field to exist; it
logs a warning and skips that part otherwise).

## Test plan
- [ ] Legacy workspace: run \`backfill-message-thread-subject\`, then
this command, then verify:
- the \`allMessageThreads\` view shows the new \`subject\` and
\`updatedAt\` columns
  - messageThread records display their \`subject\` as the record label
- [ ] Re-run the command: no-op, logs \`Nothing to fix\`
- [ ] \`--dry-run\` logs planned changes without applying them

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 21:57:35 +02:00
Baptiste DevessierandGitHub e2bd3e8ef4 Unselect widget when exiting layout customization mode (#19411) 2026-04-07 17:26:07 +00:00
Paul RastoinandGitHub c0eacedfec Workspace command decorators (#19397)
# Introduction
Migrating the workspace commands to the decorator version + timestamp
listing as for the instance commands

We've now been able to remove the upgrade command abstraction where we
needed to import all modules and order them
Now they're dynamically retrieved at upgrade runtime, sorted by
timestamp

## Instance and workspace commands name
The name is computed from the command metadata `version` `className` and
`timestamp` we have a duplicate validation at module init from the
unified registry
2026-04-07 16:33:48 +00:00
24a5273a79 i18n - docs translations (#19410)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-07 18:36:49 +02:00
Raphaël BosiandGitHub 607e708670 [COMMAND MENU ITEMS] Add navigate to settings pages commands (#19408)
Add commands to navigate to every settings page.
2026-04-07 16:06:40 +00:00
61abfd103d fix TextVariableEditor layout and add support for multi line paste (#18721)
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-07 15:58:42 +00:00
neo773andGitHub 54be3b7e87 docs: remove reference to sync metadata (#19400) 2026-04-07 15:55:23 +00:00
Paul RastoinandGitHub 137e068ced Fix yarn-install-lambda and lambda-build target (#19407)
# Size issue
```
Yarn install Lambda failed: {"errorType":"Error","errorMessage":"yarn install failed: ➤ Y/tmp/3bac8baafe6354db414389434726b02c/nodejs/node_modules/tar ENOSPC: no space left on device, write","➤ YN0000: └ Completed in 3s 57ms","➤ YN0000: · Failed with errors in 11s 684ms",""," at runYarnInstall (file:///var/task/index.mjs:48:11)"," at process.processTicksAndRejections (node:internal/process/task_queues:105:5)"," at async Runtime.handler (file:///var/task/index.mjs:119:5)"]}
```

# target esnext
setting the same target for each logic function build funnels
- sdk
- local driver
- lambda driver
2026-04-07 15:53:31 +00:00
91f8f7329a improve pricing card header [website] (#19385)
## Summary
- Reduce the plan title size from `md` to `xs`
- Switch the pricing card header layout from grid to flex for tighter
title/price control
- Tighten the title line-height and add a `black/60` color override for
the `/month...` suffix
- Add a 4px gap between the price amount and suffix
- Reduce the illustration height to 80px and shift it slightly right on
desktop

## Testing
- Not run (not requested)

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-07 15:02:48 +00:00
Raphaël BosiandGitHub d341d0d624 Refactor navigation commands to use NAVIGATION engine key with payload (#19303)
- Adds a payload JSON column to `CommandMenuItem` and introduces a
unified `NAVIGATION` engine component key that replaces all individual
GO_TO_* keys
- Navigation commands now use the payload to determine their target
(either an objectMetadataItemId or a path), making navigation commands
dynamic and eliminating the need for a hardcoded engine key per object
- Includes a 1.21 upgrade command (refactor-navigation-commands) that
migrates existing GO_TO_* items to NAVIGATION items with the appropriate
payload, and applies a CHECK constraint enforcing payload coherence



https://github.com/user-attachments/assets/4d305ba2-ae0b-4556-bb0e-e9d899777350


TODO: In a second PR, create the sync between object metadata items and
the navigation command menu items
- Object metadata item created or enabled -> Create navigation command
- Object metadata item deleted or disabled -> Delete associated
navigation command

In another PR:
- Allow `label`, `shortLabel` and `icon` to resolve the
`navigateToObjectMetadataItem` dynamically in their interpolation
instead of being hardcoded in the command menu item
- Make the icon dynamic in the command menu items as the label so that
we can resolve ${navigateToObjectMetadataItem.icon} at runTime -> This
way we won't need to keep update the command menu item icon when we
update the objectMetadataItem icon
2026-04-07 15:00:04 +00:00
5e9792009f i18n - docs translations (#19405)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-07 16:51:49 +02:00
Abdullah.andGitHub 38802bd0b8 Style the remaining website visuals. (#19401)
This PR styles the remaining website visuals. First implementation.
2026-04-07 14:33:54 +00:00
e692e97428 i18n - translations (#19404)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-07 16:37:15 +02:00
Baptiste DevessierandGitHub 1f3965e5f8 Add Manage and Placement sections in widget side panel page for record page layouts (#19310)
https://github.com/user-attachments/assets/f6120c2e-95e7-4b9b-abb5-69a10c3f2f3b
2026-04-07 14:21:35 +00:00
Thomas TrompetteandGitHub e048d03872 Improve workflow crons efficiency (#19381)
**Optimize workflow cron jobs: partition workspaces and use raw
queries**

- Split all 3 workflow cron jobs (WorkflowRunEnqueueCronJob,
WorkflowHandleStaledRunsCronJob, WorkflowCleanWorkflowRunsCronJob) to
process only 1/10th of workspaces per invocation using minute-based
partitioning, reducing per-run load
- Replace ORM repository + workspace context loading with raw SQL
queries in WorkflowRunEnqueueCronJob and
WorkflowHandleStaledRunsCronJob, avoiding costly cache/metadata
hydration for a simple existence check
2026-04-07 14:09:11 +00:00
653180d10f i18n - translations (#19403)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-07 16:10:37 +02:00
martmullandGitHub 8702300b07 App feedbacks fix option id required in apps (#19386)
fixes
https://discord.com/channels/1130383047699738754/1488226371032453292
2026-04-07 13:53:16 +00:00
WeikoandGitHub 6e23ca35e6 Pagelayout backfill command standard app (#19380)
## Context
Due to the chosen strategy for "Reset to default" feature for page
layouts. Those overridable entities need to be associated to the
Standard app to work properly (there is no "Default" state for custom
entities). Until we find a better implementation, I'm changing the
backfill command to reflect that
2026-04-07 12:49:35 +00:00
96a242eb7d fix(messaging): create messageThread.subject field metadata + column in 1-21 backfill (#19394)
## Summary
- The 1-21 \`upgrade:1-21:backfill-message-thread-subject\` command
assumed the legacy \`sync-metadata\` flow would create the new
\`messageThread.subject\` field metadata and column on existing
workspaces. That sync was removed, so the column was never added and the
backfill silently skipped.
- The command now ensures the field exists by computing the standard
\`messageThread.subject\` flat field from the twenty-standard
application and running it through
\`WorkspaceMigrationValidateBuildAndRunService\` (same pattern used by
the page-layout / command-menu-item backfills). This creates both the
field metadata row and the workspace schema column.
- After ensuring the field, the existing \`UPDATE messageThread SET
subject = ...\` runs as before.

## Test plan
- [ ] On a workspace with no \`subject\` column on \`messageThread\`,
run \`yarn command:prod upgrade:1-21:backfill-message-thread-subject\`
and confirm:
  - the field metadata row is created in \`core.\"fieldMetadata\"\`
- the \`subject\` column is created on \`workspace_<id>.messageThread\`
  - existing message threads are backfilled from the most recent message
- [ ] Re-run on the same workspace and confirm it is a no-op (field
already exists, no rows to update)
- [ ] Run on a workspace that already has the column but \`NULL\`
subjects and confirm only the backfill runs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 14:42:20 +02:00
d97a1cfc27 i18n - docs translations (#19399)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-07 14:39:30 +02:00
c844109ffc i18n - translations (#19398)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-07 14:35:24 +02:00
neo773andGitHub dd2a09576b Imap-smtp-caldav form fixes (#19392)
/closes #19273
2026-04-07 12:18:11 +00:00
martmullandGitHub fe07de63b0 Enterprise plan required for private app sharing (#19393)
## before

<img width="1512" height="696" alt="image"
src="https://github.com/user-attachments/assets/d57f398e-6ac3-4ce0-a54b-a23ae41b1890"
/>


## after

<img width="923" height="448" alt="image"
src="https://github.com/user-attachments/assets/f4fea42e-1019-4287-9302-42da143ee878"
/>
2026-04-07 12:17:59 +00:00
Paul RastoinandGitHub 1c9fc94c1f Workspace commands writes inupgradeMigration (#19379)
# Introduction
As for the instance commands we want to keep a track of what has been
run for the workspace commands
Note that the history will be updated only when the workspace command
has been run through the upgrade directly and not when run atomically

## What's next
Later we will use this history in order to determine the current
workspace's version and instance's version getting rid of the version in
database, that will be the last stone
2026-04-07 12:15:06 +00:00
d10ef156c1 i18n - translations (#19395)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-07 14:17:20 +02:00
nitinandGitHub d3ec64072b [AI] Improve AI System Prompt page layout and token display (#19391)
closes
https://discord.com/channels/1130383047699738754/1480982048050249900

<img width="1438" height="1315" alt="CleanShot 2026-04-07 at 17 02 09"
src="https://github.com/user-attachments/assets/43c5a16f-6dd0-49da-8138-a11a7476e612"
/>
2026-04-07 11:52:48 +00:00
EtienneandGitHub 1b14e7e1f1 Fix - Update package.json (#19390)
https://github.com/twentyhq/twenty/pull/19383#discussion_r3044330450
2026-04-07 11:44:23 +00:00
neo773andGitHub 6ad9566043 Add messaging upgrade command 1 21 (#19389)
```
[Nest] 50057  - 04/07/2026, 4:14:10 PM     LOG [WorkspaceIteratorService] Running on workspace 20202020-1c25-4d02-bf25-6aeccf7ea419 1/2
[Nest] 50057  - 04/07/2026, 4:14:10 PM     LOG [MigrateMessagingInfrastructureToMetadataCommand] Migrated 5 connected accounts for workspace 20202020-1c25-4d02-bf25-6aeccf7ea419
[Nest] 50057  - 04/07/2026, 4:14:10 PM     LOG [MigrateMessagingInfrastructureToMetadataCommand] Migrated 6 message channels for workspace 20202020-1c25-4d02-bf25-6aeccf7ea419
[Nest] 50057  - 04/07/2026, 4:14:10 PM     LOG [MigrateMessagingInfrastructureToMetadataCommand] Migrated 6 calendar channels for workspace 20202020-1c25-4d02-bf25-6aeccf7ea419
[Nest] 50057  - 04/07/2026, 4:14:10 PM     LOG [MigrateMessagingInfrastructureToMetadataCommand] Migrated 5 message folders for workspace 20202020-1c25-4d02-bf25-6aeccf7ea419
[Nest] 50057  - 04/07/2026, 4:14:10 PM     LOG [WorkspaceIteratorService] Running on workspace 3b8e6458-5fc1-4e63-8563-008ccddaa6db 2/2
[Nest] 50057  - 04/07/2026, 4:14:10 PM     LOG [MigrateMessagingInfrastructureToMetadataCommand] Migrated 5 connected accounts for workspace 3b8e6458-5fc1-4e63-8563-008ccddaa6db
[Nest] 50057  - 04/07/2026, 4:14:11 PM     LOG [MigrateMessagingInfrastructureToMetadataCommand] Migrated 4 message channels for workspace 3b8e6458-5fc1-4e63-8563-008ccddaa6db
[Nest] 50057  - 04/07/2026, 4:14:11 PM     LOG [MigrateMessagingInfrastructureToMetadataCommand] Migrated 4 calendar channels for workspace 3b8e6458-5fc1-4e63-8563-008ccddaa6db
[Nest] 50057  - 04/07/2026, 4:14:11 PM     LOG [MigrateMessagingInfrastructureToMetadataCommand] Migrated 10 message folders for workspace 3b8e6458-5fc1-4e63-8563-008ccddaa6db
[Nest] 50057  - 04/07/2026, 4:14:11 PM     LOG [MigrateMessagingInfrastructureToMetadataCommand] Command completed!
```
2026-04-07 11:21:27 +00:00
Félix MalfaitandGitHub f39fccc3c4 fix(messaging): split thread create/update statements and stop clobbering accumulator (#19388)
## Summary

Fixes two bugs in
`MessagingMessageService.saveMessagesWithinTransaction`.

### Bug 1 — `ON CONFLICT DO UPDATE command cannot affect row a second
time`

Reported in production logs from `MessagingMessagesImportService`.
Postgres rejects a single `INSERT … ON CONFLICT DO UPDATE` when the same
conflict target appears twice in the values list, and that's exactly
what was happening to `messageThread`.

Where it came from: #19351 added the message-thread subject refresh
feature and, in doing so, switched the existing thread `insert` to a
bulk `upsert(['id'])` over a list built by concatenating two sources:

```ts
const threadsToUpsert = [
  ...messageThreadsToCreate,        // brand-new thread rows
  ...threadSubjectUpdates entries,  // subject refreshes for existing threads
];
await messageThreadRepository.upsert(threadsToUpsert, ['id'], txManager);
```

Each list is internally unique, but they are **not disjoint**.
`enrichMessageAccumulatorWithMessageThreadToCreate`, when it sees two
messages in the same batch sharing a brand-new thread external id,
copies the first sibling's freshly-minted thread id into the second
sibling's `existingThreadInDB`. The subject-update gate later in the
loop then trusts that field and queues a subject refresh for that id —
which is also already in `messageThreadsToCreate`. Same id, same
statement, two rows → Postgres aborts the transaction and the import
retries forever on the same batch.

**Fix:** stop merging the two lists. Issue creates and subject updates
as two separate statements within the same transaction:

```ts
if (messageThreadsToCreate.length > 0) {
  await messageThreadRepository.insert(messageThreadsToCreate, txManager);
}
if (threadSubjectUpdates.size > 0) {
  await messageThreadRepository.upsert(
    Array.from(threadSubjectUpdates.entries()).map(([id, { subject }]) => ({ id, subject })),
    ['id'],
    txManager,
  );
}
```

This is closer to the pre-#19351 shape (`insert` for new rows) and
side-steps the duplicate-row constraint entirely: each statement is
internally unique (creates use freshly minted UUIDs; updates are keyed
by a `Map<id, …>`), and within the same transaction Postgres happily
applies a subject update to a row inserted by a previous statement.

### Bug 2 —
`enrichMessageAccumulatorWithExistingMessageChannelMessageAssociations`
clobbers the accumulator

Independent latent bug spotted while tracing the flow. The helper did:

```ts
if (existingMessageChannelMessageAssociation) {
  messageAccumulatorMap.set(message.externalId, {
    existingMessageInDB: existingMessage,
    existingMessageChannelMessageAssociationInDB: existingMessageChannelMessageAssociation,
  });
}
```

i.e. it **replaces** the accumulator object, dropping the
`existingThreadInDB` set just before by
`enrichMessageAccumulatorWithExistingMessageThreadIds`.

The branch only fires when re-encountering a message that's already been
fully synced on this channel (matched on `headerMessageId` AND already
has an association row) — i.e. routinely on Gmail/IMAP incremental syncs
whenever the connector re-delivers an existing message (label change,
read/unread, archive, full-resync after error, …).

When it fires, the next enrichment step sees `existingThreadInDB` as
`undefined`, falls into the "create a new thread" branch, mints a fresh
`threadToCreate`, but the main loop never queues a `messageToCreate` or
association for it (because both `existingMessageInDB` and the existing
association are still set). Net effect: **one orphan `messageThread` row
inserted per re-encountered message, with nothing referencing it.**

The existing message in the DB keeps pointing at its real thread, so
this is invisible to users — no thread fragmentation, no UI symptoms, no
error logs. Just slow accumulation of orphan thread rows that no query
joins onto. Probably worth running

```sql
SELECT COUNT(*)
FROM "messageThread" mt
WHERE NOT EXISTS (
  SELECT 1 FROM message m WHERE m."messageThreadId" = mt.id
);
```

on a busy production workspace once this lands to size whether a cleanup
migration is warranted.

**Fix:** mutate the existing accumulator in place instead of replacing
it.

## Test plan

- [x] `oxlint --type-aware` clean on touched file
- [x] `prettier` clean

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-04-07 13:23:31 +02:00
EtienneandGitHub ac1ec91f25 Direct execution - Remove conditional schema (#19383) 2026-04-07 10:22:56 +00:00
Abdullah.andGitHub d324bbfc25 Styled illustrations for Hero, ThreeCards, Helped sections. (#19387)
This PR adds styles and animations to illustrations for Hero, ThreeCards
and Helped sections.
2026-04-07 10:17:32 +00:00
7c2a9abed4 fix: prevent NaN in health indicator calculations (#19378)
## Summary

Fixes #19377

- **Redis health**: The hit rate calculation divides by zero when both
`keyspace_hits` and `keyspace_misses` are `"0"` (common on fresh
instances). The string `"0"` is truthy so the guard
`statsData.keyspace_hits ? ...` doesn't catch this case, resulting in
`0/0 = NaN`. Fixed by computing the total first and checking it's a
valid non-zero number.
- **Database health**: The cache hit ratio query returns `null` when
`pg_statio_user_tables` is empty (no user tables). `parseFloat(null)` →
`NaN`. Fixed by adding a null check.

## Test plan

- [ ] Verify health indicators display correctly on a fresh instance
with no Redis keyspace activity
- [ ] Verify health indicators display correctly on a database with no
user tables
- [ ] Existing tests in `redis.health.spec.ts` still pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: easonysliu <easonysliu@tencent.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-07 10:01:49 +00:00
c0c0cbb896 i18n - translations (#19382)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-07 11:50:21 +02:00
WeikoandGitHub a2188cb0eb Reset Fields widget implementation (#19283)
## Context
This PR implements the first steps for overridable entities resets.

<img width="1277" height="568" alt="Screenshot 2026-04-02 at 18 58 04"
src="https://github.com/user-attachments/assets/4c7f93b1-c453-4905-a919-cd6af11e0e16"
/>
2026-04-07 09:34:44 +00:00
Paul RastoinandGitHub 23874848a4 Instance commands and upgrade_migrations table (#19356)
# Introduction
Now only using typeorm to generate migrations up and down statement
We handle and maintain our own migration table history

## What's new
Now all the instance commands will live within the same module and
folder than the upgrade commands
Sequentiality comes from the timestamp located in the filename
Same sequentiality also applies to the workspace commands in the future,
for the moment still expected a as code explicit declaration

( below screen is an example see below section )
<img width="1382" height="634" alt="image"
src="https://github.com/user-attachments/assets/5610a246-4eae-485e-99f4-98fb89ad5ac8"
/>

## Existing 1.21 migrations
We won't start following this pattern in 1.21 yet at least not with the
migration that has already been released as typeorm migrations in cloud
production as they would rerun


## Small duplication
Duplicating the legacy typeorm and instance commands run in the
`run-instance-commands` to avoid any merge of interest for the moment

## Concurrency
Not handling any run in parrallel of the upgrade for the moment
2026-04-07 08:55:17 +00:00
b23d2b4e73 messaging post migration cleanup (#19365)
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-07 10:56:49 +02:00
Paul RastoinandGitHub 601dc02ed7 Fix s3 driver empty objects (#19361)
`undefined === 0` breaks the early return
2026-04-07 08:20:56 +00:00
Abdullah.andGitHub 2d552fc9fd Remove hover and scroll transitions from website. (#19369)
This PR removes hover and scroll transitions from the website. 

Per the conversation with Thomas, we should introduce them one by one
for each section.
2026-04-07 07:58:40 +00:00
f33ad53e72 chore: remove registeredCoreMigration (#19376)
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-04-07 10:02:07 +02:00
7053e1bbc5 fix: bypass permission checks in 1.21 backfill-message-thread-subject command (#19375)
## Summary
- `upgrade:1-21:backfill-message-thread-subject` was failing on every
workspace with `Method not allowed because permissions are not
implemented at datasource level`.
- The global workspace datasource gates raw `query()` calls behind
`shouldBypassPermissionChecks`. Both the column-existence probe and the
UPDATE in this command now pass that flag, matching the pattern used by
the other 1.20/1.21 upgrade commands.

## Test plan
- [ ] Re-run `yarn command:prod
upgrade:1-21:backfill-message-thread-subject` and confirm all workspaces
complete without the permissions error
- [ ] Spot-check a workspace to confirm `messageThread.subject` is
backfilled from the most recent message

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 09:59:30 +02:00
955aa9191f fix: unify settings layout prep when entering settings from outside (#19373)
## Summary

Clicking **Compose** in the emails tab without a connected account
redirects to the New Account settings page, but the settings nav drawer
was left in its previous (collapsed / "main") state — producing a
visibly half-broken transition.

### Root cause

The "enter settings" preparation (memorize previous URL + drawer state,
expand the desktop drawer, switch the mobile drawer to `'settings'`) was
duplicated **inline in three different places**:
- `NavigationDrawerOtherSection.handleSettingsClick`
- `MultiWorkspaceDropdownDefaultComponents` Settings link
- Implicitly expected (but missing) from every `useNavigateSettings`
caller

Every other entry point — `ComposeEmailButton`, `ComposeEmailCommand`,
`AIChatCreditsExhaustedMessage`, several workflow/role components — just
called `navigateSettings(...)` and skipped the prep entirely,
reproducing the bug.

### Fix

- Move the full prep into `useOpenSettingsMenu`, with a
`useIsSettingsPage()` short-circuit so internal navigation doesn't
clobber the memorized return target.
- `useNavigateSettings` delegates to `openSettingsMenu()` before
navigating — fixing every caller in one place.
- Collapse the duplicated inline logic in `NavigationDrawerOtherSection`
and `MultiWorkspaceDropdownDefaultComponents` to a single call.

Net **−9 lines**, single source of truth, no behavior change for the
existing happy paths.

## Test plan

- [x] \`nx typecheck twenty-front\` passes
- [x] \`oxlint\` + \`prettier\` clean on all 4 changed files
- [x] Existing \`useNavigateSettings\` tests pass (4/4)
- [ ] Manual: Compose button on a Person/Company/Opportunity emails tab
with no connected account → settings drawer renders fully expanded,
"Exit Settings" returns to the record
- [ ] Manual: "Settings" entry in the main nav drawer still works
(return path memorized)
- [ ] Manual: "Settings" entry in the multi-workspace dropdown still
works, and right-click → open in new tab still works (kept
\`UndecoratedLink\`)
- [ ] Manual: Navigating between settings pages does not overwrite the
memorized return URL

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 09:50:31 +02:00
3f87d27d5d fix: use AND instead of OR in neq filter for null-equivalent values (#19071)
Fixes #19070

The `neq` operator in `compute-where-condition-parts.ts` uses `OR` where
it should use `AND` when handling null-equivalent values.

Currently generates:
```sql
field != '' OR field IS NOT NULL
```

For a row where `field = ''`:
- `'' != ''` = false
- `'' IS NOT NULL` = true
- `false OR true` = true -- row incorrectly passes the filter

The `eq` operator correctly uses `OR field IS NULL` because it's
additive (match value or its null equivalent). By De Morgan's law, the
negation `neq` needs `AND field IS NOT NULL` -- exclude if the value
doesn't match AND is not a null equivalent.

With the fix:
```sql
field != '' AND field IS NOT NULL
```
- `'' != ''` = false, `'' IS NOT NULL` = true, `false AND true` = false
-- correctly excluded
- `NULL != ''` = NULL, `NULL IS NOT NULL` = false, `NULL AND false` =
false -- correctly excluded
- `'Alice' != ''` = true, `'Alice' IS NOT NULL` = true, `true AND true`
= true -- correctly included

Affects `neq` filters on TEXT fields and all composite sub-fields
(firstName, lastName, primaryEmail, primaryPhoneNumber, address
sub-fields, etc.) when filtering against null-equivalent values.

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-04-07 07:36:41 +00:00
Abdullah.andGitHub 68cd2f6d61 fix: node-tar symlink path traversal via drive-relative linkpath (#19360)
Resolves [Dependabot Alert
619](https://github.com/twentyhq/twenty/security/dependabot/619) and
[Dependabot Alert
629](https://github.com/twentyhq/twenty/security/dependabot/629).
2026-04-07 07:15:55 +00:00
Abdullah.andGitHub 8c9228cb2b fix: SVGO DoS through entity expansion in DOCTYPE (#19359)
Resolves [Dependabot Alert
604](https://github.com/twentyhq/twenty/security/dependabot/604) and
[Dependabot Alert
605](https://github.com/twentyhq/twenty/security/dependabot/605).
2026-04-07 07:15:35 +00:00
Abdullah.andGitHub 35b76539cc fix: minimatch related dependabot alerts. (#19357)
Resolves [Dependabot Alert
491](https://github.com/twentyhq/twenty/security/dependabot/491).

Expecting it to resolve a few other minimatch generated alerts too, but
merging shall confirm which ones since minimatch has a lot of different
versions being imported by different packages as a transitive
dependency.
2026-04-07 07:15:08 +00:00
Thomas des FrancsandGitHub ea4ef99565 Salesforce section (#19366)
## Summary
- add the retro `VT323` font to the marketing site and apply it across
the Salesforce pricing card and popups
- expand the Salesforce pricing simulator with per-row metadata, unique
popup messages, dynamic price calculation, enterprise shared-cost
handling, and fixed-cost totals
- align the Salesforce card UI with the wireframes: sticky pricing
header, updated checkbox states, popup styling/behavior, add-on link,
and footer cleanup
- remove obsolete shared popup constants and quote form logic tied to
the Salesforce card
- refresh nearby pricing page UI details, including sticky menu behavior
and related pricing section polish

## Testing
- `yarn workspace twenty-website-new build`
2026-04-07 07:14:31 +00:00
c7d1cd11e0 i18n - translations (#19370)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-07 08:50:50 +02:00
83d30f8b76 feat: Send email from UI — inline reply composer & SendEmail mutation (#19363)
## Summary

- **Inline email reply**: Replace external email client redirects
(Gmail/Outlook deeplinks) with an in-app email composer. Users can reply
to email threads directly from the email thread widget or via the
command menu.
- **SendEmail GraphQL mutation**: New backend mutation that reuses
`EmailComposerService` for body sanitization, recipient validation, and
SMTP dispatch via the existing outbound messaging infrastructure.
- **Side panel compose page**: Command menu "Reply" action now opens a
side-panel compose email page with pre-filled To, Subject, and
In-Reply-To fields.

### Backend
- `SendEmailResolver` with `SendEmailInput` / `SendEmailOutputDTO`
- `SendEmailModule` wired into `CoreEngineModule`
- Reuses `EmailComposerService` + `MessagingMessageOutboundService`

### Frontend
- `EmailComposer` / `EmailComposerFields` components
- `useSendEmail`, `useReplyContext`, `useEmailComposerState` hooks
- `useOpenComposeEmailInSidePanel` + `SidePanelComposeEmailPage`
- `EmailThreadWidget` inline Reply bar with toggle composer
- `ReplyToEmailThreadCommand` now opens side-panel instead of external
links

### Seeds
- Added `handle` field to message participant seeds for realistic email
addresses
- Seed `connectedAccount` and `messageChannel` in correct batch order

## Test plan

- [ ] Open an email thread on a person/company record → verify
"Reply..." bar appears below the last message
- [ ] Click "Reply..." → composer opens inline with pre-filled To and
Subject
- [ ] Type a message and click Send → email is sent via SMTP, composer
closes
- [ ] Use command menu Reply action → side panel opens with compose
email page
- [ ] Verify Send/Cancel buttons work correctly in side panel
- [ ] Test with Cc/Bcc toggle in composer fields
- [ ] Verify error handling: invalid recipients, missing connected
account


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 08:43:48 +02:00
aec43da1e2 i18n - translations (#19355)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-06 12:13:17 +02:00
646edec104 i18n - translations (#19354)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-06 12:08:11 +02:00
Félix MalfaitGitHubClaude Opus 4.6claude[bot] <41898282+claude[bot]@users.noreply.github.com>
ea572975d8 feat: generic web search driver abstraction with Exa support and billing (#19341)
## Summary

- Introduces a pluggable `WebSearchDriver` abstraction (interface,
factory, service, module) so web search is no longer tied to native
provider tools (Anthropic/OpenAI)
- **Exa** is the first driver implementation with support for
category-filtered search (company, people, news, research paper, etc.) —
particularly useful for CRM workflows
- Per-query billing for both Exa ($0.007/query) and native provider
surcharges ($0.01/query for Anthropic/OpenAI) via the existing
`USAGE_RECORDED` pipeline
- New config variables: `WEB_SEARCH_DRIVER` (EXA/DISABLED),
`EXA_API_KEY`, `WEB_SEARCH_PREFER_NATIVE` (default false — prefers Exa
over native when both available)
- `WEB_SEARCH` operation type added for usage tracking and Stripe
metering

### Architecture

```
WebSearchDriver (interface)
├── ExaDriver          — Exa neural search with category support
└── DisabledDriver     — throws when search is disabled

WebSearchDriverFactory (extends DriverFactoryBase)
└── creates driver based on WEB_SEARCH_DRIVER config

WebSearchService (facade)
├── search(query, options?, billingContext?)
├── isEnabled()
└── emits USAGE_RECORDED events per query

WebSearchTool (Tool implementation)
└── registered in ActionToolProvider, available via tool catalog
```

### Native search billing gap fixed

Anthropic and OpenAI both charge $0.01/search on top of token costs. The
token costs were already billed, but the per-call surcharge was not.
Added `countNativeWebSearchCallsFromSteps` utility +
`billNativeWebSearchUsage` to `AiBillingService`, wired into both chat
and workflow agent paths.

## Test plan

- [ ] Set `WEB_SEARCH_DRIVER=EXA` + `EXA_API_KEY=...` and verify AI chat
can search the web
- [ ] Verify category parameter works (ask about a specific
company/person)
- [ ] Set `WEB_SEARCH_DRIVER=DISABLED` and verify search tool is not
exposed
- [ ] Set `WEB_SEARCH_PREFER_NATIVE=true` with Anthropic model and
verify native search is used
- [ ] Verify usage events are emitted in ClickHouse for both Exa and
native search paths
- [ ] Verify existing billing tests pass (`npx jest
ai-billing.service.spec.ts`)


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-04-06 12:02:46 +02:00
8acfacc69c Add email thread widget and message thread record page layout (#19351)
## Summary
- Move email thread display from side panel to a dedicated record page
with a new `EMAIL_THREAD` widget type
- Add message thread as a standard object with page layout, subject
field, and backfill command
- Add reply-to-email command menu item for message thread records
- Remove old side panel message thread components in favor of the new
widget-based approach

## Type fixes
- Add `EMAIL_THREAD` to `WidgetConfigurationType`, `WidgetType`, and all
configuration/validator maps
- Create `EmailThreadConfigurationDTO` and shared
`EmailThreadConfiguration` type
- Register EMAIL_THREAD in widget type validators, configuration
resolvers, and standard widget mappings

## Test plan
- [ ] Verify message thread record pages render with the email thread
widget
- [ ] Verify email thread preview navigates to the record page instead
of opening side panel
- [ ] Verify reply-to-email command appears for message thread records
- [ ] Verify typecheck passes for both twenty-front and twenty-server
- [ ] Run existing test suites to check for regressions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 12:02:04 +02:00
Thomas des FrancsandGitHub 4aa1d71b12 few website improvements (#19353)
## Summary
- refine the home so it scrolls after home illustration scroll right
- Added few tweaks to pricing hero
2026-04-06 09:52:40 +00:00
BOHEUSandGitHub 7bf309ba73 Update last interaction app (#19332)
Rewrite to 0.8.0 SDK
2026-04-06 09:22:24 +00:00
8d61bb9ae6 Migrate messageFolder parentFolderId from UUID to externalId (#19348)
This PR migrates `messageFolder`.`parentFolderId` from an internal
`UUID` reference to external provider id.
Eliminates unnecessary lookup and complexity

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-06 09:12:44 +00:00
0d44d7c6d7 i18n - translations (#19352)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-06 10:28:59 +02:00
neo773GitHubmartmullgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>github-actionsClaude Sonnet 4.6Charles Bochet
d3f0162cf5 Remove connected account feature flag (#19286)
Co-authored-by: martmull <martmull@hotmail.fr>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@twenty.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-06 08:13:41 +00:00
Abdullah.andGitHub 3a1e112b86 Fixes and updates to the website. (#19350)
This PR implements a whole lot of fixes and updates to the website. It
adds a release-notes page, terms and conditions page, privacy policy
page, while fixing HomeStepper, ProductStepper, and WhyTwentyStepper. 3D
models still need to be styled and their sizes need to be fixed.
2026-04-06 09:42:30 +02:00
Paul RastoinandGitHub e062343802 Refactor typeorm migration lifecycle and generation (#19275)
# Introduction

Typeorm migration are now associated to a given twenty-version from the
`UPGRADE_COMMAND_SUPPORTED_VERSIONS` that the current twenty core engine
handles

This way when we upgrade we retrieve the migrations that need to be run,
this will be useful for the cross-version incremental upgrade so we
preserve sequentiality

## What's new

To generate
```sh
npx nx database:migrate:generate twenty-server -- --name add-index-to-users
```

To apply all
```sh
npx nx database:migrate twenty-server
```

## Next
Introduce slow and fast typeorm migration in order to get rid of the
save point pattern in our code base
Create a clean and dedicated `InstanceUpgradeService` abstraction
2026-04-06 07:11:47 +00:00
3747af4b5a Fix readonly date still editable (#19334)
Fixes #19319

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-06 06:42:25 +00:00
bad488494f Fix dark mode text color on permissions tab empty stat (#19336) (#19340)
Before: black text in dark mode
<img width="1224" height="860" alt="image"
src="https://github.com/user-attachments/assets/a419ef77-9358-4588-ad19-a15b57448682"
/>


After updated styling: the correct grey text in dark mode
<img width="638" height="249" alt="Screenshot 2026-04-05 at 2 47 24 PM"
src="https://github.com/user-attachments/assets/bf736464-d33c-40f1-8244-2cf5d93b7e9c"
/>

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-05 19:46:10 +00:00
Charles BochetandGitHub f25e040712 fix: replace npm pkg set with node script in set-local-version target (#19344)
## Summary

- Replaces `npm pkg set version=X` with a direct `node -e` script in the
`set-local-version` Nx target
- Fixes `CI Create App E2E minimal` workflow failures on fork PRs where
`npm pkg set` fails with `EJSONPARSE: Unexpected end of JSON input while
parsing empty string`

The `npm` command has unreliable `package.json` resolution in certain CI
checkout contexts (shallow clones of fork PR merge refs). Using `node`
directly to read/write the JSON file avoids this entirely.
2026-04-05 18:56:37 +00:00
265b2582ee Fix zh-CN sidebar translations in minimalMetadata API (#19342)
The minimalMetadata query was returning untranslated English labels for
standard objects (Companies, People, Opportunities, etc.) when users
requested zh-CN locale. This fix adds translation logic to
MinimalMetadataService using the existing I18nService.

Changes:
- Add translateLabel() method to translate standard object labels
- Pass locale from GraphQL context through resolver to service
- Only translate non-custom objects (custom objects use user-defined
labels)

Co-authored-by: Kevin Yu <kevin.yu@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 20:14:20 +02:00
ed912ec548 chore: sync AI model catalog from models.dev (#19339)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

This PR updates pricing, context windows, and model availability based
on the latest data.
New models meeting inclusion criteria (tool calling, pricing data,
context limits) are added automatically.
Deprecated models are detected based on cost-efficiency within the same
model family.

**Please review before merging** — verify no critical models were
incorrectly deprecated.

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-04-05 08:22:58 +02:00
563e27831b Clean up tool output architecture: remove wrappers, enforce ToolOutput everywhere (#19321)
## Summary

- **Remove `ExecuteToolResult` wrapper** — `execute_tool` is now a
transparent dispatcher that returns the raw `ToolOutput` from underlying
tools. No more `{ toolName, result }` envelope.
- **Type the entire execution chain as `Promise<ToolOutput>`** — from
`ToolExecutorService.dispatch()` through `resolveAndExecute()` to
`execute_tool.execute()`. Zero `Promise<unknown>` remaining in the tool
layer.
- **Use `Extract<ToolExecutionRef, ...>`** for dispatch methods,
enabling exhaustive switch checking and removing `as never` casts.
- **Relax `ToolOutput.result` to accept `null`** — removes `??
undefined` hacks at the boundary with logic function results.
- **Enforce 1-export-per-file** across tool type/interface files (split
`tool-descriptor.type.ts`, `tool-provider.interface.ts`, `tool.type.ts`,
`tool-output.type.ts`, `tool-executor.service.ts`).
- **Simplify error handling** — `wrapWithErrorHandler` and all
meta-errors (tool not found, tool excluded) now return consistent
`ToolOutput` shape with `error` as a plain string.
- **Frontend reads output directly** — removed `unwrapToolOutput`
utility; `ToolStepRenderer` and `ThinkingStepsDisplay` extract
`message`/`error` from the raw output with simple type guards.
- **Add permission error detection** for email tools via
`isInsufficientPermissionsError`, guiding the AI model to suggest
account reconnection instead of hallucinating about visibility settings.

## Test plan

- [ ] AI chat tool calls return visible output (not "null") in the UI
- [ ] Tool errors display correctly in the JSON tree
- [ ] Email draft/send tools return actionable permission errors
- [ ] Code interpreter output renders correctly
- [ ] Thinking steps display tool outputs properly


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 14:05:42 +02:00
9cc794ddb6 i18n - translations (#19330)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-04 09:34:32 +02:00
martmullandGitHub 80c5cfc2ec Add npm packages app settings list (#19324)
solves
https://discord.com/channels/1130383047699738754/1488499318762635344
2026-04-04 07:20:11 +00:00
martmullandGitHub 804e0539d9 Publish 0.8.0 (#19323)
as title
2026-04-04 06:05:51 +00:00
4db7ae20c4 i18n - translations (#19329)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-04 08:11:17 +02:00
Félix MalfaitGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>Claude Opus 4.6
8da69e0f77 Fix stored XSS via unsafe URL protocols in href attributes (#19282)
## Summary

- Fixes **GHSA-7w89-7q26-gj7q**: stored XSS via `javascript:` URIs in
BlockNote `FileBlock` `props.url`, rendered as a clickable `<a href>`.
- Audited the full codebase and hardened **all** surfaces where
user-controlled URLs are rendered as `href` or passed to `window.open`.
- Applies defense-in-depth: server-side input validation + client-side
render-time checks + lint rules to prevent regressions.

### Changes

**New utility** — `isSafeUrl` (`~/utils/isSafeUrl.ts`):  
Allowlists `http:`, `https:`, `mailto:`, `tel:` protocols and relative
paths (`/`). Returns `false` for `javascript:`, `data:`, `vbscript:`,
etc.

**Server-side** — `validateBlocknoteFieldOrThrow`:  
- Recursively walks all blocks and validates `props.url` and inline link
`href` values
- Rejects payloads with unsafe URL protocols at save time (before data
is stored)

**Client-side** — 8 components hardened:
| Component | Fix |
|-----------|-----|
| `FileBlock` (reported vuln) | `isSafeUrl` gate, fixed
`target="__blank"` → `_blank`, added `rel="noopener noreferrer"` |
| `LazyMarkdownRenderer` | `isSafeUrl` gate on markdown `<a href>`,
added `target`/`rel` |
| `EditLinkPopover` (TipTap) | Validates + auto-prefixes `https://`,
rejects unsafe URLs |
| `LinkBubbleMenu` (TipTap) | `isSafeUrl` gate on `window.open`, added
`noopener,noreferrer` |
| `AttachmentRow` | `isSafeUrl` gate on file attachment `href` |
| `URLDisplay` / `LinkDisplay` | `isSafeUrl` as second check after
`startsWith('http')` |
| `IframeWidget` | `isSafeUrl` gate on `src`, shows error state for
unsafe URLs |
| `InformationBannerMaintenance` | `isSafeUrl` gate on `window.open` |

**Lint rules** — `.oxlintrc.json`:
- `no-script-url: error` — catches `javascript:` string literals
- `react/jsx-no-script-url: error` — catches `javascript:` in JSX href
attributes

## Test plan

- [ ] Create a note via GraphQL mutation with `"url":
"javascript:void(alert(1))"` in a file block — should be rejected by
server validation
- [ ] Verify existing file attachments in notes still render and are
clickable
- [ ] Verify TipTap link insertion works for normal `https://` URLs
- [ ] Verify TipTap link insertion rejects `javascript:` URIs
- [ ] Verify markdown links in AI chat render correctly for safe URLs
- [ ] Verify URL/Link field displays still work for normal URLs
- [ ] Verify iframe widget rejects non-http(s) URLs


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 08:05:27 +02:00
282ee9ac42 i18n - docs translations (#19320)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-03 18:27:51 +02:00
neo773andGitHub 0b44c9e4e5 Remove connected account upgrade command (#19316) 2026-04-03 18:01:21 +02:00
e1a58df140 i18n - translations (#19318)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-03 17:44:32 +02:00
93c0c98495 i18n - translations (#19317)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-03 17:38:09 +02:00
martmullandGitHub f8c3960cf2 Disable setting tab when empty (#19315)
## before
<img width="1063" height="520" alt="image"
src="https://github.com/user-attachments/assets/db92432b-a30c-49e8-9246-a09d0815e6c9"
/>


## after
<img width="1049" height="490" alt="image"
src="https://github.com/user-attachments/assets/e926b065-6a65-417b-b802-0e1df6ca638f"
/>
2026-04-03 15:27:17 +00:00
EtienneandGitHub d562a384c2 Remove direct execution feature flag - WIP (#19254)
Bug fixes exposed by always-on direct execution
1. GraphQL spec compliance — data[field] = null on resolver error
direct-execution.service.ts — Changed from Promise.allSettled (which
lost the responseKey on rejection) to Promise.all with per-field
try/catch; errors now set data[responseKey] = null per spec
2. Empty object arguments skipped (extractArgumentsFromAst)
extract-arguments-from-ast.util.ts — Removed isEmptyObject check;
filter: {}, data: {} now correctly passed to resolvers instead of
silently dropped (which caused permissions to never be checked)
3. orderBy: {} factory default treated as "no ordering"
direct-execution.service.ts — Before calling the resolver, strips
orderBy: {} and orderByForRecords: {} (empty-object factory defaults
that mean "no ordering")
assert-find-many-args.util.ts / assert-group-by-args.util.ts — Accept {}
for orderBy without throwing
4. orderBy: { field: '...' } object auto-coerced to [{ field: '...' }]
array
direct-execution.service.ts — Applies GraphQL list coercion: a
non-array, non-empty orderBy object is wrapped in an array before
assertion and resolver call
5. totalCount and aggregate fields returned as strings from PostgreSQL
graphql-format-result-from-selected-fields.util.ts — Added
coerceAggregateValue that parses numeric strings to numbers for
totalCount, sum*, avg*, min*, max*, count*, percentageOf* fields
Test updates
nested-relation-queries.integration-spec.ts — Updated expected error
message from Yoga schema-validation message to direct execution resolver
message
~30 snapshot files — Updated to reflect direct execution's error
messages (different from Yoga schema-validation messages for input type
errors)
2026-04-03 15:23:01 +00:00
Thomas TrompetteandGitHub 90597e47ca Add redis cache for cron triggers (#19306)
- WorkflowCronTriggerCronJob was querying every active workspace (~700)
sequentially every minute to find cron triggers, causing regular CPU
spikes to 100% on worker pods

- Added a Redis hashset cache that stores cron triggers. On cache miss
(TTL expired, cold start, or explicit invalidation), a full scan
rebuilds the cache

- Creating/deleting a new cron trigger updates the cache, only if exists
2026-04-03 15:08:31 +00:00
Raphaël BosiandGitHub b55765a991 Fix delete/restore/destroy commands unavailable in select-all mode (#19311)
Fixes https://github.com/twentyhq/twenty/issues/19309

In exclusion mode (select all), selectedRecords is always [] since
individual record IDs aren't tracked. The noneDefined()/everyDefined()
checks return false on empty arrays by design, which hides the delete,
restore, and destroy commands from the command menu.

- Wrap selectedRecords array checks with (isSelectAll or ...) to bypass
when in exclusion mode
- Remove the `numberOfSelectedRecords < 10000` limit
- Add `upgrade:1-21:fix-select-all-command-menu-items` command to
backfill existing workspaces
- Add tests
2026-04-03 15:03:45 +00:00
0b8421a45a i18n - docs translations (#19314)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-03 16:31:55 +02:00
Thomas TrompetteandGitHub 4ba2f3b184 Read cache value for total event stream count (#19313)
As title
2026-04-03 14:19:57 +00:00
Raphaël BosiandGitHub 8543576dae Dynamic icon for command menu items (#19307)
Allow for dynamic icons in command menu items. For instance
`${objectMetadataItem.icon}` will resolve the object metadata item icon
2026-04-03 13:41:56 +00:00
1647 changed files with 64606 additions and 33151 deletions
-1
View File
@@ -85,7 +85,6 @@ npx nx run twenty-server:test:integration:with-db-reset # Run integration tests
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/[name] -d src/database/typeorm/core/core.datasource.ts
# Workspace
npx nx run twenty-server:command workspace:sync-metadata -f # Sync metadata
```
## Usage Guidelines
@@ -0,0 +1,46 @@
name: Deploy Twenty App
description: Build and deploy a Twenty app to a remote instance
inputs:
api-url:
description: Base URL of the target Twenty instance (e.g. https://my.twenty.instance)
required: true
api-key:
description: API key or access token for the target instance
required: true
runs:
using: composite
steps:
- name: Enable Corepack
shell: bash
run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
- name: Install dependencies
shell: bash
run: yarn install --immutable
- name: Configure remote
shell: bash
run: |
mkdir -p ~/.twenty
node -e "
const fs = require('fs'), path = require('path'), os = require('os');
fs.writeFileSync(path.join(os.homedir(), '.twenty', 'config.json'), JSON.stringify({
version: 1,
remotes: { target: { apiUrl: process.env.API_URL, apiKey: process.env.API_KEY } }
}, null, 2));
"
env:
API_URL: ${{ inputs.api-url }}
API_KEY: ${{ inputs.api-key }}
- name: Deploy
shell: bash
run: yarn twenty deploy --remote target
@@ -0,0 +1,46 @@
name: Install Twenty App
description: Install (or upgrade) a Twenty app on a specific workspace
inputs:
api-url:
description: Base URL of the target Twenty instance (e.g. https://my.twenty.instance)
required: true
api-key:
description: API key or access token for the target workspace
required: true
runs:
using: composite
steps:
- name: Enable Corepack
shell: bash
run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
- name: Install dependencies
shell: bash
run: yarn install --immutable
- name: Configure remote
shell: bash
run: |
mkdir -p ~/.twenty
node -e "
const fs = require('fs'), path = require('path'), os = require('os');
fs.writeFileSync(path.join(os.homedir(), '.twenty', 'config.json'), JSON.stringify({
version: 1,
remotes: { target: { apiUrl: process.env.API_URL, apiKey: process.env.API_KEY } }
}, null, 2));
"
env:
API_URL: ${{ inputs.api-url }}
API_KEY: ${{ inputs.api-key }}
- name: Install
shell: bash
run: yarn twenty install --remote target
@@ -0,0 +1,47 @@
name: Spawn Twenty App Dev Test
description: >
Starts a Twenty all-in-one test instance (server, worker, database, redis)
using the twentycrm/twenty-app-dev Docker image on port 2021.
The server is available at http://localhost:2021 with seeded demo data.
inputs:
twenty-version:
description: 'Twenty Docker Hub image tag for twenty-app-dev (e.g., "latest" or "v1.20.0").'
required: false
default: 'latest'
outputs:
server-url:
description: 'URL where the Twenty test server can be reached'
value: http://localhost:2021
api-key:
description: 'API key for the Twenty test instance'
value: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ1c2VySWQiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtOWUzYi00NmQ0LWE1NTYtODhiOWRkYzJiMDM1IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjQ5MDQ4ODE3MDR9.9S4wc0MOr5iczsomlFxZdOHD1IRDS4dnRSwNVNpctF4
runs:
using: 'composite'
steps:
- name: Start twenty-app-dev-test container
shell: bash
run: |
docker run -d \
--name twenty-app-dev-test \
-p 2021:2021 \
-e NODE_PORT=2021 \
-e SERVER_URL=http://localhost:2021 \
twentycrm/twenty-app-dev:${{ inputs.twenty-version }}
echo "Waiting for Twenty test instance to become healthy…"
TIMEOUT=180
ELAPSED=0
until curl -sf http://localhost:2021/healthz > /dev/null 2>&1; do
if [ "$ELAPSED" -ge "$TIMEOUT" ]; then
echo "::error::Twenty did not become healthy within ${TIMEOUT}s"
docker logs twenty-app-dev-test 2>&1 | tail -80
exit 1
fi
sleep 3
ELAPSED=$((ELAPSED + 3))
echo " … waited ${ELAPSED}s"
done
echo "Twenty test instance is ready at http://localhost:2021 (took ~${ELAPSED}s)"
@@ -251,6 +251,14 @@ jobs:
rm -f /tmp/current-server.pid
fi
- name: Flush Redis between server runs
run: |
# Clear all Redis caches to prevent stale data from the current branch
# server contaminating the main branch server. Both servers share the
# same Redis instance, and CoreEntityCacheService/WorkspaceCacheService
# persist cached entities across process restarts.
redis-cli -h localhost -p 6379 FLUSHALL || echo "::warning::Failed to flush Redis"
- name: Checkout main branch
run: |
git stash
@@ -25,12 +25,10 @@ jobs:
packages/twenty-sdk/**
packages/twenty-client-sdk/**
packages/twenty-shared/**
packages/twenty-server/**
!packages/create-twenty-app/package.json
!packages/twenty-sdk/package.json
!packages/twenty-client-sdk/package.json
!packages/twenty-shared/package.json
!packages/twenty-server/package.json
create-app-e2e-hello-world:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
@@ -55,6 +53,8 @@ jobs:
- 6379:6379
env:
PUBLISHABLE_PACKAGES: twenty-client-sdk twenty-sdk create-twenty-app
TWENTY_API_URL: http://localhost:3000
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
@@ -142,27 +142,24 @@ jobs:
- name: Setup server environment
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Build server
run: npx nx build twenty-server
- name: Create and setup database
- name: Create databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
npx nx run twenty-server:database:reset
- name: Setup database
run: npx nx run twenty-server:database:reset
- name: Start server
run: |
npx nx start twenty-server &
echo "Waiting for server to be ready..."
timeout 60 bash -c 'until curl -s http://localhost:3000/health; do sleep 2; done'
run: nohup npx nx start:ci twenty-server &
- name: Wait for server to be ready
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
- name: Authenticate with twenty-server
env:
SEED_API_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik'
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty remote add --api-key $SEED_API_KEY --api-url http://localhost:3000
npx --no-install twenty remote add --api-key ${{ env.TWENTY_API_KEY }} --api-url ${{ env.TWENTY_API_URL }}
- name: Deploy scaffolded app
run: |
@@ -189,8 +186,6 @@ jobs:
echo "$EXEC_OUTPUT" | grep -q 'Created company.*Hello World.*with id'
- name: Run scaffolded app integration test
env:
TWENTY_API_URL: http://localhost:3000
run: |
cd /tmp/e2e-test-workspace/test-app
yarn test
@@ -23,12 +23,10 @@ jobs:
packages/twenty-sdk/**
packages/twenty-client-sdk/**
packages/twenty-shared/**
packages/twenty-server/**
!packages/create-twenty-app/package.json
!packages/twenty-sdk/package.json
!packages/twenty-client-sdk/package.json
!packages/twenty-shared/package.json
!packages/twenty-server/package.json
create-app-e2e-minimal:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
@@ -53,6 +51,8 @@ jobs:
- 6379:6379
env:
PUBLISHABLE_PACKAGES: twenty-client-sdk twenty-sdk create-twenty-app
TWENTY_API_URL: http://localhost:3000
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
@@ -136,41 +136,26 @@ jobs:
- name: Setup server environment
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Build server
run: npx nx build twenty-server
- name: Create and setup database
- name: Create databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
npx nx run twenty-server:database:reset
- name: Setup database
run: npx nx run twenty-server:database:reset
- name: Start server
run: |
npx nx start twenty-server &
echo "Waiting for server to be ready..."
timeout 60 bash -c 'until curl -s http://localhost:3000/health; do sleep 2; done'
run: nohup npx nx start:ci twenty-server &
- name: Wait for server to be ready
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
- name: Authenticate with twenty-server
env:
SEED_API_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik'
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty remote add --api-key $SEED_API_KEY --api-url http://localhost:3000
npx --no-install twenty remote add --api-key ${{ env.TWENTY_API_KEY }} --api-url ${{ env.TWENTY_API_URL }}
- name: Deploy scaffolded app
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty deploy
- name: Install scaffolded app
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty install
- name: Run scaffolded app integration test
env:
TWENTY_API_URL: http://localhost:3000
- name: Run scaffolded app integration test (deploys, installs, and verifies the app)
run: |
cd /tmp/e2e-test-workspace/test-app
yarn test
@@ -25,12 +25,10 @@ jobs:
packages/twenty-sdk/**
packages/twenty-client-sdk/**
packages/twenty-shared/**
packages/twenty-server/**
!packages/create-twenty-app/package.json
!packages/twenty-sdk/package.json
!packages/twenty-client-sdk/package.json
!packages/twenty-shared/package.json
!packages/twenty-server/package.json
create-app-e2e-postcard:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
@@ -55,6 +53,8 @@ jobs:
- 6379:6379
env:
PUBLISHABLE_PACKAGES: twenty-client-sdk twenty-sdk create-twenty-app
TWENTY_API_URL: http://localhost:3000
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
@@ -140,27 +140,24 @@ jobs:
- name: Setup server environment
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Build server
run: npx nx build twenty-server
- name: Create and setup database
- name: Create databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
npx nx run twenty-server:database:reset
- name: Setup database
run: npx nx run twenty-server:database:reset
- name: Start server
run: |
npx nx start twenty-server &
echo "Waiting for server to be ready..."
timeout 60 bash -c 'until curl -s http://localhost:3000/health; do sleep 2; done'
run: nohup npx nx start:ci twenty-server &
- name: Wait for server to be ready
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
- name: Authenticate with twenty-server
env:
SEED_API_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik'
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty remote add --api-key $SEED_API_KEY --api-url http://localhost:3000
npx --no-install twenty remote add --api-key ${{ env.TWENTY_API_KEY }} --api-url ${{ env.TWENTY_API_URL }}
- name: Deploy scaffolded app
run: |
@@ -187,8 +184,6 @@ jobs:
echo "$EXEC_OUTPUT" | grep -q 'Created company.*Hello World.*with id'
- name: Run scaffolded app integration test
env:
TWENTY_API_URL: http://localhost:3000
run: |
cd /tmp/e2e-test-workspace/test-app
yarn test
@@ -0,0 +1,94 @@
name: CI Example App Hello World
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/twenty-apps/examples/hello-world/**
packages/twenty-sdk/**
packages/twenty-client-sdk/**
packages/twenty-shared/**
!packages/twenty-sdk/package.json
!packages/twenty-client-sdk/package.json
!packages/twenty-shared/package.json
example-app-hello-world:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
env:
TWENTY_API_URL: http://localhost:3000
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build SDK packages
run: npx nx build twenty-sdk
- name: Setup server environment
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Create databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: Setup database
run: npx nx run twenty-server:database:reset
- name: Start server
run: nohup npx nx start:ci twenty-server &
- name: Wait for server to be ready
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
- name: Run integration tests
working-directory: packages/twenty-apps/examples/hello-world
run: npx vitest run
ci-example-app-hello-world-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, example-app-hello-world]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1
@@ -0,0 +1,94 @@
name: CI Example App Postcard
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/twenty-apps/examples/postcard/**
packages/twenty-sdk/**
packages/twenty-client-sdk/**
packages/twenty-shared/**
!packages/twenty-sdk/package.json
!packages/twenty-client-sdk/package.json
!packages/twenty-shared/package.json
example-app-postcard:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
env:
TWENTY_API_URL: http://localhost:3000
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build SDK packages
run: npx nx build twenty-sdk
- name: Setup server environment
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Create databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: Setup database
run: npx nx run twenty-server:database:reset
- name: Start server
run: nohup npx nx start:ci twenty-server &
- name: Wait for server to be ready
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
- name: Run integration tests
working-directory: packages/twenty-apps/examples/postcard
run: npx vitest run
ci-example-app-postcard-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, example-app-postcard]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1
+1 -1
View File
@@ -158,7 +158,7 @@ jobs:
timeout-minutes: 30
runs-on: ubuntu-latest
env:
NODE_OPTIONS: '--max-old-space-size=4096'
NODE_OPTIONS: '--max-old-space-size=6144'
TASK_CACHE_KEY: front-task-${{ matrix.task }}
strategy:
matrix:
+2
View File
@@ -70,6 +70,8 @@ jobs:
- 6379:6379
env:
NODE_ENV: test
TWENTY_API_URL: http://localhost:3000
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
+4 -4
View File
@@ -144,15 +144,15 @@ jobs:
exit 1
- name: Server / Check for Pending Migrations
run: |
CORE_MIGRATION_OUTPUT=$(npx nx run twenty-server:typeorm migration:generate core-migration-check -d src/database/typeorm/core/core.datasource.ts || true)
CORE_MIGRATION_OUTPUT=$(npx nx database:migrate:generate twenty-server -- --name core-migration-check || true)
CORE_MIGRATION_FILE=$(ls packages/twenty-server/*core-migration-check.ts 2>/dev/null || echo "")
CORE_MIGRATION_FILE=$(ls packages/twenty-server/src/database/typeorm/core/migrations/common/*core-migration-check.ts 2>/dev/null || echo "")
if [ -n "$CORE_MIGRATION_FILE" ]; then
echo "::error::Unexpected migration files were generated. Please create a proper migration manually."
echo "::error::Unexpected migration files were generated. Please run 'npx nx database:migrate:generate twenty-server -- --name <migration-name>' and commit the result."
echo "$CORE_MIGRATION_OUTPUT"
rm -f packages/twenty-server/*core-migration-check.ts
rm -f packages/twenty-server/src/database/typeorm/core/migrations/common/*core-migration-check.ts
exit 1
fi
+1 -4
View File
@@ -75,9 +75,6 @@ npx nx run twenty-server:database:migrate:prod # Run migrations
# Generate migration (replace [name] with kebab-case descriptive name)
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/common/[name] -d src/database/typeorm/core/core.datasource.ts
# Sync metadata
npx nx run twenty-server:command workspace:sync-metadata
```
### Database Inspection (Postgres MCP)
@@ -87,7 +84,7 @@ A read-only Postgres MCP server is configured in `.mcp.json`. Use it to:
- Verify migration results (columns, types, constraints) after running migrations
- Explore the multi-tenant schema structure (core, metadata, workspace-specific schemas)
- Debug issues by querying raw data to confirm whether a bug is frontend, backend, or data-level
- Inspect metadata tables to debug GraphQL schema generation or `workspace:sync-metadata` issues
- Inspect metadata tables to debug GraphQL schema generation issues
This server is read-only — for write operations (reset, migrations, sync), use the CLI commands above.
+1 -1
View File
@@ -141,7 +141,7 @@
"cache": false,
"options": {
"cwd": "{projectRoot}",
"command": "npm pkg set version={args.releaseVersion}"
"command": "node -e \"const fs=require('fs'),p=JSON.parse(fs.readFileSync('package.json','utf8'));p.version='{args.releaseVersion}';fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\\n');\""
}
},
"storybook:build": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "1.20.0",
"version": "0.9.0-canary.0",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
@@ -0,0 +1,42 @@
name: CD
on:
push:
branches:
- main
pull_request:
types: [labeled]
permissions:
contents: read
env:
TWENTY_DEPLOY_URL: http://localhost:3000
concurrency:
group: cd-${{ github.ref }}
cancel-in-progress: true
jobs:
deploy-and-install:
if: >-
github.event_name == 'push' ||
(github.event_name == 'pull_request' && github.event.label.name == 'deploy')
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Deploy
uses: twentyhq/twenty/.github/actions/deploy-twenty-app@main
with:
api-url: ${{ env.TWENTY_DEPLOY_URL }}
api-key: ${{ secrets.TWENTY_DEPLOY_API_KEY }}
- name: Install
uses: twentyhq/twenty/.github/actions/install-twenty-app@main
with:
api-url: ${{ env.TWENTY_DEPLOY_URL }}
api-key: ${{ secrets.TWENTY_DEPLOY_API_KEY }}
@@ -6,9 +6,16 @@ on:
- main
pull_request: {}
permissions:
contents: read
env:
TWENTY_VERSION: latest
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
@@ -16,12 +23,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Spawn Twenty instance
- name: Spawn Twenty test instance
id: twenty
uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
uses: twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main
with:
twenty-version: ${{ env.TWENTY_VERSION }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Enable Corepack
run: corepack enable
@@ -30,7 +36,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
cache: yarn
- name: Install dependencies
run: yarn install --immutable
@@ -39,4 +45,4 @@ jobs:
run: yarn test
env:
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
TWENTY_API_KEY: ${{ steps.twenty.outputs.api-key }}
@@ -3,43 +3,51 @@ 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');
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.test.json');
beforeAll(async () => {
const apiUrl = process.env.TWENTY_API_URL!;
const token = process.env.TWENTY_API_KEY!;
if (!apiUrl || !token) {
throw new Error(
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
'Start a local server: yarn twenty server start\n' +
'Or set them in vitest env config.',
);
}
const assertServerIsReachable = async () => {
let response: Response;
try {
response = await fetch(`${TWENTY_API_URL}/healthz`);
response = await fetch(`${apiUrl}/healthz`);
} catch {
throw new Error(
`Twenty server is not reachable at ${TWENTY_API_URL}. ` +
`Twenty server is not reachable at ${apiUrl}. ` +
'Make sure the server is running before executing integration tests.',
);
}
if (!response.ok) {
throw new Error(`Server at ${TWENTY_API_URL} returned ${response.status}`);
throw new Error(`Server at ${apiUrl} returned ${response.status}`);
}
};
beforeAll(async () => {
await assertServerIsReachable();
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
const configFile = {
remotes: {
local: {
apiUrl: process.env.TWENTY_API_URL,
apiKey: process.env.TWENTY_API_KEY,
},
},
defaultRemote: 'local',
};
fs.mkdirSync(CONFIG_DIR, { recursive: true });
fs.writeFileSync(
path.join(TEST_CONFIG_DIR, 'config.json'),
JSON.stringify(configFile, null, 2),
CONFIG_PATH,
JSON.stringify(
{
remotes: {
local: { apiUrl, apiKey: token },
},
defaultRemote: 'local',
},
null,
2,
),
);
process.env.TWENTY_APP_ACCESS_TOKEN ??= token;
});
@@ -14,8 +14,11 @@ export default defineConfig({
include: ['src/**/*.integration-test.ts'],
setupFiles: ['src/__tests__/setup-test.ts'],
env: {
TWENTY_API_URL: process.env.TWENTY_API_URL ?? 'http://localhost:2020',
TWENTY_API_KEY:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik',
process.env.TWENTY_API_KEY ??
// Tim Apple (admin) access token for twenty-app-dev
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ1c2VySWQiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtOWUzYi00NmQ0LWE1NTYtODhiOWRkYzJiMDM1IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjQ5MDQ4ODE3MDR9.9S4wc0MOr5iczsomlFxZdOHD1IRDS4dnRSwNVNpctF4',
},
},
});
@@ -21,7 +21,7 @@ export const copyBaseApplicationProject = async ({
console.log(chalk.gray('Generating application project...'));
await fs.copy(join(__dirname, './constants/template'), appDirectory);
await renameGitignore({ appDirectory });
await renameDotfiles({ appDirectory });
await generateUniversalIdentifiers({
appDisplayName,
@@ -32,11 +32,20 @@ export const copyBaseApplicationProject = async ({
await updatePackageJson({ appName, appDirectory });
};
const renameGitignore = async ({ appDirectory }: { appDirectory: string }) => {
const gitignorePath = join(appDirectory, 'gitignore');
// npm strips dotfiles/dotdirs (.gitignore, .github/) from published packages,
// so we store them without the leading dot and rename after copying.
const renameDotfiles = async ({ appDirectory }: { appDirectory: string }) => {
const renames = [
{ from: 'gitignore', to: '.gitignore' },
{ from: 'github', to: '.github' },
];
if (await fs.pathExists(gitignorePath)) {
await fs.rename(gitignorePath, join(appDirectory, '.gitignore'));
for (const { from, to } of renames) {
const sourcePath = join(appDirectory, from);
if (await fs.pathExists(sourcePath)) {
await fs.rename(sourcePath, join(appDirectory, to));
}
}
};
@@ -2,22 +2,18 @@
Updates Last interaction and Interaction status fields based on last email date
## Requirements
- an `apiKey` - go to Settings > API & Webhooks to generate one
## Setup
1. Add and synchronize app
Add and synchronize app
```bash
cd packages/twenty-apps/community/last-email-interaction
yarn auth
yarn sync
yarn twenty remote add
yarn twenty install
```
2. Go to Settings > Integrations > Last email interaction > Settings and add required variables
## Flow
- Checks if fields are created, if not, creates them on fly
- Extracts the datetime of message and calculates the last interaction status
- Fetches all users and companies connected to them and updates their Last interaction and Interaction status fields
- Extracts the datetime of fetched message and calculates the last interaction status
- Fetches all users and companies connected to the message and updates their Last interaction and Interaction status fields
## Todo:
- update app with generated Twenty object once extending objects is possible
## Notes
- Upon install, creates fields to Person and Company objects
- Every day at midnight app goes through all companies and people records and updates their Interaction status based on Last interaction date
@@ -1,23 +0,0 @@
import { type ApplicationConfig } from 'twenty-sdk';
const config: ApplicationConfig = {
universalIdentifier: '718ed9ab-53fc-49c8-8deb-0cff78ecf0d2',
displayName: 'Last email interaction',
description:
'Updates Last interaction and Interaction status fields based on last received email',
icon: "IconMailFast",
applicationVariables: {
TWENTY_API_KEY: {
universalIdentifier: 'aae3f523-4c1f-4805-b3ee-afeb676c381e',
isSecret: true,
description: 'Required to send requests to Twenty',
},
TWENTY_API_URL: {
universalIdentifier: '6d19bb04-45bb-46aa-a4e5-4a2682c7b19d',
isSecret: false,
description: 'Optional, defaults to cloud API URL',
},
},
};
export default config;
@@ -9,19 +9,23 @@
},
"packageManager": "yarn@4.9.2",
"dependencies": {
"axios": "^1.12.2",
"twenty-sdk": "0.2.4"
},
"devDependencies": {
"@types/node": "^24.7.2"
"axios": "1.14.0",
"twenty-sdk": "0.8.0"
},
"scripts": {
"auth": "twenty auth login",
"dev": "twenty app dev",
"sync": "twenty app sync",
"uninstall": "twenty app uninstall",
"logs": "twenty app logs",
"create-entity": "twenty app add",
"help": "twenty --help"
"twenty": "twenty",
"lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint --fix -c .oxlintrc.json .",
"test": "vitest run",
"test:watch": "vitest"
},
"devDependencies": {
"@types/node": "^24.7.2",
"@types/react": "^18.2.0",
"oxlint": "^0.16.0",
"react": "^18.2.0",
"typescript": "^5.9.3",
"vite-tsconfig-paths": "^4.2.1",
"vitest": "^3.1.1"
}
}
@@ -0,0 +1,10 @@
import { defineApplication } from 'twenty-sdk';
export default defineApplication({
universalIdentifier: '718ed9ab-53fc-49c8-8deb-0cff78ecf0d2',
displayName: 'Last email interaction',
description:
'Updates Last interaction and Interaction status fields based on last received email',
icon: "IconMailFast",
defaultRoleUniversalIdentifier: '7a66af97-5056-45b2-96a9-c89f0fd181d1',
});
@@ -0,0 +1,41 @@
import { defineField, FieldType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk';
export default defineField({
universalIdentifier: '9378751e-c23b-4e84-887d-2905cb8359b4',
name: 'interactionStatus',
label: 'Interaction status',
type: FieldType.SELECT,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
description: 'Indicates the health of relation',
options: [
{
id: '39d54a6b-5a0e-4209-9a59-2a2d7b8c462b',
color: 'green',
label: 'Recent',
value: 'RECENT',
position: 1,
},
{
id: '7377d6c5-a75c-453e-a1a1-63fb9cba4e26',
color: 'yellow',
label: 'Active',
value: 'ACTIVE',
position: 2,
},
{
id: 'a8b99246-237f-4715-b21f-94a3ae14994e',
color: 'sky',
label: 'Cooling',
value: 'COOLING',
position: 3,
},
{
id: '1f05d528-eaab-4639-aba1-328050a87220',
color: 'gray',
label: 'Dormant',
value: 'DORMANT',
position: 4,
},
],
});
@@ -0,0 +1,14 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
export default defineField({
universalIdentifier: '2f195c4c-1db1-4bbe-80b6-25c2f63168b0',
name: 'lastInteraction',
label: 'Last interaction',
type: FieldType.DATE_TIME,
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
description: 'Date when the last interaction happened',
});
@@ -0,0 +1,41 @@
import { defineField, FieldType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk';
export default defineField({
universalIdentifier: 'fa342e26-9742-4db8-85b4-4d78ba18482f',
name: 'interactionStatus',
label: 'Interaction status',
type: FieldType.SELECT,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
description: 'Indicates the health of relation',
options: [
{
id: '1dfbfa99-35fb-43af-8c14-74e682a8121b',
color: 'green',
label: 'Recent',
value: 'RECENT',
position: 1,
},
{
id: '955788e8-6d64-45ba-80ea-a1a5446a0ae7',
color: 'yellow',
label: 'Active',
value: 'ACTIVE',
position: 2,
},
{
id: '7b84ca72-fac5-4c6d-ab08-b148e4b3efdf',
color: 'sky',
label: 'Cooling',
value: 'COOLING',
position: 3,
},
{
id: '04dea3e5-ec26-41cf-b23f-37abab67827a',
color: 'gray',
label: 'Dormant',
value: 'DORMANT',
position: 4,
},
],
});
@@ -0,0 +1,14 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
export default defineField({
universalIdentifier: 'bec14de7-6683-4784-91ba-62d83b5f30f7',
name: 'lastInteraction',
label: 'Last interaction',
type: FieldType.DATE_TIME,
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
description: 'Date when the last interaction happened',
});
@@ -1,270 +0,0 @@
import axios from 'axios';
import { type FunctionConfig } from 'twenty-sdk';
const TWENTY_API_KEY = process.env.TWENTY_API_KEY ?? '';
const TWENTY_URL =
process.env.TWENTY_API_URL !== '' && process.env.TWENTY_API_URL !== undefined
? `${process.env.TWENTY_API_URL}/rest`
: 'https://api.twenty.com/rest';
const create_last_interaction = (id: string) => {
return {
method: 'POST',
url: `${TWENTY_URL}/metadata/fields`,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${TWENTY_API_KEY}`,
},
data: {
type: 'DATE_TIME',
objectMetadataId: `${id}`,
name: 'lastInteraction',
label: 'Last interaction',
description: 'Date when the last interaction happened',
icon: 'IconCalendarClock',
defaultValue: null,
isNullable: true,
settings: {},
},
};
};
const create_interaction_status = (id: string) => {
return {
method: 'POST',
url: `${TWENTY_URL}/metadata/fields`,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${TWENTY_API_KEY}`,
},
data: {
type: 'SELECT',
objectMetadataId: `${id}`,
name: 'interactionStatus',
label: 'Interaction status',
description: 'Indicates the health of relation',
icon: 'IconProgress',
defaultValue: null,
isNullable: true,
settings: {},
options: [
{
color: 'green',
label: 'Recent',
value: 'RECENT',
position: 1,
},
{
color: 'yellow',
label: 'Active',
value: 'ACTIVE',
position: 2,
},
{
color: 'sky',
label: 'Cooling',
value: 'COOLING',
position: 3,
},
{
color: 'gray',
label: 'Dormant',
value: 'DORMANT',
position: 4,
},
],
},
};
};
const calculateStatus = (date: string) => {
const day = 1000 * 60 * 60 * 24;
const now = Date.now();
const messageDate = Date.parse(date);
const deltaTime = now - messageDate;
return deltaTime < 7 * day
? 'RECENT'
: deltaTime < 30 * day
? 'ACTIVE'
: deltaTime < 90 * day
? 'COOLING'
: 'DORMANT';
};
const updateInteractionStatus = async (objectName: string, id: string, messageDate: string, status: string) => {
const options = {
method: 'PATCH',
url: `${TWENTY_URL}/${objectName}/${id}`,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${TWENTY_API_KEY}`,
},
data: {
lastInteraction: messageDate,
interactionStatus: status
}
};
try {
const response = await axios.request(options);
if (response.status === 200) {
console.log('Successfully updated company last interaction field');
}
} catch (error) {
if (axios.isAxiosError(error)) {
throw error;
}
throw error;
}
}
const fetchRelatedCompanyId = async (id: string) => {
const options = {
method: 'GET',
url: `${TWENTY_URL}/people/${id}`,
headers: {
Authorization: `Bearer ${TWENTY_API_KEY}`,
},
};
try {
const req = await axios.request(options);
if (req.status === 200 && req.data.person.companyId !== null && req.data.person.companyId !== undefined) {
return req.data.person.companyId;
}
} catch (error) {
if (axios.isAxiosError(error)) {
throw error;
}
throw error;
}
}
export const main = async (params: {
properties: Record<string, any>;
recordId: string;
userId: string;
}): Promise<object | undefined> => {
if (TWENTY_API_KEY === '') {
console.log("Function exited as API key or URL hasn't been set properly");
return {};
}
const { properties, recordId } = params;
// Check if fields are created
const options = {
method: 'GET',
url: `${TWENTY_URL}/metadata/objects`,
headers: {
Authorization: `Bearer ${TWENTY_API_KEY}`,
},
};
try {
const response = await axios.request(options);
const objects = response.data.data.objects;
const company_object = objects.find(
(object: any) => object.nameSingular === 'company',
);
const company_last_interaction = company_object.fields.find(
(field: any) => field.name === 'lastInteraction',
);
const company_interaction_status = company_object.fields.find(
(field: any) => field.name === 'interactionStatus',
);
const person_object = objects.find(
(object: any) => object.nameSingular === 'person',
);
const person_last_interaction = person_object.fields.find(
(field: any) => field.name === 'lastInteraction',
);
const person_interaction_status = person_object.fields.find(
(field: any) => field.name === 'interactionStatus',
);
// If not, create them
if (company_last_interaction === undefined) {
const response2 = await axios.request(
create_last_interaction(company_object.id),
);
if (response2.status === 201) {
console.log('Successfully created company last interaction field');
}
}
if (company_interaction_status === undefined) {
const response2 = await axios.request(
create_interaction_status(company_object.id),
);
if (response2.status === 201) {
console.log('Successfully created company interaction status field');
}
}
if (person_last_interaction === undefined) {
const response2 = await axios.request(
create_last_interaction(person_object.id),
);
if (response2.status === 201) {
console.log('Successfully created person last interaction field');
}
}
if (person_interaction_status === undefined) {
const response2 = await axios.request(
create_interaction_status(person_object.id),
);
if (response2.status === 201) {
console.log('Successfully created person interaction status field');
}
}
// Extract the timestamp of message
const messageDate = properties.after.receivedAt;
const interactionStatus = calculateStatus(messageDate);
// Get the details of person and related company
const messageOptions = {
method: 'GET',
url: `${TWENTY_URL}/messages/${recordId}?depth=1`,
headers: {
Authorization: `Bearer ${TWENTY_API_KEY}`,
},
};
const messageDetails = await axios.request(messageOptions);
const peopleIds: string[] = [];
for (const participant of messageDetails.data.messages
.messageParticipants) {
peopleIds.push(participant.personId);
}
const companiesIds = [];
for (const id of peopleIds) {
companiesIds.push(await fetchRelatedCompanyId(id));
}
// Update the field value depending on the timestamp
for (const id of peopleIds) {
await updateInteractionStatus("people", id, messageDate, interactionStatus);
}
for (const id of companiesIds) {
await updateInteractionStatus("companies", id, messageDate, interactionStatus);
}
} catch (error) {
if (axios.isAxiosError(error)) {
console.error(error.message);
return {};
}
console.error(error);
return {};
}
};
export const config: FunctionConfig = {
universalIdentifier: '683966a0-b60a-424e-86b1-7448c9191bde',
name: 'test',
triggers: [
{
universalIdentifier: 'f4f1e127-87f0-4dcf-99fe-8061adf5cbe6',
type: 'databaseEvent',
eventName: 'message.created',
},
{
universalIdentifier: '4c17878f-b6b3-4d0a-8de6-967b1cb55002',
type: 'databaseEvent',
eventName: 'message.updated',
},
],
};
@@ -0,0 +1,126 @@
import { defineLogicFunction } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { calculateStatus } from '../shared/calculate-status';
const fetchAllPeople = async () => {
const client = new CoreApiClient();
const result = await client.query({
people: {
__args: {
filter: {
/*
lastInteraction: {
is: 'NOT_NULL',
},
*/
},
},
edges: {
node: {
id: true,
lastInteraction: true,
interactionStatus: true,
},
},
},
});
if (!result.people) {
throw new Error('Could not find any people');
}
return result.people.edges;
}
const fetchAllCompanies = async () => {
const client = new CoreApiClient();
const result = await client.query({
companies: {
__args: {
filter: {
/* how to fetch fields added in fields folder?
lastInteraction: {
is: 'NOT_NULL',
},
*/
},
},
edges: {
node: {
id: true,
lastInteraction: true,
interactionStatus: true,
},
},
},
});
if (!result.companies) {
throw new Error('Could not find any companies');
}
return result.companies.edges;
}
const updateCompany = async (
companyId: string,
updateData: Record<string, string>,
) => {
const client = new CoreApiClient();
const result = await client.mutation({
updateCompany: {
__args: {
id: companyId,
data: updateData,
},
id: true,
},
});
if (!result.updateCompany) {
throw new Error(`Failed to update company ${companyId}`);
}
};
const updatePerson = async (
personId: string,
updateData: Record<string, string>,
) => {
const client = new CoreApiClient();
const result = await client.mutation({
updatePerson: {
__args: {
id: personId,
data: updateData,
},
id: true,
},
});
if (!result.updatePerson) {
throw new Error(`Failed to update person ${personId}`);
}
};
const handler = async () => {
const people = await fetchAllPeople();
for (const person of people) {
const interactionStatus = calculateStatus(person.node.lastInteraction as string);
if (interactionStatus !== person.node.interactionStatus) {
await updatePerson(person.node.id, {interactionStatus: interactionStatus});
}
}
const companies = await fetchAllCompanies();
for (const company of companies) {
const interactionStatus = calculateStatus(company.node.lastInteraction as string);
if (interactionStatus !== company.node.interactionStatus) {
await updateCompany(company.node.id, {interactionStatus: interactionStatus});
}
}
};
export default defineLogicFunction({
universalIdentifier: 'c79f1f30-f369-4264-9e5b-c183577bc709',
name: 'on-cron-job',
description:
'Runs daily at midnight and updates all companies and people with correct interaction status',
timeoutSeconds: 5,
handler,
cronTriggerSettings: {
pattern: '0 0 * * *', // runs daily at midnight
},
});
@@ -0,0 +1,143 @@
import {
DatabaseEventPayload,
defineLogicFunction,
ObjectRecordCreateEvent,
} from 'twenty-sdk';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { calculateStatus } from '../shared/calculate-status';
const fetchMessageParticipants = async (messageId: string) => {
const client = new CoreApiClient();
const result = await client.query({
messageParticipants: {
__args: {
filter: {
messageId: {
eq: messageId,
},
},
},
edges: {
node: {
personId: true,
},
},
},
});
let people: string[] = [];
if (result.messageParticipants === undefined) {
return people;
}
for (const person of result.messageParticipants.edges) {
if (person.node.personId !== undefined) {
people.push(person.node.personId);
}
}
return people;
};
const fetchRelatedCompany = async (personId: string) => {
const client = new CoreApiClient();
const result = await client.query({
people: {
__args: {
filter: {
id: {
eq: personId,
},
},
},
edges: {
node: {
company: {
id: true,
},
},
},
},
});
if (
result.people === undefined ||
result.people.edges[0].node.company === undefined
) {
throw new Error(`Failed to fetch related company of person ${personId}`);
}
return result.people.edges[0].node.company.id;
};
const updateCompany = async (
companyId: string,
updateData: Record<string, string>,
) => {
const client = new CoreApiClient();
const result = await client.mutation({
updateCompany: {
__args: {
id: companyId,
data: updateData,
},
id: true,
},
});
if (!result.updateCompany) {
throw new Error(`Failed to update company ${companyId}`);
}
};
const updatePerson = async (
personId: string,
updateData: Record<string, string>,
) => {
const client = new CoreApiClient();
const result = await client.mutation({
updatePerson: {
__args: {
id: personId,
data: updateData,
},
id: true,
},
});
if (!result.updatePerson) {
throw new Error(`Failed to update person ${personId}`);
}
};
type Message = {
receivedAt: string;
};
type MessageCreatedEvent = DatabaseEventPayload<
ObjectRecordCreateEvent<Message>
>;
const handler = async (
event: MessageCreatedEvent,
): Promise<object | undefined> => {
const { properties, recordId } = event;
const interactionStatus = calculateStatus(properties.after.receivedAt);
const peopleIds: string[] = [];
peopleIds.push(...(await fetchMessageParticipants(recordId)));
const updateData = {
lastInteraction: properties.after.receivedAt,
interactionStatus: interactionStatus,
};
for (const person of peopleIds) {
const companyId = await fetchRelatedCompany(person);
await updatePerson(person, updateData);
await updateCompany(companyId, updateData);
}
return {};
};
export default defineLogicFunction({
universalIdentifier: '543432c2-6509-4ee9-90ec-15ffb4d7abfc',
name: 'on-message-created',
description: 'Triggered when new message is created',
timeoutSeconds: 5,
handler,
databaseEventTriggerSettings: {
eventName: 'message.created',
},
});
@@ -0,0 +1,143 @@
import {
DatabaseEventPayload,
defineLogicFunction,
ObjectRecordCreateEvent,
} from 'twenty-sdk';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { calculateStatus } from '../shared/calculate-status';
const fetchMessageParticipants = async (messageId: string) => {
const client = new CoreApiClient();
const result = await client.query({
messageParticipants: {
__args: {
filter: {
messageId: {
eq: messageId,
},
},
},
edges: {
node: {
personId: true,
},
},
},
});
let people: string[] = [];
if (result.messageParticipants === undefined) {
return people;
}
for (const person of result.messageParticipants.edges) {
if (person.node.personId !== undefined) {
people.push(person.node.personId);
}
}
return people;
};
const fetchRelatedCompany = async (personId: string) => {
const client = new CoreApiClient();
const result = await client.query({
people: {
__args: {
filter: {
id: {
eq: personId,
},
},
},
edges: {
node: {
company: {
id: true,
},
},
},
},
});
if (
result.people === undefined ||
result.people.edges[0].node.company === undefined
) {
throw new Error(`Failed to fetch related company of person ${personId}`);
}
return result.people.edges[0].node.company.id;
};
const updateCompany = async (
companyId: string,
updateData: Record<string, string>,
) => {
const client = new CoreApiClient();
const result = await client.mutation({
updateCompany: {
__args: {
id: companyId,
data: updateData,
},
id: true,
},
});
if (!result.updateCompany) {
throw new Error(`Failed to update company ${companyId}`);
}
};
const updatePerson = async (
personId: string,
updateData: Record<string, string>,
) => {
const client = new CoreApiClient();
const result = await client.mutation({
updatePerson: {
__args: {
id: personId,
data: updateData,
},
id: true,
},
});
if (!result.updatePerson) {
throw new Error(`Failed to update person ${personId}`);
}
};
type Message = {
receivedAt: string;
};
type MessageCreatedEvent = DatabaseEventPayload<
ObjectRecordCreateEvent<Message>
>;
const handler = async (
event: MessageCreatedEvent,
): Promise<object | undefined> => {
const { properties, recordId } = event;
const interactionStatus = calculateStatus(properties.after.receivedAt);
const peopleIds: string[] = [];
peopleIds.push(...(await fetchMessageParticipants(recordId)));
const updateData = {
lastInteraction: properties.after.receivedAt,
interactionStatus: interactionStatus,
};
for (const person of peopleIds) {
const companyId = await fetchRelatedCompany(person);
await updatePerson(person, updateData);
await updateCompany(companyId, updateData);
}
return {};
};
export default defineLogicFunction({
universalIdentifier: '9bfcb3e4-9119-4b65-b6e9-d395d0764ce5',
name: 'on-message-updated',
description: 'Triggered when message is updated',
timeoutSeconds: 5,
handler,
databaseEventTriggerSettings: {
eventName: 'message.updated',
},
});
@@ -0,0 +1,13 @@
export const calculateStatus = (date: string) => {
const day = 1000 * 60 * 60 * 24;
const now = Date.now();
const messageDate = Date.parse(date);
const deltaTime = now - messageDate;
return deltaTime < 7 * day
? 'RECENT'
: deltaTime < 30 * day
? 'ACTIVE'
: deltaTime < 90 * day
? 'COOLING'
: 'DORMANT';
};
@@ -0,0 +1,17 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"composite": true,
"types": [
"vitest/globals"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx"
],
"exclude": [
"node_modules",
"dist"
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,42 @@
name: CD
on:
push:
branches:
- main
pull_request:
types: [labeled]
permissions:
contents: read
env:
TWENTY_DEPLOY_URL: http://localhost:3000
concurrency:
group: cd-${{ github.ref }}
cancel-in-progress: true
jobs:
deploy-and-install:
if: >-
github.event_name == 'push' ||
(github.event_name == 'pull_request' && github.event.label.name == 'deploy')
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Deploy
uses: twentyhq/twenty/.github/actions/deploy-twenty-app@main
with:
api-url: ${{ env.TWENTY_DEPLOY_URL }}
api-key: ${{ secrets.TWENTY_DEPLOY_API_KEY }}
- name: Install
uses: twentyhq/twenty/.github/actions/install-twenty-app@main
with:
api-url: ${{ env.TWENTY_DEPLOY_URL }}
api-key: ${{ secrets.TWENTY_DEPLOY_API_KEY }}
@@ -6,9 +6,16 @@ on:
- main
pull_request: {}
permissions:
contents: read
env:
TWENTY_VERSION: latest
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
@@ -16,12 +23,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Spawn Twenty instance
- name: Spawn Twenty test instance
id: twenty
uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
uses: twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main
with:
twenty-version: ${{ env.TWENTY_VERSION }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Enable Corepack
run: corepack enable
@@ -30,7 +36,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
cache: yarn
- name: Install dependencies
run: yarn install --immutable
@@ -39,4 +45,4 @@ jobs:
run: yarn test
env:
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
TWENTY_API_KEY: ${{ steps.twenty.outputs.api-key }}
@@ -3,43 +3,51 @@ import * as os from 'os';
import * as path from 'path';
import { beforeAll } from 'vitest';
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:3000';
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.test.json');
beforeAll(async () => {
const apiUrl = process.env.TWENTY_API_URL!;
const token = process.env.TWENTY_API_KEY!;
if (!apiUrl || !token) {
throw new Error(
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
'Start a local server: yarn twenty server start\n' +
'Or set them in vitest env config.',
);
}
const assertServerIsReachable = async () => {
let response: Response;
try {
response = await fetch(`${TWENTY_API_URL}/healthz`);
response = await fetch(`${apiUrl}/healthz`);
} catch {
throw new Error(
`Twenty server is not reachable at ${TWENTY_API_URL}. ` +
`Twenty server is not reachable at ${apiUrl}. ` +
'Make sure the server is running before executing integration tests.',
);
}
if (!response.ok) {
throw new Error(`Server at ${TWENTY_API_URL} returned ${response.status}`);
throw new Error(`Server at ${apiUrl} returned ${response.status}`);
}
};
beforeAll(async () => {
await assertServerIsReachable();
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
const configFile = {
remotes: {
local: {
apiUrl: process.env.TWENTY_API_URL,
apiKey: process.env.TWENTY_API_KEY,
},
},
defaultRemote: 'local',
};
fs.mkdirSync(CONFIG_DIR, { recursive: true });
fs.writeFileSync(
path.join(TEST_CONFIG_DIR, 'config.json'),
JSON.stringify(configFile, null, 2),
CONFIG_PATH,
JSON.stringify(
{
remotes: {
local: { apiUrl, apiKey: token },
},
defaultRemote: 'local',
},
null,
2,
),
);
process.env.TWENTY_APP_ACCESS_TOKEN ??= token;
});
@@ -14,9 +14,11 @@ export default defineConfig({
include: ['src/**/*.integration-test.ts'],
setupFiles: ['src/__tests__/setup-test.ts'],
env: {
TWENTY_API_URL: 'http://localhost:3000',
TWENTY_API_URL: process.env.TWENTY_API_URL ?? 'http://localhost:2020',
TWENTY_API_KEY:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik',
process.env.TWENTY_API_KEY ??
// Tim Apple (admin) access token for twenty-app-dev
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ1c2VySWQiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtOWUzYi00NmQ0LWE1NTYtODhiOWRkYzJiMDM1IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjQ5MDQ4ODE3MDR9.9S4wc0MOr5iczsomlFxZdOHD1IRDS4dnRSwNVNpctF4',
},
},
});
@@ -0,0 +1,42 @@
name: CD
on:
push:
branches:
- main
pull_request:
types: [labeled]
permissions:
contents: read
env:
TWENTY_DEPLOY_URL: http://localhost:3000
concurrency:
group: cd-${{ github.ref }}
cancel-in-progress: true
jobs:
deploy-and-install:
if: >-
github.event_name == 'push' ||
(github.event_name == 'pull_request' && github.event.label.name == 'deploy')
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Deploy
uses: twentyhq/twenty/.github/actions/deploy-twenty-app@main
with:
api-url: ${{ env.TWENTY_DEPLOY_URL }}
api-key: ${{ secrets.TWENTY_DEPLOY_API_KEY }}
- name: Install
uses: twentyhq/twenty/.github/actions/install-twenty-app@main
with:
api-url: ${{ env.TWENTY_DEPLOY_URL }}
api-key: ${{ secrets.TWENTY_DEPLOY_API_KEY }}
@@ -6,9 +6,16 @@ on:
- main
pull_request: {}
permissions:
contents: read
env:
TWENTY_VERSION: latest
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
@@ -16,12 +23,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Spawn Twenty instance
- name: Spawn Twenty test instance
id: twenty
uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
uses: twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main
with:
twenty-version: ${{ env.TWENTY_VERSION }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Enable Corepack
run: corepack enable
@@ -30,7 +36,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
cache: yarn
- name: Install dependencies
run: yarn install --immutable
@@ -39,4 +45,4 @@ jobs:
run: yarn test
env:
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
TWENTY_API_KEY: ${{ steps.twenty.outputs.api-key }}
@@ -3,43 +3,51 @@ 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');
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.test.json');
beforeAll(async () => {
const apiUrl = process.env.TWENTY_API_URL!;
const token = process.env.TWENTY_API_KEY!;
if (!apiUrl || !token) {
throw new Error(
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
'Start a local server: yarn twenty server start\n' +
'Or set them in vitest env config.',
);
}
const assertServerIsReachable = async () => {
let response: Response;
try {
response = await fetch(`${TWENTY_API_URL}/healthz`);
response = await fetch(`${apiUrl}/healthz`);
} catch {
throw new Error(
`Twenty server is not reachable at ${TWENTY_API_URL}. ` +
`Twenty server is not reachable at ${apiUrl}. ` +
'Make sure the server is running before executing integration tests.',
);
}
if (!response.ok) {
throw new Error(`Server at ${TWENTY_API_URL} returned ${response.status}`);
throw new Error(`Server at ${apiUrl} returned ${response.status}`);
}
};
beforeAll(async () => {
await assertServerIsReachable();
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
const configFile = {
remotes: {
local: {
apiUrl: process.env.TWENTY_API_URL,
apiKey: process.env.TWENTY_API_KEY,
},
},
defaultRemote: 'local',
};
fs.mkdirSync(CONFIG_DIR, { recursive: true });
fs.writeFileSync(
path.join(TEST_CONFIG_DIR, 'config.json'),
JSON.stringify(configFile, null, 2),
CONFIG_PATH,
JSON.stringify(
{
remotes: {
local: { apiUrl, apiKey: token },
},
defaultRemote: 'local',
},
null,
2,
),
);
process.env.TWENTY_APP_ACCESS_TOKEN ??= token;
});
@@ -0,0 +1,59 @@
import { CoreApiClient } from 'twenty-client-sdk/core';
import { definePostInstallLogicFunction } from 'twenty-sdk';
const POST_CARDS_TO_SEED = [
{
name: 'Greetings from Paris',
content:
'Wish you were here! The Eiffel Tower looks even better in person. - Alex',
},
{
name: 'Hello from Tokyo',
content:
'The cherry blossoms are amazing this time of year. See you soon! - Sam',
},
];
const handler = async (): Promise<{
message: string;
createdIds: string[];
}> => {
console.log('Seeding 2 post cards...');
const client = new CoreApiClient();
const createdIds: string[] = [];
for (const postCard of POST_CARDS_TO_SEED) {
const { createPostCard } = await client.mutation({
createPostCard: {
__args: {
data: {
name: postCard.name,
content: postCard.content,
},
},
id: true,
},
});
if (!createPostCard?.id) {
throw new Error(`Failed to create post card "${postCard.name}"`);
}
createdIds.push(createPostCard.id);
}
console.log('Seeding complete!');
return {
message: `Seeded ${createdIds.length} post cards`,
createdIds,
};
};
export default definePostInstallLogicFunction({
universalIdentifier: '9f3d8c21-b471-4a82-8e5c-6f3a7b8c9d01',
name: 'seed-post-cards',
description: 'Seeds the workspace with 2 sample post card records.',
timeoutSeconds: 10,
handler,
});
@@ -51,28 +51,28 @@ export default defineObject({
defaultValue: `'${PostCardStatus.DRAFT}'`,
options: [
{
id: 'a1b2c3d4-0001-4000-8000-000000000001',
id: '8ab3abad-02e7-4670-9283-983d7fac7fe4',
value: PostCardStatus.DRAFT,
label: 'Draft',
position: 0,
color: 'gray',
},
{
id: 'a1b2c3d4-0002-4000-8000-000000000002',
id: '2bcdd195-6c99-4d69-84b2-e2838ee54467',
value: PostCardStatus.SENT,
label: 'Sent',
position: 1,
color: 'orange',
},
{
id: 'a1b2c3d4-0003-4000-8000-000000000003',
id: '918ff60c-c26e-4fae-8eba-3fbce04dc48b',
value: PostCardStatus.DELIVERED,
label: 'Delivered',
position: 2,
color: 'green',
},
{
id: 'a1b2c3d4-0004-4000-8000-000000000004',
id: '3c91a653-5d31-4023-be6c-a69c68d21233',
value: PostCardStatus.RETURNED,
label: 'Returned',
position: 3,
@@ -33,7 +33,7 @@ export default defineRole({
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
fieldUniversalIdentifier: CONTENT_FIELD_UNIVERSAL_IDENTIFIER,
canReadFieldValue: false,
canUpdateFieldValue: false,
canUpdateFieldValue: true,
},
],
permissionFlags: [PermissionFlag.APPLICATIONS],
@@ -14,8 +14,11 @@ export default defineConfig({
include: ['src/**/*.integration-test.ts'],
setupFiles: ['src/__tests__/setup-test.ts'],
env: {
TWENTY_API_URL: process.env.TWENTY_API_URL ?? 'http://localhost:2020',
TWENTY_API_KEY:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik',
process.env.TWENTY_API_KEY ??
// Tim Apple (admin) access token for twenty-app-dev
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ1c2VySWQiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtOWUzYi00NmQ0LWE1NTYtODhiOWRkYzJiMDM1IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjQ5MDQ4ODE3MDR9.9S4wc0MOr5iczsomlFxZdOHD1IRDS4dnRSwNVNpctF4',
},
},
});
@@ -10,25 +10,31 @@ export default defineField({
description: 'Post card category',
options: [
{
id: 'c1d2e3f4-0001-4000-8000-000000000001',
id: 'cd751c81-787d-4581-bc51-efe43f0050a7',
value: 'PERSONAL',
label: 'Personal',
color: 'blue',
position: 0,
},
{
id: 'c1d2e3f4-0002-4000-8000-000000000002',
id: 'eec437ca-5beb-41a9-a826-c9a5eca2eef4',
value: 'BUSINESS',
label: 'Business',
color: 'green',
position: 1,
},
{
id: 'c1d2e3f4-0003-4000-8000-000000000003',
id: 'a5baa37d-1047-4972-b6b8-7faae0e3eac1',
value: 'PROMOTIONAL',
label: 'Promotional',
color: 'orange',
position: 2,
},
{
value: 'OTHER',
label: 'Other',
color: 'gray',
position: 3,
},
],
});
@@ -5,6 +5,7 @@ enum PostCardStatus {
SENT = 'SENT',
DELIVERED = 'DELIVERED',
RETURNED = 'RETURNED',
LOST = 'LOST',
}
export const POST_CARD_UNIVERSAL_IDENTIFIER =
@@ -53,33 +54,40 @@ export default defineObject({
defaultValue: `'${PostCardStatus.DRAFT}'`,
options: [
{
id: 'a1b2c3d4-0001-4000-8000-000000000001',
id: '1b008e19-1e59-4a07-b187-65a20e547c4e',
value: PostCardStatus.DRAFT,
label: 'Draft',
position: 0,
color: 'gray',
},
{
id: 'a1b2c3d4-0002-4000-8000-000000000002',
id: '452b9d40-889c-4342-9697-98319394db04',
value: PostCardStatus.SENT,
label: 'Sent',
position: 1,
color: 'orange',
},
{
id: 'a1b2c3d4-0003-4000-8000-000000000003',
id: 'c2ed0b8c-a3ed-4383-aef9-e0441267bcfe',
value: PostCardStatus.DELIVERED,
label: 'Delivered',
position: 2,
color: 'green',
},
{
id: 'a1b2c3d4-0004-4000-8000-000000000004',
id: 'c57a5e08-7ef7-49b8-87e6-32d720d22802',
value: PostCardStatus.RETURNED,
label: 'Returned',
position: 3,
color: 'orange',
},
{
// No id — exercises addMissingFieldOptionIds in the object branch
value: PostCardStatus.LOST,
label: 'Lost',
position: 4,
color: 'red',
},
],
name: 'status',
},
@@ -1,42 +0,0 @@
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 }}
@@ -1 +0,0 @@
nodeLinker: node-modules
@@ -1,14 +0,0 @@
## Base documentation
- Documentation: https://docs.twenty.com/developers/extend/apps/getting-started
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/postcard
## UUID requirement
- All generated UUIDs must be valid UUID v4.
## Common Pitfalls
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab.
@@ -1,14 +0,0 @@
## Base documentation
- Documentation: https://docs.twenty.com/developers/extend/apps/getting-started
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/postcard
## UUID requirement
- All generated UUIDs must be valid UUID v4.
## Common Pitfalls
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab.
@@ -1,14 +0,0 @@
## Base documentation
- Documentation: https://docs.twenty.com/developers/extend/apps/getting-started
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/postcard
## UUID requirement
- All generated UUIDs must be valid UUID v4.
## Common Pitfalls
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab.
@@ -1,11 +0,0 @@
This is a [Twenty](https://twenty.com) application bootstrapped with [`create-twenty-app`](https://www.npmjs.com/package/create-twenty-app).
## Getting Started
Run `yarn twenty help` to list all available commands.
## Learn More
- [Twenty Apps documentation](https://docs.twenty.com/developers/extend/apps/getting-started)
- [twenty-sdk CLI reference](https://www.npmjs.com/package/twenty-sdk)
- [Discord](https://discord.gg/cx5n4Jzs57)
@@ -1,35 +0,0 @@
{
"name": "my-twenty-app",
"version": "0.1.0",
"license": "MIT",
"engines": {
"node": "^24.5.0",
"npm": "please-use-yarn",
"yarn": ">=4.0.2"
},
"keywords": [
"twenty-app"
],
"packageManager": "yarn@4.9.2",
"scripts": {
"twenty": "twenty",
"lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint --fix -c .oxlintrc.json .",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"twenty-client-sdk": "0.8.0-canary.8",
"twenty-sdk": "0.8.0-canary.8"
},
"devDependencies": {
"@types/node": "^24.7.2",
"@types/react": "^19.0.0",
"oxlint": "^0.16.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"typescript": "^5.9.3",
"vite-tsconfig-paths": "^4.2.1",
"vitest": "^3.1.1"
}
}
@@ -1,70 +0,0 @@
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
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 ?? 'Unknown error'}`,
);
}
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 ?? 'Unknown error'}`,
);
}
const installResult = await appInstall({ appPath: APP_PATH });
if (!installResult.success) {
throw new Error(
`Install failed: ${installResult.error?.message ?? 'Unknown error'}`,
);
}
});
afterAll(async () => {
const uninstallResult = await appUninstall({ appPath: APP_PATH });
if (!uninstallResult.success) {
console.warn(
`App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
);
}
});
it('should find the installed app in the applications list', async () => {
const metadataClient = new MetadataApiClient();
const result = await metadataClient.query({
findManyApplications: {
id: true,
name: true,
universalIdentifier: true,
},
});
const installedApp = result.findManyApplications.find(
(application: { universalIdentifier: string }) =>
application.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
);
expect(installedApp).toBeDefined();
});
});
@@ -1,45 +0,0 @@
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');
const assertServerIsReachable = async () => {
let response: Response;
try {
response = await fetch(`${TWENTY_API_URL}/healthz`);
} catch {
throw new Error(
`Twenty server is not reachable at ${TWENTY_API_URL}. ` +
'Make sure the server is running before executing integration tests.',
);
}
if (!response.ok) {
throw new Error(`Server at ${TWENTY_API_URL} returned ${response.status}`);
}
};
beforeAll(async () => {
await assertServerIsReachable();
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
const configFile = {
remotes: {
local: {
apiUrl: process.env.TWENTY_API_URL,
apiKey: process.env.TWENTY_API_KEY,
},
},
defaultRemote: 'local',
};
fs.writeFileSync(
path.join(TEST_CONFIG_DIR, 'config.json'),
JSON.stringify(configFile, null, 2),
);
});
@@ -1,15 +0,0 @@
import { defineApplication } from 'twenty-sdk';
import {
APP_DESCRIPTION,
APP_DISPLAY_NAME,
APPLICATION_UNIVERSAL_IDENTIFIER,
DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
export default defineApplication({
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
displayName: APP_DISPLAY_NAME,
description: APP_DESCRIPTION,
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
@@ -1,4 +0,0 @@
export const APP_DISPLAY_NAME = 'My twenty app';
export const APP_DESCRIPTION = '';
export const APPLICATION_UNIVERSAL_IDENTIFIER = '83a4b244-c80d-4923-b0d5-ae2406e83072';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'a1a28134-04d1-43ac-b8cc-b547e8cf97ba';
@@ -1,16 +0,0 @@
import { defineRole } from 'twenty-sdk';
import {
APP_DISPLAY_NAME,
DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
export default defineRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: `${APP_DISPLAY_NAME} default function role`,
description: `${APP_DISPLAY_NAME} default function role`,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: false,
});
@@ -1,42 +0,0 @@
{
"compileOnSave": false,
"compilerOptions": {
"sourceMap": true,
"declaration": true,
"outDir": "./dist",
"rootDir": ".",
"jsx": "react-jsx",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"importHelpers": true,
"allowUnreachableCode": false,
"strict": true,
"alwaysStrict": true,
"noImplicitAny": true,
"strictBindCallApply": false,
"target": "es2018",
"module": "esnext",
"lib": ["es2020", "dom"],
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"resolveJsonModule": true,
"paths": {
"src/*": ["./src/*"],
"~/*": ["./*"]
}
},
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.spec.ts",
"**/*.integration-test.ts"
],
"references": [
{
"path": "./tsconfig.spec.json"
}
]
}
@@ -1,9 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"composite": true,
"types": ["vitest/globals", "node"]
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "dist"]
}
@@ -1,21 +0,0 @@
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_KEY:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik',
},
},
});
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "twenty-client-sdk",
"version": "1.20.0",
"version": "0.9.0-canary.0",
"sideEffects": false,
"license": "AGPL-3.0",
"scripts": {
@@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { getIntrospectionQuery, buildClientSchema, printSchema } from 'graphql';
import { buildClientSchema, getIntrospectionQuery, printSchema } from 'graphql';
import { generateMetadataClient } from '../src/generate/generate-metadata-client';
@@ -584,7 +584,7 @@ type ViewField {
createdAt: DateTime!
updatedAt: DateTime!
deletedAt: DateTime
isOverridden: Boolean!
isOverridden: Boolean @deprecated(reason: "isOverridden is deprecated")
}
enum AggregateOperations {
@@ -693,7 +693,7 @@ type ViewFieldGroup {
updatedAt: DateTime!
deletedAt: DateTime
viewFields: [ViewField!]!
isOverridden: Boolean!
isOverridden: Boolean! @deprecated(reason: "isOverridden is deprecated")
}
type View {
@@ -899,7 +899,7 @@ type PageLayoutWidget {
createdAt: DateTime!
updatedAt: DateTime!
deletedAt: DateTime
isOverridden: Boolean!
isOverridden: Boolean @deprecated(reason: "isOverridden is deprecated")
}
enum WidgetType {
@@ -921,6 +921,7 @@ enum WidgetType {
WORKFLOW_RUN
FRONT_COMPONENT
RECORD_TABLE
EMAIL_THREAD
}
union PageLayoutWidgetPosition = PageLayoutWidgetGridPosition | PageLayoutWidgetVerticalListPosition | PageLayoutWidgetCanvasPosition
@@ -948,7 +949,7 @@ type PageLayoutWidgetCanvasPosition {
layoutMode: PageLayoutTabLayoutMode!
}
union WidgetConfiguration = AggregateChartConfiguration | StandaloneRichTextConfiguration | PieChartConfiguration | LineChartConfiguration | IframeConfiguration | GaugeChartConfiguration | BarChartConfiguration | CalendarConfiguration | FrontComponentConfiguration | EmailsConfiguration | FieldConfiguration | FieldRichTextConfiguration | FieldsConfiguration | FilesConfiguration | NotesConfiguration | TasksConfiguration | TimelineConfiguration | ViewConfiguration | RecordTableConfiguration | WorkflowConfiguration | WorkflowRunConfiguration | WorkflowVersionConfiguration
union WidgetConfiguration = AggregateChartConfiguration | StandaloneRichTextConfiguration | PieChartConfiguration | LineChartConfiguration | IframeConfiguration | GaugeChartConfiguration | BarChartConfiguration | CalendarConfiguration | FrontComponentConfiguration | EmailsConfiguration | EmailThreadConfiguration | FieldConfiguration | FieldRichTextConfiguration | FieldsConfiguration | FilesConfiguration | NotesConfiguration | TasksConfiguration | TimelineConfiguration | ViewConfiguration | RecordTableConfiguration | WorkflowConfiguration | WorkflowRunConfiguration | WorkflowVersionConfiguration
type AggregateChartConfiguration {
configurationType: WidgetConfigurationType!
@@ -989,6 +990,7 @@ enum WidgetConfigurationType {
WORKFLOW_RUN
FRONT_COMPONENT
RECORD_TABLE
EMAIL_THREAD
}
type StandaloneRichTextConfiguration {
@@ -1154,6 +1156,10 @@ type EmailsConfiguration {
configurationType: WidgetConfigurationType!
}
type EmailThreadConfiguration {
configurationType: WidgetConfigurationType!
}
type FieldConfiguration {
configurationType: WidgetConfigurationType!
fieldMetadataId: String!
@@ -1227,7 +1233,7 @@ type PageLayoutTab {
createdAt: DateTime!
updatedAt: DateTime!
deletedAt: DateTime
isOverridden: Boolean!
isOverridden: Boolean @deprecated(reason: "isOverridden is deprecated")
}
type PageLayout {
@@ -1628,12 +1634,15 @@ type ClientAIModelConfig {
modelFamily: ModelFamily
modelFamilyLabel: String
sdkPackage: String
inputCostPerMillionTokensInCredits: Float!
outputCostPerMillionTokensInCredits: Float!
inputCostPerMillionTokens: Float
outputCostPerMillionTokens: Float
nativeCapabilities: NativeModelCapabilities
isDeprecated: Boolean
isRecommended: Boolean
providerName: String
providerLabel: String
contextWindowTokens: Float
maxOutputTokens: Float
dataResidency: String
}
@@ -1723,17 +1732,16 @@ enum FeatureFlagKey {
IS_JSON_FILTER_ENABLED
IS_AI_ENABLED
IS_COMMAND_MENU_ITEM_ENABLED
IS_MARKETPLACE_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_DRAFT_EMAIL_ENABLED
IS_CONNECTED_ACCOUNT_MIGRATED
IS_USAGE_ANALYTICS_ENABLED
IS_RICH_TEXT_V1_MIGRATED
IS_DIRECT_GRAPHQL_EXECUTION_ENABLED
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED
IS_CONNECTED_ACCOUNT_MIGRATED
IS_RECORD_TABLE_WIDGET_ENABLED
IS_DATASOURCE_MIGRATED
}
@@ -2542,7 +2550,7 @@ type ConnectedImapSmtpCaldavAccount {
id: UUID!
handle: String!
provider: String!
accountOwnerId: UUID!
userWorkspaceId: UUID!
connectionParameters: ImapSmtpCaldavConnectionParameters
}
@@ -2570,6 +2578,7 @@ type CommandMenuItem {
position: Float!
isPinned: Boolean!
availabilityType: CommandMenuItemAvailabilityType!
payload: CommandMenuItemPayload
hotKeys: [String!]
conditionalAvailabilityExpression: String
availabilityObjectMetadataId: UUID
@@ -2596,19 +2605,11 @@ enum EngineComponentKey {
SEE_DELETED_RECORDS
CREATE_NEW_VIEW
HIDE_DELETED_RECORDS
GO_TO_PEOPLE
GO_TO_COMPANIES
GO_TO_DASHBOARDS
GO_TO_OPPORTUNITIES
GO_TO_SETTINGS
GO_TO_TASKS
GO_TO_NOTES
EDIT_RECORD_PAGE_LAYOUT
EDIT_DASHBOARD_LAYOUT
SAVE_DASHBOARD_LAYOUT
CANCEL_DASHBOARD_LAYOUT
DUPLICATE_DASHBOARD
GO_TO_WORKFLOWS
ACTIVATE_WORKFLOW
DEACTIVATE_WORKFLOW
DISCARD_DRAFT_WORKFLOW
@@ -2619,7 +2620,6 @@ enum EngineComponentKey {
ADD_NODE_WORKFLOW
TIDY_UP_WORKFLOW
DUPLICATE_WORKFLOW
GO_TO_RUNS
SEE_VERSION_WORKFLOW_RUN
SEE_WORKFLOW_WORKFLOW_RUN
STOP_WORKFLOW_RUN
@@ -2631,8 +2631,20 @@ enum EngineComponentKey {
SEARCH_RECORDS_FALLBACK
ASK_AI
VIEW_PREVIOUS_AI_CHATS
NAVIGATION
TRIGGER_WORKFLOW_VERSION
FRONT_COMPONENT_RENDERER
REPLY_TO_EMAIL_THREAD
COMPOSE_EMAIL
GO_TO_PEOPLE
GO_TO_COMPANIES
GO_TO_DASHBOARDS
GO_TO_OPPORTUNITIES
GO_TO_SETTINGS
GO_TO_TASKS
GO_TO_NOTES
GO_TO_WORKFLOWS
GO_TO_RUNS
DELETE_SINGLE_RECORD
DELETE_MULTIPLE_RECORDS
RESTORE_SINGLE_RECORD
@@ -2650,6 +2662,16 @@ enum CommandMenuItemAvailabilityType {
FALLBACK
}
union CommandMenuItemPayload = PathCommandMenuItemPayload | ObjectMetadataCommandMenuItemPayload
type PathCommandMenuItemPayload {
path: String!
}
type ObjectMetadataCommandMenuItemPayload {
objectMetadataItemId: UUID!
}
type ToolIndexEntry {
name: String!
description: String!
@@ -2756,6 +2778,54 @@ type DuplicatedDashboard {
updatedAt: String!
}
type ConnectedAccountDTO {
id: UUID!
handle: String!
provider: String!
lastCredentialsRefreshedAt: DateTime
authFailedAt: DateTime
handleAliases: [String!]
scopes: [String!]
connectionParameters: ImapSmtpCaldavConnectionParameters
lastSignedInAt: DateTime
userWorkspaceId: UUID!
createdAt: DateTime!
updatedAt: DateTime!
}
type PublicConnectionParametersOutput {
host: String!
port: Float!
username: String
secure: Boolean
}
type PublicImapSmtpCaldavConnectionParameters {
IMAP: PublicConnectionParametersOutput
SMTP: PublicConnectionParametersOutput
CALDAV: PublicConnectionParametersOutput
}
type ConnectedAccountPublicDTO {
id: UUID!
handle: String!
provider: String!
lastCredentialsRefreshedAt: DateTime
authFailedAt: DateTime
handleAliases: [String!]
scopes: [String!]
lastSignedInAt: DateTime
userWorkspaceId: UUID!
createdAt: DateTime!
updatedAt: DateTime!
connectionParameters: PublicImapSmtpCaldavConnectionParameters
}
type SendEmailOutput {
success: Boolean!
error: String
}
type EventLogRecord {
event: String!
timestamp: DateTime!
@@ -2924,20 +2994,6 @@ enum CalendarChannelContactAutoCreationPolicy {
NONE
}
type ConnectedAccountDTO {
id: UUID!
handle: String!
provider: String!
lastCredentialsRefreshedAt: DateTime
authFailedAt: DateTime
handleAliases: [String!]
scopes: [String!]
lastSignedInAt: DateTime
userWorkspaceId: UUID!
createdAt: DateTime!
updatedAt: DateTime!
}
type MessageChannel {
id: UUID!
visibility: MessageChannelVisibility!
@@ -3013,7 +3069,7 @@ type MessageFolder {
name: String
isSentFolder: Boolean!
isSynced: Boolean!
parentFolderId: UUID
parentFolderId: String
externalId: String
pendingSyncAction: MessageFolderPendingSyncAction!
messageChannelId: UUID!
@@ -3183,6 +3239,7 @@ type Query {
myMessageFolders(messageChannelId: UUID): [MessageFolder!]!
myMessageChannels(connectedAccountId: UUID): [MessageChannel!]!
myConnectedAccounts: [ConnectedAccountDTO!]!
connectedAccountById(id: UUID!): ConnectedAccountPublicDTO
connectedAccounts: [ConnectedAccountDTO!]!
myCalendarChannels(connectedAccountId: UUID): [CalendarChannel!]!
webhooks: [Webhook!]!
@@ -3220,7 +3277,6 @@ type Query {
findApplicationRegistrationStats(id: String!): ApplicationRegistrationStats!
findApplicationRegistrationVariables(applicationRegistrationId: String!): [ApplicationRegistrationVariable!]!
applicationRegistrationTarballUrl(id: String!): String
getApplicationShareLink(id: String!): String!
currentUser: User!
currentWorkspace: Workspace!
getPublicWorkspaceDataByDomain(origin: String): PublicWorkspaceData!
@@ -3328,6 +3384,7 @@ enum EventLogTable {
PAGEVIEW
OBJECT_EVENT
USAGE_EVENT
APPLICATION_LOG
}
input EventLogFiltersInput {
@@ -3370,6 +3427,7 @@ enum UsageOperationType {
AI_WORKFLOW_TOKEN
WORKFLOW_EXECUTION
CODE_EXECUTION
WEB_SEARCH
}
type Mutation {
@@ -3445,6 +3503,8 @@ type Mutation {
updatePageLayout(id: String!, input: UpdatePageLayoutInput!): PageLayout!
destroyPageLayout(id: String!): Boolean!
updatePageLayoutWithTabsAndWidgets(id: String!, input: UpdatePageLayoutWithTabsInput!): PageLayout!
resetPageLayoutWidgetToDefault(id: String!): PageLayoutWidget!
resetPageLayoutTabToDefault(id: String!): PageLayoutTab!
createPageLayoutWidget(input: CreatePageLayoutWidgetInput!): PageLayoutWidget!
updatePageLayoutWidget(id: String!, input: UpdatePageLayoutWidgetInput!): PageLayoutWidget!
destroyPageLayoutWidget(id: String!): Boolean!
@@ -3545,13 +3605,16 @@ type Mutation {
deleteSSOIdentityProvider(input: DeleteSsoInput!): DeleteSso!
editSSOIdentityProvider(input: EditSsoInput!): EditSso!
impersonate(userId: UUID!, workspaceId: UUID!): Impersonate!
sendEmail(input: SendEmailInput!): SendEmailOutput!
startChannelSync(connectedAccountId: UUID!): ChannelSyncSuccess!
saveImapSmtpCaldavAccount(accountOwnerId: UUID!, handle: String!, connectionParameters: EmailAccountConnectionParameters!, id: UUID): ImapSmtpCaldavConnectionSuccess!
updateLabPublicFeatureFlag(input: UpdateLabPublicFeatureFlagInput!): FeatureFlag!
userLookupAdminPanel(userIdentifier: String!): UserLookup!
updateWorkspaceFeatureFlag(workspaceId: UUID!, featureFlag: String!, value: Boolean!): Boolean!
setAdminAiModelEnabled(modelId: String!, enabled: Boolean!): Boolean!
setAdminAiModelsEnabled(modelIds: [String!]!, enabled: Boolean!): Boolean!
setAdminAiModelRecommended(modelId: String!, recommended: Boolean!): Boolean!
setAdminAiModelsRecommended(modelIds: [String!]!, recommended: Boolean!): Boolean!
setAdminDefaultAiModel(role: AiModelRole!, modelId: String!): Boolean!
createDatabaseConfigVariable(key: String!, value: JSON!): Boolean!
updateDatabaseConfigVariable(key: String!, value: JSON!): Boolean!
@@ -3968,6 +4031,7 @@ input CreatePageLayoutWidgetInput {
}
input UpdatePageLayoutWidgetInput {
pageLayoutTabId: UUID
title: String
type: WidgetType
objectMetadataId: UUID
@@ -4034,6 +4098,7 @@ input CreateCommandMenuItemInput {
hotKeys: [String!]
conditionalAvailabilityExpression: String
availabilityObjectMetadataId: UUID
payload: JSON
}
input UpdateCommandMenuItemInput {
@@ -4515,6 +4580,16 @@ input EditSsoInput {
status: SSOIdentityProviderStatus!
}
input SendEmailInput {
connectedAccountId: String!
to: String!
cc: String
bcc: String
subject: String!
body: String!
inReplyTo: String
}
input EmailAccountConnectionParameters {
IMAP: ConnectionParameters
SMTP: ConnectionParameters
@@ -416,7 +416,8 @@ export interface ViewField {
createdAt: Scalars['DateTime']
updatedAt: Scalars['DateTime']
deletedAt?: Scalars['DateTime']
isOverridden: Scalars['Boolean']
/** @deprecated isOverridden is deprecated */
isOverridden?: Scalars['Boolean']
__typename: 'ViewField'
}
@@ -493,6 +494,7 @@ export interface ViewFieldGroup {
updatedAt: Scalars['DateTime']
deletedAt?: Scalars['DateTime']
viewFields: ViewField[]
/** @deprecated isOverridden is deprecated */
isOverridden: Scalars['Boolean']
__typename: 'ViewFieldGroup'
}
@@ -672,11 +674,12 @@ export interface PageLayoutWidget {
createdAt: Scalars['DateTime']
updatedAt: Scalars['DateTime']
deletedAt?: Scalars['DateTime']
isOverridden: Scalars['Boolean']
/** @deprecated isOverridden is deprecated */
isOverridden?: Scalars['Boolean']
__typename: 'PageLayoutWidget'
}
export type WidgetType = 'VIEW' | 'IFRAME' | 'FIELD' | 'FIELDS' | 'GRAPH' | 'STANDALONE_RICH_TEXT' | 'TIMELINE' | 'TASKS' | 'NOTES' | 'FILES' | 'EMAILS' | 'CALENDAR' | 'FIELD_RICH_TEXT' | 'WORKFLOW' | 'WORKFLOW_VERSION' | 'WORKFLOW_RUN' | 'FRONT_COMPONENT' | 'RECORD_TABLE'
export type WidgetType = 'VIEW' | 'IFRAME' | 'FIELD' | 'FIELDS' | 'GRAPH' | 'STANDALONE_RICH_TEXT' | 'TIMELINE' | 'TASKS' | 'NOTES' | 'FILES' | 'EMAILS' | 'CALENDAR' | 'FIELD_RICH_TEXT' | 'WORKFLOW' | 'WORKFLOW_VERSION' | 'WORKFLOW_RUN' | 'FRONT_COMPONENT' | 'RECORD_TABLE' | 'EMAIL_THREAD'
export type PageLayoutWidgetPosition = (PageLayoutWidgetGridPosition | PageLayoutWidgetVerticalListPosition | PageLayoutWidgetCanvasPosition) & { __isUnion?: true }
@@ -702,7 +705,7 @@ export interface PageLayoutWidgetCanvasPosition {
__typename: 'PageLayoutWidgetCanvasPosition'
}
export type WidgetConfiguration = (AggregateChartConfiguration | StandaloneRichTextConfiguration | PieChartConfiguration | LineChartConfiguration | IframeConfiguration | GaugeChartConfiguration | BarChartConfiguration | CalendarConfiguration | FrontComponentConfiguration | EmailsConfiguration | FieldConfiguration | FieldRichTextConfiguration | FieldsConfiguration | FilesConfiguration | NotesConfiguration | TasksConfiguration | TimelineConfiguration | ViewConfiguration | RecordTableConfiguration | WorkflowConfiguration | WorkflowRunConfiguration | WorkflowVersionConfiguration) & { __isUnion?: true }
export type WidgetConfiguration = (AggregateChartConfiguration | StandaloneRichTextConfiguration | PieChartConfiguration | LineChartConfiguration | IframeConfiguration | GaugeChartConfiguration | BarChartConfiguration | CalendarConfiguration | FrontComponentConfiguration | EmailsConfiguration | EmailThreadConfiguration | FieldConfiguration | FieldRichTextConfiguration | FieldsConfiguration | FilesConfiguration | NotesConfiguration | TasksConfiguration | TimelineConfiguration | ViewConfiguration | RecordTableConfiguration | WorkflowConfiguration | WorkflowRunConfiguration | WorkflowVersionConfiguration) & { __isUnion?: true }
export interface AggregateChartConfiguration {
configurationType: WidgetConfigurationType
@@ -721,7 +724,7 @@ export interface AggregateChartConfiguration {
__typename: 'AggregateChartConfiguration'
}
export type WidgetConfigurationType = 'AGGREGATE_CHART' | 'GAUGE_CHART' | 'PIE_CHART' | 'BAR_CHART' | 'LINE_CHART' | 'IFRAME' | 'STANDALONE_RICH_TEXT' | 'VIEW' | 'FIELD' | 'FIELDS' | 'TIMELINE' | 'TASKS' | 'NOTES' | 'FILES' | 'EMAILS' | 'CALENDAR' | 'FIELD_RICH_TEXT' | 'WORKFLOW' | 'WORKFLOW_VERSION' | 'WORKFLOW_RUN' | 'FRONT_COMPONENT' | 'RECORD_TABLE'
export type WidgetConfigurationType = 'AGGREGATE_CHART' | 'GAUGE_CHART' | 'PIE_CHART' | 'BAR_CHART' | 'LINE_CHART' | 'IFRAME' | 'STANDALONE_RICH_TEXT' | 'VIEW' | 'FIELD' | 'FIELDS' | 'TIMELINE' | 'TASKS' | 'NOTES' | 'FILES' | 'EMAILS' | 'CALENDAR' | 'FIELD_RICH_TEXT' | 'WORKFLOW' | 'WORKFLOW_VERSION' | 'WORKFLOW_RUN' | 'FRONT_COMPONENT' | 'RECORD_TABLE' | 'EMAIL_THREAD'
export interface StandaloneRichTextConfiguration {
configurationType: WidgetConfigurationType
@@ -869,6 +872,11 @@ export interface EmailsConfiguration {
__typename: 'EmailsConfiguration'
}
export interface EmailThreadConfiguration {
configurationType: WidgetConfigurationType
__typename: 'EmailThreadConfiguration'
}
export interface FieldConfiguration {
configurationType: WidgetConfigurationType
fieldMetadataId: Scalars['String']
@@ -951,7 +959,8 @@ export interface PageLayoutTab {
createdAt: Scalars['DateTime']
updatedAt: Scalars['DateTime']
deletedAt?: Scalars['DateTime']
isOverridden: Scalars['Boolean']
/** @deprecated isOverridden is deprecated */
isOverridden?: Scalars['Boolean']
__typename: 'PageLayoutTab'
}
@@ -1336,12 +1345,15 @@ export interface ClientAIModelConfig {
modelFamily?: ModelFamily
modelFamilyLabel?: Scalars['String']
sdkPackage?: Scalars['String']
inputCostPerMillionTokensInCredits: Scalars['Float']
outputCostPerMillionTokensInCredits: Scalars['Float']
inputCostPerMillionTokens?: Scalars['Float']
outputCostPerMillionTokens?: Scalars['Float']
nativeCapabilities?: NativeModelCapabilities
isDeprecated?: Scalars['Boolean']
isRecommended?: Scalars['Boolean']
providerName?: Scalars['String']
providerLabel?: Scalars['String']
contextWindowTokens?: Scalars['Float']
maxOutputTokens?: Scalars['Float']
dataResidency?: Scalars['String']
__typename: 'ClientAIModelConfig'
}
@@ -1424,7 +1436,7 @@ export interface PublicFeatureFlag {
__typename: 'PublicFeatureFlag'
}
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_AI_ENABLED' | 'IS_COMMAND_MENU_ITEM_ENABLED' | 'IS_MARKETPLACE_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_DRAFT_EMAIL_ENABLED' | 'IS_USAGE_ANALYTICS_ENABLED' | 'IS_RICH_TEXT_V1_MIGRATED' | 'IS_DIRECT_GRAPHQL_EXECUTION_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' | 'IS_CONNECTED_ACCOUNT_MIGRATED' | 'IS_RECORD_TABLE_WIDGET_ENABLED' | 'IS_DATASOURCE_MIGRATED'
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_AI_ENABLED' | 'IS_COMMAND_MENU_ITEM_ENABLED' | 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_DRAFT_EMAIL_ENABLED' | 'IS_CONNECTED_ACCOUNT_MIGRATED' | 'IS_USAGE_ANALYTICS_ENABLED' | 'IS_RICH_TEXT_V1_MIGRATED' | 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' | 'IS_RECORD_TABLE_WIDGET_ENABLED' | 'IS_DATASOURCE_MIGRATED'
export interface ClientConfigMaintenanceMode {
startAt: Scalars['DateTime']
@@ -2250,7 +2262,7 @@ export interface ConnectedImapSmtpCaldavAccount {
id: Scalars['UUID']
handle: Scalars['String']
provider: Scalars['String']
accountOwnerId: Scalars['UUID']
userWorkspaceId: Scalars['UUID']
connectionParameters?: ImapSmtpCaldavConnectionParameters
__typename: 'ConnectedImapSmtpCaldavAccount'
}
@@ -2281,6 +2293,7 @@ export interface CommandMenuItem {
position: Scalars['Float']
isPinned: Scalars['Boolean']
availabilityType: CommandMenuItemAvailabilityType
payload?: CommandMenuItemPayload
hotKeys?: Scalars['String'][]
conditionalAvailabilityExpression?: Scalars['String']
availabilityObjectMetadataId?: Scalars['UUID']
@@ -2290,10 +2303,22 @@ export interface CommandMenuItem {
__typename: 'CommandMenuItem'
}
export type EngineComponentKey = 'NAVIGATE_TO_NEXT_RECORD' | 'NAVIGATE_TO_PREVIOUS_RECORD' | 'CREATE_NEW_RECORD' | 'DELETE_RECORDS' | 'RESTORE_RECORDS' | 'DESTROY_RECORDS' | 'ADD_TO_FAVORITES' | 'REMOVE_FROM_FAVORITES' | 'EXPORT_NOTE_TO_PDF' | 'EXPORT_RECORDS' | 'UPDATE_MULTIPLE_RECORDS' | 'MERGE_MULTIPLE_RECORDS' | 'IMPORT_RECORDS' | 'EXPORT_VIEW' | 'SEE_DELETED_RECORDS' | 'CREATE_NEW_VIEW' | 'HIDE_DELETED_RECORDS' | 'GO_TO_PEOPLE' | 'GO_TO_COMPANIES' | 'GO_TO_DASHBOARDS' | 'GO_TO_OPPORTUNITIES' | 'GO_TO_SETTINGS' | 'GO_TO_TASKS' | 'GO_TO_NOTES' | 'EDIT_RECORD_PAGE_LAYOUT' | 'EDIT_DASHBOARD_LAYOUT' | 'SAVE_DASHBOARD_LAYOUT' | 'CANCEL_DASHBOARD_LAYOUT' | 'DUPLICATE_DASHBOARD' | 'GO_TO_WORKFLOWS' | 'ACTIVATE_WORKFLOW' | 'DEACTIVATE_WORKFLOW' | 'DISCARD_DRAFT_WORKFLOW' | 'TEST_WORKFLOW' | 'SEE_ACTIVE_VERSION_WORKFLOW' | 'SEE_RUNS_WORKFLOW' | 'SEE_VERSIONS_WORKFLOW' | 'ADD_NODE_WORKFLOW' | 'TIDY_UP_WORKFLOW' | 'DUPLICATE_WORKFLOW' | 'GO_TO_RUNS' | 'SEE_VERSION_WORKFLOW_RUN' | 'SEE_WORKFLOW_WORKFLOW_RUN' | 'STOP_WORKFLOW_RUN' | 'SEE_RUNS_WORKFLOW_VERSION' | 'SEE_WORKFLOW_WORKFLOW_VERSION' | 'USE_AS_DRAFT_WORKFLOW_VERSION' | 'SEE_VERSIONS_WORKFLOW_VERSION' | 'SEARCH_RECORDS' | 'SEARCH_RECORDS_FALLBACK' | 'ASK_AI' | 'VIEW_PREVIOUS_AI_CHATS' | 'TRIGGER_WORKFLOW_VERSION' | 'FRONT_COMPONENT_RENDERER' | 'DELETE_SINGLE_RECORD' | 'DELETE_MULTIPLE_RECORDS' | 'RESTORE_SINGLE_RECORD' | 'RESTORE_MULTIPLE_RECORDS' | 'DESTROY_SINGLE_RECORD' | 'DESTROY_MULTIPLE_RECORDS' | 'EXPORT_FROM_RECORD_INDEX' | 'EXPORT_FROM_RECORD_SHOW' | 'EXPORT_MULTIPLE_RECORDS'
export type EngineComponentKey = 'NAVIGATE_TO_NEXT_RECORD' | 'NAVIGATE_TO_PREVIOUS_RECORD' | 'CREATE_NEW_RECORD' | 'DELETE_RECORDS' | 'RESTORE_RECORDS' | 'DESTROY_RECORDS' | 'ADD_TO_FAVORITES' | 'REMOVE_FROM_FAVORITES' | 'EXPORT_NOTE_TO_PDF' | 'EXPORT_RECORDS' | 'UPDATE_MULTIPLE_RECORDS' | 'MERGE_MULTIPLE_RECORDS' | 'IMPORT_RECORDS' | 'EXPORT_VIEW' | 'SEE_DELETED_RECORDS' | 'CREATE_NEW_VIEW' | 'HIDE_DELETED_RECORDS' | 'EDIT_RECORD_PAGE_LAYOUT' | 'EDIT_DASHBOARD_LAYOUT' | 'SAVE_DASHBOARD_LAYOUT' | 'CANCEL_DASHBOARD_LAYOUT' | 'DUPLICATE_DASHBOARD' | 'ACTIVATE_WORKFLOW' | 'DEACTIVATE_WORKFLOW' | 'DISCARD_DRAFT_WORKFLOW' | 'TEST_WORKFLOW' | 'SEE_ACTIVE_VERSION_WORKFLOW' | 'SEE_RUNS_WORKFLOW' | 'SEE_VERSIONS_WORKFLOW' | 'ADD_NODE_WORKFLOW' | 'TIDY_UP_WORKFLOW' | 'DUPLICATE_WORKFLOW' | 'SEE_VERSION_WORKFLOW_RUN' | 'SEE_WORKFLOW_WORKFLOW_RUN' | 'STOP_WORKFLOW_RUN' | 'SEE_RUNS_WORKFLOW_VERSION' | 'SEE_WORKFLOW_WORKFLOW_VERSION' | 'USE_AS_DRAFT_WORKFLOW_VERSION' | 'SEE_VERSIONS_WORKFLOW_VERSION' | 'SEARCH_RECORDS' | 'SEARCH_RECORDS_FALLBACK' | 'ASK_AI' | 'VIEW_PREVIOUS_AI_CHATS' | 'NAVIGATION' | 'TRIGGER_WORKFLOW_VERSION' | 'FRONT_COMPONENT_RENDERER' | 'REPLY_TO_EMAIL_THREAD' | 'COMPOSE_EMAIL' | 'GO_TO_PEOPLE' | 'GO_TO_COMPANIES' | 'GO_TO_DASHBOARDS' | 'GO_TO_OPPORTUNITIES' | 'GO_TO_SETTINGS' | 'GO_TO_TASKS' | 'GO_TO_NOTES' | 'GO_TO_WORKFLOWS' | 'GO_TO_RUNS' | 'DELETE_SINGLE_RECORD' | 'DELETE_MULTIPLE_RECORDS' | 'RESTORE_SINGLE_RECORD' | 'RESTORE_MULTIPLE_RECORDS' | 'DESTROY_SINGLE_RECORD' | 'DESTROY_MULTIPLE_RECORDS' | 'EXPORT_FROM_RECORD_INDEX' | 'EXPORT_FROM_RECORD_SHOW' | 'EXPORT_MULTIPLE_RECORDS'
export type CommandMenuItemAvailabilityType = 'GLOBAL' | 'RECORD_SELECTION' | 'FALLBACK'
export type CommandMenuItemPayload = (PathCommandMenuItemPayload | ObjectMetadataCommandMenuItemPayload) & { __isUnion?: true }
export interface PathCommandMenuItemPayload {
path: Scalars['String']
__typename: 'PathCommandMenuItemPayload'
}
export interface ObjectMetadataCommandMenuItemPayload {
objectMetadataItemId: Scalars['UUID']
__typename: 'ObjectMetadataCommandMenuItemPayload'
}
export interface ToolIndexEntry {
name: Scalars['String']
description: Scalars['String']
@@ -2411,6 +2436,59 @@ export interface DuplicatedDashboard {
__typename: 'DuplicatedDashboard'
}
export interface ConnectedAccountDTO {
id: Scalars['UUID']
handle: Scalars['String']
provider: Scalars['String']
lastCredentialsRefreshedAt?: Scalars['DateTime']
authFailedAt?: Scalars['DateTime']
handleAliases?: Scalars['String'][]
scopes?: Scalars['String'][]
connectionParameters?: ImapSmtpCaldavConnectionParameters
lastSignedInAt?: Scalars['DateTime']
userWorkspaceId: Scalars['UUID']
createdAt: Scalars['DateTime']
updatedAt: Scalars['DateTime']
__typename: 'ConnectedAccountDTO'
}
export interface PublicConnectionParametersOutput {
host: Scalars['String']
port: Scalars['Float']
username?: Scalars['String']
secure?: Scalars['Boolean']
__typename: 'PublicConnectionParametersOutput'
}
export interface PublicImapSmtpCaldavConnectionParameters {
IMAP?: PublicConnectionParametersOutput
SMTP?: PublicConnectionParametersOutput
CALDAV?: PublicConnectionParametersOutput
__typename: 'PublicImapSmtpCaldavConnectionParameters'
}
export interface ConnectedAccountPublicDTO {
id: Scalars['UUID']
handle: Scalars['String']
provider: Scalars['String']
lastCredentialsRefreshedAt?: Scalars['DateTime']
authFailedAt?: Scalars['DateTime']
handleAliases?: Scalars['String'][]
scopes?: Scalars['String'][]
lastSignedInAt?: Scalars['DateTime']
userWorkspaceId: Scalars['UUID']
createdAt: Scalars['DateTime']
updatedAt: Scalars['DateTime']
connectionParameters?: PublicImapSmtpCaldavConnectionParameters
__typename: 'ConnectedAccountPublicDTO'
}
export interface SendEmailOutput {
success: Scalars['Boolean']
error?: Scalars['String']
__typename: 'SendEmailOutput'
}
export interface EventLogRecord {
event: Scalars['String']
timestamp: Scalars['DateTime']
@@ -2570,21 +2648,6 @@ export type CalendarChannelVisibility = 'METADATA' | 'SHARE_EVERYTHING'
export type CalendarChannelContactAutoCreationPolicy = 'AS_PARTICIPANT_AND_ORGANIZER' | 'AS_PARTICIPANT' | 'AS_ORGANIZER' | 'NONE'
export interface ConnectedAccountDTO {
id: Scalars['UUID']
handle: Scalars['String']
provider: Scalars['String']
lastCredentialsRefreshedAt?: Scalars['DateTime']
authFailedAt?: Scalars['DateTime']
handleAliases?: Scalars['String'][]
scopes?: Scalars['String'][]
lastSignedInAt?: Scalars['DateTime']
userWorkspaceId: Scalars['UUID']
createdAt: Scalars['DateTime']
updatedAt: Scalars['DateTime']
__typename: 'ConnectedAccountDTO'
}
export interface MessageChannel {
id: Scalars['UUID']
visibility: MessageChannelVisibility
@@ -2628,7 +2691,7 @@ export interface MessageFolder {
name?: Scalars['String']
isSentFolder: Scalars['Boolean']
isSynced: Scalars['Boolean']
parentFolderId?: Scalars['UUID']
parentFolderId?: Scalars['String']
externalId?: Scalars['String']
pendingSyncAction: MessageFolderPendingSyncAction
messageChannelId: Scalars['UUID']
@@ -2746,6 +2809,7 @@ export interface Query {
myMessageFolders: MessageFolder[]
myMessageChannels: MessageChannel[]
myConnectedAccounts: ConnectedAccountDTO[]
connectedAccountById?: ConnectedAccountPublicDTO
connectedAccounts: ConnectedAccountDTO[]
myCalendarChannels: CalendarChannel[]
webhooks: Webhook[]
@@ -2774,7 +2838,6 @@ export interface Query {
findApplicationRegistrationStats: ApplicationRegistrationStats
findApplicationRegistrationVariables: ApplicationRegistrationVariable[]
applicationRegistrationTarballUrl?: Scalars['String']
getApplicationShareLink: Scalars['String']
currentUser: User
currentWorkspace: Workspace
getPublicWorkspaceDataByDomain: PublicWorkspaceData
@@ -2817,9 +2880,9 @@ export type SortDirection = 'ASC' | 'DESC'
/** Sort Nulls Options */
export type SortNulls = 'NULLS_FIRST' | 'NULLS_LAST'
export type EventLogTable = 'WORKSPACE_EVENT' | 'PAGEVIEW' | 'OBJECT_EVENT' | 'USAGE_EVENT'
export type EventLogTable = 'WORKSPACE_EVENT' | 'PAGEVIEW' | 'OBJECT_EVENT' | 'USAGE_EVENT' | 'APPLICATION_LOG'
export type UsageOperationType = 'AI_CHAT_TOKEN' | 'AI_WORKFLOW_TOKEN' | 'WORKFLOW_EXECUTION' | 'CODE_EXECUTION'
export type UsageOperationType = 'AI_CHAT_TOKEN' | 'AI_WORKFLOW_TOKEN' | 'WORKFLOW_EXECUTION' | 'CODE_EXECUTION' | 'WEB_SEARCH'
export interface Mutation {
addQueryToEventStream: Scalars['Boolean']
@@ -2894,6 +2957,8 @@ export interface Mutation {
updatePageLayout: PageLayout
destroyPageLayout: Scalars['Boolean']
updatePageLayoutWithTabsAndWidgets: PageLayout
resetPageLayoutWidgetToDefault: PageLayoutWidget
resetPageLayoutTabToDefault: PageLayoutTab
createPageLayoutWidget: PageLayoutWidget
updatePageLayoutWidget: PageLayoutWidget
destroyPageLayoutWidget: Scalars['Boolean']
@@ -2994,13 +3059,16 @@ export interface Mutation {
deleteSSOIdentityProvider: DeleteSso
editSSOIdentityProvider: EditSso
impersonate: Impersonate
sendEmail: SendEmailOutput
startChannelSync: ChannelSyncSuccess
saveImapSmtpCaldavAccount: ImapSmtpCaldavConnectionSuccess
updateLabPublicFeatureFlag: FeatureFlag
userLookupAdminPanel: UserLookup
updateWorkspaceFeatureFlag: Scalars['Boolean']
setAdminAiModelEnabled: Scalars['Boolean']
setAdminAiModelsEnabled: Scalars['Boolean']
setAdminAiModelRecommended: Scalars['Boolean']
setAdminAiModelsRecommended: Scalars['Boolean']
setAdminDefaultAiModel: Scalars['Boolean']
createDatabaseConfigVariable: Scalars['Boolean']
updateDatabaseConfigVariable: Scalars['Boolean']
@@ -3499,6 +3567,7 @@ export interface ViewFieldGenqlSelection{
createdAt?: boolean | number
updatedAt?: boolean | number
deletedAt?: boolean | number
/** @deprecated isOverridden is deprecated */
isOverridden?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
@@ -3573,6 +3642,7 @@ export interface ViewFieldGroupGenqlSelection{
updatedAt?: boolean | number
deletedAt?: boolean | number
viewFields?: ViewFieldGenqlSelection
/** @deprecated isOverridden is deprecated */
isOverridden?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
@@ -3744,6 +3814,7 @@ export interface PageLayoutWidgetGenqlSelection{
createdAt?: boolean | number
updatedAt?: boolean | number
deletedAt?: boolean | number
/** @deprecated isOverridden is deprecated */
isOverridden?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
@@ -3790,6 +3861,7 @@ export interface WidgetConfigurationGenqlSelection{
on_CalendarConfiguration?:CalendarConfigurationGenqlSelection,
on_FrontComponentConfiguration?:FrontComponentConfigurationGenqlSelection,
on_EmailsConfiguration?:EmailsConfigurationGenqlSelection,
on_EmailThreadConfiguration?:EmailThreadConfigurationGenqlSelection,
on_FieldConfiguration?:FieldConfigurationGenqlSelection,
on_FieldRichTextConfiguration?:FieldRichTextConfigurationGenqlSelection,
on_FieldsConfiguration?:FieldsConfigurationGenqlSelection,
@@ -3958,6 +4030,12 @@ export interface EmailsConfigurationGenqlSelection{
__scalar?: boolean | number
}
export interface EmailThreadConfigurationGenqlSelection{
configurationType?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface FieldConfigurationGenqlSelection{
configurationType?: boolean | number
fieldMetadataId?: boolean | number
@@ -4048,6 +4126,7 @@ export interface PageLayoutTabGenqlSelection{
createdAt?: boolean | number
updatedAt?: boolean | number
deletedAt?: boolean | number
/** @deprecated isOverridden is deprecated */
isOverridden?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
@@ -4440,12 +4519,15 @@ export interface ClientAIModelConfigGenqlSelection{
modelFamily?: boolean | number
modelFamilyLabel?: boolean | number
sdkPackage?: boolean | number
inputCostPerMillionTokensInCredits?: boolean | number
outputCostPerMillionTokensInCredits?: boolean | number
inputCostPerMillionTokens?: boolean | number
outputCostPerMillionTokens?: boolean | number
nativeCapabilities?: NativeModelCapabilitiesGenqlSelection
isDeprecated?: boolean | number
isRecommended?: boolean | number
providerName?: boolean | number
providerLabel?: boolean | number
contextWindowTokens?: boolean | number
maxOutputTokens?: boolean | number
dataResidency?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
@@ -5431,7 +5513,7 @@ export interface ConnectedImapSmtpCaldavAccountGenqlSelection{
id?: boolean | number
handle?: boolean | number
provider?: boolean | number
accountOwnerId?: boolean | number
userWorkspaceId?: boolean | number
connectionParameters?: ImapSmtpCaldavConnectionParametersGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
@@ -5465,6 +5547,7 @@ export interface CommandMenuItemGenqlSelection{
position?: boolean | number
isPinned?: boolean | number
availabilityType?: boolean | number
payload?: CommandMenuItemPayloadGenqlSelection
hotKeys?: boolean | number
conditionalAvailabilityExpression?: boolean | number
availabilityObjectMetadataId?: boolean | number
@@ -5475,6 +5558,24 @@ export interface CommandMenuItemGenqlSelection{
__scalar?: boolean | number
}
export interface CommandMenuItemPayloadGenqlSelection{
on_PathCommandMenuItemPayload?:PathCommandMenuItemPayloadGenqlSelection,
on_ObjectMetadataCommandMenuItemPayload?:ObjectMetadataCommandMenuItemPayloadGenqlSelection,
__typename?: boolean | number
}
export interface PathCommandMenuItemPayloadGenqlSelection{
path?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface ObjectMetadataCommandMenuItemPayloadGenqlSelection{
objectMetadataItemId?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface ToolIndexEntryGenqlSelection{
name?: boolean | number
description?: boolean | number
@@ -5603,6 +5704,64 @@ export interface DuplicatedDashboardGenqlSelection{
__scalar?: boolean | number
}
export interface ConnectedAccountDTOGenqlSelection{
id?: boolean | number
handle?: boolean | number
provider?: boolean | number
lastCredentialsRefreshedAt?: boolean | number
authFailedAt?: boolean | number
handleAliases?: boolean | number
scopes?: boolean | number
connectionParameters?: ImapSmtpCaldavConnectionParametersGenqlSelection
lastSignedInAt?: boolean | number
userWorkspaceId?: boolean | number
createdAt?: boolean | number
updatedAt?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface PublicConnectionParametersOutputGenqlSelection{
host?: boolean | number
port?: boolean | number
username?: boolean | number
secure?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface PublicImapSmtpCaldavConnectionParametersGenqlSelection{
IMAP?: PublicConnectionParametersOutputGenqlSelection
SMTP?: PublicConnectionParametersOutputGenqlSelection
CALDAV?: PublicConnectionParametersOutputGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
export interface ConnectedAccountPublicDTOGenqlSelection{
id?: boolean | number
handle?: boolean | number
provider?: boolean | number
lastCredentialsRefreshedAt?: boolean | number
authFailedAt?: boolean | number
handleAliases?: boolean | number
scopes?: boolean | number
lastSignedInAt?: boolean | number
userWorkspaceId?: boolean | number
createdAt?: boolean | number
updatedAt?: boolean | number
connectionParameters?: PublicImapSmtpCaldavConnectionParametersGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
export interface SendEmailOutputGenqlSelection{
success?: boolean | number
error?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface EventLogRecordGenqlSelection{
event?: boolean | number
timestamp?: boolean | number
@@ -5770,22 +5929,6 @@ export interface CalendarChannelGenqlSelection{
__scalar?: boolean | number
}
export interface ConnectedAccountDTOGenqlSelection{
id?: boolean | number
handle?: boolean | number
provider?: boolean | number
lastCredentialsRefreshedAt?: boolean | number
authFailedAt?: boolean | number
handleAliases?: boolean | number
scopes?: boolean | number
lastSignedInAt?: boolean | number
userWorkspaceId?: boolean | number
createdAt?: boolean | number
updatedAt?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface MessageChannelGenqlSelection{
id?: boolean | number
visibility?: boolean | number
@@ -5954,6 +6097,7 @@ export interface QueryGenqlSelection{
myMessageFolders?: (MessageFolderGenqlSelection & { __args?: {messageChannelId?: (Scalars['UUID'] | null)} })
myMessageChannels?: (MessageChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} })
myConnectedAccounts?: ConnectedAccountDTOGenqlSelection
connectedAccountById?: (ConnectedAccountPublicDTOGenqlSelection & { __args: {id: Scalars['UUID']} })
connectedAccounts?: ConnectedAccountDTOGenqlSelection
myCalendarChannels?: (CalendarChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} })
webhooks?: WebhookGenqlSelection
@@ -5988,7 +6132,6 @@ export interface QueryGenqlSelection{
findApplicationRegistrationStats?: (ApplicationRegistrationStatsGenqlSelection & { __args: {id: Scalars['String']} })
findApplicationRegistrationVariables?: (ApplicationRegistrationVariableGenqlSelection & { __args: {applicationRegistrationId: Scalars['String']} })
applicationRegistrationTarballUrl?: { __args: {id: Scalars['String']} }
getApplicationShareLink?: { __args: {id: Scalars['String']} }
currentUser?: UserGenqlSelection
currentWorkspace?: WorkspaceGenqlSelection
getPublicWorkspaceDataByDomain?: (PublicWorkspaceDataGenqlSelection & { __args?: {origin?: (Scalars['String'] | null)} })
@@ -6127,6 +6270,8 @@ export interface MutationGenqlSelection{
updatePageLayout?: (PageLayoutGenqlSelection & { __args: {id: Scalars['String'], input: UpdatePageLayoutInput} })
destroyPageLayout?: { __args: {id: Scalars['String']} }
updatePageLayoutWithTabsAndWidgets?: (PageLayoutGenqlSelection & { __args: {id: Scalars['String'], input: UpdatePageLayoutWithTabsInput} })
resetPageLayoutWidgetToDefault?: (PageLayoutWidgetGenqlSelection & { __args: {id: Scalars['String']} })
resetPageLayoutTabToDefault?: (PageLayoutTabGenqlSelection & { __args: {id: Scalars['String']} })
createPageLayoutWidget?: (PageLayoutWidgetGenqlSelection & { __args: {input: CreatePageLayoutWidgetInput} })
updatePageLayoutWidget?: (PageLayoutWidgetGenqlSelection & { __args: {id: Scalars['String'], input: UpdatePageLayoutWidgetInput} })
destroyPageLayoutWidget?: { __args: {id: Scalars['String']} }
@@ -6227,13 +6372,16 @@ export interface MutationGenqlSelection{
deleteSSOIdentityProvider?: (DeleteSsoGenqlSelection & { __args: {input: DeleteSsoInput} })
editSSOIdentityProvider?: (EditSsoGenqlSelection & { __args: {input: EditSsoInput} })
impersonate?: (ImpersonateGenqlSelection & { __args: {userId: Scalars['UUID'], workspaceId: Scalars['UUID']} })
sendEmail?: (SendEmailOutputGenqlSelection & { __args: {input: SendEmailInput} })
startChannelSync?: (ChannelSyncSuccessGenqlSelection & { __args: {connectedAccountId: Scalars['UUID']} })
saveImapSmtpCaldavAccount?: (ImapSmtpCaldavConnectionSuccessGenqlSelection & { __args: {accountOwnerId: Scalars['UUID'], handle: Scalars['String'], connectionParameters: EmailAccountConnectionParameters, id?: (Scalars['UUID'] | null)} })
updateLabPublicFeatureFlag?: (FeatureFlagGenqlSelection & { __args: {input: UpdateLabPublicFeatureFlagInput} })
userLookupAdminPanel?: (UserLookupGenqlSelection & { __args: {userIdentifier: Scalars['String']} })
updateWorkspaceFeatureFlag?: { __args: {workspaceId: Scalars['UUID'], featureFlag: Scalars['String'], value: Scalars['Boolean']} }
setAdminAiModelEnabled?: { __args: {modelId: Scalars['String'], enabled: Scalars['Boolean']} }
setAdminAiModelsEnabled?: { __args: {modelIds: Scalars['String'][], enabled: Scalars['Boolean']} }
setAdminAiModelRecommended?: { __args: {modelId: Scalars['String'], recommended: Scalars['Boolean']} }
setAdminAiModelsRecommended?: { __args: {modelIds: Scalars['String'][], recommended: Scalars['Boolean']} }
setAdminDefaultAiModel?: { __args: {role: AiModelRole, modelId: Scalars['String']} }
createDatabaseConfigVariable?: { __args: {key: Scalars['String'], value: Scalars['JSON']} }
updateDatabaseConfigVariable?: { __args: {key: Scalars['String'], value: Scalars['JSON']} }
@@ -6411,7 +6559,7 @@ export interface GridPositionInput {row: Scalars['Float'],column: Scalars['Float
export interface CreatePageLayoutWidgetInput {pageLayoutTabId: Scalars['UUID'],title: Scalars['String'],type: WidgetType,objectMetadataId?: (Scalars['UUID'] | null),gridPosition: GridPositionInput,position?: (Scalars['JSON'] | null),configuration: Scalars['JSON']}
export interface UpdatePageLayoutWidgetInput {title?: (Scalars['String'] | null),type?: (WidgetType | null),objectMetadataId?: (Scalars['UUID'] | null),gridPosition?: (GridPositionInput | null),position?: (Scalars['JSON'] | null),configuration?: (Scalars['JSON'] | null),conditionalDisplay?: (Scalars['JSON'] | null)}
export interface UpdatePageLayoutWidgetInput {pageLayoutTabId?: (Scalars['UUID'] | null),title?: (Scalars['String'] | null),type?: (WidgetType | null),objectMetadataId?: (Scalars['UUID'] | null),gridPosition?: (GridPositionInput | null),position?: (Scalars['JSON'] | null),configuration?: (Scalars['JSON'] | null),conditionalDisplay?: (Scalars['JSON'] | null)}
export interface CreateLogicFunctionFromSourceInput {id?: (Scalars['UUID'] | null),universalIdentifier?: (Scalars['UUID'] | null),name: Scalars['String'],description?: (Scalars['String'] | null),timeoutSeconds?: (Scalars['Float'] | null),toolInputSchema?: (Scalars['JSON'] | null),isTool?: (Scalars['Boolean'] | null),source?: (Scalars['JSON'] | null),cronTriggerSettings?: (Scalars['JSON'] | null),databaseEventTriggerSettings?: (Scalars['JSON'] | null),httpRouteTriggerSettings?: (Scalars['JSON'] | null)}
@@ -6429,7 +6577,7 @@ update: UpdateLogicFunctionFromSourceInputUpdates}
export interface UpdateLogicFunctionFromSourceInputUpdates {name?: (Scalars['String'] | null),description?: (Scalars['String'] | null),timeoutSeconds?: (Scalars['Float'] | null),sourceHandlerCode?: (Scalars['String'] | null),toolInputSchema?: (Scalars['JSON'] | null),handlerName?: (Scalars['String'] | null),sourceHandlerPath?: (Scalars['String'] | null),isTool?: (Scalars['Boolean'] | null),cronTriggerSettings?: (Scalars['JSON'] | null),databaseEventTriggerSettings?: (Scalars['JSON'] | null),httpRouteTriggerSettings?: (Scalars['JSON'] | null)}
export interface CreateCommandMenuItemInput {workflowVersionId?: (Scalars['UUID'] | null),frontComponentId?: (Scalars['UUID'] | null),engineComponentKey: EngineComponentKey,label: Scalars['String'],icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),hotKeys?: (Scalars['String'][] | null),conditionalAvailabilityExpression?: (Scalars['String'] | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null)}
export interface CreateCommandMenuItemInput {workflowVersionId?: (Scalars['UUID'] | null),frontComponentId?: (Scalars['UUID'] | null),engineComponentKey: EngineComponentKey,label: Scalars['String'],icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),hotKeys?: (Scalars['String'][] | null),conditionalAvailabilityExpression?: (Scalars['String'] | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null),payload?: (Scalars['JSON'] | null)}
export interface UpdateCommandMenuItemInput {id: Scalars['UUID'],label?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null),engineComponentKey?: (EngineComponentKey | null),hotKeys?: (Scalars['String'][] | null)}
@@ -6577,6 +6725,8 @@ export interface DeleteSsoInput {identityProviderId: Scalars['UUID']}
export interface EditSsoInput {id: Scalars['UUID'],status: SSOIdentityProviderStatus}
export interface SendEmailInput {connectedAccountId: Scalars['String'],to: Scalars['String'],cc?: (Scalars['String'] | null),bcc?: (Scalars['String'] | null),subject: Scalars['String'],body: Scalars['String'],inReplyTo?: (Scalars['String'] | null)}
export interface EmailAccountConnectionParameters {IMAP?: (ConnectionParameters | null),SMTP?: (ConnectionParameters | null),CALDAV?: (ConnectionParameters | null)}
export interface ConnectionParameters {host: Scalars['String'],port: Scalars['Float'],username?: (Scalars['String'] | null),password: Scalars['String'],secure?: (Scalars['Boolean'] | null)}
@@ -6956,7 +7106,7 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const WidgetConfiguration_possibleTypes: string[] = ['AggregateChartConfiguration','StandaloneRichTextConfiguration','PieChartConfiguration','LineChartConfiguration','IframeConfiguration','GaugeChartConfiguration','BarChartConfiguration','CalendarConfiguration','FrontComponentConfiguration','EmailsConfiguration','FieldConfiguration','FieldRichTextConfiguration','FieldsConfiguration','FilesConfiguration','NotesConfiguration','TasksConfiguration','TimelineConfiguration','ViewConfiguration','RecordTableConfiguration','WorkflowConfiguration','WorkflowRunConfiguration','WorkflowVersionConfiguration']
const WidgetConfiguration_possibleTypes: string[] = ['AggregateChartConfiguration','StandaloneRichTextConfiguration','PieChartConfiguration','LineChartConfiguration','IframeConfiguration','GaugeChartConfiguration','BarChartConfiguration','CalendarConfiguration','FrontComponentConfiguration','EmailsConfiguration','EmailThreadConfiguration','FieldConfiguration','FieldRichTextConfiguration','FieldsConfiguration','FilesConfiguration','NotesConfiguration','TasksConfiguration','TimelineConfiguration','ViewConfiguration','RecordTableConfiguration','WorkflowConfiguration','WorkflowRunConfiguration','WorkflowVersionConfiguration']
export const isWidgetConfiguration = (obj?: { __typename?: any } | null): obj is WidgetConfiguration => {
if (!obj?.__typename) throw new Error('__typename is missing in "isWidgetConfiguration"')
return WidgetConfiguration_possibleTypes.includes(obj.__typename)
@@ -7044,6 +7194,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const EmailThreadConfiguration_possibleTypes: string[] = ['EmailThreadConfiguration']
export const isEmailThreadConfiguration = (obj?: { __typename?: any } | null): obj is EmailThreadConfiguration => {
if (!obj?.__typename) throw new Error('__typename is missing in "isEmailThreadConfiguration"')
return EmailThreadConfiguration_possibleTypes.includes(obj.__typename)
}
const FieldConfiguration_possibleTypes: string[] = ['FieldConfiguration']
export const isFieldConfiguration = (obj?: { __typename?: any } | null): obj is FieldConfiguration => {
if (!obj?.__typename) throw new Error('__typename is missing in "isFieldConfiguration"')
@@ -8388,6 +8546,30 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const CommandMenuItemPayload_possibleTypes: string[] = ['PathCommandMenuItemPayload','ObjectMetadataCommandMenuItemPayload']
export const isCommandMenuItemPayload = (obj?: { __typename?: any } | null): obj is CommandMenuItemPayload => {
if (!obj?.__typename) throw new Error('__typename is missing in "isCommandMenuItemPayload"')
return CommandMenuItemPayload_possibleTypes.includes(obj.__typename)
}
const PathCommandMenuItemPayload_possibleTypes: string[] = ['PathCommandMenuItemPayload']
export const isPathCommandMenuItemPayload = (obj?: { __typename?: any } | null): obj is PathCommandMenuItemPayload => {
if (!obj?.__typename) throw new Error('__typename is missing in "isPathCommandMenuItemPayload"')
return PathCommandMenuItemPayload_possibleTypes.includes(obj.__typename)
}
const ObjectMetadataCommandMenuItemPayload_possibleTypes: string[] = ['ObjectMetadataCommandMenuItemPayload']
export const isObjectMetadataCommandMenuItemPayload = (obj?: { __typename?: any } | null): obj is ObjectMetadataCommandMenuItemPayload => {
if (!obj?.__typename) throw new Error('__typename is missing in "isObjectMetadataCommandMenuItemPayload"')
return ObjectMetadataCommandMenuItemPayload_possibleTypes.includes(obj.__typename)
}
const ToolIndexEntry_possibleTypes: string[] = ['ToolIndexEntry']
export const isToolIndexEntry = (obj?: { __typename?: any } | null): obj is ToolIndexEntry => {
if (!obj?.__typename) throw new Error('__typename is missing in "isToolIndexEntry"')
@@ -8476,6 +8658,46 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const ConnectedAccountDTO_possibleTypes: string[] = ['ConnectedAccountDTO']
export const isConnectedAccountDTO = (obj?: { __typename?: any } | null): obj is ConnectedAccountDTO => {
if (!obj?.__typename) throw new Error('__typename is missing in "isConnectedAccountDTO"')
return ConnectedAccountDTO_possibleTypes.includes(obj.__typename)
}
const PublicConnectionParametersOutput_possibleTypes: string[] = ['PublicConnectionParametersOutput']
export const isPublicConnectionParametersOutput = (obj?: { __typename?: any } | null): obj is PublicConnectionParametersOutput => {
if (!obj?.__typename) throw new Error('__typename is missing in "isPublicConnectionParametersOutput"')
return PublicConnectionParametersOutput_possibleTypes.includes(obj.__typename)
}
const PublicImapSmtpCaldavConnectionParameters_possibleTypes: string[] = ['PublicImapSmtpCaldavConnectionParameters']
export const isPublicImapSmtpCaldavConnectionParameters = (obj?: { __typename?: any } | null): obj is PublicImapSmtpCaldavConnectionParameters => {
if (!obj?.__typename) throw new Error('__typename is missing in "isPublicImapSmtpCaldavConnectionParameters"')
return PublicImapSmtpCaldavConnectionParameters_possibleTypes.includes(obj.__typename)
}
const ConnectedAccountPublicDTO_possibleTypes: string[] = ['ConnectedAccountPublicDTO']
export const isConnectedAccountPublicDTO = (obj?: { __typename?: any } | null): obj is ConnectedAccountPublicDTO => {
if (!obj?.__typename) throw new Error('__typename is missing in "isConnectedAccountPublicDTO"')
return ConnectedAccountPublicDTO_possibleTypes.includes(obj.__typename)
}
const SendEmailOutput_possibleTypes: string[] = ['SendEmailOutput']
export const isSendEmailOutput = (obj?: { __typename?: any } | null): obj is SendEmailOutput => {
if (!obj?.__typename) throw new Error('__typename is missing in "isSendEmailOutput"')
return SendEmailOutput_possibleTypes.includes(obj.__typename)
}
const EventLogRecord_possibleTypes: string[] = ['EventLogRecord']
export const isEventLogRecord = (obj?: { __typename?: any } | null): obj is EventLogRecord => {
if (!obj?.__typename) throw new Error('__typename is missing in "isEventLogRecord"')
@@ -8604,14 +8826,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const ConnectedAccountDTO_possibleTypes: string[] = ['ConnectedAccountDTO']
export const isConnectedAccountDTO = (obj?: { __typename?: any } | null): obj is ConnectedAccountDTO => {
if (!obj?.__typename) throw new Error('__typename is missing in "isConnectedAccountDTO"')
return ConnectedAccountDTO_possibleTypes.includes(obj.__typename)
}
const MessageChannel_possibleTypes: string[] = ['MessageChannel']
export const isMessageChannel = (obj?: { __typename?: any } | null): obj is MessageChannel => {
if (!obj?.__typename) throw new Error('__typename is missing in "isMessageChannel"')
@@ -8912,7 +9126,8 @@ export const enumWidgetType = {
WORKFLOW_VERSION: 'WORKFLOW_VERSION' as const,
WORKFLOW_RUN: 'WORKFLOW_RUN' as const,
FRONT_COMPONENT: 'FRONT_COMPONENT' as const,
RECORD_TABLE: 'RECORD_TABLE' as const
RECORD_TABLE: 'RECORD_TABLE' as const,
EMAIL_THREAD: 'EMAIL_THREAD' as const
}
export const enumPageLayoutTabLayoutMode = {
@@ -8943,7 +9158,8 @@ export const enumWidgetConfigurationType = {
WORKFLOW_VERSION: 'WORKFLOW_VERSION' as const,
WORKFLOW_RUN: 'WORKFLOW_RUN' as const,
FRONT_COMPONENT: 'FRONT_COMPONENT' as const,
RECORD_TABLE: 'RECORD_TABLE' as const
RECORD_TABLE: 'RECORD_TABLE' as const,
EMAIL_THREAD: 'EMAIL_THREAD' as const
}
export const enumObjectRecordGroupByDateGranularity = {
@@ -9091,17 +9307,16 @@ export const enumFeatureFlagKey = {
IS_JSON_FILTER_ENABLED: 'IS_JSON_FILTER_ENABLED' as const,
IS_AI_ENABLED: 'IS_AI_ENABLED' as const,
IS_COMMAND_MENU_ITEM_ENABLED: 'IS_COMMAND_MENU_ITEM_ENABLED' as const,
IS_MARKETPLACE_ENABLED: 'IS_MARKETPLACE_ENABLED' as const,
IS_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,
IS_PUBLIC_DOMAIN_ENABLED: 'IS_PUBLIC_DOMAIN_ENABLED' as const,
IS_EMAILING_DOMAIN_ENABLED: 'IS_EMAILING_DOMAIN_ENABLED' as const,
IS_JUNCTION_RELATIONS_ENABLED: 'IS_JUNCTION_RELATIONS_ENABLED' as const,
IS_DRAFT_EMAIL_ENABLED: 'IS_DRAFT_EMAIL_ENABLED' as const,
IS_CONNECTED_ACCOUNT_MIGRATED: 'IS_CONNECTED_ACCOUNT_MIGRATED' as const,
IS_USAGE_ANALYTICS_ENABLED: 'IS_USAGE_ANALYTICS_ENABLED' as const,
IS_RICH_TEXT_V1_MIGRATED: 'IS_RICH_TEXT_V1_MIGRATED' as const,
IS_DIRECT_GRAPHQL_EXECUTION_ENABLED: 'IS_DIRECT_GRAPHQL_EXECUTION_ENABLED' as const,
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED: 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' as const,
IS_CONNECTED_ACCOUNT_MIGRATED: 'IS_CONNECTED_ACCOUNT_MIGRATED' as const,
IS_RECORD_TABLE_WIDGET_ENABLED: 'IS_RECORD_TABLE_WIDGET_ENABLED' as const,
IS_DATASOURCE_MIGRATED: 'IS_DATASOURCE_MIGRATED' as const
}
@@ -9215,19 +9430,11 @@ export const enumEngineComponentKey = {
SEE_DELETED_RECORDS: 'SEE_DELETED_RECORDS' as const,
CREATE_NEW_VIEW: 'CREATE_NEW_VIEW' as const,
HIDE_DELETED_RECORDS: 'HIDE_DELETED_RECORDS' as const,
GO_TO_PEOPLE: 'GO_TO_PEOPLE' as const,
GO_TO_COMPANIES: 'GO_TO_COMPANIES' as const,
GO_TO_DASHBOARDS: 'GO_TO_DASHBOARDS' as const,
GO_TO_OPPORTUNITIES: 'GO_TO_OPPORTUNITIES' as const,
GO_TO_SETTINGS: 'GO_TO_SETTINGS' as const,
GO_TO_TASKS: 'GO_TO_TASKS' as const,
GO_TO_NOTES: 'GO_TO_NOTES' as const,
EDIT_RECORD_PAGE_LAYOUT: 'EDIT_RECORD_PAGE_LAYOUT' as const,
EDIT_DASHBOARD_LAYOUT: 'EDIT_DASHBOARD_LAYOUT' as const,
SAVE_DASHBOARD_LAYOUT: 'SAVE_DASHBOARD_LAYOUT' as const,
CANCEL_DASHBOARD_LAYOUT: 'CANCEL_DASHBOARD_LAYOUT' as const,
DUPLICATE_DASHBOARD: 'DUPLICATE_DASHBOARD' as const,
GO_TO_WORKFLOWS: 'GO_TO_WORKFLOWS' as const,
ACTIVATE_WORKFLOW: 'ACTIVATE_WORKFLOW' as const,
DEACTIVATE_WORKFLOW: 'DEACTIVATE_WORKFLOW' as const,
DISCARD_DRAFT_WORKFLOW: 'DISCARD_DRAFT_WORKFLOW' as const,
@@ -9238,7 +9445,6 @@ export const enumEngineComponentKey = {
ADD_NODE_WORKFLOW: 'ADD_NODE_WORKFLOW' as const,
TIDY_UP_WORKFLOW: 'TIDY_UP_WORKFLOW' as const,
DUPLICATE_WORKFLOW: 'DUPLICATE_WORKFLOW' as const,
GO_TO_RUNS: 'GO_TO_RUNS' as const,
SEE_VERSION_WORKFLOW_RUN: 'SEE_VERSION_WORKFLOW_RUN' as const,
SEE_WORKFLOW_WORKFLOW_RUN: 'SEE_WORKFLOW_WORKFLOW_RUN' as const,
STOP_WORKFLOW_RUN: 'STOP_WORKFLOW_RUN' as const,
@@ -9250,8 +9456,20 @@ export const enumEngineComponentKey = {
SEARCH_RECORDS_FALLBACK: 'SEARCH_RECORDS_FALLBACK' as const,
ASK_AI: 'ASK_AI' as const,
VIEW_PREVIOUS_AI_CHATS: 'VIEW_PREVIOUS_AI_CHATS' as const,
NAVIGATION: 'NAVIGATION' as const,
TRIGGER_WORKFLOW_VERSION: 'TRIGGER_WORKFLOW_VERSION' as const,
FRONT_COMPONENT_RENDERER: 'FRONT_COMPONENT_RENDERER' as const,
REPLY_TO_EMAIL_THREAD: 'REPLY_TO_EMAIL_THREAD' as const,
COMPOSE_EMAIL: 'COMPOSE_EMAIL' as const,
GO_TO_PEOPLE: 'GO_TO_PEOPLE' as const,
GO_TO_COMPANIES: 'GO_TO_COMPANIES' as const,
GO_TO_DASHBOARDS: 'GO_TO_DASHBOARDS' as const,
GO_TO_OPPORTUNITIES: 'GO_TO_OPPORTUNITIES' as const,
GO_TO_SETTINGS: 'GO_TO_SETTINGS' as const,
GO_TO_TASKS: 'GO_TO_TASKS' as const,
GO_TO_NOTES: 'GO_TO_NOTES' as const,
GO_TO_WORKFLOWS: 'GO_TO_WORKFLOWS' as const,
GO_TO_RUNS: 'GO_TO_RUNS' as const,
DELETE_SINGLE_RECORD: 'DELETE_SINGLE_RECORD' as const,
DELETE_MULTIPLE_RECORDS: 'DELETE_MULTIPLE_RECORDS' as const,
RESTORE_SINGLE_RECORD: 'RESTORE_SINGLE_RECORD' as const,
@@ -9401,14 +9619,16 @@ export const enumEventLogTable = {
WORKSPACE_EVENT: 'WORKSPACE_EVENT' as const,
PAGEVIEW: 'PAGEVIEW' as const,
OBJECT_EVENT: 'OBJECT_EVENT' as const,
USAGE_EVENT: 'USAGE_EVENT' as const
USAGE_EVENT: 'USAGE_EVENT' as const,
APPLICATION_LOG: 'APPLICATION_LOG' as const
}
export const enumUsageOperationType = {
AI_CHAT_TOKEN: 'AI_CHAT_TOKEN' as const,
AI_WORKFLOW_TOKEN: 'AI_WORKFLOW_TOKEN' as const,
WORKFLOW_EXECUTION: 'WORKFLOW_EXECUTION' as const,
CODE_EXECUTION: 'CODE_EXECUTION' as const
CODE_EXECUTION: 'CODE_EXECUTION' as const,
WEB_SEARCH: 'WEB_SEARCH' as const
}
export const enumAnalyticsType = {
File diff suppressed because it is too large Load Diff
@@ -50,23 +50,6 @@ npx nx run twenty-server:database:reset
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/nameOfYourMigration -d src/database/typeorm/core/core.datasource.ts
```
#### For Workspace objects
There are no migrations files, migration are generated automatically for each workspace,
stored in the database, and applied with this command
```bash
npx nx run twenty-server:command workspace:sync-metadata -f
```
<Warning>
This will drop the database and re-run the migrations and seed.
Make sure to back up any data you want to keep before running this command.
</Warning>
## Tech Stack
Twenty primarily uses NestJS for the backend.
@@ -756,7 +756,7 @@ export default defineFrontComponent({
});
```
After syncing with `yarn twenty dev`, the quick action appears in the top-right corner of the page:
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" />
@@ -1402,7 +1402,7 @@ export default defineFrontComponent({
### How bundling works
The build step (`yarn twenty dev` or `yarn twenty build`) uses esbuild to produce a single self-contained file per logic function and per front component. All imported packages are inlined into the bundle.
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.
@@ -98,6 +98,21 @@ Dev mode is only available on Twenty instances running in development (`NODE_ENV
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Dev mode terminal output" />
</div>
#### One-shot sync with `yarn twenty dev --once`
If you do not want a watcher running in the background (for example in a CI pipeline, a git hook, or a scripted workflow), pass the `--once` flag. It runs the same pipeline as `yarn twenty dev` — build manifest, bundle files, upload, sync, regenerate the typed API client — but **exits as soon as the sync completes**:
```bash filename="Terminal"
yarn twenty dev --once
```
| Command | Behavior | When to use |
|---------|----------|-------------|
| `yarn twenty dev` | Watches your source files and re-syncs on every change. Keeps running until you stop it. | Interactive local development — you want the live status panel and instant feedback loop. |
| `yarn twenty dev --once` | Performs a single build + sync, then exits with code `0` on success or `1` on failure. | Scripts, CI, pre-commit hooks, AI agents, and any non-interactive workflow. |
Both modes require a Twenty server running in development mode and an authenticated remote — the same prerequisites apply.
### See your app in Twenty
Open [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer) in your browser. Navigate to **Settings > Apps** and select the **Developer** tab. You should see your app listed under **Your Apps**:
@@ -52,7 +52,11 @@ yarn twenty deploy
### Sharing a deployed app
Tarball apps are not listed in the public marketplace, so other workspaces on the same server won't discover them by browsing. To share a deployed app:
<Warning>
Sharing private (tarball) apps across workspaces is an **Enterprise** feature. The **Distribution** tab will show an upgrade prompt instead of the share controls until your workspace has a valid Enterprise key. See [Settings > Admin Panel > Enterprise](/settings/admin-panel#enterprise) to activate it.
</Warning>
Tarball apps are not listed in the public marketplace, so other workspaces on the same server won't discover them by browsing. Once your workspace is on the Enterprise plan, you can share a deployed app like this:
1. Go to **Settings > Applications > Registrations** and open your app
2. In the **Distribution** tab, click **Copy share link**
@@ -60,18 +64,20 @@ Tarball apps are not listed in the public marketplace, so other workspaces on th
The share link uses the server's base URL (without any workspace subdomain) so it works for any workspace on the server.
<Warning>
Sharing private apps is an Enterprise feature. Go to [Settings > Admin Panel > Enterprise](/settings/admin-panel#enterprise) to enable it.
</Warning>
### Version management
When updating an already deployed tarball app, the server requires the `version` in `package.json` to be **strictly higher** (per [semver](https://semver.org) ordering) than the currently deployed version. Re-deploying the same version, or pushing a lower one, is rejected before the tarball is stored — you'll see a `VERSION_ALREADY_EXISTS` error from the CLI.
To release an update:
1. Bump the `version` field in your `package.json`
1. Bump the `version` field in your `package.json` (e.g. `1.2.3` → `1.2.4`, `1.3.0`, or `2.0.0`)
2. Run `yarn twenty deploy` (or `yarn twenty deploy --remote production`)
3. Workspaces that have the app installed will see the upgrade available in their settings
<Note>
Pre-release tags work as expected: bumping `1.0.0-rc.1` → `1.0.0-rc.2` is allowed, and a final release like `1.0.0` is correctly recognized as higher than `1.0.0-rc.5`. The version in `package.json` must itself be a valid semver string.
</Note>
{/* TODO: add screenshot of the Upgrade button */}
## Publishing to npm
@@ -189,3 +195,12 @@ You can also install apps from the command line:
```bash filename="Terminal"
yarn twenty install
```
<Note>
The server enforces semver versioning on install, mirroring the rules on deploy:
- Installing the same version that is already installed in your workspace is rejected with an `APP_ALREADY_INSTALLED` error.
- Installing a lower version than the one currently installed is rejected with a `CANNOT_DOWNGRADE_APPLICATION` error.
To install a newer version, deploy or publish it first, then re-run `yarn twenty install`.
</Note>
@@ -50,23 +50,6 @@ npx nx run twenty-server:database:reset
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/nameOfYourMigration -d src/database/typeorm/core/core.datasource.ts
```
#### لكائنات مساحة العمل
لا توجد ملفات هجيرات، يتم إنشاء الهجيرات تلقائيًا لكل مساحة عمل،
مخزنة في قاعدة البيانات، ويتم تطبيقها مع هذا الأمر
```bash
npx nx run twenty-server:command workspace:sync-metadata -f
```
<Warning>
سيؤدي هذا إلى إسقاط قاعدة البيانات وإعادة تشغيل الهجرات والبذور.
تأكد من عمل نسخة احتياطية لأي بيانات تريد الاحتفاظ بها قبل تشغيل هذا الأمر.
</Warning>
## "التقنية المستخدمة"
تستخدم Twenty بشكل أساسي NestJS للواجهة الخلفية.
@@ -81,7 +81,7 @@ import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
displayName: 'My Twenty App',
description: 'My first Twenty app',
icon: 'IconWorld',
@@ -716,6 +716,13 @@ yarn twenty exec --postInstall
المكوّنات الأمامية هي مكوّنات React تُعرَض مباشرة داخل واجهة مستخدم Twenty. تعمل ضمن **Web Worker** معزول باستخدام Remote DOM — تكون شيفرتك في صندوق عزل لكنها تُعرَض أصيلًا داخل الصفحة، وليس ضمن iframe.
#### أين يمكن استخدام مكوّنات الواجهة الأمامية
يمكن عرض مكوّنات الواجهة الأمامية في موقعين داخل Twenty:
* **اللوحة الجانبية** — المكوّنات غير عديمة الرأس تفتح في اللوحة الجانبية اليمنى. هذا هو السلوك الافتراضي عندما يتم تشغيل مكوّن واجهة أمامية من قائمة الأوامر.
* **الويدجت (لوحات المعلومات وصفحات السجلات)** — يمكن تضمين مكوّنات الواجهة الأمامية كويدجت داخل تخطيطات الصفحات. عند تكوين لوحة معلومات أو تخطيط صفحة سجل، يمكن للمستخدمين إضافة ويدجت لمكوّن واجهة أمامية.
#### مثال أساسي
أسرع طريقة لرؤية مكوّن أمامي قيد العمل هي تسجيله كأمر. إضافة حقل `command` مع `isPinned: true` يجعلُه يظهر كزر إجراء سريع في الزاوية العلوية اليمنى من الصفحة — دون الحاجة إلى تخطيط صفحة:
@@ -748,7 +755,7 @@ export default defineFrontComponent({
});
```
بعد المزامنة باستخدام `yarn twenty dev`، يظهر الإجراء السريع في الزاوية العلوية اليمنى من الصفحة:
بعد المزامنة باستخدام `yarn twenty dev` (أو تشغيل الأمر لمرة واحدة `yarn twenty dev --once`)، يظهر الإجراء السريع في الزاوية العلوية اليمنى من الصفحة:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="زر إجراء سريع في الزاوية العلوية اليمنى" />
@@ -773,9 +780,13 @@ export default defineFrontComponent({
إضافةً إلى الأوامر، يمكنك تضمين مكوّن أمامي مباشرةً في صفحة سجل عبر إضافته كودجت في **تخطيط صفحة**. راجع قسم [definePageLayout](#definepagelayout) للتفاصيل.
#### مكوّنات عديمة الواجهة (`isHeadless: true`)
#### عديم الرأس مقابل غير عديم الرأس
المكوّنات عديمة الواجهة لا تُظهر واجهة مرئية لكنها ما تزال تُشغِّل منطق React. يكون هذا مفيدًا لـ **مكوّنات التأثير** — وهي مكوّنات تُجري تأثيرات جانبية عند التثبيت، مثل مزامنة البيانات، بدء مؤقّت، الاستماع إلى الأحداث، أو إطلاق إشعار.
تأتي مكوّنات الواجهة الأمامية بوضعَي عرض يتحكّم بهما الخيار `isHeadless`:
**غير عديم الرأس (افتراضي)** — يعرض المكوّن واجهة مستخدم مرئية. عند تشغيله من قائمة الأوامر يفتح في اللوحة الجانبية. هذا هو السلوك الافتراضي عندما تكون `isHeadless` تساوي `false` أو يتم تجاهلها.
**عديم الرأس (`isHeadless: true`)** — يتم تركيب المكوّن بشكل غير مرئي في الخلفية. لا يفتح اللوحة الجانبية. تم تصميم المكوّنات عديمة الرأس لإجراءات تنفّذ منطقًا ثم تُزيل تركيبها ذاتيًا — على سبيل المثال، تشغيل مهمة غير متزامنة، أو الانتقال إلى صفحة، أو إظهار نافذة تأكيد منبثقة. تتوافق بشكل طبيعي مع مكوّنات Command في SDK الموصوفة أدناه.
```tsx src/front-components/sync-tracker.tsx
import { defineFrontComponent, useRecordId, enqueueSnackbar } from 'twenty-sdk';
@@ -802,6 +813,89 @@ export default defineFrontComponent({
نظرًا لأن المكوّن يُرجع `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';
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';
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 للوصول إلى المستخدم الحالي، والسجل، ومثيل المكوّن:
@@ -858,6 +952,50 @@ export default defineFrontComponent({
| `unmountFrontComponent()` | إلغاء تركيب المكوّن |
| `updateProgress(progress)` | تحديث مؤشّر التقدّم |
فيما يلي مثال يستخدم واجهة برمجة تطبيقات المضيف لعرض Snackbar وإغلاق اللوحة الجانبية بعد اكتمال الإجراء:
```tsx src/front-components/archive-record.tsx
import { defineFrontComponent, useRecordId } from 'twenty-sdk';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk';
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`، فسيظهر أيضًا كزر إجراء سريع في الزاوية العلوية اليمنى من الصفحة.
@@ -1263,7 +1401,7 @@ export default defineFrontComponent({
### كيف يعمل التجميع
تستخدم خطوة البناء (`yarn twenty dev` أو `yarn twenty build`) أداة esbuild لإنتاج ملف واحد مستقل لكل دالة منطقية وكل مكوّن أمامي. تُضمَّن جميع الحزم المستوردة داخل الحزمة.
تستخدم خطوة البناء أداة esbuild لإنتاج ملف واحد مستقل لكل دالة منطقية ولكل مكوّن أمامي. تُضمَّن جميع الحزم المستوردة داخل الحزمة.
**الدوال المنطقية** تعمل في بيئة Node.js. الوحدات المدمجة في Node (`fs` و`path` و`crypto` و`http` وغيرها) متاحة ولا تحتاج إلى تثبيت.
@@ -1712,3 +1850,86 @@ 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` في أعلى سير العمل.
@@ -7,7 +7,9 @@ description: أنشئ أول تطبيق Twenty خلال دقائق.
التطبيقات حاليًا في مرحلة الألفا. الميزة تعمل لكنها لا تزال قيد التطور.
</Warning>
تتيح لك التطبيقات توسيع Twenty باستخدام كائنات وحقول ووظائف منطقية ومهارات ذكاء اصطناعي ومكونات واجهة مستخدم مخصصة — جميعها تُدار ككود.
## ما هي التطبيقات؟
تتيح لك التطبيقات توسيع Twenty باستخدام كائنات وحقول مخصّصة ووظائف منطقية ومكوّنات الواجهة الأمامية ومهارات الذكاء الاصطناعي وغير ذلك — جميعها تُدار ككود. بدلًا من تكوين كل شيء عبر واجهة المستخدم، تعرّف نموذج بياناتك ومنطقك في TypeScript وتقوم بنشره إلى مساحة عمل واحدة أو أكثر.
## المتطلبات الأساسية
@@ -17,7 +19,9 @@ description: أنشئ أول تطبيق Twenty خلال دقائق.
* **Yarn 4** — يأتي مع Node.js عبر Corepack. قم بتمكينه عبر تشغيل `corepack enable`
* **Docker** — [نزّل من هنا](https://www.docker.com/products/docker-desktop/). مطلوب لتشغيل مثيل محلي من Twenty. غير مطلوب إذا كان لديك خادم Twenty قيد التشغيل بالفعل.
## الخطوة 1: إنشاء هيكل تطبيقك
## قم بإنشاء تطبيقك الأول
### أنشئ هيكل تطبيقك
افتح الطرفية وشغّل:
@@ -29,18 +33,7 @@ npx create-twenty-app@latest my-twenty-app
سيؤدي ذلك إلى إنشاء مجلد جديد باسم `my-twenty-app` يحتوي على كل ما تحتاجه.
<Note>
أداة إنشاء الهيكل تدعم الأعلام التالية:
* `--minimal` — إنشاء الهيكل للملفات الأساسية فقط، بدون أمثلة (افتراضي)
* `--exhaustive` — إنشاء الهيكل لجميع كيانات الأمثلة
* `--name <name>` — تعيين اسم التطبيق (يتخطى المطالبة)
* `--display-name <displayName>` — تعيين اسم العرض (يتخطى المطالبة)
* `--description <description>` — تعيين الوصف (يتخطى المطالبة)
* `--skip-local-instance` — تخطي مطالبة إعداد الخادم المحلي
</Note>
## الخطوة 2: إعداد مثيل محلي من Twenty
### إعداد مثيل محلي من Twenty
ستسأل أداة إنشاء الهيكل:
@@ -53,7 +46,7 @@ npx create-twenty-app@latest my-twenty-app
<img src="/images/docs/developers/extends/apps/start-instance.png" alt="هل يجب بدء المثيل المحلي؟" />
</div>
## الخطوة 3: سجّل الدخول إلى مساحة العمل الخاصة بك
### سجّل الدخول إلى مساحة العمل الخاصة بك
بعد ذلك، ستُفتح نافذة متصفح تعرض صفحة تسجيل الدخول الخاصة بـ Twenty. سجّل الدخول باستخدام حساب العرض التوضيحي المُجهَّز مسبقًا:
@@ -64,7 +57,7 @@ npx create-twenty-app@latest my-twenty-app
<img src="/images/docs/developers/extends/apps/login.png" alt="شاشة تسجيل الدخول إلى Twenty" />
</div>
## الخطوة 4: تفويض التطبيق
### قم بتفويض التطبيق
بعد تسجيل الدخول، ستظهر لك شاشة تفويض. يتيح هذا لتطبيقك التفاعل مع مساحة العمل الخاصة بك.
@@ -80,7 +73,7 @@ npx create-twenty-app@latest my-twenty-app
<img src="/images/docs/developers/extends/apps/scaffolded.png" alt="تم إنشاء هيكل التطبيق بنجاح" />
</div>
## الخطوة 5: ابدأ التطوير
### ابدأ التطوير
انتقل إلى مجلد تطبيقك الجديد وابدأ خادم التطوير:
@@ -105,7 +98,22 @@ yarn twenty dev --verbose
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="مخرجات الطرفية في وضع التطوير" />
</div>
## الخطوة 6: اعرض تطبيقك في Twenty
#### مزامنة لمرة واحدة باستخدام `yarn twenty dev --once`
إذا كنت لا تريد تشغيل مراقب في الخلفية (مثلًا في خط أنابيب CI، أو خطاف Git، أو سير عمل مُؤتمت عبر سكربت)، فمرِّر الخيار `--once`. يُشغِّل خط الأنابيب نفسه مثل `yarn twenty dev` — إنشاء بيان البناء، تجميع الملفات، الرفع، المزامنة، إعادة توليد عميل API مضبوط الأنواع — ولكنه **ينهي التنفيذ فور اكتمال المزامنة**:
```bash filename="Terminal"
yarn twenty dev --once
```
| أمر | السلوك | متى يُستخدم |
| ------------------------ | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `yarn twenty dev` | يراقب ملفات المصدر ويعيد المزامنة عند كل تغيير. يستمر بالتشغيل حتى توقفه. | تطوير محلي تفاعلي — تريد لوحة حالة مباشرة وحلقة تغذية راجعة فورية. |
| `yarn twenty dev --once` | يجري عملية بناء واحدة + مزامنة واحدة، ثم ينهي التنفيذ برمز `0` عند النجاح أو `1` عند الفشل. | البرامج النصية، وCI، وخطافات ما قبل الالتزام، ووكلاء الذكاء الاصطناعي، وأي سير عمل غير تفاعلي. |
كلا الوضعين يتطلبان خادم Twenty يعمل في وضع التطوير وجهة بعيدة موثَّقة — تنطبق المتطلبات المسبقة نفسها.
### اعرض تطبيقك في Twenty
افتح [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer) في متصفحك. انتقل إلى **Settings > Apps** واختر علامة التبويب **Developer**. يُفترض أن ترى تطبيقك مُدرجًا تحت **Your Apps**:
@@ -133,13 +141,28 @@ yarn twenty dev --verbose
أنت جاهز تمامًا! حرّر أي ملف في `src/` وسيتم التقاط التغييرات تلقائيًا.
انتقل إلى [بناء التطبيقات](/l/ar/developers/extend/apps/building) للحصول على دليل مفصّل حول إنشاء الكائنات، ودوال المنطق، ومكونات الواجهة الأمامية، والمهارات، والمزيد.
---
## ما الذي يمكنك بناؤه
تتكون التطبيقات من **كيانات** — يُعرَّف كل منها كملف TypeScript يحتوي على `export default` واحد:
| كيان | ماذا يفعل |
| ---------------------- | ------------------------------------------------------------------------------------------------- |
| **الكائنات والحقول** | عرّف نماذج بيانات مخصّصة (مثل Post Card، Invoice) مع حقول محددة النوع |
| **الوظائف المنطقية** | دوال TypeScript على جانب الخادم يتم تشغيلها عبر مسارات HTTP، وجداول cron، أو أحداث قاعدة البيانات |
| **المكوّنات الأمامية** | مكوّنات React تُعرَض داخل واجهة مستخدم Twenty (اللوحة الجانبية، الودجات، قائمة الأوامر) |
| **المهارات والوكلاء** | قدرات الذكاء الاصطناعي — تعليمات قابلة لإعادة الاستخدام ومساعدون مستقلون ذاتيًا |
| **طرق العرض والتنقّل** | طرق عرض القوائم مُعدّة مسبقًا وعناصر قائمة الشريط الجانبي لكائناتك |
| **تخطيطات الصفحات** | صفحات تفاصيل سجلات مخصصة تتضمن علامات تبويب وعناصر واجهة |
انتقل إلى [بناء التطبيقات](/l/ar/developers/extend/apps/building) للاطلاع على دليل مفصّل لكل نوع من الكيانات.
---
## هيكل المشروع
تولّد أداة إنشاء الهيكل بنية الملفات التالية (مُبيّنة بوضع `--exhaustive` الذي يتضمن أمثلة لكل نوع من الكيانات):
تولّد أداة إنشاء الهيكل بنية الملفات التالية:
```text filename="my-twenty-app/"
my-twenty-app/
@@ -148,160 +171,50 @@ my-twenty-app/
.gitignore
.nvmrc
.yarnrc.yml
.yarn/
install-state.gz
.oxlintrc.json
tsconfig.json
tsconfig.spec.json # TypeScript config for tests
vitest.config.ts # Vitest test runner configuration
tsconfig.spec.json # TypeScript config for tests
vitest.config.ts # Vitest test runner configuration
LLMS.md
README.md
.github/
└── workflows/
└── ci.yml # GitHub Actions CI workflow
public/ # Public assets (images, fonts, etc.)
└── ci.yml # GitHub Actions CI workflow
public/ # Public assets (images, fonts, etc.)
src/
├── application-config.ts # Required — main application configuration
├── __tests__/
├── setup-test.ts # Test setup (server health check, config)
│ └── app-install.integration-test.ts # Example integration test
── roles/
── default-role.ts # Default role for logic functions
├── objects/
│ └── example-object.ts # Example custom object definition
├── fields/
│ └── example-field.ts # Example standalone field definition
├── logic-functions/
│ ├── hello-world.ts # Example logic function
│ ├── create-hello-world-company.ts # Example logic function using CoreApiClient
│ ├── pre-install.ts # Runs before installation
│ └── post-install.ts # Runs after installation
├── front-components/
│ └── hello-world.tsx # Example front component
├── page-layouts/
│ └── example-record-page-layout.ts # Example page layout with front component
├── views/
│ └── example-view.ts # Example saved view definition
├── navigation-menu-items/
│ └── example-navigation-menu-item.ts # Example sidebar navigation link
├── skills/
│ └── example-skill.ts # Example AI agent skill definition
└── agents/
└── example-agent.ts # Example AI agent definition
├── application-config.ts # Required — main application configuration
├── default-role.ts # Default role for logic functions
├── constants/
│ └── universal-identifiers.ts # Auto-generated UUIDs and app metadata
── __tests__/
── setup-test.ts # Test setup (server health check, config)
└── app-install.integration-test.ts # Integration test
```
افتراضيًا (`--minimal`)، تُنشأ الملفات الأساسية فقط: `application-config.ts`، `roles/default-role.ts`، `logic-functions/pre-install.ts`، و`logic-functions/post-install.ts`. استخدم `--exhaustive` لتضمين جميع ملفات الأمثلة الموضّحة أعلاه.
### البدء من مثال
للبدء من مثال أكثر اكتمالًا يضم كائنات وحقولًا مخصّصة، ودوال المنطق، ومكوّنات الواجهة الأمامية، وغير ذلك، استخدم الخيار `--example`:
```bash filename="Terminal"
npx create-twenty-app@latest my-twenty-app --example postcard
```
تُستمد الأمثلة من الدليل [twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples) على GitHub. يمكنك أيضًا إنشاء هيكل لكيانات فردية داخل مشروع قائم باستخدام `yarn twenty add` (انظر [بناء التطبيقات](/l/ar/developers/extend/apps/building#scaffolding-entities-with-yarn-twenty-add)).
### الملفات الرئيسية
| ملف / مجلد | الغرض |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `package.json` | يصرّح باسم تطبيقك وإصداره واعتماداته. يتضمن نصًا برمجيًا باسم `twenty` بحيث يمكنك تشغيل `yarn twenty help` للاطلاع على جميع الأوامر. |
| `src/application-config.ts` | **مطلوب.** ملف الإعداد الرئيسي لتطبيقك. |
| `src/roles/` | يعرِّف الأدوار التي تتحكم بما يمكن لدوال المنطق الوصول إليه. |
| `src/logic-functions/` | دوال على جانب الخادم يتم تشغيلها عبر المسارات، وجداول cron، أو أحداث قاعدة البيانات. |
| `src/front-components/` | مكونات React تُعرَض داخل واجهة مستخدم Twenty. |
| `src/objects/` | تعريفات كائنات مخصّصة لتوسيع نموذج البيانات لديك. |
| `src/fields/` | حقول مخصّصة تُضاف إلى الكائنات الموجودة. |
| `src/views/` | تكوينات العروض المحفوظة. |
| `src/navigation-menu-items/` | روابط مخصّصة في شريط التنقل الجانبي. |
| `src/skills/` | مهارات توسّع قدرات وكلاء الذكاء الاصطناعي في Twenty. |
| `src/agents/` | وكلاء ذكاء اصطناعي مع موجهات مخصّصة. |
| `src/page-layouts/` | تخطيطات صفحات مخصّصة لعرض السجلات. |
| `src/__tests__/` | اختبارات تكامل (إعداد + اختبار مثال). |
| `public/` | أصول ثابتة (صور، خطوط) تُقدَّم مع تطبيقك. |
| ملف / مجلد | الغرض |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `package.json` | يصرّح باسم تطبيقك وإصداره واعتماداته. يتضمن نصًا برمجيًا باسم `twenty` بحيث يمكنك تشغيل `yarn twenty help` للاطلاع على جميع الأوامر. |
| `src/application-config.ts` | **مطلوب.** ملف الإعداد الرئيسي لتطبيقك. |
| `src/default-role.ts` | الدور الافتراضي الذي يتحكم بما يمكن لدوال المنطق الوصول إليه. |
| `src/constants/universal-identifiers.ts` | معرّفات UUID وبيانات التعريف للتطبيق، والمولَّدة تلقائيًا (اسم العرض، الوصف). |
| `src/__tests__/` | اختبارات تكامل (إعداد + اختبار مثال). |
| `public/` | أصول ثابتة (صور، خطوط) تُقدَّم مع تطبيقك. |
## إدارة الريموتات
## خادم التطوير المحلي
**الريموت** هو خادم 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`.
## خادم التطوير المحلي (`yarn twenty server`)
يمكن لأداة سطر الأوامر (CLI) إدارة خادم Twenty محلي يعمل داخل Docker. هذا هو الخادم نفسه الذي يبدأ تلقائيًا عند إنشاء هيكل تطبيق باستخدام `create-twenty-app`، لكن يمكنك أيضًا إدارته يدويًا.
### بدء الخادم
```bash filename="Terminal"
yarn twenty server start
```
سيؤدي ذلك إلى سحب صورة Docker `twentycrm/twenty-app-dev:latest` (إن لم تكن موجودة بالفعل)، وإنشاء حاوية باسم `twenty-app-dev`، وبدء تشغيلها على المنفذ **2020**. تنتظر أداة CLI حتى يجتاز الخادم فحص السلامة قبل الإنهاء.
يتم إنشاء حجمين في Docker للاحتفاظ بالبيانات بين عمليات إعادة التشغيل:
* `twenty-app-dev-data` — قاعدة بيانات PostgreSQL
* `twenty-app-dev-storage` — تخزين ملفات
إذا كان المنفذ 2020 مستخدمًا بالفعل، يمكنك البدء على منفذ مختلف:
```bash filename="Terminal"
yarn twenty server start --port 3030
```
تقوم أداة CLI تلقائيًا بتهيئة قيم `NODE_PORT` و`SERVER_URL` الداخلية في الحاوية لتطابق المنفذ المختار، بحيث تعمل دوال المنطق وOAuth وكل الشبكات الداخلية الأخرى بشكل صحيح.
بمجرد البدء، يُسجَّل الخادم تلقائيًا كـ `local` remote في إعدادات CLI لديك.
### التحقق من حالة الخادم
```bash filename="Terminal"
yarn twenty server status
```
يعرض ما إذا كان الخادم قيد التشغيل، وعنوان URL الخاص به، وبيانات اعتماد تسجيل الدخول الافتراضية (`tim@apple.dev` / `tim@apple.dev`).
### عرض سجلات الخادم
```bash filename="Terminal"
yarn twenty server logs
```
يبث سجلات الحاوية. استخدم `--lines` للتحكّم بعدد الأسطر الحديثة المراد عرضها:
```bash filename="Terminal"
yarn twenty server logs --lines 100
```
### إيقاف الخادم
```bash filename="Terminal"
yarn twenty server stop
```
يوقف الحاوية. تُحفَظ بياناتك في أحجام Docker — وستُستأنف الحالة مع عملية `start` التالية من حيث توقفت.
### إعادة تعيين الخادم
```bash filename="Terminal"
yarn twenty server reset
```
يزيل الحاوية **و** يحذف كلا حجمي Docker، ممّا يمحو جميع البيانات. ستنشئ عملية `start` التالية مثيلًا جديدًا من البداية.
<Note>
يتطلّب الخادم أن يكون **Docker** قيد التشغيل. إذا ظهرت لك رسالة خطأ "Docker not running"، فتأكّد من تشغيل Docker Desktop (أو خادوم Docker).
</Note>
### مرجع الأوامر
لقد قامت أداة إنشاء الهيكل بالفعل بتشغيل خادم Twenty محليًا لك. لإدارته لاحقًا، استخدم `yarn twenty server`:
| أمر | الوصف |
| -------------------------------------- | --------------------------------------------- |
@@ -313,65 +226,11 @@ yarn twenty server reset
| `yarn twenty server logs --lines 100` | عرض آخر 100 سطر من السجلات |
| `yarn twenty server reset` | حذف جميع البيانات والبدء من جديد |
## التكامل المستمر (CI) باستخدام GitHub Actions
يتم الاحتفاظ بالبيانات عبر عمليات إعادة التشغيل في وحدتي تخزين Docker (`twenty-app-dev-data` لـ PostgreSQL، و`twenty-app-dev-storage` للملفات). استخدم `reset` لمسح كل شيء والبدء من جديد.
تولّد أداة إنشاء الهيكل سير عمل 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` في أعلى سير العمل.
<Note>
يتطلّب الخادم أن يكون **Docker** قيد التشغيل. إذا ظهرت لك رسالة خطأ "Docker not running"، فتأكّد من تشغيل Docker Desktop (أو خادوم Docker).
</Note>
## إعداد يدوي (بدون المهيئ)
@@ -52,7 +52,11 @@ yarn twenty deploy
### مشاركة تطبيق منشور
تطبيقات tarball لا تُدرَج في السوق العامة، لذا لن تكتشفها مساحات العمل الأخرى على الخادم نفسه عبر الاستعراض. لمشاركة تطبيق منشور:
<Warning>
تُعد مشاركة التطبيقات الخاصة (tarball) عبر مساحات العمل ميزة ضمن **Enterprise**. ستعرض علامة التبويب **Distribution** مطالبة بالترقية بدلًا من عناصر التحكم في المشاركة حتى تحتوي مساحة العمل لديك على مفتاح Enterprise صالح. اطلع على [الإعدادات > لوحة الإدارة > Enterprise](/settings/admin-panel#enterprise) لتنشيطه.
</Warning>
تطبيقات tarball لا تُدرَج في السوق العامة، لذا لن تكتشفها مساحات العمل الأخرى على الخادم نفسه عبر الاستعراض. بمجرد أن تصبح مساحة العمل لديك ضمن خطة Enterprise، يمكنك مشاركة تطبيق تم نشره كما يلي:
1. اذهب إلى **الإعدادات > التطبيقات > التسجيلات** وافتح تطبيقك
2. في علامة التبويب **التوزيع**، انقر **نسخ رابط المشاركة**
@@ -60,18 +64,20 @@ yarn twenty deploy
يستخدم رابط المشاركة عنوان URL الأساسي للخادم (من دون أي نطاق فرعي لمساحة عمل)، لذا يعمل مع أي مساحة عمل على الخادم.
<Warning>
مشاركة التطبيقات الخاصة هي ميزة ضمن باقة Enterprise. اذهب إلى [الإعدادات > لوحة الإدارة > Enterprise](/settings/admin-panel#enterprise) لتمكينها.
</Warning>
### إدارة الإصدارات
عند تحديث تطبيق tarball منشور مسبقًا، يشترط الخادم أن تكون قيمة `version` في `package.json` **أعلى قطعًا** (وفق ترتيب [الإصدار الدلالي](https://semver.org)) من الإصدار المنشور حاليًا. إعادة نشر الإصدار نفسه، أو دفع إصدار أدنى، يُرفَض قبل تخزين ملف tarball — سترى خطأ `VERSION_ALREADY_EXISTS` من CLI.
لطرح تحديث:
1. ارفع قيمة الحقل `version` في ملف `package.json`
1. قم بزيادة الحقل `version` في ملف `package.json` (مثلًا: `1.2.3` → `1.2.4`، `1.3.0`، أو `2.0.0`)
2. شغّل `yarn twenty deploy` (أو `yarn twenty deploy --remote production`)
3. سترى مساحات العمل التي ثبّتت التطبيق الترقية متاحة في إعداداتها
<Note>
علامات ما قبل الإصدار تعمل كما هو متوقع: زيادة `1.0.0-rc.1` → `1.0.0-rc.2` مسموح بها، ويُعترَف بالإصدار النهائي مثل `1.0.0` على أنه أعلى من `1.0.0-rc.5`. يجب أن يكون الإصدار في `package.json` بنفسه سلسلة semver صالحة.
</Note>
{/* TODO: add screenshot of the Upgrade button */}
## النشر على npm
@@ -189,3 +195,12 @@ jobs:
```bash filename="Terminal"
yarn twenty install
```
<Note>
يفرض الخادم اعتماد إصدارات semver عند التثبيت، بما يعكس القواعد المطبّقة عند النشر:
* تثبيت الإصدار نفسه المثبّت بالفعل في مساحة عملك يُرفَض بخطأ `APP_ALREADY_INSTALLED`.
* تثبيت إصدار أدنى من الإصدار المثبّت حاليًا يُرفَض بخطأ `CANNOT_DOWNGRADE_APPLICATION`.
لتثبيت إصدار أحدث، انشره (deploy) أو انشره إلى السجل (publish) أولًا، ثم أعد تشغيل `yarn twenty install`.
</Note>
@@ -140,12 +140,12 @@ description: دليل شامل لاستكشاف أخطاء استيراد CSV و
#### تاريخ
**المشكلة:** تنسيق تاريخ غير معروف
**الحل:** استخدم تنسيقًا متّسقًا في جميع أنحاء الملف
**الحل:** استخدم تنسيق `YYYY-MM-DD` بشكل متّسق في جميع أنحاء الملف
```
✓ 2024-03-15 (YYYY-MM-DD - recommended)
03/15/2024 (MM/DD/YYYY)
15/03/2024 (DD/MM/YYYY)
✓ 2024-03-15 (YYYY-MM-DD)
03/15/2024 (MM/DD/YYYY)
15/03/2024 (DD/MM/YYYY)
```
#### هاتف
@@ -103,8 +103,6 @@ description: دليل كامل خطوة بخطوة لتنسيق بياناتك
استخدم تنسيقًا موحدًا في كامل ملفك:
* `YYYY-MM-DD` (مُوصى به): `2024-03-15`
* `MM/DD/YYYY`: `03/15/2024`
* `DD/MM/YYYY`: `15/03/2024`
* ISO 8601: `2024-03-15T10:30:00Z`
### حقول الأرقام
@@ -50,22 +50,6 @@ npx nx run twenty-server:database:reset
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/nameOfYourMigration -d src/database/typeorm/core/core.datasource.ts
```
#### Pro objekty Pracovní plochy
Nejsou žádné soubory migrací, migrace jsou generovány automaticky pro každou pracovní plochu, uloženy v databázi a aplikovány tímto příkazem
```bash
npx nx run twenty-server:command workspace:sync-metadata -f
```
<Warning>
Tímto se databáze smaže a znovu se spustí migrace a seedování.
Před spuštěním tohoto příkazu si nezapomeňte zálohovat všechna data, která chcete zachovat.
</Warning>
## Technologický stack
Twenty primárně používá NestJS pro backend.
@@ -81,7 +81,7 @@ import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
displayName: 'My Twenty App',
description: 'My first Twenty app',
icon: 'IconWorld',
@@ -717,6 +717,13 @@ Hlavní body:
Frontendové komponenty jsou React komponenty, které se vykreslují přímo v uživatelském rozhraní Twenty. Běží v **izolovaném Web Workeru** s využitím Remote DOM — váš kód je sandboxovaný, ale vykresluje se nativně na stránce, nikoli v iframu.
#### Kde lze použít front komponenty
Front komponenty se mohou vykreslovat na dvou místech v rámci Twenty:
* **Postranní panel** — Ne-headless front komponenty se otevírají v pravém postranním panelu. Toto je výchozí chování, když je front komponenta vyvolána z menu příkazů.
* **Widgety (nástěnky a stránky záznamů)** — Front komponenty lze vkládat jako widgety do rozložení stránek. Při konfiguraci nástěnky nebo rozložení stránky záznamu mohou uživatelé přidat widget front komponenty.
#### Základní příklad
Nejrychlejší způsob, jak vidět frontendovou komponentu v akci, je zaregistrovat ji jako **příkaz**. Přidáním pole `command` s `isPinned: true` se zobrazí jako tlačítko rychlé akce v pravém horním rohu stránky — není potřeba žádné rozvržení stránky:
@@ -749,7 +756,7 @@ export default defineFrontComponent({
});
```
Po synchronizaci pomocí `yarn twenty dev` se rychlá akce zobrazí v pravém horním rohu stránky:
Po synchronizaci pomocí `yarn twenty dev` (nebo po jednorázovém spuštění `yarn twenty dev --once`) se rychlá akce zobrazí v pravém horním rohu stránky:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Tlačítko rychlé akce v pravém horním rohu" />
@@ -774,9 +781,13 @@ Kliknutím na něj vykreslíte komponentu přímo ve stránce.
Mimo příkazy můžete frontendovou komponentu vložit přímo na stránku záznamu přidáním jako widget v **rozvržení stránky**. Podrobnosti viz sekce [definePageLayout](#definepagelayout).
#### Headless komponenty (`isHeadless: true`)
#### Headless vs. ne-headless
Headless komponenty nevykreslují viditelné UI, ale stále spouštějí logiku Reactu. To je užitečné pro **efektové komponenty** — komponenty, které po připojení provádějí vedlejší efekty, jako je synchronizace dat, spuštění časovače, naslouchání událostem nebo vyvolání oznámení.
Front komponenty existují ve dvou režimech vykreslování řízených volbou `isHeadless`:
**Ne-headless (výchozí)** — Komponenta vykreslí viditelné uživatelské rozhraní. Po vyvolání z menu příkazů se otevře v postranním panelu. Toto je výchozí chování, když je `isHeadless` `false` nebo když tato volba není uvedena.
**Headless (`isHeadless: true`)** — Komponenta se neviditelně inicializuje na pozadí. Neotevírá postranní panel. Headless komponenty jsou určené pro akce, které provedou logiku a poté se odpojí — například spuštění asynchronního úkolu, navigaci na stránku nebo zobrazení potvrzovacího modálního okna. Přirozeně se hodí ke komponentám SDK Command popsaným níže.
```tsx src/front-components/sync-tracker.tsx
import { defineFrontComponent, useRecordId, enqueueSnackbar } from 'twenty-sdk';
@@ -803,6 +814,89 @@ export default defineFrontComponent({
Protože komponenta vrací `null`, Twenty přeskočí vykreslení kontejneru — v rozvržení se neobjeví žádné prázdné místo. Komponenta má však stále přístup ke všem hookům a API komunikace s hostitelem.
#### Komponenty SDK Command
Balíček `twenty-sdk` poskytuje čtyři pomocné komponenty Command navržené pro headless front komponenty. Každá komponenta při připojení provede akci, chyby zpracuje zobrazením oznámení ve snackbaru a po dokončení automaticky odpojí front komponentu.
Importujte je z `twenty-sdk/command`:
* **`Command`** — Spustí asynchronní callback přes prop `execute`.
* **`CommandLink`** — Naviguje na cestu v aplikaci. Props: `to`, `params`, `queryParams`, `options`.
* **`CommandModal`** — Otevře potvrzovací modální okno. Pokud uživatel potvrdí, provede callback `execute`. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`.
* **`CommandOpenSidePanelPage`** — Otevře konkrétní stránku postranního panelu. Props: `page`, `pageTitle`, `pageIcon`.
Zde je kompletní příklad headless front komponenty, která pomocí `Command` spouští akci z menu příkazů:
```tsx src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk';
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',
},
});
```
A příklad s použitím `CommandModal` k vyžádání potvrzení před provedením:
```tsx src/front-components/delete-draft.tsx
import { defineFrontComponent } from 'twenty-sdk';
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',
},
});
```
#### Přístup k běhovému kontextu
Uvnitř komponenty použijte hooky SDK pro přístup k aktuálnímu uživateli, záznamu a instanci komponenty:
@@ -859,6 +953,50 @@ Frontendové komponenty mohou pomocí funkcí z `twenty-sdk` vyvolávat navigaci
| `unmountFrontComponent()` | Odmontovat komponentu |
| `updateProgress(progress)` | Aktualizovat indikátor průběhu |
Zde je příklad, který používá hostitelské API k zobrazení snackbaru a zavření postranního panelu po dokončení akce:
```tsx src/front-components/archive-record.tsx
import { defineFrontComponent, useRecordId } from 'twenty-sdk';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk';
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,
});
```
#### Možnosti příkazu
Přidání pole `command` do `defineFrontComponent` zaregistruje komponentu v příkazovém menu (Cmd+K). Pokud je `isPinned` nastaveno na `true`, zobrazí se také jako tlačítko rychlé akce v pravém horním rohu stránky.
@@ -1264,7 +1402,7 @@ export default defineFrontComponent({
### Jak funguje bundlování
Krok sestavení (`yarn twenty dev` nebo `yarn twenty build`) používá esbuild k vytvoření jediného samostatného souboru pro každou logickou funkci a každou frontendovou komponentu. Všechny importované balíčky jsou vloženy přímo do bundlu.
Krok sestavení používá esbuild k vytvoření jediného samostatného souboru pro každou logickou funkci a každou frontendovou komponentu. Všechny importované balíčky jsou vloženy přímo do bundlu.
**Logické funkce** běží v prostředí Node.js. Vestavěné moduly Node (`fs`, `path`, `crypto`, `http` atd.) jsou k dispozici a není je třeba instalovat.
@@ -1713,3 +1851,86 @@ yarn twenty uninstall
# Skip the confirmation prompt
yarn twenty uninstall --yes
```
## Správa vzdálených serverů
**Remote** je server Twenty, ke kterému se vaše aplikace připojuje. Během nastavení jej generátor kostry automaticky vytvoří. Můžete kdykoli přidat další vzdálené servery nebo mezi nimi přepínat.
```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>
```
Vaše přihlašovací údaje jsou uloženy v `~/.twenty/config.json`.
## CI s GitHub Actions
Generátor kostry vytvoří připravený k použití workflow GitHub Actions v `.github/workflows/ci.yml`. Automaticky spouští integrační testy při každém pushi do `main` a u pull requestů.
Workflow:
1. Načte váš kód (checkout).
2. Spustí dočasný server Twenty pomocí akce `twentyhq/twenty/.github/actions/spawn-twenty-docker-image`
3. Nainstaluje závislosti pomocí `yarn install --immutable`
4. Spustí `yarn test` s proměnnými `TWENTY_API_URL` a `TWENTY_API_KEY` vloženými z výstupů akce
```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 }}
```
Není potřeba konfigurovat žádné secrets — akce `spawn-twenty-docker-image` spustí efemérní server Twenty přímo v runneru a vypíše podrobnosti připojení. Secret `GITHUB_TOKEN` je poskytován GitHubem automaticky.
Chcete-li připnout konkrétní verzi Twenty místo `latest`, změňte proměnnou prostředí `TWENTY_VERSION` na začátku workflow.
@@ -7,7 +7,9 @@ description: Vytvořte svou první aplikaci Twenty během několika minut.
Aplikace jsou aktuálně v alfa fázi. Funkce funguje, ale stále se vyvíjí.
</Warning>
Aplikace vám umožňují rozšířit Twenty o vlastní objekty, pole, logické funkce, AI schopnosti a komponenty uživatelského rozhraní — vše je spravováno jako kód.
## Co jsou aplikace?
Aplikace vám umožňují rozšířit Twenty o vlastní objekty, pole, logické funkce, frontendové komponenty, AI schopnosti a další — vše je spravováno jako kód. Místo konfigurace všeho přes uživatelské rozhraní definujete v TypeScriptu svůj datový model a logiku a nasadíte je do jednoho nebo více pracovních prostorů.
## Předpoklady
@@ -17,7 +19,9 @@ Než začnete, ujistěte se, že máte ve svém počítači nainstalováno násl
* **Yarn 4** — Dodává se s Node.js prostřednictvím Corepacku. Povolte jej spuštěním `corepack enable`
* **Docker** — [Stáhnout zde](https://www.docker.com/products/docker-desktop/). Nutné pro spuštění lokální instance Twenty. Není potřeba, pokud už máte spuštěný server Twenty.
## Krok 1: Vytvořte kostru své aplikace
## Vytvořte svou první aplikaci
### Vytvořte kostru své aplikace
Otevřete terminál a spusťte:
@@ -29,18 +33,7 @@ Budete vyzváni k zadání názvu a popisu své aplikace. Stisknutím **Enter**
Tím se vytvoří nová složka s názvem `my-twenty-app` se vším potřebným.
<Note>
Generátor kostry podporuje tyto přepínače:
* `--minimal` — vygeneruje pouze nezbytné soubory, bez příkladů (výchozí)
* `--exhaustive` — vygeneruje všechny ukázkové entity
* `--name <name>` — nastaví název aplikace (přeskočí výzvu)
* `--display-name <displayName>` — nastaví zobrazovaný název (přeskočí výzvu)
* `--description <description>` — nastaví popis (přeskočí výzvu)
* `--skip-local-instance` — přeskočí výzvu k nastavení lokálního serveru
</Note>
## Krok 2: Nastavte lokální instanci Twenty
### Nastavte lokální instanci Twenty
Generátor kostry se zeptá:
@@ -53,7 +46,7 @@ Generátor kostry se zeptá:
<img src="/images/docs/developers/extends/apps/start-instance.png" alt="Spustit lokální instanci?" />
</div>
## Krok 3: Přihlaste se do svého pracovního prostoru
### Přihlaste se do svého pracovního prostoru
Poté se otevře okno prohlížeče se stránkou přihlášení do Twenty. Přihlaste se předpřipraveným demo účtem:
@@ -64,7 +57,7 @@ Poté se otevře okno prohlížeče se stránkou přihlášení do Twenty. Přih
<img src="/images/docs/developers/extends/apps/login.png" alt="Přihlašovací obrazovka Twenty" />
</div>
## Krok 4: Autorizujte aplikaci
### Autorizujte aplikaci
Po přihlášení uvidíte autorizační obrazovku. Tím umožníte vaší aplikaci pracovat s vaším pracovním prostorem.
@@ -80,7 +73,7 @@ Po autorizaci váš terminál potvrdí, že je vše nastaveno.
<img src="/images/docs/developers/extends/apps/scaffolded.png" alt="Aplikace byla úspěšně vygenerována" />
</div>
## Krok 5: Začněte vyvíjet
### Začněte vyvíjet
Přejděte do nové složky aplikace a spusťte vývojový server:
@@ -105,7 +98,22 @@ Vývojový režim je k dispozici pouze na instancích Twenty běžících v rež
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Výstup terminálu ve vývojovém režimu" />
</div>
## Krok 6: Zobrazte svou aplikaci v Twenty
#### Jednorázová synchronizace pomocí `yarn twenty dev --once`
Pokud nechcete, aby na pozadí běžel watcher (například v CI pipeline, git hooku nebo skriptovaném workflow), použijte příznak `--once`. Spouští stejnou pipeline jako `yarn twenty dev` — sestaví manifest, zabalí soubory, nahraje, synchronizuje, znovu vygeneruje typovaného klienta API — ale **ukončí se, jakmile se synchronizace dokončí**:
```bash filename="Terminal"
yarn twenty dev --once
```
| Příkaz | Chování | Kdy použít |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| `yarn twenty dev` | Sleduje vaše zdrojové soubory a při každé změně znovu spustí synchronizaci. Zůstává spuštěný, dokud jej nezastavíte. | Interaktivní lokální vývoj — chcete živý panel stavu a okamžitou zpětnovazebnou smyčku. |
| `yarn twenty dev --once` | Provede jedno sestavení + synchronizaci, poté ukončí běh s kódem `0` při úspěchu nebo `1` při neúspěchu. | Skripty, CI, pre-commit hooky, AI agenti a jakýkoli neinteraktivní pracovní postup. |
Oba režimy vyžadují server Twenty běžící v režimu vývoje a autentizovaný remote — platí stejné požadavky.
### Zobrazte svou aplikaci v Twenty
Otevřete ve svém prohlížeči [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer). Přejděte do **Settings > Apps** a vyberte kartu **Developer**. Vaše aplikace by měla být uvedena v části **Your Apps**:
@@ -133,13 +141,28 @@ Přepněte na kartu **Content**, abyste viděli vše, co vaše aplikace poskytuj
Vše je připraveno! Upravte libovolný soubor v `src/` a změny se automaticky projeví.
Přejděte na [Tvorba aplikací](/l/cs/developers/extend/apps/building) pro podrobný průvodce vytvářením objektů, logických funkcí, frontendových komponent, dovedností a dalšího.
---
## Co můžete vytvořit
Aplikace se skládají z **entit** — každá je definována jako soubor TypeScriptu s jediným `export default`:
| Entita | K čemu slouží |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| **Objekty a pole** | Definujte vlastní datové modely (např. Post Card, Invoice) s typovanými poli |
| **Logické funkce** | Serverové funkce v TypeScriptu spouštěné HTTP trasami, plánovačem cron nebo událostmi databáze |
| **Frontendové komponenty** | Komponenty Reactu, které se vykreslují v uživatelském rozhraní Twenty (postranní panel, widgety, příkazová nabídka) |
| **Dovednosti a agenti** | Schopnosti AI — opakovaně použitelné pokyny a autonomní asistenti |
| **Pohledy a navigace** | Předkonfigurované seznamové pohledy a položky postranní nabídky pro vaše objekty |
| **Rozvržení stránek** | Vlastní stránky detailu záznamu s kartami a widgety |
Přejděte na [Tvorba aplikací](/l/cs/developers/extend/apps/building) pro podrobný průvodce každým typem entity.
---
## Struktura projektu
Generátor kostry vytvoří následující strukturu souborů (zobrazeno v režimu `--exhaustive`, který zahrnuje příklady pro každý typ entity):
Nástroj pro vytvoření kostry vygeneruje následující strukturu souborů:
```text filename="my-twenty-app/"
my-twenty-app/
@@ -148,160 +171,50 @@ my-twenty-app/
.gitignore
.nvmrc
.yarnrc.yml
.yarn/
install-state.gz
.oxlintrc.json
tsconfig.json
tsconfig.spec.json # TypeScript config for tests
vitest.config.ts # Vitest test runner configuration
tsconfig.spec.json # TypeScript config for tests
vitest.config.ts # Vitest test runner configuration
LLMS.md
README.md
.github/
└── workflows/
└── ci.yml # GitHub Actions CI workflow
public/ # Public assets (images, fonts, etc.)
└── ci.yml # GitHub Actions CI workflow
public/ # Public assets (images, fonts, etc.)
src/
├── application-config.ts # Required — main application configuration
├── __tests__/
├── setup-test.ts # Test setup (server health check, config)
│ └── app-install.integration-test.ts # Example integration test
── roles/
── default-role.ts # Default role for logic functions
├── objects/
│ └── example-object.ts # Example custom object definition
├── fields/
│ └── example-field.ts # Example standalone field definition
├── logic-functions/
│ ├── hello-world.ts # Example logic function
│ ├── create-hello-world-company.ts # Example logic function using CoreApiClient
│ ├── pre-install.ts # Runs before installation
│ └── post-install.ts # Runs after installation
├── front-components/
│ └── hello-world.tsx # Example front component
├── page-layouts/
│ └── example-record-page-layout.ts # Example page layout with front component
├── views/
│ └── example-view.ts # Example saved view definition
├── navigation-menu-items/
│ └── example-navigation-menu-item.ts # Example sidebar navigation link
├── skills/
│ └── example-skill.ts # Example AI agent skill definition
└── agents/
└── example-agent.ts # Example AI agent definition
├── application-config.ts # Required — main application configuration
├── default-role.ts # Default role for logic functions
├── constants/
│ └── universal-identifiers.ts # Auto-generated UUIDs and app metadata
── __tests__/
── setup-test.ts # Test setup (server health check, config)
└── app-install.integration-test.ts # Integration test
```
Ve výchozím nastavení (`--minimal`) se vytvoří pouze základní soubory: `application-config.ts`, `roles/default-role.ts`, `logic-functions/pre-install.ts` a `logic-functions/post-install.ts`. Pro zahrnutí všech ukázkových souborů výše použijte `--exhaustive`.
### Začínáme s příkladem
Chcete-li začít s úplnějším příkladem s vlastními objekty, poli, logickými funkcemi, frontendovými komponentami a dalšími, použijte přepínač `--example`:
```bash filename="Terminal"
npx create-twenty-app@latest my-twenty-app --example postcard
```
Příklady pocházejí z adresáře [twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples) na GitHubu. Můžete také vytvořit kostru jednotlivých entit v existujícím projektu pomocí `yarn twenty add` (viz [Tvorba aplikací](/l/cs/developers/extend/apps/building#scaffolding-entities-with-yarn-twenty-add)).
### Klíčové soubory
| Soubor / Složka | Účel |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `package.json` | Definuje název, verzi a závislosti vaší aplikace. Obsahuje skript `twenty`, takže můžete spustit `yarn twenty help` a zobrazit všechny příkazy. |
| `src/application-config.ts` | **Povinné.** Hlavní konfigurační soubor vaší aplikace. |
| `src/roles/` | Definuje role, které určují, k čemu mají vaše logické funkce přístup. |
| `src/logic-functions/` | Serverové funkce spouštěné trasami, plánovačem cron nebo událostmi databáze. |
| `src/front-components/` | Komponenty Reactu, které se vykreslují uvnitř uživatelského rozhraní Twenty. |
| `src/objects/` | Vlastní definice objektů pro rozšíření vašeho datového modelu. |
| `src/fields/` | Vlastní pole přidaná k existujícím objektům. |
| `src/views/` | Konfigurace uložených zobrazení. |
| `src/navigation-menu-items/` | Vlastní odkazy v postranní navigaci. |
| `src/skills/` | Dovednosti, které rozšiřují možnosti AI agentů Twenty. |
| `src/agents/` | AI agenti s vlastními prompty. |
| `src/page-layouts/` | Vlastní rozvržení stránek pro zobrazení záznamů. |
| `src/__tests__/` | Integrační testy (nastavení + ukázkový test). |
| `public/` | Statická aktiva (obrázky, písma) poskytovaná s vaší aplikací. |
| Soubor / Složka | Účel |
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `package.json` | Definuje název, verzi a závislosti vaší aplikace. Obsahuje skript `twenty`, takže můžete spustit `yarn twenty help` a zobrazit všechny příkazy. |
| `src/application-config.ts` | **Povinné.** Hlavní konfigurační soubor vaší aplikace. |
| `src/default-role.ts` | Výchozí role, která určuje, k čemu mají vaše logické funkce přístup. |
| `src/constants/universal-identifiers.ts` | Automaticky generovaná UUID a metadata aplikace (zobrazovaný název, popis). |
| `src/__tests__/` | Integrační testy (nastavení + ukázkový test). |
| `public/` | Statická aktiva (obrázky, písma) poskytovaná s vaší aplikací. |
## Správa vzdálených serverů
## Lokální vývojový server
**Remote** je server Twenty, ke kterému se vaše aplikace připojuje. Během nastavení jej generátor kostry automaticky vytvoří. Můžete kdykoli přidat další vzdálené servery nebo mezi nimi přepínat.
```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>
```
Vaše přihlašovací údaje jsou uloženy v `~/.twenty/config.json`.
## Lokální vývojový server (`yarn twenty server`)
CLI může spravovat lokální server Twenty běžící v Dockeru. Jde o stejný server, který se spustí automaticky při vytvoření kostry aplikace pomocí `create-twenty-app`, ale můžete jej spravovat i ručně.
### Spuštění serveru
```bash filename="Terminal"
yarn twenty server start
```
Stáhne image Dockeru `twentycrm/twenty-app-dev:latest` (pokud již není k dispozici), vytvoří kontejner s názvem `twenty-app-dev` a spustí jej na portu **2020**. CLI čeká, dokud server neprojde kontrolou stavu, než vrátí řízení.
Vytvoří se dva svazky Dockeru pro zachování dat mezi restartováními:
* `twenty-app-dev-data` — databáze PostgreSQL
* `twenty-app-dev-storage` — úložiště souborů
Pokud je port 2020 již používán, můžete spustit na jiném portu:
```bash filename="Terminal"
yarn twenty server start --port 3030
```
CLI automaticky nakonfiguruje interní `NODE_PORT` a `SERVER_URL` kontejneru tak, aby odpovídaly zvolenému portu, takže logické funkce, OAuth a veškerá ostatní vnitřní síťová komunikace fungují správně.
Po spuštění je server automaticky zaregistrován jako `local` remote ve vaší konfiguraci CLI.
### Kontrola stavu serveru
```bash filename="Terminal"
yarn twenty server status
```
Zobrazí, zda server běží, jeho URL a výchozí přihlašovací údaje (`tim@apple.dev` / `tim@apple.dev`).
### Zobrazení protokolů serveru
```bash filename="Terminal"
yarn twenty server logs
```
Streamuje protokoly kontejneru. Pomocí `--lines` ovládnete, kolik posledních řádků se má zobrazit:
```bash filename="Terminal"
yarn twenty server logs --lines 100
```
### Zastavení serveru
```bash filename="Terminal"
yarn twenty server stop
```
Zastaví kontejner. Vaše data jsou zachována ve svazcích Dockeru — další `start` naváže tam, kde jste skončili.
### Resetování serveru
```bash filename="Terminal"
yarn twenty server reset
```
Odstraní kontejner **a** smaže oba svazky Dockeru, čímž vymaže všechna data. Další `start` vytvoří čistou instanci.
<Note>
Server vyžaduje, aby **Docker** běžel. Pokud vidíte chybu "Docker not running", ujistěte se, že je spuštěný Docker Desktop (nebo démon Dockeru).
</Note>
### Přehled příkazů
Nástroj pro vytvoření kostry vám již spustil lokální server Twenty. Pro jeho správu později použijte `yarn twenty server`:
| Příkaz | Popis |
| -------------------------------------- | ------------------------------------------------------ |
@@ -313,65 +226,11 @@ Odstraní kontejner **a** smaže oba svazky Dockeru, čímž vymaže všechna da
| `yarn twenty server logs --lines 100` | Zobrazí posledních 100 řádků logu |
| `yarn twenty server reset` | Smaže všechna data a začne znovu |
## CI s GitHub Actions
Data přetrvávají při restartech ve dvou svazcích Dockeru (`twenty-app-dev-data` pro PostgreSQL, `twenty-app-dev-storage` pro soubory). Pomocí `reset` vymažte vše a začněte znovu.
Generátor kostry vytvoří připravený k použití workflow GitHub Actions v `.github/workflows/ci.yml`. Automaticky spouští integrační testy při každém pushi do `main` a u pull requestů.
Workflow:
1. Načte váš kód (checkout).
2. Spustí dočasný server Twenty pomocí akce `twentyhq/twenty/.github/actions/spawn-twenty-docker-image`
3. Nainstaluje závislosti pomocí `yarn install --immutable`
4. Spustí `yarn test` s proměnnými `TWENTY_API_URL` a `TWENTY_API_KEY` vloženými z výstupů akce
```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 }}
```
Není potřeba konfigurovat žádné secrets — akce `spawn-twenty-docker-image` spustí efemérní server Twenty přímo v runneru a vypíše podrobnosti připojení. Secret `GITHUB_TOKEN` je poskytován GitHubem automaticky.
Chcete-li připnout konkrétní verzi Twenty místo `latest`, změňte proměnnou prostředí `TWENTY_VERSION` na začátku workflow.
<Note>
Server vyžaduje, aby **Docker** běžel. Pokud vidíte chybu "Docker not running", ujistěte se, že je spuštěný Docker Desktop (nebo démon Dockeru).
</Note>
## Ruční nastavení (bez scaffolderu)
@@ -52,7 +52,11 @@ yarn twenty deploy
### Sdílení nasazené aplikace
Aplikace ve formě tarball nejsou uvedeny ve veřejném tržišti, takže je ostatní pracovní prostory na tomtéž serveru procházením neobjeví. Chcete-li sdílet nasazenou aplikaci:
<Warning>
Sdílení soukromých (tarball) aplikací napříč pracovními prostory je funkcí **Enterprise**. Karta **Distribution** bude místo ovládacích prvků sdílení zobrazovat výzvu k upgradu, dokud váš pracovní prostor nebude mít platný klíč Enterprise. Přejděte do [Nastavení > Admin Panel > Enterprise](/settings/admin-panel#enterprise) a aktivujte ji.
</Warning>
Aplikace ve formě tarball nejsou uvedeny ve veřejném tržišti, takže je ostatní pracovní prostory na tomtéž serveru procházením neobjeví. Jakmile je váš pracovní prostor na tarifu Enterprise, můžete sdílet nasazenou aplikaci takto:
1. Přejděte do **Nastavení > Aplikace > Registrace** a otevřete svou aplikaci
2. Na kartě **Distribuce** klikněte na **Zkopírovat odkaz ke sdílení**
@@ -60,18 +64,20 @@ Aplikace ve formě tarball nejsou uvedeny ve veřejném tržišti, takže je ost
Odkaz ke sdílení používá základní adresu URL serveru (bez jakékoli subdomény pracovního prostoru), takže funguje pro libovolný pracovní prostor na serveru.
<Warning>
Sdílení soukromých aplikací je funkce Enterprise. Přejděte do [Nastavení > Admin Panel > Enterprise](/settings/admin-panel#enterprise) a povolte ji.
</Warning>
### Správa verzí
Při aktualizaci již nasazené tarballové aplikace server vyžaduje, aby hodnota `version` v `package.json` byla **přísně vyšší** (podle řazení [semver](https://semver.org)) než aktuálně nasazená verze. Opětovné nasazení stejné verze nebo odeslání nižší verze je odmítnuto ještě před uložením tarballu — v CLI uvidíte chybu `VERSION_ALREADY_EXISTS`.
Chcete-li vydat aktualizaci:
1. Zvyšte hodnotu pole `version` v souboru `package.json`
1. Zvyšte hodnotu pole `version` v souboru `package.json` (např. `1.2.3` → `1.2.4`, `1.3.0` nebo `2.0.0`)
2. Spusťte `yarn twenty deploy` (nebo `yarn twenty deploy --remote production`)
3. Pracovní prostory, které mají aplikaci nainstalovanou, uvidí dostupnou aktualizaci ve svém nastavení
<Note>
Předběžné tagy fungují podle očekávání: zvýšení z `1.0.0-rc.1` → `1.0.0-rc.2` je povoleno a finální vydání jako `1.0.0` je správně rozpoznáno jako vyšší než `1.0.0-rc.5`. Verze v `package.json` musí být platným řetězcem semver.
</Note>
{/* TODO: add screenshot of the Upgrade button */}
## Publikování na npm
@@ -189,3 +195,12 @@ Aplikace můžete nainstalovat také z příkazového řádku:
```bash filename="Terminal"
yarn twenty install
```
<Note>
Server při instalaci vynucuje verzování semver a zrcadlí pravidla pro nasazení:
* Instalace stejné verze, která je již nainstalována ve vašem pracovním prostoru, je odmítnuta s chybou `APP_ALREADY_INSTALLED`.
* Instalace nižší verze, než je aktuálně nainstalovaná, je odmítnuta s chybou `CANNOT_DOWNGRADE_APPLICATION`.
K instalaci novější verze ji nejprve nasaďte nebo publikujte, poté znovu spusťte `yarn twenty install`.
</Note>
@@ -140,12 +140,12 @@ Data neodpovídají očekávanému formátu pro tento typ pole.
#### Datum
**Problém:** Nerozpoznaný formát data
**Řešení:** Používejte jednotný formát v celém souboru
**Řešení:** Používejte formát `YYYY-MM-DD` jednotně v celém souboru
```
✓ 2024-03-15 (YYYY-MM-DD - recommended)
03/15/2024 (MM/DD/YYYY)
15/03/2024 (DD/MM/YYYY)
✓ 2024-03-15 (YYYY-MM-DD)
03/15/2024 (MM/DD/YYYY)
15/03/2024 (DD/MM/YYYY)
```
#### Telefon
@@ -103,8 +103,6 @@ Adresa je **vnořené pole** s více sloupci (některé mohou zůstat prázdné)
Používejte jednotný formát v celém souboru:
* `YYYY-MM-DD` (doporučeno): `2024-03-15`
* `MM/DD/YYYY`: `03/15/2024`
* `DD/MM/YYYY`: `15/03/2024`
* ISO 8601: `2024-03-15T10:30:00Z`
### Číselná pole
@@ -50,23 +50,6 @@ npx nx run twenty-server:database:reset
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/nameOfYourMigration -d src/database/typeorm/core/core.datasource.ts
```
#### Für Arbeitsbereichsobjekte
Es gibt keine Migrationsdateien, Migrationen werden automatisch für jeden Arbeitsbereich generiert,
in der Datenbank gespeichert und mit diesem Befehl angewendet
```bash
npx nx run twenty-server:command workspace:sync-metadata -f
```
<Warning>
Dadurch wird die Datenbank gelöscht und die Migrationen sowie das Seeding erneut ausgeführt.
Stellen Sie sicher, dass Sie alle Daten sichern, die Sie aufbewahren möchten, bevor Sie diesen Befehl ausführen.
</Warning>
## Technologie-Stack
Twenty verwendet in erster Linie NestJS für das Backend.
@@ -81,7 +81,7 @@ import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
displayName: 'My Twenty App',
description: 'My first Twenty app',
icon: 'IconWorld',
@@ -716,6 +716,13 @@ Hauptpunkte:
Frontend-Komponenten sind React-Komponenten, die direkt innerhalb der Twenty-UI gerendert werden. Sie laufen in einem **isolierten Web Worker** unter Verwendung von Remote DOM — Ihr Code wird in einer Sandbox ausgeführt, rendert jedoch nativ auf der Seite, nicht in einem iframe.
#### Wo Front-Komponenten verwendet werden können
Front-Komponenten können an zwei Stellen innerhalb von Twenty gerendert werden:
* **Seitenpanel** — Nicht-Headless-Front-Komponenten werden im rechten Seitenpanel geöffnet. Dies ist das Standardverhalten, wenn eine Front-Komponente über das Befehlsmenü ausgelöst wird.
* **Widgets (Dashboards und Datensatzseiten)** — Front-Komponenten können als Widgets in Seitenlayouts eingebettet werden. Beim Konfigurieren eines Dashboards oder eines Datensatzseiten-Layouts können Benutzer ein Front-Komponenten-Widget hinzufügen.
#### Einfaches Beispiel
Der schnellste Weg, eine Frontend-Komponente in Aktion zu sehen, ist, sie als Befehl zu registrieren. Das Hinzufügen eines `command`-Felds mit `isPinned: true` lässt sie als Schnellaktionsschaltfläche oben rechts auf der Seite erscheinen — kein Seitenlayout erforderlich:
@@ -748,7 +755,7 @@ export default defineFrontComponent({
});
```
Nach dem Synchronisieren mit `yarn twenty dev` erscheint die Schnellaktion oben rechts auf der Seite:
Nach dem Synchronisieren mit `yarn twenty dev` (oder durch einmaliges Ausführen von `yarn twenty dev --once`) erscheint die Schnellaktion oben rechts auf der Seite:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Schnellaktionsschaltfläche oben rechts" />
@@ -773,9 +780,13 @@ Klicken Sie darauf, um die Komponente inline zu rendern.
Über Befehle hinaus können Sie eine Frontend-Komponente direkt in eine Datensatzseite einbetten, indem Sie sie als Widget in einem **Seitenlayout** hinzufügen. Details finden Sie im Abschnitt [definePageLayout](#definepagelayout).
#### Headless-Komponenten (`isHeadless: true`)
#### Headless vs. Nicht-Headless
Headless-Komponenten rendern keine sichtbare UI, führen aber dennoch React-Logik aus. Dies ist nützlich für **Effekt-Komponenten** — Komponenten, die beim Mounten Seiteneffekte ausführen, wie z. B. das Synchronisieren von Daten, Starten eines Timers, Lauschen auf Ereignisse oder Auslösen einer Benachrichtigung.
Front-Komponenten gibt es in zwei Rendering-Modi, die durch die Option `isHeadless` gesteuert werden:
**Nicht-Headless (Standard)** — Die Komponente rendert eine sichtbare UI. Wird sie über das Befehlsmenü ausgelöst, öffnet sie sich im Seitenpanel. Dies ist das Standardverhalten, wenn `isHeadless` `false` ist oder weggelassen wird.
**Headless (`isHeadless: true`)** — Die Komponente wird unsichtbar im Hintergrund gemountet. Sie öffnet das Seitenpanel nicht. Headless-Komponenten sind für Aktionen konzipiert, die Logik ausführen und sich anschließend selbst unmounten — zum Beispiel das Ausführen einer asynchronen Aufgabe, das Navigieren zu einer Seite oder das Anzeigen eines Bestätigungsdialogs. Sie lassen sich gut mit den unten beschriebenen SDK-Command-Komponenten kombinieren.
```tsx src/front-components/sync-tracker.tsx
import { defineFrontComponent, useRecordId, enqueueSnackbar } from 'twenty-sdk';
@@ -802,6 +813,89 @@ export default defineFrontComponent({
Da die Komponente `null` zurückgibt, überspringt Twenty das Rendern eines Containers dafür — im Layout entsteht kein Leerraum. Die Komponente hat dennoch Zugriff auf alle Hooks und die Host-Kommunikations-API.
#### SDK-Command-Komponenten
Das Paket `twenty-sdk` stellt vier Command-Hilfskomponenten bereit, die für Headless-Front-Komponenten ausgelegt sind. Jede Komponente führt beim Mounten eine Aktion aus, behandelt Fehler durch Anzeige einer Snackbar-Benachrichtigung und unmountet die Front-Komponente nach Abschluss automatisch.
Importieren Sie sie aus `twenty-sdk/command`:
* **`Command`** — Führt einen asynchronen Callback über das Prop `execute` aus.
* **`CommandLink`** — Navigiert zu einem App-Pfad. Props: `to`, `params`, `queryParams`, `options`.
* **`CommandModal`** — Öffnet einen Bestätigungsdialog. Bestätigt der Benutzer, wird der Callback `execute` ausgeführt. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`.
* **`CommandOpenSidePanelPage`** — Öffnet eine bestimmte Seite im Seitenpanel. Props: `page`, `pageTitle`, `pageIcon`.
Hier ist ein vollständiges Beispiel einer Headless-Front-Komponente, die `Command` verwendet, um eine Aktion aus dem Befehlsmenü auszuführen:
```tsx src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk';
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',
},
});
```
Und ein Beispiel, das `CommandModal` verwendet, um vor der Ausführung um Bestätigung zu bitten:
```tsx src/front-components/delete-draft.tsx
import { defineFrontComponent } from 'twenty-sdk';
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',
},
});
```
#### Zugriff auf den Laufzeitkontext
Verwenden Sie innerhalb Ihrer Komponente SDK-Hooks, um auf den aktuellen Benutzer, den Datensatz und die Komponenteninstanz zuzugreifen:
@@ -858,6 +952,50 @@ Frontend-Komponenten können Navigation, Modals und Benachrichtigungen mittels F
| `unmountFrontComponent()` | Die Komponente entfernen |
| `updateProgress(progress)` | Einen Fortschrittsindikator aktualisieren |
Hier ist ein Beispiel, das die Host-API verwendet, um nach Abschluss einer Aktion eine Snackbar anzuzeigen und das Seitenpanel zu schließen:
```tsx src/front-components/archive-record.tsx
import { defineFrontComponent, useRecordId } from 'twenty-sdk';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk';
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,
});
```
#### Befehlsoptionen
Das Hinzufügen eines `command`-Felds zu `defineFrontComponent` registriert die Komponente im Befehlsmenü (Cmd+K). Wenn `isPinned` `true` ist, erscheint sie außerdem als Schnellaktionsschaltfläche oben rechts auf der Seite.
@@ -1263,7 +1401,7 @@ export default defineFrontComponent({
### Wie das Bundling funktioniert
Der Build-Schritt (`yarn twenty dev` oder `yarn twenty build`) verwendet esbuild, um pro Logikfunktion und pro Frontend-Komponente eine einzelne, in sich geschlossene Datei zu erzeugen. Alle importierten Pakete werden in das Bundle eingebettet.
Der Build-Schritt verwendet esbuild, um pro Logikfunktion und pro Frontend-Komponente eine einzelne, in sich geschlossene Datei zu erzeugen. Alle importierten Pakete werden in das Bundle eingebettet.
**Logikfunktionen** laufen in einer Node.js-Umgebung. Eingebaute Node.js-Module (`fs`, `path`, `crypto`, `http` usw.) stehen zur Verfügung und müssen nicht installiert werden.
@@ -1712,3 +1850,86 @@ yarn twenty uninstall
# Skip the confirmation prompt
yarn twenty uninstall --yes
```
## Remotes verwalten
Ein **Remote** ist ein Twenty-Server, mit dem sich Ihre App verbindet. Während der Einrichtung erstellt das Scaffolding-Tool automatisch eines für Sie. Sie können jederzeit weitere Remotes hinzufügen oder zwischen ihnen wechseln.
```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>
```
Ihre Anmeldedaten werden in `~/.twenty/config.json` gespeichert.
## CI mit GitHub Actions
Das Scaffolding-Tool erzeugt einen einsatzbereiten GitHub-Actions-Workflow in `.github/workflows/ci.yml`. Er führt Ihre Integrationstests automatisch bei jedem Push auf `main` und bei Pull Requests aus.
Der Workflow:
1. Checkt Ihren Code aus
2. Startet einen temporären Twenty-Server mit der Aktion `twentyhq/twenty/.github/actions/spawn-twenty-docker-image`
3. Installiert Abhängigkeiten mit `yarn install --immutable`
4. Führt `yarn test` aus, wobei `TWENTY_API_URL` und `TWENTY_API_KEY` aus den Aktionsausgaben injiziert werden.
```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 }}
```
Sie müssen keine Secrets konfigurieren — die Aktion `spawn-twenty-docker-image` startet einen flüchtigen Twenty-Server direkt im Runner und gibt die Verbindungsdetails aus. Das Secret `GITHUB_TOKEN` wird automatisch von GitHub bereitgestellt.
Um eine bestimmte Twenty-Version statt `latest` festzulegen, ändern Sie die Umgebungsvariable `TWENTY_VERSION` oben im Workflow.
@@ -7,7 +7,9 @@ description: Erstellen Sie in wenigen Minuten Ihre erste Twenty-App.
Apps befinden sich derzeit in der Alpha-Phase. Die Funktion ist funktionsfähig, entwickelt sich jedoch noch weiter.
</Warning>
Apps ermöglichen es Ihnen, Twenty mit benutzerdefinierten Objekten, Feldern, Logikfunktionen, KI-Fähigkeiten und UI-Komponenten zu erweitern — alles als Code verwaltet.
## Was sind Apps?
Apps ermöglichen es Ihnen, Twenty mit benutzerdefinierten Objekten, Feldern, Logikfunktionen, Frontend-Komponenten, KI-Fähigkeiten und mehr zu erweitern — alles als Code verwaltet. Anstatt alles über die UI zu konfigurieren, definieren Sie Ihr Datenmodell und Ihre Logik in TypeScript und stellen es in einem oder mehreren Workspaces bereit.
## Voraussetzungen
@@ -17,7 +19,9 @@ Bevor Sie beginnen, stellen Sie sicher, dass Folgendes auf Ihrem Rechner install
* **Yarn 4** — Wird mit Node.js über Corepack mitgeliefert. Aktivieren Sie es, indem Sie `corepack enable` ausführen
* **Docker** — [Hier herunterladen](https://www.docker.com/products/docker-desktop/). Erforderlich, um eine lokale Twenty-Instanz auszuführen. Nicht erforderlich, wenn bereits ein Twenty-Server läuft.
## Schritt 1: App-Gerüst erstellen
## Erstellen Sie Ihre erste App
### App-Gerüst erstellen
Öffnen Sie ein Terminal und führen Sie Folgendes aus:
@@ -29,18 +33,7 @@ Sie werden aufgefordert, einen Namen und eine Beschreibung für Ihre App einzuge
Dadurch wird ein neuer Ordner namens `my-twenty-app` mit allem erstellt, was Sie benötigen.
<Note>
Das Scaffolding-Tool unterstützt diese Flags:
* `--minimal` — erstellt nur die wesentlichen Dateien, keine Beispiele (Standard)
* `--exhaustive` — erstellt alle Beispiel-Entitäten
* `--name <name>` — legt den App-Namen fest (überspringt die Abfrage)
* `--display-name <displayName>` — legt den Anzeigenamen fest (überspringt die Abfrage)
* `--description <description>` — legt die Beschreibung fest (überspringt die Abfrage)
* `--skip-local-instance` — überspringt die Eingabeaufforderung zur Einrichtung des lokalen Servers
</Note>
## Schritt 2: Lokale Twenty-Instanz einrichten
### Lokale Twenty-Instanz einrichten
Das Scaffolding-Tool fragt:
@@ -53,7 +46,7 @@ Das Scaffolding-Tool fragt:
<img src="/images/docs/developers/extends/apps/start-instance.png" alt="Soll die lokale Instanz gestartet werden?" />
</div>
## Schritt 3: Bei Ihrem Arbeitsbereich anmelden
### Melden Sie sich bei Ihrem Arbeitsbereich an
Anschließend öffnet sich ein Browserfenster mit der Twenty-Anmeldeseite. Melden Sie sich mit dem vorab eingerichteten Demo-Konto an:
@@ -64,7 +57,7 @@ Anschließend öffnet sich ein Browserfenster mit der Twenty-Anmeldeseite. Melde
<img src="/images/docs/developers/extends/apps/login.png" alt="Twenty-Anmeldebildschirm" />
</div>
## Schritt 4: Die App autorisieren
### Autorisieren Sie die App
Nach der Anmeldung sehen Sie einen Autorisierungsbildschirm. Dadurch kann Ihre App mit Ihrem Arbeitsbereich interagieren.
@@ -80,7 +73,7 @@ Nach der Autorisierung bestätigt Ihr Terminal, dass alles eingerichtet ist.
<img src="/images/docs/developers/extends/apps/scaffolded.png" alt="App-Gerüst erfolgreich erstellt" />
</div>
## Schritt 5: Mit der Entwicklung beginnen
### Beginnen Sie mit der Entwicklung
Wechseln Sie in Ihren neuen App-Ordner und starten Sie den Entwicklungsserver:
@@ -105,7 +98,22 @@ Der Dev-Modus ist nur auf Twenty-Instanzen verfügbar, die im Entwicklungsmodus
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Terminalausgabe im Dev-Modus" />
</div>
## Schritt 6: Ihre App in Twenty ansehen
#### Einmalige Synchronisierung mit `yarn twenty dev --once`
Wenn Sie keinen Watcher im Hintergrund ausführen lassen möchten (zum Beispiel in einer CI-Pipeline, einem Git-Hook oder einem skriptgesteuerten Workflow), geben Sie das Flag `--once` an. Es führt dieselbe Pipeline aus wie `yarn twenty dev` — Build-Manifest erstellen, Dateien bündeln, hochladen, synchronisieren, den typisierten API-Client neu generieren — beendet sich jedoch, **sobald die Synchronisierung abgeschlossen ist**:
```bash filename="Terminal"
yarn twenty dev --once
```
| Befehl | Verhalten | Wann verwenden |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `yarn twenty dev` | Überwacht Ihre Quelldateien und synchronisiert bei jeder Änderung erneut. Läuft weiter, bis Sie es stoppen. | Interaktive lokale Entwicklung — Sie möchten das Live-Status-Panel und eine sofortige Feedback-Schleife. |
| `yarn twenty dev --once` | Führt einen einzelnen Build + Sync aus und beendet sich anschließend mit Code `0` bei Erfolg oder `1` bei einem Fehler. | Skripte, CI, Pre-Commit-Hooks, KI-Agenten und jeder nicht-interaktive Workflow. |
Beide Modi erfordern einen Twenty-Server, der im Entwicklungsmodus läuft, und ein authentifiziertes Remote — es gelten dieselben Voraussetzungen.
### Sehen Sie sich Ihre App in Twenty an
Öffnen Sie [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer) in Ihrem Browser. Navigieren Sie zu **Settings > Apps** und wählen Sie die Registerkarte **Developer**. Unter **Your Apps** sollte Ihre App aufgeführt sein:
@@ -133,13 +141,28 @@ Wechseln Sie zur Registerkarte **Content**, um alles zu sehen, was Ihre App bere
Alles erledigt! Bearbeiten Sie eine beliebige Datei in `src/`, und die Änderungen werden automatisch übernommen.
Wechseln Sie zu [Apps erstellen](/l/de/developers/extend/apps/building) für eine ausführliche Anleitung zum Erstellen von Objekten, Logikfunktionen, Frontend-Komponenten, Skills und mehr.
---
## Was Sie erstellen können
Apps bestehen aus **Entitäten** — jede ist als TypeScript-Datei mit einem einzigen `export default` definiert:
| Entität | Was sie macht |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| **Objekte & Felder** | Definieren Sie benutzerdefinierte Datenmodelle (wie Post Card, Invoice) mit typisierten Feldern |
| **Logikfunktionen** | Serverseitige TypeScript-Funktionen, die durch HTTP-Routen, Cron-Zeitpläne oder Datenbankereignisse ausgelöst werden |
| **Frontend-Komponenten** | React-Komponenten, die in der UI von Twenty gerendert werden (Seitenleiste, Widgets, Befehlsmenü) |
| **Fähigkeiten & Agenten** | KI-Funktionen — wiederverwendbare Anweisungen und autonome Assistenten |
| **Ansichten & Navigation** | Vorkonfigurierte Listenansichten und Seitenleisteneinträge für Ihre Objekte |
| **Seitenlayouts** | Benutzerdefinierte Datensatz-Detailseiten mit Tabs und Widgets |
Wechseln Sie zu [Apps erstellen](/l/de/developers/extend/apps/building) für eine ausführliche Anleitung zu jedem Entitätstyp.
---
## Projektstruktur
Das Scaffolding-Tool erzeugt die folgende Verzeichnisstruktur (gezeigt im Modus `--exhaustive`, der Beispiele für jeden Entitätstyp enthält):
Der Scaffolder erzeugt die folgende Verzeichnisstruktur:
```text filename="my-twenty-app/"
my-twenty-app/
@@ -148,160 +171,50 @@ my-twenty-app/
.gitignore
.nvmrc
.yarnrc.yml
.yarn/
install-state.gz
.oxlintrc.json
tsconfig.json
tsconfig.spec.json # TypeScript config for tests
vitest.config.ts # Vitest test runner configuration
tsconfig.spec.json # TypeScript config for tests
vitest.config.ts # Vitest test runner configuration
LLMS.md
README.md
.github/
└── workflows/
└── ci.yml # GitHub Actions CI workflow
public/ # Public assets (images, fonts, etc.)
└── ci.yml # GitHub Actions CI workflow
public/ # Public assets (images, fonts, etc.)
src/
├── application-config.ts # Required — main application configuration
├── __tests__/
├── setup-test.ts # Test setup (server health check, config)
│ └── app-install.integration-test.ts # Example integration test
── roles/
── default-role.ts # Default role for logic functions
├── objects/
│ └── example-object.ts # Example custom object definition
├── fields/
│ └── example-field.ts # Example standalone field definition
├── logic-functions/
│ ├── hello-world.ts # Example logic function
│ ├── create-hello-world-company.ts # Example logic function using CoreApiClient
│ ├── pre-install.ts # Runs before installation
│ └── post-install.ts # Runs after installation
├── front-components/
│ └── hello-world.tsx # Example front component
├── page-layouts/
│ └── example-record-page-layout.ts # Example page layout with front component
├── views/
│ └── example-view.ts # Example saved view definition
├── navigation-menu-items/
│ └── example-navigation-menu-item.ts # Example sidebar navigation link
├── skills/
│ └── example-skill.ts # Example AI agent skill definition
└── agents/
└── example-agent.ts # Example AI agent definition
├── application-config.ts # Required — main application configuration
├── default-role.ts # Default role for logic functions
├── constants/
│ └── universal-identifiers.ts # Auto-generated UUIDs and app metadata
── __tests__/
── setup-test.ts # Test setup (server health check, config)
└── app-install.integration-test.ts # Integration test
```
Standardmäßig (`--minimal`) werden nur die Kerndateien erstellt: `application-config.ts`, `roles/default-role.ts`, `logic-functions/pre-install.ts` und `logic-functions/post-install.ts`. Verwenden Sie `--exhaustive`, um alle oben gezeigten Beispieldateien einzuschließen.
### Mit einem Beispiel beginnen
Um mit einem umfassenderen Beispiel mit benutzerdefinierten Objekten, Feldern, Logikfunktionen, Frontend-Komponenten und mehr zu starten, verwenden Sie die Option `--example`:
```bash filename="Terminal"
npx create-twenty-app@latest my-twenty-app --example postcard
```
Die Beispiele stammen aus dem Verzeichnis [twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples) auf GitHub. Sie können auch einzelne Entitäten in einem bestehenden Projekt mit `yarn twenty add` erzeugen (siehe [Apps erstellen](/l/de/developers/extend/apps/building#scaffolding-entities-with-yarn-twenty-add)).
### Wichtige Dateien
| Datei / Ordner | Zweck |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `package.json` | Deklariert den App-Namen, die Version und Abhängigkeiten. Enthält ein `twenty`-Skript, sodass Sie `yarn twenty help` ausführen können, um alle Befehle anzuzeigen. |
| `src/application-config.ts` | **Erforderlich.** Die Hauptkonfigurationsdatei für Ihre App. |
| `src/roles/` | Definiert Rollen, die steuern, worauf Ihre Logikfunktionen zugreifen können. |
| `src/logic-functions/` | Serverseitige Funktionen, die durch Routen, Cron-Zeitpläne oder Datenbankereignisse ausgelöst werden. |
| `src/front-components/` | React-Komponenten, die innerhalb der Twenty-UI gerendert werden. |
| `src/objects/` | Benutzerdefinierte Objektdefinitionen zur Erweiterung Ihres Datenmodells. |
| `src/fields/` | Benutzerdefinierte Felder, die vorhandenen Objekten hinzugefügt werden. |
| `src/views/` | Konfigurationen gespeicherter Ansichten. |
| `src/navigation-menu-items/` | Benutzerdefinierte Links in der Seitenleisten-Navigation. |
| `src/skills/` | Skills, die die KI-Agenten von Twenty erweitern. |
| `src/agents/` | KI-Agenten mit benutzerdefinierten Prompts. |
| `src/page-layouts/` | Benutzerdefinierte Seitenlayouts für Datensatzansichten. |
| `src/__tests__/` | Integrationstests (Setup + Beispieltest). |
| `public/` | Statische Assets (Bilder, Schriftarten), die mit Ihrer App ausgeliefert werden. |
| Datei / Ordner | Zweck |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `package.json` | Deklariert den App-Namen, die Version und Abhängigkeiten. Enthält ein `twenty`-Skript, sodass Sie `yarn twenty help` ausführen können, um alle Befehle anzuzeigen. |
| `src/application-config.ts` | **Erforderlich.** Die Hauptkonfigurationsdatei für Ihre App. |
| `src/default-role.ts` | Standardrolle, die steuert, worauf Ihre Logikfunktionen zugreifen können. |
| `src/constants/universal-identifiers.ts` | Automatisch erzeugte UUIDs und App-Metadaten (Anzeigename, Beschreibung). |
| `src/__tests__/` | Integrationstests (Setup + Beispieltest). |
| `public/` | Statische Assets (Bilder, Schriftarten), die mit Ihrer App ausgeliefert werden. |
## Remotes verwalten
## Lokaler Entwicklungsserver
Ein **Remote** ist ein Twenty-Server, mit dem sich Ihre App verbindet. Während der Einrichtung erstellt das Scaffolding-Tool automatisch eines für Sie. Sie können jederzeit weitere Remotes hinzufügen oder zwischen ihnen wechseln.
```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>
```
Ihre Anmeldedaten werden in `~/.twenty/config.json` gespeichert.
## Lokaler Entwicklungsserver (`yarn twenty server`)
Die CLI kann einen lokalen, in Docker laufenden Twenty-Server verwalten. Dies ist derselbe Server, der automatisch gestartet wird, wenn Sie mit `create-twenty-app` eine App aufsetzen, aber Sie können ihn auch manuell verwalten.
### Server starten
```bash filename="Terminal"
yarn twenty server start
```
Dadurch wird das Docker-Image `twentycrm/twenty-app-dev:latest` heruntergeladen (falls nicht bereits vorhanden), ein Container namens `twenty-app-dev` erstellt und auf Port **2020** gestartet. Die CLI wartet, bis der Server seinen Health-Check bestanden hat, bevor sie zurückkehrt.
Es werden zwei Docker-Volumes erstellt, um Daten zwischen Neustarts beizubehalten:
* `twenty-app-dev-data` — PostgreSQL-Datenbank
* `twenty-app-dev-storage` — Dateispeicher
Wenn Port 2020 bereits verwendet wird, können Sie auf einem anderen Port starten:
```bash filename="Terminal"
yarn twenty server start --port 3030
```
Die CLI konfiguriert automatisch die internen Werte `NODE_PORT` und `SERVER_URL` des Containers passend zum gewählten Port, sodass Logikfunktionen, OAuth und alle anderen internen Netzwerkfunktionen korrekt arbeiten.
Nach dem Start wird der Server automatisch als `local`-Remote in Ihrer CLI-Konfiguration registriert.
### Serverstatus prüfen
```bash filename="Terminal"
yarn twenty server status
```
Zeigt an, ob der Server läuft, seine URL und die Standard-Anmeldedaten (`tim@apple.dev` / `tim@apple.dev`).
### Serverprotokolle anzeigen
```bash filename="Terminal"
yarn twenty server logs
```
Streamt die Containerprotokolle. Verwenden Sie `--lines`, um zu steuern, wie viele der letzten Zeilen angezeigt werden:
```bash filename="Terminal"
yarn twenty server logs --lines 100
```
### Server stoppen
```bash filename="Terminal"
yarn twenty server stop
```
Stoppt den Container. Ihre Daten bleiben in den Docker-Volumes erhalten — der nächste `start` macht dort weiter, wo Sie aufgehört haben.
### Server zurücksetzen
```bash filename="Terminal"
yarn twenty server reset
```
Entfernt den Container **und** löscht beide Docker-Volumes, wobei alle Daten gelöscht werden. Der nächste `start` erstellt eine frische Instanz.
<Note>
Der Server erfordert, dass **Docker** läuft. Wenn der Fehler "Docker not running" angezeigt wird, stellen Sie sicher, dass Docker Desktop (oder der Docker-Daemon) gestartet ist.
</Note>
### Befehlsreferenz
Der Scaffolder hat bereits einen lokalen Twenty-Server für Sie gestartet. Um ihn später zu verwalten, verwenden Sie `yarn twenty server`:
| Befehl | Beschreibung |
| -------------------------------------- | ----------------------------------------------------------- |
@@ -313,65 +226,11 @@ Entfernt den Container **und** löscht beide Docker-Volumes, wobei alle Daten ge
| `yarn twenty server logs --lines 100` | Die letzten 100 Protokollzeilen anzeigen |
| `yarn twenty server reset` | Alle Daten löschen und neu starten |
## CI mit GitHub Actions
Daten bleiben über Neustarts hinweg in zwei Docker-Volumes bestehen (`twenty-app-dev-data` für PostgreSQL, `twenty-app-dev-storage` für Dateien). Verwenden Sie `reset`, um alles zu löschen und neu zu beginnen.
Das Scaffolding-Tool erzeugt einen einsatzbereiten GitHub-Actions-Workflow in `.github/workflows/ci.yml`. Er führt Ihre Integrationstests automatisch bei jedem Push auf `main` und bei Pull Requests aus.
Der Workflow:
1. Checkt Ihren Code aus
2. Startet einen temporären Twenty-Server mit der Aktion `twentyhq/twenty/.github/actions/spawn-twenty-docker-image`
3. Installiert Abhängigkeiten mit `yarn install --immutable`
4. Führt `yarn test` aus, wobei `TWENTY_API_URL` und `TWENTY_API_KEY` aus den Aktionsausgaben injiziert werden.
```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 }}
```
Sie müssen keine Secrets konfigurieren — die Aktion `spawn-twenty-docker-image` startet einen flüchtigen Twenty-Server direkt im Runner und gibt die Verbindungsdetails aus. Das Secret `GITHUB_TOKEN` wird automatisch von GitHub bereitgestellt.
Um eine bestimmte Twenty-Version statt `latest` festzulegen, ändern Sie die Umgebungsvariable `TWENTY_VERSION` oben im Workflow.
<Note>
Der Server erfordert, dass **Docker** läuft. Wenn der Fehler "Docker not running" angezeigt wird, stellen Sie sicher, dass Docker Desktop (oder der Docker-Daemon) gestartet ist.
</Note>
## Manuelle Einrichtung (ohne Scaffolder)
@@ -52,7 +52,11 @@ yarn twenty deploy
### Eine bereitgestellte App freigeben
Tarball-Apps werden nicht im öffentlichen Marktplatz gelistet, daher entdecken andere Arbeitsbereiche auf demselben Server sie nicht durch Stöbern. So geben Sie eine bereitgestellte App frei:
<Warning>
Das Teilen privater (Tarball-)Apps über Arbeitsbereiche hinweg ist eine **Enterprise**-Funktion. Die Registerkarte **Distribution** zeigt anstelle der Freigabeoptionen eine Aufforderung zum Upgrade an, bis Ihr Arbeitsbereich über einen gültigen Enterprise-Schlüssel verfügt. Gehen Sie zu [Einstellungen > Admin-Panel > Enterprise](/settings/admin-panel#enterprise), um es zu aktivieren.
</Warning>
Tarball-Apps werden nicht im öffentlichen Marktplatz gelistet, daher entdecken andere Arbeitsbereiche auf demselben Server sie nicht durch Stöbern. Sobald sich Ihr Arbeitsbereich im Enterprise-Plan befindet, können Sie eine bereitgestellte App wie folgt freigeben:
1. Gehen Sie zu **Einstellungen > Anwendungen > Registrierungen** und öffnen Sie Ihre App
2. Klicken Sie im Tab **Distribution** auf **Freigabelink kopieren**
@@ -60,18 +64,20 @@ Tarball-Apps werden nicht im öffentlichen Marktplatz gelistet, daher entdecken
Der Freigabelink verwendet die Basis-URL des Servers (ohne Workspace-Subdomain), sodass er für jeden Arbeitsbereich auf dem Server funktioniert.
<Warning>
Das Teilen privater Apps ist eine Enterprise-Funktion. Gehen Sie zu [Einstellungen > Admin-Panel > Enterprise](/settings/admin-panel#enterprise), um es zu aktivieren.
</Warning>
### Versionsverwaltung
Beim Aktualisieren einer bereits bereitgestellten Tarball-App verlangt der Server, dass die `version` in `package.json` **strikt höher** (gemäß der [semver](https://semver.org)-Reihenfolge) ist als die derzeit bereitgestellte Version. Das erneute Bereitstellen derselben Version oder das Pushen einer niedrigeren Version wird abgelehnt, bevor das Tarball gespeichert wird — in der CLI wird ein `VERSION_ALREADY_EXISTS`-Fehler angezeigt.
So veröffentlichen Sie ein Update:
1. Erhöhen Sie das Feld `version` in Ihrer `package.json`
1. Erhöhen Sie das Feld `version` in Ihrer `package.json` (z. B. `1.2.3` → `1.2.4`, `1.3.0` oder `2.0.0`).
2. Führen Sie `yarn twenty deploy` aus (oder `yarn twenty deploy --remote production`)
3. Arbeitsbereiche, die die App installiert haben, sehen in ihren Einstellungen, dass ein Upgrade verfügbar ist.
<Note>
Pre-Release-Tags funktionieren wie erwartet: Das Erhöhen von `1.0.0-rc.1` → `1.0.0-rc.2` ist zulässig, und eine finale Version wie `1.0.0` wird korrekt als höher als `1.0.0-rc.5` erkannt. Die Version in `package.json` muss selbst eine gültige semver-Zeichenfolge sein.
</Note>
{/* TODO: add screenshot of the Upgrade button */}
## Auf npm veröffentlichen
@@ -189,3 +195,12 @@ Sie können Apps auch über die Befehlszeile installieren:
```bash filename="Terminal"
yarn twenty install
```
<Note>
Der Server erzwingt bei der Installation semver-Versionierung und spiegelt damit die Regeln beim Bereitstellen wider:
* Die Installation derselben Version, die in Ihrem Arbeitsbereich bereits installiert ist, wird mit einem `APP_ALREADY_INSTALLED`-Fehler abgelehnt.
* Die Installation einer niedrigeren Version als die aktuell installierte wird mit einem `CANNOT_DOWNGRADE_APPLICATION`-Fehler abgelehnt.
Um eine neuere Version zu installieren, stellen Sie sie zuerst bereit oder veröffentlichen Sie sie und führen Sie dann `yarn twenty install` erneut aus.
</Note>
@@ -140,12 +140,12 @@ Die Daten entsprechen nicht dem erwarteten Format für diesen Feldtyp.
#### Datum
**Problem:** Nicht erkanntes Datumsformat
**Lösung:** Verwenden Sie in der gesamten Datei ein einheitliches Format
**Lösung:** Verwenden Sie in der gesamten Datei durchgängig das Format `YYYY-MM-DD`
```
✓ 2024-03-15 (YYYY-MM-DD - empfohlen)
03/15/2024 (MM/DD/YYYY)
15/03/2024 (DD/MM/YYYY)
✓ 2024-03-15 (YYYY-MM-DD)
03/15/2024 (MM/DD/YYYY)
15/03/2024 (DD/MM/YYYY)
```
#### Telefon
@@ -103,8 +103,6 @@ Adresse ist ein **verschachteltes Feld** mit mehreren Spalten (einige können le
Verwenden Sie in Ihrer Datei ein einheitliches Format:
* `YYYY-MM-DD` (empfohlen): `2024-03-15`
* `MM/DD/YYYY`: `03/15/2024`
* `DD/MM/YYYY`: `15/03/2024`
* ISO 8601: `2024-03-15T10:30:00Z`
### Zahlenfelder
@@ -50,20 +50,6 @@ npx nx run twenty-server:database:reset
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/nameOfYourMigration -d src/database/typeorm/core/core.datasource.ts
```
#### Para objetos de Workspace
No hay archivos de migraciones, las migraciones se generan automáticamente para cada espacio de trabajo, se almacenan en la base de datos y se aplican con este comando
```bash
npx nx run twenty-server:command workspace:sync-metadata -f
```
<Warning>
Esto eliminará la base de datos y volverá a ejecutar las migraciones y semillas.
Asegúrate de respaldar cualquier dato que desees conservar antes de ejecutar este comando.
</Warning>
## Stack Tecnológico
Twenty utiliza principalmente NestJS para el backend.
@@ -372,10 +372,8 @@ Ejecución de los siguientes comandos:
```
yarn database:migrate:prod
yarn command:prod workspace:sync-metadata -f
yarn command:prod upgrade-0.22
```
El comando `yarn database:migrate:prod` aplicará las migraciones a la base de datos.
El comando `yarn command:prod workspace:sync-metadata -f` sincronizará la definición de objetos estándar a las tablas de metadatos y aplicará las migraciones requeridas a los espacios de trabajo existentes.
El comando `yarn command:prod upgrade-0.22` aplicará transformaciones de datos específicas para adaptarse a las nuevas opciones predeterminadas de instrumentación de solicitud de objetos.
@@ -43,27 +43,6 @@ npx nx run twenty-server:database:reset
```
### Migrations
#### Pour les objets dans les schémas Core/Metadata (TypeORM)
```bash
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/nameOfYourMigration -d src/database/typeorm/core/core.datasource.ts
```
#### Pour les objets de l'Espace de travail
Il n'y a pas de fichiers de migrations, les migrations sont générées automatiquement pour chaque espace de travail, stockées dans la base de données et appliquées avec cette commande
```bash
npx nx run twenty-server:command workspace:sync-metadata -f
```
<Warning>
Cela supprimera la base de données et réexécutera les migrations et l'initialisation des données.
Assurez-vous de sauvegarder toutes les données que vous souhaitez conserver avant d'exécuter cette commande.
</Warning>
## Écosystème Tech
Twenty utilise principalement NestJS pour le backend.
@@ -372,10 +372,8 @@ Exécutez les commandes suivantes :
```
yarn database:migrate:prod
yarn command:prod workspace:sync-metadata -f
yarn command:prod upgrade-0.22
```
La commande `yarn database:migrate:prod` appliquera les migrations à la base de données.
La commande `yarn command:prod workspace:sync-metadata -f` synchronisera la définition des objets standard avec les tables de métadonnées et appliquera les migrations nécessaires aux espaces de travail existants.
La commande `yarn command:prod upgrade-0.22` appliquera des transformations de données spécifiques pour sadapter aux nouvelles `defaultRequestInstrumentationOptions` dobjet.
@@ -50,22 +50,6 @@ npx nx run twenty-server:database:reset
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/nameOfYourMigration -d src/database/typeorm/core/core.datasource.ts
```
#### Per gli oggetti del Workspace
Non ci sono file di migrazioni; le migrazioni sono generate automaticamente per ogni workspace, memorizzate nel database e applicate con questo comando
```bash
npx nx run twenty-server:command workspace:sync-metadata -f
```
<Warning>
Questo eliminerà il database e rieseguirà le migrazioni e il popolamento dei dati.
Assicurati di eseguire il backup dei dati che vuoi mantenere prima di eseguire questo comando.
</Warning>
## Tech Stack
Twenty utilizza principalmente NestJS per il backend.

Some files were not shown because too many files have changed in this diff Show More