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