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 <raphael.bosi@gmail.com>
This commit is contained in:
Charles Bochet 2024-12-19 17:00:30 +01:00 committed by GitHub
parent 360c34fd18
commit 1d627039c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 123 additions and 10 deletions

View File

@ -11,16 +11,16 @@ import { computeContextStoreFilters } from '@/context-store/utils/computeContext
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite'; import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
import { useFavorites } from '@/favorites/hooks/useFavorites'; import { useFavorites } from '@/favorites/hooks/useFavorites';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; 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 { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount';
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useCallback, useContext, useState } from 'react'; import { useCallback, useContext, useState } from 'react';
import { IconTrash, isDefined } from 'twenty-ui'; 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 = ({ export const useDeleteMultipleRecordsAction = ({
objectMetadataItem, objectMetadataItem,
@ -118,7 +118,8 @@ export const useDeleteMultipleRecordsAction = ({
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
key: 'delete-multiple-records', key: 'delete-multiple-records',
label: 'Delete', label: 'Delete records',
shortLabel: 'Delete',
position, position,
Icon: IconTrash, Icon: IconTrash,
accent: 'danger', accent: 'danger',

View File

@ -36,6 +36,7 @@ export const useExportMultipleRecordsAction = ({
key: 'export-multiple-records', key: 'export-multiple-records',
position, position,
label: displayedExportProgress(progress), label: displayedExportProgress(progress),
shortLabel: 'Export',
Icon: IconDatabaseExport, Icon: IconDatabaseExport,
accent: 'default', accent: 'default',
onClick: () => download(), onClick: () => download(),

View File

@ -1,5 +1,6 @@
import { useAddToFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useAddToFavoritesSingleRecordAction'; 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 { 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 { useNavigateToNextRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToNextRecordSingleRecordAction';
import { useNavigateToPreviousRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToPreviousRecordSingleRecordAction'; import { useNavigateToPreviousRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToPreviousRecordSingleRecordAction';
import { useRemoveFromFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useRemoveFromFavoritesSingleRecordAction'; import { useRemoveFromFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useRemoveFromFavoritesSingleRecordAction';
@ -16,6 +17,7 @@ import {
IconHeart, IconHeart,
IconHeartOff, IconHeartOff,
IconTrash, IconTrash,
IconTrashX,
} from 'twenty-ui'; } from 'twenty-ui';
export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record< export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
@ -70,13 +72,29 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
], ],
actionHook: useDeleteSingleRecordAction, 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: { navigateToPreviousRecord: {
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
key: 'navigate-to-previous-record', key: 'navigate-to-previous-record',
label: 'Navigate to previous record', label: 'Navigate to previous record',
shortLabel: '', shortLabel: '',
position: 3, position: 4,
isPinned: true, isPinned: true,
Icon: IconChevronUp, Icon: IconChevronUp,
availableOn: [ActionAvailableOn.SHOW_PAGE], availableOn: [ActionAvailableOn.SHOW_PAGE],
@ -88,7 +106,7 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
key: 'navigate-to-next-record', key: 'navigate-to-next-record',
label: 'Navigate to next record', label: 'Navigate to next record',
shortLabel: '', shortLabel: '',
position: 4, position: 5,
isPinned: true, isPinned: true,
Icon: IconChevronDown, Icon: IconChevronDown,
availableOn: [ActionAvailableOn.SHOW_PAGE], availableOn: [ActionAvailableOn.SHOW_PAGE],

View File

@ -2,6 +2,7 @@ import { SingleRecordActionHookWithObjectMetadataItem } from '@/action-menu/acti
import { useCreateFavorite } from '@/favorites/hooks/useCreateFavorite'; import { useCreateFavorite } from '@/favorites/hooks/useCreateFavorite';
import { useFavorites } from '@/favorites/hooks/useFavorites'; import { useFavorites } from '@/favorites/hooks/useFavorites';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { isNull } from '@sniptt/guards';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
@ -23,7 +24,8 @@ export const useAddToFavoritesSingleRecordAction: SingleRecordActionHookWithObje
isDefined(objectMetadataItem) && isDefined(objectMetadataItem) &&
isDefined(selectedRecord) && isDefined(selectedRecord) &&
!objectMetadataItem.isRemote && !objectMetadataItem.isRemote &&
!isFavorite; !isFavorite &&
isNull(selectedRecord.deletedAt);
const onClick = () => { const onClick = () => {
if (!shouldBeRegistered) { if (!shouldBeRegistered) {

View File

@ -3,10 +3,13 @@ import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite'; import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
import { useFavorites } from '@/favorites/hooks/useFavorites'; import { useFavorites } from '@/favorites/hooks/useFavorites';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; 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 { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { isNull } from '@sniptt/guards';
import { useCallback, useContext, useState } from 'react'; import { useCallback, useContext, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
export const useDeleteSingleRecordAction: SingleRecordActionHookWithObjectMetadataItem = export const useDeleteSingleRecordAction: SingleRecordActionHookWithObjectMetadataItem =
@ -22,6 +25,8 @@ export const useDeleteSingleRecordAction: SingleRecordActionHookWithObjectMetada
objectNameSingular: objectMetadataItem.nameSingular, objectNameSingular: objectMetadataItem.nameSingular,
}); });
const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId));
const { sortedFavorites: favorites } = useFavorites(); const { sortedFavorites: favorites } = useFavorites();
const { deleteFavorite } = useDeleteFavorite(); const { deleteFavorite } = useDeleteFavorite();
@ -52,7 +57,8 @@ export const useDeleteSingleRecordAction: SingleRecordActionHookWithObjectMetada
const { isInRightDrawer, onActionExecutedCallback } = const { isInRightDrawer, onActionExecutedCallback } =
useContext(ActionMenuContext); useContext(ActionMenuContext);
const shouldBeRegistered = !isRemoteObject; const shouldBeRegistered =
!isRemoteObject && isNull(selectedRecord?.deletedAt);
const onClick = () => { const onClick = () => {
if (!shouldBeRegistered) { if (!shouldBeRegistered) {

View File

@ -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: (
<ConfirmationModal
isOpen={isDestroyRecordsModalOpen}
setIsOpen={setIsDestroyRecordsModalOpen}
title={'Permanently Destroy Record'}
subtitle={
'Are you sure you want to destroy this record? It cannot be recovered anymore.'
}
onConfirmClick={async () => {
await handleDeleteClick();
onActionExecutedCallback?.();
if (isInRightDrawer) {
closeRightDrawer();
}
}}
deleteButtonText={'Permanently Destroy Record'}
/>
),
};
};

View File

@ -27,7 +27,7 @@ export const RecordIndexActionMenuButtons = () => {
size="small" size="small"
variant="secondary" variant="secondary"
accent="default" accent="default"
title={entry.label} title={entry.shortLabel}
onClick={() => entry.onClick?.()} onClick={() => entry.onClick?.()}
ariaLabel={entry.label} ariaLabel={entry.label}
/> />

View File

@ -39,6 +39,7 @@ export const useFindManyRecordsSelectedInContextStore = ({
const { records, loading, totalCount } = useFindManyRecords({ const { records, loading, totalCount } = useFindManyRecords({
objectNameSingular: objectMetadataItem.nameSingular, objectNameSingular: objectMetadataItem.nameSingular,
filter: queryFilter, filter: queryFilter,
withSoftDeleted: true,
orderBy: [ orderBy: [
{ {
position: 'AscNullsFirst', position: 'AscNullsFirst',

View File

@ -20,6 +20,7 @@ export type UseFindManyRecordsParams<T> = ObjectMetadataItemIdentifier &
skip?: boolean; skip?: boolean;
recordGqlFields?: RecordGqlOperationGqlRecordFields; recordGqlFields?: RecordGqlOperationGqlRecordFields;
fetchPolicy?: WatchQueryFetchPolicy; fetchPolicy?: WatchQueryFetchPolicy;
withSoftDeleted?: boolean;
}; };
export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
@ -33,6 +34,7 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
onError, onError,
onCompleted, onCompleted,
cursorFilter, cursorFilter,
withSoftDeleted = false,
}: UseFindManyRecordsParams<T>) => { }: UseFindManyRecordsParams<T>) => {
const { objectMetadataItem } = useObjectMetadataItem({ const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular, objectNameSingular,
@ -61,11 +63,18 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
onCompleted, onCompleted,
}); });
const withSoftDeleterFilter = {
or: [{ deletedAt: { is: 'NULL' } }, { deletedAt: { is: 'NOT_NULL' } }],
};
const { data, loading, error, fetchMore } = const { data, loading, error, fetchMore } =
useQuery<RecordGqlOperationFindManyResult>(findManyRecordsQuery, { useQuery<RecordGqlOperationFindManyResult>(findManyRecordsQuery, {
skip: skip || !objectMetadataItem, skip: skip || !objectMetadataItem,
variables: { variables: {
filter, filter: {
...filter,
...(withSoftDeleted ? withSoftDeleterFilter : {}),
},
orderBy, orderBy,
lastCursor: cursorFilter?.cursor ?? undefined, lastCursor: cursorFilter?.cursor ?? undefined,
limit: cursorFilter?.limit ?? limit, limit: cursorFilter?.limit ?? limit,

View File

@ -45,6 +45,7 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
__typename: 'Person', __typename: 'Person',
createdAt: '2024-08-02T09:52:46.814Z', createdAt: '2024-08-02T09:52:46.814Z',
city: 'ASd', city: 'ASd',
deletedAt: null,
phones: { phones: {
primaryPhoneNumber: '781234562', primaryPhoneNumber: '781234562',
primaryPhoneCountryCode: 'FR', primaryPhoneCountryCode: 'FR',

View File

@ -244,6 +244,7 @@ export {
IconTextWrap, IconTextWrap,
IconTimelineEvent, IconTimelineEvent,
IconTrash, IconTrash,
IconTrashX,
IconUnlink, IconUnlink,
IconUpload, IconUpload,
IconUser, IconUser,

View File

@ -117,7 +117,7 @@ const StyledButton = styled.button<
border-color: ${variant === 'secondary' border-color: ${variant === 'secondary'
? !disabled && focus ? !disabled && focus
? theme.color.blue ? theme.color.blue
: theme.background.transparent.light : theme.background.transparent.medium
: focus : focus
? theme.color.blue ? theme.color.blue
: 'transparent'}; : 'transparent'};