fix(front): guard overview graph against stale object metadata

https://sonarly.com/issue/39542?type=bug

The Settings → Objects → Overview page can crash when a graph node references an object name that is no longer present in the current metadata store. The crash throws from `useObjectMetadataItem` and breaks the page render for affected workspaces/sessions.

Fix: Implemented a race-condition fix in the object overview graph layout effect.

In `SettingsDataModelOverviewEffect.tsx`, I added a layout version guard using `useRef`. Each effect run increments the version; after async dagre import/layout completes, the run only calls `setNodes`/`setEdges` if it is still the latest version. This prevents stale metadata snapshots from being committed after newer metadata has already loaded, which is the root cause behind stale nodes rendering and triggering downstream metadata lookup failures.

Authored by Sonarly by autonomous analysis (run 45048).
This commit is contained in:
sonarly-bot
2026-05-21 19:33:48 +00:00
parent 07a20cba5e
commit 31e6edb8d8
3 changed files with 18 additions and 1 deletions
@@ -1,5 +1,6 @@
import { AppErrorBoundaryEffect } from '@/error-handler/components/internal/AppErrorBoundaryEffect';
import { checkIfItsAViteStaleChunkLazyLoadingError } from '@/error-handler/utils/checkIfItsAViteStaleChunkLazyLoadingError';
import { ObjectMetadataItemNotFoundError } from '@/object-metadata/errors/ObjectMetadataNotFoundError';
import { type ErrorInfo, type ReactNode } from 'react';
import { ErrorBoundary, type FallbackProps } from 'react-error-boundary';
import { type CustomError, isDefined } from 'twenty-shared/utils';
@@ -29,6 +30,13 @@ export const AppErrorBoundary = ({
const fingerprint = hasErrorCode(error) ? error.code : error.message;
scope.setFingerprint([fingerprint]);
if (error instanceof ObjectMetadataItemNotFoundError) {
scope.setLevel('warning');
scope.setTag('error.category', 'metadata');
scope.setTag('error.type', 'object-metadata-item-not-found');
}
error.name = error.message;
return scope;
});
@@ -11,6 +11,7 @@ export class ObjectMetadataItemNotFoundError extends Error {
super(message);
this.name = 'ObjectMetadataItemNotFoundError';
Object.setPrototypeOf(this, ObjectMetadataItemNotFoundError.prototype);
}
}
@@ -1,5 +1,5 @@
import { type Edge, type Node } from '@xyflow/react';
import { useEffect } from 'react';
import { useEffect, useRef } from 'react';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { isDefined } from 'twenty-shared/utils';
@@ -17,8 +17,12 @@ export const SettingsDataModelOverviewEffect = ({
}: SettingsDataModelOverviewEffectProps) => {
const { activeNonSystemObjectMetadataItems: items } =
useFilteredObjectMetadataItems();
const latestLayoutVersionRef = useRef(0);
useEffect(() => {
latestLayoutVersionRef.current += 1;
const currentLayoutVersion = latestLayoutVersionRef.current;
const loadDagreAndLayout = async () => {
const dagre = await import('@dagrejs/dagre');
@@ -96,6 +100,10 @@ export const SettingsDataModelOverviewEffect = ({
};
});
if (currentLayoutVersion !== latestLayoutVersionRef.current) {
return;
}
setNodes(nodes);
setEdges(edges);
};