6e319283c4749daaba74eeea2b6b8b14ca4d453f
13218 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6e319283c4 |
fix: Vite 8/Rolldown build warnings in library packages (#22205)
Clean up Vite 8/Rolldown build warnings that showed up during yarn start: - `twenty-client-sdk`: `relativeImportPath.ts` now imports `node:path`, so the generate bundle treats it as a Node external instead of stubbing it for the browser. - Remove rollup’s `interop: 'auto'` from CJS output options - Rolldown don’t support it and was showing `Invalid key: Expected never but received "interop"`. - Replaced deprecated `inlineDynamicImports: true` with `codeSplitting: false` in the worker config. References: - https://v7.vite.dev/guide/rolldown#option-validation-warnings - https://vite.dev/guide/troubleshooting.html#module-externalized-for-browser-compatibility <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22205?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Signed-off-by: Parship Chowdhury <parshipchowdhury@gmail.com> Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
56deba351b |
feat(ai): reliable bulk data import via code-interpreter (#22209)
## Summary Makes AI-assisted bulk data import (CSV/Excel/spreadsheets) reliable and token-efficient by letting an entire import run inside a single code-interpreter call, with a persistent sandbox session and server-side bulk helpers. Also includes supporting improvements to attachment handling, upsert reporting, and field-permission error messages. ## Changes ### Code interpreter - **Persistent per-session kernel** in `LocalDriver`: a long-lived Python process per `sessionId` keeps variables, imports, and files alive across calls (matching E2B behavior). Falls back to the existing ephemeral per-call path when no session is provided. CAN BE REMOVED, INTERESTING FOR DEV X - Idle watchdog that self-terminates the kernel, configurable via the new `CODE_INTERPRETER_IDLE_TIMEOUT_MS` config variable; the process also exits on parent shutdown (EOF on control fd). CAN BE REMOVED, INTERESTING FOR DEV X - New `bulk_upsert` and `lookup_by` helpers on the sandbox `twenty` object for idempotent batched writes (≤200/batch) and bounded relation-ID resolution. ### Records - `upsert_many_*` now reports a `created` / `updated` / `total` split in its result and log line (new `isFreshlyCreatedRecord` util). ### AI chat - `replaceUnsupportedFileParts`: user-attached files whose MIME type the model can't handle natively (and that aren't code-interpreter-supported) are downgraded to a descriptive text note instead of being sent as unsupported file parts. Modality→MIME mapping drives native support detection. - Finalize dangling tool parts before `convertToModelMessages` to avoid malformed model messages. - Extracted shared types/constants for code-interpreter file extraction. ### Permissions - Field permission-denied exceptions now include the field name and entity name for easier debugging. ### Skill docs - Added the bulk-import recipe ## To do in following PR - [ ] Skill command migration ## Test plan - [x] Unit tests for `getNativeMimeTypesForModalities` and `replaceUnsupportedFileParts` pass - [x] Run a bulk import (>50 rows) end-to-end through the code interpreter and verify a single sandbox call handles read → resolve relations → upsert → summary - [x] Verify session persistence: define a variable in one call, use it in the next within the same session - [x] Verify the kernel self-terminates after `CODE_INTERPRETER_IDLE_TIMEOUT_MS` - [x] Verify unsupported attachments are replaced with a text note for models lacking the modality - [x] Verify `upsert_many_*` returns correct created/updated counts - [x] Verify field-restricted role triggers a permission error naming the field and entity - [ ] Test with [hotel_business.xlsx](https://github.com/user-attachments/files/29376307/hotel_business.xlsx) and simple "import record" prompt <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22209?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. --> |
||
|
|
02d62c4175 |
fix(ai-tools): make navigate_app tool schema a valid object root for direct model binding (#22284)
## Problem
Using an AI Agent step in a workflow fails with:
> Invalid schema for function 'navigate_app': schema must be a JSON
Schema of 'type: "object"', got 'type: "None"'.
## Root cause
The `navigate_app` tool declared its `inputSchema` as a top-level
`z.discriminatedUnion('type', [...])`. When serialized via
`toToolJsonSchema`, a discriminated union produces a **root-level
`anyOf`** with no top-level `"type"`:
```json
{ "anyOf": [ { "type": "object", ... }, ... ] }
<!-- This is an auto-generated description by cubic. -->
<a href="https://cubic.dev/pr/twentyhq/twenty/pull/22284?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. -->
|
||
|
|
71370d9e66 |
fix(server): dedupe in-flight application translation catalog loads (#22285)
## Problem After v2.17.0 shipped app-owned metadata translations (#22235), `POST /metadata` got slower for workspaces with translation-carrying apps installed. Sentry flagged it as an N+1 (`performance_n_plus_one_db_queries`): a single request fires **many** concurrent identical queries: ```sql SELECT … FROM "core"."applicationTranslation" WHERE "applicationRegistrationId" = $1 AND "deletedAt" IS NULL ``` Span aggregates since the deploy: **272** such spans, **avg 74 ms**, **p95 556 ms**, **max 694 ms**, all on `POST /metadata`. HTTP stays 200 — it's latency, not errors. Reported via Sentry `TWENTY-SERVER-HQY` (`twenty-v7`). ## Root cause `ApplicationTranslationCacheService` kept a TTL value cache but had **no in-flight de-duplication**. The object/field metadata resolvers call `getCatalog` directly, per record. On a cold/expired (30 s TTL) cache, many fields of the same app resolve concurrently, all miss, and each fires its own `repository.find` — a classic cache **stampede**, which queues on the connection pool and produces the 556–694 ms tail. Each read also pulls the full per-locale `messages` JSON, so the redundant reads aren't free. ## Fix Rebuild the service on the shared **`PromiseMemoizer`** primitive — the same one `WorkspaceCacheService` and `CoreEntityCacheService` already use. It pairs the 30 s TTL value cache with a `pending` promise map, so concurrent callers for the same registration **share a single in-flight query** instead of stampeding. Per-request query count for a given app goes from N → 1. - Public API (`getCatalog` / `invalidate`) is unchanged — no caller touched. - `invalidate` now clears via `memoizer.clearKeys(...)` (clears both the cached value and any in-flight read), matching `WorkspaceCacheService`. - Adds a unit test asserting 10 concurrent `getCatalog` calls trigger exactly **one** `repository.find`, plus cache-hit / empty-locale / post-invalidation reload cases. ## Notes - Process-local 30 s TTL behaviour is unchanged (deliberate; no cross-process invalidation), this only removes the redundant concurrent reads. - Verified formatting with oxfmt locally; couldn't run the server test suite in this environment, so relying on CI for typecheck/test. https://claude.ai/code/session_01NiE7o3cd3zCLZarVkJa6UA --- _Generated by [Claude Code](https://claude.ai/code/session_01NiE7o3cd3zCLZarVkJa6UA)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22285?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
13f80f0d95 |
fix: ignore IME composition Enter in chat-thread and attachment rename inputs (#22270)
## What's this PR doing? Two inline rename inputs run their action on `Enter` without ignoring the `Enter` that confirms an IME composition: - `AiChatThreadListItem` (renaming an AI chat thread). It also calls `preventDefault()`, so the composition-commit `Enter` is swallowed and the half-typed title gets saved. - `AttachmentRow` (renaming an attachment). The same `Enter` saves the unfinished name. When you type with an IME (Japanese, Chinese, Korean), the first `Enter` after typing confirms the candidate text rather than submitting, so these handlers fire with text the user hasn't finished entering. ## Why The codebase already guards this where keyboard handling goes through `useHotkeysOnFocusedElement` (`if (keyboardEvent.isComposing || keyboardEvent.keyCode === 229) return`), and the inline inputs that don't use that hook add the same check themselves: see `SettingsAccountsBlocklistInput`, `SettingsDevelopersApiKeysNew`, and the sign-up workspace forms. These two rename inputs were just missing it. ## How Add the same `isComposing || keyCode === 229` guard before the `Enter` branch. For input without an IME, `isComposing` is `false` and `keyCode` is `13`, so the rename-on-Enter behavior stays the same. This only skips the action on the composition-commit key. I checked the change against the repo's Prettier config locally. I couldn't add a unit test because jsdom doesn't dispatch real composition events (`isComposing` stays `false`), so it can't reproduce the keystroke. Happy to add an e2e test if that's preferred. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22270?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
68f4cf269d |
i18n - docs translations (#22281)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22281?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> |
||
|
|
655b788e8d |
i18n - docs translations (#22280)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22280?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> |
||
|
|
8cee8d7212 |
i18n - docs translations (#22279)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22279?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> |
||
|
|
a0493dbc4b |
i18n - docs translations (#22276)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
7b17cac772 |
i18n - docs translations (#22273)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22273?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> |
||
|
|
01cba39f71 |
i18n - docs translations (#22271)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22271?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> |
||
|
|
e69c3ae5ce |
fix(website): bump @opennextjs/cloudflare to 1.20.0 (R2 deploy on Node 24) (#22266)
## What The twenty-infra **"Deploy Website"** workflow has failed every run since 06-26 — at OpenNext's R2 incremental-cache step, **not** the build: ``` Failed to provision remote R2 bucket "twenty-website-cache-dev" for binding "NEXT_INC_CACHE_R2_BUCKET": Failed to check whether bucket exists: … Premature close ``` ## Root cause `@opennextjs/cloudflare`'s `ensureR2Bucket()` calls the Cloudflare SDK's `r2.buckets.get()`. On **Node 24** (which the deploy pins) undici truncates the **gzip-compressed** Cloudflare API response → `Premature close`. The SDK's own retries don't help (it's systematic, not flaky), and pre-creating the bucket doesn't help (it always `.get()`s first). This is **pre-existing and unrelated to the multi-locale change** (#22257): the *Build Worker* step succeeds, and the identical error appears on 06-26 runs (two days before that merged). ## Fix opennext **1.20.0** fixes this precisely — it passes `defaultHeaders: { "Accept-Encoding": "identity" }` to the Cloudflare SDK client, so the API returns **uncompressed** responses (no decompression → no premature close). **Node 24 is kept**, and no twenty-infra change is needed — the deploy runs `twenty`'s own `npx opennextjs-cloudflare`, so bumping the dep here is enough. `^1.0.0 → ^1.20.0`. **1.20.0, not the latest 1.20.1**, because the repo's `npmMinimalAgeGate: 3d` still quarantines 1.20.1 (published 06-26); 1.20.0 (06-25) is past the gate and carries the same fix. ## Verification - Resolved to `1.20.0`; confirmed `Accept-Encoding: identity` is in the installed `dist/cli/utils/ensure-r2-bucket.js`. - `nx typecheck twenty-website` green (OpenNext config API unchanged across the bump). - Diff is just `package.json` + `yarn.lock`. ⚠️ Full confirmation needs a **Deploy Website** run (exercises the build + the R2 provision step), which I can't trigger — please re-run it after merge. |
||
|
|
0569c490fd |
i18n - website translations (#22264)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22264?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> |
||
|
|
f8a9a2b249 |
i18n - website translations (#22263)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22263?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> |
||
|
|
627da33424 |
chore: bump version to 2.18.0 (#22256)
## Summary - Moves current version to previous versions array - Sets TWENTY_CURRENT_VERSION to the new version - Updates TWENTY_NEXT_VERSIONS with the next minor version - Bumps twenty-client-sdk, twenty-sdk, and create-twenty-app to the same version ## Checklist - [ ] Verify version constants are correct - [ ] Verify npm package versions match <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22256?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com> |
||
|
|
ddb3abfb23 |
fix(website): config-only Crowdin pull to match short-code catalogs (#22262)
## What `website-i18n-pull.yaml` passed inline `source` + `translation: '…/%locale%.po'` to the Crowdin action *on top of* the config file. After #22257 migrated the website to short-code catalogs (`ar.po`/`es.po`, and `crowdin-website.yml` → `%two_letters_code%`), that inline `%locale%` resolves to **full-code** filenames (`ar-SA.po`/`es-ES.po`) that no longer exist in the repo — so the pull never updates the real catalogs. ## Fix Drop the inline `source`/`translation` so the step is **config-only**, driven by `.github/crowdin-website.yml` (`%two_letters_code%`) — matching the front/app pull (`i18n-pull.yaml`), which is config-only and works. ## Also (ops, not in this diff) The pull doesn't run against `main` — its first step is `git checkout -B i18n-website origin/i18n-website`, so it operates on the long-lived `i18n-website` branch (same pattern as `i18n` for front and `i18n-docs` for docs). That branch was still at the **pre-#22257** layout (full-code `.po`, `%locale%` config, old 3-locale list), so it was reset to `main` to carry the new short-code structure. Once this PR merges, a pull run will land translations into the correct `ar.po`/`es.po`/… catalogs. |
||
|
|
012af11d77 |
feat(website): ship all documentation locales (multi-locale site) (#22257)
## What
The marketing site now serves every language the **documentation** ships
— 14 locales (`en, fr, ar, cs, de, es, it, ja, ko, pt, ro, ru, tr, zh`),
up from 3 (`en, es, fr`).
## How
- **Single source of truth.** `WEBSITE_LOCALE_LIST` derives directly
from `DOCUMENTATION_SUPPORTED_LANGUAGES` (`twenty-shared/constants`).
Add a documentation language → it flows to the website automatically.
- **Off `APP_LOCALES` entirely.** The website locale type is now
`DocumentationSupportedLanguage` (short codes), so a locale **is** its
URL segment — no short↔full mapping, and no `pt-BR`/`zh-CN` ambiguity to
resolve.
- **Removed the indirection this exposed** (it only existed because
`AppLocale` was a superset of the deployed set):
- `locale-to-url-segment` / `locale-by-url-segment` (locale == segment)
- `get-locale-messages` pass-through → callers read `MESSAGES_BY_LOCALE`
directly
- the `messages-by-locale` runtime guard → a total
`Record<DocumentationSupportedLanguage, Messages>` (a missing catalog is
now a **compile** error, not a runtime throw)
- `isWebsiteLocale` → a `string → DocumentationSupportedLanguage` type
guard
- the vestigial language-code `split('-')` in `locale-display-name`
## Catalogs
- Renamed `es-ES → es`, `fr-FR → fr`; added 11 new locales (untranslated
for now → **English fallback**).
- `crowdin-website.yml` switched to `%two_letters_code%`.
- Regenerating catalogs also synced `en.po` with current source
(`Boolean` / `Date & Time` / removed `Fields widget` from the
already-merged #22249).
- `ci-website` is unchanged — no `lingui:compile` step added; catalogs
stay committed.
## Testing
- `typecheck` · `lint` (check-conventions + oxlint + oxfmt) · 347/347
tests — all green. PR CI runs exactly lint + typecheck + test.
## Follow-up (out of repo)
Enable the 11 languages on **Crowdin project 4** so `website-i18n-pull`
backfills real translations. Until then, the new locales render with
English fallback (correct behavior).
|
||
|
|
d81b3c3fa3 |
feat(twenty-sdk): extract & compile app translations into the manifest (#22236)
## Summary **PR 2/4** of the app-metadata-translations stack. Gives app developers the authoring side, as part of the normal manifest build — and it stays out of the way of developers who don't translate. - `twenty-sdk` CLI i18n pipeline: collect translatable strings from the manifest, generate value-as-key message ids (`sha256(value)` truncated, byte-identical to the server's `generateMessageId`), a `dev i18n-extract` command to scaffold per-locale catalog files, and a compile step folded into `build` that emits `manifest.translations`. - Opt-in: no `locales/` dir → `compileApplicationTranslations` returns `undefined` → manifest is unchanged. - Adds an optional `locale` to the front-component execution context so components can translate against the host locale. ## Stack Stacks on #22235 (PR 1/4). Base branch: `claude/app-translation-1-runtime-resolution`. ## Tests Unit (vitest): extract/compile round-trip + message-id determinism. ## Verification note `yarn install` could not complete in the remote dev environment, so typecheck/lint/tests were not run locally — **CI is the source of truth**. https://claude.ai/code/session_01NiE7o3cd3zCLZarVkJa6UA --- _Generated by [Claude Code](https://claude.ai/code/session_01NiE7o3cd3zCLZarVkJa6UA)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22236?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
41c10b9ee7 |
feat(server): resolve app-owned metadata translations at runtime (#22235)
## Summary First of a **4-PR stack** that lets apps built with `twenty-sdk` translate their metadata, resolved at runtime. The standard Twenty app is modelled as "an app like any other" — `NULL applicationRegistrationId` ⟺ the standard app, no special-casing. This PR adds the server foundation and wires runtime resolution for **object** and **field** metadata: - New `applicationTranslation` core table + entity (nullable `applicationRegistrationId`, `locale`, `messages` jsonb), one row per (app, locale) to avoid multi-MB rows. - `ApplicationTranslationCacheService` (process-local, 30s TTL) + `ApplicationTranslationSyncService` (upsert + soft-delete from a manifest). - Shared `translateStandardLabel` util: application catalog → i18n bundle → source value. - Object/field resolvers + dataloaders prefetch and apply the per-app catalog. The new `applicationCatalog` param is **optional**, so standard behaviour is byte-unchanged. - Fast instance command to create the table. ## Stack **PR 1/4**, targets `main`. Followed by: (2) twenty-sdk extract/compile → `manifest.translations`, (3) resolution across the remaining metadata resolvers, (4) the per-locale standard-override editor. ## Tests Unit: `translateStandardLabel`, `resolveObjectMetadataStandardOverride` (including the application-catalog path). ## Verification note The remote dev environment for this branch could not complete `yarn install` (no package-registry egress), so typecheck/lint/tests were not run locally — **CI is the source of truth** for this stack. Changes follow existing patterns. https://claude.ai/code/session_01NiE7o3cd3zCLZarVkJa6UA --- _Generated by [Claude Code](https://claude.ai/code/session_01NiE7o3cd3zCLZarVkJa6UA)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22235?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
538b180824 |
feat(dpa): self-serve Data Processing Agreement generator (#22243)
## What
A single, region-aware DPA that serves all customers, generated
automatically from the customer's deployment. Two layers:
1. **Click-through DPA** — recorded at signup (acceptance = execution),
resolving merge fields from the deployment region. Cloud only.
2. **In-app signed-PDF generator** — Settings → Legal → Generate DPA:
preview the agreement, enter legal entity + authorized signatory,
download a PDF pre-signed by Twenty, and store the executed copy against
the workspace with its template version + timestamp. Deep-linkable at
`/dpa` (login-gated) for `twenty.com/dpa`.
## How it resolves
A typed variable matrix (`dpa-region-config.constant.ts`) maps the
deployment region to the contracting Processor entity and terms:
- **EU (default)** → Twenty.com SAS, hosting EU/Frankfurt, governing law
France, SCC section dormant.
- **US (custom)** → Twenty, Inc., hosting US, SCC section active.
Region is a deployment-wide setting (`DPA_DEPLOYMENT_REGION`, default
EU) behind a `DpaRegionService` seam so it can later become
per-workspace without touching callers. The legal text is verbatim from
the template (generated into `dpa-template.constant.ts` directly from
the source `.docx`); only the 6 merge fields are filled and the SCC
sections (7.2–7.5) stay in the document for every region per the spec —
only field values branch. Sub-processors are deferred to
trust.twenty.com (not enumerated). Billing stays decoupled (Twenty, Inc.
remains merchant of record regardless of Processor).
## UI
Standard list + create-page pattern (mirrors API keys / webhooks): a
list of executed copies (with re-download) — or the agreement preview
when none exists — and a top-right blue **Generate DPA** CTA opening a
standard create page. The "Legal" item is intentionally **not** in the
settings menu; the page is reached via the `/dpa` deep link.
## Notable implementation details
- **PDF** is rendered server-side with `@react-pdf/renderer`. The
built-in standard-14 fonts only encode ASCII and crash on the template's
curly quotes / em–en dashes / accented Latin, so Liberation Sans (OFL)
is **subset to a Latin glyph set and embedded as base64 data: URLs** —
no font files to ship or resolve at runtime (works in dev, prod-Docker
and CI).
- New `core.dpaAgreement` table via a fast instance command (FK hash
reproduced to match TypeORM).
- Self-hosted deployments (billing disabled) skip click-through
recording and stamp a prominent "not a valid agreement" banner on the
preview and PDF.
## Tests
- Unit: resolver (per-region entity/law/SCC state, EU default, no
unresolved `{{ }}`, SCC sections present in both regions, self-hosted
notice) and HTML renderer.
- Integration (`test/integration/graphql/suites/dpa`): preview has no
unresolved fields; `generateSignedDpa` renders + persists + returns a
downloadable PDF (asserted with accented input to guard the font
regression); list re-download.
## ⚠ Needs legal input before go-live (marked `TODO_CONFIRM` in
`dpa-region-config.constant.ts`)
- Registered-office addresses for Twenty.com SAS and Twenty, Inc.
- US deployment governing law (the template only specifies France).
- DPO name and the Twenty pre-signed authorized signatory name/title.
## Out of scope (flagged per spec)
Intra-group legal agreement and any Stripe/billing-entity changes. A
future e-sign provider would plug in at `DpaService.generateSignedDpa` +
the signatory input.
> Draft until the integration test passes in CI and the legal
`TODO_CONFIRM` values are supplied.
https://claude.ai/code/session_01Ahjydxx6J1souz1s1NeA9a
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Ahjydxx6J1souz1s1NeA9a)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22243?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
|
||
|
|
77c84815ef |
feat(website): rework the product-stepper Layout visual (#22249)
## What Reworks the **"Layout" step** visual of the product-page stepper (the floating record-layout-editor scene) to match the Figma references and read as a premium, layered composition. Data is unchanged — this is purely the look. ## Figma alignment - **Fields editor** — the inline-edit field row now keeps the field icon + a bordered input + blue **Done** + the eye / ⋮ (instead of a floating box); **"New fields"** renders as a uniform field row; section headers get an overflow menu; field types read `Boolean` / `Date & Time`. Dropped the redundant "Fields widget" header label. - **"General" widget** — header chevron, the real `anonymousFelix` avatar on Account Owner (reusing the shared asset the other product-feature visuals use), a **Revenue** row, and a money-bag icon for the currency field. ## Premium composition - The nav, record, and Fields editor read as one elevated **z-stack** (back → front): the nav is pushed right so its edge tucks under the record (lowest z-index), the record sits above it, and the editor stays in front — each casting a progressively stronger shadow so the depth ordering is unmistakable. - The **"New record / Enrich / Edit actions"** bar is centered over the nav–record seam (`translateX(-50%)`) to tie the layered stack together at the top. ## Testing - typecheck · oxfmt · oxlint · check-conventions — green. - Verified visually with headless Playwright on `/product` (Layout step). <img width="697" height="734" alt="image" src="https://github.com/user-attachments/assets/58ae7ffb-13d1-4310-bbdc-864589f8bc43" /> |
||
|
|
2a21eb46c0 |
perf: cap to-many relation records per parent and inline chips in table (#22206)
## Problem Record table views that show a to-many relation column (e.g. Workflows with a "Runs" column) get slow and janky to scroll when some records have many related records. Two root causes, found by profiling the page live: 1. **Backend over-fetch + unfairness.** Nested one-to-many relations were loaded with a single flat limit of `QUERY_MAX_RECORDS_FROM_RELATION * parentCount` shared across *all* parents in the page (`WHERE parentColumn IN (ids) LIMIT 60*N`, no per-parent cap). A single hot parent can consume the entire budget — returning thousands of rows for one cell, and potentially starving sibling parents of records they actually have. The `limit * parentCount` shape shows the original intent *was* a per-parent budget; it was just implemented as a global limit. 2. **Frontend DOM explosion.** `ExpandableList` mounts the *entire* child array inline (clipped with `overflow: hidden`) when unfocused, and mounts all children for measurement when focused. A cell with 2,000+ relation chips mounts ~14k DOM nodes — one observed page reached ~55k nodes for 43 rows, producing 100–300 ms main-thread long tasks on every scroll. ## Fix - **Backend:** load one-to-many relations with a true **per-parent** cap via a `LATERAL` join — each parent runs its own indexed, `LIMIT`-ed scan that stops after the per-parent budget. This is `O(perParentLimit × parentCount)` and never reads or sorts a parent's full relation set. The per-parent query is built through the workspace query builder (so it stays schema-qualified and keeps the soft-delete predicate) and wrapped as a `FROM` subquery; read/row-level permissions are enforced when records are hydrated by id, as elsewhere in the relation loader. Many-to-one is unchanged. - **Frontend:** add an opt-in `maxInlineCount` to `ExpandableList` so to-many relation cells mount only a small inline preview; the expand dropdown still renders the full fetched set. Fully backward compatible (no cap → identical behavior). ## Why LATERAL over a window function A windowed `ROW_NUMBER() OVER (PARTITION BY parent) <= limit` is correct and fair too, but a window function **cannot stop early within a partition** — it must read every matching row (and sort it). Measured on skewed data (one parent with ~4k children, on the existing single-column join index, PG16): | Approach | Time | Buffers | Rows read from the hot partition | |---|---|---|---| | Pre-PR (`LIMIT 60×N`) | 1.6 ms | 91 | ~180 total, early-stops, but **unfair** (starves siblings) | | Window (`ROW_NUMBER`) | 3.7 ms | 128 | **all ~4k + sort** | | **LATERAL (`per-parent LIMIT`)** | **0.5 ms** | **57** | **~60, index early-stop** | LATERAL matches the pre-PR read cost while being fair, needs no new index, and scales independently of how large any single relation is. ## Verification - Backend integration test (`nested-relation-per-parent-limit`): a parent with 65 children is capped at 60 while a sibling with 3 keeps all 3 — passes. - `EXPLAIN ANALYZE` on the generated SQL: Index Scan with the `LIMIT` pushed into the per-parent lateral (early-stop). - Frontend unit test for the `ExpandableList` cap. - Manual check on a table cell with 40 related records: exactly 10 chips mount inline (down from 40), no console errors, chips still clickable and the overflow count reflects the true total. |
||
|
|
4840233a1c |
i18n - website translations (#22244)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22244?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> |
||
|
|
0e22ae0521 |
feat: create calendar events on Google and Microsoft accounts (#22231)
## Context Twenty can import calendar events and send emails, but cannot create calendar events. This adds calendar event creation on connected **Google** and **Microsoft** accounts, mirroring the existing email-send architecture (`message-outbound-manager`). ## What it adds The capability is exposed three ways, all backed by the same composer → driver → persist pipeline: - **GraphQL mutation** `createCalendarEvent` (metadata API) - **AI agent tool** `create_calendar_event` (flows to MCP automatically), gated by a new `CREATE_CALENDAR_EVENT_TOOL` permission flag - **Workflow builder node** "Create Calendar Event" in the **Core** section, with a full settings form (variable interpolation supported) CalDAV/IMAP is intentionally out of scope for now (different long pole). ## Design notes - **Reuse over reinvention** — the created event is run through the existing inbound formatters (`formatGoogleCalendarEvents` / `formatMicrosoftCalendarEvents`) and persisted immediately via the existing `CalendarSaveEventsService`, so it appears in Twenty right away and is reconciled by the next provider sync (dedup on external id). Persistence is best-effort. - **OAuth scopes** — Google already requests `calendar.events` (read+write), so no change there. Microsoft moves `Calendars.Read` → `Calendars.ReadWrite`; existing Microsoft accounts must re-consent (surfaced as a clear "reconnect" error via a missing-scope check). - **Deliberate invitation semantics** — `sendInvitations` is off by default. When off, the event is created with **no attendees** on either provider, so creating an event never silently emails external people. When on, attendees are attached and notified (Google `sendUpdates: all`, Microsoft's default). This sidesteps Microsoft Graph having no per-request suppression. - **Timezone correctness** — Microsoft Graph interprets `dateTime` as wall-clock in the supplied `timeZone` and ignores the offset, so the absolute instant is converted to its wall-clock form before sending (Google honors the offset directly). Both providers end up scheduling the same instant. - **Conferencing** — optional Google Meet (`conferenceData.createRequest`, with a follow-up `events.get` to resolve the async link) / Microsoft Teams (`isOnlineMeeting`). - Attendees are a comma-separated string everywhere (tool input, GraphQL DTO, workflow input), consistent with `send_email` recipients; the composer parses to its internal list. ## Test plan - **Unit**: 45 tests covering the composer (validation, all-day boundaries, offset enforcement, timezone, scope checks, default-account resolution), both provider drivers, the dispatcher, and the workflow step-log builder. - **Integration**: `createCalendarEvent` on the `/metadata` API fails closed with a structured error for a non-existent account (the auth/ownership/validation path that doesn't require provider mocking). - **Manual**: verified the workflow node appears in the Core section, the settings form renders and round-trips (edit → autosave → reload), and the live mutation returns a structured failure for a bogus account. ## Open question for reviewers The metadata mutation `createCalendarEvent` shares a name with the core schema's auto-generated `createCalendarEvent(data:)` CRUD mutation for the CalendarEvent object — they live on different endpoints (`/metadata` vs `/graphql`) so there's no runtime conflict, but it's a potential point of confusion for API consumers. Happy to rename (e.g. `createCalendarEventOnConnectedAccount`) if preferred. ## Out of scope / follow-ups - CalDAV/IMAP support - Event update/delete and recurrence - Existing Microsoft accounts need re-consent for the widened scope <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22231?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: neo773 <neo773@protonmail.com> |
||
|
|
2662fda647 |
ci(preview-env): run preview environments on free ci-public Actions (#22245)
## Why PR preview environments were broken. The dispatch half (this repo) was fine, but the receiving `preview-env.yaml` in **ci-privileged** failed on every run — and ci-privileged is a **private** repo, so its 5h keepalive job bills paid Actions minutes anyway. The intended design (started but never wired up) runs the preview env on the **public** `twentyhq/ci-public` repo, where Actions minutes are free, and keeps no privileged token on the runner that executes PR-controlled code. This finishes that migration. ## What this PR does Repoints `preview-env-dispatch.yaml` to dispatch `preview-env.yaml` on **ci-public** instead of ci-privileged. The dispatcher app token is simply retargeted (still `actions:write` only, via `workflow_dispatch`). ## Companion PRs (must land together) - **twentyhq/ci-public** — converts `preview-env.yaml` to `workflow_dispatch`; dispatches the tunnel URL back to ci-privileged for the PR comment before any PR code runs. - **twentyhq/ci-privileged** — adds `preview-env-comment.yaml` (posts the PR comment with the App key) and deletes the now-dead `preview-env.yaml`. ## Manual prerequisites (cannot be done in a PR) - Install/authorize the `TWENTY_WORKFLOW_DISPATCHER` GitHub App on **ci-public** with `actions:write`. - On **ci-public**: add `vars.DOCKERHUB_RO_USERNAME`, `secrets.DOCKERHUB_RO_TOKEN`, and `secrets.CI_PRIVILEGED_DISPATCH_TOKEN` (fine-grained PAT, `actions:write` on ci-privileged only). ## Suggested merge order 1. ci-privileged (receiver must exist on `main` before ci-public dispatches to it) 2. ci-public (workflow must exist on `main` before this repo dispatches to it) 3. this PR ## Test plan - [ ] Open an internal PR touching `packages/twenty-docker/**`; confirm `Preview Environment Dispatch` runs and triggers `Preview Environment` on ci-public. - [ ] Confirm the trycloudflare URL sticky comment lands on the PR (posted by ci-privileged's `preview-env-comment`). - [ ] Confirm a `preview-app` label on an external contributor PR triggers the same flow. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- _Generated by [Claude Code](https://claude.ai/code/session_01VqZBvafHqdCGtHZUT216C5)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22245?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. --> |
||
|
|
03f3789d13 |
feat(website): menu adapts to the section beneath it (#22241)
## What The sticky menu now adopts the color scheme of whatever section sits beneath it as you scroll, across every page. Background, logo, and buttons ease between schemes for a smooth handoff. ## How - **Declarative contract** — sections opt in with a `data-menu-surface` attribute (`SectionShell`, the footer, and the product hero's mobile sections); `useActiveSurfaceScheme` tracks which surface sits under the menu's bottom edge (`MENU_HEIGHT_PX`) and reports its `data-scheme`. - **One provider** — `MenuStyleProvider` lives in the `(site)` layout so every page adapts; the Menu resolves its scheme as `override ?? activeScheme ?? prop`. SSR seeds the prop to each page's first section, so there's no mount flash. - **Hero handoff** — the product hero keeps the menu via a per-frame override only while its track still covers the nav band (`controlsMenu`); it releases to the observer *before* the track clears the bar, so the menu stays opaque on exit and never flashes the halftone backdrop rising behind it. The menu's `backdrop-filter` was removed (a no-op over the opaque menu, and a GPU compositing artifact). ## Also in this PR - **Menu folder reorg** to the `product-feature` convention: `components/`, `effect-components/`, `data/`, and `types/` (one domain type per file). - **`MENU_HEIGHT_PX` token** replacing the literal `64` that was duplicated across four files (menu row, the hero's scroll model and component, the observer). - **`findActiveSurfaceScheme`** extracted as a pure, unit-tested function (inclusive top / exclusive bottom, first-match, no-match, null-scheme). - **Mobile AI-section fix** — the mobile hero sections now declare the surface contract, so the menu adapts dark over the AI block on mobile (it previously stayed light). Their color is driven from `data-scheme` (single source) rather than a parallel prop. ## Testing - Jest — 16 tests (scroll model + surface-selection util). - Headless Playwright (mobile 390px) — menu `light` over the intro → `dark` over the AI section; section colors unchanged (`#fff` / `rgb(20,20,20)`). - Desktop unaffected — the mobile sections are `display:none` (zero rect), so the observer skips them and the hero override path is untouched. - Gates green — typecheck, oxlint, oxfmt, check-conventions. |
||
|
|
f041f6dfb6 |
chore: sync AI model catalog from models.dev (#22242)
Automated daily sync of `ai-providers.json` from [models.dev](https://models.dev). This PR updates pricing, context windows, and model availability based on the latest data. New models meeting inclusion criteria (tool calling, pricing data, context limits) are added automatically. Deprecated models are detected based on cost-efficiency within the same model family. **Please review before merging** — verify no critical models were incorrectly deprecated. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22242?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com> |
||
|
|
bf7a766935 |
i18n - website translations (#22234)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22234?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> |
||
|
|
ad1ac4f3c5 |
i18n - website translations (#22225)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22225?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> |
||
|
|
3525187321 |
fix(ai) - fixes (#22227)
- ai chat author fix (before : "workflow", after : "user") - https://discord.com/channels/1130383047699738754/1496872385687584768 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22227?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
d58ec64e10 |
feat(website): swap product Demo for a register Signoff; make signoff headings width-driven (#22232)
## Summary
Reworks the product page's closing section, and in doing so makes every
signoff closer width-driven.
### 1. Replace the Demo section with a register Signoff (`6a5f0c34bf`)
The product page ended on a static `AppPreview` mockup ("Try it live").
Swap it for the shared `Signoff` closer — a focused register CTA, the
same component the customers / why-twenty / partners pages use — so the
page ends by driving sign-ups instead of re-showing the product.
- New `ProductSignoff`: heading "Start moving faster today.", a
supporting line, and the **Get started + Talk to us** pair (matching
`CustomersCatalogSignoff`). Uses `SITE_URLS.appWelcome` per the
site-urls rule rather than inlining the URL.
- Removes the now-unused `product-demo` section and its 192KB background
asset.
### 2. Make Signoff headings width-driven (`0365c0d2ea`)
The signoff headings forced their two-line break with a literal `\n` in
the translated string — against the site's typography principle (fluid
type + `text-wrap: balance`, no `<br>`).
- `Signoff` now carries one default **`615px`** heading measure; all
five closers drop `\n`.
- The width was **measured, not guessed**: each heading was rendered
headlessly in the real production fonts (Aleo 300 body, Host Grotesk 300
accent — each heading mixes both) at the desktop size with `text-wrap:
balance`, then I found the common window where every heading breaks
identically: whyTwenty `[565–820]`, customers `[425–800]`, partner
`[380–730]`, product `[380–660]` — `615` sits in all of them. Each was
then verified to reproduce its exact current break at 615px.
- **Affects 4 other pages** (why-twenty, customers, customers/[slug],
partners) — all verified to render identically, and headings now reflow
on narrow viewports instead of being pinned by `\n`.
- Drops the one-off `headingMaxWidth` prop (now unused).
## Test plan
- oxfmt, oxlint, check-conventions, typecheck — all green.
- Headless measurement confirms all five signoff headings reproduce
their current desktop break at 615px (no visual regression).
- Visually confirmed the product closer and the four sibling signoffs.
|
||
|
|
e747bc3e42 |
Add backfill installation feature for pre-installed apps (#22199)
## After <img width="1070" height="514" alt="image" src="https://github.com/user-attachments/assets/9cbd2ff5-1678-4c2f-84da-074568f37c51" /> ## Summary Adds the ability to backfill application installations across all existing workspaces. This allows admins to retroactively install a pre-registered application on every active and suspended workspace through a background job, making the feature idempotent and non-blocking. ## Key Changes - **Backend Service**: Added `backfillApplicationOnAllWorkspaces()` method to `PreInstalledAppsService` that: - Validates the application registration exists - Iterates through all workspaces using `WorkspaceIteratorService` - Installs the app on each workspace - Swallows `APP_ALREADY_INSTALLED` errors for idempotency - Logs success/failure counts - **Background Job**: Created `BackfillApplicationInstallationJob` to process backfill requests asynchronously via the message queue - **GraphQL Mutation**: Added `backfillApplicationInstallation` mutation to `AdminPanelResolver` that: - Validates the application registration exists - Enqueues the background job - Returns immediately without blocking the request - **UI Components**: Enhanced `SettingsAdminApplicationRegistrationGeneralToggles` with: - New "Pre-install on new workspaces" toggle for the `isPreInstalled` flag - "Backfill on all workspaces" button with confirmation modal - Loading state and success/error snack bar feedback - **Data Model**: Added `isPreInstalled` field to `UpdateApplicationRegistrationPayload` input type - **Tests**: Added comprehensive unit tests for `PreInstalledAppsService.backfillApplicationOnAllWorkspaces()` covering: - Missing registration validation - Successful multi-workspace installation - Idempotent handling of already-installed errors - Proper error propagation for unexpected failures ## Implementation Details The backfill operation is designed to be: - **Idempotent**: Already-installed apps are skipped without error - **Non-blocking**: Runs as a background job via message queue - **Resilient**: Per-workspace failures don't block other installations - **Observable**: Logs aggregated success/failure counts for monitoring <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22199?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> |
||
|
|
fea2b8736f |
feat(website): rebuild dashboard visual faithful to twenty-front (#22218)
Rebuilds the product-feature **DashboardVisual** to mirror
twenty-front's dashboard widgets, with colors traced to twenty-front's
actual source rather than eyeballed.
## Widgets
- **Bar — "Deals by month"**: single-series `blue8` (twenty-front's
`GRAPH_DEFAULT_COLOR`), dashed `4 4` gridlines, value labels, nice
rounded Y-ticks. Replaces the old stacked bar (stacked bars aren't used
in twenty-front).
- **Donut — "Deals by stage"**: the real opportunity pipeline
(New/Screening/Meeting/Proposal/Customer), each segment colored by that
stage option's own color from the metadata
(`red/purple/sky/turquoise/yellow`), with a center total and a paginated
horizontal legend.
- **KPIs**: big-number cards (Revenue YTD / Avg deal size / Win rate).
## Responsive — `mediaUp('md')`
The dashboard is the full-width spotlight tile, whose frame is short
below md and grows to 420px at md+. So the layout keys off md: below it
collapses to **2-up KPIs + a full-width bar** (donut and the 3rd KPI
hidden, smaller breadcrumb), and the spotlight frame's mobile min-height
is bumped so the bar has room; at md+ the full 3-KPI + side-by-side
layout returns. The donut caps at its size and shrinks with its
container.
## Notes
- Follows the `product-feature` conventions (per-visual folder,
one-export-per-file, split types).
- `Tiles.tsx`: one-line spotlight mobile min-height bump (only the
dashboard uses the spotlight tile).
- typecheck + lint + build all green.
<img width="1148" height="638" alt="image"
src="https://github.com/user-attachments/assets/1eb8f120-88c2-45d1-adf9-ec00abe11006"
/>
|
||
|
|
8a257e0de3 |
Update public app names (#22228)
- update names to comply with @twentyhq/ prefix standard - update versions - upgrade twenty sdks versions <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22228?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. --> |
||
|
|
e1120d38b6 |
fix: stamp MCP and AI Agent writes with FieldActorSource.AGENT (#22215)
## Description Fixes #21437 MCP and AI Agent writes now correctly stamped with ### Problem Records created through MCP server were stamped as `WORKFLOW`, making them indistinguishable from workflow-created records. This breaks loop-protection filters that skip workflow-originated records. ### Solution - MCP writes now correctly stamped with `createdBy.source = AGENT` - AI Agent execution now uses `AGENT` instead of `MANUAL` - Added `WorkspaceCacheModule` to MCP module - Updated tests to verify AGENT source ### Files Changed - `mcp.module.ts`: Added WorkspaceCacheModule import - `mcp-protocol.service.ts`: Set AGENT source in buildMcpToolSet - `mcp-protocol.service.spec.ts`: Updated tests - `agent-actor-context.service.ts`: Changed MANUAL → AGENT - ## Type of Change - [x] Bug fix (non-breaking change) ## Checklist - [x] Code follows project style - [x] Tests added/updated - [x] Issue linked Fixes #21437 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22215?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. --> |
||
|
|
c891258f34 |
Add v2 onboarding create profile page (#22221)
<img width="3024" height="1498" alt="CleanShot 2026-06-26 at 15 30 23@2x" src="https://github.com/user-attachments/assets/8b4863a9-66ed-4da1-851b-473cedf71511" /> <img width="3022" height="1500" alt="CleanShot 2026-06-26 at 15 29 43@2x" src="https://github.com/user-attachments/assets/22fc0e94-f670-4638-975c-f06b2b2e25e8" /> Adds the v2 onboarding **Create profile** page, shown right after the import-contacts step (`PROFILE_CREATION`) for the onboarding-v2 cohort. It renders full-screen under `BlankLayout` via the shared `OnboardingV2Layout`, matching the Figma (340px column, inline round avatar uploader + First/Last row, Job Title, dark Continue). The v1 modal flow is untouched and still used for non-v2 users. Job Title is wired end-to-end: it adds a real `jobTitle` field to the `WorkspaceMember` standard object (shared metadata constant + flat field metadata + entity property) and a `2-17` workspace upgrade command to backfill the field on existing workspaces. Continue persists name + jobTitle through the existing `updateWorkspaceMemberSettings` mutation, whose allow-list picks up the new standard field automatically. Routing mirrors `SyncEmailsV2`: new `AppPath.CreateProfileV2`, lazy route, and an `isOnboardingV2`-gated branch in `usePageChangeEffectNavigateLocation` (+ tests and a Storybook story). Reviewer notes: - `jobTitle` is **write-only** for now (no read-back path: core DTO/transpiler/fragment unchanged), and the field is `isSystem`/non-UI-editable to match its siblings. Easy to surface later if wanted. - New `OnboardingProfilePictureUploader` is a compact round avatar uploader reusing the same upload mutation flow as `WorkspaceMemberPictureUploader`. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22221?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
1bf0d06e18 |
Add author to twenty apps (#22226)
as title <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22226?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. --> |
||
|
|
7caf2a14c5 |
feat(website): show partner location instead of served regions in marketplace (#22224)
## Context On the partners marketplace, the eyebrow shown below each partner's name (on both the list card and the profile header) displayed the served regions (e.g. `APAC`, `EUROPE`). The partner's actual location was buried in the "Where & how" facts list. The location is more useful at a glance, so this swaps the two. ## Changes **List card (`PartnerCard`)** - The eyebrow below the name now shows the partner's city and country instead of the first served region. **Profile page** - `PartnerProfileHeader`: the eyebrow near the title now shows the real location (city, country). - `PartnerFactsList`: the served regions move into the "Where & how" section as a `Regions` row, taking the place of the now-redundant "Based in" row. The `Regions` chip rows on the card are unchanged. https://claude.ai/code/session_01FQRRfvPpmNsjAAnZeDc5sy --- _Generated by [Claude Code](https://claude.ai/code/session_01FQRRfvPpmNsjAAnZeDc5sy)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22224?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. --> |
||
|
|
1b4bddba6f |
fix(website): polish product hero AI transition (panel reveal timing + title wrapping) (#22223)
## Summary Two desktop/tablet polish refinements to the product page hero's hero→AI transition (`product-hero`). ### 1. Delay the Ask-AI panel reveal to after the wipe (`b638d600`) The Ask-AI side panel used to reveal *during* the dark "wipe up" (keyed to the morph `0.45 → 0.70`), so it competed with the rising black edge and piled onto the heaviest motion phase — rough on weaker laptops. It's now keyed to raw scroll progress over `[0.60, 0.70]` — the previously-idle hold after the wipe settles at `0.55` — so it slides in on its own beat once the eye has followed the black up. `morphProgress` is pinned at `1` across the whole post-wipe stretch, so leaving the morph clock was the only way to time the panel *after* the wipe. The conversation playback moves with it, so the chat doesn't stream while the panel is still `width: 0`. ### 2. Keep the AI heading to three lines below desktop (`8ae69a4b`) The AI heading is the page's longest line. Below `md`, only the mobile copy renders, where the measure was frozen at `360px` while the heading font scales fluidly toward `md` — so on tablet widths the title crammed onto four lines, recovering only at `921px` when the `672px` measure kicks in. Adds an `sm`-breakpoint measure step (`560px`) scoped to the AI heading; the intro heading and the desktop measuring path keep their existing `360/672` steps. ## Test plan - `product-hero-scroll-model` jest suite updated and green (11/11), including a new post-morph panel-timing test. - Lint (`check-conventions` + `oxlint` + `oxfmt`) and typecheck green. - Visually confirmed: the panel reveals after the wipe settles, and the AI title holds three lines across tablet widths. |
||
|
|
adf56cac56 |
fix(workflow): avoid 'file not found' when duplicating unbuilt code steps (#22179)
## Context Duplicating a workflow version (e.g. when editing an active workflow) clones each Code step's logic function via copyResources, which copied both the source and the built artifact unconditionally. Logic functions created from source are not built yet (no index.mjs, isBuildUpToDate=false), so copying the missing built file threw FILE_NOT_FOUND. Copy the built artifact only when it exists. This is safe because the duplicate inherits isBuildUpToDate=false and is rebuilt lazily on activation or run. In seed dev case for example, they are created with source but never built ## Test https://github.com/user-attachments/assets/5a7febba-413b-4041-985e-f84a99a5ebda |
||
|
|
b84f748237 |
Fix navigation opened section spacing (#22222)
## Summary - Remove the collapsed empty `Opened` sidebar section when no opened object exists. - Restore the first navigation section top alignment with the settings `User` section. ## Before/After Visual verification after the fix: in the current fixture data, the first visible main sidebar section (`Favorites`) starts at y=92, matching the settings `User` section at y=92. This confirms there is no collapsed `Opened` spacer pushing the main drawer content down. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22222?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
da6a2ee300 |
fix(ai-chat): keep streams alive on silent SSE death + make the stream job idempotent (#22201)
## Problem In production, an AI-chat assistant response sometimes freezes mid-stream (partial text, looks hung), then "picks up again on its own" later without the user resending and without a known worker restart. Root cause: the **agent-chat SSE subscription has no keepalive and no silent-death detection**. - Delivery is fire-and-forget Redis pub/sub (`SubscriptionService.publishToAgentChat`) and the resolver returns the **raw** iterator — unlike `EventStreamResolver`, which heartbeats every 30s via `wrapAsyncIteratorWithLifecycle`. - During a quiet model/tool gap the connection sends no bytes, so a proxy/LB/NAT can silently drop it mid-stream. `graphql-sse` neither surfaces an error nor resumes with `Last-Event-ID`, and **nothing re-pulls the existing Redis chunk catch-up on reconnect** (it only runs on thread (re)mount / `message-persisted` refetch). - So the live view freezes; recovery only happens when the terminal `message-persisted` fires a full refetch from the DB — the observed "self-recovery". This is the **same silent-SSE-death class fixed for the DB event stream in #21061**, which was never applied to the agent-chat path. The symptom also matches #21096 (worker logs the job finishing, client never updates, reload shows the message). It is **not** queue prioritization, and it is **not** addressed by #22193 (which only stabilizes the assistant message id and removes end-of-stream flicker). A secondary, independent self-recovery path also existed: BullMQ stalled-job re-run (default 30s `lockDuration`, no idempotency guard) re-streaming the whole turn → duplicate assistant messages / double billing. ## Changes ### Commit 1 — keepalive + silent-death recovery (ports the #21061 pattern to agent chat) - **Shared:** new `keepalive` variant on `AgentChatSubscriptionEvent`. - **Server:** wrap the agent-chat subscription iterator with `wrapAsyncIteratorWithLifecycle` — emit a `keepalive` on connect and every `APPLICATION_KEEPALIVE_INTERVAL_MS` (30s) so the connection keeps flushing bytes and a dead connection becomes detectable. - **Client:** track the last received event timestamp (refreshed on every chunk/keepalive in the SSE `next` sink); new `AgentChatStreamKeepAliveEffect` forces a resubscribe + messages refetch after 90s of silence, so the durable Redis chunk list backfills the gap (`firstLiveSeq` is reset on resubscribe). ### Commit 2 — stream-job idempotency + lockDuration - Thread a `lockDuration` option through `MessageQueueWorkerOptions` + the BullMQ driver; set `aiStreamQueue` to 10 min so long streams aren't falsely stalled. - Guard `StreamAgentChatJob.handle` with a `streamId`-scoped Redis lock (`SET NX PX` + compare-and-delete release) so a stalled re-run is skipped instead of double-processing. ## Verification ⚠️ I could **not run typecheck/lint locally** — `yarn install` could not complete in this environment (transient registry network aborts before the link step, so `node_modules` never populated). **Please rely on CI for type/lint verification.** The changes are written to match existing conventions; the points most worth a reviewer's eye are the resolver's iterator typing and the ioredis `set(..., 'PX', ttl, 'NX')` overload. How to confirm the root cause in prod: a frozen client with the worker logging `StreamAgentChatJob processed in …ms` and no `[AI_CHAT_NO_TEXT]` is the silent-death signature (check reverse-proxy idle/buffering). For the secondary path, watch `aiStreamQueue` `stalled`/re-processed metrics and duplicate turns around worker restarts. ## Notes / trade-offs - The 10-min `lockDuration` means a genuinely crashed worker's job isn't reclaimed for up to 10 min; the client-side keepalive/catch-up recovers the view independently, and the idempotency lock prevents duplicates. Faster dead-worker recovery could be a follow-up. - Touches `useAgentChatSubscription.ts` / `AgentChatRuntimeEffects.tsx` / `stream-agent-chat.job.ts`, which #22193 also touches — trivial rebase expected. Opened as **draft** pending CI. https://claude.ai/code/session_018dF82A1VcsuWMxPLmdY3dm --- _Generated by [Claude Code](https://claude.ai/code/session_018dF82A1VcsuWMxPLmdY3dm)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22201?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
8f7d6c24dd |
Add v2 onboarding import contacts page and unify the onboarding v2 shell (#22212)
<img width="3024" height="1668" alt="CleanShot 2026-06-26 at 13 29 28@2x" src="https://github.com/user-attachments/assets/9bdd0029-45eb-4ddb-859f-eaab9bb61406" /> Adds the new v2 onboarding **Import contacts** step (email + calendar import), shown right after workspace creation in the v2 flow. The presentational page was designed in a previous PR; this wires it in and unifies the shell. **What changed** - Reuses and unifies the existing v2 onboarding shell: extracts `OnboardingV2Layout` + `OnboardingV2Header` (the back + logo header, now with the free-credits pill), and the `SignInUpV2` workspace-creation step renders through it (old `SignInUpV2Header` removed). - New `SyncEmailsV2` route (`/sync/emails-v2`) under `BlankLayout`, wired to the same OAuth/skip hooks as v1 `SyncEmails`. - The `SYNC_EMAIL` step routes to the new page only when `isOnboardingV2` is set (mirrors the existing `WorkspaceActivation` → `WorkspaceActivationV2` branch); the v1 modal is unchanged for the non-v2 flow. - No backend changes — reuses the `SYNC_EMAIL` status and `skipSyncEmailOnboardingStep` mutation. **Reviewer notes** - Connect defaults to `METADATA` (private) visibility to match the "Only you will be able to see your emails and events" note (v1 had a selector defaulting to `SHARE_EVERYTHING`). - The header free-credits pill shows `0` for now (no current-workspace credits source on the frontend yet). - The back button is hidden on the import page (no meaningful "back" after workspace creation); unchanged on the workspace-creation step. |
||
|
|
9747e3a7a3 |
feat(messaging): link emails by Reply-To as a REPLY_TO participant (#22216)
Relay senders (e.g. a website form sending as a shared address with the real contact in Reply-To) never linked to the contact because matching only used From/To/Cc/Bcc. Record Reply-To addresses under a new REPLY_TO participant role across the Gmail, Microsoft and IMAP drivers, excluding any that just repeat the sender. Adds the REPLY_TO option to the messageParticipant role field and a 2.17 workspace command to backfill it for existing workspaces. QAed with real test run <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22216?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. --> |
||
|
|
ebe067f65c |
Fix favorite showing non-readable objects (#22217)
## Context Navigation menu items backed by objects the user has no read permission on were correctly hidden from the Workspace section, but Favorites still showed them. Favorites are user-scoped nav items (tied to workspaceMemberId), and FavoritesSection only filtered out folder children (!item.folderId) without ever checking canReadObjectRecords. The shared upstream filter (filterAndSortNavigationMenuItems) intentionally does not apply read permissions, because its output also drives drag-and-drop position math and layout-customization/edit mode, which need the complete, unfiltered list. So read-permission filtering belongs at the display layer. ## Fix New shared hook useReadableNavigationMenuItems that centralizes the read-permission filtering logic previously duplicated across sections: wires up objectMetadataItems + views + object permissions around isNavigationMenuItemReadable filters folder children and top-level items (dropping folders whose children are all unreadable) exposes both raw filtered* outputs and isLayoutCustomizationModeEnabled-aware display* outputs FavoritesSection now applies the filter via the hook, so unreadable favorites are hidden — while still showing everything in layout-customization mode (consistent with the Workspace section). WorkspaceSectionContainer refactored to consume the same hook, removing its inline isItemReadable, the dual-map reduce, and inline filtering. ## Before ### With access <img width="842" height="570" alt="Screenshot 2026-06-25 at 14 01 38" src="https://github.com/user-attachments/assets/ae51f3c8-c178-4162-84ce-3fe49cf07987" /> ### Without access <img width="987" height="590" alt="Screenshot 2026-06-25 at 14 02 08" src="https://github.com/user-attachments/assets/94dacea3-bd77-4fcb-868d-353ed513b28c" /> ## After ### Without access <img width="1001" height="615" alt="Screenshot 2026-06-25 at 14 02 46" src="https://github.com/user-attachments/assets/e1ccd5d9-8583-4ff1-ab91-6f6d187925c5" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22217?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
fdf9f543ae |
Fix rounded page layout tab edit outline (#22214)
## Summary - Round the edited page layout tab outline to match the tab hover radius. ## Test plan - `npx oxfmt --check packages/twenty-front/src/modules/page-layout/components/PageLayoutTabListReorderableTab.tsx` - `npx oxlint --type-aware -c packages/twenty-front/.oxlintrc.json packages/twenty-front/src/modules/page-layout/components/PageLayoutTabListReorderableTab.tsx` - `npx nx run twenty-front:lint` - Browser: verified the edited `Notes` tab outline before/after locally. ## Visual <img width="1108" height="392" alt="clipboard" src="https://github.com/user-attachments/assets/1cb63180-83a9-4ef8-9c55-2b475eaaa02b" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22214?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
a22eb5a591 |
Bump call-recorder and people-data-lab apps (#22213)
as title <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22213?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
bea7857a1c |
Fix settings page root overflow (#22207)
## Summary - Clamp the fixed app shell with `overflow: hidden` so long nested settings scroll content no longer contributes to the document root scroll range. - Keeps the object permission page scrolling inside its existing settings `ScrollWrapper` instead of letting the whole page slide under the viewport. ## Screenshots Before: root document can scroll under the bottom of the viewport and exposes the gray app background.  After: the same root scroll attempt leaves the app shell fixed to the viewport.  ## Browser Validation Route tested: `/settings/members/roles/78fa69cb-1237-43b5-bfa5-5a11a47bf781/object/5ab1a16b-7811-471f-ac53-940666c667dd` - Before on `http://apple.localhost:3001`: `window.scrollTo(0, 9999)` moved the root to `scrollY=244.5`; `htmlScrollHeight=1287`, `htmlClientHeight=1043`. - After on `http://apple.localhost:3002`: the same root scroll attempt stayed at `scrollY=0`; `htmlScrollHeight=1043`, `htmlClientHeight=1043`. - The settings content still scrolls internally: wrapper `scrollHeight=1475`, `clientHeight=955`. ## Checks - `git diff --check` - `yarn oxfmt --check packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx` - `cd packages/twenty-front && npx oxlint --type-aware -c .oxlintrc.json src/modules/ui/layout/page/components/DefaultLayout.tsx` <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22207?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
d6fca7a82e |
Bump call-recorder and people-data-lab apps (#22208)
as title <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22208?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
34dc681c0e |
Use settings icon in settings drawer tab (#22204)
## Summary - Let the shared navigation drawer tab row accept a custom icon and accessible label for its navigation tab. - Use the Settings icon and Settings label when that tab row is rendered inside the settings drawer. - Keep the main navigation drawer defaulting to the Home icon. ## Screenshots ### Before <img width="420" alt="Before: settings drawer navigation tab uses the Home icon" src="https://github.com/user-attachments/assets/03b684ba-0f62-4a96-a9f1-8c6138efd9cf" /> ### After <img width="420" alt="After: settings drawer navigation tab uses the Settings icon" src="https://github.com/user-attachments/assets/3e5f4669-ed7c-4355-be92-c9cfbf160959" /> ## Validation - `yarn oxfmt --check packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerTabsRow.tsx packages/twenty-front/src/modules/navigation/components/SettingsNavigationDrawer.tsx` - `git diff --check` - `yarn nx typecheck twenty-front` Note: `oxlint` could not run locally because the installed dependencies are missing the native `@oxlint/binding-darwin-*` optional package. |