Start Jotai Migration (#17893)

## Recoil → Jotai progressive migration: infrastructure +
ChipFieldDisplay

### Benchmark

In the beginning, there was no hope:
<img width="1180" height="948" alt="image"
src="https://github.com/user-attachments/assets/f8635991-52e6-4958-8240-6ba7214132b2"
/>

Then the hope was reborn
<img width="2070" height="948" alt="image"
src="https://github.com/user-attachments/assets/be1182b9-1c8d-4fdc-ab4c-1484ad74449d"
/>



### Approach

We introduce a **V2 state management layer** backed by Jotai that
mirrors the existing Recoil API, enabling component-by-component
migration without a big-bang rewrite.

#### V2 API (Jotai-backed, Recoil-ergonomic)

- `createStateV2` / `createFamilyStateV2` — drop-in replacements for
`createState` / `createFamilyState`, returning wrapper types over Jotai
atoms
- `useRecoilValueV2`, `useRecoilStateV2`, `useFamilyRecoilValueV2`, etc.
— thin wrappers around Jotai's `useAtomValue` / `useAtom` / `useSetAtom`
- A shared `jotaiStore` (via `createStore()`) passed to a
`<JotaiProvider>` wrapping `<RecoilRoot>`, also accessible imperatively
for dual-writes

#### Dual-write bridge for progressive migration

For state shared between migrated and non-migrated components, we use
**dual-write**: writers update both the Recoil atom and the Jotai V2
atom (via `jotaiStore.set()`). This avoids sync components or extra
subscriptions.

Write sites updated: `useUpsertRecordsInStore`, `useSetRecordTableData`,
`ListenRecordUpdatesEffect`, `RecordShowEffect`,
`useLoadRecordIndexStates`, `useUpdateObjectViewOptions`.

#### First migration: ChipFieldDisplay render path

- `useChipFieldDisplay` → reads `recordStoreFamilyStateV2` via
`useFamilyRecoilValueV2` (was `useRecoilValue(recordStoreFamilyState)`)
- `RecordChip` → reads `recordIndexOpenRecordInStateV2` via
`useRecoilValueV2` (was `useRecoilValue(recordIndexOpenRecordInState)`)
- `Avatar` (twenty-ui) and event handlers (`useOpenRecordInCommandMenu`)
left on Recoil — not on the render path / in a different package

#### Pattern for migrating additional state

1. Create V2 atom: `createStateV2` or `createFamilyStateV2`
2. Add `jotaiStore.set(v2Atom, value)` at each write site
3. Switch readers to `useRecoilValueV2(v2Atom)`
4. Once all readers are migrated, remove the Recoil atom and dual-writes

#### Why not jotai-recoil-adapter?

Evaluated
[jotai-recoil-adapter](https://github.com/clockelliptic/jotai-recoil-adapter)
— not production-ready (21 open issues, no React 19, forces providerless
mode, missing types). We built a purpose-built thin layer instead.
This commit is contained in:
Charles Bochet 2026-02-12 16:05:38 +01:00 committed by GitHub
parent 08b962b0d2
commit d2f8352cb8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
87 changed files with 1086 additions and 241 deletions

View file

@ -22,6 +22,7 @@
"googleapis": "105",
"hex-rgb": "^5.0.0",
"immer": "^10.1.1",
"jotai": "^2.17.1",
"libphonenumber-js": "^1.10.26",
"lodash.camelcase": "^4.3.0",
"lodash.chunk": "^4.2.0",

View file

@ -84,6 +84,7 @@
"graphql": "16.8.1",
"graphql-sse": "^2.5.4",
"input-otp": "^1.4.2",
"jotai": "^2.17.1",
"js-cookie": "^3.0.5",
"json-2-csv": "^5.4.0",
"json-logic-js": "^2.0.5",

View file

@ -0,0 +1,18 @@
import { type Locale } from 'date-fns';
import { enUS } from 'date-fns/locale';
import { type APP_LOCALES } from 'twenty-shared/translations';
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
type DateLocaleState = {
locale?: keyof typeof APP_LOCALES;
localeCatalog: Locale;
};
export const dateLocaleStateV2 = createStateV2<DateLocaleState>({
key: 'dateLocaleStateV2',
defaultValue: {
locale: undefined,
localeCatalog: enUS,
},
});

View file

@ -6,8 +6,10 @@ import { AppRootErrorFallback } from '@/error-handler/components/AppRootErrorFal
import { ExceptionHandlerProvider } from '@/error-handler/components/ExceptionHandlerProvider';
import { SnackBarComponentInstanceContext } from '@/ui/feedback/snack-bar-manager/contexts/SnackBarComponentInstanceContext';
import { ClickOutsideListenerContext } from '@/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { i18n } from '@lingui/core';
import { I18nProvider } from '@lingui/react';
import { Provider as JotaiProvider } from 'jotai';
import { HelmetProvider } from 'react-helmet-async';
import { RecoilRoot } from 'recoil';
import { IconsProvider } from 'twenty-ui/display';
@ -17,31 +19,33 @@ initialI18nActivate();
export const App = () => {
return (
<RecoilRoot>
<AppErrorBoundary
resetOnLocationChange={false}
FallbackComponent={AppRootErrorFallback}
>
<I18nProvider i18n={i18n}>
<RecoilDebugObserverEffect />
<ApolloDevLogEffect />
<SnackBarComponentInstanceContext.Provider
value={{ instanceId: 'snack-bar-manager' }}
>
<IconsProvider>
<ExceptionHandlerProvider>
<HelmetProvider>
<ClickOutsideListenerContext.Provider
value={{ excludedClickOutsideId: undefined }}
>
<AppRouter />
</ClickOutsideListenerContext.Provider>
</HelmetProvider>
</ExceptionHandlerProvider>
</IconsProvider>
</SnackBarComponentInstanceContext.Provider>
</I18nProvider>
</AppErrorBoundary>
</RecoilRoot>
<JotaiProvider store={jotaiStore}>
<RecoilRoot>
<AppErrorBoundary
resetOnLocationChange={false}
FallbackComponent={AppRootErrorFallback}
>
<I18nProvider i18n={i18n}>
<RecoilDebugObserverEffect />
<ApolloDevLogEffect />
<SnackBarComponentInstanceContext.Provider
value={{ instanceId: 'snack-bar-manager' }}
>
<IconsProvider>
<ExceptionHandlerProvider>
<HelmetProvider>
<ClickOutsideListenerContext.Provider
value={{ excludedClickOutsideId: undefined }}
>
<AppRouter />
</ClickOutsideListenerContext.Provider>
</HelmetProvider>
</ExceptionHandlerProvider>
</IconsProvider>
</SnackBarComponentInstanceContext.Provider>
</I18nProvider>
</AppErrorBoundary>
</RecoilRoot>
</JotaiProvider>
);
};

View file

@ -26,8 +26,10 @@ import { sentryConfigState } from '@/client-config/states/sentryConfigState';
import { supportChatState } from '@/client-config/states/supportChatState';
import { type ClientConfig } from '@/client-config/types/ClientConfig';
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { useCallback } from 'react';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { isAttachmentPreviewEnabledStateV2 } from '@/client-config/states/isAttachmentPreviewEnabledStateV2';
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
import { getClientConfig } from '@/client-config/utils/getClientConfig';
import { allowRequestsToTwentyIconsState } from '@/client-config/states/allowRequestsToTwentyIcons';
@ -188,6 +190,10 @@ export const useClientConfig = (): UseClientConfigResult => {
setGoogleMessagingEnabled(clientConfig?.isGoogleMessagingEnabled);
setGoogleCalendarEnabled(clientConfig?.isGoogleCalendarEnabled);
setIsAttachmentPreviewEnabled(clientConfig?.isAttachmentPreviewEnabled);
jotaiStore.set(
isAttachmentPreviewEnabledStateV2.atom,
clientConfig?.isAttachmentPreviewEnabled,
);
setIsConfigVariablesInDbEnabled(
clientConfig?.isConfigVariablesInDbEnabled,
);

View file

@ -0,0 +1,6 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
export const isAttachmentPreviewEnabledStateV2 = createStateV2<boolean>({
key: 'isAttachmentPreviewEnabledStateV2',
defaultValue: false,
});

View file

@ -2,13 +2,13 @@ import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordIn
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { getLinkToShowPage } from '@/object-metadata/utils/getLinkToShowPage';
import { useRecordChipData } from '@/object-record/hooks/useRecordChipData';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
import { recordIndexOpenRecordInStateV2 } from '@/object-record/record-index/states/recordIndexOpenRecordInStateV2';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { canOpenObjectInSidePanel } from '@/object-record/utils/canOpenObjectInSidePanel';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import { t } from '@lingui/core/macro';
import { type MouseEvent } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import {
AvatarChip,
@ -55,7 +55,9 @@ export const RecordChip = ({
const { openRecordInCommandMenu } = useOpenRecordInCommandMenu();
const recordIndexOpenRecordIn = useRecoilValue(recordIndexOpenRecordInState);
const recordIndexOpenRecordIn = useRecoilValueV2(
recordIndexOpenRecordInStateV2,
);
const canOpenInSidePanel = canOpenObjectInSidePanel(objectNameSingular);
const isSidePanelViewOpenRecordInType =

View file

@ -1,5 +1,7 @@
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
import { recordIndexOpenRecordInStateV2 } from '@/object-record/record-index/states/recordIndexOpenRecordInStateV2';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { useUpdateCurrentView } from '@/views/hooks/useUpdateCurrentView';
import { type GraphQLView } from '@/views/types/GraphQLView';
import { type ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
@ -27,6 +29,7 @@ export const useUpdateObjectViewOptions = () => {
(openRecordIn: ViewOpenRecordInType, view: GraphQLView | undefined) => {
if (!view) return;
setRecordIndexOpenRecordIn(openRecordIn);
jotaiStore.set(recordIndexOpenRecordInStateV2.atom, openRecordIn);
updateCurrentView({
openRecordIn,
});

View file

@ -1,20 +1,18 @@
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { useFilesFieldDisplay } from '@/object-record/record-field/ui/meta-types/hooks/useFilesFieldDisplay';
import { filesFieldUploadState } from '@/object-record/record-field/ui/states/filesFieldUploadState';
import { filesFieldUploadStateV2 } from '@/object-record/record-field/ui/states/filesFieldUploadStateV2';
import { useFamilyRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useFamilyRecoilValueV2';
import { FilesDisplay } from '@/ui/field/display/components/FilesDisplay';
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
export const FilesFieldDisplay = () => {
const { recordId, fieldDefinition } = useContext(FieldContext);
const { fieldValue, disableChipClick } = useFilesFieldDisplay();
const uploadState = useRecoilValue(
filesFieldUploadState({
recordId,
fieldName: fieldDefinition.metadata.fieldName,
}),
);
const uploadState = useFamilyRecoilValueV2(filesFieldUploadStateV2, {
recordId,
fieldName: fieldDefinition.metadata.fieldName,
});
const isUploadWindowOpen = uploadState === 'UPLOAD_WINDOW_OPEN';
const isFileUploading = uploadState === 'UPLOADING_FILE';

View file

@ -3,7 +3,7 @@ import { useContext } from 'react';
import { type FieldActorValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { AuthContext } from '@/auth/contexts/AuthContext';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { useRecordFieldValueV2 } from '@/object-record/record-store/hooks/useRecordFieldValueV2';
import { isDefined } from 'twenty-shared/utils';
import { type WorkspaceMember } from '~/generated-metadata/graphql';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
@ -21,7 +21,7 @@ export const useActorFieldDisplay = (): ActorFieldDisplayValue | undefined => {
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<FieldActorValue | undefined>(
const fieldValue = useRecordFieldValueV2<FieldActorValue | undefined>(
recordId,
fieldName,
fieldDefinition,

View file

@ -1,6 +1,6 @@
import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { useRecordFieldValueV2 } from '@/object-record/record-store/hooks/useRecordFieldValueV2';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { type FieldAddressValue } from '@/object-record/record-field/ui/types/FieldMetadata';
@ -9,7 +9,7 @@ export const useAddressFieldDisplay = () => {
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<FieldAddressValue | undefined>(
const fieldValue = useRecordFieldValueV2<FieldAddressValue | undefined>(
recordId,
fieldName,
fieldDefinition,

View file

@ -4,7 +4,7 @@ import {
type FieldArrayMetadata,
type FieldArrayValue,
} from '@/object-record/record-field/ui/types/FieldMetadata';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { useRecordFieldValueV2 } from '@/object-record/record-store/hooks/useRecordFieldValueV2';
import { useContext } from 'react';
@ -13,7 +13,7 @@ export const useArrayFieldDisplay = () => {
const { fieldName } = fieldDefinition.metadata;
const fieldValue = useRecordFieldValue<FieldArrayValue | undefined>(
const fieldValue = useRecordFieldValueV2<FieldArrayValue | undefined>(
recordId,
fieldName,
fieldDefinition,

View file

@ -1,6 +1,6 @@
import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { useRecordFieldValueV2 } from '@/object-record/record-store/hooks/useRecordFieldValueV2';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
export const useBooleanFieldDisplay = () => {
@ -8,7 +8,7 @@ export const useBooleanFieldDisplay = () => {
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<boolean | undefined>(
const fieldValue = useRecordFieldValueV2<boolean | undefined>(
recordId,
fieldName,
fieldDefinition,

View file

@ -5,11 +5,11 @@ import { isFieldText } from '@/object-record/record-field/ui/types/guards/isFiel
import { isFieldUuid } from '@/object-record/record-field/ui/types/guards/isFieldUuid';
import { isNonEmptyString } from '@sniptt/guards';
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { isFieldActor } from '@/object-record/record-field/ui/types/guards/isFieldActor';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { recordStoreFamilyStateV2 } from '@/object-record/record-store/states/recordStoreFamilyStateV2';
import { useFamilyRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useFamilyRecoilValueV2';
import { isDefined } from 'twenty-shared/utils';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
@ -50,7 +50,10 @@ export const useChipFieldDisplay = () => {
? fieldDefinition.metadata.objectMetadataNameSingular
: undefined;
const recordValue = useRecoilValue(recordStoreFamilyState(recordId));
const recordValue = useFamilyRecoilValueV2(
recordStoreFamilyStateV2,
recordId,
);
if (!isNonEmptyString(objectNameSingular)) {
throw new Error('Object metadata name singular is not a non-empty string');

View file

@ -2,7 +2,7 @@ import { useContext } from 'react';
import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata';
import { isFieldCurrency } from '@/object-record/record-field/ui/types/guards/isFieldCurrency';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { useRecordFieldValueV2 } from '@/object-record/record-store/hooks/useRecordFieldValueV2';
import { FieldMetadataType } from 'twenty-shared/types';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { type FieldCurrencyValue } from '@/object-record/record-field/ui/types/FieldMetadata';
@ -18,7 +18,7 @@ export const useCurrencyFieldDisplay = () => {
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<FieldCurrencyValue | undefined>(
const fieldValue = useRecordFieldValueV2<FieldCurrencyValue | undefined>(
recordId,
fieldName,
fieldDefinition,

View file

@ -2,7 +2,7 @@ import { useContext } from 'react';
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
import { type FieldDateMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { useRecordFieldValueV2 } from '@/object-record/record-store/hooks/useRecordFieldValueV2';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
export const useDateFieldDisplay = () => {
@ -10,7 +10,7 @@ export const useDateFieldDisplay = () => {
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<string | undefined>(
const fieldValue = useRecordFieldValueV2<string | undefined>(
recordId,
fieldName,
fieldDefinition,

View file

@ -2,7 +2,7 @@ import { useContext } from 'react';
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
import { type FieldDateTimeMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { useRecordFieldValueV2 } from '@/object-record/record-store/hooks/useRecordFieldValueV2';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
export const useDateTimeFieldDisplay = () => {
@ -10,7 +10,7 @@ export const useDateTimeFieldDisplay = () => {
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<string | undefined>(
const fieldValue = useRecordFieldValueV2<string | undefined>(
recordId,
fieldName,
fieldDefinition,

View file

@ -3,7 +3,7 @@ import { useContext } from 'react';
import { type FieldEmailsValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata';
import { isFieldEmails } from '@/object-record/record-field/ui/types/guards/isFieldEmails';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { useRecordFieldValueV2 } from '@/object-record/record-store/hooks/useRecordFieldValueV2';
import { FieldMetadataType } from 'twenty-shared/types';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
@ -14,7 +14,7 @@ export const useEmailsFieldDisplay = () => {
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<FieldEmailsValue | undefined>(
const fieldValue = useRecordFieldValueV2<FieldEmailsValue | undefined>(
recordId,
fieldName,
fieldDefinition,

View file

@ -4,7 +4,7 @@ import {
type FieldFilesMetadata,
type FieldFilesValue,
} from '@/object-record/record-field/ui/types/FieldMetadata';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { useRecordFieldValueV2 } from '@/object-record/record-store/hooks/useRecordFieldValueV2';
import { useContext } from 'react';
@ -14,7 +14,7 @@ export const useFilesFieldDisplay = () => {
const { fieldName } = fieldDefinition.metadata;
const fieldValue = useRecordFieldValue<FieldFilesValue[] | undefined>(
const fieldValue = useRecordFieldValueV2<FieldFilesValue[] | undefined>(
recordId,
fieldName,
fieldDefinition,

View file

@ -2,7 +2,7 @@ import { useContext } from 'react';
import { type FieldFullNameValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { useRecordFieldValueV2 } from '@/object-record/record-store/hooks/useRecordFieldValueV2';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
export const useFullNameFieldDisplay = () => {
@ -10,7 +10,7 @@ export const useFullNameFieldDisplay = () => {
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<FieldFullNameValue | undefined>(
const fieldValue = useRecordFieldValueV2<FieldFullNameValue | undefined>(
recordId,
fieldName,
fieldDefinition,

View file

@ -3,7 +3,7 @@ import { useContext } from 'react';
import { type FieldJsonValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { useFormattedJsonFieldValue } from '@/object-record/record-field/ui/meta-types/hooks/useFormattedJsonFieldValue';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { useRecordFieldValueV2 } from '@/object-record/record-store/hooks/useRecordFieldValueV2';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
export const useJsonFieldDisplay = () => {
@ -12,7 +12,7 @@ export const useJsonFieldDisplay = () => {
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<FieldJsonValue | undefined>(
const fieldValue = useRecordFieldValueV2<FieldJsonValue | undefined>(
recordId,
fieldName,
fieldDefinition,

View file

@ -4,7 +4,7 @@ import { type FieldLinksValue } from '@/object-record/record-field/ui/types/Fiel
import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata';
import { isFieldLinks } from '@/object-record/record-field/ui/types/guards/isFieldLinks';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { useRecordFieldValueV2 } from '@/object-record/record-store/hooks/useRecordFieldValueV2';
import { FieldMetadataType } from 'twenty-shared/types';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
@ -15,7 +15,7 @@ export const useLinksFieldDisplay = () => {
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<FieldLinksValue | undefined>(
const fieldValue = useRecordFieldValueV2<FieldLinksValue | undefined>(
recordId,
fieldName,
fieldDefinition,

View file

@ -7,7 +7,7 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { useRecordFieldValueV2 } from '@/object-record/record-store/hooks/useRecordFieldValueV2';
import { type ObjectRecord } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
@ -32,7 +32,7 @@ export const useMorphRelationFromManyFieldDisplay = () => {
const button = fieldDefinition.editButtonIcon;
const morphValuesWithObjectNameSingular = useRecordFieldValue<
const morphValuesWithObjectNameSingular = useRecordFieldValueV2<
{
objectNameSingular: string;
value: ObjectRecord;

View file

@ -11,7 +11,7 @@ import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldCont
import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata';
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { useRecordFieldValueV2 } from '@/object-record/record-store/hooks/useRecordFieldValueV2';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from 'twenty-shared/utils';
@ -34,7 +34,7 @@ export const useMorphRelationToOneFieldDisplay = () => {
const button = fieldDefinition.editButtonIcon;
const morphFieldValueWithObjectName = useRecordFieldValue<{
const morphFieldValueWithObjectName = useRecordFieldValueV2<{
objectNameSingular: string;
value: ObjectRecord;
}>(recordId, fieldDefinition.metadata.fieldName, fieldDefinition);

View file

@ -6,14 +6,14 @@ import {
type FieldMultiSelectMetadata,
type FieldMultiSelectValue,
} from '@/object-record/record-field/ui/types/FieldMetadata';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { useRecordFieldValueV2 } from '@/object-record/record-store/hooks/useRecordFieldValueV2';
export const useMultiSelectFieldDisplay = () => {
const { recordId, fieldDefinition } = useContext(FieldContext);
const { fieldName } = fieldDefinition.metadata;
const fieldValue = useRecordFieldValue<FieldMultiSelectValue | undefined>(
const fieldValue = useRecordFieldValueV2<FieldMultiSelectValue | undefined>(
recordId,
fieldName,
fieldDefinition,

View file

@ -2,7 +2,7 @@ import { useContext } from 'react';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { useRecordFieldValueV2 } from '@/object-record/record-store/hooks/useRecordFieldValueV2';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata';
import { isFieldNumber } from '@/object-record/record-field/ui/types/guards/isFieldNumber';
@ -13,7 +13,7 @@ export const useNumberFieldDisplay = () => {
assertFieldMetadata(FieldMetadataType.NUMBER, isFieldNumber, fieldDefinition);
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<number | null>(
const fieldValue = useRecordFieldValueV2<number | null>(
recordId,
fieldName,
fieldDefinition,

View file

@ -4,7 +4,7 @@ import { type FieldPhonesValue } from '@/object-record/record-field/ui/types/Fie
import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata';
import { isFieldPhones } from '@/object-record/record-field/ui/types/guards/isFieldPhones';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { useRecordFieldValueV2 } from '@/object-record/record-store/hooks/useRecordFieldValueV2';
import { FieldMetadataType } from 'twenty-shared/types';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
@ -15,7 +15,7 @@ export const usePhonesFieldDisplay = () => {
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<FieldPhonesValue | undefined>(
const fieldValue = useRecordFieldValueV2<FieldPhonesValue | undefined>(
recordId,
fieldName,
fieldDefinition,

View file

@ -1,6 +1,6 @@
import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { useRecordFieldValueV2 } from '@/object-record/record-store/hooks/useRecordFieldValueV2';
import { type FieldRatingValue } from 'twenty-shared/types';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
@ -9,7 +9,7 @@ export const useRatingFieldDisplay = () => {
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<FieldRatingValue>(
const fieldValue = useRecordFieldValueV2<FieldRatingValue>(
recordId,
fieldName,
fieldDefinition,

View file

@ -8,7 +8,7 @@ import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { FIELD_EDIT_BUTTON_WIDTH } from '@/ui/field/display/constants/FieldEditButtonWidth';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { useRecordFieldValueV2 } from '@/object-record/record-store/hooks/useRecordFieldValueV2';
import { isDefined } from 'twenty-shared/utils';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata';
@ -35,7 +35,7 @@ export const useRelationFromManyFieldDisplay = () => {
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<ObjectRecord[] | undefined>(
const fieldValue = useRecordFieldValueV2<ObjectRecord[] | undefined>(
recordId,
fieldName,
fieldDefinition,

View file

@ -12,7 +12,7 @@ import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldCont
import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata';
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
import { getJoinColumnNameOrThrow } from '@/object-record/record-field/ui/utils/junction/getJoinColumnNameOrThrow';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { useRecordFieldValueV2 } from '@/object-record/record-store/hooks/useRecordFieldValueV2';
import { isDefined } from 'twenty-shared/utils';
export const useRelationToOneFieldDisplay = () => {
@ -36,7 +36,7 @@ export const useRelationToOneFieldDisplay = () => {
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<ObjectRecord | undefined>(
const fieldValue = useRecordFieldValueV2<ObjectRecord | undefined>(
recordId,
fieldName,
fieldDefinition,
@ -46,7 +46,7 @@ export const useRelationToOneFieldDisplay = () => {
fieldDefinition.metadata.settings,
);
const foreignKeyFieldValue = useRecordFieldValue<string | null | undefined>(
const foreignKeyFieldValue = useRecordFieldValueV2<string | null | undefined>(
recordId,
joinColumnName,
{ type: FieldMetadataType.UUID, metadata: { fieldName: joinColumnName } },

View file

@ -3,7 +3,7 @@ import { useContext } from 'react';
import { type FieldRichTextValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata';
import { isFieldRichText } from '@/object-record/record-field/ui/types/guards/isFieldRichText';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { useRecordFieldValueV2 } from '@/object-record/record-store/hooks/useRecordFieldValueV2';
import type { PartialBlock } from '@blocknote/core';
import { isDefined, parseJson } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@ -20,7 +20,7 @@ export const useRichTextFieldDisplay = () => {
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<FieldRichTextValue | undefined>(
const fieldValue = useRecordFieldValueV2<FieldRichTextValue | undefined>(
recordId,
fieldName,
fieldDefinition,

View file

@ -3,7 +3,7 @@ import { useContext } from 'react';
import { type FieldRichTextV2Value } from '@/object-record/record-field/ui/types/FieldMetadata';
import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata';
import { isFieldRichTextV2 } from '@/object-record/record-field/ui/types/guards/isFieldRichTextV2';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { useRecordFieldValueV2 } from '@/object-record/record-store/hooks/useRecordFieldValueV2';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
@ -18,7 +18,7 @@ export const useRichTextV2FieldDisplay = () => {
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<FieldRichTextV2Value | undefined>(
const fieldValue = useRecordFieldValueV2<FieldRichTextV2Value | undefined>(
recordId,
fieldName,
fieldDefinition,

View file

@ -2,7 +2,7 @@ import { useContext } from 'react';
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { useRecordFieldValueV2 } from '@/object-record/record-store/hooks/useRecordFieldValueV2';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import {
type FieldSelectMetadata,
@ -14,7 +14,7 @@ export const useSelectFieldDisplay = () => {
const { fieldName } = fieldDefinition.metadata;
const fieldValue = useRecordFieldValue<FieldSelectValue | undefined>(
const fieldValue = useRecordFieldValueV2<FieldSelectValue | undefined>(
recordId,
fieldName,
fieldDefinition,

View file

@ -1,6 +1,6 @@
import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { useRecordFieldValueV2 } from '@/object-record/record-store/hooks/useRecordFieldValueV2';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
export const useTextFieldDisplay = () => {
@ -10,7 +10,7 @@ export const useTextFieldDisplay = () => {
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue =
useRecordFieldValue<string | undefined>(
useRecordFieldValueV2<string | undefined>(
recordId,
fieldName,
fieldDefinition,

View file

@ -1,12 +1,12 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { type FieldUUidValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata';
import { isFieldTextValue } from '@/object-record/record-field/ui/types/guards/isFieldTextValue';
import { isFieldUuid } from '@/object-record/record-field/ui/types/guards/isFieldUuid';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { recordStoreFamilySelectorV2 } from '@/object-record/record-store/states/selectors/recordStoreFamilySelectorV2';
import { useFamilySelectorStateV2 } from '@/ui/utilities/state/jotai/hooks/useFamilySelectorStateV2';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const useUuidField = () => {
@ -16,13 +16,14 @@ export const useUuidField = () => {
const fieldName = fieldDefinition.metadata.fieldName;
const [fieldValue, setFieldValue] = useRecoilState<FieldUUidValue>(
recordStoreFamilySelector({
recordId,
fieldName: fieldName,
}),
const [fieldValue, setFieldValue] = useFamilySelectorStateV2(
recordStoreFamilySelectorV2,
{ recordId, fieldName },
);
const fieldTextValue = isFieldTextValue(fieldValue) ? fieldValue : '';
const fieldTextValue = isFieldTextValue(fieldValue as FieldUUidValue)
? (fieldValue as FieldUUidValue)
: '';
return {
fieldDefinition,

View file

@ -1,4 +1,4 @@
import { isAttachmentPreviewEnabledState } from '@/client-config/states/isAttachmentPreviewEnabledState';
import { isAttachmentPreviewEnabledStateV2 } from '@/client-config/states/isAttachmentPreviewEnabledStateV2';
import { useFileUpload } from '@/file-upload/hooks/useFileUpload';
import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts/FieldInputEventContext';
import { useFilesField } from '@/object-record/record-field/ui/meta-types/hooks/useFilesField';
@ -11,11 +11,12 @@ import { recordFieldInputIsFieldInErrorComponentState } from '@/object-record/re
import { type FieldFilesValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { filesSchema } from '@/object-record/record-field/ui/types/guards/isFieldFilesValue';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { filePreviewState } from '@/ui/field/display/states/filePreviewState';
import { filePreviewStateV2 } from '@/ui/field/display/states/filePreviewStateV2';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { useLingui } from '@lingui/react/macro';
import { useCallback, useContext, useMemo, useState } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { MULTI_ITEM_FIELD_DEFAULT_MAX_VALUES } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@ -27,9 +28,9 @@ export const FilesFieldInput = () => {
const { t } = useLingui();
const [isUploading, setIsUploading] = useState(false);
const { enqueueErrorSnackBar } = useSnackBar();
const setFilePreview = useSetRecoilState(filePreviewState);
const isAttachmentPreviewEnabled = useRecoilValue(
isAttachmentPreviewEnabledState,
const setFilePreview = useSetRecoilStateV2(filePreviewStateV2);
const isAttachmentPreviewEnabled = useRecoilValueV2(
isAttachmentPreviewEnabledStateV2,
);
const { onEscape, onClickOutside, onEnter } = useContext(

View file

@ -2,8 +2,10 @@ import { useFileUpload } from '@/file-upload/hooks/useFileUpload';
import { useUploadFilesFieldFile } from '@/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile';
import { uploadMultipleFiles } from '@/object-record/record-field/ui/meta-types/utils/uploadMultipleFiles';
import { filesFieldUploadState } from '@/object-record/record-field/ui/states/filesFieldUploadState';
import { filesFieldUploadStateV2 } from '@/object-record/record-field/ui/states/filesFieldUploadStateV2';
import { type FieldFilesValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { recordTableCellEditModePositionComponentState } from '@/object-record/record-table/states/recordTableCellEditModePositionComponentState';
@ -99,6 +101,10 @@ export const useOpenFilesFieldInput = () => {
filesFieldUploadState({ recordId, fieldName }),
'UPLOAD_WINDOW_OPEN',
);
jotaiStore.set(
filesFieldUploadStateV2.atomFamily({ recordId, fieldName }),
'UPLOAD_WINDOW_OPEN',
);
openFileUpload({
multiple: true,
@ -109,6 +115,10 @@ export const useOpenFilesFieldInput = () => {
});
set(filesFieldUploadState({ recordId, fieldName }), null);
jotaiStore.set(
filesFieldUploadStateV2.atomFamily({ recordId, fieldName }),
null,
);
if (isTableContext && isDefined(recordTableId)) {
set(
@ -131,6 +141,10 @@ export const useOpenFilesFieldInput = () => {
filesFieldUploadState({ recordId, fieldName }),
'UPLOADING_FILE',
);
jotaiStore.set(
filesFieldUploadStateV2.atomFamily({ recordId, fieldName }),
'UPLOADING_FILE',
);
try {
const uploadedFiles = await uploadMultipleFiles(
@ -146,6 +160,10 @@ export const useOpenFilesFieldInput = () => {
}
} finally {
set(filesFieldUploadState({ recordId, fieldName }), null);
jotaiStore.set(
filesFieldUploadStateV2.atomFamily({ recordId, fieldName }),
null,
);
if (isTableContext && isDefined(recordTableId)) {
set(
@ -165,6 +183,10 @@ export const useOpenFilesFieldInput = () => {
},
onCancel: () => {
set(filesFieldUploadState({ recordId, fieldName }), null);
jotaiStore.set(
filesFieldUploadStateV2.atomFamily({ recordId, fieldName }),
null,
);
if (isTableContext && isDefined(recordTableId)) {
set(

View file

@ -0,0 +1,16 @@
import { createFamilyStateV2 } from '@/ui/utilities/state/jotai/utils/createFamilyStateV2';
type FilesFieldUploadStateKey = {
recordId: string;
fieldName: string;
};
type FilesFieldUploadState = 'UPLOAD_WINDOW_OPEN' | 'UPLOADING_FILE' | null;
export const filesFieldUploadStateV2 = createFamilyStateV2<
FilesFieldUploadState,
FilesFieldUploadStateKey
>({
key: 'filesFieldUploadStateV2',
defaultValue: null,
});

View file

@ -13,6 +13,8 @@ import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/s
import { recordIndexGroupAggregateFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupAggregateFieldMetadataItemComponentState';
import { recordIndexGroupAggregateOperationComponentState } from '@/object-record/record-index/states/recordIndexGroupAggregateOperationComponentState';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
import { recordIndexOpenRecordInStateV2 } from '@/object-record/record-index/states/recordIndexOpenRecordInStateV2';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { recordIndexShouldHideEmptyRecordGroupsComponentState } from '@/object-record/record-index/states/recordIndexShouldHideEmptyRecordGroupsComponentState';
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState';
@ -200,6 +202,7 @@ export const useLoadRecordIndexStates = () => {
setRecordIndexViewType(view.type);
setRecordIndexOpenRecordIn(view.openRecordIn);
jotaiStore.set(recordIndexOpenRecordInStateV2.atom, view.openRecordIn);
setRecordIndexCalendarFieldMetadataIdState(
view.calendarFieldMetadataId ?? null,

View file

@ -0,0 +1,8 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
export const recordIndexOpenRecordInStateV2 =
createStateV2<ViewOpenRecordInType>({
key: 'recordIndexOpenRecordInStateV2',
defaultValue: ViewOpenRecordInType.SIDE_PANEL,
});

View file

@ -3,7 +3,9 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { buildFindOneRecordForShowPageOperationSignature } from '@/object-record/record-show/graphql/operations/factories/findOneRecordForShowPageOperationSignatureFactory';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { recordStoreFamilyStateV2 } from '@/object-record/record-store/states/recordStoreFamilyStateV2';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { useEffect } from 'react';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
@ -42,6 +44,10 @@ export const RecordShowEffect = ({
if (JSON.stringify(previousRecordValue) !== JSON.stringify(newRecord)) {
set(recordStoreFamilyState(recordId), newRecord);
jotaiStore.set(
recordStoreFamilyStateV2.atomFamily(recordId),
newRecord,
);
}
},
[recordId],

View file

@ -0,0 +1,22 @@
import { useAtomValue } from 'jotai';
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { recordStoreFieldValueSelectorV2 } from '@/object-record/record-store/states/selectors/recordStoreFieldValueSelectorV2';
export const useRecordFieldValueV2 = <T extends unknown>(
recordId: string,
fieldName: string,
fieldDefinition: Pick<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>,
) => {
const fieldValueAtom = recordStoreFieldValueSelectorV2({
recordId,
fieldName,
fieldDefinition: {
type: fieldDefinition.type,
metadata: fieldDefinition.metadata,
},
});
return useAtomValue(fieldValueAtom) as T | undefined;
};

View file

@ -3,7 +3,9 @@ import { useRecoilCallback } from 'recoil';
import { filterRecordOnGqlFields } from '@/object-record/cache/utils/filterRecordOnGqlFields';
import { type RecordGqlFields } from '@/object-record/graphql/record-gql-fields/types/RecordGqlFields';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { recordStoreFamilyStateV2 } from '@/object-record/record-store/states/recordStoreFamilyStateV2';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { isDefined } from 'twenty-shared/utils';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
@ -29,11 +31,16 @@ export const useUpsertRecordsInStore = () => {
: partialRecord;
if (!isDefined(currentRecord)) {
set(recordStoreFamilyState(partialRecord.id), {
const newRecord = {
id: partialRecord.id,
__typename: partialRecord.__typename,
...filteredPartialRecord,
});
};
set(recordStoreFamilyState(partialRecord.id), newRecord);
jotaiStore.set(
recordStoreFamilyStateV2.atomFamily(partialRecord.id),
newRecord,
);
continue;
}
@ -45,10 +52,15 @@ export const useUpsertRecordsInStore = () => {
: currentRecord;
if (!isDeeplyEqual(filteredCurrentRecord, filteredPartialRecord)) {
set(recordStoreFamilyState(partialRecord.id), {
const updatedRecord = {
...currentRecord,
...filteredPartialRecord,
});
};
set(recordStoreFamilyState(partialRecord.id), updatedRecord);
jotaiStore.set(
recordStoreFamilyStateV2.atomFamily(partialRecord.id),
updatedRecord,
);
}
}
},

View file

@ -0,0 +1,10 @@
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { createFamilyStateV2 } from '@/ui/utilities/state/jotai/utils/createFamilyStateV2';
export const recordStoreFamilyStateV2 = createFamilyStateV2<
ObjectRecord | null | undefined,
string
>({
key: 'recordStoreFamilyStateV2',
defaultValue: null,
});

View file

@ -0,0 +1,23 @@
import { recordStoreFamilyStateV2 } from '@/object-record/record-store/states/recordStoreFamilyStateV2';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { createWritableFamilySelectorV2 } from '@/ui/utilities/state/jotai/utils/createWritableFamilySelectorV2';
export const recordStoreFamilySelectorV2 = createWritableFamilySelectorV2<
unknown,
{ recordId: string; fieldName: string }
>({
key: 'recordStoreFamilySelectorV2',
get:
({ recordId, fieldName }) =>
({ get }) =>
get(recordStoreFamilyStateV2, recordId)?.[fieldName],
set:
({ recordId, fieldName }) =>
({ set }, newValue) => {
set(recordStoreFamilyStateV2, recordId, (prev) =>
prev
? { ...prev, [fieldName]: newValue }
: ({ [fieldName]: newValue } as ObjectRecord),
);
},
});

View file

@ -0,0 +1,117 @@
import { atom, type Atom } from 'jotai';
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
import { recordStoreFamilyStateV2 } from '@/object-record/record-store/states/recordStoreFamilyStateV2';
import { createFamilySelectorV2 } from '@/ui/utilities/state/jotai/utils/createFamilySelectorV2';
import { RelationType, type ObjectRecord } from 'twenty-shared/types';
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
const simpleFieldValueSelector = createFamilySelectorV2<
unknown,
{ recordId: string; fieldName: string }
>({
key: 'recordStoreSimpleFieldValue',
get:
({ recordId, fieldName }) =>
({ get }) =>
get(recordStoreFamilyStateV2, recordId)?.[fieldName],
});
const morphAtomCache = new Map<string, Atom<unknown>>();
const getMorphRelationFieldValueAtom = (
recordId: string,
fieldName: string,
fieldDefinition: Pick<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>,
): Atom<unknown> => {
const cacheKey = `morph__${recordId}__${fieldName}`;
const existing = morphAtomCache.get(cacheKey);
if (existing !== undefined) {
return existing;
}
const morphRelations =
(
fieldDefinition.metadata as {
morphRelations?: Array<{
type: string;
sourceFieldMetadata: { name: string };
targetObjectMetadata: {
nameSingular: string;
namePlural: string;
};
}>;
}
).morphRelations ?? [];
const derivedAtom = atom((get) => {
const record = get(recordStoreFamilyStateV2.atomFamily(recordId));
const morphValuesWithObjectName = morphRelations.map((morphRelation) => {
const computedFieldName = computeMorphRelationFieldName({
fieldName: morphRelation.sourceFieldMetadata.name,
relationType: morphRelation.type as RelationType,
targetObjectMetadataNameSingular:
morphRelation.targetObjectMetadata.nameSingular,
targetObjectMetadataNamePlural:
morphRelation.targetObjectMetadata.namePlural,
});
return {
objectNameSingular: morphRelation.targetObjectMetadata.nameSingular,
value: record?.[computedFieldName],
};
});
const relationType = morphRelations[0]?.type;
if (relationType === RelationType.ONE_TO_MANY) {
return morphValuesWithObjectName.map((morphValue) => ({
...morphValue,
value: morphValue.value ? morphValue.value : [],
})) as {
objectNameSingular: string;
value: ObjectRecord[];
}[];
}
if (relationType === RelationType.MANY_TO_ONE) {
const morphValueFiltered = morphValuesWithObjectName.filter(
(morphValue) => isDefined(morphValue.value),
);
return morphValueFiltered.length > 0
? (morphValueFiltered[0] as {
objectNameSingular: string;
value: ObjectRecord;
})
: null;
}
return null;
});
derivedAtom.debugLabel = `recordMorphFieldValue__${cacheKey}`;
morphAtomCache.set(cacheKey, derivedAtom);
return derivedAtom;
};
export const recordStoreFieldValueSelectorV2 = ({
recordId,
fieldName,
fieldDefinition,
}: {
recordId: string;
fieldName: string;
fieldDefinition: Pick<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>;
}): Atom<unknown> => {
if (isFieldMorphRelation(fieldDefinition)) {
return getMorphRelationFieldValueAtom(recordId, fieldName, fieldDefinition);
}
return simpleFieldValueSelector.selectorFamily({ recordId, fieldName });
};

View file

@ -4,6 +4,7 @@ import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { recordStoreFamilyStateV2 } from '@/object-record/record-store/states/recordStoreFamilyStateV2';
import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow';
import { useUnfocusRecordTableCell } from '@/object-record/record-table/record-table-cell/hooks/useUnfocusRecordTableCell';
import { hasUserSelectedAllRowsComponentState } from '@/object-record/record-table/record-table-row/states/hasUserSelectedAllRowsFamilyState';
@ -16,6 +17,7 @@ import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilComponentFamilyCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyCallbackState';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { isDefined } from 'twenty-shared/utils';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
@ -90,6 +92,10 @@ export const useSetRecordTableData = ({
};
set(recordStoreFamilyState(record.id), newRecord);
jotaiStore.set(
recordStoreFamilyStateV2.atomFamily(record.id),
newRecord,
);
}
}

View file

@ -13,6 +13,7 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat
import { shouldAppBeLoadingState } from '@/object-metadata/states/shouldAppBeLoadingState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { recordStoreFamilyStateV2 } from '@/object-record/record-store/states/recordStoreFamilyStateV2';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { PageLayoutContentProvider } from '@/page-layout/contexts/PageLayoutContentContext';
import {
@ -25,6 +26,7 @@ import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { FieldWidget } from '@/page-layout/widgets/field/components/FieldWidget';
import { WidgetComponentInstanceContext } from '@/page-layout/widgets/states/contexts/WidgetComponentInstanceContext';
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { ComponentDecorator } from 'twenty-ui/testing';
import {
PageLayoutTabLayoutMode,
@ -256,6 +258,16 @@ const mockCompanyRecord: ObjectRecord = {
},
};
// Sets a record in both Recoil and Jotai stores so field display hooks can read it
const setRecordInStores = (
snapshot: MutableSnapshot,
recordId: string,
record: ObjectRecord,
) => {
snapshot.set(recordStoreFamilyState(recordId), record);
jotaiStore.set(recordStoreFamilyStateV2.atomFamily(recordId), record);
};
const JestMetadataAndApolloMocksWrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: [],
});
@ -369,7 +381,7 @@ export const TextFieldWidget: Story = {
}),
pageLayoutData,
);
snapshot.set(recordStoreFamilyState(TEST_RECORD_ID), mockCompanyRecord);
setRecordInStores(snapshot, TEST_RECORD_ID, mockCompanyRecord);
};
return (
@ -461,7 +473,7 @@ export const AddressFieldWidget: Story = {
}),
pageLayoutData,
);
snapshot.set(recordStoreFamilyState(TEST_RECORD_ID), mockCompanyRecord);
setRecordInStores(snapshot, TEST_RECORD_ID, mockCompanyRecord);
};
return (
@ -556,7 +568,7 @@ export const NumberFieldWidget: Story = {
}),
pageLayoutData,
);
snapshot.set(recordStoreFamilyState(TEST_RECORD_ID), mockCompanyRecord);
setRecordInStores(snapshot, TEST_RECORD_ID, mockCompanyRecord);
};
return (
@ -648,7 +660,7 @@ export const LinkFieldWidget: Story = {
}),
pageLayoutData,
);
snapshot.set(recordStoreFamilyState(TEST_RECORD_ID), mockCompanyRecord);
setRecordInStores(snapshot, TEST_RECORD_ID, mockCompanyRecord);
};
return (
@ -740,14 +752,15 @@ export const ManyToOneRelationFieldWidget: Story = {
}),
pageLayoutData,
);
snapshot.set(recordStoreFamilyState(TEST_RECORD_ID), mockCompanyRecord);
setRecordInStores(snapshot, TEST_RECORD_ID, mockCompanyRecord);
// Set the related WorkspaceMember record for relation field display
if (
mockCompanyRecord.accountOwner !== null &&
mockCompanyRecord.accountOwner !== undefined
) {
snapshot.set(
recordStoreFamilyState(mockCompanyRecord.accountOwner.id),
setRecordInStores(
snapshot,
mockCompanyRecord.accountOwner.id,
mockCompanyRecord.accountOwner,
);
}
@ -842,12 +855,9 @@ export const OneToManyRelationFieldWidget: Story = {
}),
pageLayoutData,
);
snapshot.set(recordStoreFamilyState(TEST_RECORD_ID), mockCompanyRecord);
setRecordInStores(snapshot, TEST_RECORD_ID, mockCompanyRecord);
// Set the related Person record for ONE_TO_MANY relation display
snapshot.set(
recordStoreFamilyState(TEST_PERSON_RECORD_ID),
mockPersonRecord,
);
setRecordInStores(snapshot, TEST_PERSON_RECORD_ID, mockPersonRecord);
};
return (
@ -939,7 +949,7 @@ export const BooleanFieldWidget: Story = {
}),
pageLayoutData,
);
snapshot.set(recordStoreFamilyState(TEST_RECORD_ID), mockCompanyRecord);
setRecordInStores(snapshot, TEST_RECORD_ID, mockCompanyRecord);
};
return (
@ -1030,7 +1040,7 @@ export const CurrencyFieldWidget: Story = {
}),
pageLayoutData,
);
snapshot.set(recordStoreFamilyState(TEST_RECORD_ID), mockCompanyRecord);
setRecordInStores(snapshot, TEST_RECORD_ID, mockCompanyRecord);
};
return (
@ -1121,10 +1131,7 @@ export const EmailsFieldWidget: Story = {
}),
pageLayoutData,
);
snapshot.set(
recordStoreFamilyState(TEST_PERSON_RECORD_ID),
mockPersonRecord,
);
setRecordInStores(snapshot, TEST_PERSON_RECORD_ID, mockPersonRecord);
};
return (
@ -1216,10 +1223,7 @@ export const PhonesFieldWidget: Story = {
}),
pageLayoutData,
);
snapshot.set(
recordStoreFamilyState(TEST_PERSON_RECORD_ID),
mockPersonRecord,
);
setRecordInStores(snapshot, TEST_PERSON_RECORD_ID, mockPersonRecord);
};
return (
@ -1311,8 +1315,9 @@ export const SelectFieldWidget: Story = {
}),
pageLayoutData,
);
snapshot.set(
recordStoreFamilyState(TEST_OPPORTUNITY_RECORD_ID),
setRecordInStores(
snapshot,
TEST_OPPORTUNITY_RECORD_ID,
mockOpportunityRecord,
);
};
@ -1407,7 +1412,7 @@ export const MultiSelectFieldWidget: Story = {
}),
pageLayoutData,
);
snapshot.set(recordStoreFamilyState(TEST_RECORD_ID), mockCompanyRecord);
setRecordInStores(snapshot, TEST_RECORD_ID, mockCompanyRecord);
};
return (
@ -1503,13 +1508,15 @@ export const TimelineActivityRelationFieldWidget: Story = {
}),
pageLayoutData,
);
snapshot.set(
recordStoreFamilyState(TEST_TIMELINE_ACTIVITY_RECORD_ID),
setRecordInStores(
snapshot,
TEST_TIMELINE_ACTIVITY_RECORD_ID,
mockTimelineActivityRecord,
);
// Set the related WorkspaceMember record for TimelineActivity relation display
snapshot.set(
recordStoreFamilyState('test-workspace-member-xyz'),
setRecordInStores(
snapshot,
'test-workspace-member-xyz',
mockWorkspaceMemberRecord,
);
};
@ -1603,13 +1610,14 @@ export const ManyToOneRelationCardWidget: Story = {
}),
pageLayoutData,
);
snapshot.set(recordStoreFamilyState(TEST_RECORD_ID), mockCompanyRecord);
setRecordInStores(snapshot, TEST_RECORD_ID, mockCompanyRecord);
if (
mockCompanyRecord.accountOwner !== null &&
mockCompanyRecord.accountOwner !== undefined
) {
snapshot.set(
recordStoreFamilyState(mockCompanyRecord.accountOwner.id),
setRecordInStores(
snapshot,
mockCompanyRecord.accountOwner.id,
mockCompanyRecord.accountOwner,
);
}
@ -1713,11 +1721,8 @@ export const OneToManyRelationCardWidget: Story = {
}),
pageLayoutData,
);
snapshot.set(recordStoreFamilyState(TEST_RECORD_ID), mockCompanyRecord);
snapshot.set(
recordStoreFamilyState(TEST_PERSON_RECORD_ID),
mockPersonRecord,
);
setRecordInStores(snapshot, TEST_RECORD_ID, mockCompanyRecord);
setRecordInStores(snapshot, TEST_PERSON_RECORD_ID, mockPersonRecord);
};
return (
@ -1809,12 +1814,14 @@ export const TimelineActivityRelationCardWidget: Story = {
}),
pageLayoutData,
);
snapshot.set(
recordStoreFamilyState(TEST_TIMELINE_ACTIVITY_RECORD_ID),
setRecordInStores(
snapshot,
TEST_TIMELINE_ACTIVITY_RECORD_ID,
mockTimelineActivityRecord,
);
snapshot.set(
recordStoreFamilyState('test-workspace-member-xyz'),
setRecordInStores(
snapshot,
'test-workspace-member-xyz',
mockWorkspaceMemberRecord,
);
};
@ -1971,13 +1978,10 @@ export const OneToManyRelationCardWidgetWithProgressiveLoading: Story = {
}),
pageLayoutData,
);
snapshot.set(
recordStoreFamilyState(TEST_RECORD_ID),
companyWithManyPeople,
);
setRecordInStores(snapshot, TEST_RECORD_ID, companyWithManyPeople);
// Set each person record in the store
mockPeople.forEach((person) => {
snapshot.set(recordStoreFamilyState(person.id), person);
setRecordInStores(snapshot, person.id, person);
});
};

View file

@ -10,7 +10,9 @@ import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graph
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { recordStoreFamilyStateV2 } from '@/object-record/record-store/states/recordStoreFamilyStateV2';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { useOnDbEvent } from '@/sse-db-event/hooks/useOnDbEvent';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
@ -48,6 +50,7 @@ export const ListenRecordUpdatesEffect = ({
({ set }) =>
(record: ObjectRecord) => {
set(recordStoreFamilyState(record.id), record);
jotaiStore.set(recordStoreFamilyStateV2.atomFamily(record.id), record);
},
[],
);

View file

@ -1,8 +1,8 @@
import { type FieldDateMetadataSettings } from '@/object-record/record-field/ui/types/FieldMetadata';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { UserContext } from '@/users/contexts/UserContext';
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { dateLocaleState } from '~/localization/states/dateLocaleState';
import { dateLocaleStateV2 } from '~/localization/states/dateLocaleStateV2';
import { formatDateString } from '~/utils/string/formatDateString';
import { EllipsisDisplay } from './EllipsisDisplay';
@ -12,7 +12,7 @@ type DateDisplayProps = {
};
export const DateDisplay = ({ value, dateFieldSettings }: DateDisplayProps) => {
const { dateFormat } = useContext(UserContext);
const dateLocale = useRecoilValue(dateLocaleState);
const dateLocale = useRecoilValueV2(dateLocaleStateV2);
const formattedDate = formatDateString({
value,

View file

@ -1,12 +1,12 @@
import { type FieldDateMetadataSettings } from '@/object-record/record-field/ui/types/FieldMetadata';
import { TimeZoneAbbreviation } from '@/ui/input/components/internal/date/components/TimeZoneAbbreviation';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { UserContext } from '@/users/contexts/UserContext';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { Temporal } from 'temporal-polyfill';
import { dateLocaleState } from '~/localization/states/dateLocaleState';
import { dateLocaleStateV2 } from '~/localization/states/dateLocaleStateV2';
import { formatDateTimeString } from '~/utils/string/formatDateTimeString';
import { EllipsisDisplay } from './EllipsisDisplay';
@ -24,7 +24,7 @@ export const DateTimeDisplay = ({
dateFieldSettings,
}: DateTimeDisplayProps) => {
const { dateFormat, timeFormat, timeZone } = useContext(UserContext);
const dateLocale = useRecoilValue(dateLocaleState);
const dateLocale = useRecoilValueV2(dateLocaleStateV2);
const formattedDate = formatDateTimeString({
value,

View file

@ -1,11 +1,12 @@
import { downloadFile } from '@/activities/files/utils/downloadFile';
import { isAttachmentPreviewEnabledState } from '@/client-config/states/isAttachmentPreviewEnabledState';
import { isAttachmentPreviewEnabledStateV2 } from '@/client-config/states/isAttachmentPreviewEnabledStateV2';
import { type FieldFilesValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { FileChip } from '@/ui/field/display/components/FileChip';
import { UploadFileChip } from '@/ui/field/display/components/UploadFileChip';
import { filePreviewState } from '@/ui/field/display/states/filePreviewState';
import { filePreviewStateV2 } from '@/ui/field/display/states/filePreviewStateV2';
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { isDefined } from 'twenty-shared/utils';
type FilesDisplayProps = {
@ -21,9 +22,9 @@ export const FilesDisplay = ({
isUploadWindowOpen = false,
isFileUploading = false,
}: FilesDisplayProps) => {
const setFilePreview = useSetRecoilState(filePreviewState);
const isAttachmentPreviewEnabled = useRecoilValue(
isAttachmentPreviewEnabledState,
const setFilePreview = useSetRecoilStateV2(filePreviewStateV2);
const isAttachmentPreviewEnabled = useRecoilValueV2(
isAttachmentPreviewEnabledStateV2,
);
const handlePreview = (file: FieldFilesValue) => {

View file

@ -1,13 +1,13 @@
import { downloadFile } from '@/activities/files/utils/downloadFile';
import { filePreviewState } from '@/ui/field/display/states/filePreviewState';
import { filePreviewStateV2 } from '@/ui/field/display/states/filePreviewStateV2';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { lazy, Suspense, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { useRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { IconDownload, IconX } from 'twenty-ui/display';
import { IconButton } from 'twenty-ui/input';
@ -72,7 +72,7 @@ const StyledLoadingText = styled.div`
export const GlobalFilePreviewModal = (): JSX.Element | null => {
const { t } = useLingui();
const [filePreview, setFilePreview] = useRecoilState(filePreviewState);
const [filePreview, setFilePreview] = useRecoilStateV2(filePreviewStateV2);
const { openModal, closeModal } = useModal();
useEffect(() => {

View file

@ -0,0 +1,7 @@
import { type FieldFilesValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
export const filePreviewStateV2 = createStateV2<FieldFilesValue | null>({
key: 'filePreviewStateV2',
defaultValue: null,
});

View file

@ -0,0 +1,10 @@
import { useAtomValue } from 'jotai';
import { type FamilyStateV2 } from '@/ui/utilities/state/jotai/types/FamilyStateV2';
export const useFamilyRecoilValueV2 = <ValueType, FamilyKey>(
familyState: FamilyStateV2<ValueType, FamilyKey>,
familyKey: FamilyKey,
): ValueType => {
return useAtomValue(familyState.atomFamily(familyKey));
};

View file

@ -0,0 +1,13 @@
import { useAtom } from 'jotai';
import { type WritableFamilySelectorV2 } from '@/ui/utilities/state/jotai/types/WritableFamilySelectorV2';
export const useFamilySelectorStateV2 = <ValueType, FamilyKey>(
familySelector: WritableFamilySelectorV2<ValueType, FamilyKey>,
familyKey: FamilyKey,
): [
ValueType,
(value: ValueType | ((prev: ValueType) => ValueType)) => void,
] => {
return useAtom(familySelector.selectorFamily(familyKey));
};

View file

@ -0,0 +1,13 @@
import { useAtomValue } from 'jotai';
import { type FamilySelectorV2 } from '@/ui/utilities/state/jotai/types/FamilySelectorV2';
import { type WritableFamilySelectorV2 } from '@/ui/utilities/state/jotai/types/WritableFamilySelectorV2';
export const useFamilySelectorValueV2 = <ValueType, FamilyKey>(
familySelector:
| FamilySelectorV2<ValueType, FamilyKey>
| WritableFamilySelectorV2<ValueType, FamilyKey>,
familyKey: FamilyKey,
): ValueType => {
return useAtomValue(familySelector.selectorFamily(familyKey));
};

View file

@ -0,0 +1,13 @@
import { useAtom } from 'jotai';
import { type StateV2 } from '@/ui/utilities/state/jotai/types/StateV2';
import { type WritableSelectorV2 } from '@/ui/utilities/state/jotai/types/WritableSelectorV2';
export const useRecoilStateV2 = <ValueType>(
state: StateV2<ValueType> | WritableSelectorV2<ValueType>,
): [
ValueType,
(value: ValueType | ((prev: ValueType) => ValueType)) => void,
] => {
return useAtom(state.atom);
};

View file

@ -0,0 +1,14 @@
import { useAtomValue } from 'jotai';
import { type SelectorV2 } from '@/ui/utilities/state/jotai/types/SelectorV2';
import { type StateV2 } from '@/ui/utilities/state/jotai/types/StateV2';
import { type WritableSelectorV2 } from '@/ui/utilities/state/jotai/types/WritableSelectorV2';
export const useRecoilValueV2 = <ValueType>(
state:
| StateV2<ValueType>
| SelectorV2<ValueType>
| WritableSelectorV2<ValueType>,
): ValueType => {
return useAtomValue(state.atom);
};

View file

@ -0,0 +1,10 @@
import { useSetAtom } from 'jotai';
import { type FamilyStateV2 } from '@/ui/utilities/state/jotai/types/FamilyStateV2';
export const useSetFamilyRecoilStateV2 = <ValueType, FamilyKey>(
familyState: FamilyStateV2<ValueType, FamilyKey>,
familyKey: FamilyKey,
): ((value: ValueType | ((prev: ValueType) => ValueType)) => void) => {
return useSetAtom(familyState.atomFamily(familyKey));
};

View file

@ -0,0 +1,9 @@
import { useSetAtom } from 'jotai';
import { type StateV2 } from '@/ui/utilities/state/jotai/types/StateV2';
export const useSetRecoilStateV2 = <ValueType>(
state: StateV2<ValueType>,
): ((value: ValueType | ((prev: ValueType) => ValueType)) => void) => {
return useSetAtom(state.atom);
};

View file

@ -0,0 +1,3 @@
import { createStore } from 'jotai';
export const jotaiStore = createStore();

View file

@ -0,0 +1,7 @@
import { type Atom } from 'jotai';
export type FamilySelectorV2<ValueType, FamilyKey> = {
type: 'FamilySelectorV2';
key: string;
selectorFamily: (key: FamilyKey) => Atom<ValueType>;
};

View file

@ -0,0 +1,13 @@
import { type WritableAtom } from 'jotai';
type JotaiWritableAtom<ValueType> = WritableAtom<
ValueType,
[ValueType | ((prev: ValueType) => ValueType)],
void
>;
export type FamilyStateV2<ValueType, FamilyKey> = {
type: 'FamilyStateV2';
key: string;
atomFamily: (key: FamilyKey) => JotaiWritableAtom<ValueType>;
};

View file

@ -0,0 +1,26 @@
import { type FamilyStateV2 } from '@/ui/utilities/state/jotai/types/FamilyStateV2';
import { type StateV2 } from '@/ui/utilities/state/jotai/types/StateV2';
export type SelectorGetterV2 = {
get: {
<ValueType>(state: StateV2<ValueType>): ValueType;
<ValueType, FamilyKey>(
familyState: FamilyStateV2<ValueType, FamilyKey>,
familyKey: FamilyKey,
): ValueType;
};
};
export type SelectorSetterV2 = SelectorGetterV2 & {
set: {
<ValueType>(
state: StateV2<ValueType>,
value: ValueType | ((prev: ValueType) => ValueType),
): void;
<ValueType, FamilyKey>(
familyState: FamilyStateV2<ValueType, FamilyKey>,
familyKey: FamilyKey,
value: ValueType | ((prev: ValueType) => ValueType),
): void;
};
};

View file

@ -0,0 +1,7 @@
import { type Atom } from 'jotai';
export type SelectorV2<ValueType> = {
type: 'SelectorV2';
key: string;
atom: Atom<ValueType>;
};

View file

@ -0,0 +1,11 @@
import { type WritableAtom } from 'jotai';
export type StateV2<ValueType> = {
type: 'StateV2';
key: string;
atom: WritableAtom<
ValueType,
[ValueType | ((prev: ValueType) => ValueType)],
void
>;
};

View file

@ -0,0 +1,13 @@
import { type WritableAtom } from 'jotai';
export type WritableFamilySelectorV2<ValueType, FamilyKey> = {
type: 'WritableFamilySelectorV2';
key: string;
selectorFamily: (
key: FamilyKey,
) => WritableAtom<
ValueType,
[ValueType | ((prev: ValueType) => ValueType)],
void
>;
};

View file

@ -0,0 +1,11 @@
import { type WritableAtom } from 'jotai';
export type WritableSelectorV2<ValueType> = {
type: 'WritableSelectorV2';
key: string;
atom: WritableAtom<
ValueType,
[ValueType | ((prev: ValueType) => ValueType)],
void
>;
};

View file

@ -0,0 +1,21 @@
import { type Getter } from 'jotai';
import { type FamilyStateV2 } from '@/ui/utilities/state/jotai/types/FamilyStateV2';
import { type StateV2 } from '@/ui/utilities/state/jotai/types/StateV2';
export const buildGetHelper =
(jotaiGet: Getter) =>
<ValueType, FamilyKey = never>(
stateOrFamily: StateV2<ValueType> | FamilyStateV2<ValueType, FamilyKey>,
familyKey?: FamilyKey,
): ValueType => {
if (stateOrFamily.type === 'FamilyStateV2') {
return jotaiGet(
(stateOrFamily as FamilyStateV2<ValueType, FamilyKey>).atomFamily(
familyKey as FamilyKey,
),
);
}
return jotaiGet((stateOrFamily as StateV2<ValueType>).atom);
};

View file

@ -0,0 +1,27 @@
import { type Setter } from 'jotai';
import { type FamilyStateV2 } from '@/ui/utilities/state/jotai/types/FamilyStateV2';
import { type StateV2 } from '@/ui/utilities/state/jotai/types/StateV2';
export const buildSetHelper =
(jotaiSet: Setter) =>
<ValueType, FamilyKey = never>(
stateOrFamily: StateV2<ValueType> | FamilyStateV2<ValueType, FamilyKey>,
valueOrFamilyKey: ValueType | ((prev: ValueType) => ValueType) | FamilyKey,
familyValue?: ValueType | ((prev: ValueType) => ValueType),
): void => {
if (stateOrFamily.type === 'FamilyStateV2') {
jotaiSet(
(stateOrFamily as FamilyStateV2<ValueType, FamilyKey>).atomFamily(
valueOrFamilyKey as FamilyKey,
),
familyValue as ValueType | ((prev: ValueType) => ValueType),
);
return;
}
jotaiSet(
(stateOrFamily as StateV2<ValueType>).atom,
valueOrFamilyKey as ValueType | ((prev: ValueType) => ValueType),
);
};

View file

@ -0,0 +1,45 @@
import { atom, type Atom } from 'jotai';
import { type FamilySelectorV2 } from '@/ui/utilities/state/jotai/types/FamilySelectorV2';
import { type SelectorGetterV2 } from '@/ui/utilities/state/jotai/types/SelectorCallbacksV2';
import { buildGetHelper } from '@/ui/utilities/state/jotai/utils/buildGetHelper';
export const createFamilySelectorV2 = <ValueType, FamilyKey>({
key,
get,
}: {
key: string;
get: (familyKey: FamilyKey) => (callbacks: SelectorGetterV2) => ValueType;
}): FamilySelectorV2<ValueType, FamilyKey> => {
const atomCache = new Map<string, Atom<ValueType>>();
const selectorFamily = (familyKey: FamilyKey): Atom<ValueType> => {
const cacheKey =
typeof familyKey === 'string' ? familyKey : JSON.stringify(familyKey);
const existing = atomCache.get(cacheKey);
if (existing !== undefined) {
return existing;
}
const getForKey = get(familyKey);
const derivedAtom = atom((jotaiGet) => {
const getHelper = buildGetHelper(jotaiGet);
return getForKey({ get: getHelper });
});
derivedAtom.debugLabel = `${key}__${cacheKey}`;
atomCache.set(cacheKey, derivedAtom);
return derivedAtom;
};
return {
type: 'FamilySelectorV2',
key,
selectorFamily,
};
};

View file

@ -0,0 +1,41 @@
import { atom } from 'jotai';
import { type FamilyStateV2 } from '@/ui/utilities/state/jotai/types/FamilyStateV2';
export const createFamilyStateV2 = <ValueType, FamilyKey>({
key,
defaultValue,
}: {
key: string;
defaultValue: ValueType;
}): FamilyStateV2<ValueType, FamilyKey> => {
const atomCache = new Map<
string,
ReturnType<FamilyStateV2<ValueType, FamilyKey>['atomFamily']>
>();
const familyFunction = (
familyKey: FamilyKey,
): ReturnType<FamilyStateV2<ValueType, FamilyKey>['atomFamily']> => {
const cacheKey =
typeof familyKey === 'string' ? familyKey : JSON.stringify(familyKey);
const existing = atomCache.get(cacheKey);
if (existing !== undefined) {
return existing;
}
const baseAtom = atom(defaultValue);
baseAtom.debugLabel = `${key}__${cacheKey}`;
atomCache.set(cacheKey, baseAtom);
return baseAtom;
};
return {
type: 'FamilyStateV2',
key,
atomFamily: familyFunction,
};
};

View file

@ -0,0 +1,27 @@
import { atom } from 'jotai';
import { type SelectorGetterV2 } from '@/ui/utilities/state/jotai/types/SelectorCallbacksV2';
import { type SelectorV2 } from '@/ui/utilities/state/jotai/types/SelectorV2';
import { buildGetHelper } from '@/ui/utilities/state/jotai/utils/buildGetHelper';
export const createSelectorV2 = <ValueType>({
key,
get,
}: {
key: string;
get: (callbacks: SelectorGetterV2) => ValueType;
}): SelectorV2<ValueType> => {
const derivedAtom = atom((jotaiGet) => {
const getHelper = buildGetHelper(jotaiGet);
return get({ get: getHelper });
});
derivedAtom.debugLabel = key;
return {
type: 'SelectorV2',
key,
atom: derivedAtom,
};
};

View file

@ -0,0 +1,20 @@
import { atom } from 'jotai';
import { type StateV2 } from '@/ui/utilities/state/jotai/types/StateV2';
export const createStateV2 = <ValueType>({
key,
defaultValue,
}: {
key: string;
defaultValue: ValueType;
}): StateV2<ValueType> => {
const baseAtom = atom(defaultValue);
baseAtom.debugLabel = key;
return {
type: 'StateV2',
key,
atom: baseAtom,
};
};

View file

@ -0,0 +1,86 @@
import { atom, type WritableAtom } from 'jotai';
import {
type SelectorGetterV2,
type SelectorSetterV2,
} from '@/ui/utilities/state/jotai/types/SelectorCallbacksV2';
import { type WritableFamilySelectorV2 } from '@/ui/utilities/state/jotai/types/WritableFamilySelectorV2';
import { buildGetHelper } from '@/ui/utilities/state/jotai/utils/buildGetHelper';
import { buildSetHelper } from '@/ui/utilities/state/jotai/utils/buildSetHelper';
export const createWritableFamilySelectorV2 = <ValueType, FamilyKey>({
key,
get,
set,
}: {
key: string;
get: (familyKey: FamilyKey) => (callbacks: SelectorGetterV2) => ValueType;
set: (
familyKey: FamilyKey,
) => (callbacks: SelectorSetterV2, newValue: ValueType) => void;
}): WritableFamilySelectorV2<ValueType, FamilyKey> => {
const atomCache = new Map<
string,
WritableAtom<
ValueType,
[ValueType | ((prev: ValueType) => ValueType)],
void
>
>();
const selectorFamily = (
familyKey: FamilyKey,
): WritableAtom<
ValueType,
[ValueType | ((prev: ValueType) => ValueType)],
void
> => {
const cacheKey =
typeof familyKey === 'string' ? familyKey : JSON.stringify(familyKey);
const existing = atomCache.get(cacheKey);
if (existing !== undefined) {
return existing;
}
const getForKey = get(familyKey);
const setForKey = set(familyKey);
const derivedAtom = atom(
(jotaiGet) => {
const getHelper = buildGetHelper(jotaiGet);
return getForKey({ get: getHelper });
},
(
jotaiGet,
jotaiSet,
valueOrUpdater: ValueType | ((prev: ValueType) => ValueType),
) => {
const getHelper = buildGetHelper(jotaiGet);
const setHelper = buildSetHelper(jotaiSet);
const resolvedValue =
typeof valueOrUpdater === 'function'
? (valueOrUpdater as (prev: ValueType) => ValueType)(
getForKey({ get: getHelper }),
)
: valueOrUpdater;
setForKey({ get: getHelper, set: setHelper }, resolvedValue);
},
);
derivedAtom.debugLabel = `${key}__${cacheKey}`;
atomCache.set(cacheKey, derivedAtom);
return derivedAtom;
};
return {
type: 'WritableFamilySelectorV2',
key,
selectorFamily,
};
};

View file

@ -0,0 +1,52 @@
import { atom } from 'jotai';
import {
type SelectorGetterV2,
type SelectorSetterV2,
} from '@/ui/utilities/state/jotai/types/SelectorCallbacksV2';
import { type WritableSelectorV2 } from '@/ui/utilities/state/jotai/types/WritableSelectorV2';
import { buildGetHelper } from '@/ui/utilities/state/jotai/utils/buildGetHelper';
import { buildSetHelper } from '@/ui/utilities/state/jotai/utils/buildSetHelper';
export const createWritableSelectorV2 = <ValueType>({
key,
get,
set,
}: {
key: string;
get: (callbacks: SelectorGetterV2) => ValueType;
set: (callbacks: SelectorSetterV2, newValue: ValueType) => void;
}): WritableSelectorV2<ValueType> => {
const derivedAtom = atom(
(jotaiGet) => {
const getHelper = buildGetHelper(jotaiGet);
return get({ get: getHelper });
},
(
jotaiGet,
jotaiSet,
valueOrUpdater: ValueType | ((prev: ValueType) => ValueType),
) => {
const getHelper = buildGetHelper(jotaiGet);
const setHelper = buildSetHelper(jotaiSet);
const resolvedValue =
typeof valueOrUpdater === 'function'
? (valueOrUpdater as (prev: ValueType) => ValueType)(
get({ get: getHelper }),
)
: valueOrUpdater;
set({ get: getHelper, set: setHelper }, resolvedValue);
},
);
derivedAtom.debugLabel = key;
return {
type: 'WritableSelectorV2',
key,
atom: derivedAtom,
};
};

View file

@ -15,6 +15,7 @@ import { type PageLayout } from '@/page-layout/types/PageLayout';
import { transformPageLayout } from '@/page-layout/utils/transformPageLayout';
import { logicFunctionsState } from '@/settings/logic-functions/states/logicFunctionsState';
import { getDateFnsLocale } from '@/ui/field/display/utils/getDateFnsLocale.util';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { coreViewsState } from '@/views/states/coreViewState';
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
import { type ColorScheme } from '@/workspace-member/types/WorkspaceMember';
@ -32,6 +33,7 @@ import {
useFindManyLogicFunctionsQuery,
} from '~/generated-metadata/graphql';
import { dateLocaleState } from '~/localization/states/dateLocaleState';
import { dateLocaleStateV2 } from '~/localization/states/dateLocaleStateV2';
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { isMatchingLocation } from '~/utils/isMatchingLocation';
@ -61,10 +63,12 @@ export const MetadataProviderEffect = () => {
const localeValue = snapshot.getLoadable(dateLocaleState).getValue();
if (localeValue.locale !== newLocale) {
getDateFnsLocale(newLocale).then((localeCatalog) => {
set(dateLocaleState, {
const newValue = {
locale: newLocale,
localeCatalog: localeCatalog || enUS,
});
};
set(dateLocaleState, newValue);
jotaiStore.set(dateLocaleStateV2.atom, newValue);
});
}
},

View file

@ -8,12 +8,14 @@ import { getDateFnsLocale } from '@/ui/field/display/utils/getDateFnsLocale.util
import { Select } from '@/ui/input/components/Select';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { useRefreshAllCoreViews } from '@/views/hooks/useRefreshAllCoreViews';
import { useLingui } from '@lingui/react/macro';
import { enUS } from 'date-fns/locale';
import { APP_LOCALES } from 'twenty-shared/translations';
import { isDefined } from 'twenty-shared/utils';
import { dateLocaleState } from '~/localization/states/dateLocaleState';
import { dateLocaleStateV2 } from '~/localization/states/dateLocaleStateV2';
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
import { logError } from '~/utils/logError';
@ -63,10 +65,12 @@ export const LocalePicker = () => {
await updateWorkspaceMember({ locale: value });
const dateFnsLocale = await getDateFnsLocale(value);
setDateLocale({
const newDateLocale = {
locale: value,
localeCatalog: dateFnsLocale || enUS,
});
};
setDateLocale(newDateLocale);
jotaiStore.set(dateLocaleStateV2.atom, newDateLocale);
await dynamicActivate(value);
try {

View file

@ -1,6 +1,7 @@
import { ApolloProvider } from '@apollo/client';
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 {
createMemoryRouter,
@ -15,6 +16,7 @@ import { ClientConfigProviderEffect } from '@/client-config/components/ClientCon
import { ApolloCoreClientMockedProvider } from '@/object-metadata/hooks/__mocks__/ApolloCoreClientMockedProvider';
import { DefaultLayout } from '@/ui/layout/page/components/DefaultLayout';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { MetadataProviderEffect } from '@/users/components/MetadataProviderEffect';
import { ClientConfigProvider } from '~/modules/client-config/components/ClientConfigProvider';
import { UserProvider } from '~/modules/users/components/UserProvider';
@ -75,42 +77,44 @@ await dynamicActivate(SOURCE_LOCALE);
const Providers = () => {
return (
<RecoilRoot>
<SnackBarComponentInstanceContext.Provider
value={{ instanceId: 'snack-bar-manager' }}
>
<RecoilDebugObserverEffect />
<ApolloProvider client={mockedApolloClient}>
<I18nProvider i18n={i18n}>
<ApolloStorybookDevLogEffect />
<ClientConfigProviderEffect />
<ClientConfigProvider>
<MetadataProviderEffect />
<WorkspaceProviderEffect />
<UserProvider>
<ApolloCoreClientMockedProvider>
<ObjectMetadataItemsLoadEffect />
<ObjectMetadataItemsProvider>
<FullHeightStorybookLayout>
<HelmetProvider>
<IconsProvider>
<PrefetchDataProvider>
<RecordComponentInstanceContextsWrapper componentInstanceId="storybook-test-record">
<Outlet />
</RecordComponentInstanceContextsWrapper>
</PrefetchDataProvider>
</IconsProvider>
</HelmetProvider>
</FullHeightStorybookLayout>
</ObjectMetadataItemsProvider>
<MainContextStoreProvider />
</ApolloCoreClientMockedProvider>
</UserProvider>
</ClientConfigProvider>
</I18nProvider>
</ApolloProvider>
</SnackBarComponentInstanceContext.Provider>
</RecoilRoot>
<JotaiProvider store={jotaiStore}>
<RecoilRoot>
<SnackBarComponentInstanceContext.Provider
value={{ instanceId: 'snack-bar-manager' }}
>
<RecoilDebugObserverEffect />
<ApolloProvider client={mockedApolloClient}>
<I18nProvider i18n={i18n}>
<ApolloStorybookDevLogEffect />
<ClientConfigProviderEffect />
<ClientConfigProvider>
<MetadataProviderEffect />
<WorkspaceProviderEffect />
<UserProvider>
<ApolloCoreClientMockedProvider>
<ObjectMetadataItemsLoadEffect />
<ObjectMetadataItemsProvider>
<FullHeightStorybookLayout>
<HelmetProvider>
<IconsProvider>
<PrefetchDataProvider>
<RecordComponentInstanceContextsWrapper componentInstanceId="storybook-test-record">
<Outlet />
</RecordComponentInstanceContextsWrapper>
</PrefetchDataProvider>
</IconsProvider>
</HelmetProvider>
</FullHeightStorybookLayout>
</ObjectMetadataItemsProvider>
<MainContextStoreProvider />
</ApolloCoreClientMockedProvider>
</UserProvider>
</ClientConfigProvider>
</I18nProvider>
</ApolloProvider>
</SnackBarComponentInstanceContext.Provider>
</RecoilRoot>
</JotaiProvider>
);
};

View file

@ -1,8 +1,10 @@
import { ApolloProvider } from '@apollo/client';
import { type Decorator } from '@storybook/react-vite';
import { Provider as JotaiProvider } from 'jotai';
import { RecoilRoot } from 'recoil';
import { ApolloCoreClientMockedProvider } from '@/object-metadata/hooks/__mocks__/ApolloCoreClientMockedProvider';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { mockedApolloClient } from '~/testing/mockedApolloClient';
@ -10,12 +12,14 @@ export const RootDecorator: Decorator = (Story, context) => {
const { parameters } = context;
return (
<RecoilRoot initializeState={parameters.initializeState}>
<ApolloProvider client={mockedApolloClient}>
<ApolloCoreClientMockedProvider>
<Story />
</ApolloCoreClientMockedProvider>
</ApolloProvider>
</RecoilRoot>
<JotaiProvider store={jotaiStore}>
<RecoilRoot initializeState={parameters.initializeState}>
<ApolloProvider client={mockedApolloClient}>
<ApolloCoreClientMockedProvider>
<Story />
</ApolloCoreClientMockedProvider>
</ApolloProvider>
</RecoilRoot>
</JotaiProvider>
);
};

View file

@ -1,4 +1,5 @@
import { MockedProvider, type MockedResponse } from '@apollo/client/testing';
import { Provider as JotaiProvider } from 'jotai';
import { type ReactNode } from 'react';
import { RecoilRoot, type MutableSnapshot } from 'recoil';
@ -7,6 +8,7 @@ import { ContextStoreComponentInstanceContext } from '@/context-store/states/con
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { SnackBarComponentInstanceContext } from '@/ui/feedback/snack-bar-manager/contexts/SnackBarComponentInstanceContext';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { type InMemoryCache } from '@apollo/client';
import { JestContextStoreSetter } from '~/testing/jest/JestContextStoreSetter';
@ -26,28 +28,30 @@ export const getJestMetadataAndApolloMocksWrapper = ({
objectMetadataItems?: ObjectMetadataItem[];
}) => {
return ({ children }: { children: ReactNode }) => (
<RecoilRoot initializeState={onInitializeRecoilSnapshot}>
<SnackBarComponentInstanceContext.Provider
value={{ instanceId: 'snack-bar-manager' }}
>
<MockedProvider mocks={apolloMocks} addTypename={false} cache={cache}>
<RecordComponentInstanceContextsWrapper componentInstanceId="instanceId">
<ViewComponentInstanceContext.Provider
value={{ instanceId: 'instanceId' }}
>
<JestObjectMetadataItemSetter
objectMetadataItems={objectMetadataItems}
<JotaiProvider store={jotaiStore}>
<RecoilRoot initializeState={onInitializeRecoilSnapshot}>
<SnackBarComponentInstanceContext.Provider
value={{ instanceId: 'snack-bar-manager' }}
>
<MockedProvider mocks={apolloMocks} addTypename={false} cache={cache}>
<RecordComponentInstanceContextsWrapper componentInstanceId="instanceId">
<ViewComponentInstanceContext.Provider
value={{ instanceId: 'instanceId' }}
>
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: 'instanceId' }}
<JestObjectMetadataItemSetter
objectMetadataItems={objectMetadataItems}
>
<JestContextStoreSetter>{children}</JestContextStoreSetter>
</ContextStoreComponentInstanceContext.Provider>
</JestObjectMetadataItemSetter>
</ViewComponentInstanceContext.Provider>
</RecordComponentInstanceContextsWrapper>
</MockedProvider>
</SnackBarComponentInstanceContext.Provider>
</RecoilRoot>
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: 'instanceId' }}
>
<JestContextStoreSetter>{children}</JestContextStoreSetter>
</ContextStoreComponentInstanceContext.Provider>
</JestObjectMetadataItemSetter>
</ViewComponentInstanceContext.Provider>
</RecordComponentInstanceContextsWrapper>
</MockedProvider>
</SnackBarComponentInstanceContext.Provider>
</RecoilRoot>
</JotaiProvider>
);
};

View file

@ -33,6 +33,7 @@
"framer-motion": "^11.18.0",
"glob": "^11.1.0",
"hex-rgb": "^5.0.0",
"jotai": "^2.17.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-responsive": "^9.0.2",

View file

@ -1,8 +1,9 @@
import { styled } from '@linaria/react';
import { isNonEmptyString, isNull, isUndefined } from '@sniptt/guards';
import { useAtom } from 'jotai';
import { useContext } from 'react';
import { invalidAvatarUrlsState } from '@ui/display/avatar/components/states/isInvalidAvatarUrlState';
import { invalidAvatarUrlsAtomV2 } from '@ui/display/avatar/components/states/invalidAvatarUrlsAtomV2';
import { AVATAR_PROPERTIES_BY_SIZE } from '@ui/display/avatar/constants/AvatarPropertiesBySize';
import { type AvatarSize } from '@ui/display/avatar/types/AvatarSize';
import { type AvatarType } from '@ui/display/avatar/types/AvatarType';
@ -10,7 +11,6 @@ import { type IconComponent } from '@ui/display/icon/types/IconComponent';
import { ThemeContext } from '@ui/theme';
import { stringToThemeColorP3String } from '@ui/utilities';
import { REACT_APP_SERVER_BASE_URL } from '@ui/utilities/config';
import { useRecoilState } from 'recoil';
import { type Nullable } from 'twenty-shared/types';
import { getImageAbsoluteURI } from 'twenty-shared/utils';
@ -72,7 +72,6 @@ export type AvatarProps = {
onClick?: () => void;
};
// TODO: Remove recoil because we don't want it into twenty-ui and find a solution for invalid avatar urls
export const Avatar = ({
avatarUrl,
size = 'md',
@ -86,8 +85,8 @@ export const Avatar = ({
backgroundColor,
}: AvatarProps) => {
const { theme } = useContext(ThemeContext);
const [invalidAvatarUrls, setInvalidAvatarUrls] = useRecoilState(
invalidAvatarUrlsState,
const [invalidAvatarUrls, setInvalidAvatarUrls] = useAtom(
invalidAvatarUrlsAtomV2,
);
const avatarImageURI = isNonEmptyString(avatarUrl)

View file

@ -0,0 +1,4 @@
import { atom } from 'jotai';
export const invalidAvatarUrlsAtomV2 = atom<string[]>([]);
invalidAvatarUrlsAtomV2.debugLabel = 'invalidAvatarUrlsAtomV2';

View file

@ -11,6 +11,7 @@ export type { AvatarProps } from './avatar/components/Avatar';
export { Avatar } from './avatar/components/Avatar';
export type { AvatarGroupProps } from './avatar/components/AvatarGroup';
export { AvatarGroup } from './avatar/components/AvatarGroup';
export { invalidAvatarUrlsAtomV2 } from './avatar/components/states/invalidAvatarUrlsAtomV2';
export { invalidAvatarUrlsState } from './avatar/components/states/isInvalidAvatarUrlState';
export { AVATAR_PROPERTIES_BY_SIZE } from './avatar/constants/AvatarPropertiesBySize';
export type { AvatarSize } from './avatar/types/AvatarSize';

View file

@ -41855,6 +41855,27 @@ __metadata:
languageName: node
linkType: hard
"jotai@npm:^2.17.1":
version: 2.17.1
resolution: "jotai@npm:2.17.1"
peerDependencies:
"@babel/core": ">=7.0.0"
"@babel/template": ">=7.0.0"
"@types/react": ">=17.0.0"
react: ">=17.0.0"
peerDependenciesMeta:
"@babel/core":
optional: true
"@babel/template":
optional: true
"@types/react":
optional: true
react:
optional: true
checksum: 10c0/45e52a77a7d33d0947f3ceafe8930a161e170e1d28e934572a325267fdc0177f766761f06b38b884955f468deb27ebb530646658566ea5bbba68991317fb09a4
languageName: node
linkType: hard
"js-cookie@npm:^3.0.5":
version: 3.0.5
resolution: "js-cookie@npm:3.0.5"
@ -56895,6 +56916,7 @@ __metadata:
graphql: "npm:16.8.1"
graphql-sse: "npm:^2.5.4"
input-otp: "npm:^1.4.2"
jotai: "npm:^2.17.1"
js-cookie: "npm:^3.0.5"
json-2-csv: "npm:^5.4.0"
json-logic-js: "npm:^2.0.5"
@ -57257,6 +57279,7 @@ __metadata:
framer-motion: "npm:^11.18.0"
glob: "npm:^11.1.0"
hex-rgb: "npm:^5.0.0"
jotai: "npm:^2.17.1"
react: "npm:^18.2.0"
react-dom: "npm:^18.2.0"
react-responsive: "npm:^9.0.2"
@ -57451,6 +57474,7 @@ __metadata:
jest-environment-jsdom: "npm:30.0.0-beta.3"
jest-environment-node: "npm:^29.4.1"
jest-fetch-mock: "npm:^3.0.3"
jotai: "npm:^2.17.1"
jsdom: "npm:~22.1.0"
libphonenumber-js: "npm:^1.10.26"
lodash.camelcase: "npm:^4.3.0"