2dcf53619f568d397eae272fcd20dcbe2a06bf10
13649 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2dcf53619f |
fix(front): keep kanban header columns full-width when board overflows viewport (#22926)
## Problem Before <img width="1340" height="542" alt="Capture d’écran 2026-07-15 à 18 26 22" src="https://github.com/user-attachments/assets/472dfe13-336a-43c7-8986-6cdcb04e5217" /> After <img width="1340" height="542" alt="Capture d’écran 2026-07-15 à 18 26 12" src="https://github.com/user-attachments/assets/e393a3fa-2245-42cc-b965-f16f3feaeff6" /> The record board (Kanban) header breaks when the screen is narrower than the total column width. The stage header columns squish together to fit the viewport while the cards below keep their fixed 220px width and scroll horizontally, so the header labels no longer line up with their columns. Regression from #22323. ## Cause #22323 wrapped each column header in `DragDropColumnSortableCell`. In `fill` mode its `StyledSortableRoot` uses `min-width: 0` with the default `flex-shrink: 1`, so the header cells collapse below their column width when the board is wider than the viewport, instead of overflowing into horizontal scroll like the body. ## Fix Pin `flex-shrink: 0` in `fill` mode so header cells keep their column width and overflow in step with the body. `fill` is board-only; the record table header uses non-fill mode and is unaffected. ## Testing Opportunities → "By Stage" Kanban, narrow the window below the total column width: header stays aligned with the cards and scrolls horizontally with them. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22926?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
dd9763a7a0 |
Allow workflow run control mutations to be called with API key auth (#22924)
## Context A customer wants to control workflow runs from an external system / their own automation calling the API. Today the workflow mutations are gated by `UserAuthGuard`, which requires an interactive logged-in user (`request.user`). API-key requests set `request.workspace` but never `request.user`, so they can't call them. ## Change `UserAuthGuard` was applied at the class level on `WorkflowTriggerResolver`, blanketing all five mutations even though only `runWorkflowVersion` actually consumes the user (it looks up the workspace member to stamp `createdBy`). This drops `UserAuthGuard` from the class and keeps it only on `runWorkflowVersion`. As a result, these become callable with API-key auth: - `stopWorkflowRun` - `retryWorkflowRun` - `activateWorkflowVersion` - `deactivateWorkflowVersion` None of these ever referenced the user, so no logic depends on it. `runWorkflowVersion` stays user-only because it needs a workspace member to attribute `createdBy`. Permissioning is unchanged: `SettingsPermissionGuard(WORKFLOWS)` stays at the class level and already resolves the permission for API keys via `apiKeyId`, so a key still needs the WORKFLOWS permission. ## Notes / open questions - No actor is recorded for these operations today (they only change run/version state), so exposing them to API keys doesn't drop any audit that existed. Attributing API-key-initiated actions would be a follow-up. - If there's a deliberate product stance that workflow control should stay user-only, this is a policy change worth confirming. |
||
|
|
f4b2968a74 |
fix(server): finalize workflow runs stuck in STOPPING (#22900)
## Context A workflow run only reaches STOPPED via the in-flight worker execution: `stopWorkflowRun` just flips a RUNNING run to `STOPPING` (records intent), and the `STOPPING -> STOPPED` transition is done later inside `computeWorkflowRunStatus`, which only runs while a worker is executing the run's steps. If no worker is executing the run at that point, nothing ever finalizes it: - the worker that owned the run crashed / was killed mid-step (e.g. under heavy load), or - a step legitimately sits in RUNNING awaiting an external event that never arrives (the user stopped it). Only `ENQUEUED` runs had a staleness sweep, so `STOPPING` (and `RUNNING`) had no recovery path and would stay stuck indefinitely. This has been observed in production (~150 runs stuck in `STOPPING` after manual stops during a migration). ## Change Extend the existing staled-runs machinery to also finalize runs left in `STOPPING`: - New `stuck-stopping-runs-threshold` (1h) + `getStuckStoppingRunsFindOptions` matching `status = STOPPING AND updatedAt < now - 1h`. `updatedAt` is a TypeORM update-date column, so it reliably marks when the run entered `STOPPING`, and 1h stays above any legitimate in-flight step. - `handleStuckStoppingRunsForWorkspace` finalizes each match to `STOPPED` via `endWorkflowRun`, so `endedAt`, step infos and the `WorkflowRunStopped` metric stay consistent. It pages the backlog with keyset pagination on `(createdAt, id)`, so a page whose finalizations all fail can't stay at the front of the query and starve later runs (failed ones are retried on the next sweep). - Wired into the same cron (`WorkflowHandleStaledRunsCronJob`, every 10 min), per-workspace job, and the manual `workflow:handle-staled-runs` command — so ops can also clear an existing backlog immediately. The staled-ENQUEUED and stuck-STOPPING handlers run independently (`Promise.allSettled` in the job, separate try/catch in the command), so a failure in one doesn't block the other. Stop remains manual and unchanged; this only guarantees a stopped run eventually reaches `STOPPED`. ## Notes / scope - No schema change (reuses `updatedAt`), so no migration. - `RUNNING` runs orphaned by a worker crash have the same missing-net problem; left out of scope here (this covers the user-triggered STOPPING case). - The new detection query scans `status`/`updatedAt` like the existing ENQUEUED sweep; at very high `workflowRun` volumes an index on `(status, updatedAt)` would help — same pre-existing consideration as the ENQUEUED path. ## Tests Unit tests for `handleStuckStoppingRunsForWorkspace`: no-op when none, finalizes each match to STOPPED, pages through a multi-page backlog, and advances past a fully-failed page instead of starving later runs. Plus a unit test for the `(createdAt, id)` keyset condition in `getStuckStoppingRunsFindOptions`. Full suite green, lint + typecheck clean on changed files. Manually verified on a real instance (Postgres): seeded a `STOPPING` run aged 2h and ran `workflow:handle-staled-runs` -> transitioned to `STOPPED` with `endedAt` set; a freshly-`STOPPING` run (updatedAt now) was correctly left untouched by the 1h threshold. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22900?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
541c67d222 |
i18n - translations (#22923)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
f67eb60c57 |
feat(workflow): soft-ref core workflow/version (backfill + dual-write) (#22821)
Replaces the shared-UUID model (core row reuses the workspace record id) with a **soft-ref**: the workspace `workflow`/`workflowVersion` records carry a nullable `coreWorkflowId`/`coreWorkflowVersionId` pointing to their **own-id** core rows. This removes the assumption that workspace record ids are globally unique - which is false, since prefilled/seeded workflows share ids across workspaces. Supersedes #22776. ## In this PR **Soft-ref columns (foundation):** - **twenty-shared** `STANDARD_OBJECTS`: `workflowVersion.coreWorkflowVersionId` + `workflow.coreWorkflowId` (+ snapshot test). - **compute utils**: both as system, nullable UUID fields. - **entity classes**: the bare fields. **Version soft-ref sync:** - Core `workflowVersion` rows get their own id, derived deterministically from `workspaceId + record id` (uuidv5). Deterministic so the upsert is idempotent: a failed write-back re-derives the same id and self-heals instead of orphaning rows or colliding on the one-active-per-workflow index. - Sync = find-or-create keyed on the workspace record's `coreWorkflowVersionId`, then write the core id back onto the workspace record. - Migrating over pre-soft-ref data: purges any core row whose id equals the workspace record id before recreating, so old shared-UUID rows aren't orphaned. - Version dual-write listener reworked: delete is keyed by the core id read off `before.coreWorkflowVersionId`. Verified on a fresh `database:reset` (columns materialize, backfill produces deterministic own-id rows linked back, idempotent re-run), a simulated old shared-UUID state (stale rows purged, records re-linked), and a simulated write-back failure (retry re-links to the same id, no orphan, active-version index intact). ## Next steps (follow-up work, not in this PR) 1. Workflow-side soft-ref sync mirroring the version side (service, module, dual-write listener, backfill command). 2. Workspace command to add the two columns to existing workspaces. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22821?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
edd35c79d9 |
i18n - docs translations (#22919)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22919?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
8e03921372 |
Add CREATED workspace activation status (read path + enum migration) (#22904)
## Context Since v2 onboarding (#22303), workspaces are activated **before** the billing plan step (now the last onboarding step). Users abandoning at the plan step leave ACTIVE workspaces with a Stripe customer but no subscription (~60–110/day on cloud, 935+ so far), and no cleanup mechanism ever touches them: billing webhooks never fire (no subscription), the suspended-workspaces cron only handles SUSPENDED, the onboarding cron only handles PENDING_CREATION/ONGOING_CREATION. Target lifecycle (across two PRs): `PENDING_CREATION → ONGOING_CREATION → CREATED → ACTIVE → SUSPENDED → deleted`. **`CREATED`** = the workspace schema is provisioned but onboarding is not complete — no billing subscription yet. It is **not** considered active: | Concern | CREATED behavior | |---|---| | Sign-in / invited teammates joining | allowed (invite-team step precedes the plan step) | | Member + metadata loading (app shell) | allowed (user must finish onboarding) | | Permissions | real permission checks (no PENDING-style bypass) | | Version upgrades / workspace migrations | **included** (schema must not drift) | | Messaging/calendar/workflow/etc. crons | **excluded** — no background processing until a plan is chosen | | PLAN_REQUIRED onboarding lock | unchanged (still derived from subscription existence) | ## What this PR does (read path only) The enum addition ships as a **slow** instance command, which can run after deploy — so nothing in this PR ever **writes** `CREATED`. The write path (setting it at activation, the cleanup sweep, the backfill of the existing zombie cohort) is a follow-up PR that ships once this migration has run everywhere. - **twenty-shared**: `CREATED` enum value; `PROVISIONED_WORKSPACE_ACTIVATION_STATUSES` + `isWorkspaceProvisioned` ("schema exists": CREATED | ACTIVE | SUSPENDED), replacing `isWorkspaceActiveOrSuspended` — all call sites (server member loading, access-token workspace-member lookup, front metadata-store gates) meant "has schema/members". - **Slow instance command** (2.22.0): swaps `core.workspace_activationStatus_enum` using the rename→recreate→alter-column idiom. The CHECK constraints on `core.workspace` embed casts to the enum type and would break the swap — the command captures them from `pg_constraint`, drops them, swaps the type, and restores them. - **Pre-migration-safe queries**: Postgres rejects `IN ('CREATED', ...)` when the enum value does not exist yet — even for reads, and the instance-command runner itself queries provisioned workspaces before migrating (a fresh database could never initialize). All provisioned-status filters go through a new `activationStatusIn` util comparing on `"activationStatus"::text`, valid before and after the migration. - **Upgrade path**: workspace iterator, command runner, upgrade-status and workspace-version services iterate CREATED workspaces. Since they now cover more than ACTIVE/SUSPENDED, the stale names were renamed to `ProvisionedWorkspaceCommandRunner`, `hasProvisionedWorkspaces`, `getProvisionedWorkspaceIds`, `loadProvisionedWorkspaces` (the mechanical import rename in old version-command dirs is why this PR carries the `ci:allow-previous-version-upgrade-mutation` label). - **Sign-in**: `throwIfWorkspaceIsNotReadyForSignInUp` accepts CREATED so invited members can join during onboarding (join authorization itself is unchanged — enforced upstream in `checkAccessForSignIn`); `activateWorkspace` idempotent-retry accepts CREATED as a terminal state. - **Transitions out of CREATED** (only write ACTIVE — safe to ship now, dead until the write path lands): the Stripe webhook reactivation branch also promotes CREATED, and `syncSubscriptionToDatabase` promotes synchronously; both gated on `WORKSPACE_ACTIVATING_SUBSCRIPTION_STATUSES` (Active/Trialing — extracted from `shouldReactivateWorkspace`, behavior-preserving) so an `incomplete` subscription created by the payment-intent flow before payment never promotes the workspace. - Deliberately untouched: all background crons, permission guards, JWT strategy, PLAN_REQUIRED logic, admin panel (renders the raw status string). ## Follow-up PR (after this migration has run) 1. `activateWorkspace` sets `hasWorkspaceAnySubscription ? ACTIVE : CREATED` (billing disabled → always ACTIVE, self-hosted unchanged). 2. Cleanup: suspend CREATED workspaces older than N days (config var), handing them to the existing suspended pipeline (warn → soft-delete → destroy). 3. Backfill: cloud-only slow command moving ACTIVE workspaces with no billingSubscription row (created since Jul 1) to CREATED. ## Verification - Migration exercised against a real database via the command class: up → down → up; `enum_range` and `pg_get_constraintdef` checked after each step (constraints restored against the new type, `DEFAULT 'INACTIVE'` preserved). - Pre-migration safety exercised for real: with the migration rolled back (enum without CREATED), `run-instance-commands` — the exact fresh-database CI path that failed before the `::text` fix — completes cleanly. - End-to-end with a workspace manually set to CREATED and the branch server+front running: sign-in issues tokens, `currentUser` loads workspaceMember(s), the full app loads with no console errors; GraphQL returns `activationStatus: CREATED`. - Workspace creation ran end-to-end locally in **both billing modes** on this branch: - billing disabled: signup → workspace creation → ACTIVE immediately → onboarding completes with no plan step → app loads (unchanged behavior); - billing enabled (Stripe test mode): signup creates the Stripe customer eagerly → activation ends ACTIVE → subscription-less workspace is pinned to the plan-required page → no-card trial checkout creates a `trialing` subscription via `createDirectSubscription`/`syncSubscriptionToDatabase` → app loads. - `twenty-shared` unit tests, server specs on touched services, `lint:diff-with-main` and `typecheck` for shared/server/front all green; full CI green. |
||
|
|
cdb2590355 |
Migrate call-recorder tests off own-code vi.mock (#22902)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22902?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
36a14478ae |
i18n - translations (#22916)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
25bd2897a3 |
Add weekly layout to record calendar (#22819)
## Summary - Add a week layout to record calendar views and persist the selected layout. - Render `DATE` calendars as an all-day week and `DATE_TIME` calendars as an hourly week. - Add an optional end date field across calendar configuration, metadata, persistence, and complete-view upserts. - Use configured end values for ranged and multi-day events, with a one-hour fallback when a `DATE_TIME` end is absent or invalid. - Keep calendar cards consistent with the existing compact view, including checkbox selection and whole-card record opening. - Gate the weekly layout and end-date behavior behind the public Labs `IS_CALENDAR_WEEK_VIEW_ENABLED` workspace feature flag. ## Week interactions - Show overlapping timed events side by side and cap the visible records at two per day. - Display start and end times on timed cards, enforce a readable 30-minute minimum height, and keep today’s text contrast stronger. - Drag timed events between days and times with 30-minute snapping while preserving their duration, including zero-duration events. - Show a create button when hovering a 30-minute slot; keyboard users can focus a day, move the slot with the arrow keys, and reach the same contextual action. - Initialize new records with the selected slot time and a compatible writable end value one hour later. - Show the workspace time zone and current-time indicator in timed weeks; date-only weeks keep the all-day section without an hourly grid. ## Configuration and data loading - Only allow end fields that match the start field type, and prevent selecting the same field for both boundaries. - Load records whose ranges overlap the visible period so month and week layouts display the same relevant records. - Resolve and persist calendar end fields when updating existing views through `upsert_complete_view`. - Fall back to Month and ignore the configured end field while the flag is disabled, without overwriting either persisted setting, so re-enabling restores the previous configuration. - Expose the flag in Labs and keep it default-off for workspaces without a stored value; enable it in the development seeder. <img width="1285" height="808" alt="Screenshot 2026-07-15 at 15 50 17" src="https://github.com/user-attachments/assets/b7e3f7f1-ca77-492f-8cce-cca186ebca0b" /> |
||
|
|
0dbae2eda3 |
Address #22827 review comments and converge application file endpoints (#22868)
Follow-up to #22827, addressing the review comments left around merge time and applying the endpoint convergence discussed afterwards. ## Review comments from #22827 - **Swallowed error in dev sync asset read** ([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571513265)): the swallow is intentional (a missing public asset must not fail the whole dev sync) but it now logs a warning with the asset path and error, and the registration keeps its previously stored file for that path instead of losing it. - **`isAbsoluteUrl` location** ([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571524234)): moved to `twenty-shared/utils/url`. The server, and now also `twenty-sdk`'s `normalize-application-assets`, use the shared util. - **Soft delete vs file cleanup** ([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571589558)): per review, deleting a registration is now a hard delete. Stored assets (bytes + rows) are deleted with it, dependent rows are removed by their existing FK cascades, and installed applications keep working with their registration link nulled. No soft-delete/cron mechanism. - **Asset cap too generous** ([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571595745)): lowered to 10MB per review and documented in the publishing and public-assets docs pages. - **One missing image retriggers a full asset sync** ([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571646243)): `storeRegistrationAssets` now takes `skipAlreadyStoredPaths`; the catalog sync passes it when the package version is unchanged, so only assets missing a stored file are fetched instead of re-downloading everything. - **`existing.logo` already contains the new logo** ([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571667847)): correct, `updateFromManifest` runs first, so the previous "keep fileId when the path did not change" guard compared the new logo against itself. The fileId preservation is now keyed on the stored server file for the exact path (files are unique per `(applicationRegistrationId, path)`): a changed logo path no longer inherits the old file's id, and a transient download failure on an unchanged path still keeps the working file. This also removed the fileId-preservation bookkeeping from `storeRegistrationAssets`. ## Endpoint convergence - **Path-addressed public route for registration assets**: `GET /file/server/application-registration/:fileId` is replaced by `GET /files/application-registrations/:registrationId/*path`, mirroring the manifest's public-folder paths and leaving room for a future `:version` segment. Assets stay addressable by stable ids server-side; the fileId now only marks a path as stored. No URL is ever persisted (all are built at query time), and the old route never shipped in a release, so there is nothing to migrate. - **`Application.logoUrl` resolved server-side**: new `ResolveField` on the `Application` type builds the `/public-assets/...` display URL (or passes absolute URLs through). `useApplicationChipData` now reads it from `currentWorkspace.installedApplications`, and the frontend `buildApplicationLogoUrl` util is deleted, so clients no longer construct file URLs themselves. ## Validation - Unit: `file.controller.spec` (route renamed, traversal case added), `server-file-storage.service.spec` (`findServerFile`, `deleteByApplicationRegistrationId`), `application-registration-asset-url.service.spec` (new URL shape, url-encoding), new `isAbsoluteUrl` test; all application/file suites pass. - Live against a local server: new route serves tarball and rehosted npm assets with `public, max-age=3600` (nested paths included), 404s on missing files, unknown registrations, traversal attempts, and the removed old route; `findManyApplicationRegistrations` returns path-addressed URLs for stored assets, CDN fallback for npm, absolute passthrough; `installedApplications.logoUrl` resolves the public-assets URL and stays null for logo-less apps. Registration hard delete verified against the DB: file rows cascade, application rows keep a nulled registration link. - Typecheck + lint on twenty-server, twenty-front, twenty-shared, twenty-sdk; metadata codegen and client-sdk regenerated. |
||
|
|
907c5cc39f |
i18n - translations (#22913)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
f4ff234db8 |
feat: make record avatar/icon resolution data-driven via a configurable image identifier field (#22644)
## Summary Today the avatar/icon shown for a record is hardcoded per object — Company pulls a favicon from its domain link, Person uses `avatarUrl`, etc. This PR replaces that hardcoding with a generic, data-driven abstraction based on a configurable **image identifier field** on each object's metadata (mirroring the existing **label identifier** concept). An object's image identifier can point to: - a **`FILES`** field → the uploaded image is used directly (rounded avatar), or - a **`LINKS`** field → a favicon is derived from the primary URL via the Twenty icons service (squared avatar), gated by `ALLOW_REQUESTS_TO_TWENTY_ICONS`. This lets any object type (Opportunity, a custom "Listing", etc.) define its own avatar/icon without code changes, and makes the field configurable/overridable for standard objects. ## ❓ Open question: also allow `TEXT` → direct image URL? Right now the image identifier is restricted to `FILES` (uploaded file) and `LINKS` (favicon). We deliberately left out `TEXT` → **direct image URL** (e.g. an imported/synced photo URL stored in a text field). There's precedent for it — Person's avatar was originally a `TEXT` `avatarUrl`, and WorkspaceMember still is — and it's unambiguous (a `TEXT` field has no favicon-vs-image ambiguity, and selecting it as the image identifier is itself the declaration of intent). It's a small, clean extension: - add `TEXT` to the allowed image-identifier types, - add an explicit `TEXT → raw URL` case - `getAvatarType`: `TEXT → rounded`. Caveats: it relies on admin assertion that the text values are image URLs (no data-level guarantee), and external image URLs load third-party content in the browser (IP-leak/hotlinking, same as favicons — a proxy/cache would be the more robust long-term answer). ### ✅ Resolution Decision: **we will not support `TEXT` as an image identifier.** Image identifiers stay restricted to `FILES` and `LINKS`, and any other type fails closed (returns no avatar) on both the frontend and backend. Instead, the legacy items that still rely on a `TEXT` avatar — Person's deprecated `avatarUrl` and WorkspaceMember's `avatarUrl` — will be migrated to `FILE` fields in a follow-up PR. Until then, WorkspaceMember remains an exception (its `avatarUrl` still resolves through the existing CorePicture path), and legacy Person `avatarUrl` values that haven't been migrated will show initials placeholders. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22644?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
e31e6c7794 |
i18n - docs translations (#22912)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
a28c3a905a |
Route pre-2.19 upgrade commands through a legacy validate-build path (#22884)
## Problem Since the centralized metadata side-effect engine landed in v2.19, `WorkspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigrationFromRecord` runs `metadataSideEffectEngineService.expandWithSideEffects(...)` before building. As a result every historical upgrade command (`upgrade-version-command/1-21/*` … `2-18/*`), authored before the engine existed, now flows through it. Their operation matrix is no longer applied literally: the engine injects/cascades companions (system fields, `searchVector` field + GIN index, `searchFieldMetadata` rows, unique backing indexes) and can hard-fail on reserved-identifier collisions (`RESERVED_SYSTEM_UNIVERSAL_IDENTIFIER`). Two hazards for already-shipped commands: 1. **Collision → hard failure**: a command declaring a companion the engine now owns collides with the engine's deterministic `universalIdentifier`. 2. **Silent drift**: on object/field create/delete the engine adds/cascades companions the command author never intended, so workspaces upgraded now differ structurally from those upgraded incrementally before 2.19. Suspected real-world impact: a self-hosted user upgrading v2.6.1 → v2.21.0 hit `duplicate key value violates unique constraint "IDX_SEARCH_FIELD_METADATA_OBJECT_FIELD_UNIQUE"` in `upgrade:2-16:backfill-search-field-metadata`, because object-creating commands now cascade and pre-create the deterministic `searchFieldMetadata` rows the standalone backfill then re-inserts. ## Changes - `workspace-migration-validate-build-and-run-service.ts`: extract the shared compute-and-run tail into a private method, and add `validateBuildAndRunLegacyWorkspaceMigration` (marked `@deprecated`) that skips `expandWithSideEffects` and applies the matrix literally. The existing side-effect entry points are unchanged (the live API and application manifests depend on them). - Repoint **all** pre-2.19 upgrade command call sites (1-21 … 2-18, including `2-10 sync-call-recording-standard-objects`) to the legacy method. Only the four `2-20/*` commands (target version ≥ 2.19) remain on the side-effect path. - `2-16 backfill-search-field-metadata`: recompute `flatSearchFieldMetadataMaps` from the database before building the existing-rows dedupe set. The migration runner only invalidates the flat-maps keys a migration touched, so during a cross-version upgrade earlier commands can leave this map stale; a stale map breaks the dedupe and re-inserts rows, tripping `IDX_SEARCH_FIELD_METADATA_OBJECT_FIELD_UNIQUE`. This is the direct fix for the reported failure. - Export `FlatEntityMapsBundle` so the shared tail can be typed. - Document the side-effect vs legacy path and the selection rule in `packages/twenty-server/docs/UPGRADE_COMMANDS.md`. Selection rule: target version **< 2.19** → legacy path; **≥ 2.19** → side-effect path (default). No exceptions. ## Known gap / merge ordering The static twenty-standard definition declares all of `callRecording`'s fields (including the `searchVector` system field) but **not** its `searchVector` GIN index — every other searchable standard object declares its GIN index statically. On the legacy path, workspaces upgrading through `2-10 sync-call-recording-standard-objects` therefore create the `searchVector` column unindexed (`searchFieldMetadata` rows are created later in the same pipeline by the 2-16 backfill). The static GIN index declaration plus a backfill for already-upgraded workspaces land in a follow-up (twentyhq/core-team-issues#2672), which must ship in the same release as this PR. ## Out of scope (separate follow-ups) - `UpgradeMigrationService.getLastAttemptedInstanceCommand()` ordering. - callRecording `searchVector` GIN index static declaration + backfill (twentyhq/core-team-issues#2672, same-release dependency, see above). ## Test plan - `nx typecheck twenty-server` passes. - `nx lint:diff-with-main twenty-server` (oxlint + oxfmt) clean on changed files. - 2-20 command specs (which exercise the unchanged side-effect path) pass. --------- Co-authored-by: twenty <noreply@twenty.com> |
||
|
|
4e83a64f81 |
fix(front): read fresh metadata in SSE update path to avoid false unknown-field warnings (#22897)
## Problem Sentry `warning`: *"SSE update event for person carried fields unknown to this tab's metadata: lastInboundAt, pdlCertifications, pdlBirthYear, …"* ([TWENTY-FRONT issue](https://twenty-v7.sentry.io/issues/7610855477)). The listed fields are all custom fields created by the **People Data Labs app** (`packages/twenty-apps/public/people-data-labs`). The flow that triggers this: 1. The app installs a batch of `person` fields (emits metadata SSE events). 2. Its enrichment logic-function updates a person record with all of those fields (emits a record-update SSE event). Field creation already emits metadata SSE events, and the front applies them **synchronously** to the Jotai metadata store (`MetadataStoreSSEEffect` → `applyChanges` → `store.set`). So the store converges. The bug is that the SSE **record-update** handler doesn't read the converged store. `useTriggerOptimisticEffectFromSseUpdateEvents` reads `objectMetadataItems` from a React/Jotai closure captured at render time. The long-lived SSE subscription in `useTriggerEventStreamCreation` holds a metadata snapshot that lags the store, so even after the field-create events have been applied, the record path still sees the old field set. It then: - flags the new fields as "unknown" and logs to Sentry, and - **drops those field values** from the optimistic update (`getUnknownRecordInputFields` filters them out), so already-loaded views miss the enriched data until a refetch. Metadata events are dispatched before record events within each SSE message (`useTriggerEventStreamCreation` lines 126-128), and the store update is synchronous, so a fresh store read at processing time sees fields that converged in the same or any earlier message. ## Fix Read `objectMetadataItems` fresh from the Jotai store at event-processing time instead of from the render closure, and re-resolve the object metadata item from that fresh list. This eliminates the false-positive warnings and stops dropping legitimately-known field values. Follows the design from #22474: the metadata-event pipeline owns schema convergence; the record pipeline just reads the converged store (now actually reading the current store rather than a stale snapshot). A genuine race where the record update truly precedes the field-create event is still tolerated and still warns. ## Testing - `nx typecheck twenty-front` passes - `nx lint:diff-with-main twenty-front` passes <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22897?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
055e8b5335 |
Add tab param to open a record side panel page on a specific tab (#22905)
## Context
`CommandOpenSidePanelPage` (and `openSidePanelPage` in the front
component SDK) could open a record in the side panel, but always landed
on the default tab. This adds an optional `tab` param to the
`ViewRecord` page params so an app command can open a record directly on
a specific tab.
## What changed
- **twenty-sdk**: `OpenSidePanelPageParams` `ViewRecord` variant accepts
an optional `tab` (a page layout tab id). Since
`CommandOpenSidePanelPage` props are `OpenSidePanelPageParams`, the
component picks it up automatically.
- **twenty-front**:
- New `setRecordPageActiveTabId` util resolves the record page layout
for the object (custom layout from the store, or the default layout id)
and presets `activeTabIdComponentState` on the tab list instance
(`${pageLayoutId}-tab-list-${recordId}`), which is shared by the side
panel and the full record page.
- `useOpenRecordInSidePanel` accepts `tab` and presets the active tab
before navigating; it also applies when the record is already open in
the side panel (tab switch only).
- `useFrontComponentExecutionContext` forwards `tab` to the side panel
open, and presets the tab when falling back to full-page navigation
(mobile, or objects that can't open in the side panel).
- **Docs**: mention the optional `tab` id in the
`CommandOpenSidePanelPage` description.
Unknown tab ids are harmless: `PageLayoutTabListEffect` falls back to
the layout's default tab when the preset id doesn't exist in the layout.
Dashboards are skipped since their layout id comes from record data, not
object metadata.
## Tests
- `useOpenRecordInSidePanel`: new test asserting the active tab atom is
preset on the correct tab list instance id.
- `useFrontComponentExecutionContext`: new tests for tab passthrough to
the side panel and tab preset on full-page fallback.
- `npx nx typecheck twenty-front`, `typecheck twenty-sdk`,
`lint:diff-with-main twenty-front`, `lint twenty-sdk` all green.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01VaWY1H9RZqgJkytqZakvZi)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22905?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
|
||
|
|
14dacd8d35 |
[Slow db query] Resolve applicationId from cache in FileStorageService (#22870)
## Context Sentry flagged a recurring slow DB query (TWENTY-SERVER-HZJ): `SELECT ... FROM core.application WHERE workspaceId = $1 AND universalIdentifier = $2 AND deletedAt IS NULL LIMIT 1`, emitted on every file write under `POST /graphql` (record avatar/file fields) and `POST /metadata`. `FileStorageService` re-resolved the owning application row from `core.application` by `(workspaceId, universalIdentifier)` on every file write, uncached and synchronously in the request path. The row was only used to recover `application.id`. The workspace cache already exposes this mapping via `flatApplicationMaps.idByUniversalIdentifier`. Closes twentyhq/core-team-issues#2668. ## Changes - Injected `WorkspaceCacheService` into `FileStorageService` in place of the `ApplicationEntity` repository. - Added `resolveApplicationIdOrThrow`: resolves `applicationId` from `flatApplicationMaps.idByUniversalIdentifier` on the normal (already-committed) path, throwing `FileStorageException(FILE_NOT_FOUND)` on a cache miss. When a `queryRunner` is provided (application-creating transactions, where the freshly created row is not yet in cache), it keeps the DB read through `queryRunner.manager` so it can see uncommitted rows. - Added `resolveApplicationUniversalIdentifierOrThrow` for the by-id lookup in `deleteByFileId`, resolved from `flatApplicationMaps.byId`. - Applied the cache path to `writeFile`, `createPendingFile`, `deleteFile`, `deleteFolder`, and `deleteByFileId`. Only `writeFile` carries a `queryRunner`; the others never do. - Updated `FileStorageModule` to import `WorkspaceCacheModule` and drop the now-unused `ApplicationEntity` repository registration. No migration needed: a partial unique composite index on `(universalIdentifier, workspaceId) WHERE deletedAt IS NULL AND universalIdentifier IS NOT NULL` already exists on `ApplicationEntity` and covers the query. ## Tests Extended `file-storage.service.spec.ts`: - cache hit resolves `applicationId` without a DB call, - cache miss throws `FILE_NOT_FOUND`, - the `queryRunner` path still reads from the DB and skips the cache. All 95 file-storage unit tests pass; typecheck, oxlint, and oxfmt are clean on the touched files. --- _Generated by [Claude Code](https://claude.ai/code/session_018GUrJ26xvZpjtrGGev9jsk)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22870?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
2201917f33 |
Harden call recorder Recall API boundary (#22832)
Part 2/5 of splitting #22739. Stacked on #22831. - `RecallBotSnapshot`: Recall bot payloads are parsed once at the API boundary (`parseRecallBotSnapshot`); `getRecallBot`/`listScheduledRecallBots` return typed snapshots, flows never touch raw provider records - Retry policy: honors `Retry-After` (seconds or HTTP-date, capped at 60s), treats 409 and 507 (ad-hoc pool exhausted) as retryable with tailored delays, adds equal jitter to the linear backoff, and returns instead of sleeping past 10s in-process so invocations never sleep into their timeout - `listScheduledRecallBots` accepts a server-side `metadata__` filter and reports `truncated` instead of failing beyond 10 pages - Extracts `cancelOrEjectRecallBot` into the recall-api layer - Replaces the `CALL_RECORDER_MAX_MEDIA_FILE_SIZE_MB` server variable with a fixed 500 MB constant: uploads stream since #22652, so the cap no longer guards function memory and does not need to be operator-tunable Next: billing charge verification, divergence-scoped sync crons, webhook artifact continuation. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22832?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
58fcb3cb0f |
drop nestjs-query IDField from standalone DTOs/entities (#22881)
## What Replaces `@IDField(() => UUIDScalarType)` from `@ptc-org/nestjs-query-graphql` with the native `@Field(() => UUIDScalarType)` (`@nestjs/graphql`) across 50 DTOs and entities that are **not** wired to a nestjs-query auto-resolver. This continues the incremental migration off `@ptc-org/nestjs-query`. ## Why These 50 types only used `IDField` to type their `id` column as a UUID scalar. Since none of them are attached to a `NestjsQueryGraphQLModule.forFeature` resolver, `IDField` carries no extra behavior here — it's a plain field decorator. Co-authored-by: Abdul Rahman <abdulrahmancodes@users.noreply.github.com> |
||
|
|
75d9e0b93a |
fix(front): constrain 2FA sign-in screens and dedupe their shared shell (#22886)
## Problem On the card-less onboarding sign-in (`/welcome`), the **2FA verification** step renders with a full-viewport-wide submit button. The 2FA **verify** and **provision** forms hard-code `width: 100%` on their root `StyledForm`. That was harmless while `/welcome` rendered inside the `AuthModal` `medium` card, which bounded the width. Since #22398 removed v1 onboarding, `/welcome` renders card-less under `BlankLayout`, so nothing bounds those forms and they stretch to the full viewport. Other steps are **not** affected, which is why only 2FA looks wrong: - Sign-in form: root sets `width: ONBOARDING_CONTENT_BLOCK_WIDTH; max-width: 100%` -> capped at 440. - SSO selection / workspace-scope: base container (`min-width: 240`, no width) -> shrink-to-fit. - **2FA verify / provision: `width: 100%` -> full viewport.** ## Fix Cap the two 2FA forms at `ONBOARDING_CONTENT_BLOCK_WIDTH` (with `max-width: 100%`), so they sit in the same block as the sign-in page instead of forcing full-width. ## Refactor (same PR) The verify and provision components (both introduced together in #13141) duplicated their layout shell. Extracted the shared instruction-text and main-content blocks into `SignInUpTwoFactorAuthenticationStyles.ts`. The form container stays local to each component since the element differs (a `div` in provision, a `form` in verification). ## Verification - Reproduced the flex box-model at 1280px: `width:100%` root -> 1196px full-width button; `width: 440px` -> 440px centered button. - lint + format + typecheck pass on the changed files. |
||
|
|
f13bde1e03 |
Align call recorder vocabulary with core dialect (#22831)
Part 1/5 of splitting #22739 into a reviewable stack. Mechanical renames only, no behavior change: - `ingestion` -> `import` across data/domain/flows (`completeCallRecordingIngestion` -> `completeCallRecordingImport`, `ingestCallRecordingMedia` -> `importCallRecordingMedia`, `reconcileCallRecordingTranscriptArtifact` -> `importCallRecordingTranscript`, ...) - `reapOrphanedCallRecorders` -> `cleanupOrphanedRecallBots` - `ensureCallRecorder` -> `scheduleRecallBotForCallRecording`, `healCallRecordingsMissingBot` -> `scheduleRecallBotsForPendingCallRecordings` - `extractRecallBotConvergence` -> `extractRecallBotSyncState` - formatting drift in touched files Next in the stack: Recall API hardening, billing charge verification, divergence-scoped sync crons, webhook artifact continuation. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22831?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
c8c587ba2c |
i18n - docs translations (#22865)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22865?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
bc1ccf526f |
chore(twenty-partners): upgrade to twenty-sdk 2.21 (#22869)
## What Upgrades the `twenty-partners` internal app to consume twenty-sdk 2.21. - Bump `twenty-sdk` and `twenty-client-sdk` from `2.19.0-alpha.1` to `2.21.0` (`package.json` + `yarn.lock`). - Replace the removed `generateDefaultFieldUniversalIdentifier` helper with `getFieldUniversalIdentifier` in `partner-applications.view.ts`, renaming the `fieldName` argument to `name`. ## Why `generateDefaultFieldUniversalIdentifier` was removed from the SDK and replaced by `getFieldUniversalIdentifier`. Both compute the same deterministic uuid-v5 (`fieldMetadata:objectUID:name` under the app universal identifier), so the resolved `createdAt` field identifier is unchanged; this is the only code change required to build against 2.21. ## Verified - Install resolves to 2.21.0; runtime check confirms `getFieldUniversalIdentifier` is exported and `generateDefaultFieldUniversalIdentifier` is gone. - Type check: no real errors. - `oxlint`: 0 warnings, 0 errors. --- _Generated by [Claude Code](https://claude.ai/code/session_01RUQ2yyfZcyKWqG57HbUYxx)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22869?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
9f75506896 |
feat(workflow-tools): add get_logic_function_source tool (#22835)
## What / why
The workflow agent tools let an AI **write** a CODE step's logic
function (`update_logic_function_source`) and **list** logic functions
(`list_logic_function_tools`), but there is no tool to **read** an
existing function's source.
That's fine for greenfield generation — the agent already has in context
whatever code it just wrote. But it's a real gap when editing a function
the agent did **not** author: to safely modify an existing CODE step it
has to see the current source first, and today the only ways to get it
are the frontend (`getLogicFunctionSourceCode` query) or the DB. So the
agent is forced to either guess or ask a human to paste the code.
This adds a small read tool that closes the loop, mirroring the existing
`update_logic_function_source` tool.
## How
- New `get_logic_function_source` tool that calls the existing
`LogicFunctionFromSourceService.getSourceCode({ id, workspaceId })` —
the same service method backing the `getLogicFunctionSourceCode` GraphQL
resolver the frontend already uses. No new service logic.
- Registered in `workflow-tool.workspace-service.ts` alongside
`update_logic_function_source` (the dependency
`logicFunctionFromSourceService` is already injected).
- Unit test covering the success and error paths, matching the `get-*`
tool test convention.
## Notes
- Read-only, additive; no schema or API changes.
- Naturally pairs with `update_logic_function_source`: read → edit →
write.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22835?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
|
||
|
|
fd7935f343 |
fix(front): allow dismissing [Credits limit reached] banner (#22843)
Dismiss is session-only, he banner reappears on reload, because the underlying "out of credits" condition is still true closes #22774 fix: <img width="1222" height="147" alt="image" src="https://github.com/user-attachments/assets/082e68c1-d121-4e31-b261-386de8231896" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22843?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
5426536004 |
Fix(Twenty-front): Junction field unable to display as table (#22844)
Fixes: #22783 The junction field can now be displayed as a table. <img width="1911" height="961" alt="image" src="https://github.com/user-attachments/assets/2aad15af-baca-4563-85eb-ef0826008a7d" /> |
||
|
|
94192a2164 |
Resolve application registration logo and gallery image urls at query time (#22827)
## Context
Application registration logo and gallery image urls were baked into the
stored manifest and display columns at write time, with each source flow
doing it differently: npm catalog sync baked CDN urls, local dev sync
baked `public-assets` urls, and tarball uploads left raw manifest paths
that never displayed in the UI. The entity also carried a `logoUrl`
getter computed field.
This moves url generation to query time, the same way the workspace logo
works.
## What changed
**Read side**
- `ApplicationRegistrationAssetUrlService` builds display urls when
queried: stored files are served by fileId, absolute urls pass through
untouched, and not-yet-rehosted npm assets fall back to the registry CDN
from `sourcePackage@latestAvailableVersion`.
- The `logoUrl` getter on `ApplicationRegistrationEntity` is replaced by
`logoUrl` and `galleryImages` `@ResolveField`s on the metadata resolver,
the admin panel resolver, and a new resolver for
`ApplicationRegistrationSummary` (used by
`Application.applicationRegistration`).
- The marketplace detail/card DTOs and the public OAuth authorize DTO
(`findApplicationRegistrationByClientId`) go through the same url
builder.
- New public route `GET /file/application-registration/:id` streams
registration server files (these are instance-global marketplace assets,
also shown on the public OAuth authorize page).
`ServerFileStorageService.readServerFileById` now returns the mime type
alongside the stream.
**Write side**
- New `logoFileId` column on `applicationRegistration` (2.21 fast
instance command, constraint names match TypeORM naming), complementing
the fileIds already stored in the `galleryImages` jsonb.
- `ApplicationRegistrationAssetService` copies the manifest logo and
gallery images into instance-global server file storage, so all three
sources behave the same:
- **TARBALL**: from the uploaded package (previously only gallery images
were stored, never the logo).
- **LOCAL**: dev sync reads the already-uploaded public assets from
workspace storage (the CLI uploads files before syncing).
- **NPM**: catalog sync downloads the assets from the registry CDN.
Downloads are skipped when the package version is unchanged and the
files are already stored; failed or pending downloads fall back to CDN
urls at query time.
- Write-time url rewriting is removed
(`ManifestAssetUrlResolverService`, `resolveManifestAssetUrls`);
manifests now keep raw asset paths. Existing rows with baked absolute
urls keep working through the absolute-url passthrough, so no backfill
is needed.
- `updateFromManifest` and `upsertFromCatalog` preserve stored gallery
fileIds for unchanged paths, so installs and the hourly catalog sync no
longer clobber them.
## How it was verified
Against a local Postgres/Redis with the server running:
- Fresh database init runs the new instance command; column and FK/UQ
constraint names match TypeORM's generated names, and the CI
pending-migration check produces no diff.
- `findManyApplicationRegistrations { logoUrl galleryImages }` returns
fileId-served urls for a TARBALL registration (absolute urls passed
through), and null/[] for a LOCAL registration without assets.
- Ran `marketplace:catalog-sync` against the real npm registry: 14
packages synced, logos and gallery images rehosted from unpkg with
fileIds set; a second run re-downloaded nothing (version-unchanged
skip); `findMarketplaceAppDetail` for `twenty-linear` returns
fileId-served urls for the logo and all four gallery images.
- `GET /file/application-registration/:id` serves stored files with the
right content type (png and svg verified), 404s on unknown ids, and the
token-guarded generic `/file/:folder/:id` route still returns 403
without a token.
- Unit tests for the url builder and the assets-stored check; server
unit test suites for the application module pass; typecheck and lint
clean.
|
||
|
|
2b0b62235e |
fix: validation link for access-domains deeplinks to Invite tab (#22845)
The "validate domain" link sent in the email when adding an Access Domain wasn't working because the link didn't deep link to the Invite tab URLs are now built to include the that hash property to deep link to the target tab. Before: ``` https://example.com/settings/members?wtdId=<id>&validationToken=<token> ``` After: ``` https://example.com/settings/members?wtdId=<id>&validationToken=<token>#invite ``` <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22845?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
ca437d374f |
chore: bump version to 2.22.0 (#22867)
## Summary - Moves current version to previous versions array - Sets TWENTY_CURRENT_VERSION to the new version - Updates TWENTY_NEXT_VERSIONS with the next minor version - Bumps twenty-client-sdk, twenty-sdk, and create-twenty-app to the same version ## Checklist - [ ] Verify version constants are correct - [ ] Verify npm package versions match <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22867?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com> |
||
|
|
de75be16e2 |
harden(server): backfill and enforce workspace.databaseSchema invariant with a check constraint (#22855)
## What & why `core.workspace.databaseSchema` is meant to be set for every workspace past the creation phase. It only started being written at creation time in 2.x (dual-write since 2026-03-28, direct write since 2026-04-10); older workspaces relied on the `1-21 backfill-datasource-to-workspace` instance command, which never effectively ran on some instances. On affected rows the column could be left `NULL`. A null value on a post-creation workspace is a real integrity problem — several paths trust the column: - **REST API**: `hydrateRestRequest` throws `No data sources found` for authenticated requests. - **GraphQL API**: `getOrComputeSchemaSDL` returns `null`, so `WorkspaceSchemaFactory` hands back an empty schema. - **GraphQL introspection** (direct execution) returns `null`. This PR makes the invariant impossible to silently violate, and repairs any instance still lagging. ### On the original "No data source, skipping" logs This investigation started from `BackfillActorSourceEnumValuesCommand` logging `No data source for workspace <id>, skipping` at high volume. **That symptom is not explained by this change, and this PR is not a fix for it.** Findings: - The workspace iterator only processes `ACTIVE` + `SUSPENDED` workspaces, and on the affected instance all of those already have `databaseSchema` set (only `PENDING_CREATION` rows are null, and those are never iterated). - `getGlobalWorkspaceDataSource()` never resolves to `undefined` (it returns a value or throws), so a defined-schema workspace should never hit the skip branch. - The upgrade-aware repository proxy was investigated as a possible cause (it can short-circuit `findOne` to `null` for entities marked unavailable during an upgrade) and **exonerated**: `WorkspaceEntity` and its `databaseSchema` column carry no `@WasIntroducedInUpgrade`/`@WasRemovedInUpgrade` decorators, so `resolveEntityShapeAtUpgradeCursor` always reports the entity available and the column visible at every cursor. In other words, current code should emit zero such skips for that instance's data, so the root cause of the observed logs remains undetermined and is tracked separately. See twentyhq/core-team-issues#2666. ## Changes - **Check constraint `workspace_requires_database_schema`** (the core of this PR): enforces `databaseSchema IS NOT NULL` for any workspace past creation (`activationStatus NOT IN ('PENDING_CREATION', 'ONGOING_CREATION')`). Declared on `WorkspaceEntity` and applied in the slow instance command's `up()`. Safe against the creation flow: `databaseSchema` is written in `WorkspaceManagerService.init` (right after schema creation) long before a workspace becomes `ACTIVE`. - **Defensive backfill** (`2-21` slow instance command): repopulates `databaseSchema` where it is `NULL`/empty, deriving the schema name deterministically from the workspace id (`getWorkspaceSchemaName`) and only setting it for workspaces whose schema actually exists in `information_schema.schemata` (so `PENDING_CREATION` rows without a provisioned schema are left untouched, and stay exempt via the constraint). No-op on instances already backfilled. - `runDataMigration` runs before `up()`, so the backfill repairs legacy rows before the constraint is enforced. Keeping both in the same slow command (rather than a standalone fast command) guarantees the constraint is never added ahead of the repair. - `checkSchemaExists` gets an explicit `: Promise<boolean>` return type. ## Notes - Backfill + constraint live in a **slow** instance command, so they only apply on upgrades run with `--include-slow`. - The constraint is added **`NOT VALID`**: the backfill repairs every workspace whose Postgres schema exists, but some legacy active/suspended workspaces (e.g. carried over from very old versions, as reproduced by the cross-version upgrade from v1.22) have a null `databaseSchema` with no schema to point at and are unrepairable. `NOT VALID` enforces the invariant on all future inserts/updates without failing the upgrade on that pre-existing corruption. - No production request path was changed — the iterator and `checkSchemaExists` keep trusting the (now backfilled + constrained) column. ## Test plan - [ ] Run `database:migrate:prod --include-slow` on an instance with null `databaseSchema` rows; verify rows whose schema exists get backfilled and `PENDING_CREATION` rows are left null. - [ ] Verify the `workspace_requires_database_schema` constraint exists on `core.workspace` and rejects nulling `databaseSchema` on an active workspace. - [ ] Verify a fresh workspace creation still succeeds (constraint does not fight the `PENDING_CREATION` → `ACTIVE` transition). |
||
|
|
8e022d3c49 |
Fix stale relation table in field widget when switching records in side panel (#22829)
The relation table rendered by a FIELD widget in TABLE display mode kept its jotai component states (loaded rows, virtualization maps, loading guards, query identifiers) in instances keyed only by widget id and view id. Since the side panel record pages share those instances across records, switching to another record kept rendering the previous record's related rows until an asynchronous catch-up reload landed, and any race or error in that catch-up left the previous record's data on screen permanently. Scope the record-table widget's context store instance and record index instance by target record id (and side panel surface), the same way FieldsWidget already scopes its field list instances. Each record now gets its own table state, so a record's rows can never appear under another record, and loads that land after a record switch write into their own instance instead of the visible one. loadRecordIndexStates and setRecordGroupsFromViewGroups accept an optional recordIndexId override so the widget view load effect can populate the record-scoped instance instead of deriving the shared one from object name and view id. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22829?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
b2a4bb0e0c |
docs(apps): add Targeting System Fields page (#22856)
## What
Adds a docs page teaching app developers how to reference auto-created
**system fields** (`createdAt`, `updatedAt`, `id`, …) from views and
other entities, and makes the API it documents real by exporting
`generateDefaultFieldUniversalIdentifier` from the SDK.
## Why
System fields are provisioned by the server, so they're never declared
with `defineField()` and have no importable `universalIdentifier`
constant. Since 2.19 their universal identifier is derived
deterministically from the application id, the object id and the field
name. Hardcoding an invented id fails sync with `INVALID_VIEW_DATA:
Field metadata not found` (this is exactly what broke the
twenty-partners `createdAt` view column).
The twenty-partners app already imports
`generateDefaultFieldUniversalIdentifier` from `twenty-sdk/define`, but
the function was never exported from the SDK. This PR adds the export
and documents the pattern.
## Changes
- **New page** `data/system-fields.mdx` — "Targeting System Fields":
- Lists the 8 system fields (`id`, `createdAt`, `updatedAt`,
`deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`).
- Explains the deterministic derivation and the sync error from
hardcoding ids.
- Documents `generateDefaultFieldUniversalIdentifier({
applicationUniversalIdentifier, objectUniversalIdentifier, fieldName })`
with a full `defineView` example.
- Contrasts with standard objects (use
`STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.<object>.fields.<field>.universalIdentifier`)
and notes that `name` is a default, not system, field.
- **SDK export** — new `generate-default-field-universal-identifier.ts`
wrapping the existing `getFieldUniversalIdentifier` from
`twenty-shared/application` (`name` → `fieldName`), exported from
`define/index.ts`.
- Registered the page in `docs.json` (Data group) and cross-linked it
from the Views doc.
## Notes
`node_modules` isn't installed in this environment, so `nx typecheck`
wasn't run. The wrapper is a signature-matched pass-through and the
`twenty-shared/application` subpath + `getFieldUniversalIdentifier`
barrel export were both verified to exist.
---
_Generated by [Claude
Code](https://claude.ai/code/session_017B7VivHcqZYjn3ukestY9U)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22856?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
---------
Co-authored-by: github-actions <github-actions@twenty.com>
|
||
|
|
6201d06141 |
Preload Stripe.js before the onboarding payment step (#22858)
## Context On the plan-required onboarding step, the card form was slow to appear because Stripe.js is loaded lazily (`@stripe/stripe-js/pure`): the script download only started once the payment page rendered, and the PaymentElement iframe could only boot after that. ## What this does - Adds `usePreloadStripeForPlanRequiredStep`, called once from `OnboardingStepLayout` (the shared layout for the authenticated onboarding step routes), so Stripe.js is already loaded by the time the user reaches the payment step. The hook only triggers when billing is enabled, the workspace has no subscription yet, and a publishable key is configured, so self-hosted instances still never contact Stripe. - Moves the memoized loader to `settings/billing/utils/getStripePromise.ts`, shared by `useStripePromise` and the preload hook. - Stops caching failed script loads: previously a rejected `loadStripe` promise stayed in the cache forever, which would have made a failed preload permanently break the payment form. Now a later call retries (stripe-js re-injects the script tag on retry). - Extracts the plan-required predicate into `onboarding/utils/getIsPlanRequired.ts`, now shared with `useSetNextOnboardingStatus`. The in-app add-credit-card modal is intentionally left untouched: it has no preceding step to preload from. ## Tests - `getStripePromise.test.ts`: dedup per publishable key, retry after a failed load. - `usePreloadStripeForPlanRequiredStep.test.ts`: preloads when billing is enabled and no subscription exists; skips when billing is disabled, a subscription exists, or the key is missing. --- _Generated by [Claude Code](https://claude.ai/code/session_01NtUN99tHPZ6bPWwpYKMbpE)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22858?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
9f5d17d1f0 |
Add receipt metrics and logs to connected account sync webhooks (#22853)
Webhook deliveries from Google and Microsoft were invisible at the app level: successful notifications produced no logs and no metrics, so webhook-triggered syncs could not be told apart from cron polling. Add two counters, connected-account-sync-webhook/received/messaging and /received/calendar, mirroring the sync-job metric umbrellas, and log a line whenever a notification triggers a sync. Unmatched subscriptions keep their existing warn logs. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22853?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
1b168ac1f7 |
fix(server): gate workspaceDiscoverability behind upgrade decorator (#22818)
## Context Needs to be patched on 2.20, will craft a 2.19 equivalent with fallback asap ( be it won't be merged unlike this one ) Fixes #22662. Follow-up to #22423, which introduced `workspaceDiscoverability`. A clean 2.18.x to 2.19 upgrade breaks on login with: ``` QueryFailedError: column workspaceDiscoverability does not exist ``` `workspaceDiscoverability` was added to `WorkspaceEntity` (in #22423) as a plain, always-selected, non-nullable column, so any workspace query (including the auth-path `findAvailableWorkspacesByEmail` lookup) fails as soon as the ORM selects it, before the `2.19` upgrade command that creates the column has run. Because the Docker entrypoint is fail-open, the API starts even if the upgrade is delayed, and users hit this on their first login. ## Changes - Add `@WasIntroducedInUpgrade` to `workspaceDiscoverability`, referencing the existing `2.19.0` fast instance command that creates the column. The upgrade-aware ORM then skips the column until the command has actually added it, keeping login working during the upgrade. - Keep the GraphQL `@Field` non-nullable and add a `workspaceDiscoverability` `@ResolveField` that falls back to `WorkspaceDiscoverability.PUBLIC` while the column is hidden, so the resolver never returns `null` for the non-nullable field during the upgrade window. This mirrors the existing pattern already applied to `FileEntity.status` and `FileEntity.applicationRegistrationId`, and the resolver-default pattern already used for `fastModel` / `smartModel` / `logo`. ## Cherry-pick This fix needs to be cherry-picked onto both the **2.19** and **2.20** release branches, since affected instances are upgrading into those versions. ## Test - `validate-upgrade-aware-entity-decorators` and `resolve-entity-shape-at-upgrade-cursor` unit tests pass (the referenced upgrade command name resolves correctly). - `upgrade-aware-repository.proxy` and `upgrade-aware-entity-metadata.adapter` specs pass. - `typecheck` and lint pass for `twenty-server` and `twenty-front`. - Regenerating the GraphQL schemas produces no diff (the field stays non-nullable). |
||
|
|
8e66411203 |
i18n - translations (#22861)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22861?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
7381038452 |
Paginate admin panel app registrations list (#22734)
## Context The `findAllApplicationRegistrations` query on the admin panel Apps page (`/settings/admin-panel#apps`) loaded every application registration at once, with search and filtering done client-side. ## Changes **Server** - `findAllApplicationRegistrations` now takes `limit` / `offset` / `searchTerm` / `isPreInstalledOnly` args and returns a `PaginatedApplicationRegistrations` object (`registrations`, `totalCount`, `hasMore`), following the same pattern as `getQueueJobs`. - `ApplicationRegistrationService.findAll` uses `findAndCount` with `take`/`skip`, and moves the search (name, source package, universal identifier via `ILIKE`) and the pre-installed filter into the SQL query, mirroring how `getInstalledWorkspacesGlobal` filters installed workspaces. **Frontend** - `SettingsAdminApps` passes the page, the debounced search term (300ms, like the installed workspaces table), and the pre-installed toggle as query variables instead of filtering client-side. - Adds a Previous / Next pagination footer (25 per page) matching the queue jobs table, shown only when there is more than one page. - The "unconfigured first" ordering is kept within each page (`isConfigured` is a dataloader-resolved field, so it can't be sorted in SQL). ## Notes - Regenerated `generated-admin/graphql.ts` follows in a subsequent commit. --- _Generated by [Claude Code](https://claude.ai/code/session_015erumgPozkbNA3zPeKrrFW)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22734?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Weiko <corentin@twenty.com> |
||
|
|
b06f3c7f8f |
fix(website): query isVetted so the apps marketplace renders again (#22859)
## What Production `/apps` renders no apps. The marketplace queries still request `isFeatured`, but #22674 renamed that flag to `isVetted` across the server schema (2-20 instance command renames the DB column — same flag, same data, trust-signal semantics). Production (2.21) rejects the query: ``` Cannot query field "isFeatured" on type "MarketplaceApp" ``` The transport throws on GraphQL errors, `fetchMarketplaceApps` catches and falls back to `[]`, so the page renders the empty state. `/apps/[slug]` detail pages degrade the same way. ## Fix Rename `isFeatured` -> `isVetted` across the website marketplace module: both queries, the API response types, the `MarketplaceApp` domain type, and the vetted-first sort. 4 files, no behavior change beyond restoring the data (same column, same values). The catch-all `[]` fallback is intentionally left in place. ## Verification - Corrected query run against `https://api.twenty.com/metadata`: returns the 3 live apps (People Data Labs, Last contact, Call Recorder), `isVetted: true`. - Rename provenance confirmed: #22674 is a pure rename (paired diff, symmetric column rename, `previousName: 'isFeatured'` marker on the entity). - `nx typecheck twenty-website` + `nx lint twenty-website` green. Takes effect on the next website deploy (`force-dynamic` route, 300s revalidate). |
||
|
|
a6af730353 |
chore: upgrade call-recorder, last-contact, people-data-labs to twenty-sdk 2.20 (#22852)
## What Upgrades three public apps to `twenty-sdk` 2.20. For each app, bumped `twenty-sdk` and `twenty-client-sdk` to `2.20.0`, raised the `engines.twenty` floor to `>=2.20.0`, and regenerated `yarn.lock`: - **call-recorder**: `2.19.0` -> `2.20.0` - **last-contact** (`@twentyhq/last-contact`): `2.19.0-alpha.1` -> `2.20.0` - **people-data-labs**: `2.19.0-alpha.1` -> `2.20.0` ## Verification - Lockfile diffs are version/checksum-only; the SDK's transitive dependency set is unchanged between 2.19 and 2.20, so no new packages were introduced. - `yarn typecheck` passes cleanly for all three apps against 2.20, confirming no breaking API changes to adapt to. --- _Generated by [Claude Code](https://claude.ai/code/session_01YWiC3qAbBcE1kvBMWvaxba)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22852?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
6185c74786 |
Bypass corrupted cached front-component responses with a cache-bust query parameter (#22854)
## Context Follow-up to #22672. Users' browsers hold corrupted cached responses for front-component request URLs from before the fix. #22672 fixed serving and caching for newly built front components, but the corrupted entries already sitting in browsers keep being served and need to be bypassed programmatically. ## What changed - `fetchComponentSourceFromNetwork` appends a constant `cacheBust=v2` query parameter to the component request (`GET /rest/front-components/:id/:cacheKey`). This changes the cache key, so any corrupted response cached under the old URL is never served again and the bundle is refetched. - Existing query parameters on the URL are preserved; if the URL cannot be parsed, the request falls back to the original URL unchanged. - The presigned S3 URL from the JSON handoff is left untouched: adding a query parameter there would invalidate its SigV4 signature. - The `CacheStorage` layer keeps using the logical component URL as its key, so its checksum-verified entries and hit behavior are unchanged. ## Test plan - `fetchComponentSourceFromNetwork.spec.ts`: assertions updated to expect the cache-busted component URL, plus a new test that existing query parameters are preserved and one that the presigned fetch stays unmodified; full renderer suite passes (226 tests). - `npx nx typecheck twenty-front-component-renderer` and `npx nx lint twenty-front-component-renderer` pass. |
||
|
|
652adc3c03 |
fix(server): backfill isSystemSideEffect on system fields provisioned before 2.15 (#22850)
## Context The `isSystemSideEffect` column was introduced in **2.15** via a fast instance command that added it with `DEFAULT false`. That stamped `false` onto every pre-existing `fieldMetadata` row — including the 8 engine-owned system fields (`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`) of every object provisioned before 2.15, regardless of the creation path (API metadata **and** manifest sync). The per-workspace backfill that should have re-flagged those existing rows was explicitly deferred as out of scope in #21673 ("PR 2") and never shipped for `fieldMetadata`. Because `isSystemSideEffect` is configured with `toCompare: false`, no later sync ever repaired the stale value either. Since **2.20** the SDK no longer declares system fields in manifests. On an up-to-date instance, `twenty plan` against an unchanged app therefore diffs those stale-`false` system fields as **missing from the manifest**, and they fall through the `isSystemSideEffectFlatEntity` exclusion in `buildAllFlatEntityOperationRecordByMetadataNameFromFromTo`. Deletion inference then emits them as deletes, which the validator rejects: ``` Sync failed with 144 errors fieldMetadata: 144 errors 1..144. FIELD_MUTATION_NOT_ALLOWED: System fields cannot be deleted ``` (144 = 8 system fields × 18 custom objects, as reported on a production 2.20 instance.) ## What this PR does Adds a **2.21 workspace command** (`upgrade:2-21:backfill-system-field-is-system-side-effect`) that iterates active/suspended workspaces and flags the 8 system fields as `isSystemSideEffect: true`. - **Resolution by deterministic universal identifier**: for each object × reserved system field name it recomputes `getFieldUniversalIdentifier(applicationUID, objectUID, name)` and looks the row up in the flat maps. This is safe (and preferable to matching by `name`) because the 2.19 backfill already took over system field UIDs for every application, so an author-declared field reusing a reserved name keeps its own identifier and is never touched. An extra `isSystem` guard warn-and-skips any mismatch. - **All applications** are covered (installed apps, workspace custom app, twenty-standard): the stale flag is a function of *when* a row was provisioned, not *how*. Installed/custom apps are the acute `twenty plan` delete trap; twenty-standard has no trap today but flagging is a zero-diff no-op (`toCompare: false`) and a prerequisite for the end-state ownership invariant. - **`name` is intentionally excluded**: the 2.20 slow instance command deliberately flipped it to `false` (caller-provided default, not engine-owned); re-flagging it would undo that migration. - Supports `--dry-run`, updates only the collected rows, and invalidates the `flatFieldMetadataMaps` workspace cache after the write (a raw repository update does not invalidate it). ## Related - Resolves the pre-2.15 regression tail of twentyhq/core-team-issues#2635 - Follow-up to twentyhq/core-team-issues#2642 (system field side-effect engine migration) <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22850?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Weiko <corentin@twenty.com> |
||
|
|
b94e38c2d9 |
fix(server): stabilize flaky app-install workspace-version gate test (#22851)
## What The integration test `failing-app-installation-workspace-version.integration-spec.ts` is flaky depending on the shape of the upgrade sequence, especially right after a version bump. It intermittently fails at upload time with: ``` App requires Twenty server >=2.21.0 but this server is 2.20.0. (SERVER_VERSION_INCOMPATIBLE) ``` ## Why The test mixed two different version sources: - The upload-time check (`validateServerCompatibility`) compares the app's required version against the **instance** inferred version, i.e. the last attempted instance command (`workspaceId IS NULL`, via `getInferredVersion`). - The test's `beforeAll` instead derived the required version from the **workspace** cursor. These agree most of the time but diverge right after a version bump whose newest upgrade segment ends in workspace-scoped commands and adds no new instance command. In that state the seeded workspace cursor sits at the new version while the instance is still at the previous one. The test then uploads an app requiring `>=newVersion`, which fails the instance gate at upload time before the workspace gate under test is ever reached. ## How Derive the gate version in `beforeAll` from the last attempted instance command, mirroring exactly what `getInferredVersion()` uses. The required version is then always `>=` the instance's own version, so the upload passes; injecting that same command as a failed workspace attempt drops the workspace to the previous completed version, so the install reliably hits the workspace gate and returns `WORKSPACE_VERSION_INCOMPATIBLE` as the snapshot expects. This holds regardless of whether the newest version's segment ends in an instance or workspace command. No production code changed; the fix is confined to test setup logic. --- _Generated by [Claude Code](https://claude.ai/code/session_01UtEpU7fF4q6pydGRaawfve)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22851?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
bbe9886274 |
chore: sync AI model catalog from models.dev (#22842)
Automated daily sync of `ai-providers.json` from [models.dev](https://models.dev). This PR updates pricing, context windows, and model availability based on the latest data. New models meeting inclusion criteria (tool calling, pricing data, context limits) are added automatically. Deprecated models are detected based on cost-efficiency within the same model family. **Please review before merging** — verify no critical models were incorrectly deprecated. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22842?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com> |
||
|
|
983f03adbe |
i18n - translations (#22833)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
b0dc637dbd |
Throw proper error on duplicate emailing domain (#22790)
Adding an emailing domain that already exists blew up with a raw QueryFailedError and the client just saw a generic "An error occurred". The unique index on domain is global, so the workspace-scoped existence check never caught rows owned by another workspace. Now the check is unscoped and throws an EmailingDomainException mapped to CONFLICT with a proper user-facing message, in both the createEmailingDomain mutation and the email group channel flow. Also dropped the hardcoded catch-all snackbar on the new channel page so server messages actually reach the user. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22790?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
cb95410a51 |
ci: test twenty-apps install against latest dockerhub and local server + new trigger (#22636)
## Why App installability can silently regress from two directions, and today CI only covers one of them: 1. A **server** change (about to merge from the monorepo) breaks the ability to install the **current public apps** — a backward-compatibility regression users would hit on upgrade. 2. An **app** change breaks against a server **built from the current monorepo files** (not just the last published image), so the app and the upcoming server drift apart before either ships. Both are compatibility guarantees between the server and the app catalog. Today they are only tested from the app side, against the latest published image. This PR makes CI enforce the contract from both sides: - Any server PR must keep **every** current public app installable. - Any app PR is exercised against both the **released** server (its integration suite — what users run today) and the **upcoming** (monorepo) server (integration plus deploy + install). ## What Shared building blocks so both CIs exercise the same paths instead of duplicating them: - **`spawn-twenty-server`** (composite action) — returns a running server (`server-url` + `api-key`) from either the latest published Docker Hub image or a server built from the monorepo. Both sources expose the same contract, so callers never branch on how the server came up. - **`test-twenty-app`** (composite action) — exercises one app against a given server, delegating deploy + install to the shared `deploy-twenty-app` / `install-twenty-app` actions. - **`discover-apps`** (reusable workflow) — the single source of truth for the app matrix. Parameterized by `scope` (`public` vs `internal-and-public`) and `changed-only`, so both CIs derive their matrix from the filesystem instead of a hand-maintained list. Discovery stays automatic: a newly added public app is picked up with no CI edit, which is what keeps the "every public app" guarantee honest. Wired in: - **CI Server** gains a `server-apps-install-smoke` matrix that installs every public app (`discover-apps` with `scope: public, changed-only: false`) against the about-to-merge server, gated in `ci-server-status-check` so a regression blocks merge. - **CI Twenty Apps** discovers changed apps (`scope: internal-and-public, changed-only: true`) and runs each against both server sources — the released image and the monorepo build. ## Why the coverage differs per side (not "always everything") `test-twenty-app` has three explicit modes — `installation-and-integration-test` (integration + deploy + install), `integration-test-only` (suite only), `installation-only` (deploy + install only) — because the useful signal depends on what actually changed: - **App PR against the monorepo server → `installation-and-integration-test`.** The app changed, so run its whole suite against the upcoming server, install included. - **App PR against the released server → `integration-test-only`.** Checks the app's own suite against what users run today; install against the released image is left to the SDK e2e path. - **Server PR → `installation-only`, across all apps.** The apps did not change; the only question is "can each one still be installed." Running every app's full integration suite on every server PR would be far slower and largely redundant. Installation-only keeps this broad (the whole catalog) and cheap enough to always run and block merge. The tradeoff is deliberate: broad but shallow where nothing in the app changed, deep where it did. ## Notes / trade-offs - On app-only PRs the `local` source pays a full server build per app (the `server-build` cache is only warm on server PRs). Could be optimized later with a shared warm-up job. - SDK-local (Verdaccio) install testing stays in `ci-create-app-e2e-minimal`; this PR's `local` source targets the server build. <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22636?utm_source=github" rel="nofollow noreferrer noopener" target="_blank">``<img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg">``</a> |
||
|
|
3614183200 |
fix(server): stabilize app install version-gate integration test (#22826)
## Why The `failing-app-installation-workspace-version` integration suite fails on CI (e.g. [this run](https://github.com/twentyhq/twenty/actions/runs/29105697459/job/86405891168)): ``` App requires Twenty server >=2.21.0 but this server is 2.20.0. subCode: SERVER_VERSION_INCOMPATIBLE ``` The test uploads an app requiring `>=${TWENTY_CURRENT_VERSION}` and expects the install to be rejected by the **workspace** version gate. But after the `2.21.0` version bump, `TWENTY_CURRENT_VERSION` (`2.21.0`) moved ahead of the latest instance upgrade command (`2-20`, so `getInferredVersion()` returns `2.20.0`). The tarball upload runs the **instance** server-compat check first, which rejects `>=2.21.0` against a `2.20.0` server before the workspace gate under test is ever reached. The sibling sync test is unaffected because sync only validates workspace compatibility, not the upload-time instance check. ## What Derive the required version range from the version the instance actually reached (the workspace upgrade cursor via `extractVersionFromCommandName`) instead of the drifting `TWENTY_CURRENT_VERSION` constant. This way: - The upload passes the instance server-compat check (server satisfies `>=<current version>`). - The workspace, which resolves one version behind after the injected failed cursor, still fails the workspace gate, producing the expected `WORKSPACE_VERSION_INCOMPATIBLE` error. The error assertion keeps using the normalized snapshot (`scrubSemverVersions`), so the concrete version numbers do not leak into the snapshot and future version bumps won't churn it. --- _Generated by [Claude Code](https://claude.ai/code/session_01XqhqQ8VGJZWBuznR8nRviX)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22826?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
c9d84ba7f0 |
Disable install button in app install onboarding when no app is selected (#22822)
In the app installation onboarding step, the Install button was always clickable, even with no app selected. Clicking it with an empty selection ran the completion flow with zero apps, which is equivalent to skipping. Now the button is disabled until at least one app is selected (in addition to staying disabled while completing). The Skip button remains available for users who don't want to install anything. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22822?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
c0f5a28d5d |
i18n - docs translations (#22824)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22824?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: github-actions <github-actions@twenty.com> |