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:
-6
@@ -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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user