Handle SSE event for View and ViewField (#18218)

This PR implements a first working version of SSE for View and
ViewField.

We could improve the logic but we re-use the same actual codepath for
refreshing the views of an object.

This does not handle filters and sorts for now.

# Demo 


https://github.com/user-attachments/assets/73367f3a-a5fc-4b06-a2e2-fad3b99deec7
This commit is contained in:
Lucas Bordeau
2026-02-25 13:43:56 +01:00
committed by GitHub
parent 121788c42f
commit 2c02f2c499
17 changed files with 460 additions and 241 deletions
@@ -1,5 +1,5 @@
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { MainContextStoreProviderEffect } from '@/context-store/components/MainContextStoreProviderEffect';
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
@@ -76,7 +76,7 @@ export const MainContextStoreProvider = () => {
const shouldComputeContextStore =
(isRecordIndexPage || isRecordShowPage || isSettingsPage) &&
!showAuthModal &&
viewsEntry.status === 'loaded';
viewsEntry.status === 'up-to-date';
if (!shouldComputeContextStore) {
return null;
@@ -14,15 +14,18 @@ export const IsAppMetadataReadyEffect = () => {
const isLoggedIn = useIsLogged();
const currentUser = useAtomStateValue(currentUserState);
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const objectsEntry = useAtomFamilyStateValue(metadataStoreState, 'objects');
const objectMetadataItemsEntry = useAtomFamilyStateValue(
metadataStoreState,
'objectMetadataItems',
);
const viewsEntry = useAtomFamilyStateValue(metadataStoreState, 'views');
const setIsAppMetadataReady = useSetAtomState(isAppMetadataReadyState);
useEffect(() => {
const hasActiveWorkspace = isWorkspaceActiveOrSuspended(currentWorkspace);
const areObjectsLoaded = objectsEntry.status === 'loaded';
const areViewsLoaded = viewsEntry.status === 'loaded';
const areObjectsLoaded = objectMetadataItemsEntry.status === 'up-to-date';
const areViewsLoaded = viewsEntry.status === 'up-to-date';
if (!areObjectsLoaded) {
setIsAppMetadataReady(false);
@@ -38,7 +41,7 @@ export const IsAppMetadataReadyEffect = () => {
isLoggedIn,
currentUser,
currentWorkspace,
objectsEntry.status,
objectMetadataItemsEntry.status,
viewsEntry.status,
setIsAppMetadataReady,
]);
@@ -1,6 +1,6 @@
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { useLoadMockedObjectMetadataItems } from '@/object-metadata/hooks/useLoadMockedObjectMetadataItems';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
@@ -37,7 +37,7 @@ export const ObjectMetadataProviderInitialEffect = () => {
}
const loadedItems = store.get(objectMetadataItemsState.atom);
updateDraft('objects', loadedItems);
updateDraft('objectMetadataItems', loadedItems);
applyChanges();
setIsInitialized(true);
};
@@ -0,0 +1,78 @@
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { coreViewsState } from '@/views/states/coreViewState';
import { useStore } from 'jotai';
import { isDefined } from 'twenty-shared/utils';
import {
AllMetadataName,
type CoreViewField,
} from '~/generated-metadata/graphql';
export const ViewFieldMetadataSSEEffect = () => {
const store = useStore();
const queryId = 'view-field-metadata-sse-effect';
const { refreshCoreViewsByObjectMetadataId } =
useRefreshCoreViewsByObjectMetadataId();
useListenToEventsForQuery({
queryId,
operationSignature: {
metadataName: AllMetadataName.viewField,
variables: {},
},
});
useListenToMetadataOperationBrowserEvent({
metadataName: AllMetadataName.viewField,
onMetadataOperationBrowserEvent: (eventDetail) => {
const coreViews = store.get(coreViewsState.atom);
let viewId: string | undefined;
switch (eventDetail.operation.type) {
case 'create': {
const createdViewField = eventDetail.operation
.createdRecord as unknown as CoreViewField;
viewId = createdViewField.viewId;
break;
}
case 'update': {
const updatedViewField = eventDetail.operation
.updatedRecord as unknown as CoreViewField;
viewId = updatedViewField.viewId;
break;
}
case 'delete': {
const deletedViewFieldId = eventDetail.operation
.deletedRecordId as string;
const view = coreViews.find((coreView) =>
coreView.viewFields.some(
(viewField) => viewField.id === deletedViewFieldId,
),
);
viewId = view?.id;
break;
}
}
if (!isDefined(viewId)) {
return;
}
const view = coreViews.find((coreView) => coreView.id === viewId);
if (!isDefined(view)) {
return;
}
refreshCoreViewsByObjectMetadataId(view.objectMetadataId);
},
});
return null;
};
@@ -1,51 +1,19 @@
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { coreViewsState } from '@/views/states/coreViewState';
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
import { useFetchAndLoadIndexViews } from '@/metadata-store/hooks/useFetchAndLoadIndexViews';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useStore } from 'jotai';
import { useCallback, useEffect, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useEffect, useState } from 'react';
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
import {
ViewType as CoreViewType,
useFindAllCoreViewsLazyQuery,
} from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
const INDEX_VIEW_TYPES = [
CoreViewType.TABLE,
CoreViewType.KANBAN,
CoreViewType.CALENDAR,
];
export const ViewMetadataProviderInitialEffect = () => {
const isLoggedIn = useIsLogged();
const isCurrentUserLoaded = useAtomStateValue(isCurrentUserLoadedState);
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const store = useStore();
const [isInitialized, setIsInitialized] = useState(false);
const { updateDraft, applyChanges } = useMetadataStore();
const [findAllCoreViews] = useFindAllCoreViewsLazyQuery();
const setIndexCoreViews = useCallback(
(indexViews: CoreViewWithRelations[]) => {
const existingCoreViews = store.get(coreViewsState.atom);
const existingFieldsWidgetViews = existingCoreViews.filter(
(view) => view.type === CoreViewType.FIELDS_WIDGET,
);
const mergedViews = [...indexViews, ...existingFieldsWidgetViews];
if (!isDeeplyEqual(existingCoreViews, mergedViews)) {
store.set(coreViewsState.atom, mergedViews);
}
},
[store],
);
const { fetchAndLoadIndexViews } = useFetchAndLoadIndexViews();
useEffect(() => {
if (isInitialized) {
@@ -60,31 +28,19 @@ export const ViewMetadataProviderInitialEffect = () => {
return;
}
const loadViews = async () => {
const result = await findAllCoreViews({
variables: { viewTypes: INDEX_VIEW_TYPES },
fetchPolicy: 'network-only',
});
if (isDefined(result.data?.getCoreViews)) {
setIndexCoreViews(result.data.getCoreViews);
updateDraft('views', result.data.getCoreViews);
applyChanges();
}
const initialize = async () => {
await fetchAndLoadIndexViews();
setIsInitialized(true);
};
loadViews();
initialize();
}, [
isInitialized,
isCurrentUserLoaded,
isLoggedIn,
currentWorkspace,
findAllCoreViews,
setIndexCoreViews,
updateDraft,
applyChanges,
fetchAndLoadIndexViews,
]);
return null;
@@ -0,0 +1,75 @@
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { coreViewsState } from '@/views/states/coreViewState';
import { type CoreViewWithoutRelations } from '@/views/types/CoreViewWithoutRelations';
import { useStore } from 'jotai';
import { AllMetadataName } from '~/generated-metadata/graphql';
export const ViewMetadataSSEEffect = () => {
const queryId = 'view-metadata-sse-effect';
const store = useStore();
useListenToEventsForQuery({
queryId,
operationSignature: {
metadataName: AllMetadataName.view,
variables: {},
},
});
useListenToMetadataOperationBrowserEvent({
metadataName: AllMetadataName.view,
onMetadataOperationBrowserEvent: (eventDetail) => {
switch (eventDetail.operation.type) {
case 'create': {
const createdView = eventDetail.operation
.createdRecord as unknown as CoreViewWithoutRelations;
store.set(coreViewsState.atom, (prevCoreViews) => [
...prevCoreViews,
{
...createdView,
viewFields: [],
viewFieldGroups: [],
viewFilterGroups: [],
viewSorts: [],
viewFilters: [],
viewGroups: [],
},
]);
break;
}
case 'update': {
const updatedView = eventDetail.operation
.updatedRecord as unknown as CoreViewWithoutRelations;
store.set(coreViewsState.atom, (prevCoreViews) =>
prevCoreViews.map((coreView) =>
coreView.id === updatedView.id
? {
...coreView,
...updatedView,
}
: coreView,
),
);
break;
}
case 'delete': {
const deletedViewId = eventDetail.operation.deletedRecordId as string;
store.set(coreViewsState.atom, (prevCoreViews) =>
prevCoreViews.filter((coreView) => coreView.id !== deletedViewId),
);
break;
}
default:
return;
}
},
});
return null;
};
@@ -0,0 +1,31 @@
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { useSetIndexViews } from '@/metadata-store/hooks/useSetIndexViews';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import {
useFindAllCoreViewsLazyQuery,
ViewType,
} from '~/generated-metadata/graphql';
const INDEX_VIEW_TYPES = [ViewType.TABLE, ViewType.KANBAN, ViewType.CALENDAR];
export const useFetchAndLoadIndexViews = () => {
const [findAllCoreViews] = useFindAllCoreViewsLazyQuery();
const { updateDraft, applyChanges } = useMetadataStore();
const { setIndexViews } = useSetIndexViews();
const fetchAndLoadIndexViews = useCallback(async () => {
const result = await findAllCoreViews({
variables: { viewTypes: INDEX_VIEW_TYPES },
fetchPolicy: 'network-only',
});
if (isDefined(result.data?.getCoreViews)) {
setIndexViews(result.data.getCoreViews);
updateDraft('views', result.data.getCoreViews);
applyChanges();
}
}, [findAllCoreViews, setIndexViews, updateDraft, applyChanges]);
return { fetchAndLoadIndexViews };
};
@@ -1,17 +1,17 @@
import {
ALL_METADATA_KEYS,
metadataStoreState,
type MetadataKey,
type MetadataLoadEntry,
} from '@/metadata-store/states/metadataStoreState';
import { isAppMetadataReadyState } from '@/metadata-store/states/isAppMetadataReadyState';
import { type createStore, useStore } from 'jotai';
import {
ALL_METADATA_ENTITY_KEYS,
metadataStoreState,
type MetadataEntityKey,
type MetadataStoreItem,
} from '@/metadata-store/states/metadataStoreState';
import { useStore, type createStore } from 'jotai';
import { useCallback } from 'react';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
type JotaiStore = ReturnType<typeof createStore>;
const EMPTY_ENTRY: MetadataLoadEntry = {
const EMPTY_ENTRY: MetadataStoreItem = {
current: [],
draft: [],
status: 'empty',
@@ -31,20 +31,23 @@ const areViewsConsistentWithObjects = (
};
export const resetMetadataStore = (store: JotaiStore) => {
for (const key of ALL_METADATA_KEYS) {
for (const key of ALL_METADATA_ENTITY_KEYS) {
store.set(metadataStoreState.atomFamily(key), EMPTY_ENTRY);
}
store.set(isAppMetadataReadyState.atom, false);
};
const promoteEntry = (store: JotaiStore, key: MetadataKey) => {
const entry = store.get(metadataStoreState.atomFamily(key));
const changeMetadataEntityAsUpToDate = (
store: JotaiStore,
metadataEntityKey: MetadataEntityKey,
) => {
const entry = store.get(metadataStoreState.atomFamily(metadataEntityKey));
store.set(metadataStoreState.atomFamily(key), {
store.set(metadataStoreState.atomFamily(metadataEntityKey), {
current: entry.draft,
draft: [],
status: 'loaded',
status: 'up-to-date',
});
};
@@ -52,11 +55,11 @@ export const useMetadataStore = () => {
const store = useStore();
const updateDraft = useCallback(
(key: MetadataKey, data: object[]) => {
(key: MetadataEntityKey, data: object[]) => {
const currentEntry = store.get(metadataStoreState.atomFamily(key));
if (
currentEntry.status === 'loaded' &&
currentEntry.status === 'up-to-date' &&
isDeeplyEqual(currentEntry.current, data)
) {
return;
@@ -65,42 +68,48 @@ export const useMetadataStore = () => {
store.set(metadataStoreState.atomFamily(key), (prev) => ({
...prev,
draft: data,
status: 'draft_pending' as const,
status: 'draft-pending' as const,
}));
},
[store],
);
const applyChanges = useCallback((): boolean => {
let promoted = false;
const applyChanges = useCallback((): {
hasPersistedAnyMetadataEntity: boolean;
} => {
let hasPersistedAnyMetadataEntity = false;
for (const key of ALL_METADATA_KEYS) {
if (key === 'views') {
for (const metadataEntityKey of ALL_METADATA_ENTITY_KEYS) {
if (metadataEntityKey === 'views') {
continue;
}
const entry = store.get(metadataStoreState.atomFamily(key));
const metadataStoreEntityEntry = store.get(
metadataStoreState.atomFamily(metadataEntityKey),
);
if (entry.status === 'draft_pending') {
promoteEntry(store, key);
promoted = true;
if (metadataStoreEntityEntry.status === 'draft-pending') {
changeMetadataEntityAsUpToDate(store, metadataEntityKey);
hasPersistedAnyMetadataEntity = true;
}
}
const viewsEntry = store.get(metadataStoreState.atomFamily('views'));
if (viewsEntry.status === 'draft_pending') {
const objectsEntry = store.get(metadataStoreState.atomFamily('objects'));
if (viewsEntry.status === 'draft-pending') {
const objectsEntry = store.get(
metadataStoreState.atomFamily('objectMetadataItems'),
);
if (
areViewsConsistentWithObjects(viewsEntry.draft, objectsEntry.current)
) {
promoteEntry(store, 'views');
promoted = true;
changeMetadataEntityAsUpToDate(store, 'views');
hasPersistedAnyMetadataEntity = true;
}
}
return promoted;
return { hasPersistedAnyMetadataEntity };
}, [store]);
const reset = useCallback(() => {
@@ -17,7 +17,7 @@ export const useReloadWorkspaceMetadata = () => {
await refreshObjectMetadataItems();
const loadedObjects = store.get(objectMetadataItemsState.atom);
updateDraft('objects', loadedObjects);
updateDraft('objectMetadataItems', loadedObjects);
applyChanges();
const loadedViews = store.get(coreViewsState.atom);
@@ -36,7 +36,7 @@ export const useReloadWorkspaceMetadata = () => {
await loadMockedObjectMetadataItems();
const loadedObjects = store.get(objectMetadataItemsState.atom);
updateDraft('objects', loadedObjects);
updateDraft('objectMetadataItems', loadedObjects);
applyChanges();
}, [
resetMetadataStore,
@@ -0,0 +1,29 @@
import { coreViewsState } from '@/views/states/coreViewState';
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { ViewType } from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useSetIndexViews = () => {
const store = useStore();
const setIndexViews = useCallback(
(indexViews: CoreViewWithRelations[]) => {
const existingCoreViews = store.get(coreViewsState.atom);
const existingFieldsWidgetViews = existingCoreViews.filter(
(view) => view.type === ViewType.FIELDS_WIDGET,
);
const mergedViews = [...indexViews, ...existingFieldsWidgetViews];
if (!isDeeplyEqual(existingCoreViews, mergedViews)) {
store.set(coreViewsState.atom, mergedViews);
}
},
[store],
);
return {
setIndexViews,
};
};
@@ -1,36 +1,39 @@
import { createAtomFamilyState } from '@/ui/utilities/state/jotai/utils/createAtomFamilyState';
export type MetadataLoadStatus = 'empty' | 'draft_pending' | 'loaded';
export type MetadataEntityStoreStatus =
| 'empty'
| 'draft-pending'
| 'up-to-date';
export type MetadataKey =
| 'objects'
| 'views'
| 'pageLayouts'
| 'logicFunctions';
export const ALL_METADATA_KEYS: MetadataKey[] = [
'objects',
export const ALL_METADATA_ENTITY_KEYS = [
'objectMetadataItems',
'fieldMetadataItems',
'views',
'viewFields',
'viewFilters',
'viewSorts',
'pageLayouts',
'logicFunctions',
];
] as const;
export type MetadataLoadEntry = {
export type MetadataEntityKey = (typeof ALL_METADATA_ENTITY_KEYS)[number];
export type MetadataStoreItem = {
current: object[];
draft: object[];
status: MetadataLoadStatus;
status: MetadataEntityStoreStatus;
};
const METADATA_LOAD_ENTRY_DEFAULT: MetadataLoadEntry = {
const METADATA_STORE_ITEM_INITIAL_VALUE: MetadataStoreItem = {
current: [],
draft: [],
status: 'empty',
};
export const metadataStoreState = createAtomFamilyState<
MetadataLoadEntry,
MetadataKey
MetadataStoreItem,
MetadataEntityKey
>({
key: 'metadataStoreState',
defaultValue: METADATA_LOAD_ENTRY_DEFAULT,
defaultValue: METADATA_STORE_ITEM_INITIAL_VALUE,
});
@@ -1,3 +1,5 @@
import { ViewFieldMetadataSSEEffect } from '@/metadata-store/effect-components/ViewFieldMetadataSSEEffect';
import { ViewMetadataSSEEffect } from '@/metadata-store/effect-components/ViewMetadataSSEEffect';
import { SSEClientEffect } from '@/sse-db-event/components/SSEClientEffect';
import { SSEEventStreamEffect } from '@/sse-db-event/components/SSEEventStreamEffect';
import { SSEQuerySubscribeEffect } from '@/sse-db-event/components/SSEQuerySubscribeEffect';
@@ -13,6 +15,8 @@ export const SSEProvider = ({ children }: SSEProviderProps) => {
<SSEClientEffect />
<SSEEventStreamEffect />
<SSEQuerySubscribeEffect />
<ViewFieldMetadataSSEEffect />
<ViewMetadataSSEEffect />
{children}
</>
);
@@ -1,5 +1,5 @@
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { recordPageLayoutsState } from '@/page-layout/states/recordPageLayoutsState';
import { type PageLayout } from '@/page-layout/types/PageLayout';
import { transformPageLayout } from '@/page-layout/utils/transformPageLayout';
@@ -18,6 +18,7 @@ import {
useDestroyCoreViewFieldMutation,
useUpdateCoreViewFieldMutation,
} from '~/generated-metadata/graphql';
export const usePerformViewFieldAPIPersist = () => {
const { triggerViewFieldOptimisticEffect } =
useTriggerViewFieldOptimisticEffect();
@@ -0,0 +1,142 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState';
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 { 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.viewSorts, 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 };
};
@@ -1,24 +1,12 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState';
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 { 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 { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
import { useApplyCoreViewsForObjectMetadataId } from '@/views/hooks/useApplyCoreViewsForObjectMetadataId';
import { useCallback } from 'react';
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
import { isDefined } from 'twenty-shared/utils';
import { useFindManyCoreViewsLazyQuery } from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { useStore } from 'jotai';
export const useRefreshCoreViewsByObjectMetadataId = () => {
const store = useStore();
const [findManyCoreViewsLazy] = useFindManyCoreViewsLazyQuery();
const { applyCoreViewsForObjectMetadataId } =
useApplyCoreViewsForObjectMetadataId();
const refreshCoreViewsByObjectMetadataId = useCallback(
async (objectMetadataId: string) => {
@@ -33,123 +21,12 @@ export const useRefreshCoreViewsByObjectMetadataId = () => {
return;
}
const objectMetadataItems = store.get(objectMetadataItemsState.atom);
const objectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) => objectMetadataItem.id === objectMetadataId,
applyCoreViewsForObjectMetadataId(
objectMetadataId,
result.data.getCoreViews,
);
if (!isDefined(objectMetadataItem)) {
return;
}
const coreViewsForObjectMetadataId = store.get(
coreViewsByObjectMetadataIdFamilySelector.selectorFamily(
objectMetadataId,
),
);
const coreViewsFromResult = result.data.getCoreViews;
if (isDeeplyEqual(coreViewsForObjectMetadataId, coreViewsFromResult)) {
return;
}
store.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);
store.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);
store.set(
currentRecordFiltersComponentState.atomFamily({
instanceId: getRecordIndexIdFromObjectNamePluralAndViewId(
objectMetadataItem.namePlural,
view.id,
),
}),
mapViewFiltersToFilters(
view.viewFilters,
getFilterableFields(objectMetadataItem),
),
);
}
if (!isDeeplyEqual(coreView.viewSorts, existingView.viewSorts)) {
const view = convertCoreViewToView(coreView);
store.set(
currentRecordSortsComponentState.atomFamily({
instanceId: getRecordIndexIdFromObjectNamePluralAndViewId(
objectMetadataItem.namePlural,
view.id,
),
}),
view.viewSorts,
);
}
if (
coreView.shouldHideEmptyGroups !== existingView.shouldHideEmptyGroups
) {
const view = convertCoreViewToView(coreView);
store.set(
recordIndexShouldHideEmptyRecordGroupsComponentState.atomFamily({
instanceId: getRecordIndexIdFromObjectNamePluralAndViewId(
objectMetadataItem.namePlural,
view.id,
),
}),
view.shouldHideEmptyGroups,
);
}
}
},
[findManyCoreViewsLazy, store],
[findManyCoreViewsLazy, applyCoreViewsForObjectMetadataId],
);
return {
@@ -0,0 +1,11 @@
import { type CoreView } from '~/generated-metadata/graphql';
export type CoreViewWithoutRelations = Exclude<
CoreView,
| 'viewFields'
| 'viewFieldGroups'
| 'viewGroups'
| 'viewFilters'
| 'viewFilterGroups'
| 'viewSorts'
>;