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:
@@ -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 =
|
||||
|
||||
+3
@@ -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,
|
||||
});
|
||||
|
||||
+6
-8
@@ -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';
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+6
-3
@@ -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
-2
@@ -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
-2
@@ -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
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
+2
-2
@@ -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
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+3
-3
@@ -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 } },
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+9
-8
@@ -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,
|
||||
|
||||
+7
-6
@@ -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(
|
||||
|
||||
+22
@@ -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(
|
||||
|
||||
+16
@@ -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,
|
||||
});
|
||||
+3
@@ -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,
|
||||
|
||||
+8
@@ -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,
|
||||
});
|
||||
+6
@@ -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],
|
||||
|
||||
+22
@@ -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;
|
||||
};
|
||||
+16
-4
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+10
@@ -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,
|
||||
});
|
||||
+23
@@ -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),
|
||||
);
|
||||
},
|
||||
});
|
||||
+117
@@ -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 });
|
||||
};
|
||||
+6
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user