From 1d627039c0e4c03800b090c493b1014f3900ccaf Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Thu, 19 Dec 2024 17:00:30 +0100 Subject: [PATCH] Add possibility to destroy a record (#9144) There are two follow ups to this PR: - Bug: sometimes when opening Cmd+K from a deleted record, we are facing a global error - On Index page, actions in top right are displaying label and not short name - Implement multiple actions once refactoring on delete is complete --------- Co-authored-by: bosiraphael --- .../hooks/useDeleteMultipleRecordsAction.tsx | 7 +- .../hooks/useExportMultipleRecordsAction.tsx | 1 + .../DefaultSingleRecordActionsConfigV2.ts | 22 +++++- .../useAddToFavoritesSingleRecordAction.ts | 4 +- .../hooks/useDeleteSingleRecordAction.tsx | 8 +- .../hooks/useDestroySingleRecordAction.tsx | 73 +++++++++++++++++++ .../RecordIndexActionMenuButtons.tsx | 2 +- ...seFindManyRecordsSelectedInContextStore.ts | 1 + .../object-record/hooks/useFindManyRecords.ts | 11 ++- .../src/testing/mock-data/people.ts | 1 + .../display/icon/components/TablerIcons.ts | 1 + .../input/button/components/IconButton.tsx | 2 +- 12 files changed, 123 insertions(+), 10 deletions(-) create mode 100644 packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useDestroySingleRecordAction.tsx diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useDeleteMultipleRecordsAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useDeleteMultipleRecordsAction.tsx index 7e2c5a405c30..19fa6d75e36d 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useDeleteMultipleRecordsAction.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useDeleteMultipleRecordsAction.tsx @@ -11,16 +11,16 @@ import { computeContextStoreFilters } from '@/context-store/utils/computeContext import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite'; import { useFavorites } from '@/favorites/hooks/useFavorites'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize'; import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount'; import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; +import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useCallback, useContext, useState } from 'react'; import { IconTrash, isDefined } from 'twenty-ui'; -import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords'; -import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize'; export const useDeleteMultipleRecordsAction = ({ objectMetadataItem, @@ -118,7 +118,8 @@ export const useDeleteMultipleRecordsAction = ({ type: ActionMenuEntryType.Standard, scope: ActionMenuEntryScope.RecordSelection, key: 'delete-multiple-records', - label: 'Delete', + label: 'Delete records', + shortLabel: 'Delete', position, Icon: IconTrash, accent: 'danger', diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useExportMultipleRecordsAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useExportMultipleRecordsAction.tsx index b8cebf5fc247..935c34db1c8a 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useExportMultipleRecordsAction.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useExportMultipleRecordsAction.tsx @@ -36,6 +36,7 @@ export const useExportMultipleRecordsAction = ({ key: 'export-multiple-records', position, label: displayedExportProgress(progress), + shortLabel: 'Export', Icon: IconDatabaseExport, accent: 'default', onClick: () => download(), diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/constants/DefaultSingleRecordActionsConfigV2.ts b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/constants/DefaultSingleRecordActionsConfigV2.ts index 98477145621e..b22f7e14f1ac 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/constants/DefaultSingleRecordActionsConfigV2.ts +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/constants/DefaultSingleRecordActionsConfigV2.ts @@ -1,5 +1,6 @@ import { useAddToFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useAddToFavoritesSingleRecordAction'; import { useDeleteSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction'; +import { useDestroySingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDestroySingleRecordAction'; import { useNavigateToNextRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToNextRecordSingleRecordAction'; import { useNavigateToPreviousRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToPreviousRecordSingleRecordAction'; import { useRemoveFromFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useRemoveFromFavoritesSingleRecordAction'; @@ -16,6 +17,7 @@ import { IconHeart, IconHeartOff, IconTrash, + IconTrashX, } from 'twenty-ui'; export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record< @@ -70,13 +72,29 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record< ], actionHook: useDeleteSingleRecordAction, }, + destroySingleRecord: { + type: ActionMenuEntryType.Standard, + scope: ActionMenuEntryScope.RecordSelection, + key: 'destroy-single-record', + label: 'Permanently destroy record', + shortLabel: 'Destroy', + position: 3, + Icon: IconTrashX, + accent: 'danger', + isPinned: true, + availableOn: [ + ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION, + ActionAvailableOn.SHOW_PAGE, + ], + actionHook: useDestroySingleRecordAction, + }, navigateToPreviousRecord: { type: ActionMenuEntryType.Standard, scope: ActionMenuEntryScope.RecordSelection, key: 'navigate-to-previous-record', label: 'Navigate to previous record', shortLabel: '', - position: 3, + position: 4, isPinned: true, Icon: IconChevronUp, availableOn: [ActionAvailableOn.SHOW_PAGE], @@ -88,7 +106,7 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record< key: 'navigate-to-next-record', label: 'Navigate to next record', shortLabel: '', - position: 4, + position: 5, isPinned: true, Icon: IconChevronDown, availableOn: [ActionAvailableOn.SHOW_PAGE], diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useAddToFavoritesSingleRecordAction.ts b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useAddToFavoritesSingleRecordAction.ts index da9ee0be9e25..88c8f4c1928e 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useAddToFavoritesSingleRecordAction.ts +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useAddToFavoritesSingleRecordAction.ts @@ -2,6 +2,7 @@ import { SingleRecordActionHookWithObjectMetadataItem } from '@/action-menu/acti import { useCreateFavorite } from '@/favorites/hooks/useCreateFavorite'; import { useFavorites } from '@/favorites/hooks/useFavorites'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; +import { isNull } from '@sniptt/guards'; import { useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-ui'; @@ -23,7 +24,8 @@ export const useAddToFavoritesSingleRecordAction: SingleRecordActionHookWithObje isDefined(objectMetadataItem) && isDefined(selectedRecord) && !objectMetadataItem.isRemote && - !isFavorite; + !isFavorite && + isNull(selectedRecord.deletedAt); const onClick = () => { if (!shouldBeRegistered) { diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction.tsx index 2a8981ee093e..cb58be678a32 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction.tsx @@ -3,10 +3,13 @@ import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite'; import { useFavorites } from '@/favorites/hooks/useFavorites'; import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; +import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; +import { isNull } from '@sniptt/guards'; import { useCallback, useContext, useState } from 'react'; +import { useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-ui'; export const useDeleteSingleRecordAction: SingleRecordActionHookWithObjectMetadataItem = @@ -22,6 +25,8 @@ export const useDeleteSingleRecordAction: SingleRecordActionHookWithObjectMetada objectNameSingular: objectMetadataItem.nameSingular, }); + const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId)); + const { sortedFavorites: favorites } = useFavorites(); const { deleteFavorite } = useDeleteFavorite(); @@ -52,7 +57,8 @@ export const useDeleteSingleRecordAction: SingleRecordActionHookWithObjectMetada const { isInRightDrawer, onActionExecutedCallback } = useContext(ActionMenuContext); - const shouldBeRegistered = !isRemoteObject; + const shouldBeRegistered = + !isRemoteObject && isNull(selectedRecord?.deletedAt); const onClick = () => { if (!shouldBeRegistered) { diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useDestroySingleRecordAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useDestroySingleRecordAction.tsx new file mode 100644 index 000000000000..c024df812f5e --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useDestroySingleRecordAction.tsx @@ -0,0 +1,73 @@ +import { SingleRecordActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook'; +import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; +import { useDestroyOneRecord } from '@/object-record/hooks/useDestroyOneRecord'; +import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; +import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; +import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; +import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; +import { useCallback, useContext, useState } from 'react'; +import { useRecoilValue } from 'recoil'; +import { isDefined } from 'twenty-ui'; + +export const useDestroySingleRecordAction: SingleRecordActionHookWithObjectMetadataItem = + ({ recordId, objectMetadataItem }) => { + const [isDestroyRecordsModalOpen, setIsDestroyRecordsModalOpen] = + useState(false); + + const { resetTableRowSelection } = useRecordTable({ + recordTableId: objectMetadataItem.namePlural, + }); + + const { destroyOneRecord } = useDestroyOneRecord({ + objectNameSingular: objectMetadataItem.nameSingular, + }); + + const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId)); + + const { closeRightDrawer } = useRightDrawer(); + + const handleDeleteClick = useCallback(async () => { + resetTableRowSelection(); + + await destroyOneRecord(recordId); + }, [resetTableRowSelection, destroyOneRecord, recordId]); + + const isRemoteObject = objectMetadataItem.isRemote; + + const { isInRightDrawer, onActionExecutedCallback } = + useContext(ActionMenuContext); + + const shouldBeRegistered = + !isRemoteObject && isDefined(selectedRecord?.deletedAt); + + const onClick = () => { + if (!shouldBeRegistered) { + return; + } + + setIsDestroyRecordsModalOpen(true); + }; + + return { + shouldBeRegistered, + onClick, + ConfirmationModal: ( + { + await handleDeleteClick(); + onActionExecutedCallback?.(); + if (isInRightDrawer) { + closeRightDrawer(); + } + }} + deleteButtonText={'Permanently Destroy Record'} + /> + ), + }; + }; diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuButtons.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuButtons.tsx index 34090f9922d4..fdafd08d2c9b 100644 --- a/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuButtons.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuButtons.tsx @@ -27,7 +27,7 @@ export const RecordIndexActionMenuButtons = () => { size="small" variant="secondary" accent="default" - title={entry.label} + title={entry.shortLabel} onClick={() => entry.onClick?.()} ariaLabel={entry.label} /> diff --git a/packages/twenty-front/src/modules/context-store/hooks/useFindManyRecordsSelectedInContextStore.ts b/packages/twenty-front/src/modules/context-store/hooks/useFindManyRecordsSelectedInContextStore.ts index 4d6867e6d0f3..30b095a30189 100644 --- a/packages/twenty-front/src/modules/context-store/hooks/useFindManyRecordsSelectedInContextStore.ts +++ b/packages/twenty-front/src/modules/context-store/hooks/useFindManyRecordsSelectedInContextStore.ts @@ -39,6 +39,7 @@ export const useFindManyRecordsSelectedInContextStore = ({ const { records, loading, totalCount } = useFindManyRecords({ objectNameSingular: objectMetadataItem.nameSingular, filter: queryFilter, + withSoftDeleted: true, orderBy: [ { position: 'AscNullsFirst', diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts index 0dac0caa6d68..b72f7ed255e2 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts @@ -20,6 +20,7 @@ export type UseFindManyRecordsParams = ObjectMetadataItemIdentifier & skip?: boolean; recordGqlFields?: RecordGqlOperationGqlRecordFields; fetchPolicy?: WatchQueryFetchPolicy; + withSoftDeleted?: boolean; }; export const useFindManyRecords = ({ @@ -33,6 +34,7 @@ export const useFindManyRecords = ({ onError, onCompleted, cursorFilter, + withSoftDeleted = false, }: UseFindManyRecordsParams) => { const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular, @@ -61,11 +63,18 @@ export const useFindManyRecords = ({ onCompleted, }); + const withSoftDeleterFilter = { + or: [{ deletedAt: { is: 'NULL' } }, { deletedAt: { is: 'NOT_NULL' } }], + }; + const { data, loading, error, fetchMore } = useQuery(findManyRecordsQuery, { skip: skip || !objectMetadataItem, variables: { - filter, + filter: { + ...filter, + ...(withSoftDeleted ? withSoftDeleterFilter : {}), + }, orderBy, lastCursor: cursorFilter?.cursor ?? undefined, limit: cursorFilter?.limit ?? limit, diff --git a/packages/twenty-front/src/testing/mock-data/people.ts b/packages/twenty-front/src/testing/mock-data/people.ts index f09fc18aff47..47ef0afb4539 100644 --- a/packages/twenty-front/src/testing/mock-data/people.ts +++ b/packages/twenty-front/src/testing/mock-data/people.ts @@ -45,6 +45,7 @@ export const peopleQueryResult: { people: RecordGqlConnection } = { __typename: 'Person', createdAt: '2024-08-02T09:52:46.814Z', city: 'ASd', + deletedAt: null, phones: { primaryPhoneNumber: '781234562', primaryPhoneCountryCode: 'FR', diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts index 595f1abf228b..10125f41cbce 100644 --- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts +++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts @@ -244,6 +244,7 @@ export { IconTextWrap, IconTimelineEvent, IconTrash, + IconTrashX, IconUnlink, IconUpload, IconUser, diff --git a/packages/twenty-ui/src/input/button/components/IconButton.tsx b/packages/twenty-ui/src/input/button/components/IconButton.tsx index 93bc7e79f7ae..1501bc410142 100644 --- a/packages/twenty-ui/src/input/button/components/IconButton.tsx +++ b/packages/twenty-ui/src/input/button/components/IconButton.tsx @@ -117,7 +117,7 @@ const StyledButton = styled.button< border-color: ${variant === 'secondary' ? !disabled && focus ? theme.color.blue - : theme.background.transparent.light + : theme.background.transparent.medium : focus ? theme.color.blue : 'transparent'};