mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
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:
parent
08b962b0d2
commit
d2f8352cb8
87 changed files with 1086 additions and 241 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
|
||||
|
||||
export const isAttachmentPreviewEnabledStateV2 = createStateV2<boolean>({
|
||||
key: 'isAttachmentPreviewEnabledStateV2',
|
||||
defaultValue: false,
|
||||
});
|
||||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 } },
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
@ -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),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -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 });
|
||||
};
|
||||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
@ -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));
|
||||
};
|
||||
|
|
@ -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));
|
||||
};
|
||||
|
|
@ -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));
|
||||
};
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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));
|
||||
};
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import { createStore } from 'jotai';
|
||||
|
||||
export const jotaiStore = createStore();
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { type Atom } from 'jotai';
|
||||
|
||||
export type FamilySelectorV2<ValueType, FamilyKey> = {
|
||||
type: 'FamilySelectorV2';
|
||||
key: string;
|
||||
selectorFamily: (key: FamilyKey) => Atom<ValueType>;
|
||||
};
|
||||
|
|
@ -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>;
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { type Atom } from 'jotai';
|
||||
|
||||
export type SelectorV2<ValueType> = {
|
||||
type: 'SelectorV2';
|
||||
key: string;
|
||||
atom: Atom<ValueType>;
|
||||
};
|
||||
|
|
@ -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
|
||||
>;
|
||||
};
|
||||
|
|
@ -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
|
||||
>;
|
||||
};
|
||||
|
|
@ -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
|
||||
>;
|
||||
};
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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),
|
||||
);
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
import { atom } from 'jotai';
|
||||
|
||||
export const invalidAvatarUrlsAtomV2 = atom<string[]>([]);
|
||||
invalidAvatarUrlsAtomV2.debugLabel = 'invalidAvatarUrlsAtomV2';
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
24
yarn.lock
24
yarn.lock
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue