Removed use-context-selector completely (#12139)

This PR removes use-context-selector completely, so that any bug
associated with state synchronization between recoil and
use-context-selector disappears.

There might be a slight performance decrease on the table, but since we
have already improved the average performance per line by a lot, and
that the performance bottleneck right now is the fetch more logic and
the windowing solution we use, it is not relevant.

Also the DX has become so hindered by this parallel state logic recently
(think [cache
invalidation](https://martinfowler.com/bliki/TwoHardThings.html)), that
the main benefit we gain from this removal is the DX improvement.

Fixes https://github.com/twentyhq/twenty/issues/12123
Fixes https://github.com/twentyhq/twenty/issues/12109
This commit is contained in:
Lucas Bordeau
2025-05-20 13:35:28 +02:00
committed by GitHub
parent 9ba24b3654
commit 0553f58c52
30 changed files with 408 additions and 697 deletions

View File

@ -11,7 +11,6 @@ import { RecordShowContainer } from '@/object-record/record-show/components/Reco
import { RecordShowEffect } from '@/object-record/record-show/components/RecordShowEffect';
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { PageHeaderToggleCommandMenuButton } from '@/ui/layout/page-header/components/PageHeaderToggleCommandMenuButton';
import { PageBody } from '@/ui/layout/page/components/PageBody';
import { PageContainer } from '@/ui/layout/page/components/PageContainer';
@ -30,57 +29,55 @@ export const RecordShowPage = () => {
);
return (
<RecordFieldValueSelectorContextProvider>
<RecordFilterGroupsComponentInstanceContext.Provider
<RecordFilterGroupsComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }}
>
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }}
>
<RecordFiltersComponentInstanceContext.Provider
<RecordSortsComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }}
>
<RecordSortsComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }}
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID }}
>
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID }}
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }}
>
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }}
>
<PageContainer>
<RecordShowPageTitle
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
/>
<RecordShowPageHeader
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
<PageContainer>
<RecordShowPageTitle
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
/>
<RecordShowPageHeader
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
>
<RecordShowActionMenu />
<PageHeaderToggleCommandMenuButton />
</RecordShowPageHeader>
<PageBody>
<TimelineActivityContext.Provider
value={{
recordId: objectRecordId,
}}
>
<RecordShowActionMenu />
<PageHeaderToggleCommandMenuButton />
</RecordShowPageHeader>
<PageBody>
<TimelineActivityContext.Provider
value={{
recordId: objectRecordId,
}}
>
<RecordShowEffect
objectNameSingular={objectNameSingular}
recordId={objectRecordId}
/>
<RecordShowContainer
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
loading={false}
/>
</TimelineActivityContext.Provider>
</PageBody>
</PageContainer>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordSortsComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</RecordFilterGroupsComponentInstanceContext.Provider>
</RecordFieldValueSelectorContextProvider>
<RecordShowEffect
objectNameSingular={objectNameSingular}
recordId={objectRecordId}
/>
<RecordShowContainer
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
loading={false}
/>
</TimelineActivityContext.Provider>
</PageBody>
</PageContainer>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordSortsComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</RecordFilterGroupsComponentInstanceContext.Provider>
);
};

View File

@ -12,7 +12,6 @@ import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMe
import { useUpdateOneFieldMetadataItem } from '@/object-metadata/hooks/useUpdateOneFieldMetadataItem';
import { formatFieldMetadataItemInput } from '@/object-metadata/utils/formatFieldMetadataItemInput';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { FIELD_NAME_MAXIMUM_LENGTH } from '@/settings/data-model/constants/FieldNameMaximumLength';
@ -161,7 +160,7 @@ export const SettingsObjectFieldEdit = () => {
};
return (
<RecordFieldValueSelectorContextProvider>
<>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<FormProvider {...formConfig}>
<SubMenuTopBarContainer
@ -265,6 +264,6 @@ export const SettingsObjectFieldEdit = () => {
</SettingsPageContainer>
</SubMenuTopBarContainer>
</FormProvider>
</RecordFieldValueSelectorContextProvider>
</>
);
};

View File

