a159a68e2ce0a3de9135a306d2a96e8833bcb6b2
210 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
34b927ff23 |
feat(public-domain): bind public domains to apps + reorganize settings (#20360)
## Summary - **Public domains can now be bound to a specific app.** When a request hits an app-bound public domain, route resolution restricts logic-function matching to that app's HTTP-routed functions only — isolating each app's routes to its own domain instead of letting routes from other apps in the workspace match nondeterministically. - **Settings sidebar reorganized.** Removed the standalone Domains page. Workspace Domain → General. Approved Domains + Invitations → Members "Access" tab. Emailing Domains + Public Domains → Apps "Developer" tab. Roles → Members "Roles" tab. ## Why The use case: someone building a partner portal app or a lead-collection app declares private objects (leads, partners…) plus a few public HTTP routes. Each app needs its own domain (`partners.acme.com`, `leads.acme.com`) without those domains exposing every other app's routes in the same workspace. Today's PublicDomainEntity is workspace-scoped only, so all HTTP-routed logic functions in a workspace compete for any public domain — first match wins nondeterministically. ## Backend - Added nullable `applicationId` FK to `PublicDomainEntity` (cascade-deleted with the app); indexed for the route-trigger lookup. - New fast instance command `2-4-instance-command-fast-1798000003000-add-application-id-to-public-domain` adds the column, index, and FK constraint. - `createPublicDomain(domain, applicationId)` accepts an optional app binding; new `updatePublicDomain(domain, applicationId)` mutation rebinds/unbinds an existing domain. Both validate the application belongs to the workspace. - `WorkspaceDomainsService.resolveWorkspaceAndPublicDomain(origin)` returns both the workspace and the matched public domain in one query — replacing the old back-to-back lookups in the route-trigger hot path. `getWorkspaceByOriginOrDefaultWorkspace` is preserved as a thin wrapper. - `RouteTriggerService` filters `logicFunction` by `applicationId` when the matched public domain is app-scoped; falls back to workspace-wide when unbound. - Three sequential validation queries in `createPublicDomain` now run in parallel via `Promise.all`. ## Frontend | Old location | New location | |---|---| | Settings sidebar → Domains (standalone page) | Removed | | Domains page → Workspace Domain | General page | | Domains page → Approved Domains | Members → Access tab | | Domains page → Emailing Domains | Apps → Developer tab | | Domains page → Public Domains | Apps → Developer tab | | Settings sidebar → Roles (standalone) | Members → Roles tab | | `pages/settings/roles/` | `pages/settings/members/roles/` | - The Public Domain detail page has an Application picker that uses `Select`'s native `emptyOption` + `null` value pattern (matches `SettingsDataModelObjectIdentifiersForm`). - Members page tabs use the existing `TabListFromUrlOptionalEffect` mechanism (rendered automatically by `TabList`) for hash-based tab activation. - `/settings/members/roles` redirects to `/settings/members#roles` so role sub-pages' `navigate(SettingsPath.Roles)` lands on the Members page with the Roles tab pre-selected. - All affected breadcrumbs updated to nest under their new parents. - `SettingsPath.Roles` and friends now nest under `members/`; `Subdomain` and `CustomDomain` under `general/`; `PublicDomain` and `EmailingDomain` under `applications/`. ## Test plan - [x] `nx typecheck twenty-front` passes - [x] `nx typecheck twenty-server` passes - [x] `oxlint --type-aware` clean on all touched files - [x] `prettier --check` clean on all touched files - [x] Migration applied locally; `publicDomain.applicationId` (uuid, nullable) confirmed in DB - [x] GraphQL schema exposes `PublicDomain.applicationId`, `createPublicDomain.applicationId`, `updatePublicDomain` mutation - [x] **End-to-end route resolution scenarios verified locally:** - Domain bound to App A, function in App A → route matches ✅ - Domain bound to App B, function in App A → route does NOT match (HTTP 404 `TRIGGER_NOT_FOUND`) ✅ - Domain unbound (`applicationId = NULL`) → route matches workspace-wide ✅ - Unknown path on bound domain → returns 404 cleanly ✅ - [x] UI sanity (browser-tested at `apple.localhost:3001`): - General page shows Workspace Domain card - Members page shows Team / Access / Roles tabs - Access tab combines Invite by link + by email + Approved Domains - Roles tab embeds the role list - `/settings/members/roles` direct URL → redirects + Roles tab pre-selected - Apps Developer tab shows Emailing Domains + Public Domains sections - Public Domain detail page has Application picker dropdown listing workspace apps - Sidebar nav: "Domains" and "Roles" no longer present (now folded into General/Members) ## Notes for reviewers - Creating a public domain via the UI still requires Cloudflare credentials in the dev `.env` (`CLOUDFLARE_API_KEY`, `CLOUDFLARE_PUBLIC_DOMAIN_ZONE_ID`, `PUBLIC_DOMAIN_URL`). The DNS step is unchanged from main. - The `applicationId` column is nullable, so existing public-domain rows continue to work workspace-wide — no data backfill required. - `SettingsRolesContainer` was deleted (no longer referenced after `SettingsRoles` index page was removed). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4da8878697 |
feat: add email forwarding message channel (#19535)
## Summary - Add email forwarding as a new message channel type, allowing users to forward emails from addresses like `support@mycompany.com` into Twenty - Inbound emails arrive via S3 (SES → S3 bucket), are polled by a cron job, parsed, routed to the correct workspace/channel, and persisted as messages - Dedicated settings page at `/settings/accounts/new-email-forwarding` where users provide their source email handle and receive a unique forwarding address - Forwarding channels bypass the IMAP/mailbox sync state machine — they skip cron-driven sync, relaunch, and message-list-fetch lifecycle stages - Forwarding address section shown at the top of the Emails settings page so users can find/copy their addresses after initial setup - Tab names for forwarding channels display the user-provided handle (e.g. `support@mycompany.com`) instead of the internal routing address - Shared utilities extracted from IMAP driver: `extractThreadId`, `extractParticipants`, `extractAddresses` to avoid code duplication - Uses the existing S3 bucket (STORAGE_S3_*) with `inbound-email/` prefix — no separate bucket needed - Feature gated behind `isEmailForwardingEnabled` client config (requires `INBOUND_EMAIL_DOMAIN` + S3 storage) ## New backend modules - `InboundEmailS3ClientProvider` — lazy-initialized S3 client using existing storage config - `InboundEmailStorageService` — S3 operations (get, move to processed/unmatched/failed) - `InboundEmailParserService` — RFC 822 parsing via `postal-mime`, builds `MessageWithParticipants` - `InboundEmailImportService` — orchestrates download → parse → route → persist → archive - `MessagingInboundEmailPollCronJob` — polls S3 `incoming/` prefix, enqueues import jobs - `CreateEmailForwardingChannelInput` DTO — accepts user-provided `handle` ## New frontend components - `SettingsAccountsNewEmailForwardingChannel` — dedicated page with handle input form + forwarding address result - `SettingsAccountsEmailForwardingSection` — forwarding address list on the Emails settings page - `useConnectedAccountHandleMap` — shared hook for account ID → handle lookup - `useCreateEmailForwardingChannel` — mutation hook accepting handle parameter ## Test plan - [x] 17 unit tests for inbound email import service (all outcomes: imported, unmatched, loop_dropped, unconfigured, parse_failed, persist_failed) - [x] 16 tests for `computeSyncStatus` including EMAIL_FORWARDING cases - [x] 11 tests for `extractEnvelopeRecipient` utility - [x] TypeScript typechecks pass for both twenty-server and twenty-front - [x] Lint passes for both packages - [ ] Manual: create forwarding channel, verify forwarding address generated - [ ] Manual: send email to forwarding address, verify it appears in Twenty https://claude.ai/code/session_01KpyF6p4cUEnuaT4h8DP5Pm --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: neo773 <62795688+neo773@users.noreply.github.com> Co-authored-by: neo773 <neo773@protonmail.com> |
||
|
|
7999cd3dde |
fix: Use settings table rows and detail page for app connections (#20257)
## Summary Closes #20220 - Replace app connection-provider `SettingsListCard` rows with settings table rows that link to a per-connection detail page. - Add a connection detail page with inline display-name editing, provider and handle metadata, visibility, scopes, timestamps, reconnect, and confirmed disconnect. - Add a scoped connected-account rename mutation and persist visibility when reconnecting an existing app OAuth account. ## Tests - `./node_modules/.bin/jest packages/twenty-front/src/pages/settings/applications/__tests__/SettingsApplicationConnectionDetail.test.tsx --config packages/twenty-front/jest.config.mjs --runInBand` - `./node_modules/.bin/jest packages/twenty-front/src/pages/settings/applications/tabs/__tests__/SettingsApplicationConnectionsSection.test.tsx --config packages/twenty-front/jest.config.mjs --runInBand` - `./node_modules/.bin/jest packages/twenty-shared/src/utils/navigation/__tests__/getSettingsPath.test.ts --config packages/twenty-shared/jest.config.mjs --runInBand` - `./node_modules/.bin/jest packages/twenty-server/src/engine/metadata-modules/connected-account/resolvers/__tests__/connected-account.resolver.spec.ts --config packages/twenty-server/jest.config.mjs --runInBand` - `./node_modules/.bin/jest packages/twenty-server/src/engine/core-modules/application/application-oauth-provider/__tests__/application-oauth-provider-flow.service.spec.ts --config packages/twenty-server/jest.config.mjs --runInBand` - `./node_modules/.bin/oxlint ...` - `./node_modules/.bin/prettier --check ...` - `./node_modules/.bin/tsgo -p packages/twenty-shared/tsconfig.json` ## Notes Full frontend and server typechecks are currently blocked by unrelated existing workspace issues: - frontend implicit `any` errors in `useFrontComponentExecutionContext.ts` - server missing workspace/dependency modules such as `twenty-emails`, `twenty-client-sdk/generate`, and `@ai-sdk/azure` |
||
|
|
633553f729 |
feat(sdk): add defineCommandMenuItem (#20256)
## Summary - Add `defineCommandMenuItem` and `definePageLayoutWidget` as standalone SDK defines, mirroring the existing `definePageLayoutTab` pattern. Both entities can still be declared nested inside their parent (`defineFrontComponent.command` / `definePageLayout.tabs[].widgets[]`). - Add `CommandMenuItem` and `PageLayoutWidget` to the `SyncableEntity` enum and the dev-mode UI labels. - Wire the SDK manifest-build to extract the two new defines into top-level `commandMenuItems` / `pageLayoutWidgets` arrays on the manifest, and the server aggregator to consume them through the existing flat-entity converters. - On the server, expose `Application.commandMenuItems` (relation + DTO + service hydration in `findOneApplication`). - On the front, list command menu items in the application content tab and add a dedicated detail page with a settings tab, mirroring how `frontComponents` are surfaced. - Add `twenty add` templates and Vitest unit tests for both new defines. - Document the standalone-vs-nested pattern in `packages/twenty-sdk/README.md`. ### Why Until now, command menu items could only be declared as the nested `command:` field on `defineFrontComponent` — there was no way to register a command menu item from a separate file or from another package. The `SyncableEntity` enum had 12 values, while the server already synced 18 (including `commandMenuItem` and `pageLayoutWidget`). The same gap existed for `pageLayoutWidget`, which had no top-level define despite being synced server-side. This PR closes both gaps and aligns the SDK surface with what the server actually accepts. The standalone defines coexist with the nested form — pick one per entity, never both with the same `universalIdentifier` (the manifest aggregator will throw on duplicates). The README now documents this. ## Test plan - [x] `npx nx typecheck twenty-sdk` / `twenty-server` / `twenty-front` - [x] `npx nx lint:diff-with-main twenty-front` / `twenty-server` - [x] `npx nx lint twenty-sdk` / `twenty-shared` - [x] New unit tests: `define-command-menu-item.spec.ts`, `define-page-layout-widget.spec.ts` - [x] Existing manifest extract config tests still pass - [ ] Codegen `npx nx run twenty-front:graphql:generate --configuration=metadata` should be re-run after merge — the generated `graphql.ts` was patched manually to include `commandMenuItems` on `Application` and the `FindOneApplication` document. - [ ] Smoke test: scaffold an app with `twenty add` for both new entity types, run `twenty dev`, confirm the dev UI shows them in the sync list and the settings page surfaces command menu items in the content tab. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: martmull <martmull@hotmail.fr> Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
4852ac401a |
Add server upgrade status on admin panel (#20107)
## Summary Adds an admin upgrade-status panel that surfaces per-instance and per-workspace migration health, backed by a Redis-cached aggregate to keep the page snappy on large fleets. <img width="827" height="880" alt="Screenshot 2026-04-28 at 10 21 03" src="https://github.com/user-attachments/assets/8f88baa9-7268-4eff-bf6a-906a7f06ca91" /> <img width="804" height="892" alt="Screenshot 2026-04-28 at 10 21 11" src="https://github.com/user-attachments/assets/1e6decf8-766a-4d0e-96b1-03a9962bba3c" /> ## Computed metrics **Instance** (`InstanceUpgradeStatus`) - `inferredVersion` — version derived from the latest non-initial instance command name - `health` — `upToDate` | `behind` | `failed`, derived from the latest attempt vs. the last expected instance step in the upgrade sequence - `latestCommand` — `{ name, status, executedByVersion, errorMessage, createdAt }` from the most recent attempt **Per-workspace** (`WorkspaceUpgradeStatus`) - `workspaceId`, `displayName` - `inferredVersion`, `health`, `latestCommand` (same shape as instance), computed against the latest expected step in the sequence **Aggregate** (`AllWorkspacesUpgradeStatus`, only across `ACTIVE` / `SUSPENDED` workspaces) - `instanceUpgradeStatus` - `totalCount`, `upToDateCount`, `behindCount`, `failedCount` - `workspacesBehindIds[]`, `workspacesFailedIds[]` - `computedAt` ## Fetching strategy All reads go through `UpgradeStatusCacheService` (cache namespace: `EngineHealth`). - **Aggregate read** (`getAllWorkspacesStatus` → `getAllWorkspacesUpgradeStatus` query): reads summary + behind-ids + failed-ids in parallel; if any of the three keys is missing, full recompute (`recomputeAllWorkspaces`) is triggered, which also primes per-workspace entries. - **Per-workspace read** (`getWorkspacesStatus(ids)` → `getUpgradeStatus(ids)` query): `mget` on workspace keys; misses are recomputed individually (`recomputeWorkspace`), and aggregates are reconciled in place (count + id list deltas) without a full recompute. - **Recompute on demand**: `refreshUpgradeStatus` mutation calls `recomputeAllWorkspaces` to bypass cache and rewrite all keys. - **Auto-invalidation**: `InstanceCommandRunnerService` (fast + slow paths) and `WorkspaceCommandRunnerService` invalidate after every run via `safeInvalidateUpgradeStatusCache()` (`flushByPattern('upgrade-status:*')`). Failures in cache invalidation are swallowed and logged so they never break the migration runner. - **TTL**: `60 * 60 * 1000` ms (1 hour) on every key — protects against stale data even if a runner crashes before invalidating. ## Introduced cache keys All under the `EngineHealth` cache-storage namespace: | Key | Type | Purpose | | --- | --- | --- | | `upgrade-status:all-workspaces:summary` | `CachedAllWorkspacesStatusSummary` | Counts + instance status + `computedAt` | | `upgrade-status:all-workspaces:behind-ids` | `string[]` | Workspace ids in `behind` state | | `upgrade-status:all-workspaces:failed-ids` | `string[]` | Workspace ids in `failed` state | | `upgrade-status:workspace:<workspaceId>` | `CachedWorkspaceUpgradeStatus` | Per-workspace status (one key per workspace) | Full invalidation uses the pattern `upgrade-status:*`. ## Index added on `upgradeMigration` (already added on prod) Migration `2-2-instance-command-fast-1777308014234-addUpgradeMigrationWorkspaceIdIndex.ts`: ```sql CREATE INDEX "IDX_upgradeMigration_workspaceId_name_attempt" ON "core"."upgradeMigration" ("workspaceId", "name", "attempt") WHERE "workspaceId" IS NOT NULL; |
||
|
|
8f362186ce |
Redesign application content tab + logic function settings; add Layout detail pages (#20056)
## Summary Iterative redesign of two related areas in settings, plus a new `pages/settings/layout/` folder for read-only entity detail pages. ### Application content tab - **Grouped into three sections** — Data / Layout / Logic — each with one H2 + multiple `TableSection`-wrapped sub-tables (mirrors the role-permissions pattern). Replaces six per-category table/row components with one uniform `<SettingsApplicationContentSubtable>` + `ApplicationContentRow` shape (net **−~700 lines** across the refactor). - **All 10 row categories now clickable** for installed apps: - Objects / Fields / Logic functions / Front components → existing detail pages - Agents → existing `AiAgentDetail` - Skills → existing `AiSkillDetail` (looked up by `Skill.applicationId + name`) - Roles → existing `RoleDetail` (looked up by `Role.universalIdentifier`) - Views / Page layouts / Navigation menu items → **new** detail pages (see below) - **Lifecycle hooks visible** — `pre-install` / `post-install` logic functions are surfaced in the Trigger column instead of appearing as empty/misconfigured. ### Logic function settings (Triggers + Test tabs) - Triggers tab is now editable (HTTP / Cron / Database event / AI tool) with a `<SettingsLogicFunctionTriggerSection>` wrapper that owns the toggle, header, and read-only short-circuit. - HTTP section gets a Live URL field with copy-to-clipboard. - Each section shows a **Sample input** preview (the JSON the function will receive) using the same payload builders the Test tab uses. - Test tab: **Simulate trigger** buttons that prefill the JSON input from the configured trigger's schema. Replaces an unclickable `<Select>` (which auto-disables when there's only one option — the typical case). - Read-only behavior for installed-app functions: explicit `<Callout>` notice when there's no trigger; trigger sections render as disabled controls when there is one. - Removed the empty Environment Variables section from the Settings tab (it just told the user to go elsewhere). ### New `pages/settings/layout/` folder Three new app-scoped detail pages so users can drill into entities the GraphQL `Application` type doesn't expose by id (keyed by manifest `universalIdentifier`): - `ApplicationViewDetail` — type, object, visibility + Fields / Filters / Sorts subsections (field UIDs resolved to readable labels via `useFieldLabelByUid`) - `ApplicationPageLayoutDetail` — type, object + per-tab subsections listing widgets - `ApplicationNavigationMenuItemDetail` — type, destination (resolved), icon, color, position Each page reads from the marketplace manifest the parent app page already loads (no extra queries). Folder set up so a future "Layout" settings tab can grow here (analogous to the existing `data-model/` folder under the Data tab). ### Other consistency fixes - Breadcrumbs on every app-scoped entity detail page now include a category crumb so users know what they're looking at: `Workspace / Applications / Timely / Navigation menu items / Time entry`. - Title fallback for nav menu items uses the resolved destination (`"Time entry"`) instead of the raw enum (`"OBJECT"`). - New shared utils: `getNavigationMenuItemDestination`, `resolveManifestObjectLabel`, `getLogicFunctionTriggerLabel`, `<MonoText>`. ## Backend changes Only one minor schema-shape change (additive): added `applicationId` to the `SkillFields` GraphQL fragment and `universalIdentifier` to the `RoleFragment` so the new lookups have what they need. Generated metadata schema patched in-tree to match — regenerate with `nx run twenty-front:graphql:generate --configuration=metadata` if it drifts. ## Test plan - [ ] Application content tab on an installed app shows the 3 grouped sections; rows in each section are clickable - [ ] Click an Object → existing object detail page - [ ] Click a Field → existing field-edit page - [ ] Click an Agent / Skill / Role → existing detail page - [ ] Click a View / Page layout / Navigation menu item → new read-only detail page; subsections (Fields/Filters/Sorts for views, per-tab widgets for page layouts) populate correctly - [ ] Breadcrumbs on every entity detail page have 5 crumbs ending in `<Category> / <Entity name>` - [ ] Logic function Triggers tab: toggle each trigger type on/off, see the Sample input preview update; for installed apps, sections render as read-only - [ ] Test tab: each "Simulate trigger" button prefills the JSON editor with the matching payload shape - [ ] Functions list: a function configured as `post-install` shows "Post-install" in the Trigger column 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: claude[bot] <claude[bot]@users.noreply.github.com> |
||
|
|
097432d3a2 |
[Command Menu] Refactor layout customization conditional availability [Warning] (#19974)
closes https://discord.com/channels/1130383047699738754/1494312529286004837 |
||
|
|
de1e592cd3 |
fix(front): suppress full-page skeleton inside auth modal (#19875)
## Summary
- Lazy auth-flow routes (`SignInUp`, `Invite`, `ResetPassword`,
`CreateWorkspace`, `CreateProfile`, `SyncEmails`, `InviteTeam`,
`PlanRequired`, `PlanRequiredSuccess`, `BookCallDecision`, `BookCall`)
render through `<Outlet/>` inside `<AuthModal>`. Their `LazyRoute`
`<Suspense>` fallback was the page-level `PageContentSkeletonLoader`, so
the two grey shimmer bars painted **inside the modal box** for a few
hundred ms while each chunk downloaded.
- `LazyRoute` now accepts an optional `fallback` prop (default
unchanged: the existing page skeleton). Every auth-modal route passes
`fallback={null}` so the modal stays empty until the lazy chunk resolves
instead of flashing the shimmer.
- `AuthModal`'s inner `StyledContent` gets a `min-height: 320px` so the
framer-motion `layout` animation doesn't rapidly resize the modal as
inner steps (loader → form → password → 2FA / workspace selection) swap.
The modal can still grow for taller steps; only the rapid jump is
removed.
## Why default-parameter syntax for `fallback`
`fallback ?? <LazyRouteFallback/>` would treat an explicit `null` as "no
value" and still render the default skeleton. Using a default parameter
(`fallback = <LazyRouteFallback/>`) preserves an explicit `null` because
defaults only kick in for `undefined`.
|
||
|
|
75848ff8ea |
feat: move admin panel to dedicated /admin-panel GraphQL endpoint (#19852)
## Summary Splits admin-panel resolvers off the shared `/metadata` GraphQL endpoint onto a dedicated `/admin-panel` endpoint. The backend plumbing mirrors the existing `metadata` / `core` pattern (new scope, decorator, module, factory), and admin types now live in their own `generated-admin/graphql.ts` on the frontend — dropping 877 lines of admin noise from `generated-metadata`. ## Why - **Smaller attack surface on `/metadata`** — every authenticated user hits that endpoint; admin ops don't belong there. - **Independent complexity limits and monitoring** per endpoint. - **Cleaner module boundaries** — admin is a cross-cutting concern that doesn't match the "shared-schema configuration" meaning of `/metadata`. - **Deploy / blast-radius isolation** — a broken admin query can't affect `/metadata`. Runtime behavior, auth, and authorization are unchanged — this is a relocation, not a re-permissioning. All existing guards (`WorkspaceAuthGuard`, `UserAuthGuard`, `SettingsPermissionGuard(SECURITY)` at class level; `AdminPanelGuard` / `ServerLevelImpersonateGuard` at method level) remain on `AdminPanelResolver`. ## What changed ### Backend - `@AdminResolver()` decorator with scope `'admin'`, naming parallels `CoreResolver` / `MetadataResolver`. - `AdminPanelGraphQLApiModule` + `adminPanelModuleFactory` registered at `/admin-panel`, same Yoga hook set as the metadata factory (Sentry tracing, error handler, introspection-disabling in prod, complexity validation). - Middleware chain on `/admin-panel` is identical to `/metadata`. - `@nestjs/graphql` patch extended: `resolverSchemaScope?: 'core' | 'metadata' | 'admin'`. - `AdminPanelResolver` class decorator swapped from `@MetadataResolver()` to `@AdminResolver()` — no other changes. ### Frontend - `codegen-admin.cjs` → `src/generated-admin/graphql.ts` (982 lines). - `codegen-metadata.cjs` excludes admin paths; metadata file shrinks by 877 lines. - `ApolloAdminProvider` / `useApolloAdminClient` follow the existing `ApolloCoreProvider` / `useApolloCoreClient` pattern, wired inside `AppRouterProviders` alongside the core provider. - 37 admin consumer files migrated: imports switched to `~/generated-admin/graphql` and `client: useApolloAdminClient()` is passed to `useQuery` / `useMutation`. - Three files intentionally kept on `generated-metadata` because they consume non-admin Documents: `useHandleImpersonate.ts`, `SettingsAdminApplicationRegistrationDangerZone.tsx`, `SettingsAdminApplicationRegistrationGeneralToggles.tsx`. ### CI - `ci-server.yaml` runs all three `graphql:generate` configurations and diff-checks all three generated dirs. ## Authorization (unchanged, but audited while reviewing) Every one of the 38 methods on `AdminPanelResolver` has a method-level guard: - `AdminPanelGuard` (32 methods) — requires `canAccessFullAdminPanel === true` - `ServerLevelImpersonateGuard` (6 methods: user/workspace lookup + chat thread views) — requires `canImpersonate === true` On top of the class-level guards above. No resolver method is accessible without these flags + `SECURITY` permission in the workspace. ## Test plan - [ ] Dev server boots; `/graphql`, `/metadata`, `/admin-panel` all mapped as separate GraphQL routes (confirmed locally during development). - [ ] `nx typecheck twenty-server` passes. - [ ] `nx typecheck twenty-front` passes. - [ ] `nx lint:diff-with-main twenty-server` and `twenty-front` both clean. - [ ] Manual smoke test: log in with a user who has `canAccessFullAdminPanel=true`, open the admin panel at `/settings/admin-panel`, verify each tab loads (General, Health, Config variables, AI, Apps, Workspace details, User details, chat threads). - [ ] Manual smoke test: log in with a user who has `canImpersonate=false` and `canAccessFullAdminPanel=false`, hit `/admin-panel` directly with a raw GraphQL request, confirm permission error on every operation. - [ ] Production deploy note: reverse proxy / ingress must route the new `/admin-panel` path to the Nest server. If the proxy has an explicit allowlist, infra change required before cutover. ## Follow-ups (out of scope here) - Consider cutting over the three `SettingsAdminApplicationRegistration*` components to admin-scope versions of the app-registration operations so the admin page is fully on the admin endpoint. - The `renderGraphiQL` double-assignment in `admin-panel.module-factory.ts` is copied from `metadata.module-factory.ts` — worth cleaning up in both. |
||
|
|
6117a1d6c0 |
refactor: standardize AI acronym to Ai (PascalCase) across internal identifiers (#19837)
## Summary
The "AI" acronym was rendered inconsistently across the codebase. The
backend AI module had settled on PascalCase `Ai` (`AiAgentModule`,
`AiBillingService`, `AiChatModule`, `AiModelRegistryService`, etc.),
while frontend components, several DTOs, a few types, and shared
identifiers still used all-caps `AI` (`AIChatTab`,
`AISystemPromptPreviewDTO`, `SettingsPath.AIPrompts`, ...). CLAUDE.md
specifies PascalCase for classes; this PR normalizes everything internal
to `Ai`.
**This is a pure internal rename.** The GraphQL schema is untouched —
`@ObjectType` decorator string arguments, resolver method names (which
become Query/Mutation field names), gql template contents, and the
`generated-metadata/graphql.ts` file are preserved verbatim. The only
visible change is TypeScript identifiers and file names.
## Also folded in (adjacent cleanups)
- **`AgentModelConfigService` → `AiModelConfigService`**. Lives in
`ai-models/` and is used by multiple AI code paths, not just the Agent
entity. The "Agent" prefix was misleading.
- **`generate-text-input.dto.ts` → `generate-text.input.ts`**. The
`ai-agent/dtos/` folder already uses `<entity>.input.ts` convention for
Input classes (`create-agent.input.ts` etc.); the old path mixed
`.dto.ts` file extension with a class that has no DTO suffix. File
rename only; class stays `GenerateTextInput`.
- **Removed stale TODO** in `ai-model-config.type.ts` that asked for the
`AiModelConfig` rename that this PR performs.
## Rename methodology
Bulk rename via perl with anchored regex
`(?<!['"])(?<![A-Z.])AI([A-Z])(?=[a-z])/Ai$1/g`:
- **Lookbehind for non-uppercase** skips adjacent acronyms (`MOSAIC`,
`OIDCSSO`) and leaves `AIRBNB_ID` alone.
- **Lookbehind for non-quote** protects most string literals.
- **Lookahead for lowercase** restricts matches to PascalCase
identifiers (`AIChatTab`), leaving SCREAMING_SNAKE constants untouched.
Strict file-scope exclusions: `generated-metadata/**`, `generated/**`,
`locales/**`, `migrations/**`, `illustrations/**`, `halftone/**`, and
the two gql template files (`queries/getAISystemPromptPreview.ts`,
`mutations/uploadAIChatFile.ts`).
Post-rename reverts for identifiers where the regex was too eager:
- Backend resolver method names kept: `getAISystemPromptPreview`,
`uploadAIChatFile` (they are GraphQL field names).
- `@ObjectType('AdminAIModels')` / `('AISystemPromptPreview')` /
`('AISystemPromptSection')` kept as-is.
- Backend classes `ClientAIModelConfig` / `AdminAIModelConfig` kept
as-is (they use `@ObjectType()` with no argument, so the class name IS
the schema name).
- External-library symbols restored: `OpenAIProvider`,
`createOpenAICompatible`, `vercelAIIntegration`.
File renames use a two-step rename to work on macOS case-insensitive
filesystems: `git mv X.tsx X.tsx.tmp && git mv X.tsx.tmp renamed.tsx`.
## Diff audit
- 0 changes to migrations
- 0 changes to locale `.po` / `.ts` files
- 0 changes to `generated-metadata/graphql.ts`
- 0 changes to website illustration files (base64 blobs preserved)
- 0 renames inside user-facing translation strings (`t\`…\``,
`msg\`…\``, `<Trans>…</Trans>`)
## Test plan
- [x] `npx nx typecheck twenty-server` — PASS
- [x] `npx nx typecheck twenty-front` — PASS
- [x] `npx jest ai-model admin agent-role` — 79/79 PASS
- [x] `npx oxlint --type-aware` on 118 changed files — 0 errors
- [x] `npx prettier --check` on 118 changed files — clean
- [ ] CI
|
||
|
|
2b5b8a8b13 |
Link command menu items to specific page layout (#19706)
- Add a `pageLayoutId` foreign key to `CommandMenuItem`, allowing command menu items to be scoped to a specific page layout instead of being globally available - Filter command menu items by the current page layout on the frontend. Items with a `pageLayoutId` only appear when viewing that layout, while items without one remain globally visible - Create an effect to track the current page layout ID - Include a seed example: a "Show Notification" command pinned to the Star history standalone page layout --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
381f3ba7d9 |
Fix app design 1/2 (#19735)
comply with https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=96977-349627&m=dev ## After <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 40 37" src="https://github.com/user-attachments/assets/6d80191a-79a9-4f0f-aa4f-0e447fff4f6d" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 40 22" src="https://github.com/user-attachments/assets/4f763272-027e-4246-b455-7d46babf7d8c" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 39 11" src="https://github.com/user-attachments/assets/b9b35e18-8068-447e-821d-5ec28bb5bd16" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 39 05" src="https://github.com/user-attachments/assets/57d9318a-902f-4fd7-a2a3-5795ebe0b9dc" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 39 02" src="https://github.com/user-attachments/assets/78a33fa8-6bdd-484e-a82d-bd0f7592a623" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 38 58" src="https://github.com/user-attachments/assets/f7987aed-c6e1-4032-a611-86817655137d" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 38 55" src="https://github.com/user-attachments/assets/d1c451ab-1d2d-41e4-a059-cf4303ecabe7" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 38 48" src="https://github.com/user-attachments/assets/593cae36-2320-443f-a955-93b211a6ee3f" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 37 40" src="https://github.com/user-attachments/assets/c9f602b1-8de3-4e82-a3a6-344594a0c153" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 37 34" src="https://github.com/user-attachments/assets/b54ddddf-5dda-46c8-ace3-cffe6015825a" /> ## before <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 42 18" src="https://github.com/user-attachments/assets/c0976a0a-0124-48ec-8e7c-78627cea7063" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 42 16" src="https://github.com/user-attachments/assets/d2db926c-4040-411d-9091-8b60e7c519e6" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 42 13" src="https://github.com/user-attachments/assets/2d69f2ff-f26e-4249-91a3-2cf3d261e840" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 42 07" src="https://github.com/user-attachments/assets/1028aabc-77ac-4c51-a8c3-9a194faba87f" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 42 01" src="https://github.com/user-attachments/assets/1caa9f5e-3eaa-433c-9d3b-e0f094f16e8e" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 41 56" src="https://github.com/user-attachments/assets/f42b6976-3a8f-4591-9283-bda79bdb424b" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 41 53" src="https://github.com/user-attachments/assets/93d00df8-0091-4dfa-9ac0-f6f376be5962" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 41 43" src="https://github.com/user-attachments/assets/9deae7e5-39c1-4518-a463-6d79bc5bf132" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 41 37" src="https://github.com/user-attachments/assets/3e21b521-c47d-482c-ad41-66abfe973772" /> |
||
|
|
a88d1f4442 |
Introduce standalone page (#19675)
Add support for standalone pages: a new `PageLayout` type (`STANDALONE_PAGE`) that can be rendered independently at `/page/:pageLayoutId`, not tied to any record or object context. - New `STANDALONE_PAGE` page layout type - New `PAGE_LAYOUT` navigation menu item type: adds a `pageLayoutId` foreign key to `NavigationMenuItemEntity`, allowing sidebar items to link directly to standalone pages - New `GLOBAL_OBJECT_CONTEXT` command menu availability type: separates object-context-dependent commands (Create Record, Import, Export, See Deleted, Create View, Hide Deleted) from truly global ones, so standalone pages only show relevant commands - Frontend routing & rendering: adds a `/page/:pageLayoutId` route with its own page component, header, and command menu - Widget rendering refactor - Instance commands: two fast 1.22 migrations: `pageLayoutId` column + `STANDALONE_PAGE` enum, and `GLOBAL_OBJECT_CONTEXT` availability type enum - Workspace command: backfills existing command menu items from `GLOBAL` to `GLOBAL_OBJECT_CONTEXT` where appropriate - Dev seeds: adds a sample "Star History" standalone page with an iframe widget for local development |
||
|
|
b3354ab6e7 |
Fix multi-workspace-registration (#19685)
## Before <img width="1512" height="915" alt="image" src="https://github.com/user-attachments/assets/5cb05f76-b672-404e-b31d-ca455802f97a" /> ## After <img width="1512" height="726" alt="image" src="https://github.com/user-attachments/assets/58229c4c-3ac6-4428-9c4d-3586a2b9ee36" /> |
||
|
|
09806d7d8c |
Add admin panel workspace detail page with chat viewer (#19579)
## Overview Adds comprehensive admin panel functionality for viewing workspace details and AI chat threads. ## Changes ### Frontend - **New Routes**: Added `AdminPanelWorkspaceDetail` and `AdminPanelWorkspaceChatThread` pages with lazy loading - **New Queries**: - `getAdminWorkspaceChatThreads` - fetch chat threads for a workspace - `getAdminChatThreadMessages` - fetch messages for a specific thread - `workspaceLookupAdminPanel` - lookup workspace info and users - **New Components**: - `SettingsAdminWorkspaceDetail` - displays workspace info and chat sessions tabs - `SettingsAdminWorkspaceChatThread` - renders chat conversation with message bubbles - **Navigation**: Updated AI admin panel to link to workspace detail pages - **Settings Paths**: Added `AdminPanelWorkspaceDetail` and `AdminPanelWorkspaceChatThread` paths ### Backend - **New DTOs**: - `AdminWorkspaceChatThreadDTO` - workspace chat thread data - `AdminChatThreadMessagesDTO` - thread with messages - `AdminChatMessageDTO` - individual message with parts - **New Resolvers**: Added three queries to `AdminPanelResolver` - **New Service Methods**: - `workspaceLookup()` - fetch workspace info - `getWorkspaceChatThreads()` - list chat threads - `getChatThreadMessages()` - fetch thread messages with validation - **Module Updates**: Added entity imports for workspace, user, AI chat, and feature flag data ### Security - Added `allowImpersonation` check before accessing chat data - Validates workspace ownership and access permissions --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
65b2baca7a |
Remove IS_USAGE_ANALYTICS_ENABLED feature flag (#19566)
## Summary This PR removes the `IS_USAGE_ANALYTICS_ENABLED` feature flag and makes usage analytics features universally available. The feature flag guard has been removed from the usage analytics resolver and all conditional rendering based on this flag has been eliminated. ## Key Changes - **Removed feature flag dependency**: Deleted `IS_USAGE_ANALYTICS_ENABLED` from the `FeatureFlagKey` enum in `twenty-shared` - **Updated AI Usage tab**: Simplified `SettingsAIUsageTab` to remove enterprise access checks and feature flag conditionals, now only checks if ClickHouse is configured - **Updated Usage Analytics section**: Removed feature flag guard from `SettingsUsageAnalyticsSection` and added loading/empty state handling - **Updated AI settings navigation**: Made the Usage tab always visible in the AI settings tabs, removing conditional rendering based on feature flag - **Updated Billing Credits section**: Removed feature flag check before showing the "View usage" button - **Updated Settings routes**: Removed `SettingsProtectedRouteWrapper` with feature flag requirement from usage routes - **Updated GraphQL resolver**: Removed `@RequireFeatureFlag` decorator and `FeatureFlagGuard` from the `getUsageAnalytics` query - **Updated dev seeder**: Removed the feature flag seed entry for `IS_USAGE_ANALYTICS_ENABLED` ## Implementation Details - Usage analytics now gracefully handles loading states with `UsageSectionSkeleton` - Empty state messaging is shown when no usage data is available yet - ClickHouse configuration remains the only requirement for usage analytics functionality - All enterprise-specific gating for AI usage analytics has been removed in favor of ClickHouse availability checks https://claude.ai/code/session_01MRFVXtquL3wS7qmQkDU3AT --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
5edc034f8b |
Enqueue a snack bar on merge preview errors (#19465)
Fixes https://github.com/twentyhq/twenty/issues/19312 <img width="400" height="500" alt="Capture d’écran 2026-04-08 à 18 17 57" src="https://github.com/user-attachments/assets/41ac28b2-0208-47b4-bd85-f803c524330a" /> |
||
|
|
e2bd3e8ef4 | Unselect widget when exiting layout customization mode (#19411) | ||
|
|
3c067f072c | [AI] Improve tools tab (#19221) | ||
|
|
4d74cc0a28 |
chore: remove IS_APPLICATION_ENABLED feature flag (#19081)
## Summary - Remove the `IS_APPLICATION_ENABLED` feature flag — application features are now always enabled - Remove `@RequireFeatureFlag` decorators and `FeatureFlagGuard` from 7 application resolvers (registration, development, manifest, install, upgrade, marketplace, oauth) - Remove frontend feature flag checks: settings navigation visibility, route protection wrapper, side panel widget type gating, and role permission flag filtering - Delete the integration test for disabled-flag behavior - Clean up seed data, test mocks, and generated schema files |
||
|
|
a51b5ed589 |
fix: resolve settings/usage chart crash and add ClickHouse usage event seeds (#19039)
## Summary
- **Fix settings/usage page crash**: The `GraphWidgetLineChart`
component used on `settings/usage` was crashing with "Instance id is not
provided and cannot be found in context" because it requires
`WidgetComponentInstanceContext` (for tooltip/crosshair component
states) which is only provided inside the widget system. Wraps the
standalone chart usages with the required context provider.
- **Avoid mounting `GraphWidgetLegend` when hidden**: The legend
component calls `useIsPageLayoutInEditMode()` which requires
`PageLayoutEditModeProviderContext` — another context only available
inside the widget system. Since the settings page passes
`showLegend={false}`, the fix conditionally unmounts the legend instead
of always mounting it with a `show` prop. Applied consistently across
all four chart types (line, bar, pie, gauge).
- **Add ClickHouse usage event seeds**: Generates ~400 realistic
`usageEvent` rows spanning the past 35 days with weighted user activity,
weekday/weekend patterns, and gradual ramp-up. Enables developers to see
the usage analytics page with data locally.
## Test plan
- [ ] Navigate to `settings/usage` — page should render without errors
- [ ] Verify the daily usage line chart displays correctly
- [ ] Navigate to a user detail page from the usage list
- [ ] Verify the user detail chart renders without errors
- [ ] Run `npx nx clickhouse:seed twenty-server` and confirm usage
events are seeded
- [ ] Verify chart legend still works correctly on dashboard widgets (no
regression)
Made with [Cursor](https://cursor.com)
|
||
|
|
49afe5dbc4 |
Refactor: Unify command menu item execution (#18908)
- Makes engineComponentKey non-nullable on CommandMenuItem. Adds two new engine component keys: TRIGGER_WORKFLOW_VERSION and FRONT_COMPONENT_RENDERER to cover the former standalone cases. - Unifies the previously separated engine, front component and workflow runners into a single `CommandRunner` component with a unified `useMountCommand` hook that handles all command types. |
||
|
|
611947e031 |
fix: metadata store lifecycle during sign-in, sign-out, and locale change (#18901)
## Summary Fixes metadata lifecycle bugs during sign-in, sign-out, and locale change: - **Cross-tab sign-out**: Broadcasts sign-out via `BroadcastChannel` so other tabs clear their session gracefully instead of hitting stale-token errors - **SSE teardown on sign-out**: Handles `UNAUTHENTICATED`/`FORBIDDEN` errors in the SSE event stream effect instead of throwing unhandled errors - **Locale switch resilience**: `invalidateAndReload` now invalidates collection hashes instead of clearing the store to empty, so components never see 0 metadata items during the reload transition - **Sign-in background mock**: Uses non-throwing `objectMetadataItemFamilySelector` instead of hooks that throw on missing metadata - **View name placeholders**: Guards against `undefined` `viewName` during metadata transitions (the minimal metadata query doesn't include `name`) - **Session cleanup**: Selective `localStorage` clearing (`clearSessionLocalStorageKeys`) preserves metadata keys; `clearAllSessionLocalStorageKeys` for full clears - **Metadata reload API**: New `useMetadataStoreActions` hook as the high-level API for metadata lifecycle operations (`applyMockedMetadata`, `invalidateAndReload`, `loadMockedMetadataAtomic`) ## Test plan - [ ] Sign out on Tab A → Tab A shows sign-in page with no console errors - [ ] Tab B (logged in) receives cross-tab broadcast and redirects to sign-in - [ ] No "Forbidden resource" SSE errors in console during sign-out - [ ] Change language in Settings > Experience → no crash, metadata refreshes in background - [ ] Sign back in after sign-out → metadata loads correctly, app is functional - [ ] Re-sign-in after locale change → correct locale is preserved |
||
|
|
77d4bd9158 |
Add billing usage analytics dashboard with ClickHouse integration (#18592)
## Summary This PR adds a comprehensive billing usage analytics feature that provides detailed breakdowns of credit consumption across execution types, users, resources, and time periods. The implementation includes a new ClickHouse-backed analytics service, GraphQL API endpoint, and a frontend dashboard component. ## Key Changes ### Backend - **New BillingAnalyticsService**: Queries ClickHouse for usage breakdowns by user, resource, execution type, and time series data - **BillingEventWriterService**: Writes billing events to ClickHouse for analytics while maintaining best-effort semantics (never blocks Stripe billing) - **ClickHouse Schema**: Added `billingEvent` table with 3-year TTL for storing detailed billing event data - **GraphQL Resolver**: New `getBillingAnalytics` query that aggregates usage data for the current billing period, protected by feature flag and billing permissions - **Enhanced BillingUsageEvent**: Added `userWorkspaceId` field to track per-user credit consumption - **AI Billing Integration**: Updated AI billing service to pass `userWorkspaceId` when recording usage events ### Frontend - **SettingsBillingAnalyticsSection**: New component displaying: - Usage breakdown by execution type with progress bars - Daily usage time series chart (28-day view) - Per-user credit consumption breakdown - Per-resource (agent/workflow) credit consumption breakdown - **SettingsUsage Page**: Dedicated page for viewing usage analytics - **GraphQL Query**: `GetBillingAnalytics` query with generated hooks - **Navigation**: Added Usage menu item in settings (feature-flagged) - **Mock Data**: Included screenshot mock data for preview/testing ### Feature Flag - Added `IS_USAGE_ANALYTICS_ENABLED` feature flag to control visibility and access to analytics features ## Implementation Details - Analytics data is queried in parallel for performance - ClickHouse writes are non-blocking to ensure billing operations never fail - Progress bars use dynamic coloring from a predefined palette - Time series visualization normalizes bar heights relative to max value - Empty state handling when no analytics data is available - Responsive UI with proper text truncation for long names https://claude.ai/code/session_01Y1EqrX6PFq3EJxJq89h7DF --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
908aefe7c1 |
feat: replace hardcoded AI model constants with JSON seed catalog (#18818)
## Summary - Replaces per-provider TypeScript constant files (`openai-models.const.ts`, `anthropic-models.const.ts`, etc.) with a single `ai-providers.json` catalog as the source of truth - Adds runtime model discovery via AI SDK for self-hosted providers, with `models.dev` enrichment for pricing/capabilities - Introduces composite model IDs (`provider/modelId`) for canonical, conflict-free identification - Simplifies provider configuration: API keys are injected from environment variables (e.g., `OPENAI_API_KEY`) - Adds admin panel UI for provider management (add/remove/test), model discovery, recommended model configuration, and default fast/smart model selection per workspace - Removes deprecated config variables (`AI_DISABLED_MODEL_IDS`, `AUTO_ENABLE_NEW_AI_MODELS`, etc.) - Adds database migration for composite model ID format ## Test plan - [ ] Server typecheck passes - [ ] Frontend typecheck passes - [ ] Server unit tests pass - [ ] Frontend unit tests pass - [ ] CI pipeline green - [ ] Admin panel AI tab loads correctly - [ ] Provider discovery works for configured providers - [ ] Model recommendation toggles persist - [ ] Default fast/smart model selection works Made with [Cursor](https://cursor.com) |
||
|
|
cd651f57cb |
fix: prevent blank subdomain from being saved (#18812)
## Summary Fixes #17941 — Saving a blank subdomain causes a redirect to `.website.com`, effectively breaking the workspace. **Root cause:** Three layers all fail to reject an empty string `""`: 1. **Frontend (`SettingsDomain.tsx`):** `SaveButton` has both `onClick={onSave}` and `type="submit"`. The `onClick` fires first, calling `handleSave()` directly without running Zod validation. So `isDefined("")` returns `true`, the confirmation modal opens, and the blank subdomain is submitted. 2. **Backend DTO (`update-workspace-input.ts`):** The `subdomain` field has `@IsString()` + `@IsOptional()` but no pattern validation, so an empty string passes the DTO layer. 3. **Backend service (`workspace.service.ts:152`):** `if (payload.subdomain && ...)` — empty string is falsy in JS, so it skips `validateSubdomainOrThrow()` entirely and writes `subdomain: ""` to the database. **The crash:** After save, the redirect logic does `"myworkspace.website.com".replace("myworkspace", "")` → `".website.com"`, sending the user to an invalid URL. ## Fix - **Frontend:** Call `form.trigger()` at the start of `handleSave` to run Zod validation regardless of whether the function was invoked via `onClick` or `form.handleSubmit`. Returns early with validation error if invalid. - **Backend DTO:** Add `@Matches(/^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$/)` to reject invalid subdomains at the request validation layer (defense-in-depth). - **Backend service:** Change `if (payload.subdomain && ...)` to `if (isDefined(payload.subdomain) && ...)` so empty strings route through `validateSubdomainOrThrow()` instead of being silently skipped. ## Test plan - [x] Existing `is-subdomain-valid.util.spec.ts` tests pass (36/36) - [x] TypeScript type checks pass for both `twenty-server` and `twenty-front` - [x] oxlint passes on all changed files - [x] Prettier passes on all changed files - [ ] Manual: Navigate to Settings > Domains, clear the subdomain field, click Save — should show validation error, not redirect --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Charles Bochet <charles@twenty.com> Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com> |
||
|
|
c6f11d8adb |
fix: migrate driver modules to DriverFactoryBase lazy-loading pattern (#18731)
## Summary - Migrates `LogicFunctionModule`, `CodeInterpreterModule`, and `CaptchaModule` from the `forRootAsync` + injection token pattern to the `DriverFactoryBase` lazy-loading pattern (matching `EmailModule` and `FileStorageModule`) - Fixes #18724 where `LOGIC_FUNCTION_TYPE` was not respected in worker processes because the driver was created at module boot time before the DB config cache was loaded - Removes `isEnvOnly` from `LOGIC_FUNCTION_TYPE`, `CODE_INTERPRETER_TYPE`, `CAPTCHA_DRIVER`, `IS_MULTIWORKSPACE_ENABLED`, and `FRONTEND_URL` — these can now be safely configured via the database at runtime ## How it works Each migrated module now uses a `DriverFactory` (extending `DriverFactoryBase`) instead of a module-level async factory + Symbol injection token: 1. **Lazy creation**: `getCurrentDriver()` creates the driver on first call, after `DatabaseConfigDriver.onModuleInit()` has loaded the DB cache 2. **Auto-recreation**: If config changes in the DB, the next `getCurrentDriver()` call detects the key mismatch and creates a new driver instance 3. **Unified config**: Both server and worker read from the same database — driver config only needs to be set once ### Files deleted (old pattern) - `logic-function-module.factory.ts`, `logic-function-drivers.module.ts`, `logic-function-driver.constants.ts` - `code-interpreter-module.factory.ts` - `captcha.module-factory.ts`, `captcha-driver.constants.ts` ### Files created (new pattern) - `logic-function-driver.factory.ts` - `code-interpreter-driver.factory.ts` - `captcha-driver.factory.ts` Net: **-150 lines** ## Test plan - [x] `npx nx typecheck twenty-server` passes - [x] `npx nx lint:diff-with-main twenty-server` passes - [ ] Integration tests pass (`npx nx run twenty-server:test:integration:with-db-reset`) - [ ] Verify logic functions execute in workflow runs (the original bug) - [ ] Verify code interpreter works in workflow code steps - [ ] Verify captcha validation works on sign-up (when captcha is configured) Made with [Cursor](https://cursor.com) |
||
|
|
abdab2fb7e |
[Command menu items] Create engine commands (#18681)
## Description - Introduces a new engine command execution model that replaces the previous approach of mapping `EngineComponentKey` to React components. Instead, engine commands are now mounted headlessly via `HeadlessEngineCommandMountRoot`, with their execution context populated synchronously before mounting. - Creates new headless command components - Moves error handling from the SDK layer to the host app by wrapping all mounted commands with a new `CommandMenuItemErrorBoundary` The new flow works as follows: - When a command menu item with an `engineComponentKey` is clicked, `useCommandMenuItemFrontComponentCommands` calls `useMountEngineCommand`, which synchronously reads the current context store (object metadata, selected records, filters, view ID, etc.) and writes a `MountedEngineCommandContext` into `mountedEngineCommandsState`. - The command is then mounted into `mountedEngineCommandsState`, which triggers `HeadlessEngineCommandMountRoot` to render the corresponding headless component from `ENGINE_COMPONENT_KEY_HEADLESS_COMPONENT_MAP`, wrapped in `CommandMenuItemErrorBoundary`, `ContextStoreComponentInstanceContext.Provider`, and `EngineCommandComponentInstanceContext.Provider`. - Each command component reads its execution context and delegates to one of the 4 execution patterns: `HeadlessEngineCommandWrapperEffect` (simple actions), `HeadlessConfirmationModalEngineCommandEffect` (destructive actions needing confirmation), `HeadlessNavigateEngineCommand` (GO_TO_* commands), or `HeadlessOpenSidePanelPageEngineCommand` (SEARCH_RECORDS, ASK_AI, VIEW_PREVIOUS_AI_CHATS). - After execution, the command self-unmounts via `useUnmountEngineCommand`, which removes the entry from `mountedEngineCommandsState` and stops rendering the component. |
||
|
|
ab881350b2 |
refactor: unify layout customization mode (record pages + navigation) (#18640)
### What Unifies record page layout editing and navigation menu editing into a single global "layout customization" session. Dashboard editing stays separate. ### How it works **Two edit mode systems, one context-based read:** - `isLayoutCustomizationModeEnabledState` -- global atom for record pages + navigation - `isDashboardInEditModeComponentState` -- dashboard-only, independent per-component atom - `PageLayoutEditModeProvider` -- context that dispatches to `RecordPageLayoutEditModeProvider` (reads global atom) or `DashboardPageLayoutEditModeProvider` (reads component atom), one component per file **Session registry + independent atoms:** - `activeCustomizationPageLayoutIdsState` -- accumulates page layout IDs as user navigates during customization (`string[]`) - Save/cancel iterate the ID list and read each layout's draft/persisted atoms independently - Follows the same pattern as `settingsRoleIdsState` + `settingsDraftRoleFamilyState` **Unified UI:** - `LayoutCustomizationBar` replaces the old `NavigationMenuEditModeBar` - Enter once -- edit record layouts + navigation -- save/cancel everything together - `useSaveLayoutCustomization` orchestrates sequential save: navigation draft -- page layouts -- field widget groups - Error snackbar on partial save failure (with TODO for future atomic server mutation) **Draft protection during customization:** - `PageLayoutRelationWidgetsSyncEffect` guarded -- only updates persisted state from server, skips draft/currentLayouts while customization is active - `useExecuteTasksOnAnyLocationChange` skips draft reset when customization mode is enabled - Command execution blocked during layout customization ### Cleanup - Deleted `NavigationMenuEditModeBar`, `isNavigationMenuInEditModeState`, `isPageLayoutInEditModeComponentState`, `useIsGlobalLayoutCustomizationActive` - `DraftPageLayout` type changed from `Omit` to `Pick` (explicit fields) - Removed save/cancel from `DefaultRecordCommandMenuItemsConfig` (bar handles it now) - Extracted `useSaveFieldsWidgetGroups` from save orchestration - Split `PageLayoutEditModeProvider` into 3 separate files (one component per file, Twenty convention) ### Known issues - **Stale deleted widget after save (pre-existing on `main`)**: Delete widget -- save -- exit customization -- Apollo cache stale -- sync effect overwrites Jotai from stale data -- widget reappears until refresh. Separate PR needed, likely tied to the planned server-side `saveLayoutCustomization` atomic endpoint. ### Open questions - **Module location**: Layout customization hooks/states live in `/app` -- should they move to their own `modules/layout-customization/`? - **Atomic server mutation**: All save mutations are on metadata schema (`createNavigationMenuItem`, `deleteNavigationMenuItem`, `updateNavigationMenuItem`, `updatePageLayoutWithTabsAndWidgets`, `upsertFieldsWidget`). A single `saveLayoutCustomization` endpoint could make saves truly atomic. https://github.com/user-attachments/assets/036ef542-97f3-485b-a68f-3726002c81fb |
||
|
|
ba9aa41bba |
refactor: metadata store cleanup, SSE unification, mock metadata loading & login redirect fix (#18651)
## Summary - **SSE unification**: Replaced 11 individual SSE effect components with a single generic `MetadataStoreSSEEffect` - **Metadata store cleanup**: Merged `metadataCollectionHashesState` into `metadataStoreState` (currentCollectionHash / draftCollectionHash per entity), moved `objectMetadataItemsSelector` to `object-metadata` domain, converted `navigationMenuItemsState` to a derived selector - **Naming clarity**: Renamed `isAppMetadataReadyState` → `isMinimalMetadataReadyState`, `MetadataGater` → `MinimalMetadataGater`, `useIsLogged` → `useHasAccessTokenPair`, `patchMetadataStoreFromSSEEvent` now takes named object params - **Mock metadata loading**: Added `generate-navigation-menu-items.ts` script, rewrote `useLoadMockedMinimalMetadata` to load full objects/fields/indexes/views/navItems from generated mock data, enabling proper sign-in background rendering (table columns, view picker, navigation) - **Login/logout transitions**: `MinimalMetadataLoadEffect` manages mocked↔real metadata transitions based on auth state, `MainContextStoreProvider` computes context on auth pages for view picker support - **Login redirect fix**: `handleLoadWorkspaceAfterAuthentication` now re-enables `isAppEffectRedirectEnabled` after `loadCurrentUser()` completes, fixing the blocked post-login navigation - **Dead code removal**: Deleted `useRefreshPageLayouts`, `useApplyPageLayouts`, `useStaleMetadataEntities`, `metadataCollectionHashesState`, and all individual SSE effects ## Test plan - [x] Login from welcome page redirects to companies page - [x] Logout transitions cleanly to mocked metadata on welcome page - [x] Sign-in background shows table columns, view picker, and navigation items - [x] SSE events still update metadata store entries correctly - [x] Navigation menu items persist across page refreshes - [ ] CI: lint, typecheck, tests pass |
||
|
|
7a3540788a |
feat: uniformize metadata store with flat types, SSE alignment, presentation endpoint & localStorage (#18647)
## Summary
Uniformizes the metadata store to support **all** backend flat metadata
types, introduces a **minimal metadata endpoint** for fast initial
renders, replaces custom localStorage persistence with **Jotai's
built-in `atomWithStorage`**, and wires up a
**MinimalMetadataLoadEffect** for stale-while-revalidate loading.
### Key changes
- **All flat metadata types**: Added `FlatCommandMenuItem`,
`FlatFrontComponent`, `FlatWebhook`, `FlatRole`, `FlatRoleTarget`,
`FlatAgent`, `FlatSkill`, `FlatRowLevelPermissionPredicate`,
`FlatRowLevelPermissionPredicateGroup` — every entity in the backend
`MetadataEntityTypeMap` now has a corresponding frontend flat type
registered in `ALL_METADATA_ENTITY_KEYS` and `MetadataEntityTypeMap`.
- **Minimal metadata endpoint** (`minimalMetadata` GraphQL query): New
backend module (`MinimalMetadataModule`) returns lightweight object
metadata (names, icons, labels, flags) and basic views (id, type, key,
objectMetadataId) plus a `metadataVersion`. This enables fast first
paint before full metadata loads.
- **Jotai `atomWithStorage` for persistence**: Replaced the custom
`MetadataLocalStorageEffect` with Jotai's built-in `atomWithStorage` on
both `metadataStoreState` (family) and `metadataVersionState`. Added
`localStorageOptions` support to `createAtomFamilyState` for `{
getOnInit: true }` synchronous hydration. Each entity atom auto-persists
under keys like `metadataStoreState__objectMetadataItems`.
- **MinimalMetadataLoadEffect**: New effect mounted before
`MetadataProviderInitialEffects` that checks if the store already has
data (from Jotai localStorage hydration). If empty, it fetches minimal
metadata from the new endpoint. The full metadata load continues in
parallel, eventually enriching the store with complete data.
- **SSE effects alignment**: All metadata entity types now have
corresponding SSE effects that directly patch the metadata store via
`patchMetadataStoreFromSSEEvent`.
- **Existing selectors and joining logic**:
`objectMetadataItemsWithFieldsSelector`, `viewsWithRelationsSelector`,
`pageLayoutsWithRelationsSelector` reconstruct nested data from flat
entities for components that need it.
### Loading flow
```
App mount
→ Jotai atomWithStorage hydrates store from localStorage (sync, getOnInit)
→ MinimalMetadataLoadEffect
→ Store has data? → skip (app renders immediately)
→ Store empty? → fetch minimalMetadata endpoint → populate objects + views
→ MetadataProviderInitialEffects (full metadata load, runs in parallel)
→ LazyMetadataLoadEffect (page layouts, logic functions, nav menu, etc.)
→ IsAppMetadataReadyEffect (sets isAppMetadataReady)
```
## Test plan
- [ ] Verify app loads with empty localStorage (should fetch minimal
metadata, then full)
- [ ] Verify app loads with populated localStorage (should skip minimal
fetch, render immediately)
- [ ] Verify SSE events correctly update metadata store for all entity
types
- [ ] Verify logout clears metadata store (atom reset propagates to
localStorage)
- [ ] Verify all metadata selectors return correct joined data
- [ ] CI: lint, typecheck, tests pass
|
||
|
|
6b48f197d4 |
feat: deprecate WorkspaceFavorite in favor of NavigationMenuItem (#18624)
## Summary - **Removes the entire `modules/favorites/` directory** (~66 files, ~5000 lines deleted) — components, hooks, states, types, utils, tests, and the favorite-folder-picker sub-module - **Eliminates the dual-write pattern** where creating a favorite also created a NavigationMenuItem — all consumers now use `useCreateNavigationMenuItem` directly - **Removes `IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED` feature flag checks** from ~12 files, always taking the NavigationMenuItem code path - **Cleans up backend dual-writes** in `object-metadata.service.ts` and `twenty-standard-application.service.ts` that were creating Favorite records alongside NavigationMenuItems - **Updates prefetch system** to only load NavigationMenuItems (removes favorites prefetch effects and states) - **Cleans up test infrastructure** — updates Storybook decorators, mock data, and graphql mocks to remove favorites references ### What was intentionally kept - **Backend entity definitions** (`FavoriteWorkspaceEntity`, `FavoriteFolderWorkspaceEntity`) — these define the database schema and need a proper database migration to remove - **Cascade deletion listeners** — still needed to clean up existing Favorite data in workspaces that haven't been fully migrated - **v1.18 migration commands** — needed for workspaces upgrading from older versions --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
c1da7be6d7 |
Billing for self-hosts (#18075)
## Summary Implements enterprise licensing and per-seat billing for self-hosted environments, with Stripe as the single source of truth for subscription data. ### Components - **twenty-website** hosts the private key to sign `ENTERPRISE_KEY` and `ENTERPRISE_VALIDITY_TOKEN`. It communicates with Stripe to emit the daily `ENTERPRISE_VALIDITY_TOKEN` if the subscription is active, based on the user's Stripe subscription ID stored in `ENTERPRISE_KEY`. - **Stripe** is the single source of truth for subscription data (status, seats, billing). - **The client** (twenty-server + DB + workers) saves `ENTERPRISE_KEY` in the `keyValuePair` table (or `.env` if `IS_CONFIG_VARIABLES_IN_DB_ENABLED` is false) and the daily-renewed `ENTERPRISE_VALIDITY_TOKEN` in the `appToken` table. `ENTERPRISE_VALIDITY_TOKEN` is verified client-side using a public key to grant access to enterprise features (RLS, SSO, audit logs, etc.). ### Flow 1. When requesting an upgrade to an enterprise plan (from **Enterprise** in settings), the user is shown a modal to choose monthly/yearly billing, then redirected to Stripe to enter payment details. After checkout, they land on twenty-website where they are exposed to their `ENTERPRISE_KEY`, which they paste in the UI. It is saved in the `keyValuePair` table. On activation, a first `ENTERPRISE_VALIDITY_TOKEN` with 30-day validity is stored in the `appToken` table. 2. **Every day**, a cron job runs and does two things: - **Refreshes the validity token**: communicates with twenty-website to get a new `ENTERPRISE_VALIDITY_TOKEN` with 30-day validity if the Stripe subscription is still active. If the subscription is in cancellation, the emitted token has a validity equal to the cancellation date. If it's no longer valid, the token is not replaced. The cron only needs to run every 30 days in practice, but runs daily so it's resilient to occasional failures. - **Reports seat count**: counts active (non-soft-deleted) `UserWorkspace` entries and sends the count to twenty-website, which updates the Stripe subscription quantity with proration. Seats are also reported on first activation. If the subscription is canceled or scheduled for cancellation, the seat update is skipped. 3. `ENTERPRISE_VALIDITY_TOKEN` is verified server-side via a public key to grant access to enterprise features. ### Key concepts Three distinct checks are exposed as GraphQL fields on `Workspace`: | Field | Meaning | |---|---| | `hasValidEnterpriseKey` | Has any valid enterprise key (signed JWT **or** legacy plain string) | | `hasValidSignedEnterpriseKey` | `ENTERPRISE_KEY` is a properly signed JWT (billing portal makes sense) | | `hasValidEnterpriseValidityToken` | `ENTERPRISE_VALIDITY_TOKEN` is present and not expired (expiration depends on signed token payload, not on "expiresAt" on appToken table which is only indicative) | Feature access is gated by `isValid()` = `hasValidEnterpriseValidityToken || hasValidEnterpriseKey` (to support both new and legacy keys during transition). After transition isValid() = hasValidEnterpriseValidityToken ### Frontend states The Enterprise settings page handles multiple states: - **No key**: show "Get Enterprise" with checkout modal - **Orphaned validity token** (token valid but no signed key): prompt user to set a valid enterprise key - **Active/trialing but no validity token**: show subscription status with a "Reload validity token" action - **Active/trialing**: show full subscription info, billing portal access, cancel option - **Cancellation scheduled**: show cancellation date, billing portal - **Canceled**: show billing history link and option to start a new subscription - **Past due / Incomplete**: prompt to update payment or restart ### Temporary retro-compatibility: legacy plain-text keys Previously, enterprise features were gated by a simple check: any non-empty string in `ENTERPRISE_KEY` granted access. With this PR, we transition to a controlled system relying on signed JWTs. To avoid breaking existing self-hosted users: - **Legacy plain-text keys still grant access** to enterprise features. `hasValidEnterpriseKey` returns `true` for both signed JWTs and plain strings, and `isValid()` checks `hasValidEnterpriseKey` as a fallback when no validity token is present. - **A deprecation banner** is shown at the top of the app when `hasValidEnterpriseKey` is `true` but `hasValidSignedEnterpriseKey` is `false`, informing the user that their key format is deprecated and they should activate a new signed key. - **No billing portal or subscription management** is available for legacy keys since there is no Stripe subscription to manage. This retro-compatibility will be removed in a future version. At that point, `isValid()` will only check `hasValidEnterpriseValidityToken`. ### Edge cases - **Air-gapped / production environments**: for self-hosted clients that block external traffic (or for our own production), provide a long-lived `ENTERPRISE_VALIDITY_TOKEN` (e.g. 99 years) directly in the `appToken` table, with no `ENTERPRISE_KEY`. The daily cron will skip the refresh (no enterprise key to authenticate with), but the pre-seeded validity token will be used to grant feature access. No billing or seat reporting occurs in this mode. - **`IS_CONFIG_VARIABLES_IN_DB_ENABLED` is false**: if the user tries to activate an enterprise key but DB config writes are disabled, the backend returns a clear error asking them to add `ENTERPRISE_KEY` to their `.env` file manually. - **Canceled subscriptions**: the `/seats` endpoint skips Stripe updates for canceled or cancellation-scheduled subscriptions to avoid Stripe API errors. ### How to test - launch twenty-website on a different url (eg localhost:1002) - add ENTERPRISE_API_URL=http://localhost:3002/api/enterprise (or else) in your server .env - ask me for twenty-website's .env file content (STRIPE_SECRET_KEY; STRIPE_ENTERPRISE_MONTHLY_PRICE_ID;STRIPE_ENTERPRISE_YEARLY_PRICE_ID; ENTERPRISE_JWT_PRIVATE_KEY; ENTERPRISE_JWT_PUBLIC_KEY; NEXT_PUBLIC_WEBSITE_URL) - visit Admin panel / enterprise |
||
|
|
b346f4fb59 |
Add common loader (#18556)
To avoid white screens on reload, building a shared skeleton. Before https://github.com/user-attachments/assets/42bd0667-141d-4df4-9072-4077192cc71d After https://github.com/user-attachments/assets/e6031a72-2e25-47e3-a873-b89aaddfbd3a |
||
|
|
926dd545f4 |
Create fake hidden fields group (#18525)
## Demo https://github.com/user-attachments/assets/43f31c43-fe37-4553-ad42-fc97a948d6ea ## Ungrouped fields <img width="3456" height="2160" alt="CleanShot 2026-03-10 at 13 52 57@2x" src="https://github.com/user-attachments/assets/13d1db63-59ac-4e2b-8950-fccf430176c4" /> |
||
|
|
36bcc71f3d |
refactor(command-menu-item): rename Actions to CommandMenuItem (#18489)
actions are being renamed to command menu item, they will be migrated to server and will be served as headless front components --------- Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com> |
||
|
|
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 |
||
|
|
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` |
||
|
|
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 |
||
|
|
5853891b02 | refactor!: rename Command Menu page/navigation layer to Side Panel (#18393) | ||
|
|
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> |
||
|
|
4cfd738312 |
Headless action modal (#18270)
https://github.com/user-attachments/assets/809a281f-3c38-41df-99db-e780941acf9f |
||
|
|
6351c6c1c6 |
feat: remember original URL and redirect after login (#18308)
## Summary - Implement a return-to-path mechanism that preserves the user's intended destination across authentication flows (login, magic link, cross-domain redirects) - Uses layered persistence: Jotai atom (in-memory), sessionStorage with TTL (tab-switch resilience), URL query parameter (cross-domain propagation) - Includes path validation to prevent open redirects, automatic cleanup after successful login, and comprehensive test coverage - Replaces the unused `previousUrlState` with a robust `returnToPathState` system ## Test plan - [ ] Visit a deep link (e.g. `/objects/tasks`) while logged out — should redirect to login, then back to `/objects/tasks` after logging in - [ ] Visit an OAuth authorize link while logged out — should redirect to login, then to the authorize page - [ ] Test magic link flow: click sign-in link that opens new tab — should still redirect to original destination - [ ] Test cross-domain: visit `app.twenty.com/objects/tasks` — should preserve path through workspace domain redirect - [ ] Verify auth/onboarding paths are excluded from being saved as return paths - [ ] Verify return-to-path is cleared after successful navigation - [ ] All 215 existing `usePageChangeEffectNavigateLocation` tests pass Made with [Cursor](https://cursor.com) |
||
|
|
012d819557 |
OAuth Client — Unified ApplicationRegistration, OAuth server, and frontend (#18267)
## Summary Consolidates three separate PRs (#18260, #18261, #18262) into a single unified branch with all review feedback addressed: ### New features - **ApplicationRegistration entity** — server-level registration for OAuth apps with encrypted server variables - **OAuth 2.0 server** — authorization code, client credentials, refresh token grants with PKCE support - **OAuth discovery endpoint** — `.well-known/oauth-authorization-server` metadata - **Frontend UI** — app registration details page with credential management, redirect URI editing, and server variable configuration - **CLI integration** — `twenty dev` auto-registers apps and stores OAuth credentials locally - **Authorize consent screen** — OAuth consent page at `/authorize` showing requested scopes ### Review feedback addressed **Renames (PR #18260):** - `appRegistration` → `applicationRegistration` (entity, tables, files, imports, GraphQL types) - `appRegistrationVariable` → `applicationRegistrationVariable` - `clientId` → `oAuthClientId`, `clientSecretHash` → `oAuthClientSecretHash`, `redirectUris` → `oAuthRedirectUris`, `scopes` → `oAuthScopes` **Security fixes (PR #18261):** - Fixed redirect URI validation bypass when `oAuthRedirectUris` is an empty array - Fixed workspace isolation in `clientCredentialsGrant` — now uses `find()` with explicit handling for multiple installations - Added error logging in refresh token `catch` block instead of silently swallowing **Code quality (PR #18262):** - Split `VersionDistributionEntry` into its own file (one export per file) - Split GraphQL queries and mutations into individual files with a shared fragment - Removed unused `OAuth` entry from `AuthProviderEnum` - Added loading state to `handleRotateSecret` - Removed 27 narration-style comments from test files - Added proper guards (`PublicEndpointGuard`, `NoPermissionGuard`) to controllers and resolvers ## Test plan - [ ] Verify `twenty dev` registers an app and stores OAuth credentials - [ ] Test OAuth authorization code flow end-to-end (authorize → token → API call) - [ ] Test client credentials grant - [ ] Verify redirect URI validation rejects requests when no URIs are registered - [ ] Verify app registration detail page renders correctly - [ ] Test secret rotation with loading state - [ ] Verify server variable editing and saving - [ ] Run `npx nx database:reset twenty-server` to validate migration Closes #18260, #18261, #18262 Made with [Cursor](https://cursor.com) --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> |
||
|
|
121788c42f |
Fully deprecate old recoil (#18210)
## Summary Removes the `recoil` dependency entirely from `package.json` and `twenty-front/package.json`, completing the migration to Jotai as the sole state management library. Removes all Recoil infrastructure: `RecoilRoot` wrapper from `App.tsx` and test decorators, `RecoilDebugObserver`, Recoil-specific ESLint rules (`use-getLoadable-and-getValue-to-get-atoms`, `useRecoilCallback-has-dependency-array`), and legacy Recoil utility hooks/types (`useRecoilComponentState`, `useRecoilComponentValue`, `createComponentState`, `createFamilyState`, `getSnapshotValue`, `cookieStorageEffect`, `localStorageEffect`, etc.). Renames all `V2`-suffixed Jotai state files and types to their canonical names (e.g., `ComponentStateV2` -> `ComponentState`, `agentChatInputStateV2` -> `agentChatInputState`, `SelectorCallbacksV2` -> `SelectorCallbacks`), and removes the now-redundant V1 counterparts. Updates ~433 files across the codebase to use the renamed Jotai imports, remove Recoil imports, and clean up test wrappers (`RecoilRootDecorator` -> `JotaiRootDecorator`). |
||
|
|
d81f006979 |
Rename Jotai state hooks and utilities to consistent Atom-based naming (#18209)
## Summary Rename all Jotai-based state management hooks and utilities to a consistent naming convention that: 1. Removes legacy Recoil naming (`useRecoilXxxV2`, `createXxxV2`) 2. Always includes `Atom` in the name to distinguish from jotai native hooks 3. Follows a consistent ordering: `Atom` → `Component` → `Family` → `Type` (`State`/`Selector`) 4. Includes the type qualifier (`State` or `Selector`) in all value-reading hooks to avoid naming conflicts with jotai's native `useAtomValue` ## Naming Convention **Hooks**: `use[Set]Atom[Component][Family][State|Selector][Value|State|CallbackState]` **Utils**: `createAtom[Component][Family][State|Selector]` ## Changes ### Hooks renamed (definition files + all usages across ~1,500 files): | Old Name (Recoil) | Intermediate (Atom) | Final Name | |---|---|---| | `useRecoilValueV2` | `useAtomValue` | **`useAtomStateValue`** | | `useRecoilStateV2` | `useAtomState` | `useAtomState` | | `useSetRecoilStateV2` | `useSetAtomState` | `useSetAtomState` | | `useFamilyRecoilValueV2` | `useFamilyAtomValue` | **`useAtomFamilyStateValue`** | | `useSetFamilyRecoilStateV2` | `useSetFamilyAtomState` | **`useSetAtomFamilyState`** | | `useFamilySelectorValueV2` | `useFamilySelectorValue` | **`useAtomFamilySelectorValue`** | | `useFamilySelectorStateV2` | `useFamilySelectorState` | **`useAtomFamilySelectorState`** | | `useRecoilComponentValueV2` | `useAtomComponentValue` | **`useAtomComponentStateValue`** | | `useRecoilComponentStateV2` | `useAtomComponentState` | `useAtomComponentState` | | `useSetRecoilComponentStateV2` | `useSetAtomComponentState` | `useSetAtomComponentState` | | `useRecoilComponentFamilyValueV2` | `useAtomComponentFamilyValue` | **`useAtomComponentFamilyStateValue`** | | `useRecoilComponentFamilyStateV2` | `useAtomComponentFamilyState` | `useAtomComponentFamilyState` | | `useSetRecoilComponentFamilyStateV2` | `useSetAtomComponentFamilyState` | `useSetAtomComponentFamilyState` | | `useRecoilComponentSelectorValueV2` | `useAtomComponentSelectorValue` | `useAtomComponentSelectorValue` | | `useRecoilComponentFamilySelectorValueV2` | `useAtomComponentFamilySelectorValue` | `useAtomComponentFamilySelectorValue` | | `useRecoilComponentStateCallbackStateV2` | `useAtomComponentStateCallbackState` | `useAtomComponentStateCallbackState` | | `useRecoilComponentSelectorCallbackStateV2` | `useAtomComponentSelectorCallbackState` | `useAtomComponentSelectorCallbackState` | | `useRecoilComponentFamilyStateCallbackStateV2` | `useAtomComponentFamilyStateCallbackState` | `useAtomComponentFamilyStateCallbackState` | | `useRecoilComponentFamilySelectorCallbackStateV2` | `useAtomComponentFamilySelectorCallbackState` | `useAtomComponentFamilySelectorCallbackState` | ### Utilities renamed: | Old Name (Recoil) | Final Name | |---|---| | `createStateV2` | **`createAtomState`** | | `createFamilyStateV2` | **`createAtomFamilyState`** | | `createSelectorV2` | **`createAtomSelector`** | | `createFamilySelectorV2` | **`createAtomFamilySelector`** | | `createWritableSelectorV2` | **`createAtomWritableSelector`** | | `createWritableFamilySelectorV2` | **`createAtomWritableFamilySelector`** | | `createComponentStateV2` | **`createAtomComponentState`** | | `createComponentFamilyStateV2` | **`createAtomComponentFamilyState`** | | `createComponentSelectorV2` | **`createAtomComponentSelector`** | | `createComponentFamilySelectorV2` | **`createAtomComponentFamilySelector`** | ## Details - All definition files renamed to match new convention - All import paths and usages updated across the entire `twenty-front` package - Jotai's native `useAtomValue` is aliased as `useJotaiAtomValue` in the wrapper hook to avoid collision - Legacy Recoil files in `state/utils/` and `component-state/utils/` left untouched (separate naming scope) - Typecheck passes cleanly |
||
|
|
eaf9fe27b2 | Improve Jotai work (#18205) | ||
|
|
0f2af6a6cb |
Jotai 13 (#18178)
## Recoil to Jotai Migration — PR 13: Workflow, Page Layout, AI, Activities, Settings, SSE & Remaining Modules ### Summary This is the 13th PR in the [14-PR migration series](packages/twenty-front/src/modules/ui/utilities/state/jotai/MIGRATION_PLAN.md) to replace Recoil with Jotai as the state management library in `twenty-front`. This PR migrates the remaining consumer code across all modules — updating ~1,076 files with ~10k insertions/deletions. ### What changed **New Jotai infrastructure (13 new files):** - `createComponentSelectorV2` and `createComponentFamilySelectorV2` — instance-scoped derived atoms, replacing Recoil's component selectors - 6 new hooks: `useRecoilComponentSelectorValueV2`, `useRecoilComponentFamilySelectorValueV2`, `useRecoilComponentSelectorCallbackStateV2`, `useRecoilComponentFamilySelectorCallbackStateV2`, `useRecoilComponentStateCallbackStateV2`, `useRecoilComponentFamilyStateCallbackStateV2` - New types: `ComponentSelectorV2`, `ComponentFamilySelectorV2`, `SelectorCallbacksV2` - Extended `buildGetHelper` to support the new selector patterns **State definition migrations:** - `createState()` → `createStateV2()` (Jotai `atom()`) - `createFamilyState()` → `createFamilyStateV2()` (Jotai `atom()` with family cache) - Recoil `selector`/`selectorFamily` → Jotai derived `atom()` via V2 utilities **Hook migrations (~2,400 removed, ~1,545 added V2 equivalents):** - `useRecoilValue` → `useRecoilValueV2` - `useRecoilState` → `useRecoilStateV2` - `useSetRecoilState` → `useSetRecoilStateV2` - `useRecoilCallback` → `useCallback` + `jotaiStore.get/set` - `useRecoilComponentValue` → `useRecoilComponentValueV2` - `useSetRecoilComponentState` → `useSetRecoilComponentStateV2` - `useRecoilComponentFamilyValue` → `useRecoilComponentFamilyValueV2` / `useFamilySelectorValueV2` - ~397 direct `import from 'recoil'` removed **Deleted files (9 files):** - `dateLocaleState.ts` — consolidated - `currentAIChatThreadTitleStateV2.ts` — renamed to replace the original - `isCommandMenuOpenedState.ts` — removed - `filesFieldUploadState.ts` — replaced by V2 - `recordStoreFamilySelector.ts`, `recordStoreFieldValueSelector.ts`, `recordStoreRecordsSelector.ts` — replaced by V2 equivalents - `settingsAllRolesSelector.ts` — replaced by `useSettingsAllRoles` hook - `settingsRoleIdsStateV2.ts` — consolidated **Modules migrated:** - Workflow (diagram, steps, variables, filters) - Page Layout (widgets, fields, graph, tabs) - AI (chat, agents, browsing context) - Activities (rich text editor, targets, timeline) - Action Menu (single/multiple record actions) - Command Menu (navigation, history, pages, workflow) - Context Store - Record Board, Record Table, Record Calendar, Record Index - Record Field, Record Filter, Record Sort, Record Group - Record Drag, Record Picker, Record Store - Views (view bar, view picker, filters, sorts) - Settings (roles, permissions, SSO, security, data model, developers, domains) - SSE (event streams, subscriptions) - Spreadsheet Import - Auth, Favorites, Front Components, Information Banner |
||
|
|
0d4fe4575b |
Various SDK improvements (#18115)
## Summary - **Refactor frontend metadata loading architecture**: Split the monolithic `EagerMetadataLoadEffect` into focused provider effects (`UserMetadataProviderEffect`, `ObjectMetadataProviderEffect`, `ViewMetadataProviderEffect`) orchestrated by `MetadataProviderEffects`. Replaced `UserProvider` + `ObjectMetadataItemsProvider` with a single `MetadataGater` that gates rendering on `isAppMetadataReadyState`. The metadata store now validates view-object consistency before promoting views, and `updateDraft` skips no-op updates via deep equality checks. - **SDK CLI improvements**: Added `app:typecheck` command, improved error handling in API sync (extracts GraphQL error messages), added `serializeError` utility for human-readable error output, added `error` file status to dev mode orchestrator with UI support, and fixed ClickHouse migration/seed commands to use `transpile-only`. |
||
|
|
b51eb6471f |
Jotai 12 (#18160)
Co-authored-by: Cursor <cursoragent@cursor.com> |