55ed4b7adb3cc90772ef46ea4d5c22d54ccb3d96
5063 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
55ed4b7adb |
feat(sdk): translate front-component strings with t()/Trans/useTranslate (#22301)
## What
Lets app **front components** localize the strings they render,
extending the
existing application-translation pipeline (which today only covers
manifest
labels) to component source. App authors mark strings with a small,
familiar
API; the build extracts and bakes them; the runtime resolves them for
the
user's locale.
```tsx
import { Trans, t, msg, useTranslate } from 'twenty-sdk/front-component';
<Trans>Loading postcard…</Trans>
<Trans context="card-title">Untitled</Trans> // disambiguation
const empty = t('No content yet…'); // works outside JSX
<p>{t('Saved {count} cards', { count })}</p> // interpolation
const STATUSES = [{ id: 'draft', label: msg('Draft') }]; // lazy descriptor
```
## How
- **Runtime** (`twenty-sdk/front-component`): `t()` (eager, usable
anywhere —
event handlers, helpers, module scope), `msg()` (lazy descriptor),
`<Trans>`
(reactive JSX), `useTranslate()` / `useLocale()`. Source-string
fallback,
`{name}` interpolation, and `context` disambiguation. No build-time
macro —
these are plain runtime functions.
- **Extraction**: a `ts-morph` scan collects `t()`/`msg()`/`<Trans>`
strings
from component source into the same `locales/*.json` catalogs the
manifest
pipeline already writes (`twenty dev:translations-extract`).
- **Delivery**: `twenty dev:build` bakes the compiled per-locale catalog
into
each front-component bundle via an esbuild banner, so the runtime
resolves
with **no server or renderer changes**. Locale comes from the execution
context that already flows to the worker.
The catalog key and `generateMessageId` hashing are shared between the
node
extractor and the browser runtime; `<Trans>` text whitespace is
normalized
identically on both sides so multi-line elements resolve.
## Design notes
- Reuses the existing `extract → compile → manifest.translations`
contract and
`generateMessageId`, so component strings flow through the same
machinery as
manifest labels.
- Self-contained in `twenty-sdk` + a shared pure helper; the server is
untouched.
## Scope / follow-ups
- `twenty dev` (watch) does not bake catalogs yet — preview shows source
strings; use `twenty dev:build` (documented). Wiring the watcher is a
follow-up.
- Usage is documented in twenty-docs under **Apps → Translations**
(`developers/extend/apps/translations`).
## Tests
Unit tests for the catalog-key/interpolation helpers, the runtime
resolver
(hit/miss/context/fallback/interpolation), and the ts-morph extractor
(static `t`/`msg`/`<Trans>`, dynamic-skip, dedup, multi-line
whitespace), plus a
compile test for context→messageId. Verified with an adversarial review
pass.
https://claude.ai/code/session_01NiE7o3cd3zCLZarVkJa6UA
---
_Generated by [Claude
Code](https://claude.ai/code/session_01NiE7o3cd3zCLZarVkJa6UA)_
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22301?utm_source=github"
rel="nofollow noreferrer noopener" target="_blank">``<img alt="Review
in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg">``</a>
---------
Co-authored-by: github-actions <github-actions@twenty.com>
|
||
|
|
ad31227f7c |
Fix BlockNote placeholder alignment + paddings (#22409)
## Before <img width="660" height="958" alt="image" src="https://github.com/user-attachments/assets/017e73c3-cd55-47e3-9470-3f6c31152663" /> ## After <img width="1347" height="995" alt="file-c55b9d7bc3bd212f18f8c77a0318eaef" src="https://github.com/user-attachments/assets/3f96af62-6509-4b58-a740-cd87856422cd" /> |
||
|
|
27dea0ed0b |
Add installed workspaces view to application registration (#22359)
## After <img width="895" height="344" alt="image" src="https://github.com/user-attachments/assets/33591753-f248-45ce-b32d-cc1112f50579" /> <img width="889" height="425" alt="image" src="https://github.com/user-attachments/assets/469ee228-9abb-486f-b2ec-9efb490bb2c8" /> <img width="766" height="343" alt="image" src="https://github.com/user-attachments/assets/2d88444a-6d98-4f97-8e5d-109197cfad27" /> ## Summary Add a new "Installed workspaces" section to the application registration settings page that displays all workspaces that have installed a given application, with pagination support. ## Key Changes - **Backend Service**: Added `getInstalledWorkspaces()` method to `ApplicationRegistrationService` that queries installed applications across workspaces with pagination support - **Backend DTO**: Created `ApplicationRegistrationInstalledWorkspacesDTO` and `InstalledWorkspaceDTO` to structure the response with workspace details (id, displayName, logo, version), total count, and hasMore flag - **GraphQL Resolver**: Added `findApplicationRegistrationInstalledWorkspaces` query resolver with pagination (page parameter, default page size of 10) and proper authorization guards - **Frontend Component**: Created `SettingsApplicationRegistrationInstalledWorkspaces` component that: - Displays installed workspaces in a table with workspace logo, name, and version - Shows initial 3 workspaces with "Show all" button to expand - Implements pagination with "Show more" button to load additional pages - Handles empty state (returns null if no workspaces installed) - **GraphQL Query**: Added `FindApplicationRegistrationInstalledWorkspaces` query document for frontend data fetching - **Integration**: Integrated the new component into `SettingsApplicationRegistrationGeneralTab` ## Implementation Details - Pagination uses offset-based approach with configurable page size (10 workspaces per page) - Query results are ordered by workspace displayName and id for consistent ordering - Soft-deleted applications and workspaces are excluded from the list and counts - Apollo Client's `fetchMore` with `updateQuery` merges paginated results into the cache - Component respects existing authorization (API_KEYS_AND_WEBHOOKS permission required) - Uses existing UI components (Table, Card, Avatar, Button) from twenty-ui library - Supports internationalization with Lingui ## Screenshots The new "Installed workspaces" section on the app registration General tab (admin app detail page), captured against a local instance with a demo app installed in 14 workspaces. The three PNGs are committed under `.github/assets/screenshots/installed-workspaces/` and render inline in the **Files changed** tab of this PR: - `1-first-3-show-all.png` — Collapsed: the first 3 installed workspaces (avatar + name + installed version) with a "Show all" button. - `2-expanded-show-more.png` — "Show all": the first page of 10 workspaces, with a "Show more" button (more remain). - `3-all-paginated.png` — "Show more": all 14 workspaces loaded, button gone. Review in cubic: https://cubic.dev/pr/twentyhq/twenty/pull/22359?utm_source=github https://claude.ai/code/session_012nWtviSBdfFeHEASTtwvJ7 |
||
|
|
4d96ec489b |
Add smooth page transitions to onboarding v2 (#22392)
## Before https://github.com/user-attachments/assets/d2fcd5ce-7e34-4f07-9a52-cac8acdc37cd ## After https://github.com/user-attachments/assets/5c245949-cff9-41b2-802d-3deeb562efa6 On a full-page load of a v2 onboarding URL (the post-signup workspace-subdomain redirect), Lingui's `I18nProvider` renders `null` until the locale chunk async-activates, so the app is blank for ~2s before the verify step appears. Steps also hard-cut and flashed a loader between each other. - Show a pulsing-logo loader until the locale activates (a gate above `I18nProvider`), scoped to onboarding v2 paths so every other page is unchanged. - Cross-fade between steps and preload their chunks on entry, so navigating never flashes the loader. Frontend-only; i18n loading itself is untouched. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22392?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. --> |
||
|
|
2e6077383b |
Add install your first apps onboarding V2 step (#22347)
https://github.com/user-attachments/assets/5326d48f-1842-4db1-bc7c-94852145c035 <img width="838" height="754" alt="CleanShot 2026-06-30 at 16 25 05@2x" src="https://github.com/user-attachments/assets/5c7d53d7-4d65-4e35-aed1-edf0c104e140" /> Adds an "Install your first apps" step to the V2 onboarding, shown right after import-contacts. It lets users opt into installing marketplace apps (Call recorder and People Data Labs for now) during onboarding. - New backend `OnboardingStatus.APPS_INSTALLATION` (between SYNC_EMAIL and PROFILE_CREATION); V1 auto-skips it. - The primary button sends the selected app ids to the server via `triggerInstallAppsOnboardingStep`, which enqueues a dedicated job that installs them asynchronously so onboarding isn't blocked. Skip continues without installing. - The workspace is credited per app on successful installation. Credits are env-driven via `ONBOARDING_INSTALL_APPS_CREDITS_REWARD_PER_APP`, shown as "Earn +N free credits (1 per tool)". <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22347?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. --> |
||
|
|
49a80c72d2 |
feat: show group-by context in record show breadcrumb (#22247)
Fixes [#837](https://github.com/twentyhq/core-team-issues/issues/837) On the record show page, extend breadcrumb pagination when the current view is grouped: (`rank/total in {viewName} -> {groupValue}`) Example: `Tasks / Schedule follow-up call (1/1,800 in By Status -> To do)` https://github.com/user-attachments/assets/7038d1f5-57e5-4e85-a3e6-09ac46c5b824 https://github.com/user-attachments/assets/88134fa4-e038-4520-a970-ce058a4444b0 <img width="1427" height="173" alt="Screenshot 2026-06-27 202807" src="https://github.com/user-attachments/assets/842d911e-b4bc-4443-afcd-4c67ed007ae0" /> <img width="1426" height="183" alt="Screenshot 2026-06-27 202851" src="https://github.com/user-attachments/assets/f8d9ee31-b1cb-46fa-9e7c-8876d4b099ff" /> <img width="1427" height="178" alt="Screenshot 2026-06-27 203423" src="https://github.com/user-attachments/assets/196ed694-0edb-4b52-934c-0938d3fd4da2" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22247?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. --> --------- Signed-off-by: Parship Chowdhury <parshipchowdhury@gmail.com> Co-authored-by: bosiraphael <raphael.bosi@gmail.com> |
||
|
|
52d3735147 |
[REQUIRED FOR 2.18 RELEASE] Show the upgrade-plan step at the end of V1 onboarding (#22368)
## What On V1 onboarding the upgrade-plan step (`ChooseYourPlan`) only appeared later, once the user happened to create a record, instead of right after Invite team. ## Why The frontend advances the onboarding status optimistically in `getNextOnboardingStatus()` without refetching, and it never emitted `PLAN_REQUIRED`. So after Invite team the user was locally marked `COMPLETED` and dropped into the app; the backend's real `PLAN_REQUIRED` only surfaced on a later `GetCurrentUser` refetch. ## Fix Make `getNextOnboardingStatus()` billing-aware so it mirrors the backend: return `PLAN_REQUIRED` in the terminal branches when `isBillingEnabled && billingSubscriptions.length === 0` (using `billingSubscriptions` to match the backend's any-subscription check). The navigate hook already routes `PLAN_REQUIRED` to `/plan-required`, so no routing change is needed. Self-hosted and existing-subscription flows are unchanged. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22368?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: prastoin <paul@twenty.com> |
||
|
|
36e89c04ad |
Front fallback flat object search field metadata (#22369)
## Fix: crash on Settings → Object → Search after changing label identifier ### Problem Opening the Search section (or changing an object's label identifier) threw `t.searchFieldMetadatas is not iterable` in `SettingsObjectSearchSection`. ### Root cause `EnrichedObjectMetadataItem.searchFieldMetadatas` is typed as a non-optional array, but at runtime it can be `undefined`. `useLoadMinimalMetadata` stores the minimal objects with `objectMetadataItems as unknown as FlatObjectMetadataItem[]`. The minimal query doesn't select `searchFieldMetadataList`, so the double-cast hides that the property is missing. Until the full metadata reload lands, the object has no `searchFieldMetadatas`, and `objectMetadataItemsWithFieldsSelector` spreads that `undefined` straight through to the component, which spreads it (`[...searchFieldMetadatas]`) and crashes. (`fields`/`indexMetadatas` never hit this because they come from `Map.get()`, which is honestly typed as `| undefined` and already falls back to `[]`.) ### Fix Guarantee the array contract in `objectMetadataItemsWithFieldsSelector`, matching how `fields`/`indexMetadatas` are already defaulted: `searchFieldMetadatas: flatObject.searchFieldMetadatas ?? []`. ### Tradeoff considered The "clean" alternative is promoting `searchFieldMetadatas` to its own metadata-store entity (like `indexMetadataItems`), which would make the `?? []` type-mandated via `Map.get`. Rejected for now: it's a medium cross-package refactor (new store key, type, selectors, split/reload wiring, plus a server-side collection hash for staleness) for an entity that is never independently mutated — it only changes as a side effect of label-identifier/field updates, so independent caching buys nothing. The selector default fixes the crash with minimal surface area; the deeper cleanup (making the `as unknown as` cast honest, or splitting the store) can be deferred until search-field metadata becomes directly editable. |
||
|
|
9d361c8bb0 |
[FIX_TYPECHECK_ON_MAIN] Add missing inviteTeamMaxCreditsReward to OnboardingConfig type (#22370)
## Context The `twenty-front` typecheck is broken on `main`: ``` src/modules/onboarding/hooks/useInviteTeam.ts:154:27 - error TS2551: Property 'inviteTeamMaxCreditsReward' does not exist on type 'OnboardingConfig'. ``` This is a merge race: one PR started consuming `onboardingConfig.inviteTeamMaxCreditsReward` in `useInviteTeam.ts`, while the frontend `OnboardingConfig` type only declared `inviteTeamCreditsRewardPerUser`. The backend already returns both fields (`client-config.entity.ts` declares `inviteTeamMaxCreditsReward` and the service populates it), so this is purely a missing frontend type field. ## Changes - Add `inviteTeamMaxCreditsReward: number` to the frontend `OnboardingConfig` type. - Add the field to the config mock so `mock-data/config.ts` satisfies the type. ## Test `npx nx typecheck twenty-front` passes. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22370?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. --> |
||
|
|
5a5c829129 |
fix(page-layout): render relation field widgets in table display mode (#22220)
Adding a to-many relation field as a **Table** on a record page rendered an empty widget (header only) in several cases. This fixes three independent defects behind that. - **Morph inverse relations crashed the table.** The host-scoping view filter (`IS current record`) is built on the relation's inverse field. When that inverse is a `MORPH_RELATION` (attachments, notes, tasks…), `getFilterTypeFromFieldType` fell through to `TEXT` and the GraphQL builder threw `Unknown operand IS for TEXT filter`, unmounting the table via the ErrorBoundary. `MORPH_RELATION` now classifies as `RELATION`, and the relation filter resolves the correct morph join column (e.g. `targetPersonId`) from the current record's object type. - **Stale `viewId` on field change.** Changing the bound field on a Table widget kept the previous relation's draft view (wrong object/fields/filter). Field selection now regenerates the draft view for the new relation, or clears the stale `viewId` when the new field can't back a table. - **Label identifier could be hidden or reordered.** Relation-table widget views now pin the label-identifier field first and visible on view creation and save. Deferred: morph relation filters with arbitrary selected record ids (not just "current record") — needs target-object identity in the filter value schema. **Test:** open a Person → edit layout → add a Field widget → bind a to-many relation → switch Layout to Table. Previously empty for `attachments` (morph) and for any field changed on an existing Table widget; now scoped to the host record. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22220?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. --> |
||
|
|
aea6c3832a |
Credit workspaces for onboarding invite-team signups (#22309)
https://github.com/user-attachments/assets/6591cbb0-2b60-4f25-8b03-26b0da73f0d8 After the invite has been accepted: <img width="1606" height="286" alt="CleanShot 2026-06-30 at 11 24 47@2x" src="https://github.com/user-attachments/assets/7becf8a5-04dc-4512-ac7f-951a77e4c0ac" /> Adds a dedicated `ONBOARDING_INVITATION_TOKEN` app-token type so invitations sent during the onboarding invite-team step are distinguished from regular invites. When an invited person actually signs up, the inviting workspace is credited 0.5 credits. Reward eligibility is derived entirely server-side, with no public API parameter: an invitation is reward-eligible only while the workspace is in the onboarding invite-team step (`ONBOARDING_INVITE_TEAM_PENDING`), a flag set once at workspace creation that no public mutation can re-arm. Both token types stay valid invitations everywhere via a shared `INVITATION_APP_TOKEN_TYPES`, so invitees still join normally and appear in invite lists. Crediting is a best-effort direct call to `BillingCreditService.creditWorkspaceBalance` from the sign-in-up flow: it no-ops when billing is disabled and never blocks signup, and is bounded by a 10-invite-per-workspace cap. No DB migration needed: `appToken.type` is a text column. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22309?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. --> |
||
|
|
96e5d0f3ed |
Track total onboarding free credits in an atom (#22348)
The v2 onboarding header shows a "free credits" counter, but every page
fed it a hard-coded `0`, so it never reflected the credits the workspace
would actually receive. This tracks the running total based on the
choices made at each step.
- New `onboardingFreeCreditsState` atom (`{ importContacts, inviteTeam
}`, localStorage-backed) + `useOnboardingFreeCreditsTotal` to sum it
into the header.
- Connecting email sets the import-contacts reward (persisted so it
survives the OAuth redirect); inviting teammates sets `min(count ×
perUser, max)` on submit. Skipping a step contributes 0; the atom resets
at onboarding start.
- Counter scope is import-contacts + invite-team rewards only
(display-credit units already exposed via `onboardingConfigState`).
Plan/trial credits are out of scope.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22348?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. -->
|
||
|
|
f08b87c478 |
Fix v2 onboarding dropping to v1 after connecting email (#22351)
Connecting an email during v2 onboarding triggers a full-page OAuth round-trip that returns to `/` with no query param. `isOnboardingV2State` was an in-memory atom, so it reset to `false` on return and the navigation hook routed the user into the v1 onboarding (same break on a plain refresh). Fix: back the atom with `sessionStorage`. It survives the same-tab OAuth redirect and refresh, hydrates synchronously (`getOnInit`), and is auto-cleared by the existing `sessionStorage.clear()` on sign-out. The `onboardingV2=true` URL-param plumbing stays, since it carries the flag across the cross-subdomain signup hop. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22351?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. --> |
||
|
|
9f3ebaaf22 |
feat(messaging): sync draft emails and edit them in the thread composer (#22178)
Stop excluding drafts from sync across all three providers (Gmail DRAFT label, Microsoft/IMAP Drafts folder) and add an isDraft boolean field on Message so drafts are queryable by the API and AI agents. Drafts render in the thread with a Draft tag; clicking one opens the existing reply composer pre-filled with the draft's recipients, subject and body, and Send reuses the existing send-email flow. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22178?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: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
b0d7516951 |
Deprecate asExpression from field metadata search_vector (#22287)
## Summary Fully deprecates the cached `asExpression` / `generatedType` settings on `TS_VECTOR` (searchVector) fields. Previously the generated-column expression was stored in `FieldMetadataSettings` and kept in sync via imperative recompute side-effects. It is now **derived at DDL time** from the `searchFieldMetadata` rows that describe which fields feed the search vector, making `searchFieldMetadata` the single source of truth and removing a whole class of cache-drift bugs. This is delivered across the milestones tracked in #2587 and coordinates with the frontend migration (#1428). ## Why - The searchVector expression lived in two places (stored `settings.asExpression` + the actual generated column), kept consistent by bespoke side-effects (`recompute-search-vector-on-field-rename`, label-identifier recompute, etc.). - The frontend reconstructed the searchable-fields list by **regex-parsing** the stored `asExpression`. - Both are brittle. Deriving the expression from `searchFieldMetadata` rows at build/run time removes the cache and the parsing. ## What changed ### Server - data model & derivation - Introduce the `tsVectorFieldMetadata` relation on `searchFieldMetadata` (`tsVectorFieldMetadataId` / universal identifier) linking each searchable-field row to its target `TS_VECTOR` field. - New runtime derivation `deriveSearchVectorAsExpressionForTsVectorField` (`flat-search-field-metadata/utils/...`) used by the create-object and update-field handlers to generate the column expression from `searchFieldMetadata` rows. - Remove `asExpression` / `generatedType` from stored settings: `FieldMetadataSettings.TS_VECTOR` is now `null`; the column builder (`generate-column-definitions.util.ts`) hardcodes `generatedType: 'STORED'` and requires the derived expression. - Delete the imperative recompute side-effects and the `compute-search-vector-universal-settings-from-object-manifest` path; drop the `settings` block from all 28 standard `compute-*-standard-flat-field-metadata` utils. ### Server - migration runner - New `rebuildSearchVector` marker on `update-field` actions: the orchestrator synthesizes targeted column rebuilds (`compute-search-vector-rebuild-target-universal-identifiers.util.ts` + the deprioritize aggregator) only when a searchFieldMetadata change or indexed-field rename actually requires it - instead of rebuilding on every settings touch. - Deferrable FKs + in-flight ID resolution so a `searchFieldMetadata` row and its `TS_VECTOR` field can be created in the same transaction (deterministic UUIDs). ### Frontend (contract change, #1428) - New `SearchFieldMetadataDTO` + dataloader exposing `searchFieldMetadataList` on object metadata. - `SettingsObjectSearchSection` now reads `objectMetadataItem.searchFieldMetadatas` instead of parsing `asExpression`; new `SearchFieldMetadataItem` type, fragment, and mapping updates. ### Upgrade commands (2.18) - `2-18-instance-command-fast-...-add-ts-vector-field-metadata-id-to-search-field-metadata` - `2-18-instance-command-fast-...-make-search-field-metadata-fks-deferrable` - `2-18-instance-command-slow-...-backfill-ts-vector-field-metadata-id-on-search-field-metadata` (These were relocated from 2.16 to 2.18 and re-timestamped into an ordered block - add column -> make FK deferrable -> backfill data - since 2.16/2.17 are released.) ### Tests - Updated search-vector side-effect integration specs to assert behavior (search works) rather than the now-removed `asExpression`; removed the obsolete expression-validation specs; refreshed the application-sync snapshot (`universalSettings: null`). ## Upgrade / compatibility notes - Existing workspaces keep their stored `settings` until a later cleanup; nothing reads it anymore. The new derivation drives all DDL going forward. - Schema changes are gated behind the 2.18 instance commands above. ## Known follow-up (separate PR) https://github.com/twentyhq/core-team-issues/issues/2620 - The column rebuild (`DROP`/`ADD` of the `searchVector` STORED column) cascade-drops its GIN index and does not recreate it - a pre-existing regression on `main` inherited here. A follow-up PR will fix the rebuild handler to recreate the GIN index and add a 2.18 workspace command to recompute every search vector + strip the deprecated settings. (Planned.) ## Test plan - [ ] `npx nx typecheck twenty-server` / `twenty-front` - [ ] `npx nx lint:diff-with-main twenty-server` / `twenty-front` - [ ] Server integration: create/update/delete field, rename indexed field, update object - search returns expected records - [ ] Run the 2.18 instance commands on a seeded DB; verify `tsVectorFieldMetadataId` backfilled and FKs deferrable - [ ] Frontend: object Search settings tab lists the correct searchable fields (no `asExpression` parsing) close https://github.com/twentyhq/core-team-issues/issues/2587 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22287?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. --> |
||
|
|
1055764fff |
fix: reconcile metadata store after object creation so activity targets on new custom objects link correctly (#22331)
## Summary Creating a Task or Note on a record of a newly-created custom object failed to link them, and the console showed `Missing field 'targetTest…' while writing result` for `TaskTarget`/`NoteTarget`. After creating a custom object, the front-end metadata store was left with an inconsistent morph relation group on the default-relation objects (`taskTarget`, `noteTarget`, `attachment`, `timelineActivity`). This reconciles the store from the server after creation so the morph fields are rebuilt correctly. ## Context / root cause The DB and server are correct: the new object adds a single member (e.g. `targetTest`) to the existing `target` morph group (shared `morphId`), and the server's `objects` query collapses + renames the group to one `target` field with a full `morphRelations` array. On the client, though, the metadata store is updated incrementally after creation: - The bulk `objects` query stores morph fields already collapsed (`target` + `morphRelations`). - The new reciprocal morph member arrives via SSE/mutation as a raw, un-collapsed field row (`targetTest`, without `morphRelations`), which `objectMetadataItemsWithFieldsSelector` simply joins in. This leaves two morph fields on `taskTarget`/`noteTarget` (`target` with stale members + an un-normalized `targetTest`). `mapFieldMetadataToGraphQLQuery` then fans `targetTest` out into non-existent fields (`targetTestCompany`, `targetTestPerson`, …), which the server omits, breaking the optimistic cache write (`writeFragment`) and leaving the activity target unlinked in the UI. This is a regression from the metadata-store incremental-sync refactor (the create path stopped reconciling reciprocal morph fields on existing objects). ## Fix In `useCreateOneObjectMetadataItem`, after the incremental store updates, call `invalidateMetadataStore()` so the objects/field metadata is refetched from the server and the morph groups are rebuilt in their correct collapsed form. This mirrors the existing pattern in `useDeleteOneObjectMetadataItem`. ## Test plan - [ ] Create a new custom object. - [ ] Open a record of that object and create a Task and a Note from it. - [ ] Verify no `Missing field 'target…'` error in the console and the task/note is linked (visible in the record's Tasks/Notes and on the activity target). - [ ] Confirm existing standard objects (Company/Person/Opportunity) still link tasks/notes correctly. - [ ] Confirm object creation still updates the left nav / views as before. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22331?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. --> |
||
|
|
87fa0e4c12 |
fix(front): mark stale AnimatePresence exit page inert so it can't intercept clicks (#22328)
## Context
When navigating between the app and settings sections,
MainAppLayoutOutlet keeps the outgoing page mounted during the
AnimatePresence exit transition, and can leave a stale exit node behind
the active page, notably when the page hosts an app front component
whose Web Worker teardown blocks React from removing it.
The leftover page is invisible (opacity 0) but still captures pointer
events on top of the active route, so e.g. front-component buttons stay
clickable through the settings screen.
The existing `exit={{ pointerEvents: 'none' }}` mitigation is defeated
by descendants that set pointer-events explicitly (the front-component
container uses pointer-events: auto). Tag each transition page with its
route section and mark every non-active one `inert`, which descendants
cannot override, so any stale page is fully non-interactive.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22328?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. -->
|
||
|
|
fb800e7a2d |
fix: improve native print output for dashboards and record tables (#22272)
## Summary - Adds print-specific snapshots for dashboards so native browser print/PDF captures charts and front components instead of the app shell. - Adds record-table print snapshots for object index pages so printable tables are generated from visible rows and native print avoids virtualized blank pages. - Preserves rendered chart layers by rasterizing canvas/SVG content for print. ## AI-generated disclosure This pull request was AI-generated by Hermes Agent on behalf of Vittorio Alfieri. The changes were reviewed and tested locally before submission. ## Screenshots ### Dashboard print **Before**  **After**  ### Table records print **Before**  **After**  ## Test plan - [x] `yarn nx typecheck twenty-front` - [x] `yarn nx build twenty-front` - [x] Generated dashboard PDFs from the preview build and rasterized pages to PNG for visual verification. - [x] Generated Tasks/table-record PDFs from the preview build and verified the final page contains table content instead of blank pages. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22272?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: Félix Malfait <felix@twenty.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
facdbb5ba8 |
v2 onboarding: dedicated verify step and upgrade-free-trial as the last step (#22303)
https://github.com/user-attachments/assets/b1ee4f77-c6d7-4638-b9f1-dd801d1cc0db Completes the onboarding-v2 flow: a dedicated verify step, the reordering that makes the plan step come last, and the upgrade-free-trial page itself. ## Verify step (`/verify-v2`) After the cross-domain token exchange, v2 sign-ups land on a clean `BlankLayout` "Verifying your email" screen (fading Twenty logo) instead of the v1 `AuthModal` flashing over the background mock. The redirect target is chosen from `isOnboardingV2` (read from the Jotai store at redirect time). The pulsing logo is extracted into a shared `OnboardingPulsingLogo`, reused by the workspace-activation loader. `/verify-v2` joins the same exempt lists as `/verify` (ongoing-creation guard, metadata gater, apollo unauthenticated handler, captcha, page title) — intentionally not `useShowAuthModal`, which is what drops the modal. ## Plan step is now last `getOnboardingStatus` checks `PLAN_REQUIRED` after invite-team instead of first, so onboarding runs workspace activation → email → profile → invite → plan. This is what lets the upgrade step be reached as the final step instead of gating right after sign-up. Applies to both v1 and v2 (same order). ## Upgrade free trial page (`PlanRequiredV2` → `ChooseYourPlanV2` / `UpgradeFreeTrial`) The final step, full-screen under `BlankLayout` via `OnboardingV2Layout`, matching the Figma (billing card with the Stripe form, the "Basic / without credit card" option, trial + credits pills). Reuses the v1 `ChooseYourPlanContent` billing logic (`SubscriptionPaymentForm`, `useHandleCheckoutSession`). The "+N free credits" reward comes from `clientConfig.onboarding.upgradeCreditsReward` (sourced from `BILLING_FREE_WORKFLOW_CREDITS_FOR_TRIAL_PERIOD_WITH_CREDIT_CARD`). ## Also Fixes a latent staleness in the Apollo `onUnauthenticatedError` handler — it captured `location` from the memoized client, now read via a ref — so auth-path exemptions are correct after navigation. Note: the onboarding step order change affects v1 too (plan becomes its last step as well). |
||
|
|
2bb7fb2e9b |
Polish settings page titles and admin tables (#22305)
## Summary - Reuse the shared settings title presentation for read-only and editable settings page titles. - Polish settings AI/app icons, breadcrumb cropping, and admin detail title icons. - Align admin panel table/card typography and spacing with existing settings tables. ## Before/After <img width="1524" height="2214" alt="Settings pages before and after" src="https://github.com/user-attachments/assets/91037b25-b442-4eb2-b244-1d8280ce2cd9" /> <img width="2200" height="3268" alt="Additional settings UI before and after" src="https://github.com/user-attachments/assets/f10d7283-7031-4958-8539-649c3067cd31" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22305?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. --> |
||
|
|
1b2f2c71c3 |
Move filter group destruction after filter operations (#22248)
### The current order is: 1. Create filter groups 2. Update filter groups 3. Destroy filter groups ← **happens here** 4. Clean up store (cascade) 5. Create/update/delete filters (which may reference groups just destroyed) The fix is to move filter group destruction after filter operations, so filters that reference those groups get created/updated/deleted first. ### after fix : The persistence order is now: 1. Create filter groups 2. Update filter groups 3. Create/update/delete view filters (these can safely reference groups that still exist) 4. Destroy filter groups (only after all filter mutations are done) 5. Clean up store (cascade-deleted filters **root cause :** step 4 happened before step 3, so filter creates/updates would reference groups that had already been deleted in the same save cycle ; causing the backend to fail with "Migration execution failed" when it couldn't resolve the `viewFilterGroupId `foreign key. this fixes the bug : #21351 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22248?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: Souheyl Gouadria <souheyl.gouadria@medius.com> Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
d1854bc7e9 |
Add marketplace catalog synchronization to admin panel (#22260)
## After <img width="1476" height="658" alt="image" src="https://github.com/user-attachments/assets/25a042dd-eabf-4b71-9872-d3b627104634" /> ## Summary This PR adds the ability for admins to manually synchronize the marketplace application catalog from the admin panel. It introduces a new mutation endpoint and UI controls to trigger catalog synchronization with user feedback via snackbar notifications. ## Key Changes - **Frontend (SettingsAdminApps component)**: - Added `useSnackBar` hook for user feedback on sync success/failure - Imported `useMutation` from Apollo Client to handle the sync operation - Added `IconRefresh` and `Button` imports for the sync UI control - Created `handleSyncCatalog` function that triggers the mutation, refetches app registrations, and displays appropriate snackbar messages - Added a new "General" section with a "Synchronize catalog" button above the existing app registrations table - Button shows loading state and is disabled while sync is in progress - **Backend (AdminPanelResolver)**: - Added `syncMarketplaceCatalog` mutation that queues a `MarketplaceCatalogSyncCronJob` via the message queue - Uses `@InjectMessageQueue` decorator to inject the cron queue service - Includes job deduplication via `id: 'marketplace-catalog-sync'` to prevent multiple pending sync jobs - Protected with `@UseGuards(AdminPanelGuard)` for admin-only access - **GraphQL Schema**: - Added `SyncMarketplaceCatalog` mutation type definition - Generated corresponding TypeScript types and mutation document - **New Files**: - Created `syncMarketplaceCatalog.ts` GraphQL mutation document <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22260?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. --> |
||
|
|
a215d3cbdf |
fix: prevent crash when leaving dashboard edit mode with side panel open (#22292)
## Problem
When creating or editing a dashboard and opening a page layout side
panel (clicking the **"Add widget"** box, or editing a widget via grid
edition), clicking another nav item like **"Opportunities"** crashes the
whole app ("Sorry, something went wrong").
### Steps to reproduce
1. Go to `/objects/dashboards` and click **"+ Add new"** to create a
dashboard.
2. Click the **"Add widget"** box (or add a widget via grid edition and
open its settings).
3. Click another nav item such as **"Opportunities"**.
4. The app crashes.
## Root cause
The side panel stays mounted during its close animation, but the main
context store has already switched to the new page (the record index has
no single targeted record). The still-mounted page layout side panel
page (`SidePanelPageLayoutDashboardWidgetTypeSelect`, chart settings,
etc.) re-renders and calls `usePageLayoutIdFromContextStore`, which
throws `Error: Only one record should be selected`. With no local error
boundary, the throw propagates to the top-level boundary and crashes the
app.
## Fix
`SidePanelRouter` now skips rendering page layout side panel pages
whenever the main context store has no single targeted record — the same
condition `usePageLayoutIdFromContextStore` requires to not throw.
During navigation the panel closes cleanly instead of crashing.
This is safe because `usePageLayoutIdFromContextStore` unconditionally
throws without a single-record selection, so the guard can only skip
pages that would otherwise crash — it cannot break a currently-working
flow. The guard uses the existing `isPageLayoutSidePanelPage` helper, so
it covers all page layout side panel pages (widget type select, chart /
iframe / record table settings, record page field settings, etc.).
## Testing
- Reproduced the crash in the running app, applied the fix, and
confirmed navigating to Opportunities from both the **widget type
select** and the **chart settings** panels now lands on the
Opportunities list with no console or page errors.
- `oxlint --type-aware` and `nx typecheck twenty-front` both pass.
|
||
|
|
db7d8172f7 |
Add v2 onboarding invite team page (#22229)
<img width="3024" height="1500" alt="CleanShot 2026-06-26 at 18 09 47@2x" src="https://github.com/user-attachments/assets/e91f30a5-2763-42a0-9abf-d9fa8400870c" /> Adds the v2 onboarding **Invite team** page (`INVITE_TEAM`), shown right after the create-profile step for the onboarding-v2 cohort. It renders full-screen under `BlankLayout` via the shared `OnboardingV2Layout`, matching the Figma (340px column, email inputs with inline remove, dark Invite, Skip). Reuses all v1 invite-team logic via a new `useInviteTeam` hook (v1 `InviteTeam` now consumes it too; its UI is unchanged). Routing mirrors `SyncEmailsV2`/`CreateProfileV2`: new `AppPath.InviteTeamV2`, lazy route, and an `isOnboardingV2`-gated branch in `usePageChangeEffectNavigateLocation` (+ tests and a Storybook story). No backend changes. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22229?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. --> |
||
|
|
13f80f0d95 |
fix: ignore IME composition Enter in chat-thread and attachment rename inputs (#22270)
## What's this PR doing? Two inline rename inputs run their action on `Enter` without ignoring the `Enter` that confirms an IME composition: - `AiChatThreadListItem` (renaming an AI chat thread). It also calls `preventDefault()`, so the composition-commit `Enter` is swallowed and the half-typed title gets saved. - `AttachmentRow` (renaming an attachment). The same `Enter` saves the unfinished name. When you type with an IME (Japanese, Chinese, Korean), the first `Enter` after typing confirms the candidate text rather than submitting, so these handlers fire with text the user hasn't finished entering. ## Why The codebase already guards this where keyboard handling goes through `useHotkeysOnFocusedElement` (`if (keyboardEvent.isComposing || keyboardEvent.keyCode === 229) return`), and the inline inputs that don't use that hook add the same check themselves: see `SettingsAccountsBlocklistInput`, `SettingsDevelopersApiKeysNew`, and the sign-up workspace forms. These two rename inputs were just missing it. ## How Add the same `isComposing || keyCode === 229` guard before the `Enter` branch. For input without an IME, `isComposing` is `false` and `keyCode` is `13`, so the rename-on-Enter behavior stays the same. This only skips the action on the composition-commit key. I checked the change against the repo's Prettier config locally. I couldn't add a unit test because jsdom doesn't dispatch real composition events (`isComposing` stays `false`), so it can't reproduce the keystroke. Happy to add an e2e test if that's preferred. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22270?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. --> |
||
|
|
d81b3c3fa3 |
feat(twenty-sdk): extract & compile app translations into the manifest (#22236)
## Summary **PR 2/4** of the app-metadata-translations stack. Gives app developers the authoring side, as part of the normal manifest build — and it stays out of the way of developers who don't translate. - `twenty-sdk` CLI i18n pipeline: collect translatable strings from the manifest, generate value-as-key message ids (`sha256(value)` truncated, byte-identical to the server's `generateMessageId`), a `dev i18n-extract` command to scaffold per-locale catalog files, and a compile step folded into `build` that emits `manifest.translations`. - Opt-in: no `locales/` dir → `compileApplicationTranslations` returns `undefined` → manifest is unchanged. - Adds an optional `locale` to the front-component execution context so components can translate against the host locale. ## Stack Stacks on #22235 (PR 1/4). Base branch: `claude/app-translation-1-runtime-resolution`. ## Tests Unit (vitest): extract/compile round-trip + message-id determinism. ## Verification note `yarn install` could not complete in the remote dev environment, so typecheck/lint/tests were not run locally — **CI is the source of truth**. https://claude.ai/code/session_01NiE7o3cd3zCLZarVkJa6UA --- _Generated by [Claude Code](https://claude.ai/code/session_01NiE7o3cd3zCLZarVkJa6UA)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22236?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. --> |
||
|
|
538b180824 |
feat(dpa): self-serve Data Processing Agreement generator (#22243)
## What
A single, region-aware DPA that serves all customers, generated
automatically from the customer's deployment. Two layers:
1. **Click-through DPA** — recorded at signup (acceptance = execution),
resolving merge fields from the deployment region. Cloud only.
2. **In-app signed-PDF generator** — Settings → Legal → Generate DPA:
preview the agreement, enter legal entity + authorized signatory,
download a PDF pre-signed by Twenty, and store the executed copy against
the workspace with its template version + timestamp. Deep-linkable at
`/dpa` (login-gated) for `twenty.com/dpa`.
## How it resolves
A typed variable matrix (`dpa-region-config.constant.ts`) maps the
deployment region to the contracting Processor entity and terms:
- **EU (default)** → Twenty.com SAS, hosting EU/Frankfurt, governing law
France, SCC section dormant.
- **US (custom)** → Twenty, Inc., hosting US, SCC section active.
Region is a deployment-wide setting (`DPA_DEPLOYMENT_REGION`, default
EU) behind a `DpaRegionService` seam so it can later become
per-workspace without touching callers. The legal text is verbatim from
the template (generated into `dpa-template.constant.ts` directly from
the source `.docx`); only the 6 merge fields are filled and the SCC
sections (7.2–7.5) stay in the document for every region per the spec —
only field values branch. Sub-processors are deferred to
trust.twenty.com (not enumerated). Billing stays decoupled (Twenty, Inc.
remains merchant of record regardless of Processor).
## UI
Standard list + create-page pattern (mirrors API keys / webhooks): a
list of executed copies (with re-download) — or the agreement preview
when none exists — and a top-right blue **Generate DPA** CTA opening a
standard create page. The "Legal" item is intentionally **not** in the
settings menu; the page is reached via the `/dpa` deep link.
## Notable implementation details
- **PDF** is rendered server-side with `@react-pdf/renderer`. The
built-in standard-14 fonts only encode ASCII and crash on the template's
curly quotes / em–en dashes / accented Latin, so Liberation Sans (OFL)
is **subset to a Latin glyph set and embedded as base64 data: URLs** —
no font files to ship or resolve at runtime (works in dev, prod-Docker
and CI).
- New `core.dpaAgreement` table via a fast instance command (FK hash
reproduced to match TypeORM).
- Self-hosted deployments (billing disabled) skip click-through
recording and stamp a prominent "not a valid agreement" banner on the
preview and PDF.
## Tests
- Unit: resolver (per-region entity/law/SCC state, EU default, no
unresolved `{{ }}`, SCC sections present in both regions, self-hosted
notice) and HTML renderer.
- Integration (`test/integration/graphql/suites/dpa`): preview has no
unresolved fields; `generateSignedDpa` renders + persists + returns a
downloadable PDF (asserted with accented input to guard the font
regression); list re-download.
## ⚠ Needs legal input before go-live (marked `TODO_CONFIRM` in
`dpa-region-config.constant.ts`)
- Registered-office addresses for Twenty.com SAS and Twenty, Inc.
- US deployment governing law (the template only specifies France).
- DPO name and the Twenty pre-signed authorized signatory name/title.
## Out of scope (flagged per spec)
Intra-group legal agreement and any Stripe/billing-entity changes. A
future e-sign provider would plug in at `DpaService.generateSignedDpa` +
the signatory input.
> Draft until the integration test passes in CI and the legal
`TODO_CONFIRM` values are supplied.
https://claude.ai/code/session_01Ahjydxx6J1souz1s1NeA9a
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Ahjydxx6J1souz1s1NeA9a)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22243?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. -->
|
||
|
|
2a21eb46c0 |
perf: cap to-many relation records per parent and inline chips in table (#22206)
## Problem Record table views that show a to-many relation column (e.g. Workflows with a "Runs" column) get slow and janky to scroll when some records have many related records. Two root causes, found by profiling the page live: 1. **Backend over-fetch + unfairness.** Nested one-to-many relations were loaded with a single flat limit of `QUERY_MAX_RECORDS_FROM_RELATION * parentCount` shared across *all* parents in the page (`WHERE parentColumn IN (ids) LIMIT 60*N`, no per-parent cap). A single hot parent can consume the entire budget — returning thousands of rows for one cell, and potentially starving sibling parents of records they actually have. The `limit * parentCount` shape shows the original intent *was* a per-parent budget; it was just implemented as a global limit. 2. **Frontend DOM explosion.** `ExpandableList` mounts the *entire* child array inline (clipped with `overflow: hidden`) when unfocused, and mounts all children for measurement when focused. A cell with 2,000+ relation chips mounts ~14k DOM nodes — one observed page reached ~55k nodes for 43 rows, producing 100–300 ms main-thread long tasks on every scroll. ## Fix - **Backend:** load one-to-many relations with a true **per-parent** cap via a `LATERAL` join — each parent runs its own indexed, `LIMIT`-ed scan that stops after the per-parent budget. This is `O(perParentLimit × parentCount)` and never reads or sorts a parent's full relation set. The per-parent query is built through the workspace query builder (so it stays schema-qualified and keeps the soft-delete predicate) and wrapped as a `FROM` subquery; read/row-level permissions are enforced when records are hydrated by id, as elsewhere in the relation loader. Many-to-one is unchanged. - **Frontend:** add an opt-in `maxInlineCount` to `ExpandableList` so to-many relation cells mount only a small inline preview; the expand dropdown still renders the full fetched set. Fully backward compatible (no cap → identical behavior). ## Why LATERAL over a window function A windowed `ROW_NUMBER() OVER (PARTITION BY parent) <= limit` is correct and fair too, but a window function **cannot stop early within a partition** — it must read every matching row (and sort it). Measured on skewed data (one parent with ~4k children, on the existing single-column join index, PG16): | Approach | Time | Buffers | Rows read from the hot partition | |---|---|---|---| | Pre-PR (`LIMIT 60×N`) | 1.6 ms | 91 | ~180 total, early-stops, but **unfair** (starves siblings) | | Window (`ROW_NUMBER`) | 3.7 ms | 128 | **all ~4k + sort** | | **LATERAL (`per-parent LIMIT`)** | **0.5 ms** | **57** | **~60, index early-stop** | LATERAL matches the pre-PR read cost while being fair, needs no new index, and scales independently of how large any single relation is. ## Verification - Backend integration test (`nested-relation-per-parent-limit`): a parent with 65 children is capped at 60 while a sibling with 3 keeps all 3 — passes. - `EXPLAIN ANALYZE` on the generated SQL: Index Scan with the `LIMIT` pushed into the per-parent lateral (early-stop). - Frontend unit test for the `ExpandableList` cap. - Manual check on a table cell with 40 related records: exactly 10 chips mount inline (down from 40), no console errors, chips still clickable and the overflow count reflects the true total. |
||
|
|
0e22ae0521 |
feat: create calendar events on Google and Microsoft accounts (#22231)
## Context Twenty can import calendar events and send emails, but cannot create calendar events. This adds calendar event creation on connected **Google** and **Microsoft** accounts, mirroring the existing email-send architecture (`message-outbound-manager`). ## What it adds The capability is exposed three ways, all backed by the same composer → driver → persist pipeline: - **GraphQL mutation** `createCalendarEvent` (metadata API) - **AI agent tool** `create_calendar_event` (flows to MCP automatically), gated by a new `CREATE_CALENDAR_EVENT_TOOL` permission flag - **Workflow builder node** "Create Calendar Event" in the **Core** section, with a full settings form (variable interpolation supported) CalDAV/IMAP is intentionally out of scope for now (different long pole). ## Design notes - **Reuse over reinvention** — the created event is run through the existing inbound formatters (`formatGoogleCalendarEvents` / `formatMicrosoftCalendarEvents`) and persisted immediately via the existing `CalendarSaveEventsService`, so it appears in Twenty right away and is reconciled by the next provider sync (dedup on external id). Persistence is best-effort. - **OAuth scopes** — Google already requests `calendar.events` (read+write), so no change there. Microsoft moves `Calendars.Read` → `Calendars.ReadWrite`; existing Microsoft accounts must re-consent (surfaced as a clear "reconnect" error via a missing-scope check). - **Deliberate invitation semantics** — `sendInvitations` is off by default. When off, the event is created with **no attendees** on either provider, so creating an event never silently emails external people. When on, attendees are attached and notified (Google `sendUpdates: all`, Microsoft's default). This sidesteps Microsoft Graph having no per-request suppression. - **Timezone correctness** — Microsoft Graph interprets `dateTime` as wall-clock in the supplied `timeZone` and ignores the offset, so the absolute instant is converted to its wall-clock form before sending (Google honors the offset directly). Both providers end up scheduling the same instant. - **Conferencing** — optional Google Meet (`conferenceData.createRequest`, with a follow-up `events.get` to resolve the async link) / Microsoft Teams (`isOnlineMeeting`). - Attendees are a comma-separated string everywhere (tool input, GraphQL DTO, workflow input), consistent with `send_email` recipients; the composer parses to its internal list. ## Test plan - **Unit**: 45 tests covering the composer (validation, all-day boundaries, offset enforcement, timezone, scope checks, default-account resolution), both provider drivers, the dispatcher, and the workflow step-log builder. - **Integration**: `createCalendarEvent` on the `/metadata` API fails closed with a structured error for a non-existent account (the auth/ownership/validation path that doesn't require provider mocking). - **Manual**: verified the workflow node appears in the Core section, the settings form renders and round-trips (edit → autosave → reload), and the live mutation returns a structured failure for a bogus account. ## Open question for reviewers The metadata mutation `createCalendarEvent` shares a name with the core schema's auto-generated `createCalendarEvent(data:)` CRUD mutation for the CalendarEvent object — they live on different endpoints (`/metadata` vs `/graphql`) so there's no runtime conflict, but it's a potential point of confusion for API consumers. Happy to rename (e.g. `createCalendarEventOnConnectedAccount`) if preferred. ## Out of scope / follow-ups - CalDAV/IMAP support - Event update/delete and recurrence - Existing Microsoft accounts need re-consent for the widened scope <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22231?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: neo773 <neo773@protonmail.com> |
||
|
|
3525187321 |
fix(ai) - fixes (#22227)
- ai chat author fix (before : "workflow", after : "user") - https://discord.com/channels/1130383047699738754/1496872385687584768 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22227?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. --> |
||
|
|
e747bc3e42 |
Add backfill installation feature for pre-installed apps (#22199)
## After <img width="1070" height="514" alt="image" src="https://github.com/user-attachments/assets/9cbd2ff5-1678-4c2f-84da-074568f37c51" /> ## Summary Adds the ability to backfill application installations across all existing workspaces. This allows admins to retroactively install a pre-registered application on every active and suspended workspace through a background job, making the feature idempotent and non-blocking. ## Key Changes - **Backend Service**: Added `backfillApplicationOnAllWorkspaces()` method to `PreInstalledAppsService` that: - Validates the application registration exists - Iterates through all workspaces using `WorkspaceIteratorService` - Installs the app on each workspace - Swallows `APP_ALREADY_INSTALLED` errors for idempotency - Logs success/failure counts - **Background Job**: Created `BackfillApplicationInstallationJob` to process backfill requests asynchronously via the message queue - **GraphQL Mutation**: Added `backfillApplicationInstallation` mutation to `AdminPanelResolver` that: - Validates the application registration exists - Enqueues the background job - Returns immediately without blocking the request - **UI Components**: Enhanced `SettingsAdminApplicationRegistrationGeneralToggles` with: - New "Pre-install on new workspaces" toggle for the `isPreInstalled` flag - "Backfill on all workspaces" button with confirmation modal - Loading state and success/error snack bar feedback - **Data Model**: Added `isPreInstalled` field to `UpdateApplicationRegistrationPayload` input type - **Tests**: Added comprehensive unit tests for `PreInstalledAppsService.backfillApplicationOnAllWorkspaces()` covering: - Missing registration validation - Successful multi-workspace installation - Idempotent handling of already-installed errors - Proper error propagation for unexpected failures ## Implementation Details The backfill operation is designed to be: - **Idempotent**: Already-installed apps are skipped without error - **Non-blocking**: Runs as a background job via message queue - **Resilient**: Per-workspace failures don't block other installations - **Observable**: Logs aggregated success/failure counts for monitoring <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22199?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: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> |
||
|
|
c891258f34 |
Add v2 onboarding create profile page (#22221)
<img width="3024" height="1498" alt="CleanShot 2026-06-26 at 15 30 23@2x" src="https://github.com/user-attachments/assets/8b4863a9-66ed-4da1-851b-473cedf71511" /> <img width="3022" height="1500" alt="CleanShot 2026-06-26 at 15 29 43@2x" src="https://github.com/user-attachments/assets/22fc0e94-f670-4638-975c-f06b2b2e25e8" /> Adds the v2 onboarding **Create profile** page, shown right after the import-contacts step (`PROFILE_CREATION`) for the onboarding-v2 cohort. It renders full-screen under `BlankLayout` via the shared `OnboardingV2Layout`, matching the Figma (340px column, inline round avatar uploader + First/Last row, Job Title, dark Continue). The v1 modal flow is untouched and still used for non-v2 users. Job Title is wired end-to-end: it adds a real `jobTitle` field to the `WorkspaceMember` standard object (shared metadata constant + flat field metadata + entity property) and a `2-17` workspace upgrade command to backfill the field on existing workspaces. Continue persists name + jobTitle through the existing `updateWorkspaceMemberSettings` mutation, whose allow-list picks up the new standard field automatically. Routing mirrors `SyncEmailsV2`: new `AppPath.CreateProfileV2`, lazy route, and an `isOnboardingV2`-gated branch in `usePageChangeEffectNavigateLocation` (+ tests and a Storybook story). Reviewer notes: - `jobTitle` is **write-only** for now (no read-back path: core DTO/transpiler/fragment unchanged), and the field is `isSystem`/non-UI-editable to match its siblings. Easy to surface later if wanted. - New `OnboardingProfilePictureUploader` is a compact round avatar uploader reusing the same upload mutation flow as `WorkspaceMemberPictureUploader`. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22221?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. --> |
||
|
|
b84f748237 |
Fix navigation opened section spacing (#22222)
## Summary - Remove the collapsed empty `Opened` sidebar section when no opened object exists. - Restore the first navigation section top alignment with the settings `User` section. ## Before/After Visual verification after the fix: in the current fixture data, the first visible main sidebar section (`Favorites`) starts at y=92, matching the settings `User` section at y=92. This confirms there is no collapsed `Opened` spacer pushing the main drawer content down. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22222?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. --> |
||
|
|
da6a2ee300 |
fix(ai-chat): keep streams alive on silent SSE death + make the stream job idempotent (#22201)
## Problem In production, an AI-chat assistant response sometimes freezes mid-stream (partial text, looks hung), then "picks up again on its own" later without the user resending and without a known worker restart. Root cause: the **agent-chat SSE subscription has no keepalive and no silent-death detection**. - Delivery is fire-and-forget Redis pub/sub (`SubscriptionService.publishToAgentChat`) and the resolver returns the **raw** iterator — unlike `EventStreamResolver`, which heartbeats every 30s via `wrapAsyncIteratorWithLifecycle`. - During a quiet model/tool gap the connection sends no bytes, so a proxy/LB/NAT can silently drop it mid-stream. `graphql-sse` neither surfaces an error nor resumes with `Last-Event-ID`, and **nothing re-pulls the existing Redis chunk catch-up on reconnect** (it only runs on thread (re)mount / `message-persisted` refetch). - So the live view freezes; recovery only happens when the terminal `message-persisted` fires a full refetch from the DB — the observed "self-recovery". This is the **same silent-SSE-death class fixed for the DB event stream in #21061**, which was never applied to the agent-chat path. The symptom also matches #21096 (worker logs the job finishing, client never updates, reload shows the message). It is **not** queue prioritization, and it is **not** addressed by #22193 (which only stabilizes the assistant message id and removes end-of-stream flicker). A secondary, independent self-recovery path also existed: BullMQ stalled-job re-run (default 30s `lockDuration`, no idempotency guard) re-streaming the whole turn → duplicate assistant messages / double billing. ## Changes ### Commit 1 — keepalive + silent-death recovery (ports the #21061 pattern to agent chat) - **Shared:** new `keepalive` variant on `AgentChatSubscriptionEvent`. - **Server:** wrap the agent-chat subscription iterator with `wrapAsyncIteratorWithLifecycle` — emit a `keepalive` on connect and every `APPLICATION_KEEPALIVE_INTERVAL_MS` (30s) so the connection keeps flushing bytes and a dead connection becomes detectable. - **Client:** track the last received event timestamp (refreshed on every chunk/keepalive in the SSE `next` sink); new `AgentChatStreamKeepAliveEffect` forces a resubscribe + messages refetch after 90s of silence, so the durable Redis chunk list backfills the gap (`firstLiveSeq` is reset on resubscribe). ### Commit 2 — stream-job idempotency + lockDuration - Thread a `lockDuration` option through `MessageQueueWorkerOptions` + the BullMQ driver; set `aiStreamQueue` to 10 min so long streams aren't falsely stalled. - Guard `StreamAgentChatJob.handle` with a `streamId`-scoped Redis lock (`SET NX PX` + compare-and-delete release) so a stalled re-run is skipped instead of double-processing. ## Verification ⚠️ I could **not run typecheck/lint locally** — `yarn install` could not complete in this environment (transient registry network aborts before the link step, so `node_modules` never populated). **Please rely on CI for type/lint verification.** The changes are written to match existing conventions; the points most worth a reviewer's eye are the resolver's iterator typing and the ioredis `set(..., 'PX', ttl, 'NX')` overload. How to confirm the root cause in prod: a frozen client with the worker logging `StreamAgentChatJob processed in …ms` and no `[AI_CHAT_NO_TEXT]` is the silent-death signature (check reverse-proxy idle/buffering). For the secondary path, watch `aiStreamQueue` `stalled`/re-processed metrics and duplicate turns around worker restarts. ## Notes / trade-offs - The 10-min `lockDuration` means a genuinely crashed worker's job isn't reclaimed for up to 10 min; the client-side keepalive/catch-up recovers the view independently, and the idempotency lock prevents duplicates. Faster dead-worker recovery could be a follow-up. - Touches `useAgentChatSubscription.ts` / `AgentChatRuntimeEffects.tsx` / `stream-agent-chat.job.ts`, which #22193 also touches — trivial rebase expected. Opened as **draft** pending CI. https://claude.ai/code/session_018dF82A1VcsuWMxPLmdY3dm --- _Generated by [Claude Code](https://claude.ai/code/session_018dF82A1VcsuWMxPLmdY3dm)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22201?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. --> |
||
|
|
8f7d6c24dd |
Add v2 onboarding import contacts page and unify the onboarding v2 shell (#22212)
<img width="3024" height="1668" alt="CleanShot 2026-06-26 at 13 29 28@2x" src="https://github.com/user-attachments/assets/9bdd0029-45eb-4ddb-859f-eaab9bb61406" /> Adds the new v2 onboarding **Import contacts** step (email + calendar import), shown right after workspace creation in the v2 flow. The presentational page was designed in a previous PR; this wires it in and unifies the shell. **What changed** - Reuses and unifies the existing v2 onboarding shell: extracts `OnboardingV2Layout` + `OnboardingV2Header` (the back + logo header, now with the free-credits pill), and the `SignInUpV2` workspace-creation step renders through it (old `SignInUpV2Header` removed). - New `SyncEmailsV2` route (`/sync/emails-v2`) under `BlankLayout`, wired to the same OAuth/skip hooks as v1 `SyncEmails`. - The `SYNC_EMAIL` step routes to the new page only when `isOnboardingV2` is set (mirrors the existing `WorkspaceActivation` → `WorkspaceActivationV2` branch); the v1 modal is unchanged for the non-v2 flow. - No backend changes — reuses the `SYNC_EMAIL` status and `skipSyncEmailOnboardingStep` mutation. **Reviewer notes** - Connect defaults to `METADATA` (private) visibility to match the "Only you will be able to see your emails and events" note (v1 had a selector defaulting to `SHARE_EVERYTHING`). - The header free-credits pill shows `0` for now (no current-workspace credits source on the frontend yet). - The back button is hidden on the import page (no meaningful "back" after workspace creation); unchanged on the workspace-creation step. |
||
|
|
ebe067f65c |
Fix favorite showing non-readable objects (#22217)
## Context Navigation menu items backed by objects the user has no read permission on were correctly hidden from the Workspace section, but Favorites still showed them. Favorites are user-scoped nav items (tied to workspaceMemberId), and FavoritesSection only filtered out folder children (!item.folderId) without ever checking canReadObjectRecords. The shared upstream filter (filterAndSortNavigationMenuItems) intentionally does not apply read permissions, because its output also drives drag-and-drop position math and layout-customization/edit mode, which need the complete, unfiltered list. So read-permission filtering belongs at the display layer. ## Fix New shared hook useReadableNavigationMenuItems that centralizes the read-permission filtering logic previously duplicated across sections: wires up objectMetadataItems + views + object permissions around isNavigationMenuItemReadable filters folder children and top-level items (dropping folders whose children are all unreadable) exposes both raw filtered* outputs and isLayoutCustomizationModeEnabled-aware display* outputs FavoritesSection now applies the filter via the hook, so unreadable favorites are hidden — while still showing everything in layout-customization mode (consistent with the Workspace section). WorkspaceSectionContainer refactored to consume the same hook, removing its inline isItemReadable, the dual-map reduce, and inline filtering. ## Before ### With access <img width="842" height="570" alt="Screenshot 2026-06-25 at 14 01 38" src="https://github.com/user-attachments/assets/ae51f3c8-c178-4162-84ce-3fe49cf07987" /> ### Without access <img width="987" height="590" alt="Screenshot 2026-06-25 at 14 02 08" src="https://github.com/user-attachments/assets/94dacea3-bd77-4fcb-868d-353ed513b28c" /> ## After ### Without access <img width="1001" height="615" alt="Screenshot 2026-06-25 at 14 02 46" src="https://github.com/user-attachments/assets/e1ccd5d9-8583-4ff1-ab91-6f6d187925c5" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22217?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. --> |
||
|
|
fdf9f543ae |
Fix rounded page layout tab edit outline (#22214)
## Summary - Round the edited page layout tab outline to match the tab hover radius. ## Test plan - `npx oxfmt --check packages/twenty-front/src/modules/page-layout/components/PageLayoutTabListReorderableTab.tsx` - `npx oxlint --type-aware -c packages/twenty-front/.oxlintrc.json packages/twenty-front/src/modules/page-layout/components/PageLayoutTabListReorderableTab.tsx` - `npx nx run twenty-front:lint` - Browser: verified the edited `Notes` tab outline before/after locally. ## Visual <img width="1108" height="392" alt="clipboard" src="https://github.com/user-attachments/assets/1cb63180-83a9-4ef8-9c55-2b475eaaa02b" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22214?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. --> |
||
|
|
bea7857a1c |
Fix settings page root overflow (#22207)
## Summary - Clamp the fixed app shell with `overflow: hidden` so long nested settings scroll content no longer contributes to the document root scroll range. - Keeps the object permission page scrolling inside its existing settings `ScrollWrapper` instead of letting the whole page slide under the viewport. ## Screenshots Before: root document can scroll under the bottom of the viewport and exposes the gray app background.  After: the same root scroll attempt leaves the app shell fixed to the viewport.  ## Browser Validation Route tested: `/settings/members/roles/78fa69cb-1237-43b5-bfa5-5a11a47bf781/object/5ab1a16b-7811-471f-ac53-940666c667dd` - Before on `http://apple.localhost:3001`: `window.scrollTo(0, 9999)` moved the root to `scrollY=244.5`; `htmlScrollHeight=1287`, `htmlClientHeight=1043`. - After on `http://apple.localhost:3002`: the same root scroll attempt stayed at `scrollY=0`; `htmlScrollHeight=1043`, `htmlClientHeight=1043`. - The settings content still scrolls internally: wrapper `scrollHeight=1475`, `clientHeight=955`. ## Checks - `git diff --check` - `yarn oxfmt --check packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx` - `cd packages/twenty-front && npx oxlint --type-aware -c .oxlintrc.json src/modules/ui/layout/page/components/DefaultLayout.tsx` <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22207?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. --> |
||
|
|
34dc681c0e |
Use settings icon in settings drawer tab (#22204)
## Summary - Let the shared navigation drawer tab row accept a custom icon and accessible label for its navigation tab. - Use the Settings icon and Settings label when that tab row is rendered inside the settings drawer. - Keep the main navigation drawer defaulting to the Home icon. ## Screenshots ### Before <img width="420" alt="Before: settings drawer navigation tab uses the Home icon" src="https://github.com/user-attachments/assets/03b684ba-0f62-4a96-a9f1-8c6138efd9cf" /> ### After <img width="420" alt="After: settings drawer navigation tab uses the Settings icon" src="https://github.com/user-attachments/assets/3e5f4669-ed7c-4355-be92-c9cfbf160959" /> ## Validation - `yarn oxfmt --check packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerTabsRow.tsx packages/twenty-front/src/modules/navigation/components/SettingsNavigationDrawer.tsx` - `git diff --check` - `yarn nx typecheck twenty-front` Note: `oxlint` could not run locally because the installed dependencies are missing the native `@oxlint/binding-darwin-*` optional package. |
||
|
|
cb49a7a053 |
Add v2 onboarding loading screen while creating workspace (#22152)
https://github.com/user-attachments/assets/cc7b1d10-7495-4f21-9311-4c22c0f14771 Adds the full-screen loading screen shown while a new workspace is being created in the v2 sign-up flow (`SignInUpV2`), building on the v2 "Create your workspace" step. How it works: - Submitting the v2 create-workspace form marks the flow as v2 (`isOnboardingV2State`) and creates the workspace. The flag is carried across the cross-subdomain redirect with an `onboardingV2=true` URL param, so v2 users land on a new `/workspace-activation-v2` route instead of v1's `/workspace-activation`. - `WorkspaceActivationV2` runs the real `activateWorkspace` mutation on mount and renders the loader: a pulsing Twenty logomark above a stack of status messages that shift up one at a time, cycling once per second. There is no faked/minimum duration; it advances to the next onboarding step as soon as the workspace is activated. - On activation failure it shows a "Workspace creation failed" screen with a Retry button. v1 onboarding is unchanged. Storybook: `Modules/Auth/SignInUpWorkspaceActivationV2`. Note: The flashes will be fixed in later PRs <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22152?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. --> |
||
|
|
ce4d0f3447 |
fix: hide "Create Workspace" button when multi-workspace is disabled (#22202)
## Description This PR fixes a bug where the "Create Workspace" button was unconditionally rendered in the workspace switcher dropdown, even when single-workspace mode was active (`IS_MULTIWORKSPACE_ENABLED=false`). This created a confusing "dead-end" action for users, as clicking the button would do nothing (because the backend correctly blocks workspace creation in this mode, and the frontend skips the redirect). ### Changes made - Imported the `isMultiWorkspaceEnabledState` atom from client-config. - Evaluated `isMultiWorkspaceEnabled` inside `MultiWorkspaceDropdownDefaultComponents`. - Conditionally rendered the "Create Workspace" `<MenuItem>` only if multi-workspace is enabled. Closes #22139 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22202?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. --> |
||
|
|
b625bd1995 |
fix(ai-chat) - improvements (#22193)
- remove flickering at assistant message streamed end - add copy code - leave chat history when navigating to settings <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22193?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. --> |
||
|
|
9b57c5dfac |
Set record card header height and calendar spacing (#22187)
## Summary Sets the shared `RecordCardHeaderContainer` height to `32px`, so board and calendar card headers use the same common header size. Updates the board fetch-more card-height estimate to use the same `32px` header value. Reduces the calendar card header/content gap by removing the body top padding while keeping the existing side and bottom padding. ## Screenshots Before:  After:  ## Validation - Browser verification on `http://apple.localhost:3001/objects/opportunities?viewId=0433d066-dda7-4b2c-89e5-04d4792d193c`: visible calendar card body padding computes to `0px 4px 4px` - Browser verification on board view: visible board card headers compute to `32px` - Browser console errors: none - `git diff --check` - `prettier --write` on changed files - `oxlint --type-aware` on changed files in the running checkout |
||
|
|
7e48d36c98 |
fix(twenty-front): apply object type translations to details panel relation labels (#22090)
## Problem When users customize or translate object type names (e.g. "Company" → "Unternehmen" in German), the translated/customized names do **not** appear in the details panel. The default English names still show instead. This is because the frontend's relation metadata only carried `nameSingular`/`namePlural` (internal API identifiers), not `labelSingular`/`labelPlural` (user-facing display labels). Components that display relation names had no choice but to use the internal identifiers. Fixes #19790 ## Changes ### Data layer — add labels to the pipeline - **GraphQL fragment** (`fragment.ts`): Added `labelSingular`/`labelPlural` to `sourceObjectMetadata` and `targetObjectMetadata` in both `relation` and `morphRelations` - **Type** (`FieldMetadataItemRelation.ts`): Extended the `Pick` type to include `labelSingular`/`labelPlural` - **Field metadata type** (`FieldMetadata.ts`): Added `relationObjectMetadataLabelSingular`/`LabelPlural` to `FieldRelationMetadata` - **Mapping** (`formatFieldMetadataItemAsFieldDefinition.ts`): Maps the new label fields with `label ?? name` fallback for backwards compatibility ### Display layer — use labels for user-facing text - **RecordDetailRelationRecordsListItem**: Uses `relationObjectMetadataLabelSingular` for delete confirmation dialog title, subtitle, and button text (falls back to `nameSingular`) - **RecordDetailRelationRecordsList**: Threads `objectLabelSingular` prop through - **RecordDetailRelationSection**: Passes `labelSingular ?? nameSingular` from the looked-up object metadata - **FieldWidgetRelationCard**: Passes `relationObjectMetadataLabelSingular` from field metadata - **FieldWidgetJunctionRelationCard**: Passes `labelSingular ?? nameSingular` from object metadata lookup - **FieldWidgetMorphRelationCard**: Passes label from morph relation hook result - **useGetMorphRelationRelatedRecordsWithObjectNameSingular**: Carries `labelSingular` from matched morph relation ### Test data - Updated story/mock files with `relationObjectMetadataLabelSingular`/`LabelPlural` fields - Updated `SettingsDataModelRelationFieldPreview` with label fields in morph relation objects ## Design decisions - **Backwards compatible**: All new props are optional. Every display usage uses `label ?? name` fallback, so if `labelSingular` isn't available yet (e.g. before GraphQL regeneration), it falls back to the old behavior - **Lookup vs display separation**: `nameSingular` continues to be used for lookups, routing, and GraphQL queries (it's the identifier). `labelSingular` is only used for user-facing display text - **Minimal scope**: Only changes the display paths identified in the bug report — confirmation dialogs and relation labels in the details panel ## Test plan 1. Set workspace language to a non-English locale (e.g. German) 2. Navigate to a record with relation fields 3. Verify relation section titles and labels show translated names 4. Try to delete a related record — verify the confirmation dialog uses the translated name 5. Switch language back to English — verify everything still works correctly <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22090?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: Charles Bochet <charles@twenty.com> |
||
|
|
b3e39e2198 |
fix: relative date picker calendar display (#21895)
Part of https://github.com/twentyhq/twenty/issues/19739#issuecomment-4652034526 (Bug 1-3). Maybe it feels like theses bugs are not actually bugs, but we can maybe say it as UX improvements: specially needed in case when an user will choose any past options. ### Bug 1: calendar open on wrong month With Is Relative (e.g. Past 1 Quarter), the calendar opened on today’s month instead of the range start. After the fix, it now opens on the first month of the filtered range. **Testing:** View filter → Date field → Is Relative → Past 1 Quarter. Calendar opens on January (range start), not today’s month https://github.com/user-attachments/assets/8849d00a-4d5c-4f8a-8d31-3a62535eb311 ### Bug 2: Dates not highlighted Ranges older than ~2 months (e.g. Q1 when today is June) showed no highlighted days. Highlighting now covers the full resolved range. **Testing:** Same setup: past 1 Quarter on a date when Q1 is outside the old 2‑month window. Jan 1 - Mar 31 will highlight. https://github.com/user-attachments/assets/d21e2272-c923-4493-80ff-bdf4228842b1 ### Bug 3: No month navigation Relative mode only showed Past - 1 - Quarter controls with no way to browse months. Now see the new arrows move through months without changing the filter. <img width="377" height="455" alt="Screenshot 2026-06-20 181107" src="https://github.com/user-attachments/assets/eb51feb9-af10-489a-b166-8b8d6c642e05" /> > [!NOTE] > 1. We can't do the fixes by one by one, i have to fix them within one PR because all the fixes are inter-related, like we can't test the bug 1 fix alone without implementing bug 3. > 2. Bug 4 will be done in a separate PR which is actually the issue #19739. See https://github.com/twentyhq/twenty/issues/19739#issuecomment-4652034526 for better understanding. > 3. If you see the screen recordings, they are actually done with the alignment fixes from #21881 . So without that changes you will see the alignmemt issues in the calendar grid in your local. --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
6e2df0654b |
[Workflows] Allow iterator to take whole item as variable (#22031)
**Select the whole item in iterator loops, and iterate over a step's array output** ## Summary Two related improvements to working with lists in workflows: - Pick the current item as a whole inside an iterator loop. Previously, in a node inside the loop, you could only reference individual fields of the Iterator's current item. Now you can select the whole item (e.g. a full record) — useful for passing it straight into a downstream step. <img width="1270" height="744" alt="Screenshot 2026-06-23 at 17 02 47" src="https://github.com/user-attachments/assets/6b92e72e-ec25-4c1a-9841-3a438210e753" /> - Iterate over a step's array output. A Code / Logic Function step that returns a top-level array couldn't be fed to the Iterator: its output was flattened into indexed entries (0, 1, …) with no way to select the array as a whole. A new "Whole list" option selects the step's entire output, and the Iterator infers the per-iteration item shape from it. <img width="1026" height="728" alt="Screenshot 2026-06-23 at 17 17 53" src="https://github.com/user-attachments/assets/db07dcd8-4fb8-4db9-8b45-aa56051d9f3b" /> Together these complete the loop ergonomics: select a list → iterate → reference the current item (whole or by field) downstream — matching the model used by tools like Windmill. ## What changed - The variable picker offers a "Use the whole item" option when viewing an iterator's current item, and a "Whole list" option when a step returns a top-level array. - The Iterator's current-item schema can now be inferred from a variable pointing at a step's whole output. ## Risks for existing workflows None expected. The change is purely additive: - No DB migration and no change to how output schemas are stored or read — existing schemas, variables, and iterators behave identically. - No change to runtime variable resolution; existing {{step.field}} and current-item references are untouched. - The new options only apply to new selections (whole item / whole list); all existing paths take the unchanged code path. - The only edge case: array detection is heuristic (an output whose keys are exactly 0…n-1), so an object that happens to have those keys would also show "Whole list". This is rare for real outputs, affects nothing unless a user selects it, and fails safe — the Iterator validates its input and throws a clear "items must be an array" error if a non-array is passed. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22031?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: Charles Bochet <charles@twenty.com> |
||
|
|
c635a191bf |
fix: uneven spacing in date picker calendar grid (#21881)
### Summary While working on #19739, I found that in the date filter calendar dropdown, day cells and highlighted dates looked misaligned i.e. tighter on the right side. The solution is to apply a uniform margin in `DatePicker.tsx` and `DateTimePicker.tsx`. ### Before: <img width="377" height="436" alt="Screenshot 2026-06-20 021856" src="https://github.com/user-attachments/assets/4ef62a48-6b99-4647-95f6-bd39f43eaa26" /> <img width="442" height="518" alt="Screenshot 2026-06-20 021932" src="https://github.com/user-attachments/assets/e6e1e8f8-257a-41e5-825f-bd2fe91e372a" /> ### After: <img width="346" height="380" alt="Screenshot 2026-06-20 021813" src="https://github.com/user-attachments/assets/5fdd03fe-e61b-4fec-a0f3-f6ac4ee9effb" /> <img width="322" height="457" alt="Screenshot 2026-06-20 022013" src="https://github.com/user-attachments/assets/a33c6439-f2d5-43e7-a043-f9cf4fec770a" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21881?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. --> --------- Signed-off-by: Parship Chowdhury <parshipchowdhury@gmail.com> Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
0b31d2a6a0 |
fix(front): wrap long note/markdown text on mobile browsers (#21909)
Fixes #21929 ### Issue When viewing/reading a note's body (BlockNote rich-text / markdown content) on Android, long text overflows horizontally off the viewport in **Chrome** and **Firefox**, while it wraps correctly on **iOS Safari**. ### Root cause The note body is rendered by the BlockNote editor (`StyledEditor` in `BlockEditor.tsx`). The block/inline content had no `overflow-wrap`, and the flex-based `.bn-block-content` had no `min-width` constraint. WebKit (iOS Safari) breaks the content, but Blink (Android Chrome) and Gecko (Android Firefox) keep the intrinsic *min-content* width of the flex children, so the container expands past the viewport instead of wrapping. ### Fix Add wrapping/sizing rules to the editor container so text breaks and wraps consistently across browsers, without changing the desktop layout: - `overflow-wrap: anywhere` on `.bn-block-content` / `.bn-inline-content` — unlike `break-word`, this reduces the min-content size so flex children can actually shrink. - `min-width: 0` on `.bn-block-content` and on the editor wrapper, plus `max-width: 100%` on the wrapper. ### Test plan - Open a Note containing a very long word / URL or a long paragraph. - Android Chrome & Firefox: text now wraps within the viewport (no horizontal overflow). - iOS Safari: unchanged (still wraps). - Desktop: layout unchanged. ### Screenshots Android <img width="1080" height="1949" alt="Screenshot_20260620_231356_Chrome(1)" src="https://github.com/user-attachments/assets/bb1e38cf-198a-4ded-9dde-e0a26e637ed8" /> iPhone <img width="686" height="1280" alt="IMG_20260620_231841_096" src="https://github.com/user-attachments/assets/9cee7557-bb69-453d-bea4-a5c37de220af" /> --- ### ✅ Verified on a real Android device Reproduced and validated on a **Samsung Galaxy A71 (Android, Chrome / Blink)** using the exact note show-page DOM/CSS chain (`ScrollWrapper` → full-width container → `StyledEditor` → `.bn-mantine` `container-type: inline-size` → flex `.bn-block-content` → ProseMirror `word-wrap: break-word`): - **Without the fix:** a long unbreakable string stays on one line and overflows horizontally off the viewport (`scrollWidth ≈ 1336px` in a ~360px viewport, with a horizontal scrollbar) — matching the reported bug. - **With the fix:** the same string wraps within the viewport. Notably this does **not** reproduce on desktop Chromium — only on the mobile engine — which matches the original report (Android Chrome/Firefox broken, iOS Safari fine). The flex `.bn-block-content` (`min-width: auto`) resolves to the unbreakable token's intrinsic width on Android Blink; `min-width: 0` + `overflow-wrap: anywhere` lets it shrink and wrap. Screenshot <img width="1080" height="2400" alt="image" src="https://github.com/user-attachments/assets/827dc579-4b1e-43ea-8b28-0ca781b12d88" /> |
||
|
|
87329c8810 |
fix(ask-ai): resolve stream subscription race condition on new thread… (#21916)
## Description Resolves a race condition in the Ask AI feature where the first assistant reply in a newly created thread does not stream into the UI and only appears after sending a second message. ### What's Changed - **Immediate Thread Subscription:** Updated `useAgentChat.ts` to immediately set `currentAiChatThread` to the newly generated `threadId` instead of deferring it until after the `SEND_CHAT_MESSAGE` mutation finishes. - **The Bug:** Previously, the backend worker processed the AI chat job so quickly that the stream completed and fired the `message-persisted` event *before* the frontend established the SSE subscription. - **The Fix:** By setting the thread ID immediately, the `useAgentChatSubscription` hook now properly connects and listens to the SSE stream before the backend begins emitting chunks, guaranteeing the first message streams seamlessly. ### How to Test 1. Open the Ask AI panel and start a completely new thread. 2. Send an initial message (e.g., "Hello!"). 3. Observe that the AI's response successfully streams into the chat without needing a workaround or page refresh. Closes #21694 --------- Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com> |
||
|
|
864ea452b4 |
fix mobile side panel close (#22169)
The side panel close (X) button was hidden on all mobile views, while the back button only renders when there is navigation history. When the side panel is opened at the root (e.g. viewing a record directly with a single-item navigation stack), neither button was shown, leaving no way to dismiss the panel on mobile. Keep the close button available on mobile whenever there is no back button to fall back on, so the panel is always dismissable. ## Before https://github.com/user-attachments/assets/61891d25-26b8-4ba4-8b05-73fd44f92d89 ## After https://github.com/user-attachments/assets/41342722-bcaf-420b-83bb-3cafaec49516 |