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:
@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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')}
|
||||||
|
}`;
|
||||||
|
};
|
||||||
@ -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 }) => {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -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%;
|
||||||
|
|||||||
@ -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 = () => {
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user