From efd932e99b0c0eeee63f94d961cf20da659e8343 Mon Sep 17 00:00:00 2001 From: Weiko Date: Wed, 17 Jul 2024 17:54:37 +0200 Subject: [PATCH] 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 --- ...atFieldMetadataItemsAsFilterDefinitions.ts | 3 + ...rmatFieldMetadataItemsAsSortDefinitions.ts | 1 + .../MultipleFiltersDropdownContent.tsx | 4 ++ .../ObjectFilterDropdownRatingInput.tsx | 67 +++++++++++++++++++ .../types/FilterType.ts | 1 + .../utils/getOperandsForFilterType.ts | 7 ++ .../utils/isRecordMatchingFilter.ts | 1 + ...turnObjectDropdownFilterIntoQueryFilter.ts | 52 ++++++++++++++ .../EditableFilterDropdownButton.tsx | 8 ++- .../views/hooks/useCombinedViewFilters.ts | 17 +++-- .../useSaveCurrentViewFiltersAndSorts.ts | 14 ++-- .../views/utils/combinedViewFilters.ts | 11 ++- 12 files changed, 172 insertions(+), 14 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput.tsx 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;