Delete button in right drawer / side pannel (#7200)

fixes #7069 
@Bonapara 


https://github.com/user-attachments/assets/b1b57070-1ef4-4cc3-9907-028219245558

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
nitin
2024-10-02 23:52:55 +05:30
committed by GitHub
parent 098d43d460
commit 83e43366bb
9 changed files with 130 additions and 42 deletions

View File

@ -93,6 +93,11 @@ export const triggerDeleteRecordsOptimisticEffect = ({
objectMetadataItems, objectMetadataItems,
}); });
cache.evict({ id: cache.identify(recordToDelete) }); cache.modify({
id: cache.identify(recordToDelete),
fields: {
deletedAt: () => recordToDelete.deletedAt,
},
});
}); });
}; };

View File

@ -0,0 +1,16 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const mapSoftDeleteFieldsToGraphQLQuery = (
objectMetadataItem: Pick<ObjectMetadataItem, 'fields'>,
): string => {
const softDeleteFields = ['id', 'deletedAt'];
const fieldsThatShouldBeQueried = objectMetadataItem.fields.filter(
(field) => field.isActive && softDeleteFields.includes(field.name),
);
return `{
__typename
${fieldsThatShouldBeQueried.map((field) => field.name).join('\n')}
}`;
};

View File

@ -1,5 +1,5 @@
import { useCallback } from 'react';
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { useCallback } from 'react';
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
@ -11,7 +11,6 @@ import { capitalize } from '~/utils/string/capitalize';
type useDeleteOneRecordProps = { type useDeleteOneRecordProps = {
objectNameSingular: string; objectNameSingular: string;
refetchFindManyQuery?: boolean;
}; };
export const useDeleteOneRecord = ({ export const useDeleteOneRecord = ({
@ -38,13 +37,18 @@ export const useDeleteOneRecord = ({
const deleteOneRecord = useCallback( const deleteOneRecord = useCallback(
async (idToDelete: string) => { async (idToDelete: string) => {
const currentTimestamp = new Date().toISOString();
const deletedRecord = await apolloClient.mutate({ const deletedRecord = await apolloClient.mutate({
mutation: deleteOneRecordMutation, mutation: deleteOneRecordMutation,
variables: { idToDelete }, variables: {
idToDelete: idToDelete,
},
optimisticResponse: { optimisticResponse: {
[mutationResponseField]: { [mutationResponseField]: {
__typename: capitalize(objectNameSingular), __typename: capitalize(objectNameSingular),
id: idToDelete, id: idToDelete,
deletedAt: currentTimestamp,
}, },
}, },
update: (cache, { data }) => { update: (cache, { data }) => {

View File

@ -1,6 +1,7 @@
import gql from 'graphql-tag'; import gql from 'graphql-tag';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { mapSoftDeleteFieldsToGraphQLQuery } from '@/object-metadata/utils/mapSoftDeleteFieldsToGraphQLQuery';
import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation'; import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation';
import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/getDeleteOneRecordMutationResponseField'; import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/getDeleteOneRecordMutationResponseField';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
@ -26,12 +27,11 @@ export const useDeleteOneRecordMutation = ({
); );
const deleteOneRecordMutation = gql` const deleteOneRecordMutation = gql`
mutation DeleteOne${capitalizedObjectName}($idToDelete: ID!) { mutation DeleteOne${capitalizedObjectName}($idToDelete: ID!) {
${mutationResponseField}(id: $idToDelete) { ${mutationResponseField}(id: $idToDelete)
id ${mapSoftDeleteFieldsToGraphQLQuery(objectMetadataItem)}
} }
} `;
`;
return { return {
deleteOneRecordMutation, deleteOneRecordMutation,

View File

@ -6,6 +6,13 @@ import { RecordShowContainer } from '@/object-record/record-show/components/Reco
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage'; import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect'; import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import styled from '@emotion/styled';
const StyledRightDrawerRecord = styled.div`
height: ${({ theme }) =>
useIsMobile() ? `calc(100% - ${theme.spacing(16)})` : '100%'};
`;
export const RightDrawerRecord = () => { export const RightDrawerRecord = () => {
const viewableRecordNameSingular = useRecoilValue( const viewableRecordNameSingular = useRecoilValue(
@ -27,14 +34,16 @@ export const RightDrawerRecord = () => {
); );
return ( return (
<RecordFieldValueSelectorContextProvider> <StyledRightDrawerRecord>
<RecordValueSetterEffect recordId={objectRecordId} /> <RecordFieldValueSelectorContextProvider>
<RecordShowContainer <RecordValueSetterEffect recordId={objectRecordId} />
objectNameSingular={objectNameSingular} <RecordShowContainer
objectRecordId={objectRecordId} objectNameSingular={objectNameSingular}
loading={false} objectRecordId={objectRecordId}
isInRightDrawer={true} loading={false}
/> isInRightDrawer={true}
</RecordFieldValueSelectorContextProvider> />
</RecordFieldValueSelectorContextProvider>
</StyledRightDrawerRecord>
); );
}; };

View File

@ -26,6 +26,7 @@ import { RecordDetailRelationSection } from '@/object-record/record-show/record-
import { recordLoadingFamilyState } from '@/object-record/record-store/states/recordLoadingFamilyState'; import { recordLoadingFamilyState } from '@/object-record/record-store/states/recordLoadingFamilyState';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { recordStoreIdentifierFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreIdentifierSelector'; import { recordStoreIdentifierFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreIdentifierSelector';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported'; import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported';
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading'; import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
import { ShowPageContainer } from '@/ui/layout/page/ShowPageContainer'; import { ShowPageContainer } from '@/ui/layout/page/ShowPageContainer';
@ -69,7 +70,7 @@ export const RecordShowContainer = ({
recordLoadingFamilyState(objectRecordId), recordLoadingFamilyState(objectRecordId),
); );
const [recordFromStore] = useRecoilState<any>( const [recordFromStore] = useRecoilState<ObjectRecord | null>(
recordStoreFamilyState(objectRecordId), recordStoreFamilyState(objectRecordId),
); );

View File

@ -6,7 +6,6 @@ import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
const StyledOuterContainer = styled.div` const StyledOuterContainer = styled.div`
display: flex; display: flex;
gap: ${({ theme }) => (useIsMobile() ? theme.spacing(3) : '0')}; gap: ${({ theme }) => (useIsMobile() ? theme.spacing(3) : '0')};
height: 100%; height: 100%;
width: 100%; width: 100%;

View File

@ -45,7 +45,6 @@ export const ShowPageMoreButton = ({
const handleDelete = () => { const handleDelete = () => {
deleteOneRecord(recordId); deleteOneRecord(recordId);
closeDropdown(); closeDropdown();
navigate(navigationMemorizedUrl, { replace: true });
}; };
const handleDestroy = () => { const handleDestroy = () => {

View File

@ -1,5 +1,24 @@
import { Calendar } from '@/activities/calendar/components/Calendar';
import { EmailThreads } from '@/activities/emails/components/EmailThreads';
import { Attachments } from '@/activities/files/components/Attachments';
import { Notes } from '@/activities/notes/components/Notes';
import { ObjectTasks } from '@/activities/tasks/components/ObjectTasks';
import { TimelineActivities } from '@/activities/timelineActivities/components/TimelineActivities';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { Button } from '@/ui/input/button/components/Button';
import { ShowPageActivityContainer } from '@/ui/layout/show-page/components/ShowPageActivityContainer';
import { TabList } from '@/ui/layout/tab/components/TabList';
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { Workflow } from '@/workflow/components/Workflow';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil'; import { useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { import {
IconCalendarEvent, IconCalendarEvent,
IconCheckbox, IconCheckbox,
@ -9,30 +28,17 @@ import {
IconPaperclip, IconPaperclip,
IconSettings, IconSettings,
IconTimelineEvent, IconTimelineEvent,
IconTrash,
} from 'twenty-ui'; } from 'twenty-ui';
import { Calendar } from '@/activities/calendar/components/Calendar';
import { EmailThreads } from '@/activities/emails/components/EmailThreads';
import { Attachments } from '@/activities/files/components/Attachments';
import { Notes } from '@/activities/notes/components/Notes';
import { ObjectTasks } from '@/activities/tasks/components/ObjectTasks';
import { TimelineActivities } from '@/activities/timelineActivities/components/TimelineActivities';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ShowPageActivityContainer } from '@/ui/layout/show-page/components/ShowPageActivityContainer';
import { TabList } from '@/ui/layout/tab/components/TabList';
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { Workflow } from '@/workflow/components/Workflow';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
const StyledShowPageRightContainer = styled.div<{ isMobile: boolean }>` const StyledShowPageRightContainer = styled.div<{ isMobile: boolean }>`
display: flex; display: flex;
flex: 1 0 0; flex: 1 0 0;
flex-direction: column; flex-direction: column;
height: 100%;
justify-content: start; justify-content: start;
width: 100%; width: 100%;
height: 100%; position: relative;
`; `;
const StyledTabListContainer = styled.div` const StyledTabListContainer = styled.div`
@ -57,6 +63,26 @@ const StyledGreyBox = styled.div<{ isInRightDrawer: boolean }>`
isInRightDrawer ? theme.spacing(4) : ''}; isInRightDrawer ? theme.spacing(4) : ''};
`; `;
const StyledButtonContainer = styled.div`
align-items: center;
bottom: 0;
border-top: 1px solid ${({ theme }) => theme.border.color.light};
display: flex;
justify-content: flex-end;
padding: ${({ theme }) => theme.spacing(2)};
width: 100%;
box-sizing: border-box;
position: absolute;
width: 100%;
`;
const StyledContentContainer = styled.div<{ isInRightDrawer: boolean }>`
flex: 1;
overflow-y: auto;
padding-bottom: ${({ theme, isInRightDrawer }) =>
isInRightDrawer ? theme.spacing(16) : 0};
`;
export const TAB_LIST_COMPONENT_ID = 'show-page-right-tab-list'; export const TAB_LIST_COMPONENT_ID = 'show-page-right-tab-list';
type ShowPageRightContainerProps = { type ShowPageRightContainerProps = {
@ -107,7 +133,7 @@ export const ShowPageRightContainer = ({
const shouldDisplayCalendarTab = isCompanyOrPerson; const shouldDisplayCalendarTab = isCompanyOrPerson;
const shouldDisplayEmailsTab = emails && isCompanyOrPerson; const shouldDisplayEmailsTab = emails && isCompanyOrPerson;
const isMobile = useIsMobile() || isInRightDrawer; const isMobile = useIsMobile();
const tabs = [ const tabs = [
{ {
@ -125,7 +151,7 @@ export const ShowPageRightContainer = ({
id: 'fields', id: 'fields',
title: 'Fields', title: 'Fields',
Icon: IconList, Icon: IconList,
hide: !isMobile, hide: !(isMobile || isInRightDrawer),
}, },
{ {
id: 'timeline', id: 'timeline',
@ -225,6 +251,23 @@ export const ShowPageRightContainer = ({
return <></>; return <></>;
} }
}; };
const [isDeleting, setIsDeleting] = useState(false);
const { deleteOneRecord } = useDeleteOneRecord({
objectNameSingular: targetableObject.targetObjectNameSingular,
});
const handleDelete = async () => {
setIsDeleting(true);
await deleteOneRecord(targetableObject.id);
setIsDeleting(false);
};
const [recordFromStore] = useRecoilState<ObjectRecord | null>(
recordStoreFamilyState(targetableObject.id),
);
return ( return (
<StyledShowPageRightContainer isMobile={isMobile}> <StyledShowPageRightContainer isMobile={isMobile}>
<StyledTabListContainer> <StyledTabListContainer>
@ -235,7 +278,19 @@ export const ShowPageRightContainer = ({
/> />
</StyledTabListContainer> </StyledTabListContainer>
{summaryCard} {summaryCard}
{renderActiveTabContent()} <StyledContentContainer isInRightDrawer={isInRightDrawer}>
{renderActiveTabContent()}
</StyledContentContainer>
{isInRightDrawer && recordFromStore && !recordFromStore.deletedAt && (
<StyledButtonContainer>
<Button
Icon={IconTrash}
onClick={handleDelete}
disabled={isDeleting}
title={isDeleting ? 'Deleting...' : 'Delete'}
></Button>
</StyledButtonContainer>
)}
</StyledShowPageRightContainer> </StyledShowPageRightContainer>
); );
}; };