Skip to content

Commit efd932e

Browse files
authored
Add rating filter/sort + fix isEmpty/isNotEmpty + fix combinedViewFilters (#6310)
## Context - Adding RATING sort and filter capabilities. - Fixing isEmpty/isNotEmpty filters - Fixing combined view filters so it combines filters per field metadata and not per filter id. This is more a product question but to me it does not make sense to apply multiples filters on the same field IF the operations is wrapped in a AND. If at some point we want to put a OR instead then that would make more sense
1 parent 47ddc7b commit efd932e

12 files changed

+172
-14
lines changed

packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts

+3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export const formatFieldMetadataItemsAsFilterDefinitions = ({
3434
FieldMetadataType.Relation,
3535
FieldMetadataType.Select,
3636
FieldMetadataType.Currency,
37+
FieldMetadataType.Rating,
3738
].includes(field.type)
3839
) {
3940
return acc;
@@ -85,6 +86,8 @@ export const getFilterTypeFromFieldType = (fieldType: FieldMetadataType) => {
8586
return 'MULTI_SELECT';
8687
case FieldMetadataType.Address:
8788
return 'ADDRESS';
89+
case FieldMetadataType.Rating:
90+
return 'RATING';
8891
default:
8992
return 'TEXT';
9093
}

packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions.ts

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const formatFieldMetadataItemsAsSortDefinitions = ({
2020
FieldMetadataType.Phone,
2121
FieldMetadataType.Email,
2222
FieldMetadataType.FullName,
23+
FieldMetadataType.Rating,
2324
].includes(field.type)
2425
) {
2526
return acc;

packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/
55
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
66
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
77

8+
import { ObjectFilterDropdownRatingInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput';
89
import { MultipleFiltersDropdownFilterOnFilterChangedEffect } from './MultipleFiltersDropdownFilterOnFilterChangedEffect';
910
import { ObjectFilterDropdownDateInput } from './ObjectFilterDropdownDateInput';
1011
import { ObjectFilterDropdownFilterSelect } from './ObjectFilterDropdownFilterSelect';
@@ -70,6 +71,9 @@ export const MultipleFiltersDropdownContent = ({
7071
{['NUMBER', 'CURRENCY'].includes(
7172
filterDefinitionUsedInDropdown.type,
7273
) && <ObjectFilterDropdownNumberInput />}
74+
{filterDefinitionUsedInDropdown.type === 'RATING' && (
75+
<ObjectFilterDropdownRatingInput />
76+
)}
7377
{filterDefinitionUsedInDropdown.type === 'DATE_TIME' && (
7478
<ObjectFilterDropdownDateInput />
7579
)}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { useRecoilValue } from 'recoil';
2+
import { v4 } from 'uuid';
3+
4+
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
5+
import { RATING_VALUES } from '@/object-record/record-field/meta-types/constants/RatingValues';
6+
import { FieldRatingValue } from '@/object-record/record-field/types/FieldMetadata';
7+
import { RatingInput } from '@/ui/field/input/components/RatingInput';
8+
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
9+
10+
const convertFieldRatingValueToNumber = (rating: FieldRatingValue): string => {
11+
return rating.split('_')[1];
12+
};
13+
14+
export const convertGreaterThanRatingToArrayOfRatingValues = (
15+
greaterThanValue: number,
16+
) => {
17+
return RATING_VALUES.filter((_, index) => index + 1 > greaterThanValue);
18+
};
19+
20+
export const convertLessThanRatingToArrayOfRatingValues = (
21+
lessThanValue: number,
22+
) => {
23+
return RATING_VALUES.filter((_, index) => index + 1 <= lessThanValue);
24+
};
25+
26+
export const convertRatingToRatingValue = (rating: number) => {
27+
return `RATING_${rating}`;
28+
};
29+
30+
export const ObjectFilterDropdownRatingInput = () => {
31+
const {
32+
selectedOperandInDropdownState,
33+
filterDefinitionUsedInDropdownState,
34+
selectedFilterState,
35+
selectFilter,
36+
} = useFilterDropdown();
37+
38+
const filterDefinitionUsedInDropdown = useRecoilValue(
39+
filterDefinitionUsedInDropdownState,
40+
);
41+
const selectedOperandInDropdown = useRecoilValue(
42+
selectedOperandInDropdownState,
43+
);
44+
45+
const selectedFilter = useRecoilValue(selectedFilterState);
46+
47+
return (
48+
filterDefinitionUsedInDropdown &&
49+
selectedOperandInDropdown && (
50+
<DropdownMenuItemsContainer>
51+
<RatingInput
52+
value={selectedFilter?.value as FieldRatingValue}
53+
onChange={(newValue: FieldRatingValue) => {
54+
selectFilter?.({
55+
id: selectedFilter?.id ? selectedFilter.id : v4(),
56+
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
57+
value: convertFieldRatingValueToNumber(newValue),
58+
operand: selectedOperandInDropdown,
59+
displayValue: convertFieldRatingValueToNumber(newValue),
60+
definition: filterDefinitionUsedInDropdown,
61+
});
62+
}}
63+
/>
64+
</DropdownMenuItemsContainer>
65+
)
66+
);
67+
};

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

+1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ export type FilterType =
1212
| 'RELATION'
1313
| 'ADDRESS'
1414
| 'SELECT'
15+
| 'RATING'
1516
| 'MULTI_SELECT';

packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts

+7
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ export const getOperandsForFilterType = (
3434
ViewFilterOperand.LessThan,
3535
...emptyOperands,
3636
];
37+
case 'RATING':
38+
return [
39+
ViewFilterOperand.Is,
40+
ViewFilterOperand.GreaterThan,
41+
ViewFilterOperand.LessThan,
42+
...emptyOperands,
43+
];
3744
case 'RELATION':
3845
return [...relationOperands, ...emptyOperands];
3946
case 'SELECT':

packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts

+1
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ export const isRecordMatchingFilter = ({
143143
case FieldMetadataType.Email:
144144
case FieldMetadataType.Phone:
145145
case FieldMetadataType.Select:
146+
case FieldMetadataType.Rating:
146147
case FieldMetadataType.MultiSelect:
147148
case FieldMetadataType.Text: {
148149
return isMatchingStringFilter({

packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts

+52
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ import { Field } from '~/generated/graphql';
1818
import { generateILikeFiltersForCompositeFields } from '~/utils/array/generateILikeFiltersForCompositeFields';
1919
import { isDefined } from '~/utils/isDefined';
2020

21+
import {
22+
convertGreaterThanRatingToArrayOfRatingValues,
23+
convertLessThanRatingToArrayOfRatingValues,
24+
convertRatingToRatingValue,
25+
} from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput';
2126
import { Filter } from '../../object-filter-dropdown/types/Filter';
2227

2328
export type ObjectDropdownFilter = Omit<Filter, 'definition'> & {
@@ -187,6 +192,11 @@ const applyEmptyFilters = (
187192
[correspondingField.name]: { is: 'NULL' } as FloatFilter,
188193
};
189194
break;
195+
case 'RATING':
196+
emptyRecordFilter = {
197+
[correspondingField.name]: { is: 'NULL' } as StringFilter,
198+
};
199+
break;
190200
case 'DATE_TIME':
191201
emptyRecordFilter = {
192202
[correspondingField.name]: { is: 'NULL' } as DateFilter,
@@ -313,6 +323,48 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
313323
);
314324
}
315325
break;
326+
case 'RATING':
327+
switch (rawUIFilter.operand) {
328+
case ViewFilterOperand.Is:
329+
objectRecordFilters.push({
330+
[correspondingField.name]: {
331+
eq: convertRatingToRatingValue(parseFloat(rawUIFilter.value)),
332+
} as StringFilter,
333+
});
334+
break;
335+
case ViewFilterOperand.GreaterThan:
336+
objectRecordFilters.push({
337+
[correspondingField.name]: {
338+
in: convertGreaterThanRatingToArrayOfRatingValues(
339+
parseFloat(rawUIFilter.value),
340+
),
341+
} as StringFilter,
342+
});
343+
break;
344+
case ViewFilterOperand.LessThan:
345+
objectRecordFilters.push({
346+
[correspondingField.name]: {
347+
in: convertLessThanRatingToArrayOfRatingValues(
348+
parseFloat(rawUIFilter.value),
349+
),
350+
} as StringFilter,
351+
});
352+
break;
353+
case ViewFilterOperand.IsEmpty:
354+
case ViewFilterOperand.IsNotEmpty:
355+
applyEmptyFilters(
356+
rawUIFilter.operand,
357+
correspondingField,
358+
objectRecordFilters,
359+
rawUIFilter.definition.type,
360+
);
361+
break;
362+
default:
363+
throw new Error(
364+
`Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`,
365+
);
366+
}
367+
break;
316368
case 'NUMBER':
317369
switch (rawUIFilter.operand) {
318370
case ViewFilterOperand.GreaterThan:

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

+6-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useRecoilValue } from 'recoil';
44
import { MultipleFiltersDropdownContent } from '@/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent';
55
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
66
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
7+
import { FilterOperand } from '@/object-record/object-filter-dropdown/types/FilterOperand';
78
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
89
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
910
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
@@ -66,8 +67,11 @@ export const EditableFilterDropdownButton = ({
6667
};
6768

6869
const handleDropdownClickOutside = useCallback(() => {
69-
const { id: fieldId, value } = viewFilter;
70-
if (!value) {
70+
const { id: fieldId, value, operand } = viewFilter;
71+
if (
72+
!value &&
73+
![FilterOperand.IsEmpty, FilterOperand.IsNotEmpty].includes(operand)
74+
) {
7175
removeCombinedViewFilter(fieldId);
7276
}
7377
}, [viewFilter, removeCombinedViewFilter]);

packages/twenty-front/src/modules/views/hooks/useCombinedViewFilters.ts

+12-5
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,20 @@ export const useCombinedViewFilters = (viewBarComponentId?: string) => {
4242
}
4343

4444
const matchingFilterInCurrentView = currentView.viewFilters.find(
45-
(viewFilter) => viewFilter.id === upsertedFilter.id,
45+
(viewFilter) =>
46+
viewFilter.fieldMetadataId === upsertedFilter.fieldMetadataId,
4647
);
4748

4849
const matchingFilterInUnsavedFilters = unsavedToUpsertViewFilters.find(
49-
(viewFilter) => viewFilter.id === upsertedFilter.id,
50+
(viewFilter) =>
51+
viewFilter.fieldMetadataId === upsertedFilter.fieldMetadataId,
5052
);
5153

5254
if (isDefined(matchingFilterInUnsavedFilters)) {
5355
const updatedFilters = unsavedToUpsertViewFilters.map((viewFilter) =>
54-
viewFilter.id === matchingFilterInUnsavedFilters.id
55-
? { ...viewFilter, ...upsertedFilter }
56+
viewFilter.fieldMetadataId ===
57+
matchingFilterInUnsavedFilters.fieldMetadataId
58+
? { ...viewFilter, ...upsertedFilter, id: viewFilter.id }
5659
: viewFilter,
5760
);
5861

@@ -63,7 +66,11 @@ export const useCombinedViewFilters = (viewBarComponentId?: string) => {
6366
if (isDefined(matchingFilterInCurrentView)) {
6467
set(unsavedToUpsertViewFiltersState, [
6568
...unsavedToUpsertViewFilters,
66-
{ ...matchingFilterInCurrentView, ...upsertedFilter },
69+
{
70+
...matchingFilterInCurrentView,
71+
...upsertedFilter,
72+
id: matchingFilterInCurrentView.id,
73+
},
6774
]);
6875
set(
6976
unsavedToDeleteViewFilterIdsState,

packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFiltersAndSorts.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,14 @@ export const useSaveCurrentViewFiltersAndSorts = (
5858
const viewSortsToCreate = unsavedToUpsertViewSorts.filter(
5959
(viewSort) =>
6060
!view.viewSorts.some(
61-
(vf) => vf.fieldMetadataId === viewSort.fieldMetadataId,
61+
(vs) => vs.fieldMetadataId === viewSort.fieldMetadataId,
6262
),
6363
);
6464

6565
const viewSortsToUpdate = unsavedToUpsertViewSorts.filter((viewSort) =>
66-
view.viewSorts.some((vf) => vf.id === viewSort.id),
66+
view.viewSorts.some(
67+
(vs) => vs.fieldMetadataId === viewSort.fieldMetadataId,
68+
),
6769
);
6870

6971
await createViewSortRecords(viewSortsToCreate, view);
@@ -101,12 +103,16 @@ export const useSaveCurrentViewFiltersAndSorts = (
101103

102104
const viewFiltersToCreate = unsavedToUpsertViewFilters.filter(
103105
(viewFilter) =>
104-
!view.viewFilters.some((vf) => vf.id === viewFilter.id),
106+
!view.viewFilters.some(
107+
(vf) => vf.fieldMetadataId === viewFilter.fieldMetadataId,
108+
),
105109
);
106110

107111
const viewFiltersToUpdate = unsavedToUpsertViewFilters.filter(
108112
(viewFilter) =>
109-
view.viewFilters.some((vf) => vf.id === viewFilter.id),
113+
view.viewFilters.some(
114+
(vf) => vf.fieldMetadataId === viewFilter.fieldMetadataId,
115+
),
110116
);
111117

112118
await createViewFilterRecords(viewFiltersToCreate, view);

packages/twenty-front/src/modules/views/utils/combinedViewFilters.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,24 @@ export const combinedViewFilters = (
88
const toCreateViewFilters = toUpsertViewFilters.filter(
99
(toUpsertViewFilter) =>
1010
!viewFilters.some(
11-
(viewFilter) => viewFilter.id === toUpsertViewFilter.id,
11+
(viewFilter) =>
12+
viewFilter.fieldMetadataId === toUpsertViewFilter.fieldMetadataId,
1213
),
1314
);
1415

1516
const toUpdateViewFilters = toUpsertViewFilters.filter((toUpsertViewFilter) =>
16-
viewFilters.some((viewFilter) => viewFilter.id === toUpsertViewFilter.id),
17+
viewFilters.some(
18+
(viewFilter) =>
19+
viewFilter.fieldMetadataId === toUpsertViewFilter.fieldMetadataId,
20+
),
1721
);
1822

1923
const combinedViewFilters = viewFilters
2024
.filter((viewFilter) => !toDeleteViewFilterIds.includes(viewFilter.id))
2125
.map((viewFilter) => {
2226
const toUpdateViewFilter = toUpdateViewFilters.find(
23-
(toUpdateViewFilter) => toUpdateViewFilter.id === viewFilter.id,
27+
(toUpdateViewFilter) =>
28+
toUpdateViewFilter.fieldMetadataId === viewFilter.fieldMetadataId,
2429
);
2530

2631
return toUpdateViewFilter ?? viewFilter;

0 commit comments

Comments
 (0)