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

@ -2,12 +2,37 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconComponent, IconX } from 'twenty-ui';
const StyledChip = styled.div`
const StyledChip = styled.div<{ variant: SortOrFitlerChipVariant }>`
align-items: center;
background-color: ${({ theme }) => theme.accent.quaternary};
border: 1px solid ${({ theme }) => theme.accent.tertiary};
background-color: ${({ theme, variant }) => {
switch (variant) {
case 'danger':
return theme.background.danger;
case 'default':
default:
return theme.accent.quaternary;
}
}};
border: 1px solid
${({ theme, variant }) => {
switch (variant) {
case 'danger':
return theme.border.color.danger;
case 'default':
default:
return theme.accent.tertiary;
}
}};
border-radius: 4px;
color: ${({ theme }) => theme.color.blue};
color: ${({ theme, variant }) => {
switch (variant) {
case 'danger':
return theme.color.red;
case 'default':
default:
return theme.color.blue;
}
}};
cursor: pointer;
display: flex;
flex-direction: row;
@ -24,7 +49,7 @@ const StyledIcon = styled.div`
margin-right: ${({ theme }) => theme.spacing(1)};
`;
const StyledDelete = styled.div`
const StyledDelete = styled.div<{ variant: SortOrFitlerChipVariant }>`
align-items: center;
cursor: pointer;
display: flex;
@ -33,7 +58,15 @@ const StyledDelete = styled.div`
margin-top: 1px;
user-select: none;
&:hover {
background-color: ${({ theme }) => theme.accent.secondary};
background-color: ${({ theme, variant }) => {
switch (variant) {
case 'danger':
return theme.color.red20;
case 'default':
default:
return theme.accent.secondary;
}
}};
border-radius: ${({ theme }) => theme.border.radius.sm};
}
`;
@ -42,9 +75,12 @@ const StyledLabelKey = styled.div`
font-weight: ${({ theme }) => theme.font.weight.medium};
`;
type SortOrFitlerChipVariant = 'default' | 'danger';
type SortOrFilterChipProps = {
labelKey?: string;
labelValue: string;
variant?: SortOrFitlerChipVariant;
Icon?: IconComponent;
onRemove: () => void;
onClick?: () => void;
@ -54,6 +90,7 @@ type SortOrFilterChipProps = {
export const SortOrFilterChip = ({
labelKey,
labelValue,
variant = 'default',
Icon,
onRemove,
testId,
@ -67,7 +104,7 @@ export const SortOrFilterChip = ({
};
return (
<StyledChip onClick={onClick}>
<StyledChip onClick={onClick} variant={variant}>
{Icon && (
<StyledIcon>
<Icon size={theme.icon.size.sm} />
@ -76,6 +113,7 @@ export const SortOrFilterChip = ({
{labelKey && <StyledLabelKey>{labelKey}</StyledLabelKey>}
{labelValue}
<StyledDelete
variant={variant}
onClick={handleDeleteClick}
data-testid={'remove-icon-' + testId}
>

View File

@ -0,0 +1,30 @@
import { useIcons } from 'twenty-ui';
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
import { SortOrFilterChip } from '@/views/components/SortOrFilterChip';
import { useCombinedViewFilters } from '@/views/hooks/useCombinedViewFilters';
type VariantFilterChipProps = {
viewFilter: Filter;
};
export const VariantFilterChip = ({ viewFilter }: VariantFilterChipProps) => {
const { removeCombinedViewFilter } = useCombinedViewFilters();
const { getIcon } = useIcons();
const handleRemoveClick = () => {
removeCombinedViewFilter(viewFilter.id);
};
return (
<SortOrFilterChip
key={viewFilter.fieldMetadataId}
testId={viewFilter.fieldMetadataId}
variant={viewFilter.variant}
labelValue={viewFilter.definition.label}
Icon={getIcon(viewFilter.definition.iconName)}
onRemove={handleRemoveClick}
/>
);
};

View File

@ -1,9 +1,10 @@
import { ReactNode } from 'react';
import styled from '@emotion/styled';
import { ReactNode, useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { AddObjectFilterFromDetailsButton } from '@/object-record/object-filter-dropdown/components/AddObjectFilterFromDetailsButton';
import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
import { EditableFilterDropdownButton } from '@/views/components/EditableFilterDropdownButton';
import { EditableSortChip } from '@/views/components/EditableSortChip';
@ -14,6 +15,7 @@ import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { useResetCurrentView } from '@/views/hooks/useResetCurrentView';
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
import { VariantFilterChip } from './VariantFilterChip';
export type ViewBarDetailsProps = {
hasFilterButton?: boolean;
@ -118,6 +120,29 @@ export const ViewBarDetails = ({
const { resetCurrentView } = useResetCurrentView();
const canResetView = canPersistView && !hasFiltersQueryParams;
const { otherViewFilters, defaultViewFilters } = useMemo(() => {
if (!currentViewWithCombinedFiltersAndSorts) {
return {
otherViewFilters: [],
defaultViewFilters: [],
};
}
const otherViewFilters =
currentViewWithCombinedFiltersAndSorts.viewFilters.filter(
(viewFilter) => viewFilter.variant && viewFilter.variant !== 'default',
);
const defaultViewFilters =
currentViewWithCombinedFiltersAndSorts.viewFilters.filter(
(viewFilter) => !viewFilter.variant || viewFilter.variant === 'default',
);
return {
otherViewFilters,
defaultViewFilters,
};
}, [currentViewWithCombinedFiltersAndSorts]);
const handleCancelClick = () => {
resetCurrentView();
};
@ -136,6 +161,22 @@ export const ViewBarDetails = ({
<StyledBar>
<StyledFilterContainer>
<StyledChipcontainer>
{otherViewFilters.map((viewFilter) => (
<VariantFilterChip
key={viewFilter.fieldMetadataId}
// Why do we have two types, Filter and ViewFilter?
// Why key defition is already present in the Filter type and added on the fly here with mapViewFiltersToFilters ?
// Also as filter is spread into viewFilter, definition is present
// FixMe: Ugly hack to make it work
viewFilter={viewFilter as unknown as Filter}
/>
))}
{!!otherViewFilters.length &&
!!currentViewWithCombinedFiltersAndSorts?.viewSorts?.length && (
<StyledSeperatorContainer>
<StyledSeperator />
</StyledSeperatorContainer>
)}
{mapViewSortsToSorts(
currentViewWithCombinedFiltersAndSorts?.viewSorts ?? [],
availableSortDefinitions,
@ -143,13 +184,13 @@ export const ViewBarDetails = ({
<EditableSortChip key={sort.fieldMetadataId} viewSort={sort} />
))}
{!!currentViewWithCombinedFiltersAndSorts?.viewSorts?.length &&
!!currentViewWithCombinedFiltersAndSorts?.viewFilters?.length && (
!!defaultViewFilters.length && (
<StyledSeperatorContainer>
<StyledSeperator />
</StyledSeperatorContainer>
)}
{mapViewFiltersToFilters(
currentViewWithCombinedFiltersAndSorts?.viewFilters ?? [],
defaultViewFilters,
availableFilterDefinitions,
).map((viewFilter) => (
<ObjectFilterDropdownScope