de75be16e2d4e737075909e70f1bd84f260d22df
6419 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8e022d3c49 |
Fix stale relation table in field widget when switching records in side panel (#22829)
The relation table rendered by a FIELD widget in TABLE display mode kept its jotai component states (loaded rows, virtualization maps, loading guards, query identifiers) in instances keyed only by widget id and view id. Since the side panel record pages share those instances across records, switching to another record kept rendering the previous record's related rows until an asynchronous catch-up reload landed, and any race or error in that catch-up left the previous record's data on screen permanently. Scope the record-table widget's context store instance and record index instance by target record id (and side panel surface), the same way FieldsWidget already scopes its field list instances. Each record now gets its own table state, so a record's rows can never appear under another record, and loads that land after a record switch write into their own instance instead of the visible one. loadRecordIndexStates and setRecordGroupsFromViewGroups accept an optional recordIndexId override so the widget view load effect can populate the record-scoped instance instead of deriving the shared one from object name and view id. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22829?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
6201d06141 |
Preload Stripe.js before the onboarding payment step (#22858)
## Context On the plan-required onboarding step, the card form was slow to appear because Stripe.js is loaded lazily (`@stripe/stripe-js/pure`): the script download only started once the payment page rendered, and the PaymentElement iframe could only boot after that. ## What this does - Adds `usePreloadStripeForPlanRequiredStep`, called once from `OnboardingStepLayout` (the shared layout for the authenticated onboarding step routes), so Stripe.js is already loaded by the time the user reaches the payment step. The hook only triggers when billing is enabled, the workspace has no subscription yet, and a publishable key is configured, so self-hosted instances still never contact Stripe. - Moves the memoized loader to `settings/billing/utils/getStripePromise.ts`, shared by `useStripePromise` and the preload hook. - Stops caching failed script loads: previously a rejected `loadStripe` promise stayed in the cache forever, which would have made a failed preload permanently break the payment form. Now a later call retries (stripe-js re-injects the script tag on retry). - Extracts the plan-required predicate into `onboarding/utils/getIsPlanRequired.ts`, now shared with `useSetNextOnboardingStatus`. The in-app add-credit-card modal is intentionally left untouched: it has no preceding step to preload from. ## Tests - `getStripePromise.test.ts`: dedup per publishable key, retry after a failed load. - `usePreloadStripeForPlanRequiredStep.test.ts`: preloads when billing is enabled and no subscription exists; skips when billing is disabled, a subscription exists, or the key is missing. --- _Generated by [Claude Code](https://claude.ai/code/session_01NtUN99tHPZ6bPWwpYKMbpE)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22858?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
8e66411203 |
i18n - translations (#22861)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22861?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
7381038452 |
Paginate admin panel app registrations list (#22734)
## Context The `findAllApplicationRegistrations` query on the admin panel Apps page (`/settings/admin-panel#apps`) loaded every application registration at once, with search and filtering done client-side. ## Changes **Server** - `findAllApplicationRegistrations` now takes `limit` / `offset` / `searchTerm` / `isPreInstalledOnly` args and returns a `PaginatedApplicationRegistrations` object (`registrations`, `totalCount`, `hasMore`), following the same pattern as `getQueueJobs`. - `ApplicationRegistrationService.findAll` uses `findAndCount` with `take`/`skip`, and moves the search (name, source package, universal identifier via `ILIKE`) and the pre-installed filter into the SQL query, mirroring how `getInstalledWorkspacesGlobal` filters installed workspaces. **Frontend** - `SettingsAdminApps` passes the page, the debounced search term (300ms, like the installed workspaces table), and the pre-installed toggle as query variables instead of filtering client-side. - Adds a Previous / Next pagination footer (25 per page) matching the queue jobs table, shown only when there is more than one page. - The "unconfigured first" ordering is kept within each page (`isConfigured` is a dataloader-resolved field, so it can't be sorted in SQL). ## Notes - Regenerated `generated-admin/graphql.ts` follows in a subsequent commit. --- _Generated by [Claude Code](https://claude.ai/code/session_015erumgPozkbNA3zPeKrrFW)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22734?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Weiko <corentin@twenty.com> |
||
|
|
983f03adbe |
i18n - translations (#22833)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
b0dc637dbd |
Throw proper error on duplicate emailing domain (#22790)
Adding an emailing domain that already exists blew up with a raw QueryFailedError and the client just saw a generic "An error occurred". The unique index on domain is global, so the workspace-scoped existence check never caught rows owned by another workspace. Now the check is unscoped and throws an EmailingDomainException mapped to CONFLICT with a proper user-facing message, in both the createEmailingDomain mutation and the email group channel flow. Also dropped the hardcoded catch-all snackbar on the new channel page so server messages actually reach the user. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22790?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
c9d84ba7f0 |
Disable install button in app install onboarding when no app is selected (#22822)
In the app installation onboarding step, the Install button was always clickable, even with no app selected. Clicking it with an empty selection ran the completion flow with zero apps, which is equivalent to skipping. Now the button is disabled until at least one app is selected (in addition to staying disabled while completing). The Skip button remains available for users who don't want to install anything. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22822?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
7babc049f5 |
[Twenty-Front]: Record board column drag and drop functionality (#22323)
Closes #22321 - Used `@dnd-kit` library for core drag and drop logic. - Tried to keep as much similar to #21304 as possible. https://github.com/user-attachments/assets/bff100cf-d727-4281-a5fa-b010a373f189 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22323?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> Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com> Co-authored-by: bosiraphael <raphael.bosi@gmail.com> |
||
|
|
60f5964c64 |
Run front components in a sandboxed opaque-origin iframe (#22588)
Front components run untrusted third-party React in a Web Worker. That
worker previously shared the host origin, so it could reach
origin-scoped storage (the metadata-store IndexedDB, the
`twenty-sign-out` BroadcastChannel), cookies, and same-origin resources.
This runs the worker inside a `sandbox="allow-scripts"` (no
`allow-same-origin`) iframe, giving it an opaque origin where the
browser denies localStorage, cookies, IndexedDB, and BroadcastChannel
outright. The worker is kept inside the iframe (rather than a bare
iframe) so untrusted code always runs off the main thread; the
remote-dom render path is unchanged.
- **Transport:** host ↔ iframe ↔ worker over a re-transferred
`MessagePort` (`ThreadMessagePort`); a small bootstrap script is inlined
into the iframe via `srcdoc` (bundled at build time by a prebuild step)
and relays the port to the worker it spawns. Messages across the
boundary use a typed discriminated union with a single parse/guard.
- **Network:** under the opaque origin, direct fetches to the Twenty API
would be `Origin: null`, so the component source and SDK modules are
fetched through an allowlisted, credential-omitting `hostFetch` bridge
and blobbed inside the worker. The allowlist is single-sourced on the
host (http(s) origins only) and carried in the render context. The
bridge is mandatory (rendering fails closed if it is missing), refuses
redirects except for GET/HEAD to the known file-storage URLs, and caps
response body size.
- **SDK loading:** SDK client modules now load inside the worker through
the bridge, replacing the host-side SDK-blob state/effect/provider with
a pure `getSdkClientUrls` URL builder.
- **Isolation tests:** a unit test locks the sandbox attribute
(`allow-scripts`, never `allow-same-origin`); a browser test asserts the
worker actually gets an opaque origin with storage denied, probing
cookies by writing one rather than reading an empty jar.
Also adds a "List Companies" seed front component that queries workspace
data via the SDK client (exercising the bridge end-to-end),
single-sources the command-menu confirmation-modal result event name and
detail type in `twenty-shared` (previously a hand-synced duplicate), and
decomposes the renderer (bridge, sandbox, worker orchestration) into
small single-purpose utils with unit tests.
## How it works
```mermaid
sequenceDiagram
autonumber
participant Host as Host window (twenty-front · host origin)
participant Frame as Sandboxed iframe (allow-scripts · opaque origin)
participant Worker as Worker (untrusted component · opaque origin)
participant API as Twenty API (host origin)
rect rgb(238,242,248)
Note over Host,Worker: 1 — Boot handshake
Host->>Frame: create iframe sandbox="allow-scripts", srcdoc = inlined bootstrap script
Host->>Host: MessageChannel + ThreadMessagePort(port1)<br/>exports = host API + hostFetch
Frame-->>Host: READY
Host->>Frame: INIT + transfer port2
Frame->>Worker: spawn inlined Worker + re-transfer port2
Worker->>Worker: ThreadMessagePort(port)<br/>exports = render / updateContext
Note over Host,Worker: Port now entangles Host ↔ Worker directly
end
rect rgb(246,240,248)
Note over Host,Worker: 2 — Render
Host->>Worker: render(connection, { componentUrl, sdkClientUrls, hostFetchOrigins, token })
Worker->>Worker: override globalThis.fetch<br/>(Twenty origins → hostFetch)
end
rect rgb(248,244,238)
Note over Worker,API: 3 — Network via hostFetch bridge (opaque Origin:null cannot reach the API directly)
Worker->>Host: hostFetch(componentUrl, Bearer)
Host->>Host: origin allowlist + credentials:'omit'
Host->>API: fetch(componentUrl)
API-->>Host: source
Host-->>Worker: { status, headers, body }
Worker->>Host: hostFetch(sdkClientUrls.core / .metadata)
Host-->>Worker: SDK module sources
Worker->>Worker: blob each source in its own opaque origin → import() → run untrusted React
end
rect rgb(238,248,242)
Note over Worker,Host: 4 — Render mirror
Worker->>Host: remote-dom mutations (RemoteConnection)
Host->>Host: RemoteReceiver → RemoteRootRenderer → host DOM
end
Note over Worker: Opaque origin ⇒ browser denies localStorage,<br/>cookies, IndexedDB, BroadcastChannel
```
|
||
|
|
f667ba500c |
fix(front): clean stale morph relations from metadata store on object deletion (#22681)
## Problem After deleting a custom object (e.g. `meeting`), the app crashes with "Sorry, something went wrong" on pages that load records referencing that object through a morph relation. The console shows: ``` Target object metadata item not found for target (morph target meeting) ``` It reproduces on the machine that used the object before deletion but not on a fresh machine, which points at a stale client metadata store rather than a server issue. ## Root cause Every field carries its own server-provided `morphRelations` array; a morph relation field (note/task/timeline targets, etc.) lists every object it can point to, including the deleted one. When an object is deleted, `useDeleteOneObjectMetadataItem` and the SSE `delete` handler only remove the deleted **object** and its own fields from the metadata store. The sibling morph fields on other objects keep their now-dangling `morphRelations` entry pointing at the deleted object. Those stale entries were only meant to be cleaned up later by a collection-hash-triggered `network-only` refetch. When that reconciliation does not win, `generateDepthRecordGqlFieldsFromFields` can't resolve the deleted morph target in `objectMetadataItems` and throws, crashing the page. ## Fix Clean `morphRelations` entries referencing the deleted object from the field metadata store at deletion time, so the store stays self-consistent immediately instead of relying on an async refetch. Applied in both paths that handle object deletion: - `useDeleteOneObjectMetadataItem` (the client performing the deletion) - `MetadataStoreSSEEffect` delete handler (other tabs/clients receiving the event) The throw in `generateDepthRecordGqlFieldsFromFields` is intentionally left in place so any genuine future metadata inconsistency still surfaces rather than being silently swallowed. ## Test Added a unit test for the cleaning util covering: morph relations targeting the deleted object are removed, only changed fields are returned, non-morph fields are untouched, and nothing is returned when no relation targets the deleted object. |
||
|
|
ffdda50afc |
remove nestjs-query auto-resolver from index-metadata (#22775)
## Summary
Removes the `NestjsQueryGraphQLModule.forFeature` block from
`IndexMetadataModule`, continuing the incremental migration off
`@ptc-org/nestjs-query`.
- Drops the dead auto-generated read surface: `index` and
`indexMetadatas` queries, the `IndexConnection` /
`IndexObjectMetadataConnection` types, and the `Index.objectMetadata`
field. No client consumes these — the frontend reads indexes via
`ObjectMetadata.indexMetadatas`.
- Keeps `IndexMetadataDTO` nestjs-query-compatible (`@Authorize`,
`@FilterableField`, `@QueryOptions`, `@IDField`) because
`ObjectMetadataDTO` still references it via
`@CursorConnection('indexMetadatas')` until object-metadata is migrated.
- Hand-written `createOneIndex` / `deleteOneIndex` mutations and the
`indexFieldMetadataList` resolve-field are unchanged.
- Deletes the now-obsolete `index-metadatas` integration test and
regenerates the GraphQL schema artifacts (frontend + client-sdk).
## Breaking change
This is an intentional GraphQL schema breaking change
(`api-breaking-changes` CI will flag it)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22775?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. -->
|
||
|
|
b682162f31 |
fix(front): render +N button for right-edge-clipped chips in expandable list (#22748)
## Problem Fixes #22383. Overflowing relation & multi-select chip cells didn't show the "+N" overflow button, so hidden records/values were unreachable. There were several distinct causes behind this, addressed below. Before <img width="263" height="34" alt="Capture d’écran 2026-07-10 à 11 43 13" src="https://github.com/user-attachments/assets/1001d47b-0f54-4fbd-a5cb-83a5c32c35ac" /> After <img width="263" height="34" alt="Capture d’écran 2026-07-10 à 11 41 54" src="https://github.com/user-attachments/assets/d4f5cf2f-fda4-422e-b72d-2df3fc81a06f" /> ## Changes **1. Overflow detection missed right-edge-clipped chips** `isFirstOverflowingChildElement` used `childElement.offsetLeft > containerElement.clientWidth` (left edge past the container), which is false when a chip is only right-edge clipped. Switched to a right-edge check: `childElement.offsetLeft + childElement.offsetWidth > containerElement.clientWidth`. **2. Multi-select never used the overflow list when unfocused** `MultiSelectFieldDisplay` rendered a plain clipped `MultiSelectDisplay` when not focused, and only used `ExpandableList` on focus. That non-focused fallback painted over the focused "+N" and hid it. It now always renders through `ExpandableList` with `isChipCountDisplayed={isFocused}`, matching `RelationFromManyFieldDisplay`, so the count shows on focus only (not idle). **3. The hover portal was never actually focused** `FieldFocusContextProvider` silently ignored its `isFocused` prop (`({ children }: any)` + hard-coded `useState(false)`), so `RecordInlineCellAnchoredPortal`'s `<FieldFocusContextProvider isFocused={true}>` had no effect and the hovered cell's display always saw `isFocused=false`. That's why "+N" never appeared on hover for any multi-value field. The portal now uses the existing `FieldFocusStaticFocusedProvider`, fulfilling the intent — so relations, multi-select, emails and phones all surface their "+N" on hover. **4. "+N" was hard to read on multi-select** The "+N" chip has a transparent background (shared component), and the hover portal let the base layer's colored option chips bleed through it. Gave the hover portal content an opaque `background.primary` so nothing bleeds through; the "+N" component itself is untouched, so relations/emails/phones keep their existing look. ## Test plan - Hover a relation or multi-select field whose values overflow the cell: the "+N" button appears (and is readable), and clicking it lists the hidden records. When not hovered, no "+N" shows. - Verify fully-overflowing rows still show the correct "+N" count. |
||
|
|
4ab36a630b |
remove nestjs-query from app-token and drop unused createOneAppToken mutation (#22765)
## What Migrates `app-token` off `@ptc-org/nestjs-query` and removes the auto-generated `createOneAppToken` mutation, which was unused dead API surface. ## Why The `createOneAppToken` mutation was reachable only from the schema — no frontend query, SDK caller, or test used it. It was also non-functional (its input couldn't set the token `value`), a leftover from nestjs-query's default `create.one` being left enabled. Real app tokens (refresh, password-reset, email-verification, invitation, OAuth, enterprise) are all created directly via the repository in ~14 services, none of which touched this mutation. ## Notes - ⚠️ This removes `AppToken`, `createOneAppToken`, `CreateAppTokenInput`, and `CreateOneAppTokenInput` from the `/metadata` schema, so the **api-breaking-changes check will flag it** <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22765?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: Weiko <corentin@twenty.com> |
||
|
|
f8d3555fe7 |
i18n - translations (#22779)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22779?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
09496a0f98 |
i18n - translations (#22745)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22745?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: github-actions <github-actions@twenty.com> Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
23cae2040a |
Improve application asset management (#22564)
App manifests could point the logo and screenshots at either external
URLs or public folder paths, and that was handled inconsistently across
install, sync and the marketplace.
This makes assets always bundled files:
- Manifests now use `logo` and `galleryImages` (a `string[]` of public
folder paths) instead of `logoUrl` and `screenshots`. The old fields
still work but are deprecated. Gallery order comes from the array index.
Normalization (deprecated-field migration, and warning about + ignoring
external URLs) happens in `defineApplication`, so the warnings surface
at define time.
- Logo is stored as a File record (`logoFileId`).
- The registration gallery is configured via a `settings` jsonb column
on `applicationRegistration` (`{ galleryImages: string[] }`) — populated
from the manifest, read by the marketplace detail (falling back to the
legacy `screenshots` column, then the manifest). No dedicated gallery
table.
- The marketplace detail DTO and front now use `galleryImages`.
Verified against a local Postgres: the fast instance commands run with
no pending-migration diff, the schema is correct, and the server boots.
Typecheck, lint, codegen and the application unit tests pass.
Not included yet: rehosting assets into storage for npm catalog and
tarball registrations, versioned cache busting on the serving route, and
a backfill for existing installs.
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22564?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>
|
||
|
|
9c405384f3 |
Only propose configured apps during onboarding install step (#22712)
## What - Onboarding "Install your first apps" now proposes only apps that are actually installable: it intersects the onboarding list with `findManyMarketplaceApps`, which the backend already filters to listed + configured apps (all required server variables set). - If none are available, the step auto-skips. If the marketplace query fails, it shows an intentional fallback (heading + Skip) instead of silently skipping or rendering an empty install card. - `findManyMarketplaceApps` now accepts `universalIdentifiers`, so onboarding fetches and configuration-checks only its own apps instead of the entire catalog. ## Why Previously the step rendered all hardcoded apps regardless of configuration, only borrowing logos from the marketplace, so a user could be offered an app the admin never configured. This centralizes onboarding availability on the marketplace's existing logic and keeps the query bounded as the marketplace grows. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22712?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. --> |
||
|
|
f06ba08b16 |
Fix onboarding locale reverting to English after signup (#22675)
During onboarding the UI reverted to English after email verification. The chosen locale was never stored on the user, so the workspace member seeded from it inherited `en`, and after workspace activation `loadCurrentUser` reactivated `en` and flipped the whole UI to English regardless of the browser language. Two changes: **Backend (the actual fix):** the `signUp` resolver received `signUpInput.locale` but only used it for the verification email, creating the user without it (so it defaulted to `en`). It now passes the locale into `signUpWithoutWorkspace`, and the SSO create-a-workspace path forwards `locale` as well. The user, and the workspace member created from it at activation, now keep the chosen locale, so the post-activation reactivation no longer forces English. **Frontend:** carry the active locale across the workspace subdomain redirect (in `useBuildSearchParamsFromUrlSyncedStates`) so the freshly loaded subdomain renders `/verify` in the right language before the current user loads, instead of briefly falling back to the browser default. Minor, only affects the case where the chosen locale differs from the browser language. |
||
|
|
c8c38b6cd2 |
Fix onboarding entry animations not triggering on first load (#22724)
The staggered entry animations on the onboarding welcome/sign-in and
sync-emails screens sometimes didn't play on first load, though a
refresh reliably fixed them.
Root cause: `OnboardingTransitionOutlet` wrapped pages in
`<AnimatePresence initial={false}>`. framer-motion propagates that
`initial: false` through `PresenceContext` to every descendant motion
component on the outlet's first render, so any
`OnboardingStepAnimatedItem` that mounts during that first commit snaps
straight to its final state instead of animating. Because onboarding
pages are preloaded (`lazyWithPreload`), a warm in-app navigation
renders the page synchronously in that first commit and the cascade is
suppressed; a cold load/refresh mounts it a commit later via Suspense,
so it animates.
Fix: drop `initial={false}`. Descendants no longer inherit a blocked
initial state, so the cascade animates regardless of preload timing. The
page-level fade now also runs on first entry, matching the treatment
already used on every step-to-step transition and cold load.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22724?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. -->
|
||
|
|
a3d9efd94a |
i18n - translations (#22725)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22725?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
6897fff632 |
Rebuild email composer recipient fields as a structured chip input with person resolution and autocomplete (#22668)
# Why
The To/Cc/Bcc fields reused `FormMultiTextFieldInput`, the workflow
Tiptap tag editor, with recipients stored as a comma-separated string.
That caused every reported issue: duplicates were allowed, the field was
locked to one 32px line with a hidden horizontal scrollbar, chips did
nothing on click, `First Last <email>` could not even be typed (space
committed a tag) and was rejected by the backend when pasted, chips
could not be edited, invalid addresses only failed server-side after
pressing Send, and there was no autocomplete at all.
## The model
A recipient is `{ address, displayName? }`. Person and workspace member
are never stored in composer state; they are resolved live from the
address at render time, mirroring how `MatchParticipantService` links
`messageParticipant.handle` to `personId`/`workspaceMemberId` on the
receive side. Entities appear at the edges (autocomplete in, chip
display out); state, dedupe, validation, and send operate on addresses
only. The send path is unchanged: `SendEmailInput.to/cc/bcc` stay
comma-separated bare addresses.
# What changed
New module `activities/emails/recipients/` (the workflow editor is
untouched; its other consumers are unaffected):
- **`EmailRecipientsFieldInput`**: wrapping chip rows (up to ~3 lines,
then scroll), commit on Enter/Tab/comma/semicolon/blur, space commits
only when the buffer is already a valid email, paste parses RFC 5322
lists (names, quoted commas, semicolons, newlines), case-insensitive
dedupe with a flash on the existing chip, invalid addresses become red
chips that disable Send, double-click or keyboard editing in place with
Escape revert, Backspace select-then-delete, arrow-key chip navigation,
Ctrl/Cmd+Enter commits a pending buffer or sends when the buffer is
empty.
- **Person resolution**: chips resolve against People
(`emails.primaryEmail`, case-insensitive) and workspace members,
rendering avatar + name when known and degrading to a plain address chip
otherwise.
- **Chip menu**: person/member header, Copy email, Edit, Remove, and Add
as person for unknown addresses (creates the Person; the chip upgrades
in place).
- **Autocomplete**: blends context people (company you are composing
from, or the company behind a person/opportunity), ranked people search,
workspace members with a Team member badge, and a literal "Use this
email" row ranked first when the typed buffer is a valid address.
Suggestions exclude addresses already present in any field. Enter picks
the highlighted or top row.
- **Prefill**: replies and drafts preserve participant display names
(`getEmailDraftPrefillFromMessage`, `useReplyContext`).
- `useEmailComposerState` holds `EmailRecipient[]` per field and blocks
send on invalid recipients; the recipient-limit warning is surfaced
again in the composer.
- The Send Email engine command passes the record context so context
suggestions work from the record page action.
- `EmailsFilter` was missing from the shared `LeafFilter` union, so
nothing could filter on `emails.primaryEmail`; added (additive).
- New dependency `addressparser@1.0.1` in twenty-front, the same package
and version the server already uses to parse inbound mail headers, so
both sides parse identically. Tiny, dependency-free, browser-safe.
# Decisions and tradeoffs
- Person resolution matches on `emails.primaryEmail` only,
case-insensitively via per-address `ilike` filters (no `%` wildcards,
`%_\` escaped). `additionalEmails` is a JSONB array and not cleanly
filterable through the GraphQL filter API today; the server-side matcher
checks additional emails too, so a chip may show as a plain address even
though the send still links to the person via participant matching.
- Chip flash-on-duplicate replays its CSS animation by remounting the
chip subtree (nonce in the React key), chosen over animation-restart
hacks; the remount is invisible.
- Keyboard chip selection keeps DOM focus on the input and tracks a
virtual `selectedChipIndex` (`aria-activedescendant`) instead of roving
focus across chips: one focus point, no focus juggling, standard
combobox listbox pattern.
- `flushSync` (precedent: `Dropdown.tsx`) focuses and places the caret
after entering chip-edit mode; the alternative was a useEffect on
editing state.
- Suggestion rows `preventDefault` on mousedown so picking a suggestion
never blurs the input (blur would first commit the half-typed buffer as
a junk chip).
- Cmd/Ctrl+Enter inside a recipient field: with a non-empty buffer it
commits the buffer only; with an empty buffer it sends via an `onSubmit`
prop wired to `handleSend`. Not commit+send in one stroke: `handleSend`
holds a same-render closure over composer state, so sending in the same
event would read the pre-commit recipients. E2E also showed the side
panel's own ctrl+Enter hotkey never fires while any form field is
focused (focus-stack scoping, applies to the old composer too), which is
why the field triggers the submit itself.
- Enter with suggestions open picks the highlighted (or top) suggestion,
Gmail-style. When the typed buffer is itself a valid email, the literal
row is ranked first so Enter keeps meaning "add what I typed".
- Suggestions are disabled while editing a chip (the edit buffer holds
`Name <email>` text, a poor search query).
- Dedupe blocks within a field; across fields typed duplicates are
allowed (sometimes intentional), but suggestions exclude addresses
already present in any of To/Cc/Bcc.
- Chip menu actions never navigate: navigating the side panel (or main
view) unmounts the composer and silently destroys the draft, since
composer state is component-local with no draft persistence. "Add as
person" creates the record and shows a snackbar while the chip upgrades
in place; the person header row is informational. "Open person"
navigation should come back once drafts survive navigation.
- The reply composer gets no context record: its widget target record is
the message thread, not a person/company, and replies already prefill
participants.
- If two people share a primary email, the last fetched match wins for
chip display (no ambiguity UI).
- "Add as person" splits the display name on the first space for
firstName/lastName, the same heuristic the contact-creation manager uses
server-side.
# Deferred
- Display names on the wire (`Name <email>` in outbound headers): needs
`SendEmailInput` / `EmailComposerService.validateEmails` changes
server-side.
- Drag chips between To/Cc/Bcc; collapse-on-blur to one line with a "+N
others" summary.
- Frequency/recency ranking of suggestions from `messageParticipant`
aggregates.
- "Open person" from the chip menu, pending draft persistence across
navigation.
# Verification
Unit tests cover the parser, formatter round-trip, merge/dedupe, and the
field state machine (commit, dedupe flash, edit, cancel, keyboard
selection). Typecheck, lint, and the email module suites pass, plus the
shared and side-panel suites.
Every flow was also driven end to end with Playwright against seeded
data: prefill resolution, context and typed suggestions, keyboard
navigation and picks, dedupe flash, RFC 5322 paste, invalid chips gating
Send, wrapping, in-place editing, chip menus, clipboard copy, Add as
person with live chip upgrade, Cc/Bcc exclusions, and the Ctrl+Enter
send path (the mutation reached the server; it failed only on the seeded
account's missing refresh token, expected outside a real provider
connection).
Screenshots of each verified behavior:
https://claude.ai/code/artifact/1743f05d-422e-43d0-bbea-a34a0470c180
---
_Generated by [Claude
Code](https://claude.ai/code/session_0199wDARiw48GqVTpgWzbXWw)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22668?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. -->
|
||
|
|
3dd7dfde5a |
feat(billing): collect credit card in-app when starting subscription from billing page (#22706)
## Context
On the billing page, a trialing workspace without a payment method that
clicks "Increase" (credits section) or "Subscribe Now" (subscription
card) goes through the end-trial confirmation and then gets redirected
to the Stripe billing portal to add a card. We already have an in-app
card collection flow (`AddCreditCardModal`, Stripe PaymentElement on a
SetupIntent) used by the AI chat credits banner and the global end-trial
information banner. The billing page was the last surface still bouncing
users to the portal.
## What this does
Applies the same pattern as `AIChatNoMoreBillingCreditsBanner` and
`InformationBannerEndTrialPeriod` to the billing page:
- When the workspace is trialing and `billingHasPaymentMethod ===
false`, the `endTrialPeriod` modal now renders `AddCreditCardModal`
(in-app card entry) instead of the confirmation modal. Once the card is
added, the subscription starts via `endTrialPeriod({
skipPaymentMethodRedirect: true })`, and the user stays on the billing
page.
- When a payment method exists (or the flag is unknown), the existing
"Start Your Subscription" confirmation modal is unchanged, including its
portal-redirect fallback if the backend reports no card.
- Both entry points share the same modal instance
(`BILLING_MODAL_IDS.endTrialPeriod`), so the "Increase" button in the
credits section and the "Subscribe Now" header action both get the
in-app flow. Button labels are unchanged: they state the outcome, and
the modal explains the card step.
- `AddCreditCardModal` now closes only after `onPaymentMethodAdded`
resolves, so the form keeps a visible loading state until the
subscription is confirmed instead of closing instantly while activation
runs in the background. This also benefits the AI chat and information
banner flows that share the component.
## What stays the same
- No backend changes. `endSubscriptionTrialPeriod` still verifies the
payment method live against Stripe and calls
`ensureDefaultPaymentMethod` before activating, so the card added
through the SetupIntent becomes the default charged card.
- 3DS or redirect-based payment methods return to the billing page with
the `start-subscription-after-payment-method` param, which the globally
mounted `EndTrialAfterPaymentMethodGater` already handles.
- The trial banner ("Trial ends X, please add card details") keeps its
portal flow on purpose: its semantic is add a card while keeping the
trial, whereas `AddCreditCardModal` starts the subscription immediately.
Converting it would need a variant of the form that skips the auto-start
param. Same for the past-due "Update payment" path. Both are possible
follow-ups.
- All strings reuse existing Lingui msgids, no catalog changes needed.
## Testing
- `oxlint`, `oxfmt` and `nx typecheck twenty-front` pass.
- All 17 `settings/billing` jest suites pass.
- Could not drive the Stripe flow end-to-end in this environment (no
Stripe test keys); the composition is identical to the two surfaces
already shipped, and the state matrix (`hasPaymentMethod` false / true /
unknown, trialing / not trialing, permission gating) was traced through
both entry points.
|
||
|
|
a0cf4cc9e1 |
i18n - translations (#22714)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22714?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
dcccdbd148 |
Fix flaky "read EINVAL" in integration tests: bump msw to 2.12.14 (#22702)
# Context
Part of a CI flakiness sweep. Integration shards are intermittently
killed by an uncaught socket error that poisons a whole spec file (e.g.
`sync-failure-lifecycle.integration-spec.ts`, 4 tests, on unrelated PR
branches — example run 28940565117, shard 8):
```
Error: read EINVAL
at MockHttpSocket.emit (@mswjs/interceptors/.../MockHttpSocket.ts:161)
```
# Root cause
The integration setup routes **all** HTTP (including
supertest/node-fetch calls to the in-process app) through msw's
ClientRequest interceptor, with localhost passthrough. msw `2.12.7` pins
`@mswjs/interceptors` `0.40.0`, where:
- `passthrough()` aliases the real socket's libuv `_handle` onto the
mock socket (two owners of one handle) and forwards the real socket's
`error` events into `MockHttpSocket.emit`,
- `destroy()` never destroys the passthrough socket, so it stays
orphaned with its error-forwarding listener attached.
When the server side closes such a connection later, a read on the stale
shared handle yields `EINVAL`, forwarded into `emit()` with no request
(and no error listener) attached → uncaught exception → jest kills the
in-flight file. This is upstream
[mswjs/interceptors#753](https://github.com/mswjs/interceptors/issues/753);
the stack frames match line-for-line.
# Fix
Bump `msw` to **exactly `2.12.14`** in `twenty-server` and
`twenty-front` (shared lock entry). msw 2.12.9+ requires interceptors
`^0.41.2` → resolves 0.41.9, which ships
[interceptors#755](https://github.com/mswjs/interceptors/pull/755):
passthrough sockets get their listeners removed and are destroyed on
close, severing the exact error path above.
Notes from adversarial review:
- Pinned exact (`2.12.14`, not `^`) because the caret would resolve to
2.15.0 today — a bigger jump than reviewed.
- Compatibility checked: no deep imports of msw/interceptors anywhere;
interceptors 0.41.x externals already covered by
`transformIgnorePatterns`; `msw-storybook-addon@2.0.6` peer range
satisfied; `mockServiceWorker.js` integrity checksum unchanged between
2.12.7 and 2.12.14; 0.41.7+ adds Node 24 compatibility (repo targets
Node 24).
- Residual risk disclosed: #753 is still open upstream and the `_handle`
aliasing remains in 0.41.9 — this removes the observed orphaned-socket
path, but keep an eye out for recurrence.
Dependency-only change: 2 package.json lines + lockfile.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01AtD2wWm3EthV6t3Hs31QyB)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22702?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. -->
|
||
|
|
e38d587602 |
Welcome animation when a workspace is set up (#22622)
## Desktop https://github.com/user-attachments/assets/87999ff5-9257-4fed-90e0-781322b1db98 ## Mobile https://github.com/user-attachments/assets/0d115cb6-7bca-4229-8fde-0f188e862b00 Greets the user the moment they finish onboarding: a halftone Twenty mark (brand purple) assembles from scattered particles, shimmers, then bursts outward to reveal the freshly loaded workspace behind it. Plays once, ~3s, with a reduced-motion fallback. `PageChangeEffect` sets a transient `isWelcomeAnimationVisibleState` atom at the onboarding to workspace redirect seam (gated by `shouldShowWelcomeAnimationOnNavigate`); `WelcomeOverlay`, mounted in `WorkspaceAppProviders`, plays it and clears it. Transient by design, so it never replays on reload. ## Why a canvas rendered on another thread The overlay plays at the exact moment the workspace mounts behind it, which is one of the heaviest main-thread stretches in the app (metadata, providers, first data fetch). Anything animating on the main thread competes with that work: - framer-motion and plain main-thread `requestAnimationFrame` visibly stuttered and froze during the mount, because long tasks starve rAF. - CSS keyframes stay smooth (they run on the compositor), but can't drive a ~2900-particle halftone with per-particle physics (staggered assemble, moving shimmer band, directional burst). So the halftone runs as a Canvas 2D particle system inside a Web Worker, drawing to an `OffscreenCanvas` transferred from the main thread. The whole render loop lives off the main thread, so app-mount jank can't reach it. A main-thread path is kept as a fallback for browsers without `OffscreenCanvas` / `transferControlToOffscreen`, and Safari workers (which lack rAF) fall back to a fixed-interval loop. The renderer is a pure, DOM-free module so it bundles cleanly into the worker; dot colors come from CSS vars read on the main thread and passed in. Also fires for every onboarding completion path and skips the billing `PlanRequired` detour. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22622?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. --> |
||
|
|
cc7b41db0e |
feat(ai-chat): inject current date & per-message timestamps into agent context (#22632)
## Summary
Gives the AI chat agent temporal awareness by injecting the current date
into
the system prompt and a per-message "sent at" timestamp into each user
message,
formatted in the member's timezone. Also hardens all timezone formatting
against
the `"system"` sentinel value, which was crashing the stream job.
## What changed
**Message timestamps (new)**
- Added `injectMessageTimestamps` util: prepends a
`<message_timestamp>Sent: …</message_timestamp>`
text part to each user message before it's sent to the model, so the
agent can
reason about "yesterday", "last week", etc.
- `loadMessagesFromDB` now stores the message time in the canonical
`metadata.createdAt` slot (ISO string, JSON-serializable for the BullMQ
job
payload) instead of a non-typed top-level `createdAt` field that nothing
read.
- Migrated the AI chat message pipeline from the generic `UIMessage` to
the
typed `ExtendedUIMessage` (`chat-execution.service`,
`extract-code-interpreter-files`,
`replace-unsupported-file-parts`, and related types), since
`metadata.createdAt`
is declared on `ExtendedUIMessage`.
**Current date in context**
- System prompt now includes `Current date: …` formatted in the member's
timezone
(`system-prompt-builder.service`).
- Settings › AI prompt preview mirrors the same `Current date` line.
**Timezone safety (bug fix)**
- Workspace members default `timeZone` to the `"system"` sentinel, which
is only
resolvable client-side. Passing it (or any invalid IANA zone) to
`Intl.DateTimeFormat` throws `RangeError: Invalid time zone specified:
system`,
which was failing the stream job.
- Added `getValidTimeZoneOrUndefined`, which returns a valid IANA zone
or
`undefined` (letting the runtime fall back to its default). Used in both
`injectMessageTimestamps` and `formatCurrentDate`. This mirrors the
existing
`isValidTimeZone` convention in the calendar module.
## Notes / follow-ups
- For members who never changed `timeZone` from `"system"`, timestamps
fall back
to the server's default zone (UTC). To honor their real local time, the
frontend would need to send the browser-detected zone with the chat
request
(the same way calendar/charts already pass a resolved zone). Not
included here.
## Test plan
- [x] `inject-message-timestamps.util.spec.ts` — covers timestamp
injection,
assistant messages untouched, invalid `createdAt`, and the `"system"`
timezone no longer throwing.
- [ ] Send a chat message and confirm the agent sees the correct
date/time.
- [ ] Verify a member with `timeZone = "system"` no longer crashes the
stream job.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22632?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. -->
|
||
|
|
3a5545c753 |
chore: remove IS_MESSAGING_CALENDAR_WEBHOOK_ENABLED flag (#22680)
Messaging/calendar webhook subscriptions are now always on; drop the feature flag gate and its enum/public-flag registration. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22680?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. --> |
||
|
|
97e21c3403 |
feat(ai): optionally preselect fast or smart model when opening Ask AI (#22679)
Allow front components to preselect the AI model when opening the Ask AI side panel with a preprompt. - Add an optional `model: 'FAST' | 'SMART'` to the preprompt in the openSidePanelPage AskAI params of the front-component SDK. - openAskAiPageWithPreprompt resolves FAST to the workspace fast model and SMART to the workspace default (null selection = pinned default model), writing to agentChatUserSelectedModelState. |
||
|
|
5f3f734b34 |
fix(front): page header title overlap, Cmd+K on page layout pages & stable tooltip id (#22678)
## Summary
Three independent front-end fixes.
### 1. Settings page header title overlaps the breadcrumb
On settings detail pages (e.g. an app's logic function), a long centered
title visually overlapped the breadcrumb.
`PageCardHeader` renders the header as a CSS grid (`minmax(0, 1fr) auto
minmax(0, 1fr)`) and the centered title used `justify-self: center`.
With grid, `justify-self: center` sizes the item to its own content
width (up to its `max-width`) instead of to its grid track, so a long
title grew wider than the center track and spilled sideways over the
breadcrumb column — its `overflow: hidden` only clipped its own children
to that oversized box, not to the track.
Fix: let the centered title fill and shrink to its grid track so it
clips (with ellipsis) inside its own column instead of overflowing into
the breadcrumb.
- Center column: `auto` → `minmax(0, auto)` so it can shrink when space
is tight.
- Centered title: `justify-self: center` → `justify-self: stretch` +
`min-width: 0`.
### 2. Cmd+K does nothing on standalone page layout pages
On standalone page layout pages (`/page/:pageLayoutId`, used to render
app front components), the command menu shortcut (Cmd+K) did nothing.
Cmd+K is a global hotkey with a modifier, so it only runs when the
active focus-stack config has `enableGlobalHotkeysWithModifiers: true`.
`RecordIndexPage` and `RecordShowPage` explicitly reset the focus stack
to enable it, but `PageChangeEffect` had no case for
`AppPath.PageLayoutPage`, so the stack kept a stale config (commonly the
Settings config, which disables modifier hotkeys) and swallowed the
shortcut.
Fix: add a `PageLayoutPage` case in `PageChangeEffect` that resets the
focus stack with modifier hotkeys enabled (mirroring `RecordShowPage`),
plus a new `PageFocusId.PageLayoutPage` value.
### 3. Ever-changing / unstable tooltip element id
`OverflowingTextWithTooltip` built its element id from `title-id-${+new
Date()}`, so a new id (the current epoch time in ms) was generated on
every render — the id visibly kept increasing in the DOM. This is
unstable (the tooltip anchor `#id` churns on each render) and
collision-prone (two tooltips rendering in the same millisecond get the
same id, producing duplicate DOM ids and an ambiguous anchor).
Fix: derive the id from React's `useId()` so it is stable per instance
and unique. The colons `useId()` produces are stripped, since the id is
used inside a CSS selector (`anchorSelect={#${id}}`) where colons are
invalid.
## Test plan
- [ ] Open a settings detail page with a long title (e.g. an app logic
function named `maintain-account-team-member-name-on-created`) and
confirm the title no longer overlaps the breadcrumb, and truncates with
an ellipsis when space is tight.
- [ ] Navigate to a standalone page layout page (an app's
front-component page) and confirm Cmd+K opens the command menu,
including after coming from Settings.
- [ ] Inspect an overflowing title/tooltip in DevTools and confirm its
`id` stays stable across re-renders (no longer increments), and tooltips
still show on hover of truncated text.
|
||
|
|
0ea0c23556 |
refactor(app-marketplace): rename featured to vetted (#22674)
## What Renames the application-registration "featured" flag to "vetted" across the backend, frontend, GraphQL schema/DTOs, and the marketplace UI. "Vetted" better describes what the flag actually does today: it marks an app as reviewed and approved by the Twenty team (a trust signal), rather than "featured" which reads as spotlighting/promotion. The admin toggle description was already "Mark this app as reviewed and approved". ## How - Renamed `isFeatured` -> `isVetted` on the `ApplicationRegistration` entity, DTOs (`MarketplaceApp`, `MarketplaceAppDetail`, `UpdateApplicationRegistrationPayload`), services, GraphQL fragments, and the settings/admin UI (labels: "Featured" -> "Vetted", "Featured only" -> "Vetted only", etc.). - Renamed the `MARKETPLACE_FEATURED_APPLICATIONS` constant/file to `MARKETPLACE_VETTED_APPLICATIONS`. - Regenerated GraphQL client artifacts (`generated-metadata`, `generated-admin`, `twenty-client-sdk`). ### Database The `isFeatured` column is renamed in place to `isVetted` via a single 2.20 fast instance command (`ALTER TABLE ... RENAME COLUMN`). No new column, no data-copy backfill. - Since all 2.19 commands (including the existing `isFeatured` backfill) complete before any 2.20 command runs, the rename carries over the values that backfill set. - The entity uses `@WasRenamedInUpgrade` so the upgrade-aware layer queries the old column name until the rename step runs during an upgrade. ## Testing - `nx typecheck` and `nx lint:diff-with-main` pass for twenty-server and twenty-front. - Ran `database:reset` on a fresh dev DB: the 2.19 `isFeatured` backfill runs first, then the 2.20 rename; the column ends up as `isVetted` (and `isFeatured` no longer exists), values preserved. - Booted the server: the `@WasRenamedInUpgrade` decorator validates against the upgrade sequence, and GraphQL introspection confirms all four types expose `isVetted` and none expose `isFeatured`. - Ran the three `graphql:generate` configs and the SDK metadata client generator so the committed generated files match the generator output (field ordering included). ## Notes - The `api-breaking-changes` check flags the removal of the `isFeatured` GraphQL field — that is expected and inherent to this rename. - Translation catalogs (`locales/`) are intentionally not touched here since they are managed via Crowdin; new English strings render via Lingui's default-message fallback until translated. |
||
|
|
e751c589ca |
[Billing for self hosts] Add info in description (#22660)
Clarify how the same enterprise key is reused for one prod + one dev instance. Suggest to save / store enterprise key in clear somewhere. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22660?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: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> |
||
|
|
9423af7f67 |
feat(server): add public marketplace resolver for vetted app catalog (#22647)
## What Adds a public GraphQL resolver so unauthenticated clients (the public website) can read the listed/vetted marketplace catalog without a workspace token. - `MarketplacePublicResolver` (metadata schema) exposes two public queries guarded by `PublicEndpointGuard` + `NoPermissionGuard`: - `publicMarketplaceApps` - `publicMarketplaceAppDetail(universalIdentifier)` Both delegate to the existing `MarketplaceQueryService` (no new logic, no new REST routing). The existing workspace-guarded `findManyMarketplaceApps` / `findMarketplaceAppDetail` queries are untouched. - Adds a shared `ApplicationCategory` type in `twenty-shared` (known values plus `string` for backward compatibility) used to type `ApplicationManifest.category`. A warning is logged server-side when an app declares a category outside the known set. ## Why This is the backend half of the public apps marketplace on the website. Splitting it out so the server-side catalog exposure can be reviewed independently from the website UI. ## Follow-up The website PR (the `/apps` marketplace UI) consumes `publicMarketplaceApps` and should merge after this one. --- _Generated by [Claude Code](https://claude.ai/code/session_01GBfegArtJcoiTLSsnWPH8R)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22647?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: martmull <martin@twenty.com> |
||
|
|
72d728ea8e |
fix(messaging): add sender name to IMAP/SMTP From headers (#22603)
Manual IMAP/SMTP outbound emails were being composed with a bare email address in the From header, so recipients did not see the sender's display name. Gmail already built a proper sender header from Google profile data, but manually configured IMAP/SMTP accounts had no equivalent path and fell back to the raw address only. Fix this by storing the optional sender display name in the IMAP/SMTP/CALDAV connection parameters, exposing it through the settings flow and metadata API, and reusing a shared From-header formatter when composing outbound messages. The formatter now builds a properly encoded sender header when a name is available and falls back to the bare email address when it is not, keeping the behavior safe for blank or missing names. Gmail keeps using its existing Google-derived display name source; this change only brings manual accounts up to the same header formatting standard and removes duplicated formatting logic between outbound drivers. After this change, manual SMTP sends and IMAP draft creation include the configured sender name in the From header, while blank names are normalized away instead of producing malformed headers. Existing manual account names are preserved when updates omit the field, and edited accounts can still explicitly clear it through the settings flow. Add focused utility coverage for the shared From-header formatter. Fixes: #22608 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22603?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> |
||
|
|
b5b6e110b0 |
Merge app Public URL and Public Domains settings into one App URL section (#22666)
## Context
The app settings tab showed two sections for the same concept: a "Public
URL" block with a very technical description (Permissions-Policy, COOP,
COEP), a blue info banner about the legacy `/s/` endpoint, and a
separate "Public Domains" block. Confusing, and scary-sounding for apps
whose routes are not really "public".
## Changes
**One merged "App URL" section** (still only rendered for apps that
actually expose HTTP-triggered functions):
- Title renamed to the neutral "App URL" with a one-line description:
"This app's routes are served from this URL. Add a custom domain to use
your own."
- Read-only copyable base URL input, custom domain list card right below
it.
- Removed the info banner and all header/CORS jargon.
**Always show the real URL**: `getFunctionsBaseUrl` now takes
`serverBaseUrl` and falls back to `${serverBaseUrl}/s` instead of
returning `undefined`, so self-hosted instances (no dedicated function
domain) see their actual base URL instead of nothing. This centralizes
the fallback previously duplicated in `FrontComponentRenderer` and
`getLogicFunctionHttpUrl`.
**"Public Domain" renamed to "Custom Domain"** in all user-facing
strings (add card, footer button, detail page, snackbars). Internal
identifiers, GraphQL types and routes keep the `PublicDomain` name to
match the backend entity.
**Polish**:
- Domain rows and the add card use a world icon instead of the mail
icon.
- Row description shows "Added x days ago" instead of a raw ISO
timestamp, via a new shared `useGetAddedRelativeDateDescription` hook
also adopted by the approved access domains card that had the same
inline helper.
- `FrontComponentRenderer` consumes `functionsBaseUrl` from
`useGetLogicFunctionHttpUrl` instead of re-deriving it from the same
atoms.
- User docs updated accordingly.
## Testing
- Ran the app locally (Postgres/Redis + server + front), synced a
fixture app with an HTTP-triggered logic function, and verified the
merged section renders correctly with the copyable URL and Add Custom
Domain card, no banner, no duplicate section.
- `getLogicFunctionHttpUrl` unit tests updated and passing (7/7), `npx
nx typecheck twenty-front` and `lint:diff-with-main` green.
- Locale catalogs are untouched; Crowdin sync regenerates them from the
new source strings.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01NyEWoybMBq62cSNfAFdJAT)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22666?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. -->
|
||
|
|
ad74f75e6a |
Make onboarding steps responsive on mobile (#22659)
https://github.com/user-attachments/assets/5840936c-d9b8-412f-bfec-2d5af768660c The onboarding flow was built for a fixed 440px design with no responsive handling, so on a phone the steps were cramped: oversized padding/gaps, a decorative import-preview illustration that clipped, and side-by-side name fields. Adds targeted `@media (max-width: 768px)` rules (using the existing `MOBILE_VIEWPORT`): - Shared step page: smaller padding and gap on mobile (cascades to every step). - `UpgradeFreeTrial`: its own mobile padding (it overrides the base padding). - Header: reduced side padding. - Import preview: hide the floating calendar cards on mobile (their fixed offsets are tuned for the 440px card). - Create profile: stack the avatar + name fields vertically on mobile. Verified each step at 375px via Storybook; desktop is unchanged (media queries gated to <=768px). <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22659?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. --> |
||
|
|
48730df0d2 |
feat(workflow): scaffold core workflowVersion entity + trigger cache (phase 0) (#21674)
## What **Phase 0 (scaffold)** of migrating `workflowVersion` data to **core**. Gated by `IS_WORKFLOW_VERSION_IN_CORE_ENABLED` with **no behavior change** — nothing reads or writes the new core entity yet. ## Plan `workflowVersion` becomes a thin **workspace shell** over a core entity (the `dashboard`/`pageLayout` pattern), so navigation, the metadata relations, and the record UI keep working while the heavy data (`triggers`, `steps`) lives in core. Trigger dispatch will derive from active core versions via a per-workspace cache, letting us **eliminate** the denormalized `workflowAutomatedTrigger` object. `workflow` and `workflowRun` stay as workspace objects. Phases: **0 — scaffold (this PR)** → A — backfill + dual-write → B — switch reads to core → C — drop the workspace `trigger`/`steps` columns + the `workflowAutomatedTrigger` object. ## Included - **Core `WorkflowVersionEntity`** (`extends WorkspaceRelatedEntity`) — stores version data, with triggers as an **array** (`triggers: WorkflowTrigger[]`), a long-due shape change. Storage only: dispatch reads the primary trigger, so behavior stays single-trigger for now. - **Fast create-table instance command** for `core."workflowVersion"` (v2.19.0). - **`IS_WORKFLOW_VERSION_IN_CORE_ENABLED`** feature flag. - **Per-workspace automated-trigger cache provider** deriving CRON/DATABASE_EVENT dispatch from the active version's trigger — groundwork for removing `workflowAutomatedTrigger`. ## Notes - `WorkspaceRelatedEntity`, **not** `SyncableEntity`: this is user runtime data (like `connectedAccount`/`apiKey`/`file`), not application-manifest metadata. - No frontend behavior; the generated `FeatureFlagKey` enums are updated to include the new flag. |
||
|
|
6554440bb1 |
fix: dropping favorites to the last position in a open folder (#22360)
Fixes #9213 Before: https://github.com/user-attachments/assets/0855f9c1-07c7-4080-8b5f-94fbe3c8b505 After: https://github.com/user-attachments/assets/6ae26e89-9fe5-4ebe-b512-d8287ed2c070 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22360?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> |
||
|
|
be68c0cbab |
fix: corrected grammar in approved domain message (#22646)
title says it all- quick grammar fix in the workspace approved access domains form <img width="1484" height="610" alt="image" src="https://github.com/user-attachments/assets/bb4ce7a5-6ba4-40a7-a1f9-57c3e8d2dde0" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22646?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. --> |
||
|
|
14e4b5bd31 |
fix(rls): prefill RLS predicate fields when creating related records (#22620)
## What / Why Creating a record from a relation section (e.g. adding a child record from its parent's record page) fails with **"Record does not satisfy security constraints"** for any role restricted by row-level permissions. Root cause: `useAddNewRecordAndOpenSidePanel` builds the create payload with only the label field and the parent FK. Fields required by the role's RLS predicates (e.g. `owner = current workspace member`) are missing, so the server rejects the insert in `validateRLSPredicatesForRecords` with `RLS_VALIDATION_FAILED`. `useCreateNewIndexRecord` (the record table "+ New" path) already handles this via `buildRecordInputFromRLSPredicates()`. The relation-section creation path was simply never updated — same bug class, different entry point. ## How Spread `buildRecordInputFromRLSPredicates()` into the create payload in `useAddNewRecordAndOpenSidePanel`, mirroring `useCreateNewIndexRecord`. The record is then created with the RLS-required fields prefilled (e.g. owner = current member), so it passes server-side validation. No behavior change for roles without RLS predicates: `buildRecordInputFromRLSPredicates()` returns an empty object when there are none. ## Test plan Requires row-level permissions (Enterprise) enabled. 1. Create a role with an RLS predicate `owner IS current workspace member`. 2. Assign it to a non-admin user; create a parent record owned by that user. 3. As that user, open the parent record and add a child record from a relation section (the "+" on a one-to-many / many-to-one relation field). 4. **Before:** "Record does not satisfy security constraints". **After:** the child record is created, with owner prefilled to the current member. Also verified via REST against a self-hosted instance: inserting the child record without the owner field is rejected (HTTP 400, RLS_VALIDATION_FAILED); inserting it with `ownerId = current member` succeeds (HTTP 201). <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22620?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. --> |
||
|
|
4b127451b4 |
Fix root domain /authorize rendering workspace-scoped consent page (#22641)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22641?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. --> |
||
|
|
1914b11de2 | Unify featured/vetted app terminology to featured (#22648) | ||
|
|
435073e9c5 |
Display featured applications in marketplace (#22635)
## After <img width="1060" height="589" alt="image" src="https://github.com/user-attachments/assets/74dfadcf-8698-4404-81c6-b309cc4cbf79" /> <img width="732" alt="image" src="https://github.com/user-attachments/assets/0e1a3644-04bc-4208-aa77-3842d9db9cc8" /> <img width="797" alt="image" src="https://github.com/user-attachments/assets/0456ecce-607a-4705-8a89-c77029bfb6ac" /> - Remove IS_MARKETPLACE_SETTING_TAB_VISIBLE feature flag - add vetted toggle in admin app tab - added people data labs, last contact and call recorder to default vetted applications <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22635?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> |
||
|
|
35d3f9b89d |
fix(ai-chat): sort message parts by orderIndex on reload (#22629)
## Problem When an AI chat conversation is reloaded from the DB (page refresh or initial load), message parts are returned without guaranteed ordering. The renderer groups reasoning/thinking steps only when they are **contiguous** — so if `reasoning` parts land after the `text` part, thinking blocks appear below the final answer, and can appear duplicated or split. This only surfaces with reasoning models (OpenRouter, etc.) because those produce multiple reasoning/tool/text parts per message, making ordering observable. Simple text-only messages aren't affected. ## Root cause `AgentChatService.getMessagesForThread()` fetches `parts` via a TypeORM relation with no ORDER BY on `orderIndex`. The DB can return parts in any order. `mapDBMessagesToUIMessages()` then calls `dbMessage.parts.map(...)` directly, without sorting. A parallel server-side utility (`mapDBPartsToUIMessageParts.ts`) already sorts by `orderIndex` — this fix makes the frontend fetch path consistent with it. ## Fix Sort parts by `orderIndex` before mapping to UI parts in `mapDBMessagesToUIMessages.ts`. ```ts parts: [...dbMessage.parts] .sort((a, b) => a.orderIndex - b.orderIndex) .map(mapDBPartToUIMessagePart), ``` `orderIndex` is already included in `GetChatMessagesDocument` — no schema or query changes needed. ## Test 1. Open Ask AI with a reasoning model (e.g. via OpenRouter). 2. Run a prompt that produces thinking steps. 3. Hard-refresh the page. 4. Thinking blocks should appear collapsed above the final answer, not below it. Closes #22386 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22629?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. --> |
||
|
|
71ad0fb5fc |
Remove logo from onboarding trust badges (#22628)
Removes a logo from the trusted-by cluster on the onboarding import-contacts step and deletes the unused asset. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22628?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. --> |
||
|
|
99f54d9ea8 |
fix(front): honor user-set label on LINKS/URL social links (#22586)
## Problem On LINKS and URL fields, recognized social links **always** rendered the derived handle (e.g. `@cristiano`) and ignored any user-set `label`. This was a regression: `SocialLink` did `getDisplayValueByUrlType(...) ?? label`, and since a provider always matches for social links, `label` was never reached. Adding the Instagram/TikTok/Bluesky providers in v2.16 widened the set of affected links. | Input | Expected | Before | |-------|----------|--------| | `instagram.com/cristiano`, label `Cristiano Ronaldo Official` | `Cristiano Ronaldo Official` | `@cristiano` | ## Fix (display half of #22265) - `SocialLink`: prefer a non-empty `label`; only derive the handle from the URL as fallback (then `href`). Prop widened to `string \| null`. - `LinksDisplay` / `LinkDisplay` / `URLDisplay`: pass the **raw, nullable** label into `SocialLink` instead of a pre-coalesced string, so derivation still works when no label is set. `URLDisplay` passes `label={null}` (URL fields have no label) so handles still render. - Stories: dropped `label` args the old code silently ignored (keeps existing visual snapshots stable) and added a `WithCustomLabel` story asserting precedence. ## What's left (not in this PR) The **label input in the UI** (issue's second half) is intentionally deferred. `MultiItemFieldInput` carries the in-progress edit as a single string and seeds edits with the URL only, so exposing a Label field cleanly requires a small generalization of that shared component (not a JSON-serialization workaround). That change needs manual in-app verification and will be a follow-up. ## Verification - `twenty-ui` typecheck clean; oxlint clean on all changed files; `getDisplayValueByUrlType` tests pass (38). - Added Storybook `play` assertion for the custom-label case. Fixes #22265 (display half). Related: #16414. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22586?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. --> |
||
|
|
2a495c3477 |
feat(app): allow claiming ownership of unclaimed app registrations (#22609)
## After <img width="653" height="703" alt="image" src="https://github.com/user-attachments/assets/ebe800da-b00b-4239-99a9-e157f7bfd7a0" /> <img width="634" height="711" alt="image" src="https://github.com/user-attachments/assets/00a048bf-36b1-489f-a080-1ed2d069e625" /> ## Context App registrations track their owner via `ownerWorkspaceId`. Curated / catalog / CLI apps are seeded **unclaimed** (`ownerWorkspaceId: null`). Until now there was no way to take ownership of an unclaimed app from the UI — the only ownership action was **Transfer ownership**, which requires the caller to already be the owner, so it can't act on a null-owner app. This PR adds a way to **claim** an unclaimed app registration, and makes the owner always visible on the detail page. ## Behaviour Admin panel → app registration detail → General tab: - The **Owner** row is now always shown — an **Unclaimed** tag when there's no owner workspace (previously the row was hidden). - Danger zone buttons are ownership-aware: - **Unclaimed** app → **Delete app** + **Claim ownership** (claims it for the current workspace). - **Owned** app → **Delete app** + **Transfer ownership** (unchanged). Transfer is hidden for unclaimed apps because transferring requires the caller to already own the registration. ## Changes **Backend** - New `claimOwnership` service method: looks the registration up globally, rejects it if it already has an owner, otherwise assigns `ownerWorkspaceId` to the caller's workspace. - New `claimApplicationRegistrationOwnership` mutation, guarded by `WorkspaceAuthGuard` + `SettingsPermissionGuard(APPLICATIONS)` (same guards as transfer). - New `ClaimApplicationRegistrationOwnershipInput` DTO (`applicationRegistrationId`). **Frontend** - **Claim ownership** button (shown only when the registration has no owner workspace); opens a confirmation modal and calls the new mutation. - **Transfer ownership** button now renders only for owned registrations. - The **Owner** row in the general info card is always displayed, with an `Unclaimed` tag when there is no owner. **Generated** - Regenerated the checked-in GraphQL artifacts (`twenty-front` metadata, `twenty-client-sdk` schema/types) against the live server so codegen output matches. ## Verification - `nx typecheck twenty-front` and `nx typecheck twenty-server` pass. - `oxlint` + `oxfmt` pass on all changed source files. - Codegen is idempotent — re-running the three `graphql:generate` configs + `generate-metadata-client` produces no diff. - Verified end-to-end on the running app against the seeded unclaimed `Twenty CLI` registration (Owner shows `Unclaimed`; Danger zone shows Delete + Claim ownership). https://claude.ai/code/session_01U7rbxhBSUQRWBbdP5TmAgZ |
||
|
|
c55bfab11e |
Fix side panel open/close animation glitches in the page header and panel content (#22598)
When the side panel opens or closes, the pinned header button (e.g. New Company) was flat-clipped for the whole 300ms animation: the ⋮ toggle mounts instantly and shifts the button's slot, framer's `layout` prop compensates with a transform that the surrounding `overflow: hidden` containers don't follow, and the ResizeObserver-driven rerenders re-seed that transform every frame. Removing `layout` lets the button follow plain reflow, which cannot clip. The panel content also squeezed during the animation (labels re-truncating at every intermediate width) because the inner panel was `width: 100%` of the width-animating wrapper. Pinning it to `var(--side-panel-width)` turns the animation into a rigid drawer slide; drag-resize is unaffected since it writes the same CSS variable. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22598?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. --> |
||
|
|
d3b79320b1 |
Remove book a call step from onboarding (#22597)
The book a call screen was shown as a dedicated onboarding step after sending team invites. It is no longer part of the flow: the `BOOK_ONBOARDING` status, its pending user var, the `skipBookOnboardingStep` mutation and the `BookCallDecision` screen are removed, and onboarding completes right after the plan step. The `/book-call` Cal.com page remains, reachable only from the "Book a Call" link on the upgrade screen, with a back link to `/plan-required`. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22597?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. --> |
||
|
|
ee28ae363f |
feat(files): use direct-to-storage upload for email and AI-chat attachments (#22610)
## Context Follow-up to the direct-to-storage upload work (#22449 / #22531 / #22533 / #22576). That migrated files-field, attachments and workflow uploads off the buffered path. This PR does the same for the **last two user-facing upload surfaces**: email attachments and AI-chat files. ## What this does - Adds `EmailAttachment` and `AgentChat` to the server's `DIRECT_UPLOAD_FILE_FOLDERS` allowlist. Both folders already resolve through the workspace-custom-application path in `resolveUploadLocation`, so no other server change is needed. - Routes the two frontend hooks through the existing `useDirectFileUpload` handshake (`createFileUpload` → `PUT` → `completeFileUpload`): - `useUploadEmailAttachment` → `FileFolder.EmailAttachment` (keeps its existing `MAX_ATTACHMENT_SIZE` client check — email has a real send-size limit). - `useAiChatFileUpload` → `FileFolder.AgentChat`. Each hook keeps its public signature and return shape, so call sites are unchanged. No schema change and no codegen needed — the `CreateFileUpload`/`CompleteFileUpload` documents and the `FileFolder` enum values already exist in `generated-metadata` from #22576. ## Why these are safe to migrate Both server services (`file-ai-chat`, `file-email-attachment`) just `writeFile` (store) and return a signed URL — no synchronous processing of the bytes at upload time — so the store-and-reference direct-upload flow fits exactly, same as files-field/workflow. ## Out of scope `CorePicture` (avatars, member/workspace pictures, logos) stays on the buffered path on purpose: small images that go through server-side image handling and are served inline, where the 10 MB body limit is already appropriate. ## Tests Extends the `FileUploadService` unit spec with an `it.each` asserting `createFileUpload` supports the `EmailAttachment` and `AgentChat` folders. ## Verification `typecheck` and `lint:diff-with-main` green on both `twenty-front` and `twenty-server`. (The server jest suite couldn't run in my local sandbox due to an unrelated config-import quirk present on a clean `main` checkout too — CI runs it normally.) https://claude.ai/code/session_015UH8KWmsB9zdYaog8MFG1d --- _Generated by [Claude Code](https://claude.ai/code/session_015UH8KWmsB9zdYaog8MFG1d)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22610?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. --> |
||
|
|
8a4bcd1445 |
(Billing for self hosts) Tie enterprise key to server (#22464)
# Enterprise key: bind to a server, free dev instances, self-serve transfer, shorter license ## Summary Enterprise keys were being reused across multiple instances (e.g. one prod + one dev, or several environments), which broke seat accounting and made licensing ambiguous. This PR ties each enterprise key to a **single server**, while giving customers a legitimate, self-serve way to run a **free development instance** and to **move their key** when they replace a server. ## Product behavior ### 1. Enterprise key is bound to one server - The first server to validate an enterprise key **claims** it (claim-on-first-use). From then on, that key is bound to that one server (until unbound - see 3.). - Any other instance that presents the **same key from a different server is hard-rejected**: it does not receive a license, so enterprise features stay off there. - Each instance has a stable server identifier. If one isn't set, the instance generates and persists one automatically on first validation (in keyValuePair table), so existing customers generally don't need to do anything (unless they have disabled config variables in db then they should add it to .env). ### 2. Free development instance - Every enterprise subscription gets **one free, non-billable development instance** in addition to its production instance. - An instance registers as development by declaring its instance type as `development` (done by default when validating the enterprise key, then can be toggled from UI or by updating value in keyValuePair table). - The free dev slot is only granted while there is an **active production instance** on the same subscription (so it's a perk for paying customers, not a way to run for free). - Only **one** dev instance can be active at a time per subscription, and it is **not counted as a billable seat**. ### 3. Self-serve unbind / rebind (transfer) - Admins can **release** the binding from the enterprise settings, which frees the key so it can be **claimed by a new server**. - This is the intended path when **sunsetting an instance and standing up a new one** (migration, re-hosting, disaster recovery): release on the old/dead box, then the new box claims it on its next validation. - To prevent abuse, releases are **rate-limited (10 per rolling 30 days)**; hitting the limit shows a clear message. ### 4. Automatic release of dead servers - If a bound server stops checking in for **14 days**, its binding is considered stale and is **auto-released**, so a replacement can claim the key without any manual step. This covers the case where the old server is already gone and can't release itself. ### 5. Shorter license validity (30 → 7 days) - The license (validity token) now expires after **7 days** instead of 30. The daily background refresh keeps healthy instances licensed transparently. - This limits the value of copying a license from one instance to another, since a copied license now stops working within a week. ### 6. License issuance is rate-limited - Issuing a new license is capped at **twice per 24h, independently for production and for development**. This tolerates the normal daily refresh (including small drift between runs) while blocking bursts of license minting for cloned instances. - Hitting this limit never revokes an existing, still-valid license — the current one keeps working until it expires; the manual "refresh" button just reports that the daily limit was reached. ## What changes for existing self-hosted customers **If you run a single production instance with one enterprise key:** nothing to do. On the next validation your instance reports its server identifier, claims the binding, and keeps working. **If you reuse one key across several instances (e.g. prod + dev, or multiple environments):** only the **first** instance to validate keeps its license. The others will **lose enterprise features**. To migrate: - Keep your production instance as-is (it claims the binding). - For a secondary/testing box, mark it as a **development instance** (set the instance type to `development`) to use the free dev slot — no extra cost. - If you genuinely need multiple production instances, you'll need **separate subscriptions/keys** for each. **If you're replacing a server (decommissioning + rebuilding):** - **Release** the binding from enterprise settings on the old instance, then start the new one — it will claim the key automatically. - If the old server is already gone, just wait for the **14-day auto-release**, or contact support. **Legacy instances that can't persist a server identifier automatically:** set the server identifier explicitly in your environment configuration (the instance logs a message telling you to do so). **Offline instances:** because licenses now last 7 days, an instance that can't reach our licensing endpoint for more than a week will lose enterprise features until it can check in again. > A migration email will be sent to affected customers separately. ## Technical implementation (brief) - Binding state lives in the **subscription's billing metadata** (bound server id + last-seen timestamps for prod and dev, release timestamps, and license-issuance timestamps). No new database is introduced on the licensing side; the billing provider's subscription metadata is the source of truth. <img width="976" height="413" alt="metadata_3" src="https://github.com/user-attachments/assets/ccc64822-e177-4223-a65a-4a4602aedf0e" /> - On each validation, a pure **binding resolver** takes the reported server id + instance type + current metadata and returns `allowed` (with the metadata to persist and whether the seat is billable) or `rejected`. It handles claim-on-first-use, staleness/auto-release, the dev-requires-active-prod rule, and the single-dev-slot rule. - **Rate limits** (release + license issuance) use a shared sliding-window helper stored as pruned timestamp lists in the same metadata, so the metadata self-cleans and never grows unbounded. License issuance uses **separate windows per instance type**. - The self-hosted instance **generates and persists a server identifier** if none is configured, and sends it (plus instance type) as instance metadata on validation. - A rejected binding returns a specific error code; the instance **revokes its stored license** on that code. A license-issuance rate-limit instead **throws a typed exception that surfaces to the manual refresh** while leaving the existing license untouched; the daily refresh job swallows it. - License lifetime is a configurable duration (defaulted from 30 to **7 days**), clamped to the subscription's cancellation date when sooner. |