Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code f2428dfa8a chore: improve monitoring for NULL bytes in REST API input cause PostgreSQL reje
### Monitoring Fix: Include PostgreSQL error code in Sentry exception messages

**File:** `http-exception-handler.service.ts`

For `PostgresException` instances that still reach the 500 handler (non-22xxx errors like constraint violations, syntax errors, etc.), the exception message now includes the PostgreSQL error code: `"Data validation error. (PG error code: 42703)"`.

**Before:** Sentry showed only `InternalServerErrorException: Data validation error.` with no way to distinguish between different PostgreSQL error types without reading source code.

**After:** The PG error code is embedded in the Sentry error title, enabling:
- Better Sentry issue grouping by error code
- Faster triage — engineers can immediately identify the error class (23xxx = constraint, 42xxx = syntax/schema, 53xxx = resources)
- Easier correlation with PostgreSQL documentation
2026-03-06 13:06:12 +00:00
Sonarly Claude Code f70340a2f5 NULL bytes in REST API input cause PostgreSQL rejection, misclassified as 500
https://sonarly.com/issue/3840?type=bug

The REST API PATCH endpoint passes unsanitized string data containing \u0000 (NULL byte) characters to PostgreSQL, which rejects them. The error is then misclassified as a 500 Internal Server Error instead of 400 Bad Request.

Fix: ### Root Cause Fix: Classify PostgreSQL data exceptions as client input errors

**File:** `compute-twenty-orm-exception.ts`

Replaced the narrow check for only `INVALID_TEXT_REPRESENTATION` (PostgreSQL code `22P02`) with a broader check for all class-22 (Data Exception) errors using `errorCode.startsWith('22')`.

**Before:** Only `22P02` was classified as `INVALID_INPUT` → 400 Bad Request. All other 22xxx errors (including `22021` CHARACTER_NOT_IN_REPERTOIRE and `22P05` UNTRANSLATABLE_CHARACTER triggered by `\u0000` null bytes) fell through to the catch-all `PostgresException` → 500 Internal Server Error.

**After:** All PostgreSQL class-22 errors are now correctly classified as `TwentyORMException` with `INVALID_INPUT` code. This flows through:
1. `workspaceQueryRunnerRestApiExceptionHandler` → `BadRequestException` (400)
2. `HttpExceptionHandlerService` → returns 400 with the actual PostgreSQL error message

