From 6895902ddf7d4fbf8bbc8361b90888eb02a9d881 Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Wed, 10 Sep 2025 18:01:57 +0200 Subject: [PATCH] Remove Add New button when softDelete filter is present (#14385) Closes https://github.com/twentyhq/twenty/issues/13854 In this PR - When evaluating whether there was a soft delete filter enabled, we were only taking into account the filter that shows all deleted records, that can be enabled from the side panel ("See deleted records"). Now we are also taking into account any filter on deletedAt. - We lacked some places where we should not offer to add a new record if a soft delete filter is on, ex on the empty page + in the kanban headers - I decided not to add a constraint on api-side because I think there could be use cases when importing data where we would want to be able to create soft deleted records --- .../constants/DefaultRecordActionsConfig.tsx | 54 ++++++++++--------- .../constants/WorkflowActionsConfig.tsx | 4 +- ...eDeletedRecordsNoSelectionRecordAction.tsx | 6 ++- .../types/ShouldBeRegisteredFunctionParams.ts | 2 +- .../useShouldActionBeRegisteredParams.ts | 8 +-- .../components/RecordBoardColumnHeader.tsx | 32 ++++++----- .../RecordBoardColumnNewRecordButton.tsx | 10 ++++ .../hooks/useCheckIsSoftDeleteFilter.ts | 40 ++++++-------- .../states/hasAnySoftDeleteFilterOnView.ts | 29 ++++++++++ .../utils/getRecordFilterFieldMetadataItem.ts | 28 ++++++++++ .../utils/isRecordFilterAboutSoftDelete.ts | 29 ++++++++++ .../components/RecordTableAddNew.tsx | 9 ++++ .../RecordTableEmptyStateDisplay.tsx | 26 +++++---- .../RecordTableEmptyStateSoftDelete.tsx | 6 ++- .../RecordTableHeaderLabelIdentifierCell.tsx | 8 ++- .../views/components/ViewBarDetails.tsx | 36 ++++++------- 16 files changed, 227 insertions(+), 100 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/states/hasAnySoftDeleteFilterOnView.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterFieldMetadataItem.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordFilterAboutSoftDelete.ts diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/DefaultRecordActionsConfig.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/DefaultRecordActionsConfig.tsx index 31779f2fa5..28b4c9af68 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/DefaultRecordActionsConfig.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/DefaultRecordActionsConfig.tsx @@ -74,8 +74,9 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record< position: 0, isPinned: true, Icon: IconPlus, - shouldBeRegistered: ({ objectPermissions, isSoftDeleteFilterActive }) => - (objectPermissions.canUpdateObjectRecords && !isSoftDeleteFilterActive) ?? + shouldBeRegistered: ({ objectPermissions, hasAnySoftDeleteFilterOnView }) => + (objectPermissions.canUpdateObjectRecords && + !hasAnySoftDeleteFilterOnView) ?? false, availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION], component: , @@ -108,12 +109,12 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record< shouldBeRegistered: ({ selectedRecord, isFavorite, - isSoftDeleteFilterActive, + hasAnySoftDeleteFilterOnView, }) => !selectedRecord?.isRemote && !isFavorite && !isDefined(selectedRecord?.deletedAt) && - !isSoftDeleteFilterActive, + !hasAnySoftDeleteFilterOnView, availableOn: [ ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, ActionViewType.SHOW_PAGE, @@ -132,14 +133,14 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record< shouldBeRegistered: ({ selectedRecord, isFavorite, - isSoftDeleteFilterActive, + hasAnySoftDeleteFilterOnView, }) => isDefined(selectedRecord) && !selectedRecord?.isRemote && isDefined(isFavorite) && isFavorite && !isDefined(selectedRecord?.deletedAt) && - !isSoftDeleteFilterActive, + !hasAnySoftDeleteFilterOnView, availableOn: [ ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, ActionViewType.SHOW_PAGE, @@ -231,8 +232,8 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record< Icon: IconFileImport, accent: 'default', isPinned: false, - shouldBeRegistered: ({ isSoftDeleteFilterActive }) => - !isSoftDeleteFilterActive, + shouldBeRegistered: ({ hasAnySoftDeleteFilterOnView }) => + !hasAnySoftDeleteFilterOnView, availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION], component: , requiredPermissionFlag: PermissionFlagType.IMPORT_CSV, @@ -264,12 +265,12 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record< isPinned: true, shouldBeRegistered: ({ selectedRecord, - isSoftDeleteFilterActive, + hasAnySoftDeleteFilterOnView, objectPermissions, }) => (isDefined(selectedRecord) && !selectedRecord.isRemote && - !isSoftDeleteFilterActive && + !hasAnySoftDeleteFilterOnView && objectPermissions.canSoftDeleteObjectRecords && !isDefined(selectedRecord?.deletedAt)) ?? false, @@ -292,12 +293,12 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record< shouldBeRegistered: ({ objectPermissions, isRemote, - isSoftDeleteFilterActive, + hasAnySoftDeleteFilterOnView, numberOfSelectedRecords, }) => (objectPermissions.canSoftDeleteObjectRecords && !isRemote && - !isSoftDeleteFilterActive && + !hasAnySoftDeleteFilterOnView && isDefined(numberOfSelectedRecords) && numberOfSelectedRecords < BACKEND_BATCH_REQUEST_MAX_COUNT) ?? false, @@ -314,8 +315,8 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record< Icon: IconRotate2, accent: 'default', isPinned: false, - shouldBeRegistered: ({ isSoftDeleteFilterActive }) => - !isSoftDeleteFilterActive, + shouldBeRegistered: ({ hasAnySoftDeleteFilterOnView }) => + !hasAnySoftDeleteFilterOnView, availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION], component: , }, @@ -329,8 +330,8 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record< Icon: IconLayout, accent: 'default', isPinned: false, - shouldBeRegistered: ({ isSoftDeleteFilterActive }) => - !isSoftDeleteFilterActive, + shouldBeRegistered: ({ hasAnySoftDeleteFilterOnView }) => + !hasAnySoftDeleteFilterOnView, availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION], component: , }, @@ -344,8 +345,8 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record< Icon: IconEyeOff, accent: 'default', isPinned: false, - shouldBeRegistered: ({ isSoftDeleteFilterActive }) => - isDefined(isSoftDeleteFilterActive) && isSoftDeleteFilterActive, + shouldBeRegistered: ({ hasAnySoftDeleteFilterOnView }) => + isDefined(hasAnySoftDeleteFilterOnView) && hasAnySoftDeleteFilterOnView, availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION], component: , }, @@ -407,13 +408,13 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record< shouldBeRegistered: ({ objectPermissions, isRemote, - isSoftDeleteFilterActive, + hasAnySoftDeleteFilterOnView, numberOfSelectedRecords, }) => (objectPermissions.canDestroyObjectRecords && !isRemote && - isDefined(isSoftDeleteFilterActive) && - isSoftDeleteFilterActive && + isDefined(hasAnySoftDeleteFilterOnView) && + hasAnySoftDeleteFilterOnView && isDefined(numberOfSelectedRecords) && numberOfSelectedRecords < BACKEND_BATCH_REQUEST_MAX_COUNT) ?? false, @@ -435,13 +436,14 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record< objectPermissions, isRemote, isShowPage, - isSoftDeleteFilterActive, + hasAnySoftDeleteFilterOnView, }) => (!isRemote && isDefined(selectedRecord?.deletedAt) && objectPermissions.canSoftDeleteObjectRecords && ((isDefined(isShowPage) && isShowPage) || - (isDefined(isSoftDeleteFilterActive) && isSoftDeleteFilterActive))) ?? + (isDefined(hasAnySoftDeleteFilterOnView) && + hasAnySoftDeleteFilterOnView))) ?? false, availableOn: [ ActionViewType.SHOW_PAGE, @@ -462,13 +464,13 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record< shouldBeRegistered: ({ objectPermissions, isRemote, - isSoftDeleteFilterActive, + hasAnySoftDeleteFilterOnView, numberOfSelectedRecords, }) => (objectPermissions.canSoftDeleteObjectRecords && !isRemote && - isDefined(isSoftDeleteFilterActive) && - isSoftDeleteFilterActive && + isDefined(hasAnySoftDeleteFilterOnView) && + hasAnySoftDeleteFilterOnView && isDefined(numberOfSelectedRecords) && numberOfSelectedRecords < BACKEND_BATCH_REQUEST_MAX_COUNT) ?? false, diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/WorkflowActionsConfig.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/WorkflowActionsConfig.tsx index 7933801486..8058d50705 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/WorkflowActionsConfig.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/WorkflowActionsConfig.tsx @@ -202,8 +202,8 @@ export const WORKFLOW_ACTIONS_CONFIG = inheritActionsFromDefaultConfig({ Icon: IconHistoryToggle, accent: 'default', isPinned: true, - shouldBeRegistered: ({ isSoftDeleteFilterActive }) => - !isSoftDeleteFilterActive, + shouldBeRegistered: ({ hasAnySoftDeleteFilterOnView }) => + !hasAnySoftDeleteFilterOnView, availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION], component: ( { viewBarId: recordIndexId, }); - const { checkIsSoftDeleteFilter } = useCheckIsSoftDeleteFilter(); + const { isRecordFilterAboutSoftDelete } = useCheckIsSoftDeleteFilter(); const currentRecordFilters = useRecoilComponentValue( currentRecordFiltersComponentState, recordIndexId, ); - const deletedFilter = currentRecordFilters.find(checkIsSoftDeleteFilter); + const deletedFilter = currentRecordFilters.find( + isRecordFilterAboutSoftDelete, + ); const { removeRecordFilter } = useRemoveRecordFilter(); diff --git a/packages/twenty-front/src/modules/action-menu/actions/types/ShouldBeRegisteredFunctionParams.ts b/packages/twenty-front/src/modules/action-menu/actions/types/ShouldBeRegisteredFunctionParams.ts index 0567448a21..4f4aa8fd49 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/types/ShouldBeRegisteredFunctionParams.ts +++ b/packages/twenty-front/src/modules/action-menu/actions/types/ShouldBeRegisteredFunctionParams.ts @@ -10,7 +10,7 @@ export type ShouldBeRegisteredFunctionParams = { objectPermissions: ObjectPermissions; recordFilters?: RecordFilter[]; isShowPage?: boolean; - isSoftDeleteFilterActive?: boolean; + hasAnySoftDeleteFilterOnView?: boolean; isInRightDrawer?: boolean; isFavorite?: boolean; isRemote?: boolean; diff --git a/packages/twenty-front/src/modules/action-menu/hooks/useShouldActionBeRegisteredParams.ts b/packages/twenty-front/src/modules/action-menu/hooks/useShouldActionBeRegisteredParams.ts index c7600e6c00..6ecb29f9d3 100644 --- a/packages/twenty-front/src/modules/action-menu/hooks/useShouldActionBeRegisteredParams.ts +++ b/packages/twenty-front/src/modules/action-menu/hooks/useShouldActionBeRegisteredParams.ts @@ -10,8 +10,8 @@ import { useFavorites } from '@/favorites/hooks/useFavorites'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject'; +import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; -import { isSoftDeleteFilterActiveComponentState } from '@/object-record/record-table/states/isSoftDeleteFilterActiveComponentState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useContext } from 'react'; import { useRecoilCallback, useRecoilValue } from 'recoil'; @@ -51,8 +51,8 @@ export const useShouldActionBeRegisteredParams = ({ const { isInRightDrawer } = useContext(ActionMenuContext); - const isSoftDeleteFilterActive = useRecoilComponentValue( - isSoftDeleteFilterActiveComponentState, + const hasAnySoftDeleteFilterOnView = useRecoilComponentValue( + hasAnySoftDeleteFilterOnViewComponentSelector, ); const isShowPage = @@ -106,7 +106,7 @@ export const useShouldActionBeRegisteredParams = ({ objectPermissions, isNoteOrTask, isInRightDrawer, - isSoftDeleteFilterActive, + hasAnySoftDeleteFilterOnView, isShowPage, selectedRecord, numberOfSelectedRecords, diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx index b7fd2b506e..7b6301466d 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx @@ -7,10 +7,12 @@ import { RecordBoardColumnDropdownMenu } from '@/object-record/record-board/reco import { RecordBoardColumnHeaderAggregateDropdown } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; import { useAggregateRecordsForRecordBoardColumn } from '@/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn'; +import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView'; import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition'; import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { useToggleDropdown } from '@/ui/layout/dropdown/hooks/useToggleDropdown'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { Tag } from 'twenty-ui/components'; import { IconDotsVertical, IconPlus } from 'twenty-ui/display'; import { LightIconButton } from 'twenty-ui/input'; @@ -80,6 +82,10 @@ export const RecordBoardColumnHeader = () => { const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords; + const hasAnySoftDeleteFilterOnView = useRecoilComponentValue( + hasAnySoftDeleteFilterOnViewComponentSelector, + ); + const { createNewIndexRecord } = useCreateNewIndexRecord({ objectMetadataItem: objectMetadataItem, }); @@ -145,18 +151,20 @@ export const RecordBoardColumnHeader = () => { }); }} /> - {hasObjectUpdatePermissions && ( - { - createNewIndexRecord({ - position: 'first', - [selectFieldMetadataItem.name]: columnDefinition.value, - }); - }} - /> - )} + {hasObjectUpdatePermissions && + !hasAnySoftDeleteFilterOnView && ( + { + createNewIndexRecord({ + position: 'first', + [selectFieldMetadataItem.name]: + columnDefinition.value, + }); + }} + /> + )} )} 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 3407909131..ce62e0b46b 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 @@ -1,7 +1,9 @@ import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; +import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView'; import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { useContext } from 'react'; @@ -32,6 +34,10 @@ export const RecordBoardColumnNewRecordButton = () => { const { columnDefinition } = useContext(RecordBoardColumnContext); + const hasAnySoftDeleteFilterOnView = useRecoilComponentValue( + hasAnySoftDeleteFilterOnViewComponentSelector, + ); + const objectPermissions = useObjectPermissionsForObject( objectMetadataItem.id, ); @@ -46,6 +52,10 @@ export const RecordBoardColumnNewRecordButton = () => { return null; } + if (hasAnySoftDeleteFilterOnView) { + return null; + } + return ( { diff --git a/packages/twenty-front/src/modules/object-record/record-filter/hooks/useCheckIsSoftDeleteFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/hooks/useCheckIsSoftDeleteFilter.ts index 4280974cb5..2ab17935a8 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/hooks/useCheckIsSoftDeleteFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/hooks/useCheckIsSoftDeleteFilter.ts @@ -2,39 +2,33 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat import { SOFT_DELETE_FILTER_FIELD_NAME } from '@/object-record/record-filter/constants/SoftDeleteFilterFieldName'; import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand'; -import { isSoftDeleteFilterActiveComponentState } from '@/object-record/record-table/states/isSoftDeleteFilterActiveComponentState'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; -import { isDefined } from 'twenty-shared/utils'; +import { getRecordFilterFieldMetadataItem } from '@/object-record/record-filter/utils/getRecordFilterFieldMetadataItem'; +import { isRecordFilterAboutSoftDelete as isRecordFilterAboutSoftDeleteUtil } from '@/object-record/record-filter/utils/isRecordFilterAboutSoftDelete'; export const useCheckIsSoftDeleteFilter = () => { const { objectMetadataItems } = useObjectMetadataItems(); - const isSoftDeleteFilterActive = useRecoilComponentValue( - isSoftDeleteFilterActiveComponentState, - ); + const isRecordFilterAboutSoftDelete = (recordFilter: RecordFilter) => { + return isRecordFilterAboutSoftDeleteUtil({ + recordFilter, + objectMetadataItems, + }); + }; - const checkIsSoftDeleteFilter = (recordFilter: RecordFilter) => { - const allFieldMetadataItems = objectMetadataItems.flatMap( - (objectMetadataItem) => objectMetadataItem.fields, - ); + const isSeeDeletedRecordsFilter = (recordFilter: RecordFilter) => { + const foundFieldMetadataItem = getRecordFilterFieldMetadataItem({ + recordFilter, + objectMetadataItems, + }); - const foundFieldMetadataItem = allFieldMetadataItems.find( - (fieldMetadataItem) => - fieldMetadataItem.id === recordFilter.fieldMetadataId, - ); - - if (!isDefined(foundFieldMetadataItem)) { - throw new Error( - `Field metadata item not found for field metadata id: ${recordFilter.fieldMetadataId}`, - ); - } + const isNotEmptyFilter = + recordFilter.operand === RecordFilterOperand.IsNotEmpty; return ( foundFieldMetadataItem.name === SOFT_DELETE_FILTER_FIELD_NAME && - isSoftDeleteFilterActive && - recordFilter.operand === RecordFilterOperand.IsNotEmpty + isNotEmptyFilter ); }; - return { checkIsSoftDeleteFilter }; + return { isRecordFilterAboutSoftDelete, isSeeDeletedRecordsFilter }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/states/hasAnySoftDeleteFilterOnView.ts b/packages/twenty-front/src/modules/object-record/record-filter/states/hasAnySoftDeleteFilterOnView.ts new file mode 100644 index 0000000000..0b223812e8 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/states/hasAnySoftDeleteFilterOnView.ts @@ -0,0 +1,29 @@ +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext'; +import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; +import { isRecordFilterAboutSoftDelete } from '@/object-record/record-filter/utils/isRecordFilterAboutSoftDelete'; +import { createComponentSelector } from '@/ui/utilities/state/component-state/utils/createComponentSelector'; + +export const hasAnySoftDeleteFilterOnViewComponentSelector = + createComponentSelector({ + key: 'hasAnySoftDeleteFilterOnViewComponentSelector', + componentInstanceContext: RecordFiltersComponentInstanceContext, + get: + ({ instanceId }) => + ({ get }) => { + const objectMetadataItems = get(objectMetadataItemsState); + const currentRecordFilters = get( + currentRecordFiltersComponentState.atomFamily({ instanceId }), + ); + + const hasAnySoftDeleteFilterOnView = currentRecordFilters.some( + (recordFilter) => { + return isRecordFilterAboutSoftDelete({ + recordFilter, + objectMetadataItems, + }); + }, + ); + return hasAnySoftDeleteFilterOnView; + }, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterFieldMetadataItem.ts new file mode 100644 index 0000000000..4757fb6d9d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterFieldMetadataItem.ts @@ -0,0 +1,28 @@ +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; +import { isDefined } from 'twenty-shared/utils'; + +export const getRecordFilterFieldMetadataItem = ({ + recordFilter, + objectMetadataItems, +}: { + recordFilter: RecordFilter; + objectMetadataItems: ObjectMetadataItem[]; +}) => { + const allFieldMetadataItems = objectMetadataItems.flatMap( + (objectMetadataItem) => objectMetadataItem.fields, + ); + + const foundFieldMetadataItem = allFieldMetadataItems.find( + (fieldMetadataItem) => + fieldMetadataItem.id === recordFilter.fieldMetadataId, + ); + + if (!isDefined(foundFieldMetadataItem)) { + throw new Error( + `Field metadata item not found for field metadata id: ${recordFilter.fieldMetadataId}`, + ); + } + + return foundFieldMetadataItem; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordFilterAboutSoftDelete.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordFilterAboutSoftDelete.ts new file mode 100644 index 0000000000..cb73cddd43 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordFilterAboutSoftDelete.ts @@ -0,0 +1,29 @@ +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { SOFT_DELETE_FILTER_FIELD_NAME } from '@/object-record/record-filter/constants/SoftDeleteFilterFieldName'; +import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; +import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand'; +import { getRecordFilterFieldMetadataItem } from '@/object-record/record-filter/utils/getRecordFilterFieldMetadataItem'; +import isEmpty from 'lodash.isempty'; + +export const isRecordFilterAboutSoftDelete = ({ + recordFilter, + objectMetadataItems, +}: { + recordFilter: RecordFilter; + objectMetadataItems: ObjectMetadataItem[]; +}) => { + const foundFieldMetadataItem = getRecordFilterFieldMetadataItem({ + recordFilter, + objectMetadataItems, + }); + + const valueIsNotEmptyFilter = + (recordFilter.operand === RecordFilterOperand.Is && + !isEmpty(recordFilter.value)) || + recordFilter.operand === RecordFilterOperand.IsNotEmpty; + + return ( + foundFieldMetadataItem.name === SOFT_DELETE_FILTER_FIELD_NAME && + valueIsNotEmptyFilter + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableAddNew.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableAddNew.tsx index ba4f500f54..2620263854 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableAddNew.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableAddNew.tsx @@ -1,4 +1,5 @@ import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject'; +import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord'; import { RecordTableActionRow } from '@/object-record/record-table/record-table-row/components/RecordTableActionRow'; @@ -24,6 +25,14 @@ export const RecordTableAddNew = () => { const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords; + const hasAnySoftDeleteFilterOnView = useRecoilComponentValue( + hasAnySoftDeleteFilterOnViewComponentSelector, + ); + + if (hasAnySoftDeleteFilterOnView) { + return null; + } + if (!hasObjectUpdatePermissions || !hasRecordTableFetchedAllRecords) { 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 acd024b12b..d9d5eb3064 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 @@ -1,6 +1,8 @@ import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject'; import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly'; +import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { type IconComponent } from 'twenty-ui/display'; import { Button } from 'twenty-ui/input'; import { @@ -44,6 +46,10 @@ export const RecordTableEmptyStateDisplay = ( objectMetadataItem, }); + const hasAnySoftDeleteFilterOnView = useRecoilComponentValue( + hasAnySoftDeleteFilterOnViewComponentSelector, + ); + return ( @@ -56,15 +62,17 @@ export const RecordTableEmptyStateDisplay = ( {'buttonComponent' in props && props.buttonComponent} - {'buttonTitle' in props && !isReadOnly && ( -