Add boolean filtering (#7190) (#8700)

<img width="956" alt="filter-icp-true"
src="https://github.com/user-attachments/assets/fc5fe18d-c7b6-463d-9ce7-8e5facb7352f">

Link to issue: https://github.com/twentyhq/twenty/issues/7190
This commit is contained in:
ad-elias 2024-11-24 09:43:44 +01:00 committed by GitHub
parent fd8e0d04a2
commit bad7ad464b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 145 additions and 2 deletions

View File

@ -61,7 +61,7 @@ describe('useColumnDefinitionsFromFieldMetadata', () => {
result.current;
expect(columnDefinitions.length).toBe(21);
expect(filterDefinitions.length).toBe(15);
expect(filterDefinitions.length).toBe(17);
expect(sortDefinitions.length).toBe(14);
});
});

View File

@ -25,6 +25,7 @@ export const formatFieldMetadataItemsAsFilterDefinitions = ({
if (
![
FieldMetadataType.Boolean,
FieldMetadataType.DateTime,
FieldMetadataType.Date,
FieldMetadataType.Text,
@ -100,6 +101,8 @@ export const getFilterTypeFromFieldType = (fieldType: FieldMetadataType) => {
return 'ARRAY';
case FieldMetadataType.RawJson:
return 'RAW_JSON';
case FieldMetadataType.Boolean:
return 'BOOLEAN';
default:
return 'TEXT';
}

View File

@ -0,0 +1,113 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { v4 } from 'uuid';
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
import { BooleanDisplay } from '@/ui/field/display/components/BooleanDisplay';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { IconCheck } from 'twenty-ui';
import { isDefined } from '~/utils/isDefined';
const StyledBooleanSelectContainer = styled.div<{ selected?: boolean }>`
align-items: center;
cursor: pointer;
display: flex;
padding: ${({ theme }) =>
`${theme.spacing(2)} ${theme.spacing(2)} ${theme.spacing(2)} ${theme.spacing(1)}`};
border-radius: ${({ theme }) => theme.border.radius.sm};
&:hover {
background: ${({ theme }) => theme.background.transparent.light};
}
`;
const StyledIconCheckContainer = styled.div`
flex: 1;
display: flex;
justify-content: flex-end;
`;
export const ObjectFilterDropdownBooleanSelect = () => {
const theme = useTheme();
const options = [true, false];
const {
filterDefinitionUsedInDropdownState,
selectedOperandInDropdownState,
selectedFilterState,
selectFilter,
} = useFilterDropdown();
const { closeDropdown } = useDropdown();
const filterDefinitionUsedInDropdown = useRecoilValue(
filterDefinitionUsedInDropdownState,
);
const selectedOperandInDropdown = useRecoilValue(
selectedOperandInDropdownState,
);
const selectedFilter = useRecoilValue(selectedFilterState);
const [selectedValue, setSelectedValue] = useState<boolean | undefined>(
selectedFilter?.value === 'true',
);
useEffect(() => {
setSelectedValue(selectedFilter?.value === 'true');
}, [selectedFilter?.value]);
const handleOptionSelect = (value: boolean) => {
if (
!isDefined(filterDefinitionUsedInDropdown) ||
!isDefined(selectedOperandInDropdown)
) {
return;
}
selectFilter({
id: selectedFilter?.id ?? v4(),
definition: filterDefinitionUsedInDropdown,
operand: selectedOperandInDropdown,
displayValue: value ? 'True' : 'False',
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
value: value.toString(),
viewFilterGroupId: selectedFilter?.viewFilterGroupId,
});
setSelectedValue(value);
closeDropdown();
};
return (
<SelectableList
selectableListId="boolean-select"
selectableItemIdArray={options.map((option) => option.toString())}
hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
onEnter={(itemId) => {
handleOptionSelect(itemId === 'true');
}}
>
<DropdownMenuItemsContainer hasMaxHeight>
{options.map((option) => (
<StyledBooleanSelectContainer
key={String(option)}
onClick={() => handleOptionSelect(option)}
selected={selectedValue === option}
>
<BooleanDisplay value={option} />
{selectedFilter?.value === option.toString() && (
<StyledIconCheckContainer>
<IconCheck color={theme.grayScale.gray50} size={16} />
</StyledIconCheckContainer>
)}
</StyledBooleanSelectContainer>
))}
</DropdownMenuItemsContainer>
</SelectableList>
);
};

View File

@ -14,6 +14,7 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { isDefined } from 'twenty-ui';
import { ObjectFilterDropdownBooleanSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownBooleanSelect';
import { DATE_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/DateFilterTypes';
import { NUMBER_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/NumberFilterTypes';
import { TEXT_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/TextFilterTypes';
@ -96,6 +97,9 @@ export const ObjectFilterDropdownFilterInput = ({
<ObjectFilterDropdownOptionSelect />
</>
)}
{filterDefinitionUsedInDropdown.type === 'BOOLEAN' && (
<ObjectFilterDropdownBooleanSelect />
)}
</>
)}
</>

View File

@ -20,4 +20,5 @@ export type FilterableFieldType = PickLiteral<
| 'ACTOR'
| 'ARRAY'
| 'RAW_JSON'
| 'BOOLEAN'
>;

View File

@ -87,6 +87,8 @@ export const getOperandsForFilterDefinition = (
ViewFilterOperand.DoesNotContain,
...emptyOperands,
];
case 'BOOLEAN':
return [ViewFilterOperand.Is];
default:
return [];
}

View File

@ -4,6 +4,7 @@ import {
ActorFilter,
AddressFilter,
ArrayFilter,
BooleanFilter,
CurrencyFilter,
DateFilter,
EmailsFilter,
@ -855,6 +856,13 @@ const computeFilterRecordGqlOperationFilter = (
);
}
}
case 'BOOLEAN': {
return {
[correspondingField.name]: {
eq: filter.value === 'true',
} as BooleanFilter,
};
}
default:
throw new Error('Unknown filter type');
}

View File

@ -0,0 +1,7 @@
import { ViewFilter } from '@/views/types/ViewFilter';
export const resolveBooleanViewFilterValue = (
viewFilter: Pick<ViewFilter, 'value'>,
) => {
return viewFilter.value === 'true';
};

View File

@ -7,6 +7,7 @@ import {
resolveDateViewFilterValue,
ResolvedDateViewFilterValue,
} from './resolveDateViewFilterValue';
import { resolveBooleanViewFilterValue } from '@/views/view-filter-value/utils/resolveBooleanViewFilterValue';
type ResolvedFilterValue<
T extends FilterableFieldType,
@ -17,7 +18,9 @@ type ResolvedFilterValue<
? ReturnType<typeof resolveNumberViewFilterValue>
: T extends 'SELECT' | 'MULTI_SELECT'
? string[]
: string;
: T extends 'BOOLEAN'
? boolean
: string;
type PartialFilter<
T extends FilterableFieldType,
@ -42,6 +45,8 @@ export const resolveFilterValue = <
case 'SELECT':
case 'MULTI_SELECT':
return resolveSelectViewFilterValue(filter) as ResolvedFilterValue<T, O>;
case 'BOOLEAN':
return resolveBooleanViewFilterValue(filter) as ResolvedFilterValue<T, O>;
default:
return filter.value as ResolvedFilterValue<T, O>;
}