602db4ffeac4716957f7c298f5512bccde5a8023
422 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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` |
||
|
|
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` |
||
|
|
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> |
||
|
|
045faf018a |
Design fixes batch post linaria migration (#18509)
- Currency input - Edit email button full height - Full width ai field - Missing borders - Cmd+K icon button centered |
||
|
|
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 |
|
||
|
|
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 |
||
|
|
9d57bc39e5 |
Migrate from ESLint to OxLint (#18443)
## Summary Fully replaces ESLint with OxLint across the entire monorepo: - **Replaced all ESLint configs** (`eslint.config.mjs`) with OxLint configs (`.oxlintrc.json`) for every package: `twenty-front`, `twenty-server`, `twenty-emails`, `twenty-ui`, `twenty-shared`, `twenty-sdk`, `twenty-zapier`, `twenty-docs`, `twenty-website`, `twenty-apps/*`, `create-twenty-app` - **Migrated custom lint rules** from ESLint plugin format to OxLint JS plugin system (`@oxlint/plugins`), including `styled-components-prefixed-with-styled`, `no-hardcoded-colors`, `sort-css-properties-alphabetically`, `graphql-resolvers-should-be-guarded`, `rest-api-methods-should-be-guarded`, `max-consts-per-file`, and Jotai-related rules - **Migrated custom rule tests** from ESLint `RuleTester` + Jest to `oxlint/plugins-dev` `RuleTester` + Vitest - **Removed all ESLint dependencies** from `package.json` files and regenerated lockfiles - **Updated Nx targets** (`lint`, `lint:diff-with-main`, `fmt`) in `nx.json` and per-project `project.json` to use `oxlint` commands with proper `dependsOn` for plugin builds - **Updated CI workflows** (`.github/workflows/ci-*.yaml`) — no more ESLint executor - **Updated IDE setup**: replaced `dbaeumer.vscode-eslint` with `oxc.oxc-vscode` extension, configured `source.fixAll.oxc` and format-on-save with Prettier - **Replaced all `eslint-disable` comments** with `oxlint-disable` equivalents across the codebase - **Updated docs** (`twenty-docs`) to reference OxLint instead of ESLint - **Renamed** `twenty-eslint-rules` package to `twenty-oxlint-rules` ### Temporarily disabled rules (tracked in `OXLINT_MIGRATION_TODO.md`) | Rule | Package | Violations | Auto-fixable | |------|---------|-----------|-------------| | `twenty/sort-css-properties-alphabetically` | twenty-front | 578 | Yes | | `typescript/consistent-type-imports` | twenty-server | 3814 | Yes | | `twenty/max-consts-per-file` | twenty-server | 94 | No | ### Dropped plugins (no OxLint equivalent) `eslint-plugin-project-structure`, `lingui/*`, `@stylistic/*`, `import/order`, `prefer-arrow/prefer-arrow-functions`, `eslint-plugin-mdx`, `@next/eslint-plugin-next`, `eslint-plugin-storybook`, `eslint-plugin-react-refresh`. Partial coverage for `jsx-a11y` and `unused-imports`. ### Additional fixes (pre-existing issues exposed by merge) - Fixed `EmailThreadPreview.tsx` broken import from main rename (`useOpenEmailThreadInSidePanel`) - Restored truthiness guard in `getActivityTargetObjectRecords.ts` - Fixed `AgentTurnResolver` return types to match entity (virtual `fileMediaType`/`fileUrl` are resolved via `@ResolveField()`) ## Test plan - [x] `npx nx lint twenty-front` passes - [x] `npx nx lint twenty-server` passes - [x] `npx nx lint twenty-docs` passes - [x] Custom oxlint rules validated with Vitest: `npx nx test twenty-oxlint-rules` - [x] `npx nx typecheck twenty-front` passes - [x] `npx nx typecheck twenty-server` passes - [x] CI workflows trigger correctly with `dependsOn: ["twenty-oxlint-rules:build"]` - [x] IDE linting works with `oxc.oxc-vscode` extension |
||
|
|
c53a13417e |
Remove all styled(Component) patterns in favor of parent wrappers and props (#18430)
## Summary
Eliminates all ~350 `styled(Component)` usages across `twenty-front` and
`twenty-ui` (212 files changed). Each was replaced following these
rules:
- **Margin/layout CSS** (margin, padding, flex, align-self, width) →
wrapped in a `styled.div`/`styled.span` parent container
- **Third-party components** (Link, TextareaAutosize,
ReactPhoneNumberInput, Handle, etc.) → parent container with child CSS
selectors (`> a`, `> textarea`, `> input`, etc.)
- **Intrinsic behavior via existing props** (TableRow
`gridTemplateColumns`, TableCell `color`/`align`) → replaced
`styled(TableRow)` / `styled(TableCell)` with direct prop usage
- **Other visual overrides on twenty-ui components** (Card, Section,
TabList, Button, MenuItem, ScrollWrapper, etc.) → parent wrappers with
`> div` / `> *` child selectors
- **Extending styled.div/span** → merged all CSS into a single
`styled.div`/`styled.span`
Also adds `overflow: hidden` to parent containers wrapping
`ScrollWrapper` so scroll activates correctly with the new wrapper
structure.
### Migration patterns
| Before | After |
|--------|-------|
| `styled(Avatar)` with `margin-right` | `<StyledAvatarContainer><Avatar
/></StyledAvatarContainer>` |
| `styled(Link)` with `text-decoration: none` |
`<StyledLinkContainer><Link /></StyledLinkContainer>` with `> a { ... }`
|
| `styled(TableRow)` with `grid-template-columns` | `<TableRow
gridTemplateColumns="..." />` |
| `styled(TableCell)` with `color` / `align` | `<TableCell color={...}
align="right" />` |
| `styled(Card)` with `margin-top` | `<StyledCardContainer><Card
/></StyledCardContainer>` |
| `styled(TabList)` with `background` |
`<StyledTabListContainer><TabList /></StyledTabListContainer>` with `>
div { ... }` |
| `styled(StyledBase)` extending a `styled.div` | Single merged
`styled.div` with all styles inlined |
|
||
|
|
647c32ff3e |
Deprecate runtime theme objects in favor of CSS variables (#18402)
## Summary
- **Eliminate `ICON_SIZES` / `ICON_STROKES` constants**: all icon
dimensions are now resolved at runtime via
`resolveThemeVariableAsNumber(themeCssVariables.icon.size.X)`, ensuring
values always come from computed CSS variables
- **No more consumer imports from `twenty-ui/theme`**: moved
`ColorSchemeContext`, `ColorSchemeProvider`, `ThemeColor`,
`MAIN_COLOR_NAMES`, `getNextThemeColor`, `AnimationDuration` to
`twenty-ui/theme-constants`
- **Remove `ThemeContext` / `ThemeContextProvider` / `ThemeProvider` /
`ThemeType`**: replaced across ~300 files with `themeCssVariables` (for
CSS contexts) or `resolveThemeVariable` / `resolveThemeVariableAsNumber`
(for JS runtime values)
- **Simplify provider chain**: only `ColorSchemeProvider` remains — it
toggles `light`/`dark` class on `document.documentElement` and provides
`colorScheme` via React context
- **Fix pre-existing test failures**: `useIcons.test.ts`
(non-configurable ES module spy) and
`turnRecordFilterGroupIntoGqlOperationFilter.test.ts`
(`Omit<RecordFilter, 'id'>` type mismatch)
### Theme access pattern (before → after)
| Context | Before | After |
|---------|--------|-------|
| CSS (Linaria) | `${({ theme }) => theme.font.color.primary}` |
`${themeCssVariables.font.color.primary}` |
| JS runtime (icon size, animation) | `theme.icon.size.md` /
`ICON_SIZES.md` |
`resolveThemeVariableAsNumber(themeCssVariables.icon.size.md)` |
| Color scheme check | `theme.name === 'dark'` |
`useContext(ColorSchemeContext).colorScheme === 'dark'` |
|
||
|
|
eda905f271 |
[DevXP] Improve Linaria pre-build speed (#18382)
## Summary This PR improves Linaria/WYW pre-build speed and continues the migration of `twenty-ui` components away from runtime `ThemeContext` reads toward static CSS variables and theme constants. ### Linaria/WYW profiling plugin improvements (`twenty-shared`) - **Babel JIT warmup**: added a `buildStart` warmup step that triggers WYW's Babel JIT compilation before the real build starts, so the first real file doesn't pay the cold-start penalty - **`configResolved` hook**: detects dev vs prod mode and resolves the correct warmup file path relative to `config.root` - **Dev-only per-file logging**: slow file warnings are now gated behind `isDevMode`, keeping production/CI build output clean - **`closeBundle` summary**: moved the final top-slow-files report to `closeBundle` for accurate end-of-build reporting - **Removed noisy progress interval logging** in favor of the warmup log + final summary ### Migration from `ThemeContext` to static CSS variables / constants Across `twenty-ui`, replaced runtime `useTheme()` reads with: - `themeCssVariables` CSS custom properties (colors, spacing) - Hard-coded design-system constants (`ICON.size.md` → `16`, `ICON.stroke.sm` → `1.6`) so components no longer need a React context at render time — enabling Linaria static extraction **Components migrated:** - `Button`, `AnimatedButton`, `LightButton`, `LightIconButton`, `AnimatedLightIconButton`, `ButtonIcon`, `ButtonSoon` - `ProgressBar` (Framer Motion width animation → CSS `transition`) - `Info`, `HorizontalSeparator`, `LinkChip` - `MenuPicker`, `MenuItemLeftContent`, `MenuItemIconWithGripSwap`, `NavigationBarItem` - `JsonArrow`, `JsonNestedNode` - `ModalHeader` ### Other - Added `aria-valuenow` to `ProgressBar` for accessibility - `VisibilityHidden` component updated to inline accessibility styles |
||
|
|
b11f77df2a |
[FRONT COMPONENTS] Introduce conditionalAvailabilityExpression to command menu items (#18319)
## PR Description - Uses `expr-eval` to enable front components (SDK plugins) to define conditional availability as declarative expressions. - Moves shared types and constants to `twenty-shared` - Introduces a `conditionalAvailabilityExpression` field on `CommandMenuItemEntity`, allowing command menu items to store an `expr-eval` compatible expression string that is evaluated against a CommandMenuContext to determine if the item should be shown. - Creates an esbuild transform plugin `conditional-availability-transform-plugin` in `twenty-sdk` that converts TypeScript conditional availability expressions into `expr-eval` compatible syntax at build time, so SDK developers can write natural TS expressions that get transformed to evaluable strings. - Removes deprecated `forceRegisteredActionsByKey` state and its usage. - Creates `useCommandMenuContext` hook that builds the full `CommandMenuContext` object from React state, which is then passed to `useCommandMenuItemFrontComponentActions` for evaluating conditional availability expressions. |
||
|
|
3b2bf39565 |
Refactor modal (#18377)
## Summary - Move Modal UI components (`Modal`, `ModalContent`, `ModalHeader`, `ModalFooter`, `ModalBackdrop`) from `twenty-front` to `twenty-ui` as stateless, reusable components - Create `ModalStatefulWrapper` in `twenty-front` that connects Jotai state (`isModalOpenedComponentState`) to the stateless `Modal` via an `isOpen` prop - Rename `modalVariant` prop to `overlay` with clearer values: `'dark'` (default), `'light'` (in-container), `'transparent'` (invisible panel). Remove unused `'medium'` overlay - Rename `modalId` to `modalInstanceId` across the entire modal zone (~30 consumer files) - Extract `ModalProps` to its own file in `twenty-ui/types/ModalProps.ts`; extract `ModalStatefulWrapperProps` to its own file using `Pick<ModalProps, ...>` for shared props - Extract `ModalBackdrop` to its own file and export from `twenty-ui`; use it in `UserOrMetadataLoader` instead of a local styled component - Use `ModalFooter` in `StepNavigationButton` and `ModalHeader` in `SpreadsheetImportStepperContainer` instead of duplicated `styled.div` definitions - Remove unused `onClose` prop from stateless `Modal`; fix `typeof document` guard in `ModalStatefulWrapper` - Split shared types into individual files: `ModalSize.ts`, `ModalPadding.ts`, `ModalOverlay.ts` - Extract wyw profiling instrumentation from `vite.config.ts` into reusable `createWywProfilingPlugin` with parametrized threshold and improved logging - Delete old `Modal.tsx`, `Modal.styles.ts`, `ModalContent.tsx`, `ModalHeader.tsx`, `ModalFooter.tsx` from `twenty-front` - Add comprehensive Storybook stories in `twenty-ui` covering Default, Confirmation, Small, ExtraLarge, Closed, and Interactive variants |
||
|
|
7a2e397ad1 |
Complete linaria migration (#18361)
## Summary
Completes the migration of the frontend styling system from **Emotion**
(`@emotion/styled`, `@emotion/react`) to **Linaria** (`@linaria/react`,
`@linaria/core`), a zero-runtime CSS-in-JS library where styles are
extracted at build time.
This is the final step of the migration — all ~494 files across
`twenty-front`, `twenty-ui`, `twenty-website`, and `twenty-sdk` are now
fully converted.
## Changes
### Styling Migration (across ~480 component files)
- Replaced all `@emotion/styled` imports with `@linaria/react`
- Converted runtime theme access patterns (`({ theme }) => theme.x.y`)
to build-time `themeCssVariables` CSS custom properties
- Replaced `useTheme()` hook (from Emotion) with
`useContext(ThemeContext)` where runtime theme values are still needed
(e.g., passing colors to non-CSS props like icon components)
- Removed `@emotion/react` `css` helper usages in favor of Linaria
template literals
### Dependency & Configuration Changes
- **Removed**: `@emotion/react`, `@emotion/styled` from root
`package.json`
- **Added**: `@wyw-in-js/babel-preset`, `next-with-linaria` (for
twenty-website SSR support)
- Updated Nx generator defaults from `@emotion/styled` to
`@linaria/react` in `nx.json`
- Simplified `vite.config.ts` (removed Emotion-specific configuration)
- Updated `twenty-website/next.config.js` to use `next-with-linaria` for
SSR Linaria support
### Storybook & Testing
- Removed `ThemeProvider` from Emotion in Storybook previews
(`twenty-front`, `twenty-sdk`)
- Now relies solely on `ThemeContextProvider` for theme injection
### Documentation
- Removed the temporary `docs/emotion-to-linaria-migration-plan.md`
(migration complete)
- Updated `CLAUDE.md` and `README.md` to reflect Linaria as the styling
stack
- Updated frontend style guide docs across all locales
## How it works
Linaria extracts styles at build time via the `@wyw-in-js/vite` plugin.
All expressions in `styled` template literals must be **statically
evaluable** — no runtime theme objects or closures over component state.
- **Static styles** use `themeCssVariables` which map to CSS custom
properties (`var(--theme-color-x)`)
- **Runtime theme access** (for non-CSS use cases like icon `color`
props) uses `useContext(ThemeContext)` instead of Emotion's `useTheme()`
|
||
|
|
1db2a40961 |
Migrate twenty ui to linaria (#18307)
## Migrate twenty-ui from Emotion to Linaria
Completes the migration of all `twenty-ui` components from Emotion
(runtime CSS-in-JS) to Linaria (zero-runtime, CSS extracted at build
time).
- Replaced `@emotion/styled` with `@linaria/react` across ~170 files
- Removed all Emotion dependencies from `twenty-ui`
- Introduced a CSS custom properties-based theme system:
`themeCssVariables` where every leaf is a `var(--t-xxx)` reference,
injected onto `document.documentElement` by
`ThemeCssVariableInjectorEffect`
- No more `theme` prop threading — styled components reference
`themeCssVariables.x.y` directly at build time
- Updated `twenty-front` consumers to remove `theme={theme}` prop
passing
**Before / After:**
```tsx
// Emotion
color: ${({ theme }) => theme.font.color.primary};
padding: ${({ theme }) => theme.spacing(4)};
// Linaria
color: ${themeCssVariables.font.color.primary};
padding: ${themeCssVariables.spacing[4]};
```
### Theme architecture
Two build-time utilities produce the theme system:
- **`buildThemeReferencingRootCssVariables`** — walks the theme object
and builds a nested mirror where every leaf is a `var(--t-xxx)` string
(evaluated at build time by wyw-in-js)
- **`prepareThemeForRootCssVariableInjection`** — walks the runtime
theme and collects flat `[--css-variable-name, value]` pairs, injected
onto `document.documentElement` by `ThemeCssVariableInjectorEffect`
Both share naming conventions (`camelToKebab`, `SPACING_VALUES`,
`formatSpacingKey`) and are unit tested.
### Spacing cleanup
Spacing scale now uses integers 0–32 (generated via loop), with `0.5`
and `1.5` as the only fractional exceptions. All other fractional
spacing usages (`0.25`, `0.75`, `1.25`, `2.5`, `3.5`) were replaced with
literal pixel values across ~20 twenty-front files.
### Framer Motion integration
Linaria doesn't support `styled(motion.div)` — wrapping a motion element
with `styled()` causes the component body to be stripped at build time.
Instead, we define the styled component first, then wrap it with
`motion.create()`:
```tsx
const StyledBarBase = styled.div`
background-color: ${themeCssVariables.font.color.primary};
height: 100%;
`;
const StyledBar = motion.create(StyledBarBase);
```
### Block interpolations
Linaria doesn't support interpolations that return multiple CSS
declarations (Linaria wraps the entire block in a single `var()`,
producing invalid CSS). These were split into individual property
interpolations:
```tsx
// Emotion — single interpolation returning multiple declarations
border-left: ${({ divider, theme }) => {
const border = `1px solid ${theme.border.color.light}`;
return divider ? `border-${divider}: ${border}` : '';
}}
// Linaria — one interpolation per property
border-left: ${({ divider }) =>
divider === 'left' ? `1px solid ${themeCssVariables.border.color.light}` : 'none'};
border-right: ${({ divider }) =>
divider === 'right' ? `1px solid ${themeCssVariables.border.color.light}` : 'none'};
```
### Dynamic styles via CSS variables
When a component needs to compute styles from multiple props with
complex branching logic (e.g. `Button` combining `variant`, `accent`,
`inverted`, `disabled`, `focus`, `position`), Linaria's prop
interpolations become unwieldy. In those cases we use a
`computeDynamicStyles` function that returns a `CSSProperties` object
injected via `style={}`, referenced from the static CSS with `var()`:
```tsx
const StyledButton = styled.button`
background: var(--btn-bg);
border-color: var(--btn-border-color);
&:hover { background: var(--btn-hover-bg); }
`;
const dynamicStyles = useMemo(() => {
const s = computeButtonDynamicStyles(variant, accent, ...);
return { '--btn-bg': s.background, '--btn-hover-bg': s.hoverBackground } as CSSProperties;
}, [variant, accent, ...]);
return <StyledButton style={dynamicStyles} />;
```
### CSS var + unit concatenation
CSS custom properties can't be concatenated with unit suffixes directly
(`var(--x)px` is invalid). Values that need units use `calc()`:
```tsx
// Broken
transition: background ${themeCssVariables.animation.duration.instant}s ease;
// Fixed
transition: background calc(${themeCssVariables.animation.duration.instant} * 1s) ease;
```
|
||
|
|
bc4ae35bc4 |
Rework atom naming (#18240)
## Summary - **Enhanced the `matching-state-variable` ESLint rule** to enforce consistent naming for `ComponentState`, `FamilyState`, and `ComponentFamilyState` hooks — previously it only covered `useAtomState` and `useAtomStateValue` - **The rule now checks 12 hooks** across three categories: value hooks (`useAtomComponentStateValue`, `useAtomFamilyStateValue`, etc.), state hooks (`useAtomComponentState`, `useAtomComponentFamilyState`), and setter hooks (`useSetAtomState`, `useSetAtomComponentState`, `useSetAtomFamilyState`, `useSetAtomComponentFamilyState`) - **Fixed all 225 resulting lint violations** across 151 files, renaming variables to match their state atom names (e.g. `currentViewId` → `contextStoreCurrentViewId`, `selectedRecord` → `recordStore`). Cases where the same state is accessed with different family keys/instance IDs are suppressed with `eslint-disable-next-line`. ## Naming convention | Hook | State argument | Valid | Invalid | |------|---------------|-------|---------| | `useAtomStateValue` | `fooState` | `const foo = ...` | `const bar = ...` | | `useAtomComponentStateValue` | `fooComponentState` | `const foo = ...` | `const bar = ...` | | `useAtomFamilyStateValue` | `fooFamilyState` | `const foo = ...` | `const bar = ...` | | `useAtomComponentFamilyStateValue` | `fooComponentFamilyState` | `const foo = ...` | `const bar = ...` | | `useAtomState` | `fooState` | `const [foo, setFoo] = ...` | `const [bar, setBar] = ...` | | `useAtomComponentState` | `fooComponentState` | `const [foo, setFoo] = ...` | `const [bar, setBar] = ...` | | `useAtomComponentFamilyState` | `fooComponentFamilyState` | `const [foo, setFoo] = ...` | `const [bar, setBar] = ...` | | `useSetAtomState` | `fooState` | `const setFoo = ...` | `const setBar = ...` | | `useSetAtomComponentState` | `fooComponentState` | `const setFoo = ...` | `const setBar = ...` | | `useSetAtomFamilyState` | `fooFamilyState` | `const setFoo = ...` | `const setBar = ...` | | `useSetAtomComponentFamilyState` | `fooComponentFamilyState` | `const setFoo = ...` | `const setBar = ...` | |
||
|
|
121788c42f |
Fully deprecate old recoil (#18210)
## Summary Removes the `recoil` dependency entirely from `package.json` and `twenty-front/package.json`, completing the migration to Jotai as the sole state management library. Removes all Recoil infrastructure: `RecoilRoot` wrapper from `App.tsx` and test decorators, `RecoilDebugObserver`, Recoil-specific ESLint rules (`use-getLoadable-and-getValue-to-get-atoms`, `useRecoilCallback-has-dependency-array`), and legacy Recoil utility hooks/types (`useRecoilComponentState`, `useRecoilComponentValue`, `createComponentState`, `createFamilyState`, `getSnapshotValue`, `cookieStorageEffect`, `localStorageEffect`, etc.). Renames all `V2`-suffixed Jotai state files and types to their canonical names (e.g., `ComponentStateV2` -> `ComponentState`, `agentChatInputStateV2` -> `agentChatInputState`, `SelectorCallbacksV2` -> `SelectorCallbacks`), and removes the now-redundant V1 counterparts. Updates ~433 files across the codebase to use the renamed Jotai imports, remove Recoil imports, and clean up test wrappers (`RecoilRootDecorator` -> `JotaiRootDecorator`). |
||
|
|
d81f006979 |
Rename Jotai state hooks and utilities to consistent Atom-based naming (#18209)
## Summary Rename all Jotai-based state management hooks and utilities to a consistent naming convention that: 1. Removes legacy Recoil naming (`useRecoilXxxV2`, `createXxxV2`) 2. Always includes `Atom` in the name to distinguish from jotai native hooks 3. Follows a consistent ordering: `Atom` → `Component` → `Family` → `Type` (`State`/`Selector`) 4. Includes the type qualifier (`State` or `Selector`) in all value-reading hooks to avoid naming conflicts with jotai's native `useAtomValue` ## Naming Convention **Hooks**: `use[Set]Atom[Component][Family][State|Selector][Value|State|CallbackState]` **Utils**: `createAtom[Component][Family][State|Selector]` ## Changes ### Hooks renamed (definition files + all usages across ~1,500 files): | Old Name (Recoil) | Intermediate (Atom) | Final Name | |---|---|---| | `useRecoilValueV2` | `useAtomValue` | **`useAtomStateValue`** | | `useRecoilStateV2` | `useAtomState` | `useAtomState` | | `useSetRecoilStateV2` | `useSetAtomState` | `useSetAtomState` | | `useFamilyRecoilValueV2` | `useFamilyAtomValue` | **`useAtomFamilyStateValue`** | | `useSetFamilyRecoilStateV2` | `useSetFamilyAtomState` | **`useSetAtomFamilyState`** | | `useFamilySelectorValueV2` | `useFamilySelectorValue` | **`useAtomFamilySelectorValue`** | | `useFamilySelectorStateV2` | `useFamilySelectorState` | **`useAtomFamilySelectorState`** | | `useRecoilComponentValueV2` | `useAtomComponentValue` | **`useAtomComponentStateValue`** | | `useRecoilComponentStateV2` | `useAtomComponentState` | `useAtomComponentState` | | `useSetRecoilComponentStateV2` | `useSetAtomComponentState` | `useSetAtomComponentState` | | `useRecoilComponentFamilyValueV2` | `useAtomComponentFamilyValue` | **`useAtomComponentFamilyStateValue`** | | `useRecoilComponentFamilyStateV2` | `useAtomComponentFamilyState` | `useAtomComponentFamilyState` | | `useSetRecoilComponentFamilyStateV2` | `useSetAtomComponentFamilyState` | `useSetAtomComponentFamilyState` | | `useRecoilComponentSelectorValueV2` | `useAtomComponentSelectorValue` | `useAtomComponentSelectorValue` | | `useRecoilComponentFamilySelectorValueV2` | `useAtomComponentFamilySelectorValue` | `useAtomComponentFamilySelectorValue` | | `useRecoilComponentStateCallbackStateV2` | `useAtomComponentStateCallbackState` | `useAtomComponentStateCallbackState` | | `useRecoilComponentSelectorCallbackStateV2` | `useAtomComponentSelectorCallbackState` | `useAtomComponentSelectorCallbackState` | | `useRecoilComponentFamilyStateCallbackStateV2` | `useAtomComponentFamilyStateCallbackState` | `useAtomComponentFamilyStateCallbackState` | | `useRecoilComponentFamilySelectorCallbackStateV2` | `useAtomComponentFamilySelectorCallbackState` | `useAtomComponentFamilySelectorCallbackState` | ### Utilities renamed: | Old Name (Recoil) | Final Name | |---|---| | `createStateV2` | **`createAtomState`** | | `createFamilyStateV2` | **`createAtomFamilyState`** | | `createSelectorV2` | **`createAtomSelector`** | | `createFamilySelectorV2` | **`createAtomFamilySelector`** | | `createWritableSelectorV2` | **`createAtomWritableSelector`** | | `createWritableFamilySelectorV2` | **`createAtomWritableFamilySelector`** | | `createComponentStateV2` | **`createAtomComponentState`** | | `createComponentFamilyStateV2` | **`createAtomComponentFamilyState`** | | `createComponentSelectorV2` | **`createAtomComponentSelector`** | | `createComponentFamilySelectorV2` | **`createAtomComponentFamilySelector`** | ## Details - All definition files renamed to match new convention - All import paths and usages updated across the entire `twenty-front` package - Jotai's native `useAtomValue` is aliased as `useJotaiAtomValue` in the wrapper hook to avoid collision - Legacy Recoil files in `state/utils/` and `component-state/utils/` left untouched (separate naming scope) - Typecheck passes cleanly |
||
|
|
0f2af6a6cb |
Jotai 13 (#18178)
## Recoil to Jotai Migration — PR 13: Workflow, Page Layout, AI, Activities, Settings, SSE & Remaining Modules ### Summary This is the 13th PR in the [14-PR migration series](packages/twenty-front/src/modules/ui/utilities/state/jotai/MIGRATION_PLAN.md) to replace Recoil with Jotai as the state management library in `twenty-front`. This PR migrates the remaining consumer code across all modules — updating ~1,076 files with ~10k insertions/deletions. ### What changed **New Jotai infrastructure (13 new files):** - `createComponentSelectorV2` and `createComponentFamilySelectorV2` — instance-scoped derived atoms, replacing Recoil's component selectors - 6 new hooks: `useRecoilComponentSelectorValueV2`, `useRecoilComponentFamilySelectorValueV2`, `useRecoilComponentSelectorCallbackStateV2`, `useRecoilComponentFamilySelectorCallbackStateV2`, `useRecoilComponentStateCallbackStateV2`, `useRecoilComponentFamilyStateCallbackStateV2` - New types: `ComponentSelectorV2`, `ComponentFamilySelectorV2`, `SelectorCallbacksV2` - Extended `buildGetHelper` to support the new selector patterns **State definition migrations:** - `createState()` → `createStateV2()` (Jotai `atom()`) - `createFamilyState()` → `createFamilyStateV2()` (Jotai `atom()` with family cache) - Recoil `selector`/`selectorFamily` → Jotai derived `atom()` via V2 utilities **Hook migrations (~2,400 removed, ~1,545 added V2 equivalents):** - `useRecoilValue` → `useRecoilValueV2` - `useRecoilState` → `useRecoilStateV2` - `useSetRecoilState` → `useSetRecoilStateV2` - `useRecoilCallback` → `useCallback` + `jotaiStore.get/set` - `useRecoilComponentValue` → `useRecoilComponentValueV2` - `useSetRecoilComponentState` → `useSetRecoilComponentStateV2` - `useRecoilComponentFamilyValue` → `useRecoilComponentFamilyValueV2` / `useFamilySelectorValueV2` - ~397 direct `import from 'recoil'` removed **Deleted files (9 files):** - `dateLocaleState.ts` — consolidated - `currentAIChatThreadTitleStateV2.ts` — renamed to replace the original - `isCommandMenuOpenedState.ts` — removed - `filesFieldUploadState.ts` — replaced by V2 - `recordStoreFamilySelector.ts`, `recordStoreFieldValueSelector.ts`, `recordStoreRecordsSelector.ts` — replaced by V2 equivalents - `settingsAllRolesSelector.ts` — replaced by `useSettingsAllRoles` hook - `settingsRoleIdsStateV2.ts` — consolidated **Modules migrated:** - Workflow (diagram, steps, variables, filters) - Page Layout (widgets, fields, graph, tabs) - AI (chat, agents, browsing context) - Activities (rich text editor, targets, timeline) - Action Menu (single/multiple record actions) - Command Menu (navigation, history, pages, workflow) - Context Store - Record Board, Record Table, Record Calendar, Record Index - Record Field, Record Filter, Record Sort, Record Group - Record Drag, Record Picker, Record Store - Views (view bar, view picker, filters, sorts) - Settings (roles, permissions, SSO, security, data model, developers, domains) - SSE (event streams, subscriptions) - Spreadsheet Import - Auth, Favorites, Front Components, Information Banner |
||
|
|
b51eb6471f |
Jotai 12 (#18160)
Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
aaa6511007 |
feat: added workspace member filter for actor fields (#16628)
Closes #16619 This PR adds support for filtering ACTOR fields by `Workspace Member` subfield, enabling users to filter records by who created them with a `Me` option. I replicated the same UX and code structure as the Relation field filter for consistency. Each filter selection triggers a server-side GraphQL call. But there is client-side filtering in `isRecordMatchingFilter.ts` which instantly filters already-loaded records while the server is fetching new results. I replicated this behaviour from how `FULL_NAME` and other composite fields handle filtering. UX decision that were taken by me (Let me know if changes are needed) : - The filter shows comma separated selected workspace member names until 3 members are selected. After that it shows [no of selected members] workspace members. <img width="643" height="283" alt="Screenshot 2025-12-17 at 8 01 23 PM" src="https://github.com/user-attachments/assets/18d524f4-979b-4d92-ad64-aa7b63be7897" /> <img width="774" height="309" alt="Screenshot 2025-12-17 at 8 01 33 PM" src="https://github.com/user-attachments/assets/779f374f-1501-48f9-afa2-a78628e067b1" /> <img width="737" height="397" alt="Screenshot 2025-12-17 at 8 01 40 PM" src="https://github.com/user-attachments/assets/4e8b963e-8a0f-467d-91ae-48bca4e1d842" /> <img width="697" height="334" alt="Screenshot 2025-12-17 at 8 01 53 PM" src="https://github.com/user-attachments/assets/c9a0e4bb-48b4-486b-a4f9-d7c49ff3852c" /> <img width="684" height="343" alt="Screenshot 2025-12-17 at 8 02 04 PM" src="https://github.com/user-attachments/assets/d5c45eba-06a3-40fc-b9d0-d585dc8bc136" /> --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com> |
||
|
|
530f74e28c |
Migrate more to Jotai (#18087)
Continue jotai migration |
||
|
|
88424611ec |
Refactor and standardize isSystem field and object (#17992)
# Introduction ## Centralize system field definitions - Extract a single `PARTIAL_SYSTEM_FLAT_FIELD_METADATAS` constant as the source of truth for all 8 system fields (`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`), eliminating duplication across custom object and standard app field builders - Refactor `buildDefaultFlatFieldMetadatasForCustomObject` to use the shared constant via a new `buildObjectSystemFlatFieldMetadatas` helper ## Mark system fields as `isSystem: true` - Fields `id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector` are now properly flagged as system fields across all standard objects and custom object creation - Standard app field builders for all ~30 standard objects updated to set `isSystem: true` on `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy` - System-only standard objects (blocklist, calendar channels, message threads, etc.) now also include `createdBy`, `updatedBy`, `position`, `searchVector` field definitions that were previously missing ## Validate system fields on object creation - New transversal validation (`crossEntityTransversalValidation`) runs after all atomic entity validations in the build orchestrator, ensuring all 8 system fields are present with correct `type` and `isSystem: true` when an object is created - New `buildUniversalFlatObjectFieldByNameAndJoinColumnMaps` utility to resolve field names to universal identifiers for a given object - New exception codes: `MISSING_SYSTEM_FIELD` and `INVALID_SYSTEM_FIELD` on `ObjectMetadataExceptionCode` ## Protect system fields and objects from mutation - Field validators now block update/delete of `isSystem` fields by non-system callers (`FIELD_MUTATION_NOT_ALLOWED`) - Object validators now block update/delete of `isSystem` objects by non-system callers - `POSITION` and `TS_VECTOR` field type validators replaced: instead of rejecting creation outright, they now validate that the field is named correctly (`position` / `searchVector`) and has `isSystem: true` ## Distinguish `isSystemBuild` from `isCallerTwentyStandardApp` - New `isCallerTwentyStandardApp` utility checks whether the caller's `applicationUniversalIdentifier` matches the twenty standard app - Name-sync logic (`isFlatFieldMetadataNameSyncedWithLabel`, `areFlatObjectMetadataNamesSyncedWithLabels`) refactored to use `isCallerTwentyStandardApp` for custom suffix decisions, keeping `isSystemBuild` for mutation permission checks - `WorkspaceMigrationBuilderOptions` type updated to include `applicationUniversalIdentifier` ## Adapt frontend filtering - New `HIDDEN_SYSTEM_FIELD_NAMES` constant (`id`, `position`, `searchVector`) and `isHiddenSystemField` utility to only hide truly internal fields while keeping user-facing system fields (`createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`) visible in the UI - ~20 frontend files updated to replace `!field.isSystem` checks with `!isHiddenSystemField(field)` across record index, settings, data model, charts, workflows, spreadsheet import, aggregations, and role permissions ## Add 1.19 upgrade commands - **`backfill-system-fields-is-system`**: Raw SQL command to set `isSystem = true` on existing workspace fields matching system field names, and fix `position` field type from `NUMBER` to `POSITION` for `favorite`/`favoriteFolder` objects. Includes proper cache invalidation. - **`add-missing-system-fields-to-standard-objects`**: Codegen'd workspace migration to create missing `position`, `searchVector`, `createdBy`, `updatedBy` fields on standard objects that didn't previously have them. Runs via `WorkspaceMigrationRunnerService` in a single transaction with idempotency check. **Known limitation**: assumes all standard objects exist and are valid in the target workspace. ## Add `universalIdentifier` for system fields in standard object constants - `standard-object.constant.ts` updated to include `universalIdentifier` for `createdBy`, `updatedBy`, `position`, and `searchVector` across all standard objects - `fieldManifestType.ts` updated to support the new field manifest shape ## System relation Completely removed and backfilled all `isSystem` relation to be false false As we won't require an object to have any relation system fields ## Add integration tests - New test suite `failing-sync-application-object-system-fields` covering: missing system fields, wrong field types (`id` as TEXT, `createdAt` as TEXT, `position` as TEXT), system field deletion attempts, and system field update attempts - New test utilities: `buildDefaultObjectManifest` (builds an object manifest with all 8 system fields) and `setupApplicationForSync` (centralizes application setup) - Existing successful sync test updated to verify system fields are created with correct properties ## Next step Make the builder scope the compared entity to be the currently built app + nor twenty standard app |
||
|
|
612f7c37a5 |
Improve security settings card grouping and description overflow (#17928)
# After - Added a separtor between the two audit logs cards - Rename the audit log card to avoid repetition - Grouped "Invite by link" and "2 factor auth" in one group - Changed the card component description to always be one line max with truncation & tooltips <img width="777" height="1278" alt="CleanShot 2026-02-13 at 17 02 36" src="https://github.com/user-attachments/assets/685c792a-c85b-4521-8c1b-bd9adedc75d9" /> <img width="976" height="690" alt="b49f2eb043b6712d013618bb0a4ef7f011cf2316e1163fbdee4c293bed036ac9" src="https://github.com/user-attachments/assets/6e17aa11-ecdb-4f98-ba50-5cd9b9c5def6" /> --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com> Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
90a30263ac |
[Apps] Content + permission tabs for marketplace and installed apps (#17888)
In this PR - Content tab for marketplace apps and installed apps: complies with [figma](https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=34845-126856); addition of field section showing fields added to standard objects; initiative: arrow opens a sub-table of fields rows. (suggested because 1/ for marketplace apps we cannot redirect to the actual object page in settings since the object does not exist yet in the workspace 2/ since we dont have an quick "go back to application page" option, it can be annoying to be redirected to a different setting page when we just want to look at the content of the app objects) - Permission tab for marketplace apps and installed apps - left to do - settings tab (in another PR) There are breaking changes but it's behind a feature flag not exposed (access to marketplace) so not problematic marketplace apps https://github.com/user-attachments/assets/4c660101-50fc-47ce-b90a-8d6f17db5e74 installed apps https://github.com/user-attachments/assets/c9229ee1-e75f-4cad-8766-758b2c5b37b4 |
||
|
|
b456f79167 |
Reduce leak between gql schema (#17878)
## Reduce type leakage between GraphQL schemas ### Why Twenty runs two separate GraphQL schemas: **core** and **metadata**. NestJS's `@nestjs/graphql` uses a global `TypeMetadataStorage` that accumulates all decorated types across all modules. When each schema is built, every registered type leaks into both schemas regardless of which module it belongs to. This means the core schema's generated TypeScript (`generated/graphql.ts`) contained ~2,700 lines of types that only belong to the metadata schema (and vice versa). This creates confusion about type ownership, inflates generated code, and makes it harder to reason about which API surface each schema actually exposes. ### How **1. Patch `@nestjs/graphql` to support schema-scoped type resolution** - **(Already done)** Added a `resolverSchemaScope` option to `GqlModuleOptions`, allowing each schema to declare a scope (e.g. `'metadata'`) - `ResolversExplorerService` now filters resolvers by a `RESOLVER_SCHEMA_SCOPE` metadata key, so each schema only sees its own resolvers - `GraphQLSchemaFactory` now performs a **reachability walk** (`computeReachableTypes`) starting from scoped resolver return types and arguments, only including types that are transitively referenced — handling unions, interfaces, and prototype chains - Type definition storage and orphaned reference registry are cleared between schema builds to prevent cross-contamination **2. Register `ClientConfig` as orphaned type in metadata schema** Since `ClientConfig` is needed in the metadata schema but not directly returned by a resolver, it's explicitly declared via `buildSchemaOptions.orphanedTypes`. **3. Regenerate frontend types and fix imports** - `generated/graphql.ts` shrank by ~2,700 lines (types moved to where they belong) - `generated-metadata/graphql.ts` gained types like `ClientConfig` that were previously missing - ~500 frontend files updated to import from the correct generated file |
||
|
|
77d15356e1 |
Files v2 - Use Files field in attachment (#17707)
- Add FILES field on attachment - Adapt Attachment logic in front to use new resolver/controller - Update files-field logic to infer applicationId from fieldMetadataId + ask for fieldMetadataId in upload resolver - Design update To do in next PR : - Adapt activity files logic |
||
|
|
27da9d60eb |
fix: set isActive=true in morph migration & add system fields toggle (#17736)
## Summary
Three fixes in this PR:
### 1. Migration commands now set `isActive = true` and use generic
'Target' label
When converting relation fields to `MORPH_RELATION` type, the migration
commands now:
- Set `isActive = true` to prevent inactive fields from being selected
as the representative morph field
- Update all field labels to generic **'Target'** instead of keeping
individual labels like 'Company', 'Person'
This ensures the UI shows a coherent label ('Target') alongside 'X
Objects' for the type.
**Files changed:**
- `1-17-migrate-note-target-to-morph-relations.command.ts`
- `1-17-migrate-task-target-to-morph-relations.command.ts`
### 2. Added system fields/relations toggles in Settings
The fields and relations tables in Settings > Data Model were filtering
out system fields with no way to view them. Added a "System fields" /
"System relations" toggle (visible in advanced mode) to allow viewing
these fields.
**Files changed:**
- `SettingsObjectFieldTable.tsx`
- `SettingsObjectRelationsTable.tsx`
This matches the existing behavior on the Objects table which already
has a "System objects" toggle.
|
||
|
|
8674cf973e |
Display "Not shared" indicator for RLS-restricted relation fields (#17713)
## Context Previously, if for example a Person had a companyId but the company relation was null due to RLS, the company column appeared empty. Now it displays ForbiddenFieldDisplay to indicate the relation exists but is inaccessible. When RLS restricts access to a related record, the frontend now shows a "Not shared" indicator (with lock icon) instead of an empty field. <img width="1275" height="399" alt="Screenshot 2026-02-04 at 15 37 53" src="https://github.com/user-attachments/assets/870306f8-f811-4dc0-b23b-a33e89043a37" /> |
||
|
|
4fcc424e24 |
Files field - add files field display and input + filtering (#17637)
This PR introduces a new FILES field type for Twenty CRM, allowing users to attach multiple files to any record. - Files field display - Files field preview - Files field input - Files field filtering To test : 1/ need to activate feature flag `IS_FILES_FIELD_ENABLED` + create a new FILES field - display in read only, edit, inline, table - edit - filter - export closes https://github.com/twentyhq/core-team-issues/issues/2154 <img width="354" height="134" alt="Screenshot 2026-02-02 at 19 11 01" src="https://github.com/user-attachments/assets/3c3de89e-f6b6-4526-b710-e4c7ee1a6d30" /> <img width="1081" height="933" alt="Screenshot 2026-02-02 at 19 10 41" src="https://github.com/user-attachments/assets/7c9a7278-edb7-4d4a-882c-f7404689d011" /> <img width="1073" height="719" alt="Screenshot 2026-02-02 at 19 10 33" src="https://github.com/user-attachments/assets/79cc372b-56a2-4cbf-b4fe-023adc4753a8" /> |
||
|
|
54cf857a1a |
Revert "fix: prevent default object re-selection in relation field fo… (#17619)
I suggest to revert [this PR](https://github.com/twentyhq/twenty/pull/17313) as it had already been fixed [in this PR](https://github.com/twentyhq/twenty/pull/17209/changes) (which fixes more than it says in its title). Issue was tracked by [this ticket](https://github.com/twentyhq/core-team-issues/issues/2049) not very explicit - sorry, my fault. Code added in the PR does not add value |
||
|
|
b8d85e0b26 |
fix: prevent default object re-selection in relation field form (#17313)
Fixes #17111 ### Problem When creating a Relation field, after deselecting the pre-selected default object (e.g., Company) and selecting a different object (e.g., Opportunity), both objects would end up being selected, showing "2 Objects" instead of just the newly selected one. ### Root Cause The `initialMorphRelationsObjectMetadataIds` array was being recreated on every component render, causing react-hook-form's `Controller` to treat the `defaultValue` prop as a new value and re-apply it after user changes. ### Solution 1. **Memoized the initial value**: Used `useMemo` to ensure `initialMorphRelationsObjectMetadataIds` has a stable reference across renders 2. **Added initialization guard**: Used `useEffect` with a `useRef` flag to ensure the default value is only set once during component mount 3. **Maintained Controller defaultValue**: Kept the `defaultValue` prop on the Controller, which now works correctly with the stable memoized value ### Changes - `SettingsDataModelFieldRelationForm.tsx`: Added memoization and initialization guard to prevent default value re-application --------- Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com> |
||
|
|
1cc08b84c4 |
feat: Add bulk input mode for select options #5539 (#17459)
- Implemented bulk input mode in SettingsDataModelFieldSelectForm to allow users to input multiple options at once. - Added convertBulkTextToOptions and convertOptionsToBulkText utility functions for handling bulk text conversion. - Created tests for both conversion functions to ensure correct functionality. Screenshot: <img width="601" height="752" alt="image" src="https://github.com/user-attachments/assets/95932860-090e-4743-9bd9-7aa6747ad04f" /> <img width="543" height="362" alt="image" src="https://github.com/user-attachments/assets/84490808-2e0f-4126-951e-167b35f1b0d0" /> --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com> |
||
|
|
161689be18 |
feat: fix junction toggle persistence and add type-safe documentation paths (#17421)
## Summary - **Fix junction relation toggle not being saved**: The form schema wasn't tracking the `settings` field, so changes to `junctionTargetFieldId` weren't marked as dirty - **Add type-safe documentation paths**: Generate TypeScript constants from `base-structure.json` to prevent broken documentation links - **Create many-to-many relations documentation**: Step-by-step guide for building many-to-many relations using junction objects - **Update `getDocumentationUrl`**: Now uses shared constants from `twenty-shared` for base URL, default path, and supported languages ## Key Changes ### Junction Toggle Fix - Added `settings` field to the form schema in `SettingsDataModelFieldRelationForm.tsx` - Fixed the toggle to properly merge settings when updating `junctionTargetFieldId` ### Type-Safe Documentation Paths - New constants in `twenty-shared/constants`: - `DOCUMENTATION_PATHS` - All 161 documentation paths as typed constants - `DOCUMENTATION_SUPPORTED_LANGUAGES` - 14 supported languages - `DOCUMENTATION_BASE_URL` / `DOCUMENTATION_DEFAULT_PATH` - Generator script: `yarn docs:generate-paths` - CI integration: Added to `docs-i18n-pull.yaml` workflow ### Documentation - New article: `/user-guide/data-model/how-tos/create-many-to-many-relations` - Updated `/user-guide/data-model/capabilities/relation-fields.mdx` with Lab warning and link ## Test plan - [ ] Verify junction toggle saves correctly when enabled/disabled - [ ] Verify documentation link opens correct localized page - [ ] Verify `yarn docs:generate-paths` regenerates paths correctly |
||
|
|
0091ef5f6c |
Sync built files (#17379)
as title, upload built files to local storage --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
183cf6c803 |
Shrink table rows (#17360)
We often use the `fr` unit in a CSS grid thinking that it will force the max-width of the grid's items. However, [`1fr` is always equal to `minmax(auto, 1fr)`](https://stackoverflow.com/a/52861514). The consequences are: - By default, the width of the element will be `1fr`. - However, if the size of the element becomes wider than the resolved dimension of `1fr`, the item will expand to match its `auto` width. This can be easily encountered when an item has a really long text. The `auto` size will be the dimension of the long text displayed on a single line. If you want your element not to overflow, you can use `minmax(0, 1fr)` instead of `1fr`. This is a known behavior in the community. CSS frameworks like Tailwind CSS even made their utility classes setting grid columns use `minmax(0, xfr)` by default. <img width="1380" height="84" alt="CleanShot 2026-01-22 at 16 11 00@2x" src="https://github.com/user-attachments/assets/ace3c862-90ef-4a47-9ad5-9ae8b6e0fe40" /> See: https://www.bigbinary.com/blog/understanding-the-automatic-minimum-size-of-flex-items. Another fix is to use `min-width: 0;` on items themselves, instead of using `minmax(0, 1fr)` at the grid level. This would make it possible to create a reusable `Td` component. ## Data model - fields ### Before <img width="1120" height="1808" alt="CleanShot 2026-01-22 at 15 46 48@2x" src="https://github.com/user-attachments/assets/893115f4-219d-4a75-b628-56315298d791" /> ### After <img width="1120" height="1808" alt="CleanShot 2026-01-22 at 15 47 00@2x" src="https://github.com/user-attachments/assets/093ae231-ccae-4df5-8227-a662a3fae97e" /> ## Data model - relations ### Before <img width="1172" height="754" alt="CleanShot 2026-01-22 at 16 00 33@2x" src="https://github.com/user-attachments/assets/3367b849-a4f4-471b-b73a-a80a95368640" /> ### After <img width="1146" height="770" alt="CleanShot 2026-01-22 at 16 00 44@2x" src="https://github.com/user-attachments/assets/ff08556e-fc16-4984-bd4b-bb15f4dabf36" /> ## Object level object field permission ### Before <img width="1150" height="1902" alt="CleanShot 2026-01-22 at 15 50 16@2x" src="https://github.com/user-attachments/assets/adce1ba1-042b-4892-9a0e-af254109d03c" /> ### After <img width="1150" height="1902" alt="CleanShot 2026-01-22 at 15 50 01@2x" src="https://github.com/user-attachments/assets/55476711-2573-43ae-b1d6-b8695f4305ab" /> |
||
|
|
669da1a87d |
fix: display current object name in morph relation picker after rename (#17209)
**Summary** Issue #16963: When an object is renamed, the morph relation picker still shows the old name. **Root cause** The picker used searchRecord.objectNameSingular from the GraphQL search response, which can be stale after a rename. The search record stores the object name at query time, not the current metadata. **Solution** - Updated SingleRecordPickerMenuItem: - Added useObjectMetadataItems to access current object metadata. - Look up the current object metadata by morphItem.objectMetadataId. - Use labelSingular (or nameSingular as fallback) instead of searchRecord.objectNameSingular for display. - Updated MultipleRecordPickerMenuItemContent: - Use objectMetadataItem.labelSingular (already available as a prop) instead of searchRecord.objectNameSingular. --------- Co-authored-by: Marie Stoppa <marie.stoppa@essec.edu> |
||
|
|
3ed67b825e |
feat: implement generic many-to-many junction relation support (#16820)
## Overview
This PR implements **generic many-to-many relation support** through
junction tables (also known as associative entities or join tables).
This replaces the need for hardcoded taskTarget/noteTarget logic and
provides a flexible foundation for modeling complex entity
relationships.
## Architecture
### Data Model
Many-to-many relationships are implemented using a **junction object
pattern**:
```
┌─────────┐ ┌──────────────────┐ ┌─────────┐
│ Pet │──────>│ PetRocket │<──────│ Rocket │
│ │ 1:N │ (junction) │ N:1 │ │
│ rockets ├───────┤ pet : Pet ├───────┤ │
└─────────┘ │ rocket : Rocket │ └─────────┘
└──────────────────┘
```
The junction object (PetRocket) has:
- A `MANY_TO_ONE` relation to **Pet** (the source)
- A `MANY_TO_ONE` relation to **Rocket** (the target)
The source object (Pet) has a `ONE_TO_MANY` relation pointing to the
junction, with **field settings** that specify which target field to
follow.
### Field Settings Schema
Junction configuration is stored in `FieldMetadataRelationSettings`:
```typescript
{
relationType: "ONE_TO_MANY",
// Points to the target field on the junction object
junctionTargetFieldId?: string; // For regular relations
junctionTargetMorphId?: string; // For polymorphic relations
}
```
**Two configuration modes:**
1. **`junctionTargetFieldId`** - References a specific `RELATION` field
on the junction
2. **`junctionTargetMorphId`** - References a `morphId` group for
polymorphic targets (e.g., link to Person OR Company)
### GraphQL Query Generation
When a junction relation is detected, the GraphQL fields are generated
to fetch the nested target:
```graphql
query GetPetWithRockets {
pet(id: "...") {
rockets { # ONE_TO_MANY to junction
id
rocket { # Target field on junction
id
name
__typename
}
}
}
}
```
For polymorphic junction targets:
```graphql
caretakerPerson { id, name }
caretakerCompany { id, name }
```
## Frontend Architecture
### Display Flow
1. **Detection**: `hasJunctionConfig()` checks if field has junction
settings
2. **Config Resolution**: `getJunctionConfig()` resolves junction object
metadata and target fields
3. **Record Extraction**: `extractTargetRecordsFromJunction()` extracts
target records from junction records
4. **Rendering**: Target records displayed as chips (not junction
records)
### Edit Flow
1. **Picker Opening**: Initializes the multi-record picker with:
- Searchable object types (derived from junction target fields)
- Pre-selected items (extracted from existing junction records)
2. **Selection Handling**: Manages create/delete of junction records:
- **Select**: Creates new junction record with source + target IDs
- **Deselect**: Finds and deletes the junction record
- **Optimistic Updates**: Manually updates Recoil store before API call
### Key Trade-offs
| Decision | Trade-off |
|----------|-----------|
| Junction records managed manually | More control over optimistic
updates, but requires manual cache management |
| Settings stored per-field | Flexible (same junction can power
different views), but requires UI to configure |
| Polymorphic via morphId groups | Supports N target types, but adds
query complexity |
| Feature flag gated | Safe rollout, but requires flag management |
## Backend Changes
- **Validation**: Junction target field must exist and be a valid
`MANY_TO_ONE` relation
- **Settings**: Extended `FieldMetadataRelationSettings` type with
junction fields
- **Dev Seeder**: Added sample junction objects (PetRocket,
EmploymentHistory, PetCareAgreement) for testing
## How to Test
1. Enable the `IS_JUNCTION_RELATIONS_ENABLED` feature flag
2. Create objects with junction pattern (Pet → PetRocket → Rocket)
3. Configure the junction target in field settings (advanced mode)
4. Verify:
- Display shows target objects (Rockets), not junction records
(PetRockets)
- Picker allows selecting/deselecting targets
- Changes persist correctly
https://github.com/user-attachments/assets/d04f057a-228c-4de8-af48-76bb2d72cac1
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
|
||
|
|
30c13fc947 |
Files v2 - Add new FILES field type (#17236)
In this PR : - New field type FieldMetadataType.FILES added (as jsonb in DB) - Backend: GraphQL types, validation, REST API schema, data processor handling - Frontend: field configuration in settings - Feature flag IS_FILES_FIELD_ENABLED to gate the feature - Integration tests for create/filter validation - Dev seed: Feature flag enabled + FILES field on survey result object To do in next PRs : - Backend : -- new workspaceFile controller (for download) & new workspaceFile upload resolver (for upload) -- pre-hook/post-hook to ensure fileId existence + listener to ensure cleaning - Frontend : FILES field display/edit in table view, record, ... |
||
|
|
92eff62523 | Replace test-runner with vitest for storybook (#17187) | ||
|
|
c737028dd6 |
Move tools/eslint-rules to packages/twenty-eslint-rules (#17203)
## Summary Moves the custom ESLint rules from `tools/eslint-rules` to `packages/twenty-eslint-rules` for better organization within the monorepo packages structure. ## Changes - Move `eslint-rules` from `tools/` to `packages/twenty-eslint-rules` - Use `loadWorkspaceRules` from `@nx/eslint-plugin` to load custom rules - Update all ESLint configs to use the `twenty/` rule prefix instead of `@nx/workspace-` - Update `project.json`, `jest.config.mjs` with new paths - Update `package.json` workspaces and `nx.json` cache inputs - Update Dockerfile reference ## Technical Details The custom ESLint rules are now loaded using Nx's `loadWorkspaceRules` utility which: - Handles TypeScript transpilation automatically - Allows loading workspace rules from any directory - Provides a cleaner approach than the previous `@nx/workspace-` convention ## Testing - Verified all 17 custom ESLint rules load correctly from the new location - Verified linting works on dependent packages (twenty-front, twenty-server, etc.) |
||
|
|
2c8d3f02e1 |
feat: upgrade to Storybook version 10 (#17110)
Upgraded to Storybook 10. We still use `@storybook/test-runner` for testing since it appears it'd require more work to move from Jest to Vitest than I initially anticipated, but I completed this PR to fix `storybook:serve:dev` - it takes time to load, but it works the way it used to with Storybook 8. https://github.com/user-attachments/assets/7afc32c6-4bcf-4b37-b83b-8d00d28dda15 |
||
|
|
d7638a9075 |
Fix settings data model tables to take full width and align columns (#17104)
## Summary - Updated Relations and Fields tables in the object detail settings page to use flexible width (`1fr`) for the Name column instead of fixed pixels - Aligned column widths between both tables (`1fr 148px 148px 36px`) so the App and Type/Data type columns line up ## Test plan - [ ] Navigate to Settings > Data model > select any object (e.g., Companies) - [ ] Verify the Relations table rows take the full available width - [ ] Verify the Fields table rows take the full available width - [ ] Verify the App and Type/Data type columns are aligned between both tables |
||
|
|
5240a1818f |
feat: upgrade Storybook to version 9 (#17077)
Upgraded from 8.6.15 to 9.1.17 in two steps: - 8.6.15 -> 9.0.0 - 9.0.0 -> 9.1.17 I had to disable `storybook-addon-cookie` since it is not supported for Storybook 9. However, I do intend to upgrade to Storybook 10 when this is merged, so we can replace the aforementioned add-on with this fork specifically created to support Storybook 10 and above: https://www.npmjs.com/package/@storybook-community/storybook-addon-cookie. Additionally, once we upgrade to Version 10 successfully, I will start looking into integrating the official Vitest add-on. |
||
|
|
a46619ec55 |
fix: enable save button when changing currency default value (#16864)
Closes #16792 ## Problem When editing a currency field’s default value (for example, changing from **USD** to **UYU**), the **Save** button remained disabled. However, if the format setting was changed first (**short → full → short**), the Save button would then work correctly for currency changes. ## Root Cause The form was using **React Hook Form’s `values` mode**, which did not properly track dirty state for the `defaultValue` and `settings` fields. ## Solution Switched to the **`defaultValues` + `reset()`** pattern: - Initialize the form with `defaultValues` (using placeholder values) - Call `reset()` when `fieldMetadataItem` loads This correctly tracks dirty state by comparing against the most recent `reset` values. ## Note A similar pattern is used in: - `useWebhookForm` - `useImapSmtpCaldavConnectionForm` Those hooks call `reset()` via query callbacks. In this case, `reset()` is called inside a `useEffect` because the data comes from synchronous Recoil state rather than an async query. Found two solutions to the problem, created commits for both. I feel the useEffect pattern makes more sense here since it's cleaner in terms of code. --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
221a6262f0 |
Fix metadata relation field settings item type tag display (#16949)
# Introduction followup https://github.com/twentyhq/twenty/pull/16880 Fixing the item tag type displayal of the relation field within an object Inferring the application id at field level instead of object level ## From <img width="1162" height="1220" alt="image" src="https://github.com/user-attachments/assets/28af2242-2786-4e17-b910-3fc26871bc04" /> ## To <img width="1162" height="1220" alt="image" src="https://github.com/user-attachments/assets/63fb89a0-a853-4157-baa4-d0f758648c3e" /> |
||
|
|
de33d17809 |
Fix navigation memory after custom object rename (#16918)
This PR fixes an issue where exiting Settings after renaming a custom object could redirect to a stale object URL and result in a `404`. When a custom object name was updated, the memorized navigation URL was not kept in sync, causing redirects to use the old object route. The navigation state is now updated only when the memorized URL belongs to the renamed object, ensuring redirects always point to the correct route while preserving unrelated navigation context. Fixes https://github.com/twentyhq/twenty/issues/11291 --------- Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com> |
||
|
|
1128331cc1 |
Fix field item type tag (#16860)
# Introduction This has been fixed on main already 1 hour ago, this PR now only passes the field application id instead of the object applicationId that could be different for example when creating a custom field on a standard object For the moment not introducing any logic around application integrity directly and still relying on the isCustom and standardId definition This will have to be refactored once we deprecate the standardId |
||
|
|
4f1f0eb177 |
Redesign Object/Field tables (#16844)
Redesign the data model pages for more clarity / better distinction between fields and relations |
||
|
|
e5e5ae8e1d |
fix: guard against invalid date and undefined links field value (#16039)
closes https://github.com/twentyhq/twenty/issues/15854 - Add `isValid()` check in `formatDateISOStringToDateTime` before calling `formatInTimeZone` - Add `isDefined()` guard in `getFieldLinkDefinedLinks` (mirrors `phonesUtils` pattern) before: https://github.com/user-attachments/assets/1eb89fa4-70b6-4794-8860-0a42522598b5 https://github.com/user-attachments/assets/781c7d37-c435-4832-98d4-8e6925b51e10 after: https://github.com/user-attachments/assets/b1c22d25-4e8e-45c3-af98-be1684a5962e https://github.com/user-attachments/assets/ce084a9d-03b8-4f88-8522-a32cb1b7cf2f --------- Co-authored-by: Charles Bochet <charles@twenty.com> Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com> |