Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 2d209f1836 UpdateWorkflowVersionStep returns client input step without position validation
https://sonarly.com/issue/4115?type=bug

The UpdateWorkflowVersionStep mutation returns the raw client input step as the GraphQL response without merging position data from the existing step or validating the position object, causing a non-nullable field violation when position is present but incomplete.

Fix: ## Fix: Normalize `position` before returning and persisting the updated step

The root cause is that `updateWorkflowVersionStep` returns the raw client input step (via `enrichOutputSchema`, which is a no-op for non-ITERATOR types) directly as the GraphQL mutation response. When the client sends a `position` object that is present but incomplete (e.g. `{}` or `{x: null}`), the GraphQL executor throws `Cannot return null for non-nullable field WorkflowStepPosition.x` because the schema defines `x: Float!` as non-nullable.

The fix adds a `resolvedPosition` check after `updatedStep` is obtained: if `updatedStep.position` is defined but `x` or `y` is not, fall back to `existingStep.position` (the step stored in the DB). This uses the already-imported `isDefined` utility. The `normalizedUpdatedStep` is then used both for persisting to the DB (`updatedSteps`) and as the return value, also fixing the secondary issue of position data loss during persistence.

```typescript file=packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-update.workspace-service.ts lines=76-105
    const resolvedPosition =
      isDefined(updatedStep.position) &&
      isDefined(updatedStep.position.x) &&
      isDefined(updatedStep.position.y)
        ? updatedStep.position
        : existingStep.position;

    const normalizedUpdatedStep = { ...updatedStep, position: resolvedPosition };

    const updatedSteps = workflowVersion.steps.map((existingStep) => {
      if (existingStep.id === step.id) {
        return normalizedUpdatedStep;
      } else {
        return existingStep;
      }
    });
    ...
    return normalizedUpdatedStep;
```

This mirrors the pattern already used in `updateWorkflowVersionStepType`, which explicitly sets `position: existingStep.position` when building the updated step.
2026-03-04 10:51:46 +00:00
Abdullah.andGitHub 225f185278 fix: fast-xml-parser has stack overflow in XMLBuilder with preserve order (#18375)
Resolves [Dependabot Alert
551](https://github.com/twentyhq/twenty/security/dependabot/551).
2026-03-04 10:36:58 +01:00
Abdullah.andGitHub ca1d49c6cd fix: rollup 4 has arbitrary file write via path traversal (#18373)
Resolves [Dependabot Alert
508](https://github.com/twentyhq/twenty/security/dependabot/508).
2026-03-04 10:35:10 +01:00
Abdullah.andGitHub 2f9c94d9a0 fix: upgrade nestjs dependencies to upgrade multer transitive import (#18374)
Resolves [Dependabot Alert
549](https://github.com/twentyhq/twenty/security/dependabot/549) and
[Dependabot Alert
550](https://github.com/twentyhq/twenty/security/dependabot/550).
2026-03-04 10:34:44 +01:00
a9c4920fcc i18n - docs translations (#18370)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-04 09:16:33 +01:00
07dd27f6c4 i18n - translations (#18368)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-04 01:29:35 +01:00
Charles BochetandGitHub 7a2e397ad1 Complete linaria migration (#18361)
## Summary

Completes the migration of the frontend styling system from **Emotion**
(`@emotion/styled`, `@emotion/react`) to **Linaria** (`@linaria/react`,
`@linaria/core`), a zero-runtime CSS-in-JS library where styles are
extracted at build time.

This is the final step of the migration — all ~494 files across
`twenty-front`, `twenty-ui`, `twenty-website`, and `twenty-sdk` are now
fully converted.

## Changes

### Styling Migration (across ~480 component files)
- Replaced all `@emotion/styled` imports with `@linaria/react`
- Converted runtime theme access patterns (`({ theme }) => theme.x.y`)
to build-time `themeCssVariables` CSS custom properties
- Replaced `useTheme()` hook (from Emotion) with
`useContext(ThemeContext)` where runtime theme values are still needed
(e.g., passing colors to non-CSS props like icon components)
- Removed `@emotion/react` `css` helper usages in favor of Linaria
template literals

### Dependency & Configuration Changes
- **Removed**: `@emotion/react`, `@emotion/styled` from root
`package.json`
- **Added**: `@wyw-in-js/babel-preset`, `next-with-linaria` (for
twenty-website SSR support)
- Updated Nx generator defaults from `@emotion/styled` to
`@linaria/react` in `nx.json`
- Simplified `vite.config.ts` (removed Emotion-specific configuration)
- Updated `twenty-website/next.config.js` to use `next-with-linaria` for
SSR Linaria support

### Storybook & Testing
- Removed `ThemeProvider` from Emotion in Storybook previews
(`twenty-front`, `twenty-sdk`)
- Now relies solely on `ThemeContextProvider` for theme injection

### Documentation
- Removed the temporary `docs/emotion-to-linaria-migration-plan.md`
(migration complete)
- Updated `CLAUDE.md` and `README.md` to reflect Linaria as the styling
stack
- Updated frontend style guide docs across all locales

## How it works

Linaria extracts styles at build time via the `@wyw-in-js/vite` plugin.
All expressions in `styled` template literals must be **statically
evaluable** — no runtime theme objects or closures over component state.

- **Static styles** use `themeCssVariables` which map to CSS custom
properties (`var(--theme-color-x)`)
- **Runtime theme access** (for non-CSS use cases like icon `color`
props) uses `useContext(ThemeContext)` instead of Emotion's `useTheme()`
2026-03-04 00:50:06 +01:00
Paul RastoinandGitHub 8a3b96d911 Remove files (#18360) 2026-03-03 19:01:27 +01:00
Paul RastoinandGitHub 132a19f688 [SDK] Execute logic function e2e test (#18351)
# Introduction
Creating an e2e test covering the execute logic function public
operation
2026-03-03 17:44:19 +01:00
Charles BochetandGitHub 3bfdc2c83f chore(twenty-front): migrate command-menu, workflow, page-layout and UI modules from Emotion to Linaria (PR 4-6/10) (#18342)
## Summary

Continues the Emotion → Linaria migration (PR 4-6 from the [migration
plan](docs/emotion-to-linaria-migration-plan.md)). Migrates **311
files** across four module groups:

| Module | Files |
|---|---|
| command-menu | 53 |
| workflow | 84 |
| page-layout | 84 |
| UI (partial - first ~80 files) | ~80 |
| twenty-ui (TEXT_INPUT_STYLE) | 1 |
| misc (hooks, keyboard-shortcut-menu, file-upload) | ~9 |

### Migration patterns applied

- `import styled from '@emotion/styled'` → `import { styled } from
'@linaria/react'`
- `import { useTheme } from '@emotion/react'` → `import { useContext }
from 'react'` + `import { ThemeContext } from 'twenty-ui/theme'`
- `${({ theme }) => theme.X.Y.Z}` → `${themeCssVariables.X.Y.Z}` (static
CSS variables)
- `theme.spacing(N)` → `themeCssVariables.spacing[N]`
- `styled(motion.div)` → `motion.create(StyledBase)` (11 components)
- `styled(Component)<TypeParams>` → wrapper div approach for non-HTML
elements
- Multi-declaration interpolations split into one CSS property per
interpolation
- Interpolation return types fixed (`&&` → ternary `? : ''`)
- `TEXT_INPUT_STYLE` converted from function to static string constant
(backward compatible)
- Emotion `<Global>` replaced with `useEffect` style injection
- Complex runtime-dependent styles use CSS custom properties via
`style={}` prop

### After this PR

- **Remaining files**: ~400 (object-record: ~160, settings: ~200, UI:
~44)
- **No breaking changes**: CSS variables resolve identically to the
previous Emotion theme values
2026-03-03 16:42:03 +01:00
martmullandGitHub 8b26020a0b Fix missing omit in application config (#18354)
as title
2026-03-03 16:33:49 +01:00
b1107c823a Apollo enrich (#18277)
- apollo enrich application (via OAuth 2)
- add applicationId to var env in logic function executor
- update `getDefaultUrl` logic

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-03 14:43:10 +01:00
martmullandGitHub 5c4a1f931a Fix trigger missing (#18348)
## After

<img width="1170" height="448" alt="image"
src="https://github.com/user-attachments/assets/3a8dd77c-06d2-438e-b2d3-07d272c08588"
/>

<img width="977" height="441" alt="image"
src="https://github.com/user-attachments/assets/3e16ec94-63ce-4d09-af54-38f330bfa0c9"
/>

<img width="802" height="333" alt="image"
src="https://github.com/user-attachments/assets/0975f199-b89a-4a05-a8e4-2101f5556445"
/>
2026-03-03 14:37:17 +01:00
4266f4022a i18n - translations (#18349)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-03 14:24:43 +01:00
Thomas TrompetteandGitHub b2b3a3f860 Workflow iterator continues on faillure (#18325)
<img width="450" height="212" alt="Capture d’écran 2026-03-03 à 11 41
54"
src="https://github.com/user-attachments/assets/b2c29a48-7dc0-4b16-a085-8f305d21f7ca"
/>

New status `FAIL_SAFE` added. This status propagates to the following
nodes until reaching the iterator, that will start the new iteration.

The difference with `SKIP` is that, when the parent nodes have at least
one `FAIL_SAFE`, it becomes `FAIL_SAFE` too. While a parent 1 `SKIP` +
parent 2 `SUCCESS` => to be executed.

I also thought about just going back to the iterator as a break would,
but since we have branches, it may lead to inconsistent statuses with
parallel updates.
2026-03-03 14:18:57 +01:00
Charles BochetandGitHub d48c58640c Migrate CI runners from Depot back to GitHub-hosted runners (#18347)
## Summary
- Replaces all `depot-ubuntu-24.04` runners with `ubuntu-latest`
- Replaces all `depot-ubuntu-24.04-8` runners with
`ubuntu-latest-8-cores`
- Updates storybook build cache keys in ci-front.yaml to reflect the
runner name change

Reverts the temporary Depot migration introduced in #18163 / #18179
across all 23 workflow files.
2026-03-03 14:14:27 +01:00
Paul RastoinandGitHub 005223de8c [SDK] Make public-operations non throw (#18343)
Followup https://github.com/twentyhq/twenty/pull/18320
2026-03-03 14:04:13 +01:00
Paul RastoinandGitHub 2f09fb8c04 SDK Split command and cli logic (#18320)
# Introduction

Allow a consumer call the commands programmatically instead of passing
by the exec
To do so extract from the command definition all the core logic, created
a new error api that allow keeping same error logs granularity than
before

## Usage
```ts
import { authLogin, appUninstall, functionExecute } from 'twenty-sdk/cli';

const result = await authLogin({
  apiKey: 'my-key',
  apiUrl: 'https://my-twenty.com',
});

if (!result.success) {
  throw new Error(result.error);
}
```

## `app:build`
Introduced a new command that will allow building the whole project
without any watch setup
- Build and validate manifest
- Get or create app
- Synchronize manifest with twenty-sdk stub and no typecheck
- generate client
- Run typecheck
- Synchronize manifest again
2026-03-03 12:24:49 +01:00
neo773andGitHub 083df3e7ca OAuth Edge case crash + cleanup (#18326)
Fixes Sentry issue https://twenty-v7.sentry.io/issues/6603377117/

Also cleaned up the code with proper types removing `//
eslint-disable-next-line @typescript-eslint/no-explicit-any`
2026-03-03 12:22:12 +01:00
martmullandGitHub 2e9624858c Fix name singular updates in dev mode (#18339)
as title
2026-03-03 11:40:17 +01:00
Paul RastoinandGitHub 58e37a118c Builder runs delete update and then create (#18272)
# Introduction
We need to build and validate the flat entity operation in the following
order delete update and create
For example if not, if a created field has the same name than a deleted
one than it will fail whereas it should not
2026-03-03 11:26:06 +01:00
Paul RastoinandGitHub 0b766464e4 Composite action: Spawn twenty instance (#18317)
# Introduction

## Runs:

Public personal repo:
-
[latest](https://github.com/prastoin/twenty-app/actions/runs/22568051680/job/65368592903)
2026-03-03 11:19:08 +01:00
Charles BochetandGitHub 802a5b0af6 chore(twenty-front): migrate auth, activities, AI, pages and small modules from Emotion to Linaria (PR 2-3/10) (#18328)
## Summary

- Migrate ~200 files from `@emotion/styled` / `@emotion/react` to
`@linaria/react` + `themeCssVariables`, continuing the zero-runtime
CSS-in-JS migration (PR 2-3 of the [migration
plan](docs/emotion-to-linaria-migration-plan.md))
- Modules covered: **auth** (19), **activities** (53), **ai** (30),
**pages** (70), **action-menu** (3), **object-metadata** (4),
**onboarding** (2), **workspace** (2), **file** (3), **error-handler**
(2), **front-components** (1), **geo-map** (1), **loading** (5),
**testing** (5), plus a `style` prop addition to `TableRow`
- Handles `styled(FunctionComponent)<Props>` incompatibility with
Linaria by using CSS custom properties via `style` + `var()` references

## Test plan

- [x] `npx nx lint:diff-with-main twenty-front` passes
- [x] `npx nx typecheck twenty-front` passes
- [ ] Visual spot-check of auth, onboarding, settings, activities, and
AI chat screens
- [ ] No remaining `@emotion/styled` or `@emotion/react` imports in
migrated files


Made with [Cursor](https://cursor.com)
2026-03-03 11:17:47 +01:00
Abdul RahmanandGitHub ae291c99ba fix: record does not open in side panel after returning from fullscreen (#17131)
Closes #17089 

### 1. Can't reopen record after having navigated to its show page
After opening a record in the show page from the command menu and going
back to the index, clicking the same record again did nothing. The
command menu navigation stack was not cleared when opening in the show
page, so the "already open" check skipped reopening. We now clear the
command menu navigation stack before navigating to the show page (in
`RecordShowRightDrawerOpenRecordButton`), so the same record can be
reopened from the index.

### 2. Row doesn't highlight when opening command menu after return from
show page
After returning from the record show page to the index, the first row
click opened the command menu but the row did not highlight. The "side
panel close" event was emitted not only when the panel actually closed,
but also when opening the command menu (cleanup ran with
`isCommandMenuClosing` and always emitted the event). Listeners like
`RecordTableDeactivateRecordTableRowEffect` then deactivated the row. We
now emit the side panel close event only when the close animation
actually completes (`CommandMenuSidePanelForDesktop`), and skip emitting
it when cleanup is run from the open path (`useNavigateCommandMenu`
passes `emitSidePanelCloseEvent: false`). The table still deactivates
the row when the user closes the panel, but no longer when they open the
command menu by clicking a row.
2026-03-02 23:25:11 +00:00
7809f83e72 fix: [Note] Title not filled by default #13838 (#18297)
Fixes #13838 
When creating a note from the command menu side panel (e.g. clicking
"Add Note" in a related notes section on an Opportunity/company/people
page), the title field was not auto-focused — focus point went to body
instead.

## Root Cause

When a record opens in the side panel, there is no page navigation, so
`PageChangeEffect` (which handles title auto-focus for full-page views)
never runs. `openNewRecordTitleCell()` was simply never called for the
side-panel path.

## Fix

`openRecordInCommandMenu` is the single entry point for all side-panel
record opens, so title auto-focus is handled there once for all callers.
Previously, `useCreateNewIndexRecord` called `openRecordInCommandMenu`
and then called `openNewRecordTitleCell` separately, which would have
caused a double invocation after this fix. The redundant call has been
removed.

## Before


https://github.com/user-attachments/assets/df0d9e4f-dc25-4a0d-a49e-898a14f9c0a0

## After


https://github.com/user-attachments/assets/1a5044f7-6bb7-4333-8934-c1081b935e97

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-02 20:09:36 +00:00
27847f6ac6 i18n - translations (#18330)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-02 21:00:18 +01:00
Félix MalfaitandGitHub 6351c6c1c6 feat: remember original URL and redirect after login (#18308)
## Summary

- Implement a return-to-path mechanism that preserves the user's
intended destination across authentication flows (login, magic link,
cross-domain redirects)
- Uses layered persistence: Jotai atom (in-memory), sessionStorage with
TTL (tab-switch resilience), URL query parameter (cross-domain
propagation)
- Includes path validation to prevent open redirects, automatic cleanup
after successful login, and comprehensive test coverage
- Replaces the unused `previousUrlState` with a robust
`returnToPathState` system

## Test plan

- [ ] Visit a deep link (e.g. `/objects/tasks`) while logged out —
should redirect to login, then back to `/objects/tasks` after logging in
- [ ] Visit an OAuth authorize link while logged out — should redirect
to login, then to the authorize page
- [ ] Test magic link flow: click sign-in link that opens new tab —
should still redirect to original destination
- [ ] Test cross-domain: visit `app.twenty.com/objects/tasks` — should
preserve path through workspace domain redirect
- [ ] Verify auth/onboarding paths are excluded from being saved as
return paths
- [ ] Verify return-to-path is cleared after successful navigation
- [ ] All 215 existing `usePageChangeEffectNavigateLocation` tests pass


Made with [Cursor](https://cursor.com)
2026-03-02 19:00:48 +01:00
Abdullah.andGitHub 20a2c3836e feat: introduce role selector when inviting members to a workspace (#18085)
This PR adds an explicit role selector to the "Invite by email" flow,
requires a role choice before sending, and stores the selected role with
each invitation. The backend now accepts and persists `roleId` on
invitations and applies it when the invite is accepted, while keeping it
optional to avoid breaking existing clients and legacy invites.

---

### Frontend

- **Settings → Members → Invite by email**
- New **Role** dropdown (same `Select` pattern as member/API key role
selectors) between the email input and Invite button.
- Roles are loaded via `SettingsRolesQueryEffect` and
`settingsAllRolesSelector`; only roles with `canBeAssignedToUsers` are
shown.
- Role is **required**: form validates `roleId` (e.g.
`z.string().min(1)`) and the Invite button is disabled until a role is
selected and emails are valid.
- `WorkspaceInviteTeam` receives `roles` as a prop from the parent;
layout is responsive (e.g. stacked on small viewports).
- **Pending invitations table**
- New **Role** column showing the invitation’s role label (or "Unknown
role" for legacy invites without `roleId`), using the same roles source
for lookup.
- **Onboarding invite step**
- When sending invites during onboarding, the workspace **default role**
is used when available (`currentWorkspace?.defaultRole?.id`), so no role
selector is added there.
- **GraphQL**
- `sendInvitations` mutation accepts optional `roleId`;
`findWorkspaceInvitations` and resend mutation responses include
`roleId` on `WorkspaceInvitation`. Frontend types (e.g.
`WorkspaceInvitation`, hook variables) updated accordingly.

---

### Backend

- **API**
- `SendInvitationsInput` has an **optional** `roleId` (UUID, nullable).
The resolver normalises `null` to `undefined` so existing callers and
legacy flows are not broken.
- **Validation (when `roleId` is provided)**
- Role checks are centralised in **RoleValidationService**
(`RoleValidationModule`, in `metadata-modules/role-validation/`). It
validates that the role exists in the workspace and has
`canBeAssignedToUsers`, and throws a permissions-style error otherwise.
This avoids circular dependencies (e.g. `RoleModule` imports
`UserWorkspaceModule`, so invite/accept flows cannot depend on
`RoleModule`).
- **Send flow:** `WorkspaceInvitationResolver` and
`WorkspaceInvitationService.sendInvitations` both call
`RoleValidationService.validateRoleAssignableToUsersOrThrow` when
`roleId` is present (resolver before calling the service; service again
before creating tokens so that **resend** also validates the stored role
and fails fast if the role was deleted or made unassignable).
- **Accept flow:**
`UserWorkspaceService.addUserToWorkspaceIfUserNotInWorkspace` uses the
same service in `resolveRoleIdForNewMember` when an invitation provides
a `roleId`, then falls back to `workspace.defaultRoleId` when not.
Role/default is resolved and validated before any user/workspace/member
creation.
- **Persistence**
- Invitation app tokens store `roleId` in `context` next to `email`
(`context: { email, roleId? }`). `generateInvitationToken` and
`createWorkspaceInvitation` accept an optional `roleId` and only add it
to `context` when defined.
- **Resend**
- Resend passes the existing invitation’s `context.roleId` into
`sendInvitations`. The service validates that role (when present) before
creating the new token, so if the role was deleted or made unassignable,
resend fails with a clear error instead of sending a broken link.
- **Response shape**
- `SendInvitationsOutput.result` remains `WorkspaceInvitation[]`. When
`usePersonalInvitation` is false we only push full invitation records
(from `castAppTokenToWorkspaceInvitationUtil`), so the result always
matches the GraphQL type (`id`, `email`, `roleId`, `expiresAt`).
- **Modules**
- `WorkspaceInvitationModule` and `UserWorkspaceModule` import
**RoleValidationModule** (not `RoleModule`) and inject
**RoleValidationService** for validation. `RoleModule` imports
`RoleValidationModule` and `RoleService` delegates to
`RoleValidationService` for the same validation where the module graph
allows.

---

### Backward compatibility

- **Optional `roleId`**: Clients that don’t send `roleId` (or send
`null`) are unchanged; invitations are created without a role and the
accept flow uses the workspace default role.
- **Legacy invitations**: App tokens with only `context.email` still
work; `context.roleId` is optional and the UI can show e.g. "Unknown
role" for those in the pending-invitations table.
2026-03-02 18:58:32 +01:00
nitinandGitHub 1eb284c87f Fix command menu text/number inputs to commit on blur and cancel cleanly on Escape (#18283)
closes https://github.com/twentyhq/twenty/issues/18264




https://github.com/user-attachments/assets/7b576a00-78bc-46a2-9528-d8b3bcbdd530




https://github.com/user-attachments/assets/4102468e-e85f-46a0-8b23-e7abd77bfc95



### PR description -
This fixes flaky persistence in command menu text and number inputs.

- moved commit logic to onBlur (single commit path)
- Enter now blurs, so it uses the same commit path
- Escape now cancels edit (restores draft + exits) without persisting
- removed dependency on input click-outside commit timing

### Outcome -

- clicking anywhere outside the input now reliably persists edits
- Escape consistently discards edits
2026-03-02 15:30:51 +00:00
Charles BochetandGitHub c4140f85df chore(twenty-front): migrate small modules from Emotion to Linaria (PR 1/10) (#18314)
## Emotion → Linaria migration — PR 1 of 10

First batch of the `twenty-front` migration from Emotion (runtime
CSS-in-JS) to Linaria (zero-runtime, build-time extraction via
wyw-in-js). Covers **100 files** across 10 small standalone modules —
chosen as the lowest-risk starting point.

### Modules migrated

spreadsheet-import (28) · navigation-menu-item (17) · views (14) ·
billing (10) · blocknote-editor (7) · advanced-text-editor (7) ·
favorites (7) · navigation (4) · information-banner (3) ·
sign-in-background-mock (3)

### Migration pattern

Every file follows the same mechanical transformation:

| Emotion | Linaria |
|---|---|
| `import styled from '@emotion/styled'` | `import { styled } from
'@linaria/react'` |
| `${({ theme }) => theme.font.color.primary}` |
`${themeCssVariables.font.color.primary}` |
| `${({ theme }) => theme.spacing(4)}` |
`${themeCssVariables.spacing[4]}` |
| `const theme = useTheme()` | `const { theme } =
useContext(ThemeContext)` |
| `import { type Theme } from '@emotion/react'` | `import { type
ThemeType } from 'twenty-ui/theme'` |

`themeCssVariables` is a build-time object where every leaf is a
`var(--t-xxx)` CSS custom property reference, evaluated statically by
wyw-in-js. Runtime theme access (icon sizes, colors passed as props)
uses `useContext(ThemeContext)`.

### Gotchas encountered & fixed

- **Interpolation return types** — wyw-in-js requires `string | number`,
never `false`/`undefined`. Replaced `condition && 'css'` with `condition
? 'css' : ''`.
- **`css` tag inside `styled` templates** — Linaria `css` returns a
class name, not CSS text. Replaced with plain template strings.
- **`styled(Component)` needs `className`** — added `className` prop to
`NavigationDrawerSection`, `DropdownMenuItemsContainer`, and `Heading`.
- **`shouldForwardProp` not supported** — Linaria filters invalid DOM
props automatically for HTML elements. For custom components, used
wrapper divs where needed.
- **`FormFieldPlaceholderStyles`** — converted from Emotion `css`
function to a static string using `themeCssVariables`.
2026-03-02 16:33:40 +01:00
Charles BochetandGitHub 9c4b0f526c Refactor chip component hierarchy: AvatarChip → AvatarOrIcon (#18313)
## Summary

Cleans up the chip component hierarchy in `twenty-ui`:

- **Fix twenty-ui Storybook** — The `wyw-in-js` Vite plugin crashed on
`/@react-refresh` virtual module. Fixed by setting `enforce: 'pre'` so
it runs before the React refresh plugin injects virtual imports.
- **Rename `AvatarChip` → `AvatarOrIcon`** — The old name was
misleading. This component is not a chip — it's a polymorphic renderer
that displays either an `Avatar` (image/initials) or an `Icon` (plain or
with colored background). It's typically slotted into `Chip`/`LinkChip`
as `leftComponent`.
- **Move `rightComponentDivider` to `Chip`/`LinkChip`** — The vertical
separator between chip content and a right action (e.g. a close button)
is a chip layout concern, not an avatar concern. Added
`rightComponentDivider` boolean prop to `Chip` and `LinkChip`.
- **Remove `MultipleAvatarChip`** — Zero consumers in the codebase. The
command menu implements its own overlapping avatar layout.
- **Migrate raw icon usages** — `CalendarEventDetails` and `FileIcon`
(small size) now use `AvatarOrIcon` for consistent Chip icon rendering.
- **Enhance stories** — Full `CatalogDecorator` coverage for `Chip` and
`LinkChip` showing all variants, sizes, accents, and states.

## Component hierarchy

```
AvatarOrIcon (twenty-ui)
  ├── No Icon → renders Avatar (image or initials)
  ├── Icon + background → renders icon in colored square
  └── Icon only → renders plain icon
  Used as leftComponent/rightComponent in Chip or standalone

Chip (twenty-ui)
  ├── leftComponent (typically AvatarOrIcon)
  ├── label (with overflow tooltip)
  ├── rightComponentDivider (optional vertical separator)
  └── rightComponent (e.g. close icon via AvatarOrIcon)

LinkChip (twenty-ui)
  └── Wraps Chip inside a react-router <Link>

RecordChip (twenty-front)
  └── Composes Chip/LinkChip + AvatarOrIcon with record data
```

## `Chip` API additions

| Prop | Type | Description |
|------|------|-------------|
| `rightComponentDivider` | `boolean` | Renders a vertical separator
before `rightComponent` |

## Stories

<img width="1032" height="576" alt="image"
src="https://github.com/user-attachments/assets/fe7c7666-9b16-4545-b87e-1b53e22d462d"
/>
2026-03-02 15:48:49 +01:00
WeikoandGitHub 37bcb35391 Migrate pagelayout position frontend (#18229)
## Context
Part 1 of migrating gridPosition in favor of typed position
FE should now always send both values to the BE and use both.

Next steps: 
- Update the backend to enforce and validate the new position field + DB
migrations gridPositon -> position (type: GRID)
- Cleanup frontend usage
- Cleanup backend
2026-03-02 14:42:30 +01:00
Thomas TrompetteandGitHub 78a0197643 Prevent deletion of il-else branches (#18294)
If-else branches cannot be recreated once deleted. Only else-if branches
can. On if-else branches removal, we now remplace the node by an empty
node instead of only deleting

Also fixing nested if-else.
2026-03-02 13:52:32 +01:00
ff3326a53b i18n - translations (#18323)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-02 13:46:36 +01:00
martmullandGitHub 5e92fb4fc6 Do not console.log while consoleListener (#18322)
It can occur infinite loops

see
https://twenty-v7.sentry.io/issues/7269592888/?environment=prod&environment=prod-eu&project=4507072499810304&query=is%3Aunresolved%20!issue.type%3A%5Bperformance_consecutive_db_queries%2Cperformance_consecutive_http%2Cperformance_file_io_main_thread%2Cperformance_db_main_thread%2Cperformance_n_plus_one_db_queries%2Cperformance_n_plus_one_api_calls%2Cperformance_p95_endpoint_regression%2Cperformance_slow_db_query%2Cperformance_render_blocking_asset_span%2Cperformance_uncompressed_assets%2Cperformance_http_overhead%2Cperformance_large_http_payload%5D%20timesSeen%3A%3E10&referrer=issue-stream&sort=freq
2026-03-02 12:04:12 +00:00
Félix MalfaitandGitHub 1a8be234de OAuth security hardening: RFC compliance, PKCE binding, rate limiting (#18305)
## Summary

Follow-up to #18267. Hardens the OAuth implementation with security
fixes identified during audit:

**P0 — Critical:**
- Bind authorization codes to `client_id` in context to prevent auth
code injection (RFC 6749 §4.1.3)
- Store PKCE `code_challenge` directly in auth code context instead of a
separate `CodeChallenge` token — cryptographically binds the challenge
to its code
- Enforce `code_verifier` when `code_challenge` was used during
authorization
- Hash authorization codes (SHA-256) before storage to prevent exposure
if DB is compromised
- Add `Cache-Control: no-store` + `Pragma: no-cache` headers on token
responses (RFC 6749 §5.1)
- Add rate limiting on `/oauth/token` endpoint (20 req/min per client
via existing `ThrottlerService`)

**P1 — High:**
- Return HTTP 401 for `invalid_client` errors instead of 400 (RFC 6749
§5.2)
- Verify refresh tokens belong to the presenting client (cross-client
token theft prevention)
- Limit fields exposed by public `findApplicationRegistrationByClientId`
query to only what the frontend needs (`id`, `name`, `logoUrl`,
`websiteUrl`, `oAuthScopes`)
- Require `API_KEYS_AND_WEBHOOKS` permission for
`createApplicationRegistration` mutation

**P2/P3 — Medium/Low:**
- Add error handling and loading states to frontend Authorize page
- Rename redirect URL param from `authorizationCode` to `code` (RFC
standard)
- Add unit tests for `validateRedirectUri` utility (8 test cases)

## Test plan

- [ ] Existing OAuth integration tests updated for all changes (hashed
codes, context-based PKCE, client binding, 401 status codes, cache
headers)
- [ ] New test: auth code rejected when presented by a different client
- [ ] New test: refresh token rejected when presented by a different
client
- [ ] New test: `code_verifier` required when PKCE was used in
authorization
- [ ] New test: `Cache-Control: no-store` header present on responses
- [ ] New unit tests for `validateRedirectUri` (HTTPS, localhost,
fragments, invalid URIs)
- [ ] Verify frontend authorize page shows errors gracefully


Made with [Cursor](https://cursor.com)
2026-03-02 12:21:26 +01:00
martmullGitHubcubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
d021f7e369 Fix self host application (#18292)
- Fixes self host application
- add new telemetry information
- add serverId to identify a server instance
- remove .twenty from git tracking
- tree-shake "twenty-sdk" usage in built logic functions and front
components
- fix "twenty-sdk" version usage
- fix twenty-zapier cli

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-03-02 12:06:05 +01:00
neo773andGitHub 1b67ba6a75 Draft emails fix onblur on text input and callout banner component overflow (#18310)
Before

<img width="396" height="759" alt="SCR-20260301-daft"
src="https://github.com/user-attachments/assets/c3fb3a19-3456-424d-9fd2-dd13ed0d2ad5"
/>

After

<img width="391" height="752" alt="SCR-20260301-daio"
src="https://github.com/user-attachments/assets/80a64991-6e69-4ddf-b968-bdc788af02cd"
/>
2026-03-02 12:04:30 +01:00
5afc46ebd3 i18n - translations (#18321)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-02 12:03:13 +01:00
a06abb1d60 Fields widget rename group (#18169)
## Rename


https://github.com/user-attachments/assets/b151683a-d1ae-447f-9d9f-95a14b50608b

## Delete



https://github.com/user-attachments/assets/8da73a33-1c57-4771-b712-527b8080117d

---------

Co-authored-by: Weiko <corentin@twenty.com>
2026-03-02 10:32:31 +00:00
Félix Malfait 2a5b2746c9 Fix preview-env-dispatch: repository_dispatch requires contents:write
The `repository_dispatch` API endpoint requires `contents: write`
permission on the GITHUB_TOKEN, not `actions: write`. Our security
hardening PR inadvertently changed this to `contents: read`, breaking
the self-dispatch to the keepalive workflow.

Made-with: Cursor
2026-03-02 11:25:14 +01:00
Félix MalfaitandGitHub 0223975bbd Harden GitHub Actions: fix injections, isolate privileged operations to ci-privileged repo (#18318)
## Summary

- Fix expression injection vulnerabilities in composite actions
(`restore-cache`, `nx-affected`) and workflow files (`claude.yml`)
- Reduce overly broad permissions in `ci-utils.yaml` (Danger.js) and
`ci-breaking-changes.yaml`
- Restructure `preview-env-dispatch.yaml`: auto-trigger for members,
opt-in for contributor PRs via `preview-app` label (safe because
keepalive has no write tokens)
- Isolate all write-access operations (PR comments, cross-repo posting)
to a new dedicated
[`twentyhq/ci-privileged`](https://github.com/twentyhq/ci-privileged)
repo via `repository_dispatch`, so that workflows in twenty that execute
contributor code never have write tokens
- Create `post-ci-comments.yaml` (`workflow_run` bridge) to dispatch
breaking changes results to ci-privileged, solving the [fork PR comment
issue](https://github.com/twentyhq/twenty/pull/13713#issuecomment-3168999083)
- Delete 5 unused secrets and broken `i18n-qa-report` workflow
- Remove `TWENTY_DISPATCH_TOKEN` from twenty (moved to ci-privileged as
`CORE_TEAM_ISSUES_COMMENT_TOKEN`)
- Use `toJSON()` for all `client-payload` values to prevent JSON
injection

## Security model after this PR

| Workflow | Executes fork code? | Write tokens available? |
|----------|---------------------|------------------------|
| preview-env-keepalive | Yes | None (contents: read only) |
| preview-env-dispatch | No (base branch) | CI_PRIVILEGED_DISPATCH_TOKEN
only |
| ci-breaking-changes | Yes | None (contents: read only) |
| post-ci-comments (workflow_run) | No (default branch) |
CI_PRIVILEGED_DISPATCH_TOKEN only |
| claude.yml | No (base branch) | CI_PRIVILEGED_DISPATCH_TOKEN,
CLAUDE_CODE_OAUTH_TOKEN |
| ci-utils (Danger.js) | No (base branch) | GITHUB_TOKEN (scoped) |

All actual write tokens (`TWENTY_PR_COMMENT_TOKEN`,
`CORE_TEAM_ISSUES_COMMENT_TOKEN`) live in `twentyhq/ci-privileged` with
strict CODEOWNERS review and branch protection.

## Test plan

- [ ] Verify preview environment comments still appear on member PRs
- [ ] Verify adding `preview-app` label triggers preview for contributor
PRs
- [ ] Verify breaking changes reports still post on PRs (including fork
PRs)
- [ ] Verify Claude cross-repo responses still post on core-team-issues
- [ ] Confirm ci-privileged branch protection is enforced
2026-03-02 10:57:14 +01:00
Félix MalfaitandGitHub 8d47d8ae38 Fix E2E tests broken by redesigned navigation menu (#18315)
## Summary
- **Settings selector**: The Settings navigation item is now rendered as
a `<button>` (via `NavigationDrawerItem` with `onClick`) instead of an
`<a>` link (with `to`). Updated `leftMenu.ts` POM and
`create-kanban-view.spec.ts` to use `getByRole('button', { name:
'Settings' })`.
- **create-record URL field**: The Linkedin field interaction was
missing an initial label click to trigger the hover portal rendering.
Added `recordFieldList.getByText('Linkedin').first().click()` before the
value click, matching the pattern used by the working Emails field.

## Test plan
- [ ] E2E `signup_invite_email.spec.ts` passes (uses
`leftMenu.goToSettings()`)
- [ ] E2E `create-kanban-view.spec.ts` passes (uses Settings click
directly)
- [ ] E2E `create-record.spec.ts` passes (Linkedin URL field
interaction)
- [ ] Existing passing E2E tests remain green


Made with [Cursor](https://cursor.com)
2026-03-02 10:52:10 +01:00
Félix MalfaitandGitHub 68d2297338 Fix expression injection in cross-repo GitHub Actions workflow (#18316)
## Summary

- Fixes a script injection vulnerability in the `claude-cross-repo`
job's `actions/github-script` step where `${{ steps.prompt.outputs.repo
}}` and `${{ steps.prompt.outputs.issue_number }}` were interpolated
directly into JavaScript string literals. A crafted dispatch payload
could inject arbitrary JavaScript with access to
`secrets.TWENTY_DISPATCH_TOKEN`.
- Values are now passed via `env:` and accessed through `process.env`,
which treats them as data rather than code.

## Context

Motivated by the [hackerbot-claw
campaign](https://www.stepsecurity.io/blog/hackerbot-claw-github-actions-exploitation)
which exploited similar `${{ }}` expression injection patterns in
workflows at Microsoft, DataDog, and CNCF projects.

The broader analysis found that our workflow is **not vulnerable** to
the primary attack vector (Pwn Request via `pull_request_target` +
untrusted checkout), and `claude-code-action` already gates on write
access internally. This expression injection in the cross-repo dispatch
job was the only concrete vulnerability identified.

## Test plan

- [ ] Verify the `claude-cross-repo` job still posts comments back to
the source issue after a dispatch run
- [ ] Confirm `TARGET_REPO` and `TARGET_ISSUE` env vars are correctly
resolved from step outputs


Made with [Cursor](https://cursor.com)
2026-03-02 09:20:03 +01:00
Charles BochetandGitHub 1db2a40961 Migrate twenty ui to linaria (#18307)
## Migrate twenty-ui from Emotion to Linaria

Completes the migration of all `twenty-ui` components from Emotion
(runtime CSS-in-JS) to Linaria (zero-runtime, CSS extracted at build
time).

- Replaced `@emotion/styled` with `@linaria/react` across ~170 files
- Removed all Emotion dependencies from `twenty-ui`
- Introduced a CSS custom properties-based theme system:
`themeCssVariables` where every leaf is a `var(--t-xxx)` reference,
injected onto `document.documentElement` by
`ThemeCssVariableInjectorEffect`
- No more `theme` prop threading — styled components reference
`themeCssVariables.x.y` directly at build time
- Updated `twenty-front` consumers to remove `theme={theme}` prop
passing

**Before / After:**
```tsx
// Emotion
color: ${({ theme }) => theme.font.color.primary};
padding: ${({ theme }) => theme.spacing(4)};

// Linaria
color: ${themeCssVariables.font.color.primary};
padding: ${themeCssVariables.spacing[4]};
```

### Theme architecture

Two build-time utilities produce the theme system:

- **`buildThemeReferencingRootCssVariables`** — walks the theme object
and builds a nested mirror where every leaf is a `var(--t-xxx)` string
(evaluated at build time by wyw-in-js)
- **`prepareThemeForRootCssVariableInjection`** — walks the runtime
theme and collects flat `[--css-variable-name, value]` pairs, injected
onto `document.documentElement` by `ThemeCssVariableInjectorEffect`

Both share naming conventions (`camelToKebab`, `SPACING_VALUES`,
`formatSpacingKey`) and are unit tested.

### Spacing cleanup

Spacing scale now uses integers 0–32 (generated via loop), with `0.5`
and `1.5` as the only fractional exceptions. All other fractional
spacing usages (`0.25`, `0.75`, `1.25`, `2.5`, `3.5`) were replaced with
literal pixel values across ~20 twenty-front files.

### Framer Motion integration

Linaria doesn't support `styled(motion.div)` — wrapping a motion element
with `styled()` causes the component body to be stripped at build time.
Instead, we define the styled component first, then wrap it with
`motion.create()`:

```tsx
const StyledBarBase = styled.div`
  background-color: ${themeCssVariables.font.color.primary};
  height: 100%;
`;

const StyledBar = motion.create(StyledBarBase);
```

### Block interpolations

Linaria doesn't support interpolations that return multiple CSS
declarations (Linaria wraps the entire block in a single `var()`,
producing invalid CSS). These were split into individual property
interpolations:

```tsx
// Emotion — single interpolation returning multiple declarations
border-left: ${({ divider, theme }) => {
  const border = `1px solid ${theme.border.color.light}`;
  return divider ? `border-${divider}: ${border}` : '';
}}

// Linaria — one interpolation per property
border-left: ${({ divider }) =>
  divider === 'left' ? `1px solid ${themeCssVariables.border.color.light}` : 'none'};
border-right: ${({ divider }) =>
  divider === 'right' ? `1px solid ${themeCssVariables.border.color.light}` : 'none'};
```

### Dynamic styles via CSS variables

When a component needs to compute styles from multiple props with
complex branching logic (e.g. `Button` combining `variant`, `accent`,
`inverted`, `disabled`, `focus`, `position`), Linaria's prop
interpolations become unwieldy. In those cases we use a
`computeDynamicStyles` function that returns a `CSSProperties` object
injected via `style={}`, referenced from the static CSS with `var()`:

```tsx
const StyledButton = styled.button`
  background: var(--btn-bg);
  border-color: var(--btn-border-color);
  &:hover { background: var(--btn-hover-bg); }
`;

const dynamicStyles = useMemo(() => {
  const s = computeButtonDynamicStyles(variant, accent, ...);
  return { '--btn-bg': s.background, '--btn-hover-bg': s.hoverBackground } as CSSProperties;
}, [variant, accent, ...]);

return <StyledButton style={dynamicStyles} />;
```

### CSS var + unit concatenation

CSS custom properties can't be concatenated with unit suffixes directly
(`var(--x)px` is invalid). Values that need units use `calc()`:

```tsx
// Broken
transition: background ${themeCssVariables.animation.duration.instant}s ease;

// Fixed
transition: background calc(${themeCssVariables.animation.duration.instant} * 1s) ease;
```
2026-03-01 15:13:42 +01:00
Charles BochetandGitHub 159bb9d70a A few fixes on table performance (#18304)
## RecordTable Performance Investigation & Optimization (WIP)

Investigates what makes the RecordTable slow (14 components, ~12 hooks
per cell) and starts applying fixes.

### Key Findings (2,000 cells benchmark)

![Cell Render
Benchmark](https://github.com/twentyhq/twenty/blob/table-perf/packages/twenty-front/src/modules/object-record/record-table/__perf__/cell-render-benchmark.png?raw=true)

![State Access
Benchmark](https://github.com/twentyhq/twenty/blob/table-perf/packages/twenty-front/src/modules/object-record/record-table/__perf__/state-access-benchmark.png?raw=true)

- **Jotai atoms are the dominant cost**: 10 atom reads/cell = +312%.
Full sim with atoms = +476%.
- **Derived atoms are 3x cheaper** than individual reads (12 sources:
+93% vs +294%).
- **Component depth is expensive**: 4-level nesting = +82%, 14 wrappers
= +109%.
- **Styling engines are comparable**: Linaria vs Emotion is within
noise.
- **Context reads and useState are nearly free** vs baseline.

### Optimizations Applied

1. **Static focus providers** — replaced per-cell `useState(false)` with
static context. Eliminates 400 useState instances.
2. **Delegated onMouseMove** — single handler on table body instead of
400 per-cell handlers.
3. **Hoisted `useObjectMetadataItems()`** — moved from per-cell to
table-level context. Eliminates 400 global atom reads.

### Tooling

- Perf page at `/__perf__/table`: 17 cell render + 13 state access
benchmarks
- Render profiler: `window.__RECORD_TABLE_PROFILE = true`
- Full plan in `__perf__/PERFORMANCE_PLAN.md`

### Remaining Phases (not high priority to-be-honest)

| Phase | What | Status |
|-------|------|--------|
| 1 | Separate display from interaction | Partial |
| 2 | Flatten hierarchy (14 → ~5 components/cell) | TODO |
| 3 | Reduce atom reads per cell | Partial |
| 4 | CSS-only hover/focus | TODO |
| 5 | Event delegation, lazy Draggable | TODO |
2026-02-28 15:12:44 +01:00
b341704a0e i18n - translations (#18306)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-28 14:24:11 +01:00
d5d0f5d994 i18n - translations (#18302)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-28 14:11:52 +01:00
Félix MalfaitGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
012d819557 OAuth Client — Unified ApplicationRegistration, OAuth server, and frontend (#18267)
## Summary

Consolidates three separate PRs (#18260, #18261, #18262) into a single
unified branch with all review feedback addressed:

### New features
- **ApplicationRegistration entity** — server-level registration for
OAuth apps with encrypted server variables
- **OAuth 2.0 server** — authorization code, client credentials, refresh
token grants with PKCE support
- **OAuth discovery endpoint** —
`.well-known/oauth-authorization-server` metadata
- **Frontend UI** — app registration details page with credential
management, redirect URI editing, and server variable configuration
- **CLI integration** — `twenty dev` auto-registers apps and stores
OAuth credentials locally
- **Authorize consent screen** — OAuth consent page at `/authorize`
showing requested scopes

### Review feedback addressed

**Renames (PR #18260):**
- `appRegistration` → `applicationRegistration` (entity, tables, files,
imports, GraphQL types)
- `appRegistrationVariable` → `applicationRegistrationVariable`
- `clientId` → `oAuthClientId`, `clientSecretHash` →
`oAuthClientSecretHash`, `redirectUris` → `oAuthRedirectUris`, `scopes`
→ `oAuthScopes`

**Security fixes (PR #18261):**
- Fixed redirect URI validation bypass when `oAuthRedirectUris` is an
empty array
- Fixed workspace isolation in `clientCredentialsGrant` — now uses
`find()` with explicit handling for multiple installations
- Added error logging in refresh token `catch` block instead of silently
swallowing

**Code quality (PR #18262):**
- Split `VersionDistributionEntry` into its own file (one export per
file)
- Split GraphQL queries and mutations into individual files with a
shared fragment
- Removed unused `OAuth` entry from `AuthProviderEnum`
- Added loading state to `handleRotateSecret`
- Removed 27 narration-style comments from test files
- Added proper guards (`PublicEndpointGuard`, `NoPermissionGuard`) to
controllers and resolvers

## Test plan

- [ ] Verify `twenty dev` registers an app and stores OAuth credentials
- [ ] Test OAuth authorization code flow end-to-end (authorize → token →
API call)
- [ ] Test client credentials grant
- [ ] Verify redirect URI validation rejects requests when no URIs are
registered
- [ ] Verify app registration detail page renders correctly
- [ ] Test secret rotation with loading state
- [ ] Verify server variable editing and saving
- [ ] Run `npx nx database:reset twenty-server` to validate migration

Closes #18260, #18261, #18262


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

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-02-28 14:07:49 +01:00
Abdul RahmanGitHubCopilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>DevessierCharles Bochet
9fe2a07c55 Navbar customization v2 (#18026)
Adds color support for navigation menu items.

---------

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Devessier <baptiste@devessier.fr>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-02-28 11:48:28 +01:00
63f17eec2c i18n - translations (#18300)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-27 23:43:42 +01:00
e806d36099 Navbar with AI chats (#18161)
## Summary
Add Home/Chat tabs and a dedicated threads list in the navigation
drawer.

## Changes
- **Navbar tabs:** Tabs in the drawer to switch between Home and Chat
(with “New chat” button). Shown on desktop when expanded and on mobile
below the workspace selector.
- **Navbar threads list:** New `NavigationDrawerAIChatThreadsList` for
the Chat tab with date groups (Today / Yesterday / Older), thread rows
as `NavigationDrawerItem` (IconComment, title, timestamp). Shared
`useAIChatThreadClick` hook used by navbar and command menu; navbar
passes `resetNavigationStack: true`.
- **NavigationDrawerItem:** New `alwaysShowRightOptions` prop so the
timestamp is always visible (no hover-only).

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-02-27 23:37:14 +01:00
9f5a8735c9 i18n - docs translations (#18280)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-27 23:12:39 +01:00
c0e6aa1c0b i18n - translations (#18295)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-27 23:12:15 +01:00
Charles BochetandGitHub 9342b16aad Fix more tests 2 (#18293)
## Summary
- Migrate more hand-written test mocks to auto-generated data from a
real Twenty instance
- Add generators for views, billing plans, API keys; extend record
generator for workspace members, favorites, connected accounts, calendar
events
- Remove 9 hand-written mock files replaced by generated equivalents
- Update 16 test/story files to use generated data
- Fix WorkflowEditActionEmailBase story assertion to match configured
recipient email

## Test plan
- [x] Lint, typecheck, unit tests pass
- [ ] Storybook tests pass in CI
2026-02-27 23:11:56 +01:00
Baptiste DevessierandGitHub cfad24da48 Fix empty user id clickhouse (#18238)
- Fixes:
- Make Workspace User select work; previously, it didn't work as we were
not fetching the workspace users correctly
  - Send Object Events with valid record id and object id

## Audit logs demo


https://github.com/user-attachments/assets/92437037-d253-4810-a138-7c709550755d
2026-02-27 16:58:44 +01:00
Charles BochetandGitHub 86fbf69e95 Fix more tests (#18287)
Improve mock in front tests
2026-02-27 14:06:18 +01:00
9667b3f369 i18n - translations (#18291)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-27 14:05:24 +01:00
Abdullah.andGitHub 76c7639eb3 fix: upgrade storybook to latest to resolve dependabot alert (#18285)
Resolves [Dependabot Alert
509](https://github.com/twentyhq/twenty/security/dependabot/509).

Upgraded storybook and related packages to latest, also fixed a failing
test to match what the DOM really contains.
2026-02-27 10:58:54 +01:00
Abdullah.andGitHub 4ed09a3feb Upgrade blocknote dependencies from 0.31.1 to 0.47.0. (#18207)
This PR pgrades all BlockNote packages (@blocknote/core,
@blocknote/react, @blocknote/mantine, @blocknote/server-util,
@blocknote/xl-docx-exporter, @blocknote/xl-pdf-exporter) to 0.47.0 and
adapts the codebase to the new API.

### Changes
- Dependency upgrades: Bumped all BlockNote packages to 0.47.0, added
required Mantine v8 peer dependencies, removed unnecessary prosemirror
resolutions
- Formatting toolbar: Replaced the manual reimplementation of
FormattingToolbarController (which handled visibility, positioning,
portal rendering, text-alignment-based placement, and a
dangerouslySetInnerHTML transition trick) with BlockNote's built-in
FormattingToolbarController. The toolbar buttons themselves are
unchanged.
- Side menu: Replaced manual drag handle menu positioning and rendering
(DashboardBlockDragHandleMenu, DashboardBlockColorPicker, and their
floating configs) with BlockNote's built-in SideMenuController,
DragHandleButton, and DragHandleMenu components. Deleted 4 files that
became dead code.
- Extension API migration: Replaced deprecated editor.suggestionMenus
and editor.formattingToolbar APIs with the new extension system
(SuggestionMenu, useExtensionState, editor.getExtension())
- Slash menu fixes: Filtered out BlockNote's new default "File" item
(added in 0.47) to avoid duplicates with our custom one; added icon
mappings for new block types (Toggle List, Divider, Toggle Headings,
Headings 4-6)
- Server-side: Switched @blocknote/server-util to dynamic import() to
handle ESM-only transitive dependencies in CJS context
2026-02-27 09:16:49 +01:00
Thomas TrompetteandGitHub def5ea5764 Fix ai agent node prompt and variables (#18275)
- prompt stored on workflow lvl so input variables can be resolved and
it can evolves with versions
- make ai agent node output available as variables
2026-02-26 17:17:48 +01:00
81698ff32c i18n - docs translations (#18274)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-02-26 16:52:43 +01:00
c0c51f2ef5 i18n - translations (#18278)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-26 16:48:08 +01:00
WeikoandGitHub 3bbaff801a Add Twenty app settings custom app (#18273)
## Context
This PR adds the ability to define a front-component as a custom tab for
the application settings, allowing app creators to inject some
logic/rendering the their app settings.


## Example
```typescript
// packages/twenty-apps/my-test-app/src/front-components/settings-custom-tab.tsx
import { defineFrontComponent } from 'twenty-sdk';

export const SETTINGS_CUSTOM_TAB_UNIVERSAL_IDENTIFIER =
  'a42a88a8-21ce-4d22-bc44-d5146da64726';

export const SettingsCustomTab = () => {
  return (
    <div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
      <h2>My Test App Settings</h2>
      <p>This is a custom settings tab provided by My Test App.</p>
      <p>Application creators can customize this component freely.</p>
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: SETTINGS_CUSTOM_TAB_UNIVERSAL_IDENTIFIER,
  name: 'settings-custom-tab',
  description: 'Custom settings tab for the application',
  component: SettingsCustomTab,
});
```

```typescript
// packages/twenty-apps/my-test-app/src/application-config.ts
export default defineApplication({
  universalIdentifier: '52870ce6-e584-4bd4-bc1a-6c63c508982e',
  displayName: 'My test app',
  description: '',
  defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
  settingsCustomTabFrontComponentUniversalIdentifier:
    SETTINGS_CUSTOM_TAB_UNIVERSAL_IDENTIFIER,
});
```
<img width="786" height="757" alt="Screenshot 2026-02-26 at 14 54 09"
src="https://github.com/user-attachments/assets/fcba70db-35da-48f1-bd65-359e894a691d"
/>
2026-02-26 16:26:49 +01:00
Abdullah.andGitHub 2d2fc06265 fix: basic-ftp related dependabot alert (#18269)
Resolves [Dependabot Alert
507](https://github.com/twentyhq/twenty/security/dependabot/507).

Fixes critical SLA breach in 6 days.
2026-02-26 14:50:49 +01:00
Charles BochetandGitHub 8a7a19f312 Improve test tooling (#18259)
## Summary

Unifies test mocking tooling across Jest and Storybook, replaces
handcrafted mock data with auto-generated server-fetched data, and
restructures the mock data generation script for maintainability.

### Mock data generation

- Split `generate-mock-data.ts` into three focused modules under
`scripts/mock-data/`:
  - `utils.ts` — shared authentication, GraphQL client, and file writer
  - `generate-metadata.ts` — fetches object metadata from `/metadata`
- `generate-record-data.ts` — fetches record data from `/graphql` using
metadata-driven dynamic queries
- The orchestrator (`generate-mock-data.ts`) authenticates once and
passes the token to both generators
- Company records are now fetched from the actual server (limited to 10
records) instead of being handcrafted
- Generated files are organized under `generated/metadata/objects/` and
`generated/data/companies/`

### Unified test utilities

- Consolidated Jest and MSW mocking into shared utilities that compose
production code (`prefillRecord`, `getRecordNodeFromRecord`,
`getRecordConnectionFromRecords`) with mock metadata
- Renamed `generateEmptyJestRecordNode` → `generateMockRecordNode` and
moved to `testing/utils/`
- Extracted `generateMockRecordConnection` into its own file
- Removed `sanitizeInputForPrefill` workaround (no longer needed with
correctly shaped generated data)
2026-02-26 14:31:54 +01:00
EtienneandGitHub 2f0103faa5 OAuth Client - Add OAuth propagator (#18266)
For OAuth Server without wildcard redirect URL

https://www.benanderson.co.uk/2023/07/28/dynamic-redirect-uris-oauth/
2026-02-26 13:44:54 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
08bfbfda45 Bump @emotion/styled from 11.13.0 to 11.14.1 (#18253)
Bumps [@emotion/styled](https://github.com/emotion-js/emotion) from
11.13.0 to 11.14.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/emotion-js/emotion/releases"><code>@​emotion/styled</code>'s
releases</a>.</em></p>
<blockquote>
<h2><code>@​emotion/styled</code><a
href="https://github.com/11"><code>@​11</code></a>.14.1</h2>
<h3>Patch Changes</h3>
<ul>
<li><a
href="https://redirect.github.com/emotion-js/emotion/pull/3334">#3334</a>
<a
href="https://github.com/emotion-js/emotion/commit/0facbe47bd9099ae4ed22dc201822d910ac3dec5"><code>0facbe4</code></a>
Thanks <a
href="https://github.com/ZachRiegel"><code>@​ZachRiegel</code></a>! -
Renamed default-exported variable in <code>@emotion/styled</code> to aid
inferred import names in auto-import completions in IDEs</li>
</ul>
<h2><code>@​emotion/styled</code><a
href="https://github.com/11"><code>@​11</code></a>.14.0</h2>
<h3>Minor Changes</h3>
<ul>
<li><a
href="https://redirect.github.com/emotion-js/emotion/pull/3284">#3284</a>
<a
href="https://github.com/emotion-js/emotion/commit/a19d019bd418ebc3b9cba0e58f58b36ac2862a42"><code>a19d019</code></a>
Thanks <a
href="https://github.com/Andarist"><code>@​Andarist</code></a>! - Source
code has been migrated to TypeScript. From now on type declarations will
be emitted based on that, instead of being hand-written.</li>
</ul>
<h3>Patch Changes</h3>
<ul>
<li>Updated dependencies [<a
href="https://github.com/emotion-js/emotion/commit/e1bf17ee87ec51da1412eb5291460ea95a39d27a"><code>e1bf17e</code></a>]:
<ul>
<li><code>@​emotion/use-insertion-effect-with-fallbacks</code><a
href="https://github.com/1"><code>@​1</code></a>.2.0</li>
</ul>
</li>
</ul>
<h2><code>@​emotion/styled</code><a
href="https://github.com/11"><code>@​11</code></a>.13.5</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/emotion-js/emotion/pull/3270">#3270</a>
<a
href="https://github.com/emotion-js/emotion/commit/77d930dc708015ff6fd34a1084bb343b02d732fa"><code>77d930d</code></a>
Thanks <a
href="https://github.com/emmatown"><code>@​emmatown</code></a>! - Fix
inconsistent hashes using development vs production
bundles/<code>exports</code> conditions when using
<code>@emotion/babel-plugin</code> with <code>sourceMap: true</code>
(the default). This is particularly visible when using Emotion with the
Next.js Pages router where the <code>development</code> condition is
used when bundling code but not when importing external code with
Node.js.</p>
</li>
<li>
<p>Updated dependencies [<a
href="https://github.com/emotion-js/emotion/commit/77d930dc708015ff6fd34a1084bb343b02d732fa"><code>77d930d</code></a>]:</p>
<ul>
<li><code>@​emotion/serialize</code><a
href="https://github.com/1"><code>@​1</code></a>.3.3</li>
<li><code>@​emotion/utils</code><a
href="https://github.com/1"><code>@​1</code></a>.4.2</li>
<li><code>@​emotion/babel-plugin</code><a
href="https://github.com/11"><code>@​11</code></a>.13.5</li>
</ul>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/emotion-js/emotion/commit/49229553967b6050c92d9602eb577bdc48167e91"><code>4922955</code></a>
Version Packages (<a
href="https://redirect.github.com/emotion-js/emotion/issues/3335">#3335</a>)</li>
<li><a
href="https://github.com/emotion-js/emotion/commit/0facbe47bd9099ae4ed22dc201822d910ac3dec5"><code>0facbe4</code></a>
Renamed default-exported variable in <code>@emotion/styled</code> to aid
inferred import...</li>
<li><a
href="https://github.com/emotion-js/emotion/commit/cce67ec6b2fc94261028b4f4778aae8c3d6c5fd6"><code>cce67ec</code></a>
Bump parcel (<a
href="https://redirect.github.com/emotion-js/emotion/issues/3258">#3258</a>)</li>
<li><a
href="https://github.com/emotion-js/emotion/commit/3c19ce5997f73960679e546af47801205631dfde"><code>3c19ce5</code></a>
Version Packages (<a
href="https://redirect.github.com/emotion-js/emotion/issues/3280">#3280</a>)</li>
<li><a
href="https://github.com/emotion-js/emotion/commit/a19d019bd418ebc3b9cba0e58f58b36ac2862a42"><code>a19d019</code></a>
Convert <code>@emotion/styled</code>'s source code to TypeScript (<a
href="https://redirect.github.com/emotion-js/emotion/issues/3284">#3284</a>)</li>
<li><a
href="https://github.com/emotion-js/emotion/commit/5974e33fcb5e7aee177408684ac6fe8b38b3e353"><code>5974e33</code></a>
Fix JSX namespace <a
href="https://github.com/ts-ignores"><code>@​ts-ignores</code></a> (<a
href="https://redirect.github.com/emotion-js/emotion/issues/3282">#3282</a>)</li>
<li><a
href="https://github.com/emotion-js/emotion/commit/fc4d7bd744c205f55513dcd4e4e5134198c219de"><code>fc4d7bd</code></a>
Convert <code>@emotion/react</code>'s source code to TypeScript (<a
href="https://redirect.github.com/emotion-js/emotion/issues/3281">#3281</a>)</li>
<li><a
href="https://github.com/emotion-js/emotion/commit/8dc1a6dd19d2dc9ce435ef0aff85ccf5647f5d2e"><code>8dc1a6d</code></a>
Convert <code>@emotion/cache</code>'s source code to TypeScript (<a
href="https://redirect.github.com/emotion-js/emotion/issues/3277">#3277</a>)</li>
<li><a
href="https://github.com/emotion-js/emotion/commit/282b61d2ad4e39ea65af88351a894a903c2d42c4"><code>282b61d</code></a>
Convert <code>@emotion/css-prettifier</code>'s source code to TypeScript
(<a
href="https://redirect.github.com/emotion-js/emotion/issues/3278">#3278</a>)</li>
<li><a
href="https://github.com/emotion-js/emotion/commit/e1bf17ee87ec51da1412eb5291460ea95a39d27a"><code>e1bf17e</code></a>
Convert <code>@emotion/use-insertion-effect-with-fallbacks</code>'s
source code to TypeS...</li>
<li>Additional commits viewable in <a
href="https://github.com/emotion-js/emotion/compare/@emotion/styled@11.13.0...@emotion/styled@11.14.1">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-26 12:59:59 +01:00
Raphaël BosiandGitHub c025a0c2b8 Add events and properties to video, audio and iFrame (#18257)
- Add media-specific events
- Extend `iFrame` with missing properties
- Enrich `SerializedEventData` with media-related target fields so
serialized events carry the media element state.
- Refactor the `remote-elements` code generator to support per-element
custom events
2026-02-26 12:14:51 +01:00
Paul RastoinandGitHub 5e19361494 Allow uuidv5 for universal identifier (#18265)
Twenty apps are using v5
2026-02-26 12:13:38 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f11d76d6cf Bump @babel/preset-react from 7.26.3 to 7.28.5 (#18254)
Bumps
[@babel/preset-react](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-react)
from 7.26.3 to 7.28.5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/babel/babel/releases"><code>@​babel/preset-react</code>'s
releases</a>.</em></p>
<blockquote>
<h2>v7.28.5 (2025-10-23)</h2>
<p>Thank you <a
href="https://github.com/CO0Ki3"><code>@​CO0Ki3</code></a>, <a
href="https://github.com/Olexandr88"><code>@​Olexandr88</code></a>, and
<a href="https://github.com/youthfulhps"><code>@​youthfulhps</code></a>
for your first PRs!</p>
<h4>👓 Spec Compliance</h4>
<ul>
<li><code>babel-parser</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17446">#17446</a>
Allow <code>Runtime Errors for Function Call Assignment Targets</code>
(<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
<li><code>babel-helper-validator-identifier</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17501">#17501</a>
fix: update identifier to unicode 17 (<a
href="https://github.com/fisker"><code>@​fisker</code></a>)</li>
</ul>
</li>
</ul>
<h4>🐛 Bug Fix</h4>
<ul>
<li><code>babel-plugin-proposal-destructuring-private</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17534">#17534</a>
Allow mixing private destructuring and rest (<a
href="https://github.com/CO0Ki3"><code>@​CO0Ki3</code></a>)</li>
</ul>
</li>
<li><code>babel-parser</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17521">#17521</a>
Improve <code>@babel/parser</code> error typing (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
<li><a
href="https://redirect.github.com/babel/babel/pull/17491">#17491</a>
fix: improve ts-only declaration parsing (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-plugin-proposal-discard-binding</code>,
<code>babel-plugin-transform-destructuring</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17519">#17519</a>
fix: <code>rest</code> correctly returns plain array (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
<li><code>babel-helper-create-class-features-plugin</code>,
<code>babel-helper-member-expression-to-functions</code>,
<code>babel-plugin-transform-block-scoping</code>,
<code>babel-plugin-transform-optional-chaining</code>,
<code>babel-traverse</code>, <code>babel-types</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17503">#17503</a> Fix
<code>JSXIdentifier</code> handling in
<code>isReferencedIdentifier</code> (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-traverse</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17504">#17504</a>
fix: ensure scope.push register in anonymous fn (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
</ul>
<h4>🏠 Internal</h4>
<ul>
<li><code>babel-types</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17494">#17494</a>
Type checking babel-types scripts (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
</ul>
<h4>🏃‍♀️ Performance</h4>
<ul>
<li><code>babel-core</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17490">#17490</a>
Faster finding of locations in <code>buildCodeFrameError</code> (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
</ul>
<h4>Committers: 8</h4>
<ul>
<li>Babel Bot (<a
href="https://github.com/babel-bot"><code>@​babel-bot</code></a>)</li>
<li>Byeongho Yoo (<a
href="https://github.com/youthfulhps"><code>@​youthfulhps</code></a>)</li>
<li>Huáng Jùnliàng (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
<li>Hyeon Dokko (<a
href="https://github.com/CO0Ki3"><code>@​CO0Ki3</code></a>)</li>
<li>Nicolò Ribaudo (<a
href="https://github.com/nicolo-ribaudo"><code>@​nicolo-ribaudo</code></a>)</li>
<li><a
href="https://github.com/Olexandr88"><code>@​Olexandr88</code></a></li>
<li><a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a></li>
<li>fisker Cheung (<a
href="https://github.com/fisker"><code>@​fisker</code></a>)</li>
</ul>
<h2>v7.28.4 (2025-09-05)</h2>
<p>Thanks <a
href="https://github.com/gwillen"><code>@​gwillen</code></a> and <a
href="https://github.com/mrginglymus"><code>@​mrginglymus</code></a> for
your first PRs!</p>
<h4>🏠 Internal</h4>
<ul>
<li><code>babel-core</code>,
<code>babel-helper-check-duplicate-nodes</code>,
<code>babel-traverse</code>, <code>babel-types</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17493">#17493</a>
Update Jest to v30.1.1 (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-plugin-transform-regenerator</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17455">#17455</a>
chore: Clean up <code>transform-regenerator</code> (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/babel/babel/blob/main/CHANGELOG.md"><code>@​babel/preset-react</code>'s
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<blockquote>
<p><strong>Tags:</strong></p>
<ul>
<li>💥 [Breaking Change]</li>
<li>👓 [Spec Compliance]</li>
<li>🚀 [New Feature]</li>
<li>🐛 [Bug Fix]</li>
<li>📝 [Documentation]</li>
<li>🏠 [Internal]</li>
<li>💅 [Polish]</li>
</ul>
</blockquote>
<p><em>Note: Gaps between patch versions are faulty, broken or test
releases.</em></p>
<p>This file contains the changelog starting from v8.0.0-alpha.0.</p>
<ul>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v7.15.0-v7.28.5.md">CHANGELOG
- v7.15.0 to v7.28.5</a> for v7.15.0 to v7.28.5 changes (the last common
release between the v8 and v7 release lines was v7.28.5).</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v7.0.0-v7.14.9.md">CHANGELOG
- v7.0.0 to v7.14.9</a> for v7.0.0 to v7.14.9 changes.</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v7-prereleases.md">CHANGELOG
- v7 prereleases</a> for v7.0.0-alpha.1 to v7.0.0-rc.4 changes.</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v4.md">CHANGELOG
- v4</a>, <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v5.md">CHANGELOG
- v5</a>, and <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v6.md">CHANGELOG
- v6</a> for v4.x-v6.x changes.</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-6to5.md">CHANGELOG
- 6to5</a> for the pre-4.0.0 version changelog.</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/packages/babel-parser/CHANGELOG.md">Babylon's
CHANGELOG</a> for the Babylon pre-7.0.0-beta.29 version changelog.</li>
<li>See <a
href="https://github.com/babel/babel-eslint/releases"><code>babel-eslint</code>'s
releases</a> for the changelog before <code>@babel/eslint-parser</code>
7.8.0.</li>
<li>See <a
href="https://github.com/babel/eslint-plugin-babel/releases"><code>eslint-plugin-babel</code>'s
releases</a> for the changelog before <code>@babel/eslint-plugin</code>
7.8.0.</li>
</ul>
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<h2>v8.0.0-rc.2 (2026-02-15)</h2>
<h4>💥 Breaking Change</h4>
<ul>
<li>Other
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17766">#17766</a>
Remove unused code for old ESLint versions (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
<li><code>babel-code-frame</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17772">#17772</a>
Remove deprecated default export from <code>@babel/code-frame</code> (<a
href="https://github.com/fisker"><code>@​fisker</code></a>)</li>
</ul>
</li>
</ul>
<h4>🐛 Bug Fix</h4>
<ul>
<li><code>babel-helpers</code>,
<code>babel-plugin-transform-async-generator-functions</code>,
<code>babel-runtime-corejs3</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17797">#17797</a>
fix: Properly handle <code>await</code> in <code>finally</code> (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
<li><code>babel-parser</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17796">#17796</a>
Support ESLint 10 (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-preset-env</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17787">#17787</a>
Fix: preset-env include/exclude should accept bugfix plugins (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-generator</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17781">#17781</a>
fix: preserve trailing comma in optional call args (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
<li><a
href="https://redirect.github.com/babel/babel/pull/17774">#17774</a> Fix
<code>undefined</code> indentation when exactly 64 indents (<a
href="https://github.com/YoussefHenna"><code>@​YoussefHenna</code></a>)</li>
</ul>
</li>
<li><code>babel-standalone</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17770">#17770</a>
fix: ensure <code>targets.esmodules</code> is validated (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
</ul>
<h4>💅 Polish</h4>
<ul>
<li><code>babel-core</code></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/babel/babel/commit/61647ae2397c82c3c71f077b5ab109106a5cac0f"><code>61647ae</code></a>
v7.28.5</li>
<li><a
href="https://github.com/babel/babel/commit/42cb285b59fc99a8102d69bef6223b75617e9f46"><code>42cb285</code></a>
Improve <code>@babel/core</code> types (<a
href="https://github.com/babel/babel/tree/HEAD/packages/babel-preset-react/issues/17404">#17404</a>)</li>
<li><a
href="https://github.com/babel/babel/commit/eebd3a06021c13d335b5b0bd79734df3abbea678"><code>eebd3a0</code></a>
v7.27.1</li>
<li><a
href="https://github.com/babel/babel/commit/fdc0fb59e119ee0b38bced63867a344a5b4bc2f3"><code>fdc0fb5</code></a>
[Babel 8] Bump nodejs requirements to <code>^20.19.0 || &gt;=
22.12.0</code> (<a
href="https://github.com/babel/babel/tree/HEAD/packages/babel-preset-react/issues/17204">#17204</a>)</li>
<li><a
href="https://github.com/babel/babel/commit/cd24cc07ef6558b7f6510f9177f6393c91b0549f"><code>cd24cc0</code></a>
chore: Update TS 5.7 (<a
href="https://github.com/babel/babel/tree/HEAD/packages/babel-preset-react/issues/17053">#17053</a>)</li>
<li>See full diff in <a
href="https://github.com/babel/babel/commits/v7.28.5/packages/babel-preset-react">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by [GitHub Actions](<a
href="https://www.npmjs.com/~GitHub">https://www.npmjs.com/~GitHub</a>
Actions), a new releaser for <code>@​babel/preset-react</code> since
your current version.</p>
</details>
<br />


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

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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-26 11:57:26 +01:00
Paul RastoinandGitHub 3aedce9af7 Fix remaining non v4 uuid universal identifier (#18263) 2026-02-26 11:24:01 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8e8ecfb8a3 Bump @sentry/react from 10.27.0 to 10.40.0 (#18252)
Bumps [@sentry/react](https://github.com/getsentry/sentry-javascript)
from 10.27.0 to 10.40.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/getsentry/sentry-javascript/releases"><code>@​sentry/react</code>'s
releases</a>.</em></p>
<blockquote>
<h2>10.40.0</h2>
<h3>Important Changes</h3>
<ul>
<li>
<p><strong>feat(tanstackstart-react): Add global sentry exception
middlewares (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/19330">#19330</a>)</strong></p>
<p>The <code>sentryGlobalRequestMiddleware</code> and
<code>sentryGlobalFunctionMiddleware</code> global middlewares capture
unhandled exceptions thrown in TanStack Start API routes and server
functions. Add them as the first entries in the
<code>requestMiddleware</code> and <code>functionMiddleware</code>
arrays of <code>createStart()</code>:</p>
<pre lang="ts"><code>import { createStart } from
'@tanstack/react-start/server';
import { sentryGlobalRequestMiddleware, sentryGlobalFunctionMiddleware }
from '@sentry/tanstackstart-react';
<p>export default createStart({
requestMiddleware: [sentryGlobalRequestMiddleware, myRequestMiddleware],
functionMiddleware: [sentryGlobalFunctionMiddleware,
myFunctionMiddleware],
});
</code></pre></p>
</li>
<li>
<p><strong>feat(tanstackstart-react)!: Export Vite plugin from
<code>@sentry/tanstackstart-react/vite</code> subpath (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/19182">#19182</a>)</strong></p>
<p>The <code>sentryTanstackStart</code> Vite plugin is now exported from
a dedicated subpath. Update your import:</p>
<pre lang="diff"><code>- import { sentryTanstackStart } from
'@sentry/tanstackstart-react';
+ import { sentryTanstackStart } from
'@sentry/tanstackstart-react/vite';
</code></pre>
</li>
<li>
<p><strong>fix(node-core): Reduce bundle size by removing apm-js-collab
and requiring pino &gt;= 9.10 (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/18631">#18631</a>)</strong></p>
<p>In order to keep receiving pino logs, you need to update your pino
version to &gt;= 9.10, the reason for the support bump is to reduce the
bundle size of the node-core SDK in frameworks that cannot tree-shake
the apm-js-collab dependency.</p>
</li>
<li>
<p><strong>fix(browser): Ensure user id is consistently added to
sessions (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/19341">#19341</a>)</strong></p>
<p>Previously, the SDK inconsistently set the user id on sessions,
meaning sessions were often lacking proper coupling to the user set for
example via <code>Sentry.setUser()</code>.
Additionally, the SDK incorrectly skipped starting a new session for the
first soft navigation after the pageload.
This patch fixes these issues. As a result, metrics around sessions,
like &quot;Crash Free Sessions&quot; or &quot;Crash Free Users&quot;
might change.
This could also trigger alerts, depending on your set thresholds and
conditions.
We apologize for any inconvenience caused!</p>
<p>While we're at it, if you're using Sentry in a Single Page App or
meta framework, you might want to give the new <code>'page'</code>
session lifecycle a try!
This new mode no longer creates a session per soft navigation but
continues the initial session until the next hard page refresh.
Check out the <a
href="https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/integrations/browsersession/">docs</a>
to learn more!</p>
</li>
<li>
<p><strong>ref!(gatsby): Drop Gatsby v2 support (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/19467">#19467</a>)</strong></p>
<p>We drop support for Gatsby v2 (which still relies on webpack 4) for a
critical security update in <a
href="https://github.com/getsentry/sentry-javascript-bundler-plugins/releases/tag/5.0.0">https://github.com/getsentry/sentry-javascript-bundler-plugins/releases/tag/5.0.0</a></p>
</li>
</ul>
<h3>Other Changes</h3>
<ul>
<li>feat(astro): Add support for Astro on CF Workers (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/19265">#19265</a>)</li>
<li>feat(cloudflare): Instrument async KV API (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/19404">#19404</a>)</li>
</ul>
<!-- 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/react</code>'s
changelog</a>.</em></p>
<blockquote>
<h2>10.40.0</h2>
<h3>Important Changes</h3>
<ul>
<li>
<p><strong>feat(tanstackstart-react): Add global sentry exception
middlewares (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/19330">#19330</a>)</strong></p>
<p>The <code>sentryGlobalRequestMiddleware</code> and
<code>sentryGlobalFunctionMiddleware</code> global middlewares capture
unhandled exceptions thrown in TanStack Start API routes and server
functions. Add them as the first entries in the
<code>requestMiddleware</code> and <code>functionMiddleware</code>
arrays of <code>createStart()</code>:</p>
<pre lang="ts"><code>import { createStart } from
'@tanstack/react-start/server';
import { sentryGlobalRequestMiddleware, sentryGlobalFunctionMiddleware }
from '@sentry/tanstackstart-react/server';
<p>export default createStart({
requestMiddleware: [sentryGlobalRequestMiddleware, myRequestMiddleware],
functionMiddleware: [sentryGlobalFunctionMiddleware,
myFunctionMiddleware],
});
</code></pre></p>
</li>
<li>
<p><strong>feat(tanstackstart-react)!: Export Vite plugin from
<code>@sentry/tanstackstart-react/vite</code> subpath (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/19182">#19182</a>)</strong></p>
<p>The <code>sentryTanstackStart</code> Vite plugin is now exported from
a dedicated subpath. Update your import:</p>
<pre lang="diff"><code>- import { sentryTanstackStart } from
'@sentry/tanstackstart-react';
+ import { sentryTanstackStart } from
'@sentry/tanstackstart-react/vite';
</code></pre>
</li>
<li>
<p><strong>fix(node-core): Reduce bundle size by removing apm-js-collab
and requiring pino &gt;= 9.10 (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/18631">#18631</a>)</strong></p>
<p>In order to keep receiving pino logs, you need to update your pino
version to &gt;= 9.10, the reason for the support bump is to reduce the
bundle size of the node-core SDK in frameworks that cannot tree-shake
the apm-js-collab dependency.</p>
</li>
<li>
<p><strong>fix(browser): Ensure user id is consistently added to
sessions (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/19341">#19341</a>)</strong></p>
<p>Previously, the SDK inconsistently set the user id on sessions,
meaning sessions were often lacking proper coupling to the user set for
example via <code>Sentry.setUser()</code>.
Additionally, the SDK incorrectly skipped starting a new session for the
first soft navigation after the pageload.
This patch fixes these issues. As a result, metrics around sessions,
like &quot;Crash Free Sessions&quot; or &quot;Crash Free Users&quot;
might change.
This could also trigger alerts, depending on your set thresholds and
conditions.
We apologize for any inconvenience caused!</p>
<p>While we're at it, if you're using Sentry in a Single Page App or
meta framework, you might want to give the new <code>'page'</code>
session lifecycle a try!
This new mode no longer creates a session per soft navigation but
continues the initial session until the next hard page refresh.
Check out the <a
href="https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/integrations/browsersession/">docs</a>
to learn more!</p>
</li>
<li>
<p><strong>ref!(gatsby): Drop Gatsby v2 support (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/19467">#19467</a>)</strong></p>
<p>We drop support for Gatsby v2 (which still relies on webpack 4) for a
critical security update in <a
href="https://github.com/getsentry/sentry-javascript-bundler-plugins/releases/tag/5.0.0">https://github.com/getsentry/sentry-javascript-bundler-plugins/releases/tag/5.0.0</a></p>
</li>
</ul>
<h3>Other Changes</h3>
<ul>
<li>feat(astro): Add support for Astro on CF Workers (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/19265">#19265</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/663fd5e7e3c1808d4a636f001d768845f167668e"><code>663fd5e</code></a>
Increase bundler-tests timeout to 30s</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/8033ea380f0526cc863c6d50347fd5747ae5df32"><code>8033ea3</code></a>
release: 10.40.0</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/eb3c4d2489a77753377f7e3a320f18cd853ebf6a"><code>eb3c4d2</code></a>
Merge pull request <a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/19488">#19488</a>
from getsentry/prepare-release/10.40.0</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/9a10630c6b7524d053b96cfaafa14751b0611f33"><code>9a10630</code></a>
meta(changelog): Update changelog for 10.40.0</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/39d1ef77849223f7742999c808f7f23da0c42adf"><code>39d1ef7</code></a>
fix(deps): Bump to latest version of each minimatch major (<a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/19486">#19486</a>)</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/e8ed6d262f7f43cef8b04265794db83ab013f95c"><code>e8ed6d2</code></a>
test(nextjs): Deactivate canary test for cf-workers (<a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/19483">#19483</a>)</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/6eb320eb3e01985720238c8f08e3ac114502059b"><code>6eb320e</code></a>
chore(deps): Bump Sentry CLI to latest v2 (<a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/19477">#19477</a>)</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/8fc81d2cd4048fb41b49e773d4829d9fb799f16c"><code>8fc81d2</code></a>
fix: Bump bundler plugins to v5 (<a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/19468">#19468</a>)</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/365f7fab4e33d69363d4eb6d99e5f87e48672fba"><code>365f7fa</code></a>
chore(ci): Adapt max turns of triage issue agent (<a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/19473">#19473</a>)</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/11e5412d42f6126e5415d67d1418ffdb17f5caa6"><code>11e5412</code></a>
feat(tanstackstart-react)!: Export Vite plugin from
<code>@​sentry/tanstackstart-rea</code>...</li>
<li>Additional commits viewable in <a
href="https://github.com/getsentry/sentry-javascript/compare/10.27.0...10.40.0">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-26 11:16:24 +01:00
martmullandGitHub 120096346a Add define post isntall logic function (#18248)
As title
2026-02-26 11:09:21 +01:00
Paul RastoinandGitHub fde8168a85 Centralized universal identifier validation on create (#18258) 2026-02-26 10:48:24 +01:00
Charles BochetandGitHub 2674589b44 Remove any recoil reference from project (#18250)
## Remove all Recoil references and replace with Jotai

### Summary

- Removed every occurrence of Recoil from the entire codebase, replacing
with Jotai equivalents where applicable
- Updated `README.md` tech stack: `Recoil` → `Jotai`
- Rewrote documentation code examples to use
`createAtomState`/`useAtomState` instead of `atom`/`useRecoilState`, and
removed `RecoilRoot` wrappers
- Cleaned up source code comment and Cursor rules that referenced Recoil
- Applied changes across all 13 locale translations (ar, cs, de, es, fr,
it, ja, ko, pt, ro, ru, tr, zh)
2026-02-26 10:28:40 +01:00
Charles BochetandGitHub f325509d96 Fix Jotai state leak in frontend tests causing unbounded memory growth (#18249)
## Summary

The global `jotaiStore` singleton was shared across all tests without
reset, leaking state between test suites and causing unbounded heap
growth within each Jest worker.

- `getJestMetadataAndApolloMocksWrapper` now creates a **fresh Jotai
store per wrapper** (equivalent of RecoilRoot isolation), so each test
gets clean state
- Added `workerIdleMemoryLimit: '512MB'` to recycle workers that still
accumulate memory
- Set `maxWorkers: 3` for CI

## Performance

Measured on 48 test suites (command-menu + views + object-record hooks),
single worker:

| Metric | Before | After |
|---|---|---|
| Peak heap | **1519 MB** | **~530 MB** (-65%) |
| Final heap | **1519 MB** | **437 MB** (-71%) |
| Growth pattern | Monotonic (never freed) | Bounded sawtooth (recycled
at 512MB) |
2026-02-26 01:20:25 +01:00
a9649b06d5 followup 18044 (#18213)
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-02-26 00:55:57 +01:00
Charles BochetandGitHub 0b4bf97f35 Fix twenty-sdk typecheck race condition (#18246)
## Fix SDK first-run build failure when generated API client is missing

### Problem

When running `twenty app:dev` for the first time, the build pipeline
crashes because:

1. The esbuild front-component watcher tries to resolve
`twenty-sdk/generated`, but the generated API client doesn't exist yet —
it's only created later in the pipeline after syncing with the server.
2. The typecheck plugin (`tsc --noEmit`) runs as a blocking esbuild
`onStart` hook, so even with stub files, type errors on the empty client
classes (e.g. `Property 'query' does not exist on type 'CoreApiClient'`)
cause the build to fail before the real client can be generated.

This creates a chicken-and-egg problem: the watchers need the generated
client to build, but the client is generated after the watchers produce
their output.

### Solution

**1. Stub generated client on startup**

Added `ensureGeneratedClientStub` to `ClientService` that writes minimal
placeholder files (`CoreApiClient`, `MetadataApiClient`) into
`node_modules/twenty-sdk/generated/` if the directory doesn't already
exist. This is called in `DevModeOrchestrator.start()` before any
watchers are created, so the `twenty-sdk/generated` import always
resolves.

**2. Skip typecheck on first sync round**

Made the esbuild typecheck plugin accept a `shouldSkipTypecheck`
callback. The orchestrator starts with `skipTypecheck = true` and passes
`() => this.skipTypecheck` through the watcher chain. After the real API
client is generated, the flag is flipped to `false`, so subsequent
rebuilds enforce full type checking with the real generated types.
2026-02-25 23:47:08 +01:00
4ea8245387 i18n - docs translations (#18245)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-25 23:42:24 +01:00
0fa8063054 Handle ObjectMetadataItem, FieldMetadataItem and NavigationMenuItem with SSE events (#18235)
This PR allows to handle ObjectMetadataItem, FieldMetadataItem and
NavigationMenuItem SSE events.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-25 23:18:00 +01:00
Abdullah.andGitHub 546114d07f fix: next-mdx-remote related dependabot alerts (#18244)
Resolves [Dependabot Alert
485](https://github.com/twentyhq/twenty/security/dependabot/485) and
[Dependabot Alert
486](https://github.com/twentyhq/twenty/security/dependabot/486).

Bumped up `next-mdx-remote` to v6.0.0 and `remark-gfm` to v4.0.1 for
compatibility - no breaking changes for our use-case.
2026-02-25 23:12:23 +01:00
92824c6533 i18n - translations (#18243)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-25 22:34:27 +01:00
Charles BochetandGitHub bc4ae35bc4 Rework atom naming (#18240)
## Summary

- **Enhanced the `matching-state-variable` ESLint rule** to enforce
consistent naming for `ComponentState`, `FamilyState`, and
`ComponentFamilyState` hooks — previously it only covered `useAtomState`
and `useAtomStateValue`
- **The rule now checks 12 hooks** across three categories: value hooks
(`useAtomComponentStateValue`, `useAtomFamilyStateValue`, etc.), state
hooks (`useAtomComponentState`, `useAtomComponentFamilyState`), and
setter hooks (`useSetAtomState`, `useSetAtomComponentState`,
`useSetAtomFamilyState`, `useSetAtomComponentFamilyState`)
- **Fixed all 225 resulting lint violations** across 151 files, renaming
variables to match their state atom names (e.g. `currentViewId` →
`contextStoreCurrentViewId`, `selectedRecord` → `recordStore`). Cases
where the same state is accessed with different family keys/instance IDs
are suppressed with `eslint-disable-next-line`.

## Naming convention

| Hook | State argument | Valid | Invalid |
|------|---------------|-------|---------|
| `useAtomStateValue` | `fooState` | `const foo = ...` | `const bar =
...` |
| `useAtomComponentStateValue` | `fooComponentState` | `const foo = ...`
| `const bar = ...` |
| `useAtomFamilyStateValue` | `fooFamilyState` | `const foo = ...` |
`const bar = ...` |
| `useAtomComponentFamilyStateValue` | `fooComponentFamilyState` |
`const foo = ...` | `const bar = ...` |
| `useAtomState` | `fooState` | `const [foo, setFoo] = ...` | `const
[bar, setBar] = ...` |
| `useAtomComponentState` | `fooComponentState` | `const [foo, setFoo] =
...` | `const [bar, setBar] = ...` |
| `useAtomComponentFamilyState` | `fooComponentFamilyState` | `const
[foo, setFoo] = ...` | `const [bar, setBar] = ...` |
| `useSetAtomState` | `fooState` | `const setFoo = ...` | `const setBar
= ...` |
| `useSetAtomComponentState` | `fooComponentState` | `const setFoo =
...` | `const setBar = ...` |
| `useSetAtomFamilyState` | `fooFamilyState` | `const setFoo = ...` |
`const setBar = ...` |
| `useSetAtomComponentFamilyState` | `fooComponentFamilyState` | `const
setFoo = ...` | `const setBar = ...` |
2026-02-25 22:28:25 +01:00
EtienneandGitHub 1b805a36f3 Fix page layout widget creation with front component (#18242) 2026-02-25 22:07:51 +01:00
Baptiste DevessierandGitHub f7ea1d9500 Update doc for front components (#18196) 2026-02-25 22:04:37 +01:00
Abdullah.andGitHub cbb0f212cf fix: fast-xml-parser related dependabot alerts (#18241)
Resolves [Dependabot Alert
459](https://github.com/twentyhq/twenty/security/dependabot/459) and
[Dependabot Alert
483](https://github.com/twentyhq/twenty/security/dependabot/483).

Upgraded AWS SDKs pulling in fast-xml-parser as transitive dependency.
2026-02-25 22:01:34 +01:00
WeikoandGitHub 0af980a783 introduce metadata api client to twenty sdk (#18233)
Logic function: hello-world.ts
```typescript
import { CoreApiClient } from 'twenty-sdk/generated/core';
import { MetadataApiClient } from 'twenty-sdk/generated/metadata';

const handler = async () => {
  const coreClient = new CoreApiClient();
  const metadataClient = new MetadataApiClient();

  // Query the core /graphql endpoint — fetch some people
  const coreResult = await coreClient.query({
    people: {
      edges: {
        node: {
          id: true,
          name: {
            firstName: true,
            lastName: true,
          },
        },
      },
    },
  });

  // Query the metadata /metadata endpoint — fetch current workspace
  const metadataResult = await metadataClient.query({
    currentWorkspace: {
      id: true,
      displayName: true,
    },
  });

  return {
    coreResponse: coreResult,
    metadataResponse: metadataResult,
  };
};
```
With route trigger should now produce:
<img width="582" height="238" alt="Screenshot 2026-02-25 at 17 14 29"
src="https://github.com/user-attachments/assets/8c597113-7552-4d32-845a-352083d84ac7"
/>

```json
{
  "coreResponse": {
    "people": {
      "edges": [
        {
          "node": {
            "id": "20202020-b000-4485-94de-70c2a98daef2",
            "name": {
              "firstName": "Jeffery",
              "lastName": "Griffin"
            }
          }
        },
        {
          "node": {
            "id": "20202020-b003-415a-9051-133248495f7f",
            "name": {
              "firstName": "Terry",
              "lastName": "Melendez"
            }
          }
        },
        {
          "node": {
            "id": "20202020-b00e-4bc1-87c8-00aeb49c10f8",
            "name": {
              "firstName": "Lee",
              "lastName": "Jones"
            }
          }
        },
        {
          "node": {
            "id": "20202020-b012-44c1-9fdc-90f110962d07",
            "name": {
              "firstName": "Sarah",
              "lastName": "Hernandez"
            }
          }
        },
      ]
    }
  },
...
  "metadataResponse": {
    "currentWorkspace": {
      "id": "20202020-1c25-4d02-bf25-6aeccf7ea419",
      "displayName": "Apple"
    }
  }
}
2026-02-25 21:42:05 +01:00
Charles BochetandGitHub e01b641a05 Introduce npx nx mock:generate twenty-front (#18237)
## Add codegen script for frontend test mock data

### Summary

- Adds a new `npx nx mock:generate twenty-front` and
`generate-mock-data.ts` script that fetches object metadata from a
running server's `/metadata` endpoint, authenticates with default seeds,
and writes the result to a generated TypeScript file
(`src/testing/mock-data/generated/mock-metadata-query-result.ts`). This
replaces hand-maintained mock metadata with server-sourced data,
ensuring tests always reflect the real schema.
- Updates all frontend tests to be compatible with the newly generated
metadata, fixing hard-coded GraphQL queries, Zod validation schemas,
snapshot expectations, and Apollo mock mismatches.

### What changed

**New files**
- `scripts/generate-mock-data.ts` — codegen script that authenticates
against the server, queries `/metadata` for all object metadata (with
explicit `__typename` at every level), and writes a typed `.ts` file.
- `project.json` — added `mock:generate` Nx target (`dotenv npx tsx
scripts/generate-mock-data.ts`).

**Schema validation updates**
- `objectMetadataItemSchema.ts` — added `universalIdentifier`, made
`duplicateCriteria` nullable.
- `fieldMetadataItemSchema.ts` — added `universalIdentifier`, `morphId`,
`morphRelations`, restructured relation schema.
- `indexMetadataItemSchema.ts` — added optional `isCustom` field.
2026-02-25 21:40:05 +01:00
nitinandGitHub 50be97422d Fix dropdown scroll by restoring scroll container nesting order (#18239) 2026-02-25 19:55:08 +01:00
WeikoandGitHub 4ae8381ffc logicFunction sourceHandlerPath and builtHandlerPath manifest updates are not saved (#18230)
and delete legacy files
2026-02-25 19:24:24 +01:00
Paul RastoinandGitHub 856859f9f3 Fix front comp build/run order to be processed before dependent entities (#18232) 2026-02-25 18:52:59 +01:00
Paul RastoinandGitHub 98d67e0f6b Fix jwt auth guard for application (#18234) 2026-02-25 18:01:55 +01:00
Charles BochetandGitHub 1fa55bfd02 Fix bugs tied to jotai migration (#18227)
Fixing a few bugs:
- CommandMenu not reactive
- Filtering on index view infinite loop
2026-02-25 17:11:12 +01:00
001c2097a3 i18n - docs translations (#18231)
Created by Github action

<!-- mintlify-editor-comments:start -->
Mintlify
---
0 threads from 0 users in Mintlify

- No unresolved comments
<!-- mintlify-editor-comments:end -->

<!-- mintlify-comment-->

<a
href="https://dashboard.mintlify.com/twenty/twenty/editor/i18n-docs?source=pr_comment"
target="_blank" rel="noopener noreferrer"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://d3gk2c5xim1je2.cloudfront.net/assets/open-mintlify-editor-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://d3gk2c5xim1je2.cloudfront.net/assets/open-mintlify-editor-light.svg"><img
src="https://d3gk2c5xim1je2.cloudfront.net/assets/open-mintlify-editor-light.svg"
alt="Open in Mintlify Editor"></picture></a>

<!-- /mintlify-comment -->

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-25 16:50:28 +01:00
9107f5bbc7 feat: upgrade ai package to version six and the corresponding @ai-sdk/* packages to compatible versions (#18172)
Used the migration guide to carry out this upgrade:
https://ai-sdk.dev/docs/migration-guides/migration-guide-6-0

I have not been able to test locally due to credits.

<img width="220" height="450" alt="image"
src="https://github.com/user-attachments/assets/050b34b9-3239-4010-8c47-b43d44571994"
/>

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-02-25 16:49:26 +01:00
Raphaël BosiandGitHub 435e21d23f [FRONT COMPONENTS] Serialize relation between widget and front component (#18228)
This allows us to define page layout widgets of type front component in
an app with the front component universal identifier.
2026-02-25 15:01:50 +01:00
martmullandGitHub 448241540d Improve create-twenty-app command (#18226)
as title
2026-02-25 14:10:47 +01:00
neo773GitHubcubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
e7f958c9cf Fix Google API Availability and a edge case (#18150)
First run shows a real customer account, and the second run is my
account with all the permissions.

<img width="1490" height="386" alt="SCR-20260221-bmfd"
src="https://github.com/user-attachments/assets/1e2bd269-67ab-450d-9bf3-3c4872b90773"
/>


Simulated edge case if user has access to neither

<img width="343" height="162" alt="SCR-20260221-bloo"
src="https://github.com/user-attachments/assets/a697302b-e8b0-432e-bd15-f689da06dc0e"
/>

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-02-25 14:02:19 +01:00
Paul RastoinandGitHub 98e73791a4 Resolve involved application ids from API metadata (#18221)
# Introduction
Resolving each create|updated|deleted entities related entities
application through their universal foreingKey ( silently failing if not
found leaving the validator handling that )
In order to compute all the required application in the dependency flat
entity maps

## Tested
Creating a field on a custom local twenty-apps installed on the
workspace + view field

## Out of scope
- updated snapshot
- update graphql generated front
2026-02-25 13:58:38 +01:00
f3857c41b8 fix dashboard duplicate (#18193)
closes https://github.com/twentyhq/twenty/issues/18192
what was broken - wrong client


before - 


https://github.com/user-attachments/assets/1da60e07-622c-4884-b5bb-9eb7fc954782




after  - 


https://github.com/user-attachments/assets/fcf299f0-2515-410b-ad60-738c4b3dc322

---------

Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com>
2026-02-25 13:55:41 +01:00
Lucas BordeauandGitHub 2c02f2c499 Handle SSE event for View and ViewField (#18218)
This PR implements a first working version of SSE for View and
ViewField.

We could improve the logic but we re-use the same actual codepath for
refreshing the views of an object.

This does not handle filters and sorts for now.

# Demo 


https://github.com/user-attachments/assets/73367f3a-a5fc-4b06-a2e2-fad3b99deec7
2026-02-25 13:43:56 +01:00
Charles BochetandGitHub 121788c42f Fully deprecate old recoil (#18210)
## Summary

Removes the `recoil` dependency entirely from `package.json` and
`twenty-front/package.json`, completing the migration to Jotai as the
sole state management library.

Removes all Recoil infrastructure: `RecoilRoot` wrapper from `App.tsx`
and test decorators, `RecoilDebugObserver`, Recoil-specific ESLint rules
(`use-getLoadable-and-getValue-to-get-atoms`,
`useRecoilCallback-has-dependency-array`), and legacy Recoil utility
hooks/types (`useRecoilComponentState`, `useRecoilComponentValue`,
`createComponentState`, `createFamilyState`, `getSnapshotValue`,
`cookieStorageEffect`, `localStorageEffect`, etc.).

Renames all `V2`-suffixed Jotai state files and types to their canonical
names (e.g., `ComponentStateV2` -> `ComponentState`,
`agentChatInputStateV2` -> `agentChatInputState`, `SelectorCallbacksV2`
-> `SelectorCallbacks`), and removes the now-redundant V1 counterparts.

Updates ~433 files across the codebase to use the renamed Jotai imports,
remove Recoil imports, and clean up test wrappers (`RecoilRootDecorator`
-> `JotaiRootDecorator`).
2026-02-25 12:26:42 +01:00
Abdul RahmanandGitHub ecb7270a7b fix: delete orphan favorites before 1.18 migration (#18219) 2026-02-25 11:30:39 +01:00
martmullandGitHub e365b546d9 Deploy new cli version (#18216)
as title
2026-02-25 10:58:21 +01:00
martmullandGitHub f080b952eb Add manifest integration tests (#18203)
- add integration test on sync manifest endpoint
- fix invalid standard uuid + migration command
2026-02-25 10:37:50 +01:00
martmullandGitHub b84a8588be Fix function logs (#18215)
as title
2026-02-25 10:36:19 +01:00
martmullandGitHub e6a415b438 Fix twenty zapier (#18191)
as title
2026-02-25 10:35:46 +01:00
Charles BochetandGitHub d81f006979 Rename Jotai state hooks and utilities to consistent Atom-based naming (#18209)
## Summary

Rename all Jotai-based state management hooks and utilities to a
consistent naming convention that:
1. Removes legacy Recoil naming (`useRecoilXxxV2`, `createXxxV2`)
2. Always includes `Atom` in the name to distinguish from jotai native
hooks
3. Follows a consistent ordering: `Atom` → `Component` → `Family` →
`Type` (`State`/`Selector`)
4. Includes the type qualifier (`State` or `Selector`) in all
value-reading hooks to avoid naming conflicts with jotai's native
`useAtomValue`

## Naming Convention

**Hooks**:
`use[Set]Atom[Component][Family][State|Selector][Value|State|CallbackState]`
**Utils**: `createAtom[Component][Family][State|Selector]`

## Changes

### Hooks renamed (definition files + all usages across ~1,500 files):

| Old Name (Recoil) | Intermediate (Atom) | Final Name |
|---|---|---|
| `useRecoilValueV2` | `useAtomValue` | **`useAtomStateValue`** |
| `useRecoilStateV2` | `useAtomState` | `useAtomState` |
| `useSetRecoilStateV2` | `useSetAtomState` | `useSetAtomState` |
| `useFamilyRecoilValueV2` | `useFamilyAtomValue` |
**`useAtomFamilyStateValue`** |
| `useSetFamilyRecoilStateV2` | `useSetFamilyAtomState` |
**`useSetAtomFamilyState`** |
| `useFamilySelectorValueV2` | `useFamilySelectorValue` |
**`useAtomFamilySelectorValue`** |
| `useFamilySelectorStateV2` | `useFamilySelectorState` |
**`useAtomFamilySelectorState`** |
| `useRecoilComponentValueV2` | `useAtomComponentValue` |
**`useAtomComponentStateValue`** |
| `useRecoilComponentStateV2` | `useAtomComponentState` |
`useAtomComponentState` |
| `useSetRecoilComponentStateV2` | `useSetAtomComponentState` |
`useSetAtomComponentState` |
| `useRecoilComponentFamilyValueV2` | `useAtomComponentFamilyValue` |
**`useAtomComponentFamilyStateValue`** |
| `useRecoilComponentFamilyStateV2` | `useAtomComponentFamilyState` |
`useAtomComponentFamilyState` |
| `useSetRecoilComponentFamilyStateV2` |
`useSetAtomComponentFamilyState` | `useSetAtomComponentFamilyState` |
| `useRecoilComponentSelectorValueV2` | `useAtomComponentSelectorValue`
| `useAtomComponentSelectorValue` |
| `useRecoilComponentFamilySelectorValueV2` |
`useAtomComponentFamilySelectorValue` |
`useAtomComponentFamilySelectorValue` |
| `useRecoilComponentStateCallbackStateV2` |
`useAtomComponentStateCallbackState` |
`useAtomComponentStateCallbackState` |
| `useRecoilComponentSelectorCallbackStateV2` |
`useAtomComponentSelectorCallbackState` |
`useAtomComponentSelectorCallbackState` |
| `useRecoilComponentFamilyStateCallbackStateV2` |
`useAtomComponentFamilyStateCallbackState` |
`useAtomComponentFamilyStateCallbackState` |
| `useRecoilComponentFamilySelectorCallbackStateV2` |
`useAtomComponentFamilySelectorCallbackState` |
`useAtomComponentFamilySelectorCallbackState` |

### Utilities renamed:

| Old Name (Recoil) | Final Name |
|---|---|
| `createStateV2` | **`createAtomState`** |
| `createFamilyStateV2` | **`createAtomFamilyState`** |
| `createSelectorV2` | **`createAtomSelector`** |
| `createFamilySelectorV2` | **`createAtomFamilySelector`** |
| `createWritableSelectorV2` | **`createAtomWritableSelector`** |
| `createWritableFamilySelectorV2` |
**`createAtomWritableFamilySelector`** |
| `createComponentStateV2` | **`createAtomComponentState`** |
| `createComponentFamilyStateV2` | **`createAtomComponentFamilyState`**
|
| `createComponentSelectorV2` | **`createAtomComponentSelector`** |
| `createComponentFamilySelectorV2` |
**`createAtomComponentFamilySelector`** |

## Details

- All definition files renamed to match new convention
- All import paths and usages updated across the entire `twenty-front`
package
- Jotai's native `useAtomValue` is aliased as `useJotaiAtomValue` in the
wrapper hook to avoid collision
- Legacy Recoil files in `state/utils/` and `component-state/utils/`
left untouched (separate naming scope)
- Typecheck passes cleanly
2026-02-25 01:55:46 +01:00
Charles BochetandGitHub eaf9fe27b2 Improve Jotai work (#18205) 2026-02-24 20:38:30 +01:00
Baptiste DevessierandGitHub b339c93699 Fix app env var fetching (#18199) 2026-02-24 20:20:33 +01:00
nitinandGitHub 887371054a Host-remote refresh token implementation (#18044) 2026-02-24 19:37:29 +01:00
Raphaël BosiandGitHub b56f85f36a Allow video, audio and iFrame in front components (#18200)
Allow video, audio and iFrame in front components

+ fix css style properties starting with `--`
2026-02-24 19:04:52 +01:00
1ed13182d0 i18n - translations (#18197)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-24 18:10:28 +01:00
20d997bc5a Fix: Password validation requires data and hash arguments (#18189)
## Automated fix for [bug 5360](https://sonarly.com/issue/5360?type=bug)

**Severity:** `critical`

### Summary
When an existing user created via OAuth (with no password hash) attempts
to sign in through the signInUp password flow, their null passwordHash
is passed directly to bcrypt.compare, which throws an unhandled 'data
and hash arguments required' error instead of a user-friendly message.

### User Impact
Users who originally registered via Google or Microsoft OAuth and then
attempt to sign in using email/password through the signInUp flow cannot
authenticate. They see a server error instead of a helpful message like
'User was not created with email/password'. This blocks the user from
accessing the CRM entirely.

### Root Cause
Proximate cause: bcrypt.compare() at auth.util.ts:19 throws 'data and
hash arguments required' because the passwordHash argument is null.

1. Why did bcrypt throw? Because compareHash() was called with a null
passwordHash value. The compareHash function at auth.util.ts:19 passes
its arguments directly to bcrypt.compare without any null check.

2. Why was passwordHash null? Because SignInUpService.validatePassword()
at sign-in-up.service.ts:154 received a null passwordHash from
AuthService.validatePassword() at auth.service.ts:232. The value
userData.existingUser.passwordHash is null for users who were originally
created via an OAuth provider (Google, Microsoft) and never set a
password.

3. Why was there no null check before calling bcrypt? Because
AuthService.validatePassword() at line 229-233 does not check whether
userData.existingUser.passwordHash is null before passing it to
signInUpService.validatePassword(). The TypeScript type annotation says
passwordHash is string, but the UserEntity column is declared nullable:
true (user.entity.ts:73), so the runtime value can be null despite the
type system.

4. Why does this code path lack the null check when another path has it?
The validateLoginWithPassword() method at auth.service.ts:177 correctly
checks if (!user.passwordHash) and throws a user-friendly AuthException
'Incorrect login method'. This guard was added by Thomas Trompette on
2024-08-07 (commit 2abb6adb61). However, the signInUp flow's
validatePassword() method was introduced later by Antoine Moreaux on
2025-03-17 (commit bda835b9f8) without replicating this same defensive
check.

5. Why was this not caught? The signInUp validatePassword method was
written assuming the existingUser would always have a passwordHash when
the auth provider is Password. This assumption is incorrect because the
signInUp flow can match an existing OAuth-created user (who has no
passwordHash) when someone tries to sign up with email/password using an
email already registered via OAuth. No test covers this cross-provider
scenario in the signInUp path.

Root cause: The AuthService.validatePassword() method
(auth.service.ts:229-233) in the signInUp code path is missing a null
guard on userData.existingUser.passwordHash before passing it to
bcrypt.compare. The fix should check for null/undefined passwordHash and
throw a descriptive AuthException, mirroring the existing guard in
validateLoginWithPassword at line 177.

**Introduced by:** Antoine Moreaux on 2025-03-17 in commit
[`bda835b`](https://github.com/twentyhq/twenty/commit/bda835b9f82)

### Suggested Fix
Added a null check on userData.existingUser.passwordHash inside the
validatePassword private method of AuthService, immediately before
calling signInUpService.validatePassword. When passwordHash is null or
undefined (i.e. the user was created via OAuth and never set a
password), an AuthException with code INVALID_INPUT is thrown with the
user-friendly message 'User was not created with email/password'. This
mirrors the identical guard that already exists in
validateLoginWithPassword at line 177, preventing bcrypt.compare from
receiving a null argument and crashing with an unhandled error.

### Evidence
- **Code:**
[packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts:229
- if (userData.type === 'existingUser')
{](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts#L229)

---
*Generated by [Sonarly](https://sonarly.com)*

Co-authored-by: Sonarly Claude Code <claude-code@sonarly.com>
2026-02-24 17:52:53 +01:00
Charles BochetandGitHub 0f2af6a6cb Jotai 13 (#18178)
## Recoil to Jotai Migration — PR 13: Workflow, Page Layout, AI,
Activities, Settings, SSE & Remaining Modules

### Summary

This is the 13th PR in the [14-PR migration
series](packages/twenty-front/src/modules/ui/utilities/state/jotai/MIGRATION_PLAN.md)
to replace Recoil with Jotai as the state management library in
`twenty-front`. This PR migrates the remaining consumer code across all
modules — updating ~1,076 files with ~10k insertions/deletions.

### What changed

**New Jotai infrastructure (13 new files):**
- `createComponentSelectorV2` and `createComponentFamilySelectorV2` —
instance-scoped derived atoms, replacing Recoil's component selectors
- 6 new hooks: `useRecoilComponentSelectorValueV2`,
`useRecoilComponentFamilySelectorValueV2`,
`useRecoilComponentSelectorCallbackStateV2`,
`useRecoilComponentFamilySelectorCallbackStateV2`,
`useRecoilComponentStateCallbackStateV2`,
`useRecoilComponentFamilyStateCallbackStateV2`
- New types: `ComponentSelectorV2`, `ComponentFamilySelectorV2`,
`SelectorCallbacksV2`
- Extended `buildGetHelper` to support the new selector patterns

**State definition migrations:**
- `createState()` → `createStateV2()` (Jotai `atom()`)
- `createFamilyState()` → `createFamilyStateV2()` (Jotai `atom()` with
family cache)
- Recoil `selector`/`selectorFamily` → Jotai derived `atom()` via V2
utilities

**Hook migrations (~2,400 removed, ~1,545 added V2 equivalents):**
- `useRecoilValue` → `useRecoilValueV2`
- `useRecoilState` → `useRecoilStateV2`
- `useSetRecoilState` → `useSetRecoilStateV2`
- `useRecoilCallback` → `useCallback` + `jotaiStore.get/set`
- `useRecoilComponentValue` → `useRecoilComponentValueV2`
- `useSetRecoilComponentState` → `useSetRecoilComponentStateV2`
- `useRecoilComponentFamilyValue` → `useRecoilComponentFamilyValueV2` /
`useFamilySelectorValueV2`
- ~397 direct `import from 'recoil'` removed

**Deleted files (9 files):**
- `dateLocaleState.ts` — consolidated
- `currentAIChatThreadTitleStateV2.ts` — renamed to replace the original
- `isCommandMenuOpenedState.ts` — removed
- `filesFieldUploadState.ts` — replaced by V2
- `recordStoreFamilySelector.ts`, `recordStoreFieldValueSelector.ts`,
`recordStoreRecordsSelector.ts` — replaced by V2 equivalents
- `settingsAllRolesSelector.ts` — replaced by `useSettingsAllRoles` hook
- `settingsRoleIdsStateV2.ts` — consolidated

**Modules migrated:**
- Workflow (diagram, steps, variables, filters)
- Page Layout (widgets, fields, graph, tabs)
- AI (chat, agents, browsing context)
- Activities (rich text editor, targets, timeline)
- Action Menu (single/multiple record actions)
- Command Menu (navigation, history, pages, workflow)
- Context Store
- Record Board, Record Table, Record Calendar, Record Index
- Record Field, Record Filter, Record Sort, Record Group
- Record Drag, Record Picker, Record Store
- Views (view bar, view picker, filters, sorts)
- Settings (roles, permissions, SSO, security, data model, developers,
domains)
- SSE (event streams, subscriptions)
- Spreadsheet Import
- Auth, Favorites, Front Components, Information Banner
2026-02-24 17:37:01 +01:00
Paul RastoinandGitHub c6ec764b23 Fix ci-server ci (#18180) 2026-02-24 12:51:46 +01:00
959cbfb725 i18n - translations (#18183)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-24 12:08:42 +01:00
Félix MalfaitandGitHub 458a61ed30 Change runner from depot-ubuntu-24.04 to ubuntu-latest (#18182) 2026-02-24 10:17:38 +01:00
9a3852bf04 feat: add two-layer AI model availability filtering (#18170)
## Summary

- **Admin-level filtering**: New AI tab in admin panel with server-wide
model availability controls (whitelist/blacklist via
`AI_AUTO_ENABLE_NEW_MODELS`, `AI_DISABLED_MODEL_IDS`,
`AI_ENABLED_MODEL_IDS` config variables). Dedicated
`setAdminAiModelEnabled` mutation replaces frontend config-variable
manipulation. Filter dropdown to show/hide unconfigured and deprecated
models.
- **Workspace-level filtering**: Per-workspace controls with "Use best
models only" mode (curated list backed by `isRecommended` flag), or
custom whitelist/blacklist. Separate Smart/Fast model selectors with
"Best (...)" virtual options.
- **Security enforcement**: Both layers enforced at every backend
execution point — workspace update, agent create/update, chat execution.
Model ID validated against known models before config mutation. All
admin endpoints protected by `AdminPanelGuard`.

## Changes

### Backend (`twenty-server`)
- New config variables for admin-level model filtering
- `AiModelRegistryService`: `getAllModelsWithStatus()`,
`setModelAdminEnabled()` with model ID validation,
`isModelAdminAllowed()`
- `AdminPanelResolver`: `getAdminAiModels` query,
`setAdminAiModelEnabled` mutation
- `WorkspaceEntity`: new fields (`autoEnableNewAiModels`,
`disabledAiModelIds`, `enabledAiModelIds`, `useRecommendedModels`)
- `WorkspaceService`: model validation on `smartModel`/`fastModel`
updates
- `AgentResolver`: model availability checks on create/update
- `isModelAllowedByWorkspace` centralized utility
- `isRecommended` flag on model definitions
- Two TypeORM migrations

### Frontend (`twenty-front`)
- New `SettingsAdminAI` component with search, filter dropdown
(unconfigured/deprecated), and model toggle cards
- AI tab added to admin panel navigation
- `useWorkspaceAiModelAvailability` hook for workspace-level filtering
- `SettingsAIModelsTab` redesigned: merged sections, "Use best models
only" toggle, conditional available models list
- `getModelIcon`/`getModelProviderLabel` shared utilities with GraphQL
enum casing normalization
- Updated generated GraphQL types and mock data

## Test plan
- [ ] Toggle models on/off in admin panel AI tab and verify they
appear/disappear in workspace settings
- [ ] Enable "Use best models only" in workspace settings and verify
only recommended models are selectable
- [ ] Disable recommended mode and verify whitelist/blacklist toggles
work correctly
- [ ] Verify deprecated models hidden by default, shown greyed out when
filter enabled
- [ ] Verify unconfigured models hidden by default, shown disabled when
filter enabled
- [ ] Try setting a disabled model as Smart/Fast model — should be
rejected
- [ ] Try creating an agent with a disabled model — should be rejected
- [ ] Verify admin panel AI tab requires admin access


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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 10:14:24 +01:00
c369baf63a i18n - translations (#18176)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-24 10:07:16 +01:00
Paul RastoinandGitHub 9e8024a07a Restore depot (#18179) 2026-02-24 09:59:54 +01:00
a827681306andGitHub df196b4a03 fix: use default Apollo client for AI chat file upload (#18158)
## Summary

Fix file uploads in AI chat (Agent Chat) failing with "Failed to upload
file" for all file types.

## Root Cause

`useAIChatFileUpload.ts` was using `useApolloCoreClient()` which points
to the `/graphql` (workspace) endpoint. However, `FileUploadResolver` is
decorated with `@MetadataResolver()`, meaning `uploadFile` is only
registered on the `/metadata` endpoint.

## Fix

Replace `useApolloCoreClient()` with `useApolloClient()` from
`@apollo/client`, which is the default Apollo client pointing to the
`/metadata` endpoint. This is consistent with how
`useUploadAttachmentFile.tsx` handles file uploads.

## Changes

- Replaced `useApolloCoreClient` import with `useApolloClient` from
`@apollo/client`
- Updated the client variable to use the default Apollo client
- Removed unused `useApolloCoreClient` import

Fixes #18153
2026-02-24 09:54:29 +01:00
Paul RastoinandGitHub af0af0a237 Fix cross app installation (#18151)
# Introduction
- Fixing app dependencyFlatEntityMaps load ( to load current app +
dependent app )
- Fix empty flat entity maps and undefined optimistic override
- Refactor the optimistic rendering to be mutation oriented at
orchestrator level
2026-02-23 19:58:06 +01:00
Charles BochetandGitHub 0d4fe4575b Various SDK improvements (#18115)
## Summary

- **Refactor frontend metadata loading architecture**: Split the
monolithic `EagerMetadataLoadEffect` into focused provider effects
(`UserMetadataProviderEffect`, `ObjectMetadataProviderEffect`,
`ViewMetadataProviderEffect`) orchestrated by `MetadataProviderEffects`.
Replaced `UserProvider` + `ObjectMetadataItemsProvider` with a single
`MetadataGater` that gates rendering on `isAppMetadataReadyState`. The
metadata store now validates view-object consistency before promoting
views, and `updateDraft` skips no-op updates via deep equality checks.

- **SDK CLI improvements**: Added `app:typecheck` command, improved
error handling in API sync (extracts GraphQL error messages), added
`serializeError` utility for human-readable error output, added `error`
file status to dev mode orchestrator with UI support, and fixed
ClickHouse migration/seed commands to use `transpile-only`.
2026-02-23 19:57:02 +01:00
Thomas TrompetteandGitHub ccddd105d8 Add generate key command (#18171)
Will be useful to auto log users on app creation
2026-02-23 19:56:44 +01:00
e21a7e11b2 i18n - translations (#18175)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-23 19:56:31 +01:00
WeikoandGitHub 1ac87d106e With RLS predicates on Select, only show possible values in option picker (#18166)
## Context
Removing SELECT/MULTI_SELECT options that can't be selected due to RLS
(see https://github.com/twentyhq/core-team-issues/issues/2226)

<img width="521" height="276" alt="Screenshot 2026-02-23 at 11 30 21"
src="https://github.com/user-attachments/assets/238ff596-cd8c-4660-a709-3eb2486e23b4"
/>
<img width="279" height="200" alt="Screenshot 2026-02-23 at 11 30 03"
src="https://github.com/user-attachments/assets/7a627d74-7519-4314-9a0e-0890cb8cbf30"
/>
2026-02-23 19:25:58 +01:00
CopilotGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>DevessierBaptiste Devessier
9a0295fd3d feat: add atomic upsertFieldsWidget mutation to replace multiple view field group/field API calls (#18137)
The `useSaveFieldsWidgetGroups` hook was making multiple sequential API
calls per widget (create groups, delete groups, update groups, update
fields) — non-atomic, chatty, and with heavy diff computation on the
frontend.

## Backend

- **New input DTOs**: `UpsertFieldsWidgetInput` /
`UpsertFieldsWidgetGroupInput` / `UpsertFieldsWidgetFieldInput` — caller
passes the full desired state of groups + fields for a widget
- **`FieldsWidgetUpsertService`**: looks up widget → resolves `viewId`,
diffs against existing flat entity maps, builds optimistic group maps so
newly-created groups can be referenced by field updates in the same
pass, then runs creates/updates/deletes in a single
`validateBuildAndRunWorkspaceMigration` call
- **`upsertFieldsWidget` mutation** added to `ViewFieldGroupResolver`;
new `FIELDS_WIDGET_NOT_FOUND` exception code added and wired into the
GraphQL exception filter

## Frontend

- New `UPSERT_FIELDS_WIDGET` GQL document
- `useSaveFieldsWidgetGroups` replaces all per-operation calls with a
single mutation per widget, passing the full draft state — diff logic
moves entirely to the backend

```graphql
mutation UpsertFieldsWidget($input: UpsertFieldsWidgetInput!) {
  upsertFieldsWidget(input: $input) {
    id
    name
    position
    isVisible
    viewId
    viewFields { id isVisible position ... }
  }
}
```

Widget IDs are collected from both draft **and** persisted state so
widgets whose draft groups were cleared still trigger deletion of their
server-side groups.

<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

Start implementation

The user has attached the following file paths as relevant context:
 - CLAUDE.md

<analysis>
[Chronological Review: The conversation began with the user requesting a
review of how updates on views are stored in the frontend of record page
layouts. The user expressed a desire to migrate heavy computations and
multiple requests from the frontend to the backend. They proposed
creating a custom upsertFieldsWidget mutation to handle updates
atomically. The conversation has focused on this migration strategy and
its implementation.]

[Intent Mapping: The user explicitly requested to "check how we store
updates on views in the frontend of record page layouts" and indicated a
need to "migrate this task to the backend" by creating a custom mutation
for atomic updates.]

[Technical Inventory: The conversation involves the following technical
concepts:
- Frontend and backend architecture for handling page layout updates.
- Custom GraphQL mutation (upsertFieldsWidget) for atomic updates.
- The file in focus is page-layout-update.service.ts, which is likely
responsible for handling updates related to page layouts in the
backend.]

[Code Archaeology: The specific file being worked on is
/Users/devessier/Web/twenty/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-update.service.ts.
The user is likely looking to modify or enhance this service to
accommodate the new mutation for handling updates.]

[Progress Assessment: The user has identified the need for a backend
migration strategy but has not yet implemented any changes. The focus is
on planning the transition from frontend to backend processing.]

[Context Validation: All critical information for continuation is
captured, including the user's intent to migrate computations to the
backend and the specific file being modified.]

[Recent Commands Analysis: The last agent commands involved checking the
current file context and the user's request to summarize the
conversation. No specific tool results were generated as the focus was
on summarizing the conversation history. The agent was actively working
on summarizing the conversation when the token budget was exceeded,
which was triggered by the user's request for a summary.]

</analysis>

<summary>
1. Conversation Overview:
- Primary Objectives: The user requested to "check how we store updates
on views in the frontend of record page layouts" and expressed a desire
to "migrate this task to the backend" by creating a custom
upsertFieldsWidget mutation for atomic updates.
- Session Context: The conversation has focused on the need to shift
heavy computations from the frontend to the backend, with an emphasis on
implementing a new mutation to streamline updates.
- User Intent Evolution: The user's needs have remained consistent,
focusing on backend migration for efficiency and atomicity in updates.

2. Technical Foundation:
- Core Technology: The conversation involves a backend service likely
built with Node.js and TypeScript, as indicated by the file path.
- Framework/Library: The use of GraphQL for handling mutations is
implied through the mention of a custom mutation.
- Architectural Pattern: The user is looking to implement a
service-oriented architecture where the backend handles complex
computations instead of the frontend.
- Environment Detail: The current working file is located in a server
package of the twenty repository.

3. Codebase Status:
- File Name: page-layout-update.service.ts
- Purpose: This file is responsible for managing updates related to page
layouts in the backend.
- Current State: The user is considering modifications to implement a
new mutation for handling updates.
- Key Code Segments: Specific functions or classes have not been
detailed yet, as the focus is on planning changes.
- Dependencies: This service likely interacts with other components in
the metadata-modules related to page layouts.

4. Problem Resolution:
- Issues Encountered: The current challenge is the inefficiency of
handling updates in the frontend.
- Solutions Implemented: The proposed solution is to create a custom
mutation to handle updates atomically in the backend.
- Debugging Context: No ongoing troubleshooting efforts have been
mentioned yet.
- Lessons Learned: The need for backend processing to improve
performance has been highlighted.

5. Progress Tracking:
- Completed Tasks: No tasks have been completed yet; the user is in the
planning phase.
- Partially Complete Work: The user is preparing to implement a new
mutation for backend updates.
- Validated Outcomes: No features have been confirmed working through
testing at this stage.

6. Active Work State:
- Current Focus: The user is focused on modifying the
page-layout-update.service.ts to implement the new mutation.
- Recent Context: The last few exchanges involved discussing the
migration of update tasks from the frontend to the backend.
- Working Code: No specific code snippets have been modified yet, as the
co...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

Created from [VS
Code](https://code.visualstudio.com/docs/copilot/copilot-coding-agent).

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions,
customizing its development environment and configuring Model Context
Protocol (MCP) servers. Learn more [Copilot coding agent
tips](https://gh.io/copilot-coding-agent-tips) in the docs.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Devessier <29370468+Devessier@users.noreply.github.com>
Co-authored-by: Baptiste Devessier <baptiste@devessier.fr>
2026-02-23 19:25:47 +01:00
a0c0e7de25 [FRONT COMPONENTS] Add snackbar to the API (#18136)
## PR description

- Adds `enqueueSnackbar` to the front component host communication API,
allowing front components to display snack bar notifications (success,
error, info, warning) to the user.
- Introduces `frontComponentId` to the `FrontComponentExecutionContext`
and a `useFrontComponentId` hook, enabling front components to identify
themselves (used for dedupe keys).
- Wires error/success notifications into the `Action`, `ActionLink`, and
`ActionOpenSidePanelPage` SDK components -> actions now catch errors and
display them as snack bars, and `Action` supports an optional
`notifyOnEnd` prop for success feedback.

## Video QA

### Success example


https://github.com/user-attachments/assets/8cc53d31-d9eb-49a8-9220-f7866ec1b415

### Error example


https://github.com/user-attachments/assets/a37d65b8-0b5f-4adb-bcdb-571bb2b997c3

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-02-23 12:25:05 +01:00
EtienneGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
7cbbd082e3 Billing - Fix (#18135)
Context : https://github.com/twentyhq/private-issues/issues/425
Try to fix https://github.com/twentyhq/core-team-issues/issues/2158

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-02-23 12:13:19 +01:00
Charles BochetandGitHub 7944ca29fa Fix CI (#18168)
Fix 2 issues on CI:
- website still using ubuntu-latest
- linter that cannot use more than 2GB
2026-02-23 12:05:53 +01:00
b51eb6471f Jotai 12 (#18160)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 12:01:37 +01:00
Raphaël BosiandGitHub 22bc3004b9 [FRONT COMPONENTS] Add loader to command menu items (#18165)
## PR description

- Add a loader to the command menu items when the action is running.
- Add a new method `closeSidePanel` to the front component host api.

## Video QA


https://github.com/user-attachments/assets/c20b592a-6e4c-4cab-b5da-4cb2316acc7c
2026-02-23 11:15:45 +01:00
Charles BochetandGitHub 129d1ede86 Change runners temp (#18163)
Temporarily moving all ubuntu-latest 1 core to depot except ci-website
2026-02-23 10:53:31 +01:00
a2e3af5625 i18n - translations (#18156)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-22 15:40:35 +01:00
41f09c5a4c Add AI model pricing, Bedrock provider, and InferenceProvider/ModelFamily split (#18155)
## Summary

- **Model pricing overhaul**: All model constants updated with accurate
pricing in dollars per 1M tokens, including cached input rates, cache
creation rates, and tiered >200k context pricing
- **New providers**: Added Google (Gemini 3.x), Mistral, and AWS Bedrock
as inference providers. Bedrock serves Claude Opus 4.6 and Sonnet 4.6
via AWS, with proper credential handling following the existing S3/SES
pattern
- **InferenceProvider/ModelFamily split**: Refactored `ModelProvider`
into two orthogonal enums — `InferenceProvider` (who serves the model:
auth, SDK, metadata format) and `ModelFamily` (who created it: token
counting semantics). This eliminates growing `||` chains for token
normalization checks like `excludesCachedTokens`
- **Billing improvements**: Reasoning tokens charged at output rate,
cache token discounts applied accurately, real errors thrown to Sentry
on billing failures

## Test plan

- [x] All existing unit tests updated and passing (23 tests across 3
test files)
- [x] Lint passes for both twenty-server and twenty-front
- [ ] CI checks pass


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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-22 14:39:04 +01:00
a900b4a4b4 i18n - docs translations (#18141)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-22 12:36:36 +01:00
020cf7f3e0 i18n - translations (#18146)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-22 12:36:23 +01:00
0bccdedb4f feat: design the ai-models tab on settings ai page (#18147)
Remove AI model selectors from the settings tab on AI settings page,
rename that tab to "More" and create a separate "Models" tab as per the
design.


https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=91311-278313&m=dev

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-02-22 11:41:26 +01:00
martmullandGitHub 088be4c47b Expose standard universal identifiers (#18152)
as title
2026-02-21 23:53:55 +00:00
ad88108b28 Update README: add E2B logo and improve heading (#18144)
## Summary
- Add E2B logo to the Thanks section (after Crowdin, same height)
- Rename section heading from "Does the world need another CRM?" to "Why
Twenty" for a more confident, direct tone

## Test plan
- [ ] Verify E2B logo renders correctly on GitHub README
- [ ] Confirm all other logos still display properly

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

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 15:02:11 +01:00
Raphaël BosiandGitHub 58e8b466f3 Fix frontComponentHostCommunicationApi is not defined error (#18145)
Fix missing import
2026-02-20 18:49:03 +01:00
martmullandGitHub ba59ab8094 Add upload file in twenty client (#18129)
This function 

```typescript
import { defineLogicFunction } from 'twenty-sdk';
import TwentyClient from 'twenty-sdk/generated';

import { v4 } from 'uuid';

export const POST_INSTALL_UNIVERSAL_IDENTIFIER =
  '1312be5c-4fd6-44b3-afd0-567352616c7c';

const handler = async (): Promise<any> => {
  const client = new TwentyClient();

  const seedResult = await client.mutation({
    createCompany: {
      id: true,
      name: true,
      __args: {
        data: {
          name: `toto + ${v4()}`,
        },
      },
    },
  });

  const companyFieldUniversalIdentifier =
    '20202020-15db-460e-8166-c7b5d87ad4be';

  const uploadFileResult = await client.uploadFile(
    Buffer.from('hello world', 'utf-8'),
    'test.txt',
    undefined,
    companyFieldUniversalIdentifier,
  );

  await client.mutation({
    createAttachment: {
      id: true,
      __args: {
        data: {
          file: [{ fileId: uploadFileResult.id, label: 'hello-world.txt' }],
          fullPath: uploadFileResult.path,
          targetCompanyId: seedResult.createCompany?.id,
        },
      },
    },
  });

  return;
};

export default defineLogicFunction({
  universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
  name: 'post-install',
  description: 'Add a description for your logic function',
  timeoutSeconds: 30,
  handler,
});

```

produces this record below 

<img width="1512" height="859" alt="image"
src="https://github.com/user-attachments/assets/57dc3a6b-fd2c-4f21-8331-8f1f05403826"
/>
2026-02-20 17:11:59 +00:00
Thomas TrompetteandGitHub cd1a2712d5 Add server health check every 2 secs on app:dev (#18142)
https://github.com/user-attachments/assets/ff10503f-ebc0-417b-b0f3-8aa40dee74e2
2026-02-20 17:01:45 +00:00
Thomas TrompetteandGitHub 932adf8e4b Create view and navigation item on add-object command - to be tested (#18134)
<img width="580" height="141" alt="Capture d’écran 2026-02-20 à 15 52
11"
src="https://github.com/user-attachments/assets/40254d3a-f7f9-4f2a-a51f-c37dc1f5d167"
/>
2026-02-20 16:41:16 +00:00
Lucas BordeauandGitHub 26fabdc3d0 Minor fixes following up Temporal refactor (#18139)
Fixes https://github.com/twentyhq/core-team-issues/issues/2034
2026-02-20 15:54:38 +00:00
82617727a2 i18n - docs translations (#18131)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-20 16:33:06 +01:00
Raphaël BosiandGitHub 87922253f3 Fix front component cleanup (#18132)
Fix front component cleanup
2026-02-20 15:03:10 +00:00
Raphaël BosiandGitHub 7da8450075 [FRONT COMPONENTS] Headless components (#18096)
## Description

- Add `isHeadless` field to `FrontComponent` entity so front components
can run without rendering UI in the command menu
- Introduce headless front component mounting logic:
`HeadlessFrontComponentMountRoot` at the application root,
`useMountHeadlessFrontComponent`, and `useUnmountHeadlessFrontComponent`
hooks to mount/unmount headless components
- Expand the SDK with new action components (`Action`, `ActionLink`,
`ActionOpenSidePanelPage`) and host communication functions
(`openSidePanelPage`, `unmountFrontComponent`)
- Move `CommandMenuPages` type to twenty-shared so the SDK can reference
it for side panel navigation

## Video QA



https://github.com/user-attachments/assets/4f9e3bb1-fcd1-42be-b3f4-a97e80c2add2
2026-02-20 14:14:42 +00:00
d444648cc0 i18n - translations (#18127)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-20 14:41:58 +01:00
ec57d3fe0f Fix flaky 2FA encryption test for AES-256-CBC wrong-key behavior (#18126)
## Summary

- Fixes a flaky test in `simple-secret-encryption.util.spec.ts` that
fails ~1 in 256 runs
- AES-256-CBC doesn't guarantee wrong-key decryption throws — PKCS7
padding validation is probabilistic. When padding accidentally looks
valid, decryption silently returns garbage instead of throwing.
- Changed the test to verify the correct security property: wrong-key
decryption must never return the original secret (both throw and garbage
are acceptable)
- Audited both production `decryptSecret` call sites in
`two-factor-authentication.service.ts` — they always use the correct
key, so end users are not affected

## Test plan

- [x] Test passes 5/5 consecutive runs locally
- [x] Lint passes


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

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-20 14:41:31 +01:00
Baptiste DevessierandGitHub 00209f7e2c Wire fields widget to backend + basic edition (#17965)
Closes https://github.com/twentyhq/core-team-issues/issues/2215
Closes https://github.com/twentyhq/core-team-issues/issues/2216
Closes https://github.com/twentyhq/core-team-issues/issues/2218

## Fields widget edition demo


https://github.com/user-attachments/assets/08626d70-8fcb-4ae2-9222-10ef57e75f90

## Dashboards still work


https://github.com/user-attachments/assets/aa9a9c45-a0a2-481e-b132-bc52254778da
2026-02-20 13:18:00 +00:00
b107020ad3 Fix search edge case with permissions (#18123)
Fix E2E tests

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-20 14:08:20 +01:00
9f3f2e21d8 i18n - docs translations (#18122)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-20 13:51:43 +01:00
21472da38f i18n - translations (#18120)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-20 13:51:34 +01:00
Paul RastoinandGitHub bf1e0d0811 Typecheck main sdk (#18124) 2026-02-20 13:47:57 +01:00
Abdullah.andGitHub 917a36821a fix: hover appearing on not shared for junction relations (#18058)
Hovering over "Not Shared" in junction relations shows the placeholder
text.

<img width="1039" height="862" alt="image"
src="https://github.com/user-attachments/assets/a9523f51-c417-41b0-9926-d154c9601fd8"
/>

<br />
<br />

This PR fixes it.

<img width="1040" height="860" alt="image"
src="https://github.com/user-attachments/assets/09d5fecf-1faa-449d-86d9-a105464d31c3"
/>
2026-02-20 13:16:49 +01:00
66da296799 Unify MCP into single endpoint with lazy tool discovery (#18113)
## Summary

- Consolidates the MCP server from two endpoints (`/mcp` +
`/mcp/metadata`) into a **single `POST /mcp`** endpoint exposing five
high-level tools: `get_tool_catalog`, `learn_tools`, `execute_tool`,
`load_skills`, and `search_help_center`
- Fixes **DATABASE_CRUD tools not accessible via API key auth** by
removing an unnecessary `userId`/`userWorkspaceId` guard in
`DatabaseToolProvider` and threading `ApiKeyWorkspaceAuthContext`
through the tool context chain
- Improves tool descriptions with **STEP 1/2/3 workflow guidance** so AI
clients follow the correct discovery flow (catalog → learn → execute)
instead of guessing tool names
- Simplifies the **frontend AI settings** by removing the schema picker
dropdown (no more "Core Schema" vs "Metadata Schema" choice)

## Changes

### Backend
- **Deleted**: `mcp-metadata.controller.ts`, `mcp-metadata.service.ts`
(merged into core)
- **New**: `get-tool-catalog.tool.ts` — browsable, categorized tool
discovery
- **Fixed**: `DatabaseToolProvider.generateDescriptors` — removed guard
that blocked API key access to CRUD tools
- **Fixed**: `ToolContext` type + `ToolRegistryService` — `authContext`
now flows through so API key CRUD execution works end-to-end
- **Updated**: `McpProtocolService` — builds
`ApiKeyWorkspaceAuthContext` for API key requests
- **Updated**: All MCP tool descriptions with explicit step numbering

### Frontend
- **Simplified**: `SettingsAIMCP.tsx` — removed schema selector
dropdown, shows single MCP config

## Test plan
- [x] All 19 MCP unit tests pass (controller + protocol service)
- [x] Server and frontend lint clean
- [x] Server and frontend typecheck pass
- [x] Live-tested on localhost:3000 with API key: `get_tool_catalog`
returns 236 tools including 196 DATABASE_CRUD tools
- [x] Live-tested `execute_tool` with `find_companies` via API key —
returns real data
- [x] Tested MCP connection from Cursor IDE via project-level
`.cursor/mcp.json`

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-20 13:09:54 +01:00
3bc887e12e Add default relation to standard object on custom object in manifest (#18033)
add default relation fields when creating an object from an application

---------

Co-authored-by: prastoin <paul@twenty.com>
2026-02-20 11:49:06 +01:00
Thomas TrompetteandGitHub d060c843d1 Remove sse feature flag (#18114)
As title
2026-02-20 11:46:03 +01:00
Paul RastoinandGitHub 175df59c21 Support Skill in manifest (#18092)
# Introduction
Support skill in manifest, pre-requisite for the twenty standard app
migration
2026-02-20 11:45:42 +01:00
6ad581d178 Fix global search for CJK and non-tokenizable text (#18030)
## Summary

Fixes #12962

- Adds a two-pass ILIKE fallback to the global search service
(`search.service.ts`)
- **Fast path**: runs the tsvector query first (uses GIN index,
sub-millisecond)
- **Fallback**: only if tsvector returns fewer results than the limit,
runs an ILIKE query on `searchVector::text` to catch cases where
PostgreSQL's `simple` text search config fails to tokenize (continuous
CJK text, etc.)
- Zero performance impact for the common case (Latin text where tsvector
works)
- Also adds `escapeForIlike` utility to properly escape `%`, `_`, `\` in
user input

### Why tsvector fails for CJK

PostgreSQL's `simple` config treats continuous CJK text as a single
lexeme:
- `to_tsvector('simple', '示例商业线索')` → `'示例商业线索':1`
- Searching `商业:*` only prefix-matches from the start, so it misses `商业`
in the middle

The ILIKE fallback catches these substring matches when the tsvector
path can't.

### What this fixes

- Global search (command menu / sidebar)
- Relation picker (single and multi-object)
- Morph relation picker

All three use `search.service.ts` under the hood.

Co-authored-by: mykh-hailo (original direction in #18021)

## Test plan

- [ ] Search `示例` with records `示例商业线索` and `示例-商业-线索` → both should
appear
- [ ] Search `商业` → both should appear (previously only the hyphenated
one did)
- [ ] Search for Latin text (e.g. `john`) → same performance, results
unchanged
- [ ] Relation picker search with CJK text → results appear
- [ ] Search input with special chars like `%` or `_` → no SQL
injection, results correct


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

---------

Co-authored-by: mykh-hailo <mykh-hailo@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-20 11:02:09 +01:00
neo773andGitHub 712b1553bd Fix participant matching failing due to updating all columns instead of only changed fields (#18105)
The updateMany in matchParticipants was spreading the entire participant
object into the SET clause, which included generated columns
(searchVector) and composite field columns (createdBySource) that can't
be written to. Narrowed the update to only set personId and
workspaceMemberId.
```
[1] query failed: UPDATE "workspace_3ixj3i1a5avy16ptijtb3lae3"."calendarEventParticipant" SET "id" = $1, "createdAt" = $2, "updatedAt" = $3, "deletedAt" = $4, "createdBySource" = $5, "createdByWorkspaceMemberId" = $6, "createdByName" = $7, "createdByContext" = $8, "updatedBySource" = $9, "updatedByWorkspaceMemberId" = $10, "updatedByName" = $11, "updatedByContext" = $12, "position" = $13, "searchVector" = $14, "handle" = $15, "displayName" = $16, "isOrganizer" = $17, "responseStatus" = $18, "calendarEventId" = $19, "personId" = $20, "workspaceMemberId" = $21 WHERE "id" = $22 RETURNING * -- PARAMETERS: ["f1526103-451a-429f-9743-3a81c3a3e3aa","2026-02-19T22:23:32.354Z","2026-02-19T22:23:32.354Z",null,"MANUAL",null,"System",null,"MANUAL",null,"System",null,0,"'test@test.com':1","test@test.com","",false,"NEEDS_ACTION","fb1892a8-6daf-435b-a43b-e8fe8693eb99","60bf9e82-f1b7-4bed-a1af-7039fb9c32f5",null,"f1526103-451a-429f-9743-3a81c3a3e3aa"]
[1] error: error: column "searchVector" can only be updated to DEFAULT
[1] Exception Captured
[1]   undefined
[1]   [
[1]     PostgresException [Error]: Data validation error.
[1]         at computeTwentyORMException (/Users/huzef/Documents/bounties/twenty/packages/twenty-server/dist/engine/twenty-orm/error-handling/compute-twenty-orm-exception.js:35:19)
[1]         at WorkspaceUpdateQueryBuilder.executeMany (/Users/huzef/Documents/bounties/twenty/packages/twenty-server/dist/engine/twenty-orm/repository/workspace-update-query-builder.js:269:82)
[1]         at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
[1]         at async WorkspaceRepository.updateMany (/Users/huzef/Documents/bounties/twenty/packages/twenty-server/dist/engine/twenty-orm/repository/workspace.repository.js:234:25)
[1]         at async MatchParticipantService.matchParticipants (/Users/huzef/Documents/bounties/twenty/packages/twenty-server/dist/modules/match-participant/match-participant.service.js:93:13)
[1]         at async /Users/huzef/Documents/bounties/twenty/packages/twenty-server/dist/modules/match-participant/match-participant.service.js:160:13
[1]         at async MatchParticipantService.matchParticipantsForPeople (/Users/huzef/Documents/bounties/twenty/packages/twenty-server/dist/modules/match-participant/match-participant.service.js:130:9)
[1]         at async CalendarEventParticipantMatchParticipantJob.handle (/Users/huzef/Documents/bounties/twenty/packages/twenty-server/dist/modules/calendar/calendar-event-participant-manager/jobs/calendar-event-participant-match-participant.job.js:45:13)
[1]         at async MessageQueueExplorer.invokeProcessMethods (/Users/huzef/Documents/bounties/twenty/packages/twenty-server/dist/engine/core-modules/message-queue/message-queue.explorer.js:113:17)
[1]         at async MessageQueueExplorer.handleProcessor (/Users/huzef/Documents/bounties/twenty/packages/twenty-server/dist/engine/core-modules/message-queue/message-queue.explorer.js:104:13) {
[1]       code: '428C9'
[1]     }
[1]   ]
```
2026-02-20 10:25:10 +01:00
sonarly[bot]GitHubSonarly Claude Codeclaude[bot] <41898282+claude[bot]@users.noreply.github.com>Etienne
cc1145c701 Fix: Person avatar upload fails with unknown field error (#18109)
## Automated fix for [bug None](https://sonarly.com/issue/None?type=bug)

**Severity:** `critical`

### Summary
When uploading a person avatar with the new FILES field migration
enabled, the optimistic record validation in
computeOptimisticRecordFromInput throws because the avatarFile field may
not be recognized in the person object metadata, crashing the upload
flow.

### User Impact
Users with the IS_FILES_FIELD_MIGRATED feature flag enabled cannot
upload or change a person's avatar photo. The upload operation throws an
unhandled error, preventing the avatar update from completing.

### Root Cause
The usePersonAvatarUpload hook passes avatarFile as a field in
updateOneRecordInput when calling updateOneRecord. This input goes
through computeOptimisticRecordFromInput, which validates that every
field in the record input exists in objectMetadataItem.fields. The
avatarFile field (type FILES) was recently added as a standard field on
the person object, but may not yet be present in the frontend's cached
object metadata for all workspaces (e.g., workspaces where the metadata
has not been synced after the migration, or SSE events arriving before
metadata refresh). When avatarFile is not found in the metadata fields
array, the validation throws. The useUpdateOneRecord hook supports an
optimisticRecord parameter that bypasses this validation entirely, but
usePersonAvatarUpload did not provide it.

Introduced by Etienne in commit d0c1841f0f on 2026-02-11, which added
the avatar file migration and upload logic but did not supply an
optimisticRecord to bypass the strict field validation in
computeOptimisticRecordFromInput.

**Introduced by:** Etienne on 2026-02-11 in commit
[`d0c1841`](https://github.com/twentyhq/twenty/commit/d0c1841f0f8a6a9069bbe2e9cafacf4ff6137f82)

### Suggested Fix
Pass an explicit optimisticRecord parameter when calling updateOneRecord
from usePersonAvatarUpload. The useUpdateOneRecord hook uses
optimisticRecord via nullish coalescing (optimisticRecord ??
computeOptimisticRecordFromInput(...)), so providing it bypasses the
strict field validation in computeOptimisticRecordFromInput entirely.
The avatarFile value is extracted into a shared variable to avoid
duplication between updateOneRecordInput and optimisticRecord.

---
*Generated by [Sonarly](https://sonarly.com)*

---------

Co-authored-by: Sonarly Claude Code <claude-code@sonarly.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Etienne <etiennejouan@users.noreply.github.com>
2026-02-20 08:27:53 +00:00
Félix MalfaitGitHubCursorclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
726b1373d9 fix: convert metered tier upTo from internal to display credits in listPlans (#18108)
## Summary
- The `listPlans` GraphQL query was returning metered tier `upTo` values
in internal credit units (1000x the display value), causing "Credits by
period" and "Credit Plan" dropdowns to show "50M" instead of "50k"
- Applied the existing `INTERNAL_CREDITS_PER_DISPLAY_CREDIT` (1000)
divisor in `formatBillingDatabasePriceToMeteredPriceDTO`, matching the
conversion already used in `getMeteredProductsUsage` resolver
- Updated frontend mock data to reflect the corrected display-unit
values

## Test plan
- [x] Unit tests pass for `format-database-product-to-graphql-dto.util`
- [x] Unit tests pass for `metered-credit.service` and
`billing-credit-rollover.service`
- [ ] Verify billing page shows correct credit amounts (50k, not 50M)
for "Credits by period" and "Credit Plan"


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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-02-20 09:44:11 +01:00
783e57f5d8 i18n - translations (#18100)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-19 21:14:57 +01:00
163254baef fix: laggy edition of FormNumberFieldInput (#18011)
### What this PR do ?

Stops the delay fields from updating the workflow on every keystroke by
keeping values locally and saving them on blur, which fixes the cached
relation error

Fixed #15709

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
2026-02-19 17:16:25 +00:00
aaa6511007 feat: added workspace member filter for actor fields (#16628)
Closes #16619

This PR adds support for filtering ACTOR fields by `Workspace Member`
subfield, enabling users to filter records by who created them with a
`Me` option.

I replicated the same UX and code structure as the Relation field filter
for consistency.

Each filter selection triggers a server-side GraphQL call. But there is
client-side filtering in `isRecordMatchingFilter.ts` which instantly
filters already-loaded records while the server is fetching new results.
I replicated this behaviour from how `FULL_NAME` and other composite
fields handle filtering.


UX decision that were taken by me (Let me know if changes are needed) : 
- The filter shows comma separated selected workspace member names until
3 members are selected. After that it shows [no of selected members]
workspace members.

<img width="643" height="283" alt="Screenshot 2025-12-17 at 8 01 23 PM"
src="https://github.com/user-attachments/assets/18d524f4-979b-4d92-ad64-aa7b63be7897"
/>
<img width="774" height="309" alt="Screenshot 2025-12-17 at 8 01 33 PM"
src="https://github.com/user-attachments/assets/779f374f-1501-48f9-afa2-a78628e067b1"
/>
<img width="737" height="397" alt="Screenshot 2025-12-17 at 8 01 40 PM"
src="https://github.com/user-attachments/assets/4e8b963e-8a0f-467d-91ae-48bca4e1d842"
/>
<img width="697" height="334" alt="Screenshot 2025-12-17 at 8 01 53 PM"
src="https://github.com/user-attachments/assets/c9a0e4bb-48b4-486b-a4f9-d7c49ff3852c"
/>
<img width="684" height="343" alt="Screenshot 2025-12-17 at 8 02 04 PM"
src="https://github.com/user-attachments/assets/d5c45eba-06a3-40fc-b9d0-d585dc8bc136"
/>

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
2026-02-19 16:56:40 +00:00
WeikoandGitHub 2a670520b9 Add page layout backfill command (#18095)
## Context
Command to backfill record page layouts and related entities for legacy
workspaces.

## Test
Set SHOULD_SEED_STANDARD_RECORD_PAGE_LAYOUTS=false, reset DB then run
the command and compare with Set
SHOULD_SEED_STANDARD_RECORD_PAGE_LAYOUTS=true on a different workspace
2026-02-19 16:24:20 +00:00
Charles BochetandGitHub 10bd005021 Rework types for logic function (#18074)
## Summary

- **Consolidate logic function services**: Remove
`LogicFunctionMetadataService` and consolidate all logic function CRUD
operations into `LogicFunctionFromSourceService`, with a new
`LogicFunctionFromSourceHelperService` for shared validation/migration
logic
- **Introduce typed conversion utils following the skill pattern**: Add
`fromCreateLogicFunctionFromSourceInputToUniversalFlatLogicFunctionToCreate`
and `fromUpdateLogicFunctionFromSourceInputToFlatLogicFunctionToUpdate`
that convert DTO inputs directly to flat entities
(`UniversalFlatLogicFunction` / `FlatLogicFunction`), replacing the
previous intermediate `UpdateLogicFunctionMetadataParams` indirection
- **Simplify `CodeStepBuildService`**: Remove ~100 lines of manual
duplication logic by delegating to
`LogicFunctionFromSourceService.duplicateOneWithSource`
- **Remove completed 1-17 migration**: Delete
`MigrateWorkflowCodeStepsCommand` and associated utils that migrated
workflow code steps from serverless functions to logic functions
2026-02-19 17:25:08 +01:00
0e25aeb5be chore: upgrade @swc/core to 1.15.11 and align SWC ecosystem (#18088)
## Summary

- Upgrades `@swc/core` from 1.13.3 to **1.15.11** (swc_core v56), which
introduces CBOR-based plugin serialization replacing rkyv, eliminating
strict version-matching between SWC core and Wasm plugins
- Upgrades `@lingui/swc-plugin` from ^5.6.0 to **^5.11.0** (swc_core
50.2.3, built with `--cfg=swc_ast_unknown` for cross-version
compatibility)
- Upgrades `@swc/plugin-emotion` from 10.0.4 to **14.6.0** (swc_core 53,
also with backward-compat feature)
- Upgrades companion packages: `@swc-node/register` 1.8.0 → 1.11.1,
`@swc/helpers` ~0.5.2 → ~0.5.18, `@vitejs/plugin-react-swc` 3.11.0 →
4.2.3

### Why this is safe now

Starting from `@swc/core v1.15.0`, SWC replaced the rkyv serialization
scheme with CBOR (a self-describing format) and added `Unknown` AST enum
variants. Plugins built with `swc_core >= 47` and
`--cfg=swc_ast_unknown` are now forward-compatible across `@swc/core`
versions. Both `@lingui/swc-plugin@5.10.1+` and
`@swc/plugin-emotion@14.0.0+` have this support, meaning the old
version-matching nightmare between Lingui and SWC is largely solved.

Reference: https://github.com/lingui/swc-plugin/issues/179

## Test plan

- [x] `yarn install` resolves without errors
- [x] `npx nx build twenty-shared` succeeds
- [x] `npx nx build twenty-ui` succeeds (validates
@swc/plugin-emotion@14.6.0)
- [x] `npx nx typecheck twenty-front` succeeds
- [x] `npx nx build twenty-front` succeeds (validates vite + swc +
lingui pipeline)
- [x] `npx nx build twenty-emails` succeeds (validates lingui plugin)
- [x] Frontend jest tests pass (validates @swc/jest +
@lingui/swc-plugin)
- [x] Server jest tests pass (validates server-side SWC + lingui)

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-19 15:27:56 +00:00
BugIsGodandGitHub e051cce24f Fix: add a new style to the target text box (#18065)
### Approach
I add some new rules for the style of the target text box. (Fix: #13229
)
<img width="842" height="545" alt="target class"
src="https://github.com/user-attachments/assets/7cd0a615-392a-493b-831f-77ddb93de5fc"
/>


**This is what it looks like now.**
<img width="320" height="224" alt="image"
src="https://github.com/user-attachments/assets/26514102-e684-49ce-915d-43e5677520a5"
/>
2026-02-19 13:59:11 +00:00
Raphaël BosiandGitHub bd03073b6d [FRONT COMPONENTS] Declare command menu items in front components (#18047)
## Description

- Adds support for declaring command menu items directly within
`defineFrontComponent` via an optional command config property
- Introduces a new `CommandMenuItemManifest` type in twenty-shared and
wires it through the manifest build pipeline

## Example Of usage

```tsx
import { defineFrontComponent } from "twenty-sdk";

const TestAction = () => {
  return <div>Test Action</div>;
};

export default defineFrontComponent({
  universalIdentifier: "6c289461-0007-4a62-a99f-69e5c11a4ce7",
  name: "test-action",
  description: "Test Action",
  component: TestAction,
  command: {
    universalIdentifier: "c07df864-495f-46f3-9f5b-9d3ce2589e9b",
    label: "Run My Action",
    icon: "IconBolt",
    isPinned: false,
  },
});

```

## Video QA


https://github.com/user-attachments/assets/f910fc6a-44a9-45d1-87c5-f0ce64bb3878
2026-02-19 15:23:46 +01:00
EtienneandGitHub b3d8f8813f Remove non positive integer constraint on Nav Menu Item 2/2 (#18090)
Follow up https://github.com/twentyhq/twenty/pull/18081
2026-02-19 15:17:25 +01:00
WeikoandGitHub 37d9828458 Fix google compose scope not gated by feature flag (#18093)
## Context
New google api scope has been introduced in
https://github.com/twentyhq/twenty/pull/17793 without being gated behind
a feature flag

Microsoft is using pre-existing Mail.ReadWrite scope there is nothing to
gate

## Before
<img width="553" height="445" alt="Screenshot 2026-02-19 at 14 57 58"
src="https://github.com/user-attachments/assets/59a4f76b-d38d-492f-b013-b6cad4091a7f"
/>


## After
<img width="535" height="392" alt="Screenshot 2026-02-19 at 14 58 44"
src="https://github.com/user-attachments/assets/0337bf15-ec30-4549-bb9d-571a982dffd8"
/>
2026-02-19 15:16:54 +01:00
Thomas TrompetteGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
e16b6d6485 Create LLMS.md on create-twenty-app (#18091)
Managed to create a many to many app with minimal instructions. File
will need to be enriched with more pitfalls.

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-02-19 14:51:31 +01:00
Paul RastoinandGitHub 9626c7ce02 Twenty standard app static options id (#18089)
# Introduction
While preparing the twenty-standard as code migration to twenty-app
through sdk I've faced permanent field enum update as the id was
generated dynamically at each twenty standard app construction
Making them deterministic in order to avoid having this noise

Won't backfill this on existing workspace as it's not critical and that
we will rework the options in the future
2026-02-19 13:19:09 +00:00
martmullandGitHub 754de411fe Factorize and add public-assets/*path endpoint (#18080)
as title
2026-02-19 14:00:52 +01:00
Charles BochetandGitHub 530f74e28c Migrate more to Jotai (#18087)
Continue jotai migration
2026-02-19 13:50:21 +01:00
Paul RastoinandGitHub 5a46cf7bd3 Refactor message backfill command (#18078)
# Introduction
Atomically create the field and object to be created
And avoid synchronizing unrelated non up to date object and fields 
Followup https://github.com/twentyhq/twenty/pull/17398
2026-02-19 12:18:42 +00:00
7b10202c69 i18n - translations (#18086)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-19 13:17:01 +01:00
EtienneandGitHub 9e54a8082c Remove non positive integer constraint on Nav Menu Item (#18081)
To fit with position typed field logic + Ease favorite to nav menu item
migration (which have negative and float position)
2026-02-19 11:24:14 +00:00
Charles BochetandGitHub 1a13258302 Keep migrating to jotai (#18064)
And we continue!
2026-02-19 12:10:02 +01:00
Abdullah.andGitHub c0ea049ad7 fix: remove the error message for test failure in ci-front (#18076)
Introduced an error message on twenty-front CI earlier to try and inform
the user that test failure could be a coverage issue if no individual
test was failing. However, it led to the assumption that it must be
coverage failure in all cases even when it was test failure leading to
the CI being red.

This PR reverts the change.
2026-02-19 11:57:01 +01:00
b33f86fbf5 i18n - translations (#18079)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-19 11:56:21 +01:00
Paul RastoinandGitHub 88424611ec Refactor and standardize isSystem field and object (#17992)
# Introduction

## Centralize system field definitions
- Extract a single `PARTIAL_SYSTEM_FLAT_FIELD_METADATAS` constant as the
source of truth for all 8 system fields (`id`, `createdAt`, `updatedAt`,
`deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`),
eliminating duplication across custom object and standard app field
builders
- Refactor `buildDefaultFlatFieldMetadatasForCustomObject` to use the
shared constant via a new `buildObjectSystemFlatFieldMetadatas` helper

## Mark system fields as `isSystem: true`
- Fields `id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`,
`updatedBy`, `position`, `searchVector` are now properly flagged as
system fields across all standard objects and custom object creation
- Standard app field builders for all ~30 standard objects updated to
set `isSystem: true` on `createdAt`, `updatedAt`, `deletedAt`,
`createdBy`, `updatedBy`
- System-only standard objects (blocklist, calendar channels, message
threads, etc.) now also include `createdBy`, `updatedBy`, `position`,
`searchVector` field definitions that were previously missing

## Validate system fields on object creation
- New transversal validation (`crossEntityTransversalValidation`) runs
after all atomic entity validations in the build orchestrator, ensuring
all 8 system fields are present with correct `type` and `isSystem: true`
when an object is created
- New `buildUniversalFlatObjectFieldByNameAndJoinColumnMaps` utility to
resolve field names to universal identifiers for a given object
- New exception codes: `MISSING_SYSTEM_FIELD` and `INVALID_SYSTEM_FIELD`
on `ObjectMetadataExceptionCode`

## Protect system fields and objects from mutation
- Field validators now block update/delete of `isSystem` fields by
non-system callers (`FIELD_MUTATION_NOT_ALLOWED`)
- Object validators now block update/delete of `isSystem` objects by
non-system callers
- `POSITION` and `TS_VECTOR` field type validators replaced: instead of
rejecting creation outright, they now validate that the field is named
correctly (`position` / `searchVector`) and has `isSystem: true`

## Distinguish `isSystemBuild` from `isCallerTwentyStandardApp`
- New `isCallerTwentyStandardApp` utility checks whether the caller's
`applicationUniversalIdentifier` matches the twenty standard app
- Name-sync logic (`isFlatFieldMetadataNameSyncedWithLabel`,
`areFlatObjectMetadataNamesSyncedWithLabels`) refactored to use
`isCallerTwentyStandardApp` for custom suffix decisions, keeping
`isSystemBuild` for mutation permission checks
- `WorkspaceMigrationBuilderOptions` type updated to include
`applicationUniversalIdentifier`

## Adapt frontend filtering
- New `HIDDEN_SYSTEM_FIELD_NAMES` constant (`id`, `position`,
`searchVector`) and `isHiddenSystemField` utility to only hide truly
internal fields while keeping user-facing system fields (`createdAt`,
`updatedAt`, `deletedAt`, `createdBy`, `updatedBy`) visible in the UI
- ~20 frontend files updated to replace `!field.isSystem` checks with
`!isHiddenSystemField(field)` across record index, settings, data model,
charts, workflows, spreadsheet import, aggregations, and role
permissions

## Add 1.19 upgrade commands
- **`backfill-system-fields-is-system`**: Raw SQL command to set
`isSystem = true` on existing workspace fields matching system field
names, and fix `position` field type from `NUMBER` to `POSITION` for
`favorite`/`favoriteFolder` objects. Includes proper cache invalidation.
- **`add-missing-system-fields-to-standard-objects`**: Codegen'd
workspace migration to create missing `position`, `searchVector`,
`createdBy`, `updatedBy` fields on standard objects that didn't
previously have them. Runs via `WorkspaceMigrationRunnerService` in a
single transaction with idempotency check. **Known limitation**: assumes
all standard objects exist and are valid in the target workspace.

## Add `universalIdentifier` for system fields in standard object
constants
- `standard-object.constant.ts` updated to include `universalIdentifier`
for `createdBy`, `updatedBy`, `position`, and `searchVector` across all
standard objects
- `fieldManifestType.ts` updated to support the new field manifest shape

## System relation
Completely removed and backfilled all `isSystem` relation to be false
false
As we won't require an object to have any relation system fields

## Add integration tests
- New test suite `failing-sync-application-object-system-fields`
covering: missing system fields, wrong field types (`id` as TEXT,
`createdAt` as TEXT, `position` as TEXT), system field deletion
attempts, and system field update attempts
- New test utilities: `buildDefaultObjectManifest` (builds an object
manifest with all 8 system fields) and `setupApplicationForSync`
(centralizes application setup)
- Existing successful sync test updated to verify system fields are
created with correct properties

## Next step
Make the builder scope the compared entity to be the currently built app
+ nor twenty standard app
2026-02-19 10:13:50 +00:00
082400f751 Add objectRecordCounts query to /metadata endpoint (#18054)
## Summary

- Adds an `objectRecordCounts` query on the `/metadata` GraphQL endpoint
that returns approximate record counts for all objects in the workspace
- Uses PostgreSQL's `pg_class.reltuples` catalog stats — a single
instant query instead of N `COUNT(*)` table scans
- Replaces the previous `CombinedFindManyRecords` approach which hit the
server's 20 root resolver limit and silently showed 0 for all counts on
the settings Data Model page

### Server
- `ObjectRecordCountDTO` — GraphQL type with `objectNamePlural` and
`totalCount`
- `ObjectRecordCountService` — reads `pg_class` catalog for the
workspace schema
- Query added to `ObjectMetadataResolver` with `@MetadataResolver()` +
`NoPermissionGuard`

### Frontend
- `OBJECT_RECORD_COUNTS` query added to
`object-metadata/graphql/queries.ts`
- `useCombinedGetTotalCount` simplified to a zero-argument hook using
the new query
- `SettingsObjectTable` simplified to a single hook call

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-19 09:02:31 +00:00
EtienneandGitHub a14b0ab6ca FILES field - Attachment name display fix (#18073)
with new 'file' FILES field on attachment, UI should display attachment
name from file field value.
Issue with API users updating only 'file' FILES field (and not name
field anymore)
2026-02-19 10:04:05 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6e29f66ce8 Bump @emotion/is-prop-valid from 1.3.0 to 1.4.0 (#18068)
Bumps [@emotion/is-prop-valid](https://github.com/emotion-js/emotion)
from 1.3.0 to 1.4.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/emotion-js/emotion/releases"><code>@​emotion/is-prop-valid</code>'s
releases</a>.</em></p>
<blockquote>
<h2><code>@​emotion/is-prop-valid</code><a
href="https://github.com/1"><code>@​1</code></a>.4.0</h2>
<h3>Minor Changes</h3>
<ul>
<li><a
href="https://redirect.github.com/emotion-js/emotion/pull/3306">#3306</a>
<a
href="https://github.com/emotion-js/emotion/commit/dfae1cbd98d3ebe449ce322b38cbf4a7fbfbfe96"><code>dfae1cb</code></a>
Thanks <a
href="https://github.com/EnzoAlbornoz"><code>@​EnzoAlbornoz</code></a>!
- Adds <code>popover</code>, <code>popoverTarget</code> and
<code>popoverTargetAction</code> to the list of allowed props.</li>
</ul>
<h2><code>@​emotion/is-prop-valid</code><a
href="https://github.com/1"><code>@​1</code></a>.3.1</h2>
<h3>Patch Changes</h3>
<ul>
<li><a
href="https://redirect.github.com/emotion-js/emotion/pull/3093">#3093</a>
<a
href="https://github.com/emotion-js/emotion/commit/532ff57cafd8ba04f3b624074556ea47ec76fc9a"><code>532ff57</code></a>
Thanks <a href="https://github.com/codejet"><code>@​codejet</code></a>,
<a href="https://github.com/DustinBrett"><code>@​DustinBrett</code></a>!
- Adds <code>fetchpriority</code> and <code>fetchPriority</code> to the
list of allowed props.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/emotion-js/emotion/commit/38db311adf024fe8a24b57c687824c47abbd0957"><code>38db311</code></a>
Version Packages (<a
href="https://redirect.github.com/emotion-js/emotion/issues/3347">#3347</a>)</li>
<li><a
href="https://github.com/emotion-js/emotion/commit/dfae1cbd98d3ebe449ce322b38cbf4a7fbfbfe96"><code>dfae1cb</code></a>
Adds <code>popover</code>, <code>popoverTarget</code> and
<code>popoverTargetAction</code> to the list of allo...</li>
<li><a
href="https://github.com/emotion-js/emotion/commit/49229553967b6050c92d9602eb577bdc48167e91"><code>4922955</code></a>
Version Packages (<a
href="https://redirect.github.com/emotion-js/emotion/issues/3335">#3335</a>)</li>
<li><a
href="https://github.com/emotion-js/emotion/commit/0facbe47bd9099ae4ed22dc201822d910ac3dec5"><code>0facbe4</code></a>
Renamed default-exported variable in <code>@emotion/styled</code> to aid
inferred import...</li>
<li><a
href="https://github.com/emotion-js/emotion/commit/cce67ec6b2fc94261028b4f4778aae8c3d6c5fd6"><code>cce67ec</code></a>
Bump parcel (<a
href="https://redirect.github.com/emotion-js/emotion/issues/3258">#3258</a>)</li>
<li><a
href="https://github.com/emotion-js/emotion/commit/3c19ce5997f73960679e546af47801205631dfde"><code>3c19ce5</code></a>
Version Packages (<a
href="https://redirect.github.com/emotion-js/emotion/issues/3280">#3280</a>)</li>
<li><a
href="https://github.com/emotion-js/emotion/commit/a19d019bd418ebc3b9cba0e58f58b36ac2862a42"><code>a19d019</code></a>
Convert <code>@emotion/styled</code>'s source code to TypeScript (<a
href="https://redirect.github.com/emotion-js/emotion/issues/3284">#3284</a>)</li>
<li><a
href="https://github.com/emotion-js/emotion/commit/5974e33fcb5e7aee177408684ac6fe8b38b3e353"><code>5974e33</code></a>
Fix JSX namespace <a
href="https://github.com/ts-ignores"><code>@​ts-ignores</code></a> (<a
href="https://redirect.github.com/emotion-js/emotion/issues/3282">#3282</a>)</li>
<li><a
href="https://github.com/emotion-js/emotion/commit/fc4d7bd744c205f55513dcd4e4e5134198c219de"><code>fc4d7bd</code></a>
Convert <code>@emotion/react</code>'s source code to TypeScript (<a
href="https://redirect.github.com/emotion-js/emotion/issues/3281">#3281</a>)</li>
<li><a
href="https://github.com/emotion-js/emotion/commit/8dc1a6dd19d2dc9ce435ef0aff85ccf5647f5d2e"><code>8dc1a6d</code></a>
Convert <code>@emotion/cache</code>'s source code to TypeScript (<a
href="https://redirect.github.com/emotion-js/emotion/issues/3277">#3277</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/emotion-js/emotion/compare/@emotion/is-prop-valid@1.3.0...@emotion/is-prop-valid@1.4.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@emotion/is-prop-valid&package-manager=npm_and_yarn&previous-version=1.3.0&new-version=1.4.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-19 08:10:11 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9bcc2fc5d9 Bump eslint-config-next from 14.2.33 to 14.2.35 (#18069)
Bumps
[eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next)
from 14.2.33 to 14.2.35.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/vercel/next.js/commit/7b940d9ce96faddb9f92ff40f5e35c34ace04eb2"><code>7b940d9</code></a>
v14.2.35</li>
<li><a
href="https://github.com/vercel/next.js/commit/f3073688ce18878a674fdb9954da68e9d626a930"><code>f307368</code></a>
v14.2.34</li>
<li>See full diff in <a
href="https://github.com/vercel/next.js/commits/v14.2.35/packages/eslint-config-next">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-19 08:09:59 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
01e203a70b Bump @xyflow/react from 12.4.2 to 12.10.0 (#18070)
Bumps
[@xyflow/react](https://github.com/xyflow/xyflow/tree/HEAD/packages/react)
from 12.4.2 to 12.10.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/xyflow/xyflow/releases"><code>@​xyflow/react</code>'s
releases</a>.</em></p>
<blockquote>
<h2><code>@​xyflow/react</code><a
href="https://github.com/12"><code>@​12</code></a>.10.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5637">#5637</a> <a
href="https://github.com/xyflow/xyflow/commit/0c7261a6dc94f1aa58333a6aebcaca8ced9b5ad2"><code>0c7261a6d</code></a>
Thanks <a href="https://github.com/moklick"><code>@​moklick</code></a>!
- Add <code>zIndexMode</code> to control how z-index is calculated for
nodes and edges</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5484">#5484</a> <a
href="https://github.com/xyflow/xyflow/commit/a523919d6789995e9d0f3dd29b0b47fc3b8d8439"><code>a523919d6</code></a>
Thanks <a
href="https://github.com/peterkogo"><code>@​peterkogo</code></a>! - Add
<code>experimental_useOnNodesChangeMiddleware</code> hook</p>
</li>
</ul>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5629">#5629</a> <a
href="https://github.com/xyflow/xyflow/commit/9030fab2df8285fdfb649bda6e1e885dfd228d45"><code>9030fab2d</code></a>
Thanks <a
href="https://github.com/AlaricBaraou"><code>@​AlaricBaraou</code></a>!
- Prevent unnecessary re-render in <code>FlowRenderer</code></p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5592">#5592</a> <a
href="https://github.com/xyflow/xyflow/commit/38dbf41c464550cc803b946a4ad1f46982385a03"><code>38dbf41c4</code></a>
Thanks <a
href="https://github.com/svilen-ivanov-kubit"><code>@​svilen-ivanov-kubit</code></a>!
- Always create a new measured object in apply changes.</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5635">#5635</a> <a
href="https://github.com/xyflow/xyflow/commit/2d7fa40e2684a0fcdd4eca7800ccf2c34338e549"><code>2d7fa40e2</code></a>
Thanks <a
href="https://github.com/tornado-softwares"><code>@​tornado-softwares</code></a>!
- Update an ongoing connection when user moves node with keyboard.</p>
</li>
<li>
<p>Updated dependencies [<a
href="https://github.com/xyflow/xyflow/commit/0c7261a6dc94f1aa58333a6aebcaca8ced9b5ad2"><code>0c7261a6d</code></a>,
<a
href="https://github.com/xyflow/xyflow/commit/8598b6bc2a9d052b12d5215706382da0aa84827b"><code>8598b6bc2</code></a>,
<a
href="https://github.com/xyflow/xyflow/commit/2d7fa40e2684a0fcdd4eca7800ccf2c34338e549"><code>2d7fa40e2</code></a>]:</p>
<ul>
<li><code>@​xyflow/system</code><a
href="https://github.com/0"><code>@​0</code></a>.0.74</li>
</ul>
</li>
</ul>
<h2><code>@​xyflow/react</code><a
href="https://github.com/12"><code>@​12</code></a>.9.3</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5621">#5621</a> <a
href="https://github.com/xyflow/xyflow/commit/c1304dba7a20bb8d74c7aceb23cd80b56e4c0482"><code>c1304dba7</code></a>
Thanks <a href="https://github.com/moklick"><code>@​moklick</code></a>!
- Set <code>paneClickDistance</code> default value to
<code>1</code>.</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5578">#5578</a> <a
href="https://github.com/xyflow/xyflow/commit/00bcb9f5f45f49814b9ac19b3f55cfe069ee3773"><code>00bcb9f5f</code></a>
Thanks <a
href="https://github.com/peterkogo"><code>@​peterkogo</code></a>! - Pass
current pointer position to connection</p>
</li>
<li>
<p>Updated dependencies [<a
href="https://github.com/xyflow/xyflow/commit/00bcb9f5f45f49814b9ac19b3f55cfe069ee3773"><code>00bcb9f5f</code></a>]:</p>
<ul>
<li><code>@​xyflow/system</code><a
href="https://github.com/0"><code>@​0</code></a>.0.73</li>
</ul>
</li>
</ul>
<h2><code>@​xyflow/react</code><a
href="https://github.com/12"><code>@​12</code></a>.9.2</h2>
<h3>Patch Changes</h3>
<ul>
<li><a
href="https://redirect.github.com/xyflow/xyflow/pull/5593">#5593</a> <a
href="https://github.com/xyflow/xyflow/commit/a8ee089d7689d9a58113690c8e90e1c1e109602a"><code>a8ee089d7</code></a>
Thanks <a href="https://github.com/moklick"><code>@​moklick</code></a>!
- Reset selection box when user selects a node</li>
</ul>
<h2><code>@​xyflow/react</code><a
href="https://github.com/12"><code>@​12</code></a>.9.1</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5572">#5572</a> <a
href="https://github.com/xyflow/xyflow/commit/5ec0cac7fad21109b74839969c0818f88ddc87d9"><code>5ec0cac7f</code></a>
Thanks <a
href="https://github.com/peterkogo"><code>@​peterkogo</code></a>! - Fix
onPaneClick events being suppressed when selectionOnDrag=true</p>
</li>
<li>
<p>Updated dependencies [<a
href="https://github.com/xyflow/xyflow/commit/5ec0cac7fad21109b74839969c0818f88ddc87d9"><code>5ec0cac7f</code></a>]:</p>
<ul>
<li><code>@​xyflow/system</code><a
href="https://github.com/0"><code>@​0</code></a>.0.72</li>
</ul>
</li>
</ul>
<h2><code>@​xyflow/react</code><a
href="https://github.com/12"><code>@​12</code></a>.9.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5544">#5544</a> <a
href="https://github.com/xyflow/xyflow/commit/c17b49f4c16167da3f791430163edd592159d27d"><code>c17b49f4c</code></a>
Thanks <a
href="https://github.com/0x0f0f0f"><code>@​0x0f0f0f</code></a>! - Add
<code>EdgeToolbar</code> component</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5550">#5550</a> <a
href="https://github.com/xyflow/xyflow/commit/6ffb9f7901c32f5b335aee2517f41bf87f274f32"><code>6ffb9f790</code></a>
Thanks <a
href="https://github.com/peterkogo"><code>@​peterkogo</code></a>! -
Prevent child nodes of different parents from overlapping</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5551">#5551</a> <a
href="https://github.com/xyflow/xyflow/commit/6bb64b3ed60f26c9ea8bc01c8d62fb9bf74cd634"><code>6bb64b3ed</code></a>
Thanks <a href="https://github.com/moklick"><code>@​moklick</code></a>!
- Allow to start a selection above a node</p>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/xyflow/xyflow/blob/main/packages/react/CHANGELOG.md"><code>@​xyflow/react</code>'s
changelog</a>.</em></p>
<blockquote>
<h2>12.10.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5637">#5637</a> <a
href="https://github.com/xyflow/xyflow/commit/0c7261a6dc94f1aa58333a6aebcaca8ced9b5ad2"><code>0c7261a6d</code></a>
Thanks <a href="https://github.com/moklick"><code>@​moklick</code></a>!
- Add <code>zIndexMode</code> to control how z-index is calculated for
nodes and edges</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5484">#5484</a> <a
href="https://github.com/xyflow/xyflow/commit/a523919d6789995e9d0f3dd29b0b47fc3b8d8439"><code>a523919d6</code></a>
Thanks <a
href="https://github.com/peterkogo"><code>@​peterkogo</code></a>! - Add
<code>experimental_useOnNodesChangeMiddleware</code> hook</p>
</li>
</ul>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5629">#5629</a> <a
href="https://github.com/xyflow/xyflow/commit/9030fab2df8285fdfb649bda6e1e885dfd228d45"><code>9030fab2d</code></a>
Thanks <a
href="https://github.com/AlaricBaraou"><code>@​AlaricBaraou</code></a>!
- Prevent unnecessary re-render in <code>FlowRenderer</code></p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5592">#5592</a> <a
href="https://github.com/xyflow/xyflow/commit/38dbf41c464550cc803b946a4ad1f46982385a03"><code>38dbf41c4</code></a>
Thanks <a
href="https://github.com/svilen-ivanov-kubit"><code>@​svilen-ivanov-kubit</code></a>!
- Always create a new measured object in apply changes.</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5635">#5635</a> <a
href="https://github.com/xyflow/xyflow/commit/2d7fa40e2684a0fcdd4eca7800ccf2c34338e549"><code>2d7fa40e2</code></a>
Thanks <a
href="https://github.com/tornado-softwares"><code>@​tornado-softwares</code></a>!
- Update an ongoing connection when user moves node with keyboard.</p>
</li>
<li>
<p>Updated dependencies [<a
href="https://github.com/xyflow/xyflow/commit/0c7261a6dc94f1aa58333a6aebcaca8ced9b5ad2"><code>0c7261a6d</code></a>,
<a
href="https://github.com/xyflow/xyflow/commit/8598b6bc2a9d052b12d5215706382da0aa84827b"><code>8598b6bc2</code></a>,
<a
href="https://github.com/xyflow/xyflow/commit/2d7fa40e2684a0fcdd4eca7800ccf2c34338e549"><code>2d7fa40e2</code></a>]:</p>
<ul>
<li><code>@​xyflow/system</code><a
href="https://github.com/0"><code>@​0</code></a>.0.74</li>
</ul>
</li>
</ul>
<h2>12.9.3</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5621">#5621</a> <a
href="https://github.com/xyflow/xyflow/commit/c1304dba7a20bb8d74c7aceb23cd80b56e4c0482"><code>c1304dba7</code></a>
Thanks <a href="https://github.com/moklick"><code>@​moklick</code></a>!
- Set <code>paneClickDistance</code> default value to
<code>1</code>.</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5578">#5578</a> <a
href="https://github.com/xyflow/xyflow/commit/00bcb9f5f45f49814b9ac19b3f55cfe069ee3773"><code>00bcb9f5f</code></a>
Thanks <a
href="https://github.com/peterkogo"><code>@​peterkogo</code></a>! - Pass
current pointer position to connection</p>
</li>
<li>
<p>Updated dependencies [<a
href="https://github.com/xyflow/xyflow/commit/00bcb9f5f45f49814b9ac19b3f55cfe069ee3773"><code>00bcb9f5f</code></a>]:</p>
<ul>
<li><code>@​xyflow/system</code><a
href="https://github.com/0"><code>@​0</code></a>.0.73</li>
</ul>
</li>
</ul>
<h2>12.9.2</h2>
<h3>Patch Changes</h3>
<ul>
<li><a
href="https://redirect.github.com/xyflow/xyflow/pull/5593">#5593</a> <a
href="https://github.com/xyflow/xyflow/commit/a8ee089d7689d9a58113690c8e90e1c1e109602a"><code>a8ee089d7</code></a>
Thanks <a href="https://github.com/moklick"><code>@​moklick</code></a>!
- Reset selection box when user selects a node</li>
</ul>
<h2>12.9.1</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5572">#5572</a> <a
href="https://github.com/xyflow/xyflow/commit/5ec0cac7fad21109b74839969c0818f88ddc87d9"><code>5ec0cac7f</code></a>
Thanks <a
href="https://github.com/peterkogo"><code>@​peterkogo</code></a>! - Fix
onPaneClick events being suppressed when selectionOnDrag=true</p>
</li>
<li>
<p>Updated dependencies [<a
href="https://github.com/xyflow/xyflow/commit/5ec0cac7fad21109b74839969c0818f88ddc87d9"><code>5ec0cac7f</code></a>]:</p>
<ul>
<li><code>@​xyflow/system</code><a
href="https://github.com/0"><code>@​0</code></a>.0.72</li>
</ul>
</li>
</ul>
<h2>12.9.0</h2>
<h3>Minor Changes</h3>
<ul>
<li><a
href="https://redirect.github.com/xyflow/xyflow/pull/5544">#5544</a> <a
href="https://github.com/xyflow/xyflow/commit/c17b49f4c16167da3f791430163edd592159d27d"><code>c17b49f4c</code></a>
Thanks <a
href="https://github.com/0x0f0f0f"><code>@​0x0f0f0f</code></a>! - Add
<code>EdgeToolbar</code> component</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/xyflow/xyflow/commit/c0ed3c33a3498877ab2a5755299ff84ee659782e"><code>c0ed3c3</code></a>
chore(packages): bump</li>
<li><a
href="https://github.com/xyflow/xyflow/commit/83a312b2e1691a44223536653689f8f99f0d5b24"><code>83a312b</code></a>
chore(zIndexMode): use basic as default</li>
<li><a
href="https://github.com/xyflow/xyflow/commit/14fd41b1f153406f1a21a61bf98ea07ad8275ec6"><code>14fd41b</code></a>
change default back to elevateEdgesOnSelect=false and
zIndexMode=basic</li>
<li><a
href="https://github.com/xyflow/xyflow/commit/3680a6a0e623e19d1f983515273f11bdd355ec86"><code>3680a6a</code></a>
Merge branch 'main' into feat/zindexmode</li>
<li><a
href="https://github.com/xyflow/xyflow/commit/a523919d6789995e9d0f3dd29b0b47fc3b8d8439"><code>a523919</code></a>
chore(middleware): cleanup</li>
<li><a
href="https://github.com/xyflow/xyflow/commit/e4e3605d62c8c710fe7ddb2ec3929af0a7962a6b"><code>e4e3605</code></a>
Merge branch 'main' into middlewares</li>
<li><a
href="https://github.com/xyflow/xyflow/commit/2c05b3224a13e896dfb9a0c93f3af7bb592afc45"><code>2c05b32</code></a>
Merge branch 'feat/zindexmode' of github.com:xyflow/xyflow into
feat/zindexmode</li>
<li><a
href="https://github.com/xyflow/xyflow/commit/ddbb9280f6242794187205a207e993e2c721c244"><code>ddbb928</code></a>
chore(examples): add zindexmode</li>
<li><a
href="https://github.com/xyflow/xyflow/commit/9faca3357d5db68fdad6c96de06fd66dd1d0ba64"><code>9faca33</code></a>
Merge branch 'main' into feat/zindexmode</li>
<li><a
href="https://github.com/xyflow/xyflow/commit/4eb42952f01b947c9d36c25e6b30b7bd98224632"><code>4eb4295</code></a>
feat(svelte): add zIndexMode</li>
<li>Additional commits viewable in <a
href="https://github.com/xyflow/xyflow/commits/@xyflow/react@12.10.0/packages/react">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-19 08:09:48 +00:00
db9636bd9e i18n - docs translations (#18067)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-19 02:07:09 +01:00
Charles BochetandGitHub 36c2b0e23b Migrate dropdown to jotai (#18063)
Here we go again
2026-02-19 00:58:26 +01:00
66deb8be63 i18n - docs translations (#18062)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-18 23:34:46 +01:00
Charles BochetandGitHub 01d2269bd0 Fix website build (#18061)
As per title
2026-02-18 23:34:36 +01:00
04502e5abb Messages Message Folder Association (#17398)
This PR adds Message folder association for message channel messages,
Currently under testing phase, not ready yet.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 23:13:47 +01:00
Charles BochetandGitHub 549c7a613b Fix website build (#18057) 2026-02-18 22:40:54 +01:00
743b733e70 i18n - translations (#18056)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-18 22:40:10 +01:00
Thomas des FrancsGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>Charles BochetCharles Bochet
612f7c37a5 Improve security settings card grouping and description overflow (#17928)
# After

- Added a separtor between the two audit logs cards
- Rename the audit log card to avoid repetition
- Grouped "Invite by link" and "2 factor auth" in one group
- Changed the card component description to always be one line max with
truncation & tooltips

<img width="777" height="1278" alt="CleanShot 2026-02-13 at 17 02 36"
src="https://github.com/user-attachments/assets/685c792a-c85b-4521-8c1b-bd9adedc75d9"
/>

<img width="976" height="690"
alt="b49f2eb043b6712d013618bb0a4ef7f011cf2316e1163fbdee4c293bed036ac9"
src="https://github.com/user-attachments/assets/6e17aa11-ecdb-4f98-ba50-5cd9b9c5def6"
/>

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-02-18 22:33:37 +01:00
42108e0611 i18n - translations (#18053)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-18 22:20:32 +01:00
5741 changed files with 235807 additions and 111347 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ This directory contains Twenty's development guidelines and best practices in th
### React Development
- **react-general-guidelines.mdc** - Core React development principles (Auto-attached to React files)
- **react-state-management.mdc** - State management approaches with Recoil (Auto-attached to state files)
- **react-state-management.mdc** - State management approaches with Jotai (Auto-attached to state files)
### Testing & Quality
- **testing-guidelines.mdc** - Testing strategies and best practices (Auto-attached to test files)
+1 -1
View File
@@ -7,7 +7,7 @@ alwaysApply: true
# Twenty Architecture
## Tech Stack
- **Frontend**: React 18, TypeScript, Recoil, Styled Components, Vite
- **Frontend**: React 18, TypeScript, Jotai, Styled Components, Vite
- **Backend**: NestJS, TypeORM, PostgreSQL, Redis, GraphQL
- **Monorepo**: Nx workspace with yarn
+33 -12
View File
@@ -4,16 +4,20 @@ alwaysApply: false
---
# React State Management
## Recoil Patterns
## Jotai Patterns
```typescript
// ✅ Atoms for primitive state
export const currentUserState = atom<User | null>({
// ✅ Atoms for primitive state (use createAtomState for keyed state with optional persistence)
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
export const currentUserState = createAtomState<User | null>({
key: 'currentUserState',
default: null,
defaultValue: null,
});
// ✅ Selectors for derived state
export const userDisplayNameSelector = selector({
// ✅ Derived atoms for computed state (use createAtomSelector)
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
export const userDisplayNameSelector = createAtomSelector({
key: 'userDisplayNameSelector',
get: ({ get }) => {
const user = get(currentUserState);
@@ -21,13 +25,30 @@ export const userDisplayNameSelector = selector({
},
});
// ✅ Atom families for dynamic atoms
export const userByIdState = atomFamily<User | null, string>({
// ✅ Atom factory pattern for dynamic atoms (use createAtomFamilyState)
import { createAtomFamilyState } from '@/ui/utilities/state/jotai/utils/createAtomFamilyState';
export const userByIdState = createAtomFamilyState<User | null, string>({
key: 'userByIdState',
default: null,
defaultValue: null,
});
```
## Jotai Hooks
```typescript
// useAtomState - read and write
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
// useAtomStateValue - read only
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
// useSetAtomState - write only
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
```
## Provider
Jotai works without a Provider by default. For scoped stores or testing, use `Provider` from `jotai`.
## Local State Guidelines
```typescript
// ✅ Multiple useState for unrelated state
@@ -74,7 +95,7 @@ const increment = useCallback(() => {
```
## Performance Tips
- Use atom families for dynamic data collections
- Implement proper selector caching
- Avoid heavy computations in selectors
- Use atom factory pattern (createAtomFamilyState) for dynamic data collections
- Derived atoms (createAtomSelector) are automatically memoized by Jotai
- Avoid heavy computations in derived atoms
- Batch state updates when possible
+7 -1
View File
@@ -19,4 +19,10 @@ runs:
uses: nrwl/nx-set-shas@v4
- name: Run affected command
shell: bash
run: npx nx affected --nxBail --configuration=${{ inputs.configuration }} -t=${{ inputs.tasks }} --parallel=${{ inputs.parallel }} --exclude='*,!tag:${{ inputs.tag }}' ${{ inputs.args }}
env:
NX_CONFIGURATION: ${{ inputs.configuration }}
NX_TASKS: ${{ inputs.tasks }}
NX_PARALLEL: ${{ inputs.parallel }}
NX_TAG: ${{ inputs.tag }}
NX_ARGS: ${{ inputs.args }}
run: npx nx affected --nxBail --configuration="$NX_CONFIGURATION" -t="$NX_TASKS" --parallel="$NX_PARALLEL" --exclude="*,!tag:$NX_TAG" $NX_ARGS
+4 -1
View File
@@ -19,8 +19,11 @@ runs:
- name: Cache primary key builder
id: cache-primary-key-builder
shell: bash
env:
CACHE_KEY: ${{ inputs.key }}
REF_NAME: ${{ github.ref_name }}
run: |
echo "CACHE_PRIMARY_KEY_PREFIX=v4-${{ inputs.key }}-${{ github.ref_name }}" >> "${GITHUB_OUTPUT}"
echo "CACHE_PRIMARY_KEY_PREFIX=v4-${CACHE_KEY}-${REF_NAME}" >> "${GITHUB_OUTPUT}"
- name: Restore cache
uses: actions/cache/restore@v4
id: restore-cache
@@ -0,0 +1,80 @@
name: Spawn Twenty Docker Image
description: >
Starts a full Twenty instance (server, worker, database, redis) using Docker
Compose. The server is available at http://localhost:3000 for subsequent steps
in the caller's job.
Pulls the specified semver image tag from Docker Hub.
Designed to be consumed from external repositories (e.g., twenty-app).
inputs:
twenty-version:
description: 'Twenty Docker Hub image tag as semver (e.g., v0.40.0, v1.0.0).'
required: true
twenty-repository:
description: 'Twenty repository to checkout docker compose files from.'
required: false
default: 'twentyhq/twenty'
github-token:
description: 'GitHub token for cross-repo checkout. Required when calling from an external repository.'
required: false
default: ${{ github.token }}
outputs:
server-url:
description: 'URL where the Twenty server can be reached'
value: http://localhost:3000
access-token:
description: 'Admin access token for the Twenty instance'
value: ${{ steps.admin-token.outputs.access-token }}
runs:
using: 'composite'
steps:
- name: Validate version
shell: bash
run: |
VERSION="${{ inputs.twenty-version }}"
if ! echo "$VERSION" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "::error::twenty-version must be a semver tag (e.g., v0.40.0). Got: '$VERSION'"
exit 1
fi
- name: Checkout docker compose files
uses: actions/checkout@v4
with:
repository: ${{ inputs.twenty-repository }}
ref: ${{ inputs.twenty-version }}
token: ${{ inputs.github-token }}
sparse-checkout: |
packages/twenty-docker
sparse-checkout-cone-mode: false
path: .twenty-spawn
- name: Prepare environment
shell: bash
working-directory: ./.twenty-spawn/packages/twenty-docker
run: |
cp .env.example .env
echo "" >> .env
echo "TAG=${{ inputs.twenty-version }}" >> .env
echo "APP_SECRET=replace_me_with_a_random_string" >> .env
echo "SERVER_URL=http://localhost:3000" >> .env
- name: Start Twenty instance
shell: bash
working-directory: ./.twenty-spawn/packages/twenty-docker
run: |
docker compose up -d --wait || {
echo "::error::Docker compose failed to start or health checks timed out"
docker compose logs
exit 1
}
echo "Twenty instance is ready at http://localhost:3000"
- name: Set admin access token
id: admin-token
shell: bash
run: |
ACCESS_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik"
echo "::add-mask::$ACCESS_TOKEN"
echo "access-token=$ACCESS_TOKEN" >> "$GITHUB_OUTPUT"
+8 -188
View File
@@ -16,8 +16,6 @@ env:
permissions:
contents: read
pull-requests: write
checks: write
jobs:
changed-files-check:
@@ -584,182 +582,16 @@ jobs:
echo "::warning::REST Metadata API analysis tool error - continuing workflow"
fi
- name: Comment API Changes on PR
- name: Upload breaking changes report
if: always()
uses: actions/github-script@v7
uses: actions/upload-artifact@v4
with:
script: |
const fs = require('fs');
let hasChanges = false;
let comment = '';
try {
if (fs.existsSync('graphql-schema-diff.md')) {
const graphqlDiff = fs.readFileSync('graphql-schema-diff.md', 'utf8');
if (graphqlDiff.trim()) {
if (!hasChanges) {
comment = '## 📊 API Changes Report\n\n';
hasChanges = true;
}
comment += '### GraphQL Schema Changes\n' + graphqlDiff + '\n\n';
}
}
if (fs.existsSync('graphql-metadata-diff.md')) {
const graphqlMetadataDiff = fs.readFileSync('graphql-metadata-diff.md', 'utf8');
if (graphqlMetadataDiff.trim()) {
if (!hasChanges) {
comment = '## 📊 API Changes Report\n\n';
hasChanges = true;
}
comment += '### GraphQL Metadata Schema Changes\n' + graphqlMetadataDiff + '\n\n';
}
}
if (fs.existsSync('rest-api-diff.md')) {
const restDiff = fs.readFileSync('rest-api-diff.md', 'utf8');
if (restDiff.trim()) {
if (!hasChanges) {
comment = '## 📊 API Changes Report\n\n';
hasChanges = true;
}
comment += restDiff + '\n\n';
}
}
if (fs.existsSync('rest-metadata-api-diff.md')) {
const metadataDiff = fs.readFileSync('rest-metadata-api-diff.md', 'utf8');
if (metadataDiff.trim()) {
if (!hasChanges) {
comment = '## 📊 API Changes Report\n\n';
hasChanges = true;
}
comment += metadataDiff + '\n\n';
}
}
// Only post comment if there are changes
if (hasChanges) {
// Add branch state information only if there were conflicts
const branchState = process.env.BRANCH_STATE || 'unknown';
let branchStateNote = '';
if (branchState === 'conflicts') {
branchStateNote = '\n\n⚠️ **Note**: Could not merge with `main` due to conflicts. This comparison shows changes between the current branch and `main` as separate states.\n';
}
// Check if there are any breaking changes detected
let hasBreakingChanges = false;
let breakingChangeNote = '';
// Check for breaking changes in any of the diff files
if (fs.existsSync('rest-api-diff.md')) {
const restDiff = fs.readFileSync('rest-api-diff.md', 'utf8');
if (restDiff.includes('Breaking Changes') || restDiff.includes('🚨') ||
restDiff.includes('Removed Endpoints') || restDiff.includes('Changed Operations')) {
hasBreakingChanges = true;
}
}
if (fs.existsSync('rest-metadata-api-diff.md')) {
const metadataDiff = fs.readFileSync('rest-metadata-api-diff.md', 'utf8');
if (metadataDiff.includes('Breaking Changes') || metadataDiff.includes('🚨') ||
metadataDiff.includes('Removed Endpoints') || metadataDiff.includes('Changed Operations')) {
hasBreakingChanges = true;
}
}
// Also check GraphQL changes for breaking changes indicators
if (fs.existsSync('graphql-schema-diff.md')) {
const graphqlDiff = fs.readFileSync('graphql-schema-diff.md', 'utf8');
if (graphqlDiff.includes('Breaking changes') || graphqlDiff.includes('BREAKING')) {
hasBreakingChanges = true;
}
}
if (fs.existsSync('graphql-metadata-diff.md')) {
const graphqlMetadataDiff = fs.readFileSync('graphql-metadata-diff.md', 'utf8');
if (graphqlMetadataDiff.includes('Breaking changes') || graphqlMetadataDiff.includes('BREAKING')) {
hasBreakingChanges = true;
}
}
// Check PR title for "breaking"
const prTitle = ${{ toJSON(github.event.pull_request.title) }};
const titleContainsBreaking = prTitle.toLowerCase().includes('breaking');
if (hasBreakingChanges) {
if (titleContainsBreaking) {
breakingChangeNote = '\n\n## ✅ Breaking Change Protocol\n\n' +
'**This PR title contains "breaking" and breaking changes were detected - the CI will fail as expected.**\n\n' +
'📝 **Action Required**: Please add `BREAKING CHANGE:` to your commit message to trigger a major version bump.\n\n' +
'Example:\n```\nfeat: add new API endpoint\n\nBREAKING CHANGE: removed deprecated field from User schema\n```';
} else {
breakingChangeNote = '\n\n## ⚠️ Breaking Change Protocol\n\n' +
'**Breaking changes detected but PR title does not contain "breaking" - CI will pass but action needed.**\n\n' +
'🔄 **Options**:\n' +
'1. **If this IS a breaking change**: Add "breaking" to your PR title and add `BREAKING CHANGE:` to your commit message\n' +
'2. **If this is NOT a breaking change**: The API diff tool may have false positives - please review carefully\n\n' +
'For breaking changes, add to commit message:\n```\nfeat: add new API endpoint\n\nBREAKING CHANGE: removed deprecated field from User schema\n```';
}
}
const COMMENT_MARKER = '<!-- API_CHANGES_REPORT -->';
const commentBody = COMMENT_MARKER + '\n' + comment + branchStateNote + '\n⚠️ **Please review these API changes carefully before merging.**' + breakingChangeNote;
// Get all comments to find existing API changes comment
const {data: comments} = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
// Find our existing comment
const botComment = comments.find(comment => comment.body.includes(COMMENT_MARKER));
if (botComment) {
// Update existing comment
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: commentBody
});
console.log('Updated existing API changes comment');
} else {
// Create new comment
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody
});
console.log('Created new API changes comment');
}
} else {
console.log('No API changes detected - skipping PR comment');
// Check if there's an existing comment to remove
const {data: comments} = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const COMMENT_MARKER = '<!-- API_CHANGES_REPORT -->';
const botComment = comments.find(comment => comment.body.includes(COMMENT_MARKER));
if (botComment) {
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
});
console.log('Deleted existing API changes comment (no changes detected)');
}
}
} catch (error) {
console.log('Could not post comment:', error);
}
name: breaking-changes-report
path: |
*-diff.md
*-diff.json
if-no-files-found: ignore
retention-days: 3
- name: Cleanup servers
if: always()
@@ -771,16 +603,4 @@ jobs:
kill $(cat /tmp/main-server.pid) || true
fi
- name: Upload API specifications and diffs
if: always()
uses: actions/upload-artifact@v4
with:
name: api-specifications-and-diffs
path: |
/tmp/main-server.log
/tmp/current-server.log
*-api.json
*-schema-introspection.json
*-diff.md
*-diff.json
+66 -70
View File
@@ -98,47 +98,47 @@ jobs:
run: npx nx reset:env twenty-front
- name: Run storybook tests
run: npx nx storybook:test twenty-front --configuration=${{ matrix.storybook_scope }} --shard=${{ matrix.shard }}/${{ env.SHARD_COUNTER }}
- name: Rename coverage file
run: |
if [ -f "packages/twenty-front/coverage/storybook/coverage-final.json" ]; then
mv packages/twenty-front/coverage/storybook/coverage-final.json packages/twenty-front/coverage/storybook/coverage-shard-${{matrix.shard}}.json
else
echo "Error: coverage-final.json not found"
ls -la packages/twenty-front/coverage/storybook/ || echo "Coverage directory does not exist"
exit 1
fi
- name: Upload coverage artifact
uses: actions/upload-artifact@v4
with:
retention-days: 1
name: coverage-artifacts-${{ matrix.storybook_scope }}-${{ github.run_id }}-${{ matrix.shard }}
path: packages/twenty-front/coverage/storybook/coverage-shard-${{matrix.shard}}.json
merge-reports-and-check-coverage:
timeout-minutes: 30
runs-on: ubuntu-latest
needs: front-sb-test
env:
PATH_TO_COVERAGE: packages/twenty-front/coverage/storybook
strategy:
matrix:
storybook_scope: [modules, pages, performance]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/actions/yarn-install
- uses: actions/download-artifact@v4
with:
pattern: coverage-artifacts-${{ matrix.storybook_scope }}-${{ github.run_id }}-*
merge-multiple: true
path: coverage-artifacts
- name: Merge coverage reports
run: |
mkdir -p ${{ env.PATH_TO_COVERAGE }}
npx nyc merge coverage-artifacts ${{ env.PATH_TO_COVERAGE }}/coverage-storybook.json
- name: Checking coverage
run: npx nx storybook:coverage twenty-front --checkCoverage=true --configuration=${{ matrix.storybook_scope }}
# - name: Rename coverage file
# run: |
# if [ -f "packages/twenty-front/coverage/storybook/coverage-final.json" ]; then
# mv packages/twenty-front/coverage/storybook/coverage-final.json packages/twenty-front/coverage/storybook/coverage-shard-${{matrix.shard}}.json
# else
# echo "Error: coverage-final.json not found"
# ls -la packages/twenty-front/coverage/storybook/ || echo "Coverage directory does not exist"
# exit 1
# fi
# - name: Upload coverage artifact
# uses: actions/upload-artifact@v4
# with:
# retention-days: 1
# name: coverage-artifacts-${{ matrix.storybook_scope }}-${{ github.run_id }}-${{ matrix.shard }}
# path: packages/twenty-front/coverage/storybook/coverage-shard-${{matrix.shard}}.json
# merge-reports-and-check-coverage:
# timeout-minutes: 30
# runs-on: ubuntu-latest
# needs: front-sb-test
# env:
# PATH_TO_COVERAGE: packages/twenty-front/coverage/storybook
# strategy:
# matrix:
# storybook_scope: [modules, pages, performance]
# steps:
# - uses: actions/checkout@v4
# with:
# fetch-depth: 0
# - name: Install dependencies
# uses: ./.github/actions/yarn-install
# - uses: actions/download-artifact@v4
# with:
# pattern: coverage-artifacts-${{ matrix.storybook_scope }}-${{ github.run_id }}-*
# merge-multiple: true
# path: coverage-artifacts
# - name: Merge coverage reports
# run: |
# mkdir -p ${{ env.PATH_TO_COVERAGE }}
# npx nyc merge coverage-artifacts ${{ env.PATH_TO_COVERAGE }}/coverage-storybook.json
# - name: Checking coverage
# run: npx nx storybook:coverage twenty-front --checkCoverage=true --configuration=${{ matrix.storybook_scope }}
front-chromatic-deployment:
timeout-minutes: 30
if: false
@@ -171,6 +171,7 @@ jobs:
timeout-minutes: 30
runs-on: ubuntu-latest
env:
NODE_OPTIONS: '--max-old-space-size=4096'
TASK_CACHE_KEY: front-task-${{ matrix.task }}
strategy:
matrix:
@@ -202,14 +203,6 @@ jobs:
with:
tag: scope:frontend
tasks: ${{ matrix.task }}
- name: Check for coverage threshold failure
if: always() && steps.run-task.outcome == 'failure' && matrix.task == 'test'
shell: bash
run: |
echo "::error::The test task failed. If no individual test is failing, this is likely a coverage threshold not being met."
echo ""
echo "To debug locally, run: npx nx run twenty-front:test:ci"
exit 1
- name: Save ${{ matrix.task }} cache
uses: ./.github/actions/save-cache
with:
@@ -236,12 +229,12 @@ jobs:
run: npx nx reset:env twenty-front
- name: Build frontend
run: npx nx build twenty-front
- name: Upload frontend build artifact
uses: actions/upload-artifact@v4
with:
name: frontend-build
path: packages/twenty-front/build
retention-days: 1
# - name: Upload frontend build artifact
# uses: actions/upload-artifact@v4
# with:
# name: frontend-build
# path: packages/twenty-front/build
# retention-days: 1
e2e-test:
runs-on: ubuntu-latest
needs: [changed-files-check-e2e, front-build]
@@ -303,15 +296,18 @@ jobs:
cp packages/twenty-front/.env.example packages/twenty-front/.env
npx nx reset:env:e2e-testing-server twenty-server
- name: Download frontend build artifact
if: needs.front-build.result == 'success'
uses: actions/download-artifact@v4
with:
name: frontend-build
path: packages/twenty-front/build
# - name: Download frontend build artifact
# if: needs.front-build.result == 'success'
# uses: actions/download-artifact@v4
# with:
# name: frontend-build
# path: packages/twenty-front/build
- name: Build frontend (if not available from front-build)
if: needs.front-build.result == 'skipped'
# - name: Build frontend (if not available from front-build)
# if: needs.front-build.result == 'skipped'
# run: NODE_ENV=production NODE_OPTIONS="--max-old-space-size=10240" npx nx build twenty-front
- name: Build frontend
run: NODE_ENV=production NODE_OPTIONS="--max-old-space-size=10240" npx nx build twenty-front
- name: Build server
@@ -343,12 +339,12 @@ jobs:
- name: Run Playwright tests
run: npx nx test twenty-e2e-testing
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: packages/twenty-e2e-testing/run_results/
retention-days: 30
# - uses: actions/upload-artifact@v4
# if: always()
# with:
# name: playwright-report
# path: packages/twenty-e2e-testing/run_results/
# retention-days: 30
ci-front-status-check:
if: always() && !cancelled()
@@ -359,7 +355,7 @@ jobs:
changed-files-check,
front-task,
front-build,
merge-reports-and-check-coverage,
# merge-reports-and-check-coverage,
front-sb-test,
front-sb-build,
]
+2
View File
@@ -23,6 +23,8 @@ jobs:
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
env:
NODE_OPTIONS: '--max-old-space-size=4096'
strategy:
matrix:
task: [lint, typecheck, test]
-3
View File
@@ -9,10 +9,7 @@ on:
types: [opened, synchronize, reopened, closed]
permissions:
actions: write
checks: write
contents: write
issues: write
pull-requests: write
statuses: write
+128
View File
@@ -0,0 +1,128 @@
name: CI Zapier
on:
pull_request:
merge_group:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
env:
SERVER_SETUP_CACHE_KEY: server-setup
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/twenty-zapier/**
packages/twenty-server/**
!packages/twenty-zapier/package.json
!packages/twenty-zapier/CHANGELOG.md
server-setup:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest-8-cores
services:
postgres:
image: twentycrm/twenty-postgres-spilo
env:
PGUSER_SUPERUSER: postgres
PGPASSWORD_SUPERUSER: postgres
ALLOW_NOSSL: 'true'
SPILO_PROVIDER: 'local'
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build twenty-shared
run: npx nx build twenty-shared
- name: Server / Write .env
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Server / Build
run: npx nx build twenty-server
- name: Create and setup database
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
npx nx run twenty-server:database:reset
- name: Server / Start
run: |
npx nx start twenty-server &
echo "Waiting for server to be ready..."
timeout 60 bash -c 'until curl -s http://localhost:3000/health; do sleep 2; done'
- name: Start worker
run: |
npx nx run twenty-server:worker &
echo "Worker started"
- name: Zapier / Build
run: npx nx build twenty-zapier
- name: Zapier / Run Tests
uses: ./.github/actions/nx-affected
with:
tag: scope:zapier
tasks: test
zapier-test:
needs: server-setup
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
matrix:
task: [lint, typecheck, validate]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build
run: npx nx build twenty-zapier
- name: Run ${{ matrix.task }} task
uses: ./.github/actions/nx-affected
with:
tag: scope:zapier
tasks: ${{ matrix.task }}
ci-zapier-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, zapier-test]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1
+7 -14
View File
@@ -87,7 +87,7 @@ jobs:
exit 0
fi
ISSUE_NUMBER="${{ github.event.issue.number || github.event.pull_request.number }}"
ENCODED_BRANCH=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$BRANCH', safe=''))")
ENCODED_BRANCH=$(python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "$BRANCH")
PR_URL="https://github.com/${{ github.repository }}/compare/main...${ENCODED_BRANCH}?quick_pull=1"
BODY="⚠️ Claude ran out of turns before creating a PR. Work has been pushed to [\`$BRANCH\`](https://github.com/${{ github.repository }}/tree/$ENCODED_BRANCH).\n\n[**Create PR →**]($PR_URL)"
if [ -n "$ISSUE_NUMBER" ]; then
@@ -157,18 +157,11 @@ jobs:
"PG_DATABASE_URL": "postgres://postgres:postgres@localhost:5432/default"
}
}
- name: Post response to source issue
- name: Dispatch response to ci-privileged
if: always()
uses: actions/github-script@v7
uses: peter-evans/repository-dispatch@v2
with:
github-token: ${{ secrets.TWENTY_DISPATCH_TOKEN }}
script: |
const [owner, repo] = '${{ steps.prompt.outputs.repo }}'.split('/');
const issueNumber = parseInt('${{ steps.prompt.outputs.issue_number }}', 10);
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: `Claude finished processing this request. [See workflow run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`
});
token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
repository: twentyhq/ci-privileged
event-type: claude-cross-repo-response
client-payload: '{"repo": ${{ toJSON(steps.prompt.outputs.repo) }}, "issue_number": ${{ toJSON(steps.prompt.outputs.issue_number) }}, "run_id": ${{ toJSON(github.run_id) }}, "run_url": ${{ toJSON(format('{0}/{1}/actions/runs/{2}', github.server_url, github.repository, github.run_id)) }}}'
-118
View File
@@ -1,118 +0,0 @@
# Weekly translation QA report using Crowdin's native QA checks
name: 'Weekly Translation QA Report'
permissions:
contents: write
pull-requests: write
on:
schedule:
- cron: '0 9 * * 1' # Every Monday at 9am UTC
workflow_dispatch: # Allow manual trigger
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
qa_report:
name: Generate QA Report
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build twenty-shared
run: npx nx build twenty-shared
- name: Generate QA report from Crowdin
id: generate_report
run: |
npx ts-node packages/twenty-utils/translation-qa-report.ts || true
if [ -f TRANSLATION_QA_REPORT.md ]; then
echo "report_generated=true" >> $GITHUB_OUTPUT
# Count critical issues (exclude spellcheck)
CRITICAL=$(grep -oP '⚠️\s+\K\d+' TRANSLATION_QA_REPORT.md 2>/dev/null || echo "0")
echo "critical_issues=$CRITICAL" >> $GITHUB_OUTPUT
else
echo "report_generated=false" >> $GITHUB_OUTPUT
echo "critical_issues=0" >> $GITHUB_OUTPUT
fi
env:
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Create QA branch and commit report
if: steps.generate_report.outputs.report_generated == 'true'
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@twenty.com'
BRANCH_NAME="i18n-qa-report-$(date +%Y-%m-%d)"
git checkout -B $BRANCH_NAME
git add TRANSLATION_QA_REPORT.md
if ! git diff --staged --quiet --exit-code; then
git commit -m "docs: weekly translation QA report"
git push origin HEAD:$BRANCH_NAME --force
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
else
echo "No changes to commit"
echo "BRANCH_NAME=" >> $GITHUB_ENV
fi
- name: Create pull request
if: steps.generate_report.outputs.report_generated == 'true' && env.BRANCH_NAME != ''
run: |
CRITICAL="${{ steps.generate_report.outputs.critical_issues }}"
BODY=$(cat <<EOF
## Weekly Translation QA Report
**Critical issues (excluding spellcheck): $CRITICAL**
📊 **View in Crowdin**: https://twenty.crowdin.com/u/projects/1/all?filter=qa-issue
### For AI-Assisted Fixing
Open this PR in Cursor and say:
> "Fix the translation QA issues using the Crowdin API"
The AI can help fix:
- ✅ Variables mismatch (missing/wrong placeholders)
- ✅ Escaped Unicode sequences
- ⚠️ Tags mismatch
- ⚠️ Empty translations
### Available Scripts
\`\`\`bash
# View QA report
CROWDIN_PERSONAL_TOKEN=xxx npx ts-node packages/twenty-utils/translation-qa-report.ts
# Fix encoding issues automatically
CROWDIN_PERSONAL_TOKEN=xxx npx ts-node packages/twenty-utils/fix-crowdin-translations.ts
\`\`\`
---
*Close without merging after issues are addressed*
EOF
)
EXISTING_PR=$(gh pr list --head $BRANCH_NAME --json number --jq '.[0].number' 2>/dev/null || echo "")
if [ -n "$EXISTING_PR" ]; then
gh pr edit $EXISTING_PR --body "$BODY"
else
gh pr create \
--base main \
--head $BRANCH_NAME \
--title "i18n: Translation QA Report ($CRITICAL critical issues)" \
--body "$BODY" || true
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+71
View File
@@ -0,0 +1,71 @@
name: Post CI Comments
on:
workflow_run:
workflows: ['GraphQL and OpenAPI Breaking Changes Detection']
types: [completed]
permissions:
actions: read
jobs:
dispatch-breaking-changes:
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Get PR number from workflow run
id: pr-info
uses: actions/github-script@v7
with:
script: |
const runId = context.payload.workflow_run.id;
const headSha = context.payload.workflow_run.head_sha;
const headBranch = context.payload.workflow_run.head_branch;
const headRepo = context.payload.workflow_run.head_repository;
// workflow_run.pull_requests is empty for fork PRs,
// so fall back to searching by head SHA
let pullRequests = context.payload.workflow_run.pull_requests;
let prNumber;
if (pullRequests && pullRequests.length > 0) {
prNumber = pullRequests[0].number;
} else {
core.info(`pull_requests is empty (likely a fork PR), searching by SHA ${headSha}`);
const owner = context.repo.owner;
const repo = context.repo.repo;
const headLabel = `${headRepo.owner.login}:${headBranch}`;
const { data: prs } = await github.rest.pulls.list({
owner,
repo,
state: 'open',
head: headLabel,
per_page: 1,
});
if (prs.length > 0) {
prNumber = prs[0].number;
}
}
if (!prNumber) {
core.info('No pull request found for this workflow run');
core.setOutput('has_pr', 'false');
return;
}
core.setOutput('pr_number', prNumber);
core.setOutput('run_id', runId);
core.setOutput('has_pr', 'true');
core.info(`PR #${prNumber}, Run ID: ${runId}`);
- name: Dispatch to ci-privileged
if: steps.pr-info.outputs.has_pr == 'true'
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
repository: twentyhq/ci-privileged
event-type: breaking-changes-report
client-payload: '{"pr_number": ${{ toJSON(steps.pr-info.outputs.pr_number) }}, "run_id": ${{ toJSON(steps.pr-info.outputs.run_id) }}, "repo": ${{ toJSON(github.repository) }}, "branch_state": ${{ toJSON(github.event.workflow_run.head_branch) }}}'
+21 -6
View File
@@ -2,13 +2,8 @@ name: 'Preview Environment Dispatch'
permissions:
contents: write
actions: write
pull-requests: read
on:
# Using pull_request_target instead of pull_request to have access to secrets for external contributors
# Security note: This is safe because we're only using the repository-dispatch action with limited scope
# and not checking out or running any code from the external contributor's PR
pull_request_target:
types: [opened, synchronize, reopened, labeled]
paths:
@@ -24,7 +19,19 @@ concurrency:
jobs:
trigger-preview:
if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.action == 'reopened' || (github.event.action == 'labeled' && github.event.label.name == 'preview-app')
if: |
(github.event.action == 'labeled' && github.event.label.name == 'preview-app') ||
(
(
github.event.pull_request.author_association == 'MEMBER' ||
github.event.pull_request.author_association == 'OWNER' ||
github.event.pull_request.author_association == 'COLLABORATOR'
) && (
github.event.action == 'opened' ||
github.event.action == 'synchronize' ||
github.event.action == 'reopened'
)
)
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
@@ -35,3 +42,11 @@ jobs:
repository: ${{ github.repository }}
event-type: preview-environment
client-payload: '{"pr_number": "${{ github.event.pull_request.number }}", "pr_head_sha": "${{ github.event.pull_request.head.sha }}", "repo_full_name": "${{ github.repository }}"}'
- name: Dispatch to ci-privileged for PR comment
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
repository: twentyhq/ci-privileged
event-type: preview-env-url
client-payload: '{"pr_number": ${{ toJSON(github.event.pull_request.number) }}, "keepalive_dispatch_time": ${{ toJSON(github.event.pull_request.updated_at) }}, "repo": ${{ toJSON(github.repository) }}}'
+34 -62
View File
@@ -2,7 +2,6 @@ name: 'Preview Environment Keep Alive'
permissions:
contents: read
pull-requests: write
on:
repository_dispatch:
@@ -17,7 +16,7 @@ jobs:
uses: actions/checkout@v4
with:
ref: ${{ github.event.client_payload.pr_head_sha }}
- name: Run compose setup
run: |
echo "Patching docker-compose.yml..."
@@ -25,17 +24,17 @@ jobs:
yq eval 'del(.services.server.image)' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.server.build.context = "../../"' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.server.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i packages/twenty-docker/docker-compose.yml
yq eval 'del(.services.worker.image)' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.worker.build.context = "../../"' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.worker.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i packages/twenty-docker/docker-compose.yml
echo "Adding SIGN_IN_PREFILLED environment variable to server service..."
yq eval '.services.server.environment.SIGN_IN_PREFILLED = "${SIGN_IN_PREFILLED}"' -i packages/twenty-docker/docker-compose.yml
echo "Setting up .env file..."
cp packages/twenty-docker/.env.example packages/twenty-docker/.env
echo "Generating secrets..."
echo "" >> packages/twenty-docker/.env
echo "# === Randomly generated secrets ===" >> packages/twenty-docker/.env
@@ -46,24 +45,25 @@ jobs:
cd packages/twenty-docker/
docker compose build
working-directory: ./
- name: Create Tunnel
id: expose-tunnel
uses: codetalkio/expose-tunnel@v1.5.0
with:
service: bore.pub
port: 3000
- name: Start services with correct SERVER_URL
env:
TUNNEL_URL: ${{ steps.expose-tunnel.outputs.tunnel-url }}
run: |
cd packages/twenty-docker/
# Update the SERVER_URL with the tunnel URL
echo "Setting SERVER_URL to ${{ steps.expose-tunnel.outputs.tunnel-url }}"
echo "Setting SERVER_URL to $TUNNEL_URL"
sed -i '/SERVER_URL=/d' .env
echo "" >> .env
echo "SERVER_URL=${{ steps.expose-tunnel.outputs.tunnel-url }}" >> .env
echo "SERVER_URL=$TUNNEL_URL" >> .env
# Start the services
echo "Docker compose up..."
docker compose up -d || {
@@ -71,7 +71,7 @@ jobs:
docker compose logs
exit 1
}
echo "Waiting for services to be ready..."
count=0
while [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-db-1) = "healthy" ] || [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-server-1) = "healthy" ]; do
@@ -84,7 +84,7 @@ jobs:
fi
echo "Still waiting for services... ($count/60)"
done
echo "All services are up and running!"
working-directory: ./
@@ -99,61 +99,33 @@ jobs:
fi
working-directory: ./
- name: Output tunnel URL to logs
- name: Output tunnel URL
env:
TUNNEL_URL: ${{ steps.expose-tunnel.outputs.tunnel-url }}
run: |
echo "✅ Preview Environment Ready!"
echo "🔗 Preview URL: ${{ steps.expose-tunnel.outputs.tunnel-url }}"
echo "🔗 Preview URL: $TUNNEL_URL"
echo "⏱️ This environment will be available for 5 hours"
- name: Post comment on PR
uses: actions/github-script@v6
echo "## 🚀 Preview Environment Ready!" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "Preview URL: $TUNNEL_URL" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "This environment will automatically shut down after 5 hours." >> "$GITHUB_STEP_SUMMARY"
echo "$TUNNEL_URL" > tunnel-url.txt
- name: Upload tunnel URL artifact
uses: actions/upload-artifact@v4
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const COMMENT_MARKER = '<!-- PR_PREVIEW_ENV -->';
const commentBody = `${COMMENT_MARKER}
🚀 **Preview Environment Ready!**
Your preview environment is available at: ${{ steps.expose-tunnel.outputs.tunnel-url }}
This environment will automatically shut down when the PR is closed or after 5 hours.`;
// Get all comments
const {data: comments} = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: ${{ github.event.client_payload.pr_number }},
});
// Find our comment
const botComment = comments.find(comment => comment.body.includes(COMMENT_MARKER));
if (botComment) {
// Update existing comment
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: commentBody
});
console.log('Updated existing comment');
} else {
// Create new comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: ${{ github.event.client_payload.pr_number }},
body: commentBody
});
console.log('Created new comment');
}
name: tunnel-url
path: tunnel-url.txt
retention-days: 1
- name: Keep tunnel alive for 5 hours
run: timeout 300m sleep 18000 # Stop on whichever we reach first (300m or 5hour sleep)
- name: Cleanup
if: always()
run: |
cd packages/twenty-docker/
docker compose down -v
working-directory: ./
working-directory: ./
@@ -0,0 +1,58 @@
diff --git a/esm/cache.js b/esm/cache.js
index 07cf6d7dd99effb9c3464b620ba67a7f445224f5..248bb527923499a6be8065ee7a3613b55819c58c 100644
--- a/esm/cache.js
+++ b/esm/cache.js
@@ -69,17 +69,20 @@ export class TransformCacheCollection {
this.invalidate(cacheName, filename);
});
}
- invalidateIfChanged(filename, content) {
+ invalidateIfChanged(filename, content, _visited) {
+ const visited = _visited || new Set();
+ if (visited.has(filename)) {
+ return false;
+ }
+ visited.add(filename);
const fileEntrypoint = this.get('entrypoints', filename);
- // We need to check all dependencies of the file
- // because they might have changed as well.
if (fileEntrypoint) {
for (const [, dependency] of fileEntrypoint.dependencies) {
const dependencyFilename = dependency.resolved;
if (dependencyFilename) {
const dependencyContent = fs.readFileSync(dependencyFilename, 'utf8');
- this.invalidateIfChanged(dependencyFilename, dependencyContent);
+ this.invalidateIfChanged(dependencyFilename, dependencyContent, visited);
}
}
}
diff --git a/lib/cache.js b/lib/cache.js
index 0762ed7d3c39b31000f7aa7d8156da15403c8e64..6955410cd3c9ec53cf7a01c8346abc4c47fff791 100644
--- a/lib/cache.js
+++ b/lib/cache.js
@@ -77,17 +77,20 @@ class TransformCacheCollection {
this.invalidate(cacheName, filename);
});
}
- invalidateIfChanged(filename, content) {
+ invalidateIfChanged(filename, content, _visited) {
+ const visited = _visited || new Set();
+ if (visited.has(filename)) {
+ return false;
+ }
+ visited.add(filename);
const fileEntrypoint = this.get('entrypoints', filename);
- // We need to check all dependencies of the file
- // because they might have changed as well.
if (fileEntrypoint) {
for (const [, dependency] of fileEntrypoint.dependencies) {
const dependencyFilename = dependency.resolved;
if (dependencyFilename) {
const dependencyContent = _nodeFs.default.readFileSync(dependencyFilename, 'utf8');
- this.invalidateIfChanged(dependencyFilename, dependencyContent);
+ this.invalidateIfChanged(dependencyFilename, dependencyContent, visited);
}
}
}
+5 -3
View File
@@ -28,6 +28,8 @@ npx jest path/to/test.test.ts --config=packages/PROJECT/jest.config.mjs
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
@@ -88,7 +90,7 @@ npx nx run twenty-front:graphql:generate --configuration=metadata
## Architecture Overview
### Tech Stack
- **Frontend**: React 18, TypeScript, Recoil (state management), Emotion (styling), Vite
- **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
@@ -136,7 +138,7 @@ packages/
- Multi-line comments use multiple `//` lines, not `/** */`
### State Management
- **Recoil** for global state: atoms for primitive state, selectors for derived state, atom families for dynamic collections
- **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)`
@@ -173,7 +175,7 @@ IMPORTANT: Use Context7 for code generation, setup or configuration steps, or li
5. Run `graphql:generate` after any GraphQL schema changes
### Code Style Notes
- Use **Emotion** for styling with styled-components pattern
- 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)
+3 -2
View File
@@ -28,7 +28,7 @@ See:
🚀 [Self-hosting](https://docs.twenty.com/developers/self-hosting/docker-compose)
🖥️ [Local Setup](https://docs.twenty.com/developers/local-setup)
# Does the world need another CRM?
# Why Twenty
We built Twenty for three reasons:
@@ -109,7 +109,7 @@ Below are a few features we have implemented to date:
- [TypeScript](https://www.typescriptlang.org/)
- [Nx](https://nx.dev/)
- [NestJS](https://nestjs.com/), with [BullMQ](https://bullmq.io/), [PostgreSQL](https://www.postgresql.org/), [Redis](https://redis.io/)
- [React](https://reactjs.org/), with [Recoil](https://recoiljs.org/), [Emotion](https://emotion.sh/) and [Lingui](https://lingui.dev/)
- [React](https://reactjs.org/), with [Jotai](https://jotai.org/), [Linaria](https://linaria.dev/) and [Lingui](https://lingui.dev/)
@@ -120,6 +120,7 @@ Below are a few features we have implemented to date:
<a href="https://greptile.com"><img src="./packages/twenty-website/public/images/readme/greptile.png" height="30" alt="Greptile" /></a>
<a href="https://sentry.io/"><img src="./packages/twenty-website/public/images/readme/sentry.png" height="30" alt="Sentry" /></a>
<a href="https://crowdin.com/"><img src="./packages/twenty-website/public/images/readme/crowdin.png" height="30" alt="Crowdin" /></a>
<a href="https://e2b.dev/"><img src="./packages/twenty-website/public/images/readme/e2b.svg" height="30" alt="E2B" /></a>
</p>
Thanks to these amazing services that we use and recommend for UI testing (Chromatic), code review (Greptile), catching bugs (Sentry) and translating (Crowdin).
+4
View File
@@ -83,6 +83,10 @@ export default [
sourceTag: 'scope:frontend',
onlyDependOnLibsWithTags: ['scope:shared', 'scope:frontend'],
},
{
sourceTag: 'scope:zapier',
onlyDependOnLibsWithTags: ['scope:shared', 'scope:zapier'],
},
],
},
],
+4 -4
View File
@@ -126,7 +126,7 @@
"configurations": {
"ci": {
"ci": true,
"maxWorkers": 3
"maxWorkers": 1
},
"coverage": {
"coverageReporters": ["lcov", "text"]
@@ -276,7 +276,7 @@
"generators": {
"@nx/react": {
"application": {
"style": "@emotion/styled",
"style": "@linaria/react",
"linter": "eslint",
"bundler": "vite",
"compiler": "swc",
@@ -284,7 +284,7 @@
"projectNameAndRootFormat": "derived"
},
"library": {
"style": "@emotion/styled",
"style": "@linaria/react",
"linter": "eslint",
"bundler": "vite",
"compiler": "swc",
@@ -292,7 +292,7 @@
"projectNameAndRootFormat": "derived"
},
"component": {
"style": "@emotion/styled"
"style": "@linaria/react"
}
}
},
+17 -18
View File
@@ -2,14 +2,13 @@
"private": true,
"dependencies": {
"@apollo/client": "^3.7.17",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.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",
@@ -41,6 +40,7 @@
"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",
@@ -48,8 +48,7 @@
"react-responsive": "^9.0.2",
"react-router-dom": "^6.4.4",
"react-tooltip": "^5.13.1",
"recoil": "^0.7.7",
"remark-gfm": "^3.0.1",
"remark-gfm": "^4.0.1",
"rxjs": "^7.2.0",
"semver": "^7.5.4",
"slash": "^5.1.0",
@@ -84,17 +83,17 @@
"@sentry/types": "^8",
"@storybook-community/storybook-addon-cookie": "^5.0.0",
"@storybook/addon-coverage": "^3.0.0",
"@storybook/addon-docs": "^10.1.11",
"@storybook/addon-links": "^10.1.11",
"@storybook/addon-vitest": "^10.1.11",
"@storybook/addon-docs": "^10.2.13",
"@storybook/addon-links": "^10.2.13",
"@storybook/addon-vitest": "^10.2.13",
"@storybook/icons": "^2.0.1",
"@storybook/react-vite": "^10.1.11",
"@storybook/react-vite": "^10.2.13",
"@storybook/test-runner": "^0.24.2",
"@stylistic/eslint-plugin": "^1.5.0",
"@swc-node/register": "1.8.0",
"@swc-node/register": "1.11.1",
"@swc/cli": "^0.3.12",
"@swc/core": "1.13.3",
"@swc/helpers": "~0.5.2",
"@swc/core": "1.15.11",
"@swc/helpers": "~0.5.18",
"@swc/jest": "^0.2.39",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
@@ -137,7 +136,7 @@
"@typescript-eslint/parser": "^8.39.0",
"@typescript-eslint/utils": "^8.39.0",
"@typescript/native-preview": "^7.0.0-dev.20260116.1",
"@vitejs/plugin-react-swc": "3.11.0",
"@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",
@@ -160,7 +159,7 @@
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.4",
"eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-storybook": "^10.1.11",
"eslint-plugin-storybook": "^10.2.13",
"eslint-plugin-unicorn": "^56.0.1",
"eslint-plugin-unused-imports": "^3.0.0",
"http-server": "^14.1.1",
@@ -176,9 +175,9 @@
"raw-loader": "^4.0.2",
"rimraf": "^5.0.5",
"source-map-support": "^0.5.20",
"storybook": "^10.1.11",
"storybook": "^10.2.13",
"storybook-addon-mock-date": "2.0.0",
"storybook-addon-pseudo-states": "^10.1.11",
"storybook-addon-pseudo-states": "^10.2.13",
"supertest": "^6.1.3",
"ts-jest": "^29.1.1",
"ts-loader": "^9.2.3",
@@ -202,10 +201,10 @@
"type-fest": "4.10.1",
"typescript": "5.9.2",
"graphql-redis-subscriptions/ioredis": "^5.6.0",
"prosemirror-view": "1.40.0",
"prosemirror-transform": "1.10.4",
"@lingui/core": "5.1.2",
"@types/qs": "6.9.16"
"@types/qs": "6.9.16",
"@wyw-in-js/transform@npm:0.6.0": "patch:@wyw-in-js/transform@npm%3A0.7.0#~/.yarn/patches/@wyw-in-js-transform-npm-0.7.0-ba641dc99f.patch",
"@wyw-in-js/transform@npm:0.7.0": "patch:@wyw-in-js/transform@npm%3A0.7.0#~/.yarn/patches/@wyw-in-js-transform-npm-0.7.0-ba641dc99f.patch"
},
"version": "0.2.1",
"nx": {},
+9 -7
View File
@@ -31,10 +31,6 @@ See Twenty application documentation https://docs.twenty.com/developers/extend/c
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# If you don't use yarn@4
corepack enable
yarn install
# Get help and list all available commands
yarn twenty help
@@ -45,7 +41,7 @@ yarn twenty auth:login
yarn twenty entity:add
# Start dev mode: watches, builds, and syncs local changes to your workspace
# (also auto-generates a typed API client in node_modules/twenty-sdk/generated)
# (also auto-generates typed API clients — CoreApiClient and MetadataApiClient — in node_modules/twenty-sdk/generated)
yarn twenty app:dev
# Watch your application's function logs
@@ -54,6 +50,9 @@ yarn twenty function:logs
# Execute a function with a JSON payload
yarn twenty function:execute -n my-function -p '{"key": "value"}'
# Execute the pre-install function
yarn twenty function:execute --preInstall
# Execute the post-install function
yarn twenty function:execute --postInstall
@@ -89,12 +88,14 @@ In interactive mode, you can pick from:
- **Example front component** — a React UI component (`front-components/hello-world.tsx`)
- **Example view** — a saved view for the example object (`views/example-view.ts`)
- **Example navigation menu item** — a sidebar link (`navigation-menu-items/example-navigation-menu-item.ts`)
- **Example skill** — an AI agent skill definition (`skills/example-skill.ts`)
## What gets scaffolded
**Core files (always created):**
- `application-config.ts` — Application metadata configuration
- `roles/default-role.ts` — Default role for logic functions
- `logic-functions/pre-install.ts` — Pre-install logic function (runs before app installation)
- `logic-functions/post-install.ts` — Post-install logic function (runs after app installation)
- TypeScript configuration, ESLint, package.json, .gitignore
- A prewired `twenty` script that delegates to the `twenty` CLI from twenty-sdk
@@ -106,13 +107,14 @@ In interactive mode, you can pick from:
- `front-components/hello-world.tsx` — Example front component
- `views/example-view.ts` — Example saved view for the example object
- `navigation-menu-items/example-navigation-menu-item.ts` — Example sidebar navigation link
- `skills/example-skill.ts` — Example AI agent skill definition
## Next steps
- Run `yarn twenty help` to see all available commands.
- Use `yarn twenty auth:login` to authenticate with your Twenty workspace.
- Explore the generated project and add your first entity with `yarn twenty entity:add` (logic functions, front components, objects, roles, views, navigation menu items).
- Explore the generated project and add your first entity with `yarn twenty entity:add` (logic functions, front components, objects, roles, views, navigation menu items, skills).
- Use `yarn twenty app:dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
- Types are autogenerated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated`.
- Two typed API clients are autogenerated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated`: `CoreApiClient` (for workspace data via `/graphql`) and `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`).
## Publish your application
+2 -1
View File
@@ -1,5 +1,5 @@
const jestConfig = {
displayName: 'twenty-cli',
displayName: 'create-twenty-app',
preset: '../../jest.preset.js',
testEnvironment: 'node',
transformIgnorePatterns: ['../../node_modules/'],
@@ -15,6 +15,7 @@ const jestConfig = {
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'^package.json$': '<rootDir>/package.json',
},
moduleFileExtensions: ['ts', 'js'],
extensionsToTreatAsEsm: ['.ts'],
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "0.6.0",
"version": "0.6.3",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
@@ -0,0 +1,12 @@
## Base documentation
- Documentation: https://docs.twenty.com/developers/extend/capabilities/apps
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-sdk/src/cli/__tests__/apps/rich-app
## UUID requirement
- All generated UUIDs must be valid UUID v4.
## Common Pitfalls
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
@@ -36,6 +36,10 @@ yarn twenty function:execute # Execute a function with JSON payload
yarn twenty app:uninstall # Uninstall app from workspace
```
## LLMs instructions
Main docs and pitfalls are available in LLMS.md file.
## Learn More
To learn more about Twenty applications, take a look at the following resources:
@@ -114,6 +114,7 @@ export class CreateAppCommand {
includeExampleFrontComponent: false,
includeExampleView: false,
includeExampleNavigationMenuItem: false,
includeExampleSkill: false,
};
}
@@ -125,6 +126,7 @@ export class CreateAppCommand {
includeExampleFrontComponent: true,
includeExampleView: true,
includeExampleNavigationMenuItem: true,
includeExampleSkill: true,
};
}
@@ -164,6 +166,11 @@ export class CreateAppCommand {
value: 'navigationMenuItem',
checked: true,
},
{
name: 'Example skill (AI agent skill definition)',
value: 'skill',
checked: true,
},
],
},
]);
@@ -189,6 +196,7 @@ export class CreateAppCommand {
includeExampleView: includeView,
includeExampleNavigationMenuItem:
selectedExamples.includes('navigationMenuItem'),
includeExampleSkill: selectedExamples.includes('skill'),
};
}
@@ -225,9 +233,9 @@ export class CreateAppCommand {
console.log('');
console.log(chalk.blue('Next steps:'));
console.log(chalk.gray(` cd ${dirName}`));
console.log(chalk.gray(` corepack enable # if you don't use yarn@4`));
console.log(chalk.gray(` yarn install # if you don't use yarn@4`));
console.log(chalk.gray(' yarn auth:login # Authenticate with Twenty'));
console.log(chalk.gray(' yarn app:dev # Start dev mode'));
console.log(
chalk.gray(' yarn twenty auth:login # Authenticate with Twenty'),
);
console.log(chalk.gray(' yarn twenty app:dev # Start dev mode'));
}
}
@@ -7,4 +7,5 @@ export type ExampleOptions = {
includeExampleFrontComponent: boolean;
includeExampleView: boolean;
includeExampleNavigationMenuItem: boolean;
includeExampleSkill: boolean;
};
@@ -1,10 +1,11 @@
import * as fs from 'fs-extra';
import { join } from 'path';
import { tmpdir } from 'os';
import { copyBaseApplicationProject } from '@/utils/app-template';
import { type ExampleOptions } from '@/types/scaffolding-options';
import { GENERATED_DIR } from 'twenty-shared/application';
import { copyBaseApplicationProject } from '@/utils/app-template';
import * as fs from 'fs-extra';
import { tmpdir } from 'os';
import { join } from 'path';
import createTwentyAppPackageJson from 'package.json';
// Mock fs-extra's copy function to skip copying base template (not available during tests)
jest.mock('fs-extra', () => {
const actual = jest.requireActual('fs-extra');
return {
@@ -23,11 +24,13 @@ const ALL_EXAMPLES: ExampleOptions = {
includeExampleFrontComponent: true,
includeExampleView: true,
includeExampleNavigationMenuItem: true,
includeExampleSkill: true,
};
const NO_EXAMPLES: ExampleOptions = {
includeExampleObject: false,
includeExampleField: false,
includeExampleSkill: false,
includeExampleLogicFunction: false,
includeExampleFrontComponent: false,
includeExampleView: false,
@@ -38,7 +41,6 @@ describe('copyBaseApplicationProject', () => {
let testAppDirectory: string;
beforeEach(async () => {
// Create a unique temp directory for each test
testAppDirectory = join(
tmpdir(),
`test-twenty-app-${Date.now()}-${Math.random().toString(36).slice(2)}`,
@@ -48,7 +50,6 @@ describe('copyBaseApplicationProject', () => {
});
afterEach(async () => {
// Clean up temp directory after each test
if (testAppDirectory && (await fs.pathExists(testAppDirectory))) {
await fs.remove(testAppDirectory);
}
@@ -63,15 +64,12 @@ describe('copyBaseApplicationProject', () => {
exampleOptions: ALL_EXAMPLES,
});
// Verify src/ folder exists
const srcAppPath = join(testAppDirectory, 'src');
expect(await fs.pathExists(srcAppPath)).toBe(true);
// Verify application-config.ts exists in src/
const appConfigPath = join(srcAppPath, APPLICATION_FILE_NAME);
expect(await fs.pathExists(appConfigPath)).toBe(true);
// Verify default-role.ts exists in src/
const roleConfigPath = join(srcAppPath, 'roles', DEFAULT_ROLE_FILE_NAME);
expect(await fs.pathExists(roleConfigPath)).toBe(true);
});
@@ -91,7 +89,9 @@ describe('copyBaseApplicationProject', () => {
const packageJson = await fs.readJson(packageJsonPath);
expect(packageJson.name).toBe('my-test-app');
expect(packageJson.version).toBe('0.1.0');
expect(packageJson.dependencies['twenty-sdk']).toBe('latest');
expect(packageJson.devDependencies['twenty-sdk']).toBe(
createTwentyAppPackageJson.version,
);
expect(packageJson.scripts['twenty']).toBe('twenty');
});
@@ -109,7 +109,7 @@ describe('copyBaseApplicationProject', () => {
const gitignoreContent = await fs.readFile(gitignorePath, 'utf8');
expect(gitignoreContent).toContain('/node_modules');
expect(gitignoreContent).toContain('generated');
expect(gitignoreContent).toContain(GENERATED_DIR);
});
it('should create yarn.lock file', async () => {
@@ -140,27 +140,22 @@ describe('copyBaseApplicationProject', () => {
const appConfigPath = join(testAppDirectory, 'src', APPLICATION_FILE_NAME);
const appConfigContent = await fs.readFile(appConfigPath, 'utf8');
// Verify it uses defineApplication
expect(appConfigContent).toContain(
"import { defineApplication } from 'twenty-sdk'",
);
expect(appConfigContent).toContain('export default defineApplication({');
// Verify it imports the role identifier
expect(appConfigContent).toContain(
"import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role'",
);
// Verify display name and description
expect(appConfigContent).toContain("displayName: 'My Test App'");
expect(appConfigContent).toContain("description: 'A test application'");
// Verify it has a universalIdentifier (UUID format)
expect(appConfigContent).toMatch(
/universalIdentifier: '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'/,
);
// Verify it references the role
expect(appConfigContent).toContain(
'defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER',
);
@@ -183,29 +178,24 @@ describe('copyBaseApplicationProject', () => {
);
const roleConfigContent = await fs.readFile(roleConfigPath, 'utf8');
// Verify it uses defineRole
expect(roleConfigContent).toContain(
"import { defineRole } from 'twenty-sdk'",
);
expect(roleConfigContent).toContain('export default defineRole({');
// Verify it exports the universal identifier constant
expect(roleConfigContent).toContain(
'export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER',
);
// Verify role label includes app name
expect(roleConfigContent).toContain(
"label: 'My Test App default function role'",
);
// Verify default permissions
expect(roleConfigContent).toContain('canReadAllObjectRecords: true');
expect(roleConfigContent).toContain('canUpdateAllObjectRecords: true');
expect(roleConfigContent).toContain('canSoftDeleteAllObjectRecords: true');
expect(roleConfigContent).toContain('canDestroyAllObjectRecords: false');
// Verify it has a universalIdentifier (UUID format)
expect(roleConfigContent).toMatch(
/universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER/,
);
@@ -220,7 +210,6 @@ describe('copyBaseApplicationProject', () => {
exampleOptions: ALL_EXAMPLES,
});
// Verify fs.copy was called with correct destination
expect(fs.copy).toHaveBeenCalledTimes(1);
expect(fs.copy).toHaveBeenCalledWith(
expect.stringContaining('base-application'),
@@ -244,7 +233,6 @@ describe('copyBaseApplicationProject', () => {
});
it('should generate unique UUIDs for each application', async () => {
// Create first app
const firstAppDir = join(testAppDirectory, 'app1');
await fs.ensureDir(firstAppDir);
await copyBaseApplicationProject({
@@ -255,7 +243,6 @@ describe('copyBaseApplicationProject', () => {
exampleOptions: ALL_EXAMPLES,
});
// Create second app
const secondAppDir = join(testAppDirectory, 'app2');
await fs.ensureDir(secondAppDir);
await copyBaseApplicationProject({
@@ -266,7 +253,6 @@ describe('copyBaseApplicationProject', () => {
exampleOptions: ALL_EXAMPLES,
});
// Read both app configs
const firstAppConfig = await fs.readFile(
join(firstAppDir, 'src', APPLICATION_FILE_NAME),
'utf8',
@@ -276,7 +262,6 @@ describe('copyBaseApplicationProject', () => {
'utf8',
);
// Extract UUIDs using regex
const uuidRegex =
/universalIdentifier: '([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'/;
const firstUuid = firstAppConfig.match(uuidRegex)?.[1];
@@ -288,7 +273,6 @@ describe('copyBaseApplicationProject', () => {
});
it('should generate unique role UUIDs for each application', async () => {
// Create first app
const firstAppDir = join(testAppDirectory, 'app1');
await fs.ensureDir(firstAppDir);
await copyBaseApplicationProject({
@@ -299,7 +283,6 @@ describe('copyBaseApplicationProject', () => {
exampleOptions: ALL_EXAMPLES,
});
// Create second app
const secondAppDir = join(testAppDirectory, 'app2');
await fs.ensureDir(secondAppDir);
await copyBaseApplicationProject({
@@ -320,7 +303,6 @@ describe('copyBaseApplicationProject', () => {
'utf8',
);
// Extract UUIDs using regex
const uuidRegex =
/DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =\s*'([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'/;
const firstUuid = firstRoleConfig.match(uuidRegex)?.[1];
@@ -372,6 +354,18 @@ describe('copyBaseApplicationProject', () => {
),
),
).toBe(true);
// Install functions should always exist
expect(
await fs.pathExists(
join(srcPath, 'logic-functions', 'pre-install.ts'),
),
).toBe(true);
expect(
await fs.pathExists(
join(srcPath, 'logic-functions', 'post-install.ts'),
),
).toBe(true);
});
});
@@ -387,7 +381,6 @@ describe('copyBaseApplicationProject', () => {
const srcPath = join(testAppDirectory, 'src');
// Core files should exist
expect(await fs.pathExists(join(srcPath, APPLICATION_FILE_NAME))).toBe(
true,
);
@@ -395,7 +388,18 @@ describe('copyBaseApplicationProject', () => {
await fs.pathExists(join(srcPath, 'roles', DEFAULT_ROLE_FILE_NAME)),
).toBe(true);
// Example files should not exist
// Install functions should always exist (not gated by exampleOptions)
expect(
await fs.pathExists(
join(srcPath, 'logic-functions', 'pre-install.ts'),
),
).toBe(true);
expect(
await fs.pathExists(
join(srcPath, 'logic-functions', 'post-install.ts'),
),
).toBe(true);
expect(
await fs.pathExists(join(srcPath, 'objects', 'example-object.ts')),
).toBe(false);
@@ -437,6 +441,7 @@ describe('copyBaseApplicationProject', () => {
exampleOptions: {
includeExampleObject: false,
includeExampleField: false,
includeExampleSkill: false,
includeExampleLogicFunction: false,
includeExampleFrontComponent: true,
includeExampleView: false,
@@ -472,6 +477,7 @@ describe('copyBaseApplicationProject', () => {
appDirectory: testAppDirectory,
exampleOptions: {
includeExampleObject: false,
includeExampleSkill: false,
includeExampleField: false,
includeExampleLogicFunction: true,
includeExampleFrontComponent: false,
@@ -671,4 +677,124 @@ describe('copyBaseApplicationProject', () => {
expect(content).toContain('position: 0');
});
});
describe('pre-install logic function', () => {
it('should create pre-install.ts with definePreInstallLogicFunction and typed payload', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
exampleOptions: NO_EXAMPLES,
});
const preInstallPath = join(
testAppDirectory,
'src',
'logic-functions',
'pre-install.ts',
);
expect(await fs.pathExists(preInstallPath)).toBe(true);
const content = await fs.readFile(preInstallPath, 'utf8');
expect(content).toContain(
"import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk'",
);
expect(content).toContain(
'export default definePreInstallLogicFunction({',
);
expect(content).toContain("name: 'pre-install'");
expect(content).toContain('timeoutSeconds: 300');
expect(content).toContain(
'const handler = async (payload: InstallLogicFunctionPayload): Promise<void>',
);
expect(content).toContain('payload.previousVersion');
// Verify it has a universalIdentifier (UUID format)
expect(content).toMatch(
/universalIdentifier: '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'/,
);
});
it('should always create pre-install.ts regardless of example options', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
exampleOptions: NO_EXAMPLES,
});
const preInstallPath = join(
testAppDirectory,
'src',
'logic-functions',
'pre-install.ts',
);
expect(await fs.pathExists(preInstallPath)).toBe(true);
});
});
describe('post-install logic function', () => {
it('should create post-install.ts with definePostInstallLogicFunction and typed payload', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
exampleOptions: NO_EXAMPLES,
});
const postInstallPath = join(
testAppDirectory,
'src',
'logic-functions',
'post-install.ts',
);
expect(await fs.pathExists(postInstallPath)).toBe(true);
const content = await fs.readFile(postInstallPath, 'utf8');
expect(content).toContain(
"import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk'",
);
expect(content).toContain(
'export default definePostInstallLogicFunction({',
);
expect(content).toContain("name: 'post-install'");
expect(content).toContain('timeoutSeconds: 300');
expect(content).toContain(
'const handler = async (payload: InstallLogicFunctionPayload): Promise<void>',
);
expect(content).toContain('payload.previousVersion');
// Verify it has a universalIdentifier (UUID format)
expect(content).toMatch(
/universalIdentifier: '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'/,
);
});
it('should always create post-install.ts regardless of example options', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
exampleOptions: NO_EXAMPLES,
});
const postInstallPath = join(
testAppDirectory,
'src',
'logic-functions',
'post-install.ts',
);
expect(await fs.pathExists(postInstallPath)).toBe(true);
});
});
});
@@ -4,6 +4,7 @@ import { v4 } from 'uuid';
import { ASSETS_DIR } from 'twenty-shared/application';
import { type ExampleOptions } from '@/types/scaffolding-options';
import createTwentyAppPackageJson from 'package.json';
const SRC_FOLDER = 'src';
@@ -89,6 +90,20 @@ export const copyBaseApplicationProject = async ({
});
}
if (exampleOptions.includeExampleSkill) {
await createExampleSkill({
appDirectory: sourceFolderPath,
fileFolder: 'skills',
fileName: 'example-skill.ts',
});
}
await createDefaultPreInstallFunction({
appDirectory: sourceFolderPath,
fileFolder: 'logic-functions',
fileName: 'pre-install.ts',
});
await createDefaultPostInstallFunction({
appDirectory: sourceFolderPath,
fileFolder: 'logic-functions',
@@ -132,8 +147,7 @@ generated
# dev
/dist/
.twenty/*
!.twenty/output/
.twenty
# production
/build
@@ -260,6 +274,36 @@ export default defineLogicFunction({
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createDefaultPreInstallFunction = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const universalIdentifier = v4();
const content = `import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: '${universalIdentifier}',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createDefaultPostInstallFunction = async ({
appDirectory,
fileFolder,
@@ -271,16 +315,14 @@ const createDefaultPostInstallFunction = async ({
}) => {
const universalIdentifier = v4();
const content = `import { defineLogicFunction } from 'twenty-sdk';
const content = `import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '${universalIdentifier}';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
export default definePostInstallLogicFunction({
universalIdentifier: '${universalIdentifier}',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
@@ -424,6 +466,36 @@ export default defineNavigationMenuItem({
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createExampleSkill = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const universalIdentifier = v4();
const content = `import { defineSkill } from 'twenty-sdk';
export const EXAMPLE_SKILL_UNIVERSAL_IDENTIFIER =
'${universalIdentifier}';
export default defineSkill({
universalIdentifier: EXAMPLE_SKILL_UNIVERSAL_IDENTIFIER,
name: 'example-skill',
label: 'Example Skill',
description: 'A sample skill for your application',
icon: 'IconBrain',
content: 'Add your skill instructions here. Skills provide context and capabilities to AI agents.',
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createApplicationConfig = async ({
displayName,
description,
@@ -439,14 +511,12 @@ const createApplicationConfig = async ({
}) => {
const content = `import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '${v4()}',
displayName: '${displayName}',
description: '${description ?? ''}',
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
`;
@@ -476,10 +546,9 @@ const createPackageJson = async ({
lint: 'eslint',
'lint:fix': 'eslint --fix',
},
dependencies: {
'twenty-sdk': 'latest',
},
dependencies: {},
devDependencies: {
'twenty-sdk': createTwentyAppPackageJson.version,
typescript: '^5.9.3',
'@types/node': '^24.7.2',
'@types/react': '^18.2.0',
@@ -6,7 +6,13 @@ const execPromise = promisify(exec);
export const install = async (root: string) => {
try {
await execPromise('yarn', { cwd: root });
await execPromise('corepack enable', { cwd: root });
} catch (error: any) {
console.warn(chalk.yellow('corepack enabled failed:'), error.stderr);
}
try {
await execPromise('yarn install', { cwd: root });
} catch (error: any) {
console.error(chalk.red('yarn install failed:'), error.stdout);
}
+2 -1
View File
@@ -11,7 +11,8 @@
"noEmit": true,
"types": ["jest", "node"],
"paths": {
"@/*": ["./src/*"]
"@/*": ["./src/*"],
"package.json": ["./package.json"]
},
"jsx": "react"
},
@@ -0,0 +1,38 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn
# codegen
generated
# testing
/coverage
# dev
/dist/
.twenty/*
!.twenty/output/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# typescript
*.tsbuildinfo
@@ -0,0 +1 @@
24.5.0
@@ -0,0 +1 @@
nodeLinker: node-modules
@@ -0,0 +1,12 @@
## Base documentation
- Documentation: https://docs.twenty.com/developers/extend/capabilities/apps
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-sdk/src/cli/__tests__/apps/rich-app
## UUID requirement
- All generated UUIDs must be valid UUID v4.
## Common Pitfalls
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
@@ -0,0 +1,51 @@
This is a [Twenty](https://twenty.com) application project bootstrapped with [`create-twenty-app`](https://www.npmjs.com/package/create-twenty-app).
## Getting Started
First, authenticate to your workspace:
```bash
yarn twenty auth:login
```
Then, start development mode to sync your app and watch for changes:
```bash
yarn twenty app:dev
```
Open your Twenty instance and go to `/settings/applications` section to see the result.
## Available Commands
Run `yarn twenty help` to list all available commands. Common commands:
```bash
# Authentication
yarn twenty auth:login # Authenticate with Twenty
yarn twenty auth:logout # Remove credentials
yarn twenty auth:status # Check auth status
yarn twenty auth:switch # Switch default workspace
yarn twenty auth:list # List all configured workspaces
# Application
yarn twenty app:dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty entity:add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty function:logs # Stream function logs
yarn twenty function:execute # Execute a function with JSON payload
yarn twenty app:uninstall # Uninstall app from workspace
```
## LLMs instructions
Main docs and pitfalls are available in LLMS.md file.
## Learn More
To learn more about Twenty applications, take a look at the following resources:
- [twenty-sdk](https://www.npmjs.com/package/twenty-sdk) - learn about `twenty-sdk` tool.
- [Twenty doc](https://docs.twenty.com/) - Twenty's documentation.
- Join our [Discord](https://discord.gg/cx5n4Jzs57)
You can check out [the Twenty GitHub repository](https://github.com/twentyhq/twenty) - your feedback and contributions are welcome!
@@ -0,0 +1,29 @@
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
export default [
// Base JS recommended rules
js.configs.recommended,
// TypeScript recommended rules
...tseslint.configs.recommended,
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parserOptions: {
project: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
// Common TypeScript-friendly tweaks
'@typescript-eslint/no-unused-vars': [
'warn',
{ argsIgnorePattern: '^_' },
],
'@typescript-eslint/no-explicit-any': 'off',
'no-unused-vars': 'off', // handled by TS rule
},
},
];
@@ -0,0 +1,27 @@
{
"name": "apollo-enrich",
"version": "0.1.0",
"license": "MIT",
"engines": {
"node": "^24.5.0",
"npm": "please-use-yarn",
"yarn": ">=4.0.2"
},
"packageManager": "yarn@4.9.2",
"scripts": {
"twenty": "twenty",
"lint": "eslint",
"lint:fix": "eslint --fix"
},
"dependencies": {
"twenty-sdk": "latest"
},
"devDependencies": {
"@types/node": "^24.7.2",
"@types/react": "^18.2.0",
"eslint": "^9.32.0",
"react": "^18.2.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.50.0"
}
}
@@ -0,0 +1,54 @@
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { defineApplication } from 'twenty-sdk';
export default defineApplication({
universalIdentifier: 'ac1d2ed1-8835-4bd4-9043-28b46fdda465',
displayName: 'Apollo enrichment',
description: 'Data enrichment with Apollo to keep your data accurate',
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
settingsCustomTabFrontComponentUniversalIdentifier: '50d59f7c-eada-4731-aacd-8e45371e1040',
applicationVariables: {
APOLLO_CLIENT_ID: {
universalIdentifier: '5852219e-7757-463e-9e7c-80980203794c',
isSecret: true,
value: '',
description: 'Apollo Client ID',
},
APOLLO_CLIENT_SECRET: {
universalIdentifier: 'a032349d-9458-4381-8505-82547276434a',
isSecret: true,
value: '',
description: 'Apollo Client Secret',
},
APOLLO_OAUTH_URL: {
universalIdentifier: '1d42411c-5809-4093-873a-8121b1302475',
isSecret: false,
value: '',
description: 'Apollo OAuth URL',
},
APOLLO_REDIRECT_URI: {
universalIdentifier: 'c8d9e0f1-2a3b-4c5d-6e7f-8a9b0c1d2e3f',
isSecret: false,
value: '',
description: 'Apollo OAuth redirect URI',
},
APOLLO_REGISTERED_URL: {
universalIdentifier: '672a6fce-5565-43bc-9a3b-7f2c33620770',
isSecret: false,
value: '',
description: 'Apollo registered URL',
},
APOLLO_ACCESS_TOKEN: {
universalIdentifier: '672a6fce-5565-43bc-9a3b-7f2c33620771',
isSecret: true,
value: '',
description: 'Apollo access token',
},
APOLLO_REFRESH_TOKEN: {
universalIdentifier: '672a6fce-5565-43bc-9a3b-7f2c33620772',
isSecret: true,
value: '',
description: 'Apollo refresh token',
},
},
});
@@ -0,0 +1,16 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
export default defineField({
universalIdentifier: 'da15cfc6-3657-457d-8757-4ba11b5bb6e1',
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.NUMBER,
name: 'apolloFoundedYear',
label: 'Founded Year',
description: 'Year the company was founded, from Apollo enrichment',
icon: 'IconCalendar',
});
@@ -0,0 +1,16 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
export default defineField({
universalIdentifier: '505532f5-1fc5-4a58-8074-ba9b48650dbc',
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.TEXT,
name: 'apolloIndustry',
label: 'Apollo Industry',
description: 'Industry classification from Apollo enrichment',
icon: 'IconBuildingFactory',
});
@@ -0,0 +1,16 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
export default defineField({
universalIdentifier: 'be15e062-b065-48b4-979c-65b9a50e0cb1',
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.TEXT,
name: 'apolloShortDescription',
label: 'Apollo Description',
description: 'Short company description from Apollo enrichment',
icon: 'IconFileDescription',
});
@@ -0,0 +1,16 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
export default defineField({
universalIdentifier: 'c90ae72d-4ddf-4f22-882f-eef98c91e40e',
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.CURRENCY,
name: 'apolloTotalFunding',
label: 'Total Funding',
description: 'Total funding raised by the company, from Apollo enrichment',
icon: 'IconCash',
});
@@ -0,0 +1,220 @@
import styled from '@emotion/styled';
import { useEffect, useState } from 'react';
import { OAuthApplicationVariables } from 'src/logic-functions/get-oauth-application-variables';
import { VERIFY_PAGE_PATH } from 'src/logic-functions/get-verify-page';
import { defineFrontComponent } from 'twenty-sdk';
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
`;
const StyledSectionTitle = styled.h3`
color: #333;
font-family: 'Inter', sans-serif;
font-size: 15px;
font-weight: 600;
margin: 0 0 4px 0;
`;
const StyledSectionSubtitle = styled.p`
color: #818181;
font-family: 'Inter', sans-serif;
font-size: 13px;
font-weight: 400;
margin: 0 0 12px 0;
`;
const StyledCard = styled.div`
align-items: center;
background: #fff;
border: 1px solid #ebebeb;
border-radius: 8px;
display: flex;
gap: 12px;
padding: 16px;
`;
const StyledIconContainer = styled.div`
align-items: center;
background: #f5f5f5;
border-radius: 8px;
color: #666;
display: flex;
flex-shrink: 0;
height: 40px;
justify-content: center;
width: 40px;
`;
const StyledTextContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
gap: 2px;
min-width: 0;
`;
const StyledTitle = styled.span`
color: #333;
font-family: 'Inter', sans-serif;
font-size: 14px;
font-weight: 500;
`;
const StyledDescription = styled.span`
color: #818181;
font-family: 'Inter', sans-serif;
font-size: 13px;
`;
const StyledLink = styled.a`
align-items: center;
background: #5e5adb;
border: 1px solid rgba(0, 0, 0, 0.04);
border-radius: 4px;
box-sizing: border-box;
color: #fafafa;
cursor: pointer;
display: inline-flex;
flex-shrink: 0;
font-family: 'Inter', sans-serif;
font-size: 13px;
font-weight: 500;
gap: 4px;
height: 32px;
justify-content: center;
padding: 0 12px;
text-decoration: none;
transition: background 0.1s ease;
white-space: nowrap;
&:hover {
background: #4b47b8;
}
&:active {
background: #3c3996;
}
&:focus {
outline: none;
}
`;
const StyledConnectedStatus = styled.span`
align-items: center;
background: #10b981;
border-radius: 4px;
color: #fff;
display: inline-flex;
flex-shrink: 0;
font-family: 'Inter', sans-serif;
font-size: 13px;
font-weight: 500;
gap: 6px;
height: 32px;
padding: 0 12px;
white-space: nowrap;
`;
const StyledIcon = styled.img`
height: 24px;
width: 24px;
`;
const APOLLO_ICON_URL = 'https://twenty-icons.com/apollo.io';
const fetchOAuthApplicationVariables = async (): Promise<OAuthApplicationVariables> => {
const backEndUrl = `${process.env.TWENTY_API_URL}/s/oauth/application-variables`;
const response = await fetch(backEndUrl, {
method: 'GET',
});
const data = await response.json();
return data;
};
const buildOAuthUrl = (oauthApplicationVariables: OAuthApplicationVariables): string => {
const { apolloOAuthUrl, apolloClientId, apolloRegisteredUrl } = oauthApplicationVariables;
const redirectUri = `${apolloRegisteredUrl}auth/oauth-propagator/callback`;
const state = encodeURIComponent(`${process.env.TWENTY_API_URL}/s${VERIFY_PAGE_PATH}`);
return `${apolloOAuthUrl}?client_id=${apolloClientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code`;
};
const ApolloOAuthCta = () => {
const [oauthApplicationVariables, setOAuthApplicationVariables] =
useState<OAuthApplicationVariables | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetchOAuthApplicationVariables()
.then(setOAuthApplicationVariables)
.catch(setError)
.finally(() => setIsLoading(false));
}, []);
if (isLoading) {
return (
<StyledContainer>
<StyledSectionTitle>Connect to Apollo</StyledSectionTitle>
<StyledSectionSubtitle>Enrich your contacts with Apollo data</StyledSectionSubtitle>
<StyledCard>
<StyledIconContainer>
<StyledIcon src={APOLLO_ICON_URL} alt="Apollo" />
</StyledIconContainer>
<StyledTextContainer>
<StyledTitle>Apollo OAuth</StyledTitle>
<StyledDescription>Loading...</StyledDescription>
</StyledTextContainer>
</StyledCard>
</StyledContainer>
);
}
if (error || !oauthApplicationVariables) {
return null;
}
const isConnected = Boolean(oauthApplicationVariables.apolloAccessToken);
const oauthUrl = buildOAuthUrl(oauthApplicationVariables);
return (
<StyledContainer>
<StyledSectionTitle>Connect to Apollo</StyledSectionTitle>
<StyledSectionSubtitle>Enrich your contacts with Apollo data</StyledSectionSubtitle>
<StyledCard>
<StyledIconContainer>
<StyledIcon src={APOLLO_ICON_URL} alt="Apollo" />
</StyledIconContainer>
<StyledTextContainer>
<StyledTitle>Apollo OAuth</StyledTitle>
<StyledDescription>
{isConnected
? 'Your Apollo account is connected'
: 'Connect your Apollo account to enrich contacts'}
</StyledDescription>
</StyledTextContainer>
{isConnected ? (
<StyledConnectedStatus>
Connected
</StyledConnectedStatus>
) : (
<StyledLink href={oauthUrl} rel="noopener noreferrer">
Connect
</StyledLink>
)}
</StyledCard>
</StyledContainer>
);
};
export default defineFrontComponent({
universalIdentifier: '50d59f7c-eada-4731-aacd-8e45371e1040',
name: 'apollo-oauth-cta',
description: 'CTA button to connect to Apollo Enrichment via OAuth',
component: ApolloOAuthCta,
});
@@ -0,0 +1,105 @@
import { defineLogicFunction, RoutePayload } from "twenty-sdk";
import { MetadataApiClient } from 'twenty-sdk/generated';
export const OAUTH_TOKEN_PAIRS_PATH = '/oauth/token-pairs';
type ApolloTokenResponse = {
access_token: string;
token_type: string;
expires_in: number;
refresh_token: string;
scope: string;
created_at: number;
};
const getAuthenticationTokenPairs = async (
code: string,
clientId: string,
clientSecret: string,
): Promise<ApolloTokenResponse> => {
const formData = new URLSearchParams({
grant_type: 'authorization_code',
client_id: clientId,
client_secret: clientSecret,
code: code,
redirect_uri: 'https://hjsm0q38-3000.uks1.devtunnels.ms/auth/oauth-propagator/callback',
});
const response = await fetch('https://app.apollo.io/api/v1/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formData.toString(),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to exchange code for tokens: ${response.status} - ${errorText}`);
}
return response.json();
};
const handler = async (event: RoutePayload): Promise<any> => {
const { queryStringParameters: { code } } = event;
if (!code) {
throw new Error('Code is required');
}
const apolloClientId = process.env.APOLLO_CLIENT_ID ?? '';
const apolloClientSecret = process.env.APOLLO_CLIENT_SECRET ?? '';
const applicationId = process.env.APPLICATION_ID ?? '';
const metadataClient = new MetadataApiClient({});
const tokenPairs = await getAuthenticationTokenPairs(
code,
apolloClientId,
apolloClientSecret,
);
await metadataClient.mutation({
updateOneApplicationVariable: {
__args: {
key: 'APOLLO_ACCESS_TOKEN',
value: tokenPairs.access_token,
applicationId,
},
},
});
await metadataClient.mutation({
updateOneApplicationVariable: {
__args: {
key: 'APOLLO_REFRESH_TOKEN',
value: tokenPairs.refresh_token,
applicationId,
},
},
});
return {tokenPairs};
};
export default defineLogicFunction({
universalIdentifier: '7ccc63a7-ece1-44c0-adbe-805a1baea03a',
name: 'get-authentication-token-pairs',
description: 'Returns the Apollo authentication token pairs',
timeoutSeconds: 10,
handler,
httpRouteTriggerSettings: {
path: OAUTH_TOKEN_PAIRS_PATH,
httpMethod: 'GET',
isAuthRequired: false,
},
});
@@ -0,0 +1,32 @@
import { defineLogicFunction } from 'twenty-sdk';
export type OAuthApplicationVariables = {
apolloClientId: string;
apolloRegisteredUrl: string;
apolloOAuthUrl: string;
apolloAccessToken: string;
apolloRefreshToken: string;
};
const handler = async (): Promise<OAuthApplicationVariables> => {
const apolloClientId = process.env.APOLLO_CLIENT_ID ?? '';
const apolloRegisteredUrl = process.env.APOLLO_REGISTERED_URL ?? '';
const apolloOAuthUrl = process.env.APOLLO_OAUTH_URL ?? '';
const apolloAccessToken = process.env.APOLLO_ACCESS_TOKEN ?? '';
const apolloRefreshToken = process.env.APOLLO_REFRESH_TOKEN ?? '';
return { apolloClientId, apolloRegisteredUrl, apolloOAuthUrl, apolloAccessToken, apolloRefreshToken };
};
export default defineLogicFunction({
universalIdentifier: 'b7c3e8f1-9d4a-4e2b-8f6c-1a5d3e7b9c2f',
name: 'get-oauth-application-variables',
description: 'Returns the Apollo OAuth authorization URL',
timeoutSeconds: 10,
handler,
httpRouteTriggerSettings: {
path: '/oauth/application-variables',
httpMethod: 'GET',
isAuthRequired: false,
},
});
@@ -0,0 +1,90 @@
import { defineLogicFunction } from "twenty-sdk";
export const VERIFY_PAGE_PATH = '/oauth/verify';
const buildVerifyPageHtml = (applicationId: string): string => `<!DOCTYPE html>
<html>
<head>
<title>Apollo OAuth - Verifying...</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; text-align: center; }
.loading { color: #6b7280; }
.success { color: #10b981; }
.error { color: #ef4444; }
.spinner { border: 3px solid #f3f4f6; border-top: 3px solid #3b82f6; border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 20px auto; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
</style>
<script>
(async function() {
const applicationId = ${JSON.stringify(applicationId)};
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const baseUrl = window.location.origin;
function showError(message) {
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('spinner').style.display = 'none';
document.getElementById('title').textContent = '✗ Connection Failed';
document.getElementById('title').className = 'error';
document.getElementById('status').textContent = message;
});
if (window.opener) {
window.opener.postMessage({ type: 'APOLLO_OAUTH_ERROR', error: message }, '*');
}
}
if (!code) {
showError('Authorization code is missing. Please try connecting again.');
return;
}
try {
const response = await fetch(
baseUrl + '/s/oauth/token-pairs?code=' + encodeURIComponent(code),
{
method: 'GET',
headers: { 'Content-Type': 'application/json' }
}
);
if (!response.ok) {
const errorText = await response.text();
throw new Error('Failed to get tokens: ' + response.status + ' - ' + errorText);
}
const tokens = await response.json();
window.location.href = 'http://apple.localhost:3001/settings/applications/' + applicationId + '#custom';
} catch (error) {
showError(error.message);
}
})();
</script>
</head>
<body>
<div class="spinner" id="spinner"></div>
<h1 class="loading" id="title">Connecting to Apollo...</h1>
<p id="status">Please wait while we complete the connection.</p>
</body>
</html>`;
const handler = async (): Promise<string> => {
const applicationId = process.env.APPLICATION_ID ?? '';
return buildVerifyPageHtml(applicationId);
};
export default defineLogicFunction({
universalIdentifier: '4d74950a-d9c1-4c66-a799-89c1aea4e6b0',
name: 'get-verify-page',
description: 'Returns the Apollo OAuth verify page',
timeoutSeconds: 10,
handler,
httpRouteTriggerSettings: {
path: VERIFY_PAGE_PATH,
httpMethod: 'GET',
isAuthRequired: false,
},
});
@@ -0,0 +1,234 @@
import {
defineLogicFunction,
type DatabaseEventPayload,
type ObjectRecordUpdateEvent,
} from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/generated';
type CompanyRecord = {
id: string;
name?: string;
domainName?: {
primaryLinkUrl?: string;
primaryLinkLabel?: string;
};
};
type ApolloOrganization = {
name?: string;
website_url?: string;
linkedin_url?: string;
twitter_url?: string;
estimated_num_employees?: number;
annual_revenue?: number;
total_funding?: number;
street_address?: string;
city?: string;
state?: string;
postal_code?: string;
country?: string;
short_description?: string;
industry?: string;
founded_year?: number;
};
type ApolloEnrichResponse = {
organization?: ApolloOrganization;
};
const extractDomain = (
domainName?: CompanyRecord['domainName'],
): string | undefined => {
const url = domainName?.primaryLinkUrl;
if (!url) {
return undefined;
}
try {
const hostname = new URL(
url.startsWith('http') ? url : `https://${url}`,
).hostname;
return hostname.replace(/^www\./, '');
} catch {
return url.replace(/^(https?:\/\/)?(www\.)?/, '').split('/')[0];
}
};
const fetchApolloEnrichment = async (
domain: string,
): Promise<ApolloOrganization | undefined> => {
const response = await fetch(
`https://api.apollo.io/api/v1/organizations/enrich?domain=${encodeURIComponent(domain)}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.APOLLO_ACCESS_TOKEN ?? ''}`,
},
},
);
const data: ApolloEnrichResponse = await response.json();
return data.organization;
};
const buildCompanyUpdateData = (
apolloOrganization: ApolloOrganization,
): Record<string, unknown> => {
const updateData: Record<string, unknown> = {};
if (apolloOrganization.name) {
updateData.name = apolloOrganization.name;
}
if (apolloOrganization.estimated_num_employees) {
updateData.employees = apolloOrganization.estimated_num_employees;
}
if (apolloOrganization.linkedin_url) {
updateData.linkedinLink = {
primaryLinkUrl: apolloOrganization.linkedin_url,
primaryLinkLabel: 'LinkedIn',
};
}
if (apolloOrganization.twitter_url) {
updateData.xLink = {
primaryLinkUrl: apolloOrganization.twitter_url,
primaryLinkLabel: 'X',
};
}
if (apolloOrganization.annual_revenue) {
updateData.annualRecurringRevenue = {
amountMicros: apolloOrganization.annual_revenue * 1_000_000,
currencyCode: 'USD',
};
}
const hasAddress =
apolloOrganization.street_address ||
apolloOrganization.city ||
apolloOrganization.state ||
apolloOrganization.country;
if (hasAddress) {
updateData.address = {
addressStreet1: apolloOrganization.street_address ?? '',
addressCity: apolloOrganization.city ?? '',
addressState: apolloOrganization.state ?? '',
addressPostcode: apolloOrganization.postal_code ?? '',
addressCountry: apolloOrganization.country ?? '',
};
}
if (apolloOrganization.industry) {
updateData.apolloIndustry = apolloOrganization.industry;
}
if (apolloOrganization.short_description) {
updateData.apolloShortDescription = apolloOrganization.short_description;
}
if (apolloOrganization.founded_year) {
updateData.apolloFoundedYear = apolloOrganization.founded_year;
}
if (apolloOrganization.total_funding) {
updateData.apolloTotalFunding = {
amountMicros: apolloOrganization.total_funding * 1_000_000,
currencyCode: 'USD',
};
}
return updateData;
};
const updateCompanyInTwenty = async (
companyId: string,
updateData: Record<string, unknown>,
): Promise<void> => {
const client = new CoreApiClient();
const result = await client.mutation({
updateCompany: {
__args: {
id: companyId,
data: updateData,
},
id: true,
},
});
if (!result.updateCompany) {
throw new Error(`Failed to update company ${companyId}: no result`);
}
};
type CompanyUpdateEvent = DatabaseEventPayload<
ObjectRecordUpdateEvent<CompanyRecord>
>;
const handler = async (
event: CompanyUpdateEvent,
): Promise<object | undefined> => {
const { recordId, properties } = event;
const { after: companyAfter } = properties;
const domain = extractDomain(companyAfter?.domainName);
if (!domain) {
return { skipped: true, reason: 'no domain found on company' };
}
const apolloOrganization = await fetchApolloEnrichment(domain);
if (!apolloOrganization) {
return {
skipped: true,
reason: `no Apollo data found for ${domain}`,
};
}
const updateData = buildCompanyUpdateData(apolloOrganization);
if (Object.keys(updateData).length === 0) {
return { skipped: true, reason: 'no enrichment data to apply' };
}
await updateCompanyInTwenty(recordId, updateData);
const result = {
enriched: true,
companyId: recordId,
domain,
updatedFields: Object.keys(updateData),
};
return result;
};
export default defineLogicFunction({
universalIdentifier: '6248b3fe-a8af-404a-8e38-19df98f73d81',
name: 'on-company-updated',
description:
'Enriches company data from Apollo when the company domain is updated',
timeoutSeconds: 30,
handler,
databaseEventTriggerSettings: {
eventName: 'company.updated',
updatedFields: ['domainName'],
},
});
@@ -0,0 +1,13 @@
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
export default definePostInstallLogicFunction({
universalIdentifier: '08292efc-d7ba-4ec3-ab95-e7c33bd3a3bc',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
@@ -0,0 +1,13 @@
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'af7cd86e-149e-466a-8d60-312b6e46d604',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
@@ -0,0 +1,15 @@
import { defineRole } from 'twenty-sdk';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b8faae3f-e174-43fa-ab94-715712ae26cb';
export default defineRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Apollo enrich default function role',
description: 'Apollo enrich default function role',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: true,
});
@@ -0,0 +1,31 @@
{
"compileOnSave": false,
"compilerOptions": {
"sourceMap": true,
"declaration": true,
"outDir": "./dist",
"rootDir": ".",
"jsx": "react-jsx",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"importHelpers": true,
"allowUnreachableCode": false,
"strict": true,
"alwaysStrict": true,
"noImplicitAny": true,
"strictBindCallApply": false,
"target": "es2018",
"module": "esnext",
"lib": ["es2020", "dom"],
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"resolveJsonModule": true,
"paths": {
"src/*": ["./src/*"],
"~/*": ["./*"]
}
},
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
}
File diff suppressed because it is too large Load Diff
@@ -1,2 +1,3 @@
.yarn/install-state.gz
.env
.twenty
@@ -9,14 +9,16 @@
},
"packageManager": "yarn@4.9.2",
"scripts": {
"create-entity": "twenty app add",
"dev": "twenty app dev",
"sync": "twenty app sync",
"uninstall": "twenty app uninstall",
"auth": "twenty auth login"
"twenty": "twenty",
"auth": "twenty auth:login",
"dev": "twenty app:dev",
"build": "twenty app:build",
"typecheck": "twenty app:typecheck",
"uninstall": "twenty app:uninstall",
"entity:add": "twenty entity:add"
},
"dependencies": {
"twenty-sdk": "0.2.4"
"twenty-sdk": "0.6.3"
},
"devDependencies": {
"@types/node": "^24.7.2"
@@ -1,67 +1,55 @@
import type {
FunctionConfig,
DatabaseEventPayload,
ObjectRecordCreateEvent,
CronPayload,
import {
defineLogicFunction,
type CronPayload,
type DatabaseEventPayload,
type ObjectRecordCreateEvent,
} from 'twenty-sdk';
import Twenty, { type Person } from '../../generated';
import { CoreApiClient as Twenty, type CoreSchema } from 'twenty-sdk/generated';
type CreateNewPostCardParams =
| { name?: string }
| DatabaseEventPayload<ObjectRecordCreateEvent<Person>>
| DatabaseEventPayload<ObjectRecordCreateEvent<CoreSchema.Person>>
| CronPayload;
export const main = async (params: CreateNewPostCardParams) => {
try {
const client = new Twenty();
const handler = async (params: CreateNewPostCardParams) => {
const client = new Twenty();
const name =
'name' in params
? params.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
const name =
'name' in params
? params.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
const createPostCard = await client.mutation({
createPostCard: {
__args: {
data: {
name,
},
const createPostCard = await client.mutation({
createPostCard: {
__args: {
data: {
name,
},
name: true,
id: true,
},
});
name: true,
id: true,
},
});
console.log('createPostCard result', createPostCard);
console.log('createPostCard result', createPostCard);
return createPostCard;
} catch (error) {
console.error(error);
throw error;
}
return createPostCard;
};
export const config: FunctionConfig = {
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'create-new-post-card',
timeoutSeconds: 2,
triggers: [
{
universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
type: 'route',
path: '/post-card/create',
httpMethod: 'GET',
isAuthRequired: false,
},
{
universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
type: 'cron',
pattern: '0 0 1 1 *', // Every year 1st of January
},
{
universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
type: 'databaseEvent',
eventName: 'person.created',
},
],
};
handler,
httpRouteTriggerSettings: {
path: '/post-card/create',
httpMethod: 'GET',
isAuthRequired: false,
},
cronTriggerSettings: {
pattern: '0 0 1 1 *',
},
databaseEventTriggerSettings: {
eventName: 'person.created',
},
});
@@ -1,6 +1,6 @@
import { type ApplicationConfig } from 'twenty-sdk';
import { defineApplication } from 'twenty-sdk';
const config: ApplicationConfig = {
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
displayName: 'Hello World',
description: 'A simple hello world app',
@@ -14,6 +14,4 @@ const config: ApplicationConfig = {
},
},
defaultRoleUniversalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
};
export default config;
});
@@ -1,112 +1,89 @@
import { type Note } from '../../generated';
import { defineObject, FieldType } from 'twenty-sdk';
import {
type AddressField,
Field,
FieldType,
type FullNameField,
Object,
OnDeleteAction,
Relation,
RelationType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
const POST_CARD_STATUS = {
DRAFT: 'DRAFT',
SENT: 'SENT',
DELIVERED: 'DELIVERED',
RETURNED: 'RETURNED',
} as const;
enum PostCardStatus {
DRAFT = 'DRAFT',
SENT = 'SENT',
DELIVERED = 'DELIVERED',
RETURNED = 'RETURNED',
}
@Object({
export default defineObject({
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
nameSingular: 'postCard',
namePlural: 'postCards',
labelSingular: 'Post card',
labelPlural: 'Post cards',
description: ' A post card object',
description: 'A post card object',
icon: 'IconMail',
})
export class PostCard {
@Field({
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
type: FieldType.TEXT,
label: 'Content',
description: "Postcard's content",
icon: 'IconAbc',
})
content: string;
@Field({
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
type: FieldType.FULL_NAME,
label: 'Recipient name',
icon: 'IconUser',
})
recipientName: FullNameField;
@Field({
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
type: FieldType.ADDRESS,
label: 'Recipient address',
icon: 'IconHome',
})
recipientAddress: AddressField;
@Field({
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
type: FieldType.SELECT,
label: 'Status',
icon: 'IconSend',
defaultValue: `'${PostCardStatus.DRAFT}'`,
options: [
{
value: PostCardStatus.DRAFT,
label: 'Draft',
position: 0,
color: 'gray',
},
{
value: PostCardStatus.SENT,
label: 'Sent',
position: 1,
color: 'orange',
},
{
value: PostCardStatus.DELIVERED,
label: 'Delivered',
position: 2,
color: 'green',
},
{
value: PostCardStatus.RETURNED,
label: 'Returned',
position: 3,
color: 'orange',
},
],
})
status: PostCardStatus;
@Relation({
universalIdentifier: 'c9e2b4f4-b9ad-4427-9b42-9971b785edfe',
type: RelationType.ONE_TO_MANY,
label: 'Notes',
icon: 'IconComment',
inverseSideTargetUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.note.universalIdentifier,
onDelete: OnDeleteAction.CASCADE,
})
notes: Note[];
@Field({
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
type: FieldType.DATE_TIME,
label: 'Delivered at',
icon: 'IconCheck',
isNullable: true,
defaultValue: null,
})
deliveredAt?: Date;
}
fields: [
{
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
type: FieldType.TEXT,
name: 'content',
label: 'Content',
description: "Postcard's content",
icon: 'IconAbc',
},
{
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
type: FieldType.FULL_NAME,
name: 'recipientName',
label: 'Recipient name',
icon: 'IconUser',
},
{
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
type: FieldType.ADDRESS,
name: 'recipientAddress',
label: 'Recipient address',
icon: 'IconHome',
},
{
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
type: FieldType.SELECT,
name: 'status',
label: 'Status',
icon: 'IconSend',
defaultValue: `'${POST_CARD_STATUS.DRAFT}'`,
options: [
{
id: 'a1b2c3d4-0001-4000-8000-000000000001',
value: POST_CARD_STATUS.DRAFT,
label: 'Draft',
position: 0,
color: 'gray',
},
{
id: 'a1b2c3d4-0002-4000-8000-000000000002',
value: POST_CARD_STATUS.SENT,
label: 'Sent',
position: 1,
color: 'orange',
},
{
id: 'a1b2c3d4-0003-4000-8000-000000000003',
value: POST_CARD_STATUS.DELIVERED,
label: 'Delivered',
position: 2,
color: 'green',
},
{
id: 'a1b2c3d4-0004-4000-8000-000000000004',
value: POST_CARD_STATUS.RETURNED,
label: 'Returned',
position: 3,
color: 'orange',
},
],
},
{
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
type: FieldType.DATE_TIME,
name: 'deliveredAt',
label: 'Delivered at',
icon: 'IconCheck',
isNullable: true,
defaultValue: null,
},
],
});
@@ -1,6 +1,6 @@
import { PermissionFlag, type RoleConfig } from 'twenty-sdk';
import { defineRole, PermissionFlag } from 'twenty-sdk';
export const functionRole: RoleConfig = {
export default defineRole({
universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
label: 'Default function role',
description: 'Default role for function Twenty client',
@@ -14,7 +14,7 @@ export const functionRole: RoleConfig = {
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
objectUniversalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
@@ -23,11 +23,11 @@ export const functionRole: RoleConfig = {
],
fieldPermissions: [
{
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
objectUniversalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
fieldUniversalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
canReadFieldValue: false,
canUpdateFieldValue: false,
},
],
permissionFlags: [PermissionFlag.APPLICATIONS],
};
});
File diff suppressed because it is too large Load Diff
@@ -1,2 +1,37 @@
.yarn/install-state.gz
.env
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn
# codegen
generated
# testing
/coverage
# dev
/dist/
.twenty
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# typescript
*.tsbuildinfo
@@ -2,25 +2,6 @@
Used to manage billing and telemetry of self-hosted instances
## Requirements
- twenty-cli `npm install -g twenty-cli`
- an `apiKey`. Go to `https://twenty.com/settings/api-webhooks` to generate one
## Install to your Twenty workspace
```bash
twenty auth login
twenty app sync
```
## Environment Variables
This application requires the following environment variables to be set:
- `TWENTY_API_URL`: The Twenty instance API URL where selfHostingUser records will be created
- `TWENTY_API_KEY`: API key for authentication (generate at `/settings/api-webhooks`)
## Features
### Telemetry Webhook
@@ -9,26 +9,13 @@
},
"packageManager": "yarn@4.9.2",
"scripts": {
"auth:login": "twenty auth login",
"auth:logout": "twenty auth logout",
"auth:status": "twenty auth status",
"auth:switch": "twenty auth switch",
"auth:list": "twenty auth list",
"app:dev": "twenty app dev",
"app:sync": "twenty app sync",
"entity:add": "twenty entity add",
"function:logs": "twenty function logs",
"function:execute": "twenty function execute",
"app:uninstall": "twenty app uninstall",
"help": "twenty help",
"twenty": "twenty",
"lint": "eslint",
"lint:fix": "eslint --fix"
},
"dependencies": {
"twenty-sdk": "0.3.1"
},
"devDependencies": {
"@types/node": "^24.7.2"
"@types/node": "^24.7.2",
"twenty-sdk": "0.6.2"
},
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/src/constants/schemas/appManifest.schema.json",
"universalIdentifier": "a7070f46-3158-4b40-828f-8e6b1febc233"
@@ -1,19 +0,0 @@
import { defineApp } from 'twenty-sdk';
export default defineApp({
universalIdentifier: '94f7db30-59e5-4b09-a5fe-64cd3d4a65b0',
displayName: 'Self Hosting',
description: 'Used to manage billing and telemetry of self-hosted instances',
applicationVariables: {
TWENTY_API_KEY: {
universalIdentifier: 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d',
description: 'Twenty API key for creating selfHostingUser records',
isSecret: true,
},
TWENTY_API_URL: {
universalIdentifier: 'b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e',
description: 'Twenty API URL (e.g., https://api.twenty.com)',
isSecret: false,
},
},
});
@@ -1,18 +0,0 @@
import { FieldType, defineObject } from 'twenty-sdk';
export default defineObject({
universalIdentifier: '06f3fb53-599e-4c6b-9df6-8f731973afd7',
nameSingular: 'selfHostingUser',
namePlural: 'selfHostingUsers',
labelSingular: 'Self Hosting User',
labelPlural: 'Self Hosting Users',
fields: [
{
type: FieldType.EMAILS,
name: 'email',
label: 'Email',
description: 'The email of the self hosting user',
universalIdentifier: 'a4b7892c-431a-4d44-973e-a5481652704f',
},
],
});
@@ -1,132 +0,0 @@
import { defineFunction } from 'twenty-sdk';
import { createClient } from '../../generated';
// TODO: import from twenty-sdk when 0.4.0 is deployed
type ServerlessFunctionEvent<TBody = object> = {
headers: Record<string, string | undefined>;
queryStringParameters: Record<string, string | undefined>;
pathParameters: Record<string, string | undefined>;
body: TBody | null;
isBase64Encoded: boolean;
requestContext: {
http: {
method: string;
path: string;
};
};
};
type TelemetryEventPayload = {
action: string;
timestamp: string;
version: string;
payload: {
userId: string | null;
workspaceId: string | null;
payload?: {
events?: Array<{
userId?: string;
userEmail?: string;
userFirstName?: string;
userLastName?: string;
locale?: string;
serverUrl?: string;
}>;
};
};
};
export const main = async (
params: ServerlessFunctionEvent<TelemetryEventPayload>,
): Promise<{ success: boolean; message: string; error?: string }> => {
try {
const { action, payload } = params.body || {};
if (action !== 'user_signup') {
return {
success: true,
message: `Event type '${action}' ignored`,
};
}
const userEmail =
payload?.payload?.events?.[0]?.userEmail ||
payload?.payload?.events?.[0]?.userId;
if (!userEmail) {
return {
success: false,
message: 'No email found in telemetry event',
error: 'Missing userEmail in payload',
};
}
if (
userEmail.toLowerCase().includes('example') ||
userEmail.toLowerCase().includes('test')
) {
return {
success: true,
message: `Email '${userEmail}' ignored (contains test/example data)`,
};
}
const client = createClient({
url: `${process.env.TWENTY_API_URL}/graphql`,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.TWENTY_API_KEY}`,
},
});
// Create or update selfHostingUser record
const result = await client.mutation({
createSelfHostingUser: {
__args: {
data: {
name:
payload?.payload?.events?.[0]?.userFirstName +
' ' +
payload?.payload?.events?.[0]?.userLastName,
email: {
primaryEmail: userEmail,
additionalEmails: null,
},
},
upsert: true,
},
id: true,
email: {
primaryEmail: true,
},
},
});
return {
success: true,
message: `Self hosting user created/updated: ${result.createSelfHostingUser?.id}`,
};
} catch (error) {
return {
success: false,
message: 'Failed to process telemetry event',
error: error instanceof Error ? error.message : String(error),
};
}
};
export default defineFunction({
universalIdentifier: '10104201-622b-4a5e-9f27-8f2af19b2a3c',
name: 'telemetry-webhook',
timeoutSeconds: 5,
handler: main,
triggers: [
{
universalIdentifier: '7c8e3f5a-9b4c-4d1e-8f2a-1b3c4d5e6f7a',
type: 'route',
path: '/webhook/telemetry',
httpMethod: 'POST',
isAuthRequired: false,
},
],
});
@@ -0,0 +1,10 @@
import { defineApplication } from 'twenty-sdk';
import { UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers.constant';
export default defineApplication({
universalIdentifier: '94f7db30-59e5-4b09-a5fe-64cd3d4a65b0',
displayName: 'Self Hosting',
description: 'Used to manage billing and telemetry of self-hosted instances',
defaultRoleUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.roles.defaultRole.universalIdentifier,
});
@@ -0,0 +1,120 @@
export const UNIVERSAL_IDENTIFIERS = {
objects: {
selfHostingUser: {
universalIdentifier: '06f3fb53-599e-4c6b-9df6-8f731973afd7',
fields: {
name: { universalIdentifier: '682cccbf-9f37-4290-a94c-902c771f61e4' },
email: { universalIdentifier: 'a4b7892c-431a-4d44-973e-a5481652704f' },
personId: {
universalIdentifier: 'b453a43c-1512-48ca-8604-db750ad3ffb8',
},
domain: {
universalIdentifier: '1dfa7d4e-c8f5-4639-b58e-3392a8789f76',
},
userWorkspaceId: {
universalIdentifier: '297a7d6b-e407-4b2d-8c03-8964bc1b7805',
},
userId: {
universalIdentifier: '5c7ba3ce-1473-4e3d-8e7c-31816fcb87d8',
},
locale: {
universalIdentifier: '7b39df37-a22e-4f38-ae77-91cf3ee7c076',
},
serverUrl: {
universalIdentifier: 'f2516b77-2912-4cbb-8838-46ac5a5465d9',
},
serverId: {
universalIdentifier: 'e68a2b15-786d-4e9d-a74d-6d6d577ae721',
},
numberOfEmailsWithSameDomain: {
universalIdentifier: '0bf05db0-6771-4400-91ca-1579ec11e76e',
},
isEnriched: {
universalIdentifier: 'fefe9fd6-23ae-4046-b60b-64d17e9ff7ed',
},
triedToBeEnriched: {
universalIdentifier: 'd32c8cc3-8855-453d-bb7d-9c9c0b3f2128',
},
isPersonalEmail: {
universalIdentifier: 'f4568391-9474-4ed8-8cbb-e36d86e0f5f9',
},
isTwenty: {
universalIdentifier: 'b1acef1f-7c10-47a9-899e-aaca45b36e04',
},
personCity: {
universalIdentifier: 'ca733484-e595-4257-9ca9-9a7802fb8bcb',
},
personCountry: {
universalIdentifier: '18c06357-1b50-4d5b-82cf-1f71f286fbe4',
},
personJobFunction: {
universalIdentifier: '26e7e2c7-ea83-41e0-8c07-1fc2549a3fb4',
},
personJobTitle: {
universalIdentifier: '177908e9-1ca6-4762-9518-0df966d3e9fc',
},
personLinkedIn: {
universalIdentifier: '3515683f-7f9f-4b6d-9b16-614824d277b7',
},
personSeniority: {
universalIdentifier: '8b63855a-5915-4d6a-a6ed-ef7d8f8e5dd1',
},
companyAlexaRank: {
universalIdentifier: '7c61335b-cd4b-4eae-8b02-0db746913e36',
},
companyAnnualRevenue: {
universalIdentifier: 'a2367973-aa12-42c2-9577-fe868f61b83b',
},
companyAnnualRevenuePrinted: {
universalIdentifier: 'bc02b6af-8f48-4fde-920d-1fd3e2a8557b',
},
companyDescription: {
universalIdentifier: 'a9bb622e-56b6-42ba-8b03-17a47d707409',
},
companyEmployees: {
universalIdentifier: '8e1dbc58-d444-470f-b8fe-9eed8da4b59e',
},
companyFoundedYear: {
universalIdentifier: '3cf95527-5064-43ab-bf5e-421eb45fac5f',
},
companyFundingLatestStage: {
universalIdentifier: 'a7dcd92a-6811-490b-a8dd-fad1c19091a1',
},
companyFundingTotalAmount: {
universalIdentifier: '6fca8a11-b49a-4081-a7c9-9646f43ad7aa',
},
companyFundingTotalAmountPrinted: {
universalIdentifier: '0078f0f0-2262-4c74-aaf8-4061c6c8a1f3',
},
companyIndustries: {
universalIdentifier: '6b971b9c-6ef5-4497-989e-f9a7c72720cf',
},
companyIndustry: {
universalIdentifier: 'ab84e651-d35b-4e02-8d69-1740af3e22f7',
},
companyLinkedIn: {
universalIdentifier: '4c44b956-f880-434f-b4cd-854b82076e56',
},
companyName: {
universalIdentifier: '1a25412b-f9ce-4406-ac53-f20d1ab8c5ea',
},
companyTags: {
universalIdentifier: 'ceb64d0b-1203-4c6d-af00-39b668f5f891',
},
companyTech: {
universalIdentifier: '11dd57c3-06bb-4722-bb65-96d0a899ca91',
},
},
},
},
roles: {
defaultRole: {
universalIdentifier: '66972e19-9fdb-4336-87ce-442a17fd179c',
},
},
views: {
selfHostingUserView: {
universalIdentifier: 'e903f0ee-52cb-4537-aca8-8940e30b023d',
},
},
};
@@ -0,0 +1,29 @@
import {
defineField,
FieldType,
RelationType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
import { UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers.constant';
export const SELF_HOSTING_USER_ID_UNIVERSAL_IDENTIFIER =
'9507f244-fdea-47d5-a734-725d4dae43da';
export default defineField({
universalIdentifier: SELF_HOSTING_USER_ID_UNIVERSAL_IDENTIFIER,
name: 'selfHostingUsers',
label: 'Self hosting users',
description: 'Self hosting user related to the person',
type: FieldType.RELATION,
relationTargetFieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personId
.universalIdentifier,
relationTargetObjectMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.universalIdentifier,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
isNullable: true,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
});
@@ -0,0 +1,94 @@
import {
defineLogicFunction,
type DatabaseEventPayload,
type ObjectRecordCreateEvent,
type ObjectRecordUpdateEvent,
} from 'twenty-sdk';
import { SELF_HOSTING_USER_NAME_SINGULAR } from 'src/objects/selfHostingUser.object';
import { type SelfHostingUser } from 'twenty-sdk/generated/core';
import { CoreApiClient } from 'twenty-sdk/generated';
const handler = async (
params: DatabaseEventPayload<
| ObjectRecordCreateEvent<SelfHostingUser>
| ObjectRecordUpdateEvent<SelfHostingUser>
>,
) => {
const [object, action] = params.name.split('.');
if (object !== SELF_HOSTING_USER_NAME_SINGULAR) {
return;
}
if (!['created', 'updated'].includes(action)) {
return;
}
const email = params.properties.after.email?.primaryEmail;
if (!email) {
return;
}
const client = new CoreApiClient();
const { people } = await client.query({
people: {
edges: { node: { id: true } },
__args: {
filter: {
emails: {
primaryEmail: { eq: email },
},
},
},
},
});
let personId = people?.edges[0]?.node?.id;
if (!personId) {
const { createPerson } = await client.mutation({
createPerson: {
__args: {
data: {
name: {
firstName: params.properties.after.name?.firstName,
lastName: params.properties.after.name?.lastName,
},
emails: {
primaryEmail: email,
},
},
},
id: true,
},
});
personId = createPerson?.id;
}
await client.mutation({
updateSelfHostingUser: {
__args: {
id: params.properties.after.id,
data: {
personId,
},
},
id: true,
},
});
};
export default defineLogicFunction({
universalIdentifier: '87f0293a-997a-4c7b-85e2-e77462ccf0c5',
name: 'match-telemetry-event-with-people',
description:
'Matches self hosting users with existing people based on email address',
timeoutSeconds: 10,
handler,
databaseEventTriggerSettings: {
eventName: `${SELF_HOSTING_USER_NAME_SINGULAR}.*`,
},
});
@@ -0,0 +1,136 @@
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/generated';
import { type TelemetryEvent } from 'src/logic-functions/types/telemetry-event.type';
export const main = async (
params: RoutePayload<TelemetryEvent>,
): Promise<{
success: boolean;
message: string;
error?: string;
}> => {
try {
const {
action,
workspaceId,
userWorkspaceId,
userId,
userEmail,
userFirstName,
userLastName,
locale,
serverUrl,
serverId,
} = params.body || {};
if (action !== 'user_signup') {
return {
success: true,
message: `Event type '${action}' ignored`,
};
}
if (!userEmail) {
return {
success: true,
message: 'No email found in telemetry event',
};
}
if (
userEmail.toLowerCase().includes('example') ||
userEmail.toLowerCase().includes('test')
) {
return {
success: true,
message: `Email '${userEmail}' ignored (contains test/example data)`,
};
}
const client = new CoreApiClient();
let existingSelfHostingUserId: string | undefined = undefined;
try {
const { selfHostingUser: existingSelfHostingUser } = await client.query({
selfHostingUser: {
__args: {
filter: {
email: { primaryEmail: { eq: userEmail } },
},
},
id: true,
},
});
existingSelfHostingUserId = existingSelfHostingUser?.id;
} catch {
//
}
if (existingSelfHostingUserId) {
await client.mutation({
updateSelfHostingUser: {
__args: {
id: existingSelfHostingUserId,
data: {
name: { firstName: userFirstName, lastName: userLastName },
email: { primaryEmail: userEmail, additionalEmails: null },
userWorkspaceId,
userId,
locale,
serverUrl,
serverId,
},
},
id: true,
},
});
return {
success: true,
message: `Self hosting user ${existingSelfHostingUserId} updated`,
};
}
const { createSelfHostingUser } = await client.mutation({
createSelfHostingUser: {
__args: {
data: {
name: { firstName: userFirstName, lastName: userLastName },
email: { primaryEmail: userEmail, additionalEmails: null },
workspaceId,
userWorkspaceId,
userId,
locale,
serverUrl,
serverId,
},
},
id: true,
},
});
return {
success: true,
message: `Self hosting user ${createSelfHostingUser?.id} created`,
};
} catch (error) {
return {
success: false,
message: 'Failed to process telemetry event',
error: error instanceof Error ? error.message : String(error),
};
}
};
export default defineLogicFunction({
universalIdentifier: '10104201-622b-4a5e-9f27-8f2af19b2a3c',
name: 'telemetry-webhook',
timeoutSeconds: 10,
handler: main,
httpRouteTriggerSettings: {
path: '/webhook/telemetry',
httpMethod: 'POST',
isAuthRequired: false,
},
});
@@ -0,0 +1,12 @@
export type TelemetryEvent = {
action: string;
workspaceId?: string;
userWorkspaceId?: string;
userId: string;
userEmail?: string;
userFirstName?: string;
userLastName?: string;
locale?: string;
serverUrl: string;
serverId: string;
};
@@ -0,0 +1,11 @@
import { defineNavigationMenuItem } from 'twenty-sdk';
import { UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers.constant';
export default defineNavigationMenuItem({
universalIdentifier: 'fe3aaca4-9eda-4565-b215-5d268fbf8164',
name: 'Self host user',
icon: 'IconList',
position: 1,
viewUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.views.selfHostingUserView.universalIdentifier,
});
@@ -0,0 +1,354 @@
import {
defineObject,
FieldType,
RelationType,
OnDeleteAction,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
import { UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers.constant';
import { SELF_HOSTING_USER_ID_UNIVERSAL_IDENTIFIER } from 'src/fields/self-hosting-user-id';
export const SELF_HOSTING_USER_NAME_SINGULAR = 'selfHostingUser';
export default defineObject({
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.universalIdentifier,
nameSingular: SELF_HOSTING_USER_NAME_SINGULAR,
namePlural: 'selfHostingUsers',
labelSingular: 'Self Hosting User',
labelPlural: 'Self Hosting Users',
fields: [
{
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personId
.universalIdentifier,
name: 'person',
label: 'Person',
description: 'Person matching with the self hosting user',
type: FieldType.RELATION,
relationTargetFieldMetadataUniversalIdentifier:
SELF_HOSTING_USER_ID_UNIVERSAL_IDENTIFIER,
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
isNullable: true,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'personId',
},
},
{
type: FieldType.FULL_NAME,
name: 'name',
label: 'Name',
description: 'Name of the self hosting user',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.name
.universalIdentifier,
},
{
type: FieldType.EMAILS,
name: 'email',
label: 'Email',
description: 'The email of the self hosting user',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.email
.universalIdentifier,
},
{
type: FieldType.LINKS,
name: 'domain',
label: 'Domain',
description:
'Domain extracted from the email address (e.g. domain.com / https://domain.com/)',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.domain
.universalIdentifier,
},
{
type: FieldType.UUID,
name: 'userWorkspaceId',
label: 'User workspace Id',
description: 'User workspace id of the self hosting user',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.userWorkspaceId
.universalIdentifier,
},
{
type: FieldType.UUID,
name: 'userId',
label: 'User Id',
description: 'User id of the self hosting user',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.userId
.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'locale',
label: 'Locale',
description: 'Locale of the self hosting user',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.locale
.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'serverUrl',
label: 'Server url',
description: 'Server url of the self hosting user',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.serverUrl
.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'serverId',
label: 'Server id',
description: 'Server id of the self hosting user',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.serverId
.universalIdentifier,
},
{
type: FieldType.NUMBER,
name: 'numberOfEmailsWithSameDomain',
label: 'Number of Emails with Same Domain',
description:
'Aggregated count of self hosting users sharing the same business domain',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
.numberOfEmailsWithSameDomain.universalIdentifier,
},
{
type: FieldType.BOOLEAN,
name: 'isEnriched',
label: 'Is Enriched',
description: 'Whether the record has been enriched',
defaultValue: false,
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.isEnriched
.universalIdentifier,
},
{
type: FieldType.BOOLEAN,
name: 'triedToBeEnriched',
label: 'Tried to Be Enriched',
description: 'Whether an enrichment attempt has been made',
defaultValue: false,
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.triedToBeEnriched
.universalIdentifier,
},
{
type: FieldType.BOOLEAN,
name: 'isPersonalEmail',
label: 'Is Personal Email',
description: 'Whether the email is a personal email address',
defaultValue: true,
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.isPersonalEmail
.universalIdentifier,
},
{
type: FieldType.BOOLEAN,
name: 'isTwenty',
label: 'Is Twenty',
description: 'Whether the user is from Twenty',
defaultValue: false,
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.isTwenty
.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'personCity',
label: 'Person City',
description: 'City of the person',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personCity
.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'personCountry',
label: 'Person Country',
description: 'Country of the person',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personCountry
.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'personJobFunction',
label: 'Person Job Function',
description: 'Job function of the person',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personJobFunction
.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'personJobTitle',
label: 'Person Job Title',
description: 'Job title of the person',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personJobTitle
.universalIdentifier,
},
{
type: FieldType.LINKS,
name: 'personLinkedIn',
label: 'Person LinkedIn',
description: 'LinkedIn profile of the person',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personLinkedIn
.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'personSeniority',
label: 'Person Seniority',
description: 'Seniority level of the person',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personSeniority
.universalIdentifier,
},
{
type: FieldType.NUMBER,
name: 'companyAlexaRank',
label: 'Company Alexa Rank',
description: 'Alexa rank of the company',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyAlexaRank
.universalIdentifier,
},
{
type: FieldType.CURRENCY,
name: 'companyAnnualRevenue',
label: 'Company Annual Revenue',
description: 'Annual revenue of the company',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
.companyAnnualRevenue.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'companyAnnualRevenuePrinted',
label: 'Company Annual Revenue Printed',
description: 'Formatted annual revenue of the company',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
.companyAnnualRevenuePrinted.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'companyDescription',
label: 'Company Description',
description: 'Description of the company',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyDescription
.universalIdentifier,
},
{
type: FieldType.NUMBER,
name: 'companyEmployees',
label: 'Company Employees',
description: 'Number of employees at the company',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyEmployees
.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'companyFoundedYear',
label: 'Company Founded Year',
description: 'Year the company was founded',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyFoundedYear
.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'companyFundingLatestStage',
label: 'Company Funding Latest Stage',
description: 'Latest funding stage of the company',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
.companyFundingLatestStage.universalIdentifier,
},
{
type: FieldType.NUMBER,
name: 'companyFundingTotalAmount',
label: 'Company Funding Total Amount',
description: 'Total funding amount of the company',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
.companyFundingTotalAmount.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'companyFundingTotalAmountPrinted',
label: 'Company Funding Total Amount Printed',
description: 'Formatted total funding amount of the company',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
.companyFundingTotalAmountPrinted.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'companyIndustries',
label: 'Company Industries',
description: 'Industries the company operates in',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyIndustries
.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'companyIndustry',
label: 'Company Industry',
description: 'Primary industry of the company',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyIndustry
.universalIdentifier,
},
{
type: FieldType.LINKS,
name: 'companyLinkedIn',
label: 'Company LinkedIn',
description: 'LinkedIn page of the company',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyLinkedIn
.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'companyName',
label: 'Company Name',
description: 'Name of the company',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyName
.universalIdentifier,
},
{
type: FieldType.ARRAY,
name: 'companyTags',
label: 'Company Tags',
description: 'Tags associated with the company',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyTags
.universalIdentifier,
},
{
type: FieldType.ARRAY,
name: 'companyTech',
label: 'Company Tech',
description: 'Technologies used by the company',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyTech
.universalIdentifier,
},
],
});
@@ -0,0 +1,13 @@
import { defineRole } from 'twenty-sdk';
import { UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers.constant';
export default defineRole({
universalIdentifier:
UNIVERSAL_IDENTIFIERS.roles.defaultRole.universalIdentifier,
label: 'default role',
description: 'Add a description for your role',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: false,
});
@@ -0,0 +1,308 @@
import { defineView } from 'twenty-sdk';
import { UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers.constant';
export default defineView({
universalIdentifier:
UNIVERSAL_IDENTIFIERS.views.selfHostingUserView.universalIdentifier,
name: 'Self hosting users',
objectUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.universalIdentifier,
icon: 'IconList',
position: 0,
fields: [
{
universalIdentifier: '243a2401-cd13-440c-8dcd-649e26df36bc',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.name
.universalIdentifier,
position: 0,
isVisible: true,
size: 150,
},
{
universalIdentifier: 'dfa75ef8-d40d-416f-9f1c-3e86edfa9fce',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.email
.universalIdentifier,
position: 1,
isVisible: true,
size: 150,
},
{
universalIdentifier: '15cc9215-eb48-4487-a92e-a25d8e99702f',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.domain
.universalIdentifier,
position: 2,
isVisible: true,
size: 200,
},
{
universalIdentifier: '0f9e4f63-3664-443a-9f06-8a6cc04c1d90',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personId
.universalIdentifier,
position: 2.1,
isVisible: true,
size: 200,
},
{
universalIdentifier: 'dcf88ae8-e71d-452f-b51e-d88cbc6dd273',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.userWorkspaceId
.universalIdentifier,
position: 3,
isVisible: true,
},
{
universalIdentifier: 'aad70516-936b-41d1-b6c6-961a22299761',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.userId
.universalIdentifier,
position: 4,
isVisible: true,
},
{
universalIdentifier: '8c210eb0-bdda-476e-9f98-42f909872f2a',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.locale
.universalIdentifier,
position: 5,
isVisible: true,
},
{
universalIdentifier: '367abe85-11c4-440f-80a2-663edd6b4231',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.serverUrl
.universalIdentifier,
position: 6,
isVisible: true,
},
{
universalIdentifier: '32c199d6-ebf3-434b-81b4-e2b59a0518b7',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.serverId
.universalIdentifier,
position: 6.1,
isVisible: true,
},
{
universalIdentifier: '924ee786-ab93-44be-9d21-941ff9ffe1ac',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
.numberOfEmailsWithSameDomain.universalIdentifier,
position: 7,
isVisible: true,
},
{
universalIdentifier: '2feadf3d-e251-4356-add8-7fa70dea5401',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.isEnriched
.universalIdentifier,
position: 8,
isVisible: true,
},
{
universalIdentifier: 'de252ae6-c723-4bf7-96cf-d93f5a539f36',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.triedToBeEnriched
.universalIdentifier,
position: 9,
isVisible: true,
},
{
universalIdentifier: 'b121e8e6-b3eb-4f6c-b67e-c7c6d19e1bc5',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.isPersonalEmail
.universalIdentifier,
position: 10,
isVisible: true,
},
{
universalIdentifier: '0ada0bcc-8d6b-4df6-bcc1-78ba14cb04e6',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.isTwenty
.universalIdentifier,
position: 11,
isVisible: true,
},
{
universalIdentifier: 'ec7c8d51-ea63-41bd-9eb1-995835b94218',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personCity
.universalIdentifier,
position: 12,
isVisible: true,
},
{
universalIdentifier: '7522dd84-0d23-48e7-85dd-f0a8d9e275f8',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personCountry
.universalIdentifier,
position: 13,
isVisible: true,
},
{
universalIdentifier: '54191cb9-4d5c-466e-affb-d9ba4adeff87',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personJobFunction
.universalIdentifier,
position: 14,
isVisible: true,
size: 200,
},
{
universalIdentifier: 'ace75fc7-fb20-4e53-a9a2-6a7529befaf0',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personJobTitle
.universalIdentifier,
position: 15,
isVisible: true,
size: 180,
},
{
universalIdentifier: 'a0b42d61-4553-42eb-aca4-327b9bf9f30e',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personLinkedIn
.universalIdentifier,
position: 16,
isVisible: true,
},
{
universalIdentifier: '74bc7dd2-fe53-4ff4-8778-2768f3439571',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personSeniority
.universalIdentifier,
position: 17,
isVisible: true,
size: 180,
},
{
universalIdentifier: '61b34f41-8d56-472d-ab1e-414703c6ca12',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyAlexaRank
.universalIdentifier,
position: 18,
isVisible: true,
},
{
universalIdentifier: '5bb7d36b-6a73-4832-b41e-f67130a4708f',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
.companyAnnualRevenue.universalIdentifier,
position: 19,
isVisible: true,
},
{
universalIdentifier: 'ae6f23ce-006c-41dd-82a1-e9fe7b65bce3',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
.companyAnnualRevenuePrinted.universalIdentifier,
position: 20,
isVisible: true,
size: 250,
},
{
universalIdentifier: 'dd2a4728-a743-43bb-b096-9e7bd5125e56',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyDescription
.universalIdentifier,
position: 21,
isVisible: true,
size: 200,
},
{
universalIdentifier: 'ecca02c9-db2e-41e2-b571-b5db75054b56',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyEmployees
.universalIdentifier,
position: 22,
isVisible: true,
},
{
universalIdentifier: '2e76775b-f8b8-4184-8cd3-72d2b93edaa2',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyFoundedYear
.universalIdentifier,
position: 23,
isVisible: true,
size: 200,
},
{
universalIdentifier: 'a7eb002c-6f0c-48ba-a9eb-247c498ad9bd',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
.companyFundingLatestStage.universalIdentifier,
position: 24,
isVisible: true,
size: 240,
},
{
universalIdentifier: '61be97f6-20da-4b2b-861d-32345e0f9953',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
.companyFundingTotalAmount.universalIdentifier,
position: 25,
isVisible: true,
},
{
universalIdentifier: '55720810-3120-4e76-bcf2-2da9517edbbc',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
.companyFundingTotalAmountPrinted.universalIdentifier,
position: 26,
isVisible: true,
size: 280,
},
{
universalIdentifier: '01e31752-cbc1-499a-8ecf-504dd402d7e2',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyIndustries
.universalIdentifier,
position: 27,
isVisible: true,
size: 200,
},
{
universalIdentifier: '2e1c8b8b-469b-483e-8348-1fe3d1764e17',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyIndustry
.universalIdentifier,
position: 28,
isVisible: true,
size: 180,
},
{
universalIdentifier: '976cc8ae-6cf8-4c30-8da4-5bf61e799893',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyLinkedIn
.universalIdentifier,
position: 29,
isVisible: true,
},
{
universalIdentifier: '5f0776b3-2849-4b9b-82f0-baa38c6d889d',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyName
.universalIdentifier,
position: 30,
isVisible: true,
},
{
universalIdentifier: '86f0397a-2924-4e5c-a610-3c9ad7bb4923',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyTags
.universalIdentifier,
position: 31,
isVisible: true,
},
{
universalIdentifier: '562084f4-1242-4e60-868b-1d9b268a35b0',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyTech
.universalIdentifier,
position: 32,
isVisible: true,
},
],
});
@@ -5,13 +5,14 @@
"declaration": true,
"outDir": "./dist",
"rootDir": ".",
"jsx": "react-jsx",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"importHelpers": true,
"allowUnreachableCode": false,
"strictNullChecks": true,
"strict": true,
"alwaysStrict": true,
"noImplicitAny": true,
"strictBindCallApply": false,
@@ -26,10 +27,5 @@
"~/*": ["./*"]
}
},
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.spec.ts"
]
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
}
File diff suppressed because it is too large Load Diff
@@ -20,11 +20,8 @@ ENV KEYSTATIC_GITHUB_CLIENT_SECRET="<fake build value>"
ENV KEYSTATIC_SECRET="<fake build value>"
ENV NEXT_PUBLIC_KEYSTATIC_GITHUB_APP_SLUG="<fake build value>"
COPY ./packages/twenty-shared /app/packages/twenty-shared
COPY ./packages/twenty-ui /app/packages/twenty-ui
COPY ./packages/twenty-website /app/packages/twenty-website
RUN npx nx build twenty-shared
RUN npx nx build twenty-ui
RUN npx nx build twenty-website
FROM node:24-alpine AS twenty-website
@@ -7,9 +7,9 @@ This document outlines the best practices you should follow when working on the
## State management
React and Recoil handle state management in the codebase.
React and Jotai handle state management in the codebase.
### Use `useRecoilState` to store state
### Use Jotai atoms to store state
It's good practice to create as many atoms as you need to store your state.
@@ -20,13 +20,16 @@ It's better to use extra atoms than trying to be too concise with props drilling
</Warning>
```tsx
export const myAtomState = atom({
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
export const myAtomState = createAtomState<string>({
key: 'myAtomState',
default: 'default value',
defaultValue: 'default value',
});
export const MyComponent = () => {
const [myAtom, setMyAtom] = useRecoilState(myAtomState);
const [myAtom, setMyAtom] = useAtomState(myAtomState);
return (
<div>
@@ -43,7 +46,7 @@ export const MyComponent = () => {
Avoid using `useRef` to store state.
If you want to store state, you should use `useState` or `useRecoilState`.
If you want to store state, you should use `useState` or Jotai atoms with `useAtomState`.
See [how to manage re-renders](#managing-re-renders) if you feel like you need `useRef` to prevent some re-renders from happening.
@@ -83,8 +86,8 @@ You can apply the same for data fetching logic, with Apollo hooks.
// ❌ Bad, will cause re-renders even if data is not changing,
// because useEffect needs to be re-evaluated
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -96,9 +99,7 @@ export const PageComponent = () => {
};
export const App = () => (
<RecoilRoot>
<PageComponent />
</RecoilRoot>
<PageComponent />
);
```
@@ -106,14 +107,14 @@ export const App = () => (
// ✅ Good, will not cause re-renders if data is not changing,
// because useEffect is re-evaluated in another sibling component
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [data, setData] = useAtomState(dataState);
return <div>{data}</div>;
};
export const PageData = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -125,16 +126,16 @@ export const PageData = () => {
};
export const App = () => (
<RecoilRoot>
<>
<PageData />
<PageComponent />
</RecoilRoot>
</>
);
```
### Use recoil family states and recoil family selectors
### Use atom family states and selectors
Recoil family states and selectors are a great way to avoid re-renders.
Atom family states and selectors are a great way to avoid re-renders.
They are useful when you need to store a list of items.
@@ -83,9 +83,9 @@ See [Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks) for more de
### States
Contains the state management logic. [RecoilJS](https://recoiljs.org) handles this.
Contains the state management logic. [Jotai](https://jotai.org) handles this.
- Selectors: See [RecoilJS Selectors](https://recoiljs.org/docs/basic-tutorial/selectors) for more details.
- Selectors: Derived atoms (using `createAtomSelector`) compute values from other atoms and are automatically memoized.
React's built-in state management still handles state within a component.
@@ -52,7 +52,7 @@ The project has a clean and simple stack, with minimal boilerplate code.
- [React](https://react.dev/)
- [Apollo](https://www.apollographql.com/docs/)
- [GraphQL Codegen](https://the-guild.dev/graphql/codegen)
- [Recoil](https://recoiljs.org/docs/introduction/core-concepts)
- [Jotai](https://jotai.org/)
- [TypeScript](https://www.typescriptlang.org/)
**Testing**
@@ -76,7 +76,7 @@ To avoid unnecessary [re-renders](/developers/contribute/capabilities/frontend-d
### State Management
[Recoil](https://recoiljs.org/docs/introduction/core-concepts) handles state management.
[Jotai](https://jotai.org/) handles state management.
See [best practices](/developers/contribute/capabilities/frontend-development/best-practices-front#state-management) for more information on state management.
@@ -159,7 +159,7 @@ export enum PageHotkeyScope {
}
```
Internally, the currently selected scope is stored in a Recoil state that is shared across the application :
Internally, the currently selected scope is stored in a Jotai atom that is shared across the application :
```tsx
export const currentHotkeyScopeState = createState<HotkeyScope>({
@@ -168,10 +168,10 @@ export const currentHotkeyScopeState = createState<HotkeyScope>({
});
```
But this Recoil state should never be handled manually ! We'll see how to use it in the next section.
But this atom should never be handled manually ! We'll see how to use it in the next section.
## How is it working internally?
We made a thin wrapper on top of [react-hotkeys-hook](https://react-hotkeys-hook.vercel.app/docs/intro) that makes it more performant and avoids unnecessary re-renders.
We also create a Recoil state to handle the hotkey scope state and make it available everywhere in the application.
We also create a Jotai atom to handle the hotkey scope state and make it available everywhere in the application.
@@ -194,7 +194,7 @@ setHotkeyScopeAndMemorizePreviousScope(
### Use StyledComponents
Style the components with [styled-components](https://emotion.sh/docs/styled).
Style the components with [Linaria styled](https://github.com/callstack/linaria).
```tsx
// ❌ Bad
@@ -14,6 +14,7 @@ Apps let you build and manage Twenty customizations **as code**. Instead of conf
**What you can do today:**
- Define custom objects and fields as code (managed data model)
- Build logic functions with custom triggers
- Define skills for AI agents
- Deploy the same app across multiple workspaces
## Prerequisites
@@ -30,13 +31,6 @@ Create a new app using the official scaffolder, then authenticate and start deve
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# If you don't use yarn@4
corepack enable
yarn install
# Authenticate using your API key (you'll be prompted)
yarn twenty auth:login
# Start dev mode: automatically syncs local changes to your workspace
yarn twenty app:dev
```
@@ -44,7 +38,7 @@ yarn twenty app:dev
The scaffolder supports three modes for controlling which example files are included:
```bash filename="Terminal"
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item)
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill)
npx create-twenty-app@latest my-app
# Minimal: only core files (application-config.ts and default-role.ts)
@@ -66,6 +60,9 @@ yarn twenty function:logs
# Execute a function by name
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Execute the pre-install function
yarn twenty function:execute --preInstall
# Execute the post-install function
yarn twenty function:execute --postInstall
@@ -85,7 +82,7 @@ When you run `npx create-twenty-app@latest my-twenty-app`, the scaffolder:
- Copies a minimal base application into `my-twenty-app/`
- Adds a local `twenty-sdk` dependency and Yarn 4 configuration
- Creates config files and scripts wired to the `twenty` CLI
- Generates core files (application config, default function role, post-install function) plus example files based on the scaffolding mode
- Generates core files (application config, default function role, pre-install and post-install functions) plus example files based on the scaffolding mode
A freshly scaffolded app with the default `--exhaustive` mode looks like this:
@@ -112,16 +109,19 @@ my-twenty-app/
│ └── example-field.ts # Example standalone field definition
├── logic-functions/
│ ├── hello-world.ts # Example logic function
│ ├── pre-install.ts # Pre-install logic function
│ └── post-install.ts # Post-install logic function
├── front-components/
│ └── hello-world.tsx # Example front component
├── views/
│ └── example-view.ts # Example saved view definition
── navigation-menu-items/
└── example-navigation-menu-item.ts # Example sidebar navigation link
── navigation-menu-items/
└── example-navigation-menu-item.ts # Example sidebar navigation link
└── skills/
└── example-skill.ts # Example AI agent skill definition
```
With `--minimal`, only the core files are created (`application-config.ts`, `roles/default-role.ts`, and `logic-functions/post-install.ts`). With `--interactive`, you choose which example files to include.
With `--minimal`, only the core files are created (`application-config.ts`, `roles/default-role.ts`, `logic-functions/pre-install.ts`, and `logic-functions/post-install.ts`). With `--interactive`, you choose which example files to include.
At a high level:
@@ -142,11 +142,14 @@ The SDK detects entities by parsing your TypeScript files for **`export default
|-----------------|-------------|
| `defineObject()` | Custom object definitions |
| `defineLogicFunction()` | Logic function definitions |
| `definePreInstallLogicFunction()` | Pre-install logic function (runs before installation) |
| `definePostInstallLogicFunction()` | Post-install logic function (runs after installation) |
| `defineFrontComponent()` | Front component definitions |
| `defineRole()` | Role definitions |
| `defineField()` | Field extensions for existing objects |
| `defineView()` | Saved view definitions |
| `defineNavigationMenuItem()` | Navigation menu item definitions |
| `defineSkill()` | AI agent skill definitions |
<Note>
**File naming is flexible.** Entity detection is AST-based — the SDK scans your source files for the `export default define<Entity>({...})` pattern. You can organize your files and folders however you like. Grouping by entity type (e.g., `logic-functions/`, `roles/`) is just a convention for code organization, not a requirement.
@@ -166,8 +169,8 @@ export default defineObject({
Later commands will add more files and folders:
- `yarn twenty app:dev` will auto-generate a typed API client in `node_modules/twenty-sdk/generated` (typed Twenty client + workspace types).
- `yarn twenty entity:add` will add entity definition files under `src/` for your custom objects, functions, front components, or roles.
- `yarn twenty app:dev` will auto-generate two typed API clients in `node_modules/twenty-sdk/generated`: `CoreApiClient` (for workspace data via `/graphql`) and `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`).
- `yarn twenty entity:add` will add entity definition files under `src/` for your custom objects, functions, front components, roles, skills, and more.
## Authentication
@@ -215,11 +218,14 @@ The SDK provides helper functions for defining your app entities. As described i
| `defineApplication()` | Configure application metadata (required, one per app) |
| `defineObject()` | Define custom objects with fields |
| `defineLogicFunction()` | Define logic functions with handlers |
| `definePreInstallLogicFunction()` | Define a pre-install logic function (one per app) |
| `definePostInstallLogicFunction()` | Define a post-install logic function (one per app) |
| `defineFrontComponent()` | Define front components for custom UI |
| `defineRole()` | Configure role permissions and object access |
| `defineField()` | Extend existing objects with additional fields |
| `defineView()` | Define saved views for objects |
| `defineNavigationMenuItem()` | Define sidebar navigation links |
| `defineSkill()` | Define AI agent skills |
These functions validate your configuration at build time and provide IDE autocompletion and type safety.
@@ -320,6 +326,7 @@ Every app has a single `application-config.ts` file that describes:
- **Who the app is**: identifiers, display name, and description.
- **How its functions run**: which role they use for permissions.
- **(Optional) variables**: keyvalue pairs exposed to your functions as environment variables.
- **(Optional) pre-install function**: a logic function that runs before the app is installed.
- **(Optional) post-install function**: a logic function that runs after the app is installed.
Use `defineApplication()` to define your application configuration:
@@ -328,7 +335,6 @@ Use `defineApplication()` to define your application configuration:
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -344,7 +350,6 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -352,7 +357,7 @@ Notes:
- `universalIdentifier` fields are deterministic IDs you own; generate them once and keep them stable across syncs.
- `applicationVariables` become environment variables for your functions (for example, `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`).
- `defaultRoleUniversalIdentifier` must match the role file (see below).
- `postInstallLogicFunctionUniversalIdentifier` (optional) points to a logic function that runs automatically after the app is installed. See [Post-install functions](#post-install-functions).
- Pre-install and post-install functions are automatically detected during the manifest build. See [Pre-install functions](#pre-install-functions) and [Post-install functions](#post-install-functions).
#### Roles and permissions
@@ -425,10 +430,10 @@ Each function file uses `defineLogicFunction()` to export a configuration with a
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
import { CoreApiClient, type Person } from 'twenty-sdk/generated';
const handler = async (params: RoutePayload) => {
const client = new Twenty(); // generated typed client
const client = new CoreApiClient();
const name = 'name' in params.queryStringParameters
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
@@ -485,6 +490,43 @@ Notes:
- The `triggers` array is optional. Functions without triggers can be used as utility functions called by other functions.
- You can mix multiple trigger types in a single function.
### Pre-install functions
A pre-install function is a logic function that runs automatically before your app is installed on a workspace. This is useful for validation tasks, prerequisite checks, or preparing workspace state before the main installation proceeds.
When you scaffold a new app with `create-twenty-app`, a pre-install function is generated for you at `src/logic-functions/pre-install.ts`:
```typescript
// src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
```
You can also manually execute the pre-install function at any time using the CLI:
```bash filename="Terminal"
yarn twenty function:execute --preInstall
```
Key points:
- Pre-install functions use `definePreInstallLogicFunction()` — a specialized variant that omits trigger settings (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
- The handler receives an `InstallLogicFunctionPayload` with `{ previousVersion: string }` — the version of the app that was previously installed (or an empty string for fresh installs).
- Only one pre-install function is allowed per application. The manifest build will error if more than one is detected.
- The function's `universalIdentifier` is automatically set as `preInstallLogicFunctionUniversalIdentifier` on the application manifest during the build — you do not need to reference it in `defineApplication()`.
- The default timeout is set to 300 seconds (5 minutes) to allow for longer preparation tasks.
- Pre-install functions do not need triggers — they are invoked by the platform before installation or manually via `function:execute --preInstall`.
### Post-install functions
A post-install function is a logic function that runs automatically after your app is installed on a workspace. This is useful for one-time setup tasks such as seeding default data, creating initial records, or configuring workspace settings.
@@ -493,16 +535,14 @@ When you scaffold a new app with `create-twenty-app`, a post-install function is
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
export default definePostInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
@@ -510,17 +550,6 @@ export default defineLogicFunction({
});
```
The function is wired into your app by referencing its universal identifier in `application-config.ts`:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
You can also manually execute the post-install function at any time using the CLI:
```bash filename="Terminal"
@@ -528,8 +557,10 @@ yarn twenty function:execute --postInstall
```
Key points:
- Post-install functions are standard logic functions — they use `defineLogicFunction()` like any other function.
- The `postInstallLogicFunctionUniversalIdentifier` field in `defineApplication()` is optional. If omitted, no function runs after installation.
- Post-install functions use `definePostInstallLogicFunction()` — a specialized variant that omits trigger settings (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
- The handler receives an `InstallLogicFunctionPayload` with `{ previousVersion: string }` — the version of the app that was previously installed (or an empty string for fresh installs).
- Only one post-install function is allowed per application. The manifest build will error if more than one is detected.
- The function's `universalIdentifier` is automatically set as `postInstallLogicFunctionUniversalIdentifier` on the application manifest during the build — you do not need to reference it in `defineApplication()`.
- The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
- Post-install functions do not need triggers — they are invoked by the platform during installation or manually via `function:execute --postInstall`.
@@ -637,10 +668,10 @@ To mark a logic function as a tool, set `isTool: true` and provide a `toolInputS
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/generated';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new Twenty();
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
@@ -698,7 +729,7 @@ Key points:
Front components let you build custom React components that render within Twenty's UI. Use `defineFrontComponent()` to define components with built-in validation:
```typescript
// src/my-widget.front-component.tsx
// src/front-components/my-widget.tsx
import { defineFrontComponent } from 'twenty-sdk';
const MyWidget = () => {
@@ -720,27 +751,66 @@ export default defineFrontComponent({
Key points:
- Front components are React components that render in isolated contexts within Twenty.
- Use the `*.front-component.tsx` file suffix for automatic detection.
- The `component` field references your React component.
- Components are built and synced automatically during `yarn twenty app:dev`.
You can create new front components in two ways:
- **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new front component.
- **Manual**: Create a new `*.front-component.tsx` file and use `defineFrontComponent()`.
- **Manual**: Create a new `.tsx` file and use `defineFrontComponent()`, following the same pattern.
### Generated typed client
### Skills
The typed client is auto-generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated` based on your workspace schema. Use it in your functions:
Skills define reusable instructions and capabilities that AI agents can use within your workspace. Use `defineSkill()` to define skills with built-in validation:
```typescript
import Twenty from '~/generated';
// src/skills/example-skill.ts
import { defineSkill } from 'twenty-sdk';
const client = new Twenty();
const { me } = await client.query({ me: { id: true, displayName: true } });
export default defineSkill({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'sales-outreach',
label: 'Sales Outreach',
description: 'Guides the AI agent through a structured sales outreach process',
icon: 'IconBrain',
content: `You are a sales outreach assistant. When reaching out to a prospect:
1. Research the company and recent news
2. Identify the prospect's role and likely pain points
3. Draft a personalized message referencing specific details
4. Keep the tone professional but conversational`,
});
```
The client is re-generated automatically by `yarn twenty app:dev` whenever your objects or fields change.
Key points:
- `name` is a unique identifier string for the skill (kebab-case recommended).
- `label` is the human-readable display name shown in the UI.
- `content` contains the skill instructions — this is the text the AI agent uses.
- `icon` (optional) sets the icon displayed in the UI.
- `description` (optional) provides additional context about the skill's purpose.
You can create new skills in two ways:
- **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new skill.
- **Manual**: Create a new file and use `defineSkill()`, following the same pattern.
### Generated typed clients
Two typed clients are auto-generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated` based on your workspace schema:
- **`CoreApiClient`** — queries the `/graphql` endpoint for workspace data
- **`MetadataApiClient`** — queries the `/metadata` endpoint for workspace configuration and file uploads
```typescript
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
```
Both clients are re-generated automatically by `yarn twenty app:dev` whenever your objects or fields change.
#### Runtime credentials in logic functions
@@ -754,6 +824,51 @@ Notes:
- The API key's permissions are determined by the role referenced in your `application-config.ts` via `defaultRoleUniversalIdentifier`. This is the default role used by logic functions of your application.
- Applications can define roles to follow leastprivilege. Grant only the permissions your functions need, then point `defaultRoleUniversalIdentifier` to that role's universal identifier.
#### Uploading files
The generated `MetadataApiClient` includes an `uploadFile` method for attaching files to file-type fields on your workspace objects. Because standard GraphQL clients do not support multipart file uploads natively, the client provides this dedicated method that implements the [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec) under the hood.
```typescript
import { MetadataApiClient } from 'twenty-sdk/generated';
import * as fs from 'fs';
const metadataClient = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type (defaults to 'application/octet-stream')
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universal identifier
);
console.log(uploadedFile);
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
```
The method signature:
```typescript
uploadFile(
fileBuffer: Buffer,
filename: string,
contentType: string,
fieldMetadataUniversalIdentifier: string,
): Promise<{ id: string; path: string; size: number; createdAt: string; url: string }>
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `fileBuffer` | `Buffer` | The raw file contents |
| `filename` | `string` | The name of the file (used for storage and display) |
| `contentType` | `string` | MIME type of the file (defaults to `application/octet-stream` if omitted) |
| `fieldMetadataUniversalIdentifier` | `string` | The `universalIdentifier` of the file-type field on your object |
Key points:
- The `uploadFile` method is available on `MetadataApiClient` because the upload mutation is resolved by the `/metadata` endpoint.
- It uses the field's `universalIdentifier` (not its workspace-specific ID), so your upload code works across any workspace where your app is installed — consistent with how apps reference fields everywhere else.
- The returned `url` is a signed URL you can use to access the uploaded file.
### Hello World example
@@ -136,7 +136,7 @@ That's expected as user is unauthorized when logged out since its identity is no
Comment out checker plugin in `packages/twenty-ui/vite-config.ts` like in example below
```
plugins: [
react({ jsxImportSource: '@emotion/react' }),
react({ jsxImportSource: 'react' }),
tsconfigPaths(),
svgr(),
dts(dtsConfig),
@@ -11,7 +11,7 @@ title: كائنات مخصصة
## مخطط على مستوى عالي
<div style={{textAlign: 'center'}}>
<img src="/images/docs/server/custom-object-schema.png" alt="مخطط على مستوى عالي" />
<img src="/images/docs/server/custom-object-schema.png" alt="مخطط على مستوى عالي" />
</div>
<br />
@@ -27,7 +27,7 @@ title: كائنات مخصصة
لإضافة كائن مخصص، سيقوم عضو مساحة العمل بالاستعلام عن واجهة برمجة التطبيقات /metadata. يقوم هذا بتحديث البيانات الوصفية وفقًا لذلك ويحسب مخطط GraphQL استنادًا إلى البيانات الوصفية، ويخزنها في ذاكرة GQL للاستخدام لاحقًا.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/server/add-custom-objects.jpeg" alt="استعلام واجهة برمجة التطبيقات /metadata لإضافة الكائنات المخصصة" />
<img src="/images/docs/server/add-custom-objects.jpeg" alt="استعلام واجهة برمجة التطبيقات /metadata لإضافة الكائنات المخصصة" />
</div>
<br />
@@ -35,5 +35,5 @@ title: كائنات مخصصة
لجلب البيانات، تتضمن العملية إجراء استعلامات من خلال نقطة النهاية /graphql وتمريرها من خلال محلل الاستعلام.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/server/custom-object-schema.png" alt="استعلام نقطة النهاية /graphql لجلب البيانات" />
<img src="/images/docs/server/custom-object-schema.png" alt="استعلام نقطة النهاية /graphql لجلب البيانات" />
</div>
@@ -60,9 +60,11 @@ npx nx run twenty-server:command workspace:sync-metadata -f
```
<Warning>
سيؤدي هذا إلى إسقاط قاعدة البيانات وإعادة تشغيل الهجرات والبذور.
تأكد من عمل نسخة احتياطية لأي بيانات تريد الاحتفاظ بها قبل تشغيل هذا الأمر.
سيؤدي هذا إلى إسقاط قاعدة البيانات وإعادة تشغيل الهجرات والبذور.
تأكد من عمل نسخة احتياطية لأي بيانات تريد الاحتفاظ بها قبل تشغيل هذا الأمر.
</Warning>
## "التقنية المستخدمة"
@@ -43,7 +43,9 @@ cp .env.example .env
## التطوير
<Warning>
تأكد من تشغيل `yarn build` قبل أي أمر `zapier`.
تأكد من تشغيل `yarn build` قبل أي أمر `zapier`.
</Warning>
### تجربة
@@ -6,24 +6,29 @@ title: أفضل الممارسات
## إدارة الحالة
تقوم React و Recoil بإدارة الحالة في قاعدة الشيفرة.
تقوم React و Jotai بإدارة الحالة في قاعدة الشيفرة.
### استخدم `useRecoilState` لتخزين الحالة
### استخدم ذرات Jotai لتخزين الحالة
من الجيد إنشاء أكبر عدد ممكن من الذرات لتخزين الحالة الخاصة بك.
<Warning>
من الأفضل استخدام ذرات إضافية بدلاً من محاولة أن تكون مقتضبًا باستخدام تمرير الخصائص.
من الأفضل استخدام ذرات إضافية بدلاً من محاولة أن تكون مقتضبًا باستخدام تمرير الخصائص.
</Warning>
```tsx
export const myAtomState = atom({
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
export const myAtomState = createAtomState<string>({
key: 'myAtomState',
default: 'default value',
defaultValue: 'default value',
});
export const MyComponent = () => {
const [myAtom, setMyAtom] = useRecoilState(myAtomState);
const [myAtom, setMyAtom] = useAtomState(myAtomState);
return (
<div>
@@ -40,7 +45,7 @@ export const MyComponent = () => {
تجنب استخدام `useRef` لتخزين الحالة.
If you want to store state, you should use `useState` or `useRecoilState`.
إذا كنت ترغب في تخزين الحالة، يجب أن تستخدم `useState` أو ذرات Jotai مع `useAtomState`.
انظر [كيفية إدارة إعادة العرض](#managing-re-renders) إذا شعرت أنك بحاجة إلى `useRef` لمنع بعض إعادة العرض من الحدوث.
@@ -77,11 +82,11 @@ If you feel like you need to add a `useEffect` in your root component, you shoul
يمكنك تطبيق نفس الشيء على منطق جلب البيانات، مع الخُطافات Apollo.
```tsx
// ❌ سيّئ، سيتسبب في إعادة التصيير حتى إذا لم تتغير البيانات،
// ❌ سيئ، سيتسبب في إعادة التصيير حتى إذا لم تتغير البيانات،
// لأن useEffect يحتاج إلى إعادة التقييم
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -93,9 +98,7 @@ export const PageComponent = () => {
};
export const App = () => (
<RecoilRoot>
<PageComponent />
</RecoilRoot>
<PageComponent />
);
```
@@ -103,14 +106,14 @@ export const App = () => (
// ✅ جيّد، لن يتسبب في إعادة التصيير إذا لم تتغير البيانات،
// لأن useEffect يُعاد تقييمه في مكوّن شقيق آخر
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [data, setData] = useAtomState(dataState);
return <div>{data}</div>;
};
export const PageData = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -122,16 +125,16 @@ export const PageData = () => {
};
export const App = () => (
<RecoilRoot>
<>
<PageData />
<PageComponent />
</RecoilRoot>
</>
);
```
### استخدم حالات عائلة Recoil ومحددات عائلة Recoil
### استخدم حالات عائلة الذرات والمحددات
حالات عائلة Recoil والمحددات تعتبر طريقة رائعة لتجنب إعادة العرض.
تُعد حالات عائلة الذرات والمحددات طريقة رائعة لتجنّب عمليات إعادة التصيير.
إنها مفيدة عندما تحتاج إلى تخزين قائمة من العناصر.
@@ -82,9 +82,9 @@ module1
### الحالات
تشمل منطق إدارة الحالة. [RecoilJS](https://recoiljs.org) يتولّى ذلك.
تشمل منطق إدارة الحالة. [Jotai](https://jotai.org) يتولّى ذلك.
* المحددات: انظر [RecoilJS Selectors](https://recoiljs.org/docs/basic-tutorial/selectors) لمزيد من التفاصيل.
* المحددات: الذرات المشتقة (باستخدام `createAtomSelector`) تحسب قيماً من ذرات أخرى وتخزن نتائجها في الذاكرة مؤقتاً تلقائياً.
لا تزال إدارة الحالة المدمجة في React تتولّى الحالة داخل المكوّن.
@@ -49,7 +49,7 @@ title: أوامر الواجهة الأمامية
* "[React](https://react.dev/)"
* "[Apollo](https://www.apollographql.com/docs/)"
* "[GraphQL Codegen](https://the-guild.dev/graphql/codegen)"
* "[Recoil](https://recoiljs.org/docs/introduction/core-concepts)"
* [Jotai](https://jotai.org/)
* "[TypeScript](https://www.typescriptlang.org/)"
**الاختبار**
@@ -73,7 +73,7 @@ To avoid unnecessary [re-renders](/l/ar/developers/contribute/capabilities/front
### "إدارة الحالة"
"[Recoil](https://recoiljs.org/docs/introduction/core-concepts) يتعامل مع إدارة الحالة."
[Jotai](https://jotai.org/) يتعامل مع إدارة الحالة.
"راجع [أفضل الممارسات](/l/ar/developers/contribute/capabilities/frontend-development/best-practices-front#state-management) لمزيد من المعلومات حول إدارة الحالة."
@@ -160,7 +160,7 @@ export enum PageHotkeyScope {
}
```
داخليًا، يتم تخزين النطاق المحدد حاليًا في حالة Recoil مشتركة عبر التطبيق:
داخليًا، يتم تخزين النطاق المحدد حاليًا في ذرة Jotai مشتركة عبر التطبيق:
```tsx
export const currentHotkeyScopeState = createState<HotkeyScope>({
@@ -169,10 +169,10 @@ export const currentHotkeyScopeState = createState<HotkeyScope>({
});
```
لكن لا يجب التعامل مع هذه الحالة Recoil يدويًا! سنرى كيف يمكن استخدامها في القسم التالي.
لكن لا يجب التعامل مع هذه الذرة يدويًا! سنرى كيف يمكن استخدامها في القسم التالي.
## كيف يعمل داخليًا؟
قمنا بإنشاء غلاف رقيق فوق [react-hotkeys-hook](https://react-hotkeys-hook.vercel.app/docs/intro) والذي يجعله أكثر كفاءة ويتجنب عمليات إعادة التقديم غير الضرورية.
ونقوم أيضًا بإنشاء حالة Recoil للتعامل مع حالة نطاق المفتاح وجعلها متاحة في جميع أنحاء التطبيق.
ونقوم أيضًا بإنشاء ذرة Jotai للتعامل مع حالة نطاق مفاتيح الاختصار وجعلها متاحة في جميع أنحاء التطبيق.
@@ -194,7 +194,7 @@ setHotkeyScopeAndMemorizePreviousScope(
### استخدام مكونات منسقة
قم بتنسيق المكونات باستخدام [styled-components](https://emotion.sh/docs/styled).
قم بتنسيق المكونات باستخدام [Linaria styled](https://github.com/callstack/linaria).
```tsx
// ❌ سيء
@@ -13,7 +13,9 @@ info: تعرّف على كيفية التعاون باستخدام Figma الخ
تتوفر المميزات الرئيسية فقط للمستخدمين الذين قاموا بتسجيل الدخول، مثل وضع المطور والقدرة على اختيار إطار مخصص.
<Warning>
لن تتمكن من التعاون بفعالية بدون حساب.
لن تتمكن من التعاون بفعالية بدون حساب.
</Warning>
## هيكل فيجما
@@ -6,66 +6,66 @@ description: الدليل للمساهمين (أو المطورين الفضول
## المتطلبات الأساسية
<Tabs>
<Tab title="Linux و MacOS">
قبل أن تتمكن من تثبيت واستخدام Twenty، تأكد من تثبيت الأمور التالية على جهاز الكمبيوتر الخاص بك:
<Tab title="Linux و MacOS">
* [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [Node v24.5.0](https://nodejs.org/en/download)
* [yarn v4](https://yarnpkg.com/getting-started/install)
* [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md)
قبل أن تتمكن من تثبيت واستخدام Twenty، تأكد من تثبيت الأمور التالية على جهاز الكمبيوتر الخاص بك:
* [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [Node v24.5.0](https://nodejs.org/en/download)
* [yarn v4](https://yarnpkg.com/getting-started/install)
* [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md)
<Warning>
لن يعمل `npm` ، يجب عليك استخدام `yarn` بدلًا من ذلك. يأتي Yarn الآن مع Node.js، لذا لست بحاجة إلى تثبيته بشكل منفصل.
عليك فقط تشغيل `corepack enable` لتفعيل Yarn إذا لم تقم بذلك بعد.
</Warning>
</Tab>
<Warning>
لن يعمل `npm` ، يجب عليك استخدام `yarn` بدلًا من ذلك. يأتي Yarn الآن مع Node.js، لذا لست بحاجة إلى تثبيته بشكل منفصل.
عليك فقط تشغيل `corepack enable` لتفعيل Yarn إذا لم تقم بذلك بعد.
</Warning>
<Tab title="ويندوز (WSL)">
1. ثبّت WSL
افتح PowerShell كمسؤول ثم نفّذ:
</Tab>
```powershell
wsl --install
```
<Tab title="ويندوز (WSL)">
يجب أن ترى الآن مطالبة لإعادة تشغيل جهاز الكمبيوتر الخاص بك. إذا لم يكن كذلك، فأعد تشغيله يدويًا.
1. ثبّت WSL
افتح PowerShell كمسؤول ثم نفّذ:
```powershell
wsl --install
```
يجب أن ترى الآن مطالبة لإعادة تشغيل جهاز الكمبيوتر الخاص بك. إذا لم يكن كذلك، فأعد تشغيله يدويًا.
عند إعادة التشغيل، ستُفتح نافذة PowerShell وسيتم تثبيت Ubuntu. قد يستغرق هذا وقتًا طويلاً.
سترى مطالبة لإنشاء اسم المستخدم وكلمة المرور لتثبيت Ubuntu الخاص بك.
عند إعادة التشغيل، ستُفتح نافذة PowerShell وسيتم تثبيت Ubuntu. قد يستغرق هذا وقتًا طويلاً.
سترى مطالبة لإنشاء اسم المستخدم وكلمة المرور لتثبيت Ubuntu الخاص بك.
2. تثبيت وإعداد git
2. تثبيت وإعداد git
```bash
sudo apt-get install git
```bash
sudo apt-get install git
git config --global user.name "Your Name"
git config --global user.name "Your Name"
git config --global user.email "youremail@domain.com"
```
git config --global user.email "youremail@domain.com"
```
3. تثبيت nvm و node.js و yarn
3. تثبيت nvm و node.js و yarn
<Warning>
استخدم `nvm` لتثبيت نسخة `node` الصحيحة. الملف `.nvmrc` يضمن استخدام جميع المشاركين لنفس النسخة.
</Warning>
<Warning>
استخدم `nvm` لتثبيت نسخة `node` الصحيحة. الملف `.nvmrc` يضمن استخدام جميع المشاركين لنفس النسخة.
</Warning>
```bash
sudo apt-get install curl
```bash
sudo apt-get install curl
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
```
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
```
أغلق وأعد فتح برنامجك الطرفي لاستخدام nvm. ثم قم بتشغيل الأوامر التالية.
أغلق وأعد فتح برنامجك الطرفي لاستخدام nvm. ثم قم بتشغيل الأوامر التالية.
```bash
```bash
nvm install # يثبت إصدار node الموصى به
nvm install # يثبت إصدار node الموصى به
nvm use # استخدم إصدار node الموصى به
nvm use # استخدم إصدار node الموصى به
corepack enable
```
corepack enable
```
</Tab>
</Tab>
</Tabs>
---
@@ -75,19 +75,19 @@ description: الدليل للمساهمين (أو المطورين الفضول
في الطرفية الخاصة بك، قم بتشغيل الأمر التالي.
<Tabs>
<Tab title="SSH (موصى به)">
إذا لم تكن قد أعددت مفاتيح SSH بالفعل، يمكنك معرفة كيفية القيام بذلك [هنا](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/about-ssh).
<Tab title="SSH (موصى به)">
إذا لم تكن قد أعددت مفاتيح SSH بالفعل، يمكنك معرفة كيفية القيام بذلك [هنا](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/about-ssh).
```bash
git clone git@github.com:twentyhq/twenty.git
```
</Tab>
<Tab title="HTTPS">
```bash
git clone git@github.com:twentyhq/twenty.git
```
</Tab>
```bash
git clone https://github.com/twentyhq/twenty.git
```
<Tab title="HTTPS">
```bash
git clone https://github.com/twentyhq/twenty.git
```
</Tab>
</Tab>
</Tabs>
## الخطوة 2: انتقل إلى جذر المشروع
@@ -104,20 +104,16 @@ cd twenty
<Tab title="Linux">
**الخيار 1 (المفضل):** لتوفير قاعدة بياناتك محليًا:
استخدم الرابط التالي لتثبيت Postgresql على جهاز Linux الخاص بك: [تثبيت Postgresql](https://www.postgresql.org/download/linux/)
```bash
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
```
ملاحظة: قد تحتاج إلى إضافة `sudo -u postgres` إلى الأمر قبل `psql` لتجنب أخطاء الإذن.
**الخيار 2:** إذا كنت قد قمت بتثبيت docker:
```bash
make -C packages/twenty-docker postgres-on-docker
```
</Tab>
<Tab title="نظام Mac OS">
**الخيار 1 (المفضل):** لتوفير قاعدة بياناتك محليًا مع `brew`:
@@ -129,7 +125,6 @@ cd twenty
```
يمكنك التحقق مما إذا كان خادم PostgreSQL يعمل بتنفيذ:
```bash
brew services list
```
@@ -138,7 +133,6 @@ cd twenty
عبر Homebrew على MacOS. بدلاً من ذلك، فإنه ينشئ دور PostgreSQL يطابق
اسم المستخدم الخاص بك في MacOS (مثل "john").
للتحقق وإنشاء المستخدم `postgres` إذا لزم الأمر، اتبع هذه الخطوات:
```bash
# قم بالاتصال بPostgreSQL
psql postgres
@@ -147,59 +141,48 @@ cd twenty
```
بمجرد أن تكون عند مطالبة psql (postgres=#)، قم بتشغيل:
```bash
# قائمة الأدوار الموجودة في PostgreSQL
\du
```
```bash
# قائمة الأدوار الموجودة في PostgreSQL
\du
```
سترى مخرجات مشابهة ل:
```bash
اسم الأدوار | الخصائص | عضو في
-----------+-------------+-----------
john | مشرف نظام | {}
```
```bash
اسم الأدوار | الخصائص | عضو في
-----------+-------------+-----------
john | مشرف نظام | {}
```
إذا لم ترَ دور `postgres` مدرجًا، انتقل إلى الخطوة التالية.
قم بإنشاء دور `postgres` يدويًا:
```bash
CREATE ROLE postgres WITH SUPERUSER LOGIN;
```
```bash
CREATE ROLE postgres WITH SUPERUSER LOGIN;
```
يقوم هذا بإنشاء دور مشرف نظام باسم `postgres` مع إمكانية تسجيل الدخول.
```bash
اسم الدور | الخصائص | عضو في
-----------+-------------+-----------
postgres | مشرف نظام | {}
john | مشرف نظام | {}
```
```bash
اسم الدور | الخصائص | عضو في
-----------+-------------+-----------
postgres | مشرف نظام | {}
john | مشرف نظام | {}
```
**الخيار 2:** إذا كنت قد قمت بتثبيت docker:
```bash
make -C packages/twenty-docker postgres-on-docker
```
</Tab>
<Tab title="ويندوز (WSL)">
يجب أن تُنفذ جميع الخطوات التالية في تيرمينال WSL (داخل جهازك الافتراضي)
**الخيار 1:** لتوفير قاعدة بيانات Postgresql الخاصة بك محليًا:
استخدم الرابط التالي لتثبيت Postgresql على جهاز Linux الافتراضي الخاص بك: [تثبيت Postgresql](https://www.postgresql.org/download/linux/)
```bash
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
```
ملاحظة: قد تحتاج إلى إضافة `sudo -u postgres` إلى الأمر قبل `psql` لتجنب أخطاء الإذن.
**الخيار 2:** إذا كنت قد قمت بتثبيت docker:
تشغيل Docker على WSL يضيف طبقة إضافية من التعقيد.
استخدم هذا الخيار فقط إذا كنت مرتاحًا مع الخطوات الإضافية المتضمنة، بما في ذلك تشغيل [Docker Desktop WSL2](https://docs.docker.com/desktop/wsl).
```bash
make -C packages/twenty-docker postgres-on-docker
```
@@ -218,35 +201,28 @@ cd twenty
استخدم الرابط التالي لتثبيت Redis على جهاز Linux: [تثبيت Redis](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/)
**الخيار 2:** إذا كنت قد قمت بتثبيت docker:
```bash
make -C packages/twenty-docker redis-on-docker
```
</Tab>
<Tab title="Mac OS">
**الخيار 1 (المفضل):** لتوفير Redis الخاص بك محليًا مع `brew`:
```bash
brew install redis
```
ابدأ خادم redis الخاص بك:
`brew services start redis`
**الخيار 2:** إذا كنت قد قمت بتثبيت docker:
```bash
make -C packages/twenty-docker redis-on-docker
```
</Tab>
<Tab title="ويندوز (WSL)">
**الخيار 1:** لتوفير Redis الخاص بك محليًا:
استخدم الرابط التالي لتثبيت Redis على جهاز Linux الافتراضي الخاص بك: [تثبيت Redis](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/)
**الخيار 2:** إذا كنت قد قمت بتثبيت docker:
```bash
make -C packages/twenty-docker redis-on-docker
```
@@ -267,7 +243,7 @@ cp ./packages/twenty-server/.env.example ./packages/twenty-server/.env
```
<Info>
**وضع تعدد مساحات العمل:** بشكل افتراضي، يعمل Twenty في وضع مساحة عمل واحدة حيث يمكن إنشاء مساحة عمل واحدة فقط. لتمكين دعم تعدد مساحات العمل (مفيد لاختبار الميزات المعتمدة على النطاقات الفرعية)، عيّن `IS_MULTIWORKSPACE_ENABLED=true` في ملف الخادم `.env`. راجع [وضع تعدد مساحات العمل](/l/ar/developers/self-host/capabilities/setup#multi-workspace-mode) للحصول على التفاصيل.
**وضع تعدد مساحات العمل:** بشكل افتراضي، يعمل Twenty في وضع مساحة عمل واحدة حيث يمكن إنشاء مساحة عمل واحدة فقط. لتمكين دعم تعدد مساحات العمل (مفيد لاختبار الميزات المعتمدة على النطاقات الفرعية)، عيّن `IS_MULTIWORKSPACE_ENABLED=true` في ملف الخادم `.env`. راجع [وضع تعدد مساحات العمل](/l/ar/developers/self-host/capabilities/setup#multi-workspace-mode) للحصول على التفاصيل.
</Info>
## الخطوة 6: تثبيت التبعيات
@@ -287,15 +263,12 @@ yarn
اعتمادًا على توزيعة Linux الخاصة بك، قد يتم بدء خادم Redis تلقائيًا.
إذا لم يكن كذلك، تحقق من [دليل تثبيت Redis](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/) لتوزيعتك.
</Tab>
<Tab title="نظام Mac OS">
من المفترض أن يكون Redis قد تم تشغيله بالفعل. إذا لم يكن كذلك، قم بتشغيل:
من المفترض أن يكون Redis قد تم تشغيله بالفعل. إذا لم يكن كذلك، قم بتشغيل:
```bash
brew services start redis
```
</Tab>
<Tab title="ويندوز (WSL)">
اعتمادًا على توزيعة Linux الخاصة بك، قد يتم بدء خادم Redis تلقائيًا.
إذا لم يكن كذلك، تحقق من [دليل تثبيت ريديس](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/) لتوزيعتك.
@@ -25,7 +25,6 @@ Twenty مفتوح المصدر ويرحب بمساهمات المجتمع. سو
<Card title="تقارير الأخطاء والطلبات" icon="bug" href="/l/ar/developers/contribute/capabilities/bug-and-requests">
أبلغ عن المشكلات أو اطلب ميزات
</Card>
<Card title="تطوير الواجهة الأمامية" icon="browser" href="/l/ar/developers/contribute/capabilities/frontend-development">
ساهم في واجهة المستخدم
</Card>
@@ -17,7 +17,7 @@ import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
* **وثائق مخصصة**: يتم إنشاؤها خصيصًا لنموذج بيانات مساحة عملك
<Note>
وثائق واجهة برمجة التطبيقات المخصصة لك متاحة ضمن **الإعدادات → API & Webhooks** بعد إنشاء مفتاح API. نظرًا لأن Twenty تُنشئ واجهات برمجة تطبيقات تتطابق مع نموذج البيانات المخصص لديك، فإن الوثائق فريدة لمساحة عملك.
وثائق واجهة برمجة التطبيقات المخصصة لك متاحة ضمن **الإعدادات → API & Webhooks** بعد إنشاء مفتاح API. نظرًا لأن Twenty تُنشئ واجهات برمجة تطبيقات تتطابق مع نموذج البيانات المخصص لديك، فإن الوثائق فريدة لمساحة عملك.
</Note>
## نوعا واجهات برمجة التطبيقات
@@ -81,7 +81,7 @@ Authorization: Bearer YOUR_API_KEY
<VimeoEmbed videoId="928786722" title="إنشاء مفتاح API" />
<Warning>
يمنح مفتاح API الخاص بك الوصول إلى بيانات حساسة. لا تشاركه مع خدمات غير موثوقة. إذا تم اختراقه، عطّلْه فوراً وأنشئ مفتاحاً جديداً.
يمنح مفتاح API الخاص بك الوصول إلى بيانات حساسة. لا تشاركه مع خدمات غير موثوقة. إذا تم اختراقه، عطّلْه فوراً وأنشئ مفتاحاً جديداً.
</Warning>
### تعيين دور لمفتاح API
@@ -143,5 +143,5 @@ Authorization: Bearer YOUR_API_KEY
| **حجم الدفعة** | 60 سجل لكل استدعاء |
<Tip>
استخدم عمليات الدفعات لزيادة الإنتاجية — عالج ما يصل إلى 60 سجلًا في استدعاء API واحد بدلاً من إجراء طلبات فردية.
استخدم عمليات الدفعات لزيادة الإنتاجية — عالج ما يصل إلى 60 سجلًا في استدعاء API واحد بدلاً من إجراء طلبات فردية.
</Tip>

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