feat(deps): migrate frontend to React 19 (#21531)

## What

Migrates the frontend stack from **React 18.3 → 19.2**. The website,
sdk, companion and emails packages were already on React 19; this brings
the remaining holdouts (`twenty-front`, `twenty-ui`,
`twenty-ui-deprecated`, `twenty-front-component-renderer`) and
`twenty-server`'s email rendering onto 19, and pins a single React
version repo-wide.

## Why

React 18.x is now the legacy line. Staying current keeps us on the
patched/maintained branch and unblocks downstream library majors
(react-router 7, mantine 9, etc.) that require React 19 peers.

## Dependency bumps (required by React 19 peers / removed APIs)

| Package | From | To | Reason |
|---|---|---|---|
| react / react-dom | 18.3.1 | 19.2.3 | core |
| @hello-pangea/dnd | 16 | 18 | peer `^18 \|\| ^19` |
| react-datepicker | 6 | 9 | v<7 used removed `findDOMNode`; drops
`@types/react-datepicker` |
| react-data-grid | beta.13 | beta.59 | peer `^19.2`; new render API |
| graphiql (+ @graphiql/react, plugin-explorer) | 3 / 0.23 / 1 | 5 /
0.37 / 5.1 | peer `^18 \|\| ^19` |
| react-helmet-async | 1.3 | **@dr.pogodin/react-helmet** 3.2 | upstream
caps peer at `^18`; drop-in React 19 fork |

A `resolutions` pin enforces a single React (19.2.3) + `@types/react`
(19.2.14) across the monorepo to avoid duplicate copies / type-identity
splits. Versions are the aged lockfile patches (clears the
`npmMinimalAgeGate`).

## Code changes

- **Global `JSX` shim** (`react-jsx-global.d.ts` per package): React 19
moved the `JSX` namespace under `React.JSX`; several deps' published
types (notably `@linaria/react`'s `styled.d.ts`, which types every
`styled.x` via `keyof JSX.IntrinsicElements`) still reference the global
namespace. Without the shim, every styled component degrades to `any`
props.
- **Ref nullability**: `useRef<T>(null)` now returns `RefObject<T |
null>`; widened consumer prop/hook ref types accordingly (incl. the
shared `useListenClickOutside`).
- **react-datepicker v9**: `onChange`/`onSelect` accept `Date | null`,
`calendarStartDay` typing, `ReactDatePickerProps`→`DatePickerProps`,
relaxed the dynamic `selectsMultiple` discriminated union.
- **react-data-grid beta.59**: `formatter`→`renderCell`,
`editor`→`renderEditCell`, `headerRenderer`→`renderHeaderCell`,
`components`→`renderers`, `onRowClick`→`onCellClick`, object-shaped
`useRowSelection`, Set-based selection.
- **dnd style cast**: `@radix-ui/react-popper` augments `CSSProperties`
with a `--radix-*` index signature that dnd's closed `DraggingStyle`
doesn't satisfy → cast at the spread.

## Status / testing

-  `typecheck` green: twenty-front, twenty-ui, twenty-ui-deprecated,
twenty-front-component-renderer, twenty-server
-  build / lint / unit tests / storybook+argos / runtime smoke-test in
progress

Draft until local + CI verification completes. Notable behavior to QA
manually: spreadsheet import (data-grid), date pickers, drag-and-drop
boards/lists, GraphQL playground, page titles/favicon.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21531?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Charles Bochet
2026-06-14 15:42:22 +02:00
committed by GitHub
parent 9901fa93d9
commit 7c0136b97b
112 changed files with 1065 additions and 1217 deletions
+4 -3
View File
@@ -7,8 +7,8 @@
"@nx/storybook": "22.7.5",
"@nx/vite": "22.7.5",
"@nx/web": "22.7.5",
"@types/react": "^18.2.39",
"@types/react-dom": "^18.2.15",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@yarnpkg/types": "^4.0.0",
"concurrently": "^8.2.2",
"http-server": "^14.1.1",
@@ -26,6 +26,7 @@
"name": "twenty",
"packageManager": "yarn@4.13.0",
"resolutions": {
"@remote-dom/react/@types/react": "^19.2.0",
"graphql": "16.8.1",
"graphql-redis-subscriptions/ioredis": "5.10.1",
"@types/qs": "6.9.16",
@@ -50,7 +51,7 @@
"@react-email/ui/esbuild": "0.28.1",
"react-email/esbuild": "0.28.1"
},
"//resolutions": "Each entry is load-bearing: it forces a version OUTSIDE some parent's declared range where no fixed upstream release exists; remove each once its blocker ships. graphql 16.8.1 -> singleton pin held below msw's ^16.12.0 dep and @nestjs/graphql's ^16.11.0 peer; drop after a validated repo-wide bump to latest 16.x; graphql-redis-subscriptions/ioredis 5.10.1 -> TS type-identity dedup: twenty-server passes its ioredis client into RedisPubSub, so this must equal the exact ioredis version pinned by twenty-server and bullmq (bump in lockstep); @types/qs 6.9.16 -> holdback below the 6.9.17 ParsedQs typing break (node-saml wants ^6.9.18); @opentelemetry/api 1.9.1 -> singleton guard for the NoopMeterProvider bug (#20231): ai 6.0.x pins 1.9.0 exact vs @sentry/node ^1.9.1, drop when workspace ai >=6.0.178 AND @scalar/agent-chat moves off ai 6.0.33; chokidar ^3 -> NestJS CLI watch needs fsevents on macOS, removed in chokidar 4/5 (#20316); tmp ^0.2.7 -> CVE, zapier-platform-cli 19 (latest) pins 0.2.5 and inquirer 7/8's external-editor wants ^0.0.33; make-fetch-happen ^15 + @electron/{rebuild,node-gyp}/tar ^7.5.16 -> tar CVE eviction for the @electron/rebuild 3.x toolchain (rebuild 3.x pins tar ^6, its node-gyp fork pins tar ^6.2.1 + mfh ^10), drop when electron-forge declares @electron/rebuild >=4; @angular-devkit/core 19.2.24 -> picomatch CVE, blocked on @nestjs/cli >11.0.23 fixing the dist/src output regression (repo held at 11.0.16); yeoman-environment 6.0.1 -> CVE, zapier-platform-cli 19 (latest) pins 4.4.3; webpack-dev-server 5.2.4 -> CVE, @electron-forge/plugin-webpack (incl. 8.x alphas) still declares ^4; express/qs + @cypress/request/qs 6.15.2 -> qs CVE for old express 4.22.0/4.22.1 pinned by @mintlify/previewing and verdaccio (verdaccio also pins @cypress/request 3.0.10; all other qs parents resolve safe naturally); next/postcss 8.5.15 -> postcss CVE, every stable next pins 8.4.31 exact (fix only in 16.3.0 canaries; @react-email/ui also pins next 16.2.6); <pkg>/uuid 11.1.1 -> uuid CVE for parents pinning uuid <11 with no fixed release (sockjs dormant since 2021; @cypress/request 3.0.10 via verdaccio; @ptc-org/nestjs-query-typeorm at latest; googleapis 105 -> common 8 drops uuid but needs the googleapis >=152 migration). Preserves the intentional uuid 13.x; @cyntler/react-doc-viewer/ajv 8.20.0 -> CVE, upstream (latest 1.17.1) pins ajv ^7 but never imports it, forcing v8 is safe; wrangler/esbuild + @react-email/ui/esbuild + react-email/esbuild 0.28.1 -> esbuild dev-server path-traversal GHSA-g7r4-m6w7-qqqr (Windows, vulnerable >=0.27.3 <0.28.1, fixed in 0.28.1): wrangler exact-pins esbuild 0.27.3 (still 0.27.3 in latest 4.100.0) and @react-email/ui exact-pins 0.28.0 (still 0.28.0 in latest 6.6.0) with no fixed upstream release; react-email allows ^0.28.0 but the npmMinimalAgeGate down-selects it to the still-vulnerable 0.28.0 until 0.28.1 ages past the gate (published 2026-06-11), so it is pinned forward here. Drop the exact-pin parents once they ship an esbuild >=0.28.1 pin; drop react-email once 0.28.1 clears the age gate (it then resolves to it naturally). Our own twenty-client-sdk raises its esbuild floor to ^0.28.1 directly in its package.json instead of via a resolution",
"//resolutions": "Each entry is load-bearing: it forces a version OUTSIDE some parent's declared range where no fixed upstream release exists; remove each once its blocker ships. @remote-dom/react/@types/react ^19.2.0 -> React type-identity dedup, SCOPED to @remote-dom/react only (every other package resolves @types/react 19 naturally from the workspace ^19.2.0 ranges). @remote-dom/react (transitive via twenty-front-component-renderer) lists @types/react ^18 in its own dependencies and nests its own copy; React 18 and 19 declare ReactNode differently (19 adds bigint + Promise<AwaitedReactNode>, drops ReactFragment's {}), so the two copies are mutually non-assignable and break twenty-front's typecheck (~156 TS2322 errors). Range-aligning our own packages can't fix a third-party's transitive @types pin, hence this single scoped override (only @types/react splits; runtime react/react-dom are peer-deps that converge naturally, and @types/react-dom does not nest a copy). Drop once @remote-dom/react widens @types/react to ^19 (latest 1.2.2 still pins ^18; tracked upstream in Shopify/remote-dom#153). graphql 16.8.1 -> singleton pin held below msw's ^16.12.0 dep and @nestjs/graphql's ^16.11.0 peer; drop after a validated repo-wide bump to latest 16.x; graphql-redis-subscriptions/ioredis 5.10.1 -> TS type-identity dedup: twenty-server passes its ioredis client into RedisPubSub, so this must equal the exact ioredis version pinned by twenty-server and bullmq (bump in lockstep); @types/qs 6.9.16 -> holdback below the 6.9.17 ParsedQs typing break (node-saml wants ^6.9.18); @opentelemetry/api 1.9.1 -> singleton guard for the NoopMeterProvider bug (#20231): ai 6.0.x pins 1.9.0 exact vs @sentry/node ^1.9.1, drop when workspace ai >=6.0.178 AND @scalar/agent-chat moves off ai 6.0.33; chokidar ^3 -> NestJS CLI watch needs fsevents on macOS, removed in chokidar 4/5 (#20316); tmp ^0.2.7 -> CVE, zapier-platform-cli 19 (latest) pins 0.2.5 and inquirer 7/8's external-editor wants ^0.0.33; make-fetch-happen ^15 + @electron/{rebuild,node-gyp}/tar ^7.5.16 -> tar CVE eviction for the @electron/rebuild 3.x toolchain (rebuild 3.x pins tar ^6, its node-gyp fork pins tar ^6.2.1 + mfh ^10), drop when electron-forge declares @electron/rebuild >=4; @angular-devkit/core 19.2.24 -> picomatch CVE, blocked on @nestjs/cli >11.0.23 fixing the dist/src output regression (repo held at 11.0.16); yeoman-environment 6.0.1 -> CVE, zapier-platform-cli 19 (latest) pins 4.4.3; webpack-dev-server 5.2.4 -> CVE, @electron-forge/plugin-webpack (incl. 8.x alphas) still declares ^4; express/qs + @cypress/request/qs 6.15.2 -> qs CVE for old express 4.22.0/4.22.1 pinned by @mintlify/previewing and verdaccio (verdaccio also pins @cypress/request 3.0.10; all other qs parents resolve safe naturally); next/postcss 8.5.15 -> postcss CVE, every stable next pins 8.4.31 exact (fix only in 16.3.0 canaries; @react-email/ui also pins next 16.2.6); <pkg>/uuid 11.1.1 -> uuid CVE for parents pinning uuid <11 with no fixed release (sockjs dormant since 2021; @cypress/request 3.0.10 via verdaccio; @ptc-org/nestjs-query-typeorm at latest; googleapis 105 -> common 8 drops uuid but needs the googleapis >=152 migration). Preserves the intentional uuid 13.x; @cyntler/react-doc-viewer/ajv 8.20.0 -> CVE, upstream (latest 1.17.1) pins ajv ^7 but never imports it, forcing v8 is safe; wrangler/esbuild + @react-email/ui/esbuild + react-email/esbuild 0.28.1 -> esbuild dev-server path-traversal GHSA-g7r4-m6w7-qqqr (Windows, vulnerable >=0.27.3 <0.28.1, fixed in 0.28.1): wrangler exact-pins esbuild 0.27.3 (still 0.27.3 in latest 4.100.0) and @react-email/ui exact-pins 0.28.0 (still 0.28.0 in latest 6.6.0) with no fixed upstream release; react-email allows ^0.28.0 but the npmMinimalAgeGate down-selects it to the still-vulnerable 0.28.0 until 0.28.1 ages past the gate (published 2026-06-11), so it is pinned forward here. Drop the exact-pin parents once they ship an esbuild >=0.28.1 pin; drop react-email once 0.28.1 clears the age gate (it then resolves to it naturally). Our own twenty-client-sdk raises its esbuild floor to ^0.28.1 directly in its package.json instead of via a resolution",
"version": "0.2.1",
"nx": {},
"scripts": {
+2 -2
View File
@@ -53,8 +53,8 @@
"express": "^4.18.2",
"marked": "^5.0.0",
"openai": "^4.97.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-markdown": "^10.1.0",
"zod": "^3.25.76"
}
+2 -2
View File
@@ -24,8 +24,8 @@
"@lingui/vite-plugin": "^5.9.5",
"@react-email/ui": "6.5.0",
"@tiptap/core": "^3.4.2",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@typescript/native-preview": "^7.0.0-dev.20260116.1",
"@vitejs/plugin-react-swc": "^4.3.1",
"react-email": "6.5.0",
@@ -1,3 +1,4 @@
import { type JSX } from 'react';
import { I18nProvider } from '@lingui/react';
import { Container, Html } from '@react-email/components';
@@ -1,3 +1,4 @@
import { type JSX } from 'react';
import { Button } from '@react-email/components';
import { emailTheme } from 'src/common-style';
@@ -1,5 +1,5 @@
import { Column, Container, Row } from '@react-email/components';
import React from 'react';
import React, { type JSX } from 'react';
import { emailTheme } from 'src/common-style';
@@ -1,3 +1,4 @@
import { type JSX } from 'react';
import { Link as EmailLink } from '@react-email/components';
import { emailTheme } from 'src/common-style';
@@ -1,3 +1,4 @@
import { type JSX } from 'react';
import { Text } from '@react-email/components';
import { emailTheme } from 'src/common-style';
@@ -1,3 +1,4 @@
import { type JSX } from 'react';
import { Text } from '@react-email/components';
import { emailTheme } from 'src/common-style';
@@ -1,3 +1,4 @@
import { type JSX } from 'react';
import { Heading } from '@react-email/components';
import { emailTheme } from 'src/common-style';
@@ -1,3 +1,4 @@
import { type JSX } from 'react';
import { Heading } from '@react-email/components';
import { emailTheme } from 'src/common-style';
@@ -1,5 +1,5 @@
import { type JSONContent } from '@tiptap/core';
import { Fragment, type ReactNode } from 'react';
import { type JSX, Fragment, type ReactNode } from 'react';
import { TIPTAP_NODE_TYPES, type TipTapNodeType } from 'twenty-shared/utils';
import { bulletList } from '@/utils/email-renderer/nodes/bullet-list';
import { hardBreak } from '@/utils/email-renderer/nodes/hard-break';
@@ -24,8 +24,8 @@
"@remote-dom/core": "^1.10.1",
"@remote-dom/react": "^1.2.2",
"@sniptt/guards": "^0.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"zod": "^4.1.11"
},
"devDependencies": {
@@ -36,8 +36,8 @@
"@storybook/addon-vitest": "^10.2.13",
"@storybook/react-vite": "^10.2.13",
"@types/node": "^24.0.0",
"@types/react": "^18.2.39",
"@types/react-dom": "^18.2.15",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@typescript/native-preview": "^7.0.0-dev.20260116.1",
"@vitest/browser-playwright": "^4.1.0",
"playwright": "^1.60.0",
@@ -24,11 +24,7 @@ const FormSubmitFrontComponent = () => {
return (
<FrontComponentCard title="form:submit">
<form
data-testid="subject"
action="javascript:void(0);"
onSubmit={handleSubmit}
>
<form data-testid="subject" onSubmit={handleSubmit}>
<input
data-testid="form-field"
type="text"
@@ -22,7 +22,7 @@ const TableClickFrontComponent = () => {
tabIndex={0}
style={FILL_RECT_STYLE}
>
table
<caption>table</caption>
</table>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
@@ -23,7 +23,7 @@ const TableFocusBlurFrontComponent = () => {
tabIndex={0}
style={FILL_RECT_STYLE}
>
table
<caption>table</caption>
</table>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
@@ -406,6 +406,7 @@ const createCaretPreservingElement = (
export const createHtmlHostWrapper = (htmlTag: string) => {
const isVoid = VOID_ELEMENTS.has(htmlTag);
const isIframe = htmlTag === 'iframe';
const isForm = htmlTag === 'form';
return ({ children, ...props }: WrapperProps) => {
const setEditableFocused = useContext(FrontComponentInputFocusContext);
@@ -413,7 +414,29 @@ export const createHtmlHostWrapper = (htmlTag: string) => {
const forcedProps: Record<string, unknown> | undefined = isIframe
? { sandbox: sanitizeIframeSandbox(reactProps.sandbox) }
: undefined;
: isForm
? {
// The remote component's onSubmit is forwarded asynchronously across
// the remote-dom boundary, so its preventDefault lands too late to
// stop a native form submission (which navigates and closes the
// page). Guard synchronously on the host while still forwarding the
// event to the remote handler. (React 19 also blocks the previous
// `action="javascript:void(0)"` guard.)
onSubmit: (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const remoteOnSubmit = reactProps.onSubmit;
// The remote prop is untrusted across the remote-dom boundary, so
// it may not be a function — guard before invoking.
if (typeof remoteOnSubmit === 'function') {
(
remoteOnSubmit as (
event: React.FormEvent<HTMLFormElement>,
) => void
)(event);
}
},
}
: undefined;
if (
htmlTag === 'textarea' ||
+1
View File
@@ -90,6 +90,7 @@ const jestConfig = {
maxWorkers: 3,
workerIdleMemoryLimit: '512MB',
errorOnDeprecated: true,
testTimeout: 30000,
};
export default jestConfig;
+9 -10
View File
@@ -38,10 +38,11 @@
"@cyntler/react-doc-viewer": "^1.17.0",
"@dagrejs/dagre": "^1.1.8",
"@dnd-kit/react": "^0.3.2",
"@dr.pogodin/react-helmet": "^3.2.2",
"@floating-ui/react": "^0.24.3",
"@graphiql/plugin-explorer": "^1.0.2",
"@graphiql/react": "^0.23.0",
"@hello-pangea/dnd": "^16.2.0",
"@graphiql/plugin-explorer": "^5.1.2",
"@graphiql/react": "^0.37.6",
"@hello-pangea/dnd": "^18.0.1",
"@hookform/resolvers": "^5.2.2",
"@lingui/core": "^5.9.5",
"@lingui/detect-locale": "^5.9.5",
@@ -89,7 +90,7 @@
"file-saver": "^2.0.5",
"framer-motion": "^11.18.0",
"fuse.js": "^7.1.0",
"graphiql": "^3.1.1",
"graphiql": "^5.2.3",
"graphql": "16.8.1",
"graphql-sse": "^2.5.4",
"graphql-tag": "^2.12.6",
@@ -113,14 +114,13 @@
"papaparse": "^5.4.1",
"pluralize": "^8.0.0",
"qs": "^6.15.2",
"react": "^18.2.0",
"react-data-grid": "7.0.0-beta.13",
"react-datepicker": "^6.7.1",
"react-dom": "^18.2.0",
"react": "^19.2.0",
"react-data-grid": "7.0.0-beta.59",
"react-datepicker": "^9.1.0",
"react-dom": "^19.2.0",
"react-dropzone": "^14.2.3",
"react-error-boundary": "^4.0.11",
"react-grid-layout": "^1.5.2",
"react-helmet-async": "^1.3.0",
"react-hook-form": "^7.45.1",
"react-hotkeys-hook": "^4.4.4",
"react-imask": "^7.6.0",
@@ -175,7 +175,6 @@
"@types/jest": "^30.0.0",
"@types/js-cookie": "^3.0.3",
"@types/json-logic-js": "^2",
"@types/react-datepicker": "^6.2.0",
"@types/react-grid-layout": "^1",
"@types/uuid": "^9.0.2",
"@vitejs/plugin-react-swc": "^4.3.1",
@@ -9,7 +9,7 @@ import { i18n } from '@lingui/core';
import { I18nProvider } from '@lingui/react';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { Provider as JotaiProvider } from 'jotai';
import { HelmetProvider } from 'react-helmet-async';
import { HelmetProvider } from '@dr.pogodin/react-helmet';
import { IconsProvider } from 'twenty-ui-deprecated/display';
import { initialI18nActivate } from '~/utils/i18n/initialI18nActivate';
@@ -8,7 +8,7 @@ import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useC
import { useContext, type RefObject } from 'react';
export type RecordBoardDragSelectProps = {
boardRef: RefObject<HTMLDivElement>;
boardRef: RefObject<HTMLDivElement | null>;
};
export const RecordBoardDragSelect = ({
@@ -2,6 +2,7 @@ import { Draggable } from '@hello-pangea/dnd';
import { styled } from '@linaria/react';
import { useContext } from 'react';
import { getCssCompatibleDraggableProps } from '@/ui/layout/draggable-list/utils/getCssCompatibleDraggableProps';
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { RecordBoardCard } from '@/object-record/record-board/record-board-card/components/RecordBoardCard';
@@ -71,7 +72,9 @@ export const RecordBoardCardDraggableContainer = ({
// oxlint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided?.dragHandleProps}
// oxlint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided?.draggableProps}
{...getCssCompatibleDraggableProps(
draggableProvided.draggableProps,
)}
data-selectable-id={recordId}
data-select-disable
>
@@ -3,6 +3,7 @@ import { styled } from '@linaria/react';
import { useContext } from 'react';
import { themeCssVariables } from 'twenty-ui-deprecated/theme-constants';
import { getCssCompatibleDraggableProps } from '@/ui/layout/draggable-list/utils/getCssCompatibleDraggableProps';
import { RecordBoardCardDraggableContainer } from '@/object-record/record-board/record-board-card/components/RecordBoardCardDraggableContainer';
import { RecordBoardColumnNewRecordButton } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnNewRecordButton';
@@ -63,7 +64,9 @@ export const RecordBoardColumnCardsContainer = ({
<div
ref={draggableProvided.innerRef}
// oxlint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided.draggableProps}
{...getCssCompatibleDraggableProps(
draggableProvided.draggableProps,
)}
></div>
)}
</Draggable>
@@ -1,6 +1,7 @@
import { styled } from '@linaria/react';
import { Draggable } from '@hello-pangea/dnd';
import { getCssCompatibleDraggableProps } from '@/ui/layout/draggable-list/utils/getCssCompatibleDraggableProps';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
import { isFieldMetadataReadOnlyByPermissions } from '@/object-record/read-only/utils/internal/isFieldMetadataReadOnlyByPermissions';
@@ -80,7 +81,9 @@ export const RecordCalendarCardDraggableContainer = ({
// oxlint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided?.dragHandleProps}
// oxlint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided?.draggableProps}
{...getCssCompatibleDraggableProps(
draggableProvided.draggableProps,
)}
data-selectable-id={recordId}
>
<RecordCalendarCard recordId={recordId} />
@@ -35,7 +35,7 @@ export const Default: Story = {};
export const Performance = getProfilingStory({
componentName: 'ActorFieldDisplay',
averageThresholdInMs: 0.2,
averageThresholdInMs: 0.4,
numberOfRuns: 20,
numberOfTestsPerRun: 100,
});
@@ -57,7 +57,7 @@ export const Elipsis: Story = {
export const Performance = getProfilingStory({
componentName: 'AddressFieldDisplay',
averageThresholdInMs: 0.15,
averageThresholdInMs: 0.3,
numberOfRuns: 20,
numberOfTestsPerRun: 100,
});
@@ -28,7 +28,7 @@ export const Default: Story = {};
export const Performance = getProfilingStory({
componentName: 'BooleanFieldDisplay',
averageThresholdInMs: 0.15,
averageThresholdInMs: 0.3,
numberOfRuns: 50,
numberOfTestsPerRun: 100,
});
@@ -83,7 +83,7 @@ export const Default: Story = {};
export const Performance = getProfilingStory({
componentName: 'ChipFieldDisplay',
averageThresholdInMs: 0.3,
averageThresholdInMs: 0.6,
numberOfRuns: 20,
numberOfTestsPerRun: 100,
});
@@ -58,7 +58,7 @@ export const Bazillions: Story = {
export const Performance = getProfilingStory({
componentName: 'CurrencyFieldDisplay',
averageThresholdInMs: 0.2,
averageThresholdInMs: 0.4,
numberOfRuns: 50,
numberOfTestsPerRun: 100,
});
@@ -50,7 +50,7 @@ export const Elipsis: Story = {
export const Performance = getProfilingStory({
componentName: 'DateFieldDisplay',
averageThresholdInMs: 0.1,
averageThresholdInMs: 0.2,
numberOfRuns: 50,
numberOfTestsPerRun: 100,
});
@@ -50,7 +50,7 @@ export const Elipsis: Story = {
export const Performance = getProfilingStory({
componentName: 'DateTimeFieldDisplay',
averageThresholdInMs: 0.35,
averageThresholdInMs: 0.7,
numberOfRuns: 30,
numberOfTestsPerRun: 30,
});
@@ -46,7 +46,7 @@ export const Elipsis: Story = {
export const Performance = getProfilingStory({
componentName: 'EmailsFieldDisplay',
averageThresholdInMs: 0.5,
averageThresholdInMs: 1,
numberOfRuns: 50,
numberOfTestsPerRun: 100,
});
@@ -54,7 +54,7 @@ export const Elipsis: Story = {
export const Performance = getProfilingStory({
componentName: 'FilesFieldDisplay',
averageThresholdInMs: 0.8,
averageThresholdInMs: 1.6,
numberOfRuns: 50,
numberOfTestsPerRun: 100,
});
@@ -34,7 +34,7 @@ export const Elipsis: Story = {
export const Performance = getProfilingStory({
componentName: 'FullNameFieldDisplay',
averageThresholdInMs: 0.5,
averageThresholdInMs: 1,
numberOfRuns: 50,
numberOfTestsPerRun: 100,
});
@@ -69,7 +69,7 @@ export const Elipsis: Story = {
export const Performance = getProfilingStory({
componentName: 'LinksFieldDisplay',
averageThresholdInMs: 0.5,
averageThresholdInMs: 1,
numberOfRuns: 50,
numberOfTestsPerRun: 100,
});
@@ -67,7 +67,7 @@ export const Elipsis: Story = {
export const Performance = getProfilingStory({
componentName: 'MultiSelectFieldDisplay',
averageThresholdInMs: 0.2,
averageThresholdInMs: 0.4,
numberOfRuns: 50,
numberOfTestsPerRun: 100,
});
@@ -47,7 +47,7 @@ export const Float: Story = {
export const Performance = getProfilingStory({
componentName: 'NumberFieldDisplay',
averageThresholdInMs: 0.5,
averageThresholdInMs: 1,
numberOfRuns: 50,
numberOfTestsPerRun: 100,
});
@@ -46,7 +46,7 @@ export const WrongNumber: Story = {
export const Performance = getProfilingStory({
componentName: 'PhonesFieldDisplay',
averageThresholdInMs: 0.5,
averageThresholdInMs: 1,
numberOfRuns: 20,
numberOfTestsPerRun: 100,
});
@@ -28,7 +28,7 @@ export const Default: Story = {};
export const Performance = getProfilingStory({
componentName: 'RatingFieldDisplay',
averageThresholdInMs: 0.5,
averageThresholdInMs: 1,
numberOfRuns: 30,
numberOfTestsPerRun: 30,
});
@@ -74,7 +74,7 @@ export const Default: Story = {};
// TODO: optimize this component once we have morph many
export const Performance = getProfilingStory({
componentName: 'RelationFromManyFieldDisplay',
averageThresholdInMs: 1,
averageThresholdInMs: 2,
numberOfRuns: 20,
numberOfTestsPerRun: 100,
});
@@ -30,7 +30,7 @@ export const Default: Story = {};
export const Performance = getProfilingStory({
componentName: 'RelationFieldDisplay',
averageThresholdInMs: 0.4,
averageThresholdInMs: 0.8,
numberOfRuns: 20,
numberOfTestsPerRun: 100,
});
@@ -34,7 +34,7 @@ export const Elipsis: Story = {
export const Performance = getProfilingStory({
componentName: 'SelectFieldDisplay',
averageThresholdInMs: 0.2,
averageThresholdInMs: 0.4,
numberOfRuns: 50,
numberOfTestsPerRun: 100,
});
@@ -41,7 +41,7 @@ export const Elipsis: Story = {
export const Performance = getProfilingStory({
componentName: 'TextFieldDisplay',
averageThresholdInMs: 0.5,
averageThresholdInMs: 1,
numberOfRuns: 50,
numberOfTestsPerRun: 100,
});
@@ -5,7 +5,7 @@ export const MultipleRecordPickerOnClickOutsideEffect = ({
containerRef,
onClickOutside,
}: {
containerRef: React.RefObject<HTMLDivElement>;
containerRef: React.RefObject<HTMLDivElement | null>;
onClickOutside: () => void;
}) => {
useListenClickOutside({
@@ -10,7 +10,7 @@ import { RecordTableVirtualizedInitialDataLoadEffect } from '@/object-record/rec
export interface RecordTableBodyEffectsWrapperProps {
hasRecordGroups: boolean;
tableBodyRef: React.RefObject<HTMLDivElement>;
tableBodyRef: React.RefObject<HTMLDivElement | null>;
}
export const RecordTableBodyEffectsWrapper = ({
@@ -33,7 +33,7 @@ const StyledTableContainer = styled.div`
`;
export interface RecordTableContentProps {
tableBodyRef: React.RefObject<HTMLDivElement>;
tableBodyRef: React.RefObject<HTMLDivElement | null>;
handleDragSelectionStart: () => void;
handleDragSelectionEnd: () => void;
hasRecordGroups: boolean;
@@ -31,7 +31,7 @@ const StyledEmptyStateContainer = styled.div<{ width: number }>`
`;
export interface RecordTableEmptyProps {
tableBodyRef: React.RefObject<HTMLDivElement>;
tableBodyRef: React.RefObject<HTMLDivElement | null>;
}
export const RecordTableEmpty = ({ tableBodyRef }: RecordTableEmptyProps) => {
@@ -280,7 +280,7 @@ export const Default: Story = {};
export const Performance = getProfilingStory({
componentName: 'RecordTableCell',
averageThresholdInMs: 0.3,
averageThresholdInMs: 0.6,
numberOfRuns: 50,
numberOfTestsPerRun: 200,
warmUpRounds: 20,
@@ -11,7 +11,7 @@ import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentTyp
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
type RecordTableBodyFocusClickOutsideEffectProps = {
tableBodyRef: React.RefObject<HTMLDivElement>;
tableBodyRef: React.RefObject<HTMLDivElement | null>;
};
export const RecordTableBodyFocusClickOutsideEffect = ({
@@ -7,7 +7,7 @@ import { type RefObject } from 'react';
export const PageLayoutGridLayoutDragSelector = ({
gridContainerRef,
}: {
gridContainerRef: RefObject<HTMLDivElement>;
gridContainerRef: RefObject<HTMLDivElement | null>;
}) => {
const { startPageLayoutDragSelection } = useStartPageLayoutDragSelection();
const { changePageLayoutDragSelection } = useChangePageLayoutDragSelection();
@@ -17,6 +17,8 @@ import { type ReactNode, useId } from 'react';
import { themeCssVariables } from 'twenty-ui-deprecated/theme-constants';
import { useIsMobile } from 'twenty-ui-deprecated/utilities';
import { getCssCompatibleDraggableProps } from '@/ui/layout/draggable-list/utils/getCssCompatibleDraggableProps';
const StyledVerticalListContainer = styled.div<{
variant: PageLayoutVerticalListViewerVariant;
shouldUseWhiteBackground: boolean;
@@ -102,7 +104,7 @@ export const PageLayoutVerticalListEditor = ({
<StyledDraggableWrapper
ref={provided.innerRef}
// oxlint-disable-next-line react/jsx-props-no-spreading
{...provided.draggableProps}
{...getCssCompatibleDraggableProps(provided.draggableProps)}
isDragging={snapshot.isDragging}
>
{/* oxlint-disable-next-line react/jsx-props-no-spreading */}
@@ -3,6 +3,7 @@ import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
import { getCssCompatibleDraggableProps } from '@/ui/layout/draggable-list/utils/getCssCompatibleDraggableProps';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { FieldsConfigurationFieldEditor } from '@/page-layout/widgets/fields/components/FieldsConfigurationFieldEditor';
@@ -134,7 +135,7 @@ export const FieldsConfigurationGroupEditor = ({
<StyledGroupContainer
ref={draggableProvided.innerRef}
// oxlint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided.draggableProps}
{...getCssCompatibleDraggableProps(draggableProvided.draggableProps)}
isDragging={isDragging}
>
{/* oxlint-disable-next-line react/jsx-props-no-spreading */}
@@ -28,7 +28,7 @@ type BarChartBaseLayerEffectProps = {
showGrid: boolean;
highlightedLegendId: string | null;
allowDataTransitions: boolean;
canvasRef: RefObject<HTMLCanvasElement>;
canvasRef: RefObject<HTMLCanvasElement | null>;
};
type AnimationState = {
@@ -12,7 +12,7 @@ type BarChartHoverLayerEffectProps = {
chartHeight: number;
margins: ChartMargins;
layout: BarChartLayout;
canvasRef: RefObject<HTMLCanvasElement>;
canvasRef: RefObject<HTMLCanvasElement | null>;
};
export const BarChartHoverLayerEffect = ({
@@ -12,7 +12,7 @@ import { type RefObject } from 'react';
import { isDefined } from 'twenty-shared/utils';
type BarChartTooltipProps = {
containerRef: RefObject<HTMLDivElement>;
containerRef: RefObject<HTMLDivElement | null>;
dataByIndexValue: Map<string, BarChartDatum>;
enrichedKeys: BarChartEnrichedKey[];
formatOptions: GraphValueFormatOptions;
@@ -11,7 +11,7 @@ import { type RefObject } from 'react';
import { isDefined } from 'twenty-shared/utils';
type GraphLineChartTooltipProps = {
containerRef: RefObject<HTMLDivElement>;
containerRef: RefObject<HTMLDivElement | null>;
enrichedSeries: LineChartEnrichedSeries[];
formatOptions: GraphValueFormatOptions;
isStacked?: boolean;
@@ -11,7 +11,7 @@ import { type RefObject } from 'react';
import { isDefined } from 'twenty-shared/utils';
type GraphPieChartTooltipProps = {
containerRef: RefObject<HTMLDivElement>;
containerRef: RefObject<HTMLDivElement | null>;
enrichedData: PieChartEnrichedData[];
formatOptions: GraphValueFormatOptions;
displayType?: string;
@@ -164,7 +164,11 @@ export const SettingsObjectRelationItemTableRow = ({
return (
<TableRow
gridTemplateColumns={OBJECT_RELATION_TABLE_ROW_GRID_TEMPLATE_COLUMNS}
to={linkToNavigate}
// The row can't be a Link: it contains a nested link to the related
// object, and <a> inside <a> is invalid HTML (React 19 errors on it).
// oxlint-disable-next-line twenty/no-navigate-prefer-link
onClick={navigateToFieldEdit}
cursor="pointer"
>
<TableCell
color={themeCssVariables.font.color.primary}
@@ -230,7 +234,14 @@ export const SettingsObjectRelationItemTableRow = ({
padding={`0 ${themeCssVariables.spacing[1]} 0 ${themeCssVariables.spacing[2]}`}
>
{fieldMetadataItem.isActive ? (
<UndecoratedLink to={linkToNavigate}>
// The row navigates via onClick (it can't be a Link because it
// contains a nested link to the related object). This chevron is a
// real link to the same destination so keyboard users can still reach
// field edit; stopPropagation avoids firing the row onClick too.
<UndecoratedLink
to={linkToNavigate}
onClick={(event) => event.stopPropagation()}
>
<StyledIconChevronRightContainer>
<IconChevronRight
size={theme.icon.size.md}
@@ -6,10 +6,10 @@ import { PlaygroundSchemas } from '@/settings/playground/types/PlaygroundSchemas
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { styled } from '@linaria/react';
import { explorerPlugin } from '@graphiql/plugin-explorer';
import '@graphiql/plugin-explorer/dist/style.css';
import '@graphiql/plugin-explorer/style.css';
import { createGraphiQLFetcher } from '@graphiql/toolkit';
import { GraphiQL } from 'graphiql';
import 'graphiql/graphiql.css';
import 'graphiql/style.css';
import { useContext } from 'react';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import {
@@ -1,6 +1,8 @@
import { Draggable } from '@hello-pangea/dnd';
import { type ReactNode } from 'react';
import { getCssCompatibleDraggableProps } from '@/ui/layout/draggable-list/utils/getCssCompatibleDraggableProps';
type SidePanelAddToNavigationDraggablePlaceholderProps = {
index: number;
children: ReactNode;
@@ -19,7 +21,7 @@ export const SidePanelAddToNavigationDraggablePlaceholder = ({
<div
ref={provided.innerRef}
// oxlint-disable-next-line react/jsx-props-no-spreading
{...provided.draggableProps}
{...getCssCompatibleDraggableProps(provided.draggableProps)}
>
{children}
</div>
@@ -4,7 +4,7 @@ import { useEffect } from 'react';
import { SidePanelPages } from 'twenty-shared/types';
type SidePanelTopBarInputFocusEffectProps = {
inputRef: React.RefObject<HTMLInputElement>;
inputRef: React.RefObject<HTMLInputElement | null>;
};
export const SidePanelTopBarInputFocusEffect = ({
@@ -1,7 +1,6 @@
import { styled } from '@linaria/react';
import { useContext } from 'react';
// @ts-expect-error // Todo: remove usage of react-data-grid
import DataGrid, { type DataGridProps } from 'react-data-grid';
import { DataGrid, type DataGridProps } from 'react-data-grid';
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import {
ThemeContext,
@@ -118,7 +117,7 @@ type SpreadsheetImportTableProps<Data> = Pick<
| 'rows'
> &
Partial<
Pick<DataGridProps<Data>, 'onRowClick' | 'components' | 'onRowsChange'>
Pick<DataGridProps<Data>, 'onCellClick' | 'renderers' | 'onRowsChange'>
> & {
className?: string;
rowHeight?: number;
@@ -128,12 +127,12 @@ type SpreadsheetImportTableProps<Data> = Pick<
export const SpreadsheetImportTable = <Data,>({
className,
columns,
components,
renderers,
headerRowHeight,
rowKeyGetter,
rows,
onRowsChange,
onRowClick,
onCellClick,
onSelectedRowsChange,
selectedRows,
}: SpreadsheetImportTableProps<Data>) => {
@@ -145,7 +144,7 @@ export const SpreadsheetImportTable = <Data,>({
if (!rows?.length || !columns?.length) return null;
return (
<StyledDataGridContainer headerRowHeight={headerRowHeight}>
<StyledDataGridContainer headerRowHeight={headerRowHeight ?? undefined}>
<DataGrid
direction={rtl ? 'rtl' : 'ltr'}
rowHeight={40}
@@ -156,8 +155,8 @@ export const SpreadsheetImportTable = <Data,>({
rowKeyGetter,
onRowsChange,
rows,
components,
onRowClick,
renderers,
onCellClick,
onSelectedRowsChange,
selectedRows,
}}
@@ -1,6 +1,5 @@
// @ts-expect-error // Todo: remove usage of react-data-grid
// prettier-ignore
import { useRowSelection, type Column, type FormatterProps } from 'react-data-grid';
import { useRowSelection, type Column, type RenderCellProps } from 'react-data-grid';
import { t } from '@lingui/core/macro';
import { type ImportedRow } from '@/spreadsheet-import/types';
@@ -8,10 +7,10 @@ import { Radio } from 'twenty-ui-deprecated/input';
const SELECT_COLUMN_KEY = 'select-row';
type SelectFormatterProps = FormatterProps<unknown>;
type SelectFormatterProps = RenderCellProps<unknown>;
const SelectFormatter = (props: SelectFormatterProps) => {
const [isRowSelected, onRowSelectionChange] = useRowSelection();
const { isRowSelected, onRowSelectionChange } = useRowSelection();
return (
<Radio
@@ -38,7 +37,7 @@ export const SelectColumn: Column<any, any> = {
sortable: false,
frozen: true,
cellClass: 'rdg-radio',
formatter: SelectFormatter,
renderCell: SelectFormatter,
};
export const generateSelectionColumns = (data: ImportedRow[]) => {
@@ -28,16 +28,17 @@ export const SelectHeaderTable = ({
rows={importedRows}
columns={columns}
selectedRows={selectedRowIndexes}
onSelectedRowsChange={(newRowIndexes: number[]) => {
newRowIndexes.forEach((value) => {
if (!selectedRowIndexes.has(value)) {
setSelectedRowIndexes(new Set([value]));
onSelectedRowsChange={(newSelectedRows) => {
for (const value of newSelectedRows) {
const rowIndex = Number(value);
if (!selectedRowIndexes.has(rowIndex)) {
setSelectedRowIndexes(new Set([rowIndex]));
return;
}
});
}
}}
onRowClick={(row: any) => {
setSelectedRowIndexes(new Set([importedRows.indexOf(row)]));
onCellClick={(args) => {
setSelectedRowIndexes(new Set([importedRows.indexOf(args.row)]));
}}
headerRowHeight={0}
/>
@@ -23,7 +23,6 @@ import {
} from 'react';
import { ModalContent } from 'twenty-ui-deprecated/layout';
import { themeCssVariables } from 'twenty-ui-deprecated/theme-constants';
// @ts-expect-error Todo: remove usage of react-data-grid`
import { type RowsChangeData } from 'react-data-grid';
import { isDefined } from 'twenty-shared/utils';
import { IconTrash } from 'twenty-ui-deprecated/display';
@@ -200,7 +199,7 @@ export const ValidationStep = ({
if (!hasBeenImported) return null;
return column;
})
.filter(Boolean),
.filter(isDefined),
[fields, importedColumns],
);
@@ -306,7 +305,7 @@ export const ValidationStep = ({
columns={columns}
selectedRows={selectedRows}
onSelectedRowsChange={setSelectedRows as any} // TODO: replace 'any'
components={{
renderers={{
noRowsFallback: (
<StyledNoRowsContainer>
{filterByErrors
@@ -1,7 +1,6 @@
import { t } from '@lingui/core/macro';
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui-deprecated/theme-constants';
// @ts-expect-error // Todo: remove usage of react-data-grid
import { type Column, useRowSelection } from 'react-data-grid';
import { createPortal } from 'react-dom';
@@ -86,9 +85,9 @@ export const generateColumns = (
resizable: false,
sortable: false,
frozen: true,
formatter: (props: any) => {
renderCell: (props: any) => {
// oxlint-disable-next-line react-hooks/rules-of-hooks
const [isRowSelected, onRowSelectionChange] = useRowSelection();
const { isRowSelected, onRowSelectionChange } = useRowSelection();
return (
<StyledCheckboxContainer>
@@ -116,7 +115,7 @@ export const generateColumns = (
name: column.label,
minWidth: 150,
resizable: true,
headerRenderer: () => (
renderHeaderCell: () => (
<StyledHeaderContainer>
<StyledHeaderLabel id={formatSafeId(column.key)}>
{column.label}
@@ -136,7 +135,7 @@ export const generateColumns = (
),
editable: column.fieldType.type !== 'checkbox',
// Todo: remove usage of react-data-grid
editor: ({ row, onRowChange, onClose }: any) => {
renderEditCell: ({ row, onRowChange, onClose }: any) => {
const columnKey = column.key as keyof (ImportedStructuredRow &
ImportedStructuredRowMetadata);
let component;
@@ -166,11 +165,8 @@ export const generateColumns = (
return <StyledInputContainer>{component}</StyledInputContainer>;
},
editorOptions: {
editOnClick: true,
},
// Todo: remove usage of react-data-grid
formatter: ({ row, onRowChange }: { row: any; onRowChange: any }) => {
renderCell: ({ row, onRowChange }: { row: any; onRowChange: any }) => {
const columnKey = column.key as keyof (ImportedStructuredRow &
ImportedStructuredRowMetadata);
let component;
@@ -6,7 +6,7 @@ import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { lazy, Suspense, useEffect } from 'react';
import { type JSX, lazy, Suspense, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { isDefined } from 'twenty-shared/utils';
import { IconDownload, IconX } from 'twenty-ui-deprecated/display';
@@ -25,7 +25,7 @@ export const Default: Story = {};
export const Performance = getProfilingStory({
componentName: 'EllipsisDisplay',
averageThresholdInMs: 0.1,
averageThresholdInMs: 0.2,
numberOfRuns: 20,
numberOfTestsPerRun: 10,
});
@@ -4,7 +4,7 @@ import { type FieldAddressDraftValue } from '@/object-record/record-field/ui/typ
export const useFocusManagement = (
inputRefs: {
[key in keyof FieldAddressDraftValue]?: RefObject<HTMLInputElement>;
[key in keyof FieldAddressDraftValue]?: RefObject<HTMLInputElement | null>;
},
internalValue: FieldAddressDraftValue,
onTab?: (newAddress: FieldAddressDraftValue) => void,
@@ -1,6 +1,6 @@
import { styled } from '@linaria/react';
import { lazy, Suspense, useContext, type ComponentType } from 'react';
import type { ReactDatePickerProps as ReactDatePickerLibProps } from 'react-datepicker';
import type { DatePickerProps as ReactDatePickerLibProps } from 'react-datepicker';
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
@@ -334,10 +334,19 @@ type DatePickerProps = {
hideCalendar?: boolean;
};
type DatePickerPropsType = ReactDatePickerLibProps<
boolean | undefined,
boolean | undefined
>;
// react-datepicker v9 types its props as a discriminated union keyed on
// selectsRange/selectsMultiple. We drive selectsMultiple dynamically, which TS
// cannot narrow to a single union branch, so collapse the discriminants to plain
// optionals (selectedDates is accepted but ignored by the library at runtime).
type DatePickerPropsType = Omit<
ReactDatePickerLibProps,
'selectsRange' | 'selectsMultiple' | 'onChange' | 'formatMultipleDates'
> & {
selectsRange?: boolean;
selectsMultiple?: boolean;
selectedDates?: Date[];
onChange?: (date: Date | null) => void;
};
const ReactDatePicker = lazy<ComponentType<DatePickerPropsType>>(() =>
import('react-datepicker').then((mod) => ({
@@ -410,13 +419,19 @@ export const DatePicker = ({
onChange?.(newDate?.toString() ?? null);
};
const handleDateChange = (datePicked: Date) => {
const handleDateChange = (datePicked: Date | null) => {
if (!isDefined(datePicked)) {
return;
}
const plainDatePicked = turnJSDateToPlainDate(datePicked);
onChange?.(plainDatePicked.toString());
};
const handleDateSelect = (datePicked: Date) => {
const handleDateSelect = (datePicked: Date | null) => {
if (!isDefined(datePicked)) {
return;
}
const plainDatePicked = turnJSDateToPlainDate(datePicked);
handleClose?.(plainDatePicked.toString());
@@ -493,7 +508,9 @@ export const DatePicker = ({
openToDate={dateForDatePicker ?? undefined}
disabledKeyboardNavigation
onChange={handleDateChange}
calendarStartDay={calendarStartDay}
calendarStartDay={
calendarStartDay as 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined
}
renderCustomHeader={({
prevMonthButtonDisabled,
nextMonthButtonDisabled,
@@ -1,6 +1,6 @@
import { styled } from '@linaria/react';
import { Suspense, lazy, useContext, type ComponentType } from 'react';
import type { ReactDatePickerProps as ReactDatePickerLibProps } from 'react-datepicker';
import type { DatePickerProps as ReactDatePickerLibProps } from 'react-datepicker';
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
@@ -309,10 +309,7 @@ type DatePickerWithoutCalendarProps = {
keyboardEventsDisabled?: boolean;
};
type DatePickerPropsType = ReactDatePickerLibProps<
boolean | undefined,
boolean | undefined
>;
type DatePickerPropsType = ReactDatePickerLibProps;
const ReactDatePicker = lazy<ComponentType<DatePickerPropsType>>(() =>
import('react-datepicker').then((mod) => ({
@@ -370,13 +367,19 @@ export const DatePickerWithoutCalendar = ({
onChange?.(newDate?.toString() ?? null);
};
const handleDateChange = (datePicked: Date) => {
const handleDateChange = (datePicked: Date | null) => {
if (!isDefined(datePicked)) {
return;
}
const plainDatePicked = turnJSDateToPlainDate(datePicked);
onChange?.(plainDatePicked.toString());
};
const handleDateSelect = (datePicked: Date) => {
const handleDateSelect = (datePicked: Date | null) => {
if (!isDefined(datePicked)) {
return;
}
const plainDatePicked = turnJSDateToPlainDate(datePicked);
handleClose?.(plainDatePicked.toString());
@@ -443,7 +446,9 @@ export const DatePickerWithoutCalendar = ({
openToDate={dateForDatePicker ?? undefined}
disabledKeyboardNavigation
onChange={handleDateChange}
calendarStartDay={calendarStartDay}
calendarStartDay={
calendarStartDay as 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined
}
renderCustomHeader={({
prevMonthButtonDisabled,
nextMonthButtonDisabled,
@@ -15,7 +15,7 @@ import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { lazy, Suspense, useContext, type ComponentType } from 'react';
import type { ReactDatePickerProps as ReactDatePickerLibProps } from 'react-datepicker';
import type { DatePickerProps as ReactDatePickerLibProps } from 'react-datepicker';
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
import 'react-datepicker/dist/react-datepicker.css';
@@ -346,10 +346,19 @@ type DateTimePickerProps = {
timeZone?: string;
};
type DatePickerPropsType = ReactDatePickerLibProps<
boolean | undefined,
boolean | undefined
>;
// react-datepicker v9 types its props as a discriminated union keyed on
// selectsRange/selectsMultiple. We drive selectsMultiple dynamically, which TS
// cannot narrow to a single union branch, so collapse the discriminants to plain
// optionals (selectedDates is accepted but ignored by the library at runtime).
type DatePickerPropsType = Omit<
ReactDatePickerLibProps,
'selectsRange' | 'selectsMultiple' | 'onChange' | 'formatMultipleDates'
> & {
selectsRange?: boolean;
selectsMultiple?: boolean;
selectedDates?: Date[];
onChange?: (date: Date | null) => void;
};
const ReactDatePicker = lazy<ComponentType<DatePickerPropsType>>(() =>
import('react-datepicker').then((mod) => ({
@@ -438,13 +447,19 @@ export const DateTimePicker = ({
onChange?.(newZonedDateTime);
};
const handleDateChange = (newDate: Date) => {
const handleDateChange = (newDate: Date | null) => {
if (!isDefined(newDate)) {
return;
}
const { zonedDateTime } = getZonedDateTimeFromDatePicked(newDate);
onChange?.(zonedDateTime);
};
const handleDateSelect = (newDate: Date) => {
const handleDateSelect = (newDate: Date | null) => {
if (!isDefined(newDate)) {
return;
}
const { zonedDateTime } = getZonedDateTimeFromDatePicked(newDate);
handleClose?.(zonedDateTime);
@@ -524,7 +539,9 @@ export const DateTimePicker = ({
openToDate={shiftedDateForReactDatePicker}
disabledKeyboardNavigation
onChange={handleDateChange}
calendarStartDay={calendarStartDayNumber}
calendarStartDay={
calendarStartDayNumber as 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined
}
renderCustomHeader={({
prevMonthButtonDisabled,
nextMonthButtonDisabled,
@@ -1,6 +1,6 @@
import { Draggable } from '@hello-pangea/dnd';
import { isFunction } from '@sniptt/guards';
import { useContext } from 'react';
import { type JSX, useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { ThemeContext } from 'twenty-ui-deprecated/theme-constants';
@@ -0,0 +1,15 @@
import { type DraggableProvidedDraggableProps } from '@hello-pangea/dnd';
import { type CSSProperties } from 'react';
type CssCompatibleDraggableProps = Omit<
DraggableProvidedDraggableProps,
'style'
> & { style?: CSSProperties };
// @hello-pangea/dnd types draggableProps.style as DraggingStyle | NotDraggingStyle —
// closed interfaces that do not satisfy the `--radix-${string}` index signature
// @radix-ui/react-popper augments onto React.CSSProperties. Widen the style so the
// props can be spread onto a styled element without a per-call-site cast.
export const getCssCompatibleDraggableProps = (
draggableProps: DraggableProvidedDraggableProps,
): CssCompatibleDraggableProps => draggableProps as CssCompatibleDraggableProps;
@@ -1,3 +1,4 @@
import { type JSX } from 'react';
import { PageHeader } from '@/ui/layout/page/components/PageHeader';
import { PAGE_BAR_MIN_HEIGHT } from '@/ui/layout/page/constants/PageBarMinHeight';
import {
@@ -5,7 +5,7 @@ import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useLis
import { Key } from 'ts-key-enum';
type ModalHotkeysAndClickOutsideEffectProps = {
modalRef: React.RefObject<HTMLDivElement>;
modalRef: React.RefObject<HTMLDivElement | null>;
onEnter?: () => void;
isClosable?: boolean;
onClose?: () => void;
@@ -4,7 +4,7 @@ import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { type FocusEvent, useRef } from 'react';
import { type JSX, type FocusEvent, useRef } from 'react';
import { Key } from 'ts-key-enum';
import { isDefined } from 'twenty-shared/utils';
import {
@@ -10,7 +10,7 @@ import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomStat
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { type ReactNode, useContext } from 'react';
import { type JSX, type ReactNode, useContext } from 'react';
import { Link } from 'react-router-dom';
import { isDefined } from 'twenty-shared/utils';
import { Pill } from 'twenty-ui-deprecated/components';
@@ -1,4 +1,4 @@
import { createContext } from 'react';
import { type JSX, createContext } from 'react';
import { useSystemColorScheme } from '@/ui/theme/hooks/useSystemColorScheme';
import { persistedColorSchemeState } from '@/ui/theme/states/persistedColorSchemeState';
@@ -2,7 +2,7 @@ import { type RefObject, useEffect } from 'react';
import { isDefined } from 'twenty-shared/utils';
type NodeDimensionEffectProps = {
elementRef: RefObject<HTMLElement>;
elementRef: RefObject<HTMLElement | null>;
onDimensionChange: (dimensions: { width: number; height: number }) => void;
};
@@ -10,7 +10,7 @@ import { type SelectionBox } from '@/ui/utilities/drag-select/types/SelectionBox
import { isValidSelectionStart } from '@/ui/utilities/drag-select/utils/selectionBoxValidation';
type DragSelectProps = {
selectableItemsContainerRef: RefObject<HTMLElement>;
selectableItemsContainerRef: RefObject<HTMLElement | null>;
onDragSelectionChange: (id: string, selected: boolean) => void;
onDragSelectionStart?: (event: MouseEvent | TouchEvent) => void;
onDragSelectionEnd?: (event: MouseEvent | TouchEvent) => void;
@@ -1,6 +1,6 @@
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
import { Helmet } from 'react-helmet-async';
import { Helmet } from '@dr.pogodin/react-helmet';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { getImageAbsoluteURI } from 'twenty-shared/utils';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
@@ -1,4 +1,4 @@
import { Helmet } from 'react-helmet-async';
import { Helmet } from '@dr.pogodin/react-helmet';
type PageTitleProps = {
title: string;
@@ -9,7 +9,7 @@ import { isDefined } from 'twenty-shared/utils';
const CLICK_OUTSIDE_DEBUG_MODE = false;
export type ClickOutsideListenerProps<T extends Element> = {
refs: Array<RefObject<T>>;
refs: Array<RefObject<T | null>>;
excludedClickOutsideIds?: string[];
callback: (event: MouseEvent | TouchEvent) => void;
listenerId: string;
@@ -7,7 +7,7 @@ import { workflowDiagramRightClickMenuPositionState } from '@/workflow/workflow-
export const WorkflowDiagramRightClickCommandMenuClickOutsideEffect = ({
rightClickCommandMenuRef,
}: {
rightClickCommandMenuRef: React.RefObject<HTMLDivElement>;
rightClickCommandMenuRef: React.RefObject<HTMLDivElement | null>;
}) => {
const setWorkflowDiagramRightClickMenuPosition = useSetAtomComponentState(
workflowDiagramRightClickMenuPositionState,
@@ -23,7 +23,7 @@ import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWork
import { useGetPublicWorkspaceDataByDomain } from '@/domain-manager/hooks/useGetPublicWorkspaceDataByDomain';
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
import { useIsCurrentLocationOnDefaultDomain } from '@/domain-manager/hooks/useIsCurrentLocationOnDefaultDomain';
import { useMemo } from 'react';
import { type JSX, useMemo } from 'react';
import { SignInUpGlobalScopeFormEffect } from '@/auth/sign-in-up/components/internal/SignInUpGlobalScopeFormEffect';
import { SignInUpTwoFactorAuthenticationProvision } from '@/auth/sign-in-up/components/internal/SignInUpTwoFactorAuthenticationProvision';
@@ -12,6 +12,12 @@ import { graphqlMocks } from '~/testing/graphqlMocks';
const meta: Meta<typeof SettingsGraphQLPlayground> = {
title: 'Pages/Settings/Playground/GraphQLPlayground',
component: SettingsGraphQLPlayground,
// graphiql 5 renders a Monaco editor that cannot spin up web workers in the
// headless storybook test runner and throws unhandled async errors there
// (Cannot read properties of undefined 'toUrl'). Exclude this 3rd-party-tool
// wrapper from the vitest run; it is still available in the Storybook UI.
// (!test removes the auto-applied `test` tag the vitest addon filters on.)
tags: ['!test'],
decorators: [
(Story) => {
jotaiStore.set(playgroundApiKeyState.atom, {
@@ -1,3 +1,4 @@
import { type JSX } from 'react';
import { styled } from '@linaria/react';
const StyledLayout = styled.div<{ width?: number }>`
@@ -1,3 +1,4 @@
import { type JSX } from 'react';
import { styled } from '@linaria/react';
const StyledLayout = styled.div`
@@ -1,3 +1,4 @@
import { type JSX } from 'react';
import { type Decorator } from '@storybook/react-vite';
import {
createMemoryRouter,
@@ -1,5 +1,5 @@
import { type Decorator } from '@storybook/react-vite';
import { HelmetProvider } from 'react-helmet-async';
import { HelmetProvider } from '@dr.pogodin/react-helmet';
export const HelmetProviderDecorator: Decorator = (Story) => {
return (
@@ -2,7 +2,7 @@ import { ApolloProvider } from '@apollo/client/react';
import { loadDevMessages } from '@apollo/client/dev';
import { type Decorator } from '@storybook/react-vite';
import { Provider as JotaiProvider } from 'jotai';
import { HelmetProvider } from 'react-helmet-async';
import { HelmetProvider } from '@dr.pogodin/react-helmet';
import {
createMemoryRouter,
createRoutesFromElements,
@@ -18,7 +18,7 @@ import { MinimalMetadataGater } from '@/metadata-store/components/MinimalMetadat
import { UserMetadataProviderInitialEffect } from '@/metadata-store/effect-components/UserMetadataProviderInitialEffect';
import { MockedMetadataLoadEffect } from '~/testing/decorators/MockedMetadataLoadEffect';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { useState } from 'react';
import { type JSX, useState } from 'react';
import { ClientConfigProvider } from '~/modules/client-config/components/ClientConfigProvider';
import { mockedApolloClient } from '~/testing/mockedApolloClient';
@@ -1,3 +1,4 @@
import { type JSX } from 'react';
export type ComponentParameters<
ComponentTypeToExtract extends (args: object) => JSX.Element,
> = Parameters<ComponentTypeToExtract>[0];
+4 -4
View File
@@ -83,8 +83,8 @@
"inquirer": "^14.0.0",
"jsonc-parser": "^3.2.0",
"preact": "^10.28.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tinyglobby": "^0.2.15",
"twenty-client-sdk": "workspace:*",
"typescript": "^5.9.3",
@@ -93,8 +93,8 @@
"devDependencies": {
"@prettier/sync": "^0.5.2",
"@types/node": "^24.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@typescript/native-preview": "^7.0.0-dev.20260116.1",
"@vitest/coverage-v8": "^4.0.18",
"prettier": "^3.1.1",
+3 -3
View File
@@ -152,8 +152,8 @@
"pluralize": "8.0.0",
"postal-mime": "^2.6.1",
"psl": "^1.9.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"redis": "^4.7.0",
"reflect-metadata": "0.2.2",
"rxjs": "7.8.1",
@@ -210,7 +210,7 @@
"@types/passport-microsoft": "^2.1.0",
"@types/pluralize": "^0.0.33",
"@types/psl": "^1.1.3",
"@types/react": "^18.2.39",
"@types/react": "^19.2.0",
"@types/supertest": "^2.0.11",
"@types/unzipper": "^0",
"@types/uuid": "^9.0.2",
@@ -122,7 +122,7 @@ describe('Send Email Body Rendering', () => {
if (!element || typeof element !== 'object') return false;
if (element.type === 'br') return true;
const children = element.props?.children;
const children = (element.props as { children?: unknown })?.children;
if (!children) return false;
if (Array.isArray(children)) {
@@ -389,6 +389,7 @@ exports[`syncApplication should delete old field and create equivalent one when
"isNullable": true,
"isSystem": false,
"isUIEditable": true,
"isUIReadOnly": false,
"isUnique": false,
"kanbanAggregateOperationViewIds": [],
"kanbanAggregateOperationViewUniversalIdentifiers": [],
+5 -5
View File
@@ -26,8 +26,8 @@
"@testing-library/react": "^16.3.0",
"@types/babel__preset-env": "^7",
"@types/jest": "^30.0.0",
"@types/react": "^18.2.39",
"@types/react-dom": "^18.2.15",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@vitejs/plugin-react-swc": "^4.3.1",
"@wyw-in-js/babel-preset": "^1.1.0",
"@wyw-in-js/vite": "^1.1.0",
@@ -47,7 +47,7 @@
"vite-plugin-svgr": "^4.3.0"
},
"dependencies": {
"@linaria/react": "^6.2.1",
"@linaria/react": "^7.0.1",
"@monaco-editor/react": "^4.7.0",
"@radix-ui/colors": "^3.0.0",
"@sniptt/guards": "^0.2.0",
@@ -56,8 +56,8 @@
"framer-motion": "^11.18.0",
"glob": "^11.1.0",
"jotai": "^2.17.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-responsive": "^9.0.2",
"react-router-dom": "^6.4.4",
"react-tooltip": "^5.13.1",
@@ -1,5 +1,5 @@
import { useSetAtom } from 'jotai';
import { useEffect } from 'react';
import { type JSX, useEffect } from 'react';
import { iconsState } from '@ui/display/icon/states/iconsState';
@@ -22,5 +22,5 @@ export type ModalProps = React.PropsWithChildren & {
backdropClickOutsideId?: string;
preventClickOutside?: boolean;
onBackdropMouseDown?: (e: React.MouseEvent) => void;
modalRef?: React.RefObject<HTMLDivElement>;
modalRef?: React.RefObject<HTMLDivElement | null>;
};

Some files were not shown because too many files have changed in this diff Show More