17ff17bdec9b6abf826216c9dbe95f425ce76d59
5279 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
17ff17bdec |
Dnd library migration fixes and changes (#23752)
Follow-up to #23211. Fixed issues, and simplified where possible. The core idea: every sortable list now resolves its drop position the same way — "sortable over sortable", comparing the pointer against the hovered item's midpoint — instead of each surface owning bespoke droppable slots and end-drop zones. ## Refactors - New `resolveDropFromPointer` handles both axes in one util; items can tag their own `orientation`, so one provider can drive lists of mixed axes. - Dropped `DragDropItemDroppableSlot` and `DragDropItemDropLine` path. Record table/board headers, page-layout tabs & widgets, and fields config all derive the drop index from the hovered sortable, matching record-board cards. - `DragDropItemSortableCell` is now the single sortable primitive, with drag optionally delegated to an explicit `DragDropItemSortableHandle`. - Removed end-drop constants/types; lists now place a trailing append target and resolve the append position in the consumer's own index space. ## Fixes - Dragging a row within grouped records threw an error — the drag overlay now resolves the source row's record-group context. - Multi-select drag counter chip didn't show — drag state was read from the wrong component scope instead of the active `recordIndexId`. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23752?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Félix Malfait <felix@twenty.com> |
||
|
|
198ffbb6bd |
Cancel drag activation when the drag source unmounts mid-gesture (#23816)
## Summary Follow-up to the Sentry error [TWENTY-FRONT-HJ8](https://twenty-v7.sentry.io/issues/TWENTY-FRONT-HJ8) (`Cannot start a drag operation without a drag source`), seen on a dashboard page and discussed in #23752. A pointer drag only activates once the pointer travels past the activation constraints (distance/delay). dnd-kit's `PointerSensor` captures the pressed draggable on pointerdown, and when the constraint is satisfied it starts the drag by resolving that draggable's id in the registry. If a re-render unregistered it in between, the lookup fails and `manager.actions.start()` throws. That window is real in our UI: virtualized table rows remount under new per-instance sortable ids, and widgets/tabs remount while a page loads. The breadcrumbs of the Sentry event show the gesture straddling a navigation onto a loading dashboard, with the error firing on the activating `pointermove` 190ms later. ## Fix `PointerSensorWithSourceGuard` extends `PointerSensor` and checks the registry before starting: if the pressed draggable is gone, it cancels the gesture through the sensor's own cancel path (same one dnd-kit wires to activation aborts) instead of throwing. There is nothing left to drag at that point, so cancel is the correct outcome. `DND_KIT_SENSORS` now uses it, which covers every dnd surface. Unit tests pin the behavior with real dnd-kit internals: the base sensor throws in this scenario (documents why the guard exists, and breaks if upstream fixes it so we can remove the subclass), the guard cancels and leaves the operation idle, and a still-registered source starts normally. Fixes TWENTY-FRONT-HJ8 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23816?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. --> |
||
|
|
3f9137bd4e |
Fix widget view save paths dropping relationTargetFieldMetadataId (#23814)
## Context `viewFilter.relationTargetFieldMetadataId` (relation traversal, added in 2.6.0) is accepted and persisted by the `upsertViewWidget` mutation, and `mapViewFiltersToFilters` restores it when loading a widget view. But two frontend mappers silently dropped it, so any relation-traversal filter on a record table widget view was lost the moment the layout was saved (or the moment the user edited the widget's filters in the side panel): - `useSaveRecordTableWidgetViews` omitted the field when building the `upsertViewWidget` input - `useRecordTableWidgetFilterCallbacks` omitted it when syncing current record filters back into the widget view draft ## Changes - Carry `relationTargetFieldMetadataId` through both mappers - Add regression tests for both hooks (they fail without the fix) This is a prerequisite for nested relation field widgets (see follow-up PR), which rely on a traversal filter surviving the widget view save path. --- _Generated by [Claude Code](https://claude.ai/code/session_01Xp3AgGtc4kSP8PpgpKMWLQ)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23814?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. --> |
||
|
|
5b6734691c |
Fix workflow email body overflowing into the attachments section (#23810)
before (introduced by https://github.com/twentyhq/twenty/pull/23657) <img width="381" height="736" alt="Screenshot 2026-08-05 at 15 18 38" src="https://github.com/user-attachments/assets/91d5d1de-bffb-4508-a488-2fe6a09eca61" /> after <img width="1308" height="748" alt="image" src="https://github.com/user-attachments/assets/06a9b875-6cde-47c6-a23f-f9a1f268db42" /> ## Problem In the workflow Send Email action panel, the Body editor painted over the Attachments section when the panel was shorter than the editor's minimum height. |
||
|
|
29e68a7f87 |
Refactor outbound email content compilation (#23782)
## Integration status This is now the final landing PR for the reviewed editor/email architecture stack. | Order | Pull request | Scope | Status | | --- | --- | --- | --- | | 0 | #23657 | Advanced text editor capability presets | Merged into `main` | | 1 | **This PR** | Outbound email content compilation | Ready to land into `main` | | 2 | #23783 | Clean editor surface seam | Reviewed and merged into this branch | | 3 | #23790 | Shared editor block catalog | Reviewed and merged through #23783 | | 4 | #23791 | Canonical TipTap document persistence | Reviewed and merged through #23790 | The current branch tree contains the complete stack. Merging this PR lands all four follow-up layers. ## Architecture The stack establishes four reusable boundaries: 1. **Outbound compilation** — campaign, workflow, and one-to-one/tool email share one compiler, sanitizer policy, renderer, and plain-text derivation path. 2. **Editor surface profiles** — the generic editor owns rendering mechanics while each consuming surface declares chrome, extensions, and explicit compatibility readers. 3. **Shared block primitives** — sections, columns, HTML, images, buttons, and related commands live in the neutral advanced-editor catalog; email behavior is supplied by email schemas/rendering, not by relocating reusable blocks into an email editor. 4. **Canonical persistence** — Twenty-owned authoring persists complete, versioned TipTap JSON documents. HTML, Markdown, plain text, and BlockNote are projections or explicitly owned legacy boundaries. ## Compatibility boundaries Compatibility remains only where shipped data requires it: - workflow Send Email: versionless TipTap JSON, HTML, and plain text - inline email: HTML - AI instructions: Markdown - record rich text: BlockNote arrays and older Markdown/plain text Campaign is unshipped, so its editor, stored rows, sendability validation, and send-time compilation require the current canonical schema version. AI chat drafts are canonical-only local state; old or malformed drafts are rejected at hydration, and plain-text preprompts are converted at their entry point. ## Outbound compiler details The shared compiler owns: - strict structured email-document parsing - React-email rendering - one cached DOMPurify/JSDOM policy for structured and legacy HTML - plain-text derivation from sanitized HTML - single-pass structured-document binding resolution across text, variable tags, links, images, buttons, and raw HTML Resolved workflow values remain inert, legacy workflow and one-to-one HTML remain supported, and Campaign HTML/plain text come from the same compiled result. ## Verification - all automated standard/security reviews passed on the three merged upper PRs with no unresolved threads - shared TipTap/email codec tests: 20 passing - editor, AI draft, and workflow compatibility tests: 14 passing - campaign validation and compilation tests: 31 passing - full shared suite during development: 223 suites / 1,738 tests passing - twenty-front, twenty-shared, and twenty-server typechecks - changed-file type-aware lint and formatting checks |
||
|
|
76bf3651bb |
Fix stray bracket after AI chat chips (#23798)
Chips in the AI chat sometimes rendered with a leftover `]` after them. The reference marker is bracket-asymmetric: it opens with `[[` and closes with `[[/kind]]`, so a complete reference holds four `[` and only two `]`. The model balances that by writing `…[[/object]]]`, and the parser ended the match exactly at the close tag, leaving the extra bracket as prose next to the chip. The parser now absorbs up to as many surplus `]` as the reference opened with, and accepts an opener with extra `[` so an over-wrapped marker doesn't leak one either. The system prompt also tells the model the marker is complete as written. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23798?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. --> |
||
|
|
59672b71b8 |
Add a book-a-call onboarding step for qualified leads (#23521)
https://github.com/user-attachments/assets/76d5a14e-53bd-4195-963b-bf9bb265c8c1 Large-company signups either self-serve a small plan or drop off at the paywall without sales ever seeing them. This adds an embedded Cal.com booking step to onboarding, shown only to leads worth a call. The step sits between Invite Team and the plan step: the lead has built out a workspace by then, and sales gets a chance before checkout. It is always skippable, and a successful booking advances automatically. Qualification reuses the employee count from the People Data Labs enrichment added in #23199. `ONBOARDING_BOOK_CALL_MIN_EMPLOYEE_COUNT` sets the bar; leaving it unset means the step never appears. `CALENDAR_BOOKING_PAGE_ID` must also be configured, so the step can never strand someone on an empty embed. Enrichment is no longer gated on `IS_ONBOARDING_AI_CHAT_ENABLED`, since the book-a-call step is now a second consumer of it. `PEOPLE_DATA_LABS_API_KEY` remains the instance-level switch. The existing `/book-call` page is reused: it moves into the onboarding shell and its footer switches between Skip (as a step) and the back link (when reached from the plan page). <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23521?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. --> |
||
|
|
1d755983ff |
Feat/advanced text editor capability presets (#23657)
# Email editor for Compose campaigns
## Short version
Campaign bodies are currently plain rich text. This PR turns the
composer into an email editor: a centered email canvas with section,
column, button, divider, image and raw-HTML blocks, each editable
through a settings side panel, rendered to email-safe HTML per recipient
at send time. Modelled on Resend's Broadcast editor.
**Product**
- Email canvas with page/body styling (background, width, padding,
corner radius, border, text colour, alignment)
- Blocks: section, 2/3 columns, button, divider, raw HTML, images —
insertable from a floating left rail or the slash menu
- A **section is a container whose typography cascades to its
contents**, so one part of an email can have its own look
- Block settings panel focuses whatever you select and shows its
effective values
- Per-recipient variables (`{{firstName}}`, `{{lastName}}`,
`{{fullName}}`, `{{email}}`, `{{personId}}`) usable in text,
button/link/image URLs, image labels and raw HTML
- Image upload by drag-drop, paste or file picker
**Technical**
- Presets now declare **capabilities** instead of surfaces forking the
editor; the UI derives itself from loaded extensions
- Editor behavior lives in `twenty-front`; the versioned email-document
schema and structural traversal live in `twenty-shared`; rendering lives
in `twenty-emails` — HTML is produced server-side per recipient
- Section typography cascade is **resolved at render time**, not left to
CSS: react-email hardcodes `fontSize`/`lineHeight` on every paragraph
and Outlook ignores `inherit`
- Logic vendored from Resend (MIT); all controls rebuilt on `twenty-ui`
+ Linaria
**Also fixes:** the unsubscribe footer was being appended *after*
`</html>`, outside the document, where Gmail strips it — legally
significant since unsubscribe is required.
---
## Detailed version
### Product requirements
**Problem.** The Compose campaign body was a single rich-text field.
Marketing email needs layout — banded sections, columns, call-to-action
buttons, images with links — and it needs that layout to survive
Outlook, which means table-based HTML rather than the divs a text editor
produces. It also needs per-recipient personalisation.
**Reference.** Resend's Broadcast editor, chosen because it solves the
same problem (TipTap authoring → react-email output) and is MIT
licensed.
#### What a user can now do
| Area | Capability |
|---|---|
| Canvas | Email renders as a centered page with its own background,
width, padding, corner radius and border |
| Blocks | Section, 2/3 columns, button, divider, raw HTML, image |
| Insertion | Floating left rail (pointer-first) or the `/` slash menu
(keyboard-first) |
| Sections | Own text colour, font size, line height, letter spacing and
alignment, cascading to everything inside |
| Images | Upload by drag-drop, paste or picker; link URL, alt text,
width, spacing, border |
| Raw HTML | Edited as source in the panel, previewed on the canvas with
scripts neutralised |
| Variables | `{{firstName}}`, `{{lastName}}`, `{{fullName}}`,
`{{email}}`, `{{personId}}` in text, button URLs, link hrefs and raw
HTML |
| Settings panel | Follows selection; shows effective values; opens
automatically when a block is clicked |
#### Deliberate product decisions
- **Variables display as literal placeholders**, not prose labels, so
the syntax is copyable into HTML blocks and button URLs by hand.
- **Sections inherit until they override.** The panel shows what
actually renders rather than blank fields, but writes nothing until you
edit — so changing the body text colour still flows into sections.
- **Headings keep their own scale** inside a styled section; only
colour, family and spacing cascade, otherwise every heading would
collapse to body size.
- **Clicking a block opens its settings**, but only on whole-node
selections, so typing inside a section does not reopen a panel you just
closed.
### Technical strategy
#### 1. Capability presets (the foundation)
Per-surface variation previously worked by **forking**: three separate
`useEditor` call sites with hardcoded extension arrays. Inside the
shared tree there was no variation at all — all five surfaces received a
byte-identical extension list, and presets controlled only sizing,
chrome and serialization format. Adding email blocks that way meant
either leaking section/column nodes into the record rich-text field and
workflow email body, or writing a fourth fork.
Now:
- a preset declares a **capability list** (`basicMarks`, `headings`,
`lists`, `links`, `images`, `campaignVariables`, `slashCommand`,
`blocks`, `mentions`)
- capabilities resolve to extensions through a factory registry
- the UI derives itself from the loaded extensions via
`hasEditorExtension` — no capability list is prop-drilled into a menu,
because the `Editor` already knows what it can do
The acceptance test was collapsing the AI chat fork into an `aiChat`
preset with no visible change to that composer. `campaignBody` is the
only preset opting into the shared `EMAIL_DOCUMENT_CAPABILITIES` today.
Workflow email keeps its current field UI, but can opt into the same
canvas, block settings and image uploader later without adding another
schema or renderer.
#### 2. Schema / renderer split
The hard constraint: **our HTML is produced server-side, per recipient,
at send time**, because variables substitute into nodes rather than into
a serialized string. That rules out Resend's
`renderToReactEmail`-on-the-extension pattern.
```
twenty-front TipTap extensions + node views + shared email settings UI
twenty-shared versioned email-document schema + structural traversal
twenty-emails react-email renderers (imported by twenty-server)
twenty-server surface-specific variable resolution, validation, send
```
Logic was **vendored, not depended on** — Resend's TipTap is 3.17
against our 3.4, and their UI is Radix. We copied the schema/serializer
approach and rebuilt every control on `twenty-ui` + Linaria.
#### 3. Section typography cascade
The subtle part, and the one that would have silently shipped broken.
Section typography *looks* like it should cascade via CSS. It does not:
```js
// react-email's Text
style: { fontSize: "14px", lineHeight: "24px", ...style, ...margins }
```
Every paragraph re-declares `fontSize` and `lineHeight`, overriding any
enclosing section. `inherit` is not a fix either — Outlook's Word engine
ignores it.
So the cascade is **resolved in the renderer**: the tree walk threads
the enclosing section's typography down and writes computed values
explicitly onto each text node. Nested sections refine what they
inherit.
Verified against real rendered output:
| | rendered |
|---|---|
| paragraph inside section | `font-size:22px; color:rgb(255,0,0);
letter-spacing:2px` |
| h1 inside section | `font-size:32px` (own scale) + section colour and
spacing |
| paragraph outside | `font-size:14px`, no colour — untouched |
#### 4. Storage
`bodyTemplate` stays serialized TipTap JSON in a `TEXT` column. Block
attributes are ProseMirror node attrs, so richer blocks add keys to JSON
already being serialized — no migration, and it flows into the existing
500 ms debounced draft save unchanged.
Since the feature has not shipped, the legacy HTML-string body path was
removed rather than maintained. That is a tightening, not just a
deletion: `bodyTemplate` is writable through the record API, and the old
fallback would interpolate an arbitrary string and email it as markup. A
body that is neither empty nor a valid TipTap document is now rejected
at the send gate.
#### 5. Image hosting
Inline assets use an `EmailImage` file folder with
`ignoreExpirationToken: true` and immutable cache headers, because
recipients' mail clients never authenticate and may open an email years
later. The shared uploader returns `{ fileId, url }`; the image node
keeps both the durable file identity and its delivery URL so
ownership/lifecycle or URL resolution can evolve later without a
document migration. The server verifies the uploaded bytes and only
accepts GIF, JPEG, PNG and WebP.
This is intentionally separate from workflow/email **attachments**.
Attachments remain private files that the server reads and embeds as
MIME parts at send time; inline images need a durable recipient-facing
URL. A future workflow canvas should reuse `useUploadEmailImage` for
inline content while keeping its existing attachment control unchanged.
Adding the folder requires three registrations — the folder config, the
route guard's `SUPPORTED_FILE_FOLDERS`, and `DIRECT_UPLOAD_FILE_FOLDERS`
in the upload service.
### Bugs fixed along the way
- **Unsubscribe footer was appended after `</html>`**, outside the
document, where Gmail strips it. Legally significant, since an
unsubscribe link is required. Now inserted before `</body>`.
- **Body text colour never reached the email.**
- **`onImageUpload` was declared but never passed** by any production
call site, so drag-drop and paste image upload were inert everywhere
outside Storybook.
- **Message lists were not user-facing**, so members could not be added
from the list page.
- **Image resize wrote an undeclared `width` attribute** that TipTap
silently dropped.
- **The text bubble menu appeared over selected atom blocks** with
nothing to format.
### Review notes / known limitations
**Security posture to check.** Anything in `EmailImage` is readable by
anyone holding the URL, forever. The server now enforces an image-only
MIME allowlist from sniffed bytes, but it cannot determine whether the
image itself is confidential. This remains a deliberate trade-off for
recipient-visible inline assets.
**Test gap.** The section typography cascade has no regression test:
react-email's `render()` hangs under Jest (tried 60s), and
`twenty-emails` has no test target at all. Verified by rendering through
the built package instead. Adding a test target there is worthwhile
follow-up.
**Sending needs configuration.** `EMAILING_DOMAIN_DRIVER` defaults to
`LOG`, which fakes a messageId, reports any domain as verified, and only
logs — a campaign reaches "sent" with nothing delivered. Real sending
needs `AWS_SES`.
**Unrelated platform bug found.** The pinned "Create new record" command
throws on viewless objects like `messageListMember`, because
`recordIndexId` derives from the current view.
**Not done.** Panel chrome from the reference: breadcrumb (`Page style /
Section`), collapsible groups, per-side spacing grid, and a
variable-insert button inside link fields. All presentation over the
same data.
**Deferred.** Drag-to-reorder blocks.
`@tiptap/extension-drag-handle-react@3.4.2` matches our pinned versions
exactly, so no upgrade is needed, but its behaviour around atom node
views (HTML block, image) is unverified and belongs in its own change.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23657?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
---------
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
|
||
|
|
5effee7754 |
Fix grouping a view that can no longer be changed or removed (#23619)
Fixes #23529 https://github.com/user-attachments/assets/2dbcf5ac-9b2e-4331-b7e8-703c8c5384b5 Grouping People by Company was a one-way door: once the view was grouped, the grouping could neither be changed nor removed. Two independent bugs on the same path caused it, and both had to be fixed. ## 1. The Group by entry was disabled, so the picker was unreachable `ObjectOptionsDropdownRecordGroupsContent` disabled the `Group by` entry whenever the object had a single groupable field. People exposes exactly one (Company), so the entry was always disabled there. That entry is the only way back to the field picker once a view is grouped: `ObjectOptionsDropdownCustomView` sends `Group` to the picker while the view is ungrouped, and to the group management screen once it is grouped. With the entry disabled, the picker, and with it the `None` option, became unreachable. A table view can always drop its grouping through `None`, so the entry now stays enabled there and is only disabled for layouts that require a grouping. ## 2. The view groups created by the server were never synced back The server deletes and recreates the view groups whenever `mainGroupByFieldMetadataId` changes (`handleFlatViewUpdateSideEffect`), and returns them in the `updateView` payload. `usePerformViewAPIUpdate` only wrote the view itself back to the metadata store, so the `viewGroups` entity kept the pre-change rows. The view create path already syncs them; the update path did not. On top of that, `useHandleRecordGroupField` overwrote the groups returned by the mutation with client-generated ones whose ids matched no persisted row, and `resetRecordGroupField` bailed out on `viewGroups.length === 0`. Since a relation grouping legitimately starts with no groups, clicking `None` was a no-op even when it could be reached. - sync the view groups returned by `updateView` into the metadata store - use those groups instead of regenerating them client-side - reset the grouping based on `mainGroupByFieldMetadataId`, and reload the record index states so the table regroups and ungroups without a refresh ## 3. Drive-by: No Value missing from the widget draft preview `buildDraftViewGroupsForFieldMetadataItem` mirrors `computeFlatViewGroupsOnViewCreate` so the page layout widget preview matches what gets persisted, but it returned early for relation fields and skipped the empty group. The server keeps creating it for nullable fields, relations included, so the group appeared out of nowhere once the widget was saved. It now skips only the option groups and keeps the empty group. ## Not changed Grouping by a relation shows no groups until you add them through `New group`. That is intended, since a relation can have an unbounded number of groups, and nothing here changes it. |
||
|
|
b05d2406ec |
fix(twenty-front): stop field widget layout dropdown from crashing th… (#23784)
…e record page RecordTableWidgetViewDraftInitEffect read the page layout edit mode and the page layout instance id from context, but the widget settings side panel renders outside the page layout tree. Opening the Layout picker on a relation field widget displayed as a table threw "PageLayoutEditModeContext Context not found" and took down the whole record page. Both values are now passed in by the caller. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23784?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. --> |
||
|
|
7c9ec6a770 |
Continue the workspace setup chat in the side panel when navigating away (#23744)
https://github.com/user-attachments/assets/579a8d41-901e-41c0-85a4-f23e6bc3da8d The /workspace-setup full-page chat shows the nav drawer, so users can navigate away mid-conversation and lose sight of the chat. Leaving the page by any means (drawer link, browser back) now opens the same conversation in the Ask AI side panel, with the full-page chat visually shrinking into the panel via the panel's existing width transition. The page marks a handoff atom while mounted; the side panel consumes it in a mount layout effect (pre-paint, so no flash frame), opens the Ask AI page, and enters at full width before shrinking. The Close button still exits without reopening the panel, the Collapse button keeps its behavior and gains the same animation, and prefers-reduced-motion skips it. Mobile is unchanged since the full-screen panel would cover the destination page. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23744?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. --> |
||
|
|
8a856a4bce |
Standardize dragged element feedback (#23772)
## Summary - make dragged table rows use a consistent background across sticky cells - apply one shared opacity treatment to table rows, Kanban cards, and other dnd-kit feedback ## Before/After <img width="1400" height="1980" alt="drag-feedback-before-after" src="https://github.com/user-attachments/assets/ee1fe81f-a762-4343-8db6-c51cf63dbac8" /> |
||
|
|
29aa6e85d6 |
fix(front): only show Discard Draft when workflow has a published version (#23756)
## Problem Draft workflows expose a **Discard Draft** action, but it fails when the draft is the *only* version of the workflow. The backend refuses the delete with `The initial version of a workflow can not be deleted` (guard in `validateWorkflowVersionForDeleteOne`), yet the action was still shown. The display condition and the delete-guard disagreed: - Display condition: `every(selectedRecords, "versions.length")` -> truthy when there is **at least one** version. - Delete guard: forbids deletion unless **another** non-deleted version exists. So a workflow whose only version is a draft showed the button, and clicking it hit a `FORBIDDEN` error. ## Fix Show the action only when the workflow has a published version to fall back to, which mirrors the backend guard: ``` every(selectedRecords, "lastPublishedVersionId") and everyEquals(selectedRecords, "currentVersion.status", "DRAFT") and noneDefined(selectedRecords, "deletedAt") ``` `lastPublishedVersionId` is a plain scalar already on the workflow. `every` (truthiness) is used rather than `everyDefined` because the field is an empty string for never-published workflows, and `everyDefined` would treat `""` as present. The command-menu evaluator reads records straight from the store, and the index/table view only fetches visible columns, so the enrichment provider now also backfills `lastPublishedVersionId` (already fetched by `useWorkflowsWithCurrentVersions`) to keep the condition reliable outside the record show page. ## Existing workspaces The standard-application full sync only runs at workspace creation, so editing the constant alone would fix new workspaces but leave existing ones showing the broken button. A `2.27.0` workspace upgrade command re-syncs the `discardDraftWorkflow` availability expression for existing workspaces, updating it only when it still equals the legacy `versions.length` value (so custom expressions are left untouched). Mirrors the existing 2-23 command-menu-item sync pattern. ## Notes - The `>`-style "more than one version" comparison is not expressible in the current `conditionalAvailabilityExpression` grammar (comparison operators only reach top-level scalars like `numberOfSelectedRecords`, not per-record paths). Gating on `lastPublishedVersionId` achieves the same intent without adding a parser helper. ## Test - Fresh workflow (single draft) -> Discard Draft hidden. - Publish, then edit to create a new draft -> Discard Draft shown and works. - Unit test on the sync-operations builder: updates the legacy expression, no-ops when already synced / custom / missing. |
||
|
|
be051c8724 |
Keep the token pair as a fallback after switching to cookie auth (#23755)
`CookieSessionBootEffect` cleared the token pair the moment it switched a client onto cookie auth. That leaves the client with a single credential, and a server that still has `AUTH_COOKIE_SESSIONS_ENABLED=false` ignores the session cookie entirely — `extractSessionTokenFromRequest` early-returns when the flag is off. A cookie-only client is therefore unauthenticated against such a server, `handleTokenRenewal` finds no refresh token, and `onUnauthenticatedError` signs the user out. That is not a hypothetical state. It is every request routed to a not-yet-rolled pod while the flag is being enabled, and every request after the flag is rolled back. Requests are load-balanced per request, so a migrated client hits an old pod almost immediately and gets signed out; signing back in can migrate it again and repeat for the length of the rollout. It also means rollback was not free, contrary to how it was described: flipping the flag back to `false` signed out everyone who had already migrated, because the pair they were supposed to fall back to had been deleted. ## Approach Keep the token pair as a dormant fallback, and stop *sending* it while cookie auth is active. Both halves are needed. Retaining it without suppressing the header would be worse than the bug: `validateTokenByRequest` checks the Bearer token first and only falls back to the session cookie when there is none, so a client that keeps sending Bearer would never exercise the cookie at all, and `CookieSessionCsrfMiddleware` bypasses on any Bearer-carrying request. Cookie sessions would silently become a no-op. So: - `switchToCookieAuth` no longer nulls the token pair - the auth link omits `authorization` while cookie auth is active, leaving the cookie as the credential in use - on an unauthenticated error while cookie auth is active, the client deactivates cookie auth once per operation and falls through to the existing renewal path, which replays with a fresh Bearer The fallback deliberately goes through renewal rather than replaying immediately: access tokens live 10 minutes, so the retained one has usually expired while the client was authenticating by cookie, and an immediate replay would just fail again. `isCookieAuthActive` is read and written through `localStorage` from the link because the links run per request and must agree with the atom synchronously — a React state update lands a render too late to affect the request being built. ## Follow-up This trades the immediate removal of the token pair from `localStorage` for rollout safety, so the XSS-exfiltration surface that cookie sessions close stays open a while longer. Once cookie sessions are stable across every environment, the retained pair should be dropped — reverting to a clear on `switchToCookieAuth` is a one-line change. ## Test Three cases added to `apollo.factory.test.ts`: no Bearer header while cookie auth is active; an unauthenticated response falls back and replays with the token pair rather than calling `onUnauthenticatedError`; and the fallback is attempted only once before going through renewal. The existing `CookieSessionBootEffect` assertion that the pair is cleared is inverted to assert it is retained. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23755?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. --> |
||
|
|
4b3614b413 |
fix(front): show relation value chip (Me / record names) in advanced filters (#23718)
## Problem
In advanced filters, a relation filter on a workspace-member field (e.g.
Assignee "is Me") displayed its raw JSON value
`{"isCurrentWorkspaceMemberSelected":true,...}` instead of a readable
chip.
Regular (non-advanced) filters handle this correctly:
`EditableRelationFilterChip` computes the label at runtime via
`useComputeRecordRelationFilterLabelValue`, rendering "Me", the selected
record names, or "N members".
The advanced filter value input instead relied on the deprecated stored
`displayValue` through `getRecordFilterDisplayValue`, which has no
`RELATION` branch and falls back to the raw value. When a saved view
filter carries no `displayValue` (it defaults to the raw stringified
value in `mapViewFiltersToFilters`), the raw JSON leaked into the UI.
## Fix
- Extract the relation value-label computation into a shared hook
`useComputeRecordRelationFilterDisplayValue` (parses the relation value,
resolves "Me" + record names).
- `useComputeRecordRelationFilterLabelValue` now consumes it (regular
chips unchanged).
- The advanced filter clickable select renders a dedicated
`AdvancedFilterRelationValueInputClickableSelect` for `RELATION`
filters, computing the label at runtime just like regular filters.
## Proof
Both filter surfaces render the relation value as **Me**, not the raw
`{"isCurrentWorkspaceMemberSelected":...}` JSON. The advanced-filter
shot loads a **saved view in a fresh session** — the exact bug
condition, where the view filter carries no stored `displayValue`.
**Regular filter chip**
<img width="1280" height="760" alt="image"
src="https://github.com/user-attachments/assets/8a867f51-a538-46f2-ba21-a16bb70d85a5"
/>
**Advanced filter**
<img width="1280" height="760" alt="image"
src="https://github.com/user-attachments/assets/b09b9d70-5692-4bac-8cec-3cb006961042"
/>
## Test
Verified manually on a local instance: created a saved view with an
advanced filter `Account Owner Is Me`, then reloaded it in a fresh
session — the condition where the view filter carries no stored
`displayValue`. The value renders as "Me" instead of the raw JSON.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23718?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. -->
|
||
|
|
5bafaa0994 |
Hide onboarding credits when billing is disabled (#23717)
Onboarding advertises free credits (the header pill and the green "Earn +N free credits" tags) even when `IS_BILLING_ENABLED` is false, promising a reward that can never be granted: `creditWorkspaceBalance` already no-ops when billing is off. The server now omits the `onboarding` credit-rewards block from the client config when billing is disabled, which hides every reward tag on its own since they all render behind a defined-config guard. The header pill gets an explicit gate. Also stops treating onboarding invites as reward-eligible when billing is off, so they are minted as plain invitation tokens and the 10-invite `ONBOARDING_INVITE_TEAM_MAX_INVITES` cap no longer applies to self-hosted instances. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23717?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. --> |
||
|
|
6f6da14cc8 |
Stop showing raw chunk load errors (#23571)
On iOS Safari, a failed chunk load showed a snackbar containing the raw browser string `Importing a module script failed.` `PromiseRejectionEffect` snackbars the raw `error.message` of any unhandled rejection, so a floating dynamic import leaked browser internals to the user. It now skips the snackbar for stale chunk errors, which are still captured by Sentry. This only changes what the user sees, not the underlying fetch failure. |
||
|
|
4e3747f3a9 |
fix(twenty-front): expand HTML preview viewport in DocumentViewer (#23668) (#23676)
## Description Fixes #23668. When previewing `.html` or `.htm` files in the file preview modal, `@cyntler/react-doc-viewer` mounts an `iframe` inside `#html-renderer`. Previously, `StyledDocumentViewerContainer` applied `height: 100%; width: 100%;` to `#react-doc-viewer`, `#proxy-renderer`, and `#msdoc-renderer`, but omitted `#html-renderer` and its inner `iframe`. As a result, the preview iframe defaulted to inline iframe bounds instead of expanding to fill the modal container. This PR adds `#html-renderer`, `#html-renderer iframe`, and `iframe` selectors to `StyledDocumentViewerContainer`, ensuring HTML previews expand fully within the modal viewport. ## Testing - Verified StyledDocumentViewerContainer CSS rules target `#html-renderer` and `iframe` elements. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23676?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. --> |
||
|
|
267ecb12db | Migrate web auth from localStorage token pairs to httpOnly cookie sessions (#23642) | ||
|
|
d359496b8b |
Keep chart palette colors stable when series order changes (#23638)
https://discord.com/channels/1130383047699738754/1522812783140540538 Chart palette colors were assigned by array position, so changing the sort order (or any reordering of the data) reshuffled every series color on line, bar and pie charts. Grouping by a select field was unaffected since options carry their own colors — this only hit groupings without intrinsic colors (relations, text fields, etc). Colors are now assigned by the alphabetical rank of the series key, so a key keeps its color no matter what order the data arrives in. Side effect: existing palette-colored charts get a one-time color reassignment. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23638?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. --> |
||
|
|
2389b4f807 |
Add captcha and throttling to the password reset link (#23372)
The public `emailPasswordResetLink` mutation was the only email-taking auth mutation without `CaptchaGuard`, so bots could drive reset email spam against arbitrary addresses. - Adds `CaptchaGuard` and a `captchaToken` argument (no-op when no captcha provider is configured). The frontend sends it like sign-in does, and `/settings/profile` joins the captcha-protected paths so the Change Password button keeps working - Throttles reset emails per address, 3 per 15 minutes, and surfaces a rate limit error once the bucket is empty - Acknowledges the request as soon as the throttle passes and generates the link off the request path, so the response time no longer depends on whether the address is registered - Returns a generic success instead of distinguishing found from not-found, with matching frontend copy - Rotates the reset token in a single transaction, so a failed write can no longer revoke a still valid link This does not close user enumeration on its own: `checkUserExists` exposes `exists` on the same unauthenticated surface, and sign-in returns distinguishable errors. Tracked in #23711. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23372?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. --> |
||
|
|
22d83c75e6 |
fix(twenty-front): Scrolling and Dragging conflict on mobile devices (#23677)
Fixes: #23675 https://github.com/user-attachments/assets/1f0d4f0f-0dd6-4731-8359-6da13a5e11b3 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23677?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
e81fdbcc7a |
feat(workflow): variable pickers for Search Records limit, offset and date filters (#23696)
## Summary <img width="491" height="390" alt="Capture d’écran 2026-08-03 à 11 30 40" src="https://github.com/user-attachments/assets/6db59b8a-3e8e-41b8-80b3-c736e56b7f7f" /> Adds workflow variable pickers to the **Search Records** action for fields that previously only accepted static values: - **Limit** and **Offset** number inputs now expose the `WorkflowVariablePicker`, so they can be bound to a variable from a previous step. The stored value can be a standalone variable string; the backend coerces the resolved value back to a number. - **Date filters** using the `Is before` (`IS_BEFORE`) and `Is after or equal` (`IS_AFTER`) operands now expose the variable picker in the advanced filter side panel (previously disabled for all date filters). The backend already resolves these inputs via `resolveInput`; the only backend change is a small numeric coercion of the resolved limit/offset. ## Changes - `WorkflowEditActionFindRecords.tsx` — pass `WorkflowVariablePicker` to the Limit/Offset inputs; make `onChange` and form state variable-aware (`number | string`). - `AdvancedFilterSidePanelValueFormInput.tsx` — enable the date `VariablePicker` only for `IS_BEFORE` / `IS_AFTER`. - `useGetRecordFilterDisplayValue.ts` — return the raw variable for a standalone `{{variable}}` value so date filters don't crash `Temporal.*.from`. - `find-records-action-settings-schema.ts` — allow a string (variable) for `limit` / `offset`. - `find-records.workflow-action.ts` — coerce resolved `limit` / `offset` to numbers before querying. ## Testing Built a workflow locally (Manual trigger → Code step returning `{ limit: 2, offset: 1, sinceDate }` → Search Records) with all three fields bound to those variables. The run completed successfully; the Search Records step returned exactly 2 records (limit applied) filtered by `createdAt >= sinceDate`, confirming the backend resolves each variable. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23696?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. --> |
||
|
|
536b91c1dd |
Update record page layout card (#23654)
## Summary - Redesign the data-model Layout card to match the record-page customization design. - Add dedicated light and dark cover assets and preserve the existing customization workflow. - Reuse a dedicated discovery-hero footer across the workspace and record layout cards. - Match the Figma cover, footer, icon, typography, and spacing metrics. ## Before/After <img width="1588" height="720" alt="before-after" src="https://github.com/user-attachments/assets/28be577c-9f1b-40f1-99e2-66eeafbac75b" /> |
||
|
|
f663cd3c68 |
Move open-record-in to object metadata and member preference (#23614)
Replaces the per-view "Open in" setting with a two-level model, following up on #23422 / #23424 and superseding the closed #23446 and #23457: - `objectMetadata.openRecordIn`: `SIDE_PANEL` | `RECORD_PAGE` | `USER_CHOICE` (default `USER_CHOICE`) - `workspaceMember.openRecordIn`: `SIDE_PANEL` | `RECORD_PAGE` (default `SIDE_PANEL`), editable in Settings > Experience The rule: records open where the member prefers, unless the object pins them, and never in a panel there is no room for (mobile always resolves to the record page). ## Why Having the setting on views, objects and members at once was heavy, and view-level resolution was fragile: a chip rendered outside a view (notes, front components, kanban cards pointing at another object) had no view to read from, which is the class of bug behind #23422. Resolution is now context-free: it needs only the object, the current member and the viewport, so chips behave identically everywhere by construction. ## Changes **Object level** - New `openRecordIn` enum column on `objectMetadata`, editable through `updateOneObject` and surfaced in Settings > Data model > Object > Layout ("Open records in": Member preference / Side Panel / Record Page) - Standard definitions pin `workflow`, `workflowVersion`, `dashboard` and `messageCampaign` to the record page (matching the previously hardcoded list) and `calendarEvent` to the side panel (it has no curated record page); everything else, including `workflowRun`, follows the member preference - Apps can set it in `defineObject()` via the object manifest **Member level** - New `openRecordIn` standard field on `workspaceMember`, persisted through the existing settings path (same as `colorScheme`) and exposed in Settings > Experience **View level (deprecated)** - `view.openRecordIn` is no longer read or written by the frontend; the "Open in" entry is gone from the view options dropdown - The column, DTO field and inputs are kept for one release for API compatibility: the output field carries a `deprecationReason`, the inputs keep accepting the value with a `Deprecated:` description (NestJS silently drops input fields that have a `deprecationReason`, which would have been a breaking change) **Upgrade (2.27)** - Fast instance command adds the `objectMetadata.openRecordIn` column defaulting to `USER_CHOICE` - Workspace command adds the `workspaceMember.openRecordIn` field - Workspace command seeds the object column from the standard definitions (any non-`USER_CHOICE` value), then lifts deliberate per-view record page choices onto objects the definitions don't pin **Debt removed** - `canOpenObjectInSidePanel` hardcoded object list and its test - `ObjectOptionsDropdownLayoutOpenInContent` and the `layoutOpenIn` dropdown wiring - `DefaultViewOpenRecordIn` - Context-store/view-based resolution in `useResolveOpenRecordIn` (now reads object metadata + member + viewport) - Front components no longer guess from the current view: an explicit side-panel call honours a pinned object and the viewport, nothing else ## Verification - Ran the three upgrade commands against a live database: column created, the pinned standard objects seeded per workspace (record page pins plus calendarEvent to side panel), member field backfilled to `SIDE_PANEL`; seed rerun is a no-op - Seed command verified on a simulated pre-upgrade workspace (index view set to record page on company): pins the standard objects plus company, idempotent on rerun - Both packages typecheck and lint clean; affected unit suites and the application sync, view creation and metadata cache integration specs pass --------- Co-authored-by: Thomas des Francs <tdesfrancs@gmail.com> |
||
|
|
706d72e53e |
fix(front): reload record board groups when view groups change (#23637)
## Problem Fixes #23462 On a Kanban (record board) view grouped by a SELECT field, adding a new option to that field creates the column, but dragging a record into the new column silently fails (no move, no error) until a hard page reload. ## Root cause `RecordIndexLoadBaseOnContextStoreEffect` builds its load key from the view id and the calendar-week flag only: ``` `${contextStoreCurrentViewId}-${isCalendarWeekViewEnabled}` ``` The effect early-returns when `loadedViewKey === currentViewLoadKey`. When a new `ViewGroup` is created from the added SELECT option, the view id does not change, so the key is unchanged and `loadRecordIndexStates` never re-runs. The record group state (`recordGroupIdsComponentState` / `recordGroupDefinitionFamilyState`) stays stale, so the drop handler cannot resolve the new group's field value and the move no-ops. A hard reload fixes it because the view then loads with the new group present from the start. ## Fix Include a signature of `view.viewGroups` (ordered `id:position:isVisible`) in the load key so the effect re-runs `loadRecordIndexStates` whenever the view's groups change, not only when the view id changes. The calendar-week flag is kept in the key. ## Testing Verified end to end on a local instance against an Opportunities "By Stage" Kanban, with the record's stage change confirmed in the database: - **Before the fix:** add a new Stage option in-session, then drag a record into the new column. Dragging into an existing column persists the move; dragging into the newly created column does nothing (record's stage unchanged in DB). - **After the fix:** same flow, dragging a record into the newly created column moves it and persists the new stage in DB, with no reload. `nx typecheck twenty-front` and `oxlint` pass. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23637?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. --> |
||
|
|
d02332834b |
Fix record title cell reopening on every page refresh (#23551)
## What `location.state.isNewRecord` is set when navigating to a freshly created record so the title cell opens for naming. But router state lives in **browser history state, which survives page refreshes** — so every refresh of that record's page re-opens the title cell with an empty draft and a blinking cursor. Strip the flag after its one intended consumption in `PageChangeEffect` (react-router keeps user state under `history.state.usr`). ## Repro (on current main, any view set to open records in record page — or on mobile) 1. Create a record from a table; you land on its record page with the title focused (intended). 2. Name it, click away, then refresh the page. 3. The title cell re-opens, empty, focused — on every refresh, forever. 🤖 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/23551?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: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: bosiraphael <raphael.bosi@gmail.com> |
||
|
|
3ed11054a0 |
Keep leading + when filtering phones by calling code (#23546)
Fixes #23528 Filtering a PHONES field with `CONTAINS` / `DOES_NOT_CONTAIN` stripped every non-digit character from the filter value, so `+33` became `33` and the generated `ilike`/`like` predicates could not distinguish an international calling code from any number containing those digits. `turnRecordFilterIntoGqlOperationFilter` now preserves a leading `+` while still removing other formatting characters (spaces, dashes, parentheses). `+33 6 12` becomes `+33612`; values without a `+` are unchanged. Added a regression test in `computeViewRecordGqlOperationFilter.test.ts` for a `+`-prefixed value. Lint and typecheck pass on `twenty-shared` and `twenty-front`; the filter test suites pass in both packages. --------- Co-authored-by: Thomas Trompette <tom@twenty.com> |
||
|
|
8f9f2f390e |
fix(ai-chat): show streaming activity during and between steps (#23581)
https://github.com/user-attachments/assets/e72e313c-66f8-40af-bf48-9225422ffa78 ## Problem During a streaming turn with tool calls, the chat goes completely static in two places: - **Between two steps**: once a tool's output arrives, its row flips to past tense and nothing animates until the model's next chunk arrives (a full LLM round trip, often several seconds). This window is defined by the absence of parts, so no part-driven component can fill it — and the pre-turn "…" indicator can't either, since it's cleared on the turn's first chunk and never comes back. - **During tool execution**: the active tool row in `ThinkingStepsDisplay` is a static icon + label; the only animated element there is the orbit loader on an actively-streaming reasoning part. Users can't tell whether the AI chat is still thinking or blocked. ## Fix - **Pending thinking row between steps.** The renderer flags the trailing thinking-steps group of a streaming, error-free message (`showPendingThinkingRow`), and `ThinkingStepsDisplay` appends the thinking row (orbit loader + "Thinking") inside its rows container when none of its own steps is active (`isThinking`, which it already computes). The row occupies the exact slot where the next real step row materializes, so the handoff happens in place with no layout shift. - **One shared row component.** `AiChatThinkingRow` renders the orbit loader + "Thinking" and is used both for an actively-streaming reasoning step and for the pending row. - **Shimmer on executing tools.** Active tool rows wrap their label ("Searching the web for…") in the existing `ShimmeringText` while awaiting output, with the text as a direct child of the background-clip element so the effect applies reliably. - **Activity derived from the tool lifecycle state.** `isThinkingStepPartActive` now checks `input-streaming` / `input-available` instead of output presence, so a tool completing with a legitimate `null` output is no longer classified as still running. Why the trailing-group check is sufficient: anything in progress outside the group — streaming answer text, a running code execution card, a pending question — is itself a later render item, so the group isn't last and never gets flagged. No message-wide part scanning needed. ## Notes - The row renders only while `agentChatIsStreaming`, which the existing keepalive watchdog force-clears (with a visible connection-lost error) after ~5s of subscription silence — it cannot spin forever on a dead stream. - It never shows while waiting on the user: `ask_questions` renders as its own item after the group, and the server ends the stream on that tool anyway (`stopWhen`). - Consciously not covered, for simplicity: a pause right after a mid-turn text part or right after the routing row. ## Tests - Renderer: trailing group flagged as pending while streaming; not flagged when answer text follows or when not streaming - `ThinkingStepsDisplay`: pending row appended after completed steps, suppressed while a tool step runs, loading label shown on a running tool - `isThinkingStepPartActive`: lifecycle-state cases, including a completed tool with `null` output Lint, format, and `typecheck twenty-front` are clean. |
||
|
|
a9084604b4 |
Use the fast model for the onboarding setup chat (#23586)
The workspace setup chat ran on the smart model. The hidden kickoff turn enqueued its job without a `modelId`, and the frontend sends none unless the user picks one, so every turn fell through to `modelId ?? workspace.smartModel` in `chat-execution.service.ts`. Two halves, since the kickoff is server-initiated and the frontend never sends it: - `startHiddenKickoffStream` takes a `modelId` and the setup chat passes `workspace.fastModel`. - `useAgentChatModelId` requests `workspace.fastModel` on the setup page, so user turns follow. Everywhere else it still sends nothing and the server fallback is unchanged. `workspace.fastModel` defaults to the `default-fast-model` sentinel, so the model still resolves through the registry and stays admin-overridable. An explicit pick from the model picker still wins. |
||
|
|
53a18d7528 |
feat(workflow): route all version content readers through the flag-aware sources (#23583)
## What Follow-up to #23499. Migrates every remaining reader of `record.trigger`/`record.steps` so all version content flows through the flag-aware sources, then removes `trigger`/`steps` from the record field sets. The record CRUD path no longer carries version content anywhere in the app. Reading is still entirely behind `IS_WORKFLOW_VERSION_IN_CORE_ENABLED`: this PR changes who asks, never where the answer comes from. Flag off remains record reads (via the content hook's record branch), flag on the core query. ## Per reader | reader | now reads | | --- | --- | | `WorkflowDiagramCanvasEditable` (connect, drag-stop) | flow atom | | `useDeleteStep` | flow atom | | `WorkflowEditActionIfElseBody` (branch cleanup) | flow atom | | `SidePanelWorkflowCreateStepContent` (parent-step lookup) | flow atom | | `SidePanelWorkflowStepInfo` | flow atom (explicit instance id), falling back to `useWorkflowVersionContent` when the visualizer is not mounted | | `TestWorkflowSingleRecordCommand` | `useWorkflowVersionContent`; `ready` gates on content being loaded | | headless enrichment hook (imperative) | core content query when the flag is on, record otherwise | | `WorkflowRunVisualizerEffect` (step output schemas) | the run snapshot (`state.flow`), which is what a run should show anyway | ## Field-set slimming `useWorkflowVersion` and `useWorkflowWithCurrentVersion` stop fetching `trigger`/`steps` (identity fields only). Three call sites lost their only reason to call `useWorkflowWithCurrentVersion` and were dropped entirely. Verified by grep that no `currentVersion.trigger/steps` reads remain; the only remaining `.trigger`/`.steps` accesses are argument-taking utils whose callers now pass flow/content-sourced objects. ## Verification - `nx typecheck twenty-front` green - Front tests: 1058 green (the enrichment test gained mocks for the apollo client and flag its hook now uses) - Full-tree `oxfmt` + `oxlint --type-aware` green (3 remaining warnings are pre-existing in unrelated record-field files) - Live click-through pending, flag off and on <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23583?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. --> |
||
|
|
0d63906a58 |
Fix calendar field picker state handling (#23595)
## Context Changing a calendar date field already updated the record-index calendar state immediately, so the calendar moved to the new field before the `updateView` mutation completed. The options dropdown still derived its selected checkmark and field labels from `currentView`, which remains unchanged while persistence is pending. On slower environments this left the previous field name visible even though the calendar was already using the new field. Locally the same mismatch existed, but was only visible briefly because the mutation completed faster. ## What changed - Read the active start and end date field IDs from the record-index calendar component state in the calendar options dropdown. - Use that state for the main options label, the two-field submenu, and both field-picker selections. - Use the optimistic end-field state when filtering compatible start fields and deciding whether an incompatible end field must be cleared. - Keep the existing view mutation and calendar-state writes unchanged. ## Why The calendar and its configuration UI now share the same source of truth while persistence is pending. A field selection updates the calendar, checkmark, and contextual labels together instead of temporarily mixing optimistic calendar state with stale persisted view metadata. ## Safety and expected impact This is frontend state synchronization only. It does not change the metadata schema, API payloads, or persistence flow. Existing date and datetime compatibility rules remain in place. Users should see the selected field name update immediately, including when the metadata mutation is slow. ## Limitations This does not change mutation error handling or add rollback behavior. The calendar atoms were already updated optimistically before this change, this PR only makes the configuration UI reflect those same values. ## Validation - Reproduced the stale selection on qacoco and locally. - Verified locally that the checkmark moves immediately after selecting another date field, before the mutation closes the dropdown. - `npx nx typecheck twenty-front` - Focused type-aware oxlint on the four changed files, 0 warnings and 0 errors. - `npx oxfmt --check` on the four changed files. - `git diff --check` |
||
|
|
a747e62970 |
fix(workflow): use as draft with an existing draft and cross-object record page filters (#23524)
Fixes two workflow issues.
### "Use as draft" fails when a draft already exists
`UseAsDraftWorkflowVersionSingleRecordCommand` rendered its own
`OverrideWorkflowDraftConfirmationModal`. Headless commands are
unmounted by `HeadlessEngineCommandWrapperEffect` as soon as `execute`
resolves, so the modal was removed from the tree right after `openModal`
was called and the user saw nothing happen. This regressed when the
command was converted from a rendered `<Command onClick>` to a
self-unmounting effect.
The command now uses `HeadlessConfirmationModalEngineCommandEffect`, the
existing mechanism for headless commands that need a confirmation: it
opens the app-wide `CommandMenuConfirmationModalManager` and keeps the
command mounted until the modal emits its result.
`OverrideWorkflowDraftConfirmationModal`, its modal id and its config
state are deleted.
To keep the "Go to Draft" shortcut, the shared confirmation modal config
gains an optional `linkButton` rendered as a secondary link button that
emits a `cancel` result on click.
The command also resolved the workflow id through `useWorkflowVersion`,
which is `undefined` while the query is in flight, so it threw and
surfaced an error snackbar through the command error boundary. It now
reads the workflow id off the selected record like the sibling workflow
version commands, and waits for the workflow to load before deciding
whether a confirmation is needed.
### `workflow object doesn't have any "workflowId" field` when opening a
workflow from a version
`useRecordShowPagePagination` builds prev/next queries from the parent
view stored in `contextStoreRecordShowParentViewComponentState`.
Navigating from a workflow version record page to its workflow through
the relation chip keeps the workflow version parent view, whose relation
filter compiles to `workflowId: { in: [...] }` and is then sent against
the `workflow` object, which the API rejects.
`useQueryVariablesFromParentView` now ignores the parent view when
`parentViewObjectNameSingular` does not match the current object, which
covers every navigation path between record pages of different objects.
### Test
Added `useQueryVariablesFromParentView.test.tsx` covering both the
matching and mismatching parent view object.
Manually checked on a local instance: override with an existing draft,
"Go to Draft", cancel then re-trigger, and the no-existing-draft path,
plus navigating from a filtered workflow versions view to a workflow.
---------
Co-authored-by: Tom <tom@twenty.com>
|
||
|
|
5848c9bd30 |
Display object, field and view links as chips in the AI chat (#23573)
<img width="3840" height="1876" alt="CleanShot 2026-07-30 at 15 37 46@2x" src="https://github.com/user-attachments/assets/9fe178b9-c2fa-4b05-9c9d-0cdc80270b67" /> https://github.com/user-attachments/assets/34c4e486-c462-4300-ae98-da99f614f069 The AI chat already renders record chips from a `[[record:...]]` marker the model writes in its prose, but naming an object, field or view produced plain text. This adds three sibling markers so those render as chips too, as in the [Figma design](https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=104416-116261). - `[[object:<nameSingular>:<label>[[/object]]` links to the record index page. It is name-keyed rather than id-keyed so an object the assistant only *proposes* to create still renders as a chip, just without a link. - `[[field:<id>:<label>[[/field]]` links to the field's settings page, gated on the `DATA_MODEL` permission. - `[[view:<id>:<label>[[/view]]` links to the object index page for that view. Field and view ids must come from a tool, so an unresolvable one falls back to plain text rather than a chip that goes nowhere. The record-only parser becomes one scan over all four kinds. Alternative order is load-bearing: `[[view:<uuid>:` is shaped exactly like the legacy prefix-less record marker, so metadata kinds are tried first and only records keep the legacy `]]` terminator. Server side is prompt-only. The metadata and view tools return bare objects rather than `ToolOutput`, so there is nowhere to hang a structured reference array without wrapping every factory, and the names and ids the markers need are already in those results verbatim. Also fixes a pre-existing issue in `LazyMarkdownRenderer`: its `components` map was rebuilt on every render, and react-markdown uses each entry as the JSX element type, so every node remounted on every streamed chunk. Harmless before, expensive once the model is told to chip every metadata name it writes. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23573?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. --> |
||
|
|
6447b7f935 |
feat(workflow): make the flow atom authoritative for version content, read core behind a flag (#23499)
## What
The frontend half of the workflow-version read switch, plus the small
server query it consumes. Two ideas:
1. **One hook owns where content comes from.**
`useWorkflowVersionContent(workflowVersionId)` returns `{ trigger, steps
}` from the workspace record when `IS_WORKFLOW_VERSION_IN_CORE_ENABLED`
is off (default), and from the new `workflowVersionContent` core query
when on. Switching the source later (core-only, after the column drop)
is a change inside this one hook.
2. **`flowComponentState` becomes authoritative for the builder.** The
canvas, diagram and step output schemas derive from the jotai atom; the
atom is seeded once per version through the hook above; mutations keep
it up to date.
## Why the seeding change is required
Today `WorkflowDiagramEffect` re-seeds the atom from the Apollo record
on **every** `currentVersion` identity change. That has two
consequences:
- Three of the five step/edge hooks (`delete step`, `create edge`,
`delete edge`) never write the atom themselves; they only write the
record and the re-seed papers over it.
- The model breaks the moment content comes from a source mutations do
not write (i.e. core): the stale fetch would be re-applied over every
optimistic edit, and your just-added step would vanish from the canvas.
So the atom is now seeded **once per version**, and
`useUpdateWorkflowVersionCache` applies the mutation's
`stepsDiff`/`triggerDiff` to the atom directly. All five step/edge hooks
get that through their existing call, which closes the three-hook gap in
one move. The step-update, trigger and tidy-up hooks write the atom too.
The record-cache writes are all kept while `trigger`/`steps` still live
on the record (dropped later with the columns).
## The dead wire, now the refresh path
`shouldWorkflowRefetchRequestFamilyState` was set by
`WorkflowSSESubscribeEffect` (reconnect, other-tab create) and
**consumed by nothing**. It is now the external-refresh path: when set,
the builder refetches content and reseeds. Known trade-off: while
connected, another tab's edits no longer live-patch the canvas through
record cache updates (they arrive on reconnect, version switch or
reload). Given concurrent editing of one draft has no conflict handling
anyway, that seemed acceptable; easy to extend the SSE effect to set the
flag on update events if we want live propagation back.
## Untouched by design
- **Run visualizer**: feeds the same atom from the immutable
`workflowRun.state.flow` snapshot; that duality (version content or run
snapshot) is exactly why the atom stays separate from the record store.
- **Version visualizer** (read-only): reseeds on content change, safe
because nothing writes its instance optimistically.
- Peripheral readers of `currentVersion.trigger/steps` (test-workflow
command, headless command enrichment, if-else body, etc.) still read the
record. Correct while dual-writing continues; they move to the content
hook before workspace content writes stop (tracked in the migration
plan).
## Verification
- `nx typecheck` green on both packages; `oxfmt` + `oxlint --type-aware`
green on all 16 changed files
- Front unit tests: 134 suites / 993 tests green (the two hook tests
gained the visualizer instance context their hooks now require)
- New server integration test for `workflowVersionContent`
- **Live click-through pending**: step create/delete/duplicate, edge
create/delete, trigger edit, tidy-up, draft create/discard, activation,
version viewer, run viewer, with the flag off and on. The failure mode
this PR guards against (an edit vanishing from the canvas) does not show
up in typecheck or unit tests.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23499?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. -->
|
||
|
|
a3beea893d |
Revert the external link confirmation popup for front components (#23567)
Reverts #23270 and #23404. Links in front components navigate natively again, with no confirmation popup and no per-app trusted-origins state in localStorage. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23567?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. --> |
||
|
|
38ad13655c |
Auto-start the workspace setup chat with a data model proposal (#23437)
https://github.com/user-attachments/assets/d447372b-c1b4-4c95-bac9-a8f8efa7d4a1 When the workspace creator lands on `/workspace-setup` after onboarding, the AI chat now starts on its own: an invisible first message, built server-side from the company enrichment collected in #23199, asks the assistant to propose a data model tailored to the business. The proposal streams in; the user never sees the prompt. - New `startWorkspaceSetupChat` mutation: creator only, gated on `IS_ONBOARDING_AI_CHAT_ENABLED`, available models and credits. Idempotent per user and workspace via a `keyValuePair` pointing at the thread, so a reload or a second tab joins the same conversation instead of starting a new one. - The thread holds exactly one hidden `USER` message combining the company context and the setup instructions, which keeps the one-hidden-message-per-thread index from #23199 satisfied. It goes through a dedicated streaming path that never queues, so the prompt cannot resurface as a visible message. - The assistant only proposes. It creates nothing until the user approves, then builds the model with the `metadata-building` skill. Objects and fields get English names with labels in the user's language, and the conversation continues in that language. - With no enrichment (consumer email domain, or the integration disabled) the kickoff still runs, and the assistant asks one short question about the business before proposing. - `findLatestSentUserMessage` no longer filters out hidden messages, so a failed kickoff turn stays retryable, and the no-message chat error surface now offers retry for stream errors. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23437?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. --> |
||
|
|
079e9b8e56 |
feat(dashboard): extend number format option to bar, line and pie charts (#23505)
https://discord.com/channels/1130383047699738754/1509604545381142649 Extends the Format option (Short/Full) added for the Number widget in #21521 to bar, line and pie charts. Format controls the numbers printed on the chart face: data labels and the pie center metric. Axis ticks stay abbreviated and tooltips always show the full value. Defaults to Short, so existing charts render unchanged. Server: nullable `numberFormat` on the bar/line/pie configuration DTOs, exposed in the dashboard AI tool schema. No migration, configuration is jsonb. Deferred: - The Format row has no visible effect while data labels are off, since tooltips are always full. - Number widget format defaults differ by field type (CURRENCY defaults to Short, NUMBER to Full). Pre-existing, untouched here. https://github.com/user-attachments/assets/0778f08a-6681-4e7a-8716-fb3026d1e01f <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23505?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. --> |
||
|
|
0b335d15b3 |
Refresh billing state after ending trial period (#23534)
Fixes #23530 After adding a credit card in the billing prompt, the credits section and subscription details stayed stale until a full page refresh. The `endSubscriptionTrialPeriod` mutation only returned `status` and `hasPaymentMethod`, and the frontend hook only patched the subscription status into the workspace state. The credits query was never refetched, so granted credits kept showing trial values, and `currentPeriodEnd` (renewal date) and `billingCustomer.hasPaymentMethod` stayed outdated. The backend already syncs everything to the database synchronously before the mutation returns, so fresh data was available, just never fetched. Changes: - `BillingEndTrialPeriodDTO` now includes nullable `currentBillingSubscription` and `billingSubscriptions`, returned by the resolver on success, mirroring the other billing update mutations (`switchSubscriptionInterval`, etc.) - `useEndSubscriptionTrialPeriod` applies the full billing update via `useApplyCurrentWorkspaceBillingUpdate` (falling back to the previous status-only patch), marks the billing customer as having a payment method, and refetches `GetResourceCreditUsage` so the credits section updates for any active observer This covers all entry points that end the trial: the billing page card modal, the trial banner, the AI chat banner, and the return from the Stripe portal. --- _Generated by [Claude Code](https://claude.ai/code/session_01W1J7cW2MhaqXGfdjvHeFoX)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23534?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. --> |
||
|
|
030ee2c7cc |
Move settings tabs below page headers (#23519)
## Summary - Position the Admin Panel tabs directly below the settings header. - Position the Communication tabs directly below the settings header. - Reuse the shared settings tab bar while preserving permissions, disabled states, and hash navigation. - Keep the responsive tab overflow menu available on narrow settings cards. ## After <img width="3456" height="2008" alt="Admin Panel tabs positioned below the settings header" src="https://github.com/user-attachments/assets/c8ff965c-d4b2-4700-85e1-0763f9f0852d" /> |
||
|
|
ada7eb1d88 |
Restore the Figma halftone shapes and calm the welcome animation (#23495)
https://github.com/user-attachments/assets/0c06d190-977e-4ad6-8a02-251f341ee310 The welcome overlay settled into circles instead of the halftone from Figma: the densify step that took the dot set from 681 to 2897 emitted points, so every dash had zero length. All 681 dashes in the Figma export (`public/images/onboarding/welcome-halftone.svg`) share a constant length / stroke width ratio of 1.452, so the shape is restored by deriving the length from the stroke width, with no data regeneration. Dashes now stretch into shape while they are still flying in, rather than popping once they have landed. The rest of the pass makes the animation quieter. The shine sweep is gone, along with the highlight colour that only fed it. Particles approach from much closer on a gentler ease, the idle drift is roughly halved, and the exit is a soft outward drift instead of a burst that threw everything off screen. The white pill behind the title and the person chip's surface are removed too, so the title reads directly against the halftone. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23495?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. --> |
||
|
|
684259fd5e |
Make onboarding cards contrast with the page background (#23516)
Onboarding card surfaces used `background.secondary`, the same token as the onboarding page background, so they blended in (visible on the workspace selection step). Switched them to `background.primary`, matching the other onboarding cards (plan card, install apps, trust badges). <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23516?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. --> |
||
|
|
58b8e8afee |
Fix clipped New chat button label in localized navigation drawer (#23511)
Fixes #23503 <img width="161" height="76" alt="Capture d’écran 2026-07-29 à 17 20 25" src="https://github.com/user-attachments/assets/a6ea0a12-b91d-419f-8023-8e65d5dce65b" /> The expanded "New chat" button had a hardcoded `width: 103px`, sized for the English label. Localized labels ("Neuer Chat", "Nouveau chat") were clipped, since `OverflowingTextWithTooltip` can only truncate inside a parent it cannot resize. The expanded wrapper is now `width: max-content` with `max-width: 100%`, so it grows with the label and only truncates (with tooltip) when the sidebar has no space left. `min-width` keeps the pill from collapsing below icon size. Collapsed state is unchanged. ### Verified locally (German locale) | | wrapper width | label | |---|---|---| | before | 103px (fixed) | `Neuer C...` clipped | | after | 109px (max-content) | `Neuer Chat` in full | - English: 103px -> 98px, visually identical. - Collapsed drawer: still exactly 24x24, unchanged. - Very long label (Vietnamese-length): wrapper stops at the sidebar edge, no overflow, label truncates with tooltip. |
||
|
|
adfb96c7ce |
Keep email participant avatar colors consistent (#23444)
## Summary - Fixes issues where the same record didn't share the same avatar in multiple places of the inbox. - Add a shared avatar color seed resolver for email participants. - Apply consistent placeholder colors across participant chips and email thread previews. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23444?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. --> |
||
|
|
742f76e318 |
[BREAKING-CHANGE] Add NOT_RECORDED call recording status (#23478)
Adds NOT_RECORDED to the CallRecording status select, for meetings where nothing was captured (bot never admitted, meeting not started, nobody joined). First part of twentyhq/core-team-issues#2706, split out so existing workspaces are upgraded before the call-recorder app starts writing the new status. - NOT_RECORDED enum value + standard select option - 2-26 workspace upgrade command adding the option to existing workspaces (idempotent, same option id as the standard definition) App-side classification from Recall sub codes comes in a follow-up PR. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23478?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. --> |
||
|
|
e2e2142998 |
Add calendar date field submenu with grouped field options (#23445)
## Summary - Add a submenu for selecting calendar date fields as it was not clear enough that you could pick 2 date fields. - Group date and datetime fields with non-selectable headers ### Before <img width="240" height="252" alt="Screenshot 2026-07-29 at 11 32 54" src="https://github.com/user-attachments/assets/169bd965-676f-4ef9-8ba6-9ecac80547c6" /> ### After <img width="260" height="246" alt="Screenshot 2026-07-29 at 11 31 58" src="https://github.com/user-attachments/assets/a4471bb0-5c1f-4f5e-bddb-165a5c9da4c7" /> |
||
|
|
a0281f635b |
Restore soft-deleted junction record when re-adding a junction relation (#23371)
Fixes #23305. Removing a junction relation soft-deletes the intermediate junction record. Re-adding the same relation created a brand new record, which the composite unique index on the junction object (e.g. `personId` + `companyId`) rejected with "This record already exists". `useUpdateJunctionRelationFromCell` now creates the junction record through `useCreateManyRecords` with `upsert: true`. The server matches on the unique index with `withDeleted()` and clears `deletedAt` on the matched row instead of inserting. ## This revives, it does not create Worth being explicit, because it is a deliberate trade and not obvious from the diff. When a soft-deleted row exists for the pair, the user gets that row back. Same id, same `createdAt`, same `createdBy`, and anything attached to it (notes, files, and any fields the app added to the junction object). Verified on a dev instance: after re-adding a link through the picker, the row still reported a creation date from days earlier. That is fine for a pure link. It is a lie for a junction that carries data, for instance a `PersonCompanyRelationship` with a role and dates. The alternative designs are hard-deleting on detach (needs `canDestroyObjectRecords` on the junction object, which most roles do not grant) and partial unique indexes (needs `indexWhereClause` exposed to app-declared indexes, which the SDK does not support today). Both are larger changes. This one unblocks affected apps without requiring anything from them. Note that upsert conflict detection ignores `indexWhereClause`, so shipping partial indexes later would not change this behaviour on its own. ## Why the id handling changed The hook no longer sends a client-generated id. Under upsert the server decides which row you get, so the id in the input would be discarded. The optimistic store entry still uses a local id so the chip appears immediately, then adopts the persisted id once the mutation resolves. Without that, a revive left an id in the store that exists nowhere on the server, and the next removal failed with "This record does not exist or has been deleted". Supersedes #23365, which took the same approach but added `$upsert` to the shared `createOne` mutation. That capability already exists per call on `createMany`, and widening the shared document broke the `useCreateOneRecordMutation` and `useCreateOneRecord` tests. ## Testing Verified end to end on a dev instance with a junction object carrying a non-partial composite unique index, since the dev seed does not create one and the bug cannot reproduce without it: - before: re-adding after a removal fails with "This record already exists" - after: the original row is restored, `deletedAt` cleared, one row throughout, and removing again in the same session works, with no GraphQL errors Added an integration test for the path this depends on: a soft-deleted record matched on its unique fields alone, with no id in the input. The existing coverage only exercised upsert by explicit id. ## Known gaps, deliberately not addressed here **Toggling a link off while its creation is still in flight.** The store id is provisional until the mutation returns, so a removal issued inside that window deletes an id the server never received. Reproduced by stalling the create and clicking remove during it: `CombinedGraphQLErrors: Record not found`, and the link stays active despite the user removing it. The window is one mutation round trip. Left for a follow-up; the likely fix is a per-record operation queue so adds and removes on a field run in click order. **Junction objects with no unique index.** Remove then re-add still creates a duplicate row there, on this branch as on main, because conflict detection is driven by unique indexes. --------- Co-authored-by: Shinu Cherian <129690295+Shinu-Cherian@users.noreply.github.com> |
||
|
|
b602294f1d |
Hide the command menu button while the mobile side panel is open (#23471)
On mobile the side panel covers the page, but the page header stays mounted underneath. Its command menu button (`⌘K`, the `⋮` icon) sits at the same coordinates as the panel's own close button, so the two icons render on top of each other. Measured on a 390x844 viewport with the AI chat open: - `Command Menu` button at `x=346, y=8, 32x32` - `Close side panel` button at `x=358, y=14, 24x24` `SidePanelToggleButton` already hid itself for the command menu and search pages, but the AI chat pages (`AskAI`, `ViewPreviousAiChats`) are not in `COMMAND_MENU_SIDE_PANEL_PAGES`, so the button stayed and overlapped. ## Change Hide the button on mobile whenever the side panel is open, rather than enumerating pages — the header is not reachable behind a full-screen panel either way. Layout customization mode is the exception and keeps it: `alignWithSidePanelTopBar` deliberately repositions the button into the side panel top bar there, so that path is preserved. Desktop is unaffected. ## Testing Three cases added to `SidePanelToggleButton.test.tsx` (hidden on mobile with the panel open, kept on mobile in layout customization mode, kept on desktop with the AI chat open); the `useIsMobile` mock is now switchable per test. All 10 tests pass. Verified in the browser at 390x844: with the AI chat open only `Close side panel` remains in the top bar, and the button reappears once the panel is closed. --- _Generated by [Claude Code](https://claude.ai/code/session_018gcsCQbuTMsyFWv874p25Q)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23471?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. --> |
||
|
|
933ae9c20b |
fix(page-layout): render standalone rich text widget on record pages (#23435)
Fixes #21093 ## Problem A `STANDALONE_RICH_TEXT` widget can be created on a record page layout through the metadata API, and `getPageLayoutWidgets` returns it, but the record page renders nothing for it. `StandaloneRichTextWidget` only resolved a target id when `layoutType === PageLayoutType.DASHBOARD`, then bailed out with `return null` whenever that id was undefined. On a record page it never rendered. ## Why the guard was there It was correct when it was written. In #16437 the widget used the full `BLOCK_SCHEMA` and uploaded files: ```ts return await uploadAttachmentFile(file, { id: dashboardId, targetObjectNameSingular: CoreObjectNameSingular.Dashboard, }); ``` Without a dashboard id there was nowhere to attach an upload, and dashboards were the only layout type in play, so refusing to render was a coherent stance. #17934 then disabled file upload because the urls were not signed. It swapped in `DASHBOARD_BLOCK_SCHEMA`, dropped `useUploadAttachmentFile` and `prepareBodyWithSignedUrls`, and added `filterSupportedBlocks` to strip file blocks out of previously saved bodies. It left behind the attachments query, the `attachments` prop and the `useAttachmentSync` call. After that, `dashboardId` had one consumer left: a filter that could no longer match anything. The `return null` underneath it was guarding nothing. Record page layouts then made the widget reachable outside dashboards, and the stale guard blanked it. ## Fix The body lives on the widget configuration, not on the target record, so the widget needs no record id to display. The leftover attachment fetch has nothing to act on: - `DASHBOARD_BLOCK_SCHEMA` declares only paragraph, heading, lists, checklist, codeBlock, table and quote. No image, file, video or audio block. - The three sync utils all key off `ATTACHMENT_BLOCK_TYPES = ['image', 'file', 'video', 'audio']`, so they return empty for any body this editor can produce. - No `uploadFile` option, no `onPaste` handler, and `filterSupportedBlocks` strips unsupported blocks on load, so such a block cannot get in. So rather than generalise the attachment filter to every object type, this removes it: the `useFindManyRecords` call, the `attachments` prop, and the `useAttachmentSync` call in `StandaloneRichTextEditorContent`. It finishes the cleanup #17934 started. `useAttachmentSync` is untouched and still used by `RichTextFieldEditor`, which does support file blocks. Net result is a pure deletion, and the widget renders on every layout type. If image blocks are ever added back to `DASHBOARD_BLOCK_SCHEMA`, attachment sync will need to come back with them. ## Testing Local instance, widget created through `createPageLayoutWidget` on the default Company record page layout. - On the unpatched component the widget is absent from the page. - With the fix it renders read-only in the record page column. - Also verified with the payload shape from the issue (`markdown` set, `blocknote: null`); the server converts it to blocknote on write, so it renders too. - Verified on `calendarEvent`, an object with no `attachments` relation. Renders clean, no console or GraphQL errors. Generalising the old filter instead would have sent `targetCalendarEventId` and hit `Object attachment doesn't have any "targetCalendarEventId" field.` - Dashboard rendering unchanged, and editing still round-trips: typed into the widget in dashboard edit mode, hit Save, confirmed the new body in `core.pageLayoutWidget`. |
||
|
|
5d90fb33c0 |
Open records on a full page instead of a side panel on mobile (#23474)
On mobile the side panel covers the whole screen, so a record opened in
it arrives cramped behind an "Open" button offering the full page it
should have gone to in the first place.
`useResolveOpenRecordIn` already forces `RECORD_PAGE` on mobile via
`canDisplaySidePanel: !isMobile`, but it is a resolver callers have to
opt into, and only five do. Thirteen other call sites reach
`useOpenRecordInSidePanel` directly and get a panel on every device,
including:
- `TaskRow` and `NoteTile`, the activity lists inside a record's tabs
- `EventRowActivity`, `EventCardMessage`, `EventRowGenericLinked` on the
timeline
- `SidePanelSearchRecordsPage`, `EmailThreadPreview`,
`useOpenCreateActivityDrawer`, `useAddNewRecordAndOpenSidePanel`
## Change
Decide it inside `useOpenRecordInSidePanel` rather than at each call
site, so no caller can wedge a record into a panel by forgetting to ask.
On mobile it closes the panel and navigates to `AppPath.RecordShowPage`,
then returns before any of the side-panel setup runs.
Two details carried over so the redirect is not lossy:
- `setRecordPageActiveTabId` still runs first, so a caller passing `tab`
lands on the right tab.
- `isNewRecord` forwards `{ isNewRecord, objectRecordId,
labelIdentifierFieldName }` as navigation state, mirroring what
`useCreateNewIndexRecord` already does on its `RECORD_PAGE` branch, so a
freshly created record still opens its title for naming instead of
arriving untitled.
Side-panel-only effects are skipped rather than lost.
`runWorkflowRunOpeningInSidePanelEffects` ends in
`openWorkflowRunViewStepInSidePanel`, which auto-opens a step *in the
panel*; with no panel there is nothing for it to do, and the workflow
run's record page renders its own diagram.
The two hooks that already branch on `useResolveOpenRecordIn`
(`useOpenRecordFromIndexView`, `useCreateNewIndexRecord`) never call
into this path on mobile, so this is a no-op for them rather than a
double navigation.
Uses `useIsMobile` rather than `useIsTouchDevice`, matching
`useResolveOpenRecordIn`: this is a question of whether there is room
for a panel, not of how the user points.
## Testing
At 390x844, opening the search side panel and tapping a result now
navigates to `/object/person/<id>` with the panel closed, where it
previously stayed in the panel. Typecheck and lint clean.
---
_Generated by [Claude
Code](https://claude.ai/code/session_018gcsCQbuTMsyFWv874p25Q)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23474?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. -->
|