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 31779f2fa59..28b4c9af685 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 79338014865..8058d507050 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 0567448a21d..4f4aa8fd492 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 c7600e6c00c..6ecb29f9d38 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 b7fd2b506e9..7b6301466d2 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 34079091313..ce62e0b46b6 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 4280974cb52..2ab17935a85 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 00000000000..0b223812e8a --- /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 00000000000..4757fb6d9de --- /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 00000000000..cb73cddd431 --- /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 ba4f500f548..26202638540 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 acd024b12be..d9d5eb3064c 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 && ( -