b768441c13aed1c978522f59d2cc326f3bd5a708
10745 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b768441c13 |
Fix 2.16 search field metadata cross version upgrade (#22039)
# Introduction
Allow decorating at class scope the properties introduced in specific
upgrade command
```
@WasIntroducedInUpgrade({
upgradeCommandName:
ADD_UNIVERSAL_IDENTIFIER_AND_APPLICATION_ID_TO_SEARCH_FIELD_METADATA_UPGRADE_COMMAND_NAME,
properties: ['universalIdentifier', 'applicationId', 'position'],
})
```
Here the search field metadata has been created as it without extending
the syncableEntity a previous PR I've created now extends it, but
nothing has been protected the fact they're not decorated. Also having
to re-declare the properties would be redundant to me
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22039?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. -->
|
||
|
|
b7350eba46 |
Fix main ci: generate client-sdk (#22042)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22042?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. --> |
||
|
|
f5f8865c4e |
Add a real README for the twenty-ui package (#22038)
Replaces the twenty-ui README, which was a long internal design/migration document, with a real package README aimed at consumers. The new README covers installation, peer dependencies, a verified usage example, the available subpath entry points, theming, and development commands. It is what will be shown on the npm package page once twenty-ui is published. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22038?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. --> |
||
|
|
b8e1004534 |
Bump twenty-ui to 1.0.0-alpha.0 for first npm pre-release (#22040)
Sets `twenty-ui` to `1.0.0-alpha.0` so it can be published as the first pre-release on npm. The package name was previously published once (`0.23.4`) and fully unpublished in Sept 2024. Starting at `1.0.0-alpha.0` avoids the burned version, stays above the old number, and publishes to the `alpha` dist-tag (not `latest`) via the existing twenty-infra publish workflow, so it can be dogfooded before a stable `1.0.0`. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22040?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. --> |
||
|
|
5a55c2563e |
Update location of settings (#22033)
In the latest version the «Lab Features» are no longer called like that and are in a different location. See [Discord-Discussion](https://discord.com/channels/1130383047699738754/1508471962308051116) <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22033?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. --> |
||
|
|
2e099c91e1 |
fix(domains): show custom domain DNS records and activation status without a page reload (#22037)
## Problem
Setting up a custom domain had two confusing UX issues, both caused by
local state not being refreshed after the relevant mutation:
1. **DNS records didn't appear after saving.** After hitting save you
got the green "Custom domain updated" snackbar, but the "Domain Setup"
section (the Cloudflare/DNS records to configure) stayed empty. You had
to leave the page and come back for the records to show up.
2. **The "Custom Domain" card stayed "Inactive"** even after the DNS
records validated as "Success". Only a full page reload flipped it to
"Active".
## Root cause
**Issue 1 — stale closure.** In `useSettingsCustomDomain.handleSave`,
the `updateWorkspace` `onCompleted` callback called
`setCurrentWorkspace({ ...currentWorkspace, customDomain })` and then
`checkCustomDomainRecords()`. But `checkCustomDomainRecords` guarded on
the closed-over `currentWorkspace.customDomain`, which was still `null`
at that render. The `setCurrentWorkspace` call doesn't synchronously
update that captured value, so the guard returned early and the records
were never fetched. Remounting the page (navigate away/back) ran the
on-mount effect with a fresh workspace, which is why the trip "fixed"
it.
**Issue 2 — `isCustomDomainEnabled` never refreshed locally.** The
Active/Inactive badge is driven by
`currentWorkspace.isCustomDomainEnabled`. The backend flips this flag
inside `checkCustomDomainValidRecords`
(`custom-domain-manager.service.ts`), but the mutation didn't return it,
so the local `currentWorkspaceState` stayed stale until a full reload
re-ran the bootstrap query. The green "Success" DNS rows read from a
different source (`record.status`), which is why the rows and the badge
disagreed.
## Changes
**Issue 1**
- `checkCustomDomainRecords` now accepts the domain explicitly
(defaulting to the workspace value), so the freshly-saved domain can be
passed straight from `handleSave` instead of relying on the stale
closure. No new `useEffect` introduced.
- Fixed the Reload button so it no longer passes its click event as the
domain argument.
**Issue 2**
- Added a nullable `isCustomDomainEnabled` field to the
`DomainValidRecords` GraphQL type, populated only by the custom-domain
check (the shared public-domain flow leaves it null, so it's backward
compatible).
- The frontend now writes that value back into `currentWorkspaceState`
when the check completes, using a **functional** Jotai update so a
concurrent `customDomain` update is never clobbered. The badge flips to
"Active" as soon as validation passes — on mount, on Reload, and right
after save.
I deliberately kept this targeted rather than introducing real-time
workspace sync: `isCustomDomainEnabled` only changes server-side during
the on-demand DNS check (mount/Reload/cron), so returning it from that
mutation is sufficient and far lower risk.
## Notes
- `packages/twenty-front/src/generated-metadata/graphql.ts` was updated
to match what `graphql:generate` produces for the new schema field
(codegen requires a running backend, which isn't available in this
environment). Worth re-running codegen in CI to confirm it's
byte-identical.
- No existing unit or integration tests reference these paths.
## Test plan
- [ ] Set a custom domain → DNS records appear immediately (no
navigation needed).
- [ ] Once DNS validates, the "Custom Domain" card flips to "Active"
without a reload.
- [ ] Reload button still refreshes records.
- [ ] Public domain validation flow is unaffected.
https://claude.ai/code/session_01BB6C6bpPZMUbMzKSCydEaj
---
_Generated by [Claude
Code](https://claude.ai/code/session_01BB6C6bpPZMUbMzKSCydEaj)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22037?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. -->
|
||
|
|
f24be8eacb |
fix: keep AI chat open when opening a record full-page (e.g. workflows) (#22036)
## Problem
When the AI chat is open in the side panel and you click a workflow from
the Workflows list, the AI chat closes as you navigate to the workflow
page. Navigating between other index/list pages, or to settings, keeps
the chat open — only opening a record full-page closes it.
## Root cause
`useOpenRecordFromIndexView` unconditionally calls
`closeSidePanelMenu()` before navigating to a full-page record:
```ts
} else {
closeSidePanelMenu();
navigate(AppPath.RecordShowPage, { ... });
}
```
Workflows (and other objects excluded by `canOpenObjectInSidePanel` —
`workflow`, `workflowVersion`, `dashboard`) can't open in the side
panel, so they *always* take this branch and close the panel, including
the AI chat.
This is inconsistent with `PageChangeEffect`, which already lets the AI
chat survive navigation by exempting `SidePanelPages.AskAI`. That
exemption is why navigating between index pages or to settings doesn't
close the chat.
## Fix
Skip the close when the side panel is showing the AI chat, mirroring the
exemption already used in `PageChangeEffect`. Any other side panel page
still closes as before.
## Testing
- `nx lint:diff-with-main twenty-front` (file lints clean)
- `nx typecheck twenty-front` passes
https://claude.ai/code/session_01LtBBAxjn32FQVduyAi6B37
---
_Generated by [Claude
Code](https://claude.ai/code/session_01LtBBAxjn32FQVduyAi6B37)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22036?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. -->
|
||
|
|
766d90af7e |
Remove framer-motion from twenty-ui (#22021)
## What Removes the `framer-motion` dependency from `twenty-ui` and replaces every usage with pure CSS animations, reaching for Base UI primitives where one fits: - **Collapse/expand** (`AnimatedEaseInOut`, `AnimatedExpandableContainer`): rebuilt on Base UI `Collapsible` (CSS-animated `--collapsible-panel-height/width` + transition states). Public props unchanged, so the ~28 call sites are untouched. - **ProgressBar**: rebuilt on Base UI `Progress` (proper `role`/`aria-valuenow`). The snackbar auto-dismiss countdown now uses a CSS keyframe + `animation-play-state` (pause on hover), removing a per-frame React re-render; `useProgressAnimation` is deleted. - The remaining `Animated*` components, the circular spinner, checkmark, and the placeholder pointer parallax move to plain CSS (SCSS modules + the `duration()` helper + theme tokens). - Deletes 3 unused components (`AnimatedTranslation`, `AnimatedTextWord`, `AnimatedFadeOut`). ## Why `twenty-ui` is a publicly published library with a size budget, so dropping framer-motion shrinks what consumers ship. `twenty-front` keeps its own framer-motion; that is out of scope here. ## Notes for reviewers - A few `twenty-ui` components received framer props from `twenty-front` call sites; those were migrated (e.g. `AnimatedLightIconButton` gained a CSS `rotate` prop, and the `EMPTY_PLACEHOLDER_TRANSITION_PROPS` spreads were removed). - Behavior change: Base UI `Collapsible` animates only on open/close transitions, so the old "animate in on first mount while already open" case no longer plays (the `initial` prop is kept for API compatibility). <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22021?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. --> |
||
|
|
d4e4e2612b |
feat(front): render Instagram URLs as @handles in link fields (#21642)
LinkedIn and X links already show a readable handle in Twenty's link fields. Instagram doesn't — it just shows `instagram.com`, which isn't much help when you're scanning a record. This adds the same handling for Instagram. `instagram.com/ptcrash` now shows as `@ptcrash`, in tables, on record pages, and in the edit menu. Post and reel links (`/p/...`, `/reel/...`) have no handle, so they fall back to `Instagram`. How it works: - `Instagram` added to the `LinkType` enum - `checkUrlType` detects `instagram.com` - `getDisplayValueByUrlType` pulls the handle and prefixes `@` - a shared `isSocialLinkType` helper keeps the three display components in sync Tested with unit tests for both helpers, the updated story, and manually against a record whose Instagram field is `http://instagram.com/ptcrash`. Closes #21644 Co-authored-by: Johnny Martin <ptcrash@users.noreply.github.com> |
||
|
|
855664daa2 |
feat(timeline): activity kind registry (Layer A) (#21950)
## What & why
The timeline-activity system's contract is a magic `name` string
(`"company.updated"`, `"linked-note.created"`, `"message.linked"`)
decoded by `String.split('.')` in **four** different frontend spots and
produced by a hardcoded `if`-ladder + two listeners. It is not
extensible and it already harbored a latent bug.
This PR replaces that stringly-typed protocol with an explicit,
persisted **`kind`** contract consumed through registries on both ends.
Adding a new timeline activity type becomes: add a producer + register a
presenter — no edits to a central switch.
This is **Layer A** of a larger plan (see
`packages/twenty-server/docs/TIMELINE_ACTIVITIES_REFACTOR.md` and
`TIMELINE_ACTIVITIES_PR_A.md`). Layer B (timeline projection /
"inheritance") and Layer C (user-defined aggregation rules) are
intentionally **out of scope** here.
## 🐛 Bug fixed along the way
`calendar-event-participant.listener.ts` was writing calendar-event
timeline rows with `name: 'message.linked'` (copy-paste from the message
listener). It rendered "correctly" only by luck — the frontend routed on
`linkedObjectMetadataId → nameSingular`, never on `name`. This PR fixes
it at the source (`calendarEvent.linked` / `kind:
'linkedCalendarEvent'`), and the shared resolver also corrects
historical rows that carry the wrong `name`.
## Changes
**`twenty-shared`** — new `timeline` module
- `TimelineActivityKind` (`recordChange | linkedNote | linkedTask |
linkedMessage | linkedCalendarEvent | linkedRecord`) +
`resolveTimelineActivityDescriptor`, the **single** place that decodes
an activity into `{ kind, action }`. Reads the persisted `kind` when
present and falls back to legacy `name`/`linkedObjectMetadataId` parsing
(back-compat shim). Unit-tested (20 cases).
**`twenty-server`**
- Persist a nullable `kind` field on the `timelineActivity` standard
object (entity shape + field-metadata builder + universalIdentifier).
- Producers (`timeline-activity.service.ts`, the two participant
listeners) set `kind` explicitly; dev seeder populates it.
- Fix the `calendarEvent.linked` mislabel.
**`twenty-front`**
- Static `TIMELINE_ACTIVITY_PRESENTERS` registry replaces the render
`switch`, the icon `if`-chain, the diff-validation name-parsing, and the
`name.match(/note|task/i)` title-prefetch hack.
- New `EventRowGenericLinked` so an unknown linked object type renders a
real "linked a {object}" row instead of falling through to the wrong
(main-object) renderer.
## Migration / compatibility
- The `kind` column on this **workspace** standard object is created by
the normal workspace metadata sync — no hand-written migration. It is
**nullable**, so pre-upgrade rows degrade gracefully through the
resolver shim (they resolve correctly from `linkedObjectMetadataId` +
`name`). An optional backfill workspace command could populate `kind` on
old rows later; not required for correctness.
- No GraphQL breaking change — `kind` is additive, `name` is retained
for display/search.
## Test plan
- `twenty-shared` unit tests (resolver) ✅
- `typecheck` + `lint:diff-with-main` green on `twenty-front`,
`twenty-server`, `twenty-shared` ✅
- Reset + reseed a workspace: `kind` is populated for all seeded rows
(recordChange / linkedMessage / linkedNote / linkedTask /
linkedCalendarEvent) with no nulls ✅
- Manual end-to-end verification via Playwright on person / company
record timelines — screenshots in a follow-up comment.
Screenshots attesting the rendering (incl. the calendar fix) are posted
as a comment below.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01YRueWMo4UyaX2em8R2cdio)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21950?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. -->
|
||
|
|
5e8932001c |
Simplify Recall webhook logic function config (#22026)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22026?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. --> |
||
|
|
ceecf33555 |
fix(twenty-partners): restore partner side panel field visibility (#22024)
## Summary - Restores 17 partner profile fields in the `FIELDS_WIDGET` view backing the Partner record page side panel (My Profile, admin partner views). - Read-locks `validationStage` and `partnerTier` for the Partner role so admins still see them on the record page but partners do not on My Profile. - Renames legacy `profilePicture` label to "Profile Picture (legacy)" to distinguish from the new file field. - Bumps `twenty-partners` to **1.1.3** (already deployed to prod). ## Test plan - [ ] `yarn lint` in `packages/twenty-apps/internal/twenty-partners` — 0 errors - [ ] `yarn twenty dev --once` on a local workspace — sync succeeds - [ ] As admin: open a Partner record → side panel shows all profile fields including validationStage and partnerTier - [ ] As Partner role (My Profile): side panel shows profile fields but **not** validationStage or partnerTier - [ ] Upgrade path: install v1.1.3 on an existing workspace — view fields update in place <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22024?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. --> |
||
|
|
bab71afe54 |
fix(deps): bump vitest to 4 in twenty-meeting-bot (drops vulnerable esbuild) (#22025)
## Summary Bumps **vitest** `^3.1.1 → ^4.1.9` in `twenty-meeting-bot`, which lets **vite** resolve to **8.0.16** — and vite 8 dropped esbuild entirely (moved to rolldown). That **removes the vulnerable transitive `esbuild@0.27.7` outright**, resolving [Dependabot alert #1470](https://github.com/twentyhq/twenty/security/dependabot/1470) — GHSA-g7r4-m6w7-qqqr (esbuild dev-server arbitrary file read on Windows, `>=0.27.3 <0.28.1`). ## Why a parent-bump, not a resolution - The vulnerable esbuild came from `vite@7.3.5` (`esbuild ^0.27.0`, a `0.x` caret capped at `<0.28` — so `yarn up` couldn't reach the fix). - vite is gated by vitest's vite range: vitest **3.x** allows only `^5||^6||^7` (caps vite at 7 → esbuild 0.27); vitest **4.x** allows `^8`, and **vite 8 has no esbuild dependency at all**. - So bumping vitest lets vite resolve to 8, which **eliminates the vulnerable dependency entirely** — no `resolutions` entry to force or maintain. (Matches the repo's stated preference: fix by upgrading the parent, not by resolution.) ## Verification - `yarn install` — vite resolves to `8.0.16`; all `@esbuild/*@0.27.7` platform packages pruned; the only esbuild left is `0.28.1` (already-fixed, from another consumer). - `yarn typecheck` — passes. - `yarn test:unit` — **202 tests / 30 files pass** under vitest 4.1.9, no peer warnings; `vite-tsconfig-paths` still compatible with vite 8. - `yarn install --immutable` — passes. - (Integration `yarn test` is gated on a live Twenty server, so not run here — that requirement is independent of this bump.) - Separate yarn project — changes are confined to `twenty-meeting-bot/{package.json,yarn.lock}`; no root impact. ## Note vite 8 supports tsconfig-paths resolution natively (`resolve.tsconfigPaths: true`), so `vite-tsconfig-paths` could be dropped in a follow-up — left as-is to keep this change minimal. |
||
|
|
c6aca3f0ea |
fix(calendar): ID-first chunked import for Google and CalDAV (#22015)
Google and CalDAV returned full events and imported them inline in the list-fetch job. Large/initial syncs overran BullMQ's lock, the job stalled, the workspace query runner was released mid-import, and TypeORM threw 'Query runner already released'. Mirror the messaging pipeline: every provider now returns event IDs only, cached in Redis; the import job drains them in CALENDAR_EVENT_IMPORT_BATCH_SIZE chunks and re-enqueues until empty, so no single job runs long. Adds Google/CalDAV import-by-id services and a provider dispatcher; removes the full-events inline path. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22015?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. --> |
||
|
|
9b2ca6f3f7 |
bump sdk and app version for call recording bot app (#22019)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22019?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. --> |
||
|
|
e9d5d71cd3 |
Wire up search field metadata (#21964)
## Part 1 - Exact scope of the current PR (#21964) close https://github.com/twentyhq/core-team-issues/issues/2586 This PR introduces `searchFieldMetadata` as a first-class flat metadata entity and migrates the existing search surface onto it, with **no change to which records are searchable** (ISO with `main`). In scope (what the PR does): - New flat entity `searchFieldMetadata` (universalIdentifier, applicationId, **`position`**, maps, conversions), registered in the central flat-entity constants and the migration build orchestrator. - `searchVector.asExpression` is **derived server-side** from `searchFieldMetadata` rows (validated by `isSafeTsVectorExpression`); never trusted from client input. - **Derivation order is deterministic, driven by each row's `position`** ([compute-search-vector-as-expression-from-search-field-metadatas.util.ts](packages/twenty-server/src/engine/metadata-modules/flat-search-field-metadata/utils/compute-search-vector-as-expression-from-search-field-metadatas.util.ts)), replacing the previous non-deterministic `(createdAt, id)` sort. That sort collapsed to random UUIDs for standard fields (same `createdAt`), so any rename/relabel rewrote the `STORED` generated column to a logically-identical-but-textually-different expression and produced a permanent per-workspace diff vs the standard definition. Ordering now equals provisioning order; ties break on `universalIdentifier`. - Provisioning at object creation mirrors the existing surface exactly **and seeds `position`**: - custom objects -> the `name` field only, at `position: 0` ([build-default-search-field-metadatas-for-custom-object.util.ts](packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-search-field-metadatas-for-custom-object.util.ts)) - standard objects -> their curated `SEARCH_FIELDS_FOR_*` sets, `position` = the curated index - Backfill (instance + workspace commands in `2-16`) provisions rows for existing workspaces with the same surface **and the same positions** (standard from the curated standard maps, custom `name` = `0`), scoped to the workspace's own custom application ([build-search-field-metadata-backfill-operations.util.ts](packages/twenty-server/src/database/commands/upgrade-version-command/2-16/utils/build-search-field-metadata-backfill-operations.util.ts)). The `position` column is added in the same `2-16` fast instance command as `universalIdentifier`/`applicationId`. - Field rename of an already-indexed field recomputes `asExpression` (positions preserved, so order is stable) ([recompute-search-vector-on-field-rename.util.ts](packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-search-vector-on-field-rename.util.ts)). - Field delete drops the matching row(s) and recomputes; remaining rows keep their relative order (no renumber) ([from-delete-field-input-to-flat-field-metadatas-to-delete.util.ts](packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-delete-field-input-to-flat-field-metadatas-to-delete.util.ts)). - Object relabel is **additive** and ISO/regression-fix only: it indexes the new label identifier **appended last (`position = max(existing) + 1`)** without dropping `name` ([recompute-search-vector-on-label-identifier-update.util.ts](packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-search-vector-on-label-identifier-update.util.ts)). This is a deliberate, temporary bridge. Explicitly OUT of scope (deferred): - No API to edit `searchFieldMetadata` (no user-facing search-field configuration, including `position` — it is internal and only written by provisioning/backfill/recompute). - No auto-indexing of arbitrary searchable fields. Creating a custom TEXT/EMAILS/etc. field does NOT add it to search (the `computeSearchFieldMetadataCreationForFields` behavior was removed in `e6820ad`). - No field-type-transition handling (field type is immutable - not in `FLAT_FIELD_METADATA_EDITABLE_PROPERTIES`, so that path was dead code). - No `position` validation (uniqueness/range) and no multi-vector / per-field `weight` config — deferred to the configurable-search follow-up (#1428). Net: `searchFieldMetadata` becomes the source of truth for the *same* surface as `main`. The only intentional divergences from `main` are "relabel preserves `name`" (additive) and the deterministic `position`-ordered `asExpression` (a correctness/perf fix that is byte-identical to provisioning order, so it does not change the searchable surface). --------- Co-authored-by: Félix Malfait <felix@twenty.com> |
||
|
|
99e7c2cde0 | Improve meeting bot recording tab layout and transcript speakers ui (#22016) | ||
|
|
293ff4c462 |
Auto-generate app cover images from the app logo (#22011)
<img width="1388" height="858" alt="image" src="https://github.com/user-attachments/assets/59f16bf7-5908-4624-b3af-51416bbebba3" /> ## What When an app is built (`twenty build` / `twenty publish`), the SDK now generates a marketplace cover image and sets it as the app's screenshot, but only when the app declares a `logoUrl` and has no `screenshots`. The cover composites the app's logo and the Twenty logo over the branded halftone backdrop, matching the design reference. ## Why Most apps ship a logo but no screenshots, so their marketplace detail page had no hero visual. This gives them a polished cover for free, with no per-app design work. ## Notes for reviewers - Generation lives in the build path (`operations/build.ts`), not `buildManifest`, so `twenty dev` and the shared manifest builder are untouched. It is best-effort: on failure it logs a warning and the build continues. - The cover is written to `.twenty/output` and registered as a public asset + screenshot, so the existing copy/checksum/serve pipeline handles it unchanged. No app source files are modified. - Adds `sharp` as a runtime dependency of `twenty-sdk` (a build-time tool, like `esbuild`); it is not bundled into built apps. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22011?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. --> |
||
|
|
4f1ffa0a96 |
fix(twenty-partners): coerce null fields in TFT opportunity import (#22017)
## What The TFT `HTTP Request` action POSTs `null` for empty fields (e.g. `amountMicros:null`, `closeDate:null`). The import schema typed those as `z.number()/z.string().optional()`, which reject `null` (it is not `undefined`), so the endpoint returned `ok:false / invalid_input` before any API call. ## Fix A `dropNulls` preprocessor on the request schema converts `null` (top-level or nested) to "field absent" before validation. Null optional fields are simply omitted from the created opportunity; required `name` still fails correctly if null. No schema-shape or behaviour-contract change. ## Tests Added a case feeding the failing payload shape (`amountMicros:null`, `closeDate:null`) → `created:true` with `amount`/`closeDate` omitted. 42/42 unit pass, lint clean. Patch bump `1.1.1 → 1.1.2`. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22017?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. --> |
||
|
|
c52c983b90 |
Source app About description from README and improve internal app READMEs (#22012)
## What - The SDK manifest build now sources an app's `aboutDescription` (the long-form "About" tab content) from its `README.md`. An explicit `aboutDescription` in the config still wins, matching the existing marketplace CDN fallback. - Removed the now-duplicated `aboutDescription` from internal app configs and deleted the standalone `ABOUT_DESCRIPTION` constant files. - Rewrote internal app READMEs to read as user-facing About content: stripped developer/build/source-path noise, and expanded the thin ones. `call-recording` and `self-hosting` (one-liners over substantial apps) and `people-data-labs` were rewritten from a close reading of the code; `twenty-exa` was verified for accuracy. - Added a unit test (and a fixture README) covering README → `aboutDescription` in the build. ## Why The README and the About description were maintained separately and drifted. Making the README the single source keeps the About tab accurate and removes duplicated copy. ## Notes for reviewers - Internal apps depend on the published `twenty-sdk`, so the build change takes effect for them after an SDK release + dependency bump. Until then, published apps still get README → `aboutDescription` via the marketplace CDN sync. - Standard/Custom app descriptions are unchanged (they are resolved in the frontend, not via the manifest). <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22012?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. --> |
||
|
|
6520db22ca |
fix(deps): bump opentelemetry suite to core 2.8.0 (+ sentry 10.59) (#22010)
## Summary Bumps the OpenTelemetry suite onto the **`@opentelemetry/core` 2.8.0** wave (plus Sentry `10.51 → 10.59`, which carries the otel instrumentation), resolving [Dependabot alert #1510](https://github.com/twentyhq/twenty/security/dependabot/1510) (`@opentelemetry/core < 2.8.0`). ## Why a parent-bump, not a `resolutions` entry The vulnerable `@opentelemetry/core` is transitive, pulled in by the otel packages we declare (`exporter-metrics-otlp-http`, `exporter-prometheus`, `sdk-metrics`) **and** by `@sentry/*` (which bundles `@opentelemetry/instrumentation-*`). The otel **stable** packages pin `core` to their own exact version and are version-coupled — forcing `core` ahead of the suite via `resolutions` risks runtime breakage. So this bumps the declared parents instead. ## Changes - `twenty-server/package.json`: - `@opentelemetry/exporter-metrics-otlp-http` `^0.200.0 → ^0.219.0` - `@opentelemetry/exporter-prometheus` `^0.217.0 → ^0.219.0` - `@opentelemetry/sdk-metrics` `^2.0.0 → ^2.8.0` - `@sentry/{nestjs,node,profiling-node}` `^10.51.0 → ^10.59.0` - `yarn dedupe` collapses the remaining transitive `core@2.7.1` (caret consumers) onto `2.8.0` — the whole stable set (`core` / `resources` / `sdk-trace-base` / `sdk-metrics`) is now `2.8.0`. - **`@types/pg` added as a direct devDependency.** The newer Sentry drops the instrumentation that used to *transitively* provide `@types/pg`; twenty-server imports `pg` directly (`set-pg-date-type-parser.ts`), so it now declares its own types — fixing a latent fragility the bump exposed. ## Verification - `nx typecheck twenty-server` — **0 errors** (validates the otel/sentry API surface we call is intact). - `yarn install --immutable` passes. - No `@opentelemetry/core < 2.8.0` remains. - Lockfile churn is contained to the observability subtree (otel/sentry + their transitive deps; net **−615 lines**). > Sentry resolved to `10.59.0` rather than the just-published `10.60.0` due to the repo's `npmMinimalAgeGate`. > Worth a quick server-boot check during review to confirm Sentry/otel init at runtime. |
||
|
|
0f451897cf |
Make twenty-ui theming a consumer-facing API (#22007)
**What** - Add `useTheme()` and `useThemeColorScheme()` as the public theme accessors, and migrate the 88 internal `useContext(ThemeContext)` call sites to them. `ThemeContext` stays exported. - Make `ThemeProvider` overridable and scopeable: new `applyToRoot` (default `true`), `overrides` (a `--t-*` map), and `className` props. When scoping is requested it renders a `display: contents` wrapper that also serves as the themed portal container, exposed via `ThemeScopeContext` / `useThemeContainer()`. `AppTooltip` and `Modal` portal into that container, falling back to `document.body`. - Document the `--t-*` override contract in the README; barrels regenerated. **Why** Consumers had no stable theme accessor (they reached into the raw context) and no supported way to re-theme. This adds both without behavior change. **Reviewer notes** - The default path is unchanged: `applyToRoot` defaults to `true`, so the colorScheme class still lands on `<html>` and portaled overlays (tooltips, dropdowns, modals) stay themed. The global class is load-bearing for body portals; scoping is opt-in. - `twenty-front` is untouched (migrating its consumers is a separate follow-up). <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22007?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. --> |
||
|
|
85406a58fb |
Use minimal babel presets for wyw to fix the website Cloudflare build (#21994)
The website's Cloudflare build (`opennextjs-cloudflare` / Turbopack) was failing with `_defineProperty is not a function` while linaria/wyw evaluates `twenty-ui/dist/theme.cjs` at build time. It regressed in #21946, whose twenty-ui build rework changed the emitted `theme.cjs` so the theme objects ship as runtime object spreads (`{ ...THEME_COMMON }`). **Cause:** wyw evaluates modules in Node through `next/babel`, which pulls in `preset-env` + `transform-runtime`. Those re-lower the runtime spreads into `@babel/runtime` helpers imported as ESM; wyw then `require()`s that ESM module in a CJS context where the export is not callable, so `_defineProperty` fails. **Fix:** wyw runs in Node and needs no downleveling, so replace `next/babel` with minimal presets (`@babel/preset-typescript`, `@babel/preset-react`, `@wyw-in-js/babel-preset`, plus `@babel/plugin-transform-export-namespace-from`), matching twenty-front's wyw config. No `@babel/runtime` helpers get injected. Kept on the website side so twenty-ui keeps react/react-dom as peer deps (#21946). Note: no blocking PR check runs the website production build, so this is best validated via the website preview build or the twenty-infra deploy. |
||
|
|
16c9782c96 |
i18n - website translations (#22001)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22001?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> |
||
|
|
6a4dc37c6d |
Organize twenty-website product-feature into folder structure (#21999)
Applies the PipelineVisual folder structure to every other file in `product-feature`. - Each feature visual (Import, Tasks, Files, Emails, Dashboard, Contacts) becomes an `XVisual/` folder: shell + `index.ts` barrel, with `components/`, `data/`, `types/` (one export per file), and `utils/` as applicable. `BarChart`/`DonutChart` move into `DashboardVisual` (exclusive to it); `RecordTabHeader` stays shared. - The section's non-visual files get the same treatment: `components/` (Tiles, TileVisual, TileContent, ScrollEntrance, RecordTabHeader), `data/`, `types/`, `utils/`. `ProductFeature.tsx` stays the section shell. - Drop dead code: unused `WindowChrome` and the now-orphaned `product-feature-scene` token. No behavior change — `index.ts` barrels keep all import paths stable. Typecheck, check-conventions, oxlint, and unit tests pass locally. |
||
|
|
7b45380777 |
feat(ai): large tool output handling + navigation tools (#21982)
## Summary
Large tool outputs (e.g. a workflow run that serializes to ~70k tokens)
blow the chat context budget and force per-tool "raw" variants. This PR
handles oversized outputs generically in one place:
1. **Producer:** when a tool result exceeds a byte budget, it is spilled
to a `FileFolder.AgentChat` file and replaced with a compact `{ spilled,
outputRef, shape, hint }` envelope.
2. **Consumer:** two bounded, in-server navigation tools —
`extract_json_path` and `search_output` — let the model dig into the
spilled file by `fileId` without spinning up `code_interpreter`.
Together they add a fast, auditable middle tier between "truncated
inline preview" and "full code_interpreter relay," and enable an
enterprise "restricted" mode (spill + navigation, no sandbox).
## Data flow
```mermaid
flowchart TD
exec["resolveAndExecute / hydrateToolSet closure"] --> compact[compactToolOutput]
compact --> enabled{"spillLargeOutput enabled? (chat only)"}
enabled -->|no| inlineRaw["inline raw (MCP, workflow, sandbox bridge)"]
enabled -->|yes| size{"bytes > MAX_INLINE_TOOL_OUTPUT_BYTES?"}
size -->|no| inline["inline result"]
size -->|yes| skeleton["jsonShapeSkeleton + largeOutputHint"]
skeleton --> write["writeFile(AgentChat)"]
write --> envelope["return { spilled, outputRef, shape, hint }"]
envelope --> model[Model]
model --> nav["extract_json_path / search_output / code_interpreter (by fileId)"]
```
## Part 1 — Navigation tools (consumer)
- `extract_json_path`: extracts a sub-tree from a spilled JSON file by a
JSONPath-lite expression (dot/bracket access, array slicing,
single-level wildcard), with `maxItems`/`maxDepth` bounding. No filters
or recursive descent — those belong to `code_interpreter`.
- `search_output`: grep-like line search with context lines and
stateless `offset` pagination (`{ matches, totalMatches, hasMore }`).
- Both read from `FileFolder.AgentChat` by `fileId`, enforce their own
output byte cap, and are registered in `ActionToolProvider` (always
available; read-only).
## Part 2 — Spill producer
- Spilling slots in right after the existing `compactToolOutput` step at
the two seams in `ToolRegistryService` (`resolveAndExecute` and the
`hydrateToolSet` execute closure).
- `ToolOutputSpillService.spillIfTooLarge()` measures
`Buffer.byteLength`; over `MAX_INLINE_TOOL_OUTPUT_BYTES` (16 KB ≈ 4k
tokens) it writes the full payload and returns the envelope. Spill
failures never block the call (inline + warning).
- `jsonShapeSkeleton` computes a bounded structural map (depth 4, arrays
as `"array[N] of <type>"`, id-keyed maps collapsed, long leaves as size
markers, hard-capped at 1024 bytes) so the model knows the key paths in
one pass.
- Optional per-tool `largeOutputHint` (on the `Tool` type, threaded via
the descriptor) is used as the hint when present, else a generic hint.
The `shape` is always computed generically.
## Surfaces
Spilling is an opt-in flag (`spillLargeOutput`) mirroring
`compactOutput`:
| Surface | `spillLargeOutput` | Behavior |
| --- | --- | --- |
| AI chat / agent | `true` (in `chat-execution.service.ts`) | Spill on;
nav tools + `code_interpreter` in catalog |
| External MCP clients | unset | Raw output |
| Workflow agents | unset | Raw output |
| `code_interpreter` sandbox bridge | unset (it's an MCP call) | Raw
output |
The sandbox bridge inherits "no spill" for free via the MCP path — no
header sniffing, no `ToolContext.source` field.
## Design constraints (anti-micro-OS)
Exactly two navigation tools, no composition/piping, read-only, bounded
output. The boundary is: expressible as a single path lookup or text
search → nav tool; aggregation/correlation/transform →
`code_interpreter`.
## Notes / deviations from the plan
- `jsonShapeSkeleton` and `ToolOutputSpillService` live under the `tool`
module (not `tool-provider/output-transforms`) to avoid a `tool →
tool-provider` import cycle.
- Spill files use `{ isTemporaryFile: false, toDelete: false }` (same as
`code_interpreter`); `isTemporaryFile` here means files-field promotion,
not a TTL.
## Test plan
- [x] `extract-json-path` + `search-output` util unit tests (23 cases)
- [x] `jsonShapeSkeleton` unit tests (6) and `ToolOutputSpillService`
unit tests (4)
- [x] oxlint + oxfmt clean on changed files; `twenty-server` typecheck
clean (pre-existing unrelated errors aside)
- [ ] Manual: trigger an oversized tool result in chat, confirm the
envelope is returned and `extract_json_path` / `search_output` read the
spilled file by `fileId`
## Why no automated e2e
Spilling is chat-only and the chat path runs a live model, so the
black-box MCP integration harness can't deterministically trigger a
spill (MCP intentionally doesn't spill). The seam is small, explicit
flag-threading mirrored on `compactOutput`, covered by the unit suites.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21982?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. -->
|
||
|
|
c18787350f |
fix: month and year dropdowns in settings logs date picker (#21529)
### Summary - Fixes #21514 and Issue 2 - **Issue 1**: when opening the calendar and choosing a month or year, those lists could appear underneath the calendar, making them impossible to see and use. (Issue #21514) - **Issue 2**: after opening the calendar icon menu, clicking the month or year controls don't work, so you couldn’t actually change the month or year. Before: <img width="355" height="434" alt="607363028-0d3a302e-9dba-4d9a-b354-ad7cbcd1fba5" src="https://github.com/user-attachments/assets/b1c357d1-a7cf-4572-8737-721cf4e2597a" /> After: https://github.com/user-attachments/assets/ffc26447-ff34-4f11-a3b4-4c329e446ec4 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21529?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> |
||
|
|
e1c962acba |
feat(meeting-bot): configure Recall recording retention hours (#21978)
## Summary
- Sends an explicit Recall.ai recording retention policy when creating
or rescheduling meeting bots.
- Uses the optional server variable
`MEETING_BOT_RECORDING_RETENTION_HOURS` instead of a workspace/app
variable.
- Defaults to `166` hours (6 days and 22 hours), keeping Twenty-hosted
deployments below Recall.ai's 7-day free storage window while still
allowing self-hosters to configure a longer retention period.
## Why
Recall.ai accounts created after June 12, 2025 retain recording media
forever unless retention is configured. Twenty ingests the meeting
artifacts into its own storage, so Recall.ai media retention should be
bounded by default to avoid unnecessary third-party storage cost.
## Changes
- Replaces the days-based app variable with the server variable
`MEETING_BOT_RECORDING_RETENTION_HOURS`.
- Adds a default retention constant of `166` hours.
- Builds `recording_config.retention = { type: 'timed', hours }`
centrally through `getRecallBotRecordingConfig()`.
- Applies the same recording config to both bot creation and bot
rescheduling.
- Documents the server variable and warns that values above `168` hours
may incur Recall.ai storage charges.
- Updates Recall API tests to assert retention is sent and invalid
values fall back to the safe default.
## QA
- [x] `yarn test:unit`
- [x] `yarn lint`
- [x] `yarn exec tsc --noEmit -p tsconfig.spec.json`
- [x] `git diff --check`
- [x] Live Recall.ai bot payload includes `recording_config.retention =
{ type: 'timed', hours: 166 }`
---------
Co-authored-by: Emmanuel Hernandez <emmanuel.hernandez@clickbalance.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: ehconitin <nitinkoche03@gmail.com>
|
||
|
|
6f50b9d01e |
fix(twenty-partners): update Partner view in place on upgrade instead of deleting (#21995)
## Why Upgrading the already-installed `twenty-partners` app in place (0.5.x → 1.x) via `yarn twenty app:install` aborts during the sync reconcile: ``` view: INVALID_VIEW_DATA: Cannot delete the only view for this object (379b11d5-…) viewField: INVALID_VIEW_DATA: Label identifier view field cannot be deleted (21afcc69-…) ``` The marketplace-v2 change deleted `all-partners.view.ts`. On an installed workspace that view is the Partner object's primary view and holds the **label-identifier** viewField (the `name` column). Twenty's manifest sync refuses to delete an object's *only* view or a label-identifier viewField, so the in-place upgrade fails. (Fresh installs are unaffected; only upgrades from a version that had `all-partners` hit this.) ## What Repurpose the retired `all-partners` identity for `partners-validated` so the sync performs an **update in place** instead of a delete: - `partners-validated.view.ts` now uses the old view id `379b11d5-…`, and its `name` column reuses the old label viewField id `21afcc69-…`. - Remove the now-dangling `ALL_PARTNERS_VIEW_UNIVERSAL_IDENTIFIER` constant (its file was already gone). - Patch bump `1.1.0` → `1.1.1`. The resulting view is the intended "Partners Validated"; the other retired Partner view (`validated-partners`) deletes cleanly because the object keeps other views. ## Revision **Patch** — migration bugfix, no new behaviour. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21995?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. --> |
||
|
|
4789ba6265 |
feat(ai): add AI tools to list and inspect workflow runs (#21983)
- Add `get_workflow_run` and `list_workflow_runs` AI tools so the workflow agent can troubleshoot failed or misbehaving workflow runs — listing runs with optional filters (workflow, status, limit) and inspecting a specific run's steps, errors, and failed step logs. - Enforce `rolePermissionConfig` on all three read tools (`get_workflow_run`, `list_workflow_runs`, `get_workflow_current_version`) instead of bypassing permission checks, consistent with how `create_complete_workflow` and database CRUD tools work. - Add unit tests for the three tools covering permission forwarding, success paths, and error paths. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21983?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> |
||
|
|
47b48d83f7 |
fix(front): include isUIEditable/isRemote in CreateOneObjectMetadataItem so new objects aren't read-only until refresh (#21796)
## Problem
After creating a custom object in **Settings → Data Model**, the new
object's **"New Field"** (and **"Add relation"**) buttons are missing.
They only appear after a hard page refresh.
## Root cause
`CreateOneObjectMetadataItem`
(`packages/twenty-front/src/modules/object-metadata/graphql/mutations.ts`)
selected only a subset of object-level fields and omitted
`isUIEditable`, `isRemote`, `isSystem`, `isUICreatable`,
`universalIdentifier`, `shortcut`, and `duplicateCriteria` — all of
which are present in the shared `ObjectMetadataFields` fragment used by
the bootstrap query.
`useCreateOneObjectMetadataItem` writes the mutation response into the
metadata store via `addToDraft`. Because the mutation resolves *after*
the SSE create event and `addToDraft` replaces entries by `id`, the
reduced mutation response overwrites the fuller record that arrived over
SSE. The stored object then has `isUIEditable === undefined`, so
`isObjectMetadataReadOnly` returns `true` (`!undefined`), and
`ObjectFields` hides the action buttons via its `{!readonly && …}`
guard.
A hard refresh "fixes" it only because the bootstrap query repopulates
the store from `ObjectMetadataFields`, which includes the missing
fields.
## Fix
Add the missing object-level fields to the `CreateOneObjectMetadataItem`
selection so a newly created object matches the bootstrap shape, and
regenerate the metadata GraphQL types. No other code changes required.
## How to test
1. Go to **Settings → Data Model** and create a new custom object.
2. Open the new object's **Fields** tab.
3. ✅ The **"New Field"** button is visible immediately — no refresh
needed.
Before this change, the button was hidden until a manual refresh.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21796?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. -->
|
||
|
|
0e6d96bb5e |
fix(workflow): stop trigger/action filter Conditions from flashing on edit (#21952)
## Problem
Adding a condition to a database-event **trigger** (the new Conditions
section), the **Filter** action, or the **If/Else** action causes the
just-added condition to flash out and back in.
## Root cause
`WorkflowEditActionFilterBodyEffect` seeds the builder's local jotai
atoms from the persisted `defaultValue` through an effect that
**resynced whenever the live atoms differed from `defaultValue`** (the
atoms were in the effect deps and the equality check compared atoms vs
`defaultValue`).
A local edit writes the atoms **synchronously**, then persists through
an **async** mutation — and for an *active* workflow that mutation first
creates a draft version over the network. During that window the atoms
are ahead of the still-stale `defaultValue`, so the effect treated it as
"out of sync" and overwrote the edit back to the stale value, then wrote
it again once the save landed. That round-trip is the flash.
The resync existed for a real reason: the atoms are module-cached per
`instanceId` and persist across mounts, and the trigger shares a single
**constant** `instanceId` (`'trigger'`), so a previous trigger's filters
must be overwritten when a different one is opened. (This is also why
the `?? { stepFilterGroups: [], stepFilters: [] }` fallback was added in
#21868 — to reset builder state deterministically between trigger
edits.) So a naive "init-once" fix would reintroduce that stale-state
leak.
## Fix
Resync from `defaultValue` **only when `defaultValue` itself changes**,
tracked via the last-synced value in `useState` (not the live atoms).
This:
- never clobbers an in-flight local edit → no flash;
- still re-seeds when switching the trigger/action being edited → no
stale-state leak;
- preserves reflecting genuine external `defaultValue` changes.
The `hasInitialized*` flags are no longer needed and are removed (along
with the now-unused `stepId` prop on the effect).
## Tests
Adds a regression test covering: seeding from `defaultValue` on mount,
the **no-clobber-while-stale** invariant (the flash), and resync on a
genuine `defaultValue` change. Verified the no-clobber test **fails**
against the old "resync against live atoms" behavior and passes with the
fix.
## Verification
- `nx typecheck twenty-front` ✅
- `nx lint:diff-with-main twenty-front` ✅ (0 warnings / 0 errors)
- New unit test: 3 passing ✅
## Known residual / follow-up
On an *active* workflow, the first edit creates a draft version over the
network; making a second edit before that round-trip completes leaves a
narrow window where the optimistic echo of the first value could
momentarily win. Far narrower than the current flash-on-every-edit.
Eliminating it entirely (and resolving the still-open HIGH-severity
"constant `instanceId`" review flag from #21868) would mean giving the
trigger a unique `instanceId` per workflow version + a React `key` to
reset on remount — proposed as a separate, scoped follow-up.
https://claude.ai/code/session_01XnjtzFepMJX2VFwnQVcQbV
---
_Generated by [Claude
Code](https://claude.ai/code/session_01XnjtzFepMJX2VFwnQVcQbV)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21952?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. -->
|
||
|
|
9e31ffdf68 |
feat(messaging): webhook push sync for Gmail, Calendar and Microsoft (#21970)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21970?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. --> |
||
|
|
c7ad1ff8ee |
Route Recall webhooks by workspace metadata (#21991)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21991?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. --> |
||
|
|
c98620e14c |
Rework website PipelineVisual to match twenty-front kanban (#21989)
Reworked the product page's `PipelineVisual` to match twenty-front's kanban — board header, card layout/fields, hover states, and font weights. Also loaded and pinned Inter (twenty-front's product font) for the app-preview mockups, which previously fell back to the system font. <img width="879" height="625" alt="image" src="https://github.com/user-attachments/assets/396f0a90-114b-47f9-92a6-2e74d732a91e" /> |
||
|
|
50c4659cae |
feat(twenty-partners): import an opportunity from TFT via manual workflow (#21979)
## What
Adds a one-way, manual copy of a single Opportunity from the
**twentyfortwenty (TFT)** workspace into **partners**. No automatic/echo
sync — one record per button press.
A TFT-side **manual Workflow** (a "Run workflow" button on the
Opportunity record) → **HTTP Request** action → `POST /s/opportunities`
on the partners app. The new `import-opportunity-from-tft` logic
function:
- **Shared-secret guard** on the `x-application-secret` header vs the
existing `PARTNER_APPLICATION_SECRET` app variable (the SDK's
`isAuthRequired` only accepts user JWTs, not API keys — same pattern as
`submit-partner-application`).
- **Idempotent on `tftOpportunityId`** (a field the partners Opportunity
object already has); falls back to `name` for manual calls. Re-press →
no duplicate.
- **Find-or-create** the Company (by name) and the point-of-contact
Person (by primary email), then `createOpportunity` with `name / amount
/ closeDate / stage / companyId / pointOfContactId`.
## Out of scope
- The **TFT-side workflow is a UI step** (built once in the TFT
workspace) — it can't live in this repo. The exact HTTP action config +
body mapping lives in the workflow itself.
- Owner/workspace-member copy and stage-enum remapping are intentionally
skipped (YAGNI).
## Files
- `src/logic-functions/import-opportunity-from-tft.logic-function.ts` —
the handler + manifest.
- `src/logic-functions/__tests__/import-opportunity-from-tft.test.ts` —
unit tests (auth reject / idempotent / mapped create).
- `package.json` — version bump **0.5.5 → 0.6.0** (minor; new feature).
## Verification (local bundle)
- `yarn test:unit` 3/3 · `yarn lint` 0/0 · `yarn twenty dev --once` →
`created logicFunction import-opportunity-from-tft`, no manifest
warnings.
- Live `POST /s/opportunities` with the secret → `201
{ok:true,created:true,id}` (confirms the app role can create Opportunity
+ Company + Person). Re-POST same `tftOpportunityId` → `created:false`.
Wrong secret → `unauthorized`.
## Deploy note
Additive (new logic function + HTTP route) — upgrades cleanly with
`deploy` + `install`. `PARTNER_APPLICATION_SECRET` is already set on
prod, so no new application variable to configure.
|
||
|
|
6e7d8ef96c |
[Twenty-front]: Record table Header drag and drop functionality (#21304)
Closes #21303 and https://github.com/twentyhq/core-team-issues/issues/151 https://github.com/user-attachments/assets/45cee1be-464f-467e-a1c0-cf5354ff87db --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
46642c81c9 |
fix(front): unblock email verification on the central domain (blank modal) (#21980)
## Problem
After clicking the email-verification link on the central domain (e.g.
`app.twenty.com/verify-email?...`), a new user is left staring at a
**blank white auth modal** and onboarding never continues. The email is
actually verified — the user is just never moved off the verify-email
page.
## Root cause
`VerifyEmailEffect` (mounted on `/verify-email`) handles the
central/workspace‑agnostic domain like this:
```tsx
if (!isOnAWorkspace) {
await verifyEmailAndGetWorkspaceAgnosticToken(emailVerificationToken, email);
return enqueueSuccessSnackBar(successSnackbarParams);
}
```
It renders nothing of its own in this branch (`return <></>`) and relies
entirely on the auth hook to navigate.
The onboarding workspace-creation refactor (**#21641** "Let users pick
their workspace subdomain during sign-up", refined by **#21723**)
changed `navigateAfterMultiWorkspaceSignInUp`:
- **Before:** a user with `0` workspaces was sent through
`createWorkspace()`, which created the workspace and **redirected to the
workspace subdomain** — navigating away from `/verify-email`.
- **After:** for multi-workspace it now only does
`setSignInUpStep(SignInUpStep.WorkspaceCreation)` (the new
name/subdomain/logo form) — **no navigation**.
`signInUpStepState` is read **only by the `SignInUp` page**
(`/sign-in-up`), which renders `SignInUpWorkspaceCreationForm` for that
step. But the user is on `/verify-email`, whose route renders only
`VerifyEmailEffect` — which knows nothing about the step state and
returns an empty fragment. Nothing bridges the gap
(`usePageChangeEffectNavigateLocation` also won't redirect, because
`/verify-email` is whitelisted in `ONGOING_USER_CREATION_PATHS`), so the
user is stuck on an empty modal.
### Scope of the breakage
- **Broken:** new user, multi-workspace instance (Twenty Cloud central
domain), email verification enabled, signing up to create a workspace
(`0` workspaces). The `2+`-workspaces case (`WorkspaceSelection`) is the
same.
- **Not affected:** the single existing-workspace case (still does a
real `redirectToWorkspaceDomain`), the workspace-subdomain verification
path (`verifyEmailAndGetLoginToken` → `verifyLoginToken`), and
single-workspace self-host.
## Fix
After a successful workspace-agnostic verification, hand off to the
`SignInUp` page so it mounts and renders whatever step the hook just
set:
```tsx
if (!isOnAWorkspace) {
await verifyEmailAndGetWorkspaceAgnosticToken(emailVerificationToken, email);
enqueueSuccessSnackBar(successSnackbarParams);
return navigate(AppPath.SignInUp);
}
```
This is intentionally scoped to `VerifyEmailEffect` (the only entry
point that lives on a route which doesn't host the sign-in-up step UI).
The in-app sign-in/sign-up callers of
`navigateAfterMultiWorkspaceSignInUp` are already on `/sign-in-up`, so
they're untouched — keeping their query params (invite tokens, billing
checkout, returnToPath) intact. For the single existing-workspace edge
case, the hook's redirect still wins.
## Testing
- New `VerifyEmailEffect.test.tsx`:
- central-domain success → navigates to `AppPath.SignInUp` + shows the
success snackbar;
- failure → does **not** hand off to `SignInUp` (error state is shown);
- workspace subdomain → workspace-scoped path is untouched (no
workspace-agnostic call, no `SignInUp` hand-off).
- `nx typecheck twenty-front` ✅, `oxlint --type-aware` + `oxfmt` on
changed files ✅.
https://claude.ai/code/session_017oVwW12hC42RdCgSKK8dFP
---
_Generated by [Claude
Code](https://claude.ai/code/session_017oVwW12hC42RdCgSKK8dFP)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21980?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. -->
|
||
|
|
24533b510c |
feat(twenty-partners): marketplace v2 — Application-driven matching workspace (#21816)
## Summary Restructures the Twenty Partners app into an **Application-driven matching workspace**: leads post briefs (Opportunities), partners browse and **self-apply**, admins review applications and assign a winner. The candidacy funnel lives on `Application.state`; the deal lifecycle lives on the stock Opportunity `stage`. Version **1.0.0** — **breaking**: removes the legacy `matchStatus` field and the auto-match flow. Prod upgrade path is **uninstall → deploy → install** (not an in-place upgrade). ## How "Apply" works — no workflow, no special permission Partners apply by **creating an Application directly** from a listed brief (a normal record write, governed by the Application object permission). The `on-application-created` logic function (shipped in the manifest) then resolves the partner from `createdBy`, sets `state = APPLIED`, stamps `partner`/`partnerUser`/`lastActivityAt`, and dedupes by (opportunity, partner). - **No `WORKFLOWS` permission flag.** An earlier iteration used a manual "Apply" workflow, but running a manual workflow requires the `WORKFLOWS` flag, which **cannot be granted on an app-owned role** (the manifest sync drops `role → permissionFlag` links, and the metadata API rejects out-of-band grants on app roles). Self-apply via record-create sidesteps this entirely and is prod-viable as-is. - `Application.state` **defaults to `APPLIED`** so a partner never sees a misleading "Invited" flicker while the async handler runs. Admin invites set `INVITED` explicitly. ## ⚠️ Manual setup after install (per workspace) 1. **`yarn rls:configure`** — applies the partner row-level predicates and verifies field-locks (predicates can't ship in the manifest). Required for partner scoping. 2. **"Mark as Winner" workflow** — one manual-trigger workflow on **Application** → *Update Record* that sets `Opportunity.partner`, which drives the WON/BACKUP cascade. Admins run it (admins bypass the flag via `canUpdateAllSettings`); equivalent to editing the Opportunity's `partner` field directly. Steps in `src/workflows/README.md` (the **Apply** section there is superseded by self-apply). ## What's included - **Data model:** new `BACKUP` Application state; symmetric cascade owned by `on-opportunity-partner-won` — assign → winner `WON`, other applicants `BACKUP`; unassign → all reopen to `APPLIED`. `Opportunity.partner` is the single source of truth. - **Removed:** `matchStatus` field + `on-opportunity-auto-match` (dead). Deal lifecycle now on the stock `stage`. - **Partner row-level security (B7):** RLS predicates scope partners to their own `Partner`/`Person`/`Company`/`Application` rows; `Opportunity` is `(partnerUser IS me) OR (isListed = true)` so listed briefs are visible to all partners; `Application` is `(partnerUser IS me) OR (lastActivityAt IS EMPTY)` — the IS-EMPTY branch lets a partner's own insert pass (partnerUser is stamped just after insert). Field-locks make Opportunity `stage`/`amount` and most Application fields read-only for partners (pitch stays editable). Applied via `yarn rls:configure`. - *Trade-off:* an unstamped application (lastActivityAt null) is briefly readable by any partner — sub-second window, permanent only if the handler fails to stamp. Acceptable for an internal marketplace; the front-component Apply path (below) would remove it. - **Idempotency:** `on-application-created` dedupes duplicate applications by (opportunity, partner). - **Views & navigation**, reorganized into sections: - **Partner Workspace:** Open Briefs · My Applications · My Profile · My Deals - **Matching Admin:** Briefs to Match · Deals (board) · Applications · Applications by Opportunity · All Opportunities · Follow-up Applications · Follow-up Briefs - **Partners:** per Stage · per Country · Partner Applications · Validated (per-group COUNT) - Opportunity & Partner record **side panels** via FIELDS_WIDGET views (surface relations incl. `applications`, so a brief shows all its applications). ## Known issues / follow-ups - **Pre-existing failing unit test (not introduced here):** `on-partner-application-created › "posts a Discord embed when an APPLICATION-sourced partner is created"` — the handler/test are byte-identical to base; tracked separately. - **Follow-up views** lack the "older than 7 days" staleness filter — no confirmed relative date operand in this Twenty version (TODOs left in the views). - **Apply UX (future):** a front-component "Apply" button on the brief, calling an authenticated `/s/apply-to-brief` logic function (runs as the app), would replace the "create a record" entry point — nicer UX, and it removes the RLS IS-EMPTY trade-off. Not required to ship. ## Testing - Partner self-apply verified end-to-end as a partner (create application from a brief → lands on `APPLIED`). - Unit tests for the WON/BACKUP cascade + application handlers pass (the one failing test above is the pre-existing, unrelated Discord handler). - Lint clean (`oxlint`). <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21816?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-light.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
242b989c0e |
fix(emails): stop reply composer infinite re-render (#21935)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21935?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. --> |
||
|
|
a5aac3c21e |
Logic function handler name hardened validation (#21956)
# Introduction Introduce centralized handlerName validation for the logic function handlerName inside the flat logic function validator Even if not safe by definition, avoid string interpolation inside the local driver executor when retrieving the handler name from the parent module <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21956?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. --> |
||
|
|
a884945aca |
fix(deps): remediate HIGH image vulns (multer, ws, nodemailer) (#21984)
## What Clears the HIGH-severity AWS Inspector findings on the `twenty-server` container image. All three have stable, in-range fixes — no prereleases. | Package | From → To | CVE | Path | |---------|-----------|-----|------| | multer | 2.1.1 → **2.2.0** (resolution) | CVE-2026-5038, CVE-2026-5079 (DoS) | transitive via `@nestjs/platform-express` | | ws | 8.20.1 → **8.21.0** (resolution) | CVE-2026-48779 | pinned by `@nestjs/graphql` (8.21.0 already in tree) | | nodemailer | 8.0.10 → **9.0.1** | GHSA-p6gq-j5cr-w38f | nested in `imapflow`; bumped `imapflow` 1.3.6 → 1.4.2 which depends on nodemailer 9.0.1 | ## Notes - **multer 2.2.0 is the stable fix.** The advisories ([CVE-2026-5038](https://advisories.gitlab.com/npm/multer/CVE-2026-5038/), [CVE-2026-5079](https://advisories.gitlab.com/npm/multer/CVE-2026-5079/)) list both `2.2.0` and `3.0.0-alpha.2` as fixed; Inspector reported only the `3.0.0-alpha.2` prerelease, but we stay on the stable 2.x line. - **nodemailer:** the top-level dep was already `^9.0.1`; only `imapflow`'s nested copy was stale. imapflow 1.4.0 still ships nodemailer 8.0.10 and 1.4.1 ships 9.0.0 (< the 9.0.1 fix), so **1.4.2 is the minimum** that pulls the patched nodemailer. - Lockfile-only resolution for multer/ws (they're transitive); imapflow is a direct dep bump. yarn.lock net-shrinks from deduping. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21984?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. --> |
||
|
|
1646bdf35e |
Add twenty-partners on internal ci apps (#21975)
as title <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21975?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. --> |
||
|
|
d2083e7a1b |
Set OpenAI Responses store false for AI chat and agents (#20888)
## Summary This PR sets `openai.store = false` for Twenty's `@ai-sdk/openai` AI calls. This follows the approach discussed in #20877: instead of adding a new Twenty-specific Zero Data Retention config variable, OpenAI Responses calls no longer rely on OpenAI-stored response/item references. This should help Zero Data Retention organizations and may also avoid stale persisted-item replay errors for non-ZDR OpenAI users. Changes included: - Adds a shared OpenAI provider-options helper that merges `openai.store = false` for `@ai-sdk/openai` models. - Applies the helper to AI chat `streamText` calls. - Applies the helper to workflow/agent `generateText` calls. - Preserves OpenAI encrypted reasoning metadata through DB/UI message mappers so reasoning context can be replayed without stored OpenAI item references. - Does not add a new env/config variable. Related to issue #20877. ## Behavior / Tradeoffs This changes OpenAI Responses behavior for all Twenty OpenAI users, not only ZDR users. The intended benefit is that Twenty no longer depends on OpenAI-stored response/item references. The main tradeoff is reduced provider-side item-reference reuse for non-ZDR OpenAI users. To reduce the impact for reasoning models, this PR preserves `providerMetadata.openai.reasoningEncryptedContent` through message persistence/replay so reasoning context can still be provided without stored OpenAI item references. ## Tests - Focused server Jest tests for OpenAI provider-options merging and reasoning metadata mapping. - Focused frontend Jest test for reasoning metadata mapping. - `oxlint` and `oxfmt --check` on changed files. - `git diff --check`. --------- Co-authored-by: Charles Bochet <charles@twenty.com> Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com> |
||
|
|
2dc6034a6c |
Add twenty meeting bot to internal application ci (#21974)
- remove useless github-connector (validated with @charlesBochet) - add twenty-meeting-bot to internal apps |
||
|
|
5f22908588 |
Decouple twenty-ui Avatar from app server-URL config (#21968)
Makes `twenty-ui`'s `Avatar` render the `avatarUrl` it receives instead of building it from `window._env_`/`window.location` at module load, so the library no longer depends on the app environment. URL resolution moves to `twenty-front` via a `getAbsoluteImageUrl` helper applied at the call sites. Part of making twenty-ui a standalone library. |
||
|
|
e59e102448 |
chore: bump version to 2.16.0 (#21973)
## 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/21973?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> |
||
|
|
96a2987610 |
Fix/sanitize chart filters on save (#21958)
# Fix: sanitize chart filters referencing deactivated/deleted fields ## Summary When a field used in a chart (graph) widget filter was later **deactivated or deleted**, saving the page layout failed with a backend error such as: > Chart "...": One of the chart filters uses "...", but it was deleted. Please remove or replace this filter rule. This happened even after the user tried to remove the offending filter rule, because the invalid filter could still end up in the saved configuration. This PR makes invalid chart filters get cleaned up reliably — both as the user edits filters and, as a safety net, at save time. ## Root causes - **Edit-time persistence kept invalid filters.** `handleFiltersUpdate` persisted the current filter state to the page layout draft without sanitizing it against the object's active fields. An invalid filter (referencing a deactivated/deleted field) was re-saved on every update, blocking the configuration from being accepted. - **Save never enforced the cleanup.** `useSavePageLayout` serialized the draft as-is. The "filters referencing deactivated/deleted fields will be automatically removed on save" promise shown in the warning banner was only honored reactively (when the filter panel was actively edited), never at the actual save boundary. A chart whose filter panel wasn't touched kept its stale invalid filter in the payload. - **Query time treated inactive fields as valid.** `useGraphWidgetQueryCommon` considered all fields (including inactive ones) valid, so deactivated-field filters were never dropped when running the chart query. ## Changes ### Edit-time (keeps draft and UI in sync as you edit) - `ChartFiltersSettings` — sanitize filters in `handleFiltersUpdate` before writing to the draft, dropping any filter whose `fieldMetadataId` is not in the active-fields set. - `dropChartRecordFiltersWithDeletedFields` — enhanced to also clean up filter groups left orphaned once invalid filters are removed (iteratively removing empty groups and re-parenting checks). - `useGraphWidgetQueryCommon` — restrict valid field IDs to `isActive` fields so deactivated-field filters are silently dropped at query execution. - `ChartFiltersDeletedFieldsWarning` — updated copy to mention both deactivated and deleted fields. ### Save-time safety net (guarantees no invalid filter is ever persisted) - New `sanitizeChartFiltersInPageLayoutDraft` util — walks every chart widget in the draft and drops record filters (and now-orphaned groups) whose `fieldMetadataId` is not in the widget object's set of active fields. It leaves non-chart widgets untouched and leaves filters intact when the object metadata can't be resolved (avoids wiping valid filters during metadata loading). - `useSavePageLayout` — builds a `Map<objectMetadataId, Set<activeFieldId>>` from `useObjectMetadataItems()` and sanitizes the draft before converting it to the update input. This layer only ever removes filters whose field is genuinely deactivated/deleted — the exact set the backend rejects — and never removes filters pointing at valid fields. ## Tests - `dropChartRecordFiltersWithDeletedFields.test.ts` — extended coverage for orphaned filter-group cleanup. - `sanitizeChartFiltersInPageLayoutDraft.test.ts` — new: drops deactivated/deleted-field filters on save, keeps valid filters, cleans up orphaned groups, leaves non-chart widgets alone, and leaves filters untouched when object metadata is unresolved. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21958?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> |
||
|
|
6ce7d04f9d |
Fix field widget textarea focus reset (#21959)
Short description: Keeps the field widget textarea on a local draft value while focused so record-store rewrites do not reset the active caret. # Before Typing in an editor-mode text field can lose caret position when the global record store is rewritten by an external record update/refetch. https://github.com/user-attachments/assets/1ee7a819-2c27-4d09-aae5-814c2cf27181 # After The focused textarea should preserve the in-progress draft and caret while still updating sibling previews optimistically and flushing the final value on blur. https://github.com/user-attachments/assets/41832493-021f-46e8-bc75-722bbb1cd7b7 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21959?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. --> |
||
|
|
b1ada79d72 |
fix(front): scope relation table widget via currentRecordId (#21965)
## Context Follow-up to #21293 (merged). That PR added a bespoke `relationTableFilter` to keep a relation field rendered as a record-table widget scoped to the host record. It turns out to be redundant. ## Why it's redundant The relation table widget's view already carries an `isCurrentRecordSelected` relation filter on the inverse field — it's baked in by `useAddDraftViewForFieldRelationTableWidget` when the widget is configured. That filter is resolved through the `currentRecordId` that `FieldWidgetRelationTable` provides via `RecordFilterValueDependenciesContext`, and `turnRecordFilterIntoGqlOperationFilter` turns it into exactly `{ `${inverseField}Id`: { in: [recordId] } }`. So the hand-built `relationTableFilter` duplicated a filter the existing mechanism already produces from `currentRecordId`. ## Changes - Remove `relationTableFilter` from `RecordFilterValueDependenciesContext` - Stop reading/applying it in `useFindManyRecordIndexTableParams` and `useAggregateRecordsForRecordTableColumnFooter` - Delete the `getRelationTableFilter` util and its test - `FieldWidgetRelationTable` provides `currentRecordId` only Net −263 lines; relies on the existing `isCurrentRecordSelected` + `currentRecordId` scoping path. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21965?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. --> |