refactor: metadata store cleanup, SSE unification, mock metadata loading & login redirect fix (#18651)

## Summary
- **SSE unification**: Replaced 11 individual SSE effect components with
a single generic `MetadataStoreSSEEffect`
- **Metadata store cleanup**: Merged `metadataCollectionHashesState`
into `metadataStoreState` (currentCollectionHash / draftCollectionHash
per entity), moved `objectMetadataItemsSelector` to `object-metadata`
domain, converted `navigationMenuItemsState` to a derived selector
- **Naming clarity**: Renamed `isAppMetadataReadyState` →
`isMinimalMetadataReadyState`, `MetadataGater` → `MinimalMetadataGater`,
`useIsLogged` → `useHasAccessTokenPair`,
`patchMetadataStoreFromSSEEvent` now takes named object params
- **Mock metadata loading**: Added `generate-navigation-menu-items.ts`
script, rewrote `useLoadMockedMinimalMetadata` to load full
objects/fields/indexes/views/navItems from generated mock data, enabling
proper sign-in background rendering (table columns, view picker,
navigation)
- **Login/logout transitions**: `MinimalMetadataLoadEffect` manages
mocked↔real metadata transitions based on auth state,
`MainContextStoreProvider` computes context on auth pages for view
picker support
- **Login redirect fix**: `handleLoadWorkspaceAfterAuthentication` now
re-enables `isAppEffectRedirectEnabled` after `loadCurrentUser()`
completes, fixing the blocked post-login navigation
- **Dead code removal**: Deleted `useRefreshPageLayouts`,
`useApplyPageLayouts`, `useStaleMetadataEntities`,
`metadataCollectionHashesState`, and all individual SSE effects

## Test plan
- [x] Login from welcome page redirects to companies page
- [x] Logout transitions cleanly to mocked metadata on welcome page
- [x] Sign-in background shows table columns, view picker, and
navigation items
- [x] SSE events still update metadata store entries correctly
- [x] Navigation menu items persist across page refreshes
- [ ] CI: lint, typecheck, tests pass
This commit is contained in:
Charles Bochet
2026-03-16 00:38:11 +01:00
committed by GitHub
parent 06efee1eef
commit ba9aa41bba
161 changed files with 3106 additions and 4350 deletions
@@ -1,7 +1,6 @@
import { styled } from '@linaria/react';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
@@ -17,7 +16,6 @@ import { useAreViewSortsDifferentFromRecordSorts } from '@/views/hooks/useAreVie
import { useCanPersistViewChanges } from '@/views/hooks/useCanPersistViewChanges';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { useIsViewAnyFieldFilterDifferentFromCurrentAnyFieldFilter } from '@/views/hooks/useIsViewAnyFieldFilterDifferentFromCurrentAnyFieldFilter';
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { useSaveCurrentViewFiltersAndSorts } from '@/views/hooks/useSaveCurrentViewFiltersAndSorts';
import { VIEW_PICKER_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerDropdownId';
import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode';
@@ -39,11 +37,6 @@ export const UpdateViewButtonGroup = () => {
const { saveCurrentViewFilterAndSorts } = useSaveCurrentViewFiltersAndSorts();
const { canPersistChanges } = useCanPersistViewChanges();
const { refreshCoreViewsByObjectMetadataId } =
useRefreshCoreViewsByObjectMetadataId();
const { objectMetadataItem } = useRecordIndexContextOrThrow();
const { setViewPickerMode } = useViewPickerMode();
const contextStoreCurrentViewId = useAtomComponentStateValue(
@@ -83,7 +76,6 @@ export const UpdateViewButtonGroup = () => {
const handleUpdateViewClick = async () => {
if (!canPersistChanges) return;
await saveCurrentViewFilterAndSorts();
await refreshCoreViewsByObjectMetadataId(objectMetadataItem.id);
};
const { hasFiltersQueryParams } = useHasFiltersInQueryParams();
@@ -1,6 +1,6 @@
import { type ReactNode } from 'react';
import { useParams } from 'react-router-dom';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { ObjectSortDropdownButton } from '@/object-record/object-sort-dropdown/components/ObjectSortDropdownButton';
import { TopBar } from '@/ui/layout/top-bar/components/TopBar';
import { QueryParamsFiltersEffect } from '@/views/components/QueryParamsFiltersEffect';
@@ -26,19 +26,27 @@ type ViewBarProps = {
viewBarId: string;
className?: string;
optionsDropdownButton: ReactNode;
isReadOnly?: boolean;
};
export const ViewBar = ({
viewBarId,
className,
optionsDropdownButton,
isReadOnly = false,
}: ViewBarProps) => {
const { objectNamePlural } = useParams();
const { objectNamePlural } = useRecordIndexContextOrThrow();
if (!objectNamePlural) {
return;
}
if (isReadOnly) {
return (
<TopBar className={className} leftComponent={<ViewPickerDropdown />} />
);
}
return (
<ObjectSortDropdownComponentInstanceContext.Provider
value={{ instanceId: VIEW_SORT_DROPDOWN_ID }}
@@ -3,22 +3,21 @@ import { useEffect, useMemo, useState } from 'react';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { ViewBarFilterDropdown } from '@/views/components/ViewBarFilterDropdown';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState';
import { type RecordField } from '@/object-record/record-field/types/RecordField';
import { useRecordIndexFieldMetadataDerivedStates } from '@/object-record/record-index/hooks/useRecordIndexFieldMetadataDerivedStates';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { ViewBarFilterDropdownIds } from '@/views/constants/ViewBarFilterDropdownIds';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { coreViewsState } from '@/views/states/coreViewState';
import { userEvent, within } from 'storybook/test';
import { ComponentDecorator } from 'twenty-ui/testing';
import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator';
@@ -27,6 +26,7 @@ import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadat
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { mockedCoreViews } from '~/testing/mock-data/generated/metadata/views/mock-views-data';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
import { setTestCoreViewsInMetadataStore } from '~/testing/utils/setTestCoreViewsInMetadataStore';
const meta: Meta<typeof ViewBarFilterDropdown> = {
title: 'Modules/Views/ViewBarFilterDropdown',
@@ -43,8 +43,6 @@ const meta: Meta<typeof ViewBarFilterDropdown> = {
instanceId,
);
const setCoreViews = useSetAtomState(coreViewsState);
const mockCoreView = mockedCoreViews.find(
(v) => v.name === 'All Companies',
)!;
@@ -72,12 +70,11 @@ const meta: Meta<typeof ViewBarFilterDropdown> = {
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
setCoreViews([mockCoreView]);
setTestCoreViewsInMetadataStore(jotaiStore, [mockCoreView]);
setContextStoreCurrentViewId(mockCoreView.id);
setCurrentRecordFields(columns);
setIsLoaded(true);
}, [
setCoreViews,
setContextStoreCurrentViewId,
setCurrentRecordFields,
mockCoreView,
@@ -6,7 +6,6 @@ import { type RecordFilterGroup } from '@/object-record/record-filter-group/type
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { resetJotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups } from '@/views/hooks/useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups';
import { coreViewsState } from '@/views/states/coreViewState';
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
import { type View } from '@/views/types/View';
import { type ViewFilterGroup } from '@/views/types/ViewFilterGroup';
@@ -18,6 +17,7 @@ import { type CoreViewFilterGroup } from '~/generated-metadata/graphql';
import { getJestMetadataAndApolloMocksAndCommandMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndCommandMenuWrapper';
import { mockedCoreViews } from '~/testing/mock-data/generated/metadata/views/mock-views-data';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
import { setTestCoreViewsInMetadataStore } from '~/testing/utils/setTestCoreViewsInMetadataStore';
const mockObjectMetadataItemNameSingular = 'company';
@@ -94,7 +94,7 @@ describe('useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups', () => {
contextStoreCurrentObjectMetadataNameSingular:
mockObjectMetadataItemNameSingular,
onInitializeJotaiStore: (store) => {
store.set(coreViewsState.atom, [mockCoreView]);
setTestCoreViewsInMetadataStore(store, [mockCoreView]);
},
}),
},
@@ -186,7 +186,7 @@ describe('useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups', () => {
contextStoreCurrentObjectMetadataNameSingular:
mockObjectMetadataItemNameSingular,
onInitializeJotaiStore: (store) => {
store.set(coreViewsState.atom, [
setTestCoreViewsInMetadataStore(store, [
{ ...mockCoreView, viewFilterGroups: [] },
]);
store.set(
@@ -5,7 +5,7 @@ import { currentRecordFiltersComponentState } from '@/object-record/record-filte
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { resetJotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { coreViewsState } from '@/views/states/coreViewState';
import { useApplyCurrentViewFiltersToCurrentRecordFilters } from '@/views/hooks/useApplyCurrentViewFiltersToCurrentRecordFilters';
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
import { type View } from '@/views/types/View';
import { type ViewFilter } from '@/views/types/ViewFilter';
@@ -19,7 +19,7 @@ import {
import { getJestMetadataAndApolloMocksAndCommandMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndCommandMenuWrapper';
import { mockedCoreViews } from '~/testing/mock-data/generated/metadata/views/mock-views-data';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
import { useApplyCurrentViewFiltersToCurrentRecordFilters } from '@/views/hooks/useApplyCurrentViewFiltersToCurrentRecordFilters';
import { setTestCoreViewsInMetadataStore } from '~/testing/utils/setTestCoreViewsInMetadataStore';
const mockObjectMetadataItemNameSingular = 'company';
@@ -105,7 +105,7 @@ describe('useApplyCurrentViewFiltersToCurrentRecordFilters', () => {
mockObjectMetadataItemNameSingular,
contextStoreCurrentViewId: mockView.id,
onInitializeJotaiStore: (store) => {
store.set(coreViewsState.atom, [mockCoreView]);
setTestCoreViewsInMetadataStore(store, [mockCoreView]);
},
}),
},
@@ -195,7 +195,7 @@ describe('useApplyCurrentViewFiltersToCurrentRecordFilters', () => {
contextStoreCurrentObjectMetadataNameSingular:
mockObjectMetadataItemNameSingular,
onInitializeJotaiStore: (store) => {
store.set(coreViewsState.atom, [
setTestCoreViewsInMetadataStore(store, [
{ ...mockCoreView, viewFilters: [] },
]);
store.set(
@@ -6,17 +6,17 @@ import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/use
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { coreViewsState } from '@/views/states/coreViewState';
import { useApplyCurrentViewSortsToCurrentRecordSorts } from '@/views/hooks/useApplyCurrentViewSortsToCurrentRecordSorts';
import { type CoreViewSortEssential } from '@/views/types/CoreViewSortEssential';
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
import { type View } from '@/views/types/View';
import { type ViewSort } from '@/views/types/ViewSort';
import { isDefined } from 'twenty-shared/utils';
import { ViewSortDirection } from '~/generated-metadata/graphql';
import { getJestMetadataAndApolloMocksAndCommandMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndCommandMenuWrapper';
import { mockedCoreViews } from '~/testing/mock-data/generated/metadata/views/mock-views-data';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
import { useApplyCurrentViewSortsToCurrentRecordSorts } from '@/views/hooks/useApplyCurrentViewSortsToCurrentRecordSorts';
import { type ViewSort } from '@/views/types/ViewSort';
import { setTestCoreViewsInMetadataStore } from '~/testing/utils/setTestCoreViewsInMetadataStore';
const mockObjectMetadataItemNameSingular = 'company';
@@ -93,7 +93,7 @@ describe('useApplyCurrentViewSortsToCurrentRecordSorts', () => {
mockObjectMetadataItemNameSingular,
contextStoreCurrentViewId: mockView.id,
onInitializeJotaiStore: (store) => {
store.set(coreViewsState.atom, [mockCoreView]);
setTestCoreViewsInMetadataStore(store, [mockCoreView]);
},
}),
},
@@ -182,7 +182,7 @@ describe('useApplyCurrentViewSortsToCurrentRecordSorts', () => {
contextStoreCurrentObjectMetadataNameSingular:
mockObjectMetadataItemNameSingular,
onInitializeJotaiStore: (store) => {
store.set(coreViewsState.atom, [viewWithNoSorts]);
setTestCoreViewsInMetadataStore(store, [viewWithNoSorts]);
store.set(
contextStoreCurrentViewIdComponentState.atomFamily({
instanceId: 'instanceId',
@@ -38,7 +38,6 @@ export const usePerformViewAPIPersist = () => {
const newViewId = variables.input.id ?? v4();
if (variables.input.type === ViewType.KANBAN) {
triggerViewGroupOptimisticEffectAtViewCreation({
newViewId,
objectMetadataItemId: objectMetadataItemId,
mainGroupByFieldMetadataId:
variables.input.mainGroupByFieldMetadataId,
@@ -3,15 +3,11 @@ import { useCallback } from 'react';
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { coreViewsState } from '@/views/states/coreViewState';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { t } from '@lingui/core/macro';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { CrudOperationType } from 'twenty-shared/types';
import { upsertPropertiesOfItemIntoArrayOfObjectsComparingId } from 'twenty-shared/utils';
import { useMutation } from '@apollo/client/react';
import {
type CoreView,
type UpdateCoreViewMutationVariables,
UpdateCoreViewDocument,
} from '~/generated-metadata/graphql';
@@ -22,8 +18,6 @@ export const usePerformViewAPIUpdate = () => {
const { handleMetadataError } = useMetadataErrorHandler();
const { enqueueErrorSnackBar } = useSnackBar();
const setCoreViews = useSetAtomState(coreViewsState);
const performViewAPIUpdate = useCallback(
async (
variables: UpdateCoreViewMutationVariables,
@@ -31,30 +25,10 @@ export const usePerformViewAPIUpdate = () => {
MetadataRequestResult<Awaited<ReturnType<typeof updateCoreViewMutation>>>
> => {
try {
setCoreViews((currentCoreViews) =>
upsertPropertiesOfItemIntoArrayOfObjectsComparingId(
currentCoreViews,
{
...variables.input,
id: variables.id,
} as CoreView,
),
);
const result = await updateCoreViewMutation({
variables,
});
setCoreViews((currentCoreViews) =>
upsertPropertiesOfItemIntoArrayOfObjectsComparingId(
currentCoreViews,
{
...result.data?.updateCoreView,
id: variables.id,
} as CoreView,
),
);
return {
status: 'successful',
response: result,
@@ -75,12 +49,7 @@ export const usePerformViewAPIUpdate = () => {
};
}
},
[
setCoreViews,
updateCoreViewMutation,
handleMetadataError,
enqueueErrorSnackBar,
],
[updateCoreViewMutation, handleMetadataError, enqueueErrorSnackBar],
);
return { performViewAPIUpdate };
@@ -3,11 +3,9 @@ import { useCallback } from 'react';
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useTriggerViewFieldOptimisticEffect } from '@/views/optimistic-effects/hooks/useTriggerViewFieldOptimisticEffect';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { t } from '@lingui/core/macro';
import { CrudOperationType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { useMutation } from '@apollo/client/react';
import {
type CreateManyCoreViewFieldsMutationVariables,
@@ -21,9 +19,6 @@ import {
} from '~/generated-metadata/graphql';
export const usePerformViewFieldAPIPersist = () => {
const { triggerViewFieldOptimisticEffect } =
useTriggerViewFieldOptimisticEffect();
const [createManyCoreViewFieldsMutation] = useMutation(
CreateManyCoreViewFieldsDocument,
);
@@ -61,16 +56,6 @@ export const usePerformViewFieldAPIPersist = () => {
try {
const result = await createManyCoreViewFieldsMutation({
variables: createCoreViewFieldInputs,
update: (_cache, { data }) => {
const createdViewFields = data?.createManyCoreViewFields;
if (!isDefined(createdViewFields)) {
return;
}
triggerViewFieldOptimisticEffect({
createdViewFields,
});
},
});
return {
@@ -94,7 +79,6 @@ export const usePerformViewFieldAPIPersist = () => {
}
},
[
triggerViewFieldOptimisticEffect,
createManyCoreViewFieldsMutation,
handleMetadataError,
enqueueErrorSnackBar,
@@ -121,16 +105,6 @@ export const usePerformViewFieldAPIPersist = () => {
createCoreViewFieldInputs.map((variables) =>
updateCoreViewFieldMutation({
variables,
update: (_cache, { data }) => {
const updatedViewField = data?.updateCoreViewField;
if (!isDefined(updatedViewField)) {
return;
}
triggerViewFieldOptimisticEffect({
updatedViewFields: [updatedViewField],
});
},
}),
),
);
@@ -155,12 +129,7 @@ export const usePerformViewFieldAPIPersist = () => {
};
}
},
[
triggerViewFieldOptimisticEffect,
updateCoreViewFieldMutation,
handleMetadataError,
enqueueErrorSnackBar,
],
[updateCoreViewFieldMutation, handleMetadataError, enqueueErrorSnackBar],
);
const performViewFieldAPIDelete = useCallback(
@@ -183,16 +152,6 @@ export const usePerformViewFieldAPIPersist = () => {
deleteCoreViewFieldInputs.map((variables) =>
deleteCoreViewFieldMutation({
variables,
update: (_cache, { data }) => {
const deletedViewField = data?.deleteCoreViewField;
if (!isDefined(deletedViewField)) {
return;
}
triggerViewFieldOptimisticEffect({
deletedViewFields: [deletedViewField],
});
},
}),
),
);
@@ -217,12 +176,7 @@ export const usePerformViewFieldAPIPersist = () => {
};
}
},
[
triggerViewFieldOptimisticEffect,
deleteCoreViewFieldMutation,
handleMetadataError,
enqueueErrorSnackBar,
],
[deleteCoreViewFieldMutation, handleMetadataError, enqueueErrorSnackBar],
);
const performViewFieldAPIDestroy = useCallback(
@@ -6,7 +6,6 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { CREATE_MANY_CORE_VIEW_FIELD_GROUPS } from '@/views/graphql/mutations/createManyCoreViewFieldGroups';
import { DELETE_CORE_VIEW_FIELD_GROUP } from '@/views/graphql/mutations/deleteCoreViewFieldGroup';
import { UPDATE_CORE_VIEW_FIELD_GROUP } from '@/views/graphql/mutations/updateCoreViewFieldGroup';
import { useTriggerViewFieldGroupOptimisticEffect } from '@/views/optimistic-effects/hooks/useTriggerViewFieldGroupOptimisticEffect';
import { useMutation } from '@apollo/client/react';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { t } from '@lingui/core/macro';
@@ -32,9 +31,6 @@ type DeleteCoreViewFieldGroupMutationResult = {
};
export const usePerformViewFieldGroupAPIPersist = () => {
const { triggerViewFieldGroupOptimisticEffect } =
useTriggerViewFieldGroupOptimisticEffect();
const [createManyCoreViewFieldGroupsMutation] = useMutation<
CreateManyCoreViewFieldGroupsMutationResult,
MutationCreateManyCoreViewFieldGroupsArgs
@@ -70,16 +66,6 @@ export const usePerformViewFieldGroupAPIPersist = () => {
try {
const result = await createManyCoreViewFieldGroupsMutation({
variables: createCoreViewFieldGroupInputs,
update: (_cache, { data }) => {
const createdViewFieldGroups = data?.createManyCoreViewFieldGroups;
if (!isDefined(createdViewFieldGroups)) {
return;
}
triggerViewFieldGroupOptimisticEffect({
createdViewFieldGroups,
});
},
});
return {
@@ -103,7 +89,6 @@ export const usePerformViewFieldGroupAPIPersist = () => {
}
},
[
triggerViewFieldGroupOptimisticEffect,
createManyCoreViewFieldGroupsMutation,
handleMetadataError,
enqueueErrorSnackBar,
@@ -128,16 +113,6 @@ export const usePerformViewFieldGroupAPIPersist = () => {
updateCoreViewFieldGroupInputs.map((variables) =>
updateCoreViewFieldGroupMutation({
variables,
update: (_cache, { data }) => {
const updatedViewFieldGroup = data?.updateCoreViewFieldGroup;
if (!isDefined(updatedViewFieldGroup)) {
return;
}
triggerViewFieldGroupOptimisticEffect({
updatedViewFieldGroups: [updatedViewFieldGroup],
});
},
}),
),
);
@@ -165,7 +140,6 @@ export const usePerformViewFieldGroupAPIPersist = () => {
}
},
[
triggerViewFieldGroupOptimisticEffect,
updateCoreViewFieldGroupMutation,
handleMetadataError,
enqueueErrorSnackBar,
@@ -190,16 +164,6 @@ export const usePerformViewFieldGroupAPIPersist = () => {
deleteCoreViewFieldGroupInputs.map((variables) =>
deleteCoreViewFieldGroupMutation({
variables,
update: (_cache, { data }) => {
const deletedViewFieldGroup = data?.deleteCoreViewFieldGroup;
if (!isDefined(deletedViewFieldGroup)) {
return;
}
triggerViewFieldGroupOptimisticEffect({
deletedViewFieldGroups: [deletedViewFieldGroup],
});
},
}),
),
);
@@ -227,7 +191,6 @@ export const usePerformViewFieldGroupAPIPersist = () => {
}
},
[
triggerViewFieldGroupOptimisticEffect,
deleteCoreViewFieldGroupMutation,
handleMetadataError,
enqueueErrorSnackBar,
@@ -3,11 +3,9 @@ import { useCallback } from 'react';
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useTriggerViewFilterOptimisticEffect } from '@/views/optimistic-effects/hooks/useTriggerViewFilterOptimisticEffect';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { t } from '@lingui/core/macro';
import { CrudOperationType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { useMutation } from '@apollo/client/react';
import {
type CreateCoreViewFilterMutationVariables,
@@ -21,8 +19,6 @@ import {
} from '~/generated-metadata/graphql';
export const usePerformViewFilterAPIPersist = () => {
const { triggerViewFilterOptimisticEffect } =
useTriggerViewFilterOptimisticEffect();
const [createCoreViewFilterMutation] = useMutation(
CreateCoreViewFilterDocument,
);
@@ -59,16 +55,6 @@ export const usePerformViewFilterAPIPersist = () => {
createCoreViewFilterInputs.map((variables) =>
createCoreViewFilterMutation({
variables,
update: (_cache, { data }) => {
const createdViewFilter = data?.createCoreViewFilter;
if (!isDefined(createdViewFilter)) {
return;
}
triggerViewFilterOptimisticEffect({
createdViewFilters: [createdViewFilter],
});
},
}),
),
);
@@ -93,12 +79,7 @@ export const usePerformViewFilterAPIPersist = () => {
};
}
},
[
triggerViewFilterOptimisticEffect,
createCoreViewFilterMutation,
handleMetadataError,
enqueueErrorSnackBar,
],
[createCoreViewFilterMutation, handleMetadataError, enqueueErrorSnackBar],
);
const performViewFilterAPIUpdate = useCallback(
@@ -121,16 +102,6 @@ export const usePerformViewFilterAPIPersist = () => {
updateCoreViewFilterInputs.map((variables) =>
updateCoreViewFilterMutation({
variables,
update: (_cache, { data }) => {
const updatedViewFilter = data?.updateCoreViewFilter;
if (!isDefined(updatedViewFilter)) {
return;
}
triggerViewFilterOptimisticEffect({
updatedViewFilters: [updatedViewFilter],
});
},
}),
),
);
@@ -155,12 +126,7 @@ export const usePerformViewFilterAPIPersist = () => {
};
}
},
[
triggerViewFilterOptimisticEffect,
updateCoreViewFilterMutation,
handleMetadataError,
enqueueErrorSnackBar,
],
[updateCoreViewFilterMutation, handleMetadataError, enqueueErrorSnackBar],
);
const performViewFilterAPIDelete = useCallback(
@@ -183,16 +149,6 @@ export const usePerformViewFilterAPIPersist = () => {
deleteCoreViewFilterInputs.map((variables) =>
deleteCoreViewFilterMutation({
variables,
update: (_cache, { data }) => {
const deletedViewFilter = data?.deleteCoreViewFilter;
if (!isDefined(deletedViewFilter)) {
return;
}
triggerViewFilterOptimisticEffect({
deletedViewFilters: [deletedViewFilter],
});
},
}),
),
);
@@ -217,12 +173,7 @@ export const usePerformViewFilterAPIPersist = () => {
};
}
},
[
triggerViewFilterOptimisticEffect,
deleteCoreViewFilterMutation,
handleMetadataError,
enqueueErrorSnackBar,
],
[deleteCoreViewFilterMutation, handleMetadataError, enqueueErrorSnackBar],
);
const performViewFilterAPIDestroy = useCallback(
@@ -3,11 +3,9 @@ import { useCallback } from 'react';
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useTriggerViewGroupOptimisticEffect } from '@/views/optimistic-effects/hooks/useTriggerViewGroupOptimisticEffect';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { t } from '@lingui/core/macro';
import { CrudOperationType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { useMutation } from '@apollo/client/react';
import {
type UpdateCoreViewGroupMutationVariables,
@@ -15,9 +13,6 @@ import {
} from '~/generated-metadata/graphql';
export const usePerformViewGroupAPIPersist = () => {
const { triggerViewGroupOptimisticEffect } =
useTriggerViewGroupOptimisticEffect();
const [updateCoreViewGroupMutation] = useMutation(
UpdateCoreViewGroupDocument,
);
@@ -45,16 +40,6 @@ export const usePerformViewGroupAPIPersist = () => {
updateCoreViewGroupInputs.map((variables) =>
updateCoreViewGroupMutation({
variables,
update: (_cache, { data }) => {
const updatedViewGroup = data?.updateCoreViewGroup;
if (!isDefined(updatedViewGroup)) {
return;
}
triggerViewGroupOptimisticEffect({
updatedViewGroups: [updatedViewGroup],
});
},
}),
),
);
@@ -79,12 +64,7 @@ export const usePerformViewGroupAPIPersist = () => {
};
}
},
[
triggerViewGroupOptimisticEffect,
updateCoreViewGroupMutation,
handleMetadataError,
enqueueErrorSnackBar,
],
[updateCoreViewGroupMutation, handleMetadataError, enqueueErrorSnackBar],
);
return {
@@ -3,11 +3,9 @@ import { useCallback } from 'react';
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useTriggerViewSortOptimisticEffect } from '@/views/optimistic-effects/hooks/useTriggerViewSortOptimisticEffect';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { t } from '@lingui/core/macro';
import { CrudOperationType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { useMutation } from '@apollo/client/react';
import {
type CreateCoreViewSortMutationVariables,
@@ -21,8 +19,6 @@ import {
} from '~/generated-metadata/graphql';
export const usePerformViewSortAPIPersist = () => {
const { triggerViewSortOptimisticEffect } =
useTriggerViewSortOptimisticEffect();
const [createCoreViewSortMutation] = useMutation(CreateCoreViewSortDocument);
const [updateCoreViewSortMutation] = useMutation(UpdateCoreViewSortDocument);
const [deleteCoreViewSortMutation] = useMutation(DeleteCoreViewSortDocument);
@@ -53,16 +49,6 @@ export const usePerformViewSortAPIPersist = () => {
createCoreViewSortInputs.map((variables) =>
createCoreViewSortMutation({
variables,
update: (_cache, { data }) => {
const createdViewSort = data?.createCoreViewSort;
if (!isDefined(createdViewSort)) {
return;
}
triggerViewSortOptimisticEffect({
createdViewSorts: [createdViewSort],
});
},
}),
),
);
@@ -86,12 +72,7 @@ export const usePerformViewSortAPIPersist = () => {
};
}
},
[
triggerViewSortOptimisticEffect,
createCoreViewSortMutation,
handleMetadataError,
enqueueErrorSnackBar,
],
[createCoreViewSortMutation, handleMetadataError, enqueueErrorSnackBar],
);
const performViewSortAPIUpdate = useCallback(
@@ -114,16 +95,6 @@ export const usePerformViewSortAPIPersist = () => {
updateCoreViewSortInputs.map((variables) =>
updateCoreViewSortMutation({
variables,
update: (_cache, { data }) => {
const updatedViewSort = data?.updateCoreViewSort;
if (!isDefined(updatedViewSort)) {
return;
}
triggerViewSortOptimisticEffect({
updatedViewSorts: [updatedViewSort],
});
},
}),
),
);
@@ -148,12 +119,7 @@ export const usePerformViewSortAPIPersist = () => {
};
}
},
[
triggerViewSortOptimisticEffect,
updateCoreViewSortMutation,
handleMetadataError,
enqueueErrorSnackBar,
],
[updateCoreViewSortMutation, handleMetadataError, enqueueErrorSnackBar],
);
const performViewSortAPIDelete = useCallback(
@@ -176,21 +142,6 @@ export const usePerformViewSortAPIPersist = () => {
deleteCoreViewSortInputs.map((variables) =>
deleteCoreViewSortMutation({
variables,
update: (_cache, { data }) => {
const deletedViewSort = data?.deleteCoreViewSort;
if (!isDefined(deletedViewSort)) {
return;
}
triggerViewSortOptimisticEffect({
deletedViewSorts: [
{
id: variables.input.id,
},
],
});
},
}),
),
);
@@ -215,12 +166,7 @@ export const usePerformViewSortAPIPersist = () => {
};
}
},
[
triggerViewSortOptimisticEffect,
deleteCoreViewSortMutation,
handleMetadataError,
enqueueErrorSnackBar,
],
[deleteCoreViewSortMutation, handleMetadataError, enqueueErrorSnackBar],
);
const performViewSortAPIDestroy = useCallback(
@@ -1,173 +0,0 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState';
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { recordIndexShouldHideEmptyRecordGroupsComponentState } from '@/object-record/record-index/states/recordIndexShouldHideEmptyRecordGroupsComponentState';
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { coreViewsByObjectMetadataIdFamilySelector } from '@/views/states/selectors/coreViewsByObjectMetadataIdFamilySelector';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
import { getFilterableFields } from '@/views/utils/getFilterableFields';
import { mapViewFieldToRecordField } from '@/views/utils/mapViewFieldToRecordField';
import { mapViewFilterGroupsToRecordFilterGroups } from '@/views/utils/mapViewFilterGroupsToRecordFilterGroups';
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
import { useCallback } from 'react';
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
import { type FindManyCoreViewsQuery } from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useApplyCoreViewsForObjectMetadataId = () => {
const applyCoreViewsForObjectMetadataId = useCallback(
(
objectMetadataId: string,
coreViewsFromResult: FindManyCoreViewsQuery['getCoreViews'],
) => {
const objectMetadataItems = jotaiStore.get(objectMetadataItemsState.atom);
const objectMetadataItem = objectMetadataItems.find(
(item) => item.id === objectMetadataId,
);
if (!isDefined(objectMetadataItem)) {
return;
}
const coreViewsForObjectMetadataId = jotaiStore.get(
coreViewsByObjectMetadataIdFamilySelector.selectorFamily(
objectMetadataId,
),
);
if (isDeeplyEqual(coreViewsForObjectMetadataId, coreViewsFromResult)) {
return;
}
jotaiStore.set(
coreViewsByObjectMetadataIdFamilySelector.selectorFamily(
objectMetadataId,
),
coreViewsFromResult,
);
for (const coreView of coreViewsFromResult) {
const existingView = coreViewsForObjectMetadataId.find(
(coreViewForObjectMetadata) =>
coreViewForObjectMetadata.id === coreView.id,
);
if (!isDefined(existingView)) {
continue;
}
if (
!isDeeplyEqual(
coreView.viewFields.map((viewField) =>
removePropertiesFromRecord(viewField, ['updatedAt', 'createdAt']),
),
existingView.viewFields,
)
) {
const view = convertCoreViewToView(coreView);
jotaiStore.set(
currentRecordFieldsComponentState.atomFamily({
instanceId: getRecordIndexIdFromObjectNamePluralAndViewId(
objectMetadataItem.namePlural,
view.id,
),
}),
view.viewFields
.filter(isDefined)
.map((viewField) => mapViewFieldToRecordField(viewField)),
);
}
if (
!isDeeplyEqual(
coreView.viewFilters.map((viewFilter) =>
removePropertiesFromRecord(viewFilter, [
'createdAt',
'updatedAt',
]),
),
existingView.viewFilters,
)
) {
const view = convertCoreViewToView(coreView);
jotaiStore.set(
currentRecordFiltersComponentState.atomFamily({
instanceId: getRecordIndexIdFromObjectNamePluralAndViewId(
objectMetadataItem.namePlural,
view.id,
),
}),
mapViewFiltersToFilters(
view.viewFilters,
getFilterableFields(objectMetadataItem),
),
);
}
if (
!isDeeplyEqual(
coreView.viewFilterGroups,
existingView.viewFilterGroups,
)
) {
const view = convertCoreViewToView(coreView);
jotaiStore.set(
currentRecordFilterGroupsComponentState.atomFamily({
instanceId: getRecordIndexIdFromObjectNamePluralAndViewId(
objectMetadataItem.namePlural,
view.id,
),
}),
mapViewFilterGroupsToRecordFilterGroups(
view.viewFilterGroups ?? [],
),
);
}
if (
!isDeeplyEqual(
coreView.viewSorts.map((viewSort) =>
removePropertiesFromRecord(viewSort, ['createdAt', 'updatedAt']),
),
existingView.viewSorts,
)
) {
const view = convertCoreViewToView(coreView);
jotaiStore.set(
currentRecordSortsComponentState.atomFamily({
instanceId: getRecordIndexIdFromObjectNamePluralAndViewId(
objectMetadataItem.namePlural,
view.id,
),
}),
view.viewSorts,
);
}
if (
coreView.shouldHideEmptyGroups !== existingView.shouldHideEmptyGroups
) {
const view = convertCoreViewToView(coreView);
jotaiStore.set(
recordIndexShouldHideEmptyRecordGroupsComponentState.atomFamily({
instanceId: getRecordIndexIdFromObjectNamePluralAndViewId(
objectMetadataItem.namePlural,
view.id,
),
}),
view.shouldHideEmptyGroups,
);
}
}
},
[],
);
return { applyCoreViewsForObjectMetadataId };
};
@@ -2,7 +2,6 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataI
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist';
import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist';
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { useCallback } from 'react';
import { v4 } from 'uuid';
import { ViewType } from '~/generated-metadata/graphql';
@@ -16,8 +15,6 @@ const pendingViewCreations = new Set<string>();
export const useCreateDefaultViewForObject = () => {
const { performViewAPICreate } = usePerformViewAPIPersist();
const { performViewFieldAPICreate } = usePerformViewFieldAPIPersist();
const { refreshCoreViewsByObjectMetadataId } =
useRefreshCoreViewsByObjectMetadataId();
const createDefaultViewForObject = useCallback(
async (objectMetadataItem: ObjectMetadataItem) => {
@@ -76,17 +73,11 @@ export const useCreateDefaultViewForObject = () => {
}));
await performViewFieldAPICreate({ inputs: viewFieldInputs });
await refreshCoreViewsByObjectMetadataId(objectMetadataItem.id);
} finally {
pendingViewCreations.delete(objectMetadataItem.id);
}
},
[
performViewAPICreate,
performViewFieldAPICreate,
refreshCoreViewsByObjectMetadataId,
],
[performViewAPICreate, performViewFieldAPICreate],
);
return {
@@ -11,7 +11,6 @@ import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerform
import { usePerformViewFilterAPIPersist } from '@/views/hooks/internal/usePerformViewFilterAPIPersist';
import { usePerformViewFilterGroupAPIPersist } from '@/views/hooks/internal/usePerformViewFilterGroupAPIPersist';
import { usePerformViewSortAPIPersist } from '@/views/hooks/internal/usePerformViewSortAPIPersist';
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector';
import { type GraphQLView } from '@/views/types/GraphQLView';
import { ViewType } from '@/views/types/ViewType';
@@ -54,9 +53,6 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
const store = useStore();
const { refreshCoreViewsByObjectMetadataId } =
useRefreshCoreViewsByObjectMetadataId();
const currentRecordFilterGroups = useAtomComponentStateValue(
currentRecordFilterGroupsComponentState,
recordIndexId,
@@ -246,8 +242,6 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
}
}
await refreshCoreViewsByObjectMetadataId(objectMetadataItem.id);
return newViewId;
},
[
@@ -256,7 +250,6 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
anyFieldFilterValue,
objectMetadataItem,
performViewFieldAPICreate,
refreshCoreViewsByObjectMetadataId,
store,
currentRecordFilterGroups,
currentRecordFilters,
@@ -1,41 +0,0 @@
import { FIND_ALL_CORE_VIEWS } from '@/views/graphql/queries/findAllCoreViews';
import { coreViewsState } from '@/views/states/coreViewState';
import { type FetchPolicy } from '@apollo/client';
import { useApolloClient } from '@apollo/client/react';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { type FindAllCoreViewsQuery } from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { useStore } from 'jotai';
export const useRefreshAllCoreViews = (
fetchPolicy: FetchPolicy = 'network-only',
) => {
const store = useStore();
const client = useApolloClient();
const refreshAllCoreViews = useCallback(async () => {
const result = await client.query<FindAllCoreViewsQuery>({
query: FIND_ALL_CORE_VIEWS,
variables: {},
fetchPolicy,
});
const currentCoreViews = store.get(coreViewsState.atom);
const coreViewsFromResult = result.data?.getCoreViews;
if (
isDefined(coreViewsFromResult) &&
!isDeeplyEqual(currentCoreViews, coreViewsFromResult)
) {
store.set(coreViewsState.atom, coreViewsFromResult);
}
return result.data?.getCoreViews;
}, [client, fetchPolicy, store]);
return {
refreshAllCoreViews,
};
};
@@ -1,37 +0,0 @@
import { useApplyCoreViewsForObjectMetadataId } from '@/views/hooks/useApplyCoreViewsForObjectMetadataId';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useApolloClient } from '@apollo/client/react';
import { FindManyCoreViewsDocument } from '~/generated-metadata/graphql';
export const useRefreshCoreViewsByObjectMetadataId = () => {
const client = useApolloClient();
const { applyCoreViewsForObjectMetadataId } =
useApplyCoreViewsForObjectMetadataId();
const refreshCoreViewsByObjectMetadataId = useCallback(
async (objectMetadataId: string) => {
const result = await client.query({
query: FindManyCoreViewsDocument,
variables: {
objectMetadataId,
},
fetchPolicy: 'network-only',
});
if (!isDefined(result.data?.getCoreViews)) {
return;
}
applyCoreViewsForObjectMetadataId(
objectMetadataId,
result.data.getCoreViews,
);
},
[client, applyCoreViewsForObjectMetadataId],
);
return {
refreshCoreViewsByObjectMetadataId,
};
};
@@ -1,10 +1,8 @@
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { usePerformViewFilterGroupAPIPersist } from '@/views/hooks/internal/usePerformViewFilterGroupAPIPersist';
import { useCanPersistViewChanges } from '@/views/hooks/useCanPersistViewChanges';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { coreViewsState } from '@/views/states/coreViewState';
import { getViewFilterGroupsToCreate } from '@/views/utils/getViewFilterGroupsToCreate';
import { getViewFilterGroupsToDelete } from '@/views/utils/getViewFilterGroupsToDelete';
import { getViewFilterGroupsToUpdate } from '@/views/utils/getViewFilterGroupsToUpdate';
@@ -28,8 +26,6 @@ export const useSaveRecordFilterGroupsToViewFilterGroups = () => {
const store = useStore();
const setCoreViews = useSetAtomState(coreViewsState);
const saveRecordFilterGroupsToViewFilterGroups = useCallback(async () => {
if (!canPersistChanges || !isDefined(currentView)) {
return;
@@ -59,33 +55,6 @@ export const useSaveRecordFilterGroupsToViewFilterGroups = () => {
newViewFilterGroups,
);
const viewFiltersToOptimisticallyCascadeDelete =
currentView.viewFilters.filter((viewFilter) =>
viewFilterGroupsToDelete.some(
(viewFilterGroupToDelete) =>
viewFilterGroupToDelete.id === viewFilter.viewFilterGroupId,
),
);
for (const viewFilterToCascadeDelete of viewFiltersToOptimisticallyCascadeDelete) {
setCoreViews((currentCoreViews) => {
const updatedCoreViews = currentCoreViews.map((coreView) => {
if (coreView.id !== currentView.id) {
return coreView;
}
return {
...coreView,
viewFilters: coreView.viewFilters.filter(
(viewFilter) => viewFilter.id !== viewFilterToCascadeDelete.id,
),
};
});
return updatedCoreViews;
});
}
const viewFilterGroupsToUpdate = getViewFilterGroupsToUpdate(
currentViewFilterGroups,
newViewFilterGroups,
@@ -109,7 +78,6 @@ export const useSaveRecordFilterGroupsToViewFilterGroups = () => {
performViewFilterGroupAPICreate,
performViewFilterGroupAPIUpdate,
performViewFilterGroupAPIDelete,
setCoreViews,
]);
return {
@@ -4,7 +4,7 @@ import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/h
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { usePerformViewFilterAPIPersist } from '@/views/hooks/internal/usePerformViewFilterAPIPersist';
import { useCanPersistViewChanges } from '@/views/hooks/useCanPersistViewChanges';
import { coreViewsState } from '@/views/states/coreViewState';
import { coreViewsSelector } from '@/views/states/selectors/coreViewsSelector';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
import { getViewFiltersToCreate } from '@/views/utils/getViewFiltersToCreate';
import { getViewFiltersToDelete } from '@/views/utils/getViewFiltersToDelete';
@@ -33,7 +33,7 @@ export const useSaveRecordFiltersToViewFilters = () => {
const store = useStore();
const saveRecordFiltersToViewFilters = useCallback(async () => {
const views = store.get(coreViewsState.atom);
const views = store.get(coreViewsSelector.atom);
const currentCoreView = views.find(
(view) => view.id === contextStoreCurrentViewId,
@@ -7,7 +7,6 @@ import { useLoadRecordIndexStates } from '@/object-record/record-index/hooks/use
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useCanPersistViewChanges } from '@/views/hooks/useCanPersistViewChanges';
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector';
import { type GraphQLView } from '@/views/types/GraphQLView';
import { type View } from '@/views/types/View';
@@ -32,8 +31,6 @@ export const useUpdateCurrentView = () => {
const store = useStore();
const [updateOneCoreView] = useMutation(UpdateCoreViewDocument);
const { refreshCoreViewsByObjectMetadataId } =
useRefreshCoreViewsByObjectMetadataId();
const getViewGroupsToCreateAtViewUpdate = useMemo(() => {
return ({
@@ -143,8 +140,6 @@ export const useUpdateCurrentView = () => {
if (isDefined(view.type)) {
setRecordIndexViewType(view.type);
}
await refreshCoreViewsByObjectMetadataId(currentView.objectMetadataId);
}
},
[
@@ -153,7 +148,6 @@ export const useUpdateCurrentView = () => {
getViewGroupsToCreateAtViewUpdate,
loadRecordIndexStates,
objectMetadataItem,
refreshCoreViewsByObjectMetadataId,
setRecordIndexViewType,
store,
updateOneCoreView,
@@ -6,18 +6,12 @@ import { convertExtendedAggregateOperationToAggregateOperation } from '@/object-
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { usePerformViewAPIUpdate } from '@/views/hooks/internal/usePerformViewAPIUpdate';
import { useCanPersistViewChanges } from '@/views/hooks/useCanPersistViewChanges';
import { coreViewsState } from '@/views/states/coreViewState';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
import { useCallback } from 'react';
import {
isDefined,
upsertIntoArrayOfObjectsComparingId,
} from 'twenty-shared/utils';
import { isDefined } from 'twenty-shared/utils';
import { type CoreView } from '~/generated-metadata/graphql';
import { useStore } from 'jotai';
export const useUpdateViewAggregate = () => {
const store = useStore();
const { canPersistChanges } = useCanPersistViewChanges();
const contextStoreCurrentViewId = useAtomComponentStateValue(
contextStoreCurrentViewIdComponentState,
@@ -67,13 +61,6 @@ export const useUpdateViewAggregate = () => {
return;
}
store.set(coreViewsState.atom, (currentCoreViews) =>
upsertIntoArrayOfObjectsComparingId(
currentCoreViews,
updatedCoreView,
),
);
const updatedView = convertCoreViewToView(updatedCoreView);
loadRecordIndexStates(updatedView, objectMetadataItem);
@@ -84,7 +71,6 @@ export const useUpdateViewAggregate = () => {
contextStoreCurrentViewId,
performViewAPIUpdate,
loadRecordIndexStates,
store,
],
);
@@ -1,23 +1,16 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useTriggerViewGroupOptimisticEffect } from '@/views/optimistic-effects/hooks/useTriggerViewGroupOptimisticEffect';
import { type ViewGroup } from '@/views/types/ViewGroup';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { type CoreViewGroup } from '~/generated-metadata/graphql';
const useViewsSideEffectsOnViewGroups = () => {
const { triggerViewGroupOptimisticEffect } =
useTriggerViewGroupOptimisticEffect();
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const triggerViewGroupOptimisticEffectAtViewCreation = ({
newViewId,
mainGroupByFieldMetadataId,
objectMetadataItemId,
}: {
newViewId: string;
mainGroupByFieldMetadataId?: string | null;
objectMetadataItemId: string;
}) => {
@@ -63,19 +56,6 @@ const useViewsSideEffectsOnViewGroups = () => {
} satisfies ViewGroup);
}
triggerViewGroupOptimisticEffect({
createdViewGroups: viewGroupsToCreate.map(
({ __typename, ...viewGroup }) =>
({
...viewGroup,
viewId: newViewId,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
}) as Omit<CoreViewGroup, 'workspaceId'>,
),
});
return { viewGroupsToCreate };
};
@@ -1,163 +0,0 @@
import { coreViewsState } from '@/views/states/coreViewState';
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
import { useApolloClient } from '@apollo/client/react';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { type CoreViewFieldGroup } from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { useStore } from 'jotai';
type ViewFieldGroupForOptimisticEffect = Omit<
CoreViewFieldGroup,
'workspaceId'
>;
export const useTriggerViewFieldGroupOptimisticEffect = () => {
const store = useStore();
const apolloClient = useApolloClient();
const cache = apolloClient.cache;
const triggerViewFieldGroupOptimisticEffect = useCallback(
({
createdViewFieldGroups = [],
updatedViewFieldGroups = [],
deletedViewFieldGroups = [],
}: {
createdViewFieldGroups?: ViewFieldGroupForOptimisticEffect[];
updatedViewFieldGroups?: ViewFieldGroupForOptimisticEffect[];
deletedViewFieldGroups?: Pick<CoreViewFieldGroup, 'id' | 'viewId'>[];
}) => {
const coreViews = store.get(coreViewsState.atom);
let newCoreViews = [...coreViews];
createdViewFieldGroups.forEach((createdViewFieldGroup) => {
cache.modify<CoreViewWithRelations>({
id: cache.identify({
__typename: 'CoreView',
id: createdViewFieldGroup.viewId,
}),
fields: {
viewFieldGroups: (existingViewFieldGroups, { toReference }) =>
[
...(existingViewFieldGroups ?? []),
toReference(createdViewFieldGroup),
].filter(isDefined),
},
});
const toBeModifiedCoreView = newCoreViews.find(
(coreView) => coreView.id === createdViewFieldGroup.viewId,
);
if (isDefined(toBeModifiedCoreView)) {
newCoreViews = [
...newCoreViews.filter(
(coreView) => coreView.id !== createdViewFieldGroup.viewId,
),
{
...toBeModifiedCoreView,
viewFieldGroups: [
...(toBeModifiedCoreView.viewFieldGroups ?? []),
createdViewFieldGroup,
],
},
];
}
});
updatedViewFieldGroups.forEach((updatedViewFieldGroup) => {
cache.modify<CoreViewWithRelations>({
id: cache.identify({
__typename: 'CoreView',
id: updatedViewFieldGroup.viewId,
}),
fields: {
viewFieldGroups: (
existingViewFieldGroups,
{ readField, toReference },
) =>
existingViewFieldGroups
?.map((viewFieldGroup) => {
const viewFieldGroupId = readField<string>(
'id',
viewFieldGroup,
);
if (viewFieldGroupId === updatedViewFieldGroup.id) {
return toReference(updatedViewFieldGroup);
}
return viewFieldGroup;
})
?.filter(isDefined) ?? [],
},
});
const toBeModifiedCoreView = newCoreViews.find(
(coreView) => coreView.id === updatedViewFieldGroup.viewId,
);
if (isDefined(toBeModifiedCoreView)) {
newCoreViews = [
...newCoreViews.filter(
(coreView) => coreView.id !== updatedViewFieldGroup.viewId,
),
{
...toBeModifiedCoreView,
viewFieldGroups: [
...(toBeModifiedCoreView.viewFieldGroups ?? []).filter(
(viewFieldGroup) =>
viewFieldGroup.id !== updatedViewFieldGroup.id,
),
updatedViewFieldGroup,
],
},
];
}
});
deletedViewFieldGroups.forEach(
(deletedViewFieldGroup: Pick<CoreViewFieldGroup, 'id' | 'viewId'>) => {
cache.modify<CoreViewWithRelations>({
id: cache.identify({
__typename: 'CoreView',
id: deletedViewFieldGroup.viewId,
}),
fields: {
viewFieldGroups: (existingViewFieldGroups, { readField }) =>
existingViewFieldGroups?.filter(
(viewFieldGroup) =>
readField('id', viewFieldGroup) !==
deletedViewFieldGroup.id,
) ?? [],
},
});
const toBeModifiedCoreView = newCoreViews.find(
(coreView) => coreView.id === deletedViewFieldGroup.viewId,
);
if (isDefined(toBeModifiedCoreView)) {
newCoreViews = [
...newCoreViews.filter(
(coreView) => coreView.id !== deletedViewFieldGroup.viewId,
),
{
...toBeModifiedCoreView,
viewFieldGroups: (
toBeModifiedCoreView.viewFieldGroups ?? []
).filter(
(viewFieldGroup) =>
viewFieldGroup.id !== deletedViewFieldGroup.id,
),
},
];
}
},
);
if (!isDeeplyEqual(coreViews, newCoreViews)) {
store.set(coreViewsState.atom, newCoreViews);
}
},
[cache, store],
);
return {
triggerViewFieldGroupOptimisticEffect,
};
};
@@ -1,147 +0,0 @@
import { coreViewsState } from '@/views/states/coreViewState';
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
import { useApolloClient } from '@apollo/client/react';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { type CoreViewField } from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { useStore } from 'jotai';
export const useTriggerViewFieldOptimisticEffect = () => {
const store = useStore();
const apolloClient = useApolloClient();
const cache = apolloClient.cache;
const triggerViewFieldOptimisticEffect = useCallback(
({
createdViewFields = [],
updatedViewFields = [],
deletedViewFields = [],
}: {
createdViewFields?: Omit<CoreViewField, 'workspaceId'>[];
updatedViewFields?: Omit<CoreViewField, 'workspaceId'>[];
deletedViewFields?: Pick<CoreViewField, 'id' | 'viewId'>[];
}) => {
const coreViews = store.get(coreViewsState.atom);
let newCoreViews = [...coreViews];
createdViewFields.forEach((createdViewField) => {
cache.modify<CoreViewWithRelations>({
id: cache.identify({
__typename: 'CoreView',
id: createdViewField.viewId,
}),
fields: {
viewFields: (existingViewFields, { toReference }) =>
[
...(existingViewFields ?? []),
toReference(createdViewField),
].filter(isDefined),
},
});
const toBeModifiedCoreView = newCoreViews.find(
(coreView) => coreView.id === createdViewField.viewId,
);
if (isDefined(toBeModifiedCoreView)) {
newCoreViews = [
...newCoreViews.filter(
(coreView) => coreView.id !== createdViewField.viewId,
),
{
...toBeModifiedCoreView,
viewFields: [
...toBeModifiedCoreView.viewFields,
createdViewField,
],
},
];
}
});
updatedViewFields.forEach((updatedViewField) => {
cache.modify<CoreViewWithRelations>({
id: cache.identify({
__typename: 'CoreView',
id: updatedViewField.viewId,
}),
fields: {
viewFields: (existingViewFields, { readField, toReference }) =>
existingViewFields
.map((viewField) => {
const viewFieldId = readField<string>('id', viewField);
if (viewFieldId === updatedViewField.id) {
return toReference(updatedViewField);
}
return viewField;
})
.filter(isDefined),
},
});
const toBeModifiedCoreView = newCoreViews.find(
(coreView) => coreView.id === updatedViewField.viewId,
);
if (isDefined(toBeModifiedCoreView)) {
newCoreViews = [
...newCoreViews.filter(
(coreView) => coreView.id !== updatedViewField.viewId,
),
{
...toBeModifiedCoreView,
viewFields: [
...toBeModifiedCoreView.viewFields.filter(
(viewField) => viewField.id !== updatedViewField.id,
),
updatedViewField,
],
},
];
}
});
deletedViewFields.forEach(
(deletedViewField: Pick<CoreViewField, 'id' | 'viewId'>) => {
cache.modify<CoreViewWithRelations>({
id: cache.identify({
__typename: 'CoreView',
id: deletedViewField.viewId,
}),
fields: {
viewFields: (existingViewFields, { readField }) =>
existingViewFields.filter(
(viewField) =>
readField('id', viewField) !== deletedViewField.id,
),
},
});
const toBeModifiedCoreView = newCoreViews.find(
(coreView) => coreView.id === deletedViewField.viewId,
);
if (isDefined(toBeModifiedCoreView)) {
newCoreViews = [
...newCoreViews.filter(
(coreView) => coreView.id !== deletedViewField.viewId,
),
{
...toBeModifiedCoreView,
viewFields: toBeModifiedCoreView.viewFields.filter(
(viewField) => viewField.id !== deletedViewField.id,
),
},
];
}
},
);
if (!isDeeplyEqual(coreViews, newCoreViews)) {
store.set(coreViewsState.atom, newCoreViews);
}
},
[cache, store],
);
return {
triggerViewFieldOptimisticEffect,
};
};
@@ -1,147 +0,0 @@
import { coreViewsState } from '@/views/states/coreViewState';
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
import { useApolloClient } from '@apollo/client/react';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { type CoreViewFilter } from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { useStore } from 'jotai';
type UpdatedDeletedCoreViewFilter = {
createdViewFilters?: Omit<CoreViewFilter, 'workspaceId'>[];
updatedViewFilters?: Omit<CoreViewFilter, 'workspaceId'>[];
deletedViewFilters?: Pick<CoreViewFilter, 'id' | 'viewId'>[];
};
export const useTriggerViewFilterOptimisticEffect = () => {
const store = useStore();
const apolloClient = useApolloClient();
const cache = apolloClient.cache;
const triggerViewFilterOptimisticEffect = useCallback(
({
createdViewFilters = [],
updatedViewFilters = [],
deletedViewFilters = [],
}: UpdatedDeletedCoreViewFilter) => {
const coreViews = store.get(coreViewsState.atom);
let newCoreViews = [...coreViews];
createdViewFilters.forEach((createdViewFilter) => {
cache.modify<CoreViewWithRelations>({
id: cache.identify({
__typename: 'CoreView',
id: createdViewFilter.viewId,
}),
fields: {
viewFilters: (existingViewFilters, { toReference }) =>
[
...(existingViewFilters ?? []),
toReference(createdViewFilter),
].filter(isDefined),
},
});
const toBeModifiedCoreView = newCoreViews.find(
(coreView) => coreView.id === createdViewFilter.viewId,
);
if (isDefined(toBeModifiedCoreView)) {
newCoreViews = [
...newCoreViews.filter(
(coreView) => coreView.id !== createdViewFilter.viewId,
),
{
...toBeModifiedCoreView,
viewFilters: [
...toBeModifiedCoreView.viewFilters,
createdViewFilter,
],
},
];
}
});
updatedViewFilters.forEach((updatedViewFilter) => {
cache.modify<CoreViewWithRelations>({
id: cache.identify({
__typename: 'CoreView',
id: updatedViewFilter.viewId,
}),
fields: {
viewFilters: (existingViewFilters, { readField, toReference }) =>
existingViewFilters
.map((viewFilter) => {
const viewFilterId = readField<string>('id', viewFilter);
if (viewFilterId === updatedViewFilter.id) {
return toReference(updatedViewFilter);
}
return viewFilter;
})
.filter(isDefined),
},
});
const toBeModifiedCoreView = newCoreViews.find(
(coreView) => coreView.id === updatedViewFilter.viewId,
);
if (isDefined(toBeModifiedCoreView)) {
newCoreViews = [
...newCoreViews.filter(
(coreView) => coreView.id !== updatedViewFilter.viewId,
),
{
...toBeModifiedCoreView,
viewFilters: [
...toBeModifiedCoreView.viewFilters.filter(
(viewFilter) => viewFilter.id !== updatedViewFilter.id,
),
updatedViewFilter,
],
},
];
}
});
deletedViewFilters.forEach(
(deletedViewFilter: Pick<CoreViewFilter, 'id' | 'viewId'>) => {
cache.modify<CoreViewWithRelations>({
id: cache.identify({
__typename: 'CoreView',
id: deletedViewFilter.viewId,
}),
fields: {
viewFilters: (existingViewFilters, { readField }) =>
existingViewFilters.filter(
(viewFilter) =>
readField('id', viewFilter) !== deletedViewFilter.id,
),
},
});
const toBeModifiedCoreView = newCoreViews.find(
(coreView) => coreView.id === deletedViewFilter.viewId,
);
if (isDefined(toBeModifiedCoreView)) {
newCoreViews = [
...newCoreViews.filter(
(coreView) => coreView.id !== deletedViewFilter.viewId,
),
{
...toBeModifiedCoreView,
viewFilters: toBeModifiedCoreView.viewFilters.filter(
(viewFilter) => viewFilter.id !== deletedViewFilter.id,
),
},
];
}
},
);
if (!isDeeplyEqual(coreViews, newCoreViews)) {
store.set(coreViewsState.atom, newCoreViews);
}
},
[cache, store],
);
return {
triggerViewFilterOptimisticEffect,
};
};
@@ -1,146 +0,0 @@
import { coreViewsState } from '@/views/states/coreViewState';
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
import { useApolloClient } from '@apollo/client/react';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { type CoreViewGroup } from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { useStore } from 'jotai';
type UpdatedDeletedCoreViewGroup = {
createdViewGroups?: Omit<CoreViewGroup, 'workspaceId'>[];
updatedViewGroups?: Omit<CoreViewGroup, 'workspaceId'>[];
deletedViewGroups?: Pick<CoreViewGroup, 'id' | 'viewId'>[];
};
export const useTriggerViewGroupOptimisticEffect = () => {
const store = useStore();
const apolloClient = useApolloClient();
const cache = apolloClient.cache;
const triggerViewGroupOptimisticEffect = useCallback(
({
createdViewGroups = [],
updatedViewGroups = [],
deletedViewGroups = [],
}: UpdatedDeletedCoreViewGroup) => {
const coreViews = store.get(coreViewsState.atom);
let newCoreViews = [...coreViews];
createdViewGroups.forEach((createdViewGroup) => {
cache.modify<CoreViewWithRelations>({
id: cache.identify({
__typename: 'CoreView',
id: createdViewGroup.viewId,
}),
fields: {
viewGroups: (existingViewGroups, { toReference }) =>
[
...(existingViewGroups ?? []),
toReference(createdViewGroup),
].filter(isDefined),
},
});
const toBeModifiedCoreView = newCoreViews.find(
(coreView) => coreView.id === createdViewGroup.viewId,
);
if (isDefined(toBeModifiedCoreView)) {
newCoreViews = [
...newCoreViews.filter(
(coreView) => coreView.id !== createdViewGroup.viewId,
),
{
...toBeModifiedCoreView,
viewGroups: [
...toBeModifiedCoreView.viewGroups,
createdViewGroup,
],
},
];
}
});
updatedViewGroups.forEach((updatedViewGroup) => {
cache.modify<CoreViewWithRelations>({
id: cache.identify({
__typename: 'CoreView',
id: updatedViewGroup.viewId,
}),
fields: {
viewGroups: (existingViewGroups, { readField, toReference }) =>
existingViewGroups
.map((viewGroup) => {
const viewGroupId = readField<string>('id', viewGroup);
if (viewGroupId === updatedViewGroup.id) {
return toReference(updatedViewGroup);
}
return viewGroup;
})
.filter(isDefined),
},
});
const toBeModifiedCoreView = newCoreViews.find(
(coreView) => coreView.id === updatedViewGroup.viewId,
);
if (isDefined(toBeModifiedCoreView)) {
newCoreViews = [
...newCoreViews.filter(
(coreView) => coreView.id !== updatedViewGroup.viewId,
),
{
...toBeModifiedCoreView,
viewGroups: [
...toBeModifiedCoreView.viewGroups.filter(
(viewGroup) => viewGroup.id !== updatedViewGroup.id,
),
updatedViewGroup,
],
},
];
}
});
deletedViewGroups.forEach((deletedViewGroup) => {
cache.modify<CoreViewWithRelations>({
id: cache.identify({
__typename: 'CoreView',
id: deletedViewGroup.viewId,
}),
fields: {
viewGroups: (existingViewGroups, { readField }) =>
existingViewGroups.filter(
(viewGroup) =>
readField('id', viewGroup) !== deletedViewGroup.id,
),
},
});
const toBeModifiedCoreView = newCoreViews.find(
(coreView) => coreView.id === deletedViewGroup.viewId,
);
if (isDefined(toBeModifiedCoreView)) {
newCoreViews = [
...newCoreViews.filter(
(coreView) => coreView.id !== deletedViewGroup.viewId,
),
{
...toBeModifiedCoreView,
viewGroups: toBeModifiedCoreView.viewGroups.filter(
(viewGroup) => viewGroup.id !== deletedViewGroup.id,
),
},
];
}
});
if (!isDeeplyEqual(coreViews, newCoreViews)) {
store.set(coreViewsState.atom, newCoreViews);
}
},
[cache, store],
);
return {
triggerViewGroupOptimisticEffect,
};
};
@@ -1,149 +0,0 @@
import { coreViewsState } from '@/views/states/coreViewState';
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
import { useApolloClient } from '@apollo/client/react';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { type CoreViewSort } from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useTriggerViewSortOptimisticEffect = () => {
const store = useStore();
const apolloClient = useApolloClient();
const cache = apolloClient.cache;
const triggerViewSortOptimisticEffect = useCallback(
({
createdViewSorts = [],
updatedViewSorts = [],
deletedViewSorts = [],
}: {
createdViewSorts?: Omit<CoreViewSort, 'workspaceId'>[];
updatedViewSorts?: Omit<CoreViewSort, 'workspaceId'>[];
deletedViewSorts?: Pick<CoreViewSort, 'id'>[];
}) => {
const coreViews = store.get(coreViewsState.atom);
let newCoreViews = [...coreViews];
createdViewSorts.forEach((createdViewSort) => {
cache.modify<CoreViewWithRelations>({
id: cache.identify({
__typename: 'CoreView',
id: createdViewSort.viewId,
}),
fields: {
viewSorts: (existingViewSorts, { toReference }) =>
[
...(existingViewSorts ?? []),
toReference(createdViewSort),
].filter(isDefined),
},
});
const toBeModifiedCoreView = newCoreViews.find(
(coreView) => coreView.id === createdViewSort.viewId,
);
if (isDefined(toBeModifiedCoreView)) {
newCoreViews = [
...newCoreViews.filter(
(coreView) => coreView.id !== createdViewSort.viewId,
),
{
...toBeModifiedCoreView,
viewSorts: [...toBeModifiedCoreView.viewSorts, createdViewSort],
},
];
}
});
updatedViewSorts.forEach((updatedViewSort) => {
cache.modify<CoreViewWithRelations>({
id: cache.identify({
__typename: 'CoreView',
id: updatedViewSort.viewId,
}),
fields: {
viewSorts: (existingViewSorts, { readField, toReference }) =>
existingViewSorts
.map((viewSort) => {
const viewSortId = readField<string>('id', viewSort);
if (viewSortId === updatedViewSort.id) {
return toReference(updatedViewSort);
}
return viewSort;
})
.filter(isDefined),
},
});
const toBeModifiedCoreView = newCoreViews.find(
(coreView) => coreView.id === updatedViewSort.viewId,
);
if (isDefined(toBeModifiedCoreView)) {
newCoreViews = [
...newCoreViews.filter(
(coreView) => coreView.id !== updatedViewSort.viewId,
),
{
...toBeModifiedCoreView,
viewSorts: [
...toBeModifiedCoreView.viewSorts.filter(
(viewSort) => viewSort.id !== updatedViewSort.id,
),
updatedViewSort,
],
},
];
}
});
deletedViewSorts.forEach((deletedViewSort: Pick<CoreViewSort, 'id'>) => {
const viewId = coreViews.find((coreView) =>
coreView.viewSorts.some(
(viewSort) => viewSort.id === deletedViewSort.id,
),
)?.id;
if (!viewId) {
return;
}
cache.modify<CoreViewWithRelations>({
id: cache.identify({
__typename: 'CoreView',
id: viewId,
}),
fields: {
viewSorts: (existingViewSorts, { readField }) =>
existingViewSorts.filter(
(viewSort) => readField('id', viewSort) !== deletedViewSort.id,
),
},
});
const toBeModifiedCoreView = newCoreViews.find(
(coreView) => coreView.id === viewId,
);
if (isDefined(toBeModifiedCoreView)) {
newCoreViews = [
...newCoreViews.filter((coreView) => coreView.id !== viewId),
{
...toBeModifiedCoreView,
viewSorts: toBeModifiedCoreView.viewSorts.filter(
(viewSort) => viewSort.id !== deletedViewSort.id,
),
},
];
}
});
if (!isDeeplyEqual(coreViews, newCoreViews)) {
store.set(coreViewsState.atom, newCoreViews);
}
},
[cache, store],
);
return {
triggerViewSortOptimisticEffect,
};
};
@@ -1,7 +0,0 @@
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
export const coreViewsState = createAtomState<CoreViewWithRelations[]>({
key: 'coreViewsState',
defaultValue: [],
});
@@ -1,4 +1,4 @@
import { coreViewsState } from '@/views/states/coreViewState';
import { coreViewsSelector } from '@/views/states/selectors/coreViewsSelector';
import { ViewKey } from '@/views/types/ViewKey';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
@@ -12,7 +12,7 @@ export const coreIndexViewIdFromObjectMetadataItemFamilySelector =
get:
({ objectMetadataItemId }) =>
({ get }) => {
const coreViews = get(coreViewsState);
const coreViews = get(coreViewsSelector);
const views = coreViews.map(convertCoreViewToView);
return views?.find(
(view) =>
@@ -1,5 +1,5 @@
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
import { coreViewsState } from '@/views/states/coreViewState';
import { coreViewsSelector } from '@/views/states/selectors/coreViewsSelector';
import { type View } from '@/views/types/View';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
@@ -11,7 +11,7 @@ export const coreViewFromViewIdFamilySelector = createAtomFamilySelector<
get:
({ viewId }) =>
({ get }) => {
const coreViews = get(coreViewsState);
const coreViews = get(coreViewsSelector);
const views = coreViews.map(convertCoreViewToView);
return views?.find((view) => view.id === viewId);
},
@@ -1,4 +1,4 @@
import { coreViewsState } from '@/views/states/coreViewState';
import { coreViewsSelector } from '@/views/states/selectors/coreViewsSelector';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
@@ -8,7 +8,7 @@ export const coreViewIdsFromObjectMetadataItemFamilySelector =
get:
({ objectMetadataItemId }) =>
({ get }) => {
const coreViews = get(coreViewsState);
const coreViews = get(coreViewsSelector);
const views = coreViews.map(convertCoreViewToView);
return views
.filter((view) => view.objectMetadataId === objectMetadataItemId)
@@ -1,10 +1,10 @@
import { coreViewsState } from '@/views/states/coreViewState';
import { coreViewsSelector } from '@/views/states/selectors/coreViewsSelector';
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
export const coreViewLengthFamilySelector = createAtomSelector<number>({
key: 'coreViewLengthFamilySelector',
get: ({ get }) => {
const coreViews = get(coreViewsState);
const coreViews = get(coreViewsSelector);
return coreViews?.length ?? 0;
},
});
@@ -1,29 +1,16 @@
import { coreViewsState } from '@/views/states/coreViewState';
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
import { createAtomWritableFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomWritableFamilySelector';
import { coreViewsSelector } from '@/views/states/selectors/coreViewsSelector';
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
export const coreViewsByObjectMetadataIdFamilySelector =
createAtomWritableFamilySelector<CoreViewWithRelations[], string>({
createAtomFamilySelector<CoreViewWithRelations[], string>({
key: 'coreViewsByObjectMetadataIdFamilySelector',
get:
(objectMetadataId) =>
({ get }) => {
const coreViews = get(coreViewsState);
const coreViews = get(coreViewsSelector);
return coreViews.filter(
(view) => view.objectMetadataId === objectMetadataId,
);
},
set:
(objectMetadataId) =>
({ get, set }, newViewsForObject) => {
const currentCoreViews = get(coreViewsState);
const viewsFromOtherObjects = currentCoreViews.filter(
(view) => view.objectMetadataId !== objectMetadataId,
);
const newCoreViews = [
...viewsFromOtherObjects,
...(Array.isArray(newViewsForObject) ? newViewsForObject : []),
];
set(coreViewsState, newCoreViews);
},
});
@@ -1,5 +1,5 @@
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
import { coreViewsState } from '@/views/states/coreViewState';
import { coreViewsSelector } from '@/views/states/selectors/coreViewsSelector';
import { type View } from '@/views/types/View';
import { ViewType } from '@/views/types/ViewType';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
@@ -10,7 +10,7 @@ export const coreViewsFromObjectMetadataItemFamilySelector =
get:
({ objectMetadataItemId }) =>
({ get }) => {
const coreViews = get(coreViewsState);
const coreViews = get(coreViewsSelector);
const views = coreViews.map(convertCoreViewToView);
return views
.filter(
@@ -0,0 +1,95 @@
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { type FlatView } from '@/metadata-store/types/FlatView';
import { type FlatViewField } from '@/metadata-store/types/FlatViewField';
import { type FlatViewFieldGroup } from '@/metadata-store/types/FlatViewFieldGroup';
import { type FlatViewFilter } from '@/metadata-store/types/FlatViewFilter';
import { type FlatViewFilterGroup } from '@/metadata-store/types/FlatViewFilterGroup';
import { type FlatViewGroup } from '@/metadata-store/types/FlatViewGroup';
import { type FlatViewSort } from '@/metadata-store/types/FlatViewSort';
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
export const coreViewsSelector = createAtomSelector<CoreViewWithRelations[]>({
key: 'coreViewsSelector',
get: ({ get }) => {
const flatViews = get(metadataStoreState, 'views').current as FlatView[];
const flatViewFields = get(metadataStoreState, 'viewFields')
.current as FlatViewField[];
const flatViewFilters = get(metadataStoreState, 'viewFilters')
.current as FlatViewFilter[];
const flatViewSorts = get(metadataStoreState, 'viewSorts')
.current as FlatViewSort[];
const flatViewGroups = get(metadataStoreState, 'viewGroups')
.current as FlatViewGroup[];
const flatViewFilterGroups = get(metadataStoreState, 'viewFilterGroups')
.current as FlatViewFilterGroup[];
const flatViewFieldGroups = get(metadataStoreState, 'viewFieldGroups')
.current as FlatViewFieldGroup[];
const viewFieldsByViewId = new Map<string, FlatViewField[]>();
const viewFiltersByViewId = new Map<string, FlatViewFilter[]>();
const viewSortsByViewId = new Map<string, FlatViewSort[]>();
const viewGroupsByViewId = new Map<string, FlatViewGroup[]>();
const viewFilterGroupsByViewId = new Map<string, FlatViewFilterGroup[]>();
const viewFieldGroupsByViewId = new Map<string, FlatViewFieldGroup[]>();
for (const field of flatViewFields) {
const existing = viewFieldsByViewId.get(field.viewId) ?? [];
existing.push(field);
viewFieldsByViewId.set(field.viewId, existing);
}
for (const filter of flatViewFilters) {
const existing = viewFiltersByViewId.get(filter.viewId) ?? [];
existing.push(filter);
viewFiltersByViewId.set(filter.viewId, existing);
}
for (const sort of flatViewSorts) {
const existing = viewSortsByViewId.get(sort.viewId) ?? [];
existing.push(sort);
viewSortsByViewId.set(sort.viewId, existing);
}
for (const group of flatViewGroups) {
const existing = viewGroupsByViewId.get(group.viewId) ?? [];
existing.push(group);
viewGroupsByViewId.set(group.viewId, existing);
}
for (const filterGroup of flatViewFilterGroups) {
if ('viewId' in filterGroup && typeof filterGroup.viewId === 'string') {
const existing = viewFilterGroupsByViewId.get(filterGroup.viewId) ?? [];
existing.push(filterGroup);
viewFilterGroupsByViewId.set(filterGroup.viewId, existing);
}
}
for (const fieldGroup of flatViewFieldGroups) {
if ('viewId' in fieldGroup && typeof fieldGroup.viewId === 'string') {
const existing = viewFieldGroupsByViewId.get(fieldGroup.viewId) ?? [];
existing.push(fieldGroup);
viewFieldGroupsByViewId.set(fieldGroup.viewId, existing);
}
}
return flatViews.map((view) => ({
...view,
viewFields: viewFieldsByViewId.get(view.id) ?? [],
viewFilters: viewFiltersByViewId.get(view.id) ?? [],
viewSorts: viewSortsByViewId.get(view.id) ?? [],
viewGroups: viewGroupsByViewId.get(view.id) ?? [],
viewFilterGroups: viewFilterGroupsByViewId.get(view.id) ?? [],
viewFieldGroups: (viewFieldGroupsByViewId.get(view.id) ?? []).map(
(fieldGroup) => ({
...fieldGroup,
viewFields: (viewFieldsByViewId.get(view.id) ?? []).filter(
(field) =>
'viewFieldGroupId' in field &&
field.viewFieldGroupId === fieldGroup.id,
),
}),
),
})) as CoreViewWithRelations[];
},
});
@@ -1,11 +0,0 @@
import { type CoreView } from '~/generated-metadata/graphql';
export type CoreViewWithoutRelations = Exclude<
CoreView,
| 'viewFields'
| 'viewFieldGroups'
| 'viewGroups'
| 'viewFilters'
| 'viewFilterGroups'
| 'viewSorts'
>;
@@ -7,7 +7,6 @@ import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/use
import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist';
import { useChangeView } from '@/views/hooks/useChangeView';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { coreViewsByObjectMetadataIdFamilySelector } from '@/views/states/selectors/coreViewsByObjectMetadataIdFamilySelector';
import { coreViewsFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreViewsFromObjectMetadataItemFamilySelector';
import { useCloseAndResetViewPicker } from '@/views/view-picker/hooks/useCloseAndResetViewPicker';
import { viewPickerIsDirtyComponentState } from '@/views/view-picker/states/viewPickerIsDirtyComponentState';
@@ -72,18 +71,10 @@ export const useDestroyViewFromCurrentState = (viewBarInstanceId?: string) => {
changeView(remainingViews[0].id);
}
store.set(
coreViewsByObjectMetadataIdFamilySelector.selectorFamily(
objectMetadataItem.id,
),
(views) => views.filter((view) => view.id !== viewPickerReferenceViewId),
);
await performViewAPIDestroy({ id: viewPickerReferenceViewId });
}, [
currentView,
closeAndResetViewPicker,
objectMetadataItem.id,
changeView,
performViewAPIDestroy,
store,