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:
@ -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}
|
||||
>
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -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
|
||||
|
||||
@ -3,6 +3,7 @@ import { ViewFilterOperand } from './ViewFilterOperand';
|
||||
export type ViewFilter = {
|
||||
__typename: 'ViewFilter';
|
||||
id: string;
|
||||
variant?: 'default' | 'danger';
|
||||
fieldMetadataId: string;
|
||||
operand: ViewFilterOperand;
|
||||
value: string;
|
||||
|
||||
Reference in New Issue
Block a user