diff --git a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventDetails.tsx b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventDetails.tsx
index c538eb7b7a..11d121cecb 100644
--- a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventDetails.tsx
+++ b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventDetails.tsx
@@ -157,10 +157,12 @@ export const CalendarEventDetails = ({
const renderField = (fieldMetadataItem: FieldMetadataItem) => {
const isReadOnly = isRecordFieldReadOnly({
isRecordReadOnly,
+ isSystemObject: objectMetadataItem.isSystem,
objectPermissions,
fieldMetadataItem: {
id: fieldMetadataItem.id,
isUIReadOnly: fieldMetadataItem.isUIReadOnly ?? false,
+ isCustom: fieldMetadataItem.isCustom ?? false,
},
});
diff --git a/packages/twenty-front/src/modules/command-menu-item/record/constants/DefaultRecordCommandMenuItemsConfig.tsx b/packages/twenty-front/src/modules/command-menu-item/record/constants/DefaultRecordCommandMenuItemsConfig.tsx
index bb533af7d7..6004afbcfc 100644
--- a/packages/twenty-front/src/modules/command-menu-item/record/constants/DefaultRecordCommandMenuItemsConfig.tsx
+++ b/packages/twenty-front/src/modules/command-menu-item/record/constants/DefaultRecordCommandMenuItemsConfig.tsx
@@ -116,8 +116,13 @@ export const DEFAULT_RECORD_COMMAND_MENU_ITEMS_CONFIG: Record<
position: 2,
isPinned: true,
Icon: IconPlus,
- shouldBeRegistered: ({ objectPermissions, hasAnySoftDeleteFilterOnView }) =>
- (objectPermissions.canUpdateObjectRecords &&
+ shouldBeRegistered: ({
+ objectMetadataItem,
+ objectPermissions,
+ hasAnySoftDeleteFilterOnView,
+ }) =>
+ (!objectMetadataItem?.isSystem &&
+ objectPermissions.canUpdateObjectRecords &&
!hasAnySoftDeleteFilterOnView) ??
false,
availableOn: [CommandMenuItemViewType.INDEX_PAGE_NO_SELECTION],
@@ -440,8 +445,10 @@ export const DEFAULT_RECORD_COMMAND_MENU_ITEMS_CONFIG: Record<
Icon: IconFileImport,
accent: 'default',
isPinned: false,
- shouldBeRegistered: ({ hasAnySoftDeleteFilterOnView }) =>
- !hasAnySoftDeleteFilterOnView,
+ shouldBeRegistered: ({
+ objectMetadataItem,
+ hasAnySoftDeleteFilterOnView,
+ }) => !objectMetadataItem?.isSystem && !hasAnySoftDeleteFilterOnView,
availableOn: [CommandMenuItemViewType.INDEX_PAGE_NO_SELECTION],
component: ,
requiredPermissionFlag: PermissionFlagType.IMPORT_CSV,
diff --git a/packages/twenty-front/src/modules/context-store/components/MainContextStoreProvider.tsx b/packages/twenty-front/src/modules/context-store/components/MainContextStoreProvider.tsx
index 51e3f5ecd2..3ed5991e4a 100644
--- a/packages/twenty-front/src/modules/context-store/components/MainContextStoreProvider.tsx
+++ b/packages/twenty-front/src/modules/context-store/components/MainContextStoreProvider.tsx
@@ -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 =
diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCustomView.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCustomView.tsx
index fd0533d21d..1e230f1508 100644
--- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCustomView.tsx
+++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCustomView.tsx
@@ -14,11 +14,13 @@ import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
+import { coreViewsFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreViewsFromObjectMetadataItemFamilySelector';
import { ViewKey } from '@/views/types/ViewKey';
import { ViewType, viewTypeIconMapping } from '@/views/types/ViewType';
import { useDestroyViewFromCurrentState } from '@/views/view-picker/hooks/useDestroyViewFromCurrentState';
import { viewPickerReferenceViewIdComponentState } from '@/views/view-picker/states/viewPickerReferenceViewIdComponentState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
import { useLingui } from '@lingui/react/macro';
import { capitalize, isDefined } from 'twenty-shared/utils';
import {
@@ -64,7 +66,13 @@ export const ObjectOptionsDropdownCustomView = ({
)
: undefined;
+ const viewsOnCurrentObject = useAtomFamilySelectorValue(
+ coreViewsFromObjectMetadataItemFamilySelector,
+ { objectMetadataItemId: objectMetadataItem.id },
+ );
+
const isDefaultView = currentView?.key === ViewKey.Index;
+ const isLastView = viewsOnCurrentObject.length <= 1;
const recordIndexCalendarLayout = useAtomStateValue(
recordIndexCalendarLayoutState,
@@ -272,14 +280,18 @@ export const ObjectOptionsDropdownCustomView = ({
onClick={() => handleDelete()}
LeftIcon={IconTrash}
text={t`Delete view`}
- disabled={isDefaultView}
+ disabled={isDefaultView || isLastView}
/>
- {isDefaultView && (
+ {(isDefaultView || isLastView) && (
{
fieldMetadataItem: {
id: 'field-123',
isUIReadOnly: false,
+ isCustom: false,
},
});
@@ -39,6 +40,7 @@ describe('isRecordFieldReadOnly', () => {
fieldMetadataItem: {
id: 'field-123',
isUIReadOnly: false,
+ isCustom: false,
},
});
@@ -57,6 +59,7 @@ describe('isRecordFieldReadOnly', () => {
fieldMetadataItem: {
id: 'field-123',
isUIReadOnly: false,
+ isCustom: false,
},
});
@@ -69,6 +72,7 @@ describe('isRecordFieldReadOnly', () => {
fieldMetadataItem: {
id: 'field-123',
isUIReadOnly: true,
+ isCustom: false,
},
});
@@ -81,6 +85,48 @@ describe('isRecordFieldReadOnly', () => {
fieldMetadataItem: {
id: 'field-123',
isUIReadOnly: false,
+ isCustom: false,
+ },
+ });
+
+ expect(result).toBe(false);
+ });
+
+ it('should return true when field is non-custom on a system object', () => {
+ const result = isRecordFieldReadOnly({
+ ...mockParams,
+ isSystemObject: true,
+ fieldMetadataItem: {
+ id: 'field-123',
+ isUIReadOnly: false,
+ isCustom: false,
+ },
+ });
+
+ expect(result).toBe(true);
+ });
+
+ it('should return false when field is custom on a system object', () => {
+ const result = isRecordFieldReadOnly({
+ ...mockParams,
+ isSystemObject: true,
+ fieldMetadataItem: {
+ id: 'field-123',
+ isUIReadOnly: false,
+ isCustom: true,
+ },
+ });
+
+ expect(result).toBe(false);
+ });
+
+ it('should return false when isSystemObject is not provided', () => {
+ const result = isRecordFieldReadOnly({
+ ...mockParams,
+ fieldMetadataItem: {
+ id: 'field-123',
+ isUIReadOnly: false,
+ isCustom: false,
},
});
diff --git a/packages/twenty-front/src/modules/object-record/read-only/utils/isRecordFieldReadOnly.ts b/packages/twenty-front/src/modules/object-record/read-only/utils/isRecordFieldReadOnly.ts
index 64ae9ce0db..a8ec3deece 100644
--- a/packages/twenty-front/src/modules/object-record/read-only/utils/isRecordFieldReadOnly.ts
+++ b/packages/twenty-front/src/modules/object-record/read-only/utils/isRecordFieldReadOnly.ts
@@ -4,13 +4,18 @@ import { type ObjectPermission } from '~/generated-metadata/graphql';
type IsRecordFieldReadOnlyParams = {
isRecordReadOnly: boolean;
- fieldMetadataItem: Pick;
+ isSystemObject?: boolean;
+ fieldMetadataItem: Pick<
+ FieldMetadataItem,
+ 'id' | 'isUIReadOnly' | 'isCustom'
+ >;
objectPermissions: ObjectPermission;
};
export const isRecordFieldReadOnly = ({
objectPermissions,
isRecordReadOnly,
+ isSystemObject,
fieldMetadataItem,
}: IsRecordFieldReadOnlyParams) => {
const fieldReadOnlyByPermissions = isFieldMetadataReadOnlyByPermissions({
@@ -20,6 +25,7 @@ export const isRecordFieldReadOnly = ({
return (
isRecordReadOnly ||
+ (isSystemObject === true && fieldMetadataItem.isCustom !== true) ||
fieldMetadataItem.isUIReadOnly ||
fieldReadOnlyByPermissions
);
diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardBody.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardBody.tsx
index 0c3d8b2d17..6e97b124af 100644
--- a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardBody.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardBody.tsx
@@ -22,7 +22,8 @@ import { useContext } from 'react';
export const RecordBoardCardBody = () => {
const { recordId, isRecordReadOnly } = useContext(RecordBoardCardContext);
- const { updateOneRecord, objectPermissions } = useContext(RecordBoardContext);
+ const { updateOneRecord, objectPermissions, objectMetadataItem } =
+ useContext(RecordBoardContext);
const {
labelIdentifierFieldMetadataItem,
@@ -72,12 +73,15 @@ export const RecordBoardCardBody = () => {
isLabelIdentifier: false,
isRecordFieldReadOnly: isRecordFieldReadOnly({
isRecordReadOnly,
+ isSystemObject: objectMetadataItem.isSystem,
objectPermissions,
fieldMetadataItem: {
id: recordField.fieldMetadataItemId,
isUIReadOnly:
correspondingFieldDefinition.metadata.isUIReadOnly ??
false,
+ isCustom:
+ correspondingFieldDefinition.metadata.isCustom ?? false,
},
}),
fieldDefinition: correspondingFieldDefinition,
diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnNewRecordButton.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnNewRecordButton.tsx
index 4f75d5701e..07442f0f6d 100644
--- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnNewRecordButton.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnNewRecordButton.tsx
@@ -52,6 +52,10 @@ export const RecordBoardColumnNewRecordButton = () => {
return null;
}
+ if (objectMetadataItem.isSystem) {
+ return null;
+ }
+
if (hasAnySoftDeleteFilterOnView) {
return null;
}
diff --git a/packages/twenty-front/src/modules/object-record/record-calendar/components/RecordCalendarAddNew.tsx b/packages/twenty-front/src/modules/object-record/record-calendar/components/RecordCalendarAddNew.tsx
index 070b3c4077..8f90621bff 100644
--- a/packages/twenty-front/src/modules/object-record/record-calendar/components/RecordCalendarAddNew.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-calendar/components/RecordCalendarAddNew.tsx
@@ -62,6 +62,7 @@ export const RecordCalendarAddNew = ({
if (
hasAnySoftDeleteFilterOnView === true ||
hasObjectUpdatePermissions === false ||
+ objectMetadataItem.isSystem === true ||
calendarFieldMetadataItem === undefined ||
isCalendarFieldReadOnly === true
) {
diff --git a/packages/twenty-front/src/modules/object-record/record-calendar/record-calendar-card/components/RecordCalendarCardBody.tsx b/packages/twenty-front/src/modules/object-record/record-calendar/record-calendar-card/components/RecordCalendarCardBody.tsx
index a3e0a81738..dc4344086d 100644
--- a/packages/twenty-front/src/modules/object-record/record-calendar/record-calendar-card/components/RecordCalendarCardBody.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-calendar/record-calendar-card/components/RecordCalendarCardBody.tsx
@@ -82,12 +82,15 @@ export const RecordCalendarCardBody = ({
isLabelIdentifier: false,
isRecordFieldReadOnly: isRecordFieldReadOnly({
isRecordReadOnly,
+ isSystemObject: objectMetadataItem.isSystem,
objectPermissions,
fieldMetadataItem: {
id: recordField.fieldMetadataItemId,
isUIReadOnly:
correspondingFieldDefinition.metadata.isUIReadOnly ??
false,
+ isCustom:
+ correspondingFieldDefinition.metadata.isCustom ?? false,
},
}),
fieldDefinition: correspondingFieldDefinition,
diff --git a/packages/twenty-front/src/modules/object-record/record-field-list/components/RecordFieldList.tsx b/packages/twenty-front/src/modules/object-record/record-field-list/components/RecordFieldList.tsx
index 66e44fefc5..f92a0af084 100644
--- a/packages/twenty-front/src/modules/object-record/record-field-list/components/RecordFieldList.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field-list/components/RecordFieldList.tsx
@@ -124,6 +124,7 @@ export const RecordFieldList = ({
})}`,
isRecordFieldReadOnly: isRecordFieldReadOnly({
isRecordReadOnly,
+ isSystemObject: objectMetadataItem.isSystem,
objectPermissions:
getObjectPermissionsFromMapByObjectMetadataId({
objectPermissionsByObjectMetadataId,
@@ -132,6 +133,7 @@ export const RecordFieldList = ({
fieldMetadataItem: {
id: fieldMetadataItem.id,
isUIReadOnly: fieldMetadataItem.isUIReadOnly ?? false,
+ isCustom: fieldMetadataItem.isCustom ?? false,
},
}),
}}
@@ -172,6 +174,7 @@ export const RecordFieldList = ({
isDisplayModeFixHeight: true,
isRecordFieldReadOnly: isRecordFieldReadOnly({
isRecordReadOnly,
+ isSystemObject: objectMetadataItem.isSystem,
objectPermissions:
getObjectPermissionsFromMapByObjectMetadataId({
objectPermissionsByObjectMetadataId,
@@ -180,6 +183,7 @@ export const RecordFieldList = ({
fieldMetadataItem: {
id: fieldMetadataItem.id,
isUIReadOnly: fieldMetadataItem.isUIReadOnly ?? false,
+ isCustom: fieldMetadataItem.isCustom ?? false,
},
}),
onMouseEnter: () =>
@@ -246,6 +250,7 @@ export const RecordFieldList = ({
isDisplayModeFixHeight: true,
isRecordFieldReadOnly: isRecordFieldReadOnly({
isRecordReadOnly,
+ isSystemObject: objectMetadataItem.isSystem,
objectPermissions:
getObjectPermissionsFromMapByObjectMetadataId({
objectPermissionsByObjectMetadataId,
@@ -254,6 +259,7 @@ export const RecordFieldList = ({
fieldMetadataItem: {
id: fieldMetadataItem.id,
isUIReadOnly: fieldMetadataItem.isUIReadOnly ?? false,
+ isCustom: fieldMetadataItem.isCustom ?? false,
},
}),
}}
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 38a33077e7..d6c97d1199 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,6 +3,7 @@ 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 { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector';
import { useEffect, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
@@ -23,8 +24,13 @@ export const RecordIndexLoadBaseOnContextStoreEffect = () => {
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
+ const { createDefaultViewForObject } = useCreateDefaultViewForObject();
+
useEffect(() => {
- if (loadedViewId === contextStoreCurrentViewId) {
+ if (
+ isDefined(contextStoreCurrentViewId) &&
+ loadedViewId === contextStoreCurrentViewId
+ ) {
return;
}
@@ -35,6 +41,8 @@ export const RecordIndexLoadBaseOnContextStoreEffect = () => {
if (isDefined(view)) {
loadRecordIndexStates(view, objectMetadataItem);
setLoadedViewId(contextStoreCurrentViewId);
+ } else {
+ createDefaultViewForObject(objectMetadataItem);
}
}, [
contextStoreCurrentViewId,
@@ -42,6 +50,7 @@ export const RecordIndexLoadBaseOnContextStoreEffect = () => {
loadedViewId,
objectMetadataItem,
view,
+ createDefaultViewForObject,
]);
return <>>;
diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupAddNew.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupAddNew.tsx
index 189458073f..8a8c90f477 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupAddNew.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupAddNew.tsx
@@ -66,7 +66,7 @@ export const RecordTableNoRecordGroupAddNew = () => {
return null;
}
- if (isRecordTableCreateDisabled(objectMetadataItem.nameSingular)) {
+ if (isRecordTableCreateDisabled(objectMetadataItem)) {
return null;
}
diff --git a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx
index 2408e46ba7..ced91b4207 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx
@@ -85,7 +85,7 @@ export const RecordTableEmptyStateDisplay = (
{'buttonTitle' in props &&
!isReadOnly &&
!hasAnySoftDeleteFilterOnView &&
- !isRecordTableCreateDisabled(objectMetadataItem.nameSingular) && (
+ !isRecordTableCreateDisabled(objectMetadataItem) && (