Handle ObjectMetadataItem, FieldMetadataItem and NavigationMenuItem with SSE events (#18235)

This PR allows to handle ObjectMetadataItem, FieldMetadataItem and
NavigationMenuItem SSE events.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Lucas Bordeau
2026-02-25 23:18:00 +01:00
committed by GitHub
parent 546114d07f
commit 0fa8063054
8 changed files with 185 additions and 11 deletions
@@ -0,0 +1,37 @@
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { useStore } from 'jotai';
import { AllMetadataName } from '~/generated-metadata/graphql';
export const FieldMetadataSSEEffect = () => {
const queryId = 'field-metadata-sse-effect';
const store = useStore();
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
const { updateDraft, applyChanges } = useMetadataStore();
useListenToEventsForQuery({
queryId,
operationSignature: {
metadataName: AllMetadataName.fieldMetadata,
variables: {},
},
});
useListenToMetadataOperationBrowserEvent({
metadataName: AllMetadataName.fieldMetadata,
onMetadataOperationBrowserEvent: async () => {
await refreshObjectMetadataItems();
const loadedObjects = store.get(objectMetadataItemsState.atom);
updateDraft('objectMetadataItems', loadedObjects);
applyChanges();
},
});
return null;
};
@@ -0,0 +1,63 @@
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { prefetchNavigationMenuItemsState } from '@/prefetch/states/prefetchNavigationMenuItemsState';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { useStore } from 'jotai';
import { isDefined } from 'twenty-shared/utils';
import {
AllMetadataName,
useFindManyNavigationMenuItemsLazyQuery,
} from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const NavigationMenuItemSSEEffect = () => {
const queryId = 'navigation-menu-item-sse-effect';
const store = useStore();
const { updateDraft, applyChanges } = useMetadataStore();
const [findManyNavigationMenuItemsLazy] =
useFindManyNavigationMenuItemsLazyQuery();
useListenToEventsForQuery({
queryId,
operationSignature: {
metadataName: AllMetadataName.navigationMenuItem,
variables: {},
},
});
useListenToMetadataOperationBrowserEvent({
metadataName: AllMetadataName.navigationMenuItem,
onMetadataOperationBrowserEvent: async () => {
const result = await findManyNavigationMenuItemsLazy({
fetchPolicy: 'network-only',
});
if (!isDefined(result.data?.navigationMenuItems)) {
return;
}
const existingNavigationMenuItems = store.get(
prefetchNavigationMenuItemsState.atom,
);
if (
!isDeeplyEqual(
existingNavigationMenuItems,
result.data.navigationMenuItems,
)
) {
store.set(
prefetchNavigationMenuItemsState.atom,
result.data.navigationMenuItems,
);
updateDraft('navigationMenuItems', result.data.navigationMenuItems);
applyChanges();
}
},
});
return null;
};
@@ -0,0 +1,65 @@
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { prefetchNavigationMenuItemsState } from '@/prefetch/states/prefetchNavigationMenuItemsState';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { useStore } from 'jotai';
import {
AllMetadataName,
useFindManyNavigationMenuItemsLazyQuery,
} from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const ObjectMetadataItemSSEEffect = () => {
const queryId = 'object-metadata-sse-effect';
const store = useStore();
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
const { updateDraft, applyChanges } = useMetadataStore();
const [findManyNavigationMenuItemsLazy] =
useFindManyNavigationMenuItemsLazyQuery();
useListenToEventsForQuery({
queryId,
operationSignature: {
metadataName: AllMetadataName.objectMetadata,
variables: {},
},
});
useListenToMetadataOperationBrowserEvent({
metadataName: AllMetadataName.objectMetadata,
onMetadataOperationBrowserEvent: async () => {
await refreshObjectMetadataItems();
const loadedObjects = store.get(objectMetadataItemsState.atom);
updateDraft('objectMetadataItems', loadedObjects);
applyChanges();
const navigationMenuItemsResult = await findManyNavigationMenuItemsLazy({
fetchPolicy: 'network-only',
});
const existingNavigationMenuItems = store.get(
prefetchNavigationMenuItemsState.atom,
);
if (
!isDeeplyEqual(
existingNavigationMenuItems,
navigationMenuItemsResult.data?.navigationMenuItems,
)
) {
store.set(
prefetchNavigationMenuItemsState.atom,
navigationMenuItemsResult.data?.navigationMenuItems ?? [],
);
}
},
});
return null;
};
@@ -9,7 +9,7 @@ import {
type CoreViewField,
} from '~/generated-metadata/graphql';
export const ViewFieldMetadataSSEEffect = () => {
export const ViewFieldSSEEffect = () => {
const store = useStore();
const queryId = 'view-field-metadata-sse-effect';
@@ -5,7 +5,7 @@ import { type CoreViewWithoutRelations } from '@/views/types/CoreViewWithoutRela
import { useStore } from 'jotai';
import { AllMetadataName } from '~/generated-metadata/graphql';
export const ViewMetadataSSEEffect = () => {
export const ViewSSEEffect = () => {
const queryId = 'view-metadata-sse-effect';
const store = useStore();
@@ -14,6 +14,7 @@ export const ALL_METADATA_ENTITY_KEYS = [
'viewSorts',
'pageLayouts',
'logicFunctions',
'navigationMenuItems',
] as const;
export type MetadataEntityKey = (typeof ALL_METADATA_ENTITY_KEYS)[number];
@@ -6,7 +6,6 @@ import { AppPath } from 'twenty-shared/types';
import { getAppPath, isDefined } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { getObjectMetadataNamePluralFromViewId } from '@/favorites/utils/getObjectMetadataNamePluralFromViewId';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
import {
@@ -33,13 +32,16 @@ export const sortNavigationMenuItems = (
);
if (isDefined(view)) {
const { namePlural } = getObjectMetadataNamePluralFromViewId(
view,
objectMetadataItems,
);
const objectMetadataItem = objectMetadataItems.find(
(meta) => meta.id === view.objectMetadataId,
);
if (!isDefined(objectMetadataItem)) {
return null;
}
const namePlural = objectMetadataItem.namePlural;
const isIndexView = view.key === ViewKey.Index;
const labelIdentifier =
isIndexView && isDefined(objectMetadataItem)
@@ -1,5 +1,8 @@
import { ViewFieldMetadataSSEEffect } from '@/metadata-store/effect-components/ViewFieldMetadataSSEEffect';
import { ViewMetadataSSEEffect } from '@/metadata-store/effect-components/ViewMetadataSSEEffect';
import { FieldMetadataSSEEffect } from '@/metadata-store/effect-components/FieldMetadataSSEEffect';
import { NavigationMenuItemSSEEffect } from '@/metadata-store/effect-components/NavigationMenuItemSSEEffect';
import { ObjectMetadataItemSSEEffect } from '@/metadata-store/effect-components/ObjectMetadataItemSSEEffect';
import { ViewFieldSSEEffect } from '@/metadata-store/effect-components/ViewFieldSSEEffect';
import { ViewSSEEffect } from '@/metadata-store/effect-components/ViewSSEEffect';
import { SSEClientEffect } from '@/sse-db-event/components/SSEClientEffect';
import { SSEEventStreamEffect } from '@/sse-db-event/components/SSEEventStreamEffect';
import { SSEQuerySubscribeEffect } from '@/sse-db-event/components/SSEQuerySubscribeEffect';
@@ -15,8 +18,11 @@ export const SSEProvider = ({ children }: SSEProviderProps) => {
<SSEClientEffect />
<SSEEventStreamEffect />
<SSEQuerySubscribeEffect />
<ViewFieldMetadataSSEEffect />
<ViewMetadataSSEEffect />
<NavigationMenuItemSSEEffect />
<FieldMetadataSSEEffect />
<ObjectMetadataItemSSEEffect />
<ViewFieldSSEEffect />
<ViewSSEEffect />
{children}
</>
);