This is correct because [all 22xxx codes](https://www.postgresql.org/docs/current/errcodes-appendix.html) are "Data Exception" errors caused by invalid input data, not server failures. The UNIQUE_VIOLATION (23505) check runs before this, so it's unaffected.
2026-03-06 13:06:12 +00:00
Charles BochetandGitHub d37ed7e07c Optimize merge queue to only run E2E and integrate prettier into lint (#18459)
## Summary

- **Merge queue optimization**: Created a dedicated
`ci-merge-queue.yaml` workflow that only runs Playwright E2E tests on
`ubuntu-latest-8-cores`. Removed `merge_group` trigger from all 7
existing CI workflows (front, server, shared, website, sdk, zapier,
docker-compose). The merge queue goes from ~30+ parallel jobs to a
single focused E2E job.
- **Label-based merge queue simulation**: Added `run-merge-queue` label
support so developers can trigger the exact merge queue E2E pipeline on
any open PR before it enters the queue.
- **Prettier in lint**: Chained `prettier --check` into `lint` and
`prettier --write` into `lint --configuration=fix` across `nx.json`
defaults, `twenty-front`, and `twenty-server`. Prettier formatting
errors are now caught by `lint` and fixed by `lint:fix` /
`lint:diff-with-main --configuration=fix`.

## After merge (manual repo settings)

Update GitHub branch protection required status checks:
1. Remove old per-workflow merge queue checks (`ci-front-status-check`,
`ci-e2e-status-check`, `ci-server-status-check`, etc.)
2. Add `ci-merge-queue-status-check` as the required check for the merge
queue
2026-03-06 13:20:57 +01:00
WeikoandGitHub d825ac06dd fix server production build (#18458)
## Context
- fuse.js was imported in navigate-app-tool.ts but only declared in the
root package.json (has been like this for months but for the first time
being used in the server)
- This works locally due to yarn hoisting, but breaks in Docker
production builds

```bash
Error: Cannot find module 'fuse.js'
Require stack:
- /app/packages/twenty-server/dist/engine/core-modules/tool/tools/navigate-tool/navigate-app-tool.js
- /app/packages/twenty-server/dist/engine/core-modules/tool-provider/providers/action-tool.provider.js
- /app/packages/twenty-server/dist/engine/core-modules/tool-provider/tool-provider.module.js
- /app/packages/twenty-server/dist/engine/metadata-modules/ai/ai-agent-execution/ai-agent-execution.module.js
- /app/packages/twenty-server/dist/engine/metadata-modules/ai/ai-agent-monitor/ai-agent-monitor.module.js
- /app/packages/twenty-server/dist/engine/metadata-modules/metadata-engine.module.js
- /app/packages/twenty-server/dist/engine/api/graphql/core-graphql-api.module.js
- /app/packages/twenty-server/dist/app.module.js
- /app/packages/twenty-server/dist/main.js
    at Module._resolveFilename (node:internal/modules/cjs/loader:1456:15)
    at defaultResolveImpl (node:internal/modules/cjs/loader:1066:19)
    at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1071:22)
    at Module._load (node:internal/modules/cjs/loader:1242:25)
    at wrapModuleLoad (node:internal/modules/cjs/loader:255:19)
    at Module.require (node:internal/modules/cjs/loader:1556:12)
    at require (node:internal/modules/helpers:152:16)
    at Object.<anonymous> (/app/packages/twenty-server/dist/engine/core-modules/tool/tools/navigate-tool/navigate-app-tool.js:13:54)
    at Module._compile (node:internal/modules/cjs/loader:1812:14)
    at Object..js (node:internal/modules/cjs/loader:1943:10) {
  code: 'MODULE_NOT_FOUND',
  ```

Fix: Adding the dependency in twenty-server package.json to make it available in production builds
2026-03-06 11:44:18 +01:00
BugIsGodandGitHub 1c898f36d6 Fix workflow nodes color in dark mode (#18456)
## Why
`ThemeProvider` already exposes `colorScheme` (`'light' | 'dark'`) via
`ThemeContext`, but `WorkflowDiagramCanvasBase` was only
extracting`theme` and never passing `colorMode` to `ReactFlow`.
Fix: #18453 

## Before
<img width="1384" height="745" alt="image"
src="https://github.com/user-attachments/assets/e50db287-c27b-4157-a5c6-59f2d6eab656"
/>

## After

<img width="1413" height="747" alt="image"
src="https://github.com/user-attachments/assets/db98f048-29fd-41bc-a3ae-cec7435dc93b"
/>
2026-03-06 11:06:52 +01:00
Charles BochetandGitHub 364c944ca6 Improve build performance 2x (#18449)
## Summary

Front Before:
<img width="1199" height="670" alt="image"
src="https://github.com/user-attachments/assets/b978f67c-c0a6-49fc-bedd-a443f11c365d"
/>

Front After:
<img width="1199" height="670" alt="image"
src="https://github.com/user-attachments/assets/a4939dbb-a8b4-4c74-978c-daa7f27d00f3"
/>


Server Before:
<img width="1199" height="670" alt="image"
src="https://github.com/user-attachments/assets/da53e97f-ec65-4224-a656-ca41040aef6e"
/>


Server After:
<img width="1199" height="670" alt="image"
src="https://github.com/user-attachments/assets/8cdf3885-f515-4d6c-989f-a421a4e8206c"
/>


### CI Server Pipeline Restructuring
- Split monolithic `server-setup` job into three parallel jobs:
`server-build`, `server-lint-typecheck`, and `server-validation`
- `server-build` only handles build + Nx cache save (~1m vs old 3.5m),
unblocking downstream jobs faster
- `server-lint-typecheck` runs in parallel with no DB dependency
- `server-validation` handles DB setup, migration checks, and GraphQL
generation checks in parallel with tests
- Make `server-test` (unit tests) fully independent — no longer waits
for server-setup, builds its own artifacts
- Increase integration test shards from 8 to 10 for better parallelism
- Expected critical path reduction: ~10m → ~7m (~30% faster)

### CI Front Pipeline Improvements
- Use artifact upload/download for storybook build instead of rebuilding
in test shards
- Serve pre-built storybook via `http-server` in test jobs, with
`STORYBOOK_URL` env var
- Update `vitest.config.ts` to use `storybookUrl` when `STORYBOOK_URL`
is set
- Remove redundant `twenty-shared`, `twenty-ui`, `twenty-sdk` builds
from storybook test shards

### Vite Build Optimizations
- Conditionally enable `rollup-plugin-visualizer` behind `ANALYZE=true`
env var (not loaded by default)
- Broaden Istanbul coverage exclusions to skip test files, stories,
mocks, and decorators
- Remove `@tabler/icons-react` alias from twenty-front and storybook
configs
- Bundle `@tabler/icons-react` into twenty-ui instead of treating it as
an external dependency
- Add lazy loading with `React.lazy` + `Suspense` for all page-level
route components in `useCreateAppRouter`
2026-03-06 11:02:26 +01:00
aa062644c8 Fixed scrollbar height issue in Kanban view and adjusted the calendar view to adjust with the new change (#18367)
fix for #18331 

## Issue 
The height of the container for the kanban board and calendar was set
after calculating the offset from the top bar which contains the
filters. The main issue was that it was calculated wrong. To fix this, I
have added flex:1 to ensure that the board and calendar will grow into
the empty space

## Proof of successful change
The video below shows that the scrollbar is accessible now and that the
calendar view is also adjusted to not cause any errors because of the
new change introduced.


https://github.com/user-attachments/assets/7f58342f-6cbf-4d30-878a-ec57f1e6666a

---------

Co-authored-by: Thomas Trompette <thomas.trompette@sfr.fr>
2026-03-06 10:24:49 +01:00
3d7cb4499f i18n - translations (#18454)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-06 08:53:00 +01:00
9d808302aa i18n - docs translations (#18452)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-06 08:52:50 +01:00
Félix MalfaitandGitHub 514d0017ea Refactor application module architecture for clarity and explicitness (#18432)
## Summary

- **Module reorganization**: Moved `ApplicationUpgradeService` and cron
jobs to `application-upgrade/`, `ApplicationSyncService` to
`application-manifest/`, and
`runWorkspaceMigration`/`uninstallApplication` mutations to the manifest
resolver — each module now has a single clear responsibility.
- **Explicit install flow**: Removed implicit `ApplicationEntity`
creation from `ApplicationSyncService`. The install service and dev
resolver now explicitly create the `ApplicationEntity` before syncing.
npm packages are resolved at registration time to extract manifest
metadata (universalIdentifier, name, description, etc.), eliminating the
`reconcileUniversalIdentifier` hack.
- **Better error handling**: Frontend hooks now surface actual server
error messages in snackbars instead of swallowing them. Replaced the
ugly `ConfirmationModal` for transfer ownership with a proper form
modal. Fixed `SettingsAdminTableCard` row height overflow and corrected
the `yarn-engine` asset path.

## Test plan
- [ ] Register an npm package — verify manifest metadata (name,
description, universalIdentifier) is extracted correctly
- [ ] Install a registered npm app on a workspace — verify
ApplicationEntity is created and sync succeeds
- [ ] Test `app:dev` CLI flow — verify local app registration and sync
work
- [ ] Upload a tarball — verify registration and install flow
- [ ] Transfer ownership — verify the new modal UX works
- [ ] Verify error messages appear correctly in snackbars when
operations fail


Made with [Cursor](https://cursor.com)
2026-03-06 08:45:08 +01:00
90cced0e74 i18n - docs translations (#18451)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-06 02:24:32 +01:00
4d0b8a8644 i18n - translations (#18450)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-06 02:23:59 +01:00
Charles BochetandGitHub 9d57bc39e5 Migrate from ESLint to OxLint (#18443)
## Summary

Fully replaces ESLint with OxLint across the entire monorepo:

- **Replaced all ESLint configs** (`eslint.config.mjs`) with OxLint
configs (`.oxlintrc.json`) for every package: `twenty-front`,
`twenty-server`, `twenty-emails`, `twenty-ui`, `twenty-shared`,
`twenty-sdk`, `twenty-zapier`, `twenty-docs`, `twenty-website`,
`twenty-apps/*`, `create-twenty-app`
- **Migrated custom lint rules** from ESLint plugin format to OxLint JS
plugin system (`@oxlint/plugins`), including
`styled-components-prefixed-with-styled`, `no-hardcoded-colors`,
`sort-css-properties-alphabetically`,
`graphql-resolvers-should-be-guarded`,
`rest-api-methods-should-be-guarded`, `max-consts-per-file`, and
Jotai-related rules
- **Migrated custom rule tests** from ESLint `RuleTester` + Jest to
`oxlint/plugins-dev` `RuleTester` + Vitest
- **Removed all ESLint dependencies** from `package.json` files and
regenerated lockfiles
- **Updated Nx targets** (`lint`, `lint:diff-with-main`, `fmt`) in
`nx.json` and per-project `project.json` to use `oxlint` commands with
proper `dependsOn` for plugin builds
- **Updated CI workflows** (`.github/workflows/ci-*.yaml`) — no more
ESLint executor
- **Updated IDE setup**: replaced `dbaeumer.vscode-eslint` with
`oxc.oxc-vscode` extension, configured `source.fixAll.oxc` and
format-on-save with Prettier
- **Replaced all `eslint-disable` comments** with `oxlint-disable`
equivalents across the codebase
- **Updated docs** (`twenty-docs`) to reference OxLint instead of ESLint
- **Renamed** `twenty-eslint-rules` package to `twenty-oxlint-rules`

### Temporarily disabled rules (tracked in `OXLINT_MIGRATION_TODO.md`)

| Rule | Package | Violations | Auto-fixable |
|------|---------|-----------|-------------|
| `twenty/sort-css-properties-alphabetically` | twenty-front | 578 | Yes
|
| `typescript/consistent-type-imports` | twenty-server | 3814 | Yes |
| `twenty/max-consts-per-file` | twenty-server | 94 | No |

### Dropped plugins (no OxLint equivalent)

`eslint-plugin-project-structure`, `lingui/*`, `@stylistic/*`,
`import/order`, `prefer-arrow/prefer-arrow-functions`,
`eslint-plugin-mdx`, `@next/eslint-plugin-next`,
`eslint-plugin-storybook`, `eslint-plugin-react-refresh`. Partial
coverage for `jsx-a11y` and `unused-imports`.

### Additional fixes (pre-existing issues exposed by merge)

- Fixed `EmailThreadPreview.tsx` broken import from main rename
(`useOpenEmailThreadInSidePanel`)
- Restored truthiness guard in `getActivityTargetObjectRecords.ts`
- Fixed `AgentTurnResolver` return types to match entity (virtual
`fileMediaType`/`fileUrl` are resolved via `@ResolveField()`)

## Test plan

- [x] `npx nx lint twenty-front` passes
- [x] `npx nx lint twenty-server` passes
- [x] `npx nx lint twenty-docs` passes
- [x] Custom oxlint rules validated with Vitest: `npx nx test
twenty-oxlint-rules`
- [x] `npx nx typecheck twenty-front` passes
- [x] `npx nx typecheck twenty-server` passes
- [x] CI workflows trigger correctly with `dependsOn:
["twenty-oxlint-rules:build"]`
- [x] IDE linting works with `oxc.oxc-vscode` extension
2026-03-06 01:03:50 +01:00
Charles BochetandGitHub b421efbff7 fix: remove add record on workflow runs/versions (#18448)
## Summary

- Disable manual record creation (add button, + header button, add new
row) for **WorkflowRun** and **WorkflowVersion** objects since these are
system-managed and should not be created manually
- Fix vertical centering of the record table empty state placeholder
(regression from `styled(Component)` refactor in #18430 — the wrapper
lost `height: 100%` / `width: 100%`)

## Test plan

- [ ] Navigate to Workflow Runs index page → empty state should show
centered placeholder **without** "Add a Workflow Run" button
- [ ] Navigate to Workflow Versions index page → empty state should show
centered placeholder **without** "Add a Workflow Version" button
- [ ] Navigate to any other object index page (e.g. People, Companies) →
empty state should still show the "Add a ..." button and be centered
- [ ] Verify the + button in the record table header is hidden for
workflow runs/versions
- [ ] Verify the "Add New" row at the bottom of the table is hidden for
workflow runs/versions
2026-03-05 23:57:34 +01:00
Charles BochetandGitHub e27a8b5107 Fix Workflow layout show page (#18447)
## Summary

Fixes the workflow show page being blank after the `styled(Component)`
removal in #18430.

- PR #18430 replaced `styled(PageBody)` with a plain `div` wrapper
(`StyledPageBodyForDesktopContainer`) around `PageBody`, but the wrapper
defaulted to `display: block`
- `PageBody`'s internal container uses `flex: 1 1 auto` to size itself,
which requires a flex parent — the block wrapper broke height
propagation, causing React Flow's container to have 0 height
- Added `display: flex; flex-direction: column` to both
`StyledPageBodyForDesktopContainer` and
`StyledPageBodyForMobileContainer` to restore the flex chain

## Test plan

- [x] Open a workflow record show page → diagram nodes are visible
- [x] Open a company/person record show page → fields, tabs, and content
render correctly
- [x] Lint and typecheck pass
2026-03-05 23:02:32 +01:00
Charles BochetandGitHub 4797f97a95 fix: vertical alignment of +N More tab overflow button (#18446)
## Summary
- Add `align-items: center` to tab list `StyledContainer` so the
overflow button aligns vertically with tabs
- Remove ineffective `> * { height }` hack from `TabMoreButton` (was
being reset by `all: unset` in `StyledTabButton`)

## Test plan
- Open a record detail page with enough tabs to trigger the "+N More"
overflow
- Verify the overflow button is vertically centered with the visible
tabs
2026-03-05 22:20:14 +01:00
2f3399fd5f i18n - docs translations (#18445)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-05 21:33:59 +01:00
61f9cf9260 i18n - translations (#18442)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-05 20:37:14 +01:00
ef003fb929 fix blocklist (#18332)
- The schema generator marked both the FK scalar and connect relation
input as required for non-nullable `MANY_TO_ONE` relations, but the
resolver rejects when both are provided making create mutations
impossible
- Fixed by making the connect input always optional in create input
types (the FK scalar still enforces the constraint)
- Added `createOne` pre-query hook for blocklist with ownership
validation


https://github.com/user-attachments/assets/aaae83d4-4747-4d16-a87c-8d8cad79d25d

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-05 20:26:04 +01:00
Baptiste DevessierandGitHub 62a634831d feat: create specialized component for header (#18438)
## Before



https://github.com/user-attachments/assets/afb6ff1d-2489-42b8-80ea-8f6dbc032629



## After


https://github.com/user-attachments/assets/6cf87609-bb0f-4bc2-8273-bce2f226aec2
2026-03-05 19:57:46 +01:00
1ec7244d1b i18n - docs translations (#18440)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-05 19:54:32 +01:00
Lucas BordeauandGitHub 13e569eaac Removed z-index dynamic logic for table (#18436)
This PR removes the complex logic that was used to manage z-index
switching to have the hovered cell portal correctly behave when its
borders were overlaping cells ones.

We now have the hovered portal inside a cell, thus removing the need for
a z-index dynamic logic.

The code has been simplified in the parts where the logic was
implemented and the constant that holds the all z indices for the tables
is still needed but with less options.
2026-03-05 19:38:05 +01:00
Charles BochetandGitHub 1affa1e004 chore(front): remove vite-plugin-checker background TS/ESLint checks (#18437)
## Summary

Removes `vite-plugin-checker` and all references to
`VITE_DISABLE_TYPESCRIPT_CHECKER` / `VITE_DISABLE_ESLINT_CHECKER`.

These background checks are no longer needed because our dev experience
now relies on **independent** linters and type-checkers:
- `npx nx lint:diff-with-main twenty-front` for ESLint
- `npx nx typecheck twenty-front` for TypeScript

Running these as separate processes (rather than inside Vite) is faster,
gives cleaner output, and avoids the significant memory overhead that
`vite-plugin-checker` introduces during `vite dev` and `vite build`. The
old env vars to disable them are removed from `vite.config.ts`,
`package.json` scripts, `nx.json`, `.env.example`, and all translated
docs.
2026-03-05 18:55:04 +01:00
Charles BochetandGitHub c53a13417e Remove all styled(Component) patterns in favor of parent wrappers and props (#18430)
## Summary

Eliminates all ~350 `styled(Component)` usages across `twenty-front` and
`twenty-ui` (212 files changed). Each was replaced following these
rules:

- **Margin/layout CSS** (margin, padding, flex, align-self, width) →
wrapped in a `styled.div`/`styled.span` parent container
- **Third-party components** (Link, TextareaAutosize,
ReactPhoneNumberInput, Handle, etc.) → parent container with child CSS
selectors (`> a`, `> textarea`, `> input`, etc.)
- **Intrinsic behavior via existing props** (TableRow
`gridTemplateColumns`, TableCell `color`/`align`) → replaced
`styled(TableRow)` / `styled(TableCell)` with direct prop usage
- **Other visual overrides on twenty-ui components** (Card, Section,
TabList, Button, MenuItem, ScrollWrapper, etc.) → parent wrappers with
`> div` / `> *` child selectors
- **Extending styled.div/span** → merged all CSS into a single
`styled.div`/`styled.span`

Also adds `overflow: hidden` to parent containers wrapping
`ScrollWrapper` so scroll activates correctly with the new wrapper
structure.

### Migration patterns

| Before | After |
|--------|-------|
| `styled(Avatar)` with `margin-right` | `<StyledAvatarContainer><Avatar
/></StyledAvatarContainer>` |
| `styled(Link)` with `text-decoration: none` |
`<StyledLinkContainer><Link /></StyledLinkContainer>` with `> a { ... }`
|
| `styled(TableRow)` with `grid-template-columns` | `<TableRow
gridTemplateColumns="..." />` |
| `styled(TableCell)` with `color` / `align` | `<TableCell color={...}
align="right" />` |
| `styled(Card)` with `margin-top` | `<StyledCardContainer><Card
/></StyledCardContainer>` |
| `styled(TabList)` with `background` |
`<StyledTabListContainer><TabList /></StyledTabListContainer>` with `>
div { ... }` |
| `styled(StyledBase)` extending a `styled.div` | Single merged
`styled.div` with all styles inlined |
2026-03-05 18:16:25 +01:00
Lucas BordeauandGitHub e5e3132ddd Add ESLint rules to disallow jotaiStore and direct atomFamily usage in selectors (#18422)
Introduce two new ESLint rules that prevent the use of `jotaiStore` and
direct calls to `.atomFamily()` or `.selectorFamily()` within component
selector `get` callbacks.

These rules promote cleaner and more reactive code practices.

Fixed file touched by those new rules :
`calendarDayRecordIdsComponentFamilySelector`
2026-03-05 17:58:52 +01:00
Baptiste DevessierandGitHub 4965790ecc Backfill record page layouts for custom objects (#18428) 2026-03-05 17:52:21 +01:00
fc2b1de860 fix: composite field sub-menu not showing in advanced filter (#18395)
## Problem
While working on the IS/IS_NOT filter feature (#15317 ), I found this
problem. So I want to submit a separate pr to fix it at first.
In the advanced filter, clicking on a composite field (Emails, Phones,
Links) was not showing the sub-field selection menu.

  ## Root cause
Related to #18178 (Recoil → Jotai migration).
`AdvancedFilterFieldSelectMenu` was writing composite field states using
`advancedFilterFieldSelectDropdownId` as the instance ID. But the reader
components (`AdvancedFilterFieldSelectDropdownContent`,
`AdvancedFilterSubFieldSelectMenu`) resolve the instance ID from React
context, which has a different value — so they were reading from a
different Jotai atom instance and `isSelectingCompositeField` was always
`false`.

  ## Fix
Remove the 3 explicit instance IDs so the writer uses context, matching
the readers.

 ## Before


https://github.com/user-attachments/assets/dde54077-0eaf-453c-a638-bec6d6fe4d55

## After


https://github.com/user-attachments/assets/01916bcf-0ee8-49ad-bb54-9d8f10571069


Hope I understood it correctly.

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-03-05 17:51:59 +01:00
12257f4cc7 i18n - translations (#18429)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-05 17:51:21 +01:00
Paul RastoinandGitHub 57d8954973 [SDK] Pure ESM (#18427)
# Introduction
While testing the sdk and overall apps in
https://github.com/prastoin/twenty-app-hello-world
Faced a lot of pure `CJS` external dependencies import issue

Replaced all the cjs deps to either esm equivalent or node native
replacement
2026-03-05 17:19:01 +01:00
WeikoandGitHub cfeea43eaf Improve workspace auth context surface (#18164) 2026-03-05 15:13:59 +00:00
nitinandGitHub 5853891b02 refactor!: rename Command Menu page/navigation layer to Side Panel (#18393) 2026-03-05 15:46:31 +01:00
Paul RastoinandGitHub 38ad0820c0 Fix server logs leak (#18423)
# Introduction

Previously the auth jwt stragegy would lod the whole user entity in the
auth user context
On an exception it would completely get logged on the pods


## Security layer
- 0/ Updating the type system ( devxp only though )
- 1/ The jwt auth stragegy only load a specific sub set of the user
entity
- 2/ Sanitizing at the exception log level directly in case of a user
context
- 3/ Sanitizing at the console driver

The last two sanitization could sound a bit redundant though they're
still good fallback to keep in case new path occurs in the cb
2026-03-05 14:40:23 +01:00
Charles BochetandGitHub 647c32ff3e Deprecate runtime theme objects in favor of CSS variables (#18402)
## Summary

- **Eliminate `ICON_SIZES` / `ICON_STROKES` constants**: all icon
dimensions are now resolved at runtime via
`resolveThemeVariableAsNumber(themeCssVariables.icon.size.X)`, ensuring
values always come from computed CSS variables
- **No more consumer imports from `twenty-ui/theme`**: moved
`ColorSchemeContext`, `ColorSchemeProvider`, `ThemeColor`,
`MAIN_COLOR_NAMES`, `getNextThemeColor`, `AnimationDuration` to
`twenty-ui/theme-constants`
- **Remove `ThemeContext` / `ThemeContextProvider` / `ThemeProvider` /
`ThemeType`**: replaced across ~300 files with `themeCssVariables` (for
CSS contexts) or `resolveThemeVariable` / `resolveThemeVariableAsNumber`
(for JS runtime values)
- **Simplify provider chain**: only `ColorSchemeProvider` remains — it
toggles `light`/`dark` class on `document.documentElement` and provides
`colorScheme` via React context
- **Fix pre-existing test failures**: `useIcons.test.ts`
(non-configurable ES module spy) and
`turnRecordFilterGroupIntoGqlOperationFilter.test.ts`
(`Omit<RecordFilter, 'id'>` type mismatch)

### Theme access pattern (before → after)

| Context | Before | After |
|---------|--------|-------|
| CSS (Linaria) | `${({ theme }) => theme.font.color.primary}` |
`${themeCssVariables.font.color.primary}` |
| JS runtime (icon size, animation) | `theme.icon.size.md` /
`ICON_SIZES.md` |
`resolveThemeVariableAsNumber(themeCssVariables.icon.size.md)` |
| Color scheme check | `theme.name === 'dark'` |
`useContext(ColorSchemeContext).colorScheme === 'dark'` |
2026-03-05 14:39:01 +01:00
martmullandGitHub 7293d4c1f8 Fix missing test input values (#18424)
- refactor
- fix issue
2026-03-05 14:36:36 +01:00
martmullandGitHub 1acbf28316 Only update value at creation (#18350)
as title
2026-03-05 13:08:17 +00:00
Charles BochetandGitHub 1decd40eea Remove unecessary queries for aggregate (#18421)
As per title.
2026-03-05 13:24:56 +01:00
1b9d188e4a Added SSE effect for view relations objects (#18386)
This PR adds what is necessary for having SSE working for view relations
: fields, filters, filter groups and sorts.

This should allow to have AI working well while creating views with
detailed filtering and sorting.

## Demo


https://github.com/user-attachments/assets/026c7fb5-8e1a-4498-b7f4-d16993e5a7c4

## Fixes

Also fixed in this PR while working on the filter area : 
- Advanced filter does not update
- Advanced filter sub field selection is broken (due to Jotai migration)
- No view fields when creating a new view
- Error on advanced filter deletion (cascade delete wasn't taken into
account on the frontend)
- Bug advanced filter creation

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-03-05 12:20:33 +01:00
Baptiste DevessierandGitHub 57499342f1 Set widget position's type according to parent tab (#18411)
Fixes workspaces seeded a few weeks ago and containing position=NULL
widgets
2026-03-05 11:48:40 +01:00
ecbc0ac013 i18n - translations (#18419)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-05 11:47:29 +01:00
Abdullah.andGitHub c571473d67 fix: SVGO DoS through entity expansion in DOCTYPE (#18416)
Resolves [Dependabot Alert
604](https://github.com/twentyhq/twenty/security/dependabot/604) and
[Dependabot Alert
605](https://github.com/twentyhq/twenty/security/dependabot/605).
2026-03-05 11:43:17 +01:00
2a82df7073 AI tools to create a demo workspace (#18236)
This PR adds the necessary tool to create a demo workspace with :
relevant custom objects and fields, mock data and a real dashboard with
graph widgets.

It is still a bit under-optimized and slow but it works.

This PR also adds an AI tool that allows to see what happens in real
time, it navigates the app and waits when necessary.

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-03-05 11:39:31 +01:00
Thomas TrompetteandGitHub a2f80d882b Stop catching all workflow errors (#18392)
Steps now throw WorkflowStepExecutorException. Then workflow executor
decides if error should be catch or not.

Since tools are not only used in workflow and these do not throw, we may
still miss errors here.

Workflow jobs now only catch errors to end the workflow run and throw.
2026-03-05 11:24:36 +01:00
Raphaël BosiandGitHub abd9709291 Update Command Menu Item entity (#18391)
Closes https://github.com/twentyhq/core-team-issues/issues/2256
2026-03-05 11:21:56 +01:00
9d4ff7820d i18n - translations (#18415)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-05 11:14:03 +01:00
nitinandGitHub 4cfd738312 Headless action modal (#18270)
https://github.com/user-attachments/assets/809a281f-3c38-41df-99db-e780941acf9f
2026-03-05 11:13:46 +01:00
Félix MalfaitandGitHub 0e89c96170 feat: add npm and tarball app distribution with upgrade mechanism (#18358)
## Summary

- **npm + tarball app distribution**: Apps can be installed from the npm
registry (public or private) or uploaded as `.tar.gz` tarballs, with
`AppRegistrationSourceType` tracking the origin
- **Upgrade mechanism**: `AppUpgradeService` checks for newer versions,
supports rollback for npm-sourced apps, and a cron job runs every 6
hours to update `latestAvailableVersion` on registrations
- **Security hardening**: Tarball extraction uses path traversal
protection, and `enableScripts: false` in `.yarnrc.yml` disables all
lifecycle scripts during `yarn install` to prevent RCE
- **Frontend**: "Install from npm" and "Upload tarball" modals, upgrade
button on app detail page, blue "Update" badge on installed apps table
when a newer version is available
- **Marketplace catalog sync**: Hourly cron job syncs a hardcoded
catalog index into `ApplicationRegistration` entities
- **Integration tests**: Coverage for install, upgrade, tarball upload,
and catalog sync flows

## Backend changes

| Area | Files |
|------|-------|
| Entity & migration | `ApplicationRegistrationEntity` (sourceType,
sourcePackage, latestAvailableVersion), `ApplicationEntity`
(applicationRegistrationId), migration |
| Services | `AppPackageResolverService`, `ApplicationInstallService`,
`AppUpgradeService`, `MarketplaceCatalogSyncService` |
| Cron jobs | `MarketplaceCatalogSyncCronJob` (hourly),
`AppVersionCheckCronJob` (every 6h) |
| REST endpoint | `AppRegistrationUploadController` — tarball upload
with secure extraction |
| Resolver | `MarketplaceResolver` — simplified `installMarketplaceApp`
(removed redundant `sourcePackage` arg) |
| Security | `.yarnrc.yml` — `enableScripts: false` to block postinstall
RCE |

## Frontend changes

| Area | Files |
|------|-------|
| Modals | `SettingsInstallNpmAppModal`, `SettingsUploadTarballModal`,
`SettingsAppModalLayout` |
| Hooks | `useUploadAppTarball`, `useInstallMarketplaceApp` (cleaned up)
|
| Upgrade UI | `SettingsApplicationVersionContainer`,
`SettingsApplicationDetailAboutTab` |
| Badge | `SettingsApplicationTableRow` — blue "Update" tag,
`SettingsApplicationsInstalledTab` — fetches registrations for version
comparison |
| Styling | Migrated to Linaria (matching main) |

## Test plan

- [ ] Install an app from npm via the "Install from npm" modal
- [ ] Upload a `.tar.gz` tarball via the "Upload tarball" modal
- [ ] Verify upgrade badge appears when `latestAvailableVersion >
version`
- [ ] Verify upgrade flow from app detail page
- [ ] Run integration tests: `app-distribution.integration-spec.ts`,
`marketplace-catalog-sync.integration-spec.ts`
- [ ] Verify `enableScripts: false` blocks postinstall scripts during
yarn install


Made with [Cursor](https://cursor.com)
2026-03-05 10:34:08 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Abdullah
bfa50f566e Bump @clickhouse/client from 1.11.0 to 1.18.1 (#18410)
Bumps [@clickhouse/client](https://github.com/ClickHouse/clickhouse-js)
from 1.11.0 to 1.18.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/ClickHouse/clickhouse-js/releases"><code>@​clickhouse/client</code>'s
releases</a>.</em></p>
<blockquote>
<h2>1.18.1</h2>
<h2>Improvements</h2>
<ul>
<li>Setting <code>log.level</code> default value to
<code>ClickHouseLogLevel.WARN</code> instead of
<code>ClickHouseLogLevel.OFF</code> to provide better visibility into
potential issues without overwhelming users with too much information by
default.</li>
</ul>
<pre lang="ts"><code>const client = createClient({
  // ...
  log: {
level: ClickHouseLogLevel.WARN, // default is now
ClickHouseLogLevel.WARN instead of ClickHouseLogLevel.OFF
  },
})
</code></pre>
<ul>
<li>Logging is now lazy, which means that the log messages will only be
constructed if the log level is appropriate for the message. This can
improve performance in cases where constructing the log message is
expensive, and the log level is set to ignore such messages. See
<code>ClickHouseLogLevel</code> enum for the complete list of log
levels. (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/520">#520</a>)</li>
</ul>
<pre lang="ts"><code>const client = createClient({
  // ...
  log: {
level: ClickHouseLogLevel.TRACE, // to log everything available down to
the network level events
  },
})
</code></pre>
<ul>
<li>Enhanced the logging of the HTTP request / socket lifecycle with
additional trace messages and context such as Connection ID (UUID) and
Request ID and Socket ID that embed the connection ID for ease of
tracing the logs of a particular request across the connection
lifecycle. To enable such logs, set the <code>log.level</code> config
option to <code>ClickHouseLogLevel.TRACE</code>. (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/567">#567</a>)</li>
</ul>
<pre
lang="console"><code>[2026-02-25T09:19:13.511Z][TRACE][@clickhouse/client][Connection]
Insert: received 'close' event, 'free' listener removed
Arguments: {
  operation: 'Insert',
  connection_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c',
  query_id: '9dfda627-39a2-41a6-9fc9-8f8716574826',
  request_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c:3',
  socket_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c:2',
  event: 'close'
}
[2026-02-25T09:19:13.502Z][TRACE][@clickhouse/client][Connection] Query:
reusing socket
Arguments: {
  operation: 'Query',
  connection_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c',
  query_id: 'ad0127e8-b1c7-4ed6-9681-c0162f7a0ea9',
  request_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c:4',
  socket_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c:2',
  usage_count: 1
}
</code></pre>
<ul>
<li>A step towards structured logging: the client now passes rich
context to the logger <code>args</code> parameter (e.g.
<code>connection_id</code>, <code>query_id</code>,
<code>request_id</code>, <code>socket_id</code>). (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/576">#576</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/ClickHouse/clickhouse-js/blob/main/CHANGELOG.md"><code>@​clickhouse/client</code>'s
changelog</a>.</em></p>
<blockquote>
<h1>1.18.1</h1>
<h2>Improvements</h2>
<ul>
<li>Setting <code>log.level</code> default value to
<code>ClickHouseLogLevel.WARN</code> instead of
<code>ClickHouseLogLevel.OFF</code> to provide better visibility into
potential issues without overwhelming users with too much information by
default.</li>
</ul>
<pre lang="ts"><code>const client = createClient({
  // ...
  log: {
level: ClickHouseLogLevel.WARN, // default is now
ClickHouseLogLevel.WARN instead of ClickHouseLogLevel.OFF
  },
})
</code></pre>
<ul>
<li>Logging is now lazy, which means that the log messages will only be
constructed if the log level is appropriate for the message. This can
improve performance in cases where constructing the log message is
expensive, and the log level is set to ignore such messages. See
<code>ClickHouseLogLevel</code> enum for the complete list of log
levels. (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/520">#520</a>)</li>
</ul>
<pre lang="ts"><code>const client = createClient({
  // ...
  log: {
level: ClickHouseLogLevel.TRACE, // to log everything available down to
the network level events
  },
})
</code></pre>
<ul>
<li>Enhanced the logging of the HTTP request / socket lifecycle with
additional trace messages and context such as Connection ID (UUID) and
Request ID and Socket ID that embed the connection ID for ease of
tracing the logs of a particular request across the connection
lifecycle. To enable such logs, set the <code>log.level</code> config
option to <code>ClickHouseLogLevel.TRACE</code>. (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/567">#567</a>)</li>
</ul>
<pre
lang="console"><code>[2026-02-25T09:19:13.511Z][TRACE][@clickhouse/client][Connection]
Insert: received 'close' event, 'free' listener removed
Arguments: {
  operation: 'Insert',
  connection_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c',
  query_id: '9dfda627-39a2-41a6-9fc9-8f8716574826',
  request_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c:3',
  socket_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c:2',
  event: 'close'
}
[2026-02-25T09:19:13.502Z][TRACE][@clickhouse/client][Connection] Query:
reusing socket
Arguments: {
  operation: 'Query',
  connection_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c',
  query_id: 'ad0127e8-b1c7-4ed6-9681-c0162f7a0ea9',
  request_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c:4',
  socket_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c:2',
  usage_count: 1
}
</code></pre>
<ul>
<li>A step towards structured logging: the client now passes rich
context to the logger <code>args</code> parameter (e.g.
<code>connection_id</code>, <code>query_id</code>,
<code>request_id</code>, <code>socket_id</code>). (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/576">#576</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/ClickHouse/clickhouse-js/commit/cbdd7bf20904626956e0ff7808d17015813400c1"><code>cbdd7bf</code></a>
Release 1.18.1 (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/590">#590</a>)</li>
<li><a
href="https://github.com/ClickHouse/clickhouse-js/commit/c9f61ebb3a2ec6201f87417e30c4fc4271451ae8"><code>c9f61eb</code></a>
Beta 1.18.0 (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/588">#588</a>)</li>
<li><a
href="https://github.com/ClickHouse/clickhouse-js/commit/d0f67b71ef896d47fc3d8d0942612ced22aa79dc"><code>d0f67b7</code></a>
Split public and internal <code>drainStream</code> and cover with tests
(<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/578">#578</a>)</li>
<li><a
href="https://github.com/ClickHouse/clickhouse-js/commit/535e9b726e328ce8468c159f935c27910731f4bb"><code>535e9b7</code></a>
Remove <code>unsafeLogUnredactedQueries</code> for now (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/580">#580</a>)</li>
<li><a
href="https://github.com/ClickHouse/clickhouse-js/commit/44e73c73019a3956c1fac67ce0f4f170b3a4f19a"><code>44e73c7</code></a>
Default log level to <code>WARN</code> (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/581">#581</a>)</li>
<li><a
href="https://github.com/ClickHouse/clickhouse-js/commit/5146fbc13e5c23d08e2cc5773bea0adf58d83a5c"><code>5146fbc</code></a>
Focus AI on security and API stability (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/579">#579</a>)</li>
<li><a
href="https://github.com/ClickHouse/clickhouse-js/commit/b7b1d8d7ffe9b6786c9e883379d3edc5a5ed5c58"><code>b7b1d8d</code></a>
Trivial E2E test against <code>beta</code> (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/577">#577</a>)</li>
<li><a
href="https://github.com/ClickHouse/clickhouse-js/commit/761e29ebb5d1bd7a107d2535b385b6565b7120f5"><code>761e29e</code></a>
Structured logs, part 1 (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/576">#576</a>)</li>
<li><a
href="https://github.com/ClickHouse/clickhouse-js/commit/fd23dd7fc9e91ff810a7bb45984a24682ba25482"><code>fd23dd7</code></a>
Provide more context in logs for connection and request handling (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/567">#567</a>)</li>
<li><a
href="https://github.com/ClickHouse/clickhouse-js/commit/a7866e72e356244cae9d20d9fef38a6aafe68ba8"><code>a7866e7</code></a>
Adjusting CI DevX (<a
href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/574">#574</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/ClickHouse/clickhouse-js/compare/1.11.0...1.18.1">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~4b819b88c84b">4b819b88c84b</a>, a new
releaser for <code>@​clickhouse/client</code> since your current
version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@clickhouse/client&package-manager=npm_and_yarn&previous-version=1.11.0&new-version=1.18.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>
Co-authored-by: Abdullah <125115953+mabdullahabaid@users.noreply.github.com>
2026-03-05 10:11:32 +01:00
Abdullah.andGitHub 338a38682d feat: upgrade nx to latest (#18404)
Upgraded NX to resolve some dependabot alerts caused by transitive
dependencies, but after the upgrade, it appears those transitive
dependency issues were not fixed by NX in the first place.

Creating this PR with the upgrade regardless to avoid wasted work. Used
`npx nx@latest migrate latest` from the documentation to automate the
upgrade and it bumped all the dependencies changed in `package.json` for
compatibility - `react-router-dom` and `swc` ones too.

Ran tests, ran builds, started the development server and used the
application - everything looks good after the upgrade.
2026-03-05 09:36:33 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Abdullah
6a2e0182ab Bump @blocknote/server-util from 0.47.0 to 0.47.1 (#18408)
Bumps
[@blocknote/server-util](https://github.com/TypeCellOS/BlockNote/tree/HEAD/packages/server-util)
from 0.47.0 to 0.47.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/TypeCellOS/BlockNote/releases"><code>@​blocknote/server-util</code>'s
releases</a>.</em></p>
<blockquote>
<h2>v0.47.1</h2>
<h2>0.47.1 (2026-03-02)</h2>
<h3>🩹 Fixes</h3>
<ul>
<li>typeerror cannot read properties of undefined (<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2522">#2522</a>)</li>
<li>handle more delete key cases (<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2126">#2126</a>)</li>
<li>add delay for <code>data-active</code> in collab cursors (<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2383">#2383</a>)</li>
<li>disable slash menu in table content <a
href="https://github.com/TypeCellOS/BlockNote/tree/HEAD/packages/server-util/issues/2408">#2408</a>
(<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2504">#2504</a>,
<a
href="https://redirect.github.com/TypeCellOS/BlockNote/issues/2408">#2408</a>)</li>
<li><strong>ai:</strong> selections broken due to floating-ui focus
manager (<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2527">#2527</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Matthew Lipski <a
href="https://github.com/matthewlipski"><code>@​matthewlipski</code></a></li>
<li>Nick Perez</li>
<li>Yousef</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/TypeCellOS/BlockNote/blob/main/CHANGELOG.md"><code>@​blocknote/server-util</code>'s
changelog</a>.</em></p>
<blockquote>
<h2>0.47.1 (2026-03-02)</h2>
<h3>🩹 Fixes</h3>
<ul>
<li>typeerror cannot read properties of undefined (<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2522">#2522</a>)</li>
<li>handle more delete key cases (<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2126">#2126</a>)</li>
<li>add delay for <code>data-active</code> in collab cursors (<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2383">#2383</a>)</li>
<li>disable slash menu in table content <a
href="https://github.com/TypeCellOS/BlockNote/tree/HEAD/packages/server-util/issues/2408">#2408</a>
(<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2504">#2504</a>,
<a
href="https://redirect.github.com/TypeCellOS/BlockNote/issues/2408">#2408</a>)</li>
<li><strong>ai:</strong> selections broken due to floating-ui focus
manager (<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2527">#2527</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Matthew Lipski <a
href="https://github.com/matthewlipski"><code>@​matthewlipski</code></a></li>
<li>Nick Perez</li>
<li>Yousef</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/TypeCellOS/BlockNote/commit/d5d056fe3d5362e73fb72e3e3bf1f839aee3e875"><code>d5d056f</code></a>
chore(release): publish 0.47.1</li>
<li>See full diff in <a
href="https://github.com/TypeCellOS/BlockNote/commits/v0.47.1/packages/server-util">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@blocknote/server-util&package-manager=npm_and_yarn&previous-version=0.47.0&new-version=0.47.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>
Co-authored-by: Abdullah <125115953+mabdullahabaid@users.noreply.github.com>
2026-03-05 09:22:06 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Abdullah
72086fe111 Bump @dagrejs/dagre from 1.1.3 to 1.1.8 (#18409)
Bumps [@dagrejs/dagre](https://github.com/dagrejs/dagre) from 1.1.3 to
1.1.8.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/dagrejs/dagre/commit/7e4d15f191678f7f05f3c86d9071a193230e7e00"><code>7e4d15f</code></a>
Building for release</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/d3908e2c13148c9143db585accc10ae0b6634657"><code>d3908e2</code></a>
Bumping version</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/ce295f8e073c4fe96c9e36ecf08ae2940e5e6a10"><code>ce295f8</code></a>
Build for release</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/b64b9057726eee17f24f73579ba0668527276448"><code>b64b905</code></a>
Bumping the version</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/de169d24c13d06c1e9c560f4f4f8f98650109b94"><code>de169d2</code></a>
Merge pull request <a
href="https://redirect.github.com/dagrejs/dagre/issues/481">#481</a>
from Nathan-Fenner/nf/improve-network-simplex-perform...</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/065e0d8374f4c1c35a7cb4b84df37aaa31598d86"><code>065e0d8</code></a>
improve performance of graph node ranking</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/00d3178d671e49de9c032e3abd281dc9f2739e73"><code>00d3178</code></a>
Typo</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/3982a69d2b323b06aa969a4ec09829d37fe6e7bd"><code>3982a69</code></a>
Bump version and set as pre-release</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/1339f5516508dba0cbcc4ef1c0587e7384bec23d"><code>1339f55</code></a>
Building for release</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/9459f01bc815f16b87db727821d8401acbad2cd3"><code>9459f01</code></a>
Bumping the version</li>
<li>Additional commits viewable in <a
href="https://github.com/dagrejs/dagre/compare/v1.1.3...v1.1.8">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

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


</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Abdullah <125115953+mabdullahabaid@users.noreply.github.com>
2026-03-05 09:21:45 +01:00
228865bd94 i18n - translations (#18398)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-04 23:55:59 +01:00
Abdullah.andGitHub 2493adbb87 fix: minimatch related dependabot alerts (#18396)
This PR fixes a good number of dependabot alerts associated to minimatch
transitive import.

There are a total of 28 alerts, merging this shall confirm which ones
remain open afterwards and make it easier to diagnose.
2026-03-04 23:47:02 +01:00
26f0a416a1 File storage cleaning (#18381)
- Remove feature flag
- Remove legacy methods in file-upload and file-service
- Migrate AI Chat to new file management

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-04 23:46:03 +01:00
Charles BochetandGitHub c41a8e2b23 [DevXP] Simplify twenty-ui theme system: replace auto-generated files with static CSS variables (#18389)
## Summary

Now that Twenty has fully migrated from Emotion to Linaria, the theme
system has been simplified to remove unnecessary complexity that existed
only to support the old runtime injection pattern.

### What changed

- **Deleted** `generateThemeConstants.ts` script and the entire
`generated/` directory — no more auto-generation
- **Added** `theme-light.css` and `theme-dark.css`: static CSS files
with 991 custom properties each, scoped under `.light` and `.dark`
selectors respectively
- **Moved** `themeCssVariables.ts` out of `generated/` and hand-maintain
it as a static `as const` object of `var(--t-*)` references (Linaria can
statically evaluate these at build time)
- **Extracted** numeric constants (`MOBILE_VIEWPORT`, `ICON_SIZES`,
`ICON_STROKES`) into a new `constants.ts` — CSS variables can't be used
in media queries or as numeric icon size props
- **Simplified** `ThemeContextProvider`: removed
`ThemeCssVariableInjectorEffect` entirely; now uses a single
`useLayoutEffect` to toggle `.light`/`.dark` class on `<html>`
- **Added** `class="light"` to `index.html` as default to prevent FOUC
before React hydration

### Why

The previous setup maintained a dual system: JS theme objects
(`THEME_LIGHT`/`THEME_DARK`) used at runtime, plus a generation script
that produced CSS variable entry arrays, which were then injected into
the DOM by `ThemeCssVariableInjectorEffect`. With Linaria, theme values
only need to be CSS custom properties — the JS objects were redundant.
This PR removes ~250 lines of infrastructure while keeping the same
theming capabilities.
2026-03-04 23:30:25 +01:00
neo773GitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>Charles Bochetcubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
4c001778c2 fix google signup edge case (#18365)
Fixes an edge case when a user signs up with Google and the profile
avatar network request times out, we crash instead of creating the user
without an avatar.

Added `axios-retry` to retry max 2 times and if it still fails we
gracefully skip avatar image instead of crashing

Fixes
Sentry TWENTY-SERVER-FDQ
Sonarly https://sonarly.com/issue/6564

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-03-04 22:51:55 +01:00
Thomas TrompetteandGitHub 911a46aa45 Improve workflow perfs (#18376)
Workflow crons take a few minutes to run. Loading each repo takes ~200
to 300ms locally. Adding a lite mode so it takes less than 100ms.
Also doing batch promises.

Finally, cleaning runs timeout when there are too many. Doing batches as
well.
2026-03-04 18:29:12 +01:00
Paul RastoinandGitHub c53d281960 Fix invalid universal identifier format command cache flush (#18385)
# Introduction
Invalidating command impacted metadata and related metadata caches
entries
2026-03-04 17:43:01 +01:00
7f6b270a76 i18n - translations (#18388)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-04 17:32:14 +01:00
Abdul RahmanandGitHub c94657dc0a Navbar AI chats followup (#18336)
Addresses review comments from
[PR#18161](https://github.com/twentyhq/twenty/pull/18161)
2026-03-04 15:15:43 +00:00
aeedcf3353 Enable password reset from app.twenty.com with workspace fallback (#18271)
## Summary
- add a working `Forgot your password?` flow on `app.twenty.com` sign-in
- keep existing workspace-domain reset behavior
- when triggered without workspace context, resolve a workspace from the
user when possible, otherwise fallback to `app.twenty.com` reset URL

## Backend
- make `workspaceId` optional in `emailPasswordResetLink` input
- allow nullable `workspaceId` in password reset token DTO
- update reset token generation to accept optional `workspaceId`
- when missing, resolve first workspace by user membership
- if no workspace is found, persist token with `workspaceId = null`
- send reset links via:
  - workspace URL when `workspaceId` exists
  - app front URL + reset path when `workspaceId` is null

## Frontend
- make reset-link mutation `workspaceId` variable optional
- regenerate/patched generated metadata types accordingly
- add `Forgot your password?` in global password step
- allow reset request without workspace context in
`useHandleResetPassword`
- make reset page auto sign-in domain-aware (`workspace` vs `app`)
- apply design-system spacing above the global forgot-password link
(`theme.spacing(4)`)

## Tests
- extend reset-password service tests for:
  - explicit workspace id
  - inferred workspace when workspace id is missing
  - app-domain fallback when no workspace is found
- extend reset-password hook tests for with/without workspace context
- add focused global form test for forgot-password link rendering/click
behavior

## Product behavior for users with multiple workspaces
- no workspace chooser is shown in this flow
- backend uses the first resolvable workspace membership for the
reset-link domain
- password change remains account-level and works across all workspaces


Feature has been tested and is working 

<img width="3268" height="2106" alt="CleanShot 2026-02-26 at 14 09
14@2x"
src="https://github.com/user-attachments/assets/b5db3bed-f3aa-4d35-b54e-66e4d99141f9"
/>

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-03-04 15:14:38 +00:00
Charles BochetandGitHub eda905f271 [DevXP] Improve Linaria pre-build speed (#18382)
## Summary

This PR improves Linaria/WYW pre-build speed and continues the migration
of `twenty-ui` components away from runtime `ThemeContext` reads toward
static CSS variables and theme constants.

### Linaria/WYW profiling plugin improvements (`twenty-shared`)

- **Babel JIT warmup**: added a `buildStart` warmup step that triggers
WYW's Babel JIT compilation before the real build starts, so the first
real file doesn't pay the cold-start penalty
- **`configResolved` hook**: detects dev vs prod mode and resolves the
correct warmup file path relative to `config.root`
- **Dev-only per-file logging**: slow file warnings are now gated behind
`isDevMode`, keeping production/CI build output clean
- **`closeBundle` summary**: moved the final top-slow-files report to
`closeBundle` for accurate end-of-build reporting
- **Removed noisy progress interval logging** in favor of the warmup log
+ final summary

### Migration from `ThemeContext` to static CSS variables / constants

Across `twenty-ui`, replaced runtime `useTheme()` reads with:
- `themeCssVariables` CSS custom properties (colors, spacing)
- Hard-coded design-system constants (`ICON.size.md` → `16`,
`ICON.stroke.sm` → `1.6`) so components no longer need a React context
at render time — enabling Linaria static extraction

**Components migrated:**
- `Button`, `AnimatedButton`, `LightButton`, `LightIconButton`,
`AnimatedLightIconButton`, `ButtonIcon`, `ButtonSoon`
- `ProgressBar` (Framer Motion width animation → CSS `transition`)
- `Info`, `HorizontalSeparator`, `LinkChip`
- `MenuPicker`, `MenuItemLeftContent`, `MenuItemIconWithGripSwap`,
`NavigationBarItem`
- `JsonArrow`, `JsonNestedNode`
- `ModalHeader`

### Other
- Added `aria-valuenow` to `ProgressBar` for accessibility
- `VisibilityHidden` component updated to inline accessibility styles
2026-03-04 17:04:16 +01:00
be01a85d67 i18n - translations (#18387)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-04 17:03:54 +01:00
999dcd4468 Increase size of input in test setting logic function tab (#18369)
## Before
<img width="1031" height="836" alt="image"
src="https://github.com/user-attachments/assets/475ca1be-f7c4-49d0-b329-649dbe8da489"
/>


## After

<img width="1195" height="862" alt="image"
src="https://github.com/user-attachments/assets/b1bac131-e562-4439-8f8e-bda4d6e2a646"
/>

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-04 16:49:13 +01:00
Raphaël BosiandGitHub b11f77df2a [FRONT COMPONENTS] Introduce conditionalAvailabilityExpression to command menu items (#18319)
## PR Description

- Uses `expr-eval` to enable front components (SDK plugins) to define
conditional availability as declarative expressions.
- Moves shared types and constants to `twenty-shared`
- Introduces a `conditionalAvailabilityExpression` field on
`CommandMenuItemEntity`, allowing command menu items to store an
`expr-eval` compatible expression string that is evaluated against a
CommandMenuContext to determine if the item should be shown.
- Creates an esbuild transform plugin
`conditional-availability-transform-plugin` in `twenty-sdk` that
converts TypeScript conditional availability expressions into
`expr-eval` compatible syntax at build time, so SDK developers can write
natural TS expressions that get transformed to evaluable strings.
- Removes deprecated `forceRegisteredActionsByKey` state and its usage.
- Creates `useCommandMenuContext` hook that builds the full
`CommandMenuContext` object from React state, which is then passed to
`useCommandMenuItemFrontComponentActions` for evaluating conditional
availability expressions.
2026-03-04 16:33:58 +01:00
Thomas TrompetteandGitHub f09a9cc25a Replace align-center with padding (#18384)
Toggle using align-self prevents the use of align-items

Before
<img width="323" height="54" alt="Capture d’écran 2026-03-04 à 16 11
22"
src="https://github.com/user-attachments/assets/08154592-7901-4cb4-84b8-d3091c7d5555"
/>

After
<img width="323" height="54" alt="Capture d’écran 2026-03-04 à 16 11
14"
src="https://github.com/user-attachments/assets/2a6071f6-50cd-4947-85e0-0c0a5b1685cd"
/>
2026-03-04 16:15:56 +01:00
d59d2efb8b i18n - translations (#18383)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-04 15:46:01 +01:00
nitinandGitHub 07803f232f fix: use ForbiddenException in DevelopmentGuard to prevent Sentry noise (#18378) 2026-03-04 15:36:11 +01:00
aaa483c020 i18n - translations (#18380)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-04 15:35:13 +01:00
Paul RastoinGitHubThomas TrompettebosiraphaelWeikogithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>github-actionsCharles Bochet
845a1934d3 Tt call recording app (#18281)
Co-authored-by: Thomas Trompette <thomas.trompette@sfr.fr>
Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
Co-authored-by: Weiko <corentin@twenty.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@twenty.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-04 14:11:57 +00:00
c97d872b9f [BREAKING_CHANGE_VIEW_SORT] Refactor view sort to v2 (#17609)
Fixes https://github.com/twentyhq/core-team-issues/issues/2187

---------

Co-authored-by: prastoin <paul@twenty.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
2026-03-04 12:48:58 +00:00
EtienneandGitHub 906a0aed38 Common API - Filter validation layer (#18187)
Closes https://github.com/twentyhq/core-team-issues/issues/1627

**FilterArgProcessor consolidation:**
Refactored to both validate AND transform filter values in a single pass
Coerced string inputs to native types (e.g., "1" → 1, "true" → true -
useful for Rest input)
Returns transformed filter instead of just validating
Removed overrideFilterByFieldMetadata calls from all computeArgs methods
**QueryRunnerArgsFactory cleanup**
**Testing:**
Add unit testing
uncomment integration tests
2026-03-04 12:10:44 +00:00
nitinandGitHub 80d054563e followup: centralize widget common properties and add widget bulk update integration tests (#18225)
followup
https://github.com/twentyhq/twenty/pull/18015#pullrequestreview-3818929035
2026-03-04 11:47:56 +00:00
Baptiste DevessierandGitHub 5b544809f7 Support ungrouped fields + improve edition UX (#18224)
## Demo


https://github.com/user-attachments/assets/59e530ea-1c5b-44be-a012-42551e68221c

## Demo – creating a new group


https://github.com/user-attachments/assets/e8511bc3-d586-422c-aca8-b02794a0c84f

## Demo – ungrouped fields


https://github.com/user-attachments/assets/6ded4a90-fb08-485e-ad08-086f3a970752

Closes https://github.com/twentyhq/core-team-issues/issues/2232
Closes https://github.com/twentyhq/core-team-issues/issues/2237
Closes https://github.com/twentyhq/core-team-issues/issues/2238
2026-03-04 11:45:14 +00:00
Charles BochetandGitHub 3b2bf39565 Refactor modal (#18377)
## Summary

- Move Modal UI components (`Modal`, `ModalContent`, `ModalHeader`,
`ModalFooter`, `ModalBackdrop`) from `twenty-front` to `twenty-ui` as
stateless, reusable components
- Create `ModalStatefulWrapper` in `twenty-front` that connects Jotai
state (`isModalOpenedComponentState`) to the stateless `Modal` via an
`isOpen` prop
- Rename `modalVariant` prop to `overlay` with clearer values: `'dark'`
(default), `'light'` (in-container), `'transparent'` (invisible panel).
Remove unused `'medium'` overlay
- Rename `modalId` to `modalInstanceId` across the entire modal zone
(~30 consumer files)
- Extract `ModalProps` to its own file in
`twenty-ui/types/ModalProps.ts`; extract `ModalStatefulWrapperProps` to
its own file using `Pick<ModalProps, ...>` for shared props
- Extract `ModalBackdrop` to its own file and export from `twenty-ui`;
use it in `UserOrMetadataLoader` instead of a local styled component
- Use `ModalFooter` in `StepNavigationButton` and `ModalHeader` in
`SpreadsheetImportStepperContainer` instead of duplicated `styled.div`
definitions
- Remove unused `onClose` prop from stateless `Modal`; fix `typeof
document` guard in `ModalStatefulWrapper`
- Split shared types into individual files: `ModalSize.ts`,
`ModalPadding.ts`, `ModalOverlay.ts`
- Extract wyw profiling instrumentation from `vite.config.ts` into
reusable `createWywProfilingPlugin` with parametrized threshold and
improved logging
- Delete old `Modal.tsx`, `Modal.styles.ts`, `ModalContent.tsx`,
`ModalHeader.tsx`, `ModalFooter.tsx` from `twenty-front`
- Add comprehensive Storybook stories in `twenty-ui` covering Default,
Confirmation, Small, ExtraLarge, Closed, and Interactive variants
2026-03-04 13:22:31 +01:00
Paul RastoinandGitHub 995793c0ac [CREATE_APP] Integration testing scaffold (#18345)
# Introduction
Adding integration test scaffold to the create twenty app and an example
to the hello world app
This PR also fixes all the sdk e2e tests in local

## `HELLO_WORLD`
Removed the legacy implem in the `twenty-apps` folder, replacing it by
an exhaustive app generation

## Next step
Will in another PR add workflows for CI testing

## Open question
- Should we still add vitest config and dep even if the user did not ask
for the integration test example ? -> currently we don't
- That's the perfect timing to identify if we're ok to handle seed
workspace authentication with the known api key
2026-03-04 13:12:13 +01: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
3768 changed files with 111902 additions and 50847 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ alwaysApply: true
## Formatting Standards
- **Prettier**: 2-space indentation, single quotes, trailing commas, semicolons
- **Print width**: 80 characters
- **ESLint**: No unused imports, consistent import ordering, prefer const over let
- **Oxlint**: No unused imports, consistent import ordering, prefer const over let
## Naming Conventions
```typescript
+53
View File
@@ -0,0 +1,53 @@
---
description: ESM dependency guidelines for twenty-sdk and create-twenty-app packages
globs: ["packages/twenty-sdk/**", "packages/create-twenty-app/**"]
alwaysApply: false
---
# ESM Dependency Guidelines
## Context
`twenty-sdk` and `create-twenty-app` are published as dual-format npm packages (ESM `.mjs` + CJS `.cjs`). Dependencies listed in `dependencies` are **externalized** by the Vite/Rollup build — they are not bundled, and consumers resolve them from `node_modules` at runtime.
This means **CJS-only dependencies break the ESM output**. When Rollup emits `import { foo } from 'cjs-package'`, Node.js ESM cannot resolve named exports from CommonJS modules, causing `SyntaxError: Named export 'foo' not found`.
## Rules
### Only add ESM-compatible dependencies
Before adding a new dependency to `package.json`, verify it supports ESM:
- Check for `"type": "module"` in its `package.json`
- Or check for an `"exports"` map with ESM entries
- Or check for a `"module"` field pointing to an ESM build
### Use native `node:fs/promises` for standard fs operations
```typescript
// ✅ Import native fs functions directly
import { readFile, writeFile, mkdir, rm, cp } from 'node:fs/promises';
import { createWriteStream, existsSync } from 'node:fs';
// ✅ Import only custom helpers from fs-utils (no native re-exports)
import { pathExists, ensureDir, emptyDir, copy, move, remove, readJson, writeJson, ensureFile } from '@/cli/utilities/file/fs-utils';
// ❌ Don't use fs-extra (CJS-only, breaks ESM bundle)
import * as fs from 'fs-extra';
// ❌ Don't use import * as fs from fs-utils (it doesn't re-export native fs)
import * as fs from '@/cli/utilities/file/fs-utils';
```
### Use `@/cli/utilities/string/kebab-case` instead of lodash
```typescript
// ✅ Use internal utility
import { kebabCase } from '@/cli/utilities/string/kebab-case';
// ❌ Don't use lodash single-function packages (CJS-only, unmaintained)
import kebabCase from 'lodash.kebabcase';
```
### When no ESM alternative exists
If a CJS-only package has no ESM replacement (e.g. `archiver`), add it to the `cjsOnlyPackages` list in `vite.config.node.ts` so it gets inlined into the bundle instead of externalized.
+3
View File
@@ -15,6 +15,9 @@ inputs:
runs:
using: "composite"
steps:
- name: Fetch main branch for diff
shell: bash
run: git fetch origin main --depth=1
- name: Get last successful commit
uses: nrwl/nx-set-shas@v4
- name: Run affected command
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-depth: 10
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@v45
+1 -1
View File
@@ -70,7 +70,7 @@ jobs:
- name: Checkout current branch
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-depth: 10
- name: Try to merge main into current branch
id: merge_attempt
+1 -1
View File
@@ -37,7 +37,7 @@ jobs:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build
+3 -4
View File
@@ -21,7 +21,6 @@ jobs:
files: |
package.json
packages/twenty-docs/**
eslint.config.mjs
docs-lint:
needs: changed-files-check
@@ -37,11 +36,11 @@ jobs:
- name: Fetch local actions
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Docs / Lint English MDX files
run: npx eslint "packages/twenty-docs/{developers,user-guide,twenty-ui,getting-started,snippets}/**/*.mdx" --max-warnings 0
- name: Docs / Lint
run: npx nx lint twenty-docs
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build twenty-emails
+31 -140
View File
@@ -1,10 +1,8 @@
name: CI Front and E2E
name: CI Front
on:
pull_request:
merge_group:
permissions:
contents: read
@@ -29,15 +27,6 @@ jobs:
packages/twenty-shared/**
packages/twenty-sdk/**
!packages/twenty-sdk/package.json
changed-files-check-e2e:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/**
!packages/create-twenty-app/package.json
!packages/twenty-sdk/package.json
playwright.config.ts
.github/workflows/ci-front.yaml
front-sb-build:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
@@ -53,7 +42,7 @@ jobs:
- name: Fetch local actions
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Diagnostic disk space issue
@@ -62,6 +51,12 @@ jobs:
run: npx nx reset:env twenty-front
- name: Front / Build storybook
run: npx nx storybook:build twenty-front
- name: Upload storybook build
uses: actions/upload-artifact@v4
with:
name: storybook-static
path: packages/twenty-front/storybook-static
retention-days: 1
- name: Save storybook build cache
uses: ./.github/actions/save-cache
with:
@@ -78,26 +73,41 @@ jobs:
env:
SHARD_COUNTER: 4
REACT_APP_SERVER_BASE_URL: http://localhost:3000
STORYBOOK_URL: http://localhost:6006
steps:
- name: Fetch local actions
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Restore storybook build cache
uses: ./.github/actions/restore-cache
with:
key: ${{ env.STORYBOOK_BUILD_CACHE_KEY_FOR_RESTORE_ACTION }}
- name: Clean stale storybook vitest cache
run: rm -rf packages/twenty-front/node_modules/.cache/storybook
- name: Build dependencies
run: |
npx nx build twenty-shared
npx nx build twenty-ui
npx nx build twenty-sdk
- name: Download storybook build
uses: actions/download-artifact@v4
with:
name: storybook-static
path: packages/twenty-front/storybook-static
- name: Install Playwright
run: |
cd packages/twenty-front
npx playwright install
- name: Front / Write .env
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: Serve storybook & run tests
run: |
npx http-server packages/twenty-front/storybook-static --port 6006 --silent &
timeout 30 bash -c 'until curl -sf http://localhost:6006 > /dev/null 2>&1; do sleep 1; done'
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
@@ -125,7 +135,7 @@ jobs:
# steps:
# - uses: actions/checkout@v4
# with:
# fetch-depth: 0
# fetch-depth: 10
# - name: Install dependencies
# uses: ./.github/actions/yarn-install
# - uses: actions/download-artifact@v4
@@ -150,7 +160,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Restore storybook build cache
@@ -184,7 +194,7 @@ jobs:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Restore ${{ matrix.task }} cache
@@ -214,6 +224,7 @@ jobs:
runs-on: ubuntu-latest-8-cores
env:
NODE_OPTIONS: "--max-old-space-size=10240"
ANALYZE: "true"
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
@@ -222,7 +233,7 @@ jobs:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Front / Write .env
@@ -235,117 +246,6 @@ jobs:
# name: frontend-build
# path: packages/twenty-front/build
# retention-days: 1
e2e-test:
runs-on: ubuntu-latest
needs: [changed-files-check-e2e, front-build]
if: |
always() &&
needs.changed-files-check-e2e.outputs.any_changed == 'true' &&
(needs.front-build.result == 'success' || needs.front-build.result == 'skipped') &&
(github.event_name == 'push' || github.event_name == 'merge_group' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'run-e2e')))
timeout-minutes: 30
env:
NODE_OPTIONS: "--max-old-space-size=10240"
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:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Check system resources
run: |
echo "Available memory:"
free -h
echo "Available disk space:"
df -h
echo "CPU info:"
lscpu
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build twenty-shared
run: npx nx build twenty-shared
- name: Install Playwright Browsers
run: npx nx setup twenty-e2e-testing
- name: Setup environment files
run: |
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: 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
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: Start server
run: |
npx nx start twenty-server &
echo "Waiting for server to be ready..."
timeout 60 bash -c 'until curl -s http://localhost:3000/health; do sleep 2; done'
- name: Start frontend
run: |
npm_config_yes=true npx serve -s packages/twenty-front/build -l 3001 &
echo "Waiting for frontend to be ready..."
timeout 60 bash -c 'until curl -s http://localhost:3001; do sleep 2; done'
- name: Start worker
run: |
npx nx run twenty-server:worker &
echo "Worker started"
- 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
ci-front-status-check:
if: always() && !cancelled()
timeout-minutes: 5
@@ -363,12 +263,3 @@ jobs:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1
ci-e2e-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check-e2e, e2e-test]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1
+118
View File
@@ -0,0 +1,118 @@
name: CI Merge Queue
on:
merge_group:
pull_request:
types: [labeled, synchronize, opened, reopened]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
e2e-test:
if: >
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' &&
contains(github.event.pull_request.labels.*.name, 'run-merge-queue'))
runs-on: ubuntu-latest-8-cores
timeout-minutes: 30
env:
NODE_OPTIONS: "--max-old-space-size=10240"
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:
- uses: actions/checkout@v4
with:
fetch-depth: 10
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build twenty-shared
run: npx nx build twenty-shared
- name: Install Playwright Browsers
run: npx nx setup twenty-e2e-testing
- name: Setup environment files
run: |
cp packages/twenty-front/.env.example packages/twenty-front/.env
npx nx reset:env:e2e-testing-server twenty-server
- name: Build frontend
run: NODE_ENV=production npx nx build twenty-front
- name: Build server
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: Start server
run: |
npx nx start twenty-server &
echo "Waiting for server to be ready..."
timeout 60 bash -c 'until curl -s http://localhost:3000/health; do sleep 2; done'
- name: Start frontend
run: |
npm_config_yes=true npx serve -s packages/twenty-front/build -l 3001 &
echo "Waiting for frontend to be ready..."
timeout 60 bash -c 'until curl -s http://localhost:3001; do sleep 2; done'
- name: Start worker
run: |
npx nx run twenty-server:worker &
echo "Worker started"
- name: Run Playwright tests
run: npx nx test twenty-e2e-testing
- name: Upload Playwright results
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-results
path: |
packages/twenty-e2e-testing/run_results/
packages/twenty-e2e-testing/test-results/
retention-days: 7
ci-merge-queue-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [e2e-test]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1
+2 -4
View File
@@ -1,8 +1,6 @@
name: CI SDK
on:
merge_group:
pull_request:
permissions:
@@ -35,7 +33,7 @@ jobs:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build
@@ -78,7 +76,7 @@ jobs:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build
+76 -32
View File
@@ -3,8 +3,6 @@ name: CI Server
on:
pull_request:
merge_group:
permissions:
contents: read
@@ -13,7 +11,7 @@ concurrency:
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
env:
SERVER_SETUP_CACHE_KEY: server-setup
SERVER_BUILD_CACHE_KEY: server-build
jobs:
changed-files-check:
@@ -27,11 +25,59 @@ jobs:
packages/twenty-front/src/generated-metadata/**
packages/twenty-emails/**
packages/twenty-shared/**
server-setup:
server-build:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest-8-cores
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Restore server build cache
id: restore-server-build-cache
uses: ./.github/actions/restore-cache
with:
key: ${{ env.SERVER_BUILD_CACHE_KEY }}
- name: Build twenty-shared
run: npx nx build twenty-shared
- name: Server / Write .env
run: npx nx reset:env twenty-server
- name: Server / Build
run: npx nx build twenty-server
- name: Save server build cache
uses: ./.github/actions/save-cache
with:
key: ${{ steps.restore-server-build-cache.outputs.cache-primary-key }}
server-lint-typecheck:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest-8-cores
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build twenty-shared
run: npx nx build twenty-shared
- name: Server / Run lint & typecheck
uses: ./.github/actions/nx-affected
with:
tag: scope:backend
tasks: lint,typecheck
server-validation:
needs: server-build
timeout-minutes: 30
runs-on: ubuntu-latest-8-cores
services:
postgres:
image: twentycrm/twenty-postgres-spilo
@@ -55,21 +101,15 @@ jobs:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Restore server setup
id: restore-server-setup-cache
- name: Restore server build cache
uses: ./.github/actions/restore-cache
with:
key: ${{ env.SERVER_SETUP_CACHE_KEY }}
key: ${{ env.SERVER_BUILD_CACHE_KEY }}
- name: Build twenty-shared
run: npx nx build twenty-shared
- name: Server / Run lint & typecheck
uses: ./.github/actions/nx-affected
with:
tag: scope:backend
tasks: lint,typecheck
- name: Server / Write .env
run: npx nx reset:env twenty-server
- name: Server / Build
@@ -84,10 +124,8 @@ jobs:
run: |
timeout 30s npx nx run twenty-server:worker || exit_code=$?
if [ $exit_code -eq 124 ]; then
# If timeout was reached (exit code 124), consider it a success
exit 0
elif [ $exit_code -ne 0 ]; then
# If worker failed for other reasons, fail the build
exit $exit_code
fi
- name: Server / Start
@@ -120,11 +158,9 @@ jobs:
fi
- name: GraphQL / Check for Pending Generation
run: |
# Run GraphQL generation commands
npx nx run twenty-front:graphql:generate
npx nx run twenty-front:graphql:generate --configuration=metadata
# Check if GraphQL generated files were modified
if ! git diff --quiet -- packages/twenty-front/src/generated packages/twenty-front/src/generated-metadata; then
echo "::error::GraphQL schema changes detected. Please run 'npx nx run twenty-front:graphql:generate' and 'npx nx run twenty-front:graphql:generate --configuration=metadata' and commit the changes."
echo ""
@@ -137,25 +173,24 @@ jobs:
echo ""
exit 1
fi
- name: Save server setup
uses: ./.github/actions/save-cache
with:
key: ${{ steps.restore-server-setup-cache.outputs.cache-primary-key }}
server-test:
needs: server-build
timeout-minutes: 30
runs-on: ubuntu-latest-8-cores
needs: server-setup
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Restore server setup
- name: Restore server build cache
uses: ./.github/actions/restore-cache
with:
key: ${{ env.SERVER_SETUP_CACHE_KEY }}
key: ${{ env.SERVER_BUILD_CACHE_KEY }}
- name: Build twenty-shared
run: npx nx build twenty-shared
- name: Server / Run Tests
uses: ./.github/actions/nx-affected
with:
@@ -165,11 +200,11 @@ jobs:
server-integration-test:
timeout-minutes: 30
runs-on: ubuntu-latest-8-cores
needs: server-setup
needs: server-build
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5, 6, 7, 8]
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
services:
postgres:
image: twentycrm/twenty-postgres-spilo
@@ -207,12 +242,12 @@ jobs:
ANALYTICS_ENABLED: true
CLICKHOUSE_URL: "http://default:clickhousePassword@localhost:8123/twenty"
CLICKHOUSE_PASSWORD: clickhousePassword
SHARD_COUNTER: 8
SHARD_COUNTER: 10
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Update .env.test for integrations tests
@@ -223,10 +258,10 @@ jobs:
echo "BILLING_STRIPE_BASE_PLAN_PRODUCT_ID=test-base-plan-product-id" >> .env.test
echo "BILLING_STRIPE_WEBHOOK_SECRET=test-webhook-secret" >> .env.test
echo "BILLING_PLAN_REQUIRED_LINK=http://localhost:3001/stripe-redirection" >> .env.test
- name: Restore server setup
- name: Restore server build cache
uses: ./.github/actions/restore-cache
with:
key: ${{ env.SERVER_SETUP_CACHE_KEY }}
key: ${{ env.SERVER_BUILD_CACHE_KEY }}
- name: Server / Build
run: npx nx build twenty-server
- name: Build dependencies
@@ -247,11 +282,20 @@ jobs:
tasks: 'test:integration'
configuration: 'with-db-reset'
args: --shard=${{ matrix.shard }}/${{ env.SHARD_COUNTER }}
ci-server-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, server-setup, server-test, server-integration-test]
needs:
[
changed-files-check,
server-build,
server-lint-typecheck,
server-validation,
server-test,
server-integration-test,
]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
+2 -4
View File
@@ -1,8 +1,6 @@
name: CI Shared
on:
merge_group:
pull_request:
permissions:
@@ -36,13 +34,13 @@ jobs:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Run ${{ matrix.task }} task
uses: ./.github/actions/nx-affected
with:
tag: scope:frontend
tag: scope:shared
tasks: ${{ matrix.task }}
ci-shared-status-check:
if: always() && !cancelled()
@@ -4,8 +4,6 @@ permissions:
contents: read
on:
merge_group:
pull_request:
concurrency:
+1 -3
View File
@@ -4,8 +4,6 @@ permissions:
contents: read
on:
merge_group:
pull_request:
concurrency:
@@ -42,7 +40,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
+2 -4
View File
@@ -3,8 +3,6 @@ name: CI Zapier
on:
pull_request:
merge_group:
permissions:
contents: read
@@ -52,7 +50,7 @@ jobs:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
@@ -107,7 +105,7 @@ jobs:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build
+1 -1
View File
@@ -164,4 +164,4 @@ jobs:
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)) }}}'
client-payload: '{"repo": ${{ toJSON(steps.prompt.outputs.repo) }}, "issue_number": ${{ toJSON(steps.prompt.outputs.issue_number) }}, "run_id": ${{ toJSON(github.run_id) }}, "run_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}'
+1 -1
View File
@@ -28,7 +28,7 @@ coverage
dist
storybook-static
*.tsbuildinfo
.eslintcache
.oxlintcache
.nyc_output
test-results/
dump.rdb
+1 -1
View File
@@ -1,7 +1,7 @@
{
"recommendations": [
"arcanis.vscode-zipfs",
"dbaeumer.vscode-eslint",
"oxc.oxc-vscode",
"esbenp.prettier-vscode",
"figma.figma-vscode-extension",
"firsttris.vscode-jest-runner",
+10 -7
View File
@@ -4,25 +4,28 @@
"files.insertFinalNewline": true,
"files.trimTrailingWhitespace": true,
"[typescript]": {
"editor.formatOnSave": false,
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.fixAll.oxc": "explicit",
"source.addMissingImports": "always",
"source.organizeImports": "always"
}
},
"[javascript]": {
"editor.formatOnSave": false,
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.fixAll.oxc": "explicit",
"source.addMissingImports": "always",
"source.organizeImports": "always"
}
},
"[typescriptreact]": {
"editor.formatOnSave": false,
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.fixAll.oxc": "explicit",
"source.addMissingImports": "always",
"source.organizeImports": "always"
}
@@ -48,7 +51,7 @@
"search.exclude": {
"**/.yarn": true
},
"eslint.debug": true,
"oxc.lint.enable": true,
"files.associations": {
".cursorrules": "markdown"
},
+13 -9
View File
@@ -37,8 +37,8 @@
"path": "../packages/twenty-zapier"
},
{
"name": "tools/eslint-rules",
"path": "../tools/eslint-rules"
"name": "packages/twenty-oxlint-rules",
"path": "../packages/twenty-oxlint-rules"
},
{
"name": "packages/twenty-e2e-testing",
@@ -49,23 +49,26 @@
"editor.formatOnSave": false,
"files.eol": "auto",
"[typescript]": {
"editor.formatOnSave": false,
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.fixAll.oxc": "explicit",
"source.addMissingImports": "always"
}
},
"[javascript]": {
"editor.formatOnSave": false,
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.fixAll.oxc": "explicit",
"source.addMissingImports": "always"
}
},
"[typescriptreact]": {
"editor.formatOnSave": false,
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.fixAll.oxc": "explicit",
"source.addMissingImports": "always"
}
},
@@ -88,7 +91,7 @@
"typescript.preferences.importModuleSpecifier": "non-relative",
"[javascript][typescript][typescriptreact]": {
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.fixAll.oxc": "explicit",
"source.addMissingImports": "always"
}
},
@@ -98,6 +101,7 @@
"files.exclude": {
"packages/": true
},
"oxc.lint.enable": true,
"jest.runMode": "on-demand",
"jest.disabledWorkspaceFolders": [
"ROOT",
+2 -2
View File
@@ -90,7 +90,7 @@ npx nx run twenty-front:graphql:generate --configuration=metadata
## Architecture Overview
### Tech Stack
- **Frontend**: React 18, TypeScript, Jotai (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
@@ -175,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)
+1 -1
View File
@@ -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 [Jotai](https://jotai.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/)
-423
View File
@@ -1,423 +0,0 @@
# Emotion → Linaria Migration Plan: twenty-front
## Overview
Migrate all Emotion (`@emotion/styled`, `@emotion/react`) usages in
`packages/twenty-front/src` to Linaria (`@linaria/react`, `@linaria/core`),
following the same patterns already established in the `twenty-ui` package.
Linaria is a **zero-runtime** CSS-in-JS library. Styles are extracted at
build time by [wyw-in-js](https://wyw-in-js.dev/) (the Vite plugin is
`@wyw-in-js/vite`, already configured in `twenty-front/vite.config.ts`).
This means every expression inside a `styled` or `css` template literal
must be statically evaluable at build time — no runtime theme objects,
no closures over component state, no side-effects.
**Total files to migrate: ~998**
| Category | Files | Description |
|---|---|---|
| styled-only | 694 | Import `@emotion/styled` but not `useTheme` |
| styled + useTheme | 224 | Import both `@emotion/styled` and `useTheme` |
| useTheme-only | 79 | Import `useTheme` but not `@emotion/styled` |
| css / Global only | 1 | Import `css` or `Global` from `@emotion/react` only |
## 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`
`themeCssVariables` is the build-time object; every leaf resolves to a CSS
`var()` reference. It is safe to use inside `styled` and `css` templates
because wyw-in-js can evaluate it statically.
## Migration Patterns
### 1. `styled` import
```diff
- import styled from '@emotion/styled';
+ import { styled } from '@linaria/react';
```
### 2. Theme access in styled components
Replace Emotion's `({ theme }) =>` prop-function pattern with static
`themeCssVariables` references:
```diff
+ import { themeCssVariables } from 'twenty-ui/theme';
const StyledTitle = styled.span`
- color: ${({ theme }) => theme.font.color.primary};
- font-size: ${({ theme }) => theme.font.size.lg};
+ color: ${themeCssVariables.font.color.primary};
+ font-size: ${themeCssVariables.font.size.lg};
`;
```
### 3. Spacing
`theme.spacing(N)` is a function; in Linaria it becomes an indexed lookup:
```diff
- margin-top: ${({ theme }) => theme.spacing(3)};
+ margin-top: ${themeCssVariables.spacing[3]};
```
For multi-arg spacing like `theme.spacing(2, 4)``"8px 16px"`:
```diff
- padding: ${({ theme }) => theme.spacing(2, 4)};
+ padding: ${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[4]};
```
The spacing scale covers integers 032 plus `0.5` and `1.5`. Any other
fractional values (`0.25`, `0.75`, `1.25`, `2.5`, `3.5`) must be replaced
with literal pixel values (e.g. `theme.spacing(2.5)``10px`).
### 4. `useTheme` → `useContext(ThemeContext)`
For runtime theme access (icon sizes, animation durations, conditional logic
outside of styled components):
```diff
- import { useTheme } from '@emotion/react';
+ import { useContext } from 'react';
+ import { ThemeContext } from 'twenty-ui/theme';
const MyComponent = () => {
- const theme = useTheme();
+ const { theme } = useContext(ThemeContext);
return <Icon size={theme.icon.size.sm} />;
};
```
### 5. `css` template literal
Linaria's `css` (from `@linaria/core`) returns a **class name string**, not
a serialized style object like Emotion's `css`. This has two consequences:
**Standalone usage** — apply via `className`, not the `css` prop:
```diff
- import { css } from '@emotion/react';
+ import { css } from '@linaria/core';
const myClass = css`
text-decoration: none;
`;
- <Link css={myClass} />
+ <Link className={myClass} />
```
**Inside `styled` templates** — do NOT nest `css` tags. Linaria's `css`
returns a class name, not raw CSS text, so interpolating it inside `styled`
produces broken output. Use plain strings instead:
```diff
// WRONG — css`` returns a class name, not CSS text
${({ handle }) =>
handle === 'left'
- ? css`left: ${themeCssVariables.spacing[1]};`
- : css`right: ${themeCssVariables.spacing[1]};`}
+ ? `left: ${themeCssVariables.spacing[1]};`
+ : `right: ${themeCssVariables.spacing[1]};`}
```
### 6. Interpolation return types
wyw-in-js requires prop interpolation functions to return `string | number`.
They must **never** return `false`, `undefined`, or `null`. Replace
short-circuit `&&` with ternary expressions:
```diff
// WRONG — returns false when condition is false
- ${({ isActive }) => isActive && `background: ${themeCssVariables.color.blue};`}
// CORRECT
+ ${({ isActive }) => isActive ? `background: ${themeCssVariables.color.blue};` : ''}
```
### 7. Block interpolations (multi-declaration returns)
Linaria wraps each interpolation result in a single CSS custom property
(`var(--xxx)`). An interpolation that returns **multiple CSS declarations**
produces invalid CSS. Split into one interpolation per property:
```diff
// WRONG — single interpolation returning multiple declarations
- ${({ divider, theme }) => {
- const border = `1px solid ${theme.border.color.light}`;
- return divider === 'left' ? `border-left: ${border}` : `border-right: ${border}`;
- }}
// CORRECT — 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'};
```
### 8. CSS var + unit concatenation
CSS custom properties can't be concatenated with unit suffixes directly
(`var(--x)px` is invalid). Use `calc()` to attach units:
```diff
- transition: background ${themeCssVariables.animation.duration.instant}s ease;
+ transition: background calc(${themeCssVariables.animation.duration.instant} * 1s) ease;
```
### 9. `styled(Component)` requires `className`
Linaria's `styled(Component)` works by passing a generated `className` to
the wrapped component. The component **must** accept and forward a
`className` prop — otherwise the styles are silently lost. If the component
doesn't support it, either add `className` support or use a wrapper div.
Linaria also does **not** support Emotion's `shouldForwardProp` option.
Custom props on HTML elements are automatically filtered by Linaria's
runtime (via `@emotion/is-prop-valid`). For custom components, all props are
forwarded — ensure the wrapped component ignores unknown props gracefully.
### 10. `type Theme` → `type ThemeType`
```diff
- import { type Theme } from '@emotion/react';
+ import { type ThemeType } from 'twenty-ui/theme';
```
### 11. 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 by
wyw-in-js. Define the styled component first, then wrap with
`motion.create()`:
```tsx
const StyledBarBase = styled.div`
background-color: ${themeCssVariables.font.color.primary};
height: 100%;
`;
const StyledBar = motion.create(StyledBarBase);
```
### 12. Dynamic styles via CSS variables
When a component needs to compute styles from multiple props with complex
branching logic (e.g. combining `variant`, `accent`, `disabled`, `focus`),
Linaria's prop interpolations become unwieldy. Use a `computeDynamicStyles`
helper 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} />;
```
### 13. `Global` component
Replace Emotion's `<Global styles={...} />` with standard CSS or the
`ThemeCssVariableInjectorEffect` pattern from twenty-ui.
### 14. `ThemeProvider`
The `BaseThemeProvider` already wraps children with both Emotion's
`ThemeProvider` and Linaria's `ThemeContextProvider`. Once all Emotion usages
are gone, the Emotion `ThemeProvider` wrapper can be removed.
---
## PR Breakdown
Files are grouped to keep each PR around ~100 files with consistent review
surface. We start with the simplest, lowest-risk modules.
### PR 1 (~97 files) — Small standalone modules
Low-risk modules with mostly simple `styled`-only patterns.
| Module | Files |
|---|---|
| spreadsheet-import | 28 |
| billing | 10 |
| views | 14 |
| navigation-menu-item | 14 |
| blocknote-editor | 7 |
| advanced-text-editor | 7 |
| favorites | 7 |
| navigation | 4 |
| information-banner | 3 |
| sign-in-background-mock | 3 |
### PR 2 (~100 files) — Auth, tiny modules, loading, testing, pages (part 1)
| Module | Files |
|---|---|
| auth | 19 |
| action-menu | 3 |
| object-metadata | 3 |
| onboarding | 2 |
| workspace | 2 |
| file | 2 |
| error-handler | 2 |
| front-components | 1 |
| geo-map | 1 |
| hooks | 1 |
| loading | 5 |
| testing | 5 |
| pages (first ~55 files) | ~55 |
### PR 3 (~97 files) — Pages (remaining) + activities + AI
| Module | Files |
|---|---|
| pages (remaining ~16 files) | ~16 |
| activities | 53 |
| ai | 28 |
### PR 4 (~100 files) — Command-menu + workflow (part 1)
| Module | Files |
|---|---|
| command-menu | 53 |
| workflow (first ~47 files) | ~47 |
### PR 5 (~115 files) — Workflow (remaining) + page-layout
| Module | Files |
|---|---|
| workflow (remaining ~32 files) | ~32 |
| page-layout | 83 |
### PR 6 (~100 files) — UI module (part 1)
| Module | Files |
|---|---|
| ui (first ~100 files) | ~100 |
### PR 7 (~85 files) — UI module (remaining) + object-record (start)
| Module | Files |
|---|---|
| ui (remaining ~23 files) | ~23 |
| object-record (first ~62 files) | ~62 |
### PR 8 (~100 files) — Object-record (continued)
| Module | Files |
|---|---|
| object-record (next ~100 files) | ~100 |
### PR 9 (~100 files) — Settings (part 1)
| Module | Files |
|---|---|
| settings (first ~100 files) | ~100 |
### PR 10 (~102 files) — Settings (part 2) + final cleanup
| Module | Files |
|---|---|
| settings (remaining ~102 files) | ~102 |
| css/Global-only file | 1 |
### Post-migration PR — Remove Emotion
Once all PRs are merged:
- Remove `ThemeProvider` from `@emotion/react` in `BaseThemeProvider`
- Remove `@emotion/styled` and `@emotion/react` dependencies
- Remove `@styled/typescript-styled-plugin` from tsconfig
- Clean up any remaining Emotion-related configuration
---
## Risk Assessment
| Risk | Mitigation |
|---|---|
| wyw-in-js evaluates at build time; dynamic expressions may fail | Use `themeCssVariables` for static theme values; pass dynamic values as component props or via `style={}` CSS variables |
| `theme.spacing(N)` function → `themeCssVariables.spacing[N]` index | Pre-computed for integers 032 plus 0.5 and 1.5; other fractional values → literal pixel values |
| `useTheme` used for runtime logic (not just styles) | Replace with `useContext(ThemeContext)`, destructure `{ theme }` |
| Multi-arg `theme.spacing(a, b, c)` | Split into individual `themeCssVariables.spacing[N]` references |
| `css` tag inside `styled` templates | Linaria `css` returns class name, not CSS text; use plain strings inside `styled` |
| Interpolation returns `false` / `undefined` | wyw-in-js requires `string \| number`; use ternary `? : ''` instead of `&&` |
| Block interpolations (multiple declarations) | Split into one interpolation per CSS property |
| `var(--x)px` concatenation | Use `calc(var(--x) * 1px)` |
| `styled(motion.div)` stripped by wyw-in-js | Use `motion.create(StyledBase)` pattern |
| `styled(Component)` with no `className` prop | Add `className` support to wrapped component or use wrapper div |
| Complex multi-prop style branching | Use `computeDynamicStyles` + `style={}` + `var()` references |
---
## Known Issues & TODOs
### `styled(Component)` from `twenty-ui` — style overrides silently dropped
When `twenty-front` uses `styled(SomeComponent)` to extend a pre-built
component from `twenty-ui` (e.g. `Card`, `CardContent`, `PropertyBox`,
`Chip`, `Pill`, `Avatar`, `StyledHoverableMenuItemBase`, `MenuItemLeftContent`,
`TabList`, `NavigationDrawerSection`), the style overrides are silently
ignored at runtime. This happens because `wyw-in-js` in `twenty-front`
cannot resolve the base class name of a component that was already compiled
in another package.
**Current workaround:** Replace `styled(Component)` with a plain
`styled.div` (or wrapper div) that duplicates the needed base styles.
This fixes the visual regression but loses the component's built-in
behavior (e.g. `CardContent`'s framer-motion animation, `PropertyBox`'s
layout-context padding logic, `Card`'s border/overflow).
**Proper fix (TODO):** Add customization props directly to the base
components so consumers can override styles without wrapping:
- `Card` / `CardContent` — add `padding`, `backgroundColor`, `borderColor` props
- `PropertyBox` — add `padding`, `height`, `noPadding` props
- Other frequently extended components — audit and add props as needed
**Affected files (using workaround today):**
- `CalendarEventDetails.tsx``styled(PropertyBox)``styled.div`, `styled(Chip)` → wrapper
- `CalendarEventParticipantsResponseStatusField.tsx``styled(PropertyBox)``styled.div`
- `CalendarEventNotSharedContent.tsx``styled(Card)` / `styled(CardContent)``styled.div`
- `CalendarDayCardContent.tsx``styled(CardContent)``styled(motion.div)`
- `ActivityRow.tsx``styled(CardContent)``styled.div`
- `EmailThreadPreview.tsx``styled(Avatar)` → wrapper div
- `SignInAppNavigationDrawerMock.tsx``styled(NavigationDrawerSection)` → wrapper div
- `DefaultLayout.tsx``styled(SignInAppNavigationDrawerMock)` → wrapper div
- `LastUsedPill.tsx``styled(Pill)` → wrapper span
- `Logo.tsx` — dynamic `background-image` via inline `style` instead of prop
- `DatePicker.tsx` / `DateTimePicker.tsx``styled(StyledHoverableMenuItemBase)` / `styled(MenuItemLeftContent)``styled.div`
- `SelectControl.tsx` / `MultiSelectControl.tsx``styled(IconChevronDown)` → wrapper div
---
## Validation Checklist (per PR)
- [ ] `npx nx lint:diff-with-main twenty-front` passes
- [ ] `npx nx typecheck twenty-front` passes
- [ ] `npx nx test twenty-front` passes
- [ ] Visual spot-check of affected components in the app
- [ ] No remaining `@emotion/styled` or `@emotion/react` imports in migrated files
-226
View File
@@ -1,226 +0,0 @@
import js from '@eslint/js';
import nxPlugin from '@nx/eslint-plugin';
import typescriptEslint from '@typescript-eslint/eslint-plugin';
import typescriptParser from '@typescript-eslint/parser';
import importPlugin from 'eslint-plugin-import';
import linguiPlugin from 'eslint-plugin-lingui';
import * as mdxPlugin from 'eslint-plugin-mdx';
import preferArrowPlugin from 'eslint-plugin-prefer-arrow';
import prettierPlugin from 'eslint-plugin-prettier';
import unicornPlugin from 'eslint-plugin-unicorn';
import unusedImportsPlugin from 'eslint-plugin-unused-imports';
import jsoncParser from 'jsonc-eslint-parser';
const twentyRules = await nxPlugin.loadWorkspaceRules(
'packages/twenty-eslint-rules',
);
export default [
// Base JavaScript configuration
js.configs.recommended,
// Lingui recommended rules
linguiPlugin.configs['flat/recommended'],
// Global ignores
{
ignores: ['**/node_modules/**'],
},
// Base configuration for all files
{
files: ['**/*.{js,jsx,ts,tsx}'],
plugins: {
prettier: prettierPlugin,
lingui: linguiPlugin,
'@nx': nxPlugin,
'prefer-arrow': preferArrowPlugin,
import: importPlugin,
'unused-imports': unusedImportsPlugin,
unicorn: unicornPlugin,
},
rules: {
// General rules
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
'no-console': [
'warn',
{ allow: ['group', 'groupCollapsed', 'groupEnd'] },
],
'no-control-regex': 0,
'no-debugger': 'error',
'no-duplicate-imports': 'error',
'no-undef': 'off',
'no-unused-vars': 'off',
// Nx rules
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{
sourceTag: 'scope:apps',
onlyDependOnLibsWithTags: ['scope:apps', 'scope:sdk'],
},
{
sourceTag: 'scope:sdk',
onlyDependOnLibsWithTags: ['scope:sdk', 'scope:shared'],
},
{
sourceTag: 'scope:create-app',
onlyDependOnLibsWithTags: ['scope:create-app', 'scope:shared'],
},
{
sourceTag: 'scope:shared',
onlyDependOnLibsWithTags: ['scope:shared'],
},
{
sourceTag: 'scope:backend',
onlyDependOnLibsWithTags: ['scope:shared', 'scope:backend'],
},
{
sourceTag: 'scope:frontend',
onlyDependOnLibsWithTags: ['scope:shared', 'scope:frontend'],
},
{
sourceTag: 'scope:zapier',
onlyDependOnLibsWithTags: ['scope:shared', 'scope:zapier'],
},
],
},
],
// Import rules
'import/no-relative-packages': 'error',
'import/no-useless-path-segments': 'error',
'import/no-duplicates': ['error', { considerQueryString: true }],
// Prefer arrow functions
'prefer-arrow/prefer-arrow-functions': [
'error',
{
disallowPrototype: true,
singleReturnOnly: false,
classPropertiesAllowed: false,
},
],
// Unused imports
'unused-imports/no-unused-imports': 'warn',
'unused-imports/no-unused-vars': [
'warn',
{
vars: 'all',
varsIgnorePattern: '^_',
args: 'after-used',
argsIgnorePattern: '^_',
},
],
},
},
// TypeScript specific configuration
{
files: ['**/*.{ts,tsx}'],
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
plugins: {
'@typescript-eslint': typescriptEslint,
},
rules: {
// TypeScript rules
'no-redeclare': 'off', // Turn off base rule for TypeScript
'@typescript-eslint/no-redeclare': 'error', // Use TypeScript-aware version
'@typescript-eslint/ban-ts-comment': 'error',
'@typescript-eslint/consistent-type-imports': [
'error',
{
prefer: 'type-imports',
fixStyle: 'inline-type-imports',
},
],
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/no-empty-object-type': [
'error',
{
allowInterfaces: 'with-single-extends',
},
],
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-unused-vars': 'off',
},
},
// JavaScript specific configuration
{
files: ['*.{js,jsx}'],
rules: {
// JavaScript-specific rules if needed
},
},
// Test files
{
files: [
'*.spec.@(ts|tsx|js|jsx)',
'*.integration-spec.@(ts|tsx|js|jsx)',
'*.test.@(ts|tsx|js|jsx)',
],
languageOptions: {
globals: {
jest: true,
describe: true,
it: true,
expect: true,
beforeEach: true,
afterEach: true,
beforeAll: true,
afterAll: true,
},
},
rules: {
'@typescript-eslint/no-non-null-assertion': 'off',
},
},
// JSON files
{
files: ['**/*.json'],
languageOptions: {
parser: jsoncParser,
},
},
// MDX files
{
...mdxPlugin.flat,
plugins: {
...mdxPlugin.flat.plugins,
'@nx': nxPlugin,
twenty: { rules: twentyRules },
},
},
mdxPlugin.flatCodeBlocks,
{
files: ['**/*.mdx'],
rules: {
'no-unused-vars': 'off',
'unused-imports/no-unused-imports': 'off',
'unused-imports/no-unused-vars': 'off',
// Enforce JSX tags on separate lines to prevent Crowdin translation issues
'twenty/mdx-component-newlines': 'error',
// Disallow angle bracket placeholders to prevent Crowdin translation errors
'twenty/no-angle-bracket-placeholders': 'error',
},
},
];
+12 -26
View File
@@ -40,34 +40,30 @@
"dependsOn": ["^build"]
},
"lint": {
"executor": "@nx/eslint:lint",
"executor": "nx:run-commands",
"cache": true,
"outputs": ["{options.outputFile}"],
"options": {
"eslintConfig": "{projectRoot}/eslint.config.mjs",
"cache": true,
"cacheLocation": "{workspaceRoot}/.cache/eslint"
"cwd": "{projectRoot}",
"command": "npx oxlint -c .oxlintrc.json . && (prettier . --check --cache --cache-location ../../.cache/prettier/{projectRoot} --cache-strategy metadata || (echo 'ERROR: Prettier formatting check failed! Fix with: npx nx lint --configuration=fix' && false))"
},
"configurations": {
"ci": {
"cacheStrategy": "content"
},
"ci": {},
"fix": {
"fix": true
"command": "npx oxlint --fix -c .oxlintrc.json . && prettier . --write --cache --cache-location ../../.cache/prettier/{projectRoot} --cache-strategy metadata"
}
},
"dependsOn": ["^build"]
"dependsOn": ["^build", "twenty-oxlint-rules:build"]
},
"lint:diff-with-main": {
"executor": "nx:run-commands",
"cache": false,
"options": {
"command": "git diff --name-only --diff-filter=d main | grep -E '{args.pattern}' | grep '^{projectRoot}/' | xargs sh -c 'if [ $# -gt 0 ]; then npx eslint --config {projectRoot}/eslint.config.mjs \"$@\"; fi' _",
"command": "FILES=$(git diff --name-only --diff-filter=d main -- {projectRoot}/ | grep -E '{args.pattern}'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint -c {projectRoot}/.oxlintrc.json $FILES && (prettier --check $FILES || (echo 'ERROR: Prettier formatting check failed! Fix with: npx nx lint:diff-with-main --configuration=fix' && false)))",
"pattern": "\\.(ts|tsx|js|jsx)$"
},
"configurations": {
"fix": {
"command": "git diff --name-only --diff-filter=d main | grep -E '{args.pattern}' | grep '^{projectRoot}/' | xargs sh -c 'if [ $# -gt 0 ]; then npx eslint --config {projectRoot}/eslint.config.mjs --fix \"$@\"; fi' _"
"command": "FILES=$(git diff --name-only --diff-filter=d main -- {projectRoot}/ | grep -E '{args.pattern}'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint --fix -c {projectRoot}/.oxlintrc.json $FILES && prettier --write $FILES)"
}
}
},
@@ -147,7 +143,7 @@
"outputs": ["{projectRoot}/{options.output-dir}"],
"options": {
"cwd": "{projectRoot}",
"command": "NODE_OPTIONS='--max-old-space-size=10240' VITE_DISABLE_TYPESCRIPT_CHECKER=true storybook build --test",
"command": "NODE_OPTIONS='--max-old-space-size=10240' storybook build --test",
"output-dir": "storybook-static",
"config-dir": ".storybook"
},
@@ -255,14 +251,6 @@
}
}
},
"@nx/eslint:lint": {
"cache": true,
"inputs": [
"default",
"{workspaceRoot}/eslint.config.mjs",
"{workspaceRoot}/packages/twenty-eslint-rules/**/*"
]
},
"@nx/vite:build": {
"cache": true,
"dependsOn": ["^build"],
@@ -276,23 +264,21 @@
"generators": {
"@nx/react": {
"application": {
"style": "@emotion/styled",
"linter": "eslint",
"style": "@linaria/react",
"bundler": "vite",
"compiler": "swc",
"unitTestRunner": "jest",
"projectNameAndRootFormat": "derived"
},
"library": {
"style": "@emotion/styled",
"linter": "eslint",
"style": "@linaria/react",
"bundler": "vite",
"compiler": "swc",
"unitTestRunner": "jest",
"projectNameAndRootFormat": "derived"
},
"component": {
"style": "@emotion/styled"
"style": "@linaria/react"
}
}
},
+16 -37
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,12 +40,13 @@
"lodash.snakecase": "^4.1.1",
"lodash.upperfirst": "^4.3.1",
"microdiff": "^1.3.2",
"next-with-linaria": "^1.3.0",
"planer": "^1.2.0",
"pluralize": "^8.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-responsive": "^9.0.2",
"react-router-dom": "^6.4.4",
"react-router-dom": "^6.30.3",
"react-tooltip": "^5.13.1",
"remark-gfm": "^4.0.1",
"rxjs": "^7.2.0",
@@ -72,14 +72,13 @@
"@graphql-codegen/typescript": "^3.0.4",
"@graphql-codegen/typescript-operations": "^3.0.4",
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
"@nx/eslint": "22.3.3",
"@nx/eslint-plugin": "22.3.3",
"@nx/jest": "22.3.3",
"@nx/js": "22.3.3",
"@nx/react": "22.3.3",
"@nx/storybook": "22.3.3",
"@nx/vite": "22.3.3",
"@nx/web": "22.3.3",
"@nx/jest": "22.5.4",
"@nx/js": "22.5.4",
"@nx/react": "22.5.4",
"@nx/storybook": "22.5.4",
"@nx/vite": "22.5.4",
"@nx/web": "22.5.4",
"@oxlint/plugins": "^1.51.0",
"@sentry/types": "^8",
"@storybook-community/storybook-addon-cookie": "^5.0.0",
"@storybook/addon-coverage": "^3.0.0",
@@ -89,11 +88,10 @@
"@storybook/icons": "^2.0.1",
"@storybook/react-vite": "^10.2.13",
"@storybook/test-runner": "^0.24.2",
"@stylistic/eslint-plugin": "^1.5.0",
"@swc-node/register": "1.11.1",
"@swc/cli": "^0.3.12",
"@swc/core": "1.15.11",
"@swc/helpers": "~0.5.18",
"@swc-node/register": "^1.11.1",
"@swc/cli": "^0.7.10",
"@swc/core": "^1.15.11",
"@swc/helpers": "~0.5.19",
"@swc/jest": "^0.2.39",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
@@ -132,9 +130,6 @@
"@types/react-dom": "^18.2.15",
"@types/supertest": "^2.0.11",
"@types/uuid": "^9.0.2",
"@typescript-eslint/eslint-plugin": "^8.39.0",
"@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": "4.2.3",
"@vitest/browser-playwright": "^4.0.18",
@@ -146,22 +141,6 @@
"danger": "^13.0.4",
"dotenv-cli": "^7.4.4",
"esbuild": "^0.25.10",
"eslint": "^9.32.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-lingui": "^0.9.0",
"eslint-plugin-mdx": "^3.6.2",
"eslint-plugin-prefer-arrow": "^1.2.3",
"eslint-plugin-prettier": "^5.1.2",
"eslint-plugin-project-structure": "^3.9.1",
"eslint-plugin-react": "^7.37.2",
"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.2.13",
"eslint-plugin-unicorn": "^56.0.1",
"eslint-plugin-unused-imports": "^3.0.0",
"http-server": "^14.1.1",
"jest": "29.7.0",
"jest-environment-jsdom": "30.0.0-beta.3",
@@ -170,7 +149,7 @@
"jsdom": "~22.1.0",
"msw": "^2.12.7",
"msw-storybook-addon": "^2.0.6",
"nx": "22.3.3",
"nx": "22.5.4",
"prettier": "^3.1.1",
"raw-loader": "^4.0.2",
"rimraf": "^5.0.5",
@@ -230,7 +209,7 @@
"packages/twenty-apps",
"packages/twenty-cli",
"packages/create-twenty-app",
"packages/twenty-eslint-rules"
"packages/twenty-oxlint-rules"
]
},
"prettier": {
+47
View File
@@ -0,0 +1,47 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript", "import", "unicorn"],
"categories": {
"correctness": "off"
},
"ignorePatterns": ["node_modules", "dist"],
"rules": {
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
"no-console": "off",
"no-control-regex": "off",
"no-debugger": "error",
"no-duplicate-imports": "error",
"no-undef": "off",
"no-unused-vars": "off",
"no-redeclare": "off",
"import/no-duplicates": "error",
"typescript/no-redeclare": "error",
"typescript/ban-ts-comment": "error",
"typescript/consistent-type-imports": [
"error",
{
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}
],
"typescript/explicit-function-return-type": "off",
"typescript/explicit-module-boundary-types": "off",
"typescript/no-empty-object-type": [
"error",
{
"allowInterfaces": "with-single-extends"
}
],
"typescript/no-empty-function": "off",
"typescript/no-explicit-any": "off",
"typescript/no-unused-vars": [
"warn",
{
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}
]
}
}
@@ -0,0 +1 @@
dist
+17 -7
View File
@@ -19,9 +19,11 @@ Create Twenty App is the official scaffolding CLI for building apps on top of [T
- Strong TypeScript support and typed client generation
## Documentation
See Twenty application documentation https://docs.twenty.com/developers/extend/capabilities/apps
## Prerequisites
- Node.js 24+ (recommended) and Yarn 4
- A Twenty workspace and an API key (create one at https://app.twenty.com/settings/api-webhooks)
@@ -64,11 +66,11 @@ yarn twenty app:uninstall
Control which example files are included when creating a new app:
| Flag | Behavior |
|------|----------|
| `-e, --exhaustive` | **(default)** Creates all example files without prompting |
| `-m, --minimal` | Creates only core files (`application-config.ts` and `default-role.ts`) |
| `-i, --interactive` | Prompts you to select which examples to include |
| Flag | Behavior |
| ------------------- | ----------------------------------------------------------------------- |
| `-e, --exhaustive` | **(default)** Creates all example files without prompting |
| `-m, --minimal` | Creates only core files (`application-config.ts` and `default-role.ts`) |
| `-i, --interactive` | Prompts you to select which examples to include |
```bash
# Default: all examples included
@@ -82,6 +84,7 @@ npx create-twenty-app@latest my-app -i
```
In interactive mode, you can pick from:
- **Example object** — a custom CRM object definition (`objects/example-object.ts`)
- **Example field** — a custom field on the example object (`fields/example-field.ts`)
- **Example logic function** — a server-side handler with HTTP trigger (`logic-functions/hello-world.ts`)
@@ -89,18 +92,21 @@ In interactive mode, you can pick from:
- **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`)
- **Integration test** — a vitest integration test verifying app installation (`__tests__/app-install.integration-test.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
- TypeScript configuration, Oxlint, package.json, .gitignore
- A prewired `twenty` script that delegates to the `twenty` CLI from twenty-sdk
**Example files (controlled by scaffolding mode):**
- `objects/example-object.ts` — Example custom object with a text field
- `fields/example-field.ts` — Example standalone field extending the example object
- `logic-functions/hello-world.ts` — Example logic function with HTTP trigger
@@ -108,16 +114,18 @@ In interactive mode, you can pick from:
- `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
- `__tests__/app-install.integration-test.ts` — Integration test that builds, installs, and verifies the app (includes `vitest.config.ts`, `tsconfig.spec.json`, and a setup file)
## 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, skills).
- Use `yarn twenty app:dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
- 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
Applications are currently stored in `twenty/packages/twenty-apps`.
You can share your application with all Twenty users:
@@ -142,9 +150,11 @@ git push
Our team reviews contributions for quality, security, and reusability before merging.
## Troubleshooting
- Auth prompts not appearing: run `yarn twenty auth:login` again and verify the API key permissions.
- Types not generated: ensure `yarn twenty app:dev` is running — it autogenerates the typed client.
## Contributing
- See our [GitHub](https://github.com/twentyhq/twenty)
- Join our [Discord](https://discord.gg/cx5n4Jzs57)
@@ -1,20 +0,0 @@
import baseConfig from '../../eslint.config.mjs';
export default [
...baseConfig,
{
ignores: ['**/dist/**'],
},
{
files: ['**/*.{js,jsx,ts,tsx}'],
rules: {
'prettier/prettier': 'error',
},
},
{
rules: {
'no-console': 'off',
},
ignores: ['src/**/*.ts', '!src/cli/**/*.ts'],
},
];
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "0.6.3",
"version": "0.6.4",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
+1 -13
View File
@@ -25,19 +25,7 @@
}
},
"typecheck": {},
"lint": {
"options": {
"lintFilePatterns": ["{projectRoot}/src/**/*.{ts,json}"],
"maxWarnings": 0
},
"configurations": {
"ci": {
"lintFilePatterns": ["{projectRoot}/src/**/*.{ts,json}"],
"maxWarnings": 0
},
"fix": {}
}
},
"lint": {},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
@@ -0,0 +1,19 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript"],
"categories": {
"correctness": "off"
},
"ignorePatterns": ["node_modules", "dist"],
"rules": {
"no-unused-vars": "off",
"typescript/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_"
}
],
"typescript/no-explicit-any": "off"
}
}
@@ -4,6 +4,7 @@
- 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
@@ -36,6 +36,17 @@ yarn twenty function:execute # Execute a function with JSON payload
yarn twenty app:uninstall # Uninstall app from workspace
```
## Integration Tests
If your project includes the example integration test (`src/__tests__/app-install.integration-test.ts`), you can run it with:
```bash
# Make sure a Twenty server is running at http://localhost:3000
yarn test
```
The test builds and installs the app, then verifies it appears in the applications list. Test configuration (API URL and API key) is defined in `vitest.config.ts`.
## LLMs instructions
Main docs and pitfalls are available in LLMS.md file.
@@ -1,29 +0,0 @@
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
},
},
];
@@ -27,5 +27,11 @@
"~/*": ["./*"]
}
},
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.spec.ts",
"**/*.integration-test.ts"
]
}
@@ -115,6 +115,7 @@ export class CreateAppCommand {
includeExampleView: false,
includeExampleNavigationMenuItem: false,
includeExampleSkill: false,
includeExampleIntegrationTest: false,
};
}
@@ -127,6 +128,7 @@ export class CreateAppCommand {
includeExampleView: true,
includeExampleNavigationMenuItem: true,
includeExampleSkill: true,
includeExampleIntegrationTest: true,
};
}
@@ -171,12 +173,19 @@ export class CreateAppCommand {
value: 'skill',
checked: true,
},
{
name: 'Integration test (vitest test verifying app installation)',
value: 'integrationTest',
checked: true,
},
],
},
]);
const includeField = selectedExamples.includes('field');
const includeView = selectedExamples.includes('view');
const includeExampleIntegrationTest =
selectedExamples.includes('integrationTest');
const includeObject =
selectedExamples.includes('object') || includeField || includeView;
@@ -197,6 +206,7 @@ export class CreateAppCommand {
includeExampleNavigationMenuItem:
selectedExamples.includes('navigationMenuItem'),
includeExampleSkill: selectedExamples.includes('skill'),
includeExampleIntegrationTest,
};
}
@@ -8,4 +8,5 @@ export type ExampleOptions = {
includeExampleView: boolean;
includeExampleNavigationMenuItem: boolean;
includeExampleSkill: boolean;
includeExampleIntegrationTest: boolean;
};
@@ -1,10 +1,10 @@
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';
import { join } from 'path';
import { GENERATED_DIR } from 'twenty-shared/application';
jest.mock('fs-extra', () => {
const actual = jest.requireActual('fs-extra');
@@ -25,6 +25,16 @@ const ALL_EXAMPLES: ExampleOptions = {
includeExampleView: true,
includeExampleNavigationMenuItem: true,
includeExampleSkill: true,
includeExampleIntegrationTest: true,
};
const SEED_TSCONFIG = {
compilerOptions: { paths: { 'src/*': ['./src/*'] } },
exclude: ['node_modules', 'dist', '**/*.integration-test.ts'],
};
const seedTsconfig = async (directory: string) => {
await fs.writeJson(join(directory, 'tsconfig.json'), SEED_TSCONFIG);
};
const NO_EXAMPLES: ExampleOptions = {
@@ -35,6 +45,7 @@ const NO_EXAMPLES: ExampleOptions = {
includeExampleFrontComponent: false,
includeExampleView: false,
includeExampleNavigationMenuItem: false,
includeExampleIntegrationTest: false,
};
describe('copyBaseApplicationProject', () => {
@@ -46,6 +57,7 @@ describe('copyBaseApplicationProject', () => {
`test-twenty-app-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
await fs.ensureDir(testAppDirectory);
await seedTsconfig(testAppDirectory);
jest.clearAllMocks();
});
@@ -149,11 +161,18 @@ describe('copyBaseApplicationProject', () => {
"import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role'",
);
expect(appConfigContent).toContain(
'export const APPLICATION_UNIVERSAL_IDENTIFIER',
);
expect(appConfigContent).toContain(
'universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER',
);
expect(appConfigContent).toContain("displayName: 'My Test App'");
expect(appConfigContent).toContain("description: 'A test application'");
expect(appConfigContent).toMatch(
/universalIdentifier: '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'/,
/APPLICATION_UNIVERSAL_IDENTIFIER =\s*'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'/,
);
expect(appConfigContent).toContain(
@@ -235,6 +254,7 @@ describe('copyBaseApplicationProject', () => {
it('should generate unique UUIDs for each application', async () => {
const firstAppDir = join(testAppDirectory, 'app1');
await fs.ensureDir(firstAppDir);
await seedTsconfig(firstAppDir);
await copyBaseApplicationProject({
appName: 'app-one',
appDisplayName: 'App One',
@@ -245,6 +265,7 @@ describe('copyBaseApplicationProject', () => {
const secondAppDir = join(testAppDirectory, 'app2');
await fs.ensureDir(secondAppDir);
await seedTsconfig(secondAppDir);
await copyBaseApplicationProject({
appName: 'app-two',
appDisplayName: 'App Two',
@@ -263,7 +284,7 @@ describe('copyBaseApplicationProject', () => {
);
const uuidRegex =
/universalIdentifier: '([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'/;
/APPLICATION_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 = firstAppConfig.match(uuidRegex)?.[1];
const secondUuid = secondAppConfig.match(uuidRegex)?.[1];
@@ -275,6 +296,7 @@ describe('copyBaseApplicationProject', () => {
it('should generate unique role UUIDs for each application', async () => {
const firstAppDir = join(testAppDirectory, 'app1');
await fs.ensureDir(firstAppDir);
await seedTsconfig(firstAppDir);
await copyBaseApplicationProject({
appName: 'app-one',
appDisplayName: 'App One',
@@ -285,6 +307,7 @@ describe('copyBaseApplicationProject', () => {
const secondAppDir = join(testAppDirectory, 'app2');
await fs.ensureDir(secondAppDir);
await seedTsconfig(secondAppDir);
await copyBaseApplicationProject({
appName: 'app-two',
appDisplayName: 'App Two',
@@ -355,6 +378,12 @@ describe('copyBaseApplicationProject', () => {
),
).toBe(true);
expect(
await fs.pathExists(
join(srcPath, '__tests__', 'app-install.integration-test.ts'),
),
).toBe(true);
// Install functions should always exist
expect(
await fs.pathExists(
@@ -428,6 +457,11 @@ describe('copyBaseApplicationProject', () => {
),
),
).toBe(false);
expect(
await fs.pathExists(
join(srcPath, '__tests__', 'app-install.integration-test.ts'),
),
).toBe(false);
});
});
@@ -446,6 +480,7 @@ describe('copyBaseApplicationProject', () => {
includeExampleFrontComponent: true,
includeExampleView: false,
includeExampleNavigationMenuItem: false,
includeExampleIntegrationTest: false,
},
});
@@ -483,6 +518,7 @@ describe('copyBaseApplicationProject', () => {
includeExampleFrontComponent: false,
includeExampleView: false,
includeExampleNavigationMenuItem: false,
includeExampleIntegrationTest: false,
},
});
@@ -540,6 +576,7 @@ describe('copyBaseApplicationProject', () => {
it('should generate unique UUIDs for example objects across apps', async () => {
const firstAppDir = join(testAppDirectory, 'app1');
await fs.ensureDir(firstAppDir);
await seedTsconfig(firstAppDir);
await copyBaseApplicationProject({
appName: 'app-one',
appDisplayName: 'App One',
@@ -550,6 +587,7 @@ describe('copyBaseApplicationProject', () => {
const secondAppDir = join(testAppDirectory, 'app2');
await fs.ensureDir(secondAppDir);
await seedTsconfig(secondAppDir);
await copyBaseApplicationProject({
appName: 'app-two',
appDisplayName: 'App Two',
@@ -738,6 +776,44 @@ describe('copyBaseApplicationProject', () => {
});
});
describe('integration test', () => {
it('should include vitest and test scripts in package.json when enabled', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
exampleOptions: ALL_EXAMPLES,
});
const packageJson = await fs.readJson(
join(testAppDirectory, 'package.json'),
);
expect(packageJson.scripts.test).toBe('vitest run');
expect(packageJson.scripts['test:watch']).toBe('vitest');
expect(packageJson.devDependencies.vitest).toBeDefined();
});
it('should not include vitest or test scripts when disabled', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
exampleOptions: NO_EXAMPLES,
});
const packageJson = await fs.readJson(
join(testAppDirectory, 'package.json'),
);
expect(packageJson.scripts.test).toBeUndefined();
expect(packageJson.scripts['test:watch']).toBeUndefined();
expect(packageJson.devDependencies.vitest).toBeUndefined();
});
});
describe('post-install logic function', () => {
it('should create post-install.ts with definePostInstallLogicFunction and typed payload', async () => {
await copyBaseApplicationProject({
@@ -0,0 +1,144 @@
import { scaffoldIntegrationTest } from '@/utils/test-template';
import * as fs from 'fs-extra';
import { tmpdir } from 'os';
import { join } from 'path';
describe('scaffoldIntegrationTest', () => {
let testAppDirectory: string;
let sourceFolderPath: string;
beforeEach(async () => {
testAppDirectory = join(
tmpdir(),
`test-twenty-app-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
sourceFolderPath = join(testAppDirectory, 'src');
await fs.ensureDir(sourceFolderPath);
await fs.writeJson(join(testAppDirectory, 'tsconfig.json'), {
compilerOptions: {
paths: { 'src/*': ['./src/*'] },
},
exclude: ['node_modules', 'dist', '**/*.integration-test.ts'],
});
});
afterEach(async () => {
if (testAppDirectory && (await fs.pathExists(testAppDirectory))) {
await fs.remove(testAppDirectory);
}
});
describe('integration test file', () => {
it('should create app-install.integration-test.ts with correct structure', async () => {
await scaffoldIntegrationTest({
appDirectory: testAppDirectory,
sourceFolderPath,
});
const testPath = join(
sourceFolderPath,
'__tests__',
'app-install.integration-test.ts',
);
expect(await fs.pathExists(testPath)).toBe(true);
const content = await fs.readFile(testPath, 'utf8');
expect(content).toContain(
"import { appBuild, appUninstall } from 'twenty-sdk/cli'",
);
expect(content).toContain(
"import { MetadataApiClient } from 'twenty-sdk/generated'",
);
expect(content).toContain(
"import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config'",
);
expect(content).toContain('TWENTY_TEST_API_KEY');
expect(content).toContain('assertServerIsReachable');
expect(content).toContain('appBuild');
expect(content).toContain('appUninstall');
expect(content).toContain('findManyApplications');
expect(content).toContain('APPLICATION_UNIVERSAL_IDENTIFIER');
});
});
describe('setup-test file', () => {
it('should create setup-test.ts with SDK config bootstrap', async () => {
await scaffoldIntegrationTest({
appDirectory: testAppDirectory,
sourceFolderPath,
});
const setupTestPath = join(
sourceFolderPath,
'__tests__',
'setup-test.ts',
);
expect(await fs.pathExists(setupTestPath)).toBe(true);
const content = await fs.readFile(setupTestPath, 'utf8');
expect(content).toContain('.twenty-sdk-test');
expect(content).toContain('config.json');
expect(content).toContain('process.env.TWENTY_API_URL');
expect(content).toContain('process.env.TWENTY_TEST_API_KEY');
});
});
describe('vitest config', () => {
it('should create vitest.config.ts with env vars and setup file', async () => {
await scaffoldIntegrationTest({
appDirectory: testAppDirectory,
sourceFolderPath,
});
const vitestConfigPath = join(testAppDirectory, 'vitest.config.ts');
expect(await fs.pathExists(vitestConfigPath)).toBe(true);
const content = await fs.readFile(vitestConfigPath, 'utf8');
expect(content).toContain('TWENTY_TEST_API_KEY');
expect(content).toContain('TWENTY_API_URL');
expect(content).toContain('setup-test.ts');
expect(content).toContain('tsconfig.spec.json');
expect(content).toContain('integration-test.ts');
});
});
describe('tsconfig.spec.json', () => {
it('should create tsconfig.spec.json extending the base tsconfig', async () => {
await scaffoldIntegrationTest({
appDirectory: testAppDirectory,
sourceFolderPath,
});
const tsconfigSpecPath = join(testAppDirectory, 'tsconfig.spec.json');
expect(await fs.pathExists(tsconfigSpecPath)).toBe(true);
const tsconfigSpec = await fs.readJson(tsconfigSpecPath);
expect(tsconfigSpec.extends).toBe('./tsconfig.json');
expect(tsconfigSpec.compilerOptions.composite).toBe(true);
expect(tsconfigSpec.include).toContain('src/**/*.ts');
expect(tsconfigSpec.exclude).not.toContain('**/*.integration-test.ts');
});
it('should add a reference to tsconfig.spec.json in tsconfig.json', async () => {
await scaffoldIntegrationTest({
appDirectory: testAppDirectory,
sourceFolderPath,
});
const tsconfig = await fs.readJson(
join(testAppDirectory, 'tsconfig.json'),
);
expect(tsconfig.references).toEqual([{ path: './tsconfig.spec.json' }]);
});
});
});
@@ -1,9 +1,10 @@
import * as fs from 'fs-extra';
import { join } from 'path';
import { v4 } from 'uuid';
import { ASSETS_DIR } from 'twenty-shared/application';
import { v4 } from 'uuid';
import { type ExampleOptions } from '@/types/scaffolding-options';
import { scaffoldIntegrationTest } from '@/utils/test-template';
import createTwentyAppPackageJson from 'package.json';
const SRC_FOLDER = 'src';
@@ -23,7 +24,11 @@ export const copyBaseApplicationProject = async ({
}) => {
await fs.copy(join(__dirname, './constants/base-application'), appDirectory);
await createPackageJson({ appName, appDirectory });
await createPackageJson({
appName,
appDirectory,
includeExampleIntegrationTest: exampleOptions.includeExampleIntegrationTest,
});
await createGitignore(appDirectory);
@@ -98,6 +103,13 @@ export const copyBaseApplicationProject = async ({
});
}
if (exampleOptions.includeExampleIntegrationTest) {
await scaffoldIntegrationTest({
appDirectory,
sourceFolderPath,
});
}
await createDefaultPreInstallFunction({
appDirectory: sourceFolderPath,
fileFolder: 'logic-functions',
@@ -167,6 +179,7 @@ yarn-error.log*
# typescript
*.tsbuildinfo
*.d.ts
`;
await fs.writeFile(join(appDirectory, '.gitignore'), gitignoreContent);
@@ -509,11 +522,16 @@ const createApplicationConfig = async ({
fileFolder?: string;
fileName: string;
}) => {
const universalIdentifier = v4();
const content = `import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
export const APPLICATION_UNIVERSAL_IDENTIFIER =
'${universalIdentifier}';
export default defineApplication({
universalIdentifier: '${v4()}',
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
displayName: '${displayName}',
description: '${description ?? ''}',
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
@@ -527,10 +545,34 @@ export default defineApplication({
const createPackageJson = async ({
appName,
appDirectory,
includeExampleIntegrationTest,
}: {
appName: string;
appDirectory: string;
includeExampleIntegrationTest: boolean;
}) => {
const scripts: Record<string, string> = {
twenty: 'twenty',
lint: 'oxlint -c .oxlintrc.json .',
'lint:fix': 'oxlint --fix -c .oxlintrc.json .',
};
const devDependencies: Record<string, string> = {
typescript: '^5.9.3',
'@types/node': '^24.7.2',
'@types/react': '^18.2.0',
react: '^18.2.0',
oxlint: '^0.16.0',
'twenty-sdk': createTwentyAppPackageJson.version,
};
if (includeExampleIntegrationTest) {
scripts.test = 'vitest run';
scripts['test:watch'] = 'vitest';
devDependencies.vitest = '^3.1.1';
devDependencies['vite-tsconfig-paths'] = '^4.2.1';
}
const packageJson = {
name: appName,
version: '0.1.0',
@@ -541,21 +583,8 @@ const createPackageJson = async ({
yarn: '>=4.0.2',
},
packageManager: 'yarn@4.9.2',
scripts: {
twenty: 'twenty',
lint: 'eslint',
'lint:fix': 'eslint --fix',
},
dependencies: {},
devDependencies: {
'twenty-sdk': createTwentyAppPackageJson.version,
typescript: '^5.9.3',
'@types/node': '^24.7.2',
'@types/react': '^18.2.0',
react: '^18.2.0',
eslint: '^9.32.0',
'typescript-eslint': '^8.50.0',
},
scripts,
devDependencies,
};
await fs.writeFile(
@@ -0,0 +1,226 @@
import * as fs from 'fs-extra';
import { join } from 'path';
const SEED_API_KEY =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik';
export const scaffoldIntegrationTest = async ({
appDirectory,
sourceFolderPath,
}: {
appDirectory: string;
sourceFolderPath: string;
}) => {
await createIntegrationTest({
appDirectory: sourceFolderPath,
fileFolder: '__tests__',
fileName: 'app-install.integration-test.ts',
});
await createSetupTest({
appDirectory: sourceFolderPath,
fileFolder: '__tests__',
fileName: 'setup-test.ts',
});
await createVitestConfig(appDirectory);
await createTsconfigSpec(appDirectory);
};
const createVitestConfig = async (appDirectory: string) => {
const content = `import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [
tsconfigPaths({
projects: ['tsconfig.spec.json'],
ignoreConfigErrors: true,
}),
],
test: {
testTimeout: 120_000,
hookTimeout: 120_000,
include: ['src/**/*.integration-test.ts'],
setupFiles: ['src/__tests__/setup-test.ts'],
env: {
TWENTY_API_URL: 'http://localhost:3000',
TWENTY_TEST_API_KEY:
'${SEED_API_KEY}',
},
},
});
`;
await fs.writeFile(join(appDirectory, 'vitest.config.ts'), content);
};
const createTsconfigSpec = async (appDirectory: string) => {
const tsconfigSpec = {
extends: './tsconfig.json',
compilerOptions: {
composite: true,
types: ['vitest/globals'],
},
include: ['src/**/*.ts', 'src/**/*.tsx'],
exclude: ['node_modules', 'dist'],
};
await fs.writeFile(
join(appDirectory, 'tsconfig.spec.json'),
JSON.stringify(tsconfigSpec, null, 2),
);
const tsconfigPath = join(appDirectory, 'tsconfig.json');
const tsconfig = await fs.readJson(tsconfigPath);
tsconfig.references = [{ path: './tsconfig.spec.json' }];
await fs.writeFile(tsconfigPath, JSON.stringify(tsconfig, null, 2));
};
const createSetupTest = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const content = `import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { beforeAll } from 'vitest';
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
beforeAll(() => {
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
const configFile = {
profiles: {
default: {
apiUrl: process.env.TWENTY_API_URL,
apiKey: process.env.TWENTY_TEST_API_KEY,
},
},
};
fs.writeFileSync(
path.join(TEST_CONFIG_DIR, 'config.json'),
JSON.stringify(configFile, null, 2),
);
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createIntegrationTest = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const content = `import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
import { appBuild, appUninstall } from 'twenty-sdk/cli';
import { MetadataApiClient } from 'twenty-sdk/generated';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const APP_PATH = process.cwd();
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:3000';
const assertServerIsReachable = async () => {
let response: Response;
try {
response = await fetch(\`\${TWENTY_API_URL}/healthz\`);
} catch {
throw new Error(
\`Twenty server is not reachable at \${TWENTY_API_URL}. \` +
'Make sure the server is running before executing integration tests.',
);
}
if (!response.ok) {
throw new Error(\`Server at \${TWENTY_API_URL} returned \${response.status}\`);
}
};
describe('App installation', () => {
let appInstalled = false;
beforeAll(async () => {
await assertServerIsReachable();
const buildResult = await appBuild({
appPath: APP_PATH,
onProgress: (message: string) => console.log(\`[build] \${message}\`),
});
if (!buildResult.success) {
throw new Error(
\`App build failed: \${buildResult.error?.message ?? 'Unknown error'}\`,
);
}
appInstalled = true;
});
afterAll(async () => {
if (!appInstalled) {
return;
}
const uninstallResult = await appUninstall({ appPath: APP_PATH });
if (!uninstallResult.success) {
console.warn(
\`App uninstall failed: \${uninstallResult.error?.message ?? 'Unknown error'}\`,
);
}
});
it('should find the installed app in the applications list', async () => {
const apiKey = process.env.TWENTY_TEST_API_KEY;
if (!apiKey) {
throw new Error(
'No API key found. Set TWENTY_TEST_API_KEY in your vitest config env.',
);
}
const metadataClient = new MetadataApiClient({
url: \`\${TWENTY_API_URL}/metadata\`,
headers: {
Authorization: \`Bearer \${apiKey}\`,
},
});
const result = await metadataClient.query({
findManyApplications: {
id: true,
name: true,
universalIdentifier: true,
},
});
const installedApp = result.findManyApplications.find(
(application: { universalIdentifier: string }) =>
application.universalIdentifier ===
APPLICATION_UNIVERSAL_IDENTIFIER,
);
expect(installedApp).toBeDefined();
});
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
+2 -1
View File
@@ -23,5 +23,6 @@
"**/__mocks__/**/*",
"vite.config.ts",
"jest.config.mjs"
]
],
"exclude": ["src/constants/base-application/vitest.config.ts"]
}
@@ -0,0 +1,38 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript", "import", "unicorn"],
"categories": {
"correctness": "off"
},
"ignorePatterns": ["node_modules"],
"rules": {
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
"no-console": ["warn", { "allow": ["group", "groupCollapsed", "groupEnd"] }],
"no-control-regex": "off",
"no-debugger": "error",
"no-duplicate-imports": "error",
"no-undef": "off",
"no-unused-vars": "off",
"no-redeclare": "off",
"import/no-duplicates": "error",
"typescript/no-redeclare": "error",
"typescript/ban-ts-comment": "error",
"typescript/consistent-type-imports": ["error", {
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}],
"typescript/explicit-function-return-type": "off",
"typescript/explicit-module-boundary-types": "off",
"typescript/no-empty-object-type": ["error", {
"allowInterfaces": "with-single-extends"
}],
"typescript/no-empty-function": "off",
"typescript/no-explicit-any": "off",
"typescript/no-unused-vars": ["warn", {
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}]
}
}
@@ -1,29 +0,0 @@
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
},
},
];
@@ -10,8 +10,8 @@
"packageManager": "yarn@4.9.2",
"scripts": {
"twenty": "twenty",
"lint": "eslint",
"lint:fix": "eslint --fix"
"lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
},
"dependencies": {
"twenty-sdk": "latest"
@@ -19,9 +19,8 @@
"devDependencies": {
"@types/node": "^24.7.2",
"@types/react": "^18.2.0",
"eslint": "^9.32.0",
"oxlint": "^0.16.0",
"react": "^18.2.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.50.0"
"typescript": "^5.9.3"
}
}
@@ -1,7 +1,6 @@
import { defineLogicFunction, RoutePayload } from "twenty-sdk";
import { MetadataApiClient } from 'twenty-sdk/generated';
export const OAUTH_TOKEN_PAIRS_PATH = '/oauth/token-pairs';
type ApolloTokenResponse = {
@@ -53,16 +52,8 @@ const handler = async (event: RoutePayload): Promise<any> => {
const apolloClientSecret = process.env.APOLLO_CLIENT_SECRET ?? '';
const applicationId = process.env.APPLICATION_ID ?? '';
const metadataClient = new MetadataApiClient({});
const tokenPairs = await getAuthenticationTokenPairs(
code,
apolloClientId,
@@ -5,8 +5,6 @@ import {
} from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/generated';
type CompanyRecord = {
id: string;
name?: string;
@@ -38,8 +36,6 @@ type ApolloEnrichResponse = {
organization?: ApolloOrganization;
};
const extractDomain = (
domainName?: CompanyRecord['domainName'],
): string | undefined => {
@@ -172,7 +168,6 @@ const updateCompanyInTwenty = async (
}
};
type CompanyUpdateEvent = DatabaseEventPayload<
ObjectRecordUpdateEvent<CompanyRecord>
>;
@@ -181,7 +176,6 @@ const handler = async (
event: CompanyUpdateEvent,
): Promise<object | undefined> => {
const { recordId, properties } = event;
const { after: companyAfter } = properties;
@@ -206,7 +200,6 @@ const handler = async (
return { skipped: true, reason: 'no enrichment data to apply' };
}
await updateCompanyInTwenty(recordId, updateData);
const result = {
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,40 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript", "import", "unicorn"],
"categories": {
"correctness": "off"
},
"ignorePatterns": ["node_modules"],
"rules": {
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
"no-console": ["warn", { "allow": ["group", "groupCollapsed", "groupEnd"] }],
"no-control-regex": "off",
"no-debugger": "error",
"no-duplicate-imports": "error",
"no-undef": "off",
"no-unused-vars": "off",
"no-redeclare": "off",
"import/no-duplicates": "error",
"typescript/no-redeclare": "error",
"typescript/ban-ts-comment": "error",
"typescript/consistent-type-imports": ["error", {
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}],
"typescript/explicit-function-return-type": "off",
"typescript/explicit-module-boundary-types": "off",
"typescript/no-empty-object-type": ["error", {
"allowInterfaces": "with-single-extends"
}],
"typescript/no-empty-function": "off",
"typescript/no-explicit-any": "off",
"typescript/no-unused-vars": ["warn", {
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}]
}
}
@@ -26,21 +26,6 @@
"typecheck": {
"dependsOn": ["^build"]
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": [
"{options.outputFile}"
],
"options": {
"lintFilePatterns": [
"packages/twenty-apps/community/fireflies/**/*.{ts,tsx,js,jsx}"
]
},
"configurations": {
"fix": {
"fix": true
}
}
}
"lint": {}
}
}
@@ -5,7 +5,7 @@
* Usage: yarn setup:fields
*/
/* eslint-disable no-console */
/* oxlint-disable no-console */
import * as dotenv from 'dotenv';
import * as path from 'path';
@@ -1,4 +1,4 @@
/* eslint-disable no-console */
/* oxlint-disable no-console */
import * as dotenv from 'dotenv';
import * as path from 'path';
import { fileURLToPath } from 'url';
@@ -1,4 +1,4 @@
/* eslint-disable no-console */
/* oxlint-disable no-console */
import * as dotenv from 'dotenv';
import * as path from 'path';
import { fileURLToPath } from 'url';
@@ -1,4 +1,4 @@
/* eslint-disable no-console */
/* oxlint-disable no-console */
/**
* Fetch historical Fireflies meetings and insert into Twenty.
*
@@ -219,4 +219,3 @@ main().catch((error) => {
process.exit(1);
});
@@ -1,4 +1,4 @@
/* eslint-disable no-console */
/* oxlint-disable no-console */
/**
* Fetch a Fireflies meeting by ID and insert it into Twenty using the same path
* as the webhook handler.
@@ -1,4 +1,4 @@
/* eslint-disable no-console */
/* oxlint-disable no-console */
/**
* Test script for Fireflies webhook against local Twenty instance
*
@@ -81,4 +81,3 @@ describe('HistoricalImporter', () => {
});
});
@@ -158,4 +158,3 @@ export class HistoricalImporter {
}
}
@@ -72,7 +72,7 @@ export class AppLogger {
debug(message: string, ...args: unknown[]): void {
this.captureLog('debug', message, ...args);
if (this.shouldLog('debug')) {
// eslint-disable-next-line no-console
// oxlint-disable-next-line no-console
console.log(`[${this.context}] ${message}`, ...args);
}
}
@@ -80,7 +80,7 @@ export class AppLogger {
info(message: string, ...args: unknown[]): void {
this.captureLog('info', message, ...args);
if (this.shouldLog('info')) {
// eslint-disable-next-line no-console
// oxlint-disable-next-line no-console
console.log(`[${this.context}] ${message}`, ...args);
}
}
@@ -88,7 +88,7 @@ export class AppLogger {
warn(message: string, ...args: unknown[]): void {
this.captureLog('warn', message, ...args);
if (this.shouldLog('warn')) {
// eslint-disable-next-line no-console
// oxlint-disable-next-line no-console
console.warn(`[${this.context}] ${message}`, ...args);
}
}
@@ -96,7 +96,7 @@ export class AppLogger {
error(message: string, ...args: unknown[]): void {
this.captureLog('error', message, ...args);
if (this.shouldLog('error')) {
// eslint-disable-next-line no-console
// oxlint-disable-next-line no-console
console.error(`[${this.context}] ${message}`, ...args);
}
}
@@ -104,7 +104,7 @@ export class AppLogger {
// For fatal errors, security issues, or data corruption - always visible
critical(message: string, ...args: unknown[]): void {
this.captureLog('error', `CRITICAL: ${message}`, ...args);
// eslint-disable-next-line no-console
// oxlint-disable-next-line no-console
console.error(`[${this.context}] CRITICAL: ${message}`, ...args);
}
@@ -1,3 +1,2 @@
export { Meeting } from './meeting';
@@ -330,7 +330,6 @@ export class WebhookHandler {
}
}
private async createFailedMeetingRecord(params: unknown, error: string): Promise<void> {
try {
const twentyApiKey = process.env.TWENTY_API_KEY || '';
@@ -0,0 +1,54 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "typescript", "import", "unicorn"],
"categories": {
"correctness": "off"
},
"ignorePatterns": [
"node_modules"
],
"rules": {
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
"no-console": ["warn", { "allow": ["group", "groupCollapsed", "groupEnd"] }],
"no-control-regex": "off",
"no-debugger": "error",
"no-duplicate-imports": "error",
"no-undef": "off",
"no-unused-vars": "off",
"no-redeclare": "off",
"import/no-duplicates": "error",
"react/no-unescaped-entities": "off",
"react/prop-types": "off",
"react/jsx-key": "off",
"react/display-name": "off",
"react/jsx-uses-react": "off",
"react/react-in-jsx-scope": "off",
"react/jsx-no-useless-fragment": "off",
"react/jsx-props-no-spreading": ["error", { "explicitSpread": "ignore" }],
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"typescript/no-redeclare": "error",
"typescript/ban-ts-comment": "error",
"typescript/consistent-type-imports": ["error", {
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}],
"typescript/explicit-function-return-type": "off",
"typescript/explicit-module-boundary-types": "off",
"typescript/no-empty-object-type": ["error", {
"allowInterfaces": "with-single-extends"
}],
"typescript/no-empty-function": "off",
"typescript/no-explicit-any": "off",
"typescript/no-unused-vars": ["warn", {
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}]
}
}
@@ -1,69 +0,0 @@
import typescriptParser from '@typescript-eslint/parser';
import path from 'path';
import { fileURLToPath } from 'url';
import reactConfig from '../../../../twenty-eslint-rules/eslint.config.react.mjs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default [
// Extend shared React configuration
...reactConfig,
// Global ignores
{
ignores: [
'**/node_modules/**',
],
},
// TypeScript project-specific configuration
{
files: ['**/*.{ts,tsx}'],
languageOptions: {
parser: typescriptParser,
parserOptions: {
project: [path.resolve(__dirname, 'tsconfig.*.json')],
ecmaFeatures: {
jsx: true,
},
},
},
rules: {
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{
sourceTag: 'scope:sdk',
onlyDependOnLibsWithTags: ['scope:sdk'],
},
{
sourceTag: 'scope:shared',
onlyDependOnLibsWithTags: ['scope:shared'],
},
{
sourceTag: 'scope:backend',
onlyDependOnLibsWithTags: ['scope:shared', 'scope:backend'],
},
{
sourceTag: 'scope:frontend',
onlyDependOnLibsWithTags: ['scope:shared', 'scope:frontend'],
},
{
sourceTag: 'scope:zapier',
onlyDependOnLibsWithTags: ['scope:shared'],
},
{
sourceTag: 'scope:browser-extension',
onlyDependOnLibsWithTags: ['scope:twenty-ui', 'scope:browser-extension']
}
],
},
],
}
},
];
@@ -43,7 +43,6 @@ export default defineContentScript({
},
});
ctx.addEventListener(window, 'wxt:locationchange', ({newUrl, }) => {
const injectedBtn = document.querySelector('[data-id="twenty-btn"]');
if(personPattern.includes(newUrl) && !injectedBtn) ui.mount();
@@ -8,7 +8,6 @@ const StyledButton = styled.button`
--text-color: #0a66c2;
--bg-color: #00000000;
font-size: ${({theme}) => theme.spacing(3.5)};
font-weight: ${({theme}) => theme.font.weight.semiBold};
font-family: ${({theme}) => theme.font.family};
@@ -26,7 +25,6 @@ const StyledButton = styled.button`
transition-timing-function: cubic-bezier(.4, 0, .2, 1);
transition-duration: 167ms;
&:hover {
background-color: var(--hover-bg-color);
color: var(--hover-color);
@@ -1,6 +1,5 @@
import { ThemeProvider } from '@emotion/react';
import type React from 'react';
import { THEME_DARK, ThemeContextProvider } from 'twenty-ui/theme';
import { ColorSchemeProvider } from 'twenty-ui/theme-constants';
type ThemeContextProps = {
children: React.ReactNode;
@@ -8,10 +7,6 @@ type ThemeContextProps = {
export const ThemeContext = ({ children }: ThemeContextProps) => {
return (
<ThemeProvider theme={THEME_DARK}>
<ThemeContextProvider theme={THEME_DARK}>
{children}
</ThemeContextProvider>
</ThemeProvider>
<ColorSchemeProvider colorScheme="dark">{children}</ColorSchemeProvider>
);
};
@@ -1,5 +0,0 @@
import type { ThemeType } from 'twenty-ui/theme';
declare module '@emotion/react' {
export interface Theme extends ThemeType {}
}
@@ -1,2 +0,0 @@
TWENTY_API_KEY=<SET_YOUR_TWENTY_API_KEY>
TWENTY_API_URL=<SET_YOUR_TWENTY_API_URL>
+37 -2
View File
@@ -1,3 +1,38 @@
.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
*.d.ts
+1
View File
@@ -0,0 +1 @@
24.5.0
@@ -0,0 +1,38 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript", "import", "unicorn"],
"categories": {
"correctness": "off"
},
"ignorePatterns": ["node_modules"],
"rules": {
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
"no-console": ["warn", { "allow": ["group", "groupCollapsed", "groupEnd"] }],
"no-control-regex": "off",
"no-debugger": "error",
"no-duplicate-imports": "error",
"no-undef": "off",
"no-unused-vars": "off",
"no-redeclare": "off",
"import/no-duplicates": "error",
"typescript/no-redeclare": "error",
"typescript/ban-ts-comment": "error",
"typescript/consistent-type-imports": ["error", {
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}],
"typescript/explicit-function-return-type": "off",
"typescript/explicit-module-boundary-types": "off",
"typescript/no-empty-object-type": ["error", {
"allowInterfaces": "with-single-extends"
}],
"typescript/no-empty-function": "off",
"typescript/no-explicit-any": "off",
"typescript/no-unused-vars": ["warn", {
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}]
}
}
+12
View File
@@ -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.
+47 -26
View File
@@ -1,41 +1,62 @@
# Hello world
This is a [Twenty](https://twenty.com) application project bootstrapped with [`create-twenty-app`](https://www.npmjs.com/package/create-twenty-app).
A minimal hello-world application built with an alpha version of the twenty-cli.
## Getting Started
⚠️ Since this project uses an early alpha release, expect breaking changes and evolving features.
This example will gradually gain complexity and capabilities as the twenty-cli matures with future updates.
## Requirements
- twenty-cli `npm install -g twenty-cli`
- an `apiKey`. Go to `/settings/api-webhooks` to generate one
## Install to your Twenty workspace
First, authenticate to your workspace:
```bash
cp .env.example .env
yarn twenty auth:login
```
- replace `<SET_YOUR_TWENTY_API_KEY>` and `<SET_YOUR_TWENTY_API_URL>` accordingly
Then, start development mode to sync your app and watch for changes:
```bash
twenty auth login
twenty app sync
yarn twenty app:dev
```
## What it does
- creates a new object `postCard` with a `name` field
- creates a serverless function `create-new-post-card`
- two triggers on `create-new-post-card`:
- one route trigger on `GET` `/post-card/create?recipient=RecipientName`
- one databaseEvent trigger on `people.created` events
Open your Twenty instance and go to `/settings/applications` section to see the result.
## Development
## Available Commands
Run dev mode and see application updates on your workspace instantly
Run `yarn twenty help` to list all available commands. Common commands:
```bash
twenty app dev
# 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
```
## Integration Tests
If your project includes the example integration test (`src/__tests__/app-install.integration-test.ts`), you can run it with:
```bash
# Make sure a Twenty server is running at http://localhost:3000
yarn test
```
The test builds and installs the app, then verifies it appears in the applications list. Test configuration (API URL and API key) is defined in `vitest.config.ts`.
## 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!
+13 -11
View File
@@ -1,5 +1,5 @@
{
"name": "hello-world",
"name": "@twentyhq/hello-world",
"version": "0.2.2",
"license": "MIT",
"engines": {
@@ -10,17 +10,19 @@
"packageManager": "yarn@4.9.2",
"scripts": {
"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.6.3"
"lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint --fix -c .oxlintrc.json .",
"test": "vitest run",
"test:watch": "vitest"
},
"devDependencies": {
"@types/node": "^24.7.2"
"@types/node": "^24.7.2",
"@types/react": "^18.2.0",
"oxlint": "^0.16.0",
"react": "^18.2.0",
"twenty-sdk": "0.6.3",
"typescript": "^5.9.3",
"vite-tsconfig-paths": "^4.2.1",
"vitest": "^3.1.1"
}
}
@@ -0,0 +1,92 @@
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
import { appBuild, appUninstall } from 'twenty-sdk/cli';
import { MetadataApiClient } from 'twenty-sdk/generated';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const APP_PATH = process.cwd();
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:3000';
const assertServerIsReachable = async () => {
let response: Response;
try {
response = await fetch(`${TWENTY_API_URL}/healthz`);
} catch {
throw new Error(
`Twenty server is not reachable at ${TWENTY_API_URL}. ` +
'Make sure the server is running before executing integration tests.',
);
}
if (!response.ok) {
throw new Error(`Server at ${TWENTY_API_URL} returned ${response.status}`);
}
};
describe('App installation', () => {
let appInstalled = false;
beforeAll(async () => {
await assertServerIsReachable();
const buildResult = await appBuild({
appPath: APP_PATH,
onProgress: (message: string) => console.log(`[build] ${message}`),
});
if (!buildResult.success) {
throw new Error(
`App build failed: ${buildResult.error?.message ?? 'Unknown error'}`,
);
}
appInstalled = true;
});
afterAll(async () => {
if (!appInstalled) {
return;
}
const uninstallResult = await appUninstall({ appPath: APP_PATH });
if (!uninstallResult.success) {
console.warn(
`App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
);
}
});
it('should find the installed app in the applications list', async () => {
const apiKey = process.env.TWENTY_TEST_API_KEY;
if (!apiKey) {
throw new Error(
'No API key found. Set TWENTY_TEST_API_KEY in your vitest config env.',
);
}
const metadataClient = new MetadataApiClient({
url: `${TWENTY_API_URL}/metadata`,
headers: {
Authorization: `Bearer ${apiKey}`,
},
});
const result = await metadataClient.query({
findManyApplications: {
id: true,
name: true,
universalIdentifier: true,
},
});
const installedApp = result.findManyApplications.find(
(application: { universalIdentifier: string }) =>
application.universalIdentifier ===
APPLICATION_UNIVERSAL_IDENTIFIER,
);
expect(installedApp).toBeDefined();
});
});
@@ -0,0 +1,24 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { beforeAll } from 'vitest';
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
beforeAll(() => {
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
const configFile = {
profiles: {
default: {
apiUrl: process.env.TWENTY_API_URL,
apiKey: process.env.TWENTY_TEST_API_KEY,
},
},
};
fs.writeFileSync(
path.join(TEST_CONFIG_DIR, 'config.json'),
JSON.stringify(configFile, null, 2),
);
});
@@ -1,55 +0,0 @@
import {
defineLogicFunction,
type CronPayload,
type DatabaseEventPayload,
type ObjectRecordCreateEvent,
} from 'twenty-sdk';
import { CoreApiClient as Twenty, type CoreSchema } from 'twenty-sdk/generated';
type CreateNewPostCardParams =
| { name?: string }
| DatabaseEventPayload<ObjectRecordCreateEvent<CoreSchema.Person>>
| CronPayload;
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 createPostCard = await client.mutation({
createPostCard: {
__args: {
data: {
name,
},
},
name: true,
id: true,
},
});
console.log('createPostCard result', createPostCard);
return createPostCard;
};
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'create-new-post-card',
timeoutSeconds: 2,
handler,
httpRouteTriggerSettings: {
path: '/post-card/create',
httpMethod: 'GET',
isAuthRequired: false,
},
cronTriggerSettings: {
pattern: '0 0 1 1 *',
},
databaseEventTriggerSettings: {
eventName: 'person.created',
},
});
@@ -0,0 +1,12 @@
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
export const APPLICATION_UNIVERSAL_IDENTIFIER =
'1badae7c-8a42-4dea-b4b8-3c56e77c2f9a';
export default defineApplication({
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
displayName: 'Hello world',
description: '',
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
@@ -1,17 +0,0 @@
import { defineApplication } from 'twenty-sdk';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
displayName: 'Hello World',
description: 'A simple hello world app',
icon: 'IconWorld',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
description: 'Default recipient name for postcards',
value: 'Alex Karp',
isSecret: false,
},
},
defaultRoleUniversalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
});
@@ -0,0 +1,11 @@
import { defineField, FieldType } from 'twenty-sdk';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/objects/example-object';
export default defineField({
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
universalIdentifier: '2c503a0d-36c9-49ec-b82f-4fafe0eb6f47',
type: FieldType.NUMBER,
name: 'priority',
label: 'Priority',
description: 'Priority level for the example item (1-10)',
});
@@ -0,0 +1,17 @@
import { defineFrontComponent } from 'twenty-sdk';
export const HelloWorld = () => {
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>Hello, World!</h1>
<p>This is your first front component.</p>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: '26c17445-fbfb-4b34-99d6-f461e734ca97',
name: 'hello-world-front-component',
description: 'A sample front component',
component: HelloWorld,
});
@@ -0,0 +1,18 @@
import { defineLogicFunction } from 'twenty-sdk';
const handler = async (): Promise<{ message: string }> => {
return { message: 'Hello, World!' };
};
export default defineLogicFunction({
universalIdentifier: '4f0b7137-1399-4e50-ac00-3c3bb2555c38',
name: 'hello-world-logic-function',
description: 'A simple logic function',
timeoutSeconds: 5,
handler,
httpRouteTriggerSettings: {
path: '/hello-world-logic-function',
httpMethod: 'GET',
isAuthRequired: false,
},
});
@@ -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: 'c1410017-8536-42aa-a188-4bfc5a1c3dae',
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: '68d005d4-1110-4fa0-8227-71e06d6b9f30',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
@@ -0,0 +1,14 @@
import { defineNavigationMenuItem } from 'twenty-sdk';
export default defineNavigationMenuItem({
universalIdentifier: '574a895f-1511-4b38-9d28-d6b8436738ff',
name: 'example-navigation-menu-item',
icon: 'IconList',
position: 0,
// Link to a view:
// viewUniversalIdentifier: '...',
// Or link to an object:
// targetObjectUniversalIdentifier: '...',
// Or link to an external URL:
// link: 'https://example.com',
});
@@ -0,0 +1,28 @@
import { defineObject, FieldType } from 'twenty-sdk';
export const EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER =
'b75cfe84-18ce-47da-812a-53e25ee094af';
export const NAME_FIELD_UNIVERSAL_IDENTIFIER =
'6ab9c690-06ce-455e-a2c9-8067a9747f96';
export default defineObject({
universalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
nameSingular: 'exampleItem',
namePlural: 'exampleItems',
labelSingular: 'Example item',
labelPlural: 'Example items',
description: 'A sample custom object',
icon: 'IconBox',
labelIdentifierFieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
fields: [
{
universalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.TEXT,
name: 'name',
label: 'Name',
description: 'Name of the example item',
icon: 'IconAbc',
},
],
});
@@ -1,89 +0,0 @@
import { defineObject, FieldType } from 'twenty-sdk';
const POST_CARD_STATUS = {
DRAFT: 'DRAFT',
SENT: 'SENT',
DELIVERED: 'DELIVERED',
RETURNED: 'RETURNED',
} as const;
export default defineObject({
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
nameSingular: 'postCard',
namePlural: 'postCards',
labelSingular: 'Post card',
labelPlural: 'Post cards',
description: 'A post card object',
icon: 'IconMail',
fields: [
{
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
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,
},
],
});
@@ -0,0 +1,14 @@
import { defineRole } from 'twenty-sdk';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'f14afc30-f2fa-4f70-9b12-903c5f852225';
export default defineRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Hello world default function role',
description: 'Hello world default function role',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: false,
});
@@ -1,33 +0,0 @@
import { defineRole, PermissionFlag } from 'twenty-sdk';
export default defineRole({
universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectUniversalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
},
],
fieldPermissions: [
{
objectUniversalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
fieldUniversalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
canReadFieldValue: false,
canUpdateFieldValue: false,
},
],
permissionFlags: [PermissionFlag.APPLICATIONS],
});
@@ -0,0 +1,13 @@
import { defineSkill } from 'twenty-sdk';
export const EXAMPLE_SKILL_UNIVERSAL_IDENTIFIER =
'4f00dd76-c07b-4d55-a43a-7f17e7f6440a';
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.',
});
@@ -0,0 +1,10 @@
import { defineView } from 'twenty-sdk';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/objects/example-object';
export default defineView({
universalIdentifier: 'e574b32c-c058-492a-8a5c-780b844a8735',
name: 'example-view',
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
position: 0,
});
+28 -4
View File
@@ -5,21 +5,45 @@
"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,
"target": "es2018",
"module": "esnext",
"lib": ["es2020", "dom"],
"lib": [
"es2020",
"dom"
],
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"resolveJsonModule": true,
"paths": {
"src/*": [
"./src/*"
],
"~/*": [
"./*"
]
}
},
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
}
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.spec.ts",
"**/*.integration-test.ts"
],
"references": [
{
"path": "./tsconfig.spec.json"
}
]
}
@@ -0,0 +1,17 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"composite": true,
"types": [
"vitest/globals"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx"
],
"exclude": [
"node_modules",
"dist"
]
}
@@ -0,0 +1,22 @@
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [
tsconfigPaths({
projects: ['tsconfig.spec.json'],
ignoreConfigErrors: true,
}),
],
test: {
testTimeout: 120_000,
hookTimeout: 120_000,
include: ['src/**/*.integration-test.ts'],
setupFiles: ['src/__tests__/setup-test.ts'],
env: {
TWENTY_API_URL: 'http://localhost:3000',
TWENTY_TEST_API_KEY:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik',
},
},
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,42 @@
# 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
# Remove once prod ready
.twenty
@@ -0,0 +1 @@
24.5.0

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