feat: soft delete (#6576)

Implement soft delete on standards and custom objects.
This is a temporary solution, when we drop `pg_graphql` we should rely
on the `softDelete` functions of TypeORM.

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Jérémy M
2024-08-16 21:20:02 +02:00
committed by GitHub
parent 20d84755bb
commit db54469c8a
118 changed files with 1675 additions and 492 deletions

View File

@ -336,6 +336,7 @@ const StyledButton = styled('button', {
flex-direction: row;
font-family: ${({ theme }) => theme.font.family};
font-weight: 500;
font-size: ${({ theme }) => theme.font.size.md};
gap: ${({ theme }) => theme.spacing(1)};
height: ${({ size }) => (size === 'small' ? '24px' : '32px')};
justify-content: ${({ justify }) => justify};

View File

@ -1,3 +1,4 @@
import isPropValid from '@emotion/is-prop-valid';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Link } from 'react-router-dom';
@ -19,12 +20,11 @@ export type FloatingButtonProps = {
to?: string;
};
const shouldForwardProp = (prop: string) =>
!['applyBlur', 'applyShadow', 'focus', 'position', 'size', 'to'].includes(
prop,
);
const StyledButton = styled('button', { shouldForwardProp })<
const StyledButton = styled('button', {
shouldForwardProp: (prop) =>
!['applyBlur', 'applyShadow', 'focus', 'position', 'size'].includes(prop) &&
isPropValid(prop),
})<
Pick<
FloatingButtonProps,
| 'size'

View File

@ -1,6 +1,6 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { ComponentProps, ReactNode } from 'react';
import { ReactNode } from 'react';
import { useRecoilValue } from 'recoil';
import {
IconChevronDown,
@ -18,7 +18,7 @@ import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
export const PAGE_BAR_MIN_HEIGHT = 40;
const StyledTopBarContainer = styled.div`
const StyledTopBarContainer = styled.div<{ width?: number }>`
align-items: center;
background: ${({ theme }) => theme.background.noisy};
color: ${({ theme }) => theme.font.color.primary};
@ -31,6 +31,7 @@ const StyledTopBarContainer = styled.div`
padding-left: 0;
padding-right: ${({ theme }) => theme.spacing(3)};
z-index: 20;
width: ${({ width }) => width + 'px' || '100%'};
@media (max-width: ${MOBILE_VIEWPORT}px) {
padding-left: ${({ theme }) => theme.spacing(3)};
@ -76,8 +77,8 @@ const StyledTopBarButtonContainer = styled.div`
margin-right: ${({ theme }) => theme.spacing(1)};
`;
type PageHeaderProps = ComponentProps<'div'> & {
title: string;
type PageHeaderProps = {
title: ReactNode;
hasClosePageButton?: boolean;
onClosePage?: () => void;
hasPaginationButtons?: boolean;
@ -87,6 +88,7 @@ type PageHeaderProps = ComponentProps<'div'> & {
navigateToNextRecord?: () => void;
Icon: IconComponent;
children?: ReactNode;
width?: number;
};
export const PageHeader = ({
@ -100,13 +102,14 @@ export const PageHeader = ({
navigateToNextRecord,
Icon,
children,
width,
}: PageHeaderProps) => {
const isMobile = useIsMobile();
const theme = useTheme();
const isNavigationDrawerOpen = useRecoilValue(isNavigationDrawerOpenState);
return (
<StyledTopBarContainer>
<StyledTopBarContainer width={width}>
<StyledLeftContainer>
{!isMobile && !isNavigationDrawerOpen && (
<StyledTopBarButtonContainer>
@ -143,7 +146,11 @@ export const PageHeader = ({
)}
{Icon && <Icon size={theme.icon.size.md} />}
<StyledTitleContainer data-testid="top-bar-title">
<OverflowingTextWithTooltip text={title} />
{typeof title === 'string' ? (
<OverflowingTextWithTooltip text={title} />
) : (
title
)}
</StyledTitleContainer>
</StyledTopBarIconStyledTitleContainer>
</StyledLeftContainer>

View File

@ -1,15 +1,21 @@
import React from 'react';
import styled from '@emotion/styled';
import React from 'react';
const StyledPanel = styled.div`
background: ${({ theme }) => theme.background.primary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.md};
height: 100%;
overflow: auto;
overflow-x: auto;
overflow-y: hidden;
width: 100%;
`;
export const PagePanel = ({ children }: { children: React.ReactNode }) => (
type PagePanelProps = {
children: React.ReactNode;
hasInformationBar?: boolean;
};
export const PagePanel = ({ children }: PagePanelProps) => (
<StyledPanel>{children}</StyledPanel>
);

View File

@ -1,16 +1,18 @@
import styled from '@emotion/styled';
import { JSX } from 'react';
import { JSX, ReactNode } from 'react';
import { IconComponent } from 'twenty-ui';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { InformationBannerWrapper } from '@/information-banner/components/InformationBannerWrapper';
import { OBJECT_SETTINGS_WIDTH } from '@/settings/data-model/constants/ObjectSettings';
import { PageBody } from './PageBody';
import { PageHeader } from './PageHeader';
type SubMenuTopBarContainerProps = {
children: JSX.Element | JSX.Element[];
title: string;
title: string | ReactNode;
actionButton?: ReactNode;
Icon: IconComponent;
className?: string;
};
@ -25,6 +27,7 @@ const StyledContainer = styled.div<{ isMobile: boolean }>`
export const SubMenuTopBarContainer = ({
children,
title,
actionButton,
Icon,
className,
}: SubMenuTopBarContainerProps) => {
@ -32,7 +35,13 @@ export const SubMenuTopBarContainer = ({
return (
<StyledContainer isMobile={isMobile} className={className}>
{isMobile && <PageHeader title={title} Icon={Icon} />}
<PageHeader
title={title}
Icon={Icon}
width={OBJECT_SETTINGS_WIDTH + 4 * 8}
>
{actionButton}
</PageHeader>
<PageBody>
<InformationBannerWrapper />
{children}

View File

@ -1,7 +1,7 @@
import { useNavigate } from 'react-router-dom';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { IconDotsVertical, IconTrash } from 'twenty-ui';
import { useNavigate } from 'react-router-dom';
import { useRecoilState, useRecoilValue } from 'recoil';
import { IconDotsVertical, IconRestore, IconTrash } from 'twenty-ui';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
@ -11,6 +11,9 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { useDestroyManyRecords } from '@/object-record/hooks/useDestroyManyRecords';
import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { Dropdown } from '../../dropdown/components/Dropdown';
import { DropdownMenu } from '../../dropdown/components/DropdownMenu';
@ -32,6 +35,12 @@ export const ShowPageMoreButton = ({
const { deleteOneRecord } = useDeleteOneRecord({
objectNameSingular,
});
const { destroyManyRecords } = useDestroyManyRecords({
objectNameSingular,
});
const { restoreManyRecords } = useRestoreManyRecords({
objectNameSingular,
});
const handleDelete = () => {
deleteOneRecord(recordId);
@ -39,6 +48,21 @@ export const ShowPageMoreButton = ({
navigate(navigationMemorizedUrl, { replace: true });
};
const handleDestroy = () => {
destroyManyRecords([recordId]);
closeDropdown();
navigate(navigationMemorizedUrl, { replace: true });
};
const handleRestore = () => {
restoreManyRecords([recordId]);
closeDropdown();
};
const [recordFromStore] = useRecoilState<any>(
recordStoreFamilyState(recordId),
);
return (
<StyledContainer>
<Dropdown
@ -56,12 +80,29 @@ export const ShowPageMoreButton = ({
dropdownComponents={
<DropdownMenu>
<DropdownMenuItemsContainer>
<MenuItem
onClick={handleDelete}
accent="danger"
LeftIcon={IconTrash}
text="Delete"
/>
{recordFromStore && !recordFromStore.deletedAt && (
<MenuItem
onClick={handleDelete}
accent="danger"
LeftIcon={IconTrash}
text="Delete"
/>
)}
{recordFromStore && recordFromStore.deletedAt && (
<>
<MenuItem
onClick={handleDestroy}
accent="danger"
LeftIcon={IconTrash}
text="Destroy"
/>
<MenuItem
onClick={handleRestore}
LeftIcon={IconRestore}
text="Restore"
/>
</>
)}
</DropdownMenuItemsContainer>
</DropdownMenu>
}

View File

@ -1,6 +1,6 @@
import styled from '@emotion/styled';
import { Fragment } from 'react';
import { Link } from 'react-router-dom';
import styled from '@emotion/styled';
type BreadcrumbProps = {
className?: string;
@ -9,10 +9,10 @@ type BreadcrumbProps = {
const StyledWrapper = styled.nav`
align-items: center;
color: ${({ theme }) => theme.font.color.extraLight};
color: ${({ theme }) => theme.font.color.secondary};
display: flex;
font-size: ${({ theme }) => theme.font.size.lg};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
font-size: ${({ theme }) => theme.font.size.md};
// font-weight: ${({ theme }) => theme.font.weight.semiBold};
gap: ${({ theme }) => theme.spacing(2)};
line-height: ${({ theme }) => theme.text.lineHeight.lg};
`;
@ -23,7 +23,7 @@ const StyledLink = styled(Link)`
`;
const StyledText = styled.span`
color: ${({ theme }) => theme.font.color.tertiary};
color: ${({ theme }) => theme.font.color.primary};
`;
export const Breadcrumb = ({ className, links }: BreadcrumbProps) => (