41d5d80a65ae51cddafbfa0ed967b6a61acb8ee9
234 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
41d5d80a65 |
Migrate Company and Person standard fields in preparation for the enrichment app (#21171)
# Migrate Company and Person standard fields in preparation for the
enrichment app
## Why
Our standard `Person`/`Company` objects accumulated fields that aren't
generic to every
business, while missing a more universal revenue field that essentially
every CRM ships.
This PR makes the **Standard application** hold a tighter, more
universal set of fields,
and sets the stage for a follow-up PR that introduces a **People Data
Labs enrichment app**
to populate them.
## What changes
### Standard fields
**Demoted (Standard → Workspace Custom application)** — not generic
enough to ship as standard:
| Object | Field | Type |
| ------- | ------------------------------ | -------- |
| Company | annualRecurringRevenue (ARR) | CURRENCY |
| Company | employees | NUMBER |
| Company | idealCustomerProfile (ICP) | BOOLEAN |
| Company | xLink (X/Twitter) | LINKS |
| Person | xLink (X/Twitter) | LINKS |
| Person | city | TEXT |
**Added (new generic Standard field)** — present in
Salesforce/HubSpot/Zoho, PDL-populatable:
| Object | Field | Type |
| ------- | ------------- |
-------------------------------------------------------- |
| Company | annualRevenue | CURRENCY (generic total revenue; replaces
the niche ARR) |
### Behavior by workspace
* **New workspaces:** demoted fields are gone; `annualRevenue` is
**active**.
* **Existing workspaces:** demoted fields are **preserved as active
custom fields, data intact**;
`annualRevenue` is created **inactive (opt-in)** with its column ready,
so a later activation
is a metadata-only toggle.
### Upgrade commands (v2.9)
Three idempotent, per-workspace commands, run in timestamp order:
1. **`upgrade:2-9:move-demoted-standard-fields-to-custom-application`**
(1799000040000) —
re-owns the 6 demoted fields to the workspace custom application
(`isCustom = true`,
new `applicationId` + fresh `universalIdentifier`), keeping their data
and active state.
2. **`upgrade:2-9:rename-conflicting-custom-fields`** (1799000045000) —
if a workspace already
has a *custom* field named `annualRevenue`, renames it to
`annualRevenueCustom`
(data preserved via column rename) so the standard field can be added.
Skips non-custom matches.
3. **`upgrade:2-9:add-inactive-generic-standard-fields`**
(1799000050000) — creates
`Company.annualRevenue` on existing workspaces as inactive, guarded to
skip workspaces
missing the target object or where the name is still taken.
**Failure model:** the workspace iterator isolates failures per
workspace (one workspace failing
never affects others); within a workspace the runner records per-command
status and resumes on the
next run, and every command is idempotent, so partial runs self-heal.
### Supporting changes
* **Field-option color palette:** widened the `TagColor` union
(`twenty-shared` `FieldMetadataOptions`
+ the field-metadata `options.input` DTO) from 10 colors to the full
theme palette, benefiting any
future SELECT/MULTI_SELECT field.
* **Dev seeder:**
* The default "Annual Recurring Revenue" dashboard widget now points at
the generic
`annualRevenue` field (renamed to "Annual Revenue").
* Removed the "Companies by Size (Stacked by City)" widget (relied on
the demoted `employees`).
* `employees` is dropped from company data seeds and re-added as a
**custom** field seed, so dev
workspaces still get an `employees` column matching the demoted
behavior.
### Cleanup
Front-end record types (`Company.ts`/`Person.ts`), the
`getDisplayNameFromParticipant` test mock,
metadata integration specs, the Zapier `crud_record` test, and the
regenerated
`get-standard-object-metadata-related-entity-ids` snapshot.
## ⚠️ Breaking change (intentional)
Removes standard fields `Company.annualRecurringRevenue`,
`Company.employees`,
`Company.idealCustomerProfile`, `Company.xLink`, `Person.xLink`, and
`Person.city` from the core
GraphQL schema (replaced by `Company.annualRevenue`).
This is why the breaking-changes check reports a large number of
removals — `graphql-inspector`
flags any removed object field plus its derived
aggregate/order-by/filter/update types.
**Mitigation:** the
`upgrade:2-9:move-demoted-standard-fields-to-custom-application` command
re-owns these fields as custom fields per workspace, preserving their
name and data, so existing
tenants keep working. New workspaces won't have them.
|
||
|
|
75df1f3997 |
chore(settings): address review comments from PR 21072 (#21121)
## Summary Round through bosiraphael's 31 review threads on the merged PR #21072 (discovery hero + ephemeral playground token). The user asked to apply each suggestion only where it adds value, so this PR is split into three buckets. ### Comments (~17 threads) - Tightened security-rationale / CSS-gotcha / API-doc comments to one or two factual lines - Kept (shortened) the comments above `RequireAccessTokenGuard` call sites — without them a future reader could remove the guard and silently reopen the escalation hole - Kept (shortened) the in-memory-only rationale on `playgroundApiKeyState` for the same reason - Kept `flex: 1 + min-height: 0` CSS gotcha on `SubMenuTopBarContainer` — non-obvious and easy to break ### Structure / extraction - Move `WEBHOOK_TABLE_ROW_GRID_TEMPLATE_COLUMNS` to its own constants file (one-export-per-file) - Split `SettingsAgentToolsTab` and `SettingsAgentToolsTable` across queries/, hooks/, types/, utils/: - `graphql/queries/findManyApplicationsForToolTable.ts` - `graphql/queries/findManyMarketplaceAppsForToolTable.ts` - `hooks/useSettingsAgentToolsTable.ts` (data loading + index merging) - `types/SettingsAgentToolItem|Application|MarketplaceApp` - `utils/getToolApplicationId|getToolLink` - Extract `SettingsAiModelsTab` optimistic mutations into `hooks/useSettingsAiModelsActions` (handleModelFieldChange, handleUseRecommendedToggle, handleModelToggle, handleToggleAllVisibleModels) - Extract `SettingsAI.handleCreateTool` into `hooks/useCreateTool` - Drop unnecessary `useMemo` wrappers on `heroTabs` arrays (SettingsObjects, SettingsLayout) - Simplify `MenuItemToggle` handler in SettingsAgentSkillsTab: `onToggleChange={setShowDeactivated}` (no longer wrapping with arrow + read of stale `!showDeactivated`) ### Hero assets - Replace placeholder `customize-illustration` with per-page exports - Rename `layout/customize-illustration-{light,dark}.png` → `layout/cover-{light,dark}.png` - Add `cover-{light,dark}.png` for **applications** and **members** (they were both pointing at the layout placeholder as a TODO) - Overwrite `data-model/cover-*.png`, `playground/cover-*.png`, `ai/ai-tools-cover-*.png` with the new exports ## Test plan - [ ] `npx nx typecheck twenty-front` ✅ - [ ] `npx nx typecheck twenty-server` ✅ - [ ] `npx nx lint twenty-front` ✅ (oxlint + oxfmt, 0 warnings/errors) - [ ] `/settings/layout`, `/settings/data-model`, `/settings/applications`, `/settings/ai`, `/settings/api-webhooks`, `/settings/members` each render the new hero illustration (light + dark) - [ ] AI tab: tool list still loads, search + Custom/Managed/Standard filters still work, "New Tool" still navigates to detail - [ ] AI tab: Models tab — smart/fast model select, "Use best models only" toggle, per-model checkboxes, toggle-all all still optimistic+revert on error - [ ] Skills tab: "Deactivated" toggle still flips show/hide - [ ] Webhooks table still uses the 1fr 28px grid |
||
|
|
f4ead89956 |
refactor(twenty-orm): migrate 23 grandfathered entities to WorkspaceScopedRepository (#20987)
## Summary Follow-up to #20953. Migrates 23 of the 30 entities that were left in `WORKSPACE_SCOPED_EXEMPTIONS` last time, so the lint rule's workspaceId-enforcement default now covers most of the core/metadata schema. ### Migrated (23 entities, 88 files, 22 commits) | Family | Entities | |---|---| | Trivial caches | `NavigationMenuItem`, `Skill`, `DataSource`, `Webhook`, `CommandMenuItem`, `IndexMetadata` | | Views | `View`, `ViewField`, `ViewFieldGroup`, `ViewFilter`, `ViewFilterGroup`, `ViewGroup`, `ViewSort` | | Layouts | `PageLayout`, `PageLayoutTab`, `PageLayoutWidget` | | Roles & permissions | `Role`, `RoleTarget`, `PermissionFlag`, `ObjectPermission`, `FieldPermission`, `RowLevelPermissionPredicate`, `RowLevelPermissionPredicateGroup` | For each entity: swap `@InjectRepository(X)` → `@InjectWorkspaceScopedRepository(X)` (and the field type → `WorkspaceScopedRepository<X>`); rewrite every call site to pass `workspaceId` as the first arg (stripped from `where`/criteria — the wrapper throws if you include it now); register `provideWorkspaceScopedRepository(X)` in every owning NestJS module; update affected spec providers to `getWorkspaceScopedRepositoryToken(X)`. ### Rule update - `ApplicationRegistrationVariableEntity` was misclassified — moved to `STRUCTURAL_EXEMPTIONS` (no `workspaceId` column; it's keyed on `applicationRegistrationId` at the instance level). - 22 of the 23 migrated entities removed from `WORKSPACE_SCOPED_EXEMPTIONS` entirely (zero remaining raw `@InjectRepository` sites). - `RoleTargetEntity` also removed; one call site in `user-workspace.service.ts` keeps a raw injection with an `eslint-disable` + reason because `softRemove(...)` is not on the wrapper API yet (the migration would require threading `workspaceId` through `deleteUserWorkspace`'s three callers). ### Still exempted (7 entities, follow-up PRs) | Entity | Why deferred | |---|---| | `ApplicationEntity` | ~50 sites with several cross-workspace lookups by id (auth, OAuth, file-storage, cleanup) | | `CalendarChannelEntity` / `MessageChannelEntity` | Use `.increment(...)` (not on wrapper) and `repository.manager.transaction(...)` — wrapper needs to grow `.increment` + the transaction sites need `withManager` or dual-inject | | `FieldMetadataEntity` / `ObjectMetadataEntity` | The metadata services `extends TypeOrmQueryService<X>` and `super(rawRepo)` — requires dual-inject or reworking the inheritance | | `KeyValuePairEntity` | Allows `workspaceId: IsNull()` for instance-level config; wrapper rejects null | | `UpgradeMigrationEntity` | Same — instance-level + cross-workspace ledger | ## Test plan - [x] `npx nx typecheck twenty-server` — clean - [x] `npx nx lint twenty-server` — clean (0/0) - [x] All 10 affected unit specs pass (115 tests) — api-key, agent-role, permissions, workspace-roles-permissions-cache, view-filter-group, workflow-version-step-operations, two-factor-authentication (service + resolver), user-workspace, file - [ ] Server integration tests in CI |
||
|
|
c5606212f2 |
Ses outbound followup (#20610)
This pull request unifies outbound with inbound under the new feature and the new email groups feature. These are workspace level shared inboxes that are shared between all workspace members. outbound sending with SES works, we only listen for tenant status events, rest is managed by AWS PR refactors old code and webhook to be split for outbound and inbound for proper separation | Area | Change | |---|---| | AWS SES driver | Split into `AwsSesRegisterDomainService` (tenant + identity + DKIM + MAIL FROM + configuration-set + EventBridge dest + contact list) and `AwsSesSendEmailService` (SendEmail). | | Reputation webhook | New `/webhooks/messaging/ses/outbound` route. SES → EventBridge (`Sending Status Enabled/Disabled` on default bus) → SNS → router → `SesOutboundSendingStateHandlerService` updates `emailing_domain.tenantStatus`. | | Inbound webhook | Refactored into `SesInboundWebhookRouterService` + `SesInboundMailHandlerService`. Shared `SnsSignatureVerifierService` + `SnsSubscriptionConfirmerService` across both routes. | | Global uniqueness | New migration + instance command: `emailing_domain.domain` is now globally unique (one tenant per domain across workspaces). | | Tenant status | New `emailing_domain.tenantStatus` column (`ACTIVE` / `PAUSED`) + `EmailingDomainTenantStatusService`. | | Send-email mutation | New `sendEmailViaDomain` GraphQL mutation + DTOs. | | Cleanup | `EmailingDomainWorkspaceCleanupJob` wired into `WorkspaceService.deleteWorkspace` — tears down SES tenant association + identity on workspace delete. | | Settings UI | Rewritten around reusable `SettingsTableListSection`. "Email Group" → "Email Handle" rename. New cells for status/source/forwarding. Outbound domains surfaced on workspace settings page. | ### Env vars (new) All in `config-variables.ts`, group `AWS_SES_SETTINGS`, all optional: - `AWS_SES_REGION` — `@IsAWSRegion`, consumed by `AwsSesClientProvider` + driver factory - `AWS_SES_ACCOUNT_ID` — used for ARN construction in driver factory - `SES_SNS_TOPIC_ARN_ALLOWLIST` — **shared** by inbound + outbound webhook routers, comma-separated list of accepted SNS topic ARNs (verified via `sns-payload-validator`) ### Migrations - `1778862608620-add-emailing-domain-tenant-status` (fast) — adds `tenantStatus` column. - `1778865501791-unique-emailing-domain-globally` (slow, idempotent) — enforces global uniqueness on `domain`. - Instance commands bumped to `2.5`. ### Infra dependency Two coupled twenty-infra PRs: - `ses-inbound-email` — receipt-rule + inbound SNS topic + S3 bucket policy + KMS grant + `email_group_*` outputs. - `ses-outbound-tf` — EventBridge rule + outbound SNS topic + SES IAM policy + outbound `webhook_url` subscription. **Based on `ses-inbound-email`.** Merge order: inbound first, then outbound. Outbound PR's chart edit owns the comma-joined `SES_SNS_TOPIC_ARN_ALLOWLIST` value (both ARNs). Features lives under `/settings/general` <img width="1496" height="845" alt="SCR-20260519-ofhi-2" src="https://github.com/user-attachments/assets/a025485a-09f7-4131-91cd-0067690ff18d" /> --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Félix Malfait <FelixMalfait@users.noreply.github.com> |
||
|
|
b8de469f37 |
Refactor and centralize file mimeType integrity check and sanitization (#20889)
# Introduction closes https://github.com/twentyhq/private-issues/issues/484 This PR refactors the writeFile API to never expect to be passed a mimetype, its extract is done programmatically low level so any callers will pass through Same for the file sanitization ## IANA override Disclaimer for consistency we existing behavior we wanted to always have `application/typescript` - should we rather consider fallbacking to octect-steam instead ? - Any pulbic assets that has .ts will now also fallback to `application/typescript` instead of the official IANA ## Integration Added coverage |
||
|
|
76e144e85a |
Deprecate messageChannel messageFolder calendarChannel standard objects (#20836)
# Introduction Removing old standard objects `messageChannel` and `messageFolder` and `calendarChannel` --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
9b9c97a049 |
Deprecate and backfill delete ConnectedAccount twenty standard object (#20752)
# Introduction Following connected account permissions refactor and encryption Removing the old workspace schema twenty standard application connectedAccount objects and related standard fields and index - a lot of deadcode - instance command backfill cleaning the connected account object from workspaces |
||
|
|
04eb913551 |
chore(page-layout): remove IS_RECORD_PAGE_LAYOUT_* feature flags (#20556)
## Summary
- Both \`IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED\` and
\`IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED\` are force-enabled on
every existing workspace by the 1.23.0 upgrade command
\`BackfillRecordPageLayoutsCommand\` and seeded enabled for new
workspaces via \`DEFAULT_FEATURE_FLAGS\` +
\`seed-feature-flags.util.ts\`. They are no longer load-bearing.
- Unwrap all \`if (flag) { … }\` conditionals to their enabled branch on
both server and front.
- Delete legacy fallback files that only the disabled branch reached:
\`PageLayoutRelationWidgetsSyncEffect\`,
\`usePageLayoutWithRelationWidgets\`,
\`reInjectDynamicRelationWidgetsFromDraft\`,
\`injectRelationWidgetsIntoLayout\`, \`isDynamicRelationWidget\` (and
their tests).
- Strip the two \`enableFeatureFlags\` calls from the 1.23 upgrade
command — the page-layout backfill data logic itself is kept intact
since old workspaces upgrading from < 1.23 still need it.
- No DB cleanup migration: stale \`featureFlag\` rows are left in place,
matching the precedent set by #20531 and #20460.
Net diff: 37 files, +106 / -1727.
## Test plan
- [x] \`npx nx typecheck twenty-shared twenty-server twenty-front\` —
all pass
- [x] \`npx nx lint:diff-with-main twenty-server twenty-front\` — all
pass
- [x] \`cd packages/twenty-front && npx jest page-layout\` — 1240 tests,
all pass
- [x] \`cd packages/twenty-server && npx jest
workspace-entity-manager.spec\` — pass
- [ ] Manual smoke: open a record page, verify tabs render and \"Edit
Layout\" command-menu action is available
- [ ] Manual smoke: Settings → Data model → object → Layout tab is
visible (and hidden for remote / Dashboard objects)
- [ ] Manual smoke: edit a tab title, save, reload — confirm persistence
|
||
|
|
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> |
||
|
|
bbd9720ab3 |
[Dashboards] [Warning] Remove gauge chart support and delete existing widgets (#20172)
## Summary Removes gauge chart from the chart-type picker and deletes existing gauge widgets via a workspace migration. The gauge was rendering a hardcoded `0.7 / "Progress"` stub regardless of configuration -- never wired to real data. The contract stays in place. We keep `WidgetConfigurationType.GAUGE_CHART`, the DTO, the GraphQL union member, and the gauge folder -- so stored gauge JSON still resolves through the schema. The render path falls through to `default: return null`, so any un-migrated gauge widget renders as an empty cell, not a crash. This PR just removes existing gauge widgets if there are any (via `upgrade:2-3:delete-gauge-widgets`). The deliberate cleanup -- deleting the type definitions, the gauge folder, the DTO -- comes in a follow-up PR after the migration has run. --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
f7d812fca6 |
fix: disable sync on seeded message and calendar channels (#20168)
## Summary The dev seeder creates `ConnectedAccount` records with fake OAuth tokens (`'exampleRefreshToken'` / `'exampleAccessToken'`) and points `MessageChannel` / `CalendarChannel` records at them with `isSyncEnabled: true`. When the sync cron jobs run in the demo workspace, they: 1. Pick up these seeded channels (filter is `isSyncEnabled: true` + pending sync stage) 2. Try to refresh the fake OAuth tokens 3. Mark the channels as `FAILED_INSUFFICIENT_PERMISSIONS` 4. Surface a "Sync lost with mailbox X — please reconnect" banner in the UI This banner appears every time the demo workspace is loaded, even though nothing is actually broken. ## Fix Set `isSyncEnabled: false` on all 12 seeded channels (6 message, 6 calendar). This is the canonical "don't sync this channel" mechanism — the same state a real user lands in when they toggle sync off in account settings. ## Why this approach - **ConnectedAccount records stay**: the demo workspace still shows Tim, Jony, Phil, Jane as having connected their email/calendar — realistic - **Pre-seeded messages and calendar events stay visible**: those don't depend on `isSyncEnabled` - **Crons no longer pick them up**: they filter on `isSyncEnabled: true`, so `false` short-circuits the entire sync attempt — no failure, no banner - **Semantically correct**: the seeded accounts have fake tokens that were never going to sync successfully; `isSyncEnabled: true` was effectively a lie - **No production code touched**: no `isDemo` flags, no magic-string detection, no workspace-ID filters in the cron path ## Alternatives considered and rejected - **Add an `isDemo` flag**: schema change, leaks demo knowledge into production tables - **Skip channels with fake tokens (`example*` pattern)**: hacky magic-string detection in the auth refresh path - **Filter demo workspace IDs in the cron**: production paths shouldn't reference demo IDs - **Don't activate demo workspaces**: breaks the demo workspace UX entirely ## Test plan - [ ] Reset the database and reseed (`npx nx database:reset twenty-server`) - [ ] Load the demo workspace — confirm no "Sync lost with mailbox" banner appears - [ ] Confirm seeded connected accounts still show in Settings → Accounts - [ ] Confirm pre-seeded messages and calendar events still appear in the UI - [ ] Confirm a real connected account (added via OAuth) still syncs normally — its channel will have `isSyncEnabled: true` and the cron will pick it up ## Related Companion to https://github.com/twentyhq/twenty/pull/20167, which removes the `--dev-mode` cron filter that was masking this banner issue in the dev image. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
30b8663a74 |
chore: remove IS_AI_ENABLED feature flag (#19916)
## Summary - AI is now GA, so the public/lab `IS_AI_ENABLED` flag is removed from `FeatureFlagKey`, the public flag catalog, and the dev seeder. - Drops every backend `@RequireFeatureFlag(IS_AI_ENABLED)` guard (agent, agent chat, chat subscription, role-to-agent assignment, workflow AI step creation) and the now-unused `FeatureFlagModule`/`FeatureFlagGuard` wiring in the AI and workflow modules. - Removes frontend gating from settings nav, role permissions/assignment/applicability, command menu hotkeys, side panel, mobile/drawer nav, and the agent chat provider so AI UI is always on. Tests and generated GraphQL/SDK schemas updated accordingly. ## Test plan - [x] `npx nx typecheck twenty-shared` - [x] `npx nx typecheck twenty-server` - [x] `npx nx typecheck twenty-front` - [x] `npx nx lint:diff-with-main twenty-server` - [x] `npx nx lint:diff-with-main twenty-front` - [x] `npx jest --config=packages/twenty-server/jest.config.mjs feature-flag` - [x] `npx jest --config=packages/twenty-server/jest.config.mjs workspace-entity-manager` - [ ] Manual smoke test: AI features still accessible without any flag row in `featureFlag` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a4cc7fb9c5 |
[Upgrade] Fix workspace creation cursor (#19701)
## Summary ### Problem The upgrade migration system required new workspaces to always start from a workspace command, which was too rigid. When the system was mid-upgrade within an instance command (IC) segment, workspace creation would fail or produce inconsistent state. ### Solution #### Workspace-scoped instance command rows Instance commands now write upgrade migration rows for **all active/suspended workspaces** alongside the global row. This means every workspace has a complete migration history, including instance command records. - `InstanceCommandRunnerService` reloads `activeOrSuspendedWorkspaceIds` immediately before writing records (both success and failure paths) to mitigate race conditions with concurrent workspace creation. - `recordUpgradeMigration` in `UpgradeMigrationService` accepts a discriminated union over `status`, handles `error: unknown` formatting internally, and writes global + workspace rows in batch. #### Flexible initial cursor for new workspaces `getInitialCursorForNewWorkspace` now accepts the last **attempted** (not just completed) instance command with its status: - If the IC is `completed` and the next step is a workspace segment → cursor is set to the last WC of that segment (existing behavior). - If the IC is `failed` or not the last of its segment → cursor is set to that IC itself, preserving its status. This allows workspaces to be created at any point during the upgrade lifecycle, including mid-IC-segment and after IC failure. #### Relaxed workspace segment validation `validateWorkspaceCursorsAreInWorkspaceSegment` accepts workspaces whose cursor is: 1. Within the current workspace segment, OR 2. At the immediately preceding instance command with `completed` status (handles the `-w` single-workspace upgrade scenario). Workspaces with cursors in a previous segment, ahead of the current segment, or at a preceding IC with `failed` status are rejected. ### Test plan created empty workspaces to allow testing upgrade with several active workspaces |
||
|
|
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 |
||
|
|
eb13378760 |
Fix Quick Lead command menu item not appearing (#19635)
- Refactored prefillWorkflowCommandMenuItems and prefillFrontComponentCommandMenuItems to use validateBuildAndRunWorkspaceMigration instead of raw TypeORM createQueryBuilder inserts - This ensures the flat entity cache is properly updated when seeding command menu items, fixing the Quick Lead item not appearing after workspace creation - Moved command menu item prefill calls outside the transaction since they now go through the migration pipeline |
||
|
|
69d228d8a1 |
Deprecate IS_RECORD_TABLE_WIDGET_ENABLED feature flag (#19662)
## Summary - Removes the `IS_RECORD_TABLE_WIDGET_ENABLED` feature flag, making the record table widget unconditionally available in dashboard widget type selection - The flag was already seeded as `true` for all new workspaces and only gated UI visibility in one component (`SidePanelPageLayoutDashboardWidgetTypeSelect`) - Cleans up the flag from `FeatureFlagKey` enum, dev seeder, and test mocks ## Analysis The flag only controlled whether the "View" (Record Table) widget option appeared in the dashboard widget type selector. The entire record table widget infrastructure (rendering, creation hooks, GraphQL types, `RECORD_TABLE` enum in `WidgetType`) is independent of the flag and fully implemented. No backend logic depends on this flag. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
bf5cc68f25 |
Rename standard and custom apps (#19631)
as title no migration for existing apps, changes only apply on new workspaces |
||
|
|
21142d98fe |
Implement cross version upgrade (#19559)
# Introduction Refactoring the upgrade engine to handle cross version upgrade, completely getting rid of the semver `version` at db and runtime level It remains a visual a listing indicator for or CD process but also during devenv in order to prepare next release Will write a release process runbook documentation on how to handle upgrade step patch, command insertion etc as it needs to be cascaded across all the involved supported version **The upgrade sequence model:** The sequence is a flat, ordered array of upgrade steps (`UpgradeStep[]`), built from the registry by chaining all versions in order, each version contributing its fast-instance → slow-instance → workspace commands sorted by timestamp. Version is metadata for logging, not used in the algorithm. **Segments:** The sequence naturally splits into alternating segments of contiguous instance steps and contiguous workspace steps. The runner processes segments in order: - **Instance segment:** Run sequentially from the instance cursor. Each step runs once globally. - **Workspace segment:** Each workspace independently walks from its own cursor through the end of the segment. Workspaces are independent within a segment — they can be at different positions. - **Synchronization (workspace → instance):** The runner blocks before entering an instance segment. All active/suspended workspaces must have completed the last workspace step of the preceding workspace segment. If any workspace failed, abort. This is the only explicit synchronization point. - Instance → workspace ordering is implicit — the runner processes segments sequentially, so the instance segment naturally completes before the workspace segment begins. full docs https://gist.github.com/prastoin/e62106d455fd72d6b6ebada8351e5492 ## Version constants & type-level deprecation Version management is split into three atomic constants: `TWENTY_PREVIOUS_VERSIONS`, `TWENTY_CURRENT_VERSION`, and `TWENTY_NEXT_VERSIONS`. Two derived constants compose them: `CROSS_UPGRADE_SUPPORTED_VERSIONS` (previous + current — what the engine runs) and `ALL_TWENTY_VERSIONS` (the full ordered tuple including next). The registry service validates at module init that no version is duplicated across constants and that at least one previous version exists. A `DeprecatedSinceVersion<RemoveAtVersion, T>` type utility resolves to `T` while `TWENTY_CURRENT_VERSION` is below `RemoveAtVersion`, and to `never` once it reaches it — turning deprecation into a compile-time guarantee via `IndexOf` and `IsGreaterOrEqual` generics in `twenty-shared`. ### `workspace.version` column deprecation The column is replaced by cursor-based state inference from `UpgradeMigration` records, but cannot be dropped in 1.22: workspaces activated during 1.21 predate the cursor system and need their initial cursor backfilled first (`backfillWorkspaceCreatedIn1_21_0Cursors`). This backfill itself depends on a new `isInitial` column on `UpgradeMigration`, bootstrapped via a targeted TypeORM migration before the upgrade sequence runs. Both functions and the entity field are typed with `DeprecatedSinceVersion<'1.23.0', ...>`. When `TWENTY_CURRENT_VERSION` reaches `1.23.0`, compile errors force their removal — and the pre-declared `DropWorkspaceVersionColumnFastInstanceCommand` takes over to drop the column. ## What's next - ci cross version upgrade ( wip ) - banner asking to contact twenty administrator if workspace is outdated - upgrade healthcheck cli ## New unit/integ test pattern Create a dedicated `createNestApp` that consumes a real database in order not to have to mack any database interaction to the `upgradeMigrations` allowing full coverage of the whole `upgradeRunnerService.run` core logic |
||
|
|
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> |
||
|
|
f6423f5925 |
Remove DataSourceService and clean up datasource migration logic (#19532)
## Summary - **Drop the `objectMetadata.dataSourceId` foreign key and index** via a 1-22 fast instance command — column kept nullable for data preservation - **Delete `DataSourceService`, `DataSourceModule`, and `DataSourceException`** — all code now uses `workspace.databaseSchema` directly - **Remove `IS_DATASOURCE_MIGRATED` feature flag** from default flags and all branching logic - **Simplify workspace/object creation pipelines** — `WorkspaceManagerService`, `DevSeederService`, and the object creation action handler no longer route through `DataSourceService` - **Keep `DataSourceEntity` and the `dataSource` table** for historical data — entity stripped of all ORM relations |
||
|
|
8a10071253 |
add workspaceId to indirect entities (#19522)
Required for `workspace:export` command --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
8702300b07 |
App feedbacks fix option id required in apps (#19386)
fixes https://discord.com/channels/1130383047699738754/1488226371032453292 |
||
|
|
6e23ca35e6 |
Pagelayout backfill command standard app (#19380)
## Context Due to the chosen strategy for "Reset to default" feature for page layouts. Those overridable entities need to be associated to the Standard app to work properly (there is no "Default" state for custom entities). Until we find a better implementation, I'm changing the backfill command to reflect that |
||
|
|
83d30f8b76 |
feat: Send email from UI — inline reply composer & SendEmail mutation (#19363)
## Summary - **Inline email reply**: Replace external email client redirects (Gmail/Outlook deeplinks) with an in-app email composer. Users can reply to email threads directly from the email thread widget or via the command menu. - **SendEmail GraphQL mutation**: New backend mutation that reuses `EmailComposerService` for body sanitization, recipient validation, and SMTP dispatch via the existing outbound messaging infrastructure. - **Side panel compose page**: Command menu "Reply" action now opens a side-panel compose email page with pre-filled To, Subject, and In-Reply-To fields. ### Backend - `SendEmailResolver` with `SendEmailInput` / `SendEmailOutputDTO` - `SendEmailModule` wired into `CoreEngineModule` - Reuses `EmailComposerService` + `MessagingMessageOutboundService` ### Frontend - `EmailComposer` / `EmailComposerFields` components - `useSendEmail`, `useReplyContext`, `useEmailComposerState` hooks - `useOpenComposeEmailInSidePanel` + `SidePanelComposeEmailPage` - `EmailThreadWidget` inline Reply bar with toggle composer - `ReplyToEmailThreadCommand` now opens side-panel instead of external links ### Seeds - Added `handle` field to message participant seeds for realistic email addresses - Seed `connectedAccount` and `messageChannel` in correct batch order ## Test plan - [ ] Open an email thread on a person/company record → verify "Reply..." bar appears below the last message - [ ] Click "Reply..." → composer opens inline with pre-filled To and Subject - [ ] Type a message and click Send → email is sent via SMTP, composer closes - [ ] Use command menu Reply action → side panel opens with compose email page - [ ] Verify Send/Cancel buttons work correctly in side panel - [ ] Test with Cc/Bcc toggle in composer fields - [ ] Verify error handling: invalid recipients, missing connected account Made with [Cursor](https://cursor.com) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
8acfacc69c |
Add email thread widget and message thread record page layout (#19351)
## Summary - Move email thread display from side panel to a dedicated record page with a new `EMAIL_THREAD` widget type - Add message thread as a standard object with page layout, subject field, and backfill command - Add reply-to-email command menu item for message thread records - Remove old side panel message thread components in favor of the new widget-based approach ## Type fixes - Add `EMAIL_THREAD` to `WidgetConfigurationType`, `WidgetType`, and all configuration/validator maps - Create `EmailThreadConfigurationDTO` and shared `EmailThreadConfiguration` type - Register EMAIL_THREAD in widget type validators, configuration resolvers, and standard widget mappings ## Test plan - [ ] Verify message thread record pages render with the email thread widget - [ ] Verify email thread preview navigates to the record page instead of opening side panel - [ ] Verify reply-to-email command appears for message thread records - [ ] Verify typecheck passes for both twenty-front and twenty-server - [ ] Run existing test suites to check for regressions 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
d3f0162cf5 |
Remove connected account feature flag (#19286)
Co-authored-by: martmull <martmull@hotmail.fr> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions <github-actions@twenty.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
80c5cfc2ec |
Add npm packages app settings list (#19324)
solves https://discord.com/channels/1130383047699738754/1488499318762635344 |
||
|
|
885ab8b444 |
Seed Front Components (#19220)
https://github.com/user-attachments/assets/6b0887e8-8ba4-49c9-9371-e3db3e9fdde8 |
||
|
|
9f95c4763c |
[COMMAND MENU ITEMS] Remove deprecated code (#19199)
This PR is the first one of a cleanup after upgrading command menu items to V2. |
||
|
|
08b041633a |
chore: remove 1-19 upgrade commands and navigation menu item feature flags (#19083)
## Summary - Deletes the entire `1-19` upgrade version command directory (7 files, ~1500 lines) and removes all references from the upgrade module and command runner. - Removes `IS_NAVIGATION_MENU_ITEM_ENABLED` and `IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED` feature flags from the enum, seed data, generated schema files, and test mocks. These flags had no remaining feature-gated code — navigation menu items are now always enabled. |
||
|
|
a3f468ef98 |
chore: remove IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED and IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED feature flags (#19082)
## Summary - Removes `IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED` feature flag, making row-level permission predicates always enabled. Removes early-return guards from query builders (select, update, insert) and the shared utility, the public feature flag metadata entry, and `updateFeatureFlag` calls from integration tests. - Removes `IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED` feature flag, making whole-day datetime filtering always enabled. Simplifies filter input components and hooks to always use date-only format for `IS` operand on `DATE_TIME` fields. - Cleans up enum definitions, seed data, generated schema files, and test mocks for both flags. |
||
|
|
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 |
||
|
|
28de34cf20 |
chore: remove IS_DASHBOARD_V2_ENABLED feature flag (#19079)
## Summary - Remove the `IS_DASHBOARD_V2_ENABLED` feature flag from the codebase - Dashboard V2 features (gauge charts, line charts, pie charts) are now always enabled - Remove the validator gate that blocked gauge chart creation/update without the flag - Clean up all related code: seed data, dev-seeder service, widget seeds, and test mocks |
||
|
|
e8cb086b64 |
chore: remove completed migration feature flags and upgrade commands <= 1.18 (#19074)
## Summary - Remove 3 completed migration feature flags: `IS_ATTACHMENT_MIGRATED`, `IS_NOTE_TARGET_MIGRATED`, `IS_TASK_TARGET_MIGRATED` — these were already enabled by default for all new workspaces via `DEFAULT_FEATURE_FLAGS` - Delete all upgrade command directories for versions <= 1.18 (`1-16/`, `1-17/`, `1-18/`) along with their module registrations, removing ~6,600 lines of dead migration code - Simplify frontend utility functions (`getActivityTargetObjectFieldIdName`, `getActivityTargetsFilter`, `getActivityTargetFieldNameForObject`, `generateActivityTargetMorphFieldKeys`, `findActivitiesOperationSignatureFactory`) by removing the `isMorphRelation` parameter and always using the morph relation path - Remove feature flag checks from 7 frontend hooks/components that were gating attachment and activity target behavior behind the removed flags - Simplify `buildDefaultRelationFlatFieldMetadatasForCustomObject` server util to always treat attachment, noteTarget, and taskTarget as morph relations without checking feature flags |
||
|
|
c407341912 |
feat: optimize hot database queries with multi-layer caching (#19068)
## Summary Introduces multi-layer caching for the 5 most frequent database queries identified in production (Sentry data), targeting the JWT authentication hot path and cron job logic. ### Problem Our database is under heavy load from uncached queries on the auth hot path: - `WorkspaceEntity` lookups: **638 queries/min** - `ApiKeyEntity` lookups: **491 queries/min** - `UserEntity` lookups: **147 queries/min** - `UserWorkspaceEntity` lookups: **143 queries/min** - `LogicFunctionEntity` lookups: **1800 queries/min** (cron job) ### Solution **1. New `CoreEntityCacheService`** for non-workspace-scoped entities (Workspace, User, UserWorkspace): - Mirrors `WorkspaceCacheService` architecture (in-process Map + Redis with hash validation) - Provider pattern with `@CoreEntityCache` decorator - Keyed by entity primary key (not workspaceId) - 100ms local TTL, Redis-backed hash validation for cross-instance consistency - Three providers: `WorkspaceEntityCacheProviderService`, `UserEntityCacheProviderService`, `UserWorkspaceEntityCacheProviderService` **2. New `apiKeyMap` WorkspaceCache** for workspace-scoped API key lookups: - `WorkspaceApiKeyMapCacheService` loads all API keys for a workspace into a map by ID - Leverages existing `WorkspaceCacheService` infrastructure - Cache invalidation on API key create/update/revoke **3. `CronTriggerCronJob` refactored** to use existing `flatLogicFunctionMaps` workspace cache: - Eliminates per-workspace `LogicFunctionEntity` repository queries (~1800/min) - Filters cached data in-memory instead **4. `JwtAuthStrategy` refactored** to use caches for all entity lookups: - Workspace, User, UserWorkspace → `CoreEntityCacheService` - ApiKey → `WorkspaceCacheService` (`apiKeyMap`) - Impersonation queries kept as direct DB queries (rare path, requires relations) **5. Cache invalidation** wired into mutation paths: - `WorkspaceService` → invalidates `workspaceEntity` on save/update/delete - `ApiKeyService` → invalidates `apiKeyMap` on create/update/revoke ### Architecture ``` Request → JwtAuthStrategy ├── Workspace lookup → CoreEntityCacheService (in-process → Redis → DB) ├── User lookup → CoreEntityCacheService (in-process → Redis → DB) ├── UserWorkspace lookup → CoreEntityCacheService (in-process → Redis → DB) └── ApiKey lookup → WorkspaceCacheService (in-process → Redis → DB) CronTriggerCronJob └── LogicFunction lookup → WorkspaceCacheService (flatLogicFunctionMaps) ``` ### Expected Impact | Query | Before | After | |-------|--------|-------| | WorkspaceEntity | 638/min | ~0 (cached) | | ApiKeyEntity | 491/min | ~0 (cached) | | UserEntity | 147/min | ~0 (cached) | | UserWorkspaceEntity | 143/min | ~0 (cached) | | LogicFunctionEntity | 1800/min | ~0 (cached) | ### Not included (ongoing separately) - DataSourceEntity query optimization (IS_DATASOURCE_MIGRATED migration) - ObjectMetadataEntity query optimization (already partially cached) |
||
|
|
50ea560e57 |
Seed company workflow for email upserts (#18909)
@thomtrp --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
33a474c8e6 |
Add record table widget feature flag (#18960)
Introduce a new feature flag for the record table widget, enabling conditional rendering and state management based on its status. Update related components and tests accordingly. --------- Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com> |
||
|
|
4ea2e32366 |
Refactor twenty client sdk provisioning for logic function and front-component (#18544)
## 1. The `twenty-client-sdk` Package (Source of Truth)
The monorepo package at `packages/twenty-client-sdk` ships with:
- A **pre-built metadata client** (static, generated from a fixed
schema)
- A **stub core client** that throws at runtime (`CoreApiClient was not
generated...`)
- Both ESM (`.mjs`) and CJS (`.cjs`) bundles in `dist/`
- A `package.json` with proper `exports` map for
`twenty-client-sdk/core`, `twenty-client-sdk/metadata`, and
`twenty-client-sdk/generate`
## 2. Generation & Upload (Server-Side, at Migration Time)
**When**: `WorkspaceMigrationRunnerService.run()` executes after a
metadata schema change.
**What happens in `SdkClientGenerationService.generateAndStore()`**:
1. Copies the stub `twenty-client-sdk` package from the server's assets
(resolved via `SDK_CLIENT_PACKAGE_DIRNAME` — from
`dist/assets/twenty-client-sdk/` in production, or from `node_modules`
in dev)
2. Filters out `node_modules/` and `src/` during copy — only
`package.json` + `dist/` are kept (like an npm publish)
3. Calls `replaceCoreClient()` which uses `@genql/cli` to introspect the
**application-scoped** GraphQL schema and generates a real
`CoreApiClient`, then compiles it to ESM+CJS and overwrites
`dist/core.mjs` and `dist/core.cjs`
4. Archives the **entire package** (with `package.json` + `dist/`) into
`twenty-client-sdk.zip`
5. Uploads the single archive to S3 under
`FileFolder.GeneratedSdkClient`
6. Sets `isSdkLayerStale = true` on the `ApplicationEntity` in the
database
## 3. Invalidation Signal
The `isSdkLayerStale` boolean column on `ApplicationEntity` is the
invalidation mechanism:
- **Set to `true`** by `generateAndStore()` after uploading a new client
archive
- **Checked** by both logic function drivers before execution — if
`true`, they rebuild their local layer
- **Set back to `false`** by `markSdkLayerFresh()` after the driver has
successfully consumed the new archive
Default is `false` so existing applications without a generated client
aren't affected.
## 4a. Logic Functions — Local Driver
**`ensureSdkLayer()`** is called before every execution:
1. Checks if the local SDK layer directory exists AND `isSdkLayerStale`
is `false` → early return
2. Otherwise, cleans the local layer directory
3. Calls `downloadAndExtractToPackage()` which streams the zip from S3
directly to disk and extracts the full package into
`<tmpdir>/sdk/<workspaceId>-<appId>/node_modules/twenty-client-sdk/`
4. Calls `markSdkLayerFresh()` to set `isSdkLayerStale = false`
**At execution time**, `assembleNodeModules()` symlinks everything from
the deps layer's `node_modules/` **except** `twenty-client-sdk`, which
is symlinked from the SDK layer instead. This ensures the logic
function's `import ... from 'twenty-client-sdk/core'` resolves to the
generated client.
## 4b. Logic Functions — Lambda Driver
**`ensureSdkLayer()`** is called during `build()`:
1. Checks if `isSdkLayerStale` is `false` and an existing Lambda layer
ARN exists → early return
2. Otherwise, deletes all existing layer versions for this SDK layer
name
3. Calls `downloadArchiveBuffer()` to get the raw zip from S3 (no disk
extraction)
4. Calls `reprefixZipEntries()` which streams the zip entries into a
**new zip** with the path prefix
`nodejs/node_modules/twenty-client-sdk/` — this is the Lambda layer
convention path. All done in memory, no disk round-trip
5. Publishes the re-prefixed zip as a new Lambda layer via
`publishLayer()`
6. Calls `markSdkLayerFresh()`
**At function creation**, the Lambda is created with **two layers**:
`[depsLayerArn, sdkLayerArn]`. The SDK layer is listed last so it
overwrites the stub `twenty-client-sdk` from the deps layer (later
layers take precedence in Lambda's `/opt` merge).
## 5. Front Components
Front components are built by `app:build` with `twenty-client-sdk/core`
and `twenty-client-sdk/metadata` as **esbuild externals**. The stored
`.mjs` in S3 has unresolved bare import specifiers like `import {
CoreApiClient } from 'twenty-client-sdk/core'`.
SDK import resolution is split between the **frontend host** (fetching &
caching SDK modules) and the **Web Worker** (rewriting imports):
**Server endpoints**:
- `GET /rest/front-components/:id` —
`FrontComponentService.getBuiltComponentStream()` returns the **raw
`.mjs`** directly from file storage. No bundling, no SDK injection.
- `GET /rest/sdk-client/:applicationId/:moduleName` —
`SdkClientController` reads a single file (e.g. `dist/core.mjs`) from
the generated SDK archive via
`SdkClientGenerationService.readFileFromArchive()` and serves it as
JavaScript.
**Frontend host** (`FrontComponentRenderer` in `twenty-front`):
1. Queries `FindOneFrontComponent` which returns `applicationId`,
`builtComponentChecksum`, `usesSdkClient`, and `applicationTokenPair`
2. If `usesSdkClient` is `true`, renders
`FrontComponentRendererWithSdkClient` which calls the
`useApplicationSdkClient` hook
3. `useApplicationSdkClient({ applicationId, accessToken })` checks the
Jotai atom family cache for existing blob URLs. On cache miss, fetches
both SDK modules from `GET /rest/sdk-client/:applicationId/core` and
`/metadata`, creates **blob URLs** for each, and stores them in the atom
family
4. Once the blob URLs are cached, passes them as `sdkClientUrls`
(already blob URLs, not server URLs) to `SharedFrontComponentRenderer` →
`FrontComponentWorkerEffect` → worker's `render()` call via
`HostToWorkerRenderContext`
**Worker** (`remote-worker.ts` in `twenty-sdk`):
1. Fetches the raw component `.mjs` source as text
2. If `sdkClientUrls` are provided and the source contains SDK import
specifiers (`twenty-client-sdk/core`, `twenty-client-sdk/metadata`),
**rewrites** the bare specifiers to the blob URLs received from the host
(e.g. `'twenty-client-sdk/core'` → `'blob:...'`)
3. Creates a blob URL for the rewritten source and `import()`s it
4. Revokes only the component blob URL after the module is loaded — the
SDK blob URLs are owned and managed by the host's Jotai cache
This approach eliminates server-side esbuild bundling on every request,
caches SDK modules per application in the frontend, and keeps the
worker's job to a simple string rewrite.
## Summary Diagram
```
app:build (SDK)
└─ twenty-client-sdk stub (metadata=real, core=stub)
│
▼
WorkspaceMigrationRunnerService.run()
└─ SdkClientGenerationService.generateAndStore()
├─ Copy stub package (package.json + dist/)
├─ replaceCoreClient() → regenerate core.mjs/core.cjs
├─ Zip entire package → upload to S3
└─ Set isSdkLayerStale = true
│
┌────────┴────────────────────┐
▼ ▼
Logic Functions Front Components
│ │
├─ Local Driver ├─ GET /rest/sdk-client/:appId/core
│ └─ downloadAndExtract │ → core.mjs from archive
│ → symlink into │
│ node_modules ├─ Host (useApplicationSdkClient)
│ │ ├─ Fetch SDK modules
└─ Lambda Driver │ ├─ Create blob URLs
└─ downloadArchiveBuffer │ └─ Cache in Jotai atom family
→ reprefixZipEntries │
→ publish as Lambda ├─ GET /rest/front-components/:id
layer │ → raw .mjs (no bundling)
│
└─ Worker (browser)
├─ Fetch component .mjs
├─ Rewrite imports → blob URLs
└─ import() rewritten source
```
## Next PR
- Estimate perf improvement by implementing a redis caching for front
component client storage ( we don't even cache front comp initially )
- Implem frontent blob invalidation sse event from server
---------
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
|
||
|
|
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> |
||
|
|
c107d804d2 |
Create command menu items for workflows with manual trigger (#18746)
- Automatically create and sync command menu items for all workflows with a manual trigger - Refactor `useCommandMenuItemsFromBackend` - Prefill a _Quick Lead_ workflow command menu item during workspace setup and dev seeding - Add a ready prop to `HeadlessEngineCommandWrapperEffect` to prevent premature execution when async data hasn't loaded yet --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
9cb21e71fa |
feat: secure and user-scope metadata resolvers for messaging infrastructure (#18787)
## Summary Builds on the messaging infrastructure migration (#18784) by securing and user-scoping all 4 metadata resolvers: ### DTOs secured - **ConnectedAccountDTO**: `@HideField()` on `accessToken`, `refreshToken`, `connectionParameters`, `oidcTokenClaims` - **MessageChannelDTO / CalendarChannelDTO**: `@HideField()` on `syncCursor` - **MessageFolderDTO**: `@HideField()` on `syncCursor`, `externalId` - **UpdateMessageFolderInputUpdates**: stripped to only `isSynced` (removed `name`, `syncCursor`, `pendingSyncAction`) ### Resolvers user-scoped via `@AuthUserWorkspaceId()` - `myConnectedAccounts` — returns only the calling user's accounts (no permission guard) - `myMessageChannels(connectedAccountId?)` — returns channels for the user's connected accounts - `myCalendarChannels(connectedAccountId?)` — same pattern - `myMessageFolders(messageChannelId?)` — returns folders through the ownership chain ### Admin-only listing with permission guard - `connectedAccounts` query retained with `SettingsPermissionGuard(CONNECTED_ACCOUNTS)` for admin listing of all workspace accounts ### Unsafe mutations removed - Removed `createConnectedAccount`, `updateConnectedAccount` (OAuth/IMAP flows create/refresh tokens server-side) - Removed `create*`/`delete*` mutations from MessageChannel, CalendarChannel, MessageFolder (managed by sync engine) ### Update mutations restricted with ownership verification - `deleteConnectedAccount(id)` — verifies `entity.userWorkspaceId === currentUserWorkspaceId` - `updateMessageChannel` / `updateCalendarChannel` / `updateMessageFolder` — verify ownership through connected account chain - New `OWNERSHIP_VIOLATION` exception codes map to `ForbiddenError` in GraphQL ### `@AuthUserWorkspaceId` decorator hardened - Added `allowUndefined` option (default: `false`) — throws `ForbiddenException` if `userWorkspaceId` is undefined (e.g. API key auth) - Existing callers updated to `@AuthUserWorkspaceId({ allowUndefined: true })` where needed - New user-scoped resolvers enforce non-undefined `userWorkspaceId` at decorator level ### Exception handler chaining - `MessageFolderGraphqlApiExceptionInterceptor`, `MessageChannelGraphqlApiExceptionInterceptor`, `CalendarChannelGraphqlApiExceptionInterceptor` chain upstream exception handling (ConnectedAccountException, MessageChannelException) for correct `ForbiddenError` propagation ### Metadata services enhanced - `findByUserWorkspaceId()`, `getUserConnectedAccountIds()`, `findByConnectedAccountIds()`, `findByMessageChannelIds()` - `findBy*ForUser()` methods encapsulate ownership checks before querying - `verifyOwnership()` on all 4 services with proper chain validation - Named parameters throughout for clarity ### Dev seeds for both schemas - Added JANE to connected account, message channel, calendar channel workspace seeds - Created message folder workspace seeds (TIM, JONY, JANE) - New `seed-metadata-entities.util.ts` seeds core schema tables (connectedAccount, messageChannel, calendarChannel, messageFolder) with same IDs as workspace seeds, mapping `accountOwnerId` → `userWorkspaceId` ### Integration tests (using seeds, not raw SQL) - 4 test suites (`connected-account`, `message-channel`, `calendar-channel`, `message-folder`) - Tests use seeded data IDs from seed constants — no raw SQL inserts/deletes - Tests read via GraphQL resolvers - Tests cover: user scoping, admin permission checks, sensitive field exclusion, ownership enforcement on mutations ### Frontend migration - Feature-flag-gated hooks (`useMyConnectedAccounts`, `useMyMessageChannels`, `useMyCalendarChannels`, `useMyMessageFolders`) - When `IS_CONNECTED_ACCOUNT_MIGRATED` is on: hooks use metadata API (`POST /metadata`) - When flag is off: hooks use existing workspace API (`POST /graphql`, current behavior) - Settings account pages updated to use new hooks - `useEffect` extracted to `SettingsAccountsSelectedMessageChannelEffect` component per project conventions - Error messages translated with Lingui ## Test plan - [x] Server typecheck passes - [x] Server lint passes - [x] Server unit tests pass (477 suites, 4269 tests) - [x] Frontend typecheck passes - [x] Frontend lint passes - [x] Integration tests verify user-scoping, ownership enforcement, hidden fields - [ ] CI green --------- Co-authored-by: neo773 <neo773@protonmail.com> |
||
|
|
7c8f060b08 |
Fix orphan navigation menu items for deleted views (#18791)
## Summary Fixes #18757 This fixes a set of Favorites / navigation-menu-item integrity problems related to deleted views, stale hidden items, and upgraded workspaces with orphaned navigation items. ## What changed - delete `navigationMenuItem` entries when their favorited view is deleted - keep the client metadata store in sync immediately when a view is deleted - determine whether a view is already favorited from visible valid navigation items instead of raw stale items - add a `1.20.0` upgrade repair command that deletes orphan navigation menu items and normalizes positions - add regression coverage for deletion of both record-based and view-based navigation menu items ## Details Server: - extend `NavigationMenuItemDeletionService` so cleanup applies to deleted views as well as deleted records - add regression tests covering record-based deletion, view-based deletion, and no-op behavior - add `DeleteOrphanNavigationMenuItemsCommand` to remove orphaned items pointing to: - deleted views - deleted records - missing folders - normalize positions per scope (`userWorkspaceId + folderId`) after repair - wire the new repair command into the `1.20.0` upgrade flow Frontend: - add `useRemoveNavigationMenuItemByViewId` - remove the related navigation item from client metadata immediately when deleting a view - use sorted / visible navigation items for favorite detection so stale hidden rows do not block re-adding a favorite ## Why Issue `#18757` reports mismatches between Favorites shown in the UI and rows users can still find in the database. We found that current Favorites behavior is driven by `navigationMenuItem`, not the legacy `favorite` table, and that stale / orphaned `navigationMenuItem` rows could: - remain after deleting a favorited view - stay hidden from the UI if they point to invalid targets - still cause the UI to think a view was already favorited - persist in workspaces with migration damage from skipped sequential upgrades This patch addresses those cases directly and adds an upgrade-time repair path for older corrupted workspaces. ## Validation Passed: - `./node_modules/.bin/jest --config packages/twenty-server/jest.config.mjs --runInBand packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/services/__tests__/navigation-menu-item-deletion.service.spec.ts` - `./node_modules/.bin/tsc -p packages/twenty-front/tsconfig.json --noEmit --pretty false` Known unrelated existing failure: - `./node_modules/.bin/tsc -p packages/twenty-server/tsconfig.json --noEmit --pretty false` The server typecheck failure is pre-existing and unrelated to this branch. Current errors are around `@file-type/pdf` module resolution and `is-psl-parsed-domain.type.ts`. --------- Co-authored-by: Charles Bochet <charles@twenty.com> Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com> |
||
|
|
2f095c8903 |
Scaffold light twenty app dev container (#18734)
as title |
||
|
|
fe4512f80c |
Add record page layout tabs editing (#18702)
- Fix duplication of tabs in dashboards that didn't open the duplicated tab in the side panel: replaced the logic that used Math.round with an algorithm that switches the positions of the elements. We will keep relying on integers, but it will work well in all cases. - Make dnd work with record page layouts, where there is a pinned tab - Make tab movements work with pinned tab, too - Allow user to set a tab as the pinned tab - Make backend changes to be able to save tabs with `layoutMode` https://github.com/user-attachments/assets/ce1130fa-71df-49ba-ba2f-6f971e15dd49 --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> |
||
|
|
eb51f8e6da |
Remove front component and command menu item old seeds (#18737)
Deprecated seeds |
||
|
|
731e297147 |
Twenty sdk cli oauth (#18638)
<img width="1418" height="804" alt="image" src="https://github.com/user-attachments/assets/de6c8222-6496-4a71-bc21-7e5e1269d5cb" /> --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com> |
||
|
|
46e515436e |
Deprecate legacy RICH_TEXT field metadata type (#18623)
## Summary - Removes the deprecated `RICH_TEXT` (V1) field metadata type from the codebase entirely - Adds a 1.20 upgrade command that migrates existing `RICH_TEXT` fields to `TEXT` in `core.fieldMetadata` - Cleans up ~70 files across `twenty-shared`, `twenty-server`, `twenty-front`, `twenty-sdk`, and `twenty-zapier` ## Context `RICH_TEXT` was a legacy field type that stored rich text as a single `text` column. It was already **read-only** — writes threw errors directing users to `RICH_TEXT_V2` instead. `RICH_TEXT_V2` is the current approach: a composite type with `blocknote` (editor JSON) and `markdown` subfields. Keeping the deprecated type added maintenance burden without any value. Since the underlying database column type for `RICH_TEXT` was already `text` (same as `TEXT`), the migration only needs to update the metadata — no data migration or column changes required. ## Changes ### Upgrade command (new) - `1-20-migrate-rich-text-to-text.command.ts` — runs `UPDATE core."fieldMetadata" SET "type" = 'TEXT' WHERE "type" = 'RICH_TEXT'` per workspace, with cache invalidation ### Enum & shared types - Removed `RICH_TEXT` from `FieldMetadataType` enum - Removed from `FieldMetadataDefaultValueMapping`, `isFieldMetadataTextKind` ### Server (~30 files) - Removed from type mapper (scalar, filter, order-by), data processors, input transformer, filter operators, zod schemas, column type mapping, searchable fields, RLS matching, OpenAPI schema, fake value generators - Removed from field creation flow and field metadata type validator - Updated dev seeder Pet `bio` field to `TEXT` - Cleaned up mocks, snapshots, integration tests ### Frontend (~25 files) - Deleted: `RichTextFieldDisplay`, `isFieldRichText`, `isFieldRichTextValue`, `useRichTextFieldDisplay` - Removed from `FieldDisplay`, `usePersistField`, `isFieldValueEmpty`, `isRecordMatchingFilter`, `generateEmptyFieldValue`, `isFieldCellSupported`, spreadsheet import, workflow fake values - Removed from settings types, field type configs, and field creation exclusion list - Updated tests, mocks, and stories ### SDK & Zapier - Removed from generated GraphQL schema and TypeScript types - Removed from Zapier `computeInputFields` |
||
|
|
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 |
||
|
|
e8f8189167 |
[COMMAND MENU ITEMS] Add engine component key (#18554)
## PR Description In the process of migrating all the existing commands to the backend, we stumbled across a couple of problems that made us reconsider the full migration. This PR introduces a way for command menu items to bypass front components and to directly reference a frontend component from twenty front. It: - Introduces a `engineFrontComponentKey` field on `CommandMenuItem` as an alternative to `frontComponentId` and `workflowVersionId`, allowing command menu items to reference frontend components by key directly rather than requiring a FrontComponent entity - Updates the DB constraint to allow exactly one of `workflowVersionId`, `frontComponentId`, or `engineFrontComponentKey` ### All standard command menu items from the frontend which use `standardFrontComponentKey` These are all commands that execute a GraphQL query or a mutation. Two mains concerned have been raised that made us go with this (temporary) architecture instead: - If those commands are part of the standard application, they can only alter objects from that application and not custom objects. - We would need to implement a way to trigger optimistic rendering from the front components, which might take some time to implement. List: - Create new record - Delete (single record) - Delete records (multiple) - Restore record - Restore records (multiple) - Permanently destroy record - Permanently destroy records (multiple) - Add to favorites - Remove from favorites - Merge records - Duplicate Dashboard - Save Dashboard - Save Page Layout - Activate Workflow - Deactivate Workflow - Discard Draft (workflow) - Test Workflow - Tidy up workflow - Duplicate Workflow - Stop (workflow run) - Use as draft (workflow version) --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
2de022afcf |
Add standard command menu items (#18527)
## Add standard command menu items
### Summary
This PR introduces standard command menu items, migrating hardcoded
command menu actions to the backend command menu item architecture
powered by front components. It adds a new `twenty-standard-application`
package that defines, builds, and registers front components as standard
command menu items, gated behind the `IS_COMMAND_MENU_ITEM_ENABLED`
feature flag.
### Description
- **New `twenty-standard-application` package**: Contains front
component definitions with an esbuild-based build pipeline that
generates minified `.mjs` bundles and a manifest with checksums.
- **Server-side registration**: New constants register all items with
metadata (labels, icons, positions, availability types, conditional
expressions). A `StandardFrontComponentUploadService` uploads built
components to file storage.
- **`FALLBACK` availability type**: New enum value for command menu
items that appear as fallback options (e.g., "Search Records" fallback).
- **`CommandMenuContextApi` refactor**
- **Conditional availability enhancements**: New array-based helper
functions for evaluating multi-record conditions.
- **Frontend wiring** (twenty-front):
`useCommandMenuItemFrontComponentCommands`
## Next steps
Only simple commands have been implemented for now:
- **Navigation (9)** -- `CommandLink`: go-to-companies,
go-to-dashboards, go-to-notes, go-to-opportunities, go-to-people,
go-to-runs, go-to-settings, go-to-tasks, go-to-workflows
- **Side panel (4)** -- `CommandOpenSidePanelPage`: ask-ai,
search-records, search-records-fallback, view-previous-ai-chats
We still have to implement front components for all the following
commands:
All have placeholder `execute` logic (`async () => {}`) with a `// TODO:
implement execute logic` comment:
**Record (22)**
- `add-to-favorites`, `remove-from-favorites`
- `create-new-record`, `create-new-view`
- `delete-single-record`, `delete-multiple-records`
- `destroy-single-record`, `destroy-multiple-records`
- `restore-single-record`, `restore-multiple-records`
- `export-from-record-index`, `export-from-record-show`,
`export-multiple-records`, `export-note-to-pdf`, `export-view`
- `hide-deleted-records`, `see-deleted-records`
- `import-records`, `merge-multiple-records`, `update-multiple-records`
- `navigate-to-next-record`, `navigate-to-previous-record`
**Page layout (3)** -- `cancel-record-page-layout`,
`edit-record-page-layout`, `save-record-page-layout`
**Dashboard (4)** -- `cancel-dashboard-layout`, `duplicate-dashboard`,
`edit-dashboard-layout`, `save-dashboard-layout`
**Workflow (10)** -- `activate-workflow`, `add-node-workflow`,
`deactivate-workflow`, `discard-draft-workflow`, `duplicate-workflow`,
`see-active-version-workflow`, `see-runs-workflow`,
`see-versions-workflow`, `test-workflow`, `tidy-up-workflow`
**Workflow version (4)** -- `see-runs-workflow-version`,
`see-versions-workflow-version`, `see-workflow-workflow-version`,
`use-as-draft-workflow-version`
**Workflow run (3)** -- `see-version-workflow-run`,
`see-workflow-workflow-run`, `stop-workflow-run`
|