From 14d8105f227eec45a0e06e3455b2533f81b0a9cf Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Tue, 16 Jun 2026 11:08:48 +0200 Subject: [PATCH] fix(front): remove runtime default-view creation fallback (#21652) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem `useCreateDefaultViewForObject` was a temporary runtime fallback that created a view + one view field per field (each with a fresh `v4()` id) whenever `RecordIndexLoadBaseOnContextStoreEffect` found no view for the current view id. Because the created view got a fresh id that never matched the requested `contextStoreCurrentViewId`, the next load missed again and re-created another duplicate — leaking `core.view` / `core.viewField` rows without bound (notably during the 2.13.0 cache-first bootstrap window). #21592 made the fallback idempotent as a stop-gap, but the mechanism is no longer needed at all: standard/index views are created server-side at object creation and during standard app installation, so the client never needs to mint them. ## Change Remove the fallback entirely: - Delete `useCreateDefaultViewForObject`. - In `RecordIndexLoadBaseOnContextStoreEffect`, when no view resolves for the current id, do nothing and let the loaded views settle (the effect re-runs once the view is present and loads it). ## Note This removes the leak at the source for any client running the new bundle. Clients still on old cached JS will keep creating duplicates until they reload; the already-leaked rows are being cleaned up separately via SQL. Review in cubic --- ...ecordIndexLoadBaseOnContextStoreEffect.tsx | 6 -- .../hooks/useCreateDefaultViewForObject.ts | 97 ------------------- 2 files changed, 103 deletions(-) delete mode 100644 packages/twenty-front/src/modules/views/hooks/useCreateDefaultViewForObject.ts diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexLoadBaseOnContextStoreEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexLoadBaseOnContextStoreEffect.tsx index 39c03ba1bd..114866f79a 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexLoadBaseOnContextStoreEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexLoadBaseOnContextStoreEffect.tsx @@ -3,7 +3,6 @@ import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/ import { useLoadRecordIndexStates } from '@/object-record/record-index/hooks/useLoadRecordIndexStates'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue'; -import { useCreateDefaultViewForObject } from '@/views/hooks/useCreateDefaultViewForObject'; import { viewFromViewIdFamilySelector } from '@/views/states/selectors/viewFromViewIdFamilySelector'; import { useEffect, useState } from 'react'; import { isDefined } from 'twenty-shared/utils'; @@ -24,8 +23,6 @@ export const RecordIndexLoadBaseOnContextStoreEffect = () => { const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow(); - const { createDefaultViewForObject } = useCreateDefaultViewForObject(); - useEffect(() => { if ( isDefined(contextStoreCurrentViewId) && @@ -41,8 +38,6 @@ export const RecordIndexLoadBaseOnContextStoreEffect = () => { if (isDefined(view)) { loadRecordIndexStates(view, objectMetadataItem); setLoadedViewId(contextStoreCurrentViewId); - } else { - createDefaultViewForObject(objectMetadataItem); } }, [ contextStoreCurrentViewId, @@ -50,7 +45,6 @@ export const RecordIndexLoadBaseOnContextStoreEffect = () => { loadedViewId, objectMetadataItem, view, - createDefaultViewForObject, ]); return <>; diff --git a/packages/twenty-front/src/modules/views/hooks/useCreateDefaultViewForObject.ts b/packages/twenty-front/src/modules/views/hooks/useCreateDefaultViewForObject.ts deleted file mode 100644 index ab44b5d700..0000000000 --- a/packages/twenty-front/src/modules/views/hooks/useCreateDefaultViewForObject.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; -import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField'; -import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist'; -import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist'; -import { viewsSelector } from '@/views/states/selectors/viewsSelector'; -import { useStore } from 'jotai'; -import { useCallback } from 'react'; -import { v4 } from 'uuid'; -import { ViewType } from '~/generated-metadata/graphql'; - -const DEFAULT_VIEW_FIELD_SIZE = 180; - -const pendingViewCreations = new Set(); - -// TODO: This runtime fallback logic is temporary -// System views will later be created declaratively in the database during Standard app installation. -export const useCreateDefaultViewForObject = () => { - const { performViewAPICreate } = usePerformViewAPIPersist(); - const { performViewFieldAPICreate } = usePerformViewFieldAPIPersist(); - const store = useStore(); - - const createDefaultViewForObject = useCallback( - async (objectMetadataItem: EnrichedObjectMetadataItem) => { - const objectAlreadyHasView = store - .get(viewsSelector.atom) - .some((view) => view.objectMetadataId === objectMetadataItem.id); - - if (objectAlreadyHasView) { - return; - } - - if (pendingViewCreations.has(objectMetadataItem.id)) { - return; - } - - pendingViewCreations.add(objectMetadataItem.id); - - try { - const newViewId = v4(); - - const viewResult = await performViewAPICreate( - { - input: { - id: newViewId, - name: `All ${objectMetadataItem.labelPlural}`, - icon: objectMetadataItem.icon ?? 'IconList', - objectMetadataId: objectMetadataItem.id, - type: ViewType.TABLE, - }, - }, - objectMetadataItem.id, - ); - - if (viewResult.status !== 'successful') { - return; - } - - const eligibleFields = objectMetadataItem.fields.filter( - (field) => - field.isActive && - !isHiddenSystemField(field) && - field.name !== 'deletedAt', - ); - - const sortedFields = eligibleFields.toSorted((fieldA, fieldB) => { - const isFieldALabelIdentifier = - fieldA.id === objectMetadataItem.labelIdentifierFieldMetadataId; - const isFieldBLabelIdentifier = - fieldB.id === objectMetadataItem.labelIdentifierFieldMetadataId; - - if (isFieldALabelIdentifier) return -1; - if (isFieldBLabelIdentifier) return 1; - - return 0; - }); - - const viewFieldInputs = sortedFields.map((field, index) => ({ - id: v4(), - viewId: newViewId, - fieldMetadataId: field.id, - position: index, - size: DEFAULT_VIEW_FIELD_SIZE, - isVisible: true, - })); - - await performViewFieldAPICreate({ inputs: viewFieldInputs }); - } finally { - pendingViewCreations.delete(objectMetadataItem.id); - } - }, - [performViewAPICreate, performViewFieldAPICreate, store], - ); - - return { - createDefaultViewForObject, - }; -};