Add system view fallback (#18536)

## Context
The goal is to add a "See records" button in all objects that would
redirect to that view (this will be done in a later PR). See screenshot
below.
<img width="665" height="312" alt="Screenshot 2026-03-10 at 15 54 36"
src="https://github.com/user-attachments/assets/6e23a75b-cff0-4d93-bce8-b5481b05c6f6"
/>

## Implementation
- If a view does not exist on an object, there is a **temporary**
fallback where the frontend creates the missing view as a custom view
when going over the object index page
- System objects are now surfaced but we don't want their records to be
editable, they will be readonly (mostly, all fields will be non-editable
except for their custom fields).
- We can't create a new record of a system object, some actions are also
hidden.
- The backend now rejects if you are trying to delete the last view of
an object
This commit is contained in:
Weiko
2026-03-11 13:48:02 +01:00
committed by GitHub
parent 99f885306e
commit 00c3cd1051
31 changed files with 343 additions and 37 deletions
@@ -10,13 +10,14 @@ import { coreViewsState } from '@/views/states/coreViewState';
import { useLocation, useParams, useSearchParams } from 'react-router-dom';
import { AppPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { ViewKey } from '~/generated-metadata/graphql';
import { ViewKey, ViewType } from '~/generated-metadata/graphql';
import { isMatchingLocation } from '~/utils/isMatchingLocation';
const getViewId = (
viewIdFromQueryParams: string | null,
indexViewId?: string,
lastVisitedViewId?: string,
firstAvailableViewId?: string,
) => {
if (isDefined(viewIdFromQueryParams)) {
return viewIdFromQueryParams;
@@ -30,6 +31,10 @@ const getViewId = (
return indexViewId;
}
if (isDefined(firstAvailableViewId)) {
return firstAvailableViewId;
}
return undefined;
};
@@ -46,7 +51,7 @@ export const MainContextStoreProvider = () => {
const objectNameSingular = useParams().objectNameSingular ?? '';
const [searchParams] = useSearchParams();
const viewIdQueryParam = searchParams.get('viewId');
const viewIdQueryParamRaw = searchParams.get('viewId');
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const metadataStore = useAtomFamilyStateValue(metadataStoreState, 'views');
@@ -60,17 +65,49 @@ export const MainContextStoreProvider = () => {
const { getLastVisitedViewIdFromObjectNamePlural } = useLastVisitedView();
const lastVisitedViewId = getLastVisitedViewIdFromObjectNamePlural(
const viewIdQueryParamView = coreViews.find(
(view) => view.id === viewIdQueryParamRaw,
);
const viewIdQueryParam =
isDefined(viewIdQueryParamView) &&
viewIdQueryParamView.type !== ViewType.FIELDS_WIDGET
? viewIdQueryParamRaw
: null;
const lastVisitedViewIdRaw = getLastVisitedViewIdFromObjectNamePlural(
objectMetadataItem?.namePlural ?? '',
);
const lastVisitedView = coreViews.find(
(view) => view.id === lastVisitedViewIdRaw,
);
const lastVisitedViewId =
isDefined(lastVisitedView) &&
lastVisitedView.type !== ViewType.FIELDS_WIDGET
? lastVisitedViewIdRaw
: undefined;
const indexViewId = coreViews.find(
(view) =>
view.objectMetadataId === objectMetadataItem?.id &&
view.key === ViewKey.INDEX,
)?.id;
const viewId = getViewId(viewIdQueryParam, indexViewId, lastVisitedViewId);
const firstAvailableViewId = coreViews.find(
(view) =>
view.objectMetadataId === objectMetadataItem?.id &&
view.type !== ViewType.FIELDS_WIDGET,
)?.id;
const viewId = getViewId(
viewIdQueryParam,
indexViewId,
lastVisitedViewId,
firstAvailableViewId,
);
const showAuthModal = useShowAuthModal();
const shouldComputeContextStore =