Commit Graph

13668 Commits

Author SHA1 Message Date
Weiko a7de3ce3a5 Allow non-compact all-day calendar cards to show full content (#22953)
## Summary
- Render all-day calendar items as full `RecordCalendarCard` content in
non-compact views, while keeping compact cards clickable as a whole.
- Rework the all-day time grid layout so the label and day cells align
cleanly in a dedicated grid row.
- Add coverage for the new card behavior and for filtering out
`DATE_TIME` records from the all-day lane.

### Week (compact)
<img width="1308" height="812" alt="Screenshot 2026-07-16 at 15 30 02"
src="https://github.com/user-attachments/assets/24f74f22-86c1-4326-8c65-92ee2c3e8c92"
/>

### Week (non compact)
**NEW**
<img width="1311" height="789" alt="Screenshot 2026-07-16 at 15 29 52"
src="https://github.com/user-attachments/assets/8445c8c5-c952-47e9-ba24-c21d63352e79"
/>

### Month
<img width="1310" height="822" alt="Screenshot 2026-07-16 at 15 29 41"
src="https://github.com/user-attachments/assets/aa33cf48-c602-49e6-bfb1-b9ab1c798bcb"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22953?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. -->
2026-07-16 15:59:12 +02:00
Paul Rastoin 7939cd8684 feat(server): app lifecycle metrics (install/uninstall/upgrade + marketplace publish) (#22656)
## What

Adds product metrics for the app/marketplace lifecycle so they can be
graphed in Grafana. No app-lifecycle metrics existed before; the flows
only logged.

### New counters (`MetricsKeys`)
- `app-install/succeeded` · `app-install/failed`
- `app-upgrade/succeeded` · `app-upgrade/failed`
- `app-uninstall/succeeded` · `app-uninstall/failed`
- `app-registration/created` (new app published) ·
`app-registration/version-published` (new version available)

All carry `universalIdentifier`, `appName`, `sourceType` attributes
(plus `version`, and `errorCode` on failures).

### New gauge
- `twenty_app_installed_workspaces_total` — observable gauge emitting
the top 100 external apps by installed-workspace count (excludes
built-in LOCAL apps). Powers a "most installed apps" leaderboard;
combine with the 24h install/uninstall event counters for recent
activity.

## Where metrics are emitted
- **Install / upgrade**
(`ApplicationInstallService.doInstallApplication`): success + failure
branches, distinguished by the existing `isVersionUpgrade` flag.
- **Uninstall** (`ApplicationInstallResolver.uninstallApplication`): at
the resolver, deliberately *not* in the sync service, so
rollback-triggered internal uninstalls (fired from the install catch
block) don't pollute uninstall counts.
- **Publish / new version**: `upsertFromCatalog` (npm marketplace sync),
`checkForUpdates` (npm version poll), and the tarball CLI publish path.

### Exactly-once version-published
Both the catalog-sync and version-check crons converge
`latestAvailableVersion`. Each emission point is **change-guarded**
(`stored !== incoming`), so whichever cron observes the change first
emits, and the other becomes a no-op. No double counting, no
race-dependent misses.

## Pipeline
Metrics flow through the existing OTel -> ClickHouse path and can be
graphed from the `twenty-product-metrics` dashboard (dashboard changes
live in infra-twenty, not this PR).

## Test plan
- [x] `nx typecheck twenty-server`
- [x] oxlint + oxfmt on changed files
- [x] `oauth-discovery.controller.spec` (the one existing spec touching
these services) passes
- [ ] Reviewer: sanity-check metric names/attributes and cardinality
choices (no `workspaceId` attribute, LOCAL apps excluded from the gauge)


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22656?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: martmull <martmull@hotmail.fr>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-07-16 15:53:59 +02:00
Weiko a5108d512f Block filters on restricted fields (#22873)
## Summary
- Reject filter conditions that target fields without read access,
including relation traversals
- Add unit and integration coverage for denied field filter access

The issue is an information leak through filtering: a user who cannot
read a field can still infer its values from totalCount

### Manual reproduction
- Create or use a non-admin role.
- Give it read access to People records.
- Disable read access to the Person jobTitle field.
- Assign a test member to that role.
- Authenticate as that member.

Run:
```gql
query People($filter: PersonFilterInput) {
  people(filter: $filter, first: 0) {
    totalCount
  }
}
Variables:
{
  "filter": {
    "jobTitle": {
      "like": "Par%"
    }
  }
}
```
Ensure at least one Person has a matching jobTitle.
Before the fix, the request succeeds:
```gql
{
  "data": {
    "people": {
      "totalCount": 1
    }
  }
}
```
The caller can probe restricted values using different filters.
After the fix, it returns a permission error:
```gql
{
  "errors": [
    {
      "message": "Permission denied"
    }
  ]
}
```
The same should happen through a relation filter, for example filtering
Companies by a restricted Person field:
```gql
query Companies($filter: CompanyFilterInput) {
  companies(filter: $filter, first: 0) {
    totalCount
  }
}
{
  "filter": {
    "people": {
      "jobTitle": {
        "like": "Par%"
      }
    }
  }
}
```
2026-07-16 15:19:46 +02:00
Thomas Trompette 988f8ff900 feat(workflow): provision coreWorkflowVersionId on existing workspaces and link them (#22944)
Stacked on the write-back guard hotfix (#22940). Completes the version
soft-ref for workspaces that predate the `coreWorkflowVersionId` field.

## Why
New standard fields aren't auto-synced to existing workspaces; they're
only built at workspace creation or added by an explicit upgrade
command. Deployed 2.20/2.21 instances also carry **legacy core rows with
`id = record.id`** (the pre-soft-ref shared-UUID model, from the
already-run #22663 backfill). The original backfill has already run
there and won't re-run (tracking is by command name), so the migration
to soft-ref has to be **new appended commands**.

## What (two appended 2-22 workspace commands)
1. `add-workflow-version-core-soft-ref-field` (`1784193206000`): adds
the `coreWorkflowVersionId` system field to existing workspaces missing
it (flat-entity migration, `add-message-campaign-stat-fields` pattern).
Idempotent, dry-run aware, skips workspaces without the
`workflowVersion` object.
2. `backfill-workflow-version-core-links` (`1784193207000`): re-runs the
sync (`upsertToCore`). For each version, `upsertToCore` **purges the
legacy shared-id core row** (`id === record id`) then upserts a
deterministic own-id row and writes the link back onto the workspace
record. Targeted per-id delete — it does not wipe unrelated core rows.

Ordering: both run after the original 2-20 backfill. On instances that
run 2-20 fresh (e.g. 2.19 → 2.22) that backfill creates core rows and —
via the hotfix guard — skips the write-back until the field exists;
command 1 provisions the field; command 2 purges + relinks. On
already-migrated 2.21 instances the 2-20 backfill won't re-run, so
command 2 is what clears their shared-id rows.

The same per-id purge in `upsertToCore` also covers the dual-write path:
between the 2.22 deploy and command 2 running, an edited version would
otherwise collide with its shared-id row on the one-active-per-workflow
index.

Scope: version side only. The workflow-side equivalent
(`coreWorkflowId`) ships with the workflow-side sync PR; `core.workflow`
was never backfilled in prod, so it has no legacy shared-id rows.

## Test
- Unit: guard skips write-back when the field is absent; runs it when
present.
- Happy path (fresh reset): original backfill → add-field no-op → link →
4/4 linked, own ids, 0 duplicates.
- Deployed migration (simulated 2.21: shared-id ACTIVE core rows + field
removed): add-field re-provisions → link purges the shared-id rows and
rebuilds → records linked to own-id rows, 0 duplicates, exactly one
ACTIVE core version per workflow (index intact), no collision.
- Idempotent re-run: still 4 core rows, links resolve.
- Typecheck + lint + oxfmt clean.
2026-07-16 13:18:37 +00:00
Weiko fdb78b35d0 Handle scalar select filter values when recomputing view filters (#22930)
## Summary
Fixing `Internal Server Error: Unexpected invalid view filter value for
filter`

Updating a select field’s options failed with an INTERNAL_SERVER_ERROR
when the field had an associated IS_NOT_EMPTY view filter.
The affected filter stored an empty string as its value:
```
operand: IS_NOT_EMPTY
value: ""
```

### Root cause
The option-update side effect treated every associated select filter
value as an option array. It attempted to parse the empty string and
then rejected the result because it was not an array.
However, IS_NOT_EMPTY is a value-less operand, so its empty value is
valid and should not participate in option recomputation.

### Fix
Skip option-value recomputation for operands that do not expect a value,
including IS_NOT_EMPTY.
Normalize legacy scalar select-filter values using the same logic as
filter validation.
Leave subfield filters unchanged.
Preserve compatibility with legacy filter-value representations until
they are migrated to the canonical JSON format.
2026-07-16 12:52:10 +00:00
Thomas Trompette e9bc06a830 fix(workflow): skip core version id write-back when the field is missing (#22940)
## What
The version soft-ref sync (#22821, on main / the 2.22 line, not yet
released) added a **write-back** step: after copying a `workflowVersion`
into `core.workflowVersion`, it sets `coreWorkflowVersionId` on the
workspace record. On workspaces that predate that field (new standard
fields aren't auto-provisioned onto existing workspaces), the write-back
throws:

`Field metadata for field "coreWorkflowVersionId" is missing in object
metadata workflowVersion` (from `formatData`).

This breaks the sync wherever it runs on an unprovisioned workspace —
the backfill (when it runs under the new code) and the dual-write (on
any workflow-version create/update). Observed on staging while upgrading
to 2.22: 2 of 70 workspaces. **2.20 itself was fine** — it ran the old
shared-UUID sync, which had no write-back.

## Fix
Guard the write-back: check the workspace's `workflowVersion` object for
the `coreWorkflowVersionId` field (via `workspaceCacheService` flat
field maps) and skip it with a warning if absent, instead of throwing.
The core row is still upserted; the workspace gets linked later once the
field is provisioned.

## Follow-up
Provisioning `coreWorkflowVersionId` onto existing workspaces and
linking them is #22944 (version side). The workflow-side equivalent
follows with the workflow-side sync PR.

## Test
Unit test covers both branches: field absent → write-back skipped, no
throw, core upsert still runs; field present → write-back runs.
Typecheck + lint clean.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22940?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. -->
2026-07-16 14:25:11 +02:00
Akash! 18b746d525 fix(metadata): invalidate metadata store on SSE reconnect (#22504) (#22562)
## Fixes

Fixes #22504

## Description

This PR fixes a bug where metadata updates (like creating a new field or
a page layout) were missed if an open tab was temporarily disconnected
from the server (e.g., tab backgrounded or a network blip).

Because the frontend's metadata store relies on live SSE deltas for
freshness, any gap in the connection meant the new metadata would never
reach the client unless a full reload occurred. This often resulted in
"No Data" states for newly created layouts or widgets.

**Changes:**
- Updated `SSEClientEffect.tsx` to call `invalidateMetadataStore()` upon
a successful SSE reconnection.
- Re-syncing the metadata store on reconnect ensures that any events
missed during the disconnected gap are retrieved durably without
requiring a manual page refresh.

## Testing

1. Open a record page tab in a workspace.
2. From an app front component or DevTools, trigger a field creation via
the metadata API (`createOneField` / `page-layout` mutations).
3. Briefly disconnect the network or restart the server so the SSE
stream drops during the mutation.
4. Re-establish the connection.
5. The tab should automatically refetch the metadata and reflect the new
field/layout without needing a manual reload, instead of getting stuck
in a "No Data" state.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22562?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. -->
2026-07-16 14:23:20 +02:00
github-actions[bot] 5d9d33b513 i18n - translations (#22951)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-16 14:17:19 +02:00
Weiko 2a7e87125f Remove calendar week view feature flag from public flags (#22950)
## Summary
- Remove `IS_CALENDAR_WEEK_VIEW_ENABLED` from the public feature flag
registry
- Keep the remaining public feature flags unchanged

## Note
Needs more polish before being released in the Lab
2026-07-16 14:09:58 +02:00
Paul Rastoin 217cf650aa Decide workspace destiny from live data and all sub (#22943)
## Context

A production customer was stuck on the billing settings page: their
workspace was `SUSPENDED` (with `suspendedAt` set) while their
subscription was `active` in the database.

## Problem

Stripe webhook events can be delivered out of order or processed
concurrently ([Stripe explicitly does not guarantee
ordering](https://docs.stripe.com/webhooks#events-ordering)), and
`BillingWebhookSubscriptionService.processStripeEvent` made its
suspend/reactivate decision from stale and incomplete data:

- The decision used the **event payload's** subscription status, while
the subscription row was upserted from a **live Stripe fetch** — so the
two could diverge. Around trial end, Stripe emits `active → past_due`
then (once the customer pays) `past_due → active` within a short window.
If the stale `past_due` event is processed last, it suspends the
workspace while writing an `active` subscription to the DB. Nothing
self-heals from that state: the workspace stays suspended while the
cleanup cron warns and eventually soft-deletes it.
- The decision only looked at the **event's own subscription**, but
suspension is a workspace-level decision and a Stripe customer can hold
several subscriptions (plan switch, cancel-then-resubscribe). A
`customer.subscription.deleted` event for the old subscription is
*genuinely* canceled — only the sibling subscription proves the customer
is still paying.
- The workspace snapshot was read at the top of the handler, before
several slow awaits, so a concurrent event could change it mid-flight.

## Fix

Every event now converges the workspace to the current Stripe state,
regardless of delivery order:

- **Live input**: fetch the customer's not-ended subscriptions from
Stripe (`subscriptions.list` without a `status` param excludes the
unbounded canceled history server-side; an explicit
`NOT_ENDED_SUBSCRIPTION_STATUSES` filter additionally drops
`incomplete_expired`, which would otherwise block suspension forever
since it is neither suspend-worthy nor activating). The event's
subscription is taken from that list, or fetched directly by id when
absent (deletion events, deleted customers) — same retrieve the code
used before. The DB upsert uses this live object, never the payload.
- **Workspace-level decision over all live subscriptions**: suspend only
when **every** subscription warrants it, reactivate as soon as **one**
is activating (`active`/`trialing`), and deliberately do nothing in
between — e.g. a `past_due` subscription in its payment-retry grace
period blocks suspension without triggering reactivation.
- **Guarded transitions (compare-and-swap)**:
`WorkspaceService.suspendWorkspace`/`reactivateWorkspace` now apply
their UPDATE only when the workspace is still in a state the transition
is valid from, and return whether they applied. Repeated suspensions
keep the first `suspendedAt` so the cleanup countdown stays anchored to
the original suspension date; the deletion-warning cleanup job is only
enqueued when a reactivation actually applied. The suspend path re-reads
the workspace right before deciding and switches exhaustively on
`activationStatus` (`assertUnreachable` in `default`), preserving the
previous behavior including suspend-over-reactivate precedence.

## Tests

Unit tests cover the incident scenario and its neighbors: a stale
`past_due` event after payment reactivates instead of suspending; a live
`unpaid` state suspends even when the payload says `active`; a canceled
subscription event does not suspend (and reactivates) when a sibling
`active`/`trialing` subscription exists; a sibling in `past_due` grace
blocks suspension without reactivating; the direct-retrieve fallback
handles subscriptions absent from the customer list; the guarded
reactivation skips the cleanup job when it did not apply; and
soft-deleted workspaces are never transitioned.
2026-07-16 14:09:09 +02:00
Charles Bochet 7ec7774087 Fix infinite redirect loop when logging out of a suspended workspace (#22949)
## Problem

Clicking **Log out** on a workspace suspended for a past-due
subscription locks the tab into an infinite redirect loop that hammers
the server with unauthenticated GraphQL requests until the tab is
closed.

Reproduced on cloud: workspace with `Pro plan • Past due` (activation
status `SUSPENDED`), user forced onto `/settings/billing`, click Log out
→ tab freezes, URL flip-flops between `/welcome` and
`/settings/billing`, requests stream out continuously.

## Root cause

`clearSession` nulls the `tokenPairState` atom and removes the persisted
session localStorage keys, but leaves the **in-memory**
`currentWorkspaceState`/`currentUserState` atoms populated, relying on
the subsequent `window.location.assign('/welcome')` reload to reset
them.

`PageChangeEffect` keeps running until that reload commits, and the
intermediate state (no token + suspended workspace) makes
`usePageChangeEffectNavigateLocation` ping-pong:

- on `/settings/billing`: no token → navigate to `/welcome`
- on `/welcome`: the no-token guard is skipped (`SignInUp` is in
`ONGOING_USER_CREATION_PATHS`), then `isWorkspaceSuspended` reads the
**stale** workspace atom → navigate back to `/settings/billing`

The synchronous navigation loop pegs the main thread, so the pending
full-page navigation never commits and the loop never resets. Every
bounce remounts pages whose queries refire without a token (each one
erroring `UNAUTHENTICATED`), plus Sentry envelopes — the server spam.

Verified during repro: mid-loop the tab had `tokenPairState="null"` and
no `currentWorkspaceState` in localStorage (the loop runs fully
unauthenticated off the in-memory atom), and an injected
`localStorage.setItem` wrapper survived the whole loop, proving the page
never reloaded.

## Fix

Clear the same in-memory auth atoms in `clearSession` that
`onUnauthenticatedError` (useApolloFactory) already clears:
`currentUserState`, `currentWorkspaceState`,
`currentWorkspaceMemberState`, `currentUserWorkspaceState`. With the
workspace atom gone, the suspended guard can't fire after logout, the
ping-pong never starts, and the redirect to `/welcome` commits normally.

## Test

Extended the `useAuth` sign-out test: seeds a suspended
`currentWorkspaceState` and a `currentUserState` before `signOut()` and
asserts both are null afterwards.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22949?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. -->
2026-07-16 14:05:56 +02:00
Charles Bochet 9860871eac perf(server): lower workspace local cache cap to 6,000 entries (#22946)
## Context

#21954 raised `MAX_LOCAL_CACHE_ENTRIES` from 1,000 to 7,500 to stop the
per-pod workspace metadata cache from thrashing against Redis. That
worked: the cache node's egress dropped from ~1.1-1.8 TB/day to ~0.5-0.7
TB/day and has stayed there.

The memory side of the trade turned out tighter than the sizing assumed.
Prod-eu metrics over the past month:

- Average server pod working set rose from ~1.9-2.4 GiB (before the
rollout on 06/26) to ~3.0-3.4 GiB, with individual pods peaking at
3.5-3.6 GiB, right at the `--max-old-space-size=3500` heap ceiling on 4
GiB pods.
- The `workspace-metadata-cache/local-eviction` counter added in #21954
stayed at zero for three weeks, then on 07/15 between 08:30 and 09:30
UTC all 7 server pods hit the 7,500-entry cap within an hour (no deploy
that morning; organic growth crossed the threshold during the European
morning peak). Since then the fleet evicts continuously (~167k
entries/day).
- Server pods were terminated with reason OOMKilled on 8 days of the
past month (1-2 pods each time).

The per-entry payload also grew since the cap was sized: search field
metadata backfills, two new per-workspace cache components
(`flatSearchFieldMetadataMaps`, `workflowAutomatedTriggerMaps`), and the
heap-only `ORMEntityMetadatas` entries. So the byte ceiling implied by
7,500 entries now sits exactly at the heap limit instead of comfortably
under it.

## What this does

Lower `MAX_LOCAL_CACHE_ENTRIES` 7,500 → 6,000. At the measured ~170-200
KB average entry size this frees roughly 300-500 MB of steady-state heap
per pod, restoring headroom under the 3.5 GB old-space limit.

Traffic cost should be small: eviction churn at the 7,500 cap only moved
cache egress from ~0.55 to ~0.65-0.68 TB/day, so a 20% smaller cap
should keep the bulk of the #21954 reduction. The eviction counter gives
us the feedback loop; if egress climbs materially we can split the
difference, and if pods still run hot the next lever is byte-aware
eviction for the few known-heavy keys.
2026-07-16 12:34:02 +02:00
github-actions[bot] 1b9152d4c5 chore: sync AI model catalog from models.dev (#22939)
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.

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-07-16 08:47:13 +02:00
github-actions[bot] 5f8baa9761 i18n - docs translations (#22933)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-15 21:00:23 +02:00
Weiko 9c2d87a846 Fix role lookup query and align calendar layout behavior (#22928)
## Summary
Fixes slow getRoles requests caused by loading several one-to-many role
relations in a single TypeORM query.

The previous query produced a Cartesian product across role targets,
permission flags, object permissions, and field permissions. In
production, a workspace with 9 roles expanded into more than 32,000
database rows before TypeORM hydration.

This change:
- Reads roles and their related permissions from the existing flat-map
caches.
- Hydrates relations using the same findManyWithRelationsFromCache
pattern as ViewService.
- Removes the expensive joined query from the getRoles path.
- Preserves the existing GraphQL response shape.

## Performance
### Before:
9 roles
32,257 SQL result rows
Approximately 10 seconds observed request latency
### After:
No Cartesian SQL query
Cache-backed lookups and in-memory relation hydration

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22928?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. -->
2026-07-15 20:56:09 +02:00
nitin 79f3a5243a Add callAppRoute to RestApiClient (#22863)
Adds a `callAppRoute` method to `RestApiClient` in
`twenty-client-sdk/rest`. It calls one of the app's own HTTP routes
using the injected `TWENTY_FUNCTIONS_URL`, resolved internally the same
way the client already resolves `TWENTY_API_URL`, so app code no longer
reads env vars or knows how function routes are hosted.

Both app runtimes already go through `RestApiClient` for route calls
(logic functions and front components), so both get this in one place;
front components keep the existing 401 token-refresh flow.

Pairs with #22825, which makes the injected `TWENTY_FUNCTIONS_URL`
callable in every topology (app custom domain -> workspace isolated
functions domain -> `SERVER_URL/s`).

Once this ships in an SDK release, Call Recorder's own-route plumbing
(logic-function and front-component utils) drops its URL resolution and
calls `client.callAppRoute(path, body)`.

---------

Co-authored-by: martmull <martmull@hotmail.fr>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-07-15 18:07:16 +00:00
martmull 67ed2689ce Unify manifest apply pipeline between application install and dev sync (#22921)
First step of unifying application behaviors across sources (NPM,
TARBALL, LOCAL), following up on #22868. No storage layout changes.

## Problem

Marketplace/tarball installs (`ApplicationInstallService`) and CLI dev
sync (`ApplicationDevelopmentService`) each implemented the second half
of application delivery — metadata sync, SDK client generation,
registration refresh — with quietly diverging behavior:

- Install decided SDK regeneration on `!isVersionUpgrade` (application
row exists), dev sync on `!application.version`. The version-based check
is the robust one: a failed first install leaves an application row
without a version, and the row-based check then skipped SDK generation
on retry unless the schema changed.
- Install refreshed the registration manifest (`updateFromManifest`)
without syncing its variable schemas, while catalog sync, tarball
upload, and dev sync all do. An NPM install that bumped
`latestAvailableVersion` with new `serverVariables` left the
registration's variable schemas stale until the next catalog cron.
- The guards protecting shared registrations from workspace writes
(npm-sourced or not owned by the workspace) lived only inside dev sync's
`syncRegistrationMetadata`.

## Change

New `ApplicationManifestApplyService` (application-manifest module) owns
those steps for both flows:

- `applyManifestToWorkspace`: `synchronizeFromManifest` + SDK client
generation when it's the first successful apply (no persisted version)
or the schema changed. Used by install and dev sync.
- `refreshRegistrationFromManifest`: guarded registration update +
variable schema sync. Callers acting for a workspace (dev sync) pass
`onlyIfOwnedByWorkspaceId`, moving the npm/ownership guard into the
shared service; installs pass `latestAvailableVersion` +
`preventVersionDowngrade` as before. Returns whether anything was
written so dependent side effects (registration asset store in dev sync)
skip together with it.
- `ApplicationRegistrationService.updateFromManifest` now returns
whether it wrote, so variable schemas are only synced from manifests
that were actually applied — previously a downgraded install would still
have synced variables from the older manifest if we had naively added
the sync there.

Install and dev services lose the duplicated logic (and their direct
`SdkClientGenerationService` / variable-service dependencies); hooks and
file writes stay where they were.

## Behavior deltas (all deliberate)

- NPM/TARBALL installs now sync registration variable schemas when they
refresh the registration.
- Retrying a failed first install regenerates the SDK client even when
the schema diff is empty.

## Follow-ups (agreed plan, separate PRs)

Single semver version gate; one registration-metadata writer with lock
coverage for dev-sync/tarball; unified upgrade incl. TARBALL;
recoverable upgrades + stale file cleanup; tarball storage decoupled
from the owner workspace; orphan cleanup cron; faster public-asset
serving (ETag/304); parallel install file writes; PREBUILT execution
mode for app logic functions.

## Validation

- New spec `application-manifest-apply.service.spec.ts` (9 tests: SDK
generation matrix, ownership/npm guards, downgrade-skip short-circuits
variable sync)
- All 26 application suites pass; typecheck, oxlint (type-aware) and
oxfmt green on twenty-server

---
_Generated by [Claude
Code](https://claude.ai/code/session_015L4zvAL9bsk7azYkbhcZSg)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22921?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. -->
2026-07-15 19:36:01 +02:00
github-actions[bot] a69852c1b0 i18n - translations (#22927)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-15 18:58:32 +02:00
Weiko 8cf462d5f7 Add day view support to record calendar (#22922)
## Summary
- Add a calendar day view and wire it into the record calendar layout
selection
- Update the top bar, time grid, and week/day drag and drop handling to
support the new view
- Extend supported layout logic and public feature flags for calendar
day view access
- Add coverage for calendar view content, calendar container behavior,
top bar behavior, day view rendering, supported layout resolution, and
week event drop handling

<img width="1276" height="852" alt="Screenshot 2026-07-15 at 17 48 33"
src="https://github.com/user-attachments/assets/b1d9d255-2d64-4adb-82b9-3e500cb0d561"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22922?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. -->
2026-07-15 16:50:23 +00:00
Thomas Trompette 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. -->
2026-07-15 16:43:37 +00:00
Thomas Trompette 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.
2026-07-15 16:21:53 +00:00
Thomas Trompette 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. -->
2026-07-15 16:13:26 +00:00
github-actions[bot] 541c67d222 i18n - translations (#22923)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-15 17:58:15 +02:00
Thomas Trompette 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. -->
2026-07-15 17:50:00 +02:00
github-actions[bot] 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>
2026-07-15 17:14:17 +02:00
Charles Bochet 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.
2026-07-15 17:03:17 +02:00
nitin 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. -->
2026-07-15 16:56:26 +02:00
github-actions[bot] 36a14478ae i18n - translations (#22916)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-15 16:38:28 +02:00
Weiko 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"
/>
2026-07-15 16:30:18 +02:00
martmull 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.
2026-07-15 16:12:28 +02:00
github-actions[bot] 907c5cc39f i18n - translations (#22913)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-15 15:54:27 +02:00
Abdul Rahman 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. -->
2026-07-15 19:15:47 +05:30
github-actions[bot] e31e6c7794 i18n - docs translations (#22912)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-15 15:12:59 +02:00
Paul Rastoin 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>
2026-07-15 14:50:26 +02:00
Thomas Trompette 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. -->
2026-07-15 11:53:08 +00:00
martmull 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. -->
2026-07-15 13:39:11 +02:00
Paul Rastoin 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. -->
2026-07-15 10:04:22 +00:00
nitin 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. -->
2026-07-15 15:20:18 +05:30
Abdul Rahman 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>
2026-07-14 17:16:59 +02:00
Charles Bochet 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.
2026-07-14 17:16:01 +02:00
nitin 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. -->
2026-07-14 01:51:39 +05:30
github-actions[bot] 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>
2026-07-13 20:31:17 +02:00
Paul Rastoin 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. -->
2026-07-13 18:12:14 +02:00
Joshua Freedman 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. -->
2026-07-13 17:41:00 +02:00
Harshit Raizada 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. -->
2026-07-13 17:34:09 +02:00
Priyanshu Bartwal 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"
/>
2026-07-13 17:32:18 +02:00
martmull 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.
2026-07-13 17:09:34 +02:00
Brahm Lower 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. -->
2026-07-13 16:55:38 +02:00
twenty-pr[bot] 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>
2026-07-13 16:49:18 +02:00
Paul Rastoin 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).
2026-07-13 13:12:43 +00:00