Skip to content

Commit 58f94b0

Browse files
committed
feat: soft delete front-end wip
1 parent 286b6c5 commit 58f94b0

File tree

7 files changed

+219
-10
lines changed

7 files changed

+219
-10
lines changed

packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/Filter.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { FilterDefinition } from './FilterDefinition';
44

55
export type Filter = {
66
id: string;
7+
variant?: 'default' | 'trash';
78
fieldMetadataId: string;
89
value: string;
910
displayValue: string;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { useCallback } from 'react';
2+
import { v4 } from 'uuid';
3+
4+
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
5+
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
6+
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
7+
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
8+
import { useCombinedViewFilters } from '@/views/hooks/useCombinedViewFilters';
9+
import { isDefined } from '~/utils/isDefined';
10+
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
11+
12+
type UseHandleToggleTrashColumnFilterProps = {
13+
objectNameSingular: string;
14+
viewBarId: string;
15+
};
16+
17+
export const useHandleToggleTrashColumnFilter = ({
18+
viewBarId,
19+
objectNameSingular,
20+
}: UseHandleToggleTrashColumnFilterProps) => {
21+
const { objectMetadataItem } = useObjectMetadataItem({
22+
objectNameSingular,
23+
});
24+
25+
const { columnDefinitions } =
26+
useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
27+
28+
const { upsertCombinedViewFilter } = useCombinedViewFilters(viewBarId);
29+
30+
const handleToggleTrashColumnFilter = useCallback(() => {
31+
const trashFieldMetadata = objectMetadataItem.fields.find(
32+
(field) => field.name === 'deletedAt',
33+
);
34+
35+
if (!isDefined(trashFieldMetadata)) return;
36+
37+
const correspondingColumnDefinition = columnDefinitions.find(
38+
(columnDefinition) =>
39+
columnDefinition.fieldMetadataId === trashFieldMetadata.id,
40+
);
41+
42+
if (!isDefined(correspondingColumnDefinition)) return;
43+
44+
const filterType = getFilterTypeFromFieldType(
45+
correspondingColumnDefinition?.type,
46+
);
47+
48+
const newFilter: Filter = {
49+
id: v4(),
50+
variant: 'trash',
51+
fieldMetadataId: trashFieldMetadata.id,
52+
operand: ViewFilterOperand.IsNotEmpty,
53+
displayValue: '',
54+
definition: {
55+
label: 'Trash',
56+
iconName: 'IconTrash',
57+
fieldMetadataId: trashFieldMetadata.id,
58+
type: filterType,
59+
},
60+
value: '',
61+
};
62+
63+
upsertCombinedViewFilter(newFilter);
64+
}, [columnDefinitions, objectMetadataItem.fields, upsertCombinedViewFilter]);
65+
66+
return handleToggleTrashColumnFilter;
67+
};

packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx

+15
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
IconFileImport,
99
IconSettings,
1010
IconTag,
11+
IconTrash,
1112
} from 'twenty-ui';
1213

1314
import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular';
@@ -37,6 +38,8 @@ import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
3738
import { ViewType } from '@/views/types/ViewType';
3839
import { useLocation } from 'react-router-dom';
3940
import { useSetRecoilState } from 'recoil';
41+
import { useCombinedViewFilters } from '@/views/hooks/useCombinedViewFilters';
42+
import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter';
4043

4144
type RecordIndexOptionsMenu = 'fields' | 'hiddenFields';
4245

