**Stacked on #20527**
https://github.com/user-attachments/assets/48995655-401a-4c35-8094-e88da8408bdd
## Summary
Surfaces the one-hop relation traversal added in #20527 through the
existing **composite sub-field dropdown pattern**. Clicking a
MANY_TO_ONE relation field in the "+ Filter" picker now opens the same
second-level dropdown that composite fields (FULL_NAME, ADDRESS,
CURRENCY, etc.) already use — populated with the target object's
filterable fields. Picking one (e.g. `Company → Name`) builds a filter
that serializes to the nested GraphQL filter the backend now accepts: `{
company: { name: { ilike: "%X%" } } }`.
No new components. The whole feature reuses
`AdvancedFilterSubFieldSelectMenu` + the existing
`subFieldNameUsedInDropdownComponentState` + the existing `MenuItem
hasSubMenu` indicator. Only the conditions that gate the sub-menu (and
the sub-menu's content for relations) were broadened.
## What landed
| File | Change |
|---|---|
| `ObjectFilterDropdownFilterSelectMenuItem` | Sub-menu chevron now
shows on MANY_TO_ONE relations (`isManyToOneRelationField` util). |
| `AdvancedFilterFieldSelectMenu` | Relation clicks open the sub-menu
alongside composite clicks. |
| `AdvancedFilterSubFieldSelectMenu` | New branch: when the sub-menu
type is `'RELATION'`, render the target object's filterable fields via
`useFilterableFieldMetadataItems(targetObjectMetadataId)`. Composite
logic untouched. |
| `objectFilterDropdownSubMenuFieldType` state | Widened to accept a
`'RELATION'` sentinel. Role-permissions sub-field menu narrows it back
out (it doesn't traverse relations). |
| `useSelectFieldUsedInAdvancedFilterDropdown` | New optional
`targetFieldMetadataItem` arg. When present, the stored RecordFilter's
`type` is the target field's type so the operand picker and value input
render the target's operands (`'TEXT'` operators when filtering
`company.name`, etc.). |
| `turnRecordFilterIntoGqlOperationFilter` (shared) | When the filter
targets a `RELATION` field with a `subFieldName`, synthesize a
field-metadata for the target, recurse to build the inner filter, then
wrap it under the relation field's name → `{ relationName: {
targetFieldName: { ...operator } } }`. |
`RecordFilter.subFieldName` stays narrowly typed as
`CompositeFieldSubFieldName` so the wide downstream consumers
(`shouldShowFilterTextInput`, composite handlers in the serializer,
etc.) don't change. The relation target field's name is stored through a
narrowly-scoped cast at the dropdown's storage point — the serializer
checks `filter.type === 'RELATION'` before interpreting it as a target
field name, so the cast can't be mis-read by composite-only code paths.
## Test plan
- [ ] Open a table view on People, click "+ Filter", click "Company" →
sub-menu opens with Company's filterable fields
- [ ] Pick "Name" → operand picker shows TEXT operators (Contains,
Equals, …)
- [ ] Type "Airbnb" → filter applies, table shows people whose company
name contains "Airbnb"
- [ ] Verify network tab: the GraphQL filter variable is `{ company: {
name: { ilike: "%Airbnb%" } } }`
- [ ] Same flow with a composite target field (e.g. `Company →
annualRecurringRevenue → amountMicros`) — should work end-to-end
(backend supports composite-within-relation; #20527 has an integration
test covering this)
- [ ] Composite fields (FULL_NAME, ADDRESS) still open their normal
sub-menu and filter correctly — no regression
- [ ] Role-permissions field-select sub-field menu is unaffected (it
bails out early on the RELATION sentinel)
## Out of scope
- ONE_TO_MANY traversal (no backend support yet)
- Aggregates (`people.count > 5`)
- Persisting relation-traversal filters into a saved view (ViewFilter
has no `relationPath` column yet; that's a separate slice)
- REST API DSL changes
- AI Tools
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
## Summary
Removes gauge chart from the chart-type picker and deletes existing
gauge widgets via a workspace migration. The gauge was rendering a
hardcoded `0.7 / "Progress"` stub regardless of configuration -- never
wired to real data.
The contract stays in place. We keep
`WidgetConfigurationType.GAUGE_CHART`, the DTO, the GraphQL union
member, and the gauge folder -- so stored gauge JSON still resolves
through the schema. The render path falls through to `default: return
null`, so any un-migrated gauge widget renders as an empty cell, not a
crash.
This PR just removes existing gauge widgets if there are any (via
`upgrade:2-3:delete-gauge-widgets`). The deliberate cleanup -- deleting
the type definitions, the gauge folder, the DTO -- comes in a follow-up
PR after the migration has run.
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
## Summary
Replaces the bolted-on `isTool` + `toolInputSchema` fields on
`LogicFunctionManifest` with two distinct, opt-in triggers that align
with the existing `cron` / `databaseEvent` / `httpRoute` trigger
pattern:
- **`toolTriggerSettings`** — exposes the function as an AI tool (chat /
MCP / function calling). Uses standard JSON Schema (the format LLMs
natively understand).
- **`workflowActionTriggerSettings`** — exposes the function as a step
in the visual workflow builder. Uses Twenty's rich `InputSchema` so the
builder can render proper `FieldMetadataType`-aware editors, variable
pickers, labels, and an optional `outputSchema`.
A function can opt into none, one, or both. Each surface gets the schema
format appropriate for it.
### Why
`isTool: true` previously exposed the function as both an AI tool AND a
workflow node, with the same JSON Schema feeding both — but the workflow
builder really wants Twenty's `InputSchema` (with `CURRENCY`,
`RELATION`, `EMAILS`, etc.) and the AI surface really wants standard
JSON Schema. Today the workflow builder hacks around this by treating
JSON Schema as `InputSchema`, which silently breaks for any
non-primitive field type. Splitting the triggers fixes that and lets
each surface evolve independently.
### Migration
- **Fast** instance command adds the two new nullable columns.
- **Slow** instance command backfills `toolTriggerSettings` +
`workflowActionTriggerSettings` from `isTool=true` rows (preserving
today's both-surfaces behaviour) then drops the legacy columns.
### Stacked
Stacked on top of #20181. Merge that first, then this.
## Test plan
- [ ] CI green (oxlint, typecheck, jest, vitest)
- [ ] Run `--include-slow` upgrade against a workspace with existing
`isTool=true` logic functions; verify both new columns populated and old
columns dropped
- [ ] Verify AI chat sees migrated tool functions (Linear create-issue,
Exa search) and can call them with the JSON Schema
- [ ] Add an AI-tool function from the Settings UI (toggles
`toolTriggerSettings`) and verify it shows up in chat
- [ ] Add a workflow-action function from the Settings UI (toggles
`workflowActionTriggerSettings`) and verify it appears in the workflow
node picker
- [ ] In the workflow builder, edit a `LOGIC_FUNCTION` step and verify
input fields render (no more JSON-Schema-as-InputSchema hack)
- [ ] Try defining a function with no triggers in the SDK and verify
`defineLogicFunction` rejects it
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: martmull <martmull@hotmail.fr>
# Introduction
Support multiple selected record ids for headless front components
### Changes
**Added:**
- `recordIds: string[]` field to `FrontComponentExecutionContext`
- `useRecordIds()` hook to get all selected record IDs
**Deprecated:**
- `recordId` field - use `recordIds` instead
- `useRecordId()` hook - use `useRecordIds()` instead
Backward compatibility is preserved
## Summary
- Fix side panel hotkeys (Ctrl+K, Escape, etc.) breaking when opening
records from the record index table
- Ensure `side-panel-focus` is always restored in the focus stack when
navigating within an already-open side panel
- Remove stale `globalHotkeysConfig` on `record-index` focus item that
persisted after the side panel closed
## Problem
When clicking records in the table to open them in the side panel,
`useLeaveTableFocus` called `resetFocusStackToRecordIndex` which wiped
the entire focus stack, including the `side-panel-focus` entry. Since
`openSidePanel` early-returned when the panel was already open,
`side-panel-focus` was never restored. Additionally,
`resetFocusStackToRecordIndex` set `enableGlobalHotkeysWithModifiers:
false` on the remaining `record-index` item when the side panel was
open, and this stale config persisted after the panel closed,
permanently blocking all hotkeys.
## Fix
- **`useNavigateSidePanel.ts`**: Move `pushFocusItemToFocusStack` before
the `isSidePanelOpened` early-return so the side panel's focus entry is
always present in the stack
- **`useResetFocusStackToRecordIndex.ts`**: Always set
`enableGlobalHotkeysWithModifiers: true` on the `record-index` item.
Hotkey scoping when the side panel is open is handled by
`side-panel-focus` sitting on top of the focus stack
https://github.com/user-attachments/assets/ad25befb-338d-4166-9580-18d4e92d6f9b
## Summary
- AI is now GA, so the public/lab `IS_AI_ENABLED` flag is removed from
`FeatureFlagKey`, the public flag catalog, and the dev seeder.
- Drops every backend `@RequireFeatureFlag(IS_AI_ENABLED)` guard (agent,
agent chat, chat subscription, role-to-agent assignment, workflow AI
step creation) and the now-unused `FeatureFlagModule`/`FeatureFlagGuard`
wiring in the AI and workflow modules.
- Removes frontend gating from settings nav, role
permissions/assignment/applicability, command menu hotkeys, side panel,
mobile/drawer nav, and the agent chat provider so AI UI is always on.
Tests and generated GraphQL/SDK schemas updated accordingly.
## Test plan
- [x] `npx nx typecheck twenty-shared`
- [x] `npx nx typecheck twenty-server`
- [x] `npx nx typecheck twenty-front`
- [x] `npx nx lint:diff-with-main twenty-server`
- [x] `npx nx lint:diff-with-main twenty-front`
- [x] `npx jest --config=packages/twenty-server/jest.config.mjs
feature-flag`
- [x] `npx jest --config=packages/twenty-server/jest.config.mjs
workspace-entity-manager`
- [ ] Manual smoke test: AI features still accessible without any flag
row in `featureFlag`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
The "AI" acronym was rendered inconsistently across the codebase. The
backend AI module had settled on PascalCase `Ai` (`AiAgentModule`,
`AiBillingService`, `AiChatModule`, `AiModelRegistryService`, etc.),
while frontend components, several DTOs, a few types, and shared
identifiers still used all-caps `AI` (`AIChatTab`,
`AISystemPromptPreviewDTO`, `SettingsPath.AIPrompts`, ...). CLAUDE.md
specifies PascalCase for classes; this PR normalizes everything internal
to `Ai`.
**This is a pure internal rename.** The GraphQL schema is untouched —
`@ObjectType` decorator string arguments, resolver method names (which
become Query/Mutation field names), gql template contents, and the
`generated-metadata/graphql.ts` file are preserved verbatim. The only
visible change is TypeScript identifiers and file names.
## Also folded in (adjacent cleanups)
- **`AgentModelConfigService` → `AiModelConfigService`**. Lives in
`ai-models/` and is used by multiple AI code paths, not just the Agent
entity. The "Agent" prefix was misleading.
- **`generate-text-input.dto.ts` → `generate-text.input.ts`**. The
`ai-agent/dtos/` folder already uses `<entity>.input.ts` convention for
Input classes (`create-agent.input.ts` etc.); the old path mixed
`.dto.ts` file extension with a class that has no DTO suffix. File
rename only; class stays `GenerateTextInput`.
- **Removed stale TODO** in `ai-model-config.type.ts` that asked for the
`AiModelConfig` rename that this PR performs.
## Rename methodology
Bulk rename via perl with anchored regex
`(?<!['"])(?<![A-Z.])AI([A-Z])(?=[a-z])/Ai$1/g`:
- **Lookbehind for non-uppercase** skips adjacent acronyms (`MOSAIC`,
`OIDCSSO`) and leaves `AIRBNB_ID` alone.
- **Lookbehind for non-quote** protects most string literals.
- **Lookahead for lowercase** restricts matches to PascalCase
identifiers (`AIChatTab`), leaving SCREAMING_SNAKE constants untouched.
Strict file-scope exclusions: `generated-metadata/**`, `generated/**`,
`locales/**`, `migrations/**`, `illustrations/**`, `halftone/**`, and
the two gql template files (`queries/getAISystemPromptPreview.ts`,
`mutations/uploadAIChatFile.ts`).
Post-rename reverts for identifiers where the regex was too eager:
- Backend resolver method names kept: `getAISystemPromptPreview`,
`uploadAIChatFile` (they are GraphQL field names).
- `@ObjectType('AdminAIModels')` / `('AISystemPromptPreview')` /
`('AISystemPromptSection')` kept as-is.
- Backend classes `ClientAIModelConfig` / `AdminAIModelConfig` kept
as-is (they use `@ObjectType()` with no argument, so the class name IS
the schema name).
- External-library symbols restored: `OpenAIProvider`,
`createOpenAICompatible`, `vercelAIIntegration`.
File renames use a two-step rename to work on macOS case-insensitive
filesystems: `git mv X.tsx X.tsx.tmp && git mv X.tsx.tmp renamed.tsx`.
## Diff audit
- 0 changes to migrations
- 0 changes to locale `.po` / `.ts` files
- 0 changes to `generated-metadata/graphql.ts`
- 0 changes to website illustration files (base64 blobs preserved)
- 0 renames inside user-facing translation strings (`t\`…\``,
`msg\`…\``, `<Trans>…</Trans>`)
## Test plan
- [x] `npx nx typecheck twenty-server` — PASS
- [x] `npx nx typecheck twenty-front` — PASS
- [x] `npx jest ai-model admin agent-role` — 79/79 PASS
- [x] `npx oxlint --type-aware` on 118 changed files — 0 errors
- [x] `npx prettier --check` on 118 changed files — clean
- [ ] CI
Adds the ability to change the icon of a record page layout tab from the
side panel in tab edit mode, and sets a default icon for newly-created
record page tabs (no default for dashboards).
<img width="916" height="312" alt="Screenshot 2026-04-17 at 19 55 51"
src="https://github.com/user-attachments/assets/d9f57e89-d40d-483e-b508-5d7318df1ef5"
/>
## Context
"Reset to default" action is rejected by the backend for custom entities
because there is no "default" concept for them
## Implementation
Grey out the Reset to default action on record page-layout tabs and
widgets when the entity either has no applicationId yet (unsaved draft —
previously slipped through the existing check), or belongs to the
workspace custom application.
<img width="926" height="370" alt="Screenshot 2026-04-17 at 18 50 31"
src="https://github.com/user-attachments/assets/c7c163f4-17a6-4b69-a66d-90f9085d27a2"
/>
## Summary
- `SettingsApplicationCustomTab` renders `FrontComponentRenderer` which
calls `useFrontComponentExecutionContext` →
`useLayoutRenderingContext()`, but the settings page never provided a
`LayoutRenderingProvider`
- Every other render site (side panel, command menu, record pages) wraps
`FrontComponentRenderer` with this provider — it was just missed here
- Opening the "Custom" tab in Settings → Applications crashes with:
`LayoutRenderingContext Context not found`
- Fix: wrap with `LayoutRenderingProvider` using `DASHBOARD` layout type
and no target record, matching the pattern used in
`SidePanelFrontComponentPage`
## Test plan
- [ ] Open Settings → Applications → any app with a custom settings tab
- [ ] Click the "Custom" tab — should render the front component without
crashing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
Add support for standalone pages: a new `PageLayout` type
(`STANDALONE_PAGE`) that can be rendered independently at
`/page/:pageLayoutId`, not tied to any record or object context.
- New `STANDALONE_PAGE` page layout type
- New `PAGE_LAYOUT` navigation menu item type: adds a `pageLayoutId`
foreign key to `NavigationMenuItemEntity`, allowing sidebar items to
link directly to standalone pages
- New `GLOBAL_OBJECT_CONTEXT` command menu availability type: separates
object-context-dependent commands (Create Record, Import, Export, See
Deleted, Create View, Hide Deleted) from truly global ones, so
standalone pages only show relevant commands
- Frontend routing & rendering: adds a `/page/:pageLayoutId` route with
its own page component, header, and command menu
- Widget rendering refactor
- Instance commands: two fast 1.22 migrations: `pageLayoutId` column +
`STANDALONE_PAGE` enum, and `GLOBAL_OBJECT_CONTEXT` availability type
enum
- Workspace command: backfills existing command menu items from `GLOBAL`
to `GLOBAL_OBJECT_CONTEXT` where appropriate
- Dev seeds: adds a sample "Star History" standalone page with an iframe
widget for local development
## Context
Moving isActive filtering to the frontend for page layout tabs and
widgets, hiding inactive entities from the UI while keeping them in
state for future reactivation
Next we will implement deactivated standard tab re-activation during tab
creation (cc @Devessier)
<img width="234" height="303" alt="📋 Menu (Slots)"
src="https://github.com/user-attachments/assets/17a25ac6-55e2-4778-b7f0-e7554ed69704"
/>
## Summary
- Removes the `IS_RECORD_TABLE_WIDGET_ENABLED` feature flag, making the
record table widget unconditionally available in dashboard widget type
selection
- The flag was already seeded as `true` for all new workspaces and only
gated UI visibility in one component
(`SidePanelPageLayoutDashboardWidgetTypeSelect`)
- Cleans up the flag from `FeatureFlagKey` enum, dev seeder, and test
mocks
## Analysis
The flag only controlled whether the "View" (Record Table) widget option
appeared in the dashboard widget type selector. The entire record table
widget infrastructure (rendering, creation hooks, GraphQL types,
`RECORD_TABLE` enum in `WidgetType`) is independent of the flag and
fully implemented. No backend logic depends on this flag.
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
## PR description
- The command menu in the side panel now reads directly from
`MAIN_CONTEXT_STORE_INSTANCE_ID` instead of snapshotting the main
context store into a separate side-panel instance when opening. This
keeps the command menu always in sync with the current page state
(selection, filters, view, etc.).
- Removed the broadening/reset-to-selection feature (Backspace to clear
context, "Reset to" button) since the command menu no longer maintains
its own copy of the context.
## Video QA
https://github.com/user-attachments/assets/5d5bc664-b6d4-431d-a271-6ce23d8a4ae0
- Defer backend view creation for FIELDS widgets to prevent orphan views
when users cancel or leave before saving. Views are now created
optimistically in Jotai state (client-side UUID) and only persisted to
the database when the page layout is saved.
- Align the frontend's default field groups fallback with the backend
logic: split into General/Other groups, hide relation fields by default,
and filter invisible fields consistently across all code paths.
## Summary
Refactored AI chat state management to use component family states
instead of global atoms. This change enables proper state isolation per
chat thread, preventing state leakage between different chat instances.
## Key Changes
- **Converted global atoms to component family states:**
- `agentChatErrorState` → `agentChatErrorComponentFamilyState`
- `agentChatIsStreamingState` →
`agentChatIsStreamingComponentFamilyState`
- `agentChatFirstLiveSeqState` →
`agentChatFirstLiveSeqComponentFamilyState`
- `agentChatHandleEventCallbackState` →
`agentChatHandleEventCallbackComponentFamilyState`
- `agentChatUsageState` → `agentChatUsageComponentFamilyState`
- `currentAIChatThreadTitleState` →
`currentAIChatThreadTitleComponentFamilyState`
- **Updated state access patterns:**
- Introduced `useAtomComponentFamilyStateCallbackState` hook to get
family state callbacks
- Changed from direct atom access to family key-based access using `{
threadId }` as the family key
- Updated all components and hooks to use the new family state patterns
- **Removed instance ID dependency:**
- Eliminated `AGENT_CHAT_INSTANCE_ID` constant usage
- Simplified state access by using thread ID directly as the family key
- **Updated affected components and hooks:**
- `useAgentChatSubscription`: Now creates family state atoms per thread
- `AgentChatMessagesFetchEffect`: Uses family state callbacks for event
handling
- `AgentChatThreadInitializationEffect`: Sets family state values per
thread
- `useAIChatThreadClick`: Updates family states when switching threads
- `AIChatErrorUnderMessageList`, `AIChatLastMessageWithStreamingState`,
`AIChatEmptyState`, `AIChatStandaloneError`, `SendMessageButton`,
`AIChatContextUsageButton`, `SidePanelAskAIInfo`: Updated to use family
state values with thread ID
## Implementation Details
- All new component family states use
`AgentChatComponentInstanceContext` for proper component-level isolation
- Family key structure: `{ threadId: string | null }`
- Removed `disposed` flag logic as it's no longer needed with proper
state isolation
- Updated dependency arrays in hooks to include family callback
functions
https://claude.ai/code/session_01D8syiJtCXu5bEHSr5KYkCt
---------
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
Fixes the side panel close animation cleanup
(`sidePanelCloseAnimationCompleteCleanup`) never running during the
close transition.
This eventually fixes the issue where in navbar edit mode, clicking on a
nav item sometimes doesn't get selected, the side panel opens but shows
`Select a navigation item to edit` instead of the selected item's
settings. The root cause was stale `isSidePanelClosing` state from a
previous close that wasn't cleaned up, causing the next open to run
cleanup synchronously (without emitting the close event), which
interfered with the navigation item selection flow.
### Root cause
The CSS `transition` on `StyledSidePanelWrapper` used
`${themeCssVariables.animation.duration.normal}s`, which Linaria
converts to a CSS custom property value like `width
var(--t-animation-duration-normal)s`. After CSS variable substitution,
`0.3` and `s` become two separate tokens, not a valid `<time>` dimension
(`0.3s`). The browser rejects the entire transition declaration, falls
back to `all 0s`, and the width change happens instantly with no
`transitionend` event.
### Fix
- Use `calc(var(--t-animation-duration-normal) * 1s)` to properly
construct the `<time>` value (consistent with ~30 other usages in the
codebase).
- Filter `handleTransitionEnd` to only process `width` transitions on
the wrapper element itself, ignoring bubbled child `background-color`
transitions.
## Before
https://github.com/user-attachments/assets/496ccbff-dc96-487d-a994-451dbf2a5165
## After
https://github.com/user-attachments/assets/78255240-1d7d-42cd-9b56-25627613883a
## Summary
Aligns **object metadata** icons with the **tinted tile** look
everywhere we show a workspace object, and **retires** the
navigation-only `NavigationMenuItemStyleIcon` wrapper in favor of
**shared** UI primitives under `@/ui/display` and `@/object-metadata`.
## What changed
### Global tinted icon building blocks (`@/ui/display`)
- **`TintedIconTile`** / **`StyledTintedIconTileContainer`** support
optional **`size`** and **`stroke`**, and grow the tile when **`size`**
is set so layouts match previous `theme.icon` usage.
- Shared helpers and constants for theme color parsing and tinted
backgrounds/borders/icon color (e.g.
**`getTintedIconTileStyleFromColor`**, **`parseThemeColor`**,
**`getColorFromTheme`**, related constants).
### Object metadata icon (`@/object-metadata`)
- **`ObjectMetadataIcon`** composes **`TintedIconTile`** with
**`getObjectColorWithFallback`**, forwards optional **`size`** /
**`stroke`** for **visual parity** with old `getIcon` + explicit sizing.
- **`getSelectOptionIconFromObjectMetadataItem`** returns an
**`IconComponent`** for selects/menus that expect a component, not a
React node.
### Navigation module cleanup
- **Removed** **`NavigationMenuItemStyleIcon`**; call sites use
**`ObjectMetadataIcon`**, **`TintedIconTile`**, and/or the shared
**`getTintedIconTileStyleFromColor`** pipeline so the same treatment is
**not** tied to the navigation package.
- **`NavigationMenuItemIcon`**, view/link overlays, DnD handle, and
sidebar editor flows updated to use the shared pattern where they render
object (or tinted) icons.
### Product surfaces updated (non-exhaustive)
- **Settings:** role object picker/rows, data model
tables/graph/overview, object preview summary, webhooks entity list,
morph relation multiselect.
- **Workflows:** create/update/delete/upsert/find records, triggers,
filters, variables dropdowns, AI agent object rows, object dropdowns.
- **Shell:** side panel object filter / data sources / folder chrome
where object icons appear.
- **Records:** index header icon, show breadcrumb styling.
- **Activity:** timeline event icon when linked object metadata applies.
- **`NavigationDrawerItem`:** tinted branch aligned with shared
**`TintedIconTile`** behavior.
---------
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
- Removes the intermediate `CommandMenuItemConfig` /
`CommandConfigContext` / `CommandMenuItemDisplay` abstraction layers,
replacing them with a single `CommandMenuItemRenderer` that renders
directly from the command menu items from the backend
- Eliminates the server-items/ subdirectory by moving its contents
(hooks/, contexts/, states/, display/, edit/) up into the parent
command-menu-item/ module, removing an unnecessary nesting level.
## Context
- Extend the FIELD widget to support RICH_TEXT fields alongside existing
RELATION/MORPH_RELATION fields
- Add EDITOR display mode that renders a full rich text editor, and
FIELD display mode that shows a compact single-line preview
- Enforce mutual exclusivity: EDITOR is only available for RICH_TEXT,
CARD only for RELATION, FIELD for both
- Refactor widget configuration into a centralized FIELD_WIDGET_CONFIG
constant and shared useFieldWidgetEligibleFields hook for better
scalability. Later we might use discriminative union from graphql
scehma)
<details>
<summary>
EDITOR mode
</summary>
<img width="1095" height="731" alt="Screenshot 2026-04-09 at 17 31 41"
src="https://github.com/user-attachments/assets/cebffd0e-07ea-4f74-a3dc-ef987daa17ea"
/>
</details>
<details>
<summary>
FIELD mode
</summary>
<img width="986" height="378" alt="Screenshot 2026-04-09 at 17 35 00"
src="https://github.com/user-attachments/assets/c90a8046-fdd0-4321-8ba6-f47d89e9d42a"
/>
</details>
<details>
<summary>
Open FIELD mode
</summary>
<img width="758" height="480" alt="Screenshot 2026-04-09 at 17 35 04"
src="https://github.com/user-attachments/assets/c53cc120-0b6f-47a0-808c-27b26f9f53ec"
/>
</details>
## Context
- Add "Reset to default" for page layout tabs and widgets — backend
mutation resets overrides, reactivates deactivated children, and deletes
custom children
- Fix override write bug where mutating an overridable property (e.g.
widget title) on a standard-app entity incorrectly overwrote the base
column instead of writing to the overrides JSONB —
PageLayoutUpdateService now uses resolveFlatEntityOverridableProperties
for accurate diffing and routes properties through
sanitizeOverridableEntityInput
- Deprecate isOverridden - We need more time to think about this
feature. Currently this adds too much complexity for a very small
benefit
https://github.com/user-attachments/assets/a84546c8-1e15-4d9e-a489-0825cf8b8ed2
## Summary
- **Inline email reply**: Replace external email client redirects
(Gmail/Outlook deeplinks) with an in-app email composer. Users can reply
to email threads directly from the email thread widget or via the
command menu.
- **SendEmail GraphQL mutation**: New backend mutation that reuses
`EmailComposerService` for body sanitization, recipient validation, and
SMTP dispatch via the existing outbound messaging infrastructure.
- **Side panel compose page**: Command menu "Reply" action now opens a
side-panel compose email page with pre-filled To, Subject, and
In-Reply-To fields.
### Backend
- `SendEmailResolver` with `SendEmailInput` / `SendEmailOutputDTO`
- `SendEmailModule` wired into `CoreEngineModule`
- Reuses `EmailComposerService` + `MessagingMessageOutboundService`
### Frontend
- `EmailComposer` / `EmailComposerFields` components
- `useSendEmail`, `useReplyContext`, `useEmailComposerState` hooks
- `useOpenComposeEmailInSidePanel` + `SidePanelComposeEmailPage`
- `EmailThreadWidget` inline Reply bar with toggle composer
- `ReplyToEmailThreadCommand` now opens side-panel instead of external
links
### Seeds
- Added `handle` field to message participant seeds for realistic email
addresses
- Seed `connectedAccount` and `messageChannel` in correct batch order
## Test plan
- [ ] Open an email thread on a person/company record → verify
"Reply..." bar appears below the last message
- [ ] Click "Reply..." → composer opens inline with pre-filled To and
Subject
- [ ] Type a message and click Send → email is sent via SMTP, composer
closes
- [ ] Use command menu Reply action → side panel opens with compose
email page
- [ ] Verify Send/Cancel buttons work correctly in side panel
- [ ] Test with Cc/Bcc toggle in composer fields
- [ ] Verify error handling: invalid recipients, missing connected
account
Made with [Cursor](https://cursor.com)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
- Move email thread display from side panel to a dedicated record page
with a new `EMAIL_THREAD` widget type
- Add message thread as a standard object with page layout, subject
field, and backfill command
- Add reply-to-email command menu item for message thread records
- Remove old side panel message thread components in favor of the new
widget-based approach
## Type fixes
- Add `EMAIL_THREAD` to `WidgetConfigurationType`, `WidgetType`, and all
configuration/validator maps
- Create `EmailThreadConfigurationDTO` and shared
`EmailThreadConfiguration` type
- Register EMAIL_THREAD in widget type validators, configuration
resolvers, and standard widget mappings
## Test plan
- [ ] Verify message thread record pages render with the email thread
widget
- [ ] Verify email thread preview navigates to the record page instead
of opening side panel
- [ ] Verify reply-to-email command appears for message thread records
- [ ] Verify typecheck passes for both twenty-front and twenty-server
- [ ] Run existing test suites to check for regressions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## PR description
The previous approach used a detached flag to represent selection mode,
which could was not synced sync with actual UI selection state. This
change ties the edit mode's selection/fallback behavior directly to
whether records are truly selected in the record index.
- Removes `commandMenuItemEditSelectionModeState` (a standalone 'none' |
'selection' atom) and replaces it with
`mainContextStoreHasSelectedRecordsSelector`, which derives selection
state from the real record selection in the context store.
- Adds `useSelectFirstRecordForEditMode` to programmatically select the
first record (table row or kanban card) when toggling to selection mode,
and useResetRecordIndexSelection to clear selection when toggling off or
exiting edit mode.
- Ensures record selection is properly cleaned up when exiting layout
customization mode or closing the side panel.
## Video QA
### Table
https://github.com/user-attachments/assets/1d845809-8369-4872-b3a9-a0281b8e464b
### Board
https://github.com/user-attachments/assets/325c2d58-4ca0-408a-ab4a-66b97d9d70de
## Context
Add a new "Manage" section in FIELDS widget side panel with a "Delete
widget" action
Next step: Deleting a non-custom fields widget should be reversible
("Reset to default" followup)
<img width="1286" height="398" alt="Screenshot 2026-03-31 at 15 17 31"
src="https://github.com/user-attachments/assets/9ee63c14-52f1-470e-9112-4538271dc6fc"
/>
## Summary
- Remove the `IS_APPLICATION_ENABLED` feature flag — application
features are now always enabled
- Remove `@RequireFeatureFlag` decorators and `FeatureFlagGuard` from 7
application resolvers (registration, development, manifest, install,
upgrade, marketplace, oauth)
- Remove frontend feature flag checks: settings navigation visibility,
route protection wrapper, side panel widget type gating, and role
permission flag filtering
- Delete the integration test for disabled-flag behavior
- Clean up seed data, test mocks, and generated schema files
## Summary
- Remove the `IS_DASHBOARD_V2_ENABLED` feature flag from the codebase
- Dashboard V2 features (gauge charts, line charts, pie charts) are now
always enabled
- Remove the validator gate that blocked gauge chart creation/update
without the flag
- Clean up all related code: seed data, dev-seeder service, widget
seeds, and test mocks
In navbar edit mode, selecting an item for edit stored
selectedNavigationMenuItemIdInEditModeState (and related
pending-insertion state). Closing the side panel did not reset those
atoms, so the nav item stayed visually selected. Reset both when the
panel closes so the highlight matches the closed panel; reopening in
edit mode then starts from the generic “new item” entry unless the user
picks an item again.
Introduce a new feature flag for the record table widget, enabling
conditional rendering and state management based on its status. Update
related components and tests accordingly.
---------
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>