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
This commit is contained in:
@ -34,6 +34,7 @@ export const formatFieldMetadataItemsAsFilterDefinitions = ({
|
|||||||
FieldMetadataType.Relation,
|
FieldMetadataType.Relation,
|
||||||
FieldMetadataType.Select,
|
FieldMetadataType.Select,
|
||||||
FieldMetadataType.Currency,
|
FieldMetadataType.Currency,
|
||||||
|
FieldMetadataType.Rating,
|
||||||
].includes(field.type)
|
].includes(field.type)
|
||||||
) {
|
) {
|
||||||
return acc;
|
return acc;
|
||||||
@ -85,6 +86,8 @@ export const getFilterTypeFromFieldType = (fieldType: FieldMetadataType) => {
|
|||||||
return 'MULTI_SELECT';
|
return 'MULTI_SELECT';
|
||||||
case FieldMetadataType.Address:
|
case FieldMetadataType.Address:
|
||||||
return 'ADDRESS';
|
return 'ADDRESS';
|
||||||
|
case FieldMetadataType.Rating:
|
||||||
|
return 'RATING';
|
||||||
default:
|
default:
|
||||||
return 'TEXT';
|
return 'TEXT';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,7 @@ export const formatFieldMetadataItemsAsSortDefinitions = ({
|
|||||||
FieldMetadataType.Phone,
|
FieldMetadataType.Phone,
|
||||||
FieldMetadataType.Email,
|
FieldMetadataType.Email,
|
||||||
FieldMetadataType.FullName,
|
FieldMetadataType.FullName,
|
||||||
|
FieldMetadataType.Rating,
|
||||||
].includes(field.type)
|
].includes(field.type)
|
||||||
) {
|
) {
|
||||||
return acc;
|
return acc;
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/
|
|||||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||||
|
|
||||||
|
import { ObjectFilterDropdownRatingInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput';
|
||||||
import { MultipleFiltersDropdownFilterOnFilterChangedEffect } from './MultipleFiltersDropdownFilterOnFilterChangedEffect';
|
import { MultipleFiltersDropdownFilterOnFilterChangedEffect } from './MultipleFiltersDropdownFilterOnFilterChangedEffect';
|
||||||
import { ObjectFilterDropdownDateInput } from './ObjectFilterDropdownDateInput';
|
import { ObjectFilterDropdownDateInput } from './ObjectFilterDropdownDateInput';
|
||||||
import { ObjectFilterDropdownFilterSelect } from './ObjectFilterDropdownFilterSelect';
|
import { ObjectFilterDropdownFilterSelect } from './ObjectFilterDropdownFilterSelect';
|
||||||
@ -70,6 +71,9 @@ export const MultipleFiltersDropdownContent = ({
|
|||||||
{['NUMBER', 'CURRENCY'].includes(
|
{['NUMBER', 'CURRENCY'].includes(
|
||||||
filterDefinitionUsedInDropdown.type,
|
filterDefinitionUsedInDropdown.type,
|
||||||
) && <ObjectFilterDropdownNumberInput />}
|
) && <ObjectFilterDropdownNumberInput />}
|
||||||
|
{filterDefinitionUsedInDropdown.type === 'RATING' && (
|
||||||
|
<ObjectFilterDropdownRatingInput />
|
||||||
|
)}
|
||||||
{filterDefinitionUsedInDropdown.type === 'DATE_TIME' && (
|
{filterDefinitionUsedInDropdown.type === 'DATE_TIME' && (
|
||||||
<ObjectFilterDropdownDateInput />
|
<ObjectFilterDropdownDateInput />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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 && (
|
||||||
|
<DropdownMenuItemsContainer>
|
||||||
|
<RatingInput
|
||||||
|
value={selectedFilter?.value as FieldRatingValue}
|
||||||
|
onChange={(newValue: FieldRatingValue) => {
|
||||||
|
selectFilter?.({
|
||||||
|
id: selectedFilter?.id ? selectedFilter.id : v4(),
|
||||||
|
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
|
||||||
|
value: convertFieldRatingValueToNumber(newValue),
|
||||||
|
operand: selectedOperandInDropdown,
|
||||||
|
displayValue: convertFieldRatingValueToNumber(newValue),
|
||||||
|
definition: filterDefinitionUsedInDropdown,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -12,4 +12,5 @@ export type FilterType =
|
|||||||
| 'RELATION'
|
| 'RELATION'
|
||||||
| 'ADDRESS'
|
| 'ADDRESS'
|
||||||
| 'SELECT'
|
| 'SELECT'
|
||||||
|
| 'RATING'
|
||||||
| 'MULTI_SELECT';
|
| 'MULTI_SELECT';
|
||||||
|
|||||||
@ -34,6 +34,13 @@ export const getOperandsForFilterType = (
|
|||||||
ViewFilterOperand.LessThan,
|
ViewFilterOperand.LessThan,
|
||||||
...emptyOperands,
|
...emptyOperands,
|
||||||
];
|
];
|
||||||
|
case 'RATING':
|
||||||
|
return [
|
||||||
|
ViewFilterOperand.Is,
|
||||||
|
ViewFilterOperand.GreaterThan,
|
||||||
|
ViewFilterOperand.LessThan,
|
||||||
|
...emptyOperands,
|
||||||
|
];
|
||||||
case 'RELATION':
|
case 'RELATION':
|
||||||
return [...relationOperands, ...emptyOperands];
|
return [...relationOperands, ...emptyOperands];
|
||||||
case 'SELECT':
|
case 'SELECT':
|
||||||
|
|||||||
@ -143,6 +143,7 @@ export const isRecordMatchingFilter = ({
|
|||||||
case FieldMetadataType.Email:
|
case FieldMetadataType.Email:
|
||||||
case FieldMetadataType.Phone:
|
case FieldMetadataType.Phone:
|
||||||
case FieldMetadataType.Select:
|
case FieldMetadataType.Select:
|
||||||
|
case FieldMetadataType.Rating:
|
||||||
case FieldMetadataType.MultiSelect:
|
case FieldMetadataType.MultiSelect:
|
||||||
case FieldMetadataType.Text: {
|
case FieldMetadataType.Text: {
|
||||||
return isMatchingStringFilter({
|
return isMatchingStringFilter({
|
||||||
|
|||||||
@ -18,6 +18,11 @@ import { Field } from '~/generated/graphql';
|
|||||||
import { generateILikeFiltersForCompositeFields } from '~/utils/array/generateILikeFiltersForCompositeFields';
|
import { generateILikeFiltersForCompositeFields } from '~/utils/array/generateILikeFiltersForCompositeFields';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
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';
|
import { Filter } from '../../object-filter-dropdown/types/Filter';
|
||||||
|
|
||||||
export type ObjectDropdownFilter = Omit<Filter, 'definition'> & {
|
export type ObjectDropdownFilter = Omit<Filter, 'definition'> & {
|
||||||
@ -187,6 +192,11 @@ const applyEmptyFilters = (
|
|||||||
[correspondingField.name]: { is: 'NULL' } as FloatFilter,
|
[correspondingField.name]: { is: 'NULL' } as FloatFilter,
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
case 'RATING':
|
||||||
|
emptyRecordFilter = {
|
||||||
|
[correspondingField.name]: { is: 'NULL' } as StringFilter,
|
||||||
|
};
|
||||||
|
break;
|
||||||
case 'DATE_TIME':
|
case 'DATE_TIME':
|
||||||
emptyRecordFilter = {
|
emptyRecordFilter = {
|
||||||
[correspondingField.name]: { is: 'NULL' } as DateFilter,
|
[correspondingField.name]: { is: 'NULL' } as DateFilter,
|
||||||
@ -313,6 +323,48 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
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':
|
case 'NUMBER':
|
||||||
switch (rawUIFilter.operand) {
|
switch (rawUIFilter.operand) {
|
||||||
case ViewFilterOperand.GreaterThan:
|
case ViewFilterOperand.GreaterThan:
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { useRecoilValue } from 'recoil';
|
|||||||
import { MultipleFiltersDropdownContent } from '@/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent';
|
import { MultipleFiltersDropdownContent } from '@/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent';
|
||||||
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
||||||
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
|
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 { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||||
@ -66,8 +67,11 @@ export const EditableFilterDropdownButton = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDropdownClickOutside = useCallback(() => {
|
const handleDropdownClickOutside = useCallback(() => {
|
||||||
const { id: fieldId, value } = viewFilter;
|
const { id: fieldId, value, operand } = viewFilter;
|
||||||
if (!value) {
|
if (
|
||||||
|
!value &&
|
||||||
|
![FilterOperand.IsEmpty, FilterOperand.IsNotEmpty].includes(operand)
|
||||||
|
) {
|
||||||
removeCombinedViewFilter(fieldId);
|
removeCombinedViewFilter(fieldId);
|
||||||
}
|
}
|
||||||
}, [viewFilter, removeCombinedViewFilter]);
|
}, [viewFilter, removeCombinedViewFilter]);
|
||||||
|
|||||||
@ -42,17 +42,20 @@ export const useCombinedViewFilters = (viewBarComponentId?: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const matchingFilterInCurrentView = currentView.viewFilters.find(
|
const matchingFilterInCurrentView = currentView.viewFilters.find(
|
||||||
(viewFilter) => viewFilter.id === upsertedFilter.id,
|
(viewFilter) =>
|
||||||
|
viewFilter.fieldMetadataId === upsertedFilter.fieldMetadataId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const matchingFilterInUnsavedFilters = unsavedToUpsertViewFilters.find(
|
const matchingFilterInUnsavedFilters = unsavedToUpsertViewFilters.find(
|
||||||
(viewFilter) => viewFilter.id === upsertedFilter.id,
|
(viewFilter) =>
|
||||||
|
viewFilter.fieldMetadataId === upsertedFilter.fieldMetadataId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isDefined(matchingFilterInUnsavedFilters)) {
|
if (isDefined(matchingFilterInUnsavedFilters)) {
|
||||||
const updatedFilters = unsavedToUpsertViewFilters.map((viewFilter) =>
|
const updatedFilters = unsavedToUpsertViewFilters.map((viewFilter) =>
|
||||||
viewFilter.id === matchingFilterInUnsavedFilters.id
|
viewFilter.fieldMetadataId ===
|
||||||
? { ...viewFilter, ...upsertedFilter }
|
matchingFilterInUnsavedFilters.fieldMetadataId
|
||||||
|
? { ...viewFilter, ...upsertedFilter, id: viewFilter.id }
|
||||||
: viewFilter,
|
: viewFilter,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -63,7 +66,11 @@ export const useCombinedViewFilters = (viewBarComponentId?: string) => {
|
|||||||
if (isDefined(matchingFilterInCurrentView)) {
|
if (isDefined(matchingFilterInCurrentView)) {
|
||||||
set(unsavedToUpsertViewFiltersState, [
|
set(unsavedToUpsertViewFiltersState, [
|
||||||
...unsavedToUpsertViewFilters,
|
...unsavedToUpsertViewFilters,
|
||||||
{ ...matchingFilterInCurrentView, ...upsertedFilter },
|
{
|
||||||
|
...matchingFilterInCurrentView,
|
||||||
|
...upsertedFilter,
|
||||||
|
id: matchingFilterInCurrentView.id,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
set(
|
set(
|
||||||
unsavedToDeleteViewFilterIdsState,
|
unsavedToDeleteViewFilterIdsState,
|
||||||
|
|||||||
@ -58,12 +58,14 @@ export const useSaveCurrentViewFiltersAndSorts = (
|
|||||||
const viewSortsToCreate = unsavedToUpsertViewSorts.filter(
|
const viewSortsToCreate = unsavedToUpsertViewSorts.filter(
|
||||||
(viewSort) =>
|
(viewSort) =>
|
||||||
!view.viewSorts.some(
|
!view.viewSorts.some(
|
||||||
(vf) => vf.fieldMetadataId === viewSort.fieldMetadataId,
|
(vs) => vs.fieldMetadataId === viewSort.fieldMetadataId,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const viewSortsToUpdate = unsavedToUpsertViewSorts.filter((viewSort) =>
|
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);
|
await createViewSortRecords(viewSortsToCreate, view);
|
||||||
@ -101,12 +103,16 @@ export const useSaveCurrentViewFiltersAndSorts = (
|
|||||||
|
|
||||||
const viewFiltersToCreate = unsavedToUpsertViewFilters.filter(
|
const viewFiltersToCreate = unsavedToUpsertViewFilters.filter(
|
||||||
(viewFilter) =>
|
(viewFilter) =>
|
||||||
!view.viewFilters.some((vf) => vf.id === viewFilter.id),
|
!view.viewFilters.some(
|
||||||
|
(vf) => vf.fieldMetadataId === viewFilter.fieldMetadataId,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const viewFiltersToUpdate = unsavedToUpsertViewFilters.filter(
|
const viewFiltersToUpdate = unsavedToUpsertViewFilters.filter(
|
||||||
(viewFilter) =>
|
(viewFilter) =>
|
||||||
view.viewFilters.some((vf) => vf.id === viewFilter.id),
|
view.viewFilters.some(
|
||||||
|
(vf) => vf.fieldMetadataId === viewFilter.fieldMetadataId,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await createViewFilterRecords(viewFiltersToCreate, view);
|
await createViewFilterRecords(viewFiltersToCreate, view);
|
||||||
|
|||||||
@ -8,19 +8,24 @@ export const combinedViewFilters = (
|
|||||||
const toCreateViewFilters = toUpsertViewFilters.filter(
|
const toCreateViewFilters = toUpsertViewFilters.filter(
|
||||||
(toUpsertViewFilter) =>
|
(toUpsertViewFilter) =>
|
||||||
!viewFilters.some(
|
!viewFilters.some(
|
||||||
(viewFilter) => viewFilter.id === toUpsertViewFilter.id,
|
(viewFilter) =>
|
||||||
|
viewFilter.fieldMetadataId === toUpsertViewFilter.fieldMetadataId,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const toUpdateViewFilters = toUpsertViewFilters.filter((toUpsertViewFilter) =>
|
const toUpdateViewFilters = toUpsertViewFilters.filter((toUpsertViewFilter) =>
|
||||||
viewFilters.some((viewFilter) => viewFilter.id === toUpsertViewFilter.id),
|
viewFilters.some(
|
||||||
|
(viewFilter) =>
|
||||||
|
viewFilter.fieldMetadataId === toUpsertViewFilter.fieldMetadataId,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const combinedViewFilters = viewFilters
|
const combinedViewFilters = viewFilters
|
||||||
.filter((viewFilter) => !toDeleteViewFilterIds.includes(viewFilter.id))
|
.filter((viewFilter) => !toDeleteViewFilterIds.includes(viewFilter.id))
|
||||||
.map((viewFilter) => {
|
.map((viewFilter) => {
|
||||||
const toUpdateViewFilter = toUpdateViewFilters.find(
|
const toUpdateViewFilter = toUpdateViewFilters.find(
|
||||||
(toUpdateViewFilter) => toUpdateViewFilter.id === viewFilter.id,
|
(toUpdateViewFilter) =>
|
||||||
|
toUpdateViewFilter.fieldMetadataId === viewFilter.fieldMetadataId,
|
||||||
);
|
);
|
||||||
|
|
||||||
return toUpdateViewFilter ?? viewFilter;
|
return toUpdateViewFilter ?? viewFilter;
|
||||||
|
|||||||
Reference in New Issue
Block a user