@@ -59,6 +62,8 @@ export const RecordIndexOptionsDropdownContent = ({
5962
RecordIndexOptionsMenu | undefined
6063
>(undefined);
6164

65+
const { upsertCombinedViewFilter } = useCombinedViewFilters();
66+
6267
const resetMenu = () => setCurrentMenu(undefined);
6368

6469
const handleSelectMenu = (option: RecordIndexOptionsMenu) => {
@@ -88,6 +93,11 @@ export const RecordIndexOptionsDropdownContent = ({
8893
hiddenTableColumns,
8994
} = useRecordIndexOptionsForTable(recordIndexId);
9095

96+
const handleToggleTrashColumnFilter = useHandleToggleTrashColumnFilter({
97+
objectNameSingular,
98+
viewBarId: recordIndexId,
99+
});
100+
91101
const {
92102
visibleBoardFields,
93103
hiddenBoardFields,
@@ -153,6 +163,11 @@ export const RecordIndexOptionsDropdownContent = ({
153163
LeftIcon={IconFileExport}
154164
text={displayedExportProgress(progress)}
155165
/>
166+
<MenuItem
167+
onClick={handleToggleTrashColumnFilter}
168+
LeftIcon={IconTrash}
169+
text="Trash"
170+
/>
156171
</DropdownMenuItemsContainer>
157172
)}
158173
{currentMenu === 'fields' && (

packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx

+45-7
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,37 @@ import { useTheme } from '@emotion/react';
22
import styled from '@emotion/styled';
33
import { IconComponent, IconX } from 'twenty-ui';
44

5-
const StyledChip = styled.div`
5+
const StyledChip = styled.div<{ variant: SortOrFitlerChipVariant }>`
66
align-items: center;
7-
background-color: ${({ theme }) => theme.accent.quaternary};
8-
border: 1px solid ${({ theme }) => theme.accent.tertiary};
7+
background-color: ${({ theme, variant }) => {
8+
switch (variant) {
9+
case 'delete':
10+
return theme.background.danger;
11+
case 'default':
12+
default:
13+
return theme.accent.quaternary;
14+
}
15+
}};
16+
border: 1px solid
17+
${({ theme, variant }) => {
18+
switch (variant) {
19+
case 'delete':
20+
return theme.border.color.danger;
21+
case 'default':
22+
default:
23+
return theme.accent.tertiary;
24+
}
25+
}};
926
border-radius: 4px;
10-
color: ${({ theme }) => theme.color.blue};
27+
color: ${({ theme, variant }) => {
28+
switch (variant) {
29+
case 'delete':
30+
return theme.color.red;
31+
case 'default':
32+
default:
33+
return theme.color.blue;
34+
}
35+
}};
1136
cursor: pointer;
1237
display: flex;
1338
flex-direction: row;
@@ -24,7 +49,7 @@ const StyledIcon = styled.div`
2449
margin-right: ${({ theme }) => theme.spacing(1)};
2550
`;
2651

27-
const StyledDelete = styled.div`
52+
const StyledDelete = styled.div<{ variant: SortOrFitlerChipVariant }>`
2853
align-items: center;
2954
cursor: pointer;
3055
display: flex;
@@ -33,7 +58,15 @@ const StyledDelete = styled.div`
3358
margin-top: 1px;
3459
user-select: none;
3560
&:hover {
36-
background-color: ${({ theme }) => theme.accent.secondary};
61+
background-color: ${({ theme, variant }) => {
62+
switch (variant) {
63+
case 'delete':
64+
return theme.color.red20;
65+
case 'default':
66+
default:
67+
return theme.accent.secondary;
68+
}
69+
}};
3770
border-radius: ${({ theme }) => theme.border.radius.sm};
3871
}
3972
`;
@@ -42,9 +75,12 @@ const StyledLabelKey = styled.div`
4275
font-weight: ${({ theme }) => theme.font.weight.medium};
4376
`;
4477

78+
type SortOrFitlerChipVariant = 'default' | 'delete';
79+
4580
type SortOrFilterChipProps = {
4681
labelKey?: string;
4782
labelValue: string;
83+
variant?: SortOrFitlerChipVariant;
4884
Icon?: IconComponent;
4985
onRemove: () => void;
5086
onClick?: () => void;
@@ -54,6 +90,7 @@ type SortOrFilterChipProps = {
5490
export const SortOrFilterChip = ({
5591
labelKey,
5692
labelValue,
93+
variant = 'default',
5794
Icon,
5895
onRemove,
5996
testId,
@@ -67,7 +104,7 @@ export const SortOrFilterChip = ({
67104
};
68105

69106
return (
70-
<StyledChip onClick={onClick}>
107+
<StyledChip onClick={onClick} variant={variant}>
71108
{Icon && (
72109
<StyledIcon>
73110
<Icon size={theme.icon.size.sm} />
@@ -76,6 +113,7 @@ export const SortOrFilterChip = ({
76113
{labelKey && <StyledLabelKey>{labelKey}</StyledLabelKey>}
77114
{labelValue}
78115
<StyledDelete
116+
variant={variant}
79117
onClick={handleDeleteClick}
80118
data-testid={'remove-icon-' + testId}
81119
>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useIcons } from 'twenty-ui';
2+
3+
import { SortOrFilterChip } from '@/views/components/SortOrFilterChip';
4+
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
5+
import { useCombinedViewFilters } from '@/views/hooks/useCombinedViewFilters';
6+
import { useMemo } from 'react';
7+
8+
type VariantFilterChipProps = {
9+
viewFilter: Filter;
10+
};
11+
12+
export const VariantFilterChip = ({ viewFilter }: VariantFilterChipProps) => {
13+
const { removeCombinedViewFilter } = useCombinedViewFilters();
14+
15+
const { getIcon } = useIcons();
16+
17+
const handleRemoveClick = () => {
18+
// FixMe: Why it's not working ?
19+
removeCombinedViewFilter(viewFilter.fieldMetadataId);
20+
};
21+
22+
const variant = useMemo(() => {
23+
switch (viewFilter.variant) {
24+
case 'trash':
25+
return 'delete';
26+
case 'default':
27+
default:
28+
return 'default';
29+
}
30+
}, [viewFilter.variant]);
31+
32+
return (
33+
<SortOrFilterChip
34+
key={viewFilter.fieldMetadataId}
35+
testId={viewFilter.fieldMetadataId}
36+
variant={variant}
37+
labelValue={viewFilter.definition.label}
38+
Icon={getIcon(viewFilter.definition.iconName)}
39+
onRemove={handleRemoveClick}
40+
/>
41+
);
42+
};

packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx

+48-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ReactNode } from 'react';
1+
import { ReactNode, useMemo } from 'react';
22
import styled from '@emotion/styled';
33
import { useRecoilValue } from 'recoil';
44

@@ -14,6 +14,8 @@ import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
1414
import { useResetCurrentView } from '@/views/hooks/useResetCurrentView';
1515
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
1616
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
17+
import { VariantFilterChip } from './VariantFilterChip';
18+
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
1719

1820
export type ViewBarDetailsProps = {
1921
hasFilterButton?: boolean;
@@ -118,6 +120,29 @@ export const ViewBarDetails = ({
118120
const { resetCurrentView } = useResetCurrentView();
119121
const canResetView = canPersistView && !hasFiltersQueryParams;
120122

123+
const { otherViewFilters, defaultViewFilters } = useMemo(() => {
124+
if (!currentViewWithCombinedFiltersAndSorts) {
125+
return {
126+
otherViewFilters: [],
127+
defaultViewFilters: [],
128+
};
129+
}
130+
131+
const otherViewFilters =
132+
currentViewWithCombinedFiltersAndSorts.viewFilters.filter(
133+
(viewFilter) => viewFilter.variant && viewFilter.variant !== 'default',
134+
);
135+
const defaultViewFilters =
136+
currentViewWithCombinedFiltersAndSorts.viewFilters.filter(
137+
(viewFilter) => !viewFilter.variant || viewFilter.variant === 'default',
138+
);
139+
140+
return {
141+
otherViewFilters,
142+
defaultViewFilters,
143+
};
144+
}, [currentViewWithCombinedFiltersAndSorts]);
145+
121146
const handleCancelClick = () => {
122147
resetCurrentView();
123148
};
@@ -132,24 +157,44 @@ export const ViewBarDetails = ({
132157
return null;
133158
}
134159

160+
console.log(
161+
'currentViewWithCombinedFiltersAndSorts?.viewFilters: ',
162+
currentViewWithCombinedFiltersAndSorts?.viewFilters,
163+
);
164+
135165
return (
136166
<StyledBar>
137167
<StyledFilterContainer>
138168
<StyledChipcontainer>
169+
{otherViewFilters.map((viewFilter) => (
170+
<VariantFilterChip
171+
key={viewFilter.fieldMetadataId}
172+
// Why do we have two types, Filter and ViewFilter?
173+
// Why key defition is already present in the Filter type and added on the fly here with mapViewFiltersToFilters ?
174+
// FixMe: Ugly hack to make it work
175+
viewFilter={viewFilter as unknown as Filter}
176+
/>
177+
))}
178+
{!!otherViewFilters.length &&
179+
!!currentViewWithCombinedFiltersAndSorts?.viewSorts?.length && (
180+
<StyledSeperatorContainer>
181+
<StyledSeperator />
182+
</StyledSeperatorContainer>
183+
)}
139184
{mapViewSortsToSorts(
140185
currentViewWithCombinedFiltersAndSorts?.viewSorts ?? [],
141186
availableSortDefinitions,
142187
).map((sort) => (
143188
<EditableSortChip key={sort.fieldMetadataId} viewSort={sort} />
144189
))}
145190
{!!currentViewWithCombinedFiltersAndSorts?.viewSorts?.length &&
146-
!!currentViewWithCombinedFiltersAndSorts?.viewFilters?.length && (
191+
!!defaultViewFilters.length && (
147192
<StyledSeperatorContainer>
148193
<StyledSeperator />
149194
</StyledSeperatorContainer>
150195
)}
151196
{mapViewFiltersToFilters(
152-
currentViewWithCombinedFiltersAndSorts?.viewFilters ?? [],
197+
defaultViewFilters,
153198
availableFilterDefinitions,
154199
).map((viewFilter) => (
155200
<ObjectFilterDropdownScope

packages/twenty-front/src/modules/views/types/ViewFilter.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ViewFilterOperand } from './ViewFilterOperand';
33
export type ViewFilter = {
44
__typename: 'ViewFilter';
55
id: string;
6+
variant?: 'default' | 'trash';
67
fieldMetadataId: string;
78
operand: ViewFilterOperand;
89
value: string;

0 commit comments

Comments
 (0)