From 83e43366bb8fd0b1c8c86dd964d5f2d0bbc65c54 Mon Sep 17 00:00:00 2001 From: nitin <142569587+ehconitin@users.noreply.github.com> Date: Wed, 2 Oct 2024 23:52:55 +0530 Subject: [PATCH] Delete button in right drawer / side pannel (#7200) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes #7069 @Bonapara https://github.com/user-attachments/assets/b1b57070-1ef4-4cc3-9907-028219245558 --------- Co-authored-by: Félix Malfait --- .../triggerDeleteRecordsOptimisticEffect.ts | 7 +- .../mapSoftDeleteFieldsToGraphQLQuery.ts | 16 ++++ .../object-record/hooks/useDeleteOneRecord.ts | 10 +- .../hooks/useDeleteOneRecordMutation.ts | 12 +-- .../components/RightDrawerRecord.tsx | 27 ++++-- .../components/RecordShowContainer.tsx | 3 +- .../ui/layout/page/ShowPageContainer.tsx | 1 - .../components/ShowPageMoreButton.tsx | 1 - .../components/ShowPageRightContainer.tsx | 95 +++++++++++++++---- 9 files changed, 130 insertions(+), 42 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-metadata/utils/mapSoftDeleteFieldsToGraphQLQuery.ts diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect.ts index 1a3a1aa89..0aa4c51e9 100644 --- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect.ts +++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect.ts @@ -93,6 +93,11 @@ export const triggerDeleteRecordsOptimisticEffect = ({ objectMetadataItems, }); - cache.evict({ id: cache.identify(recordToDelete) }); + cache.modify({ + id: cache.identify(recordToDelete), + fields: { + deletedAt: () => recordToDelete.deletedAt, + }, + }); }); }; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/mapSoftDeleteFieldsToGraphQLQuery.ts b/packages/twenty-front/src/modules/object-metadata/utils/mapSoftDeleteFieldsToGraphQLQuery.ts new file mode 100644 index 000000000..701e524b5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/utils/mapSoftDeleteFieldsToGraphQLQuery.ts @@ -0,0 +1,16 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; + +export const mapSoftDeleteFieldsToGraphQLQuery = ( + objectMetadataItem: Pick, +): 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')} + }`; +}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts index bf7dcd778..3b33fe3e7 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts @@ -1,5 +1,5 @@ -import { useCallback } from 'react'; import { useApolloClient } from '@apollo/client'; +import { useCallback } from 'react'; import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; @@ -11,7 +11,6 @@ import { capitalize } from '~/utils/string/capitalize'; type useDeleteOneRecordProps = { objectNameSingular: string; - refetchFindManyQuery?: boolean; }; export const useDeleteOneRecord = ({ @@ -38,13 +37,18 @@ export const useDeleteOneRecord = ({ const deleteOneRecord = useCallback( async (idToDelete: string) => { + const currentTimestamp = new Date().toISOString(); + const deletedRecord = await apolloClient.mutate({ mutation: deleteOneRecordMutation, - variables: { idToDelete }, + variables: { + idToDelete: idToDelete, + }, optimisticResponse: { [mutationResponseField]: { __typename: capitalize(objectNameSingular), id: idToDelete, + deletedAt: currentTimestamp, }, }, update: (cache, { data }) => { diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecordMutation.ts b/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecordMutation.ts index ea8a3d458..ae7557bed 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecordMutation.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecordMutation.ts @@ -1,6 +1,7 @@ import gql from 'graphql-tag'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { mapSoftDeleteFieldsToGraphQLQuery } from '@/object-metadata/utils/mapSoftDeleteFieldsToGraphQLQuery'; import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation'; import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/getDeleteOneRecordMutationResponseField'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; @@ -26,12 +27,11 @@ export const useDeleteOneRecordMutation = ({ ); const deleteOneRecordMutation = gql` - mutation DeleteOne${capitalizedObjectName}($idToDelete: ID!) { - ${mutationResponseField}(id: $idToDelete) { - id - } - } - `; + mutation DeleteOne${capitalizedObjectName}($idToDelete: ID!) { + ${mutationResponseField}(id: $idToDelete) + ${mapSoftDeleteFieldsToGraphQLQuery(objectMetadataItem)} + } +`; return { deleteOneRecordMutation, diff --git a/packages/twenty-front/src/modules/object-record/record-right-drawer/components/RightDrawerRecord.tsx b/packages/twenty-front/src/modules/object-record/record-right-drawer/components/RightDrawerRecord.tsx index 49b27cdfe..e68ce3883 100644 --- a/packages/twenty-front/src/modules/object-record/record-right-drawer/components/RightDrawerRecord.tsx +++ b/packages/twenty-front/src/modules/object-record/record-right-drawer/components/RightDrawerRecord.tsx @@ -6,6 +6,13 @@ import { RecordShowContainer } from '@/object-record/record-show/components/Reco import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage'; import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect'; 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 = () => { const viewableRecordNameSingular = useRecoilValue( @@ -27,14 +34,16 @@ export const RightDrawerRecord = () => { ); return ( - - - - + + + + + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx index 967825f3a..5f23472fe 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx @@ -26,6 +26,7 @@ import { RecordDetailRelationSection } from '@/object-record/record-show/record- import { recordLoadingFamilyState } from '@/object-record/record-store/states/recordLoadingFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; 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 { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading'; import { ShowPageContainer } from '@/ui/layout/page/ShowPageContainer'; @@ -69,7 +70,7 @@ export const RecordShowContainer = ({ recordLoadingFamilyState(objectRecordId), ); - const [recordFromStore] = useRecoilState( + const [recordFromStore] = useRecoilState( recordStoreFamilyState(objectRecordId), ); diff --git a/packages/twenty-front/src/modules/ui/layout/page/ShowPageContainer.tsx b/packages/twenty-front/src/modules/ui/layout/page/ShowPageContainer.tsx index 9a1aa473b..79d877866 100644 --- a/packages/twenty-front/src/modules/ui/layout/page/ShowPageContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/page/ShowPageContainer.tsx @@ -6,7 +6,6 @@ import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; const StyledOuterContainer = styled.div` display: flex; - gap: ${({ theme }) => (useIsMobile() ? theme.spacing(3) : '0')}; height: 100%; width: 100%; diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageMoreButton.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageMoreButton.tsx index 9cecd4558..30efbdf71 100644 --- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageMoreButton.tsx +++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageMoreButton.tsx @@ -45,7 +45,6 @@ export const ShowPageMoreButton = ({ const handleDelete = () => { deleteOneRecord(recordId); closeDropdown(); - navigate(navigationMemorizedUrl, { replace: true }); }; const handleDestroy = () => { diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx index 0df732bfe..c06129296 100644 --- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx @@ -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 { useRecoilValue } from 'recoil'; +import { useState } from 'react'; +import { useRecoilState, useRecoilValue } from 'recoil'; import { IconCalendarEvent, IconCheckbox, @@ -9,30 +28,17 @@ import { IconPaperclip, IconSettings, IconTimelineEvent, + IconTrash, } 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 }>` display: flex; flex: 1 0 0; flex-direction: column; + height: 100%; justify-content: start; width: 100%; - height: 100%; + position: relative; `; const StyledTabListContainer = styled.div` @@ -57,6 +63,26 @@ const StyledGreyBox = styled.div<{ isInRightDrawer: boolean }>` 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'; type ShowPageRightContainerProps = { @@ -107,7 +133,7 @@ export const ShowPageRightContainer = ({ const shouldDisplayCalendarTab = isCompanyOrPerson; const shouldDisplayEmailsTab = emails && isCompanyOrPerson; - const isMobile = useIsMobile() || isInRightDrawer; + const isMobile = useIsMobile(); const tabs = [ { @@ -125,7 +151,7 @@ export const ShowPageRightContainer = ({ id: 'fields', title: 'Fields', Icon: IconList, - hide: !isMobile, + hide: !(isMobile || isInRightDrawer), }, { id: 'timeline', @@ -225,6 +251,23 @@ export const ShowPageRightContainer = ({ 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( + recordStoreFamilyState(targetableObject.id), + ); + return ( @@ -235,7 +278,19 @@ export const ShowPageRightContainer = ({ /> {summaryCard} - {renderActiveTabContent()} + + {renderActiveTabContent()} + + {isInRightDrawer && recordFromStore && !recordFromStore.deletedAt && ( + + + + )} ); };