fix(front): remove runtime default-view creation fallback (#21652)

## 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.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21652?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Charles Bochet
2026-06-16 11:08:48 +02:00
committed by GitHub
parent e83fa2108d
commit 14d8105f22
2 changed files with 0 additions and 103 deletions
@@ -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 <></>;
@@ -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<string>();
// 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,
};
};