5effee7754b9b00d3558197a4c19cbee4b2d7e72
467 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. --> |
||
|
|
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. --> |
||
|
|
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. --> |
||
|
|
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`. |
||
|
|
3f0236b590 |
fix(page-layout): let the column surface win over the solo presentation (#23412)
## Why In the side panel and on mobile, a tab holding a single widget renders with no gutter at all: the field list sits flush against the panel border. Regression from #23109. `getWidgetCardVariant` checked the derived presentation before the surface: ```ts if (presentation === 'solo') return 'solo'; const isSideColumnContext = isInPinnedTab || isMobile || isInSidePanel; ``` So a single-widget tab resolved to `'solo'` even in the side panel or on mobile, and `'solo'` has no branch in `WidgetCard`'s padding switch, so it falls through to `0`. The same widget used to match `variant === 'side-column' && !isEditable` and get `spacing[3]` (12px). The pinned left panel escaped this only because `PageLayoutLeftPanel` hardcodes `presentation: 'stack'` — the rule was already there ("the pinned left panel is always a column, a surface rule not a widget rule"), just applied at one call site instead of being the rule. ## Why only the Home tab looks broken Every widget that used to live on a `CANVAS` tab carries its own gutter, so losing the card padding costs them nothing: | Widget | Own horizontal padding | |---|---| | Timeline | `spacing[6]` | | Notes | `spacing[6]` | | Files | `spacing[6]` | | Tasks | `spacing[6]` | | **Fields** | **none** | `Fields` was the only widget on a `VERTICAL_LIST` tab, so it was the only one relying on the card for its gutter, and the only one that ends up flush. ## What Resolve the surface first: a column surface (pinned panel, side panel, mobile) is always a column of cards, whatever the tab presentation is. Solo stays a main-tab-area concept. Header visibility is untouched: `showHeader` keys off `presentation`, not the variant, so a solo tab still shows no bare title row. The `Fields` widget does not regain the header it lost in #23109. ## Measured Side panel, custom object whose Home tab holds a single Fields widget (1600x1000, panel at x=1200): | | Card padding | First label x | |---|---|---| | main | `0px` | 1221 | | this PR | `12px` | 1233 | 12px restored, matching what the pinned left panel gives the same widget. ## Trade-off worth a second opinion In the side panel and on mobile, the activity widgets now resolve to `'side-column'` instead of `'solo'`, so they pick up the card's 12px on top of their own 24px, i.e. 36px instead of 24px. Nothing overlaps or clips, but it is a visible change on those tabs. If you would rather keep them at 24px, the follow-up is to drop the intrinsic `spacing[6]` from the activity cards and let the surface own the gutter everywhere. ## Test plan - `getWidgetCardVariant` tests extended: `'side-column'` now wins over `'solo'` for each of `isInPinnedTab` / `isMobile` / `isInSidePanel`. 13 tests pass. - 88 suites / 620 tests across `page-layout/widgets` pass. - `lint:diff-with-main twenty-front` clean. - Verified against a local stack: side panel on a single-widget Home tab, before and after. |
||
|
|
1e58c3073c |
Feat/email composer improvements (#23188)
- Move composer to dedicated page - Add test email option - Auto saved as draft can be revisited from `objects/messageCampaigns` later - Campaign stats component https://github.com/user-attachments/assets/9e523116-e79b-496d-9c9d-3887e0c9213f <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23188?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> |
||
|
|
1e5b8e6db4 |
fix(front): add accessible labels to icon-only options dropdown triggers (#23325)
## Problem Refs #23127. Icon-only `LightIconButton` "more options" triggers (`IconDotsVertical`) render without an accessible name, failing WCAG 4.1.2 (button-name) — screen readers announce nothing for them. ## Fix Add `aria-label={t`More options`}` to the affected dropdown triggers. `LightIconButton` already forwards `aria-label` and sets `aria-hidden` on the icon when a label is present, so this is purely additive — no behavioral or visual change. Scoped to a coherent set of options-menu triggers (attachments, public domains, SSO, connected accounts, field group config). Other unlabeled icon buttons can follow in separate PRs. ## Verification `oxlint --type-aware` and `oxfmt` pass on all changed files. The label is i18n-wrapped via the existing `useLingui` macro already imported in each component. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23325?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. --> |
||
|
|
6cc7ed7570 |
Make solo tabs first-class: derived presentation, native editing, unified widget header (#23109)
## Why
Full-page record tabs (Timeline, Tasks, Notes, Files, Emails, Calendar,
Flow) were encoded by storing a `CANVAS` layout mode. That made them a
separate species: editing one didn't feel native (no drag handles, no
way to add a second widget, the tab couldn't adapt), and the widget
pipeline was full of `layoutMode === CANVAS` branches.
This PR replaces the stored mode with two derived rules and one unified
header grammar:
> **Presentation is derived from content, never stored.**
> A list tab with exactly **one widget** renders it **solo**
(full-bleed, it owns the tab). Anything else is a **stack** of boxed
cards. **Edit mode always shows the stack structure.**
No widget taxonomy, no per-type branches: any lone widget owns its tab.
## What
**Presentation model**
- `getTabPresentation({ widgets, layoutMode, isInEditMode })`: solo iff
a list tab has exactly one widget in view mode; grid tabs (dashboards)
and edit mode are always stacks. The pinned left panel is always a
column (a surface rule, not a widget rule).
- Solo view rendering is identical to the old CANVAS rendering
(container height, internal scroll).
- Stacked widgets in the main tab area get one bounded slot rule
(`max-height` + own scroll) so no widget swallows the tab;
pinned/side-column stacks keep their flowing behavior. This only binds
on user-composed mixed tabs, which could not exist before.
**Native editing (the point of the PR)**
- Every record-page tab is edited through the same vertical-list editor:
drag handle, reorder, remove, add widget. Add a second widget to a
Timeline tab and it becomes a stack; remove back down to one and it's
solo again. Nothing is stored, nothing to migrate.
- Fixes the stuck-drag bug found while testing the preview: widgets
publishing header info republished a fresh object on every render
(activity cards build their action from non-memoized hook returns), and
since the widget chrome reads that state above the widget content, any
tab with an activity card sat in an infinite render loop. The loop
starved React's transition lane, which dnd-kit's drop teardown waits on,
so the drag clone and drop outlines froze on screen after a drop. The
header hook now republishes only on real value changes and routes
onClick through a stable wrapper, so callers need no memoization. The
page-layout drag provider also disables the Feedback drop animation so
clone cleanup is synchronous at drop time.
**Unified widget header API**
- A widget's content can publish header info to its chrome via
`usePublishWidgetHeaderInfo({ count, primaryAction })`: a count rendered
in grey next to the title, and a primary action (icon button with
accessible name) on the right in view mode. Instance-scoped state keyed
by widget id, so third-party widgets (front components) can use the same
seam later; the hook no-ops outside a page layout (stories, previews)
and is safe to call with inline, non-memoized values.
- A solo widget's header only appears when the widget published
something: the tab label already names it, so a bare title row adds
nothing. Timeline/Flow tabs stay exactly as today.
- Emails, Tasks, Notes, Files, Calendar publish their count (query
totals, not loaded-page lengths) and action (Compose, New task, New
note, Add file) and stop rendering internal title rows ("Inbox 12", "All
5"): exactly one header per widget everywhere, same grammar.
`ComposeEmailButton`, `AddTaskButton` and the title/button plumbing in
`NoteList`/`AttachmentList`/`TaskList` are deleted.
**Object-aware tabs**
- The hardcoded `SYSTEM_OBJECT_TABS` title allowlist is gone. A tab
renders based on whether the target object supports its widgets: widgets
that read through a relation (Tasks, Notes, Files, Timeline) require the
relation field to exist and be active, while Emails and Calendar
aggregate through the messaging timeline, so a missing participants
relation is fine (Company) and a deactivated one is an explicit opt-out.
System objects on the shared default layout keep exactly Home +
Timeline, now by derivation instead of hardcoded titles.
**Data cleanup**
- Seeds (frontend defaults, server standard template, `twenty app`
scaffolder, docs) write `VERTICAL_LIST`;
`PageLayoutTabLayoutMode.CANVAS` is `@deprecated`, kept read-only for
layouts persisted before this change (they render correctly through the
derivation; no data migration, by design: an in-place flip can't pass
the widget-position/tab-layoutMode validator atomically, and it isn't
needed).
- Locale catalogs are intentionally untouched: the i18n pipeline
extracts and translates the new header labels on main; they fall back to
their English source until then.
## Deliberate view-mode changes (approved)
- A lone widget of any type now owns its tab full-bleed: lone Fields tab
(mobile/side panel), lone rich-text Note tab, lone chart, and the
message-thread page lose their card box.
- Activity tabs show the unified header (title, grey count, + action)
instead of their internal "Inbox 12"-style rows.
Everything else is pixel-parity, including solo scroll behavior and
dashboards.
## Test plan
- `nx typecheck twenty-front` / `twenty-server`: clean; oxlint/oxfmt on
the changeset: clean
- 239 suites / 1474 tests across page-layout, activities, side-panel
pass, including new tests for `getTabPresentation` (count-based,
edit-mode override) and `usePublishWidgetHeaderInfo` (publish, cleanup
on unmount, no-op outside a widget, referential stability across
re-renders with inline actions, latest-onClick wrapper)
- `getTabsRenderableForTargetObject` tests covering missing vs
deactivated relations, Emails/Calendar without a participants relation,
and non-relation widgets
- Stuck-drag repro verified fixed end to end against a local stack with
an instrumented dnd-kit: before the fix the affected tab committed ~65
renders/second at idle and drops never tore down; after it, idle commits
are flat and every drop cleans up
|
||
|
|
3ee8fc0973 |
Add front component skeleton loader (#23261)
Front components (dashboard widget, side panel, settings preview) showed blank space during their entire load. They now show a shimmering full-area skeleton continuously, from the lazy chunk load through metadata fetch, token/SDK wait, and worker boot, until the real UI mounts. The skeleton is threaded down as an optional `loadingFallback` prop so the shared `twenty-front-component-renderer` package stays dependency-free (react-loading-skeleton stays in twenty-front). The command-menu headless component opts out by not passing a fallback, so it stays blank as before. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23261?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. --> |
||
|
|
3a8f086d15 |
Converge drag and drop on shared dnd-kit primitives, remove @hello-pangea/dnd (#23211)
Follow-ups recorded in #23023, done in one pass. ## Shared primitives - Folded `PageLayoutWidgetSortableItem` and `PageLayoutWidgetDropLine` into the shared `DragDropItemSortableCell` / new `DragDropItemDropLine` (new `data`, `dropLine`, `highlightWhileDragging`, `hasTransition` props). - Added generic `DragDropProviderDragStartEvent` (and DragMove/DragOver/DragEnd/DropTarget) helpers and deleted the 7 copied `Parameters<...>` extractions across the dnd hooks. - Replaced the `useMovePageLayoutWidgetUp/Down` implementations (~140 lines) with `moveWidgetWithinTabInDraft`. - Migrated the remaining page-layout test suites onto `pageLayoutDraftFixtures`. ## Tab reordering off Pangea - Tabs are sortable cells on the same provider as widget drags, segregated by dnd type, so widget drops on tab buttons keep working while tabs reorder. - Reordering is ID based (`reorderTabInDraft`: insert before the hovered tab), which keeps the pinned first tab in place without index arithmetic. - Preserved overflow behaviors: the dropdown stays open while a tab drag is in flight, dropping a tab on the "+N More" button appends it and opens the dropdown, and both the visible strip and the overflow list have end drop zones. ## Fields configuration editors off Pangea - Group reorder, field reorder and cross-group field moves now run on the shared cells (same drop line and end-zone patterns). ## DraggableList off Pangea - `DraggableList` / `DraggableItem` keep their consumer-facing API — the ~9 consumers now type their handlers with a local `DraggableListDropResult` instead of pangea's `DropResult` — but run on the shared sortable cells; each list's uuid group doubles as its dnd type so nested lists stay isolated from page-level providers. - Items register their index in a list-scoped registry so the end drop zone can resolve the append index at drop time (with insert-before semantics an item could otherwise never reach the last position). - Deleted three dead files that only existed for pangea plumbing (the side panel navigation placeholder, `getCssCompatibleDraggableProps`, the orphaned `recordGroupPendingDragEndReorderState`). ## Record table row drag off Pangea - Rows register through `useSortable` directly on the row element — no wrapper div, so row CSS, sticky cells and virtualization stay untouched — with the grip cell wired as the drag handle via the shared sortable handle ref context. - Both table modes (virtualized flat list and record groups) share a `DragOverlay` clone that replaces pangea's virtual-mode `renderClone`, and end drop zones per record group (and after the virtualized list) allow dropping after the last row or into an empty group. - The drop handlers keep their pangea-shaped result object, retyped as a local `RecordDragDropResult`, so the position computation logic is untouched. ## Pangea removed `@hello-pangea/dnd` is gone from `package.json` and the lockfile, along with its orphaned transitive entries (`css-box-model`, `raf-schd`, `react-redux`, `redux`). Nothing in the repo imports it anymore. ## Dashboards: cross-tab widget drag for grids react-grid-layout drags never enter dnd-kit, so the bridge hit-tests the pointer against the tab buttons' `data-page-layout-tab-drop-target-id` rects during grid drags, highlights the hovered tab through state, and on drop moves the widget to the destination grid below its existing content (`moveWidgetToGridTabInDraft`, `buildTabWidgetLayouts`). The grid's own post-drag layout commit is suppressed once so it does not overwrite the cross-tab move. ## Fixes found while testing - With `feedback: 'clone'`, the drag source is its own initial drop target and its placeholder is a DOM clone taken at drag start, so the drop line rendered into the source got baked into the placeholder and stuck there for the whole drag. The line is now hidden on the source cell, leaving a single indicator at the actual target. - Reorderable tabs collapsed to text height and sat top-aligned next to "+ New Tab" because the sortable cell wrapper defaults to `display: block; height: auto`, breaking the tab height chain — the tab list now uses the cell's `fill` mode so tabs stretch to the strip height again. ## Testing Playwright against the dev app: - Record page: widget reorder up and down in the pinned column (single blue drop line at the target), drag to another tab via its tab button (highlight + move), drag back into content at a specific position, chained cross-tab moves, tab reorder with vertical drop line, new tab creation. - Overflow (narrow viewport): drop a tab on "+N More" (appends last, dropdown opens), reorder inside the dropdown (stays open), drag a tab from the dropdown back to the visible strip. - Dashboard: grid drag within a tab, cross-tab drag onto a tab button (hover highlight, widget lands below destination content, remaining widgets keep their positions), save and reload persistence in both directions. - Fields editor: field reorder, group reorder, field move across groups, plus the Move Up / Move Down widget actions. Since the pangea-removal commits: - Typecheck, oxlint and oxfmt green over the full front source; unit suites green including the migrated `useStartRecordDrag` test (jest needed a scoped transform exemption for `@preact/signals-core` once dnd-kit reached the side-panel suites). - Storybook visual regression unchanged across ~700 stories — expected, since the migrated surfaces render identical DOM at rest (drop lines and drag overlays only exist mid-drag). - The tab strip fix reverses the exact regression mechanism: the sortable cell wrapper defaulted to `display: block; height: auto`, collapsing the tab height chain next to the full-height "+ New Tab" button; `fill` restores the stretch. --- _Generated by [Claude Code](https://claude.ai/code/session_01XKRCzzu8oGyocXZtFp7VEG)_ <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23211?utm_source=github" rel="nofollow noreferrer noopener" target="_blank">``<img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg">``</a> |
||
|
|
65d399c70d |
feat(page-layout): drag widgets across tabs on record pages (#23023)
## What When editing a record page layout, you can now drag a widget out of one tab and into another. The most common case works: drag from the left column (the pinned first tab, in full mode) into the tab you're currently viewing. Ways to move a widget across tabs: - **Into the visible tab's content** — drop it into another vertical-list tab's list to place it at a precise index. A blue line shows exactly where it will land. - **Onto a tab button** — drop a widget onto another tab's button to move it to that tab; the button highlights while hovered. - **Into an empty tab / the end of a tab** — an end-of-list drop zone (wrapping the add-widget area) accepts the widget, so an empty tab is a valid drop target and widgets can be appended to the end of a populated one. Within-tab reordering keeps working as before, now with the same blue drop-line indicator. ## How The record-page widget list is migrated from `@hello-pangea/dnd` to `@dnd-kit/react` (already used elsewhere in the app, e.g. navigation-menu-item and record-board). A single `DragDropProvider` spans the left column, the tab bar, and the active tab content, which is what makes cross-list drag possible — Pangea scopes each list to its own context, so cross-tab drag wasn't expressible there. - Widgets are dnd-kit sortables grouped by `tabId`; dropping into a different group is a cross-tab move. - The drop line uses `useSortable().isDropTarget` on the targeted widget, plus an end-of-list droppable for append/empty-tab. - Each record-page tab button is a `useDroppable` target for widgets, opt-in per vertical-list tab so canvas/grid tabs keep their native placement. - The drag lifecycle lives in one router hook (`usePageLayoutWidgetDragAndDrop`) that routes to two pure, unit-tested draft utils (`moveWidgetWithinTabInDraft`, `moveWidgetToTabInDraft`). The side-panel "Move to tab" action shares the same `moveWidgetToTabInDraft` util, so drag and menu paths converge on one mutation. - Drop resolution reuses the shared module #23071 landed: `getDestinationIndex` compensates same-tab downward moves for the source-removal shift so the drop line and the landing slot agree, and the shared `preventNativeDragStart` guard stops links/images inside widget content from starting a native URL drag. ## Scope Deliberately staged to the record-page widget list. Not included (follow-ups): - Tab reordering and the field-config editors still use `@hello-pangea/dnd`; finishing the full page-layout removal of Pangea is separate. - Grid/dashboard cross-tab drag (the source there is `react-grid-layout`, which needs a cross-system bridge). - #23071 has merged and this branch sits on it; the remaining convergence is a follow-up: fold `PageLayoutWidgetSortableItem`/`PageLayoutWidgetDropLine` into the shared `DragDropItem*` cells, export a generic drag-event-type helper to delete the 7 copied `Parameters<...>` extractions, replace `useMovePageLayoutWidgetUp/Down` with `moveWidgetWithinTabInDraft`, and migrate the remaining page-layout test suites onto `pageLayoutDraftFixtures`. ## Testing - Unit tests for `moveWidgetToTabInDraft` and `moveWidgetWithinTabInDraft` (incl. the non-vertical destination guard); the three suites now share one fixture module (`page-layout/testing/pageLayoutDraftFixtures`). - The downward off-by-one is covered by the shared `getDestinationIndex` unit tests from #23071. - Full `page-layout` suite green (161 files / 1055 tests), plus typecheck, lint, and format. - The drag interaction itself (drop precision incl. downward same-tab drops, line/highlight, clone feedback) still needs a manual pass in the running app. |
||
|
|
d1c70ab0bf |
Fix dashboard record table widget aggregate persistence (#23008)
## Summary - Update dashboard record-table widget aggregate changes to write into the widget draft while page layout edit mode is active. - Include `aggregateOperation` when saving record-table widget view fields through `upsertViewWidget`. - Persist aggregate operations server-side for widget view-field create, update, and clear flows. - Add frontend utility tests and backend integration coverage for widget aggregate create/update/clear behavior. Fixes #22934. ## Why Dashboard record-table widgets use their own draft view state while a page layout is being edited. The aggregate footer path was resolving fields through the normal current-view flow and then trying to persist immediately, which can miss widget draft fields and fail before the save flow runs. This change keeps aggregate edits in the widget draft during page layout editing, then saves the aggregate operation with the rest of the widget view configuration. ## Validation - `npx nx lint twenty-front` - `npx nx typecheck twenty-front` - `npx nx test twenty-front --configuration=ci` - `npx nx build twenty-front` - `npx nx build twenty-server` - `npx nx lint twenty-server --configuration=ci` - `npx nx typecheck twenty-server` - `npx nx test twenty-server --configuration=ci` - `npx nx jest --config ./jest-integration.config.ts --logHeapUsage --runTestsByPath test/integration/metadata/suites/view/upsert-view-widget.integration-spec.ts` - `git diff --check` Disclosure: I used AI-assisted coding tools while preparing this PR. I reviewed the changes myself, tested them, and take responsibility for the implementation and any follow-up revisions needed. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23008?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. --> |
||
|
|
3ad3e8bd1a |
feat: kanban, calendar and group-by table layouts for dashboard view widgets (#22963)
## Context Dashboard view widgets previously only rendered flat tables. This PR ships the full feature: **Table with group-by**, **Kanban**, and **Calendar** layouts for dashboard view widgets — server API + frontend, end-to-end. (Originally staged as a 4-PR stack — #22966, #22967, #22968 — consolidated here per review.) ## Server / API - **View typing.** Adds `KANBAN_WIDGET` and `CALENDAR_WIDGET` to `ViewType` (following the `TABLE_WIDGET` precedent) so widget-backing views keep their layout in `view.type` while staying excluded from record-index pickers. Shared `getViewLayoutFromViewType()` maps widget types to their base layout; `isWidgetViewType()` centralizes the exclusions that were previously hardcoded per-site. - **Migrations.** Two fast instance commands (**2.23**): `ALTER TYPE core.view_type_enum ADD VALUE` for both values, and a widened `CHK_VIEW_CALENDAR_INTEGRITY` constraint covering `CALENDAR_WIDGET` (entity `@Check` updated for fresh installs). - **Validation.** `FlatViewValidatorService` keys kanban/calendar validation on the mapped layout, so widget views get the same invariants as index views (kanban needs a groupable group-by field; calendar needs a date field + layout). Calendar widget views default to month; a non-month (DAY/WEEK) layout is rejected at the API level **unless** the `IS_CALENDAR_WEEK_VIEW_ENABLED` feature flag is enabled for the workspace — the same flag that gates day/week on index calendars. - **API.** `upsertViewWidget` (LAYOUTS permission) accepts a nested `view` settings input (`type`, `mainGroupByFieldMetadataId`, `shouldHideEmptyGroups`, kanban aggregate/column-width, calendar layout/fields). Routes through the standard update path, so `viewGroups` auto-generate from SELECT options exactly like index views. Only widget view types accepted; only `RECORD_TABLE` widgets can change view settings. - **AI tools.** `create-complete-dashboard` + `create_view` now use/allow the `*_WIDGET` types (previously they created plain `TABLE` views that leak into index pickers). ## Frontend **Settings panel.** The **Source** (object) row comes first, since which layouts are available depends on it. The **Layout** row below is a working dropdown (Table / Kanban / Calendar); layouts the source object can't support are **disabled with a hint** ("Needs a Select field" / "Needs a Date field") rather than hidden. Group-by row (select fields; searchable) with a **Hide empty groups** toggle while grouped; **Date field** row replaces Group by while Calendar is active, and — when the `IS_CALENDAR_WEEK_VIEW_ENABLED` flag is on — a **Calendar view** row (Day / Week / Month) appears beside it; **Limit** row hidden while grouped (only the flat virtualized loader enforces it). Kanban keeps its group-by locked (no `None` option). **Instant edit-mode preview.** Draft snapshots carry `viewGroups`; picking a group-by synthesizes them client-side (`buildDraftViewGroupsForFieldMetadataItem`, mirroring the server's generation), so grouped tables/boards preview immediately before dashboard save. On save, `upsertViewWidget` responses hand back the server-generated groups, which replace the client-generated ones in the persisted snapshot. **Renderers.** `RecordTableWidgetRendererContent` branches on the backing view's layout: `RecordBoardWidget` (wraps the standard `RecordBoardContainer`) and `RecordCalendarWidget` (mounts the existing `RecordCalendar`, which renders month / day / week) inside the same per-widget provider sandbox the table uses. **Read-only semantics.** Two flags with distinct scopes, each documented on its state: - `isRecordBoardViewSettingsReadOnlyComponentState` — locks the board chrome that edits view settings (add group, column reorder/resize/menu, aggregates); **card drag still updates records** under object permissions. - `isRecordCalendarReadOnlyComponentState` — widget calendars are read-only by default (no drag, no add-new, no in-calendar layout switch); cards open the side panel. The one exception, behind `IS_CALENDAR_WEEK_VIEW_ENABLED`: a **live (non edit-mode) day/week** widget calendar allows drag-to-reschedule and record creation under object permissions. Month calendars and edit-mode previews stay read-only. **Calendar state componentization.** The calendar module's three settings move from global atoms to component states keyed on `RecordCalendarComponentInstanceContext` (same pattern as record-board), so several calendar widgets and an index-page calendar can coexist without leaking state. All readers resolve the ambient instance; calendar unit tests updated. **Multi-instance fixes that also fix index pages:** record drag states were written against a different instance than every reader resolves (now use the ambient instance); the board sticky-header DOM id is namespaced per board; dragged board cards portal to `document.body` while dragging so react-grid-layout's transforms can't offset the clone from the pointer. ## Scope (v1) - Widget calendars are month-only and read-only by default. With `IS_CALENDAR_WEEK_VIEW_ENABLED` enabled, day/week layouts become selectable (UI + API) and live day/week widget calendars support drag-to-reschedule and record creation under object permissions. - Widget group-by offers SELECT fields only (server auto-generates groups from options; widgets have no per-record add-group flow). ## Tests - Integration: `upsert-view-widget-view-settings.integration-spec.ts` (9 tests — group auto-creation, invalid type/field rejections, non-month calendar widget rejected while the week/day flag is off and accepted once it's enabled, combined settings+fields call); pre-existing `upsert-view-widget` suite (20) green. - Front: new suites for draft view-group generation and snapshot clone/build utils; calendar suites componentized; full `twenty-front` jest, typecheck, oxlint green; `twenty-server` typecheck + lint green. - Browser-verified end-to-end (real dev server + seeded workspace): configure → live edit-mode preview → save → reload for all three layouts; measured drag with pointer inside the card; index-page calendar re-verified (with the week/day flag enabled). https://claude.ai/code/session_01E5N87kwwZWhDtEQaP72cMf |
||
|
|
055e8b5335 |
Add tab param to open a record side panel page on a specific tab (#22905)
## Context
`CommandOpenSidePanelPage` (and `openSidePanelPage` in the front
component SDK) could open a record in the side panel, but always landed
on the default tab. This adds an optional `tab` param to the
`ViewRecord` page params so an app command can open a record directly on
a specific tab.
## What changed
- **twenty-sdk**: `OpenSidePanelPageParams` `ViewRecord` variant accepts
an optional `tab` (a page layout tab id). Since
`CommandOpenSidePanelPage` props are `OpenSidePanelPageParams`, the
component picks it up automatically.
- **twenty-front**:
- New `setRecordPageActiveTabId` util resolves the record page layout
for the object (custom layout from the store, or the default layout id)
and presets `activeTabIdComponentState` on the tab list instance
(`${pageLayoutId}-tab-list-${recordId}`), which is shared by the side
panel and the full record page.
- `useOpenRecordInSidePanel` accepts `tab` and presets the active tab
before navigating; it also applies when the record is already open in
the side panel (tab switch only).
- `useFrontComponentExecutionContext` forwards `tab` to the side panel
open, and presets the tab when falling back to full-page navigation
(mobile, or objects that can't open in the side panel).
- **Docs**: mention the optional `tab` id in the
`CommandOpenSidePanelPage` description.
Unknown tab ids are harmless: `PageLayoutTabListEffect` falls back to
the layout's default tab when the preset id doesn't exist in the layout.
Dashboards are skipped since their layout id comes from record data, not
object metadata.
## Tests
- `useOpenRecordInSidePanel`: new test asserting the active tab atom is
preset on the correct tab list instance id.
- `useFrontComponentExecutionContext`: new tests for tab passthrough to
the side panel and tab preset on full-page fallback.
- `npx nx typecheck twenty-front`, `typecheck twenty-sdk`,
`lint:diff-with-main twenty-front`, `lint twenty-sdk` all green.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01VaWY1H9RZqgJkytqZakvZi)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22905?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. -->
|
||
|
|
5426536004 |
Fix(Twenty-front): Junction field unable to display as table (#22844)
Fixes: #22783 The junction field can now be displayed as a table. <img width="1911" height="961" alt="image" src="https://github.com/user-attachments/assets/2aad15af-baca-4563-85eb-ef0826008a7d" /> |
||
|
|
8e022d3c49 |
Fix stale relation table in field widget when switching records in side panel (#22829)
The relation table rendered by a FIELD widget in TABLE display mode kept its jotai component states (loaded rows, virtualization maps, loading guards, query identifiers) in instances keyed only by widget id and view id. Since the side panel record pages share those instances across records, switching to another record kept rendering the previous record's related rows until an asynchronous catch-up reload landed, and any race or error in that catch-up left the previous record's data on screen permanently. Scope the record-table widget's context store instance and record index instance by target record id (and side panel surface), the same way FieldsWidget already scopes its field list instances. Each record now gets its own table state, so a record's rows can never appear under another record, and loads that land after a record switch write into their own instance instead of the visible one. loadRecordIndexStates and setRecordGroupsFromViewGroups accept an optional recordIndexId override so the widget view load effect can populate the record-scoped instance instead of deriving the shared one from object name and view id. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22829?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
6c40c7b91a |
Deterministic system field universal identifier (#22565)
# Introduction Close twentyhq/core-team-issues#2641 Auto-provisioned field metadata used to get its `universalIdentifier` from three unrelated sources: random `v4()` on the server when creating custom objects, hardcoded values in `STANDARD_OBJECTS`, and an ad-hoc `v5` derivation in the SDK manifest build. This PR unifies all of them behind the shared `getFieldUniversalIdentifier` derivation: ``` universalIdentifier = f(applicationUniversalIdentifier, objectUniversalIdentifier, fieldName) ``` ## Ownership model The rollout is built on an explicit split of who owns a field's universal identifier: - **The 8 system fields** (`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`) are **server-owned**. Their universal identifiers are always the deterministic derivation, on **every** application (standard, workspace-custom, installed). Clients cannot provide custom values: a temporary check in `validateObjectMetadataSystemFieldsIntegrity` rejects any non-derived system field identifier at migration build time. This check stands in until system fields are generated exclusively server side by the metadata side-effect engine and stripped from client inputs — at which point it becomes structurally impossible to send one. - **`name` is a default field, not a system field**: it is auto-provisioned when absent (server side for custom objects, SDK side for application objects) but authors can define their own. It is only derived where it is guaranteed to be auto-provisioned. In particular, standard objects keep their **historical hardcoded** `name` identifiers: the standard app authors its `name` fields like any installed app would, and moving those identifiers would break every installed application referencing them (e.g. views on `opportunity.name`). - **User-created and author-provided fields** keep random / explicit identifiers, untouched. ## Server - `validateObjectMetadataSystemFieldsIntegrity` now validates, on top of the existing type/`isSystem` checks, that each system field's `universalIdentifier` equals the deterministic derivation. Runs for every object creation going through the migration orchestrator: app sync, custom object creation, standard provisioning - `build-default-flat-field-metadatas-for-custom-object.util.ts` derives the system field identifiers (and the auto-provisioned `name`) with `getFieldUniversalIdentifier` instead of `v4()` - `build-default-relation-flat-field-metadatas-for-custom-object.util.ts` derives both the forward and the reverse default relation field identifiers deterministically - `generateMorphOrRelationFlatFieldMetadataPair` accepts optional `sourceFieldUniversalIdentifier` / `targetFieldUniversalIdentifier` so callers can inject deterministic values; user-created relations still default to `v4()` ## twenty-shared - `STANDARD_OBJECTS` system field identifiers (the 8) are now computed at module load via `buildStandardObjectSystemFields`; `name` and every other identifier keep their hardcoded values - New snapshot test pinning **every** universal identifier of `STANDARD_OBJECTS`: any identifier change now requires an explicit snapshot update and should ship with a coordinated backfill ## SDK (breaking, pre-GA) - `generateDefaultFieldUniversalIdentifier` delegates to `getFieldUniversalIdentifier` and now requires `applicationUniversalIdentifier` - Reverse default relation field identifiers are derived from the field's real coordinates (standard object UID + actual field name, e.g. `targetRocket` on `attachment`) instead of the legacy custom-object UID + synthetic `${fieldName}Inverse` hash input. Field *names* are unchanged - The manifest build threads the application universal identifier through default field injection (two-pass over object configs) - `twenty dev:add` now resolves the application universal identifier upfront and refuses to scaffold anything until `defineApplication` declares one — no more `fill-later` placeholder for the app UID in generated files ## Upgrade A 2.19 **workspace command** backfills existing `fieldMetadata.universalIdentifier` rows to the deterministic derivation. Coverage follows the ownership model: - **The 8 system fields**: taken over for **every application**, whatever value they currently hold. This is both safe and required now that sync rejects non-derived values — leaving a row unconverged would make its application unsyncable - **`name`**: workspace-custom app → always taken over (server-generated, no author to clobber); installed applications → only rows still carrying the legacy SDK derivation are recomputed, author-provided identifiers are never touched; standard app → never touched (hardcoded in `STANDARD_OBJECTS`) - **Default relation fields**: workspace-custom app → forward fields on custom objects and reverse fields on the standard relation objects; installed applications → legacy-derivation probe only All identifiers of a workspace are updated inside a single transaction, then the command flushes the field-metadata-related workspace caches and bumps the metadata version. Stored `applicationRegistration.manifest` snapshots are intentionally **not** rewritten: installs and upgrades always sync from the `manifest.json` inside the resolved package (npm/tarball), the stored column is only used for display/marketplace purposes. ## Breaking behavior for old packages (fail closed) Packages built with an older SDK carry legacy system field identifiers in their tarball `manifest.json`. Installing or upgrading such a package now fails with an explicit `INVALID_SYSTEM_FIELD` validation error ("universal identifier is not deterministic") instead of silently mismatching against the backfilled rows and triggering a destructive delete+create. The remediation is to rebuild the package with the new SDK; the backfill has already converged the installed rows, so the rebuilt manifest syncs cleanly. ## Test plan - [x] `twenty-sdk` unit tests (526 tests) and typecheck - [x] `twenty-shared` unit tests (1635 tests) including the `STANDARD_OBJECTS` snapshot; `name` identifiers verified byte-for-byte identical to `main` - [x] Lint and typecheck clean on all touched packages - [x] Integration: create a custom object and verify system + default relation field identifiers match the deterministic derivation (`create-one-object-metadata-deterministic-field-universal-identifiers`, 13 assertions passing) - [x] Integration: `failing-sync-application-object-system-fields` extended with a non-derived system field identifier case; all identifiers in the spec pinned deterministically so snapshots embedding expected/actual values are stable across runs (verified with a double run) - [x] Integration: all application sync suites pass with the derived system field identifiers now required by the `buildDefaultObjectManifest` test helper (9 suites, 20 tests) - [x] Full test-database reset: standard app provisioning and seeded workspaces pass the new validation - [x] SDK manifest build verified on the postcard example app: all auto-generated default field identifiers match the derivation - [ ] Run `upgrade:2-19:backfill-deterministic-field-universal-identifiers` (dry-run then real) on a seeded workspace and verify identifier convergence with a rebuilt app manifest |
||
|
|
e609320666 | Squirclesssss 🟦🔵 (#22535) | ||
|
|
3aeb2b0d5d |
Self-host Inter and DM Mono fonts (#22506)
Replaces the two Google Fonts stylesheets with self-hosted fonts via `@fontsource`, imported in the app entry (and Storybook preview), and removes the duplicate Inter that BlockNote was shipping. ## Why - **The caching argument for Google Fonts is dead.** Browsers partition the HTTP cache by top-level site (Chrome 86+, Firefox 85+, Safari even earlier), so a font cached from another website is never reused on ours. Every first-time visitor downloads the fonts either way — Google just adds a detour. - **Faster first paint.** This removes two render-blocking cross-origin stylesheets from `index.html` (DNS + TLS to `fonts.googleapis.com`, then a second connection to `fonts.gstatic.com`, with no preconnect today). The fonts now ship from our own `/assets` alongside the rest of the app, behind the same CDN and cache policy. - **Privacy.** Visitor IPs are no longer sent to Google on every page load. A German court ruled in 2022 that Google Fonts embedding violates GDPR, and privacy-conscious self-hosters currently have no way to opt out of the dependency. - **Air-gapped / offline self-hosted instances** currently render fallback system fonts; they now get the real ones. ## Font unification We were actually loading Inter from two places: the Google stylesheet, plus `@blocknote/core/fonts/inter.css` (8 weights, latin-only, woff+woff2) imported by the two rich-text editors — whichever loaded last won the cascade. Both are gone; `@fontsource` is now the single source: - Inter 400/500/600 (theme weights) + 700 (rich-text bold, previously only covered by the BlockNote copy) - DM Mono 400/500 (DM Mono has no 600 upstream; the old Google link requested one anyway) Fontsource ships the same `unicode-range` subsets as the Google CSS, so browsers still only download the subset they need (~60KB of woff2 for latin), and non-latin locales keep full coverage — which the latin-only BlockNote copy didn't provide. ## Notes - The BlockNote PDF export (`exportBlockNoteEditorToPdf.ts`) still fetches Inter TTFs from `fonts.gstatic.com` at export time — react-pdf needs TTF files, left unchanged here. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22506?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. --> |
||
|
|
ad31227f7c |
Fix BlockNote placeholder alignment + paddings (#22409)
## Before <img width="660" height="958" alt="image" src="https://github.com/user-attachments/assets/017e73c3-cd55-47e3-9470-3f6c31152663" /> ## After <img width="1347" height="995" alt="file-c55b9d7bc3bd212f18f8c77a0318eaef" src="https://github.com/user-attachments/assets/3f96af62-6509-4b58-a740-cd87856422cd" /> |
||
|
|
5a5c829129 |
fix(page-layout): render relation field widgets in table display mode (#22220)
Adding a to-many relation field as a **Table** on a record page rendered an empty widget (header only) in several cases. This fixes three independent defects behind that. - **Morph inverse relations crashed the table.** The host-scoping view filter (`IS current record`) is built on the relation's inverse field. When that inverse is a `MORPH_RELATION` (attachments, notes, tasks…), `getFilterTypeFromFieldType` fell through to `TEXT` and the GraphQL builder threw `Unknown operand IS for TEXT filter`, unmounting the table via the ErrorBoundary. `MORPH_RELATION` now classifies as `RELATION`, and the relation filter resolves the correct morph join column (e.g. `targetPersonId`) from the current record's object type. - **Stale `viewId` on field change.** Changing the bound field on a Table widget kept the previous relation's draft view (wrong object/fields/filter). Field selection now regenerates the draft view for the new relation, or clears the stale `viewId` when the new field can't back a table. - **Label identifier could be hidden or reordered.** Relation-table widget views now pin the label-identifier field first and visible on view creation and save. Deferred: morph relation filters with arbitrary selected record ids (not just "current record") — needs target-object identity in the filter value schema. **Test:** open a Person → edit layout → add a Field widget → bind a to-many relation → switch Layout to Table. Previously empty for `attachments` (morph) and for any field changed on an existing Table widget; now scoped to the host record. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22220?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
9f3ebaaf22 |
feat(messaging): sync draft emails and edit them in the thread composer (#22178)
Stop excluding drafts from sync across all three providers (Gmail DRAFT label, Microsoft/IMAP Drafts folder) and add an isDraft boolean field on Message so drafts are queryable by the API and AI agents. Drafts render in the thread with a Draft tag; clicking one opens the existing reply composer pre-filled with the draft's recipients, subject and body, and Send reuses the existing send-email flow. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22178?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
fb800e7a2d |
fix: improve native print output for dashboards and record tables (#22272)
## Summary - Adds print-specific snapshots for dashboards so native browser print/PDF captures charts and front components instead of the app shell. - Adds record-table print snapshots for object index pages so printable tables are generated from visible rows and native print avoids virtualized blank pages. - Preserves rendered chart layers by rasterizing canvas/SVG content for print. ## AI-generated disclosure This pull request was AI-generated by Hermes Agent on behalf of Vittorio Alfieri. The changes were reviewed and tested locally before submission. ## Screenshots ### Dashboard print **Before**  **After**  ### Table records print **Before**  **After**  ## Test plan - [x] `yarn nx typecheck twenty-front` - [x] `yarn nx build twenty-front` - [x] Generated dashboard PDFs from the preview build and rasterized pages to PNG for visual verification. - [x] Generated Tasks/table-record PDFs from the preview build and verified the final page contains table content instead of blank pages. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22272?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Félix Malfait <felix@twenty.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
fdf9f543ae |
Fix rounded page layout tab edit outline (#22214)
## Summary - Round the edited page layout tab outline to match the tab hover radius. ## Test plan - `npx oxfmt --check packages/twenty-front/src/modules/page-layout/components/PageLayoutTabListReorderableTab.tsx` - `npx oxlint --type-aware -c packages/twenty-front/.oxlintrc.json packages/twenty-front/src/modules/page-layout/components/PageLayoutTabListReorderableTab.tsx` - `npx nx run twenty-front:lint` - Browser: verified the edited `Notes` tab outline before/after locally. ## Visual <img width="1108" height="392" alt="clipboard" src="https://github.com/user-attachments/assets/1cb63180-83a9-4ef8-9c55-2b475eaaa02b" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22214?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
269c8ef400 |
feat(front): allow advanced relation fields in FieldWidget selector (#22005)
## After <img width="706" height="760" alt="image" src="https://github.com/user-attachments/assets/d118285f-baab-4187-988c-d0180d61a629" /> <img width="707" height="372" alt="image" src="https://github.com/user-attachments/assets/5676d829-2ec1-494e-a8f0-5998f1f0c3c8" /> ## Summary The FieldWidget field-selection dropdown currently filters out relation fields whose target is a system object, so users can't pick fields like `calendarEventParticipants` on the CalendarEvent record page. The widget itself can render them just fine as boxed relations — the restriction only lives in the picker. This unblocks the consistency story from #22003 (revert of #21857): once shipped, participants can be added to the calendar event record page via the existing FieldWidget mechanism instead of a bespoke side-panel page. ## Changes - `isFieldCellSupported`: adds an opt-in `includeSystemObjectRelations` option that skips the `isObjectMetadataAvailableForRelation` system check. - `useFieldListFieldMetadataItems`: forwards the option through to `isFieldCellSupported`. Default is `false`, so all existing callers keep current behavior. - `useFieldWidgetEligibleFields`: turns the option on, so the FieldWidget selector now surfaces fields like `calendarEventParticipants`, `messageParticipants`, etc. ## Test plan - [x] `nx typecheck twenty-front` - [x] `nx lint:diff-with-main twenty-front` - [ ] CI - [ ] Manually verify the FieldWidget dropdown now lists `calendarEventParticipants` on a CalendarEvent record page, and that selecting it renders a participants list via the existing relation card/field widget. https://claude.ai/code/session_01RnMcjL35wdCRzpXN257RLJ --- _Generated by [Claude Code](https://claude.ai/code/session_01RnMcjL35wdCRzpXN257RLJ)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22005?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. --> |
||
|
|
3943898642 |
Remove custom widget from calendarEvent page layout (#22046)
## Before <img width="608" height="759" alt="image" src="https://github.com/user-attachments/assets/e75f6cc9-aef8-4247-a88d-6992b3936d3d" /> ## After <img width="844" height="658" alt="image" src="https://github.com/user-attachments/assets/446a13f2-e40c-4e1c-a1ed-0920fe5065b3" /> remove https://github.com/twentyhq/twenty/pull/22016 custom widget and replace with regular field widget does not match figma design but avoid introducing specific behavior for calendarEvent <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22046?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
766d90af7e |
Remove framer-motion from twenty-ui (#22021)
## What Removes the `framer-motion` dependency from `twenty-ui` and replaces every usage with pure CSS animations, reaching for Base UI primitives where one fits: - **Collapse/expand** (`AnimatedEaseInOut`, `AnimatedExpandableContainer`): rebuilt on Base UI `Collapsible` (CSS-animated `--collapsible-panel-height/width` + transition states). Public props unchanged, so the ~28 call sites are untouched. - **ProgressBar**: rebuilt on Base UI `Progress` (proper `role`/`aria-valuenow`). The snackbar auto-dismiss countdown now uses a CSS keyframe + `animation-play-state` (pause on hover), removing a per-frame React re-render; `useProgressAnimation` is deleted. - The remaining `Animated*` components, the circular spinner, checkmark, and the placeholder pointer parallax move to plain CSS (SCSS modules + the `duration()` helper + theme tokens). - Deletes 3 unused components (`AnimatedTranslation`, `AnimatedTextWord`, `AnimatedFadeOut`). ## Why `twenty-ui` is a publicly published library with a size budget, so dropping framer-motion shrinks what consumers ship. `twenty-front` keeps its own framer-motion; that is out of scope here. ## Notes for reviewers - A few `twenty-ui` components received framer props from `twenty-front` call sites; those were migrated (e.g. `AnimatedLightIconButton` gained a CSS `rotate` prop, and the `EMPTY_PLACEHOLDER_TRANSITION_PROPS` spreads were removed). - Behavior change: Base UI `Collapsible` animates only on open/close transitions, so the old "animate in on first mount while already open" case no longer plays (the `initial` prop is kept for API compatibility). <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22021?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
242b989c0e |
fix(emails): stop reply composer infinite re-render (#21935)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21935?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
96a2987610 |
Fix/sanitize chart filters on save (#21958)
# Fix: sanitize chart filters referencing deactivated/deleted fields ## Summary When a field used in a chart (graph) widget filter was later **deactivated or deleted**, saving the page layout failed with a backend error such as: > Chart "...": One of the chart filters uses "...", but it was deleted. Please remove or replace this filter rule. This happened even after the user tried to remove the offending filter rule, because the invalid filter could still end up in the saved configuration. This PR makes invalid chart filters get cleaned up reliably — both as the user edits filters and, as a safety net, at save time. ## Root causes - **Edit-time persistence kept invalid filters.** `handleFiltersUpdate` persisted the current filter state to the page layout draft without sanitizing it against the object's active fields. An invalid filter (referencing a deactivated/deleted field) was re-saved on every update, blocking the configuration from being accepted. - **Save never enforced the cleanup.** `useSavePageLayout` serialized the draft as-is. The "filters referencing deactivated/deleted fields will be automatically removed on save" promise shown in the warning banner was only honored reactively (when the filter panel was actively edited), never at the actual save boundary. A chart whose filter panel wasn't touched kept its stale invalid filter in the payload. - **Query time treated inactive fields as valid.** `useGraphWidgetQueryCommon` considered all fields (including inactive ones) valid, so deactivated-field filters were never dropped when running the chart query. ## Changes ### Edit-time (keeps draft and UI in sync as you edit) - `ChartFiltersSettings` — sanitize filters in `handleFiltersUpdate` before writing to the draft, dropping any filter whose `fieldMetadataId` is not in the active-fields set. - `dropChartRecordFiltersWithDeletedFields` — enhanced to also clean up filter groups left orphaned once invalid filters are removed (iteratively removing empty groups and re-parenting checks). - `useGraphWidgetQueryCommon` — restrict valid field IDs to `isActive` fields so deactivated-field filters are silently dropped at query execution. - `ChartFiltersDeletedFieldsWarning` — updated copy to mention both deactivated and deleted fields. ### Save-time safety net (guarantees no invalid filter is ever persisted) - New `sanitizeChartFiltersInPageLayoutDraft` util — walks every chart widget in the draft and drops record filters (and now-orphaned groups) whose `fieldMetadataId` is not in the widget object's set of active fields. It leaves non-chart widgets untouched and leaves filters intact when the object metadata can't be resolved (avoids wiping valid filters during metadata loading). - `useSavePageLayout` — builds a `Map<objectMetadataId, Set<activeFieldId>>` from `useObjectMetadataItems()` and sanitizes the draft before converting it to the update input. This layer only ever removes filters whose field is genuinely deactivated/deleted — the exact set the backend rejects — and never removes filters pointing at valid fields. ## Tests - `dropChartRecordFiltersWithDeletedFields.test.ts` — extended coverage for orphaned filter-group cleanup. - `sanitizeChartFiltersInPageLayoutDraft.test.ts` — new: drops deactivated/deleted-field filters on save, keeps valid filters, cleans up orphaned groups, leaves non-chart widgets alone, and leaves filters untouched when object metadata is unresolved. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21958?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
6ce7d04f9d |
Fix field widget textarea focus reset (#21959)
Short description: Keeps the field widget textarea on a local draft value while focused so record-store rewrites do not reset the active caret. # Before Typing in an editor-mode text field can lose caret position when the global record store is rewritten by an external record update/refetch. https://github.com/user-attachments/assets/1ee7a819-2c27-4d09-aae5-814c2cf27181 # After The focused textarea should preserve the in-progress draft and caret while still updating sibling previews optimistically and flushing the final value on blur. https://github.com/user-attachments/assets/41832493-021f-46e8-bc75-722bbb1cd7b7 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21959?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
b1ada79d72 |
fix(front): scope relation table widget via currentRecordId (#21965)
## Context Follow-up to #21293 (merged). That PR added a bespoke `relationTableFilter` to keep a relation field rendered as a record-table widget scoped to the host record. It turns out to be redundant. ## Why it's redundant The relation table widget's view already carries an `isCurrentRecordSelected` relation filter on the inverse field — it's baked in by `useAddDraftViewForFieldRelationTableWidget` when the widget is configured. That filter is resolved through the `currentRecordId` that `FieldWidgetRelationTable` provides via `RecordFilterValueDependenciesContext`, and `turnRecordFilterIntoGqlOperationFilter` turns it into exactly `{ `${inverseField}Id`: { in: [recordId] } }`. So the hand-built `relationTableFilter` duplicated a filter the existing mechanism already produces from `currentRecordId`. ## Changes - Remove `relationTableFilter` from `RecordFilterValueDependenciesContext` - Stop reading/applying it in `useFindManyRecordIndexTableParams` and `useAggregateRecordsForRecordTableColumnFooter` - Delete the `getRelationTableFilter` util and its test - `FieldWidgetRelationTable` provides `currentRecordId` only Net −263 lines; relies on the existing `isCurrentRecordSelected` + `currentRecordId` scoping path. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21965?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
068a8d4efe |
fix(front): keep relation field record tables scoped to the host record (#21293)
## Problem
When a relation field is added to a record page as a record **table**
widget
(Page Layouts → a `FIELD` widget with `fieldDisplayMode: TABLE` and a
`viewId`),
the table renders the **global** list of the related object instead of
only the
records related to the current record.
Steps to reproduce:
1. On a Company record page layout, add a to-many relation field (e.g.
`Opportunities`) as a widget and set its display mode to **Table** with
a view
(so it shows columns).
2. Open a Company record.
3. The Opportunities table lists *all* opportunities in the workspace,
not just
the ones linked to that company.
Note: when the same relation widget has **no** `viewId`, it is correctly
scoped
to the record — but then it can't render custom columns. So custom
columns and
relation-scoping were effectively mutually exclusive.
## Root cause
`FieldWidgetRelationTable` renders the related records through
`RecordTableWidgetRendererContent` using the widget's `viewId`. That
path loads
the view's filters and fetches the related object's records, but **never
applies
the relation filter** that constrains the table to the host record. With
a
`viewId` present, the table therefore shows the whole object.
The relation filter itself already exists elsewhere —
`RecordDetailRelationSection` builds
``{ `${inverseRelationFieldName}Id`: { in: [recordId] } }`` for its
aggregate.
It just isn't applied on the table path.
## Fix
- Add a pure helper `getRelationTableFilter()` that builds the
host-relation
filter for a to-many relation field (morph-aware, mirroring
`RecordDetailRelationSection`).
- `FieldWidgetRelationTable` computes this filter and passes it down via
the
existing `RecordFilterValueDependenciesContext` (new optional
`relationTableFilter`).
- `useFindManyRecordIndexTableParams` (rows) and
`useAggregateRecordsForRecordTableColumnFooter` (footer aggregates) AND
this
filter into their queries.
The filter is scoped to the relation-table instance through the context
and
defaults to `undefined`, so **every other table (record index, kanban,
dashboards, …) is unaffected** — `combineFilters` / object spread treat
the
absent filter as a no-op. No backend changes.
## Tests
- New unit tests for `getRelationTableFilter` (to-many → foreign-key
filter;
to-one → none; unresolved relation type / field → none; morph relation;
missing morph target names → none).
- `nx typecheck twenty-front`, `nx lint twenty-front`, and the new
`nx test twenty-front` suite pass locally.
## Screenshots
Same record (a "Centre" with 0 related theory allocations and 34 related
orders), same page-layout (relation fields shown as Table widgets with a
view).
**Before** — with a `viewId`, the relation tables show the *global*
lists: the
Theory Allocations table is full of allocations belonging to *other*
records,
and Collateral Orders shows 60 (the whole object's first page) instead
of 34.
<!-- drag the BEFORE screenshot here -->
**After** — the same tables are scoped to the record: Theory Allocations
is
empty (this record has none) and Collateral Orders shows exactly its 34
orders,
with the view's columns (Status / Total Value / Date).
<!-- drag the AFTER screenshot here -->
## Verification
Verified on a self-hosted instance running the equivalent change (the
four
touched files are byte-identical on `main` and the latest release tag):
a
relation table widget with a `viewId` now shows only the host record's
related
rows **with** the view's columns, the footer aggregates match the
visible rows,
and the global record index is unchanged. Confirmed across records with
different related-record counts (e.g. a record with 34 related orders
shows 34;
a record with 1 shows 1; records with 0 show an empty table).
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
|
||
|
|
eeca9cd42e |
fix(front): isolate record table dashboard widget filters on duplicate (#21936)
closes https://discord.com/channels/1130383047699738754/1518291134382608394 https://github.com/user-attachments/assets/931e4e88-44e8-4634-a7f9-e0564bd80fff <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21936?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. --> |
||
|
|
6237598a30 |
Fix meeting bot CalendarEvent field visibility and editability (#21883)
- Add the meeting bot preference field to the CalendarEvent record page fields view. - Use a Standard-app ownership gate for record field read-only logic. - Allow app-owned and workspace-custom fields on system objects to follow isUIEditable and permissions. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21883?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. --> |
||
|
|
b8ea742a88 |
fix(front): respect user number format for counts and aggregates (#21894)
## Problem
Several user-facing numbers were rendered raw (e.g. `153909`) instead of
honoring the workspace member's **Number format** preference (e.g. `153
909` with `Spaces and comma`). The formatting utilities already existed
(`formatNumber` / `useNumberFormat`) but were not applied on these
surfaces.
## Root cause
`transformAggregateRawValueIntoAggregateDisplayValue` — the shared
helper behind every table/board/chart aggregate — returned the `COUNT`
branch as a raw string and never threaded the user's locale format into
`formatNumber` for the other branches (so they silently fell back to
`COMMAS_AND_DOT`).
Its existing `numberFormat` param actually held the chart `SHORT`/`FULL`
abbreviation setting, so it is renamed to `chartNumberFormat`, and a new
`numberFormat: NumberFormat` now carries the locale separators.
## Surfaces fixed
- Record table footer aggregates, including the raw **"Count all"**
total
- Record board column / group-section aggregates
- Aggregate chart and pie-chart center metric (including their raw
`COUNT` early-returns)
- View picker `<view> · <count>` total
- Record show breadcrumb pagination `(x/y)`
- Record index header and side panel `N selected` counts
The board-column header needs no change — it now receives an
already-formatted string from the transform.
## Out of scope (intentionally left raw)
The editable `SettingsCounter` input (formatting would break parsing),
the advanced-filter pill, the `+N` overflow badge, and the AI routing
debug display.
## Testing
- New + existing unit tests pass
(`transformAggregateRawValueIntoAggregateDisplayValue`, `formatNumber`,
`useNumberFormat`), with added locale-aware coverage (`SPACES_AND_COMMA`
→ `153 909`, `DOTS_AND_COMMA` → `153.909`).
- `nx typecheck twenty-front`, oxlint and oxfmt on the diff all pass.
> Note: two i18n strings change placeholder shape (`{count} selected` →
`{0} selected`); a `lingui:extract` will refresh the catalogs (runtime
falls back to source text meanwhile).
https://claude.ai/code/session_013XNL2Xa11Bw7fsnPFQgsGX
---
_Generated by [Claude
Code](https://claude.ai/code/session_013XNL2Xa11Bw7fsnPFQgsGX)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21894?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. -->
|
||
|
|
973b35989e |
Add standard record page layout for calendar events (#21857)
Moves calendar event details from the bespoke side-panel page to the standard record page layout system. - Adds standard calendar event record page metadata, fields view, widgets, tests, snapshots, and upgrade command for existing workspaces. - Opens calendar events through the generic ViewRecord side-panel path. - Adds participants and call recordings as standard field widgets. - Removes the old custom calendar event side-panel page and related side-panel enum/config entry. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21857?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. --> https://github.com/user-attachments/assets/c1f88cac-1615-478c-a3dd-87d0c61ab9a8 <img width="3024" height="1658" alt="CleanShot 2026-06-19 at 19 01 27@2x" src="https://github.com/user-attachments/assets/c3df5705-ff08-446e-ac3c-6ccb11cf21ec" /> <img width="3024" height="1658" alt="CleanShot 2026-06-19 at 19 01 19@2x" src="https://github.com/user-attachments/assets/633525db-310c-4462-8458-a72068cc1432" /> |
||
|
|
9f30915f6f |
fix(metadata): remove deprecated isCustom from Objects and Fields (#21799)
## Context Follow-up to #21228, which deprecated `isCustom` on object/field metadata but kept it exposed because the frontend still relied on it. This removes it from the GraphQL API and the frontend entirely. ## Implementation ### Server - Remove `isCustom` `@Field` from the `Object`, `Field`, and `MinimalObjectMetadata` GraphQL types - Remove the `isCustom` `@ResolveField` resolvers and the `isCustomLoader` dataloader (+ payload/interface) - Remove `isCustom` as an internal `@HideField()` on the Object/Field DTOs used by the i18n standard-override gate > Use an explicit isStandard instead (which is the correct gating) ### Frontend - Add `getIsMetadataItemCustom` helper + `useGetIsMetadataItemCustom` hook: an item is custom when `applicationId === currentWorkspace.workspaceCustomApplication.id` - Migrate all consumers off `objectMetadataItem.isCustom` / `fieldMetadataItem.isCustom`; `isRecordFieldReadOnly` now takes a precomputed `isFieldCustom` - Drop `isCustom` from the metadata fragment/mutations/minimal query, FE types, zod schemas, and mock generators; regenerate GraphQL types ## Notes - Breaking change on the (already-deprecated) `Object.isCustom` / `Field.isCustom` GraphQL fields and the `isCustom` filter - FE semantic is "belongs to the workspace custom app" (third-party-app objects/fields are treated as non-custom) - `isCustom` on IndexMetadata / View / Skill / Agent is a separate column and is untouched - Breaking changes on REST metadata API |
||
|
|
8034c7725f |
Reorganize twenty-ui into best-practice component domains and per-component folders (#21745)
Reorganizes `twenty-ui`'s component organization to follow how the best
UI libraries (MUI, Mantine, Base UI, Polaris) structure their source,
now that the package has stabilized.
**Taxonomy** — dissolves the meaningless `components/` junk-drawer and
the 107-file `display/` mega-category. New domains/subpaths:
`data-display`, `typography`, `icon`, `surfaces`; `feedback` and
`layout` absorb the rest (banners/callout/info + placeholders →
feedback; modal/card → surfaces; motion + separators → layout).
**Per-component layout** — every component is now
`<domain>/<ComponentName>/<ComponentName>.tsx` with colocated
styles/stories/types, `internal/` for private helpers and `parts/` for
re-exported compound sub-parts. The redundant inner `/components/` is
gone. `icon` and `json-visualizer` are kept as cohesive subsystems.
**Also:** adds a tree-shakeable root barrel (`import { Button } from
'twenty-ui'`), the generator now owns `individual-entry.ts`, and a real
barrel-leak bug is fixed (private `internals/` parts were leaking into
the public API).
Consumer imports (~1.2k files) and the `twenty-sdk` UI aggregator were
updated by codemod. The change is **export-neutral** except 16
intentionally-removed private internals symbols (all verified
unconsumed). Gates green: typecheck, lint, build, size-limit, storybook.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21745?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. -->
|
||
|
|
177afde866 |
[BREAKING CHANGE] fix chart cache collisions with key-based data plumbing (#21743)
closes https://discord.com/channels/1130383047699738754/1514946035317997709 This fixes Apollo cache collisions for pie slices and line series by keeping chart bucket identity as key end-to-end, matching how bar chart already works. What changed -- - Renamed pie/line chart response identity from id to key in the chart data path. - Kept key through frontend chart hooks, types, stories, and tooltip/drilldown logic. - Only adapt key to id at actual external boundaries like Nivo and GraphWidgetLegend. - Added/updated tests covering cache normalization and chart data behavior. before - <img width="2600" height="844" alt="CleanShot 2026-06-17 at 20 17 14@2x" src="https://github.com/user-attachments/assets/b9ee83e9-db4b-423e-8668-a7beb4c4c62e" /> after - <img width="2614" height="800" alt="CleanShot 2026-06-17 at 20 16 17@2x" src="https://github.com/user-attachments/assets/674a5417-ffc2-441d-9484-e1126438254c" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21743?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. --> |
||
|
|
102c530d0f |
Add limit on view widget (#21718)
<img width="1345" height="463" alt="image" src="https://github.com/user-attachments/assets/a5d9ac2f-6375-4956-895d-3675aa9bebc1" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21718?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> |
||
|
|
9c9c34fccf |
Remove twenty-ui-deprecated and migrate frontend to twenty-ui (#21596)
Migrates `twenty-front`, `twenty-sdk`, and `twenty-front-component-renderer` from `twenty-ui-deprecated` to `twenty-ui` (mechanical import swap — the packages have API parity) and deletes the deprecated package along with its workspace/CI/config wiring. Also adds `@linaria/react`/`@linaria/core` as direct deps of `twenty-front` (it used them transitively via the deprecated package). Note: move the required status check from `ci-ui-status-check` to `ci-new-ui-status-check`. Argos: the Storybook box-model/button-reset baseline shift (the bulk of the visual diffs) is isolated in #21665 — Storybook now loads twenty-ui's global `reset.scss`, which the production app already ships. Once #21665 merges and this branch is rebased, the remaining Argos diffs are component-level visual-parity items only. |
||
|
|
ecc7b38b75 |
Remove randomness from flaky twenty-front Storybook stories (#21594)
Several twenty-front Storybook stories were flagged flaky by Argos because they render different pixels across runs. This removes the non-determinism behind them. **What changed** - **Images** — replaced random `picsum.photos` URLs in the Logo and TabList stories with the existing `AVATAR_URL_MOCK`, and added global MSW handlers in `.storybook/preview.tsx` that serve a deterministic image for every remote host (picsum, twenty-icons.com, twentyhq.github.io, etc.) so no story depends on a network image load. - **Numbers** — the line-chart story built its data with `Math.random()`; now uses a deterministic formula. - **Dates** — the terminal "long output" story stamped its lines with `new Date()`; now uses a fixed base timestamp. The calendar-channel date/time format previews and example event used render-time `Date.now()`/`new Date()` in shared components; they now use a fixed reference date (`DateTimeSettingsPreviewDate`). - **Lazy-load timing** — the date-picker story now waits for the lazily-loaded calendar before the snapshot. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21594?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. --> |
||
|
|
c848ac34fd |
Fix default view widget visibility (#21590)
Tim logged on the left, Phil on the right, Tim created view widget ## Before <img width="1512" height="938" alt="image" src="https://github.com/user-attachments/assets/7f993f41-a244-42db-b56a-b17c15fb3409" /> ## After Fix Tim created a second view widget, Phil can see it <img width="1512" height="982" alt="image" src="https://github.com/user-attachments/assets/3908a70d-538b-4adf-95df-7373a2f6e269" /> ## After slow migration Phil can see first widget <img width="1512" height="982" alt="image" src="https://github.com/user-attachments/assets/6b30429e-1acf-4119-ba32-26db3155975e" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21590?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Weiko <corentin@twenty.com> |
||
|
|
f45c54679c |
[Fix] : fix: Allow label identifier system fields in view creation and fix resulting duplicate header columns (#19009)
fixes #18994 After : <img width="767" height="230" alt="Screenshot 2026-03-26 at 7 11 04 PM" src="https://github.com/user-attachments/assets/73de0154-7da1-48fa-92fc-d51a3ef5b06e" /> Before : <img width="905" height="314" alt="Screenshot 2026-03-26 at 6 56 38 PM" src="https://github.com/user-attachments/assets/89b1249b-8889-4034-a6c8-d41330154c1a" /> --------- Co-authored-by: Arun kumar <arunkumar@Aruns-MacBook-Air.local> Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
7c0136b97b |
feat(deps): migrate frontend to React 19 (#21531)
## What Migrates the frontend stack from **React 18.3 → 19.2**. The website, sdk, companion and emails packages were already on React 19; this brings the remaining holdouts (`twenty-front`, `twenty-ui`, `twenty-ui-deprecated`, `twenty-front-component-renderer`) and `twenty-server`'s email rendering onto 19, and pins a single React version repo-wide. ## Why React 18.x is now the legacy line. Staying current keeps us on the patched/maintained branch and unblocks downstream library majors (react-router 7, mantine 9, etc.) that require React 19 peers. ## Dependency bumps (required by React 19 peers / removed APIs) | Package | From | To | Reason | |---|---|---|---| | react / react-dom | 18.3.1 | 19.2.3 | core | | @hello-pangea/dnd | 16 | 18 | peer `^18 \|\| ^19` | | react-datepicker | 6 | 9 | v<7 used removed `findDOMNode`; drops `@types/react-datepicker` | | react-data-grid | beta.13 | beta.59 | peer `^19.2`; new render API | | graphiql (+ @graphiql/react, plugin-explorer) | 3 / 0.23 / 1 | 5 / 0.37 / 5.1 | peer `^18 \|\| ^19` | | react-helmet-async | 1.3 | **@dr.pogodin/react-helmet** 3.2 | upstream caps peer at `^18`; drop-in React 19 fork | A `resolutions` pin enforces a single React (19.2.3) + `@types/react` (19.2.14) across the monorepo to avoid duplicate copies / type-identity splits. Versions are the aged lockfile patches (clears the `npmMinimalAgeGate`). ## Code changes - **Global `JSX` shim** (`react-jsx-global.d.ts` per package): React 19 moved the `JSX` namespace under `React.JSX`; several deps' published types (notably `@linaria/react`'s `styled.d.ts`, which types every `styled.x` via `keyof JSX.IntrinsicElements`) still reference the global namespace. Without the shim, every styled component degrades to `any` props. - **Ref nullability**: `useRef<T>(null)` now returns `RefObject<T | null>`; widened consumer prop/hook ref types accordingly (incl. the shared `useListenClickOutside`). - **react-datepicker v9**: `onChange`/`onSelect` accept `Date | null`, `calendarStartDay` typing, `ReactDatePickerProps`→`DatePickerProps`, relaxed the dynamic `selectsMultiple` discriminated union. - **react-data-grid beta.59**: `formatter`→`renderCell`, `editor`→`renderEditCell`, `headerRenderer`→`renderHeaderCell`, `components`→`renderers`, `onRowClick`→`onCellClick`, object-shaped `useRowSelection`, Set-based selection. - **dnd style cast**: `@radix-ui/react-popper` augments `CSSProperties` with a `--radix-*` index signature that dnd's closed `DraggingStyle` doesn't satisfy → cast at the spread. ## Status / testing - ✅ `typecheck` green: twenty-front, twenty-ui, twenty-ui-deprecated, twenty-front-component-renderer, twenty-server - ⏳ build / lint / unit tests / storybook+argos / runtime smoke-test in progress Draft until local + CI verification completes. Notable behavior to QA manually: spreadsheet import (data-grid), date pickers, drag-and-drop boards/lists, GraphQL playground, page titles/favicon. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21531?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. --> |
||
|
|
fefb6cdb94 |
feat(page-layout): add number format option to aggregate chart widget (#21521)
## Context Closes #21522 Large values in the dashboard **Number** widget are always abbreviated (e.g. `1300090` → `1.3m`) with no way to display the full number. Following a discussion with the core team who were interested in this feature (https://discordapp.com/channels/1130383047699738754/1509604545381142649) , this adds a **Format** option in the **Style** section of the Number (aggregate chart) widget, letting users choose between **Short** (abbreviated, current behavior) and **Full** (complete number with thousand separators). Only the displayed value of the Number widget is affected — axes, labels and tooltips of other chart types are intentionally left untouched. ## What's inside **Server** - New `ChartNumberFormat` GraphQL enum (`SHORT` / `FULL`), following the `AxisNameDisplay` pattern - The existing — and previously unused — `format` field on `AggregateChartConfigurationDTO` is now typed with this enum and validated with `@IsEnum` - The dashboard AI tool schema (`widget.schema.ts`) accepts the new `format` option - Regenerated GraphQL types and the `twenty-client-sdk` metadata client to reflect the enum **Front** - New **Format** setting in the Style section of the Number widget settings, with a Short/Full selection dropdown (same pattern as the Axis name setting) - `transformAggregateRawValueIntoAggregateDisplayValue` takes an optional `numberFormat`: - `FULL` → full number via `formatNumber` (currency values keep up to 2 decimals) - `SHORT` → abbreviated via `formatToShortNumber` - not set → behavior unchanged (currency short, number full), so existing widgets and the record table/board footers render exactly as before ## Screenshots | Full UI Look | <img width="1917" height="955" alt="Twenty_Showcas_FullShort" src="https://github.com/user-attachments/assets/05d05779-395d-4e1a-8ff0-964f6fbef182" /> | Menu UI Look | <img width="291" height="308" alt="Screenshot_2" src="https://github.com/user-attachments/assets/82b5a1de-32fe-46ec-a9b8-add11ab4c6cd" /> ## Tests - Extended `transformAggregateRawValueIntoAggregateDisplayValue` unit tests with SHORT/FULL cases for currency and number fields - Updated the page-layout-widget creation/update integration tests and snapshots to use `ChartNumberFormat.SHORT` <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21521?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
1efa3567ef |
Rename isUIReadOnly to isUIEditable, add isUICreatable, expose both to app developers (#21504)
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
# UI capability flags: `isUIEditable` + `isUICreatable`
## Per-verb capability model
This PR replaces the negative `isUIReadOnly` metadata flag with
positive, per-verb capability flags (à la Salesforce
`createable`/`updateable`):
- **`isUIEditable: boolean`, default `true`** — rename of `isUIReadOnly`
with inverted polarity, on **both** `objectMetadata` and
`fieldMetadata`. It is one concept ("can the user edit this through the
generic UI?") at two altitudes, so it carries one name at both levels.
- **`isUICreatable: boolean`, default `true`** — new, **object-level
only** (fields have no create verb). When `false`, no generic UI
affordance to create a record of this object appears anywhere (table "+"
buttons, board column add, calendar add, relation-section "Add new",
record picker "Add new", command-menu create action and its keyboard
shortcut).
Both flags are **UI-affordance flags only**: the server does not block
create/edit mutations based on them, so the system, API, and workflows
continue to mutate these records freely. They are orthogonal statements
about the object's nature with no implication rule in the data model.
Because today's inline creation UX creates a blank record the user must
then edit, the frontend create predicate currently requires both
`isUICreatable` and effective editability.
There is no CREATE permission in `ObjectPermissions`; the frontend keeps
gating creation on `canUpdateObjectRecords` as a proxy, ANDed with the
new flags.
## Unified create predicate
All generic creation entry points now flow through one predicate,
`canCreateRecordsForObjectMetadataItem` (`isUICreatable` && not
`isSystem` && not effectively read-only, where effective read-only
covers `isUIEditable`, `isRemote`, and the `canUpdateObjectRecords`
proxy via `isObjectMetadataReadOnly`). This deletes the previously
hardcoded suppression lists:
- `isRecordTableCreateDisabled.ts` and its hardcoded
`WorkflowRun`/`WorkflowVersion` list — deleted; those objects (plus
`workspaceMember`) now declare `isUICreatable: false` in the standard
application instead.
- The hardcoded `workspaceMember` guard inside
`useAddNewRecordAndOpenSidePanel.ts` — deleted.
- The `CREATE_NEW_RECORD` command menu item's availability expression
now checks `objectMetadataItem.isUICreatable`, `isUIEditable`,
`isSystem`, and `isRemote`; a workspace upgrade command re-syncs the
expression in existing workspaces.
Component-local conditions (soft-delete filter active, layout
customization mode) stay in their components.
## GraphQL compatibility and removal plan
The schema delta versus main is **purely additive plus deprecations —
zero breaking changes**:
- `isUIReadOnly` remains on both the ObjectMetadata and FieldMetadata
GraphQL output types for **one release** as a deprecated field computed
as `!isUIEditable` (`deprecationReason: 'Use isUIEditable'`). The Twenty
frontend no longer queries it.
- `isUIReadOnly` also remains on the **input side** for one release
(`CreateFieldInput`, `UpdateFieldInput`, `FieldFilter`, `ObjectFilter`),
keeping the schema shape identical to main for those members. On create
it acts as a legacy alias mapped to `!isUIReadOnly` (`isUIEditable` wins
when both are provided); on update it is ignored, exactly as on main (it
was never an editable property). Filtering on the deprecated member
keeps working until the column is dropped at upgrade time; after that it
is a deprecated no-op surface kept only for schema compatibility.
**Removal plan for next release: drop `isUIReadOnly` from the output
DTOs (and resolvers' `@ResolveField`s), from the input/filter types,
from the create-input mapping, and the `@WasRemovedInUpgrade`-retained
entity columns and decorators.**
## ⚠️ Webhook / database-event payload shape change
The `database-event-payload` type in `twenty-shared` got a clean rename
(no alias): metadata snapshots in webhook and database-event payloads
now carry `isUIEditable` (and `isUICreatable` at object level) **instead
of** `isUIReadOnly`, with inverted polarity. Consumers of these payloads
that read `isUIReadOnly` must switch to `isUIEditable`.
## New manifest properties (app-developer DX)
Application developers can now set these flags in their app manifests
(purely additive — existing manifests and older `twenty-sdk` versions
are unaffected, defaults apply when omitted):
- `objects[].isUICreatable?: boolean` (default `true`)
- `objects[].isUIEditable?: boolean` (default `true`)
- `fields[].isUIEditable?: boolean` (default `true`)
The manifest converters previously hardcoded `isUIReadOnly: false`; they
now read the manifest values with `?? true` defaults. The types are
re-exported through `twenty-sdk` from `twenty-shared`.
## Migration & backfill
- One fast instance command: adds `isUIEditable` (NOT NULL default
`true`) on `core."objectMetadata"` and `core."fieldMetadata"`, backfills
`isUIEditable = false` exactly where `isUIReadOnly = true`, drops
`isUIReadOnly`, and adds `isUICreatable` (default `true`) on
`objectMetadata`. The `down` is the exact inverse. Uses `ADD/DROP COLUMN
IF (NOT) EXISTS`, matching the 2-12 drop-`isCustom` precedent. Verified
up and down in separate transactions against a dev database with exact
backfill counts.
- **Cross-version upgrade safety (multi-version self-hosted jumps):**
the upgrade sequence interleaves per version (instance → workspace
commands), so pre-2.13 workspace commands run **before** the 2.13 rename
when an old instance jumps several versions. Following the `isCustom`
precedent: `isUIEditable`/`isUICreatable` are marked
`@WasIntroducedInUpgrade` and `isUIReadOnly` stays on both entities as
`@WasRemovedInUpgrade`, so the upgrade-aware entity metadata adapter
hides the not-yet-existing columns (and keeps the legacy column live) at
pre-2.13 cursors. **No committed upgrade command outside the 2-13
directory is modified**: the old 1-21/2-8/2-9 commands keep their
original `isUIReadOnly: true` inputs, which still compile (entity
property retained, deprecated create-input alias mapped) and still
produce the correct legacy column writes pre-rename.
- A 2-13 workspace command (`sync-standard-ui-capability-flags`)
re-syncs `isUICreatable` **and** `isUIEditable` on standard objects and
`isUIEditable` on standard fields from the standard-application
definitions. This backfills `isUICreatable: false` on
`workflowRun`/`workflowVersion`/`workspaceMember` and heals fields
created mid-cross-upgrade by pre-2.13 commands (whose hidden
`isUIEditable` value cannot reach the insert). Both 2-13 sync commands
pass `isSystemBuild: true` — the flat metadata validator otherwise
rejects direct updates to system objects (verified against a
deliberately drifted dev database; the run is idempotent).
- A second 2-13 workspace command re-syncs the create-record command
availability expression.
## Testing
- Unit tests for `canCreateRecordsForObjectMetadataItem`
(flag/permission/system combinations) and for the manifest converters
(flags set / omitted → defaults).
- Full `upgrade --dry-run` boots the sequence (107 steps) and validates
the upgrade-aware decorator references; both 2-13 sync commands verified
end to end against real drift and re-run idempotently.
- Schema verified by live introspection after the input-alias restore:
all four input/filter members match main, output deprecations intact;
frontend metadata types and `twenty-client-sdk` schema regenerated from
the running server.
- Read-only-related and touched jest suites pass on both packages;
typecheck and lint pass on `twenty-server` and `twenty-front`.
<!-- CURSOR_AGENT_PR_BODY_END -->
<div><a
href="https://cursor.com/agents/bc-0f3e04cb-b04a-40be-8330-5609c4538e8a"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a
href="https://cursor.com/background-agent?bcId=bc-0f3e04cb-b04a-40be-8330-5609c4538e8a"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a> </div>
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21504?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: Cursor Agent <cursoragent@cursor.com>
|
||
|
|
cfb9772179 |
feat(server): convert view to overridable entity (#21436)
## Context Every entity created as a side effect of object creation must support the overridable pattern (`isActive` + `overrides` + override routing) before we can re-own side effects to their true application. Starting with View. viewField, viewFieldGroup, pageLayoutTab and pageLayoutWidget already extend `OverridableEntity`. This PR brings `view` to the same pattern. ## What this does - `ViewEntity` now extends `OverridableEntity<ViewOverrides>` (adds `isActive` boolean + `overrides` jsonb). All editable view properties are overridable; the 3 fieldMetadata foreign keys are converted to/from universal identifiers like viewField's `viewFieldGroupId`. - **Update**: mutations on a view not owned by the caller (e.g. standard views like "All Companies") are written into `overrides` instead of mutating the row. Reads merge overrides in the DTO. - **Delete/destroy**: views not owned by the caller are deactivated (`isActive = false`) instead of deleted. ~~- **INDEX invariant**: `key = INDEX` views can only be created via object-creation side effect. The API now rejects creating, deleting or destroying INDEX views (object-deletion cascade is unaffected). This was not really needed for this migration but was flagged during implementation.~~ - **Front**: views with `isActive = false` are filtered out of the views selector. - Fast instance command adds the two columns (`2-12-instance-command-fast-...-view-overridable-entity.ts`). ## Notes - Custom (caller-owned) views behave exactly as before: direct updates, soft delete. - View-group side effects (kanban groups) are computed on the override-merged view so overridden `mainGroupByFieldMetadataId` works. |
||
|
|
61b76b681e |
fix: i18n missing hardcoded strings in settings (#21424)
## What Two user-visible strings in the Settings area were never wrapped with Lingui, so they were excluded from i18n extraction and shipped untranslated regardless of the selected language: - **"Remote"** — the type chip shown for remote objects in **Settings → Data Model** (`SettingsItemTypeTag`) - **"Done"** — the confirm button of the fields configuration group rename input (`FieldsConfigurationGroupRenameInput`) ## Changes - Wrap the `Chip` `label` with the `t` macro in `SettingsItemTypeTag.tsx` (the `placeholder` / `placeholderColorSeed` props are intentionally left as-is — they drive the avatar initial and color hash, not display text). - Wrap the `Button` `title` with the existing `t` from `useLingui()` in `FieldsConfigurationGroupRenameInput.tsx`. - Add the corresponding source entries to `en.po` so Crowdin can propagate the translations to all supported locales. Both follow i18n patterns already used throughout the codebase — these two were simply missed. ## Screenshots Both components rendered via Storybook (source `en` locale) after the change — the strings now resolve through Lingui's `t` macro without breaking rendering:  ## How to test 1. Switch the workspace language to a non-English locale. 2. Go to **Settings → Data Model** with a remote object present → the type chip reads "Remote" translated. 3. Rename a fields configuration group → the confirm button reads "Done" translated. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |