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) && (