@ -3,7 +3,6 @@ import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataIt
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsDataModelNewFieldBreadcrumbDropDown } from '@/settings/data-model/components/SettingsDataModelNewFieldBreadcrumbDropDown';
@ -27,6 +26,8 @@ import pick from 'lodash.pick';
import { useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { useParams, useSearchParams } from 'react-router-dom';
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
import { z } from 'zod';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { useNavigateApp } from '~/hooks/useNavigateApp';
@ -35,8 +36,6 @@ import { DEFAULT_ICONS_BY_FIELD_TYPE } from '~/pages/settings/data-model/constan
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
type SettingsDataModelNewFieldFormValues = z.infer<
ReturnType<typeof settingsFieldFormSchema>
@ -200,87 +199,85 @@ export const SettingsObjectNewFieldConfigure = () => {
if (!activeObjectMetadataItem) return null;
return (
<RecordFieldValueSelectorContextProvider>
<FormProvider // eslint-disable-next-line react/jsx-props-no-spreading
{...formConfig}
>
<SubMenuTopBarContainer
title={t`2. Configure field`}
links={[
{
children: t`Workspace`,
href: getSettingsPath(SettingsPath.Workspace),
},
{
children: t`Objects`,
href: getSettingsPath(SettingsPath.Objects),
},
{
children: activeObjectMetadataItem.labelPlural,
href: getSettingsPath(SettingsPath.ObjectDetail, {
objectNamePlural,
}),
},
<FormProvider // eslint-disable-next-line react/jsx-props-no-spreading
{...formConfig}
>
<SubMenuTopBarContainer
title={t`2. Configure field`}
links={[
{
children: t`Workspace`,
href: getSettingsPath(SettingsPath.Workspace),
},
{
children: t`Objects`,
href: getSettingsPath(SettingsPath.Objects),
},
{
children: activeObjectMetadataItem.labelPlural,
href: getSettingsPath(SettingsPath.ObjectDetail, {
objectNamePlural,
}),
},
{ children: <SettingsDataModelNewFieldBreadcrumbDropDown /> },
]}
actionButton={
<SaveAndCancelButtons
isSaveDisabled={!canSave}
isCancelDisabled={isSubmitting}
onCancel={() =>
navigate(
SettingsPath.ObjectNewFieldSelect,
{
objectNamePlural,
},
{
fieldType,
},
)
}
onSave={formConfig.handleSubmit(handleSave)}
{ children: <SettingsDataModelNewFieldBreadcrumbDropDown /> },
]}
actionButton={
<SaveAndCancelButtons
isSaveDisabled={!canSave}
isCancelDisabled={isSubmitting}
onCancel={() =>
navigate(
SettingsPath.ObjectNewFieldSelect,
{
objectNamePlural,
},
{
fieldType,
},
)
}
onSave={formConfig.handleSubmit(handleSave)}
/>
}
>
<SettingsPageContainer>
<Section>
<H2Title
title={t`Icon and Name`}
description={t`The name and icon of this field`}
/>
}
>
<SettingsPageContainer>
<Section>
<H2Title
title={t`Icon and Name`}
description={t`The name and icon of this field`}
/>
<SettingsDataModelFieldIconLabelForm
maxLength={FIELD_NAME_MAXIMUM_LENGTH}
canToggleSyncLabelWithName={
fieldType !== FieldMetadataType.RELATION
}
/>
</Section>
<Section>
<H2Title
title={t`Values`}
description={t`The values of this field`}
/>
<SettingsDataModelFieldSettingsFormCard
fieldMetadataItem={{
icon: formConfig.watch('icon'),
label: formConfig.watch('label') || 'New Field',
settings: formConfig.watch('settings') || null,
type: fieldType as FieldMetadataType,
}}
objectMetadataItem={activeObjectMetadataItem}
/>
</Section>
<Section>
<H2Title
title={t`Description`}
description={t`The description of this field`}
/>
<SettingsDataModelFieldDescriptionForm />
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</FormProvider>
</RecordFieldValueSelectorContextProvider>
<SettingsDataModelFieldIconLabelForm
maxLength={FIELD_NAME_MAXIMUM_LENGTH}
canToggleSyncLabelWithName={
fieldType !== FieldMetadataType.RELATION
}
/>
</Section>
<Section>
<H2Title
title={t`Values`}
description={t`The values of this field`}
/>
<SettingsDataModelFieldSettingsFormCard
fieldMetadataItem={{
icon: formConfig.watch('icon'),
label: formConfig.watch('label') || 'New Field',
settings: formConfig.watch('settings') || null,
type: fieldType as FieldMetadataType,
}}
objectMetadataItem={activeObjectMetadataItem}
/>
</Section>
<Section>
<H2Title
title={t`Description`}
description={t`The description of this field`}
/>
<SettingsDataModelFieldDescriptionForm />
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</FormProvider>
);
};

View File

@ -1,5 +1,4 @@
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsDataModelNewFieldBreadcrumbDropDown } from '@/settings/data-model/components/SettingsDataModelNewFieldBreadcrumbDropDown';
import { SETTINGS_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsFieldTypeConfigs';
@ -10,15 +9,15 @@ import { AppPath } from '@/types/AppPath';
import { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { zodResolver } from '@hookform/resolvers/zod';
import { t } from '@lingui/core/macro';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { useParams } from 'react-router-dom';
import { isDefined } from 'twenty-shared/utils';
import { z } from 'zod';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { useNavigateApp } from '~/hooks/useNavigateApp';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
export const settingsDataModelFieldTypeFormSchema = z.object({
type: z.enum(
@ -64,32 +63,30 @@ export const SettingsObjectNewFieldSelect = () => {
if (!activeObjectMetadataItem) return null;
return (
<RecordFieldValueSelectorContextProvider>
<FormProvider // eslint-disable-next-line react/jsx-props-no-spreading
{...formMethods}
<FormProvider // eslint-disable-next-line react/jsx-props-no-spreading
{...formMethods}
>
<SubMenuTopBarContainer
title={t`1. Select a field type`}
links={[
{ children: t`Workspace`, href: '/settings/workspace' },
{ children: t`Objects`, href: '/settings/objects' },
{
children: activeObjectMetadataItem.labelPlural,
href: getSettingsPath(SettingsPath.ObjectDetail, {
objectNamePlural,
}),
},
{ children: <SettingsDataModelNewFieldBreadcrumbDropDown /> },
]}
>
<SubMenuTopBarContainer
title={t`1. Select a field type`}
links={[
{ children: t`Workspace`, href: '/settings/workspace' },
{ children: t`Objects`, href: '/settings/objects' },
{
children: activeObjectMetadataItem.labelPlural,
href: getSettingsPath(SettingsPath.ObjectDetail, {
objectNamePlural,
}),
},
{ children: <SettingsDataModelNewFieldBreadcrumbDropDown /> },
]}
>
<SettingsPageContainer>
<SettingsObjectNewFieldSelector
objectNamePlural={objectNamePlural}
excludedFieldTypes={excludedFieldTypes}
/>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</FormProvider>
</RecordFieldValueSelectorContextProvider>
<SettingsPageContainer>
<SettingsObjectNewFieldSelector
objectNamePlural={objectNamePlural}
excludedFieldTypes={excludedFieldTypes}
/>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</FormProvider>
);
};