Compare commits

..
Author SHA1 Message Date
Sonarly Claude Code 36952cf81c fix(dns-manager): throw proper DnsManagerException when hostname not found in Cloudflare
https://sonarly.com/issue/33567?type=bug

When a user checks DNS records for their custom domain, and the hostname is not registered in Cloudflare's custom hostnames despite being set in the workspace database, `refreshHostname()` crashes with a generic "Value not defined" error instead of a proper domain-specific exception.
2026-05-02 19:11:16 +00:00
nitinandGitHub 4f439bbe43 [AI] Prefer batch tools in system prompts (#20173) 2026-05-01 14:55:15 +02:00
4fbd8b207d chore: sync AI model catalog from models.dev (#20178)
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 <[email protected]>
2026-05-01 08:50:48 +02:00
f7d812fca6 fix: disable sync on seeded message and calendar channels (#20168)
## Summary

The dev seeder creates `ConnectedAccount` records with fake OAuth tokens
(`'exampleRefreshToken'` / `'exampleAccessToken'`) and points
`MessageChannel` / `CalendarChannel` records at them with
`isSyncEnabled: true`. When the sync cron jobs run in the demo
workspace, they:

1. Pick up these seeded channels (filter is `isSyncEnabled: true` +
pending sync stage)
2. Try to refresh the fake OAuth tokens
3. Mark the channels as `FAILED_INSUFFICIENT_PERMISSIONS`
4. Surface a "Sync lost with mailbox X — please reconnect" banner in the
UI

This banner appears every time the demo workspace is loaded, even though
nothing is actually broken.

## Fix

Set `isSyncEnabled: false` on all 12 seeded channels (6 message, 6
calendar). This is the canonical "don't sync this channel" mechanism —
the same state a real user lands in when they toggle sync off in account
settings.

## Why this approach

- **ConnectedAccount records stay**: the demo workspace still shows Tim,
Jony, Phil, Jane as having connected their email/calendar — realistic
- **Pre-seeded messages and calendar events stay visible**: those don't
depend on `isSyncEnabled`
- **Crons no longer pick them up**: they filter on `isSyncEnabled:
true`, so `false` short-circuits the entire sync attempt — no failure,
no banner
- **Semantically correct**: the seeded accounts have fake tokens that
were never going to sync successfully; `isSyncEnabled: true` was
effectively a lie
- **No production code touched**: no `isDemo` flags, no magic-string
detection, no workspace-ID filters in the cron path

## Alternatives considered and rejected

- **Add an `isDemo` flag**: schema change, leaks demo knowledge into
production tables
- **Skip channels with fake tokens (`example*` pattern)**: hacky
magic-string detection in the auth refresh path
- **Filter demo workspace IDs in the cron**: production paths shouldn't
reference demo IDs
- **Don't activate demo workspaces**: breaks the demo workspace UX
entirely

## Test plan

- [ ] Reset the database and reseed (`npx nx database:reset
twenty-server`)
- [ ] Load the demo workspace — confirm no "Sync lost with mailbox"
banner appears
- [ ] Confirm seeded connected accounts still show in Settings →
Accounts
- [ ] Confirm pre-seeded messages and calendar events still appear in
the UI
- [ ] Confirm a real connected account (added via OAuth) still syncs
normally — its channel will have `isSyncEnabled: true` and the cron will
pick it up

## Related

Companion to https://github.com/twentyhq/twenty/pull/20167, which
removes the `--dev-mode` cron filter that was masking this banner issue
in the dev image.

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

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-05-01 07:52:01 +02:00
c3a320c27b fix: register all cron jobs in twenty-app-dev image (#20167)
## Summary

The `twenty-app-dev` Docker image previously passed `--dev-mode` to
`cron:register:all`, which skipped all calendar, messaging, and workflow
sync cron jobs (only 4 generic crons were registered). This caused
periodic sync to silently stop after the initial import for community
members using the dev image as their actual instance.

## What changed

- Removed `--dev-mode` flag from
`packages/twenty-docker/twenty-app-dev/rootfs/etc/s6-overlay/scripts/register-crons.sh`
so the dev image registers all cron jobs (matching production behavior)
- Removed the now-unused `--dev-mode` option, `DEV_MODE_COMMANDS` set,
and conditional filtering logic from `cron-register-all.command.ts`

## Why this is safe

- **No log noise**: cron jobs gracefully no-op when no connected
accounts exist — they query for pending channels, find zero, and exit
early
- **No false banner**: the "reconnect account" banner only shows when a
user explicitly connected an account whose OAuth later fails, which is
correct behavior. No seed/demo data creates connected accounts, so a
fresh dev instance won't see any banner
- **Hiding crons just hid the symptom**: silently breaking sync with no
user feedback is worse than showing the banner if OAuth is misconfigured

## Context

Surfaced by a community member who reported that calendar sync cron jobs
never appeared in the queue after restarting the dev image, and only the
initial import worked. `--dev-mode` was added in #19138 as an
optimization for development but it doesn't match how the dev image is
actually used by community members deploying Twenty.

## Test plan

- [ ] Build/run the `twenty-app-dev` image
- [ ] Confirm worker logs show all cron jobs registering (calendar,
messaging, workflow, etc.)
- [ ] With no connected accounts: confirm no errors or log noise
- [ ] With a connected Google calendar: confirm periodic sync triggers
after ~5 minutes

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

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-05-01 07:51:21 +02:00
WeikoandGitHub 85752f8a61 Bump 2.3.0 (#20169) 2026-04-30 19:34:43 +02:00
8a0225e974 Dispatch root package.json hoisted deps and devDeps (#20140)
# Introduction
Dispatching root package.json devDeps, prod deps
Taking care of keeping non imported module used at build/ci level in the
root package.json

## Motivation
Avoid redundant deps declaration, better scoping allow better workspace
deps granularity installation.

<img width="385" height="247" alt="image"
src="https://github.com/user-attachments/assets/9d7162ec-ba01-4f58-8563-38333733fdf0"
/>

---------

Co-authored-by: Charles Bochet <[email protected]>
2026-04-30 16:50:22 +00:00
abc67efd40 i18n - docs translations (#20166)
Created by Github action

Co-authored-by: github-actions <[email protected]>
2026-04-30 18:50:57 +02:00
636deffb93 i18n - translations (#20164)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-04-30 18:01:45 +02:00
nitinandGitHub e1828b6f41 [AI] Add thread actions, filters, and archive support (#20068)
## PR Description

### Summary
- Add AI chat thread actions: rename, archive (soft-delete via
`deletedAt`), and hard-delete with confirmation.
- Add chat thread filtering by status (active/archived/all), group-by
mode, and last activity.
- Rework drawer/side-panel thread lists to share thread sections, item
menus, archive icons, and empty-state behavior.
- Extend server chat thread model/API with `deletedAt`, mutations,
broadcasts, and archive-aware stream guards.

### Decisions
- Two-stage lifecycle: Archive sets `deletedAt` (soft); Delete is a
separate action on archived threads that hard-deletes the row. Aligns
with Twenty's soft-delete convention (Felix's suggestion).
- `lastMessageAt` is derived from `MAX(agentMessage.createdAt)` on read,
not stored. List query does inline aggregation for sort; `@ResolveField`
covers single-thread / mutation paths so the schema contract is honest
everywhere. Matches `timeline-messaging.service.ts` precedent and the
existing `totalInputCredits` / `totalOutputCredits` `@ResolveField`
pattern in the same resolver.
- Replaced auto-CRUD `chatThreads` (cursor-paginated Connection) with a
custom `[AgentChatThreadDTO!]` resolver. Frontend metadata-store treats
threads as a flat collection and filters/sorts client-side, so cursor
pagination was performative.
- Sending in an archived chat unarchives it optimistically on the client
and authoritatively on the server.
- Grouping and last-activity filtering use `lastMessageAt ?? updatedAt`
so archive/rename don't bump threads in the list.
- Kept metadata-store core API unchanged; AI chat uses the same local
cast pattern already used by other metadata-store partial updates.


https://github.com/user-attachments/assets/1b179b7b-1a2a-4a7a-aa0a-c88f6f051a87
2026-04-30 15:42:10 +00:00
WeikoandGitHub 4b76457217 Select application excluding logo (#20159)
## Context
This is a temporary fix for cross-version upgrade process, a better fix
would be to expose an hasInstanceCommandBeenRun() util (and later a
decorator)
2026-04-30 15:13:54 +00:00
martmullandGitHub d8bd717f1f Update doc screenshots (#20160)
fixes
https://discord.com/channels/1130383047699738754/1496579088889024542
2026-04-30 14:54:49 +00:00
b44fb1ad23 fix(security): reject ?token= URL query parameter for authentication (#20154)
## Summary

Removes the `?token=` URL query-parameter fallback from JWT
authentication. Every authenticated route (`/graphql`, `/metadata`,
REST, etc.) used to accept a full workspace JWT in the URL alongside the
`Authorization` header. The fallback was intended for the REST API
Playground only, but it was wired into the global Passport JWT extractor
and applied to every route.

URL-borne tokens leak into:
- Server access logs (nginx / Apache / CDN / proxy / load balancer)
- Log aggregators (Datadog, CloudWatch, Loki, Sumo, …)
- Browser history (and synced across devices)
- `Referer` headers when navigating to external pages
- Browser extensions with `tabs`/`webNavigation` permissions

A leaked log line was equivalent to a leaked workspace credential for
the lifetime of the token.

## What changed

- **`jwt-wrapper.service.ts`** — `extractJwtFromRequest()` is now
header-only (`ExtractJwt.fromAuthHeaderAsBearerToken()`). No URL
fallback anywhere in the system.
- **`open-api.service.ts` / `base-schema.utils.ts`** — Dropped the
`token?: string` plumbing that propagated the URL token into the schema
description. The "Authentication" section gains a "Never put your token
in a URL" warning. The "Usage with LLMs" section is rewritten to point
at the **Twenty MCP server** (header-authenticated, exposes typed tools
— the right tool for AI agents) instead of telling users to paste
tokenized OpenAPI URLs into Cursor/ChatGPT.
- **`RestPlayground.tsx`** — Now fetches the OpenAPI schema with
`Authorization: Bearer ${playgroundApiKey}` and passes the JSON document
to Scalar via `spec.content` instead of constructing a URL with
`?token=`. Aborts in-flight fetches on unmount/key change.
- **New integration test** — Asserts `?token=` is rejected on `/rest/*`,
`/graphql`, `/metadata`, and that `/rest/open-api/core?token=` returns
the unauthenticated base schema (no workspace object paths).

## Why not keep `?token=` scoped to the OpenAPI endpoint only

The first instinct was to narrow the fallback to just
`/rest/open-api/*`, since that endpoint is what the Scalar playground
component fetches. But the same log-leakage attack still applies to that
endpoint — the workspace JWT would still sit in access logs, just from
one URL pattern instead of all of them. The cleaner long-term fix is to
remove the URL pattern entirely and let the playground fetch with a
header (Scalar supports `spec.content` natively). For LLM agent use, the
MCP server is a strictly better path — typed tools, OAuth or
header-based API key auth, no tokens in URLs anywhere.

## Not affected

File downloads at `file-url.service.ts` also use `?token=` URLs but with
separate, short-lived `FILE`-typed tokens validated by
`file-by-id.guard.ts` directly (not via `extractJwtFromRequest`). That
mechanism is scoped per-file with limited TTL and is acceptable.

## Action required for users

Anyone who previously pasted `?token=` URLs into LLM tools, scripts,
bookmarks, or shared configs should rotate their workspace API keys.
Those tokens are likely captured in server logs / chat histories
somewhere.

## Test plan

- [x] `npx nx typecheck twenty-server` — clean
- [x] `npx nx typecheck twenty-front` — clean
- [x] `npx nx lint:diff-with-main twenty-server` — clean
- [x] `npx nx lint:diff-with-main twenty-front` — clean
- [x] OpenAPI utils unit tests + snapshots — 11/11 pass
- [ ] Run the new integration test against a live server: `nx run
twenty-server:test:integration:with-db-reset` and verify
`url-token-auth-rejection.integration-spec.ts` passes
- [ ] Manually open Settings → Playground → REST, confirm the schema
loads (now via Bearer header instead of `?token=` URL)
- [ ] Manually verify `POST /metadata?token=<jwt>` (no Authorization
header) returns Forbidden, and the same request with the token in the
header returns the user

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

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-04-30 17:00:26 +02:00
f32b03a3ec i18n - docs translations (#20161)
Created by Github action

Co-authored-by: github-actions <[email protected]>
2026-04-30 16:58:55 +02:00
martmullandGitHub c8e405cb4e Add twenty sdk server upgrade command (#20158)
##
The command pulls the image, compares it against the one the container
was created from, and only recreates the container if the image actually
changed. Your data volumes are preserved — only the container is
replaced.
2026-04-30 13:03:41 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Charles Bochet
83db37d33f chore(deps): bump @sentry/profiling-node from 10.27.0 to 10.51.0 (#20149)
Bumps
[@sentry/profiling-node](https://github.com/getsentry/sentry-javascript)
from 10.27.0 to 10.51.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/getsentry/sentry-javascript/releases"><code>@​sentry/profiling-node</code>'s
releases</a>.</em></p>
<blockquote>
<h2>10.51.0</h2>
<h3>Important Changes</h3>
<ul>
<li>
<p><strong>feat(cloudflare): Add trace propagation for RPC method calls
(<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/20343">#20343</a>)</strong></p>
<p>Trace context is now propagated across Cloudflare Workers RPC calls,
connecting traces between Workers and Durable Objects.
This feature is opt-in and requires setting
<code>enableRpcTracePropagation: true</code> in your SDK
configuration:</p>
<pre lang="ts"><code>// Worker
export default Sentry.withSentry(
  env =&gt; ({
    dsn: env.SENTRY_DSN,
    enableRpcTracePropagation: true,
  }),
  handler,
);
<p>// Durable Object<br />
export const MyDurableObject =
Sentry.instrumentDurableObjectWithSentry(<br />
env =&gt; ({<br />
dsn: env.SENTRY_DSN,<br />
enableRpcTracePropagation: true,<br />
}),<br />
MyDurableObjectBase,<br />
);<br />
</code></pre></p>
</li>
<li>
<p><strong>feat(hono)!: Change setup for <code>@sentry/hono/node</code>
(<code>init</code> in external file) (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/20497">#20497</a>)</strong></p>
<p>To improve Node.js instrumentation, the <code>sentry()</code>
middleware exported from <code>@sentry/hono/node</code> no longer
accepts configuration options.
Instead, you must configure the SDK by calling
<code>Sentry.init()</code> in a dedicated instrumentation file that runs
before your application code (read more in the <a
href="https://github.com/getsentry/sentry-javascript/blob/develop/packages/hono/README.md">Hono
SDK readme</a>:</p>
<pre lang="ts"><code>// instrument.mjs (or instrument.ts)
import * as Sentry from '@sentry/hono/node';
<p>Sentry.init({<br />
dsn: '<strong>DSN</strong>',<br />
tracesSampleRate: 1.0,<br />
});<br />
</code></pre></p>
</li>
<li>
<p><strong>feat(nitro): Add <code>@sentry/nitro</code> SDK (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/19224">#19224</a>)</strong></p>
<p>A new <code>@sentry/nitro</code> package provides first-class Sentry
support for <a href="https://nitro.build/">Nitro</a> applications, with
HTTP handler and error instrumentation, middleware tracing, request
isolation, and build-time source map uploading via
<code>withSentryConfig</code>.
Read more in the <a
href="https://docs.sentry.io/platforms/javascript/guides/nitro/">Nitro
SDK docs</a> and the <a
href="https://github.com/getsentry/sentry-javascript/blob/develop/packages/nitro/README.md">Nitro
SDK readme</a>.</p>
</li>
</ul>
<h3>Other Changes</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md"><code>@​sentry/profiling-node</code>'s
changelog</a>.</em></p>
<blockquote>
<h2>10.51.0</h2>
<h3>Important Changes</h3>
<ul>
<li>
<p><strong>feat(cloudflare): Add trace propagation for RPC method calls
(<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/20343">#20343</a>)</strong></p>
<p>Trace context is now propagated across Cloudflare Workers RPC calls,
connecting traces between Workers and Durable Objects.
This feature is opt-in and requires setting
<code>enableRpcTracePropagation: true</code> in your SDK
configuration:</p>
<pre lang="ts"><code>// Worker
export default Sentry.withSentry(
  env =&gt; ({
    dsn: env.SENTRY_DSN,
    enableRpcTracePropagation: true,
  }),
  handler,
);
<p>// Durable Object<br />
export const MyDurableObject =
Sentry.instrumentDurableObjectWithSentry(<br />
env =&gt; ({<br />
dsn: env.SENTRY_DSN,<br />
enableRpcTracePropagation: true,<br />
}),<br />
MyDurableObjectBase,<br />
);<br />
</code></pre></p>
</li>
<li>
<p><strong>feat(hono)!: Change setup for <code>@sentry/hono/node</code>
(<code>init</code> in external file) (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/20497">#20497</a>)</strong></p>
<p>To improve Node.js instrumentation, the <code>sentry()</code>
middleware exported from <code>@sentry/hono/node</code> no longer
accepts configuration options.
Instead, you must configure the SDK by calling
<code>Sentry.init()</code> in a dedicated instrumentation file that runs
before your application code (read more in the <a
href="https://github.com/getsentry/sentry-javascript/blob/develop/packages/hono/README.md">Hono
SDK readme</a>:</p>
<pre lang="ts"><code>// instrument.mjs (or instrument.ts)
import * as Sentry from '@sentry/hono/node';
<p>Sentry.init({<br />
dsn: '<strong>DSN</strong>',<br />
tracesSampleRate: 1.0,<br />
});<br />
</code></pre></p>
</li>
<li>
<p><strong>feat(nitro): Add <code>@sentry/nitro</code> SDK (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/19224">#19224</a>)</strong></p>
<p>A new <code>@sentry/nitro</code> package provides first-class Sentry
support for <a href="https://nitro.build/">Nitro</a> applications, with
HTTP handler and error instrumentation, middleware tracing, request
isolation, and build-time source map uploading via
<code>withSentryConfig</code>.
Read more in the <a
href="https://docs.sentry.io/platforms/javascript/guides/nitro/">Nitro
SDK docs</a> and the <a
href="https://github.com/getsentry/sentry-javascript/blob/develop/packages/nitro/README.md">Nitro
SDK readme</a>.</p>
</li>
</ul>
<h3>Other Changes</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/dc0b839ff4896cf90a02f5c1a6de54a31302dcf3"><code>dc0b839</code></a>
release: 10.51.0</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/b3cabee9a9348b9e67332262d44d3d1900424199"><code>b3cabee</code></a>
Merge pull request <a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/20599">#20599</a>
from getsentry/prepare-release/10.51.0</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/3be99a9afa77e49578e6839e4b32f97fb04fb0f8"><code>3be99a9</code></a>
meta(changelog): Update changelog for 10.51.0</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/bea1aad42277db894d5a299bfec3cdd633d6baf0"><code>bea1aad</code></a>
test(browser): Unflake some more tests (<a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/20591">#20591</a>)</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/50aa0859b3a188d34d0317dab3ad57f2140f02fe"><code>50aa085</code></a>
test(node): Unflake postgres tests (<a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/20593">#20593</a>)</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/1166839112c4766f210124dc0486ebbfd6db104b"><code>1166839</code></a>
fix(hono): Distinguish <code>.use()</code> middleware in sub-apps from
<code>.all()</code> handlers...</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/217ad4a69554281806eccbfeac1b27c4f43f6ffa"><code>217ad4a</code></a>
test(node): Fix flaky ANR test (<a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/20592">#20592</a>)</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/91ffb3fac90835ab160f8152527a54a5d64f3250"><code>91ffb3f</code></a>
test(node): Fix flaky worker thread integration test (<a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/20588">#20588</a>)</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/c4e3902c9297147158e730f017aba96e83ef619e"><code>c4e3902</code></a>
chore(ci): Do not report flaky test issues if we cannot find a test name
(<a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/20">#20</a>...</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/c0005cd387f3a7ea6fbb2e85041562c7f32e0484"><code>c0005cd</code></a>
test(node): Update timeout for cron integration tests (<a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/20586">#20586</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/getsentry/sentry-javascript/compare/10.27.0...10.51.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@sentry/profiling-node&package-manager=npm_and_yarn&previous-version=10.27.0&new-version=10.51.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] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Charles Bochet <[email protected]>
2026-04-30 12:02:34 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Charles Bochet
5bdbfe651e chore(deps): bump postal-mime from 2.6.1 to 2.7.4 (#20150)
Bumps [postal-mime](https://github.com/postalsys/postal-mime) from 2.6.1
to 2.7.4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/postalsys/postal-mime/releases">postal-mime's
releases</a>.</em></p>
<blockquote>
<h2>v2.7.4</h2>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.3...v2.7.4">2.7.4</a>
(2026-03-17)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>add missing originalKey to Header type and Uint8Array to Attachment
content (<a
href="https://github.com/postalsys/postal-mime/commit/92cc91c1c8477e0462cb0e93ddf8ea6aec6534d0">92cc91c</a>)</li>
<li>include originalKey in parsed headers output (<a
href="https://github.com/postalsys/postal-mime/commit/83521c87f62e5e095ae09913c70798f20e2ab347">83521c8</a>)</li>
<li>preserve __esModule and .default in CJS build for bundler interop
(<a
href="https://github.com/postalsys/postal-mime/commit/1466910e31608b9e5307724ecc6a0a3a70556048">1466910</a>)</li>
<li>prevent RFC 2047 encoded-word address fabrication (<a
href="https://github.com/postalsys/postal-mime/commit/844f92023d49d819ef13b9ad5c50b7c346eb02d3">844f920</a>)</li>
</ul>
<h2>v2.7.3</h2>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.2...v2.7.3">2.7.3</a>
(2026-01-09)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>correct TypeScript type definitions to match implementation (<a
href="https://github.com/postalsys/postal-mime/commit/b225d7cca422cb9bc3ab5301e94c4c0bef9a69e2">b225d7c</a>)</li>
</ul>
<h2>v2.7.2</h2>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.1...v2.7.2">2.7.2</a>
(2026-01-08)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>add null checks for contentType.parsed access (<a
href="https://github.com/postalsys/postal-mime/commit/ad8f4c62e0972fd0244859ee5a5184b2cac26395">ad8f4c6</a>)</li>
<li>improve RFC compliance for MIME parsing (<a
href="https://github.com/postalsys/postal-mime/commit/e004c3acb29d72ed7eaf1b0b66351cf8b82b970d">e004c3a</a>)</li>
</ul>
<h2>v2.7.1</h2>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.0...v2.7.1">2.7.1</a>
(2025-12-22)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>Add null checks for contentDisposition.parsed access (<a
href="https://github.com/postalsys/postal-mime/commit/fd54c37093cc64737c6bb17986bc9d052d2d5add">fd54c37</a>)</li>
</ul>
<h2>v2.7.0</h2>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.6.1...v2.7.0">2.7.0</a>
(2025-12-22)</h2>
<h3>Features</h3>
<ul>
<li>add headerLines property exposing raw header lines (<a
href="https://github.com/postalsys/postal-mime/commit/c79a02ab05d9cac44e05e95a433752ff292aa5eb">c79a02a</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/postalsys/postal-mime/blob/master/CHANGELOG.md">postal-mime's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.3...v2.7.4">2.7.4</a>
(2026-03-17)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>add missing originalKey to Header type and Uint8Array to Attachment
content (<a
href="https://github.com/postalsys/postal-mime/commit/92cc91c1c8477e0462cb0e93ddf8ea6aec6534d0">92cc91c</a>)</li>
<li>include originalKey in parsed headers output (<a
href="https://github.com/postalsys/postal-mime/commit/83521c87f62e5e095ae09913c70798f20e2ab347">83521c8</a>)</li>
<li>preserve __esModule and .default in CJS build for bundler interop
(<a
href="https://github.com/postalsys/postal-mime/commit/1466910e31608b9e5307724ecc6a0a3a70556048">1466910</a>)</li>
<li>prevent RFC 2047 encoded-word address fabrication (<a
href="https://github.com/postalsys/postal-mime/commit/844f92023d49d819ef13b9ad5c50b7c346eb02d3">844f920</a>)</li>
</ul>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.2...v2.7.3">2.7.3</a>
(2026-01-09)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>correct TypeScript type definitions to match implementation (<a
href="https://github.com/postalsys/postal-mime/commit/b225d7cca422cb9bc3ab5301e94c4c0bef9a69e2">b225d7c</a>)</li>
</ul>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.1...v2.7.2">2.7.2</a>
(2026-01-08)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>add null checks for contentType.parsed access (<a
href="https://github.com/postalsys/postal-mime/commit/ad8f4c62e0972fd0244859ee5a5184b2cac26395">ad8f4c6</a>)</li>
<li>improve RFC compliance for MIME parsing (<a
href="https://github.com/postalsys/postal-mime/commit/e004c3acb29d72ed7eaf1b0b66351cf8b82b970d">e004c3a</a>)</li>
</ul>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.0...v2.7.1">2.7.1</a>
(2025-12-22)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>Add null checks for contentDisposition.parsed access (<a
href="https://github.com/postalsys/postal-mime/commit/fd54c37093cc64737c6bb17986bc9d052d2d5add">fd54c37</a>)</li>
</ul>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.6.1...v2.7.0">2.7.0</a>
(2025-12-22)</h2>
<h3>Features</h3>
<ul>
<li>add headerLines property exposing raw header lines (<a
href="https://github.com/postalsys/postal-mime/commit/c79a02ab05d9cac44e05e95a433752ff292aa5eb">c79a02a</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/postalsys/postal-mime/commit/178f1ef0b1cd0047e1b8e690beabfec541b4daa7"><code>178f1ef</code></a>
chore(master): release 2.7.4 (<a
href="https://redirect.github.com/postalsys/postal-mime/issues/88">#88</a>)</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/1f7ba618d42d34b779157dfa33794cbae383a24d"><code>1f7ba61</code></a>
chore: bump devDependencies</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/83521c87f62e5e095ae09913c70798f20e2ab347"><code>83521c8</code></a>
fix: include originalKey in parsed headers output</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/b0d7b11550a2a3c65a52a2adf4f8281058023cab"><code>b0d7b11</code></a>
test: improve test coverage across codebase</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/ebc5ce619649d13ad72f4d12414f3e337a9e248c"><code>ebc5ce6</code></a>
refactor: simplify and clean up codebase</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/1466910e31608b9e5307724ecc6a0a3a70556048"><code>1466910</code></a>
fix: preserve __esModule and .default in CJS build for bundler
interop</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/844f92023d49d819ef13b9ad5c50b7c346eb02d3"><code>844f920</code></a>
fix: prevent RFC 2047 encoded-word address fabrication</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/24dc6c64dfb43d89a8c8837ec941c96ebfa2c1fa"><code>24dc6c6</code></a>
test: update type check test with originalKey property</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/92cc91c1c8477e0462cb0e93ddf8ea6aec6534d0"><code>92cc91c</code></a>
fix: add missing originalKey to Header type and Uint8Array to Attachment
content</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/aa5baeafa6ffd093ab447c22d20e5da25051faff"><code>aa5baea</code></a>
docs: add link to full documentation site</li>
<li>Additional commits viewable in <a
href="https://github.com/postalsys/postal-mime/compare/v2.6.1...v2.7.4">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=postal-mime&package-manager=npm_and_yarn&previous-version=2.6.1&new-version=2.7.4)](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] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Charles Bochet <[email protected]>
2026-04-30 11:38:44 +00:00
3fbb70c13f i18n - docs translations (#20157)
Created by Github action

Co-authored-by: github-actions <[email protected]>
2026-04-30 12:49:31 +02:00
c6b6c824d4 i18n - translations (#20156)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-04-30 11:55:45 +02:00
b21bf66b38 i18n - translations (#20155)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-04-30 11:50:00 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
22838ac6de chore(deps-dev): bump @babel/preset-typescript from 7.24.7 to 7.28.5 (#20151)
Bumps
[@babel/preset-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript)
from 7.24.7 to 7.28.5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/babel/babel/releases"><code>@​babel/preset-typescript</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-typescript</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.4 (2026-04-29)</h2>
<h4>👓 Spec Compliance</h4>
<ul>
<li><code>babel-parser</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17954">#17954</a>
fix(parser): ts parser small fixes (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
<li><a
href="https://redirect.github.com/babel/babel/pull/17923">#17923</a>
Support flow extends bound (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
<li><a
href="https://redirect.github.com/babel/babel/pull/17888">#17888</a> TS
parser small fixes (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
<li><a
href="https://redirect.github.com/babel/babel/pull/17865">#17865</a>
Fix(parser): flow parser small fixes (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-generator</code>, <code>babel-parser</code>,
<code>babel-plugin-transform-spread</code>, <code>babel-types</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17871">#17871</a>
Disallow super call after new (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
</ul>
<h4>💥 Breaking Change</h4>
<ul>
<li><code>babel-cli</code>,
<code>babel-helper-transform-fixture-test-runner</code>,
<code>babel-helpers</code>, <code>babel-node</code>,
<code>babel-register</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17938">#17938</a>
Bundle more packages (<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/17937">#17937</a>
Remove <code>Scope#buildUndefinedNode</code> (<a
href="https://github.com/nicolo-ribaudo"><code>@​nicolo-ribaudo</code></a>)</li>
</ul>
</li>
<li><code>babel-helper-wrap-function</code>,
<code>babel-plugin-transform-block-scoping</code>,
<code>babel-plugin-transform-regenerator</code>,
<code>babel-traverse</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17907">#17907</a>
Remove <code>NodePath#toComputedKey</code> (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
<li><code>babel-plugin-external-helpers</code>,
<code>babel-template</code>, <code>babel-traverse</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17830">#17830</a>
Replace remaining whitelist/blacklist with inclusive alternatives (<a
href="https://github.com/stuckvgn"><code>@​stuckvgn</code></a>)</li>
</ul>
</li>
<li><code>babel-plugin-transform-property-mutators</code>,
<code>babel-standalone</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17882">#17882</a>
Remove <code>@babel/plugin-transform-property-mutators</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>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-typescript/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-typescript/issues/17204">#17204</a>)</li>
<li><a
href="https://github.com/babel/babel/commit/5c350eab83dd12268add44cce0eeda6c898211e3"><code>5c350ea</code></a>
v7.27.0</li>
<li><a
href="https://github.com/babel/babel/commit/ca4865a7f43a6a56aec242e23e4a3e318cf0ca92"><code>ca4865a</code></a>
Fix: align behaviour to tsc <code>rewriteRelativeImportExtensions</code>
(<a
href="https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript/issues/17118">#17118</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-typescript/issues/17053">#17053</a>)</li>
<li><a
href="https://github.com/babel/babel/commit/63d30381c169780460e01bdb6669c5e01af1dfbe"><code>63d3038</code></a>
v7.26.0</li>
<li><a
href="https://github.com/babel/babel/commit/bfa56c49569f0bfd5579e0e1870ffa92f74fad48"><code>bfa56c4</code></a>
Support <code>import()</code> in <code>rewriteImportExtensions</code>
(<a
href="https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript/issues/16794">#16794</a>)</li>
<li><a
href="https://github.com/babel/babel/commit/b07957ebb316a1e2fc67454fc7423508bb942e63"><code>b07957e</code></a>
v7.25.9</li>
<li>Additional commits viewable in <a
href="https://github.com/babel/babel/commits/v7.28.5/packages/babel-preset-typescript">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/~GitHub%20Actions">GitHub Actions</a>, a new
releaser for <code>@​babel/preset-typescript</code> since your current
version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@babel/preset-typescript&package-manager=npm_and_yarn&previous-version=7.24.7&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] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-30 09:32:13 +00:00
martmullandGitHub bddd23fd9c Fix application icons (#20142)
fixes application chip (icon Name) in all setting tables

## After
<img width="1200" height="896" alt="image"
src="https://github.com/user-attachments/assets/bd377f47-1d52-4142-b904-f2ce90c1db78"
/>
<img width="1200" height="917" alt="image"
src="https://github.com/user-attachments/assets/f49cc742-f11e-47e3-86ed-34beffe493c7"
/>
<img width="1234" height="878" alt="image"
src="https://github.com/user-attachments/assets/2ab459de-5f9d-4d39-9490-eec4ed9ee432"
/>
<img width="1239" height="845" alt="image"
src="https://github.com/user-attachments/assets/3c1bf258-285a-47b9-a60d-05ba1564334d"
/>
<img width="1183" height="907" alt="image"
src="https://github.com/user-attachments/assets/715b2470-2d88-48e3-88ac-d3daf3451717"
/>
<img width="1300" height="912" alt="image"
src="https://github.com/user-attachments/assets/d7c829fa-bf1d-4f19-82de-a8bf29e22bfa"
/>
2026-04-30 09:32:04 +00:00
842e679cc6 fix(billing): gate AI credit-cap at entry points instead of workflow executor (#20096)
## Background

The 2026-04-26 incident saw 716M Sonnet 4.6 tokens consumed in a single
trial workspace. Two causes: failed agent executions weren't billed
(addressed by #20065) and the credit-cap gate had been removed from
`WorkflowExecutorWorkspaceService.executeStep` in #19904, leaving no
enforcement point at all.

## Why not just revert #19904

#19904 was right that gating at the workflow executor is too coarse.
When one user exhausted a workspace's credits via chat, *all* workflows
hard-failed mid-run — including cheap DB/CRUD/branch automations costing
essentially nothing. Reverting would re-introduce that cliff.

## New design: gate at the AI entry points

The chat resolver already gates this way
(`agent-chat.resolver.ts:137-148`). This PR replicates the same pattern
at every other point where the workspace can incur real AI cost:

- `executeAgent` in `agent-async-executor.service.ts`
- the REST handler in `ai-generate-text.controller.ts`
- `generateThreadTitle` in `agent-title-generation.service.ts`

In each, after auth/validation: skip if `IS_BILLING_ENABLED` is false;
otherwise call `BillingService.canBillMeteredProduct(workspaceId,
BillingProductKey.WORKFLOW_NODE_EXECUTION)`; on `false`, throw
`BillingException(BILLING_CREDITS_EXHAUSTED)`. No new method, no new
exception code, no new product key.

This matches industry convention (Lovable/Replit also gate at the
expensive-operation boundary, not at every cheap step).

## Deliberately not gated

- `WorkflowExecutorWorkspaceService.executeStep` — the design choice is
now intentional, so the #19904 TODO is replaced by a one-line
absolute-behavior comment explaining why the gate isn't here. Cheap
workflow steps (DB CRUD, branching, action steps) are not gated, so a
chat-driven cap exhaustion does not block non-AI automations.
- `repair-tool-call.util` — repair is a sub-call inside an already-gated
AI flow. If the parent is gated, repair will naturally not run. Adding a
gate here adds complexity without value.

## Net effect

A workspace that exhausts credits via chat or AI agent stops making AI
calls. Its non-AI workflows continue running normally. A workflow with
both AI and non-AI steps fails at the AI step with
`BILLING_CREDITS_EXHAUSTED`, but downstream non-AI steps that don't
depend on the AI output still run.

## Conflicts

This PR overlaps with three other in-flight PRs in the same files. None
of them touch the gate logic; rebasing on top of any of them is trivial:

- #20065 (agent-async-executor): adds `workspaceId` to `executeAgent`
args and bills in `finally`. The gate at the top of `executeAgent` from
this PR sits naturally above that.
- #20066 (REST controller): adds usage billing to the controller.
- #20067 (title gen): adds usage billing to title generation and
tool-call repair.

Recommend landing #20065/#20066/#20067 first; this PR rebases trivially
on top.

## Tests

Out of scope per the PR series convention. The existing chat-resolver
gate isn't unit-tested either; this PR follows the same precedent.
Follow-up: add integration coverage that exercises a workspace at
`hasReachedCurrentPeriodCap=true` against each of the three new gates
plus the pre-existing chat-resolver gate.

## Future follow-ups

- Per-user soft cap inside a workspace (the Lovable Business-tier
pattern), so one user can't exhaust the workspace's cap.
- Pre-flight cost estimate so the user sees an "approaching cap" warning
before the hard stop.
- Rename `BillingProductKey.WORKFLOW_NODE_EXECUTION` — the name predates
this design choice and is misleading now that it gates AI entry points
rather than workflow nodes.

## Test plan

- [ ] Trigger a workspace into `hasReachedCurrentPeriodCap=true`.
- [ ] Send a chat message — expect failure with
`BILLING_CREDITS_EXHAUSTED`.
- [ ] Run a workflow whose only AI step is an `ai-agent` action — expect
that step to fail with `BILLING_CREDITS_EXHAUSTED`, downstream non-AI
steps still run.
- [ ] POST to `/rest/ai/generate-text` — expect
`BILLING_CREDITS_EXHAUSTED`.
- [ ] Create a new chat thread (which kicks off `generateThreadTitle`) —
expect `BILLING_CREDITS_EXHAUSTED`.
- [ ] Run a workflow with no AI step (only DB CRUD/branching/actions) —
expect it to run unaffected.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-04-29 18:48:06 +02:00
neo773andGitHub a713f8d87f CalDAV: support Digest auth (#20135)
Adds digest auth support for CalDAV, mostly used by legacy servers

/closes https://github.com/twentyhq/twenty/issues/19922
2026-04-29 16:09:21 +00:00
EtienneandGitHub fd6d5f895d Ai Chat - Caching optim (#20126)
EDIT : 
- solving auto-caching from Anthropic by updating ai-sdk/anthropic +
adding providerOption at stream level
- concerning Bedrock, it needs breakpoint


**1. Breakpoints were only on the system prompt**

The code already placed a cache marker on the system prompt (~10K
tokens). But the conversation history — which can grow to hundreds of
thousands of tokens — had no marker, so Anthropic re-read it at full
price on every turn.

The fix adds a prepareStep hook inside streamText that stamps the last
message with a cache breakpoint before every LLM call. Anthropic then
caches the entire conversation prefix, and subsequent turns read it at
$0.30/M instead of $3/M.

prepareStep is used rather than a one-shot pre-processing step because
an agentic turn makes multiple internal LLM calls as tool results
accumulate — the hook refreshes the breakpoint before each one.

**2. Bedrock was using the wrong field**

The system prompt marker for Bedrock was set as cacheControl: { type:
'ephemeral' } — which is the Anthropic wire format. The Bedrock Converse
API expects cachePoint: { type: 'default' }. The system prompt was
silently not being cached on Bedrock at all.

Both the system prompt and the new prepareStep now go through a shared
getCacheProviderOptions helper that returns the correct field per
provider.

**3. Persisted cached token usage to monitor cache strat. efficiency**
2026-04-29 14:42:12 +00:00
11628d19a3 add recurring calendar events for google cal (#19748)
Co-authored-by: Charles Bochet <[email protected]>
2026-04-29 14:06:57 +00:00
46ba5fd16c i18n - translations (#20138)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-04-29 15:51:28 +02:00
nitinandGitHub 4649736d49 [Command Menu] Fix record-selection command filtering in edit mode (#20034)
https://github.com/user-attachments/assets/fe1461c7-0d5c-4c6f-8c2e-2cf569e7de90

## What

Fix `RECORD_SELECTION` items leaking into the command menu when nothing
is selected, and unify how the menu renders in normal vs edit mode.

## The bug

`RECORD_SELECTION`-availability items were showing up even when
`numberOfSelectedRecords === 0`. New util
`doesCommandMenuItemMatchSelectionState` gates them, applied
consistently in the runtime provider and the editor.

## The refactor

`PinnedCommandMenuItemButtonsEditMode` was a 140-line near-duplicate of
`PinnedCommandMenuItemButtons` with its own (drifting) filter logic.
Killed it. Edit mode now flows through the same
`CommandMenuContextProvider` with a new `isInPreviewMode` flag — one
filter chain, one rendering path.

## Behavior in edit mode

**Header (pinned buttons in page header):**
- Runs the full filter chain — object metadata, page type, selection
state, page layout, *and the conditional availability expression*
- Buttons render at full styling but are inert via `pointer-events:
none` + `cursor: not-allowed`
- Preview now reflects exactly what users will see on the live page (not
a grayed-out approximation)

**Side panel editor:**
- New `useEditableCommandMenuItems` hook
- Same filters as runtime *minus* the conditional availability
expression and `FALLBACK` items — so it surfaces everything that's
actually configurable for this page context
- Still gates on selection state — if no records selected,
`RECORD_SELECTION` items are hidden from the editor too. Open to
feedback if we'd rather always show them so users can pin them ahead of
time.

## Misc

- `usePinnedCommandMenuItemsInlineLayout` — visible count now waits
until every item is measured before committing. Fixes a flash of wrong
counts on mount/resize
- Renamed `useCommandMenuContextApi` → `useCurrentCommandMenuContextApi`
— name now conveys it reads from the *current* scoped context store
- Copy: "Records selected" → "Record(s) selected"
2026-04-29 13:34:28 +00:00
Abdullah.andGitHub 51384bc085 refactor: optimize website visual runtime (#20120)
Refactors the website visual runtime to make WebGL-heavy sections more
reliable and less expensive.

This adds shared image/model loading caches, safer WebGL context
recovery, staggered visual mounting, and static rendering for decorative
Helped card visuals. It also removes a large bespoke Helped renderer in
favor of the shared halftone model canvas, reduces scroll/layout work in
the Helped section, and cleans up duplicated model-loading code across
several visuals.
2026-04-29 12:44:47 +00:00
neo773andGitHub abfa6200dd ssrf hardening (#19963)
Hardened CalDav with new approach of wrapping axios ssrf http agent to
fetch via `@lifeomic/axios-fetch` because `tsdav` only accept `fetch`
override.

Also Hardened test endpoint
2026-04-29 12:13:19 +00:00
nitinGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>nitin
480e5796ec fix(ai): render record links inside markdown headings in AI chat (#20074)
## Summary

Adds `h1`–`h6` component overrides to `LazyMarkdownRenderer` so that
`[[record:...]]` references placed inside markdown headings in the AI
chat are parsed by `processChildrenForRecordLinks` and rendered as
clickable `RecordLink` chips, matching the behavior already in place for
`p`, `li`, `td`, `th`, and `a`.

Fixes #20072

## Test plan

- [ ] In AI chat, ask a question whose answer places a record reference
inside a markdown heading (e.g. `## Found [[person:uuid:John Doe]]`) and
confirm a clickable `RecordLink` chip renders instead of the raw
`[[...]]` text.
- [ ] Verify heading styling (sizes, weights, margins) is unchanged.

Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: nitin <[email protected]>
2026-04-29 13:50:32 +02:00
Félix MalfaitGitHubClaude Opus 4.7claude[bot] <41898282+claude[bot]@users.noreply.github.com>
d8e2de48e6 Fix stale UI state after stop-impersonation (#20088)
## Summary

A customer reported that after **Stop Impersonating**, the sidebar still
showed the impersonated user's pinned favorites, the AI chat tab toggle,
and the AI chat history — even though the original admin's session was
correctly restored.

## Root cause

The refactor in #19597 replaced the previous `signOut()`-based stop flow
with an in-place token swap, but only cleared Apollo cache + reloaded
the user. Several user-scoped client stores were left untouched:

- **`metadataStoreState`** is localStorage-backed
(`navigationMenuItems`,
  `agentChatThreads`, `views`, `pageLayouts`, etc.) and only refreshed
  by `MinimalMetadataLoadEffect`. That effect is gated by
  `metadataLoadedVersion` + `desiredLoadState`, neither of which flips
  on a same-workspace token swap, so the effect never re-runs.
- **In-memory AI atoms** (`currentAiChatThreadState`,
`agentChatInputState`,
  `hasInitializedAgentChatThreadsState`) keep pointing at the
  impersonated user's selected thread / input.
- **Session localStorage keys** (`agentChatDraftsByThreadIdState`,
`lastVisitedObjectMetadataItemIdState`,
`lastVisitedViewPerObjectMetadataItemState`,
  `playgroundApiKeyState`) carry the impersonated user's drafts and
  navigation state.

`clearSession()` (used by logout) avoids this because it calls
`applyMockedMetadata()` and flips `desiredLoadState` mocked↔real, which
chain-triggers a full metadata reload on next sign-in.

## Fix

Extract a `resetUserScopedClientState` helper inside
`useImpersonationSession` that:

1. Calls `clearSessionLocalStorageKeys()` to drop user-scoped
localStorage
   keys.
2. Resets the in-memory AI session atoms.
3. Marks `metadataStoreState['agentChatThreads']` as `'empty'`.
`useLoadStaleMetadataEntities` does **not** handle this entity key, so
   without an explicit reset to `'empty'` the
   `AgentChatThreadInitializationEffect` (which only fires on `'empty'`)
   would never refetch.
4. Calls `invalidateMetadataStore()` to clear all
`currentCollectionHash`
   values and bump `metadataLoadedVersion`, forcing
   `MinimalMetadataLoadEffect` to re-run and refetch
   `navigationMenuItems`, `views`, `pageLayouts`, etc. against the new
   token.

The helper is applied to both `startImpersonating` and
`stopImpersonating`
— start had the same latent bug; the impersonated user could see the
admin's favorites until the cache happened to refresh.

## Test plan

- [ ] As an admin user, pin some favorites in the sidebar
- [ ] Impersonate a user with different favorites → favorites should
      switch to the impersonated user's
- [ ] Click "Stop Impersonating" → sidebar should immediately show the
      admin's favorites (not the impersonated user's)
- [ ] As an admin **without** AI permission, impersonate a user **with**
AI permission, open AI chat, send a message, then stop impersonating
      → AI chat history should be empty / inaccessible (the AI tab
      visibility itself is fixed in a separate PR)
- [ ] Type a draft in AI chat as the impersonated user → after stop, the
      draft should be gone
- [ ] Verify regular sign-out still works while impersonating
- [ ] Verify the impersonation banner still shows / hides correctly

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-04-29 13:37:12 +02:00
Paul RastoinandGitHub b49e58dfc6 Fix click house migration (#20127)
`it does not support renaming of multiple tables in single query.`

@etiennejouan manually fix the corrupted clickhouse instance
2026-04-29 10:48:21 +00:00
a6118b7dc3 i18n - translations (#20125)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-04-29 11:57:14 +02:00
Abdul RahmanandGitHub 19ee9444ed add UpsertViewWidget resolver (#20053) 2026-04-29 09:41:02 +00:00
Paul RastoinandGitHub b92617d46e Copy twenty-shared in twenty-website deploy (#20124) 2026-04-29 08:44:54 +00:00
c476c6c80b chore: sync AI model catalog from models.dev (#20122)
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 <[email protected]>
2026-04-29 08:45:09 +02:00
Paul RastoinandGitHub d2dda67596 Fix upgrade --start-from-workspace-id (#20116)
# Introduction
Prevent using both `--start-from-workspace-id` and `--workspace`

When any of the two are being passed we prevent passing to the next
instance segment, it would require an upgrade re run even if legit

When `--start-from-workspace-id` is passed we filter from all the
fetched active or suspended workspace ids and apply equivalent filter as
before
2026-04-28 16:46:15 +00:00
Abdullah.andGitHub 6aec449a56 refactor: harden website runtime, routing, and hero visual (#20113)
This centralizes routing/SEO ownership, replaces deprecated middleware
with proxy, adds safer lifecycle/runtime primitives, and introduces
visual error boundaries so broken WebGL/canvas-heavy visuals can fail
gracefully instead of taking down the page. It also hardens animation,
resize, visibility, cleanup, and WebGL fallback paths for a broader
range of browsers and devices.

The hero visual was split from large monolithic files into focused
domain folders for shell, pages, shared primitives, window interactions,
terminal conversation, prompt, editor, and traffic-light behavior.
Legacy unused section code was removed, visual configs were extracted,
state/geometry logic was moved into testable modules, and coverage was
added across routing, SEO, lifecycle, animation, visual runtime,
halftone behavior, and hero interactions.

More changes on the way, but this should make the website a lot more
stable - disabling WebGL on Firefox and loading the website does not
cause crashes on local any longer, will test on dev once this is merged.
2026-04-28 17:15:28 +02:00
7ea1dfdd49 i18n - translations (#20115)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-04-28 16:48:25 +02:00
3290bf3ab1 fix(rest-api): prevent silent pagination failures and include valid options in enum validation errors (#20092)
# Summary 
(fixes #20044)

This PR implements two fixes for the REST API to enforce stricter
validation and provide better error messages.

Issue 1: Cursor parameter silently ignored
Problem: When users provided common cursor aliases (e.g., cursor, after,
before) instead of the correct parameter names (starting_after,
ending_before), the API silently ignored them and returned page 1 on
every request.
Solution: Added strict validation to detect common cursor aliases and
throw a clear error directing users to use the correct parameter names.
Files modified:
-
packages/twenty-server/src/engine/api/rest/input-request-parsers/rest-input-request-parser.exception.ts
-
packages/twenty-server/src/engine/api/rest/input-request-parsers/starting-after-parser-utils/parse-starting-after-rest-request.util.ts
-
packages/twenty-server/src/engine/api/rest/input-request-parsers/ending-before-parser-utils/parse-ending-before-rest-request.util.ts
- Test files for both parsers
Example error:
Invalid cursor parameter 'cursor'. Use 'starting_after' for pagination.
---
Issue 2: OpportunityStageEnum not validated on REST
Problem: When creating or updating opportunities via REST with an
invalid stage value, the API either silently dropped the value or
returned a generic error without listing valid options.
Solution: Updated the SELECT field validation to include valid options
in the error message.
Files modified:
-
packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-rating-and-select-field-or-throw.util.ts
- Test file
Example error:
Invalid value "BAD_VALUE" for field "stage". Valid values are: NEW,
SCREENING, MEETING, PROPOSAL, CUSTOMER
---
### Testing
- Added 5 new test cases for `parse-starting-after-rest-request.util.ts`
- Added 6 new test cases for `parse-ending-before-rest-request.util.ts`
- Added 1 new test case for
`validate-rating-and-select-field-or-throw.util.ts`
- All 21 tests passing
---
Breaking Changes
None - correct usage is unaffected.

---------

Co-authored-by: Etienne <[email protected]>
2026-04-28 14:31:18 +00:00
EtienneandGitHub fbaea0639a Billing - optimize usageEvent CH table (#20019)
- Update usageEvent clickhouse table, partitioning, indexing and
projection (auto materialized view) to optimize credit usage queries
- Add caching for available credits and billing subscription


To do in next PR: deprecate enforceCapUsage cron. Bonus : real-time on
billingSubscription
2026-04-28 14:06:49 +00:00
2a3b8adcfb i18n - translations (#20114)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-04-28 16:17:27 +02:00
Félix MalfaitGitHubClaude Opus 4.7claude[bot] <claude[bot]@users.noreply.github.com>
8f362186ce Redesign application content tab + logic function settings; add Layout detail pages (#20056)
## Summary

Iterative redesign of two related areas in settings, plus a new
`pages/settings/layout/` folder for read-only entity detail pages.

### Application content tab

- **Grouped into three sections** — Data / Layout / Logic — each with
one H2 + multiple `TableSection`-wrapped sub-tables (mirrors the
role-permissions pattern). Replaces six per-category table/row
components with one uniform `<SettingsApplicationContentSubtable>` +
`ApplicationContentRow` shape (net **−~700 lines** across the refactor).
- **All 10 row categories now clickable** for installed apps:
- Objects / Fields / Logic functions / Front components → existing
detail pages
  - Agents → existing `AiAgentDetail`
- Skills → existing `AiSkillDetail` (looked up by `Skill.applicationId +
name`)
- Roles → existing `RoleDetail` (looked up by
`Role.universalIdentifier`)
- Views / Page layouts / Navigation menu items → **new** detail pages
(see below)
- **Lifecycle hooks visible** — `pre-install` / `post-install` logic
functions are surfaced in the Trigger column instead of appearing as
empty/misconfigured.

### Logic function settings (Triggers + Test tabs)

- Triggers tab is now editable (HTTP / Cron / Database event / AI tool)
with a `<SettingsLogicFunctionTriggerSection>` wrapper that owns the
toggle, header, and read-only short-circuit.
- HTTP section gets a Live URL field with copy-to-clipboard.
- Each section shows a **Sample input** preview (the JSON the function
will receive) using the same payload builders the Test tab uses.
- Test tab: **Simulate trigger** buttons that prefill the JSON input
from the configured trigger's schema. Replaces an unclickable `<Select>`
(which auto-disables when there's only one option — the typical case).
- Read-only behavior for installed-app functions: explicit `<Callout>`
notice when there's no trigger; trigger sections render as disabled
controls when there is one.
- Removed the empty Environment Variables section from the Settings tab
(it just told the user to go elsewhere).

### New `pages/settings/layout/` folder

Three new app-scoped detail pages so users can drill into entities the
GraphQL `Application` type doesn't expose by id (keyed by manifest
`universalIdentifier`):

- `ApplicationViewDetail` — type, object, visibility + Fields / Filters
/ Sorts subsections (field UIDs resolved to readable labels via
`useFieldLabelByUid`)
- `ApplicationPageLayoutDetail` — type, object + per-tab subsections
listing widgets
- `ApplicationNavigationMenuItemDetail` — type, destination (resolved),
icon, color, position

Each page reads from the marketplace manifest the parent app page
already loads (no extra queries). Folder set up so a future "Layout"
settings tab can grow here (analogous to the existing `data-model/`
folder under the Data tab).

### Other consistency fixes

- Breadcrumbs on every app-scoped entity detail page now include a
category crumb so users know what they're looking at: `Workspace /
Applications / Timely / Navigation menu items / Time entry`.
- Title fallback for nav menu items uses the resolved destination
(`"Time entry"`) instead of the raw enum (`"OBJECT"`).
- New shared utils: `getNavigationMenuItemDestination`,
`resolveManifestObjectLabel`, `getLogicFunctionTriggerLabel`,
`<MonoText>`.

## Backend changes

Only one minor schema-shape change (additive): added `applicationId` to
the `SkillFields` GraphQL fragment and `universalIdentifier` to the
`RoleFragment` so the new lookups have what they need. Generated
metadata schema patched in-tree to match — regenerate with `nx run
twenty-front:graphql:generate --configuration=metadata` if it drifts.

## Test plan

- [ ] Application content tab on an installed app shows the 3 grouped
sections; rows in each section are clickable
- [ ] Click an Object → existing object detail page
- [ ] Click a Field → existing field-edit page
- [ ] Click an Agent / Skill / Role → existing detail page
- [ ] Click a View / Page layout / Navigation menu item → new read-only
detail page; subsections (Fields/Filters/Sorts for views, per-tab
widgets for page layouts) populate correctly
- [ ] Breadcrumbs on every entity detail page have 5 crumbs ending in
`<Category> / <Entity name>`
- [ ] Logic function Triggers tab: toggle each trigger type on/off, see
the Sample input preview update; for installed apps, sections render as
read-only
- [ ] Test tab: each "Simulate trigger" button prefills the JSON editor
with the matching payload shape
- [ ] Functions list: a function configured as `post-install` shows
"Post-install" in the Trigger column

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Co-authored-by: claude[bot] <claude[bot]@users.noreply.github.com>
2026-04-28 16:08:15 +02:00
martmullandGitHub 8998009805 Stop reseting isListed and is featured after each sync (#20111)
isListed and isFeatured are manually updated by the admin, it should not
be updated if the application already exists
2026-04-28 13:23:42 +00:00
Abdul RahmanandGitHub a710c105cf Fix orphan views by deferring record table widget view creation to dashboard save (#20006) 2026-04-28 10:06:26 +00:00
Sai Sathwik PandGitHub a5cd64daf5 refactor: standardize JsonStringified casing (#20101)
## Summary
- Rename safeParseRelativeDateFilterJSONStringified to
safeParseRelativeDateFilterJsonStringified
- Update the matching utility file, exports, tests, and workflow usages

Part of #19839.

## Validation
- CI passed
2026-04-28 09:01:26 +00:00
df3e217d64 fix(ai-billing): bill executeAgent in a finally block so failed runs don't leak (#20065)
## Summary

`AgentAsyncExecutorService.executeAgent` consumes Anthropic tokens at
two points (the main `generateText` and the optional structured-output
sub-call). Billing was previously the **caller's** responsibility,
executed only after `executeAgent` returned. If `executeAgent` threw —
e.g. when `structuredResult.output == null` for a schema-mismatched
response, or anything caught by the catch-and-rethrow — we paid
Anthropic but never recorded a `usageEvent`. Likely the dominant source
of the 716M-token-vs-3.27-credits discrepancy seen on the affected
workspace in the 2026-04-26 incident.

## What changed

- Inject `AiBillingService` into `AgentAsyncExecutorService`. Add
`workspaceId` (required), `userWorkspaceId`, and `operationType`
(default `AI_WORKFLOW_TOKEN`) to `executeAgent`'s args.
- Capture `accumulatedUsage`, `cacheCreationTokens`, and
`nativeWebSearchCallCount` into mutable locals as each `generateText`
resolves. A throw between the main and structured-output calls still
bills the first call's tokens; the schema-validation throw still bills
the merged usage.
- Wrap the body in `try { ... } finally { ... }`. The finally calls
`calculateAndBillUsage` and `billNativeWebSearchUsage`, each guarded by
its own `try/catch + logger.error` so a billing exception can't mask the
original execution error or block the second emit.
- `ai-agent.workflow-action.ts`: pass the new args; drop the
now-redundant billing calls and `AiBillingService` injection.
`AiBillingModule` removed from this action's module imports.
- `run-evaluation-input.job.ts`: pass `workspaceId` (already in `data`)
and `userWorkspaceId: null`. **As a side effect, the eval pipeline now
bills correctly** — closing an additional billing leak from the audit
(`RunEvaluationInputJob` previously called `executeAgent` and discarded
`executionResult.usage`).

## Behavior change worth calling out

Previously, failed agent executions were silently free. They will now be
billed for the tokens Anthropic charged us. This is intentional and
correct.

## Test plan
- [ ] Trigger a workflow agent action that succeeds — `usageEvent` count
should match what was previously emitted.
- [ ] Trigger a workflow agent with a JSON response schema and ambiguous
input that produces a non-schema-conforming output
(`structuredResult.output == null`) — verify a `usageEvent` row is now
written for the consumed tokens (was 0 rows previously).
- [ ] Trigger a `runEvaluationInput` GraphQL mutation — verify a
`usageEvent` row is written (was 0 rows previously).

## Notes for review
- Conflicts trivially with the Sentry-context PR on
`run-evaluation-input.job.ts`. Recommend merging Sentry-context first;
this PR's 2-line argument addition rebases inside that PR's
`aiCallContextService.run(...)` callback wrapper.
- A small follow-up after this lands: thread `billingContext` through
the `experimental_repairToolCall` callback in
`agent-async-executor.service.ts:198` (currently marked with a TODO from
the title-gen+repair-tool PR).

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-04-28 10:43:53 +02:00
1c06506256 chore: sync AI model catalog from models.dev (#20106)
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 <[email protected]>
2026-04-28 08:49:33 +02:00
Mohamed Houssein DouiciandGitHub d271ba06f1 chore: use navigation menu item type into the generated navigation menu layout (#20099)
This is a small PR to fix an issue that came up when I created an object
with the default navigation menu item.

<img width="811" height="461" alt="image"
src="https://github.com/user-attachments/assets/70c7f55d-3d41-48ed-8cf9-5f649ea41f49"
/>
2026-04-28 06:23:00 +00:00
7d35979fcf i18n - docs translations (#20100)
Created by Github action

Co-authored-by: github-actions <[email protected]>
2026-04-28 00:31:22 +02:00
8d3047c1fc i18n - docs translations (#20097)
Created by Github action

Co-authored-by: github-actions <[email protected]>
2026-04-27 22:44:15 +02:00
Félix MalfaitGitHubClaude Opus 4.7claude[bot] <41898282+claude[bot]@users.noreply.github.com>
e632b7dbb9 fix(ai-billing): bill POST /rest/ai/generate-text usage to ClickHouse (#20066)
## Summary

`POST /rest/ai/generate-text` calls `generateText` and returns `usage`
to the client without emitting a `usageEvent`. Authenticated, gated only
by `PermissionFlagType.AI` — any workspace user with that permission
could call it in a loop without billing. Identified during the
2026-04-26 incident audit.

## What changed

- Inject `AiBillingService` into `AiGenerateTextController`.
- Add `@AuthUserWorkspaceId() userWorkspaceId: string` to source the
user-workspace identifier.
- Wrap the `generateText` call in `try { ... return ... } finally { ...
}` so billing fires even if the controller throws after Anthropic was
paid.
- Bill with `UsageOperationType.AI_WORKFLOW_TOKEN` and
`cacheCreationTokens: result.usage.inputTokenDetails?.cacheWriteTokens
?? 0`.
- Inner `try/catch` around the billing emit so a billing error can't
break the response.
- One-line module change: `AiGenerateTextModule` imports
`AiBillingModule` (NestJS DI requirement).

## Test plan
- [ ] Call `POST /rest/ai/generate-text` with a small prompt; verify a
`usageEvent` row appears in ClickHouse for the workspace with the
correct token count and `operationType = AI_WORKFLOW_TOKEN`.
- [ ] Call with a malformed model id that throws after the API key is
validated — verify no spurious billing call occurs (no Anthropic call
was made).

## Notes for review
- Response shape unchanged.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-04-27 21:16:19 +02:00
5b3ee3f1a7 fix(ai-billing): bill thread title generation and tool-call repair (#20067)
## Summary

Two `generateText` call sites were unbilled — identified during the
2026-04-26 incident audit:

- **Thread title generation**
(`AgentTitleGenerationService.generateThreadTitle`) — fires once per new
chat thread; low-volume but completeness matters.
- **Tool-call repair** (`repairToolCall` util, used inside
`experimental_repairToolCall` callbacks) — can fire `MAX_STEPS` times
per agent turn if a model gets stuck producing malformed tool calls.

## What changed

- `AgentTitleGenerationService` — inject `AiBillingService`, expand
`generateThreadTitle` signature to accept `workspaceId` and
`userWorkspaceId`, bill in a `finally` block with
`UsageOperationType.AI_CHAT_TOKEN`. Restructured so the no-default-model
short-circuit happens before the `try`, avoiding a fake billing call.
- `repair-tool-call.util.ts` — added an optional `billingContext` arg
containing `aiBillingService`, `modelId`, `workspaceId`,
`userWorkspaceId`, `operationType`. Wraps the `generateText` call in
`try/finally`; bills in finally with the operation type provided by the
caller. Optional so existing callers keep compiling.
- `chat-execution.service.ts` — threads `billingContext`
(`AI_CHAT_TOKEN`) into the repair callback.
- `agent-chat.service.ts` — passes `workspaceId` and
`thread.userWorkspaceId` to `generateThreadTitle`.
- `agent-async-executor.service.ts` — TODO comment marking that the
repair callback should thread billing once `executeAgent` accepts
`workspaceId` (depends on the executeAgent-finally PR).

## Follow-up

After the executeAgent-finally PR lands, `workspaceId` is in scope at
the `experimental_repairToolCall` callback in
`agent-async-executor.service.ts:198`. Thread `billingContext` through
to fire repair-call billing for workflow agents too, and remove the
TODO. Tiny follow-up.

## Test plan
- [ ] Create a new chat thread; verify a `usageEvent` row is written for
the title generation call.
- [ ] Send a chat message that triggers tool-call repair (e.g. force a
malformed tool call); verify a `usageEvent` row for the repair sub-call.

## Notes for review
- `billingContext` arg made **optional** to allow incremental rollout —
the executeAgent-finally PR plus a small follow-up will close the
agent-async-executor side.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-04-27 20:54:40 +02:00
875795cc30 feat(sentry): propagate workspace context to all spans (#20064)
## Summary

After the 2026-04-26 token-usage incident, identifying the responsible
workspace from a Sentry trace required a Postgres scavenger hunt —
Vercel AI SDK auto-instrumentation captures token counts and model name
but no twenty-specific identifiers, and that same gap exists for every
other auto-instrumented span (HTTP outbound, Postgres queries, GraphQL
resolvers, Redis, etc.).

This PR plugs that gap globally, not just for AI:

- A small utility
(`packages/twenty-server/src/engine/core-modules/sentry/utils/sentry-workspace-context.util.ts`)
that writes workspace identifiers onto Sentry's active isolation scope
as a `twenty` context block plus filterable tags and a `Sentry.setUser`
call.
- Two hook points covering all server traffic:
- **`WorkspaceAuthContextMiddleware`** — already runs after token
hydration on the GraphQL, metadata, admin-panel, and REST routes. It now
calls the utility once per authenticated request, before delegating to
`withWorkspaceAuthContext`.
- **`BullMQDriver.work` and `SyncDriver.processJob`** — every queue job
now runs inside `Sentry.withIsolationScope` and applies workspace
context from `job.data.workspaceId` (skipping silently for system jobs
that don't carry one).
- A `beforeSendSpan` hook in `instrument.ts` that reads the scope's
`twenty` context block back and projects it onto every span as
`twenty.workspace.id` and (when available) `twenty.user_workspace.id` —
dotted-namespace naming consistent with OTel/Sentry conventions like
`user.id` and `http.response.status_code`. Spans without a workspace
context (unauthenticated traffic) pass through untouched.

## Why this shape

Sentry's docs position `beforeSendSpan` as a per-span hook. The previous
iteration set context only at AI-specific call sites, which left non-AI
spans (DB queries, outbound HTTP, regular GraphQL queries, workflow
steps not touching AI) entirely unenriched. Hooking the two existing
global boundaries — auth middleware for HTTP/GraphQL/REST, and the queue
driver `work()` callback for background jobs — covers every
authenticated span across the app with no per-handler instrumentation.

## What's not in this PR

AI-specific identifiers (`twenty.agent.id`, `twenty.thread.id`,
`twenty.turn.id`, `twenty.workflow_run.id`) are out of scope here.
They're useful additions but require either propagating the IDs through
the call stack or a more fine-grained scope (per-step, per-turn) than
the request/job boundary, which is best handled in follow-up PRs that
target those specific call sites.

## Test plan

- [ ] Make any authenticated GraphQL request locally and confirm the
resulting span(s) in Sentry carry `twenty.workspace.id` and (for
user-authenticated routes) `twenty.user_workspace.id`.
- [ ] Make any authenticated REST request and confirm the same.
- [ ] Trigger a queue job (chat stream, agent turn evaluation, workflow
run, etc.) and confirm spans produced inside the worker carry
`twenty.workspace.id`.
- [ ] Confirm that DB and outbound HTTP spans produced under the
request/job also carry the workspace tag — these previously had no
twenty-specific identifiers.
- [ ] In the Sentry UI, filter events by the `twenty.workspace.id` tag
and confirm matching events appear.

## Notes for review

- Sentry init lives in `instrument.ts`, loaded before Nest bootstraps,
so `beforeSendSpan` runs outside Nest DI and reads context off the
isolation scope rather than holding a service reference.
- The middleware change is three lines; the BullMQ wrap is a single
`Sentry.withIsolationScope` around the existing job handler body; the
SyncDriver wrap mirrors it for the dev/test path. No new modules or DI
providers.
- The previous iteration's `AiCallContextService` and per-handler
`setContext` / `withContext` calls have been removed in favor of these
two hooks.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-04-27 20:53:50 +02:00
350b835fbc i18n - docs translations (#20095)
Created by Github action

Co-authored-by: github-actions <[email protected]>
2026-04-27 20:48:05 +02:00
ef49dad0bc i18n - docs translations (#20093)
Created by Github action

Co-authored-by: github-actions <[email protected]>
2026-04-27 18:51:56 +02:00
Mohamed Houssein DouiciandGitHub e3a668a054 chore: export the enum view calendar layout to avoid typing errors on apps (#20090)
This is a small PR to fix an issue that came up when I created an app
with a custom object. I think the enum `ViewCalendarLayout` was not
caught as part of the exports.

<img width="1624" height="1114" alt="image"
src="https://github.com/user-attachments/assets/7bca3d0d-0e68-48ef-ac5f-6a3375733499"
/>
2026-04-27 16:35:22 +00:00
74536e6f98 Gate AI chat navigation entries by AI_SETTINGS permission (#20089)
## Summary

The AI chat history tab in the navigation drawer (and the corresponding
"New chat" icon in the mobile bottom bar) were rendered for every
authenticated user, regardless of whether they had `AI_SETTINGS`
permission. The actual chat content load is already gated — see
`AgentChatThreadInitializationEffect` — but the entry points were not,
so users without AI access saw a tab they could open and find empty.

This regressed when the `IS_AI_ENABLED` feature flag was removed in
#19916. The flag check was the only gate; nothing replaced it with a
permission check.

## Fix

Three small changes, all using
`useHasPermissionFlag(PermissionFlagType.AI_SETTINGS)`:

- **`MainNavigationDrawerTabsRow`** — return `null` when the user
  lacks AI access. This row only contains AI controls (the chat
  history tab pill + the "New chat" button), so without AI access
  there is nothing meaningful to render.
- **`MainNavigationDrawer`** — only render
`NavigationDrawerAiChatContent`
  when the user has AI access **and** the active tab is
  `AI_CHAT_HISTORY`. This is defensive: with the tabs row hidden the
  user can no longer switch to AI, but the active-tab atom could still
  carry `AI_CHAT_HISTORY` from a previous state (notably right after
  stopping impersonation of a user who had AI). Without this guard,
  the AI panel would briefly stay rendered.
- **`MobileNavigationBar`** — drop the `newAiChat` item when the user
  lacks AI access.

Surfaced by the same customer report that motivated #20088
(stop-impersonation
state cleanup), but this is a separate, pre-existing bug — admins
without AI permission see the tab regardless of impersonation.

## Test plan

- [ ] As a user **with** `AI_SETTINGS` permission, verify the AI chat
      tab and "New chat" button still appear and work in both the
      desktop sidebar and mobile bottom bar
- [ ] As a user **without** `AI_SETTINGS` permission, verify:
  - the tabs row at the top of the sidebar is gone (only the
    navigation menu shows below)
  - the "New chat" mobile bottom-bar icon is gone
  - the AI chat panel does not appear even after navigating away from
    a state where it was previously active

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-04-27 18:21:14 +02:00
b6ec2acb56 i18n - docs translations (#20087)
Created by Github action

Co-authored-by: github-actions <[email protected]>
2026-04-27 16:59:00 +02:00
nitinandGitHub bb5f294c5a [AI] Collapse NativeToolBinder to a single bind() entry (#20051)
The binder doesnt need an agent or full tool context -- just a model and
options.
Single `bind(model, options)` entry!


Builds on #20022.
2026-04-27 16:35:59 +02:00
13aaea597e i18n - docs translations (#20083)
Created by Github action

Co-authored-by: github-actions <[email protected]>
2026-04-27 14:56:00 +02:00
ec893c2767 i18n - translations (#20081)
Created by Github action

Co-authored-by: github-actions <[email protected]>
2026-04-27 14:34:12 +02:00
Paul RastoinandGitHub f8c1ad5b3c Filtered upgrade logs when stopping before starting next instance segment (#20078)
# Introduction
In a nutshell, added a more readable logs that relates that when
performing a workspace fitlered workspace you can only browser the
workspace commands sequence

This PR is not a fix, but a log improvement
As before when cross-upgrading a single workspace you would be facing a
`instance` sync barrier error as not all your workspaces would have been
updated

Now logging and early returning instead of letting the guard hard throw

## Tests
Created a dedicated test for instance prevention on filtered upgrade
Standardized `migrationRecordToKey` usage across all upgrade integration
tests suites
2026-04-27 12:12:04 +00:00
Abdullah.andGitHub a90895e167 [Website] locale-segment routing and shared Lingui factory (#20079)
**1. Shared Lingui factory in `twenty-shared`**

- Extracted `createI18nInstanceFactory` into
`packages/twenty-shared/src/i18n/create-i18n-instance-factory.ts` so
every package gets the same per-render Lingui bootstrap with a
per-locale singleton cache and a `SOURCE_LOCALE` fallback.
- `twenty-emails/src/utils/i18n.utils.ts` now consumes the shared
factory.

**2. `twenty-website-new` Lingui bootstrap + Crowdin wiring**

- `lingui.config.ts`, `src/lib/i18n/*`, `nx run
twenty-website-new:lingui:{extract,compile}`.
- 31 locale PO files generated; minified compiled output kept out of
Prettier and Oxlint.
- `i18n-{push,pull}.yaml` workflows updated to include
`twenty-website-new` in Crowdin sync.

**3. `app/[locale]/...` segment routing with English at the root**

- All marketing routes moved under `src/app/[locale]/`; static
generation preserved (15 routes × 31 locales = 465 prerendered URLs).
- Middleware behavior:
  - `/{en}/...`         → 301 redirect to unprefixed canonical.
  - `/{non-en}/...`     → pass through, set `NEXT_LOCALE` cookie.

### What this PR explicitly does not do (deferred)

- Lingui-wrapping the actual marketing copy. Keys, build pipeline, and
  runtime are wired; copy migration is a separate, reviewer-friendlier
  PR.
2026-04-27 12:11:30 +00:00
Charles BochetandGitHub 9b7f7d059a docs: document rawBody on RoutePayload (#20080)
## Summary

Follow-up to #20061, which added `rawBody?: string` to
`LogicFunctionEvent` / `RoutePayload` so logic functions can verify
HMAC-style webhook signatures (GitHub's `X-Hub-Signature-256`, Stripe,
…) against the exact bytes that were received.

That PR did not update the public SDK docs, so the new field is
effectively invisible to developers consuming `RoutePayload` from
`twenty-sdk`. This PR adds a single row to the `RoutePayload` structure
table in `logic-functions.mdx` so the field is discoverable.

Addresses the review feedback on
https://github.com/twentyhq/twenty/pull/20061#discussion_r3147065014.
2026-04-27 12:06:55 +00:00
95fee18126 fix(server): match IMAP \Noselect attribute case-insensitively (#20043)
## Summary

`ImapGetAllFoldersService.isMailboxSelectable` checked
`mailbox.flags?.has('\\Noselect')`, which is case-sensitive. Per [RFC
3501 §6.3.8](https://www.rfc-editor.org/rfc/rfc3501#section-6.3.8), IMAP
attribute names are case-insensitive — different servers spell the flag
differently:

| Server | Spelling |
|---|---|
| Dovecot | `\Noselect` |
| Stalwart | `\NoSelect` |
| Cyrus | `\Noselect` |
| RFC examples | `\NOSELECT` |

The previous check only caught Dovecot's spelling. On other servers,
virtual namespace placeholders (e.g. Stalwart's `Shared Folders` parent)
passed through `isMailboxSelectable` and got persisted as folders. When
`MessagingMessageListFetchJob` later ran, it issued `SELECT "Shared
Folders"`, the server correctly rejected it with `NO [NONEXISTENT]
Mailbox does not exist.`, and the entire message-list fetch failed for
the channel.

## Reproduction

1. Connect an IMAP account whose server advertises a `\NoSelect` (or
`\NOSELECT`) namespace placeholder. Stalwart Mail v0.15.x exhibits this
with shared mailboxes:
   ```
   * LIST (\NoSelect) "/" "Shared Folders"
   ```
2. The folder discovery job persists it as a syncable folder.
3. `MessagingMessageListFetchJob` runs and fails:
   ```
   IMAP: Error fetching message list: D0 SELECT "Shared Folders"
   responseStatus: NO  serverResponseCode: NONEXISTENT
   ```

## Fix

Iterate the flag set and lowercase-compare against `'\\noselect'`. This
matches every legal spelling without changing behavior for compliant
`\Noselect` clients.

```diff
 private isMailboxSelectable(mailbox: ListResponse): boolean {
-  return !mailbox.flags?.has('\\Noselect');
+  if (!mailbox.flags) return true;
+  for (const flag of mailbox.flags) {
+    if (flag.toLowerCase() === '\\noselect') return false;
+  }
+  return true;
 }
```

## Test plan

- [x] Existing `\Noselect` test cases (`should not issue STATUS against
a \Noselect folder`, parent-reference preservation, Sent-folder
exclusion) still pass — the lowercase comparison subsumes them.
- [x] New parameterized test covers `\Noselect`, `\NoSelect`,
`\NOSELECT`, `\noselect` spellings against a Stalwart-style `Shared
Folders` namespace placeholder, asserting Twenty does **not** issue
`STATUS` on the placeholder and does **not** include it in the
discovered folder set.

---------

Co-authored-by: neo773 <[email protected]>
2026-04-27 11:58:02 +00:00
neo773andGitHub 5b8804ff06 gmail extract body from deeply nested MIME parts (#19989)
/closes #19879
2026-04-27 11:56:50 +00:00
neo773andGitHub 6545ca274c fix(messaging): refactor SentMessagePersistenceService (#20077)
refactored `SentMessagePersistenceService` to be thin wrapper over
`saveMessagesAndEnqueueContactCreation`
this fixes a bug where the old logic did not call match participants
causing messages to not show up
2026-04-27 11:56:41 +00:00
15c52d3a39 Documentation update (#20059)
Add missing info about how to edit navigation bar, add many-to-many
relation and get Enterprise key for Org license on self-hosted

---------

Co-authored-by: Charles Bochet <[email protected]>
2026-04-27 11:51:42 +00:00
3db1af9a17 fix(logic-function): forward raw request body for HMAC signature verification (#20061)
## Summary

- Add optional `rawBody?: string` to `LogicFunctionEvent` and forward it
from the route trigger so HMAC-based webhook signatures (GitHub's
`X-Hub-Signature-256`, Stripe, …) can be verified by user logic
functions.
- Update `github-connector`'s `getRawBodyForSignature` to prefer
`event.rawBody` (with the existing string/base64/null fallbacks kept for
older runtimes).

## Why

GitHub computes `X-Hub-Signature-256` over the **raw bytes** of the
request body. The receiver must verify against those exact bytes — key
order, whitespace and unicode escaping all matter, so the parsed JSON
body cannot be re-serialized to them.

Today the route trigger calls `extractBody(request)` which returns the
parsed object only. NestJS already preserves the raw body on
`request.rawBody` (the app is bootstrapped with `rawBody: true` in
`main.ts`), but it was never propagated into `LogicFunctionEvent`.

As a result the github-connector's webhook handler always took the "raw
body unavailable" branch and rejected every delivery (after #19961 /
962c2b3c14). With this change, signature verification can succeed
end-to-end.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-04-27 11:36:02 +00:00
Paul RastoinandGitHub 1e7c1691f4 [CI] Prevent previous version upgrade sequence mutation (#20075)
# Introduction
Prevent any PR to target a previous already released twenty version by
mistake.

Especially useful for existing opened PR introducing commands into an
upgrade that has just been released leading to a
`TWENTY_CURRENT_VERSION` bump

<img width="3150" height="1158" alt="image"
src="https://github.com/user-attachments/assets/b83d211f-a061-4d63-ae7a-354d7851ec08"
/>

## Bypass
If intentional add `ci:allow-previous-version-upgrade-mutation` label to
the PR and re-run the failed job

<img width="3150" height="1158" alt="image"
src="https://github.com/user-attachments/assets/f94ee630-d87b-4477-9e50-bf6773a8a280"
/>

This will require a brand new ci from a commit introduced after the
label has been added
2026-04-27 11:15:49 +00:00
577312f121 chore: sync AI model catalog from models.dev (#20073)
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 <[email protected]>
2026-04-27 08:46:56 +02:00
fb8b9cb86c Add admin avatars and app logos (#20001)
## Summary
- Add user avatars and workspace logos to the admin general table and
workspace member detail view
- Show app icons in the admin app registrations table and reuse the
shared application display component
- Expose the needed avatar and logo fields through admin GraphQL queries
and backend lookup/statistics services
- Keep workspace fallback behavior consistent when no logo is set and
clean up a few local table styling duplicates

## Testing
- `./node_modules/.bin/tsc -p packages/twenty-front/tsconfig.json
--noEmit --pretty false`
- Manual UI verification of the admin general, workspace detail, and
apps tables

---------

Co-authored-by: Charles Bochet <[email protected]>
2026-04-26 17:15:20 +00:00
Charles BochetandGitHub 499067ae14 fix(logic-function): serialize LocalDriver layer builds with cache lock (#20054)
## Summary

Fixes a race condition in `LocalDriver` where concurrent `execute()`
calls for the same application can trash each other's layer build,
causing logic function executions to fail with either:

- `Error: ENOENT: process.cwd failed with error no such file or
directory, the current working directory was likely removed without
changing the working directory, uv_cwd` (the yarn-install child's `cwd`
got wiped mid-run), or
- `ENOENT: no such file or directory, open '…/<pkg>/<file>.js.map'`
raised from yarn's berry link step (files under the deps layer vanish
while yarn is extracting).

Both are thrown from `…/deps/<checksum>/.yarn/releases/yarn-4.9.2.cjs` —
i.e. the yarn process `copyYarnEngineAndBuildDependencies` spawns with
`cwd: buildDirectory`.

## Root cause

`LocalDriver.createLayerIfNotExist` (and `ensureSdkLayer`) both follow a
check-then-act pattern with no mutual exclusion:

```ts
if (await pathExists(depsNodeModulesPath)) return;
await fs.rm(depsLayerPath, { recursive: true, force: true });
await copyDependenciesInMemory(...);
await copyYarnEngineAndBuildDependencies(depsLayerPath); // spawns yarn with cwd=depsLayerPath
```

The deps layer path is shared across all `execute()` calls that match a
given `yarnLockChecksum`. Two concurrent callers (e.g. a webhook
invocation + a cron-triggered logic function firing while the first
run's layer is still being built) both see no `node_modules`, both
`fs.rm` the directory, and one's yarn child ends up with its `cwd` or
its extraction target gone.

## Fix

Wrap the critical sections with `CacheLockService.withLock` — the same
cache-backed lock the Lambda driver already uses for its layer/executor
builds:

- `createLayerIfNotExist` → lock key
`local-driver-deps-layer:${yarnLockChecksum ?? 'default'}`
- `ensureSdkLayer` → lock key
`local-driver-sdk-layer:${workspaceId}:${applicationUniversalIdentifier}`

A fast-path `pathExists` check is kept outside the lock (so warm
executions still skip lock acquisition entirely), and the existence +
staleness check is **repeated inside the lock** so followers no-op after
the leader finishes.

Lock parameters mirror the Lambda driver's layer-build lock (`ttl=120s`,
`retry=500ms`, `maxRetries=240`).

`cacheLockService` is now passed to `LocalDriver` from
`LogicFunctionDriverFactory` (it was already injected there for the
Lambda driver).
2026-04-26 12:44:40 +00:00
nitinandGitHub ca84d28157 [AI] ai usage line chart date gap filling (#20048)
https://github.com/user-attachments/assets/ecd37dfb-daae-41c3-8316-a9d923463eca
2026-04-26 12:41:16 +00:00
nitinandGitHub d1a4902460 [AI] Drop 'serialization' from tool output naming (#20052)
didnt made sense anymore -- we dont really 'serialize'
2026-04-26 14:10:01 +02:00
Paul RastoinandGitHub 89ad87aa64 Make twenty-front build env agnostic (#20055)
## Introduction
In aim to reduce and optimize the number of twenty-front build we do
during our cd process and allow twenty-front build promotion

### Build time

**Nothing is baked.** The `build/` directory is a clean, env-agnostic
artifact. `index.html` contains the empty placeholder:

```html
<script id="twenty-env-config">
  window._env_ = {
    // This will be overwritten
  };
</script>
```

The JS bundles contain no hardcoded server URL.

---

### Deploy mode 1: Frontend served by the backend (Docker / NestJS)

1. Container starts, NestJS boots in `main.ts`
2. `generateFrontConfig()` runs, reads `process.env.SERVER_URL`
3. Rewrites `dist/front/index.html`, replacing the placeholder with:
   ```html
   <script id="twenty-env-config">
     window._env_ = {
       REACT_APP_SERVER_BASE_URL: "https://api.example.com"
     };
   </script>
   ```
4. NestJS serves the static `dist/front/` directory
5. Browser loads `index.html`, `window._env_` is set before the app JS
executes
6. `src/config/index.ts` reads `window._env_.REACT_APP_SERVER_BASE_URL`
and uses it

---

### Deploy mode 2: Frontend served standalone (CDN / nginx / static
server)

1. Take the `build/` artifact as-is
2. Before serving, run at deploy time:
   ```bash
REACT_APP_SERVER_BASE_URL=https://api.example.com sh
./scripts/inject-runtime-env.sh
   ```
3. This does the same `sed` replacement on `build/index.html`
4. Serve the `build/` directory with your static server of choice
5. Same resolution in the browser:
`window._env_.REACT_APP_SERVER_BASE_URL` is picked up by
`src/config/index.ts`

---

### Fallback: no injection at all

If neither mechanism runs (e.g. local dev with `vite dev`),
`window._env_.REACT_APP_SERVER_BASE_URL` is `undefined`, and
`getDefaultUrl()` kicks in:
- **Localhost**: returns `http://localhost:3000`
- **Non-localhost**: returns same-origin (`window.location.origin`)
2026-04-26 07:05:54 +00:00
Charles BochetandGitHub 0c5c9c844e feat(github-connector): exclude self-reviews from review counts (#20050)
## Summary

- Adds an `isSelfReview` boolean to the consolidated `PullRequestReview`
record (`true` when the reviewer is the same contributor as the PR
author).
- Filters `isSelfReview === true` rows out of the top-reviewers
leaderboard (`top-contributors`) and per-contributor review stats
(`contributor-stats`) so contributors are credited only for reviews on
**other people's** PRs.
- Both the live ingestion path (`fetch-prs`) and the recompute job
(`recompute-pull-request-reviews`) now thread `prAuthorId` to
`buildConsolidatedRow`, so re-running either is sufficient to backfill
existing rows — no extra migration needed.

## Test plan

- [x] `yarn test
src/modules/github/pull-request-review/utils/consolidate-reviews.integration-test.ts`
— 20 tests passing, including new coverage for the `isSelfReview` helper
and `buildConsolidatedRow` payload.
- [ ] After merge: re-run `fetch-prs` (or
`recompute-pull-request-reviews`) once on a target workspace to backfill
`isSelfReview` on existing `PullRequestReview` rows, then verify the
top-reviewers leaderboard no longer credits self-reviews.


Made with [Cursor](https://cursor.com)
2026-04-25 23:34:01 +02:00
Charles BochetandGitHub 41571ea377 feat(front-component-renderer): forward offset/movement coordinates on serialised events (#20046)
## Summary

Adds `offsetX`, `offsetY`, `movementX`, `movementY` to
`SerializedEventData` and the host event serialiser so apps can reason
about element-relative pointer positions without trying to read the host
element's bounding rect (which is impossible from a remote-DOM worker).

## Motivation

I was building a front-component with click-to-drop-pin and trackpad
pan/zoom (custom OSM tile renderer). Two real bugs surfaced from the
current event-serialisation surface:

1. **Wheel pan/zoom was broken.** The host already forwards
`deltaX`/`deltaY`, but app authors naturally read them off the
React-style event handler argument as `e.deltaX`/`e.deltaY`. Because
remote-DOM bridges everything via `RemoteEvent extends
CustomEvent<Detail>`, the payload actually arrives at `e.detail.deltaX`.
Reading the wrong place gives `undefined`, and `undefined < 0 ===
false`, so every wheel notch zoomed in the same direction. App code now
uses `e.detail`, but this was a sharp papercut worth flagging in docs /
a helper (separate change).

2. **Element-local click coords are unobtainable from a worker.** With
only `clientX/Y`, an app needs the stage's bounding rect to translate
viewport coordinates to local — which can't be read across the worker
boundary. `offsetX`/`offsetY` close that gap with a one-read solution.
`movementX`/`movementY` round out the set for any future drag-style
interactions if `mousemove` later joins the allow-list.
2026-04-25 10:12:28 +00:00
570038ad65 chore: sync AI model catalog from models.dev (#20045)
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 <[email protected]>
2026-04-25 08:26:54 +02:00
Charles BochetandGitHub 88add35f0b Bump twenty-sdk, twenty-client-sdk, create-twenty-app to 2.1.0 (#20041)
## Summary

- Bumps `twenty-sdk` from `2.1.0-canary.1` to `2.1.0`.
- Bumps `twenty-client-sdk` from `2.1.0-canary.1` to `2.1.0`.
- Bumps `create-twenty-app` from `2.1.0-canary.1` to `2.1.0`.
2026-04-24 23:10:40 +02:00
Charles BochetandGitHub d74e3fa3b5 chore(server): bump current version to 2.2.0 (#20040)
## Summary

We are releasing Twenty v2.2.0. This PR sets up the
upgrade-version-command machinery for the new release line:

- Promote `2.1.0` into `TWENTY_PREVIOUS_VERSIONS` (it just shipped)
- Set `TWENTY_CURRENT_VERSION` to `2.2.0`
- Reset `TWENTY_NEXT_VERSIONS` to `[]`
- Refresh the `InstanceCommandGenerationService` snapshots to reflect
the new current version (`2.2.0` / `2-2-` slug)
- Update the failing-sequence-runner snapshot to include `2.2.0` in the
covered versions list

The `2-2/` upgrade-version-command module is already in place and wired
into `WorkspaceCommandProviderModule`, so future upgrade commands
targeting `2.2.0` can land directly under `2-2/` (or be generated
against `--version 2.2.0`).
2026-04-24 22:19:16 +02:00
Charles BochetandGitHub aad77b5315 Bump twenty-sdk, twenty-client-sdk, create-twenty-app to 2.1.0-canary.1 (#20038)
## Summary

- Bumps `twenty-sdk` from `2.1.0` to `2.1.0-canary.1`.
- Bumps `twenty-client-sdk` from `2.0.0` to `2.1.0-canary.1`.
- Bumps `create-twenty-app` from `2.0.0` to `2.1.0-canary.1`.
2026-04-24 19:57:02 +00:00
bba102efe3 chore: remove accidentally committed .claude-pr/ directory (#20036)
## Summary

Removes the `.claude-pr/` directory at the repo root, which contains
byte-identical duplicates of `CLAUDE.md` and `.mcp.json`.

## Why

- Added in #19517 (a PR about file-attachment support for agent chat),
unrelated to its content — looks like an accidental commit of a local
scratch directory.
- Not referenced from any workflow, script, or doc in the repository.
`.github/workflows/claude.yml` uses the root `CLAUDE.md` / `.mcp.json`,
not the copies under `.claude-pr/`.
- Because it's a duplicate of the source of truth, it will silently
drift out of sync over time.

## Test plan

- [x] Confirmed no references to `.claude-pr` anywhere in `*.yml`,
`*.yaml`, `*.json`, `*.md`, `*.sh`, `*.ts`, `*.tsx`
- [x] Confirmed `.github/workflows/claude.yml` does not reference it
- [x] `.claude-pr/CLAUDE.md` and `.claude-pr/.mcp.json` are
byte-identical to the root copies at the time of this PR

If this directory is actually needed by an internal tool I missed, feel
free to close — happy to be corrected.

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

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-04-24 21:53:16 +02:00
1157 changed files with 50625 additions and 36441 deletions
-22
View File
@@ -1,22 +0,0 @@
{
"mcpServers": {
"postgres": {
"type": "stdio",
"command": "bash",
"args": ["-c", "source packages/twenty-server/.env && npx -y @modelcontextprotocol/server-postgres \"$PG_DATABASE_URL\""],
"env": {}
},
"playwright": {
"type": "stdio",
"command": "npx",
"args": ["@playwright/mcp@latest", "--no-sandbox", "--headless"],
"env": {}
},
"context7": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@upstash/context7-mcp"],
"env": {}
}
}
}
-223
View File
@@ -1,223 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Twenty is an open-source CRM built with modern technologies in a monorepo structure. The codebase is organized as an Nx workspace with multiple packages.
## Key Commands
### Development
```bash
# Start development environment (frontend + backend + worker)
yarn start
# Individual package development
npx nx start twenty-front # Start frontend dev server
npx nx start twenty-server # Start backend server
npx nx run twenty-server:worker # Start background worker
```
### Testing
```bash
# Preferred: run a single test file (fast)
npx jest path/to/test.test.ts --config=packages/PROJECT/jest.config.mjs
# Run all tests for a package
npx nx test twenty-front # Frontend unit tests
npx nx test twenty-server # Backend unit tests
npx nx run twenty-server:test:integration:with-db-reset # Integration tests with DB reset
# To run an indivual test or a pattern of tests, use the following command:
cd packages/{workspace} && npx jest "pattern or filename"
# Storybook
npx nx storybook:build twenty-front
npx nx storybook:test twenty-front
# When testing the UI end to end, click on "Continue with Email" and use the prefilled credentials.
```
### Code Quality
```bash
# Linting (diff with main - fastest, always prefer this)
npx nx lint:diff-with-main twenty-front
npx nx lint:diff-with-main twenty-server
npx nx lint:diff-with-main twenty-front --configuration=fix # Auto-fix
# Linting (full project - slower, use only when needed)
npx nx lint twenty-front
npx nx lint twenty-server
# Type checking
npx nx typecheck twenty-front
npx nx typecheck twenty-server
# Format code
npx nx fmt twenty-front
npx nx fmt twenty-server
```
### Build
```bash
# Build packages (twenty-shared must be built first)
npx nx build twenty-shared
npx nx build twenty-front
npx nx build twenty-server
```
### Database Operations
```bash
# Database management
npx nx database:reset twenty-server # Reset database
npx nx run twenty-server:database:init:prod # Initialize database
npx nx run twenty-server:database:migrate:prod # Run instance commands (fast only)
# Generate an instance command (fast or slow)
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow>
```
### Database Inspection (Postgres MCP)
A read-only Postgres MCP server is configured in `.mcp.json`. Use it to:
- Inspect workspace data, metadata, and object definitions while developing
- 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 issues
This server is read-only — for write operations (reset, migrations, sync), use the CLI commands above.
### GraphQL
```bash
# Generate GraphQL types (run after schema changes)
npx nx run twenty-front:graphql:generate
npx nx run twenty-front:graphql:generate --configuration=metadata
```
## Architecture Overview
### Tech Stack
- **Frontend**: React 18, TypeScript, Jotai (state management), Linaria (styling), Vite
- **Backend**: NestJS, TypeORM, PostgreSQL, Redis, GraphQL (with GraphQL Yoga)
- **Monorepo**: Nx workspace managed with Yarn 4
### Package Structure
```
packages/
├── twenty-front/ # React frontend application
├── twenty-server/ # NestJS backend API
├── twenty-ui/ # Shared UI components library
├── twenty-shared/ # Common types and utilities
├── twenty-emails/ # Email templates with React Email
├── twenty-website/ # Next.js documentation website
├── twenty-zapier/ # Zapier integration
└── twenty-e2e-testing/ # Playwright E2E tests
```
### Key Development Principles
- **Functional components only** (no class components)
- **Named exports only** (no default exports)
- **Types over interfaces** (except when extending third-party interfaces)
- **String literals over enums** (except for GraphQL enums)
- **No 'any' type allowed** — strict TypeScript enforced
- **Event handlers preferred over useEffect** for state updates
- **Props down, events up** — unidirectional data flow
- **Composition over inheritance**
- **No abbreviations** in variable names (`user` not `u`, `fieldMetadata` not `fm`)
### Naming Conventions
- **Variables/functions**: camelCase
- **Constants**: SCREAMING_SNAKE_CASE
- **Types/Classes**: PascalCase (suffix component props with `Props`, e.g. `ButtonProps`)
- **Files/directories**: kebab-case with descriptive suffixes (`.component.tsx`, `.service.ts`, `.entity.ts`, `.dto.ts`, `.module.ts`)
- **TypeScript generics**: descriptive names (`TData` not `T`)
### File Structure
- Components under 300 lines, services under 500 lines
- Components in their own directories with tests and stories
- Use `index.ts` barrel exports for clean imports
- Import order: external libraries first, then internal (`@/`), then relative
### Comments
- Use short-form comments (`//`), not JSDoc blocks
- Explain WHY (business logic), not WHAT
- Do not comment obvious code
- Multi-line comments use multiple `//` lines, not `/** */`
### State Management
- **Jotai** for global state: atoms for primitive state, selectors for derived state, atom families for dynamic collections
- Component-specific state with React hooks (`useState`, `useReducer` for complex logic)
- GraphQL cache managed by Apollo Client
- Use functional state updates: `setState(prev => prev + 1)`
### Backend Architecture
- **NestJS modules** for feature organization
- **TypeORM** for database ORM with PostgreSQL
- **GraphQL** API with code-first approach
- **Redis** for caching and session management
- **BullMQ** for background job processing
### Database & Upgrade Commands
- **PostgreSQL** as primary database
- **Redis** for caching and sessions
- **ClickHouse** for analytics (when enabled)
- When changing entity files, generate an **instance command** (`database:migrate:generate --name <name> --type <fast|slow>`)
- **Fast** instance commands handle schema changes; **slow** ones add a `runDataMigration` step for data backfills
- **Workspace commands** iterate over all active/suspended workspaces for per-workspace upgrades
- Commands use `@RegisteredInstanceCommand` and `@RegisteredWorkspaceCommand` decorators for automatic discovery
- Include both `up` and `down` logic in instance commands
- Never delete or rewrite committed instance command `up`/`down` logic
- See `packages/twenty-server/docs/UPGRADE_COMMANDS.md` for full documentation
### Utility Helpers
Use existing helpers from `twenty-shared` instead of manual type guards:
- `isDefined()`, `isNonEmptyString()`, `isNonEmptyArray()`
## Development Workflow
IMPORTANT: Use Context7 for code generation, setup or configuration steps, or library/API documentation. Automatically use the Context7 MCP tools to resolve library IDs and get library docs without waiting for explicit requests.
### Before Making Changes
1. Always run linting (`lint:diff-with-main`) and type checking after code changes
2. Test changes with relevant test suites (prefer single-file test runs)
3. Ensure instance commands are generated for entity changes (`database:migrate:generate`)
4. Check that GraphQL schema changes are backward compatible
5. Run `graphql:generate` after any GraphQL schema changes
### Code Style Notes
- Use **Linaria** for styling with zero-runtime CSS-in-JS (styled-components pattern)
- Follow **Nx** workspace conventions for imports
- Use **Lingui** for internationalization
- Apply security first, then formatting (sanitize before format)
### Testing Strategy
- **Test behavior, not implementation** — focus on user perspective
- **Test pyramid**: 70% unit, 20% integration, 10% E2E
- Query by user-visible elements (text, roles, labels) over test IDs
- Use `@testing-library/user-event` for realistic interactions
- Descriptive test names: "should [behavior] when [condition]"
- Clear mocks between tests with `jest.clearAllMocks()`
## Dev Environment Setup
All dev environments (Claude Code web, Cursor, local) use one script:
```bash
bash packages/twenty-utils/setup-dev-env.sh
```
This handles everything: starts Postgres + Redis (auto-detects local services vs Docker), creates databases, and copies `.env` files. Idempotent — safe to run multiple times.
- `--docker` — force Docker mode (uses `packages/twenty-docker/docker-compose.dev.yml`)
- `--down` — stop services
- `--reset` — wipe data and restart fresh
- **Skip the setup script** for tasks that only read code — architecture questions, code review, documentation, etc.
**Note:** CI workflows (GitHub Actions) manage services via Actions service containers and run setup steps individually — they don't use this script.
## Important Files
- `nx.json` - Nx workspace configuration with task definitions
- `tsconfig.base.json` - Base TypeScript configuration
- `package.json` - Root package with workspace definitions
- `.cursor/rules/` - Detailed development guidelines and best practices
+11
View File
@@ -7,6 +7,17 @@ inputs:
runs:
using: 'composite'
steps:
- name: Free disk space for install
if: runner.os == 'Linux'
shell: bash
run: |
# Default GitHub images ship large SDKs this repo does not use; removing
# them avoids ENOSPC when restoring or linking a full Yarn node_modules.
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
sudo rm -rf /opt/hostedtoolcache/CodeQL
df -h
- name: Cache primary key builder
id: globals
shell: bash
+78
View File
@@ -78,6 +78,83 @@ jobs:
tag: scope:backend
tasks: lint,typecheck
server-previous-version-upgrade-mutation-guard:
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Get changed upgrade-version-command files
id: changed-files
uses: tj-actions/changed-files@v45
with:
files: |
packages/twenty-server/src/database/commands/upgrade-version-command/**
- name: Check upgrade version commands are in current version only
if: >
steps.changed-files.outputs.any_changed == 'true' &&
!contains(github.event.pull_request.labels.*.name, 'ci:allow-previous-version-upgrade-mutation')
run: |
VERSION_CONSTANT_FILE="packages/twenty-server/src/engine/core-modules/upgrade/constants/twenty-current-version.constant.ts"
CURRENT_VERSION=$(sed -n "s/.*TWENTY_CURRENT_VERSION = '\([0-9.]*\)'.*/\1/p" "$VERSION_CONSTANT_FILE")
if [ -z "$CURRENT_VERSION" ]; then
echo "::error::Could not extract TWENTY_CURRENT_VERSION from $VERSION_CONSTANT_FILE"
exit 1
fi
CURRENT_DIR=$(echo "$CURRENT_VERSION" | sed -E 's/^([0-9]+)\.([0-9]+)\..*/\1-\2/')
echo "Current version: $CURRENT_VERSION (directory: $CURRENT_DIR)"
ADDED_OFFENDERS=""
MODIFIED_OFFENDERS=""
check_files() {
local category="$1"
shift
for file in "$@"; do
VERSION_DIR=$(echo "$file" | sed -n 's|.*upgrade-version-command/\([0-9]*-[0-9]*\)/.*|\1|p')
if [ -n "$VERSION_DIR" ] && [ "$VERSION_DIR" != "$CURRENT_DIR" ]; then
if [ "$category" = "added" ]; then
ADDED_OFFENDERS="$ADDED_OFFENDERS\n - $file (version directory: $VERSION_DIR)"
else
MODIFIED_OFFENDERS="$MODIFIED_OFFENDERS\n - $file (version directory: $VERSION_DIR)"
fi
fi
done
}
check_files "added" ${{ steps.changed-files.outputs.added_files }}
check_files "modified" ${{ steps.changed-files.outputs.modified_files }}
if [ -n "$ADDED_OFFENDERS" ] || [ -n "$MODIFIED_OFFENDERS" ]; then
echo "This PR touches upgrade command files outside the current version directory ($CURRENT_DIR / $CURRENT_VERSION)."
if [ -n "$ADDED_OFFENDERS" ]; then
echo ""
echo "New files added to non-current version directories:"
echo -e "$ADDED_OFFENDERS"
fi
if [ -n "$MODIFIED_OFFENDERS" ]; then
echo ""
echo "Existing files modified in non-current version directories:"
echo -e "$MODIFIED_OFFENDERS"
fi
echo ""
echo "If this is intentional, add the label 'ci:allow-previous-version-upgrade-mutation' to this PR and re-run CI."
echo "Otherwise, please move your changes to the current version directory ($CURRENT_DIR)."
echo "::error::Upgrade commands were added or modified in non-current version directories."
exit 1
fi
server-validation:
needs: server-build
timeout-minutes: 30
@@ -311,6 +388,7 @@ jobs:
changed-files-check,
server-build,
server-lint-typecheck,
server-previous-version-upgrade-mutation-guard,
server-validation,
server-test,
server-integration-test,
+2
View File
@@ -58,6 +58,7 @@ jobs:
npx nx run twenty-server:lingui:compile --strict
npx nx run twenty-emails:lingui:compile --strict
npx nx run twenty-front:lingui:compile --strict
npx nx run twenty-website-new:lingui:compile --strict
continue-on-error: true
- name: Stash any changes before pulling translations
@@ -115,6 +116,7 @@ jobs:
npx nx run twenty-server:lingui:compile
npx nx run twenty-emails:lingui:compile
npx nx run twenty-front:lingui:compile
npx nx run twenty-website-new:lingui:compile
git status
git add .
if ! git diff --staged --quiet --exit-code; then
+2
View File
@@ -41,6 +41,7 @@ jobs:
npx nx run twenty-server:lingui:extract
npx nx run twenty-emails:lingui:extract
npx nx run twenty-front:lingui:extract
npx nx run twenty-website-new:lingui:extract
- name: Check and commit extracted files
id: check_extract_changes
@@ -60,6 +61,7 @@ jobs:
npx nx run twenty-server:lingui:compile
npx nx run twenty-emails:lingui:compile
npx nx run twenty-front:lingui:compile
npx nx run twenty-website-new:lingui:compile
- name: Check and commit compiled files
id: check_compile_changes
+1 -155
View File
@@ -1,172 +1,18 @@
{
"private": true,
"dependencies": {
"@apollo/client": "^4.0.0",
"@floating-ui/react": "^0.24.3",
"@linaria/core": "^6.2.0",
"@linaria/react": "^6.2.1",
"@radix-ui/colors": "^3.0.0",
"@sniptt/guards": "^0.2.0",
"@tabler/icons-react": "^3.31.0",
"@wyw-in-js/babel-preset": "^1.0.6",
"@wyw-in-js/vite": "^0.7.0",
"archiver": "^7.0.1",
"danger-plugin-todos": "^1.3.1",
"date-fns": "^2.30.0",
"date-fns-tz": "^2.0.0",
"deep-equal": "^2.2.2",
"file-type": "16.5.4",
"framer-motion": "^11.18.0",
"fuse.js": "^7.1.0",
"googleapis": "105",
"hex-rgb": "^5.0.0",
"immer": "^10.1.1",
"jotai": "^2.17.1",
"libphonenumber-js": "^1.10.26",
"lodash.camelcase": "^4.3.0",
"lodash.chunk": "^4.2.0",
"lodash.compact": "^3.0.1",
"lodash.escaperegexp": "^4.1.2",
"lodash.groupby": "^4.6.0",
"lodash.identity": "^3.0.0",
"lodash.isempty": "^4.4.0",
"lodash.isequal": "^4.5.0",
"lodash.isobject": "^3.0.2",
"lodash.kebabcase": "^4.1.1",
"lodash.mapvalues": "^4.6.0",
"lodash.merge": "^4.6.2",
"lodash.omit": "^4.5.0",
"lodash.pickby": "^4.6.0",
"lodash.snakecase": "^4.1.1",
"lodash.upperfirst": "^4.3.1",
"microdiff": "^1.3.2",
"next-with-linaria": "^1.3.0",
"planer": "^1.2.0",
"pluralize": "^8.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-responsive": "^9.0.2",
"react-router-dom": "^6.30.3",
"react-tooltip": "^5.13.1",
"remark-gfm": "^4.0.1",
"rxjs": "^7.2.0",
"semver": "^7.5.4",
"slash": "^5.1.0",
"temporal-polyfill": "^0.3.0",
"ts-key-enum": "^2.0.12",
"tslib": "^2.8.1",
"type-fest": "4.10.1",
"typescript": "5.9.2",
"uuid": "^9.0.0",
"vite-tsconfig-paths": "^4.2.1",
"xlsx-ugnis": "^0.19.3",
"zod": "^4.1.11"
},
"devDependencies": {
"@babel/core": "^7.14.5",
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.24.6",
"@chromatic-com/storybook": "^4.1.3",
"@graphql-codegen/cli": "^3.3.1",
"@graphql-codegen/client-preset": "^4.1.0",
"@graphql-codegen/typescript": "^3.0.4",
"@graphql-codegen/typescript-operations": "^3.0.4",
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
"@nx/jest": "22.5.4",
"@nx/js": "22.5.4",
"@nx/react": "22.5.4",
"@nx/storybook": "22.5.4",
"@nx/vite": "22.5.4",
"@nx/web": "22.5.4",
"@oxlint/plugins": "^1.51.0",
"@sentry/types": "^8",
"@storybook-community/storybook-addon-cookie": "^5.0.0",
"@storybook/addon-coverage": "^3.0.0",
"@storybook/addon-docs": "^10.3.3",
"@storybook/addon-links": "^10.3.3",
"@storybook/addon-vitest": "^10.3.3",
"@storybook/icons": "^2.0.1",
"@storybook/react-vite": "^10.3.3",
"@storybook/test-runner": "^0.24.2",
"@swc-node/register": "^1.11.1",
"@swc/cli": "^0.7.10",
"@swc/core": "^1.15.11",
"@swc/helpers": "~0.5.19",
"@swc/jest": "^0.2.39",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@types/addressparser": "^1.0.3",
"@types/bcrypt": "^5.0.0",
"@types/bytes": "^3.1.1",
"@types/chrome": "^0.0.267",
"@types/deep-equal": "^1.0.1",
"@types/fs-extra": "^11.0.4",
"@types/graphql-fields": "^1.3.6",
"@types/inquirer": "^9.0.9",
"@types/jest": "^30.0.0",
"@types/lodash.camelcase": "^4.3.7",
"@types/lodash.compact": "^3.0.9",
"@types/lodash.escaperegexp": "^4.1.9",
"@types/lodash.groupby": "^4.6.9",
"@types/lodash.identity": "^3.0.9",
"@types/lodash.isempty": "^4.4.7",
"@types/lodash.isequal": "^4.5.7",
"@types/lodash.isobject": "^3.0.7",
"@types/lodash.kebabcase": "^4.1.7",
"@types/lodash.mapvalues": "^4.6.9",
"@types/lodash.omit": "^4.5.9",
"@types/lodash.pickby": "^4.6.9",
"@types/lodash.snakecase": "^4.1.7",
"@types/lodash.upperfirst": "^4.3.7",
"@types/ms": "^0.7.31",
"@types/node": "^24.0.0",
"@types/passport-google-oauth20": "^2.0.11",
"@types/passport-jwt": "^3.0.8",
"@types/passport-microsoft": "^2.1.0",
"@types/pluralize": "^0.0.33",
"@types/react": "^18.2.39",
"@types/react-datepicker": "^6.2.0",
"@types/react-dom": "^18.2.15",
"@types/supertest": "^2.0.11",
"@types/uuid": "^9.0.2",
"@typescript/native-preview": "^7.0.0-dev.20260116.1",
"@vitejs/plugin-react-swc": "4.2.3",
"@vitest/browser-playwright": "^4.0.18",
"@vitest/coverage-istanbul": "^4.0.18",
"@vitest/coverage-v8": "^4.0.18",
"@yarnpkg/types": "^4.0.0",
"chromatic": "^6.18.0",
"concurrently": "^8.2.2",
"danger": "^13.0.4",
"dotenv-cli": "^7.4.4",
"esbuild": "^0.25.10",
"http-server": "^14.1.1",
"jest": "29.7.0",
"jest-environment-jsdom": "30.0.0-beta.3",
"jest-environment-node": "^29.4.1",
"jest-fetch-mock": "^3.0.3",
"jsdom": "~22.1.0",
"msw": "^2.12.7",
"msw-storybook-addon": "^2.0.6",
"nx": "22.5.4",
"prettier": "^3.1.1",
"raw-loader": "^4.0.2",
"rimraf": "^5.0.5",
"source-map-support": "^0.5.20",
"storybook": "^10.3.3",
"storybook-addon-mock-date": "2.0.0",
"storybook-addon-pseudo-states": "^10.3.3",
"supertest": "^6.1.3",
"ts-jest": "^29.1.1",
"ts-loader": "^9.2.3",
"ts-node": "10.9.1",
"tsc-alias": "^1.8.16",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.17.0",
"verdaccio": "^6.3.1",
"vite": "^7.0.0",
"vitest": "^4.0.18"
"verdaccio": "^6.3.1"
},
"engines": {
"node": "^24.5.0",
+6 -1
View File
@@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "2.0.0",
"version": "2.2.0",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
@@ -40,12 +40,17 @@
"uuid": "^13.0.0"
},
"devDependencies": {
"@swc/core": "^1.15.11",
"@swc/jest": "^0.2.39",
"@types/fs-extra": "^11.0.0",
"@types/inquirer": "^9.0.0",
"@types/jest": "^30.0.0",
"@types/lodash.camelcase": "^4.3.7",
"@types/lodash.kebabcase": "^4.1.7",
"@types/lodash.startcase": "^4",
"@types/node": "^20.0.0",
"jest": "29.7.0",
"jest-environment-node": "^29.4.1",
"twenty-shared": "workspace:*",
"typescript": "^5.9.2",
"vite": "^7.0.0",
+3
View File
@@ -26,6 +26,7 @@ const program = new Command(packageJson.name)
'--skip-local-instance',
'Skip the local Twenty instance setup prompt',
)
.option('-y, --yes', 'Auto-confirm prompts (e.g. start existing container)')
.helpOption('-h, --help', 'Display this help message.')
.action(
async (
@@ -36,6 +37,7 @@ const program = new Command(packageJson.name)
displayName?: string;
description?: string;
skipLocalInstance?: boolean;
yes?: boolean;
},
) => {
if (directory && !/^[a-z0-9-]+$/.test(directory)) {
@@ -59,6 +61,7 @@ const program = new Command(packageJson.name)
displayName: options?.displayName,
description: options?.description,
skipLocalInstance: options?.skipLocalInstance,
yes: options?.yes,
});
},
);
@@ -11,7 +11,9 @@ import * as path from 'path';
import { basename } from 'path';
import {
authLoginOAuth,
checkDockerRunning,
ConfigService,
containerExists,
detectLocalServer,
serverStart,
type ServerStartResult,
@@ -27,6 +29,7 @@ type CreateAppOptions = {
displayName?: string;
description?: string;
skipLocalInstance?: boolean;
yes?: boolean;
};
export class CreateAppCommand {
@@ -71,7 +74,7 @@ export class CreateAppCommand {
let serverResult: ServerStartResult | undefined;
if (!options.skipLocalInstance) {
const shouldStartServer = await this.shouldStartServer();
const shouldStartServer = await this.shouldStartServer(options.yes);
if (shouldStartServer) {
const startResult = await serverStart({
@@ -223,13 +226,35 @@ export class CreateAppCommand {
);
}
private async shouldStartServer(): Promise<boolean> {
private async shouldStartServer(autoConfirm?: boolean): Promise<boolean> {
const existingServerUrl = await detectLocalServer();
if (existingServerUrl) {
return true;
}
if (checkDockerRunning() && containerExists()) {
if (autoConfirm) {
return true;
}
const { startExisting } = await inquirer.prompt([
{
type: 'confirm',
name: 'startExisting',
message:
'An existing Twenty server container was found. Would you like to start it?',
default: true,
},
]);
return startExisting;
}
if (autoConfirm) {
return true;
}
const { startDocker } = await inquirer.prompt([
{
type: 'confirm',
@@ -1,6 +1,6 @@
{
"name": "github-connector",
"version": "0.1.0",
"version": "0.2.0",
"license": "MIT",
"engines": {
"node": "^24.5.0",
@@ -77,6 +77,7 @@ describe('PullRequestReview object', () => {
expect(names).toContain('firstSubmittedAt');
expect(names).toContain('lastSubmittedAt');
expect(names).toContain('eventCount');
expect(names).toContain('isSelfReview');
expect(names).toContain('reviewer');
expect(names).toContain('pullRequest');
expect(names).toContain('reviewEvents');
@@ -82,7 +82,17 @@ describe('verifyGitHubSignature', () => {
});
describe('getRawBodyForSignature', () => {
it('returns the string as-is for string body', () => {
it('prefers event.rawBody when the runtime forwarded it', () => {
const original = '{ "action": "opened", "number": 42 }';
expect(
getRawBodyForSignature({
body: { action: 'opened', number: 42 },
rawBody: original,
}),
).toBe(original);
});
it('falls back to string body when rawBody is not provided', () => {
expect(
getRawBodyForSignature({ body: '{"a":1}', isBase64Encoded: false }),
).toBe('{"a":1}');
@@ -119,3 +129,22 @@ describe('verifyGitHubSignature with parsed body', () => {
});
});
});
describe('end-to-end: server forwards rawBody, signature verifies', () => {
it('verifies a signature when rawBody is present alongside parsed body', () => {
const original = '{ "action": "opened", "number": 42 }';
const event = {
body: { action: 'opened', number: 42 },
rawBody: original,
isBase64Encoded: false,
};
const rawBody = getRawBodyForSignature(event);
const result = verifyGitHubSignature({
rawBody,
signatureHeader: sign(original),
secret: SECRET,
});
expect(result.ok).toBe(true);
});
});
@@ -7,7 +7,11 @@ export type SignatureVerificationResult =
export function getRawBodyForSignature(event: {
body: unknown;
isBase64Encoded?: boolean;
rawBody?: string;
}): string | null {
if (typeof event.rawBody === 'string') {
return event.rawBody;
}
const raw = event.body;
if (raw == null) return '';
if (typeof raw === 'string') {
@@ -298,7 +298,12 @@ const handler = async (
const res = await client.query({
pullRequestReviews: {
__args: {
filter: { reviewerId: { eq: contributorId } },
filter: {
and: [
{ reviewerId: { eq: contributorId } },
{ isSelfReview: { eq: false } },
],
},
orderBy: [{ firstSubmittedAt: 'DescNullsLast' }],
first: PAGE_SIZE,
after: cursor,
@@ -198,6 +198,7 @@ const handler = async (
const res = await client.query({
pullRequestReviews: {
__args: {
filter: { isSelfReview: { eq: false } },
orderBy: [{ firstSubmittedAt: 'DescNullsLast' }],
first: PAGE_SIZE,
after: cursor,
@@ -15,5 +15,6 @@ export async function batchUpsertConsolidatedReviews(
eventCount: true,
reviewerId: true,
pullRequestId: true,
isSelfReview: true,
}) as Promise<PullRequestReviewRow[]>;
}
@@ -23,7 +23,10 @@ type ReviewEventNode = {
reviewerId: string | null;
pullRequestId: string | null;
reviewer: { ghLogin: string | null } | null;
pullRequest: { githubNumber: number | null } | null;
pullRequest: {
githubNumber: number | null;
authorId: string | null;
} | null;
};
type GroupContext = {
@@ -31,6 +34,7 @@ type GroupContext = {
reviewerId: string | null;
prNumber: number | null;
reviewerLogin: string | null;
prAuthorId: string | null;
events: { state: ReviewEventState; submittedAt: string | null }[];
};
@@ -68,7 +72,7 @@ const handler = async (_event: RoutePayload<unknown>) => {
reviewerId: true,
pullRequestId: true,
reviewer: { ghLogin: true },
pullRequest: { githubNumber: true },
pullRequest: { githubNumber: true, authorId: true },
},
},
pageInfo: { hasNextPage: true, endCursor: true },
@@ -95,6 +99,7 @@ const handler = async (_event: RoutePayload<unknown>) => {
reviewerId: node.reviewerId,
prNumber: node.pullRequest?.githubNumber ?? null,
reviewerLogin: node.reviewer?.ghLogin ?? null,
prAuthorId: node.pullRequest?.authorId ?? null,
events: [],
};
groups.set(key, group);
@@ -105,6 +110,9 @@ const handler = async (_event: RoutePayload<unknown>) => {
if (group.prNumber === null && node.pullRequest?.githubNumber != null) {
group.prNumber = node.pullRequest.githubNumber;
}
if (group.prAuthorId === null && node.pullRequest?.authorId) {
group.prAuthorId = node.pullRequest.authorId;
}
group.events.push({
state: node.state,
submittedAt: node.submittedAt,
@@ -123,6 +131,7 @@ const handler = async (_event: RoutePayload<unknown>) => {
prNumber: group.prNumber,
reviewerLogin: group.reviewerLogin,
events: group.events,
prAuthorId: group.prAuthorId,
}),
);
}
@@ -24,6 +24,9 @@ export const REVIEW_EVENT_COUNT_FIELD_UNIVERSAL_IDENTIFIER =
export const PULL_REQUEST_REVIEW_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER =
'b4d61187-1b78-5d6e-9752-79f994ff6d55';
export const REVIEW_IS_SELF_REVIEW_FIELD_UNIVERSAL_IDENTIFIER =
'c1f3e8a2-9b5d-4e7f-8a6c-2d4b6f8e1a93';
enum ReviewState {
APPROVED = 'APPROVED',
CHANGES_REQUESTED = 'CHANGES_REQUESTED',
@@ -116,5 +119,15 @@ export default defineObject({
icon: 'IconHash',
defaultValue: 0,
},
{
universalIdentifier: REVIEW_IS_SELF_REVIEW_FIELD_UNIVERSAL_IDENTIFIER,
name: 'isSelfReview',
type: FieldType.BOOLEAN,
label: 'Self review',
description:
'True when the reviewer is the same contributor as the PR author. Self reviews are excluded from review-count aggregations (top reviewers, contributor stats, etc.) so contributors are credited only for reviews on other people\u2019s PRs.',
icon: 'IconUserCheck',
defaultValue: false,
},
],
});
@@ -8,4 +8,5 @@ export type PullRequestReviewRow = {
eventCount?: number | null;
reviewerId?: string | null;
pullRequestId?: string | null;
isSelfReview?: boolean | null;
};
@@ -12,6 +12,7 @@ export type ConsolidatedReviewUpsertInput = {
eventCount: number;
reviewerId: string | null;
pullRequestId: string;
isSelfReview: boolean;
};
export type BuildConsolidatedRowParams = {
@@ -20,8 +21,23 @@ export type BuildConsolidatedRowParams = {
prNumber: number | null;
reviewerLogin: string | null;
events: ReviewEventForConsolidation[];
/**
* Author of the PR being reviewed. When non-null and equal to `reviewerId`
* the consolidated row is flagged as a self-review so downstream
* aggregations (top reviewers, contributor stats, etc.) can exclude it.
*/
prAuthorId?: string | null;
};
export const isSelfReview = (
reviewerId: string | null,
prAuthorId: string | null | undefined,
): boolean =>
reviewerId !== null &&
prAuthorId !== null &&
prAuthorId !== undefined &&
reviewerId === prAuthorId;
export const buildReviewKey = (
pullRequestId: string,
reviewerId: string | null,
@@ -49,5 +65,6 @@ export const buildConsolidatedRow = (
eventCount: verdict.eventCount,
reviewerId: params.reviewerId,
pullRequestId: params.pullRequestId,
isSelfReview: isSelfReview(params.reviewerId, params.prAuthorId ?? null),
};
};
@@ -8,6 +8,7 @@ import {
buildConsolidatedRow,
buildReviewKey,
buildConsolidatedTitle,
isSelfReview,
} from 'src/modules/github/pull-request-review/utils/build-consolidated-row';
const evt = (
@@ -140,6 +141,71 @@ describe('buildConsolidatedRow', () => {
eventCount: 3,
reviewerId: 'rev-2',
pullRequestId: 'pr-1',
isSelfReview: false,
});
});
it('flags isSelfReview when prAuthorId equals reviewerId', () => {
const row = buildConsolidatedRow({
pullRequestId: 'pr-1',
reviewerId: 'alice',
prNumber: 7,
reviewerLogin: 'alice',
prAuthorId: 'alice',
events: [evt('COMMENTED', '2025-04-01T10:00:00Z')],
});
expect(row.isSelfReview).toBe(true);
});
it('does not flag isSelfReview when prAuthorId differs from reviewerId', () => {
const row = buildConsolidatedRow({
pullRequestId: 'pr-1',
reviewerId: 'alice',
prNumber: 7,
reviewerLogin: 'alice',
prAuthorId: 'bob',
events: [evt('APPROVED', '2025-04-01T10:00:00Z')],
});
expect(row.isSelfReview).toBe(false);
});
it('does not flag isSelfReview when prAuthorId is missing', () => {
const row = buildConsolidatedRow({
pullRequestId: 'pr-1',
reviewerId: 'alice',
prNumber: 7,
reviewerLogin: 'alice',
events: [evt('APPROVED', '2025-04-01T10:00:00Z')],
});
expect(row.isSelfReview).toBe(false);
});
it('does not flag isSelfReview when reviewerId is null (ghost reviewer)', () => {
const row = buildConsolidatedRow({
pullRequestId: 'pr-1',
reviewerId: null,
prNumber: 7,
reviewerLogin: null,
prAuthorId: null,
events: [evt('COMMENTED', '2025-04-01T10:00:00Z')],
});
expect(row.isSelfReview).toBe(false);
});
});
describe('isSelfReview', () => {
it('returns true only when both ids are present and equal', () => {
expect(isSelfReview('a', 'a')).toBe(true);
});
it('returns false when ids differ', () => {
expect(isSelfReview('a', 'b')).toBe(false);
});
it('returns false when either side is null/undefined', () => {
expect(isSelfReview(null, 'a')).toBe(false);
expect(isSelfReview('a', null)).toBe(false);
expect(isSelfReview('a', undefined)).toBe(false);
expect(isSelfReview(null, null)).toBe(false);
});
});
@@ -110,9 +110,16 @@ const handler = async (event: RoutePayload<FetchPrsPayload>) => {
reviewerId: string | null;
prNumber: number;
reviewerLogin: string | null;
prAuthorId: string | null;
events: { state: ReviewEventState; submittedAt: string | null }[];
};
const authorIdByPullRequestId = new Map<string, string | null>();
for (const pr of prData) {
const id = prIdByNumber.get(pr.githubNumber);
if (id) authorIdByPullRequestId.set(id, pr.authorId);
}
let skippedReviews = 0;
const reviewEventData: ReviewEventInput[] = [];
const groups = new Map<GroupKey, GroupContext>();
@@ -141,6 +148,7 @@ const handler = async (event: RoutePayload<FetchPrsPayload>) => {
reviewerId,
prNumber: pr.number,
reviewerLogin: review.author?.login ?? null,
prAuthorId: authorIdByPullRequestId.get(pullRequestId) ?? null,
events: [],
};
groups.set(key, group);
@@ -177,6 +185,7 @@ const handler = async (event: RoutePayload<FetchPrsPayload>) => {
prNumber: group.prNumber,
reviewerLogin: group.reviewerLogin,
events: group.events,
prAuthorId: group.prAuthorId,
}),
);
const consolidatedRecords = await timed(
@@ -8,7 +8,6 @@ export default defineApplication({
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
displayName: 'Postcard App',
description: 'Send postcards easily with Twenty',
icon: 'IconWorld',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
@@ -4,6 +4,5 @@ export default defineApplication({
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000001',
displayName: 'Root App',
description: 'An app with all entities at root level',
icon: 'IconFolder',
defaultRoleUniversalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000002',
});
@@ -5,7 +5,6 @@ export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
displayName: 'Rich App',
description: 'A simple rich app',
icon: 'IconWorld',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "twenty-client-sdk",
"version": "2.0.0",
"version": "2.2.0",
"sideEffects": false,
"license": "AGPL-3.0",
"scripts": {
@@ -46,6 +46,8 @@
"graphql": "^16.8.1"
},
"devDependencies": {
"@typescript/native-preview": "^7.0.0-dev.20260116.1",
"tsc-alias": "^1.8.16",
"twenty-shared": "workspace:*",
"typescript": "^5.9.2",
"vite": "^7.0.0",
@@ -580,6 +580,7 @@ type Application {
id: UUID!
name: String!
description: String
logo: String
version: String
universalIdentifier: String!
packageJsonChecksum: String
@@ -2202,7 +2203,6 @@ type MarketplaceApp {
id: String!
name: String!
description: String!
icon: String!
author: String!
category: String!
logo: String
@@ -2609,19 +2609,6 @@ type Skill {
updatedAt: DateTime!
}
type AgentChatThread {
id: UUID!
title: String
totalInputTokens: Int!
totalOutputTokens: Int!
contextWindowTokens: Int
conversationSize: Int!
totalInputCredits: Float!
totalOutputCredits: Float!
createdAt: DateTime!
updatedAt: DateTime!
}
type AgentMessage {
id: UUID!
threadId: UUID!
@@ -2634,6 +2621,21 @@ type AgentMessage {
createdAt: DateTime!
}
type AgentChatThread {
id: ID!
title: String
totalInputTokens: Int!
totalOutputTokens: Int!
contextWindowTokens: Int
conversationSize: Int!
totalInputCredits: Float!
totalOutputCredits: Float!
createdAt: DateTime!
updatedAt: DateTime!
deletedAt: DateTime
lastMessageAt: DateTime
}
type AiSystemPromptSection {
title: String!
content: String!
@@ -2661,22 +2663,6 @@ type AgentChatEvent {
event: JSON!
}
type AgentChatThreadEdge {
"""The node containing the AgentChatThread"""
node: AgentChatThread!
"""Cursor for this node."""
cursor: ConnectionCursor!
}
type AgentChatThreadConnection {
"""Paging information"""
pageInfo: PageInfo!
"""Array of edges."""
edges: [AgentChatThreadEdge!]!
}
type AgentTurnEvaluation {
id: UUID!
turnId: UUID!
@@ -2993,22 +2979,13 @@ type Query {
webhooks: [Webhook!]!
webhook(id: UUID!): Webhook
minimalMetadata: MinimalMetadata!
chatThreads: [AgentChatThread!]!
chatThread(id: UUID!): AgentChatThread!
chatMessages(threadId: UUID!): [AgentMessage!]!
chatStreamCatchupChunks(threadId: UUID!): ChatStreamCatchupChunks!
getAiSystemPromptPreview: AiSystemPromptPreview!
skills: [Skill!]!
skill(id: UUID!): Skill
chatThreads(
"""Limit or page results."""
paging: CursorPaging! = {first: 10}
"""Specify to filter the records returned."""
filter: AgentChatThreadFilter! = {}
"""Specify to sort results."""
sorting: [AgentChatThreadSort!]! = [{field: updatedAt, direction: DESC}]
): AgentChatThreadConnection!
agentTurns(agentId: UUID!): [AgentTurn!]!
checkUserExists(email: String!, captchaToken: String): CheckUserExist!
checkWorkspaceInviteHashIsValid(inviteHash: String!): WorkspaceInviteHashValid!
@@ -3057,56 +3034,6 @@ input AgentIdInput {
id: UUID!
}
input AgentChatThreadFilter {
and: [AgentChatThreadFilter!]
or: [AgentChatThreadFilter!]
id: UUIDFilterComparison
updatedAt: DateFieldComparison
}
input DateFieldComparison {
is: Boolean
isNot: Boolean
eq: DateTime
neq: DateTime
gt: DateTime
gte: DateTime
lt: DateTime
lte: DateTime
in: [DateTime!]
notIn: [DateTime!]
between: DateFieldComparisonBetween
notBetween: DateFieldComparisonBetween
}
input DateFieldComparisonBetween {
lower: DateTime!
upper: DateTime!
}
input AgentChatThreadSort {
field: AgentChatThreadSortFields!
direction: SortDirection!
nulls: SortNulls
}
enum AgentChatThreadSortFields {
id
updatedAt
}
"""Sort Directions"""
enum SortDirection {
ASC
DESC
}
"""Sort Nulls Options"""
enum SortNulls {
NULLS_FIRST
NULLS_LAST
}
input EventLogQueryInput {
table: EventLogTable!
filters: EventLogFiltersInput
@@ -3193,6 +3120,7 @@ type Mutation {
updateView(id: String!, input: UpdateViewInput!): View!
deleteView(id: String!): Boolean!
destroyView(id: String!): Boolean!
upsertViewWidget(input: UpsertViewWidgetInput!): View!
createViewSort(input: CreateViewSortInput!): ViewSort!
updateViewSort(input: UpdateViewSortInput!): ViewSort!
deleteViewSort(input: DeleteViewSortInput!): Boolean!
@@ -3291,6 +3219,10 @@ type Mutation {
createChatThread: AgentChatThread!
sendChatMessage(threadId: UUID!, text: String!, messageId: UUID!, browsingContext: JSON, modelId: String, fileIds: [UUID!]): SendChatMessageResult!
stopAgentChatStream(threadId: UUID!): Boolean!
renameChatThread(id: UUID!, title: String!): AgentChatThread!
archiveChatThread(id: UUID!): AgentChatThread!
unarchiveChatThread(id: UUID!): AgentChatThread!
deleteChatThread(id: UUID!): Boolean!
deleteQueuedChatMessage(messageId: UUID!): Boolean!
createSkill(input: CreateSkillInput!): Skill!
updateSkill(input: UpdateSkillInput!): Skill!
@@ -3510,6 +3442,59 @@ input UpdateViewInput {
shouldHideEmptyGroups: Boolean
}
input UpsertViewWidgetInput {
"""The id of the view widget (page layout widget)."""
widgetId: UUID!
"""The view fields to upsert."""
viewFields: [UpsertViewWidgetViewFieldInput!]
"""The view filters to upsert."""
viewFilters: [UpsertViewWidgetViewFilterInput!]
"""The view filter groups to upsert."""
viewFilterGroups: [UpsertViewWidgetViewFilterGroupInput!]
"""The view sorts to upsert."""
viewSorts: [UpsertViewWidgetViewSortInput!]
}
input UpsertViewWidgetViewFieldInput {
"""The id of an existing view field to update."""
viewFieldId: UUID
"""
The field metadata id. Used to create a new view field when viewFieldId is not provided.
"""
fieldMetadataId: UUID
isVisible: Boolean!
position: Float!
size: Float
}
input UpsertViewWidgetViewFilterInput {
id: UUID
fieldMetadataId: UUID!
operand: ViewFilterOperand = CONTAINS
value: JSON!
viewFilterGroupId: UUID
positionInViewFilterGroup: Float
subFieldName: String
}
input UpsertViewWidgetViewFilterGroupInput {
id: UUID
parentViewFilterGroupId: UUID
logicalOperator: ViewFilterGroupLogicalOperator = AND
positionInViewFilterGroup: Float
}
input UpsertViewWidgetViewSortInput {
id: UUID
fieldMetadataId: UUID!
direction: ViewSortDirection = ASC
}
input CreateViewSortInput {
id: UUID
fieldMetadataId: UUID!
@@ -414,6 +414,7 @@ export interface Application {
id: Scalars['UUID']
name: Scalars['String']
description?: Scalars['String']
logo?: Scalars['String']
version?: Scalars['String']
universalIdentifier: Scalars['String']
packageJsonChecksum?: Scalars['String']
@@ -1940,7 +1941,6 @@ export interface MarketplaceApp {
id: Scalars['String']
name: Scalars['String']
description: Scalars['String']
icon: Scalars['String']
author: Scalars['String']
category: Scalars['String']
logo?: Scalars['String']
@@ -2304,20 +2304,6 @@ export interface Skill {
__typename: 'Skill'
}
export interface AgentChatThread {
id: Scalars['UUID']
title?: Scalars['String']
totalInputTokens: Scalars['Int']
totalOutputTokens: Scalars['Int']
contextWindowTokens?: Scalars['Int']
conversationSize: Scalars['Int']
totalInputCredits: Scalars['Float']
totalOutputCredits: Scalars['Float']
createdAt: Scalars['DateTime']
updatedAt: Scalars['DateTime']
__typename: 'AgentChatThread'
}
export interface AgentMessage {
id: Scalars['UUID']
threadId: Scalars['UUID']
@@ -2331,6 +2317,22 @@ export interface AgentMessage {
__typename: 'AgentMessage'
}
export interface AgentChatThread {
id: Scalars['ID']
title?: Scalars['String']
totalInputTokens: Scalars['Int']
totalOutputTokens: Scalars['Int']
contextWindowTokens?: Scalars['Int']
conversationSize: Scalars['Int']
totalInputCredits: Scalars['Float']
totalOutputCredits: Scalars['Float']
createdAt: Scalars['DateTime']
updatedAt: Scalars['DateTime']
deletedAt?: Scalars['DateTime']
lastMessageAt?: Scalars['DateTime']
__typename: 'AgentChatThread'
}
export interface AiSystemPromptSection {
title: Scalars['String']
content: Scalars['String']
@@ -2363,22 +2365,6 @@ export interface AgentChatEvent {
__typename: 'AgentChatEvent'
}
export interface AgentChatThreadEdge {
/** The node containing the AgentChatThread */
node: AgentChatThread
/** Cursor for this node. */
cursor: Scalars['ConnectionCursor']
__typename: 'AgentChatThreadEdge'
}
export interface AgentChatThreadConnection {
/** Paging information */
pageInfo: PageInfo
/** Array of edges. */
edges: AgentChatThreadEdge[]
__typename: 'AgentChatThreadConnection'
}
export interface AgentTurnEvaluation {
id: Scalars['UUID']
turnId: Scalars['UUID']
@@ -2591,13 +2577,13 @@ export interface Query {
webhooks: Webhook[]
webhook?: Webhook
minimalMetadata: MinimalMetadata
chatThreads: AgentChatThread[]
chatThread: AgentChatThread
chatMessages: AgentMessage[]
chatStreamCatchupChunks: ChatStreamCatchupChunks
getAiSystemPromptPreview: AiSystemPromptPreview
skills: Skill[]
skill?: Skill
chatThreads: AgentChatThreadConnection
agentTurns: AgentTurn[]
checkUserExists: CheckUserExist
checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid
@@ -2633,16 +2619,6 @@ export interface Query {
__typename: 'Query'
}
export type AgentChatThreadSortFields = 'id' | 'updatedAt'
/** Sort Directions */
export type SortDirection = 'ASC' | 'DESC'
/** Sort Nulls Options */
export type SortNulls = 'NULLS_FIRST' | 'NULLS_LAST'
export type EventLogTable = 'WORKSPACE_EVENT' | 'PAGEVIEW' | 'OBJECT_EVENT' | 'USAGE_EVENT' | 'APPLICATION_LOG'
export type UsageOperationType = 'AI_CHAT_TOKEN' | 'AI_WORKFLOW_TOKEN' | 'WORKFLOW_EXECUTION' | 'CODE_EXECUTION' | 'WEB_SEARCH'
@@ -2675,6 +2651,7 @@ export interface Mutation {
updateView: View
deleteView: Scalars['Boolean']
destroyView: Scalars['Boolean']
upsertViewWidget: View
createViewSort: ViewSort
updateViewSort: ViewSort
deleteViewSort: Scalars['Boolean']
@@ -2773,6 +2750,10 @@ export interface Mutation {
createChatThread: AgentChatThread
sendChatMessage: SendChatMessageResult
stopAgentChatStream: Scalars['Boolean']
renameChatThread: AgentChatThread
archiveChatThread: AgentChatThread
unarchiveChatThread: AgentChatThread
deleteChatThread: Scalars['Boolean']
deleteQueuedChatMessage: Scalars['Boolean']
createSkill: Skill
updateSkill: Skill
@@ -3313,6 +3294,7 @@ export interface ApplicationGenqlSelection{
id?: boolean | number
name?: boolean | number
description?: boolean | number
logo?: boolean | number
version?: boolean | number
universalIdentifier?: boolean | number
packageJsonChecksum?: boolean | number
@@ -4922,7 +4904,6 @@ export interface MarketplaceAppGenqlSelection{
id?: boolean | number
name?: boolean | number
description?: boolean | number
icon?: boolean | number
author?: boolean | number
category?: boolean | number
logo?: boolean | number
@@ -5318,6 +5299,20 @@ export interface SkillGenqlSelection{
__scalar?: boolean | number
}
export interface AgentMessageGenqlSelection{
id?: boolean | number
threadId?: boolean | number
turnId?: boolean | number
agentId?: boolean | number
role?: boolean | number
status?: boolean | number
parts?: AgentMessagePartGenqlSelection
processedAt?: boolean | number
createdAt?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface AgentChatThreadGenqlSelection{
id?: boolean | number
title?: boolean | number
@@ -5329,20 +5324,8 @@ export interface AgentChatThreadGenqlSelection{
totalOutputCredits?: boolean | number
createdAt?: boolean | number
updatedAt?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface AgentMessageGenqlSelection{
id?: boolean | number
threadId?: boolean | number
turnId?: boolean | number
agentId?: boolean | number
role?: boolean | number
status?: boolean | number
parts?: AgentMessagePartGenqlSelection
processedAt?: boolean | number
createdAt?: boolean | number
deletedAt?: boolean | number
lastMessageAt?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
@@ -5384,24 +5367,6 @@ export interface AgentChatEventGenqlSelection{
__scalar?: boolean | number
}
export interface AgentChatThreadEdgeGenqlSelection{
/** The node containing the AgentChatThread */
node?: AgentChatThreadGenqlSelection
/** Cursor for this node. */
cursor?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface AgentChatThreadConnectionGenqlSelection{
/** Paging information */
pageInfo?: PageInfoGenqlSelection
/** Array of edges. */
edges?: AgentChatThreadEdgeGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
export interface AgentTurnEvaluationGenqlSelection{
id?: boolean | number
turnId?: boolean | number
@@ -5616,19 +5581,13 @@ export interface QueryGenqlSelection{
webhooks?: WebhookGenqlSelection
webhook?: (WebhookGenqlSelection & { __args: {id: Scalars['UUID']} })
minimalMetadata?: MinimalMetadataGenqlSelection
chatThreads?: AgentChatThreadGenqlSelection
chatThread?: (AgentChatThreadGenqlSelection & { __args: {id: Scalars['UUID']} })
chatMessages?: (AgentMessageGenqlSelection & { __args: {threadId: Scalars['UUID']} })
chatStreamCatchupChunks?: (ChatStreamCatchupChunksGenqlSelection & { __args: {threadId: Scalars['UUID']} })
getAiSystemPromptPreview?: AiSystemPromptPreviewGenqlSelection
skills?: SkillGenqlSelection
skill?: (SkillGenqlSelection & { __args: {id: Scalars['UUID']} })
chatThreads?: (AgentChatThreadConnectionGenqlSelection & { __args: {
/** Limit or page results. */
paging: CursorPaging,
/** Specify to filter the records returned. */
filter: AgentChatThreadFilter,
/** Specify to sort results. */
sorting: AgentChatThreadSort[]} })
agentTurns?: (AgentTurnGenqlSelection & { __args: {agentId: Scalars['UUID']} })
checkUserExists?: (CheckUserExistGenqlSelection & { __args: {email: Scalars['String'], captchaToken?: (Scalars['String'] | null)} })
checkWorkspaceInviteHashIsValid?: (WorkspaceInviteHashValidGenqlSelection & { __args: {inviteHash: Scalars['String']} })
@@ -5675,14 +5634,6 @@ export interface AgentIdInput {
/** The id of the agent. */
id: Scalars['UUID']}
export interface AgentChatThreadFilter {and?: (AgentChatThreadFilter[] | null),or?: (AgentChatThreadFilter[] | null),id?: (UUIDFilterComparison | null),updatedAt?: (DateFieldComparison | null)}
export interface DateFieldComparison {is?: (Scalars['Boolean'] | null),isNot?: (Scalars['Boolean'] | null),eq?: (Scalars['DateTime'] | null),neq?: (Scalars['DateTime'] | null),gt?: (Scalars['DateTime'] | null),gte?: (Scalars['DateTime'] | null),lt?: (Scalars['DateTime'] | null),lte?: (Scalars['DateTime'] | null),in?: (Scalars['DateTime'][] | null),notIn?: (Scalars['DateTime'][] | null),between?: (DateFieldComparisonBetween | null),notBetween?: (DateFieldComparisonBetween | null)}
export interface DateFieldComparisonBetween {lower: Scalars['DateTime'],upper: Scalars['DateTime']}
export interface AgentChatThreadSort {field: AgentChatThreadSortFields,direction: SortDirection,nulls?: (SortNulls | null)}
export interface EventLogQueryInput {table: EventLogTable,filters?: (EventLogFiltersInput | null),first?: (Scalars['Int'] | null),after?: (Scalars['String'] | null)}
export interface EventLogFiltersInput {eventType?: (Scalars['String'] | null),userWorkspaceId?: (Scalars['String'] | null),dateRange?: (EventLogDateRangeInput | null),recordId?: (Scalars['String'] | null),objectMetadataId?: (Scalars['String'] | null)}
@@ -5725,6 +5676,7 @@ export interface MutationGenqlSelection{
updateView?: (ViewGenqlSelection & { __args: {id: Scalars['String'], input: UpdateViewInput} })
deleteView?: { __args: {id: Scalars['String']} }
destroyView?: { __args: {id: Scalars['String']} }
upsertViewWidget?: (ViewGenqlSelection & { __args: {input: UpsertViewWidgetInput} })
createViewSort?: (ViewSortGenqlSelection & { __args: {input: CreateViewSortInput} })
updateViewSort?: (ViewSortGenqlSelection & { __args: {input: UpdateViewSortInput} })
deleteViewSort?: { __args: {input: DeleteViewSortInput} }
@@ -5823,6 +5775,10 @@ export interface MutationGenqlSelection{
createChatThread?: AgentChatThreadGenqlSelection
sendChatMessage?: (SendChatMessageResultGenqlSelection & { __args: {threadId: Scalars['UUID'], text: Scalars['String'], messageId: Scalars['UUID'], browsingContext?: (Scalars['JSON'] | null), modelId?: (Scalars['String'] | null), fileIds?: (Scalars['UUID'][] | null)} })
stopAgentChatStream?: { __args: {threadId: Scalars['UUID']} }
renameChatThread?: (AgentChatThreadGenqlSelection & { __args: {id: Scalars['UUID'], title: Scalars['String']} })
archiveChatThread?: (AgentChatThreadGenqlSelection & { __args: {id: Scalars['UUID']} })
unarchiveChatThread?: (AgentChatThreadGenqlSelection & { __args: {id: Scalars['UUID']} })
deleteChatThread?: { __args: {id: Scalars['UUID']} }
deleteQueuedChatMessage?: { __args: {messageId: Scalars['UUID']} }
createSkill?: (SkillGenqlSelection & { __args: {input: CreateSkillInput} })
updateSkill?: (SkillGenqlSelection & { __args: {input: UpdateSkillInput} })
@@ -5944,6 +5900,30 @@ export interface CreateViewInput {id?: (Scalars['UUID'] | null),name: Scalars['S
export interface UpdateViewInput {id?: (Scalars['UUID'] | null),name?: (Scalars['String'] | null),type?: (ViewType | null),icon?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isCompact?: (Scalars['Boolean'] | null),openRecordIn?: (ViewOpenRecordIn | null),kanbanAggregateOperation?: (AggregateOperations | null),kanbanAggregateOperationFieldMetadataId?: (Scalars['UUID'] | null),anyFieldFilterValue?: (Scalars['String'] | null),calendarLayout?: (ViewCalendarLayout | null),calendarFieldMetadataId?: (Scalars['UUID'] | null),visibility?: (ViewVisibility | null),mainGroupByFieldMetadataId?: (Scalars['UUID'] | null),shouldHideEmptyGroups?: (Scalars['Boolean'] | null)}
export interface UpsertViewWidgetInput {
/** The id of the view widget (page layout widget). */
widgetId: Scalars['UUID'],
/** The view fields to upsert. */
viewFields?: (UpsertViewWidgetViewFieldInput[] | null),
/** The view filters to upsert. */
viewFilters?: (UpsertViewWidgetViewFilterInput[] | null),
/** The view filter groups to upsert. */
viewFilterGroups?: (UpsertViewWidgetViewFilterGroupInput[] | null),
/** The view sorts to upsert. */
viewSorts?: (UpsertViewWidgetViewSortInput[] | null)}
export interface UpsertViewWidgetViewFieldInput {
/** The id of an existing view field to update. */
viewFieldId?: (Scalars['UUID'] | null),
/** The field metadata id. Used to create a new view field when viewFieldId is not provided. */
fieldMetadataId?: (Scalars['UUID'] | null),isVisible: Scalars['Boolean'],position: Scalars['Float'],size?: (Scalars['Float'] | null)}
export interface UpsertViewWidgetViewFilterInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],operand?: (ViewFilterOperand | null),value: Scalars['JSON'],viewFilterGroupId?: (Scalars['UUID'] | null),positionInViewFilterGroup?: (Scalars['Float'] | null),subFieldName?: (Scalars['String'] | null)}
export interface UpsertViewWidgetViewFilterGroupInput {id?: (Scalars['UUID'] | null),parentViewFilterGroupId?: (Scalars['UUID'] | null),logicalOperator?: (ViewFilterGroupLogicalOperator | null),positionInViewFilterGroup?: (Scalars['Float'] | null)}
export interface UpsertViewWidgetViewSortInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],direction?: (ViewSortDirection | null)}
export interface CreateViewSortInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],direction?: (ViewSortDirection | null),viewId: Scalars['UUID']}
export interface UpdateViewSortInput {
@@ -8019,14 +7999,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const AgentChatThread_possibleTypes: string[] = ['AgentChatThread']
export const isAgentChatThread = (obj?: { __typename?: any } | null): obj is AgentChatThread => {
if (!obj?.__typename) throw new Error('__typename is missing in "isAgentChatThread"')
return AgentChatThread_possibleTypes.includes(obj.__typename)
}
const AgentMessage_possibleTypes: string[] = ['AgentMessage']
export const isAgentMessage = (obj?: { __typename?: any } | null): obj is AgentMessage => {
if (!obj?.__typename) throw new Error('__typename is missing in "isAgentMessage"')
@@ -8035,6 +8007,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const AgentChatThread_possibleTypes: string[] = ['AgentChatThread']
export const isAgentChatThread = (obj?: { __typename?: any } | null): obj is AgentChatThread => {
if (!obj?.__typename) throw new Error('__typename is missing in "isAgentChatThread"')
return AgentChatThread_possibleTypes.includes(obj.__typename)
}
const AiSystemPromptSection_possibleTypes: string[] = ['AiSystemPromptSection']
export const isAiSystemPromptSection = (obj?: { __typename?: any } | null): obj is AiSystemPromptSection => {
if (!obj?.__typename) throw new Error('__typename is missing in "isAiSystemPromptSection"')
@@ -8075,22 +8055,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const AgentChatThreadEdge_possibleTypes: string[] = ['AgentChatThreadEdge']
export const isAgentChatThreadEdge = (obj?: { __typename?: any } | null): obj is AgentChatThreadEdge => {
if (!obj?.__typename) throw new Error('__typename is missing in "isAgentChatThreadEdge"')
return AgentChatThreadEdge_possibleTypes.includes(obj.__typename)
}
const AgentChatThreadConnection_possibleTypes: string[] = ['AgentChatThreadConnection']
export const isAgentChatThreadConnection = (obj?: { __typename?: any } | null): obj is AgentChatThreadConnection => {
if (!obj?.__typename) throw new Error('__typename is missing in "isAgentChatThreadConnection"')
return AgentChatThreadConnection_possibleTypes.includes(obj.__typename)
}
const AgentTurnEvaluation_possibleTypes: string[] = ['AgentTurnEvaluation']
export const isAgentTurnEvaluation = (obj?: { __typename?: any } | null): obj is AgentTurnEvaluation => {
if (!obj?.__typename) throw new Error('__typename is missing in "isAgentTurnEvaluation"')
@@ -8822,21 +8786,6 @@ export const enumAllMetadataName = {
webhook: 'webhook' as const
}
export const enumAgentChatThreadSortFields = {
id: 'id' as const,
updatedAt: 'updatedAt' as const
}
export const enumSortDirection = {
ASC: 'ASC' as const,
DESC: 'DESC' as const
}
export const enumSortNulls = {
NULLS_FIRST: 'NULLS_FIRST' as const,
NULLS_LAST: 'NULLS_LAST' as const
}
export const enumEventLogTable = {
WORKSPACE_EVENT: 'WORKSPACE_EVENT' as const,
PAGEVIEW: 'PAGEVIEW' as const,
File diff suppressed because it is too large Load Diff
@@ -4,6 +4,6 @@ set -e
echo "==> START Registering cron jobs"
cd /app/packages/twenty-server
yarn command:prod cron:register:all --dev-mode
yarn command:prod cron:register:all
echo "==> DONE"
@@ -11,10 +11,12 @@ COPY ./nx.json .
COPY ./.yarn/releases /app/.yarn/releases
COPY ./.yarn/patches /app/.yarn/patches
COPY ./packages/twenty-oxlint-rules /app/packages/twenty-oxlint-rules
COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/package.json
COPY ./packages/twenty-website-new/package.json /app/packages/twenty-website-new/package.json
RUN yarn
COPY ./packages/twenty-shared /app/packages/twenty-shared
COPY ./packages/twenty-website-new /app/packages/twenty-website-new
RUN npx nx build twenty-website-new
-7
View File
@@ -49,8 +49,6 @@ RUN yarn workspaces focus --production twenty-emails twenty-shared twenty-sdk tw
FROM common-deps AS twenty-front-build
ARG REACT_APP_SERVER_BASE_URL
COPY ./packages/twenty-front /app/packages/twenty-front
COPY ./packages/twenty-front-component-renderer /app/packages/twenty-front-component-renderer
COPY ./packages/twenty-ui /app/packages/twenty-ui
@@ -138,9 +136,6 @@ USER 1000
FROM twenty-server AS twenty
ARG REACT_APP_SERVER_BASE_URL
ENV REACT_APP_SERVER_BASE_URL=$REACT_APP_SERVER_BASE_URL
COPY --chown=1000 --from=twenty-front-build /app/packages/twenty-front/build /app/packages/twenty-server/dist/front
LABEL org.opencontainers.image.description="Twenty image with backend and frontend."
@@ -232,7 +227,6 @@ RUN mkdir -p /data/postgres /data/redis /app/packages/twenty-server/.local-stora
&& chown -R postgres:postgres /data/postgres \
&& chown 1000:1000 /data/redis /app/packages/twenty-server/.local-storage
ARG REACT_APP_SERVER_BASE_URL
ARG APP_VERSION=0.0.0
ENV S6_KEEP_ENV=1
@@ -241,7 +235,6 @@ ENV PG_DATABASE_URL=postgres://twenty:twenty@localhost:5432/default \
REDIS_URL=redis://localhost:6379 \
STORAGE_TYPE=local \
APP_SECRET=twenty-app-dev-secret-not-for-production \
REACT_APP_SERVER_BASE_URL=$REACT_APP_SERVER_BASE_URL \
APP_VERSION=$APP_VERSION \
NODE_ENV=development \
NODE_PORT=2020 \
@@ -77,7 +77,6 @@ export default defineApplication({
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
displayName: 'My Twenty App',
description: 'My first Twenty app',
icon: 'IconWorld',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
@@ -92,7 +92,7 @@ Dev mode is only available on Twenty instances running in development (`NODE_ENV
</Warning>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Dev mode terminal output" />
<img src="/images/docs/developers/extends/apps/dev.png" alt="Dev mode terminal output" />
</div>
#### One-shot sync with `yarn twenty dev --once`
@@ -127,13 +127,7 @@ Click on **My twenty app** to open its **application registration**. A registrat
Click **View installed app** to see the installed app. The **About** tab shows the current version and management options:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Installed app — About tab" />
</div>
Switch to the **Content** tab to see everything your app provides — objects, fields, logic functions, and agents:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-4.png" alt="Installed app — Content tab" />
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Installed app" />
</div>
You are all set! Edit any file in `src/` and the changes will be picked up automatically.
@@ -219,13 +213,31 @@ The scaffolder already started a local Twenty server for you. To manage it later
| `yarn twenty server start --port 3030` | Start on a custom port |
| `yarn twenty server start --test` | Start a separate test instance on port 2021 |
| `yarn twenty server stop` | Stop the server (preserves data) |
| `yarn twenty server status` | Show server status, URL, and credentials |
| `yarn twenty server status` | Show server status, URL, version, and credentials |
| `yarn twenty server logs` | Stream server logs |
| `yarn twenty server logs --lines 100` | Show the last 100 log lines |
| `yarn twenty server reset` | Delete all data and start fresh |
| `yarn twenty server upgrade` | Pull the latest `twenty-app-dev` image and recreate the container |
| `yarn twenty server upgrade 2.2.0` | Upgrade to a specific version |
Data is persisted across restarts in two Docker volumes (`twenty-app-dev-data` for PostgreSQL, `twenty-app-dev-storage` for files). Use `reset` to wipe everything and start fresh.
### Upgrading the server image
Use `yarn twenty server upgrade` to check for a newer `twenty-app-dev` Docker image and update the container. The command pulls the image, compares it against the one the container was created from, and only recreates the container if the image actually changed. Your data volumes are preserved — only the container is replaced.
```bash filename="Terminal"
# Upgrade to the latest version (skips recreation if already up to date)
yarn twenty server upgrade
# Upgrade to a specific version
yarn twenty server upgrade 2.2.0
```
If a newer image is available and the container was running, the upgrade command automatically starts a new container with the updated image. Run `yarn twenty server start` afterward to wait for it to become healthy. If the image hasn't changed, the container is left untouched.
You can verify the running version with `yarn twenty server status`, which displays the `APP_VERSION` from the container.
### Running a test instance
Pass `--test` to any `server` command to manage a second, fully isolated instance — useful for running integration tests or experimenting without touching your main dev data.
@@ -234,9 +246,10 @@ Pass `--test` to any `server` command to manage a second, fully isolated instanc
|---------|-------------|
| `yarn twenty server start --test` | Start the test instance (defaults to port 2021) |
| `yarn twenty server stop --test` | Stop the test instance |
| `yarn twenty server status --test` | Show test instance status, URL, and credentials |
| `yarn twenty server status --test` | Show test instance status, URL, version, and credentials |
| `yarn twenty server logs --test` | Stream test instance logs |
| `yarn twenty server reset --test` | Wipe test data and start fresh |
| `yarn twenty server upgrade --test` | Upgrade the test instance image |
The test instance runs in its own Docker container (`twenty-app-dev-test`) with dedicated volumes (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) and config, so it can run in parallel with your main instance without conflicts. Combine `--test` with `--port` to override the default 2021.
@@ -101,6 +101,7 @@ The `RoutePayload` type has the following structure:
| `queryStringParameters` | `Record<string, string \| undefined>` | Query string parameters (multiple values joined with commas) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }`|
| `pathParameters` | `Record<string, string \| undefined>` | Path parameters extracted from the route pattern | `/users/:id`, `/users/123` -> `{ id: '123' }` |
| `body` | `object \| null` | Parsed request body (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
| `rawBody` | `string \| undefined` | Original UTF-8 request body, before JSON parsing. Useful for verifying HMAC-style webhook signatures (e.g. GitHub's `X-Hub-Signature-256`, Stripe). `undefined` when the runtime did not preserve it. | |
| `isBase64Encoded` | `boolean` | Whether the body is base64 encoded | |
| `requestContext.http.method` | `string` | HTTP method (GET, POST, PUT, PATCH, DELETE) | |
| `requestContext.http.path` | `string` | Raw request path | |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 773 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 724 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 662 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

@@ -94,15 +94,16 @@ const handler = async (event: RoutePayload) => {
يحتوي نوع `RoutePayload` على البنية التالية:
| الخاصية | النوع | الوصف | مثال |
| ---------------------------- | ------------------------------------------------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------- |
| `headers` | `Record\<string, string \| undefined>` | رؤوس HTTP (فقط تلك المدرجة في `forwardedRequestHeaders`) | انظر القسم أدناه |
| `queryStringParameters` | `Record\<string, string \| undefined>` | معلمات سلسلة الاستعلام (تُضمّ القيم المتعددة باستخدام فواصل) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
| `pathParameters` | `Record\<string, string \| undefined>` | معلمات المسار المستخرجة من نمط المسار | `/users/:id`, `/users/123` -> `{ id: '123' }` |
| `body` | `object \| null` | جسم الطلب المُحلَّل (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
| `isBase64Encoded` | `boolean` | ما إذا كان جسم الطلب مُرمَّزًا بترميز base64 | |
| `requestContext.http.method` | `string` | طريقة HTTP (GET, POST, PUT, PATCH, DELETE) | |
| `requestContext.http.path` | `string` | المسار الخام للطلب | |
| الخاصية | النوع | الوصف | مثال |
| ---------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `headers` | `Record\<string, string \| undefined>` | رؤوس HTTP (فقط تلك المدرجة في `forwardedRequestHeaders`) | انظر القسم أدناه |
| `queryStringParameters` | `Record\<string, string \| undefined>` | معلمات سلسلة الاستعلام (تُضمّ القيم المتعددة باستخدام فواصل) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
| `pathParameters` | `Record\<string, string \| undefined>` | معلمات المسار المستخرجة من نمط المسار | `/users/:id`, `/users/123` -> `{ id: '123' }` |
| `body` | `object \| null` | جسم الطلب المُحلَّل (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
| `rawBody` | `string \| undefined` | Original UTF-8 request body, before JSON parsing. Useful for verifying HMAC-style webhook signatures (e.g. GitHub's `X-Hub-Signature-256`, Stripe). `undefined` when the runtime did not preserve it. | |
| `isBase64Encoded` | `boolean` | ما إذا كان جسم الطلب مُرمَّزًا بترميز base64 | |
| `requestContext.http.method` | `string` | طريقة HTTP (GET, POST, PUT, PATCH, DELETE) | |
| `requestContext.http.path` | `string` | المسار الخام للطلب | |
#### forwardedRequestHeaders
@@ -19,7 +19,7 @@ description: تعرّف على خطط تسعير Twenty وكيفية التبد
* دعم قياسي
<Note>
الميزات المتميزة (SSO وأذونات على مستوى الصف) غير مشمولة في خطة Pro.
Premium features (SSO, row-level permissions and AI usage data) are not included in the Pro plan.
</Note>
### المؤسسة (سحابي)
@@ -27,7 +27,7 @@ description: تعرّف على خطط تسعير Twenty وكيفية التبد
للفرق الأكبر ذات الاحتياجات المتقدّمة:
* كل ما في Pro
* **ميزات متميزة**: تكامل SSO وأذونات على مستوى الصف
* **Premium features**: SSO integration, row-level permissions and AI usage data
* دعم متميز
## خطط الاستضافة الذاتية
@@ -45,7 +45,7 @@ description: تعرّف على خطط تسعير Twenty وكيفية التبد
للفرق التي تحتاج إلى ميزات متميزة أثناء الاستضافة الذاتية:
* جميع ميزات Pro
* **ميزات متميزة**: تكامل SSO وأذونات على مستوى الصف
* **Premium features**: SSO integration, row-level permissions and AI usage data
* دعم فريق Twenty
* لا يُشترط نشر الشيفرة المخصّصة كمفتوح المصدر قبل التوزيع
@@ -55,6 +55,7 @@ description: تعرّف على خطط تسعير Twenty وكيفية التبد
* **تكامل SSO**: تسجيل دخول أحادي مع موفّر الهوية لديك
* **أذونات على مستوى الصف**: تحكّم دقيق في الوصول على مستوى السجل
* **AI usage data**: Track AI consumption across the workspace
## التبديل بين الخطط
@@ -77,3 +78,15 @@ description: تعرّف على خطط تسعير Twenty وكيفية التبد
### التبديل إلى الفوترة الشهرية
تواصل مع الدعم للعودة إلى الفوترة الشهرية.
## Obtain an Enterprise Key for Organization (Self-Hosted)
To use the Organization (Self-Hosted) plan, you need to obtain an Enterprise key:
1. Go to **Settings → Admin Panel → Enterprise**
<img src="/images/user-guide/billing/enterprise-key.png" alt="Enterprise key" />
2. Click **Get Enterprise Key**
3. When you are redirected to Stripe, enter your payment details and confirm
4. When your Enterprise key is displayed, paste it into the Enterprise settings page and activate the Organization license
@@ -16,6 +16,11 @@ description: الأسئلة الشائعة حول تسعير Twenty والفوت
لا تتوفر الميزات المتميزة إلا في خطط Organization (السحابة أو الاستضافة الذاتية):
* **تكامل SSO**: تسجيل الدخول الأحادي مع موفر الهوية لديك
* **أذونات على مستوى السجل**: تحكم دقيق في الوصول على مستوى السجل
* **بيانات استخدام الذكاء الاصطناعي**: تتبع استهلاك الذكاء الاصطناعي عبر مساحة العمل
</Accordion>
<Accordion title="هل خطة Organization على السحابة وخطة Organization ذاتية الاستضافة متماثلتان؟">
كلتاهما تقدمان الميزات المتميزة نفسها. ومع ذلك، لا يمكن استخدام اشتراك السحابة لعمليات النشر ذاتية الاستضافة. يتطلب النشر ذاتي الاستضافة مفتاح Enterprise.
</Accordion>
<Accordion title="هل تقدمون مقاعد مجانية للمستخدمين العارضين فقط؟">
@@ -53,38 +53,45 @@ People ←→ Project Assignments ←→ Projects
1. اذهب إلى **الإعدادات → نموذج البيانات**
2. انقر **+ كائن جديد**
3. سمِّه تسمية وصفية (مثلًا: "تعيين مشروع"، "عضو فريق"، "طلب منتج")
4. انقر على **حفظ**
4. فعِّل خيار "تخطي إنشاء حقل الاسم"
<img src="/images/user-guide/fields/new-pivot-object.png" alt="كائن ربط جديد" />
5. انقر على **حفظ**
<Tip>
**اتفاقية التسمية**: استخدم اسمًا يصف العلاقة، مثل "تعيين مشروع" أو "عضوية الفريق". هذا يجعل نموذج البيانات أسهل في الفهم.
</Tip>
## الخطوة 2: إنشاء علاقات من كائن الربط
## الخطوة 2: إنشاء علاقات بين الكائنات وكائن الربط
أضِف حقول علاقة من كائن الربط إلى كلا الكائنين اللذين تريد ربطهما.
أضف حقول العلاقة من كلٍ من الكائنين لديك إلى كائن الربط.
### العلاقة الأولى (كائن الربطالكائن A)
### العلاقة الأولى (الكائن A → كائن الربط)
1. حدِّد كائن الربط في **الإعدادات → نموذج البيانات**
2. انقر **+ إضافة حقل**
3. اختر **العلاقة** كنوع الحقل
4. اختر الكائن الأول (مثلًا، "الأشخاص")
5. عيِّن نوع العلاقة إلى **متعدد-إلى-واحد** (يمكن لعديد من التعيينات الارتباط بشخص واحد)
6. قم بتسمية الحقول:
* الحقل على كائن الربط: مثلًا، "شخص"
* الحقل على الأشخاص: مثلًا، "تعيينات المشروع"
7. انقر على **حفظ**
### العلاقة الثانية (كائن الربط → الكائن B)
1. وأنت ما زلت في كائن الربط، انقر **+ إضافة حقل**
2. اختر **العلاقة** كنوع الحقل
3. اختر الكائن الثاني (مثلًا، "المشاريع")
4. عيِّن نوع العلاقة إلى **متعدد-إلى-واحد**
1. حدِّد الكائن الأول لديك في **الإعدادات → نموذج البيانات**
2. انقر **+ إضافة علاقة**
3. اختر كائن الربط (مثلًا، "تعيينات المشروع")
4. عيِّن نوع العلاقة إلى **واحد-إلى-متعدد** (يمكن لشخص واحد الارتباط بالعديد من التعيينات)
5. قم بتسمية الحقول:
* الحقل على الأشخاص: مثلًا، "تعيينات المشروع"
* الحقل على كائن الربط: مثلًا، "شخص"
6. انقر على **حفظ**
### العلاقة الثانية (الكائن B → كائن الربط)
1. حدِّد الكائن الثاني لديك في **الإعدادات → نموذج البيانات**
2. انقر **+ إضافة علاقة**
3. اختر كائن الربط (مثلًا، "تعيينات المشروع")
4. عيِّن نوع العلاقة إلى **واحد-إلى-متعدد** (يمكن لمشروع واحد الارتباط بالعديد من التعيينات)
5. فعّل **"هذه علاقة بكائن ربط"**
<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}} />
6. قم بتسمية الحقول:
* الحقل على كائن الربط: مثلًا، "مشروع"
* الحقل على المشاريع: مثلًا، "أعضاء الفريق"
6. انقر على **حفظ**
7. انقر على **حفظ**
## الخطوة 3: ضبط عرض علاقة الربط
@@ -98,18 +105,6 @@ People ←→ Project Assignments ←→ Projects
6. حدِّد **العلاقة الهدف** (مثلًا، "مشروع" — الحقل على كائن الربط الذي يشير إلى الجانب الآخر)
7. انقر على **حفظ**
{/* TODO: Add image
<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}}/>
*/}
كرِّر على الكائن الآخر:
1. اختر "المشاريع" في نموذج البيانات
2. حرِّر حقل العلاقة "أعضاء الفريق"
3. فعّل مفتاح الربط
4. حدِّد "شخص" كالعلاقة الهدف
5. حفظ
## النتيجة
بعد التكوين:
@@ -130,15 +125,15 @@ People ←→ Project Assignments ←→ Projects
### إضافة علاقات
1. **تعيين مشروع → الأشخاص**
* النوع: متعدد-إلى-واحد
* الحقل على التعيين: "شخص"
1. **الأشخاص → تعيين مشروع**
* النوع: واحد-إلى-متعدد
* الحقل على الأشخاص: "تعيينات المشروع"
* الحقل على التعيين: "شخص"
2. **تعيين مشروع → المشاريع**
* النوع: متعدد-إلى-واحد
* الحقل على التعيين: "مشروع"
2. **المشاريع → تعيين مشروع**
* النوع: واحد-إلى-متعدد
* الحقل على المشاريع: "أعضاء الفريق"
* الحقل على التعيين: "مشروع"
### ضبط عرض علاقة الربط
@@ -30,3 +30,9 @@ description: خصص الشريط الجانبي الأيسر ليتوافق مع
## قائمة الأوامر
اضغط `Cmd+K` (أو `Ctrl+K`) لفتح قائمة الأوامر — شريط بحث للوصول السريع يتيح لك الانتقال إلى أي سجل أو طريقة عرض أو إجراء دون التنقل عبر الشريط الجانبي.
## تخصيص الشريط الجانبي
لتخصيص الشريط الجانبي، مرّر المؤشر فوق قسم "مساحة العمل" في الشريط الجانبي وانقر على أيقونة مفتاح الربط.
<img src="/images/user-guide/layout/navigation-edit-icon.png" alt="أيقونة تحرير التنقّل" />
@@ -3,6 +3,8 @@ title: صفحات السجل
description: خصص تخطيط صفحات تفاصيل السجلات باستخدام علامات تبويب وأدوات.
---
## نظرة عامة
عند فتح سجل في Twenty، تتكون صفحة التفاصيل من **علامات تبويب** و**أدوات**. كلاهما قابل للتخصيص بالكامل حسب نوع الكائن.
## علامات التبويب
@@ -38,12 +40,20 @@ description: خصص تخطيط صفحات تفاصيل السجلات باستخ
1. افتح أي سجل
2. اضغط على `Cmd+K` وابحث عن "تحرير تخطيط صفحة السجل"
أو
1. انتقل إلى الإعدادات > نموذج البيانات > الكائن الذي تختاره > التخطيط
2. انقر على الزر "تخصيص صفحة السجل" لذلك الكائن
3. أنت الآن في وضع التخصيص:
* **أضف أدوات** من منتقي الأدوات
* **اسحب الأدوات** لإعادة وضعها على الشبكة
* **غيّر حجم الأدوات** بسحب حوافها
* **اضبط الحقول** المعروضة داخل كل أداة
* **أدِر علامات التبويب** — أضف، أزل، أعد التسمية، وأعد الترتيب
4. احفظ تغييراتك — سيتم تطبيقها على جميع السجلات لذلك النوع من الكائنات
## ظهور الحقول
@@ -95,15 +95,16 @@ const handler = async (event: RoutePayload) => {
Typ `RoutePayload` má následující strukturu:
| Vlastnost | Typ | Popis | Příklad |
| ---------------------------- | ------------------------------------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------- |
| `headers` | `Record\<string, string \| undefined>` | Záhlaví HTTP (pouze ta uvedená v `forwardedRequestHeaders`) | viz sekci níže |
| `queryStringParameters` | `Record\<string, string \| undefined>` | Parametry query stringu (více hodnot spojených čárkami) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
| `pathParameters` | `Record\<string, string \| undefined>` | Parametry cesty extrahované ze vzoru trasy | `/users/:id`, `/users/123` -> `{ id: '123' }` |
| `body` | `object \| null` | Parsované tělo požadavku (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
| `isBase64Encoded` | `boolean` | Zda je tělo kódováno base64 | |
| `requestContext.http.method` | `string` | Metoda HTTP (GET, POST, PUT, PATCH, DELETE) | |
| `requestContext.http.path` | `string` | Nezpracovaná cesta požadavku | |
| Vlastnost | Typ | Popis | Příklad |
| ---------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `headers` | `Record\<string, string \| undefined>` | Záhlaví HTTP (pouze ta uvedená v `forwardedRequestHeaders`) | viz sekci níže |
| `queryStringParameters` | `Record\<string, string \| undefined>` | Parametry query stringu (více hodnot spojených čárkami) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
| `pathParameters` | `Record\<string, string \| undefined>` | Parametry cesty extrahované ze vzoru trasy | `/users/:id`, `/users/123` -> `{ id: '123' }` |
| `body` | `object \| null` | Parsované tělo požadavku (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
| `rawBody` | `string \| undefined` | Original UTF-8 request body, before JSON parsing. Useful for verifying HMAC-style webhook signatures (e.g. GitHub's `X-Hub-Signature-256`, Stripe). `undefined` when the runtime did not preserve it. | |
| `isBase64Encoded` | `boolean` | Zda je tělo kódováno base64 | |
| `requestContext.http.method` | `string` | Metoda HTTP (GET, POST, PUT, PATCH, DELETE) | |
| `requestContext.http.path` | `string` | Nezpracovaná cesta požadavku | |
#### forwardedRequestHeaders
@@ -19,7 +19,7 @@ Pro týmy připravené škálovat:
* Standardní podpora
<Note>
Prémiové funkce (SSO a oprávnění na úrovni řádků) nejsou součástí plánu Pro.
Premium features (SSO, row-level permissions and AI usage data) are not included in the Pro plan.
</Note>
### Organizace (Cloud)
@@ -27,7 +27,7 @@ Prémiové funkce (SSO a oprávnění na úrovni řádků) nejsou součástí pl
Pro větší týmy s pokročilými potřebami:
* Vše z plánu Pro
* **Prémiové funkce**: integrace SSO a oprávnění na úrovni řádků
* **Premium features**: SSO integration, row-level permissions and AI usage data
* Prioritní podpora
## Plány pro selfhosting
@@ -45,7 +45,7 @@ Hostujte Twenty na vlastní infrastruktuře bez poplatků:
Pro týmy, které při selfhostingu potřebují prémiové funkce:
* Všechny funkce plánu Pro
* **Prémiové funkce**: integrace SSO a oprávnění na úrovni řádků
* **Premium features**: SSO integration, row-level permissions and AI usage data
* Podpora týmu Twenty
* Není vyžadováno zveřejnit vlastní kód jako opensource před distribucí
@@ -55,6 +55,7 @@ Prémiové funkce jsou dostupné pouze v plánech Organizace (Cloud nebo self
* **Integrace SSO**: jednotné přihlášení s vaším poskytovatelem identity
* **Oprávnění na úrovni řádků**: jemně odstupňované řízení přístupu na úrovni záznamu
* **AI usage data**: Track AI consumption across the workspace
## Přepínání plánů
@@ -77,3 +78,15 @@ Chcete-li přejít na nižší plán, kontaktujte podporu.
### Přepnout na měsíční fakturaci
Chcete-li přepnout zpět na měsíční fakturaci, kontaktujte podporu.
## Obtain an Enterprise Key for Organization (Self-Hosted)
To use the Organization (Self-Hosted) plan, you need to obtain an Enterprise key:
1. Go to **Settings → Admin Panel → Enterprise**
<img src="/images/user-guide/billing/enterprise-key.png" alt="Enterprise key" />
2. Click **Get Enterprise Key**
3. When you are redirected to Stripe, enter your payment details and confirm
4. When your Enterprise key is displayed, paste it into the Enterprise settings page and activate the Organization license
@@ -16,6 +16,11 @@ Pokud chcete hostovat sami a potřebujete prémiové funkce (SSO a oprávnění
Prémiové funkce jsou dostupné pouze v plánech Organization (Cloud nebo Self-Hosted):
* **Integrace SSO**: Jednotné přihlášení s vaším poskytovatelem identity
* **Oprávnění na úrovni řádků**: Jemně granulované řízení přístupu na úrovni záznamu
* **AI usage data**: Track AI consumption across the workspace
</Accordion>
<Accordion title="Is Organization plan on cloud and Organization plan on self-hosted the same?">
They offer the same premium features. However, a cloud subscription cannot be used for self-hosted deployments. Self-hosted requires an Enterprise key.
</Accordion>
<Accordion title="Nabízíte volná místa pro uživatele pouze s přístupem ke čtení?">
@@ -53,38 +53,45 @@ Nejprve vytvořte mezilehlý objekt, který bude uchovávat propojení.
1. Přejděte do **Nastavení → Datový model**
2. Klikněte na **+ New object**
3. Pojmenujte jej výstižně (např. "Project Assignment", "Team Member", "Product Order")
4. Klikněte na **Uložit**
4. Přepněte "Přeskočit vytvoření pole Název" na zapnuto
<img src="/images/user-guide/fields/new-pivot-object.png" alt="Nový spojovací objekt" />
5. Klikněte na **Uložit**
<Tip>
**Konvence pojmenování**: Použijte název, který popisuje vztah, například "Project Assignment" nebo "Team Membership". Tím bude datový model srozumitelnější.
</Tip>
## Krok 2: Vytvořte vztahy ze spojovacího objektu
## Krok 2: Vytvořte vztahy mezi objekty a spojovacím objektem
Přidejte relační pole ze spojovacího objektu do obou objektů, které chcete propojit.
Přidejte vztahová pole z obou objektů do spojovacího objektu.
### První vztah (Junction → Objekt A)
### První vztah (Objekt A → Junction)
1. Vyberte svůj spojovací objekt v **Settings → Data Model**
2. Klikněte na **+ Add Field**
3. Zvolte **Relation** jako typ pole
4. Vyberte první objekt (např. "People")
5. Nastavte typ vztahu na **Many-to-One** (mnoho přiřazení může odkazovat na jednu osobu)
6. Pojmenujte pole:
* Pole na spojovacím objektu: např. "Person"
* Pole na People: např. "Project Assignments"
7. Klikněte na **Uložit**
### Druhý vztah (Junction → Objekt B)
1. Stále na spojovacím objektu klikněte na **+ Add Field**
2. Zvolte **Relation** jako typ pole
3. Vyberte druhý objekt (např. "Projects")
4. Nastavte typ vztahu na **Many-to-One**
1. Vyberte svůj první objekt v **Settings → Data Model**
2. Klikněte na **+ Přidat vztah**
3. Vyberte spojovací objekt (např. "Project Assignments")
4. Nastavte typ vztahu na **One-To-Many** (jedna osoba může být propojena s mnoha přiřazeními)
5. Pojmenujte pole:
* Pole na People: např. "Project Assignments"
* Pole na spojovacím objektu: např. "Person"
6. Klikněte na **Uložit**
### Druhý vztah (Objekt B → Junction)
1. Vyberte svůj druhý objekt v **Settings → Data Model**
2. Klikněte na **+ Přidat vztah**
3. Vyberte spojovací objekt (např. "Project Assignments")
4. Nastavte typ vztahu na **One-To-Many** (jeden projekt může být propojen s mnoha přiřazeními)
5. Povolte **"This is a relation to a Junction Object"**
<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}} />
6. Pojmenujte pole:
* Pole na spojovacím objektu: např. "Project"
* Pole na Projects: např. "Team Members"
6. Klikněte na **Uložit**
7. Klikněte na **Uložit**
## Krok 3: Nakonfigurujte zobrazení vztahu přes spojovací objekt
@@ -98,18 +105,6 @@ Nyní nakonfigurujte zdrojové objekty tak, aby zobrazovaly propojené záznamy
6. Vyberte **Target relation** (např. "Project" — pole na spojovacím objektu, které ukazuje na druhou stranu)
7. Klikněte na **Uložit**
{/* TODO: Add image
<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}}/>
*/}
Opakujte pro druhý objekt:
1. Vyberte "Projects" v Data Model
2. Upravte relační pole "Team Members"
3. Povolte přepínač pro vztah přes spojovací objekt
4. Vyberte "Person" jako cílový vztah
5. Uložit
## Výsledek
Po konfiguraci:
@@ -130,15 +125,15 @@ Zde je kompletní postup:
### Přidejte vztahy
1. **Project Assignment → People**
* Typ: Many-to-One
* Pole na Assignment: "Person"
1. **People → Project Assignment**
* Typ: One-to-Many
* Pole na People: "Project Assignments"
* Pole na Assignment: "Person"
2. **Project Assignment → Projects**
* Typ: Many-to-One
* Pole na Assignment: "Project"
2. **Projects → Project Assignment**
* Typ: One-to-Many
* Pole na Projects: "Team Members"
* Pole na Assignment: "Project"
### Nakonfigurujte zobrazení spojovacího objektu
@@ -30,3 +30,9 @@ Přidejte odkazy na externí nástroje přímo do postranního panelu. Užitečn
## Nabídka příkazů
Stiskněte `Cmd+K` (nebo `Ctrl+K`) pro otevření nabídky příkazů — rychlá vyhledávací lišta pro přechod na libovolný záznam, zobrazení nebo akci bez procházení postranního panelu.
## Přizpůsobení postranního panelu
Chcete-li přizpůsobit postranní panel, najeďte myší na sekci "Pracovní prostor" v postranním panelu a klikněte na ikonu klíče.
<img src="/images/user-guide/layout/navigation-edit-icon.png" alt="Ikona pro úpravu navigace" />
@@ -3,6 +3,8 @@ title: Stránky záznamů
description: Přizpůsobte rozvržení jednotlivých stránek detailu záznamu pomocí karet a widgetů.
---
## Přehled
Když v Twenty otevřete záznam, stránka detailu se skládá z **karet** a **widgetů**. Obojí lze pro každý typ objektu plně přizpůsobit.
## Karty
@@ -38,12 +40,20 @@ Widgety jsou stavební prvky uvnitř každé karty. Mezi dostupné typy widgetů
1. Otevřete libovolný záznam
2. Stiskněte `Cmd+K` a vyhledejte "Upravit rozložení stránky záznamu"
nebo
1. Přejděte do Nastavení > Datový model > objekt dle vašeho výběru > Rozložení
2. Klikněte na tlačítko "Přizpůsobit stránku záznamu" pro daný objekt
3. Nyní jste v režimu přizpůsobení:
* **Přidávejte widgety** z výběru widgetů
* **Přetahujte widgety** pro jejich přemístění v mřížce
* **Změňte velikost widgetů** tažením jejich okrajů
* **Nakonfigurujte pole** zobrazovaná v jednotlivých widgetech
* **Spravujte karty** — přidávejte, odebírejte, přejmenovávejte, měňte pořadí
4. Uložte změny — budou platit pro všechny záznamy daného typu objektu
## Viditelnost polí
@@ -77,7 +77,6 @@ export default defineApplication({
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
displayName: 'My Twenty App',
description: 'My first Twenty app',
icon: 'IconWorld',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
@@ -92,7 +92,7 @@ Der Dev-Modus ist nur auf Twenty-Instanzen verfügbar, die im Entwicklungsmodus
</Warning>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Terminalausgabe im Dev-Modus" />
<img src="/images/docs/developers/extends/apps/dev.png" alt="Terminalausgabe im Dev-Modus" />
</div>
#### Einmalige Synchronisierung mit `yarn twenty dev --once`
@@ -127,13 +127,7 @@ Klicken Sie auf **My twenty app**, um die **Anwendungsregistrierung** zu öffnen
Klicken Sie auf **View installed app**, um die installierte App anzuzeigen. Die Registerkarte **About** zeigt die aktuelle Version und Verwaltungsoptionen:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Installierte App — Registerkarte About" />
</div>
Wechseln Sie zur Registerkarte **Content**, um alles zu sehen, was Ihre App bereitstellt — Objekte, Felder, Logikfunktionen und Agenten:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-4.png" alt="Installierte App — Registerkarte Content" />
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Installierte App" />
</div>
Alles erledigt! Bearbeiten Sie eine beliebige Datei in `src/`, und die Änderungen werden automatisch übernommen.
@@ -213,30 +207,49 @@ Die Beispiele stammen aus dem Verzeichnis [twenty-apps/examples](https://github.
Der Scaffolder hat bereits einen lokalen Twenty-Server für Sie gestartet. Um ihn später zu verwalten, verwenden Sie `yarn twenty server`:
| Befehl | Beschreibung |
| -------------------------------------- | ----------------------------------------------------------- |
| `yarn twenty server start` | Lokalen Server starten (lädt das Image bei Bedarf herunter) |
| `yarn twenty server start --port 3030` | Auf einem benutzerdefinierten Port starten |
| `yarn twenty server start --test` | Starten Sie eine separate Testinstanz auf Port 2021 |
| `yarn twenty server stop` | Server stoppen (Daten bleiben erhalten) |
| `yarn twenty server status` | Serverstatus, URL und Anmeldedaten anzeigen |
| `yarn twenty server logs` | Serverprotokolle streamen |
| `yarn twenty server logs --lines 100` | Die letzten 100 Protokollzeilen anzeigen |
| `yarn twenty server reset` | Alle Daten löschen und neu starten |
| Befehl | Beschreibung |
| -------------------------------------- | -------------------------------------------------------------------------------- |
| `yarn twenty server start` | Lokalen Server starten (lädt das Image bei Bedarf herunter) |
| `yarn twenty server start --port 3030` | Auf einem benutzerdefinierten Port starten |
| `yarn twenty server start --test` | Starten Sie eine separate Testinstanz auf Port 2021 |
| `yarn twenty server stop` | Server stoppen (Daten bleiben erhalten) |
| `yarn twenty server status` | Serverstatus, URL, Version und Anmeldedaten anzeigen |
| `yarn twenty server logs` | Serverprotokolle streamen |
| `yarn twenty server logs --lines 100` | Die letzten 100 Protokollzeilen anzeigen |
| `yarn twenty server reset` | Alle Daten löschen und neu starten |
| `yarn twenty server upgrade` | Das neueste `twenty-app-dev`-Image herunterladen und den Container neu erstellen |
| `yarn twenty server upgrade 2.2.0` | Auf eine bestimmte Version aktualisieren |
Daten bleiben über Neustarts hinweg in zwei Docker-Volumes bestehen (`twenty-app-dev-data` für PostgreSQL, `twenty-app-dev-storage` für Dateien). Verwenden Sie `reset`, um alles zu löschen und neu zu beginnen.
### Aktualisieren des Server-Images
Verwenden Sie `yarn twenty server upgrade`, um nach einem neueren `twenty-app-dev`-Docker-Image zu suchen und den Container zu aktualisieren. Der Befehl lädt das Image herunter, vergleicht es mit dem, aus dem der Container erstellt wurde, und erstellt den Container nur neu, wenn sich das Image tatsächlich geändert hat. Ihre Daten-Volumes bleiben erhalten — nur der Container wird ersetzt.
```bash filename="Terminal"
# Upgrade to the latest version (skips recreation if already up to date)
yarn twenty server upgrade
# Upgrade to a specific version
yarn twenty server upgrade 2.2.0
```
Wenn ein neueres Image verfügbar ist und der Container lief, startet der Upgrade-Befehl automatisch einen neuen Container mit dem aktualisierten Image. Führen Sie anschließend `yarn twenty server start` aus, um zu warten, bis er betriebsbereit ist. Wenn sich das Image nicht geändert hat, bleibt der Container unverändert.
Sie können die laufende Version mit `yarn twenty server status` überprüfen; dieser Befehl zeigt die `APP_VERSION` des Containers an.
### Eine Testinstanz ausführen
Übergeben Sie `--test` an jeden `server`-Befehl, um eine zweite, vollständig isolierte Instanz zu verwalten — nützlich, um Integrationstests auszuführen oder zu experimentieren, ohne Ihre Hauptentwicklungsdaten anzutasten.
| Befehl | Beschreibung |
| ---------------------------------- | ----------------------------------------------------- |
| `yarn twenty server start --test` | Die Testinstanz starten (standardmäßig Port 2021) |
| `yarn twenty server stop --test` | Die Testinstanz stoppen |
| `yarn twenty server status --test` | Status, URL und Anmeldedaten der Testinstanz anzeigen |
| `yarn twenty server logs --test` | Protokolle der Testinstanz streamen |
| `yarn twenty server reset --test` | Alle Testdaten löschen und neu starten |
| Befehl | Beschreibung |
| ----------------------------------- | -------------------------------------------------------------- |
| `yarn twenty server start --test` | Die Testinstanz starten (standardmäßig Port 2021) |
| `yarn twenty server stop --test` | Die Testinstanz stoppen |
| `yarn twenty server status --test` | Status, URL, Version und Anmeldedaten der Testinstanz anzeigen |
| `yarn twenty server logs --test` | Protokolle der Testinstanz streamen |
| `yarn twenty server reset --test` | Alle Testdaten löschen und neu starten |
| `yarn twenty server upgrade --test` | Das Image der Testinstanz aktualisieren |
Die Testinstanz läuft in einem eigenen Docker-Container (`twenty-app-dev-test`) mit dedizierten Volumes (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) und eigener Konfiguration, sodass sie parallel zu Ihrer Hauptinstanz ohne Konflikte ausgeführt werden kann. Kombinieren Sie `--test` mit `--port`, um den Standardport 2021 zu überschreiben.
@@ -94,15 +94,16 @@ const handler = async (event: RoutePayload) => {
Der Typ `RoutePayload` hat die folgende Struktur:
| Eigenschaft | Typ | Beschreibung | Beispiel |
| ---------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `headers` | `Record\<string, string \| undefined>` | HTTP-Header (nur die in `forwardedRequestHeaders` aufgelisteten) | siehe Abschnitt unten |
| `queryStringParameters` | `Record\<string, string \| undefined>` | Query-String-Parameter (mehrere Werte mit Kommas verbunden) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
| `pathParameters` | `Record\<string, string \| undefined>` | Aus dem Routenmuster extrahierte Pfadparameter | `/users/:id`, `/users/123` -> `{ id: '123' }` |
| `body` | `object \| null` | Geparster Request-Body (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
| `isBase64Encoded` | `boolean` | Gibt an, ob der Body Base64-codiert ist | |
| `requestContext.http.method` | `Zeichenkette` | HTTP-Methode (GET, POST, PUT, PATCH, DELETE) | |
| `requestContext.http.path` | `Zeichenkette` | Rohpfad der Anfrage | |
| Eigenschaft | Typ | Beschreibung | Beispiel |
| ---------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `headers` | `Record\<string, string \| undefined>` | HTTP-Header (nur die in `forwardedRequestHeaders` aufgelisteten) | siehe Abschnitt unten |
| `queryStringParameters` | `Record\<string, string \| undefined>` | Query-String-Parameter (mehrere Werte mit Kommas verbunden) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
| `pathParameters` | `Record\<string, string \| undefined>` | Aus dem Routenmuster extrahierte Pfadparameter | `/users/:id`, `/users/123` -> `{ id: '123' }` |
| `body` | `object \| null` | Geparster Request-Body (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
| `rawBody` | `string \| undefined` | Ursprünglicher UTF-8-Request-Body vor dem JSON-Parsing. Nützlich zur Verifizierung von Webhook-Signaturen im HMAC-Stil (z. B. GitHubs `X-Hub-Signature-256`, Stripe). `undefined`, wenn die Laufzeitumgebung es nicht beibehalten hat. | |
| `isBase64Encoded` | `boolean` | Gibt an, ob der Body Base64-codiert ist | |
| `requestContext.http.method` | `Zeichenkette` | HTTP-Methode (GET, POST, PUT, PATCH, DELETE) | |
| `requestContext.http.path` | `Zeichenkette` | Rohpfad der Anfrage | |
#### forwardedRequestHeaders
@@ -19,7 +19,7 @@ Für Teams, die bereit sind zu skalieren:
* Standard-Support
<Note>
Premiumfunktionen (SSO und Berechtigungen auf Zeilenebene) sind im Pro-Plan nicht enthalten.
Premiumfunktionen (SSO, Berechtigungen auf Zeilenebene und KI-Nutzungsdaten) sind im Pro-Tarif nicht enthalten.
</Note>
### Organisation (Cloud)
@@ -27,7 +27,7 @@ Premiumfunktionen (SSO und Berechtigungen auf Zeilenebene) sind im Pro-Plan nich
Für größere Teams mit erweiterten Anforderungen:
* Alles aus Pro
* **Premiumfunktionen**: SSO-Integration und Berechtigungen auf Zeilenebene
* **Premiumfunktionen**: SSO-Integration, Berechtigungen auf Zeilenebene und KI-Nutzungsdaten
* Vorrangiger Support
## Selbstgehostete Pläne
@@ -45,7 +45,7 @@ Hosten Sie Twenty auf Ihrer eigenen Infrastruktur kostenlos:
Für Teams, die beim Selbsthosting Premiumfunktionen benötigen:
* Alle Pro-Funktionen
* **Premiumfunktionen**: SSO-Integration und Berechtigungen auf Zeilenebene
* **Premiumfunktionen**: SSO-Integration, Berechtigungen auf Zeilenebene und KI-Nutzungsdaten
* Support durch das Twenty-Team
* Keine Verpflichtung, benutzerdefinierten Code vor der Weitergabe als Open Source zu veröffentlichen
@@ -55,6 +55,7 @@ Premiumfunktionen sind nur in den Organisation-Plänen (Cloud oder Selbstgehoste
* **SSO-Integration**: Single Sign-On mit Ihrem Identitätsanbieter
* **Berechtigungen auf Zeilenebene**: Feingranulare Zugriffskontrolle auf Datensatzebene
* **KI-Nutzungsdaten**: Verfolgen Sie den KI-Verbrauch in Ihrem Arbeitsbereich
## Pläne wechseln
@@ -77,3 +78,15 @@ Wenden Sie sich an den Support, um Ihren Plan herabzustufen.
### Zur monatlichen Abrechnung wechseln
Wenden Sie sich an den Support, um wieder zur monatlichen Abrechnung zu wechseln.
## Enterprise-Schlüssel für Organization (Self-Hosted) abrufen
Um den Tarif Organization (Self-Hosted) zu verwenden, müssen Sie einen Enterprise-Schlüssel abrufen:
1. Gehen Sie zu **Einstellungen → Admin-Panel → Enterprise**
<img src="/images/user-guide/billing/enterprise-key.png" alt="Enterprise-Schlüssel" />
2. Klicken Sie auf **Enterprise-Schlüssel abrufen**
3. Wenn Sie zu Stripe weitergeleitet werden, geben Sie Ihre Zahlungsdaten ein und bestätigen Sie
4. Wenn Ihr Enterprise-Schlüssel angezeigt wird, fügen Sie ihn auf der Seite Enterprise-Einstellungen ein und aktivieren Sie die Organization-Lizenz
@@ -16,6 +16,11 @@ Wenn Sie selbst hosten möchten und die Premium-Funktionen (SSO und Berechtigung
Premium-Funktionen sind nur in den Organization-Tarifen (Cloud oder Self-Hosted) verfügbar:
* **SSO-Integration**: Single Sign-On mit Ihrem Identitätsanbieter
* **Berechtigungen auf Zeilenebene**: feingranulare Zugriffskontrolle auf Datensatzebene
* **KI-Nutzungsdaten**: Verfolgen Sie den KI-Verbrauch in Ihrem Arbeitsbereich
</Accordion>
<Accordion title="Sind der Organisationsplan in der Cloud und der Organisationsplan bei selbst gehosteter Bereitstellung gleich?">
Sie bieten dieselben Premium-Funktionen. Ein Cloud-Abonnement kann jedoch nicht für selbst gehostete Bereitstellungen verwendet werden. Für selbst gehostete Bereitstellungen ist ein Enterprise-Schlüssel erforderlich.
</Accordion>
<Accordion title="Bieten Sie kostenlose Plätze für Nur-Ansicht-Benutzer an?">
@@ -53,38 +53,45 @@ Erstellen Sie zunächst das Zwischenobjekt, das die Verknüpfungen speichert.
1. Gehen Sie zu **Einstellungen → Datenmodell**
2. Klicken Sie auf **+ Neues Objekt**
3. Benennen Sie es aussagekräftig (z. B. "Projektzuweisung", "Teammitglied", "Produktbestellung")
4. Klicken Sie auf **Speichern**
4. Schalten Sie "Erstellen eines Namensfelds überspringen" ein
<img src="/images/user-guide/fields/new-pivot-object.png" alt="Neues Verknüpfungsobjekt" />
5. Klicken Sie auf **Speichern**
<Tip>
**Namenskonvention**: Verwenden Sie einen Namen, der die Beziehung beschreibt, z. B. "Projektzuweisung" oder "Teammitgliedschaft". Das macht das Datenmodell leichter verständlich.
</Tip>
## Schritt 2: Beziehungen vom Verknüpfungsobjekt erstellen
## Schritt 2: Beziehungen zwischen Objekten und dem Verknüpfungsobjekt erstellen
Fügen Sie vom Verknüpfungsobjekt aus Beziehungsfelder zu beiden Objekten hinzu, die Sie verbinden möchten.
Fügen Sie für jedes Ihrer beiden Objekte Beziehungsfelder zum Verknüpfungsobjekt hinzu.
### Erste Beziehung (Verknüpfung → Objekt A)
### Erste Beziehung (Objekt A → Verknüpfung)
1. Wählen Sie Ihr Verknüpfungsobjekt unter **Einstellungen → Datenmodell** aus
2. Klicken Sie auf **+ Feld hinzufügen**
3. Wählen Sie **Relation** als Feldtyp
4. Wählen Sie das erste Objekt aus (z. B. "Personen")
5. Legen Sie den Beziehungstyp auf **Viele-zu-Eins** fest (viele Zuweisungen können mit einer Person verknüpft werden)
6. Benennen Sie die Felder:
* Feld auf der Verknüpfung: z. B. "Person"
* Feld bei Personen: z. B. "Projektzuweisungen"
7. Klicken Sie auf **Speichern**
### Zweite Beziehung (Verknüpfung → Objekt B)
1. Bleiben Sie im Verknüpfungsobjekt und klicken Sie auf **+ Feld hinzufügen**
2. Wählen Sie **Relation** als Feldtyp
3. Wählen Sie das zweite Objekt aus (z. B. "Projekte")
4. Legen Sie den Beziehungstyp auf **Viele-zu-Eins** fest
1. Wählen Sie Ihr erstes Objekt unter **Einstellungen → Datenmodell** aus
2. Klicken Sie auf **+ Beziehung hinzufügen**
3. Wählen Sie das Verknüpfungsobjekt aus (z. B. "Projektzuweisungen")
4. Legen Sie den Beziehungstyp auf **Eins-zu-Viele** fest (eine Person kann mit vielen Zuweisungen verknüpft werden)
5. Benennen Sie die Felder:
* Feld bei Personen: z. B. "Projektzuweisungen"
* Feld auf der Verknüpfung: z. B. "Person"
6. Klicken Sie auf **Speichern**
### Zweite Beziehung (Objekt B → Verknüpfung)
1. Wählen Sie Ihr zweites Objekt unter **Einstellungen → Datenmodell** aus
2. Klicken Sie auf **+ Beziehung hinzufügen**
3. Wählen Sie das Verknüpfungsobjekt aus (z. B. "Projektzuweisungen")
4. Legen Sie den Beziehungstyp auf **Eins-zu-Viele** fest (ein Projekt kann mit vielen Zuweisungen verknüpft werden)
5. Aktivieren Sie **"Dies ist eine Beziehung zu einem Verknüpfungsobjekt"**
<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}} />
6. Benennen Sie die Felder:
* Feld auf der Verknüpfung: z. B. "Projekt"
* Feld bei Projekten: z. B. "Teammitglieder"
6. Klicken Sie auf **Speichern**
7. Klicken Sie auf **Speichern**
## Schritt 3: Anzeige der Verknüpfungsbeziehung konfigurieren
@@ -98,18 +105,6 @@ Konfigurieren Sie nun die Quellobjekte so, dass verknüpfte Datensätze direkt a
6. Wählen Sie die **Zielbeziehung** aus (z. B. "Projekt" — das Feld an der Verknüpfung, das auf die andere Seite zeigt)
7. Klicken Sie auf **Speichern**
{/* TODO: Add image
<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}}/>
*/}
Wiederholen Sie dies für das andere Objekt:
1. Wählen Sie "Projekte" im Datenmodell aus
2. Bearbeiten Sie das Beziehungsfeld "Teammitglieder"
3. Aktivieren Sie den Verknüpfungsschalter
4. Wählen Sie "Person" als Zielbeziehung aus
5. Speichern
## Ergebnis
Nach der Konfiguration:
@@ -130,15 +125,15 @@ Hier ist eine vollständige Schritt-für-Schritt-Anleitung:
### Beziehungen hinzufügen
1. **Projektzuweisung → Personen**
* Typ: Viele-zu-Eins
* Feld bei Zuweisung: "Person"
1. **Personen → Projektzuweisung**
* Typ: Eins-zu-Viele
* Feld bei Personen: "Projektzuweisungen"
* Feld bei Zuweisung: "Person"
2. **Projektzuweisung → Projekte**
* Typ: Viele-zu-Eins
* Feld bei Zuweisung: "Projekt"
2. **Projekte → Projektzuweisung**
* Typ: Eins-zu-Viele
* Feld bei Projekten: "Teammitglieder"
* Feld bei Zuweisung: "Projekt"
### Verknüpfungsanzeige konfigurieren
@@ -30,3 +30,9 @@ Fügen Sie Links zu externen Tools direkt in der Seitenleiste hinzu. Nützlich,
## Befehlsmenü
Drücken Sie `Cmd+K` (oder `Ctrl+K`), um das Befehlsmenü zu öffnen — eine Suchleiste für den Schnellzugriff, mit der Sie zu jedem Datensatz, jeder Ansicht oder Aktion springen können, ohne durch die Seitenleiste zu navigieren.
## Anpassung der Seitenleiste
Um die Seitenleiste anzupassen, bewegen Sie den Mauszeiger über den Abschnitt "Arbeitsbereich" in der Seitenleiste und klicken Sie auf das Schraubenschlüssel-Symbol.
<img src="/images/user-guide/layout/navigation-edit-icon.png" alt="Symbol zum Bearbeiten der Navigation" />
@@ -3,6 +3,8 @@ title: Datensatzseiten
description: Passen Sie das Layout einzelner Datensatz-Detailseiten mit Tabs und Widgets an.
---
## Übersicht
Wenn Sie in Twenty einen Datensatz öffnen, besteht die Detailseite aus **Tabs** und **Widgets**. Beide lassen sich je Objekttyp vollständig anpassen.
## Registerkarten
@@ -38,12 +40,20 @@ Widgets sind die Bausteine in jedem Tab. Zu den verfügbaren Widget-Typen gehör
1. Öffnen Sie einen beliebigen Datensatz
2. Drücken Sie `Cmd+K` und suchen Sie nach "Layout der Datensatzseite bearbeiten"
oder
1. Gehen Sie zu Einstellungen > Datenmodell > Objekt Ihrer Wahl > Layout
2. Klicken Sie für dieses Objekt auf die Schaltfläche "Datensatzseite anpassen"
3. Sie befinden sich jetzt im Anpassungsmodus:
* **Widgets hinzufügen** aus der Widget-Auswahl
* **Widgets ziehen**, um sie im Raster neu zu positionieren
* **Widgets in der Größe ändern**, indem Sie ihre Ränder ziehen
* **Felder konfigurieren**, die in jedem Widget angezeigt werden
* **Tabs verwalten** — hinzufügen, entfernen, umbenennen, neu anordnen
4. Speichern Sie Ihre Änderungen — sie gelten für alle Datensätze dieses Objekttyps
## Feldsichtbarkeit
@@ -94,15 +94,16 @@ const handler = async (event: RoutePayload) => {
Il tipo `RoutePayload` ha la seguente struttura:
| Proprietà | Tipo | Descrizione | Esempio |
| ---------------------------- | ------------------------------------------------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `headers` | `Record\<string, string \| undefined>` | Intestazioni HTTP (solo quelle elencate in `forwardedRequestHeaders`) | vedi la sezione sotto |
| `queryStringParameters` | `Record\<string, string \| undefined>` | Parametri della query string (valori multipli uniti da virgole) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
| `pathParameters` | `Record\<string, string \| undefined>` | Parametri di percorso estratti dal pattern della route | `/users/:id`, `/users/123` -> `{ id: '123' }` |
| `body` | `object \| null` | Corpo della richiesta analizzato (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
| `isBase64Encoded` | `boolean` | Indica se il corpo è codificato in base64 | |
| `requestContext.http.method` | `string` | Metodo HTTP (GET, POST, PUT, PATCH, DELETE) | |
| `requestContext.http.path` | `string` | Percorso della richiesta non elaborato | |
| Proprietà | Tipo | Descrizione | Esempio |
| ---------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `headers` | `Record\<string, string \| undefined>` | Intestazioni HTTP (solo quelle elencate in `forwardedRequestHeaders`) | vedi la sezione sotto |
| `queryStringParameters` | `Record\<string, string \| undefined>` | Parametri della query string (valori multipli uniti da virgole) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
| `pathParameters` | `Record\<string, string \| undefined>` | Parametri di percorso estratti dal pattern della route | `/users/:id`, `/users/123` -> `{ id: '123' }` |
| `body` | `object \| null` | Corpo della richiesta analizzato (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
| `rawBody` | `string \| undefined` | Original UTF-8 request body, before JSON parsing. Useful for verifying HMAC-style webhook signatures (e.g. GitHub's `X-Hub-Signature-256`, Stripe). `undefined` when the runtime did not preserve it. | |
| `isBase64Encoded` | `boolean` | Indica se il corpo è codificato in base64 | |
| `requestContext.http.method` | `string` | Metodo HTTP (GET, POST, PUT, PATCH, DELETE) | |
| `requestContext.http.path` | `string` | Percorso della richiesta non elaborato | |
#### forwardedRequestHeaders
@@ -19,7 +19,7 @@ Per i team pronti a scalare:
* Supporto standard
<Note>
Le funzionalità premium (SSO e autorizzazioni a livello di riga) non sono incluse nel piano Pro.
Premium features (SSO, row-level permissions and AI usage data) are not included in the Pro plan.
</Note>
### Organizzazione (Cloud)
@@ -27,7 +27,7 @@ Le funzionalità premium (SSO e autorizzazioni a livello di riga) non sono inclu
Per i team più numerosi con esigenze avanzate:
* Tutto ciò che è incluso in Pro
* **Funzionalità premium**: integrazione SSO e autorizzazioni a livello di riga
* **Premium features**: SSO integration, row-level permissions and AI usage data
* Supporto prioritario
## Piani selfhosted
@@ -45,7 +45,7 @@ Esegui Twenty sulla tua infrastruttura senza costi:
Per i team che necessitano di funzionalità premium con il selfhosting:
* Tutte le funzionalità di Pro
* **Funzionalità premium**: integrazione SSO e autorizzazioni a livello di riga
* **Premium features**: SSO integration, row-level permissions and AI usage data
* Supporto del team di Twenty
* Nessun requisito di pubblicare il codice personalizzato come open source prima della distribuzione
@@ -55,6 +55,7 @@ Le funzionalità premium sono disponibili solo nei piani Organizzazione (Cloud o
* **Integrazione SSO**: Single SignOn con il tuo provider di identità
* **Autorizzazioni a livello di riga**: controllo degli accessi granulare a livello di record
* **AI usage data**: Track AI consumption across the workspace
## Cambio piano
@@ -77,3 +78,15 @@ Contatta il supporto per eseguire il downgrade del tuo piano.
### Passa alla fatturazione mensile
Contatta il supporto per tornare alla fatturazione mensile.
## Obtain an Enterprise Key for Organization (Self-Hosted)
To use the Organization (Self-Hosted) plan, you need to obtain an Enterprise key:
1. Go to **Settings → Admin Panel → Enterprise**
<img src="/images/user-guide/billing/enterprise-key.png" alt="Enterprise key" />
2. Click **Get Enterprise Key**
3. When you are redirected to Stripe, enter your payment details and confirm
4. When your Enterprise key is displayed, paste it into the Enterprise settings page and activate the Organization license
@@ -16,6 +16,11 @@ Se vuoi effettuare il self-hosting e hai bisogno delle funzionalità Premium (SS
Le funzionalità Premium sono disponibili solo nei piani Organization (Cloud o Self-Hosted):
* **Integrazione SSO**: Single Sign-On con il tuo provider di identità
* **Autorizzazioni a livello di riga**: controllo degli accessi granulare a livello di record
* **Dati di utilizzo dell'IA**: Tieni traccia del consumo dell'IA nel tuo spazio di lavoro
</Accordion>
<Accordion title="Il piano Organization su cloud e il piano Organization in modalità self-hosted sono la stessa cosa?">
Offrono le stesse funzionalità Premium. Tuttavia, un abbonamento cloud non può essere utilizzato per distribuzioni self-hosted. La modalità self-hosted richiede una chiave Enterprise.
</Accordion>
<Accordion title="Offrite posti gratuiti per utenti solo visualizzazione?">
@@ -53,38 +53,45 @@ Per prima cosa, crea l'oggetto intermedio che conterrà i collegamenti.
1. Vai a **Impostazioni → Modello dati**
2. Fai clic su **+ Nuovo oggetto**
3. Assegnagli un nome descrittivo (ad es., "Assegnazione al progetto", "Membro del team", "Ordine del prodotto")
4. Clicca su **Salva**
4. Attiva l'opzione "Salta la creazione di un campo Nome"
<img src="/images/user-guide/fields/new-pivot-object.png" alt="Nuovo oggetto pivot" />
5. Clicca su **Salva**
<Tip>
**Convenzione di denominazione**: Usa un nome che descriva la relazione, come "Assegnazione al progetto" o "Appartenenza al team". Questo rende il modello di dati più facile da comprendere.
</Tip>
## Passaggio 2: Crea le relazioni dall'oggetto di giunzione
## Passaggio 2: Crea relazioni tra gli oggetti e l'oggetto di giunzione
Aggiungi campi di relazione dall'oggetto di giunzione a entrambi gli oggetti che desideri collegare.
Aggiungi campi di relazione da ciascuno dei due oggetti all'oggetto di giunzione.
### Prima relazione (Giunzione → Oggetto A)
### Prima relazione (Oggetto A → Oggetto di giunzione)
1. Seleziona il tuo oggetto di giunzione in **Impostazioni → Modello dati**
2. Fai clic su **+ Aggiungi campo**
3. Scegli **Relazione** come tipo di campo
4. Seleziona il primo oggetto (ad es., "Persone")
5. Imposta il tipo di relazione su **Molti-a-uno** (molte assegnazioni possono collegarsi a una persona)
6. Assegna un nome ai campi:
* Campo sull'oggetto di giunzione: ad es., "Persona"
* Campo su Persone: ad es., "Assegnazioni ai progetti"
7. Clicca su **Salva**
### Seconda relazione (Giunzione → Oggetto B)
1. Sempre sull'oggetto di giunzione, fai clic su **+ Aggiungi campo**
2. Scegli **Relazione** come tipo di campo
3. Seleziona il secondo oggetto (ad es., "Progetti")
4. Imposta il tipo di relazione su **Molti-a-uno**
1. Seleziona il tuo primo oggetto in **Impostazioni → Modello dati**
2. Fai clic su **+ Aggiungi relazione**
3. Seleziona l'oggetto di giunzione (ad es., "Assegnazioni al progetto")
4. Imposta il tipo di relazione su **Uno-a-molti** (una persona può collegarsi a molte assegnazioni)
5. Assegna un nome ai campi:
* Campo su Persone: ad es., "Assegnazioni ai progetti"
* Campo sull'oggetto di giunzione: ad es., "Persona"
6. Clicca su **Salva**
### Seconda relazione (Oggetto B → Oggetto di giunzione)
1. Seleziona il tuo secondo oggetto in **Impostazioni → Modello dati**
2. Fai clic su **+ Aggiungi relazione**
3. Seleziona l'oggetto di giunzione (ad es., "Assegnazioni al progetto")
4. Imposta il tipo di relazione su **Uno-a-molti** (un progetto può collegarsi a molte assegnazioni)
5. Abilita **"Questa è una relazione con un oggetto di giunzione"**
<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}} />
6. Assegna un nome ai campi:
* Campo sull'oggetto di giunzione: ad es., "Progetto"
* Campo su Progetti: ad es., "Membri del team"
6. Clicca su **Salva**
7. Clicca su **Salva**
## Passaggio 3: Configura la visualizzazione della relazione di giunzione
@@ -98,18 +105,6 @@ Ora configura gli oggetti sorgente per visualizzare direttamente i record colleg
6. Seleziona la **Relazione di destinazione** (ad es., "Progetto" — il campo sull'oggetto di giunzione che punta all'altro lato)
7. Clicca su **Salva**
{/* TODO: Add image
<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}}/>
*/}
Ripeti per l'altro oggetto:
1. Seleziona "Progetti" in Modello dati
2. Modifica il campo di relazione "Membri del team"
3. Abilita l'interruttore di giunzione
4. Seleziona "Persona" come relazione di destinazione
5. Salva
## Risultato
Dopo la configurazione:
@@ -130,15 +125,15 @@ Ecco una procedura completa:
### Aggiungi relazioni
1. **Assegnazione al progetto → Persone**
* Tipo: Molti-a-uno
* Campo su Assegnazione: "Persona"
1. **Persone → Assegnazione al progetto**
* Tipo: Uno-a-molti
* Campo su Persone: "Assegnazioni ai progetti"
* Campo su Assegnazione: "Persona"
2. **Assegnazione al progetto → Progetti**
* Tipo: Molti-a-uno
* Campo su Assegnazione: "Progetto"
2. **Progetti → Assegnazione al progetto**
* Tipo: Uno-a-molti
* Campo su Progetti: "Membri del team"
* Campo su Assegnazione: "Progetto"
### Configura la visualizzazione della giunzione
@@ -30,3 +30,9 @@ Aggiungi collegamenti a strumenti esterni direttamente nella barra laterale. Uti
## Menu Comandi
Premi `Cmd+K` (o `Ctrl+K`) per aprire il menu comandi — una barra di ricerca ad accesso rapido per passare a qualsiasi record, vista o azione senza usare la barra laterale.
## Personalizzare la barra laterale
Per personalizzare la barra laterale, passa il mouse sulla sezione "Workspace" nella barra laterale e fai clic sull'icona a forma di chiave inglese.
<img src="/images/user-guide/layout/navigation-edit-icon.png" alt="Icona di modifica della navigazione" />
@@ -3,6 +3,8 @@ title: Pagine dei record
description: Personalizza il layout delle singole pagine di dettaglio dei record con schede e widget.
---
## Panoramica
Quando apri un record in Twenty, la pagina di dettaglio è composta da **schede** e **widget**. Entrambi sono completamente personalizzabili per ciascun tipo di oggetto.
## Schede
@@ -38,12 +40,20 @@ I widget sono gli elementi costitutivi all'interno di ogni scheda. Tipi di widge
1. Apri un record qualsiasi
2. Premi `Cmd+K` e cerca "Modifica il layout della pagina del record"
o
1. Vai a Impostazioni > Modello dati > oggetto di tua scelta > Layout
2. Fai clic sul pulsante "Personalizza pagina del record" per quell'oggetto
3. Ora sei in modalità di personalizzazione:
* **Aggiungi widget** dal selettore di widget
* **Trascina i widget** per riposizionarli sulla griglia
* **Ridimensiona i widget** trascinando i bordi
* **Configura i campi** mostrati all'interno di ciascun widget
* **Gestisci le schede** — aggiungi, rimuovi, rinomina, riordina
4. Salva le modifiche — si applicano a tutti i record di quel tipo di oggetto
## Visibilità dei campi
@@ -77,7 +77,6 @@ export default defineApplication({
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
displayName: 'My Twenty App',
description: 'My first Twenty app',
icon: 'IconWorld',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
@@ -92,7 +92,7 @@ O modo de desenvolvimento só está disponível em instâncias do Twenty em modo
</Warning>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Saída do terminal no modo de desenvolvimento" />
<img src="/images/docs/developers/extends/apps/dev.png" alt="Saída do terminal no modo de desenvolvimento" />
</div>
#### Sincronização única com `yarn twenty dev --once`
@@ -127,13 +127,7 @@ Clique em **My twenty app** para abrir o seu **registro do aplicativo**. Um regi
Clique em **View installed app** para ver o aplicativo instalado. A aba **About** mostra a versão atual e as opções de gerenciamento:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Aplicativo instalado — aba About" />
</div>
Altere para a aba **Content** para ver tudo o que seu aplicativo oferece — objetos, campos, funções de lógica e agentes:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-4.png" alt="Aplicativo instalado — aba Content" />
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Aplicação instalada" />
</div>
Tudo pronto! Edite qualquer arquivo em `src/` e as alterações serão detectadas automaticamente.
@@ -213,30 +207,49 @@ Os exemplos são obtidos do diretório [twenty-apps/examples](https://github.com
A ferramenta de scaffolding já iniciou um servidor local do Twenty para você. Para gerenciá-lo depois, use `yarn twenty server`:
| Comando | Descrição |
| -------------------------------------- | ------------------------------------------------------ |
| `yarn twenty server start` | Inicia o servidor local (baixa a imagem se necessário) |
| `yarn twenty server start --port 3030` | Iniciar em uma porta personalizada |
| `yarn twenty server start --test` | Inicie uma instância de teste separada na porta 2021 |
| `yarn twenty server stop` | Interrompe o servidor (preserva os dados) |
| `yarn twenty server status` | Mostra o status do servidor, a URL e as credenciais |
| `yarn twenty server logs` | Transmite os logs do servidor |
| `yarn twenty server logs --lines 100` | Mostra as últimas 100 linhas de log |
| `yarn twenty server reset` | Exclui todos os dados e inicia do zero |
| Comando | Descrição |
| -------------------------------------- | ----------------------------------------------------------------- |
| `yarn twenty server start` | Inicia o servidor local (baixa a imagem se necessário) |
| `yarn twenty server start --port 3030` | Iniciar em uma porta personalizada |
| `yarn twenty server start --test` | Inicie uma instância de teste separada na porta 2021 |
| `yarn twenty server stop` | Interrompe o servidor (preserva os dados) |
| `yarn twenty server status` | Mostra o status do servidor, a URL, a versão e as credenciais |
| `yarn twenty server logs` | Transmite os logs do servidor |
| `yarn twenty server logs --lines 100` | Mostra as últimas 100 linhas de log |
| `yarn twenty server reset` | Exclui todos os dados e inicia do zero |
| `yarn twenty server upgrade` | Baixe a imagem mais recente `twenty-app-dev` e recrie o contêiner |
| `yarn twenty server upgrade 2.2.0` | Atualizar para uma versão específica |
Os dados são persistidos entre reinicializações em dois volumes do Docker (`twenty-app-dev-data` para PostgreSQL, `twenty-app-dev-storage` para arquivos). Use `reset` para apagar tudo e começar do zero.
### Atualizando a imagem do servidor
Use `yarn twenty server upgrade` para verificar se há uma imagem do Docker `twenty-app-dev` mais recente e atualizar o container. O comando baixa a imagem, a compara com aquela a partir da qual o container foi criado e só recria o container se a imagem realmente tiver sido alterada. Seus volumes de dados são preservados — apenas o container é substituído.
```bash filename="Terminal"
# Upgrade to the latest version (skips recreation if already up to date)
yarn twenty server upgrade
# Upgrade to a specific version
yarn twenty server upgrade 2.2.0
```
Se uma imagem mais recente estiver disponível e o container estiver em execução, o comando de atualização iniciará automaticamente um novo container com a imagem atualizada. Em seguida, execute `yarn twenty server start` para aguardar até que ele fique saudável. Se a imagem não tiver mudado, o container permanece inalterado.
Você pode verificar a versão em execução com `yarn twenty server status`, que exibe o `APP_VERSION` do container.
### Executando uma instância de teste
Passe `--test` para qualquer comando de `server` para gerenciar uma segunda instância totalmente isolada — útil para executar testes de integração ou experimentar sem tocar nos seus dados principais de desenvolvimento.
| Comando | Descrição |
| ---------------------------------- | ------------------------------------------------------------- |
| `yarn twenty server start --test` | Inicia a instância de teste (padrão: porta 2021) |
| `yarn twenty server stop --test` | Interrompe a instância de teste |
| `yarn twenty server status --test` | Mostra o status da instância de teste, a URL e as credenciais |
| `yarn twenty server logs --test` | Transmite os logs da instância de teste |
| `yarn twenty server reset --test` | Exclui os dados de teste e inicia do zero |
| Comando | Descrição |
| ----------------------------------- | ----------------------------------------------------------------------- |
| `yarn twenty server start --test` | Inicia a instância de teste (padrão: porta 2021) |
| `yarn twenty server stop --test` | Interrompe a instância de teste |
| `yarn twenty server status --test` | Mostra o status da instância de teste, a URL, a versão e as credenciais |
| `yarn twenty server logs --test` | Transmite os logs da instância de teste |
| `yarn twenty server reset --test` | Exclui os dados de teste e inicia do zero |
| `yarn twenty server upgrade --test` | Atualiza a imagem da instância de teste |
A instância de teste é executada em seu próprio contêiner Docker (`twenty-app-dev-test`) com volumes dedicados (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) e configuração própria, para que possa ser executada em paralelo com sua instância principal sem conflitos. Combine `--test` com `--port` para substituir a porta padrão 2021.
@@ -94,15 +94,16 @@ const handler = async (event: RoutePayload) => {
O tipo `RoutePayload` tem a seguinte estrutura:
| Propriedade | Tipo | Descrição | Exemplo |
| ---------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `headers` | `Record\<string, string \| undefined>` | Cabeçalhos HTTP (apenas aqueles listados em `forwardedRequestHeaders`) | veja a seção abaixo |
| `queryStringParameters` | `Record\<string, string \| undefined>` | Parâmetros de query string (valores múltiplos unidos por vírgulas) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
| `pathParameters` | `Record\<string, string \| undefined>` | Parâmetros de caminho extraídos do padrão de rota | `/users/:id`, `/users/123` -> `{ id: '123' }` |
| `body` | `object \| null` | Corpo da requisição analisado (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
| `isBase64Encoded` | `boolean` | Se o corpo está codificado em base64 | |
| `requestContext.http.method` | `string` | Método HTTP (GET, POST, PUT, PATCH, DELETE) | |
| `requestContext.http.path` | `string` | Caminho bruto da requisição | |
| Propriedade | Tipo | Descrição | Exemplo |
| ---------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `headers` | `Record\<string, string \| undefined>` | Cabeçalhos HTTP (apenas aqueles listados em `forwardedRequestHeaders`) | veja a seção abaixo |
| `queryStringParameters` | `Record\<string, string \| undefined>` | Parâmetros de query string (valores múltiplos unidos por vírgulas) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
| `pathParameters` | `Record\<string, string \| undefined>` | Parâmetros de caminho extraídos do padrão de rota | `/users/:id`, `/users/123` -> `{ id: '123' }` |
| `body` | `object \| null` | Corpo da requisição analisado (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
| `rawBody` | `string \| undefined` | Corpo da solicitação UTF-8 original, antes da análise de JSON. Útil para verificar assinaturas de webhook no estilo HMAC (por exemplo, `X-Hub-Signature-256` do GitHub, Stripe). `undefined` quando o ambiente de execução não o preservou. | |
| `isBase64Encoded` | `boolean` | Se o corpo está codificado em base64 | |
| `requestContext.http.method` | `string` | Método HTTP (GET, POST, PUT, PATCH, DELETE) | |
| `requestContext.http.path` | `string` | Caminho bruto da requisição | |
#### forwardedRequestHeaders
@@ -19,7 +19,7 @@ Para equipes prontas para escalar:
* Suporte padrão
<Note>
Recursos premium (SSO e permissões em nível de linha) não estão incluídos no plano Pro.
Os recursos Premium (SSO, permissões em nível de linha e dados de uso de IA) não estão incluídos no plano Pro.
</Note>
### Organização (Nuvem)
@@ -27,7 +27,7 @@ Recursos premium (SSO e permissões em nível de linha) não estão incluídos n
Para equipes maiores com necessidades avançadas:
* Tudo do Pro
* **Recursos premium**: integração SSO e permissões em nível de linha
* **Recursos Premium**: integração com SSO, permissões em nível de linha e dados de uso de IA
* Suporte prioritário
## Planos auto-hospedados
@@ -45,7 +45,7 @@ Hospede o Twenty na sua própria infraestrutura sem custo:
Para equipes que precisam de recursos premium enquanto se auto-hospedam:
* Todos os recursos do Pro
* **Recursos premium**: integração SSO e permissões em nível de linha
* **Recursos Premium**: integração com SSO, permissões em nível de linha e dados de uso de IA
* Suporte da equipe Twenty
* Sem necessidade de publicar código personalizado como código aberto antes de distribuir
@@ -55,6 +55,7 @@ Os recursos premium estão disponíveis apenas nos planos Organização (Nuvem o
* **Integração SSO**: Single Sign-On com seu provedor de identidade
* **Permissões em nível de linha**: controle de acesso granular no nível de registro
* **Dados de uso de IA**: Acompanhe o consumo de IA em todo o espaço de trabalho
## Mudança de planos
@@ -77,3 +78,15 @@ Entre em contato com o suporte para fazer downgrade do seu plano.
### Mudar para faturamento mensal
Entre em contato com o suporte para voltar ao faturamento mensal.
## Obtenha uma chave Enterprise para Organization (Self-Hosted)
Para usar o plano Organization (Self-Hosted), você precisa obter uma chave Enterprise:
1. Vá para **Configurações → Painel de Administração → Enterprise**
<img src="/images/user-guide/billing/enterprise-key.png" alt="Chave Enterprise" />
2. Clique em **Obter chave Enterprise**
3. Quando você for redirecionado ao Stripe, insira seus dados de pagamento e confirme
4. Quando sua chave Enterprise for exibida, cole-a na página de configurações do Enterprise e ative a licença do Organization
@@ -16,6 +16,11 @@ Se você quiser hospedar por conta própria e precisar dos recursos Premium (SSO
Os recursos Premium estão disponíveis apenas nos planos Organization (Cloud ou Self-Hosted):
* **Integração de SSO**: Single Sign-On com seu provedor de identidade
* **Permissões em nível de linha**: Controle de acesso granular no nível de registro
* **Dados de uso de IA**: Acompanhe o consumo de IA em todo o espaço de trabalho
</Accordion>
<Accordion title="O plano Organization na nuvem e o plano Organization em ambiente autohospedado são iguais?">
Ambos oferecem os mesmos recursos Premium. No entanto, uma assinatura na nuvem não pode ser usada para implantações autohospedadas. O ambiente autohospedado requer uma chave Enterprise.
</Accordion>
<Accordion title="Você oferece assentos gratuitos para usuários apenas visualizadores?">
@@ -53,38 +53,45 @@ Primeiro, crie o objeto intermediário que manterá as conexões.
1. Vá a **Definições → Modelo de Dados**
2. Clique em **+ Novo objeto**
3. Dê um nome descritivo (por exemplo, "Atribuição de Projeto", "Membro da Equipe", "Pedido de Produto")
4. Clique em **Salvar**
4. Ative a opção "Ignorar a criação de um campo Nome"
<img src="/images/user-guide/fields/new-pivot-object.png" alt="Novo objeto pivô" />
5. Clique em **Salvar**
<Tip>
**Convenção de nomenclatura**: Use um nome que descreva a relação, como "Atribuição de Projeto" ou "Participação na Equipe". Isso torna o modelo de dados mais fácil de entender.
</Tip>
## Etapa 2: Criar Relações a partir do Objeto de Junção
## Etapa 2: Crie relações entre os objetos e o objeto de junção
Adicione campos de relação do objeto de junção para ambos os objetos que deseja conectar.
Adicione campos de relação de cada um dos seus dois objetos ao objeto de junção.
### Primeira Relação (Junção → Objeto A)
### Primeira Relação (Objeto A → Junção)
1. Selecione seu objeto de junção em **Settings → Data Model**
2. Clique em **+ Adicionar campo**
3. Escolha **Relação** como o tipo de campo
4. Selecione o primeiro objeto (por exemplo, "Pessoas")
5. Defina o tipo de relação como **Muitos-para-um** (muitas atribuições podem se vincular a uma pessoa)
6. Nomeie os campos:
* Campo na junção: por exemplo, "Pessoa"
* Campo em Pessoas: por exemplo, "Atribuições de Projeto"
7. Clique em **Salvar**
### Segunda Relação (Junção → Objeto B)
1. Ainda no objeto de junção, clique em **+ Add Field**
2. Escolha **Relação** como o tipo de campo
3. Selecione o segundo objeto (por exemplo, "Projetos")
4. Defina o tipo de relação como **Muitos-para-um**
1. Selecione seu primeiro objeto em **Settings → Data Model**
2. Clique em **+ Adicionar relação**
3. Selecione o objeto de junção (por exemplo, "Atribuições de Projeto")
4. Defina o tipo de relação como **Um-para-muitos** (uma pessoa pode se vincular a muitas atribuições)
5. Nomeie os campos:
* Campo em Pessoas: por exemplo, "Atribuições de Projeto"
* Campo na junção: por exemplo, "Pessoa"
6. Clique em **Salvar**
### Segunda Relação (Objeto B → Junção)
1. Selecione seu segundo objeto em **Settings → Data Model**
2. Clique em **+ Adicionar relação**
3. Selecione o objeto de junção (por exemplo, "Atribuições de Projeto")
4. Defina o tipo de relação como **Um-para-muitos** (um projeto pode se vincular a muitas atribuições)
5. Ative **"Esta é uma relação com um objeto de junção"**
<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}} />
6. Nomeie os campos:
* Campo na junção: por exemplo, "Projeto"
* Campo em Projetos: por exemplo, "Membros da Equipe"
6. Clique em **Salvar**
7. Clique em **Salvar**
## Etapa 3: Configurar a Exibição da Relação de Junção
@@ -98,18 +105,6 @@ Agora configure os objetos de origem para exibir diretamente os registros vincul
6. Selecione a **Relação de destino** (por exemplo, "Projeto" — o campo na junção que aponta para o outro lado)
7. Clique em **Salvar**
{/* TODO: Add image
<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}}/>
*/}
Repita para o outro objeto:
1. Selecione "Projetos" em Data Model
2. Edite o campo de relação "Membros da Equipe"
3. Ative o alternador de junção
4. Selecione "Pessoa" como a relação de destino
5. Salvar
## Resultado
Após a configuração:
@@ -130,15 +125,15 @@ Aqui está um passo a passo completo:
### Adicionar Relações
1. **Atribuição de Projeto → Pessoas**
* Tipo: Muitos-para-um
* Campo em Atribuição: "Pessoa"
1. **Pessoas → Atribuição de Projeto**
* Tipo: Um-para-muitos
* Campo em Pessoas: "Atribuições de Projeto"
* Campo em Atribuição: "Pessoa"
2. **Atribuição de Projeto → Projetos**
* Tipo: Muitos-para-um
* Campo em Atribuição: "Projeto"
2. **Projetos → Atribuição de Projeto**
* Tipo: Um-para-muitos
* Campo em Projetos: "Membros da Equipe"
* Campo em Atribuição: "Projeto"
### Configurar Exibição de Junção
@@ -30,3 +30,9 @@ Adicione links para ferramentas externas diretamente na barra lateral. Útil par
## Menu de Comandos
Pressione `Cmd+K` (ou `Ctrl+K`) para abrir o menu de comandos — uma barra de pesquisa de acesso rápido para ir a qualquer registro, visualização ou ação sem navegar pela barra lateral.
## Personalizando a barra lateral
Para personalizar a barra lateral, passe o cursor sobre a seção "Workspace" na barra lateral e clique no ícone de chave inglesa.
<img src="/images/user-guide/layout/navigation-edit-icon.png" alt="Ícone de edição da navegação" />
@@ -3,6 +3,8 @@ title: Páginas de Registos
description: Personalize o layout das páginas de detalhe de registos individuais com abas e widgets.
---
## Visão Geral
Ao abrir um registo no Twenty, a página de detalhe é composta por **abas** e **widgets**. Ambos são totalmente personalizáveis por tipo de objeto.
## Abas
@@ -38,12 +40,20 @@ Os widgets são os componentes básicos dentro de cada aba. Tipos de widget disp
1. Abra qualquer registro
2. Pressione `Cmd+K` e pesquise por "Edit record page layout"
ou
1. Vá para Configurações > Modelo de dados > objeto de sua escolha > Layout
2. Clique no botão "Personalizar página de registro" para esse objeto
3. Agora você está no modo de personalização:
* **Adicionar widgets** no seletor de widgets
* **Arraste widgets** para reposicioná-los na grade
* **Redimensione os widgets** arrastando suas bordas
* **Configure campos** exibidos em cada widget
* **Gerencie abas** — adicione, remova, renomeie, reordene
4. Salve suas alterações — elas se aplicam a todos os registros desse tipo de objeto
## Visibilidade de campos
@@ -77,7 +77,6 @@ export default defineApplication({
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
displayName: 'My Twenty App',
description: 'My first Twenty app',
icon: 'IconWorld',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
@@ -92,7 +92,7 @@ yarn twenty dev --verbose
</Warning>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Вывод терминала в режиме разработки" />
<img src="/images/docs/developers/extends/apps/dev.png" alt="Вывод терминала в режиме разработки" />
</div>
#### Разовая синхронизация с `yarn twenty dev --once`
@@ -127,13 +127,7 @@ yarn twenty dev --once
Нажмите **View installed app**, чтобы посмотреть установленное приложение. Вкладка **About** показывает текущую версию и параметры управления:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Установленное приложение — вкладка About" />
</div>
Переключитесь на вкладку **Content**, чтобы увидеть всё, что предоставляет ваше приложение: объекты, поля, логические функции и агенты:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-4.png" alt="Установленное приложение — вкладка Content" />
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Установленное приложение" />
</div>
Готово! Отредактируйте любой файл в `src/`, и изменения будут подхвачены автоматически.
@@ -213,30 +207,49 @@ npx create-twenty-app@latest my-twenty-app --example postcard
Генератор каркаса уже запустил для вас локальный сервер Twenty. Чтобы управлять им позже, используйте `yarn twenty server`:
| Команда | Описание |
| -------------------------------------- | -------------------------------------------------------------- |
| `yarn twenty server start` | Запустить локальный сервер (при необходимости скачивает образ) |
| `yarn twenty server start --port 3030` | Запустить на пользовательском порту |
| `yarn twenty server start --test` | Запускает отдельный тестовый экземпляр на порту 2021 |
| `yarn twenty server stop` | Остановить сервер (данные сохраняются) |
| `yarn twenty server status` | Показать состояние сервера, URL и учётные данные |
| `yarn twenty server logs` | Потоковый вывод журналов сервера |
| `yarn twenty server logs --lines 100` | Показать последние 100 строк журнала |
| `yarn twenty server reset` | Удалить все данные и начать с чистого листа |
| Команда | Описание |
| -------------------------------------- | ------------------------------------------------------------------ |
| `yarn twenty server start` | Запустить локальный сервер (при необходимости скачивает образ) |
| `yarn twenty server start --port 3030` | Запустить на пользовательском порту |
| `yarn twenty server start --test` | Запускает отдельный тестовый экземпляр на порту 2021 |
| `yarn twenty server stop` | Остановить сервер (данные сохраняются) |
| `yarn twenty server status` | Показать состояние сервера, URL, версию и учётные данные |
| `yarn twenty server logs` | Потоковый вывод журналов сервера |
| `yarn twenty server logs --lines 100` | Показать последние 100 строк журнала |
| `yarn twenty server reset` | Удалить все данные и начать с чистого листа |
| `yarn twenty server upgrade` | Загрузить последний образ `twenty-app-dev` и пересоздать контейнер |
| `yarn twenty server upgrade 2.2.0` | Обновить до конкретной версии |
Данные сохраняются между перезапусками в двух томах Docker (`twenty-app-dev-data` для PostgreSQL, `twenty-app-dev-storage` для файлов). Используйте `reset`, чтобы стереть всё и начать заново.
### Обновление образа сервера
Используйте `yarn twenty server upgrade`, чтобы проверить наличие более нового Docker-образа `twenty-app-dev` и обновить контейнер. Команда скачивает образ, сравнивает его с тем, из которого был создан контейнер, и пересоздаёт контейнер только если образ действительно изменился. Ваши тома данных сохраняются — заменяется только контейнер.
```bash filename="Terminal"
# Upgrade to the latest version (skips recreation if already up to date)
yarn twenty server upgrade
# Upgrade to a specific version
yarn twenty server upgrade 2.2.0
```
Если доступен более новый образ и контейнер был запущен, команда обновления автоматически запускает новый контейнер с обновлённым образом. Затем выполните `yarn twenty server start`, чтобы дождаться перехода контейнера в состояние healthy. Если образ не изменился, контейнер остаётся без изменений.
Вы можете проверить запущенную версию с помощью `yarn twenty server status` — эта команда показывает `APP_VERSION` из контейнера.
### Запуск тестового экземпляра
Передайте `--test` любой команде `server`, чтобы управлять вторым, полностью изолированным экземпляром — это полезно для запуска интеграционных тестов или экспериментов, не затрагивая ваши основные данные разработки.
| Команда | Описание |
| ---------------------------------- | ------------------------------------------------------------- |
| `yarn twenty server start --test` | Запустить тестовый экземпляр (по умолчанию — порт 2021) |
| `yarn twenty server stop --test` | Остановить тестовый экземпляр |
| `yarn twenty server status --test` | Показать состояние тестового экземпляра, URL и учётные данные |
| `yarn twenty server logs --test` | Выводить журналы тестового экземпляра в потоковом режиме |
| `yarn twenty server reset --test` | Удалить тестовые данные и начать с чистого листа |
| Команда | Описание |
| ----------------------------------- | --------------------------------------------------------------------- |
| `yarn twenty server start --test` | Запустить тестовый экземпляр (по умолчанию — порт 2021) |
| `yarn twenty server stop --test` | Остановить тестовый экземпляр |
| `yarn twenty server status --test` | Показать состояние тестового экземпляра, URL, версию и учётные данные |
| `yarn twenty server logs --test` | Выводить журналы тестового экземпляра в потоковом режиме |
| `yarn twenty server reset --test` | Удалить тестовые данные и начать с чистого листа |
| `yarn twenty server upgrade --test` | Обновить образ тестового экземпляра |
Тестовый экземпляр запускается в собственном контейнере Docker (`twenty-app-dev-test`) с выделенными томами (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) и собственной конфигурацией, поэтому он может работать параллельно с вашим основным экземпляром без конфликтов. Совместите `--test` с `--port`, чтобы переопределить значение по умолчанию (2021).
@@ -94,15 +94,16 @@ const handler = async (event: RoutePayload) => {
Тип `RoutePayload` имеет следующую структуру:
| Свойство | Тип | Описание | Пример |
| ---------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `headers` | `Record\<string, string \| undefined>` | HTTP-заголовки (только перечисленные в `forwardedRequestHeaders`) | см. раздел ниже |
| `queryStringParameters` | `Record\<string, string \| undefined>` | Параметры строки запроса (несколько значений объединяются запятыми) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
| `pathParameters` | `Record\<string, string \| undefined>` | Параметры пути, извлечённые из шаблона маршрута | `/users/:id`, `/users/123` -> `{ id: '123' }` |
| `body` | `object \| null` | Разобранное тело запроса (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
| `isBase64Encoded` | `boolean` | Является ли тело закодированным в base64 | |
| `requestContext.http.method` | `string` | Метод HTTP (GET, POST, PUT, PATCH, DELETE) | |
| `requestContext.http.path` | `string` | Необработанный путь запроса | |
| Свойство | Тип | Описание | Пример |
| ---------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `headers` | `Record\<string, string \| undefined>` | HTTP-заголовки (только перечисленные в `forwardedRequestHeaders`) | см. раздел ниже |
| `queryStringParameters` | `Record\<string, string \| undefined>` | Параметры строки запроса (несколько значений объединяются запятыми) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
| `pathParameters` | `Record\<string, string \| undefined>` | Параметры пути, извлечённые из шаблона маршрута | `/users/:id`, `/users/123` -> `{ id: '123' }` |
| `body` | `object \| null` | Разобранное тело запроса (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
| `rawBody` | `string \| undefined` | Исходное тело запроса в кодировке UTF-8, до разбора JSON. Полезно для проверки подписей вебхуков в стиле HMAC (например, `X-Hub-Signature-256` от GitHub, Stripe). `undefined`, если среда выполнения не сохранила его. | |
| `isBase64Encoded` | `boolean` | Является ли тело закодированным в base64 | |
| `requestContext.http.method` | `string` | Метод HTTP (GET, POST, PUT, PATCH, DELETE) | |
| `requestContext.http.path` | `string` | Необработанный путь запроса | |
#### forwardedRequestHeaders
@@ -19,7 +19,7 @@ Twenty предлагает гибкие тарифы для команд люб
* Стандартная поддержка
<Note>
Премиальные функции (SSO и разрешения на уровне записей) не входят в план Pro.
Премиальные функции (SSO, разрешения на уровне строк и данные об использовании ИИ) не включены в тариф Pro.
</Note>
### Организация (облако)
@@ -27,7 +27,7 @@ Twenty предлагает гибкие тарифы для команд люб
Для крупных команд с расширенными потребностями:
* Все, что есть в Pro
* **Премиальные функции**: интеграция с SSO и разрешения на уровне записей
* **Премиальные функции**: интеграция SSO, разрешения на уровне строк и данные об использовании ИИ
* Приоритетная поддержка
## Планы для самостоятельного размещения
@@ -45,7 +45,7 @@ Twenty предлагает гибкие тарифы для команд люб
Для команд, которым нужны премиальные функции при самостоятельном размещении:
* Все возможности Pro
* **Премиальные функции**: интеграция с SSO и разрешения на уровне записей
* **Премиальные функции**: интеграция SSO, разрешения на уровне строк и данные об использовании ИИ
* Поддержка от команды Twenty
* Не требуется публиковать пользовательский код с открытым исходным кодом перед распространением
@@ -55,6 +55,7 @@ Twenty предлагает гибкие тарифы для команд люб
* **Интеграция с SSO**: единый вход через вашего поставщика идентификации
* **Разрешения на уровне записей**: тонкая настройка управления доступом на уровне записей
* **Данные об использовании ИИ**: Отслеживайте потребление ИИ в рабочем пространстве
## Переключение планов
@@ -77,3 +78,15 @@ Twenty предлагает гибкие тарифы для команд люб
### Переход на ежемесячную оплату
Свяжитесь со службой поддержки, чтобы вернуться на ежемесячную оплату.
## Получите ключ Enterprise для организации (самостоятельный хостинг)
Чтобы использовать тариф Organization (Self-Hosted), необходимо получить ключ Enterprise:
1. Перейдите в **Настройки → Панель администратора → Enterprise**
<img src="/images/user-guide/billing/enterprise-key.png" alt="Ключ Enterprise" />
2. Нажмите **Получить ключ Enterprise**
3. Когда вы будете перенаправлены на Stripe, введите платёжные данные и подтвердите
4. Когда ваш ключ Enterprise будет отображён, вставьте его на странице настроек Enterprise и активируйте лицензию Organization
@@ -16,6 +16,11 @@ description: Часто задаваемые вопросы о тарифах и
Премиум-функции доступны только в планах Organization (облако или Self-Hosted):
* **Интеграция с SSO**: единый вход с вашим провайдером идентификации
* **Разрешения на уровне записей**: детализированное управление доступом на уровне записей
* **Данные об использовании ИИ**: Отслеживайте потребление ИИ в рабочем пространстве
</Accordion>
<Accordion title="Одинаков ли тариф Organization в облаке и в самостоятельно размещаемой версии?">
Они предлагают одинаковые премиум-функции. Однако облачную подписку нельзя использовать для развертываний на собственном хостинге. Для самостоятельно размещаемой версии требуется ключ Enterprise.
</Accordion>
<Accordion title="Предлагаете ли вы бесплатные места для пользователей с правами только просмотра?">
@@ -27,7 +32,7 @@ description: Часто задаваемые вопросы о тарифах и
</Accordion>
<Accordion title="Где я могу переключить свою подписку на план Pro?">
Пожалуйста, свяжитесь с нашей командой напрямую через Поддержку, в данный момент нет простого способа сделать это через пользовательский интерфейс.
Пожалуйста, свяжитесь с нашей командой напрямую через раздел «Поддержка», в данный момент нет простого способа сделать это через пользовательский интерфейс.
</Accordion>
<Accordion title="Где я могу переключить свою подписку на годовой период?">
@@ -53,38 +53,45 @@ People ←→ Project Assignments ←→ Projects
1. Перейдите в **Настройки → Модель данных**
2. Нажмите **+ Новый объект**
3. Дайте ему понятное имя (например, "Project Assignment", "Team Member", "Product Order")
4. Нажмите **Сохранить**
4. Включите переключатель «Пропустить создание поля „Имя“»
<img src="/images/user-guide/fields/new-pivot-object.png" alt="Новый объект-связка" />
5. Нажмите **Сохранить**
<Tip>
**Рекомендации по именованию**: Используйте название, описывающее связь, например "Project Assignment" или "Team Membership". Так модель данных становится более понятной.
</Tip>
## Шаг 2: Создайте связи из объекта-связки
## Шаг 2: Создайте связи между объектами и объектом-связкой
Добавьте поля связи из объекта-связки к обоим объектам, которые вы хотите соединить.
Добавьте поля связи из каждого из двух объектов в объект-связку.
### Первая связь (Объект-связка → Объект A)
### Первая связь (Объект A → Объект-связка)
1. Выберите ваш объект-связку в **Настройки → Модель данных**
2. Нажмите **+ Добавить поле**
3. Выберите тип поля **Связь**
4. Выберите первый объект (например, "Люди")
5. Установите тип связи **Многие-к-одному** (много назначений могут ссылаться на одного человека)
6. Назовите поля:
* Поле на объекте-связке: например, "Person"
* Поле в объекте Люди: например, "Project Assignments"
7. Нажмите **Сохранить**
### Вторая связь (Объект-связка → Объект B)
1. Оставаясь на объекте-связке, нажмите **+ Добавить поле**
2. Выберите тип поля **Связь**
3. Выберите второй объект (например, "Проекты")
4. Установите тип связи **Многие-к-одному**
1. Выберите ваш первый объект в **Настройках → Модель данных**
2. Нажмите **+ Добавить связь**
3. Выберите объект-связку (например, "Назначения по проектам")
4. Установите тип связи **Один-ко-многим** (один человек может быть связан со многими назначениями)
5. Назовите поля:
* Поле в объекте Люди: например, "Project Assignments"
* Поле на объекте-связке: например, "Person"
6. Нажмите **Сохранить**
### Вторая связь (Объект B → Объект-связка)
1. Выберите ваш второй объект в **Настройках → Модель данных**
2. Нажмите **+ Добавить связь**
3. Выберите объект-связку (например, "Назначения по проектам")
4. Установите тип связи **Один-ко-многим** (один проект может быть связан со многими назначениями)
5. Включите **"Это связь с объектом-связкой"**
<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}} />
6. Назовите поля:
* Поле на объекте-связке: например, "Project"
* Поле в объекте Проекты: например, "Team Members"
6. Нажмите **Сохранить**
7. Нажмите **Сохранить**
## Шаг 3: Настройте отображение связи через объект-связку
@@ -98,18 +105,6 @@ People ←→ Project Assignments ←→ Projects
6. Выберите **Целевую связь** (например, "Project" — поле на объекте-связке, указывающее на другую сторону)
7. Нажмите **Сохранить**
{/* TODO: Add image
<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}}/>
*/}
Повторите для другого объекта:
1. Выберите "Проекты" в Модели данных
2. Отредактируйте поле связи "Team Members"
3. Включите переключатель объекта-связки
4. Выберите "Person" как целевую связь
5. Сохранить
## Результат
После настройки:
@@ -130,15 +125,15 @@ People ←→ Project Assignments ←→ Projects
### Добавьте связи
1. **Project Assignment → Люди**
* Тип: Многие-к-одному
* Поле в объекте Assignment: "Person"
1. **Люди → Назначение на проект**
* Тип: Один-ко-многим
* Поле в объекте Люди: "Project Assignments"
* Поле в объекте Assignment: "Person"
2. **Project Assignment → Проекты**
* Тип: Многие-к-одному
* Поле в объекте Assignment: "Project"
2. **Проекты → Назначение на проект**
* Тип: Один-ко-многим
* Поле в объекте Проекты: "Team Members"
* Поле в объекте Assignment: "Project"
### Настройте отображение объекта-связки
@@ -30,3 +30,9 @@ description: Настройте левую боковую панель под т
## Меню команд
Нажмите `Cmd+K` (или `Ctrl+K`), чтобы открыть меню команд — строку быстрого поиска для перехода к любой записи, представлению или действию, не используя боковую панель.
## Настройка боковой панели
Чтобы настроить боковую панель, наведите курсор на раздел "Workspace" на боковой панели и нажмите значок гаечного ключа.
<img src="/images/user-guide/layout/navigation-edit-icon.png" alt="Значок редактирования навигации" />
@@ -3,6 +3,8 @@ title: Страницы записей
description: Настройте макет отдельных страниц сведений о записях с вкладками и виджетами.
---
## Обзор
Когда вы открываете запись в Twenty, страница сведений состоит из **вкладок** и **виджетов**. Оба полностью настраиваются для каждого типа объекта.
## Вкладки
@@ -38,12 +40,20 @@ description: Настройте макет отдельных страниц с
1. Откройте любую запись
2. Нажмите `Cmd+K` и найдите "Изменить макет страницы записи"
или
1. Перейдите в Настройки > Модель данных > объект по вашему выбору > Макет
2. Нажмите кнопку "Настроить страницу записи" для этого объекта
3. Теперь вы в режиме настройки:
* **Добавляйте виджеты** из списка виджетов
* **Перетаскивайте виджеты**, чтобы изменить их положение на сетке
* **Изменяйте размер виджетов**, перетаскивая их границы
* **Настраивайте поля**, отображаемые в каждом виджете
* **Управляйте вкладками** — добавляйте, удаляйте, переименовывайте, меняйте порядок
4. Сохраните изменения — они применятся ко всем записям этого типа объекта
## Видимость полей
@@ -77,7 +77,6 @@ export default defineApplication({
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
displayName: 'My Twenty App',
description: 'My first Twenty app',
icon: 'IconWorld',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
@@ -92,7 +92,7 @@ Geliştirme modu yalnızca geliştirme ortamında (`NODE_ENV=development`) çal
</Warning>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Geliştirme modu terminal çıktısı" />
<img src="/images/docs/developers/extends/apps/dev.png" alt="Geliştirme modu terminal çıktısı" />
</div>
#### Tek seferlik eşitleme `yarn twenty dev --once` ile
@@ -127,13 +127,7 @@ Tarayıcınızda [http://localhost:2020/settings/applications#developer](http://
Yüklü uygulamayı görmek için **View installed app**'e tıklayın. **About** sekmesi, geçerli sürümü ve yönetim seçeneklerini gösterir:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Yüklü uygulama — About sekmesi" />
</div>
Uygulamanızın sağladığı her şeyi — nesneler, alanlar, mantık işlevleri ve ajanlar — görmek için **Content** sekmesine geçin:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-4.png" alt="Yüklü uygulama — Content sekmesi" />
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Yüklü uygulama" />
</div>
Her şey hazır! `src/` içindeki herhangi bir dosyayı düzenleyin; değişiklikler otomatik olarak alınacaktır.
@@ -213,30 +207,49 @@ npx create-twenty-app@latest my-twenty-app --example postcard
İskelet oluşturucu sizin için zaten yerel bir Twenty sunucusu başlattı. Daha sonra yönetmek için `yarn twenty server` komutunu kullanın:
| Komut | Açıklama |
| -------------------------------------- | --------------------------------------------------------------- |
| `yarn twenty server start` | Yerel sunucuyu başlatır (gerekirse imajı çeker) |
| `yarn twenty server start --port 3030` | Özel bir portta başlatır |
| `yarn twenty server start --test` | 2021 numaralı bağlantı noktasında ayrı bir test örneği başlatın |
| `yarn twenty server stop` | Sunucuyu durdurur (verileri korur) |
| `yarn twenty server status` | Sunucu durumunu, URL'yi ve kimlik bilgilerini gösterir |
| `yarn twenty server logs` | Sunucu günlüklerini akış olarak iletir |
| `yarn twenty server logs --lines 100` | Son 100 günlük satırını gösterir |
| `yarn twenty server reset` | Tüm verileri siler ve sıfırdan başlatır |
| Komut | Açıklama |
| -------------------------------------- | --------------------------------------------------------------------- |
| `yarn twenty server start` | Yerel sunucuyu başlatır (gerekirse imajı çeker) |
| `yarn twenty server start --port 3030` | Özel bir portta başlatır |
| `yarn twenty server start --test` | 2021 numaralı bağlantı noktasında ayrı bir test örneği başlatın |
| `yarn twenty server stop` | Sunucuyu durdurur (verileri korur) |
| `yarn twenty server status` | Sunucu durumunu, URL'yi, sürümü ve kimlik bilgilerini gösterir |
| `yarn twenty server logs` | Sunucu günlüklerini akış olarak iletir |
| `yarn twenty server logs --lines 100` | Son 100 günlük satırını gösterir |
| `yarn twenty server reset` | Tüm verileri siler ve sıfırdan başlatır |
| `yarn twenty server upgrade` | En son `twenty-app-dev` imajını çeker ve konteyneri yeniden oluşturur |
| `yarn twenty server upgrade 2.2.0` | Belirli bir sürüme yükseltin |
Veriler, yeniden başlatmalar arasında iki Docker biriminde kalıcıdır (PostgreSQL için `twenty-app-dev-data`, dosyalar için `twenty-app-dev-storage`). Her şeyi silip baştan başlamak için `reset` kullanın.
### Sunucu imajını yükseltme
Daha yeni bir `twenty-app-dev` Docker imajı olup olmadığını kontrol etmek ve konteyneri güncellemek için `yarn twenty server upgrade` komutunu kullanın. Komut imajı çeker, konteynerin oluşturulduğu imajla karşılaştırır ve imaj gerçekten değiştiyse yalnızca konteyneri yeniden oluşturur. Veri birimleriniz korunur — yalnızca konteyner değiştirilir.
```bash filename="Terminal"
# Upgrade to the latest version (skips recreation if already up to date)
yarn twenty server upgrade
# Upgrade to a specific version
yarn twenty server upgrade 2.2.0
```
Daha yeni bir imaj mevcutsa ve konteyner çalışıyorsa, yükseltme komutu güncellenmiş imajla yeni bir konteyneri otomatik olarak başlatır. Ardından sağlıklı hale gelmesini beklemek için `yarn twenty server start` komutunu çalıştırın. İmaj değişmediyse, konteyner olduğu gibi bırakılır.
`yarn twenty server status` ile çalışan sürümü doğrulayabilirsiniz; bu komut, konteynerden `APP_VERSION` değerini görüntüler.
### Test örneği çalıştırma
`server` komutlarının herhangi birine `--test` parametresini vererek ikinci, tamamen yalıtılmış bir örneği yönetin — entegrasyon testlerini çalıştırmak veya ana geliştirme verilerinize dokunmadan denemeler yapmak için kullanışlıdır.
| Komut | Açıklama |
| ---------------------------------- | -------------------------------------------------------------- |
| `yarn twenty server start --test` | Test örneğini başlatır (varsayılan bağlantı noktası 2021'dir) |
| `yarn twenty server stop --test` | Test örneğini durdurur |
| `yarn twenty server status --test` | Test örneğinin durumunu, URL'yi ve kimlik bilgilerini gösterir |
| `yarn twenty server logs --test` | Test örneği günlüklerini akış olarak iletir |
| `yarn twenty server reset --test` | Test verilerini siler ve sıfırdan başlatır |
| Komut | Açıklama |
| ----------------------------------- | ---------------------------------------------------------------------- |
| `yarn twenty server start --test` | Test örneğini başlatır (varsayılan bağlantı noktası 2021'dir) |
| `yarn twenty server stop --test` | Test örneğini durdurur |
| `yarn twenty server status --test` | Test örneğinin durumunu, URL'yi, sürümü ve kimlik bilgilerini gösterir |
| `yarn twenty server logs --test` | Test örneği günlüklerini akış olarak iletir |
| `yarn twenty server reset --test` | Test verilerini siler ve sıfırdan başlatır |
| `yarn twenty server upgrade --test` | Test örneğinin imajını yükseltin |
Test örneği, kendine ait bir Docker konteynerinde (`twenty-app-dev-test`), ayrılmış birimlerle (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) ve yapılandırmayla çalışır; böylece ana örneğinizle çakışma olmadan paralel olarak çalışabilir. Varsayılan 2021'i geçersiz kılmak için `--test` ile `--port`'u birlikte kullanın.
@@ -95,15 +95,16 @@ const handler = async (event: RoutePayload) => {
`RoutePayload` türünün yapısı şu şekildedir:
| Özellik | Tür | Açıklama | Örnek |
| ---------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `headers` | `Record\<string, string \| undefined>` | HTTP başlıkları (`forwardedRequestHeaders` içinde listelenenlerle sınırlı) | aşağıdaki bölüme bakın |
| `queryStringParameters` | `Record\<string, string \| undefined>` | Sorgu dizesi parametreleri (birden çok değer virgülle birleştirilir) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
| `pathParameters` | `Record\<string, string \| undefined>` | Rota deseninden çıkarılan yol parametreleri | `/users/:id`, `/users/123` -> `{ id: '123' }` |
| `body` | `object \| null` | Ayrıştırılmış istek gövdesi (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
| `isBase64Encoded` | `boolean` | Gövdenin base64 ile kodlanıp kodlanmadığı | |
| `requestContext.http.method` | `string` | HTTP yöntemi (GET, POST, PUT, PATCH, DELETE) | |
| `requestContext.http.path` | `string` | Ham istek yolu | |
| Özellik | Tür | Açıklama | Örnek |
| ---------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `headers` | `Record\<string, string \| undefined>` | HTTP başlıkları (`forwardedRequestHeaders` içinde listelenenlerle sınırlı) | aşağıdaki bölüme bakın |
| `queryStringParameters` | `Record\<string, string \| undefined>` | Sorgu dizesi parametreleri (birden çok değer virgülle birleştirilir) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
| `pathParameters` | `Record\<string, string \| undefined>` | Rota deseninden çıkarılan yol parametreleri | `/users/:id`, `/users/123` -> `{ id: '123' }` |
| `body` | `object \| null` | Ayrıştırılmış istek gövdesi (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
| `rawBody` | `string \| undefined` | JSON ayrıştırılmadan önceki özgün UTF-8 istek gövdesi. HMAC tarzı webhook imzalarını doğrulamak için kullanışlıdır (ör. GitHub'ın `X-Hub-Signature-256`, Stripe). Çalışma zamanı onu korumadığında `undefined` olur. | |
| `isBase64Encoded` | `boolean` | Gövdenin base64 ile kodlanıp kodlanmadığı | |
| `requestContext.http.method` | `string` | HTTP yöntemi (GET, POST, PUT, PATCH, DELETE) | |
| `requestContext.http.path` | `string` | Ham istek yolu | |
#### forwardedRequestHeaders
@@ -19,7 +19,7 @@ Büyümeye hazır ekipler için:
* Standart destek
<Note>
Premium özellikler (SSO ve satır düzeyi izinler) Pro planına dahil değildir.
Premium özellikler (SSO, satır düzeyi izinler ve Yapay Zeka kullanım verileri) Pro plana dahil değildir.
</Note>
### Kuruluş (Bulut)
@@ -27,7 +27,7 @@ Premium özellikler (SSO ve satır düzeyi izinler) Pro planına dahil değildir
Gelişmiş ihtiyaçları olan daha büyük ekipler için:
* Pro'daki her şey
* **Premium özellikler**: SSO entegrasyonu ve satır düzeyi izinler
* **Premium özellikler**: SSO entegrasyonu, satır düzeyi izinler ve Yapay Zeka kullanım verileri
* Öncelikli destek
## Kendi Kendine Barındırılan Planlar
@@ -45,7 +45,7 @@ Twenty'yi kendi altyapınızda hiçbir ücret ödemeden barındırın:
Kendi kendine barındırma yaparken premium özelliklere ihtiyaç duyan ekipler için:
* Tüm Pro özellikleri
* **Premium özellikler**: SSO entegrasyonu ve satır düzeyi izinler
* **Premium özellikler**: SSO entegrasyonu, satır düzeyi izinler ve Yapay Zeka kullanım verileri
* Twenty ekibinden destek
* Dağıtmadan önce özel kodu açık kaynak olarak yayınlama zorunluluğu yoktur.
@@ -55,6 +55,7 @@ Premium özellikler yalnızca Kuruluş planlarında (Bulut veya Kendi Kendine Ba
* **SSO entegrasyonu**: Kimlik sağlayıcınızla Tek Oturum Açma
* **Satır düzeyi izinler**: Kayıt düzeyinde ayrıntılı erişim denetimi
* **Yapay Zeka kullanım verileri**: Çalışma alanı genelinde Yapay Zeka tüketimini takip edin
## Planlar Arasında Geçiş
@@ -77,3 +78,15 @@ Planınızı düşürmek için destek ekibiyle iletişime geçin.
### Aylığa Geçiş Yap
Aylık faturalamaya geri dönmek için destek ekibiyle iletişime geçin.
## Organizasyon (Kendi Sunucunuzda Barındırılan) için Kurumsal Anahtar edinin
Organizasyon (Kendi Sunucunuzda Barındırılan) planını kullanmak için bir Kurumsal Anahtar edinmeniz gerekir:
1. **Ayarlar → Yönetici Paneli → Kurumsal** bölümüne gidin
<img src="/images/user-guide/billing/enterprise-key.png" alt="Kurumsal Anahtar" />
2. **Kurumsal Anahtar Alın**'ı tıklayın
3. Stripe'a yönlendirildiğinizde ödeme bilgilerinizi girin ve onaylayın
4. Kurumsal anahtarınız görüntülendiğinde, bunu Kurumsal ayarlar sayfasına yapıştırın ve Organizasyon lisansını etkinleştirin
@@ -16,6 +16,11 @@ Kendiniz barındırmak istiyor ve Premium özelliklere (SSO ve satır düzeyi iz
Premium özellikler yalnızca Kuruluş planlarında (Bulut veya Kendi Barındırmalı) kullanılabilir:
* **SSO entegrasyonu**: Kimlik sağlayıcınızla Tek Oturum Açma
* **Satır düzeyi izinler**: Kayıt düzeyinde ayrıntılı erişim denetimi
* **Yapay Zeka kullanım verileri**: Çalışma alanı genelinde Yapay Zeka tüketimini takip edin
</Accordion>
<Accordion title="Buluttaki Organization planı ile kendi barındırmalı ortamdaki Organization planı aynı mı?">
İkisi de aynı Premium özellikleri sunar. Ancak bulut aboneliği, kendi barındırmalı dağıtımlarda kullanılamaz. Kendi barındırmalı kurulumlar için bir Enterprise anahtarı gerekir.
</Accordion>
<Accordion title="Salt görüntüleme kullanıcıları için ücretsiz koltuklar sunuyor musunuz?">
@@ -53,38 +53,45 @@ Bağlantı ilişkisi anahtarını etkinleştirdiğinizde, Twenty aradaki bağlan
1. **Ayarlar → Veri Modeli** bölümüne gidin
2. **+ Yeni nesne**'ye tıklayın
3. Açıklayıcı bir ad verin (örn. "Project Assignment", "Team Member", "Product Order")
4. **Kaydet**'e tıklayın
4. "Ad alanı oluşturmayı atla" seçeneğini açın
<img src="/images/user-guide/fields/new-pivot-object.png" alt="Yeni pivot nesnesi" />
5. **Kaydet**'e tıklayın
<Tip>
**Adlandırma kuralı**: "Project Assignment" veya "Team Membership" gibi ilişkiyi tanımlayan bir ad kullanın. Bu, veri modelinin anlaşılmasını kolaylaştırır.
</Tip>
## Adım 2: Bağlantı Nesnesinden İlişkiler Oluşturun
## Adım 2: Nesneler ile Bağlantı nesnesi arasında ilişkiler oluşturun
Bağlamak istediğiniz her iki nesneye de bağlantı nesnesinden ilişki alanları ekleyin.
İki nesnenizin her birinden bağlantı nesnesine ilişki alanları ekleyin.
### İlk İlişki (Bağlantı → Nesne A)
### İlk İlişki (Nesne A → Bağlantı)
1. **Ayarlar → Veri Modeli**'nde bağlantı nesnenizi seçin
2. **+ Alan Ekle**'ye tıklayın
3. Alan türü olarak **İlişki**'yi seçin
4. İlk nesneyi seçin (örn. "People")
5. İlişki türünü **Çoktan-Bire** olarak ayarlayın (birçok atama bir kişiye bağlanabilir)
6. Alanları adlandırın:
* Bağlantı üzerindeki alan: örn. "Person"
* People üzerindeki alan: örn. "Project Assignments"
7. **Kaydet**'e tıklayın
### İkinci İlişki (Bağlantı → Nesne B)
1. Hâlâ bağlantı nesnesindeyken, **+ Add Field**'e tıklayın
2. Alan türü olarak **İlişki**'yi seçin
3. İkinci nesneyi seçin (örn. "Projects")
4. İlişki türünü **Çoktan-Bire** olarak ayarlayın
1. İlk nesnenizi **Ayarlar → Veri Modeli**'nde seçin
2. **+ İlişki Ekle**'ye tıklayın
3. Bağlantı nesnesini seçin (örn. "Project Assignments")
4. İlişki türünü **Birden-Çoğa** olarak ayarlayın (bir kişi birçok atamaya bağlanabilir)
5. Alanları adlandırın:
* People üzerindeki alan: örn. "Project Assignments"
* Bağlantı üzerindeki alan: örn. "Person"
6. **Kaydet**'e tıklayın
### İkinci İlişki (Nesne B → Bağlantı)
1. İkinci nesnenizi **Ayarlar → Veri Modeli**'nde seçin
2. **+ İlişki Ekle**'ye tıklayın
3. Bağlantı nesnesini seçin (örn. "Project Assignments")
4. İlişki türünü **Birden-Çoğa** olarak ayarlayın (bir proje birçok atamaya bağlanabilir)
5. **"Bu, bir bağlantı nesnesine kurulan bir ilişkidir"** seçeneğini etkinleştirin
<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}} />
6. Alanları adlandırın:
* Bağlantı üzerindeki alan: örn. "Project"
* Projects üzerindeki alan: örn. "Team Members"
6. **Kaydet**'e tıklayın
7. **Kaydet**'e tıklayın
## Adım 3: Bağlantı İlişkisi Görüntüsünü Yapılandırın
@@ -98,18 +105,6 @@ Bağlamak istediğiniz her iki nesneye de bağlantı nesnesinden ilişki alanlar
6. **Hedef ilişki**yi seçin (örn. "Project" — bağlantı üzerindeki, diğer tarafı işaret eden alan)
7. **Kaydet**'e tıklayın
{/* TODO: Add image
<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}}/>
*/}
Diğer nesne için tekrarlayın:
1. Veri Modeli'nde "Projects"i seçin
2. "Team Members" ilişki alanını düzenleyin
3. Bağlantı anahtarını etkinleştirin
4. Hedef ilişki olarak "Person"ı seçin
5. Kaydet
## Sonuç
Yapılandırmadan sonra:
@@ -130,15 +125,15 @@ Bağlantı nesnesi hâlâ mevcuttur ve bağlantıları saklar, ancak kullanıcı
### İlişkiler Ekleyin
1. **Project Assignment → People**
* Tür: Çoktan-Bire
* Assignment üzerindeki alan: "Person"
1. **People → Project Assignment**
* Tür: Birden-Çoğa
* People üzerindeki alan: "Project Assignments"
* Assignment üzerindeki alan: "Person"
2. **Project Assignment → Projects**
* Tür: Çoktan-Bire
* Assignment üzerindeki alan: "Project"
2. **Projects → Project Assignment**
* Tür: Birden-Çoğa
* Projects üzerindeki alan: "Team Members"
* Assignment üzerindeki alan: "Project"
### Bağlantı Görüntüsünü Yapılandırın
@@ -30,3 +30,9 @@ Harici araçlara yönelik bağlantıları doğrudan kenar çubuğuna ekleyin. Wi
## Komut Menüsü
Komut menüsünü açmak için `Cmd+K` (veya `Ctrl+K`) tuşlarına basın — kenar çubuğunda gezinmeden herhangi bir kayda, görünüme veya eyleme atlamak için hızlı erişim sunan bir arama çubuğu.
## Kenar Çubuğunu Özelleştirme
Kenar çubuğunu özelleştirmek için kenar çubuğundaki "Workspace" bölümünün üzerine gelin ve anahtar simgesine tıklayın.
<img src="/images/user-guide/layout/navigation-edit-icon.png" alt="Gezinti düzenleme simgesi" />
@@ -3,6 +3,8 @@ title: Kayıt Sayfaları
description: Her bir kayıt ayrıntı sayfasının düzenini sekmeler ve widget'larla özelleştirin.
---
## Genel Bakış
Twenty'de bir kaydı açtığınızda, ayrıntı sayfası **sekmeler** ve **widget'lar** içerir. Her ikisi de her nesne türü bazında tamamen özelleştirilebilir.
## Sekmeler
@@ -38,12 +40,20 @@ Widget'lar, her sekmenin içindeki yapı taşlarıdır. Kullanılabilir widget t
1. Herhangi bir kaydı açın
2. `Cmd+K` tuşlarına basın ve "Kayıt sayfası yerleşimini düzenle" ifadesini arayın
veya
1. Ayarlar > Veri modeli > seçtiğiniz nesne > Düzen'e gidin
2. O nesne için "Kayıt sayfasını özelleştir" düğmesine tıklayın
3. Artık özelleştirme modundasınız:
* **Widget ekleyin** widget seçicisinden
* **Widget'ları sürükleyin** ve ızgara üzerinde yeniden konumlandırın
* **Widget'ları yeniden boyutlandırın** kenarlarını sürükleyerek
* **Alanları yapılandırın** — her bir widget içinde gösterilen
* **Sekmeleri yönetin** — ekleyin, kaldırın, yeniden adlandırın, yeniden sıralayın
4. Değişikliklerinizi kaydedin — bu değişiklikler o nesne türündeki tüm kayıtlara uygulanır
## Alan görünürlüğü
@@ -77,7 +77,6 @@ export default defineApplication({
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
displayName: 'My Twenty App',
description: 'My first Twenty app',
icon: 'IconWorld',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
@@ -92,7 +92,7 @@ yarn twenty dev --verbose
</Warning>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="开发模式终端输出" />
<img src="/images/docs/developers/extends/apps/dev.png" alt="开发模式终端输出" />
</div>
#### 使用 `yarn twenty dev --once` 进行一次性同步
@@ -127,13 +127,7 @@ yarn twenty dev --once
点击 **查看已安装的应用** 以查看已安装的应用。 **关于** 选项卡显示当前版本和管理选项:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="已安装的应用 — “关于”选项卡" />
</div>
切换到 **内容** 选项卡,以查看你的应用提供的全部内容——对象、字段、逻辑函数和智能体:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-4.png" alt="已安装的应用 — “内容”选项卡" />
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="已安装的应用" />
</div>
一切就绪! 编辑 `src/` 中的任意文件,更改会被自动检测到。
@@ -213,30 +207,49 @@ npx create-twenty-app@latest my-twenty-app --example postcard
脚手架工具已经为你启动了一个本地 Twenty 服务器。 稍后要进行管理,请使用 `yarn twenty server`
| 命令 | 描述 |
| -------------------------------------- | -------------------- |
| `yarn twenty server start` | 启动本地服务器(按需拉取镜像) |
| `yarn twenty server start --port 3030` | 在自定义端口启动 |
| `yarn twenty server start --test` | 在 2021 端口启动一个独立的测试实例 |
| `yarn twenty server stop` | 停止服务器(保留数据) |
| `yarn twenty server status` | 显示服务器状态、URL 和凭据 |
| `yarn twenty server logs` | 流式输出服务器日志 |
| `yarn twenty server logs --lines 100` | 显示最近 100 行日志 |
| `yarn twenty server reset` | 删除所有数据并全新开始 |
| 命令 | 描述 |
| -------------------------------------- | -------------------------------- |
| `yarn twenty server start` | 启动本地服务器(按需拉取镜像) |
| `yarn twenty server start --port 3030` | 在自定义端口启动 |
| `yarn twenty server start --test` | 在 2021 端口启动一个独立的测试实例 |
| `yarn twenty server stop` | 停止服务器(保留数据) |
| `yarn twenty server status` | 显示服务器状态、URL、版本和凭据 |
| `yarn twenty server logs` | 流式输出服务器日志 |
| `yarn twenty server logs --lines 100` | 显示最近 100 行日志 |
| `yarn twenty server reset` | 删除所有数据并全新开始 |
| `yarn twenty server upgrade` | 拉取最新的 `twenty-app-dev` 镜像并重新创建容器 |
| `yarn twenty server upgrade 2.2.0` | 升级到指定版本 |
数据在重启后会保留,存储于两个 Docker 卷中(`twenty-app-dev-data` 用于 PostgreSQL`twenty-app-dev-storage` 用于文件)。 使用 `reset` 清空所有内容并重新开始。
### 升级服务器镜像
使用 `yarn twenty server upgrade` 来检查是否有更新的 `twenty-app-dev` Docker 镜像并更新容器。 该命令会拉取镜像,与容器创建时所用的镜像进行比较,仅当镜像确实发生变化时才会重新创建容器。 您的数据卷将被保留——只会替换容器。
```bash filename="Terminal"
# Upgrade to the latest version (skips recreation if already up to date)
yarn twenty server upgrade
# Upgrade to a specific version
yarn twenty server upgrade 2.2.0
```
如果有可用的新镜像且容器正在运行,升级命令会使用更新后的镜像自动启动一个新容器。 随后运行 `yarn twenty server start`,等待其变为健康状态。 如果镜像没有变化,容器将保持不变。
您可以使用 `yarn twenty server status` 验证正在运行的版本,该命令会显示容器中的 `APP_VERSION`。
### 运行测试实例
向任何 `server` 命令传递 `--test` 以管理第二个、完全隔离的实例——这对于运行集成测试或在不影响主开发数据的情况下进行试验非常有用。
| 命令 | 描述 |
| ---------------------------------- | ------------------- |
| `yarn twenty server start --test` | 启动测试实例 (默认端口为 2021) |
| `yarn twenty server stop --test` | 停止测试实例 |
| `yarn twenty server status --test` | 显示测试实例状态、URL 和凭据 |
| `yarn twenty server logs --test` | 流式输出测试实例日志 |
| `yarn twenty server reset --test` | 清除测试数据并全新开始 |
| 命令 | 描述 |
| ----------------------------------- | ------------------- |
| `yarn twenty server start --test` | 启动测试实例 (默认端口为 2021) |
| `yarn twenty server stop --test` | 停止测试实例 |
| `yarn twenty server status --test` | 显示测试实例状态、URL、版本和凭据 |
| `yarn twenty server logs --test` | 流式输出测试实例日志 |
| `yarn twenty server reset --test` | 清除测试数据并全新开始 |
| `yarn twenty server upgrade --test` | 升级测试实例镜像 |
测试实例在其独立的 Docker 容器 (`twenty-app-dev-test`) 中运行,配有专用的卷 (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) 和配置,因此可以与主实例并行运行且不会发生冲突。 将 `--test` 与 `--port` 一起使用以覆盖默认的 2021 端口。
@@ -95,15 +95,16 @@ const handler = async (event: RoutePayload) => {
`RoutePayload` 类型具有以下结构:
| 属性 | 类型 | 描述 | 示例 |
| ---------------------------- | ------------------------------------------------------- | --------------------------------------------- | -------------------------------------------------------------------------- |
| `headers` | `Record\<string, string \| undefined>` | HTTP 请求头(仅限 `forwardedRequestHeaders` 中列出的那些) | 见下文 |
| `queryStringParameters` | `Record\<string, string \| undefined>` | 查询字符串参数(多个值以逗号连接) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
| `pathParameters` | `Record\<string, string \| undefined>` | 从路由模式中提取的路径参数 | `/users/:id``/users/123` -> `{ id: '123' }` |
| `body` | `object \| null` | 已解析的请求体(JSON) | `{ id: 1 }` -> `{ id: 1 }` |
| `isBase64Encoded` | `boolean` | 请求体是否为 base64 编码 | |
| `requestContext.http.method` | `string` | HTTP 方法(GET、POST、PUT、PATCH、DELETE | |
| `requestContext.http.path` | `string` | 原始请求路径 | |
| 属性 | 类型 | 描述 | 示例 |
| ---------------------------- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `headers` | `Record\<string, string \| undefined>` | HTTP 请求头(仅限 `forwardedRequestHeaders` 中列出的那些) | 见下文 |
| `queryStringParameters` | `Record\<string, string \| undefined>` | 查询字符串参数(多个值以逗号连接) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
| `pathParameters` | `Record\<string, string \| undefined>` | 从路由模式中提取的路径参数 | `/users/:id``/users/123` -> `{ id: '123' }` |
| `body` | `object \| null` | 已解析的请求体(JSON) | `{ id: 1 }` -> `{ id: 1 }` |
| `rawBody` | `string \| undefined` | 在 JSON 解析之前的原始 UTF-8 请求体。 用于验证 HMAC 风格的 Webhook 签名(例如 GitHub 的 `X-Hub-Signature-256`、Stripe)。 当运行时未保留它时为 `undefined`。 | |
| `isBase64Encoded` | `boolean` | 请求体是否为 base64 编码 | |
| `requestContext.http.method` | `string` | HTTP 方法(GET、POST、PUT、PATCH、DELETE | |
| `requestContext.http.path` | `string` | 原始请求路径 | |
#### forwardedRequestHeaders
@@ -19,7 +19,7 @@ description: 了解 Twenty 的定价方案以及如何在它们之间切换。
* 标准支持
<Note>
Pro 计划不包含高级功能(SSO行级权限)。
Pro 方案不包含高级功能(SSO行级权限和 AI 使用数据)。
</Note>
### 组织计划(云端)
@@ -27,7 +27,7 @@ Pro 计划不包含高级功能(SSO 和行级权限)。
适用于具有高级需求的大型团队:
* 包含 Pro 的全部内容
* **高级功能**SSO 集成行级权限
* **高级功能**SSO 集成行级权限和 AI 使用数据
* 优先支持
## 自托管方案
@@ -45,7 +45,7 @@ Pro 计划不包含高级功能(SSO 和行级权限)。
适用于在自托管同时需要高级功能的团队:
* 所有 Pro 功能
* **高级功能**SSO 集成行级权限
* **高级功能**SSO 集成行级权限和 AI 使用数据
* Twenty 团队支持
* 分发前无需将自定义代码开源
@@ -55,6 +55,7 @@ Pro 计划不包含高级功能(SSO 和行级权限)。
* **SSO 集成**:与您的身份提供商进行单点登录
* **行级权限**:在记录级别进行细粒度访问控制
* **AI 使用数据**:在整个工作区跟踪 AI 消耗
## 切换计划
@@ -77,3 +78,15 @@ Pro 计划不包含高级功能(SSO 和行级权限)。
### 切换为按月计费
联系支持以切换回按月计费。
## 为 Organization (Self-Hosted) 获取企业密钥
要使用 Organization (Self-Hosted) 方案,您需要获取企业密钥:
1. 转到 **设置 → 管理员面板 → 企业版**
<img src="/images/user-guide/billing/enterprise-key.png" alt="企业密钥" />
2. 单击 **获取企业密钥**
3. 当您被重定向到 Stripe 时,输入您的付款信息并确认
4. 当显示出您的企业密钥时,将其粘贴到企业版设置页面并激活 Organization 许可证

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