96f3ff0e9051002d2fb04e6b513c1d34fe02ed0c
1871 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
96f3ff0e90 |
Add record table widget to dashboards (#18747)
## Demo https://github.com/user-attachments/assets/584de452-544a-41f8-ae9f-4be9e9d0cd9f ## Problem - Dashboards only supported chart widgets — tabular record data had no inline widget type - `RecordTable` was tightly coupled to the record index: HTML IDs, CSS variables, and hover portals were global strings with no per-instance scoping, so multiple tables on the same page would collide - `updateRecordTableCSSVariable`, `RECORD_TABLE_HTML_ID`, and cell portal IDs were hardcoded — placing two tables caused hover portals and CSS column widths to bleed across instances - Grid drag-select captured record UUIDs as cell IDs, producing `NaN` layout coordinates and a full-page freeze on second widget creation ## Fix - `RECORD_TABLE` is now a valid widget type across the full stack — server DTOs, DB enum migration, universal config mapping, GraphQL codegen, shared types (`RecordTableConfigurationDto`, `WidgetType`, `addRecordTableWidgetType` migration) - A record table widget can be placed on a dashboard and boots from a View ID with no record index dependency — `StandaloneRecordTableProvider` + `StandaloneRecordTableViewLoadEffect` (wraps existing `RecordTableWithWrappers` unchanged) - Selecting a data source auto-creates a dedicated View with up to 6 initial fields; switching source or deleting the widget cleans up the View — `useCreateViewForRecordTableWidget` + `useDeleteViewForRecordTableWidget` - The settings panel exposes source, field visibility/reorder, filter conditions, sort rules, and editable widget title — `SidePanelPageLayoutRecordTableSettings` + sub-pages, matching chart widget pattern - Filters, sorts, and aggregate operations update the table in real time but only persist to the View on explicit dashboard save — `useSaveRecordTableWidgetsViewDataOnDashboardSave` (diff + flush on save) - Headers are always non-interactive (no dropdown, no cursor pointer); columns are resizable only in edit mode; cells are non-editable in both modes — `isRecordTableColumnHeadersReadOnlyComponentState`, `isRecordTableColumnResizableComponentState`, `isRecordTableCellsNonEditableComponentState` (Jotai component states) - Hover portals and CSS column widths no longer bleed between multiple table widgets — `getRecordTableHtmlId(tableId)`, `getRecordTableCellId(tableId, …)`, `updateRecordTableCSSVariable(tableId, …)` scope all DOM IDs and CSS variables per instance - Clicking inside a widget's content area no longer opens the settings panel — `WidgetCardContent` stops click propagation when editable, limiting settings-open to the card header and chrome - Second widget creation no longer freezes the page — `PageLayoutGridLayout` drag-select filters by `cell-` prefix to exclude record UUIDs from grid cell detection ## Follow-up fixes **Widget save flow** - Saving a dashboard silently dropped record table widget changes (column visibility, order, filters, sorts, aggregates) because widget data save was bundled inside the layout save and only ran when layout structure changed - Widget data now persists independently via `useSavePageLayoutWidgetsData`, called in all save paths (dashboard save, record page save, layout customization save); saves are also skipped when nothing has changed **Drag-and-drop / checkbox columns in widget** - Record table widgets showed the drag handle column and checkbox selection column even though row reordering and multi-select are meaningless in a read-only widget - Two new component states (`isRecordTableDragColumnHiddenComponentState`, `isRecordTableCheckboxColumnHiddenComponentState`) hide each column independently; widget tables now display only data columns **Sticky column layout** - Sticky positioning of the first three columns used `:nth-of-type` CSS selectors — when drag or checkbox columns were hidden, the selector targeted the wrong column and the first data column didn't stick - Sticky CSS now targets semantic class names (`RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH_CLASS_NAME`, etc.) so sticky behavior is correct regardless of which columns are hidden **Save/Cancel buttons during edit mode** - Save and Cancel command-menu buttons were unpinned during dashboard edit mode because the pin logic excluded all items while `isPageInEditMode` was true - Items whose availability expression contains `isPageInEditMode` are now exempted from the unpin rule; Save/Cancel stay pinned during editing **Title input auto-focus** - Selecting "Record Table" as widget type auto-focused the title input, interrupting the configuration flow - `focusTitleInput` is now `false` when navigating to record table settings **Morph relation field error** - A field with missing `morphRelations` metadata crashed the page with a "refresh" error from `mapObjectMetadataToGraphQLQuery` - Now returns an empty array and silently omits the field from the query instead of crashing **`updateRecordMutation` prop removal** - `RecordTableWithWrappers` required callers to pass an `updateRecordMutation` callback, duplicating `useUpdateOneRecord` at every usage site - The mutation is now owned inside `RecordTableContextProvider` via `RecordTableUpdateContext`; the prop is gone **Standalone → Widget module rename** - `record-table-standalone` module renamed to `record-table-widget` — `StandaloneRecordTable` → `RecordTableWidget`, `StandaloneRecordTableViewLoadEffect` → `RecordTableWidgetViewLoadEffect`, etc. **RecordTableRow cell extraction** - Row rendering logic (`RecordTableCellDragAndDrop`, `RecordTableCellCheckbox`, `RecordTableFieldsCells`, hotkey/arrow-key effects) was duplicated between `RecordTableRow` and `RecordTableRowVirtualizedFullData` - Extracted `RecordTableRowCells` (shared cell content) and `RecordTableStaticTr` (non-draggable `<tr>` wrapper); when drag column is hidden, rows render inside a static `<tr>` instead of the draggable wrapper **View load effect metadata tracking** - `RecordTableWidgetViewLoadEffect` now tracks `objectMetadataItem.updatedAt` alongside `viewId` to re-load states when metadata changes (e.g. field additions), preventing stale column data **Data source dropdown deduplication** - Extracted `filterReadableActiveObjectMetadataItems` util, shared by both chart and record table data source dropdowns — removes duplicated permission-filtering logic **RECORD_TABLE view identifier mapping (server)** - Added `RECORD_TABLE` case to `fromPageLayoutWidgetConfigurationToUniversalConfiguration` and `fromUniversalConfigurationToFlatPageLayoutWidgetConfiguration` so widget views are properly mapped during workspace import/export **GraphQL error handler typing (server)** - `formatError` parameter changed from `any` to `unknown`; `workspaceQueryRunnerGraphqlApiExceptionHandler` broadened from `QueryFailedErrorWithCode` to `Error | QueryFailedError` — removes unsafe type casts **Save hook signature** - `useSaveRecordTableWidgetsViewDataOnDashboardSave` no longer takes `pageLayoutId` in constructor; receives it as a callback parameter, eliminating the need for `useAtomComponentStateCallbackState` **Customize Dashboard hidden during edit mode** - The "Customize Dashboard" command was still visible while already editing — its `conditionalAvailabilityExpression` now includes `not isPageInEditMode` **Fields dropdown split** - `RecordTableFieldsDropdownContent` (300+ lines) split into `RecordTableFieldsDropdownVisibleFieldsContent` and `RecordTableFieldsDropdownHiddenFieldsContent` **Checkbox placeholder cleanup** - Removed unnecessary `StyledRecordTableTdContainer` wrapper from `RecordTableCellCheckboxPlaceholder` |
||
|
|
cec23e89fa |
Fix batch update optimistic and prevent accidental mass-update (#17213)
## Problem - ⚠️ Multi-edit could silently update ALL records of an object with no undo, no ctrl-z - After a batch update the table showed stale data — `useIncrementalUpdateManyRecords` had no explicit `findMany` refetch and `skipOptimisticEffect` was missing - Clicking inside the side panel deselected all kanban cards — `RecordBoardClickOutsideEffect` uses `refs:[]` and the side panel had no `data-click-outside-id` - Clicking a currency/select dropdown inside the panel also triggered deselection — `FloatingPortal` renders outside the side panel DOM, bypassing the click-outside-id exclusion - An empty selection silently matched every record — `computeContextStoreFilters` returned `undefined` filter when `selectedRecordIds` was `[]` ## Fix - Table refreshes correctly after batch update — `useRefetchFindManyRecords` explicitly refetches `FindMany<Object>` queries; `useIncrementalUpdateManyRecords` adds `skipOptimisticEffect: true` and calls it in `finally` - Clicking the side panel no longer deselects kanban cards — `SidePanelForDesktop` carries `data-click-outside-id`; `RecordBoardClickOutsideEffect` + `RecordTableBodyFocusClickOutsideEffect` exclude it - Clicking dropdowns inside the panel no longer deselects either — `ParentClickOutsideIdContext` propagates the side panel ID into `FloatingPortal` content via `DropdownInternalContainer` - Empty selection can no longer match all records — `computeContextStoreFilters` returns `{ id: { in: [] } }` instead of `undefined` - Apply is disabled with no selection; a confirmation modal shows the count + no-undo warning before executing — `UpdateMultipleRecordsContainer` ## Not included - Undo / snapshot restore — requires backend changes, out of scope ## Blast radius - `ParentClickOutsideIdContext` touches `DropdownInternalContainer` (207 `<Dropdown>` usages). `parentClickOutsideId` is `undefined` everywhere outside the side panel → attribute not rendered → zero behavioral change for existing consumers. --------- Co-authored-by: Samuel Arbibe <samuelarbibe@Samuels-MacBook-Pro.local> Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
611947e031 |
fix: metadata store lifecycle during sign-in, sign-out, and locale change (#18901)
## Summary Fixes metadata lifecycle bugs during sign-in, sign-out, and locale change: - **Cross-tab sign-out**: Broadcasts sign-out via `BroadcastChannel` so other tabs clear their session gracefully instead of hitting stale-token errors - **SSE teardown on sign-out**: Handles `UNAUTHENTICATED`/`FORBIDDEN` errors in the SSE event stream effect instead of throwing unhandled errors - **Locale switch resilience**: `invalidateAndReload` now invalidates collection hashes instead of clearing the store to empty, so components never see 0 metadata items during the reload transition - **Sign-in background mock**: Uses non-throwing `objectMetadataItemFamilySelector` instead of hooks that throw on missing metadata - **View name placeholders**: Guards against `undefined` `viewName` during metadata transitions (the minimal metadata query doesn't include `name`) - **Session cleanup**: Selective `localStorage` clearing (`clearSessionLocalStorageKeys`) preserves metadata keys; `clearAllSessionLocalStorageKeys` for full clears - **Metadata reload API**: New `useMetadataStoreActions` hook as the high-level API for metadata lifecycle operations (`applyMockedMetadata`, `invalidateAndReload`, `loadMockedMetadataAtomic`) ## Test plan - [ ] Sign out on Tab A → Tab A shows sign-in page with no console errors - [ ] Tab B (logged in) receives cross-tab broadcast and redirects to sign-in - [ ] No "Forbidden resource" SSE errors in console during sign-out - [ ] Change language in Settings > Experience → no crash, metadata refreshes in background - [ ] Sign back in after sign-out → metadata loads correctly, app is functional - [ ] Re-sign-in after locale change → correct locale is preserved |
||
|
|
4c6e102493 |
Display found items after full items loaded (#18914)
The performCombinedFindManyRecords call was previously fire-and-forget (.then()), meaning the loading state was set to false and the picker became interactive before the full records were written into the Apollo cache. Fixes https://github.com/twentyhq/twenty/issues/17669 Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com> |
||
|
|
7b6fb52df7 |
fix: validate blocknote JSON in rich text fields (#18902)
## Summary - **Backend**: Add JSON validation for the `blocknote` subfield in rich text API inputs — rejects values that aren't valid JSON or aren't arrays (BlockNote content is always `PartialBlock[]`). This prevents corrupted data from being persisted to the database. - **Frontend**: Replace all 5 unprotected `JSON.parse` calls on blocknote content with the safe `parseJson` utility from `twenty-shared`. Invalid content now degrades gracefully (empty block / empty string / unchanged passthrough) instead of crashing the app. - **Tests**: Added integration tests for invalid blocknote JSON (both GraphQL and REST), unit tests for the new validation, and updated existing test constants to use valid BlockNote JSON. ## Context A user reported a `SyntaxError: Expected ',' or ']' after array element` crash caused by malformed blocknote JSON stored in the database. The data had `"children":[]` nested inside the `content` array instead of as a sibling property. The API accepted this invalid JSON because it only validated that `blocknote` was a string, not that it contained valid JSON. On the frontend, 5 call sites used bare `JSON.parse` with no error handling, causing a white-screen crash. ## Test plan - [x] Unit tests pass: `validate-rich-text-field-or-throw.util.spec.ts` (10/10) - [x] Integration tests pass: `rich-text-field-create-input-validation` (8/8) - [ ] Verify creating a note with valid rich text still works end-to-end - [ ] Verify API returns clear error when blocknote contains invalid JSON - [ ] Verify frontend renders empty block instead of crashing when encountering corrupted data 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
c6d4162b73 |
Fix: Background color selected row + border bottom settings tab (#18870)
<img width="1285" height="73" alt="Capture d’écran 2026-03-23 à 18 58 09" src="https://github.com/user-attachments/assets/a01cfc8e-6492-4ebe-a47a-b504a73e616c" /> <img width="574" height="59" alt="Capture d’écran 2026-03-23 à 18 58 39" src="https://github.com/user-attachments/assets/4947ec02-e151-48fb-87e9-86dbc0ed4fe9" /> |
||
|
|
d5a7dec117 |
refactor: rename ObjectMetadataItem to EnrichedObjectMetadataItem and clean up metadata flows (#18830)
## Summary - Renames `ObjectMetadataItem` to `EnrichedObjectMetadataItem` across the entire frontend (~440 files) to clarify that this type includes derived fields (`readableFields`, `updatableFields`, nested `fields[]`, `indexMetadatas[]`) computed at read time from the metadata store - Creates `splitObjectMetadataGqlResponse` that goes directly from a GraphQL `ObjectMetadataItemsQuery` response to flat store items (combining the old `mapPaginatedObjectMetadataItemsToObjectMetadataItems` + `splitObjectMetadataItemWithRelated` two-step flow into one call) - Removes `ObjectMetadataItemWithRelated` type and all "WithRelated" naming - Renames `generatedMockObjectMetadataItems` to `generateTestEnrichedObjectMetadataItemsMock` to make it clear this is test-only enriched data - Deletes `useLoadMockedObjectMetadataItems` hook (consolidated into `useLoadMockedMinimalMetadata`) - Ensures nothing destined for the metadata store computes `readableFields`/`updatableFields` (preventing the localStorage bloat from #18809) ## Type hierarchy (before → after) **Before:** ``` ObjectMetadataItemsQuery → mapPaginated → ObjectMetadataItemWithRelated → enrich → ObjectMetadataItem → split → FlatObjectMetadataItem (store) ``` **After:** ``` ObjectMetadataItemsQuery → splitObjectMetadataGqlResponse → FlatObjectMetadataItem (store) → mapPaginated + enrich (tests only) → EnrichedObjectMetadataItem ``` ## Test plan - [x] `npx nx typecheck twenty-front` passes - [x] `npx nx test twenty-front` passes (767 suites, 4505 tests) - [x] `npx nx lint twenty-front` passes - [ ] CI checks pass Made with [Cursor](https://cursor.com) |
||
|
|
bdaff0b7e2 |
Fetch load more on group by (#18811)
Fixes https://github.com/twentyhq/twenty/issues/18587 "Load More" in grouped record table view (aggregated view) which was broken — clicking it loaded no records and made the button disappear Root cause: the Load More button called fetchMore on a useLazyQuery that was never executed, causing Apollo to throw Invariant Violation: 'fetchMore' cannot be called before executing the query. The component now uses fetchMoreRecords from the same useQuery that powers the table data. Before https://github.com/user-attachments/assets/5266424b-7f35-4261-b7b7-9f7bc3eb8ad6 After https://github.com/user-attachments/assets/866fe7d7-720a-40c7-bb28-267b85bd98d5 --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
d69e4d7008 |
fix: prevent FIND_RECORDS from silently dropping unresolved filter variables (#18814)
## Summary Fixes #18744 — The workflow FIND_RECORDS action silently drops filter conditions when a variable resolves to null/empty, causing the query to return **all records** instead of erroring. **Root cause (three compounding layers):** 1. **`variable-resolver.ts`** — `resolveString` returns `undefined` when a variable lookup fails (e.g., `{{steps.trigger.output.userId}}` where `userId` doesn't exist in context). The return type says `string` but `evalFromContext` actually returns `undefined` at runtime. 2. **`checkIfShouldSkipFiltering.ts`** — Treats `undefined`/`null`/`""` values as "skip this filter." This is correct for the **UI filter builder** (user hasn't finished typing), but wrong for **workflow execution** (variable resolution failed = misconfigured workflow). 3. **`find-records.workflow-action.ts`** — When all filters are silently skipped, `computeRecordGqlOperationFilter` returns `{}` (match everything). The query runs with no filter, returning all records — silently succeeding with wrong results. ## Fix Added validation in `find-records.workflow-action.ts` **after** `resolveInput` but **before** `computeRecordGqlOperationFilter`. For each filter with a value-requiring operand (i.e., not IS_EMPTY, IS_NOT_EMPTY, IS_IN_PAST, IS_IN_FUTURE, IS_TODAY), if the resolved value is `undefined`, `null`, or `""`, throw `INVALID_STEP_INPUT` with a descriptive error message. **Why this approach:** - Scoped to the workflow executor — does **not** break the UI filter builder's intentional skip-on-empty behavior - Does not change shared utilities (`checkIfShouldSkipFiltering`, `resolveInput`) used across the app - Fails fast with a clear error instead of silently returning wrong data - 1 file changed, 23 lines added ## Test plan - [x] Backend typecheck passes - [x] oxlint passes (0 warnings, 0 errors) - [x] Prettier passes - [ ] Manual: Create a workflow with FIND_RECORDS using a variable that doesn't exist → should error with "Filter condition has an empty value after variable resolution" instead of returning all records - [ ] Manual: Create a workflow with FIND_RECORDS using IS_EMPTY operand (no value needed) → should still work correctly --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
03a2abb305 |
fix: board view loads all records instead of showing skeleton placeholders (#18824)
## Summary - The record board (Kanban view) only loaded the first 10 records per column, then showed skeleton placeholder cards for the rest without ever fetching the remaining data - **Root cause**: The `RecordBoardFetchMoreInViewTriggerComponent` (IntersectionObserver) is positioned at the bottom of the entire board. With 10 real cards + 10 skeleton cards per column (~3500px of content), the trigger div was pushed far beyond the 1600px `rootMargin` detection zone, so `recordBoardShouldFetchMore` never became `true` and fetch-more was never triggered - **Fix**: The initial query now sets `recordBoardShouldFetchMore = true` when columns need more data, and `triggerRecordBoardFetchMore` uses an internal `while` loop to load all remaining pages in a single invocation — making it immune to the InView component racing to reset the flag between React render cycles |
||
|
|
42269cc45b |
fix(links): preserve percent-encoded URLs during normalization (#18792)
## Summary This preserves percent-encoded payloads when normalizing links fields. `lowercaseUrlOriginAndRemoveTrailingSlash` was decoding the path and query string while lowercasing the URL origin. That changes URLs where encoded payloads are semantically significant, such as Google Maps links containing `%2F` segments. Closes #18698. ## Changes - stop decoding the path/query payload in `lowercaseUrlOriginAndRemoveTrailingSlash` - preserve the raw path, query, and hash while still lowercasing the origin and trimming a trailing slash - update shared URL normalization tests to assert encoded payloads stay encoded - add a server-side regression test covering imported links field normalization ## Validation - `corepack yarn jest --config packages/twenty-shared/jest.config.mjs packages/twenty-shared/src/utils/url/__tests__/lowercaseUrlOriginAndRemoveTrailingSlash.test.ts --runInBand` - `corepack yarn jest --config packages/twenty-server/jest.config.mjs packages/twenty-server/src/engine/core-modules/record-transformer/utils/__tests__/transform-links-value.util.spec.ts --runInBand` - `corepack yarn nx test twenty-server --runInBand --testFile=src/engine/core-modules/record-transformer/utils/__tests__/transform-links-value.util.spec.ts` --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
8005b35b56 |
Fix relation connect where failing on mixed-case email and URL (#18605)
Related to issue #17711 Follow-up to PR #17774 which fixed the frontend link normalization only ## Summary - Normalize `primaryEmail` (lowercase) and `primaryLinkUrl` in relation `connect.where` composite values - Applied in both frontend spreadsheet import and backend `DataArgProcessorService` to cover all entry points (UI import, GraphQL API) --------- Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com> |
||
|
|
b86e6189c0 |
Remove postition from Timeline Activities + fix workflow title placeholder (#18777)
- Position were not properly displayed because we never implemented a display for this - Untitled placeholder was not displayed anymore <img width="359" height="117" alt="Capture d’écran 2026-03-19 à 17 11 25" src="https://github.com/user-attachments/assets/64c90d81-8262-4176-ae25-804748e36b1e" /> |
||
|
|
994215e0dc |
Update store on data model mutation (#18684)
- enrich SSE events with relations - remove queries from sse metadata events - on sse event, manage store - on object/field metadata changes, manage store TODO left: - fix other metadata items --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions <github-actions@twenty.com> Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
abdab2fb7e |
[Command menu items] Create engine commands (#18681)
## Description - Introduces a new engine command execution model that replaces the previous approach of mapping `EngineComponentKey` to React components. Instead, engine commands are now mounted headlessly via `HeadlessEngineCommandMountRoot`, with their execution context populated synchronously before mounting. - Creates new headless command components - Moves error handling from the SDK layer to the host app by wrapping all mounted commands with a new `CommandMenuItemErrorBoundary` The new flow works as follows: - When a command menu item with an `engineComponentKey` is clicked, `useCommandMenuItemFrontComponentCommands` calls `useMountEngineCommand`, which synchronously reads the current context store (object metadata, selected records, filters, view ID, etc.) and writes a `MountedEngineCommandContext` into `mountedEngineCommandsState`. - The command is then mounted into `mountedEngineCommandsState`, which triggers `HeadlessEngineCommandMountRoot` to render the corresponding headless component from `ENGINE_COMPONENT_KEY_HEADLESS_COMPONENT_MAP`, wrapped in `CommandMenuItemErrorBoundary`, `ContextStoreComponentInstanceContext.Provider`, and `EngineCommandComponentInstanceContext.Provider`. - Each command component reads its execution context and delegates to one of the 4 execution patterns: `HeadlessEngineCommandWrapperEffect` (simple actions), `HeadlessConfirmationModalEngineCommandEffect` (destructive actions needing confirmation), `HeadlessNavigateEngineCommand` (GO_TO_* commands), or `HeadlessOpenSidePanelPageEngineCommand` (SEARCH_RECORDS, ASK_AI, VIEW_PREVIOUS_AI_CHATS). - After execution, the command self-unmounts via `useUnmountEngineCommand`, which removes the entry from `mountedEngineCommandsState` and stops rendering the component. |
||
|
|
ab881350b2 |
refactor: unify layout customization mode (record pages + navigation) (#18640)
### What Unifies record page layout editing and navigation menu editing into a single global "layout customization" session. Dashboard editing stays separate. ### How it works **Two edit mode systems, one context-based read:** - `isLayoutCustomizationModeEnabledState` -- global atom for record pages + navigation - `isDashboardInEditModeComponentState` -- dashboard-only, independent per-component atom - `PageLayoutEditModeProvider` -- context that dispatches to `RecordPageLayoutEditModeProvider` (reads global atom) or `DashboardPageLayoutEditModeProvider` (reads component atom), one component per file **Session registry + independent atoms:** - `activeCustomizationPageLayoutIdsState` -- accumulates page layout IDs as user navigates during customization (`string[]`) - Save/cancel iterate the ID list and read each layout's draft/persisted atoms independently - Follows the same pattern as `settingsRoleIdsState` + `settingsDraftRoleFamilyState` **Unified UI:** - `LayoutCustomizationBar` replaces the old `NavigationMenuEditModeBar` - Enter once -- edit record layouts + navigation -- save/cancel everything together - `useSaveLayoutCustomization` orchestrates sequential save: navigation draft -- page layouts -- field widget groups - Error snackbar on partial save failure (with TODO for future atomic server mutation) **Draft protection during customization:** - `PageLayoutRelationWidgetsSyncEffect` guarded -- only updates persisted state from server, skips draft/currentLayouts while customization is active - `useExecuteTasksOnAnyLocationChange` skips draft reset when customization mode is enabled - Command execution blocked during layout customization ### Cleanup - Deleted `NavigationMenuEditModeBar`, `isNavigationMenuInEditModeState`, `isPageLayoutInEditModeComponentState`, `useIsGlobalLayoutCustomizationActive` - `DraftPageLayout` type changed from `Omit` to `Pick` (explicit fields) - Removed save/cancel from `DefaultRecordCommandMenuItemsConfig` (bar handles it now) - Extracted `useSaveFieldsWidgetGroups` from save orchestration - Split `PageLayoutEditModeProvider` into 3 separate files (one component per file, Twenty convention) ### Known issues - **Stale deleted widget after save (pre-existing on `main`)**: Delete widget -- save -- exit customization -- Apollo cache stale -- sync effect overwrites Jotai from stale data -- widget reappears until refresh. Separate PR needed, likely tied to the planned server-side `saveLayoutCustomization` atomic endpoint. ### Open questions - **Module location**: Layout customization hooks/states live in `/app` -- should they move to their own `modules/layout-customization/`? - **Atomic server mutation**: All save mutations are on metadata schema (`createNavigationMenuItem`, `deleteNavigationMenuItem`, `updateNavigationMenuItem`, `updatePageLayoutWithTabsAndWidgets`, `upsertFieldsWidget`). A single `saveLayoutCustomization` endpoint could make saves truly atomic. https://github.com/user-attachments/assets/036ef542-97f3-485b-a68f-3726002c81fb |
||
|
|
f1dfcfd163 |
refactor(twenty-front): reorganize NavigationMenuItem module into common/display/edit subfolders (#18691)
## Summary - Reorganizes the `navigation-menu-item` frontend module from a flat structure into `common/`, `display/`, and `edit/` subfolders with type-specific subdirectories (`link/`, `folder/`, `object/`, `view/`, `record/`) - Every file is now in a leaf folder describing its type: `components/`, `hooks/`, `utils/`, `types/`, or `constants/` - Moves 14 NavigationMenuItem-related components out of `object-metadata/` and `side-panel/pages/` into the `navigation-menu-item` module where they belong - Creates type-specific display utility functions (e.g., `getLinkNavigationMenuItemLabel`, `getObjectNavigationMenuItemComputedLink`) to replace generic switch-based functions - Unifies the Favorites section drag-and-drop from `@hello-pangea/dnd` to `@dnd-kit/react`, matching the Workspace section's DnD library - Renames Favorites section components from `CurrentWorkspaceMember*` to `Favorites*` for clarity - Deletes unused `FavoritesDragDropProviderContent` and `NavbarDragProvider` ## Test plan - [x] `npx nx typecheck twenty-front` passes - [x] `npx nx lint:diff-with-main twenty-front` passes (0 warnings, 0 errors) - [x] `npx nx test twenty-front` passes (763 suites, 4467 tests) - [ ] Verify favorites drag-and-drop still works in the UI (reorder items, move between folders) - [ ] Verify workspace edit mode drag-and-drop still works - [ ] Verify "add to navigation" drag from command menu/side panel still works |
||
|
|
1be87eb97b |
chore: frontend dead code removal and naming cleanup (#18690)
## Summary - **Delete 10 unused files**: 7 hooks (`useWorkflowRunUnsafe`, `useGetViewById`, `useCreateViewFieldGroup`, `useDeleteViewFieldGroup`, `useUpdateViewFieldGroup`, `useCreateManyViewFieldGroups`, `useMoveViewColumns` + test), 1 component (`SettingsSummaryCard`), 1 utility (`createEventContext`) - **Rename `objectMetadataItemsState` → `objectMetadataItemsSelector`** across ~85 files to accurately reflect it is a derived selector (via `createAtomSelector`), not a base Jotai atom ## Details ### Dead code removed | Type | Name | Reason | |------|------|--------| | Hook | `useWorkflowRunUnsafe` | Never imported — duplicate of `useWorkflowRun` without schema validation | | Hook | `useGetViewById` | Never imported — `useViewById` is used instead | | Hook | `useCreateViewFieldGroup` | Never imported — CRUD done via `usePerformViewFieldGroupAPIPersist` | | Hook | `useDeleteViewFieldGroup` | Same as above | | Hook | `useUpdateViewFieldGroup` | Same as above | | Hook | `useCreateManyViewFieldGroups` | Same as above | | Hook | `useMoveViewColumns` | Only imported by its own test — no production usage | | Component | `SettingsSummaryCard` | Never imported anywhere | | Utility | `createEventContext` | Never imported anywhere | ### Rename `objectMetadataItemsState` is created via `createAtomSelector` (it derives from `objectMetadataItemsWithFieldsSelector`), so naming it `*State` is misleading. Renamed to `objectMetadataItemsSelector` for consistency with sibling selectors like `objectMetadataItemsByNamePluralMapSelector`. |
||
|
|
a121d00ddd |
feat: add color property to ObjectMetadata for object icon customization (#18672)
## Summary - Adds a `color` column to `ObjectMetadataEntity` with full GraphQL support so object icon colors are persisted at the metadata level - Adds a `type` column to `NavigationMenuItemEntity` (enum: `OBJECT`, `VIEW`, `FOLDER`, `LINK`, `RECORD`) replacing field-based type inference - Updates frontend to read object colors from `objectMetadata.color` (falling back to standard defaults) in the sidebar nav, record index header, and record show breadcrumb - Simplifies `NavigationMenuItemIcon` color resolution via `getEffectiveNavigationMenuItemColor` util ## Color rules | Item type | Color source | Editable in sidebar? | |-----------|-------------|---------------------| | **Object** | `objectMetadata.color` | Yes — persisted to `objectMetadata.color` on Save | | **Folder** | `navigationMenuItem.color` | Yes | | **Link** | Fixed default (`DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK`) | No | | **View** | `objectMetadata.color` (from the parent object) | No | | **Record** | None | No | - **Object** items represent the whole object (e.g. "Companies") and point to the INDEX view. Changing their color updates `objectMetadata.color` via `useSaveObjectMetadataColorsFromDraft`. - **View** items represent specific non-INDEX views. Their color comes from the parent object's metadata (read-only). - Only **folders** store their color on `navigationMenuItem.color` — enforced by `hasNavigationMenuItemOwnColor` util. - `getEffectiveNavigationMenuItemColor` returns `objectColor` for both OBJECT and VIEW items, folder's own color for folders, and the fixed default for links. ## NavigationMenuItemType enum - Shared enum created in `twenty-shared` with values: `OBJECT`, `VIEW`, `FOLDER`, `LINK`, `RECORD` - Registered as a GraphQL enum on the backend - Replaces string literals across entity, DTOs, input, converters, and frontend hooks - Migration backfills existing rows: INDEX views → `OBJECT`, non-INDEX views → `VIEW`, based on join with the view table ## Design decisions - **OBJECT vs VIEW distinction**: Items pointing to INDEX views are typed as `OBJECT` (represent the whole object, color editable). Items pointing to non-INDEX views are typed as `VIEW` (specific view, color read-only from parent object). - **Dual color storage**: `navigationMenuItem.color` is preserved for folders only. Objects use `objectMetadata.color` as their source of truth. - **Type discriminator**: The `type` column replaces field-based inference (checking `viewId`, `link`, `targetRecordId` presence) with an explicit enum, simplifying `isNavigationMenuItemLink` / `isNavigationMenuItemFolder` to simple `item.type ===` checks. - **No settings page color picker**: Object color editing is done from the sidebar edit panel, not the data model settings page. ## Test plan - [ ] Verify objects display their default standard colors in the sidebar - [ ] Verify object color editing works in the sidebar edit panel (persists to objectMetadata.color) - [ ] Verify folder color editing works in the sidebar edit panel - [ ] Verify views, links, and records do NOT show a color picker in the sidebar edit panel - [ ] Run `npx nx typecheck twenty-front` and `npx nx typecheck twenty-server` - [ ] Verify the database migrations add `color` to `objectMetadata` and `type` to `navigationMenuItem` Made with [Cursor](https://cursor.com) |
||
|
|
5c745059ad |
refactor: remove "core" naming from views and eliminate converter layer (#18667)
## Summary
- **Remove all "core" prefixes** from the views system — the
metadata-based storage migration is complete, so `CoreView`,
`coreViewsSelector`, `getCoreViews`, etc. are now just `View`,
`viewsSelector`, `getViews`
- **Eliminate the entire converter layer** (15 files, ~850 lines
deleted) — `convertCoreViewToView` and all sub-converters were either
no-ops or trivially adding `__typename` / mapping identical enum values.
Local enums now re-export from generated GraphQL types directly (single
source of truth)
- **Unify `View` and `ViewWithRelations`** into one type —
`ViewWithRelations` is now a type alias for `View`, selectors return
data directly without conversion
### Backend
- Rename `@ObjectType('CoreView')` → `@ObjectType('View')` (and all
sub-entities)
- Rename resolver methods: `getCoreViews` → `getViews`, `createCoreView`
→ `createView`, etc.
- Rename `FIND_ALL_CORE_VIEWS_GRAPHQL_OPERATION` →
`FIND_ALL_VIEWS_GRAPHQL_OPERATION`
### Frontend
- Delete 15 converter files (`convertGqlView*ToView*`,
`convertView*ToGql`, `convertViewWithRelationsToView`)
- Re-export `ViewType`, `ViewKey`, `ViewFilterGroupLogicalOperator` from
generated enums (no more duplicate enum definitions with different
casing)
- Replace `ViewOpenRecordInType` with `ViewOpenRecordIn` from generated
- Remove `__typename` from all local view sub-types
- Remove unused `variant` from `ViewFilter`, make `displayValue` and
`definition` optional
- Rename ~45 GraphQL query/mutation files and all selectors to drop
"core" prefix
- Delete unused `viewsWithRelationsSelector`
|
||
|
|
ba9aa41bba |
refactor: metadata store cleanup, SSE unification, mock metadata loading & login redirect fix (#18651)
## Summary - **SSE unification**: Replaced 11 individual SSE effect components with a single generic `MetadataStoreSSEEffect` - **Metadata store cleanup**: Merged `metadataCollectionHashesState` into `metadataStoreState` (currentCollectionHash / draftCollectionHash per entity), moved `objectMetadataItemsSelector` to `object-metadata` domain, converted `navigationMenuItemsState` to a derived selector - **Naming clarity**: Renamed `isAppMetadataReadyState` → `isMinimalMetadataReadyState`, `MetadataGater` → `MinimalMetadataGater`, `useIsLogged` → `useHasAccessTokenPair`, `patchMetadataStoreFromSSEEvent` now takes named object params - **Mock metadata loading**: Added `generate-navigation-menu-items.ts` script, rewrote `useLoadMockedMinimalMetadata` to load full objects/fields/indexes/views/navItems from generated mock data, enabling proper sign-in background rendering (table columns, view picker, navigation) - **Login/logout transitions**: `MinimalMetadataLoadEffect` manages mocked↔real metadata transitions based on auth state, `MainContextStoreProvider` computes context on auth pages for view picker support - **Login redirect fix**: `handleLoadWorkspaceAfterAuthentication` now re-enables `isAppEffectRedirectEnabled` after `loadCurrentUser()` completes, fixing the blocked post-login navigation - **Dead code removal**: Deleted `useRefreshPageLayouts`, `useApplyPageLayouts`, `useStaleMetadataEntities`, `metadataCollectionHashesState`, and all individual SSE effects ## Test plan - [x] Login from welcome page redirects to companies page - [x] Logout transitions cleanly to mocked metadata on welcome page - [x] Sign-in background shows table columns, view picker, and navigation items - [x] SSE events still update metadata store entries correctly - [x] Navigation menu items persist across page refreshes - [ ] CI: lint, typecheck, tests pass |
||
|
|
40ff109179 |
feat: migrate objectMetadata reads to granular metadata store (#18643)
## Summary
Consolidates `objectMetadataItems` onto the metadata store as the
**single source of truth**, replacing the previous dual-store approach
(separate `objectMetadataItemsState` atom + untyped
`metadataStoreState`).
### Architecture: three-layer design
```
┌─────────────────────────────────────────────────────────┐
│ Store Layer (granular, typed) │
│ objectMetadataItems → FlatObjectMetadataItem[] │
│ fieldMetadataItems → FlatFieldMetadataItem[] │
│ indexMetadataItems → FlatIndexMetadataItem[] │
└────────────────┬────────────────────────────────────────┘
│ .current (never draft)
┌────────────────▼────────────────────────────────────────┐
│ Selectors (typed read-only) │
│ objectMetadataItemsSelector │
│ fieldMetadataItemsSelector │
│ indexMetadataItemsSelector │
│ metadataStoreStatusFamilySelector │
│ isSystemObjectByNameSingularFamilySelector (narrow) │
│ activeObjectNameSingularsSelector (narrow) │
└────────────────┬────────────────────────────────────────┘
│ joins objects + fields + indexes + permissions
┌────────────────▼────────────────────────────────────────┐
│ Joining Selector │
│ objectMetadataItemsWithFieldsSelector │
│ → produces full ObjectMetadataItem[] with │
│ readableFields / updatableFields from permissions │
│ → 12 existing selectors repointed here │
└─────────────────────────────────────────────────────────┘
```
### Key changes
- **Granular flat types** (`FlatObjectMetadataItem`,
`FlatFieldMetadataItem`, `FlatIndexMetadataItem`) — objects stored
without embedded fields/indexes, matching backend "Flat" naming
convention
- **Typed write API** — `updateDraft` is now generic via
`MetadataEntityTypeMap`, giving compile-time safety on what data shape
goes to each key
- **Write path refactored** — fetch → split into flat entities via
`splitObjectMetadataItemWithRelated` → write to metadata store directly.
No more dual-write through `objectMetadataItemsState`. Permissions
enrichment moved from write path into the joining selector.
- **SSE effects write directly** — `ObjectMetadataItemSSEEffect` and
`FieldMetadataSSEEffect` now patch the store from the SSE event payload
(create/update/delete) instead of triggering a full re-fetch
- **`objectMetadataItemsState` bridge** — converted from writable
`createAtomState` to read-only `createAtomSelector` that delegates to
the joining selector. All 100+ existing consumers continue to work
without code changes.
- **All selectors use Twenty state API** — `createAtomSelector` /
`createAtomFamilySelector` throughout, no raw `atom()`
- **Narrow selectors** for hot paths —
`isSystemObjectByNameSingularFamilySelector` and
`activeObjectNameSingularsSelector` read from flat objects only,
avoiding re-renders when fields/indexes/permissions change. Placed in
`object-metadata/states/` as higher-level business selectors.
- **Test helper** — `setTestObjectMetadataItemsInMetadataStore` for
tests that need to set up composite object metadata through the store
(clearly named as a testing utility)
### Naming conventions
- `ObjectMetadataItemWithRelated` — type for objects with embedded
fields/indexes (input to split utility)
- `FlatObjectMetadataItem` / `FlatFieldMetadataItem` /
`FlatIndexMetadataItem` — granular store types
- Selector names don't expose "Current" — that's an internal detail of
the metadata store API
### Future work
- Optimistic update API (`updateCurrentOptimistically` with rollback)
- Migrate remaining entities (views, pageLayouts, etc.) to the same
pattern
- Gradually remove `objectMetadataItemsState` bridge once all direct
imports are replaced
## Test plan
- [x] `npx nx typecheck twenty-front` passes
- [x] `npx nx lint:diff-with-main twenty-front` passes
- [ ] Verify app loads correctly with metadata from the store
- [ ] Verify SSE updates (object/field changes) propagate correctly
- [ ] Run existing test suites to confirm no regressions
|
||
|
|
602db4ffea |
feat: enable Rich Text as a creatable field type (#18634)
## Summary - Removes `RICH_TEXT` from the excluded/hidden field types in the settings UI so users can create rich text fields on any object (not just Note/Task) - Creates a generic `RichTextFieldEditor` component that uses standard `useUpdateOneRecord` for persistence, decoupled from the Note/Task-specific `ActivityRichTextEditor` - Updates the inline `RichTextFieldInput` and side panel to route to the appropriate editor based on object type (activity editor for Note/Task, generic editor for everything else) ## Details ### Tier 1 — Settings UI unlock - Removed `RICH_TEXT` from `excludedFieldTypes` in `SettingsObjectNewFieldSelect.tsx` - Removed `RICH_TEXT` from `SettingsExcludedFieldType` type union - Added `RICH_TEXT` to `previewableTypes` in `SettingsDataModelFieldSettingsFormCard` ### Tier 2 — Generic inline editing - New `RichTextFieldEditor` — a generic BlockNote editor that works for any object using `useUpdateOneRecord` (no activity-specific coupling) - `RichTextFieldInput` now branches: `ActivityRichTextEditor` for Note/Task, `RichTextFieldEditor` for all other objects - Generalized side panel state (`viewableRichTextComponentState`) from `activityId`/`activityObjectNameSingular` to `recordId`/`objectNameSingular`/`fieldName` - `useOpenRichTextInSidePanel` now accepts an optional `fieldName` parameter ### Tier 3 — Verification - Search: only `markdown` subfield is indexed (correct behavior) - Filters: `RichTextFilter` GraphQL input type already exists - Import/export: `markdown` subfield is already marked `isImportable: true` |
||
|
|
6b48f197d4 |
feat: deprecate WorkspaceFavorite in favor of NavigationMenuItem (#18624)
## Summary - **Removes the entire `modules/favorites/` directory** (~66 files, ~5000 lines deleted) — components, hooks, states, types, utils, tests, and the favorite-folder-picker sub-module - **Eliminates the dual-write pattern** where creating a favorite also created a NavigationMenuItem — all consumers now use `useCreateNavigationMenuItem` directly - **Removes `IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED` feature flag checks** from ~12 files, always taking the NavigationMenuItem code path - **Cleans up backend dual-writes** in `object-metadata.service.ts` and `twenty-standard-application.service.ts` that were creating Favorite records alongside NavigationMenuItems - **Updates prefetch system** to only load NavigationMenuItems (removes favorites prefetch effects and states) - **Cleans up test infrastructure** — updates Storybook decorators, mock data, and graphql mocks to remove favorites references ### What was intentionally kept - **Backend entity definitions** (`FavoriteWorkspaceEntity`, `FavoriteFolderWorkspaceEntity`) — these define the database schema and need a proper database migration to remove - **Cascade deletion listeners** — still needed to clean up existing Favorite data in workspaces that haven't been fully migrated - **v1.18 migration commands** — needed for workspaces upgrading from older versions --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
d9eb317bb5 |
feat: rename RICH_TEXT_V2 → RICH_TEXT in codebase (keep DB value) (#18628)
## Summary - Renames the `FieldMetadataType` enum key from `RICH_TEXT_V2` to `RICH_TEXT` across the entire codebase, while keeping the underlying string value as `'RICH_TEXT_V2'` to maintain PostgreSQL database compatibility - Renames all related types, guards, hooks, components, and files from `*RichTextV2*` / `*rich-text-v2*` to `*RichText*` / `*rich-text*` (e.g. `FormRichTextV2FieldInput` → `FormRichTextFieldInput`, `isFieldRichTextV2` → `isFieldRichText`) - Updates generated files (GraphQL schema, SDK types) to use the new key while preserving the `RICH_TEXT_V2` string value for DB/API layer - Updates i18n locale files, test snapshots, and integration tests to reflect the rename ## Context The legacy `RICH_TEXT` (V1) field type was deprecated and migrated to `TEXT` in a previous PR (#18623). With V1 gone, the `RICH_TEXT_V2` naming is no longer necessary — `RICH_TEXT` is now the canonical name. The DB enum value stays `'RICH_TEXT_V2'` to avoid confusion with the just-deprecated V1 type and to prevent a database migration. ## Test plan - [x] `twenty-server` typecheck passes - [x] `twenty-front` typecheck passes (only pre-existing Apollo client errors remain) - [x] `twenty-server` lint passes - [x] `twenty-front` lint passes - [x] `twenty-shared` build passes - [ ] CI passes Made with [Cursor](https://cursor.com) |
||
|
|
46e515436e |
Deprecate legacy RICH_TEXT field metadata type (#18623)
## Summary - Removes the deprecated `RICH_TEXT` (V1) field metadata type from the codebase entirely - Adds a 1.20 upgrade command that migrates existing `RICH_TEXT` fields to `TEXT` in `core.fieldMetadata` - Cleans up ~70 files across `twenty-shared`, `twenty-server`, `twenty-front`, `twenty-sdk`, and `twenty-zapier` ## Context `RICH_TEXT` was a legacy field type that stored rich text as a single `text` column. It was already **read-only** — writes threw errors directing users to `RICH_TEXT_V2` instead. `RICH_TEXT_V2` is the current approach: a composite type with `blocknote` (editor JSON) and `markdown` subfields. Keeping the deprecated type added maintenance burden without any value. Since the underlying database column type for `RICH_TEXT` was already `text` (same as `TEXT`), the migration only needs to update the metadata — no data migration or column changes required. ## Changes ### Upgrade command (new) - `1-20-migrate-rich-text-to-text.command.ts` — runs `UPDATE core."fieldMetadata" SET "type" = 'TEXT' WHERE "type" = 'RICH_TEXT'` per workspace, with cache invalidation ### Enum & shared types - Removed `RICH_TEXT` from `FieldMetadataType` enum - Removed from `FieldMetadataDefaultValueMapping`, `isFieldMetadataTextKind` ### Server (~30 files) - Removed from type mapper (scalar, filter, order-by), data processors, input transformer, filter operators, zod schemas, column type mapping, searchable fields, RLS matching, OpenAPI schema, fake value generators - Removed from field creation flow and field metadata type validator - Updated dev seeder Pet `bio` field to `TEXT` - Cleaned up mocks, snapshots, integration tests ### Frontend (~25 files) - Deleted: `RichTextFieldDisplay`, `isFieldRichText`, `isFieldRichTextValue`, `useRichTextFieldDisplay` - Removed from `FieldDisplay`, `usePersistField`, `isFieldValueEmpty`, `isRecordMatchingFilter`, `generateEmptyFieldValue`, `isFieldCellSupported`, spreadsheet import, workflow fake values - Removed from settings types, field type configs, and field creation exclusion list - Updated tests, mocks, and stories ### SDK & Zapier - Removed from generated GraphQL schema and TypeScript types - Removed from Zapier `computeInputFields` |
||
|
|
4b6c8d52e5 |
Improve type safety and remove unnecessary store operations (#18622)
## Summary This PR improves type safety across the codebase by replacing generic `any` types with proper TypeScript types, removes unnecessary record store operations, and adds TODO comments for future refactoring of useEffect hooks. ## Key Changes ### Type Safety Improvements - **SettingsAgentTurnDetail.tsx**: Replaced `any` type annotations with proper `AgentMessage` type from generated GraphQL types - **useCreateManyRecords.ts**: Added `RecordGqlNode` type for better type safety when handling mutation responses - **useLazyFindOneRecord.ts**: Replaced generic `Record<string, any>` with `Record<string, RecordGqlNode>` for improved type checking ### Removed Unnecessary Operations - **EventCardCalendarEvent.tsx**: Removed unused `useUpsertRecordsInStore` hook and its associated useEffect that was upserting calendar event records to the store - **EventCardMessage.tsx**: Removed unused `useUpsertRecordsInStore` hook and its associated useEffect that was upserting message records to the store ### Conditional Query Execution - **useLoadCurrentUser.ts**: Made the `FindAllCoreViewsDocument` query conditional - only executes when `isOnAWorkspace` is true, preventing unnecessary queries for users not on a workspace ### Documentation - Added TODO comments in multiple files (`useAgentChatData.ts`, `useWorkspaceFromInviteHash.ts`, `useGetPublicWorkspaceDataByDomain.ts`, `useFindManyRecords.ts`, `useSingleRecordPickerPerformSearch.ts`) referencing PR #18584 for future refactoring of useEffect hooks to avoid unnecessary re-renders ## Implementation Details - The removal of store upsert operations suggests these records are already being managed elsewhere or the operations were redundant - Type improvements maintain backward compatibility while providing better IDE support and compile-time checking - Conditional query execution reduces unnecessary network requests and improves performance for non-workspace users https://claude.ai/code/session_01YQErkoHotMvM6VL3JkWAqV --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
b470cb21a1 |
Upgrade Apollo Client to v4 and refactor error handling (#18584)
## Summary This PR upgrades Apollo Client from v3.10.0 to v4 and refactors error handling patterns across the codebase to use a new centralized `useSnackBarOnQueryError` hook. ## Key Changes - **Dependency Update**: Upgraded `@apollo/client` from `^3.10.0` to `^3.11.0` in root package.json - **New Hook**: Added `useSnackBarOnQueryError` hook for centralized Apollo query error handling with snack bar notifications - **Error Handling Refactor**: Updated 100+ files to use the new error handling pattern: - Removed direct `ApolloError` imports where no longer needed - Replaced manual error handling logic with `useSnackBarOnQueryError` hook - Simplified error handling in hooks and components across multiple modules - **GraphQL Codegen**: Updated codegen configuration files to work with Apollo Client v3.11.0 - **Type Definitions**: Added TypeScript declaration file for `apollo-upload-client` module - **Test Updates**: Updated test files to reflect new error handling patterns ## Notable Implementation Details - The new `useSnackBarOnQueryError` hook provides a consistent way to handle Apollo query errors with automatic snack bar notifications - Changes span across multiple feature areas: auth, object records, settings, workflows, billing, and more - All changes maintain backward compatibility while improving code maintainability and reducing duplication - Jest configuration updated to work with the new Apollo Client version https://claude.ai/code/session_019WGZ6Rd7sEHuBg9sTrXRqJ --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
6a3281a18d |
Bug fix batches (#18588)
- clear sse state on logout - fix no record not selectable through keyboard - fix book a call design - fix error notif design |
||
|
|
cb3e32df86 |
Fix AI demo workspace skill (#18575)
This PR fixes what allows to have a working demo workspace skill. - Skill updated many times into something that works - Fixed infinite loop in AI chat by memoizing ai-sdk output - Finished navigateToView implementation - Increased MAX_STEPS to 300 so the chat don't quit in the middle of a long running skill - Added CreateManyRelationFields |
||
|
|
db5b4d9c6c |
fix: replace unsafe JSON.parse casts with parseJson in filter dropdowns (#18513)
## Problem Four filter dropdown components were calling `JSON.parse(filter.value) as string[]` to parse stored filter state. This throws a `SyntaxError` if the value is malformed (truncated URL, stale localStorage, migration artifact), crashing the entire dropdown with no recovery. ## Solution Replace with the existing `parseJson<string[]>` utility from `twenty-shared`, which wraps `JSON.parse` in a try/catch and returns `null` on failure. The `?? []` fallback gracefully degrades to an empty selection instead of crashing. All four files had an explicit `// TODO: replace by a safe parse` marking this as a known issue. ## Testing No new tests — `parseJson` is already tested in `twenty-shared`. No new logic introduced. ## issue link #18514 |
||
|
|
a024a04e01 |
Fix breadcrumb infinite loop (#18561)
`RecordTableNoRecordGroupScrollToPreviousRecordEffect` uses `useAtomState(lastShowPageRecordIdState)` to read the atom value and check whether to trigger an effect. Inside `run()`, it calls `setLastShowPageRecordId(null)` to reset the atom, then` triggerInitialRecordTableDataLoad()` which fires many `store.set()` calls on other atoms. These high-frequency store updates cause the component to re-render before Jotai's internal useReducer dispatch (propagating the null value) is processed by React. The result: useAtomState returns a stale non-null value on every subsequent render, even though the Jotai store already holds null. The effect re-runs, sees the stale non-null value, calls `run()` again, creating an infinite loop. This is a Jotai v2 edge case where useAtom's rendered value desyncs from the actual store value under high-frequency concurrent updates. ### The fix Read lastShowPageRecordId directly from the Jotai store via `store.get()` inside the effect instead of relying on the rendered value from useAtomState. This guarantees the effect always sees the true store value and correctly skips when the atom is null. |
||
|
|
b346f4fb59 |
Add common loader (#18556)
To avoid white screens on reload, building a shared skeleton. Before https://github.com/user-attachments/assets/42bd0667-141d-4df4-9072-4077192cc71d After https://github.com/user-attachments/assets/e6031a72-2e25-47e3-a873-b89aaddfbd3a |
||
|
|
00c3cd1051 |
Add system view fallback (#18536)
## Context The goal is to add a "See records" button in all objects that would redirect to that view (this will be done in a later PR). See screenshot below. <img width="665" height="312" alt="Screenshot 2026-03-10 at 15 54 36" src="https://github.com/user-attachments/assets/6e23a75b-cff0-4d93-bce8-b5481b05c6f6" /> ## Implementation - If a view does not exist on an object, there is a **temporary** fallback where the frontend creates the missing view as a custom view when going over the object index page - System objects are now surfaced but we don't want their records to be editable, they will be readonly (mostly, all fields will be non-editable except for their custom fields). - We can't create a new record of a system object, some actions are also hidden. - The backend now rejects if you are trying to delete the last view of an object |
||
|
|
1d95670252 |
Fix form field select + form field number (#18538)
Before <img width="398" height="138" alt="Capture d’écran 2026-03-10 à 16 23 19" src="https://github.com/user-attachments/assets/c28c0a7f-6911-4c77-a45c-42a79073a92a" /> After <img width="398" height="138" alt="Capture d’écran 2026-03-10 à 16 32 56" src="https://github.com/user-attachments/assets/b93d2fe9-5928-4803-8d5e-30a0a9eeb28f" /> Addition: - hover on forget password - settings field width |
||
|
|
2de022afcf |
Add standard command menu items (#18527)
## Add standard command menu items
### Summary
This PR introduces standard command menu items, migrating hardcoded
command menu actions to the backend command menu item architecture
powered by front components. It adds a new `twenty-standard-application`
package that defines, builds, and registers front components as standard
command menu items, gated behind the `IS_COMMAND_MENU_ITEM_ENABLED`
feature flag.
### Description
- **New `twenty-standard-application` package**: Contains front
component definitions with an esbuild-based build pipeline that
generates minified `.mjs` bundles and a manifest with checksums.
- **Server-side registration**: New constants register all items with
metadata (labels, icons, positions, availability types, conditional
expressions). A `StandardFrontComponentUploadService` uploads built
components to file storage.
- **`FALLBACK` availability type**: New enum value for command menu
items that appear as fallback options (e.g., "Search Records" fallback).
- **`CommandMenuContextApi` refactor**
- **Conditional availability enhancements**: New array-based helper
functions for evaluating multi-record conditions.
- **Frontend wiring** (twenty-front):
`useCommandMenuItemFrontComponentCommands`
## Next steps
Only simple commands have been implemented for now:
- **Navigation (9)** -- `CommandLink`: go-to-companies,
go-to-dashboards, go-to-notes, go-to-opportunities, go-to-people,
go-to-runs, go-to-settings, go-to-tasks, go-to-workflows
- **Side panel (4)** -- `CommandOpenSidePanelPage`: ask-ai,
search-records, search-records-fallback, view-previous-ai-chats
We still have to implement front components for all the following
commands:
All have placeholder `execute` logic (`async () => {}`) with a `// TODO:
implement execute logic` comment:
**Record (22)**
- `add-to-favorites`, `remove-from-favorites`
- `create-new-record`, `create-new-view`
- `delete-single-record`, `delete-multiple-records`
- `destroy-single-record`, `destroy-multiple-records`
- `restore-single-record`, `restore-multiple-records`
- `export-from-record-index`, `export-from-record-show`,
`export-multiple-records`, `export-note-to-pdf`, `export-view`
- `hide-deleted-records`, `see-deleted-records`
- `import-records`, `merge-multiple-records`, `update-multiple-records`
- `navigate-to-next-record`, `navigate-to-previous-record`
**Page layout (3)** -- `cancel-record-page-layout`,
`edit-record-page-layout`, `save-record-page-layout`
**Dashboard (4)** -- `cancel-dashboard-layout`, `duplicate-dashboard`,
`edit-dashboard-layout`, `save-dashboard-layout`
**Workflow (10)** -- `activate-workflow`, `add-node-workflow`,
`deactivate-workflow`, `discard-draft-workflow`, `duplicate-workflow`,
`see-active-version-workflow`, `see-runs-workflow`,
`see-versions-workflow`, `test-workflow`, `tidy-up-workflow`
**Workflow version (4)** -- `see-runs-workflow-version`,
`see-versions-workflow-version`, `see-workflow-workflow-version`,
`use-as-draft-workflow-version`
**Workflow run (3)** -- `see-version-workflow-run`,
`see-workflow-workflow-run`, `stop-workflow-run`
|
||
|
|
25d9f2fcce |
fix: respect number format in currency input (#18469)
Fixed #18355 Currency fields ignored the workspace number format when editing: display showed e.g. 5 982,77 € (French style) but the input forced US style (5,982.77) and rejected comma as decimal. Fix: CurrencyInput now uses useNumberFormat() and passes the correct thousandsSeparator and radix to the IMask input so edit mode matches the chosen format (comma/space, dot/comma, etc.). Files: CurrencyInput.tsx (use format for mask), new CurrencyInput.test.tsx . --------- Co-authored-by: root <root@dragon.second> Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com> |
||
|
|
7b9939b43e |
fix: validate input before formatting in MultiItemFieldInput (#18334)
Reorder validateInput to run before formatInput to prevent parsePhoneNumber from throwing INVALID_COUNTRY on bad input. Fixes TWENTY-FRONT-5RQ /closes https://github.com/twentyhq/twenty/issues/17670 --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
dd58eb6814 |
Fix linaria css regressions (#18492)
before <img width="310" height="60" alt="SCR-20260309-ctul" src="https://github.com/user-attachments/assets/7141d495-b8f2-4fd6-bf3b-36bb2b11d1aa" /> after (fixed icon alignment) <img width="235" height="67" alt="SCR-20260309-ctel" src="https://github.com/user-attachments/assets/36078039-93dc-4c2c-b553-0bbcde4cb81c" /> before <img width="637" height="318" alt="SCR-20260309-ctnp" src="https://github.com/user-attachments/assets/34b66129-d619-43a2-8896-aa92b511644e" /> after (fixed chart colors) <img width="650" height="317" alt="SCR-20260309-cthj" src="https://github.com/user-attachments/assets/82c095b1-34bb-4ae4-a8f2-7a3746a31b0a" /> before <img width="909" height="650" alt="image" src="https://github.com/user-attachments/assets/14649aed-bfa8-4b9d-aa35-f4de2bfaddd6" /> after (fixed buttons text color) <img width="930" height="646" alt="SCR-20260309-csob" src="https://github.com/user-attachments/assets/c724a849-dabe-406c-8258-0674211374f2" /> before <img width="544" height="141" alt="image" src="https://github.com/user-attachments/assets/815c3b70-2f7c-42ca-8a32-3fbd5fe4c556" /> after (fixed missing border on :active state) <img width="554" height="145" alt="image" src="https://github.com/user-attachments/assets/845b1afd-36b6-4ae4-b6ef-c49ccbd89c10" /> --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
033d297695 |
feat: add visual time picker to DateTimePicker (#15057) (#17952)
## Summary Added a visual time picker dropdown for DateTime fields, replacing the previous text input. Users can now select hours and minutes through an intuitive scrollable interface. (Fixes #15057 ## Changes - **New component**: Add a `TimePickerDropdown` - Visual picker with scrollable hour/minute columns - **Updated**: `DateTimePickerHeader` - Implemented time picker dropdown in `DateTimePickerHeader` and Move month/year picker to right side ## Snapshots <img width="493" height="421" alt="image" src="https://github.com/user-attachments/assets/3bd1f0a0-0ac2-473d-935e-d9f28b0e40e2" /> https://github.com/user-attachments/assets/daa5cba5-c86c-46aa-a634-0f5c04523af1 **If there is no enough place at right, auto-move month/year selector to the left side** Hi, @Bonapara I followed the Figma you shared to complete this feature. Could you please review it for me? Thanks a lot. --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com> |
||
|
|
dee55b635f |
Table refactor : removed z-index dynamic logic completely and flex-wrap (#18466)
This PR removes the leftovers from the z-index dynamic logic removal. It also removes the flex-wrap mechanism that was used to have all the cells in the same div, and instead creates a container for each part of the table : header, body and footer, so that z-index management becomes straighforward. We also fix some minor bugs. ## Demo https://github.com/user-attachments/assets/29dc4966-376d-4eb1-9e37-99769e77f4f4 https://github.com/user-attachments/assets/78218517-812a-4531-84c3-067700b46b59 |
||
|
|
1656bb5568 |
[Feat] : add source to actor fields (#18118)
fixes #18099 Simple implementation of the matchingSourceValues to be searched in the ACTOR case in turnRecordFilterIntoRecordGqlOperationFilter <img width="1239" height="494" alt="Screenshot 2026-02-20 at 5 21 14 PM" src="https://github.com/user-attachments/assets/20ee076e-dccd-4747-a1ab-38d649f6591e" /> --------- Co-authored-by: Arun kumar <arunkumar@Aruns-MacBook-Air.local> Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com> |
||
|
|
82a1179e23 |
Fix empty record index page (#18500)
Before <img width="1507" height="851" alt="Capture d’écran 2026-03-09 à 15 27 17" src="https://github.com/user-attachments/assets/6f93686d-a3f9-4fb1-a02e-d6b1a8347120" /> After <img width="1507" height="851" alt="Capture d’écran 2026-03-09 à 15 26 53" src="https://github.com/user-attachments/assets/68a44260-0332-48c7-92bf-7bef466a7a05" /> |
||
|
|
36bcc71f3d |
refactor(command-menu-item): rename Actions to CommandMenuItem (#18489)
actions are being renamed to command menu item, they will be migrated to server and will be served as headless front components --------- Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com> |
||
|
|
5b28e59ca7 | Navbar drag drop using dnd kit (#18288) | ||
|
|
fea47aa9f8 |
Add twenty/folder-structure custom oxlint rule (#18467)
## Summary
- Re-implements `eslint-plugin-project-structure`'s folder structure
enforcement as a custom oxlint rule (`twenty/folder-structure`),
recovering functionality lost during the ESLint → Oxlint migration
- Validates `src/modules/` structure: kebab-case module folder names,
allowed subdirectories (hooks, utils, components, states, types,
graphql, etc.), hook file naming (`use{PascalCase}.(ts|tsx)`), util file
naming (`{camelCase}.(ts|tsx)`), and module nesting depth (max 4 levels)
- Enabled as `"warn"` in twenty-front with 403 pre-existing violations
to address incrementally
## What the rule checks
| Check | Example valid | Example invalid |
|-------|-------------|-----------------|
| Module names kebab-case | `object-record/` | `graphWidgetBarChart/` |
| Allowed subdirs only | `hooks/`, `components/`, `utils/` |
`random-stuff/` |
| Hook file naming | `useMyHook.ts` | `badName.ts` |
| Util file naming | `buildQuery.ts` | `build-query.ts` |
| Max nesting depth 4 | `a/b/c/d/hooks/` | `a/b/c/d/e/hooks/` |
| Utils kebab-case subfolders | `utils/cron-to-human/` |
`utils/camelCase/` |
## Pre-existing violations (403 total)
| Category | Count | Examples |
|----------|-------|---------|
| Non-kebab-case module names | 160 | `graphWidgetBarChart`,
`AIChatThreads` |
| Module depth > 4 | 215 |
`settings/roles/role-permissions/object-level-permissions/field-permissions`
|
| Util file naming | 22 | `.util.ts` suffix, kebab-case, PascalCase
filenames |
| Misc (hooks, tests) | 6 | Non-hook files in hooks/, folders in test
dirs |
|
||
|
|
73268535dc |
Added record filter hidden fields in query (#18149)
Fixes https://github.com/twentyhq/twenty/issues/17506 Hidden fields are now queried when they are in record filters, to avoid optimistic and filtering bugs with hidden fields. |
||
|
|
faee5ee63d |
fix: morph relation persist uses wrong foreign key naming, producing invalid field parentObjectId. (#18352)
Solves [Sonarly Issue 8116](https://sonarly.com/issue/8116). ### Problem Editing a morph relation field (e.g. "Parent Object" on Task) via the field widget was broken in two ways: 1. **Setting a value** sent the wrong foreign key name (`parentObjectId` instead of target-specific keys like `parentObjectCompanyId`), causing the relation to not save. 2. **Detaching** never sent a request at all — the early return check `valueToPersist?.id === currentValue?.id` evaluated to `undefined === undefined` when the morph field wasn't loaded in the store, silently skipping the update. The record detail section worked fine because it uses a separate hook (`useMorphPersistManyToOne`). ### Fix Added proper morph relation handling in `usePersistField` so all persistence goes through this single hook consistently: - Compute the correct FK name using `computeMorphRelationFieldName` (e.g. `parentObjectCompanyId`) instead of deriving it from the field name directly. - Null all morph FK columns before setting the target one, ensuring only one FK is non-null at a time (consistent with `useMorphPersistManyToOne`). - Fix the early return to only skip when **setting** a value that matches the current one — detach always proceeds. - Derive `currentRelationId` via a type guard instead of an `as` cast. |
||
|
|
cac4999e9f |
fix: handle Escape in date/datetime pickers and remove ValidationStep any (#18107)
## Summary - **DatePicker / DateTimePicker:** Call onEscape when user presses Escape (fixes FIXME). - **FormDateFieldInput:** Revert input/picker on Escape; handle Escape in text input. - **FormDateTimeFieldInput:** Remove FIXME. - **ValidationStep:** Replace any with typed callback. --------- Co-authored-by: Thomas Trompette <thomas.trompette@sfr.fr> |
||
|
|
ef499b6d47 |
Re-enable disabled lint rules and right-size CI runners (#18461)
## Summary - Re-enable one lint rule that was temporarily disabled during the ESLint-to-Oxlint migration: - **`twenty/sort-css-properties-alphabetically`** in twenty-front — 578 violations auto-fixed across 390 files - Document why **`typescript/consistent-type-imports`** cannot be auto-fixed in twenty-server: NestJS relies on `emitDecoratorMetadata` for DI, so converting constructor parameter imports to `import type` erases them at compile time and breaks dependency injection at runtime - Right-size CI runners, reducing 8-core usage from 18 jobs to 3: | Change | Jobs | Rationale | |--------|------|-----------| | **Keep 8-core** | `ci-merge-queue/e2e-test`, `ci-front/front-sb-build`, `ci-front/front-build` | Heavy builds needing max CPU + memory (10GB NODE_OPTIONS, full Storybook webpack bundling) | | **8-core → 4-core** | `ci-server` (build, lint-typecheck, validation, test, integration-test), `ci-front/front-sb-test`, `ci-zapier/server-setup`, `ci-sdk/sdk-e2e-test` | Already sharded into 10-12 parallel instances, I/O-bound (DB/Redis), or moderate single builds | | **8-core → 2-core** | `ci-emails/emails-test` | Trivially lightweight (build + curl health check) | | **Removed** | `ci-front/front-chromatic-deployment` | Dead code — permanently disabled with `if: false` | - Fix merge queue CI issues: - **Concurrency**: Use `merge_group.base_ref` instead of unique merge group ref so new queue entries cancel previous runs - **Required status checks**: Add `merge_group` trigger to all 6 required CI workflows (front, server, shared, website, docker-compose, sdk) with `changed-files-check` auto-skipped for merge_group events — status check jobs auto-pass without re-running full CI - **Build caching**: Add Nx build cache restore/save to E2E test job with fallback to `main` branch cache for faster frontend and server builds ## Test plan - [ ] CI passes on this PR (verifies lint rule auto-fix works) - [ ] Verify 4-core runner jobs complete within their 30-minute timeouts - [ ] Verify merge queue status checks auto-pass (ci-front-status-check, ci-server-status-check, etc.) - [ ] Verify merge queue E2E concurrency cancels previous runs when a new PR enters the queue |