This PR:
- Breaks useAgentChatData into focused effect components (streaming,
fetch,
init, auto-scroll, diff sync)
- Splits message list into non-last (stable) + last (streaming/error) to
prevent full re-renders on each stream chunk
- Adds scroll-to-bottom button and MutationObserver-based auto-scroll on
thread switch
- Lifts loading state from context to atoms
- Adds areEqual to selector
factories
We could improve further but this sets up a robust architecture for
further refactoring.
## Messages flow
The flow of messages loading and streaming is now more solid.
Everything goes out from `AgentChatAiSdkStreamEffect`, whether loaded
from the DB or streaming directly, and every consumers is using only one
atom `agentChatMessagesComponentFamilyState`
## Data sync effect with callbacks new hook
See
`packages/twenty-front/src/modules/apollo/hooks/useQueryWithCallbacks.ts`
which allows to fix Apollo v4 migration leftovers and is an
implementation of the pattern we talked about with @charlesBochet
We could refine this pattern in another PR.
# Before
https://github.com/user-attachments/assets/84e7a96f-6790-405d-8a73-2dacbf783be5
# After
https://github.com/user-attachments/assets/4c692e3a-2413-4513-abcc-44d0da311203
Co-authored-by: Charles Bochet <charles@twenty.com>
## Summary
- Restores the original migration timestamp (`1773945207801`) that was
accidentally changed to `1774005903909` during PR #18787
- Keeps the content improvements (default column values) from that PR
intact
## Test plan
- [ ] Verify migration runs correctly with `npx nx run
twenty-server:database:migrate:prod`
Made with [Cursor](https://cursor.com)
## Summary
This preserves percent-encoded payloads when normalizing links fields.
`lowercaseUrlOriginAndRemoveTrailingSlash` was decoding the path and
query string while lowercasing the URL origin. That changes URLs where
encoded payloads are semantically significant, such as Google Maps links
containing `%2F` segments.
Closes#18698.
## Changes
- stop decoding the path/query payload in
`lowercaseUrlOriginAndRemoveTrailingSlash`
- preserve the raw path, query, and hash while still lowercasing the
origin and trimming a trailing slash
- update shared URL normalization tests to assert encoded payloads stay
encoded
- add a server-side regression test covering imported links field
normalization
## Validation
- `corepack yarn jest --config packages/twenty-shared/jest.config.mjs
packages/twenty-shared/src/utils/url/__tests__/lowercaseUrlOriginAndRemoveTrailingSlash.test.ts
--runInBand`
- `corepack yarn jest --config packages/twenty-server/jest.config.mjs
packages/twenty-server/src/engine/core-modules/record-transformer/utils/__tests__/transform-links-value.util.spec.ts
--runInBand`
- `corepack yarn nx test twenty-server --runInBand
--testFile=src/engine/core-modules/record-transformer/utils/__tests__/transform-links-value.util.spec.ts`
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
## Summary
- Reverts the `useSortedNavigationMenuItems` coupling in
`ViewPickerOptionDropdown` introduced by #18791
- The viewPicker now uses `useNavigationMenuItemsData` directly for the
`isFavorite` check, keeping view ordering and navigation menu item
ordering independent
## Why
PR #18791 replaced `useNavigationMenuItemsData` with
`useSortedNavigationMenuItems` in the viewPicker for favorite detection.
While the intent was to filter out stale/orphaned navigation items, this
created an unnecessary coupling: `useSortedNavigationMenuItems`
subscribes to `viewsSelector` and `objectMetadataItemsSelector`, making
the viewPicker transitively dependent on the navigation menu item
ordering system. View ordering in the picker (driven by `view.position`)
and navigation menu item ordering (driven by
`navigationMenuItem.position`) should remain decorrelated.
## Test plan
- [ ] Open the viewPicker dropdown and verify views are listed in
correct order
- [ ] Drag-and-drop to reorder views in the viewPicker — confirm it
works
- [ ] Verify the "Add to Favorite" / "Manage favorite" label still
correctly reflects favorite state
- [ ] Reorder navigation menu items in the sidebar — confirm viewPicker
order is unaffected
Made with [Cursor](https://cursor.com)
## Summary
Fixes#18757
This fixes a set of Favorites / navigation-menu-item integrity problems
related to deleted views, stale hidden items, and upgraded workspaces
with orphaned navigation items.
## What changed
- delete `navigationMenuItem` entries when their favorited view is
deleted
- keep the client metadata store in sync immediately when a view is
deleted
- determine whether a view is already favorited from visible valid
navigation items instead of raw stale items
- add a `1.20.0` upgrade repair command that deletes orphan navigation
menu items and normalizes positions
- add regression coverage for deletion of both record-based and
view-based navigation menu items
## Details
Server:
- extend `NavigationMenuItemDeletionService` so cleanup applies to
deleted views as well as deleted records
- add regression tests covering record-based deletion, view-based
deletion, and no-op behavior
- add `DeleteOrphanNavigationMenuItemsCommand` to remove orphaned items
pointing to:
- deleted views
- deleted records
- missing folders
- normalize positions per scope (`userWorkspaceId + folderId`) after
repair
- wire the new repair command into the `1.20.0` upgrade flow
Frontend:
- add `useRemoveNavigationMenuItemByViewId`
- remove the related navigation item from client metadata immediately
when deleting a view
- use sorted / visible navigation items for favorite detection so stale
hidden rows do not block re-adding a favorite
## Why
Issue `#18757` reports mismatches between Favorites shown in the UI and
rows users can still find in the database. We found that current
Favorites behavior is driven by `navigationMenuItem`, not the legacy
`favorite` table, and that stale / orphaned `navigationMenuItem` rows
could:
- remain after deleting a favorited view
- stay hidden from the UI if they point to invalid targets
- still cause the UI to think a view was already favorited
- persist in workspaces with migration damage from skipped sequential
upgrades
This patch addresses those cases directly and adds an upgrade-time
repair path for older corrupted workspaces.
## Validation
Passed:
- `./node_modules/.bin/jest --config
packages/twenty-server/jest.config.mjs --runInBand
packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/services/__tests__/navigation-menu-item-deletion.service.spec.ts`
- `./node_modules/.bin/tsc -p packages/twenty-front/tsconfig.json
--noEmit --pretty false`
Known unrelated existing failure:
- `./node_modules/.bin/tsc -p packages/twenty-server/tsconfig.json
--noEmit --pretty false`
The server typecheck failure is pre-existing and unrelated to this
branch. Current errors are around `@file-type/pdf` module resolution and
`is-psl-parsed-domain.type.ts`.
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
On top of [previous closed
PR](https://github.com/twentyhq/twenty/pull/18713) from @FelixMalfait :
- add a schema-creation-skipping optimization
- extract a handler-per-operation pattern,
- add runtime input validation guards,
- integrate with the standard workspace cache
- add gql-style error handling
To do/optimize/check :
- gql parsing and null backfilling
## Intro
This PR introduces a **direct GraphQL execution path** that bypasses
per-workspace GraphQL schema generation for workspace data queries (CRUD
on user-defined objects like companies, people, tasks, etc.).
## Why
In the current architecture, every workspace gets its own
dynamically-generated GraphQL schema reflecting its custom objects and
fields. This costs **~20MB of RAM per workspace per pod** and takes time
to build. For a multi-tenant SaaS with thousands of workspaces, this is
a significant infrastructure cost and a latency bottleneck (especially
on cold starts or cache misses).
The insight is that most workspace queries (`findMany`, `createOne`,
`updateOne`, etc.) don't actually *need* the full schema — they can be
routed directly to the existing Common API query runners by parsing the
GraphQL AST and matching resolver names against object metadata. The
schema is only truly needed for introspection, subscriptions, or queries
that mix core and workspace resolvers.
## How It Works
1. A Yoga `onRequest` plugin intercepts incoming GraphQL requests
2. It parses the query AST and checks if all top-level fields map to
generated workspace resolvers (e.g. `findManyCompanies`,
`createOnePerson`)
3. If yes, it executes them directly against the query runners, skipping
schema generation entirely
4. If the query contains introspection, subscriptions, or core-only
resolvers, it falls through to the normal path
5. Even for mixed queries it can't fully handle, it sets
`skipWorkspaceSchemaCreation` to avoid building the schema when
unnecessary
The whole thing is gated behind the
`IS_DIRECT_GRAPHQL_EXECUTION_ENABLED` feature flag for safe incremental
rollout.
**Net effect**: dramatically lower memory footprint and faster response
times for the vast majority of workspace API calls.
---------
Co-authored-by: Félix Malfait <felix@twenty.com>
Fixes https://github.com/twentyhq/private-issues/issues/432
## Problem
When a user's invoice goes unpaid, Stripe moves their subscription to
`unpaid` status, and Twenty suspends the workspace. But if the user pays
that invoice while a new billing period has started, Stripe has already
generated a new **draft** invoice for that period. Since the draft isn't
finalized or paid, Stripe doesn't reactivate the subscription — the
workspace stays suspended indefinitely.
## What was missing
- No handler for the `invoice.paid` Stripe webhook event
- No mechanism to finalize draft invoices that accumulated during the
`unpaid` period
- No way to reset the workspace deletion countdown (`suspendedAt`) when
a user shows payment intent
## What was added
### 1. `StripeInvoiceService` — new Stripe SDK wrapper
- `listDraftInvoices(stripeSubscriptionId)` — lists all draft invoices
for a subscription
- `finalizeInvoice(invoiceId)` — finalizes a draft with `auto_advance:
true` so Stripe auto-charges it
### 2. `INVOICE_PAID` enum value
Added to `BillingWebhookEvent` so the controller can route it.
### 3. `processInvoicePaid()` in `BillingWebhookInvoiceService`
New private handler that:
- Fetches all draft invoices for the subscription
- Filters to only those whose `period_end` is in the past (already
overdue)
- Finalizes each one (with error handling per invoice to avoid blocking
the webhook)
- If the workspace is suspended, resets `suspendedAt` to now (buys time
before deletion)
### 4. Controller routing
`INVOICE_FINALIZED` and `INVOICE_PAID` are now grouped in the same
switch case, both calling `processStripeEvent(data, eventType)`, which
forks internally in the service.
## Expected recovery flow
User pays overdue invoice
→ Stripe fires invoice.paid
→ Handler finalizes past-due draft invoices (auto_advance: true)
→ Stripe auto-charges them
→ Subscription becomes active
→ customer.subscription.updated fires
→ Existing logic unsuspends workspace
→ suspendedAt refreshed (resets deletion countdown while payments
cascade)
## Summary
Fixes#18610
After completing the CreateProfile onboarding step,
`currentWorkspaceMembersState` (plural, used by `useActorFieldDisplay`
to resolve "Created by" names) was never updated with the new name —
only `currentWorkspaceMemberState` (singular) was. This caused "Created
by" fields to display "Untitled" for the remainder of the session.
**Root cause analysis:**
The issue reporter attributed the bug to empty
`core.user.firstName/lastName`, but `createdBy` display doesn't use
`core.user` at all. The actual flow:
1. Sign-up creates workspace member with empty names (copied from empty
`core.user`)
2. `GetCurrentUserDocument` loads `currentWorkspaceMembersState` with
empty names
3. CreateProfile step updates workspace member in DB +
`currentWorkspaceMemberState` (singular) ✓
4. `currentWorkspaceMembersState` (plural) is **never refreshed** — the
query is skipped once `currentUser` exists
5. `useActorFieldDisplay` looks up from the stale plural state → empty
name → "Untitled"
**Fix:** Update `currentWorkspaceMembersState` alongside
`currentWorkspaceMemberState` in the CreateProfile submit handler.
## Summary
Fixes "missing FROM-clause entry for table" SQL error when using
`orderByForRecords` with a relation field (e.g. `{ company: { name:
"AscNullsFirst" } }`) in a `groupBy` query.
### Root cause
This is a regression from #18005 (Feb 17). That PR correctly removed a
duplicate `applyOrderToBuilder()` call on the groupBy subquery (which
was conflicting with the `ROW_NUMBER() OVER (... ORDER BY ...)` window
function), but in doing so it also removed the LEFT JOINs that
`applyOrderToBuilder` was adding for relation fields.
After that change, ordering relied solely on `getOrderByRawSQL()` inside
`applyPartitionByToBuilder()`, which builds raw SQL for the window
function but never added the required JOINs. Scalar field ordering (e.g.
`name`, `position`) kept working since those don't need JOINs, but
relation field ordering (e.g. `company.name`) broke.
### Fix
- `getOrderByRawSQL` now returns `relationJoins` alongside the SQL
string so callers get the join info they need
- `applyPartitionByToBuilder` in `GroupByWithRecordsService` adds the
required LEFT JOINs before building the `ROW_NUMBER()` window function,
mirroring the pattern used by `applyOrderToBuilder` in the `findMany`
path
## Test plan
- [x] Manually tested locally with a GraphQL query matching the
customer's failing pattern (`orderByForRecords: [{ company: { name:
"AscNullsFirst" } }]`)
- [x] Added integration tests for ascending and descending relation
field ordering in `group-by-with-records-resolver.integration-spec.ts`
- [x] Typecheck passes
- [x] Lint passes
- [x] CI green
## Summary
- Some production workspaces have `SELECT` or `MULTI_SELECT`
fieldMetadata options that are missing the `id` property (e.g.
`[{"color":"yellow","label":"Draft","value":"DRAFT","position":0},
...]`).
- Adds a new upgrade command
`upgrade:1-20:backfill-select-field-option-ids` that queries all
SELECT/MULTI_SELECT fieldMetadata per workspace, detects options missing
an `id`, and backfills them with a UUID v4.
- The command is idempotent (no-op when all options already have ids),
supports `--dry-run`, and invalidates workspace caches after patching.
## Summary
### Fix 1: Autogrow input unclickable when value is empty string
- Fixes the last name input on the Person record show page being
unclickable on regular screens
### Fix 2: Surface nested migration errors in add-missing-system-fields
command
- `AddMissingSystemFieldsToStandardObjectsCommand` calls
`workspaceMigrationRunnerService.run()` directly and was re-throwing
`WorkspaceMigrationRunnerException` without reading its nested `errors`
- Extracted `getNestedErrorMessages()` helper (reused by both
`isUniqueViolationError` and `enrichErrorMessage`)
- Both catch sites now call `enrichErrorMessage()` before re-throwing,
appending nested error details to the message
**Before:**
```
ERROR [UpgradeCommand] Error in workspace ...: Migration action 'create' for 'fieldMetadata' failed
ERROR [UpgradeCommand] undefined
```
**After:**
```
ERROR [UpgradeCommand] Error in workspace ...: Migration action 'create' for 'fieldMetadata' failed (metadata: duplicate key value violates unique constraint "...")
```
## Test plan
- [x] Lint passes
- [x] Typecheck passes
- [x] Verify upgrade command errors now include nested error details
- [x] Verify autogrow input is clickable when value is empty string
## Summary
- Removes a redundant direct `cookieStorage.setItem('tokenPair', ...)`
call in `handleSetAuthTokens` that was overwriting the Jotai-managed
cookie (which has a 180-day expiry) with a session cookie (no expiry)
- This caused users to be logged out whenever their browser fully
closed, instead of staying authenticated for 180 days
## Root cause
In April 2025 (`a7e6564017`), a direct `cookieStorage.setItem` call was
added alongside `setTokenPair()` as a workaround because Recoil's
`onSet` effect fired too late for the Apollo client to read the token
synchronously.
In February 2026 (`674f4353cd`), `tokenPairState` was migrated from
Recoil to Jotai's `atomWithStorage`, which writes the cookie
**synchronously** with a 180-day `expires`. The old direct write was
left in place and now runs *after* the Jotai write, overwriting the
cookie without an `expires` — making it a session cookie.
## Test plan
- [ ] Log in to the app
- [ ] Inspect the `tokenPair` cookie in DevTools → Application → Cookies
- [ ] Verify the cookie has an expiration date ~180 days from now (not
"Session")
- [ ] Close and reopen the browser — confirm you remain logged in
Made with [Cursor](https://cursor.com)
## Summary
- Migrates 4 entities (`connectedAccount`, `messageChannel`,
`calendarChannel`, `messageFolder`) from per-workspace schemas to the
shared `core` metadata schema
- Introduces a `IS_CONNECTED_ACCOUNT_MIGRATED` feature flag to control
the migration: when enabled, reads come from core metadata and all
writes are dual-written to both workspace and core
- Extracts 12 enums from workspace entity files to `twenty-shared` for
reuse across frontend and backend
- Creates new TypeORM entities, metadata services, GraphQL
resolvers/DTOs, and exception interceptors per entity
- Each entity owns its own data access module
(`ConnectedAccountDataAccessModule`, `MessageChannelDataAccessModule`,
`CalendarChannelDataAccessModule`, `MessageFolderDataAccessModule`) — no
umbrella infrastructure module
- Adds a 1.20 upgrade command that backfills data from workspace schemas
to core (preserving UUIDs) and enables the feature flag
- Replaces direct repository access with data access service calls
across ~50 files in messaging, calendar, and connected-account modules
- Adds `lastSignedInAt` and `oidcTokenClaims` fields to the new
`ConnectedAccountEntity`
- Drops unused `lastSyncHistoryId` field from the migrated connected
account entity
## Test plan
- [x] Lint passes (`npx nx lint:diff-with-main twenty-server`)
- [x] Typecheck passes (`npx nx typecheck twenty-server`)
- [x] All unit tests pass (477 suites, 4267 tests, 0 failures)
- [ ] Manual test: verify messaging sync works with feature flag
disabled (existing behavior)
- [ ] Manual test: run upgrade command on a workspace, verify data
backfilled to core tables
- [ ] Manual test: verify messaging/calendar sync works with feature
flag enabled (dual-write path)
- [ ] Manual test: verify GraphQL metadata resolvers return correct data
when flag enabled
Previously, the Opened section was always hidden for all workflow
objects. We now base the “in nav” check on the full set of object
metadata IDs that have a workspace nav item, instead of only active
non-system objects. So: if an object has a nav item (e.g. workflow
runs), it no longer appears under Opened; if it doesn’t, it can appear
there. The hook was simplified to only expose
`objectMetadataIdsInWorkspaceNav`, and the unused return value was
removed.
## Summary
When clicking the ✕ button on an uploaded file in the AI chatbot context
preview, the click event bubbled up to the `StyledClickableContainer`
parent, which triggered `handleClick` (opening the file preview modal)
instead of — or in addition to — calling `onRemove`.
**Root cause:** `StyledClickableContainer` has `onClick={handleClick}`
at the div level. The `AvatarOrIcon` (X button) `onClick={onRemove}`
callback didn't stop propagation, so the event continued to bubble and
opened the preview.
**Fix:** Wrap `onRemove` in a `handleRemove` callback that calls
`e.stopPropagation()` before invoking the original handler.
Fixes#18298
---------
Co-authored-by: victorjzq <zhiqiangjia@users.noreply.github.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
Related to issue #17711
Follow-up to PR #17774 which fixed the frontend link normalization only
## Summary
- Normalize `primaryEmail` (lowercase) and `primaryLinkUrl` in relation
`connect.where` composite values
- Applied in both frontend spreadsheet import and backend
`DataArgProcessorService` to cover all entry points (UI import, GraphQL
API)
---------
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
Bumps [@dagrejs/dagre](https://github.com/dagrejs/dagre) from 1.1.3 to
1.1.8.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="7e4d15f191"><code>7e4d15f</code></a>
Building for release</li>
<li><a
href="d3908e2c13"><code>d3908e2</code></a>
Bumping version</li>
<li><a
href="ce295f8e07"><code>ce295f8</code></a>
Build for release</li>
<li><a
href="b64b905772"><code>b64b905</code></a>
Bumping the version</li>
<li><a
href="de169d24c1"><code>de169d2</code></a>
Merge pull request <a
href="https://redirect.github.com/dagrejs/dagre/issues/481">#481</a>
from Nathan-Fenner/nf/improve-network-simplex-perform...</li>
<li><a
href="065e0d8374"><code>065e0d8</code></a>
improve performance of graph node ranking</li>
<li><a
href="00d3178d67"><code>00d3178</code></a>
Typo</li>
<li><a
href="3982a69d2b"><code>3982a69</code></a>
Bump version and set as pre-release</li>
<li><a
href="1339f55165"><code>1339f55</code></a>
Building for release</li>
<li><a
href="9459f01bc8"><code>9459f01</code></a>
Bumping the version</li>
<li>Additional commits viewable in <a
href="https://github.com/dagrejs/dagre/compare/v1.1.3...v1.1.8">compare
view</a></li>
</ul>
</details>
<br />
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
</details>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [@ai-sdk/mistral](https://github.com/vercel/ai) from 3.0.20 to
3.0.25.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="4c1613ac8f"><code>4c1613a</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/13131">#13131</a>)</li>
<li><a
href="64ac0fdd80"><code>64ac0fd</code></a>
Backport: fix(security): validate redirect targets in download functions
to p...</li>
<li><a
href="f622bf85c1"><code>f622bf8</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/13126">#13126</a>)</li>
<li><a
href="e2a59ef927"><code>e2a59ef</code></a>
Backport: fix(provider/google): preserve groundingMetadata when streamed
befo...</li>
<li><a
href="ebf43a6765"><code>ebf43a6</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/13120">#13120</a>)</li>
<li><a
href="2589004732"><code>2589004</code></a>
Backport: feat(openai): add GPT-5.4 model support (<a
href="https://redirect.github.com/vercel/ai/issues/13117">#13117</a>)</li>
<li><a
href="d23121fd71"><code>d23121f</code></a>
Backport: chore(ai): add optional ChatRequestOptions to
`addToolApprovalRespo...</li>
<li><a
href="55a2acf625"><code>55a2acf</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/13101">#13101</a>)</li>
<li><a
href="45d71c381b"><code>45d71c3</code></a>
Backport: fix(google): use VALIDATED function calling mode when any tool
has ...</li>
<li><a
href="ee92dc7664"><code>ee92dc7</code></a>
ci(release): add <code>--tag ai-v6</code> to <code>ci:release</code>
script (<a
href="https://redirect.github.com/vercel/ai/issues/13096">#13096</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/vercel/ai/compare/@ai-sdk/mistral@3.0.20...@ai-sdk/mistral@3.0.25">compare
view</a></li>
</ul>
</details>
<br />
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
</details>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
## Summary
- Fixes merge queue PRs blocking each other by changing the concurrency
group in `ci-merge-queue.yaml`
- The old concurrency group used `merge_group.base_ref` which resolves
to `refs/heads/main` for every PR, causing all merge queue entries to
serialize behind a single concurrency slot
- Now uses `github.ref` (unique per entry:
`refs/heads/gh-readonly-queue/main/pr-NUMBER-SHA`), matching what all
other CI workflows already do
## Recommended ruleset changes (in GitHub Settings > Rules > Rulesets >
"CI Status Checks")
- **Grouping strategy**: Switch `ALLGREEN` to `NONE` -- each PR is still
tested against the correct base (including all PRs ahead of it in the
queue), but failures only affect the failing PR instead of ejecting the
entire group. `max_entries_to_build: 5` still allows parallel
speculative testing.
- **`min_entries_to_merge_wait_minutes`**: Reduce from 5 to 1 -- the
5-minute wait adds unnecessary latency to every merge.
## Test plan
- [ ] Enqueue 2+ PRs in the merge queue and verify both trigger e2e
tests in parallel instead of one blocking the other
## Summary
- **Restores `convertViewFilterValueToString()` calls** that were lost
when the converter layer was removed in #18667. The GraphQL
`ViewFilter.value` is typed as `JSON` (can be a string, array, or
object), but the frontend type system expects a `string`. Without
stringification, SELECT/MULTI_SELECT filter values (e.g. `['LOST']`)
reach `arrayOfStringsOrVariablesSchema` as raw arrays, causing a
`ZodError: expected string, received array`.
- **Fixes applied at two data boundary points**: `splitViewWithRelated`
(primary entry from metadata store) and `mapViewFiltersToFilters` (which
also accepts `GqlViewFilter[]` directly).
Fixes a production regression introduced by #18667.
## Test plan
- [ ] Apply a SELECT filter (e.g. filter Opportunities by Stage =
"Lost") — should no longer throw ZodError
- [ ] Apply a MULTI_SELECT filter — should work correctly
- [ ] Verify filters with multiple selected values work (e.g. Stage is
"Lost" or "Won")
- [ ] Verify empty filters and "is not" operands still work
- [ ] Verify filters loaded from saved views still work after page
refresh
Made with [Cursor](https://cursor.com)
fix CSS selector > button not reaching Button component's inner element
### Reproduce
- when you click Data model and create a new field, you will see this
bug.
- When you import record and check the height of remove button in the
final validation step.
### Root Reason
the Button component internally renders a wrapper div around the
<button> element, so > button only reaches the intermediate div, not the
actual button.
### Fix
- padding-right not applied → chevron overlapped with text
- ValidationStep: height: 24px not applied → Remove button was 32px
instead of 24px
<img width="1288" height="376" alt="image"
src="https://github.com/user-attachments/assets/885cd8b0-1fe2-484a-8425-70f52b784ecb"
/>
<img width="1001" height="291" alt="image"
src="https://github.com/user-attachments/assets/0b489478-8fbc-4f7e-886a-38012eeb07ef"
/>
## Summary
- Migrates `LogicFunctionModule`, `CodeInterpreterModule`, and
`CaptchaModule` from the `forRootAsync` + injection token pattern to the
`DriverFactoryBase` lazy-loading pattern (matching `EmailModule` and
`FileStorageModule`)
- Fixes#18724 where `LOGIC_FUNCTION_TYPE` was not respected in worker
processes because the driver was created at module boot time before the
DB config cache was loaded
- Removes `isEnvOnly` from `LOGIC_FUNCTION_TYPE`,
`CODE_INTERPRETER_TYPE`, `CAPTCHA_DRIVER`, `IS_MULTIWORKSPACE_ENABLED`,
and `FRONTEND_URL` — these can now be safely configured via the database
at runtime
## How it works
Each migrated module now uses a `DriverFactory` (extending
`DriverFactoryBase`) instead of a module-level async factory + Symbol
injection token:
1. **Lazy creation**: `getCurrentDriver()` creates the driver on first
call, after `DatabaseConfigDriver.onModuleInit()` has loaded the DB
cache
2. **Auto-recreation**: If config changes in the DB, the next
`getCurrentDriver()` call detects the key mismatch and creates a new
driver instance
3. **Unified config**: Both server and worker read from the same
database — driver config only needs to be set once
### Files deleted (old pattern)
- `logic-function-module.factory.ts`,
`logic-function-drivers.module.ts`, `logic-function-driver.constants.ts`
- `code-interpreter-module.factory.ts`
- `captcha.module-factory.ts`, `captcha-driver.constants.ts`
### Files created (new pattern)
- `logic-function-driver.factory.ts`
- `code-interpreter-driver.factory.ts`
- `captcha-driver.factory.ts`
Net: **-150 lines**
## Test plan
- [x] `npx nx typecheck twenty-server` passes
- [x] `npx nx lint:diff-with-main twenty-server` passes
- [ ] Integration tests pass (`npx nx run
twenty-server:test:integration:with-db-reset`)
- [ ] Verify logic functions execute in workflow runs (the original bug)
- [ ] Verify code interpreter works in workflow code steps
- [ ] Verify captcha validation works on sign-up (when captcha is
configured)
Made with [Cursor](https://cursor.com)
- Fix duplication of tabs in dashboards that didn't open the duplicated
tab in the side panel: replaced the logic that used Math.round with an
algorithm that switches the positions of the elements. We will keep
relying on integers, but it will work well in all cases.
- Make dnd work with record page layouts, where there is a pinned tab
- Make tab movements work with pinned tab, too
- Allow user to set a tab as the pinned tab
- Make backend changes to be able to save tabs with `layoutMode`
https://github.com/user-attachments/assets/ce1130fa-71df-49ba-ba2f-6f971e15dd49
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Introduces a progress indicator for command menu items (both engine
commands and front components). When a command is running, the menu item
now displays a percentage alongside the loader spinner instead of just a
spinner.
- Added `commandMenuItemProgressFamilyState` to track per-item progress
and `CommandListItemLoader` to render it
- Exposed `updateProgress` in the twenty-sdk public API so front
components can report execution progress back to the host
- Wired progress reporting into `ExportMultipleRecordsCommand` as the
first consumer, showing CSV export progress
- Progress state is cleaned up on unmount for both engine commands and
headless front components
- Refactor
## Summary
- **Dynamic SSE headers**: The `graphql-sse` client was created with a
static `Authorization` header captured at creation time. When the access
token refreshed, the SSE client kept using the expired token on every
reconnection attempt, causing up to 10 wasted retries before the client
was disposed and recreated. Now `headers` is passed as a function that
reads the latest token from the Jotai store on each connection attempt.
- **Token renewal retry with error classification**:
`handleTokenRenewal` previously had zero retry tolerance — any failure
during `renewToken` (including transient network errors, server 500s, or
timeouts) triggered an immediate full logout via
`onUnauthenticatedError()`. Now the renewal retries up to 3 times with
linear backoff for transient errors. Only explicit server rejections
(`CombinedGraphQLErrors`, e.g. expired/revoked refresh token) skip
retries and proceed to logout immediately.
- **Preserved error types in `renewTokenMutation`**: The old code caught
all errors and re-threw them as a generic `new Error('Something went
wrong...')`, destroying the original error type. Callers couldn't
distinguish a GraphQL auth rejection from a network failure. Now errors
propagate with their original type.
- **Simplified SSE retry handler**: With dynamic headers handling token
freshness automatically, the retry handler no longer needs the
`initialTokenForSseClient` comparison to detect token mismatches. It now
only resets the SSE client when the user has logged out (no token) or
after 10+ consecutive failures.
## Test plan
- [ ] Log in, wait for access token to expire (~30 min or configure
shorter expiry), verify no unexpected logout occurs
- [ ] Simulate transient network failure during token renewal (e.g.
throttle network in devtools), verify the retry logic recovers without
logging out
- [ ] Verify SSE real-time updates continue working after a token
refresh
- [ ] Verify genuine logout still works when refresh token is actually
invalid/expired
- [ ] Open multiple browser tabs, verify token refresh works correctly
across all tabs without "suspicious activity" revocation
Made with [Cursor](https://cursor.com)
## Summary
- Move `BackfillNavigationMenuItemTypeCommand` from the 1-19 to the 1-20
upgrade path and split the DB transaction into two phases (data
backfill, then schema changes) to avoid the PostgreSQL error "cannot
ALTER TABLE because it has pending trigger events."
- Fix backfill logic to prefer `OBJECT` over `VIEW` for navigation menu
items that have `targetObjectMetadataId`, and correct already mis-typed
items. Tighten the `CHECK` constraint to enforce `viewId IS NULL` for
`OBJECT` type items.
- On the frontend, force `navigationMenuItems` into `staleEntityKeys`
when the server's `minimalMetadata` response omits the collection hash
(happens when the Redis cache hasn't been warmed after an upgrade),
ensuring the sidebar loads navigation items.
## Test plan
- [ ] Upgrade from 1.18 or 1.19 to 1.20 and verify the migration
completes without errors
- [ ] Verify navigation menu items of type `OBJECT` do not have a
`viewId` set in the database
- [ ] Sign out and sign in — confirm navigation menu items appear in the
sidebar on first load
- [ ] Verify `VIEW`-typed items also appear correctly in the sidebar
Made with [Cursor](https://cursor.com)