diff --git a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts
index 318073af1..5ae8f9741 100644
--- a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts
+++ b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts
@@ -34,6 +34,7 @@ export const formatFieldMetadataItemsAsFilterDefinitions = ({
FieldMetadataType.Relation,
FieldMetadataType.Select,
FieldMetadataType.Currency,
+ FieldMetadataType.Rating,
].includes(field.type)
) {
return acc;
@@ -85,6 +86,8 @@ export const getFilterTypeFromFieldType = (fieldType: FieldMetadataType) => {
return 'MULTI_SELECT';
case FieldMetadataType.Address:
return 'ADDRESS';
+ case FieldMetadataType.Rating:
+ return 'RATING';
default:
return 'TEXT';
}
diff --git a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions.ts b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions.ts
index b62f7fb35..56aca5fcc 100644
--- a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions.ts
+++ b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions.ts
@@ -20,6 +20,7 @@ export const formatFieldMetadataItemsAsSortDefinitions = ({
FieldMetadataType.Phone,
FieldMetadataType.Email,
FieldMetadataType.FullName,
+ FieldMetadataType.Rating,
].includes(field.type)
) {
return acc;
diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx
index cd49fd096..142f3d8ca 100644
--- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx
+++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx
@@ -5,6 +5,7 @@ import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
+import { ObjectFilterDropdownRatingInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput';
import { MultipleFiltersDropdownFilterOnFilterChangedEffect } from './MultipleFiltersDropdownFilterOnFilterChangedEffect';
import { ObjectFilterDropdownDateInput } from './ObjectFilterDropdownDateInput';
import { ObjectFilterDropdownFilterSelect } from './ObjectFilterDropdownFilterSelect';
@@ -70,6 +71,9 @@ export const MultipleFiltersDropdownContent = ({
{['NUMBER', 'CURRENCY'].includes(
filterDefinitionUsedInDropdown.type,
) && }
+ {filterDefinitionUsedInDropdown.type === 'RATING' && (
+
+ )}
{filterDefinitionUsedInDropdown.type === 'DATE_TIME' && (
)}
diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput.tsx
new file mode 100644
index 000000000..73d0b4bbb
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput.tsx
@@ -0,0 +1,67 @@
+import { useRecoilValue } from 'recoil';
+import { v4 } from 'uuid';
+
+import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
+import { RATING_VALUES } from '@/object-record/record-field/meta-types/constants/RatingValues';
+import { FieldRatingValue } from '@/object-record/record-field/types/FieldMetadata';
+import { RatingInput } from '@/ui/field/input/components/RatingInput';
+import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
+
+const convertFieldRatingValueToNumber = (rating: FieldRatingValue): string => {
+ return rating.split('_')[1];
+};
+
+export const convertGreaterThanRatingToArrayOfRatingValues = (
+ greaterThanValue: number,
+) => {
+ return RATING_VALUES.filter((_, index) => index + 1 > greaterThanValue);
+};
+
+export const convertLessThanRatingToArrayOfRatingValues = (
+ lessThanValue: number,
+) => {
+ return RATING_VALUES.filter((_, index) => index + 1 <= lessThanValue);
+};
+
+export const convertRatingToRatingValue = (rating: number) => {
+ return `RATING_${rating}`;
+};
+
+export const ObjectFilterDropdownRatingInput = () => {
+ const {
+ selectedOperandInDropdownState,
+ filterDefinitionUsedInDropdownState,
+ selectedFilterState,
+ selectFilter,
+ } = useFilterDropdown();
+
+ const filterDefinitionUsedInDropdown = useRecoilValue(
+ filterDefinitionUsedInDropdownState,
+ );
+ const selectedOperandInDropdown = useRecoilValue(
+ selectedOperandInDropdownState,
+ );
+
+ const selectedFilter = useRecoilValue(selectedFilterState);
+
+ return (
+ filterDefinitionUsedInDropdown &&
+ selectedOperandInDropdown && (
+
+ {
+ selectFilter?.({
+ id: selectedFilter?.id ? selectedFilter.id : v4(),
+ fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
+ value: convertFieldRatingValueToNumber(newValue),
+ operand: selectedOperandInDropdown,
+ displayValue: convertFieldRatingValueToNumber(newValue),
+ definition: filterDefinitionUsedInDropdown,
+ });
+ }}
+ />
+
+ )
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterType.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterType.ts
index e541b8daf..7b081d87b 100644
--- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterType.ts
+++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterType.ts
@@ -12,4 +12,5 @@ export type FilterType =
| 'RELATION'
| 'ADDRESS'
| 'SELECT'
+ | 'RATING'
| 'MULTI_SELECT';
diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts
index 7f189009b..f1196ea79 100644
--- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts
+++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts
@@ -34,6 +34,13 @@ export const getOperandsForFilterType = (
ViewFilterOperand.LessThan,
...emptyOperands,
];
+ case 'RATING':
+ return [
+ ViewFilterOperand.Is,
+ ViewFilterOperand.GreaterThan,
+ ViewFilterOperand.LessThan,
+ ...emptyOperands,
+ ];
case 'RELATION':
return [...relationOperands, ...emptyOperands];
case 'SELECT':
diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts
index 0af419f37..b5ec81678 100644
--- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts
+++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts
@@ -143,6 +143,7 @@ export const isRecordMatchingFilter = ({
case FieldMetadataType.Email:
case FieldMetadataType.Phone:
case FieldMetadataType.Select:
+ case FieldMetadataType.Rating:
case FieldMetadataType.MultiSelect:
case FieldMetadataType.Text: {
return isMatchingStringFilter({
diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts
index 1b08eace5..7cecb828b 100644
--- a/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts
+++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts
@@ -18,6 +18,11 @@ import { Field } from '~/generated/graphql';
import { generateILikeFiltersForCompositeFields } from '~/utils/array/generateILikeFiltersForCompositeFields';
import { isDefined } from '~/utils/isDefined';
+import {
+ convertGreaterThanRatingToArrayOfRatingValues,
+ convertLessThanRatingToArrayOfRatingValues,
+ convertRatingToRatingValue,
+} from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput';
import { Filter } from '../../object-filter-dropdown/types/Filter';
export type ObjectDropdownFilter = Omit & {
@@ -187,6 +192,11 @@ const applyEmptyFilters = (
[correspondingField.name]: { is: 'NULL' } as FloatFilter,
};
break;
+ case 'RATING':
+ emptyRecordFilter = {
+ [correspondingField.name]: { is: 'NULL' } as StringFilter,
+ };
+ break;
case 'DATE_TIME':
emptyRecordFilter = {
[correspondingField.name]: { is: 'NULL' } as DateFilter,
@@ -313,6 +323,48 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
);
}
break;
+ case 'RATING':
+ switch (rawUIFilter.operand) {
+ case ViewFilterOperand.Is:
+ objectRecordFilters.push({
+ [correspondingField.name]: {
+ eq: convertRatingToRatingValue(parseFloat(rawUIFilter.value)),
+ } as StringFilter,
+ });
+ break;
+ case ViewFilterOperand.GreaterThan:
+ objectRecordFilters.push({
+ [correspondingField.name]: {
+ in: convertGreaterThanRatingToArrayOfRatingValues(
+ parseFloat(rawUIFilter.value),
+ ),
+ } as StringFilter,
+ });
+ break;
+ case ViewFilterOperand.LessThan:
+ objectRecordFilters.push({
+ [correspondingField.name]: {
+ in: convertLessThanRatingToArrayOfRatingValues(
+ parseFloat(rawUIFilter.value),
+ ),
+ } as StringFilter,
+ });
+ break;
+ case ViewFilterOperand.IsEmpty:
+ case ViewFilterOperand.IsNotEmpty:
+ applyEmptyFilters(
+ rawUIFilter.operand,
+ correspondingField,
+ objectRecordFilters,
+ rawUIFilter.definition.type,
+ );
+ break;
+ default:
+ throw new Error(
+ `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`,
+ );
+ }
+ break;
case 'NUMBER':
switch (rawUIFilter.operand) {
case ViewFilterOperand.GreaterThan:
diff --git a/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx b/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx
index ee5f17968..23613d5c3 100644
--- a/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx
+++ b/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx
@@ -4,6 +4,7 @@ import { useRecoilValue } from 'recoil';
import { MultipleFiltersDropdownContent } from '@/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent';
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
+import { FilterOperand } from '@/object-record/object-filter-dropdown/types/FilterOperand';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
@@ -66,8 +67,11 @@ export const EditableFilterDropdownButton = ({
};
const handleDropdownClickOutside = useCallback(() => {
- const { id: fieldId, value } = viewFilter;
- if (!value) {
+ const { id: fieldId, value, operand } = viewFilter;
+ if (
+ !value &&
+ ![FilterOperand.IsEmpty, FilterOperand.IsNotEmpty].includes(operand)
+ ) {
removeCombinedViewFilter(fieldId);
}
}, [viewFilter, removeCombinedViewFilter]);
diff --git a/packages/twenty-front/src/modules/views/hooks/useCombinedViewFilters.ts b/packages/twenty-front/src/modules/views/hooks/useCombinedViewFilters.ts
index 042ef8f33..fb93ea820 100644
--- a/packages/twenty-front/src/modules/views/hooks/useCombinedViewFilters.ts
+++ b/packages/twenty-front/src/modules/views/hooks/useCombinedViewFilters.ts
@@ -42,17 +42,20 @@ export const useCombinedViewFilters = (viewBarComponentId?: string) => {
}
const matchingFilterInCurrentView = currentView.viewFilters.find(
- (viewFilter) => viewFilter.id === upsertedFilter.id,
+ (viewFilter) =>
+ viewFilter.fieldMetadataId === upsertedFilter.fieldMetadataId,
);
const matchingFilterInUnsavedFilters = unsavedToUpsertViewFilters.find(
- (viewFilter) => viewFilter.id === upsertedFilter.id,
+ (viewFilter) =>
+ viewFilter.fieldMetadataId === upsertedFilter.fieldMetadataId,
);
if (isDefined(matchingFilterInUnsavedFilters)) {
const updatedFilters = unsavedToUpsertViewFilters.map((viewFilter) =>
- viewFilter.id === matchingFilterInUnsavedFilters.id
- ? { ...viewFilter, ...upsertedFilter }
+ viewFilter.fieldMetadataId ===
+ matchingFilterInUnsavedFilters.fieldMetadataId
+ ? { ...viewFilter, ...upsertedFilter, id: viewFilter.id }
: viewFilter,
);
@@ -63,7 +66,11 @@ export const useCombinedViewFilters = (viewBarComponentId?: string) => {
if (isDefined(matchingFilterInCurrentView)) {
set(unsavedToUpsertViewFiltersState, [
...unsavedToUpsertViewFilters,
- { ...matchingFilterInCurrentView, ...upsertedFilter },
+ {
+ ...matchingFilterInCurrentView,
+ ...upsertedFilter,
+ id: matchingFilterInCurrentView.id,
+ },
]);
set(
unsavedToDeleteViewFilterIdsState,
diff --git a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFiltersAndSorts.ts b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFiltersAndSorts.ts
index 1cfe064b0..6370fe0ca 100644
--- a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFiltersAndSorts.ts
+++ b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFiltersAndSorts.ts
@@ -58,12 +58,14 @@ export const useSaveCurrentViewFiltersAndSorts = (
const viewSortsToCreate = unsavedToUpsertViewSorts.filter(
(viewSort) =>
!view.viewSorts.some(
- (vf) => vf.fieldMetadataId === viewSort.fieldMetadataId,
+ (vs) => vs.fieldMetadataId === viewSort.fieldMetadataId,
),
);
const viewSortsToUpdate = unsavedToUpsertViewSorts.filter((viewSort) =>
- view.viewSorts.some((vf) => vf.id === viewSort.id),
+ view.viewSorts.some(
+ (vs) => vs.fieldMetadataId === viewSort.fieldMetadataId,
+ ),
);
await createViewSortRecords(viewSortsToCreate, view);
@@ -101,12 +103,16 @@ export const useSaveCurrentViewFiltersAndSorts = (
const viewFiltersToCreate = unsavedToUpsertViewFilters.filter(
(viewFilter) =>
- !view.viewFilters.some((vf) => vf.id === viewFilter.id),
+ !view.viewFilters.some(
+ (vf) => vf.fieldMetadataId === viewFilter.fieldMetadataId,
+ ),
);
const viewFiltersToUpdate = unsavedToUpsertViewFilters.filter(
(viewFilter) =>
- view.viewFilters.some((vf) => vf.id === viewFilter.id),
+ view.viewFilters.some(
+ (vf) => vf.fieldMetadataId === viewFilter.fieldMetadataId,
+ ),
);
await createViewFilterRecords(viewFiltersToCreate, view);
diff --git a/packages/twenty-front/src/modules/views/utils/combinedViewFilters.ts b/packages/twenty-front/src/modules/views/utils/combinedViewFilters.ts
index 09dda1fd2..dc205c394 100644
--- a/packages/twenty-front/src/modules/views/utils/combinedViewFilters.ts
+++ b/packages/twenty-front/src/modules/views/utils/combinedViewFilters.ts
@@ -8,19 +8,24 @@ export const combinedViewFilters = (
const toCreateViewFilters = toUpsertViewFilters.filter(
(toUpsertViewFilter) =>
!viewFilters.some(
- (viewFilter) => viewFilter.id === toUpsertViewFilter.id,
+ (viewFilter) =>
+ viewFilter.fieldMetadataId === toUpsertViewFilter.fieldMetadataId,
),
);
const toUpdateViewFilters = toUpsertViewFilters.filter((toUpsertViewFilter) =>
- viewFilters.some((viewFilter) => viewFilter.id === toUpsertViewFilter.id),
+ viewFilters.some(
+ (viewFilter) =>
+ viewFilter.fieldMetadataId === toUpsertViewFilter.fieldMetadataId,
+ ),
);
const combinedViewFilters = viewFilters
.filter((viewFilter) => !toDeleteViewFilterIds.includes(viewFilter.id))
.map((viewFilter) => {
const toUpdateViewFilter = toUpdateViewFilters.find(
- (toUpdateViewFilter) => toUpdateViewFilter.id === viewFilter.id,
+ (toUpdateViewFilter) =>
+ toUpdateViewFilter.fieldMetadataId === viewFilter.fieldMetadataId,
);
return toUpdateViewFilter ?? viewFilter;