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:
@ -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',
|
||||
|
||||
@ -36,6 +36,7 @@ export const useExportMultipleRecordsAction = ({
|
||||
key: 'export-multiple-records',
|
||||
position,
|
||||
label: displayedExportProgress(progress),
|
||||
shortLabel: 'Export',
|
||||
Icon: IconDatabaseExport,
|
||||
accent: 'default',
|
||||
onClick: () => download(),
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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'}
|
||||
/>
|
||||
),
|
||||
};
|
||||
};
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -39,6 +39,7 @@ export const useFindManyRecordsSelectedInContextStore = ({
|
||||
const { records, loading, totalCount } = useFindManyRecords({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
filter: queryFilter,
|
||||
withSoftDeleted: true,
|
||||
orderBy: [
|
||||
{
|
||||
position: 'AscNullsFirst',
|
||||
|
||||
@ -20,6 +20,7 @@ export type UseFindManyRecordsParams<T> = ObjectMetadataItemIdentifier &
|
||||
skip?: boolean;
|
||||
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||
fetchPolicy?: WatchQueryFetchPolicy;
|
||||
withSoftDeleted?: boolean;
|
||||
};
|
||||
|
||||
export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
@ -33,6 +34,7 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
onError,
|
||||
onCompleted,
|
||||
cursorFilter,
|
||||
withSoftDeleted = false,
|
||||
}: UseFindManyRecordsParams<T>) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
@ -61,11 +63,18 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
onCompleted,
|
||||
});
|
||||
|
||||
const withSoftDeleterFilter = {
|
||||
or: [{ deletedAt: { is: 'NULL' } }, { deletedAt: { is: 'NOT_NULL' } }],
|
||||
};
|
||||
|
||||
const { data, loading, error, fetchMore } =
|
||||
useQuery<RecordGqlOperationFindManyResult>(findManyRecordsQuery, {
|
||||
skip: skip || !objectMetadataItem,
|
||||
variables: {
|
||||
filter,
|
||||
filter: {
|
||||
...filter,
|
||||
...(withSoftDeleted ? withSoftDeleterFilter : {}),
|
||||
},
|
||||
orderBy,
|
||||
lastCursor: cursorFilter?.cursor ?? undefined,
|
||||
limit: cursorFilter?.limit ?? limit,
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -244,6 +244,7 @@ export {
|
||||
IconTextWrap,
|
||||
IconTimelineEvent,
|
||||
IconTrash,
|
||||
IconTrashX,
|
||||
IconUnlink,
|
||||
IconUpload,
|
||||
IconUser,
|
||||
|
||||
@ -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'};
|
||||
|
||||
Reference in New Issue
Block a user