Ability to filter by composite's subfields (#6832)

# This PR

- Fix #6425 

See https://github.com/twentyhq/twenty/issues/7188 because there's some
more work to do.

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Pacifique LINJANJA
2024-10-08 11:25:42 +02:00
committed by GitHub
parent af4f3cebb0
commit 4156d7821c
57 changed files with 1424 additions and 972 deletions

View File

@ -6,6 +6,7 @@ import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { ObjectFilterDropdownRecordSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect';
import { ObjectFilterDropdownSourceSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect';
import { ObjectFilterDropdownTextSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextSearchInput';
import { isActorSourceCompositeFilter } from '@/object-record/object-filter-dropdown/utils/isActorSourceCompositeFilter';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
@ -98,9 +99,10 @@ export const MultipleFiltersDropdownContent = ({
'ACTOR',
'ARRAY',
'PHONES',
].includes(filterDefinitionUsedInDropdown.type) && (
<ObjectFilterDropdownTextSearchInput />
)}
].includes(filterDefinitionUsedInDropdown.type) &&
!isActorSourceCompositeFilter(
filterDefinitionUsedInDropdown,
) && <ObjectFilterDropdownTextSearchInput />}
{['NUMBER', 'CURRENCY'].includes(
filterDefinitionUsedInDropdown.type,
) && <ObjectFilterDropdownNumberInput />}
@ -116,7 +118,7 @@ export const MultipleFiltersDropdownContent = ({
<ObjectFilterDropdownRecordSelect />
</>
)}
{filterDefinitionUsedInDropdown.type === 'SOURCE' && (
{isActorSourceCompositeFilter(filterDefinitionUsedInDropdown) && (
<>
<DropdownMenuSeparator />
<ObjectFilterDropdownSourceSelect />

View File

@ -1,16 +1,27 @@
import styled from '@emotion/styled';
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { ObjectFilterSelectMenu } from '@/object-record/object-filter-dropdown/components/ObjectFilterSelectMenu';
import { ObjectFilterSelectSubMenu } from '@/object-record/object-filter-dropdown/components/ObjectFilterSelectSubMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { ObjectFilterDropdownFilterSelectCompositeFieldSubMenu } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu';
import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId';
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { useSelectFilter } from '@/object-record/object-filter-dropdown/hooks/useSelectFilter';
import { currentSubMenuState } from '@/object-record/object-filter-dropdown/states/subMenuStates';
import { CompositeFilterableFieldType } from '@/object-record/object-filter-dropdown/types/CompositeFilterableFieldType';
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope';
import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/isCompositeField';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
import { useRecoilState } from 'recoil';
import { isDefined } from 'twenty-ui';
import { useRecoilValue } from 'recoil';
import { isDefined, useIcons } from 'twenty-ui';
import { getOperandsForFilterDefinition } from '../utils/getOperandsForFilterType';
export const StyledInput = styled.input`
background: transparent;
@ -39,19 +50,33 @@ export const StyledInput = styled.input`
`;
export const ObjectFilterDropdownFilterSelect = () => {
const [searchText, setSearchText] = useState('');
const [subMenuFieldType, setSubMenuFieldType] =
useState<CompositeFilterableFieldType | null>(null);
const [firstLevelFilterDefinition, setFirstLevelFilterDefinition] =
useState<FilterDefinition | null>(null);
const {
setFilterDefinitionUsedInDropdown,
setSelectedOperandInDropdown,
setObjectFilterDropdownSearchInput,
objectFilterDropdownSearchInputState,
} = useFilterDropdown();
const objectFilterDropdownSearchInput = useRecoilValue(
objectFilterDropdownSearchInputState,
);
const availableFilterDefinitions = useRecoilComponentValueV2(
availableFilterDefinitionsComponentState,
);
const [currentSubMenu, setCurrentSubMenu] =
useRecoilState(currentSubMenuState);
const sortedAvailableFilterDefinitions = [...availableFilterDefinitions]
.sort((a, b) => a.label.localeCompare(b.label))
.filter((item) =>
item.label.toLocaleLowerCase().includes(searchText.toLocaleLowerCase()),
item.label
.toLocaleLowerCase()
.includes(objectFilterDropdownSearchInput.toLocaleLowerCase()),
);
const selectableListItemIds = sortedAvailableFilterDefinitions.map(
@ -76,21 +101,96 @@ export const ObjectFilterDropdownFilterSelect = () => {
selectFilter({ filterDefinition: selectedFilterDefinition });
};
useEffect(() => {
return () => {
setCurrentSubMenu(null);
};
}, [setCurrentSubMenu]);
const setHotkeyScope = useSetHotkeyScope();
const { getIcon } = useIcons();
return !currentSubMenu ? (
<ObjectFilterSelectMenu
searchText={searchText}
setSearchText={setSearchText}
sortedAvailableFilterDefinitions={sortedAvailableFilterDefinitions}
selectableListItemIds={selectableListItemIds}
handleEnter={handleEnter}
/>
) : (
<ObjectFilterSelectSubMenu />
const handleSelectFilter = (availableFilterDefinition: FilterDefinition) => {
setFilterDefinitionUsedInDropdown(availableFilterDefinition);
if (
availableFilterDefinition.type === 'RELATION' ||
availableFilterDefinition.type === 'SELECT'
) {
setHotkeyScope(RelationPickerHotkeyScope.RelationPicker);
}
setSelectedOperandInDropdown(
getOperandsForFilterDefinition(availableFilterDefinition)[0],
);
setObjectFilterDropdownSearchInput('');
};
const handleSubMenuBack = () => {
setSubMenuFieldType(null);
setFirstLevelFilterDefinition(null);
};
const shouldShowFirstLevelMenu = !isDefined(subMenuFieldType);
return (
<>
{shouldShowFirstLevelMenu ? (
<>
<StyledInput
value={objectFilterDropdownSearchInput}
autoFocus
placeholder="Search fields"
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setObjectFilterDropdownSearchInput(event.target.value)
}
/>
<SelectableList
hotkeyScope={FiltersHotkeyScope.ObjectFilterDropdownButton}
selectableItemIdArray={selectableListItemIds}
selectableListId={OBJECT_FILTER_DROPDOWN_ID}
onEnter={handleEnter}
>
<DropdownMenuItemsContainer>
{[...availableFilterDefinitions]
.sort((a, b) => a.label.localeCompare(b.label))
.filter((item) =>
item.label
.toLocaleLowerCase()
.includes(
objectFilterDropdownSearchInput.toLocaleLowerCase(),
),
)
.map((availableFilterDefinition, index) => (
<SelectableItem
itemId={availableFilterDefinition.fieldMetadataId}
>
<MenuItem
key={`select-filter-${index}`}
testId={`select-filter-${index}`}
onClick={() => {
if (isCompositeField(availableFilterDefinition.type)) {
setSubMenuFieldType(availableFilterDefinition.type);
setFirstLevelFilterDefinition(
availableFilterDefinition,
);
} else {
handleSelectFilter(availableFilterDefinition);
}
}}
LeftIcon={getIcon(availableFilterDefinition.iconName)}
text={availableFilterDefinition.label}
hasSubMenu={isCompositeField(
availableFilterDefinition.type,
)}
/>
</SelectableItem>
))}
</DropdownMenuItemsContainer>
</SelectableList>
</>
) : (
<ObjectFilterDropdownFilterSelectCompositeFieldSubMenu
fieldType={subMenuFieldType}
firstLevelFieldDefinition={firstLevelFilterDefinition}
onBack={handleSubMenuBack}
/>
)}
</>
);
};

View File

@ -0,0 +1,98 @@
import { StyledInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect';
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { CompositeFilterableFieldType } from '@/object-record/object-filter-dropdown/types/CompositeFilterableFieldType';
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel';
import { getFilterableFieldTypeLabel } from '@/object-record/object-filter-dropdown/utils/getFilterableFieldTypeLabel';
import { getOperandsForFilterDefinition } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType';
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { useState } from 'react';
import { IconApps, IconChevronLeft, useIcons } from 'twenty-ui';
type ObjectFilterDropdownFilterSelectCompositeFieldSubMenuProps = {
fieldType: CompositeFilterableFieldType;
firstLevelFieldDefinition: FilterDefinition | null;
onBack: () => void;
};
export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = ({
fieldType,
firstLevelFieldDefinition,
onBack,
}: ObjectFilterDropdownFilterSelectCompositeFieldSubMenuProps) => {
const [searchText, setSearchText] = useState('');
const { getIcon } = useIcons();
const {
setFilterDefinitionUsedInDropdown,
setSelectedOperandInDropdown,
setObjectFilterDropdownSearchInput,
} = useFilterDropdown();
const handleSelectFilter = (definition: FilterDefinition | null) => {
if (definition !== null) {
setFilterDefinitionUsedInDropdown(definition);
setSelectedOperandInDropdown(
getOperandsForFilterDefinition(definition)[0],
);
setObjectFilterDropdownSearchInput('');
}
};
const options = SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[
fieldType
].filterableSubFields
.sort((a, b) => a.localeCompare(b))
.filter((item) =>
item.toLocaleLowerCase().includes(searchText.toLocaleLowerCase()),
);
return (
<>
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={onBack}>
{getFilterableFieldTypeLabel(fieldType)}
</DropdownMenuHeader>
<StyledInput
value={searchText}
autoFocus
placeholder="Search fields"
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setSearchText(event.target.value)
}
/>
<DropdownMenuItemsContainer>
<MenuItem
key={`select-filter-${-1}`}
testId={`select-filter-${-1}`}
onClick={() => {
handleSelectFilter(firstLevelFieldDefinition);
}}
LeftIcon={IconApps}
text={`Any ${getFilterableFieldTypeLabel(fieldType)} field`}
/>
{options.map((subFieldName, index) => (
<MenuItem
key={`select-filter-${index}`}
testId={`select-filter-${index}`}
onClick={() =>
firstLevelFieldDefinition &&
handleSelectFilter({
...firstLevelFieldDefinition,
label: getCompositeSubFieldLabel(fieldType, subFieldName),
compositeFieldName: subFieldName,
})
}
text={getCompositeSubFieldLabel(fieldType, subFieldName)}
LeftIcon={getIcon(firstLevelFieldDefinition?.iconName)}
/>
))}
</DropdownMenuItemsContainer>
</>
);
};

View File

@ -1,14 +1,10 @@
import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId';
import { useSelectFilter } from '@/object-record/object-filter-dropdown/hooks/useSelectFilter';
import {
currentParentFilterDefinitionState,
currentSubMenuState,
} from '@/object-record/object-filter-dropdown/states/subMenuStates';
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
import { hasSubMenuFilter } from '@/object-record/object-filter-dropdown/utils/hasSubMenuFilter';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { MenuItemSelect } from '@/ui/navigation/menu-item/components/MenuItemSelect';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useRecoilValue } from 'recoil';
import { useIcons } from 'twenty-ui';
export type ObjectFilterDropdownFilterSelectMenuItemProps = {
@ -28,24 +24,12 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({
isSelectedItemIdSelector(filterDefinition.fieldMetadataId),
);
const hasSubMenu = hasSubMenuFilter(filterDefinition.type);
const { getIcon } = useIcons();
const setCurrentSubMenu = useSetRecoilState(currentSubMenuState);
const setCurrentParentFilterDefinition = useSetRecoilState(
currentParentFilterDefinitionState,
);
const handleClick = () => {
resetSelectedItem();
if (hasSubMenu) {
setCurrentSubMenu(filterDefinition.type);
setCurrentParentFilterDefinition(filterDefinition);
} else {
selectFilter({ filterDefinition });
}
selectFilter({ filterDefinition });
};
return (
@ -55,7 +39,6 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({
onClick={handleClick}
LeftIcon={getIcon(filterDefinition.iconName)}
text={filterDefinition.label}
hasSubMenu={hasSubMenu}
/>
);
};

View File

@ -9,7 +9,7 @@ import { isDefined } from '~/utils/isDefined';
import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue';
import { getOperandLabel } from '../utils/getOperandLabel';
import { getOperandsForFilterType } from '../utils/getOperandsForFilterType';
import { getOperandsForFilterDefinition } from '../utils/getOperandsForFilterType';
export const ObjectFilterDropdownOperandSelect = () => {
const {
@ -31,9 +31,9 @@ export const ObjectFilterDropdownOperandSelect = () => {
const selectedFilter = useRecoilValue(selectedFilterState);
const operandsForFilterType = getOperandsForFilterType(
filterDefinitionUsedInDropdown?.type,
);
const operandsForFilterType = isDefined(filterDefinitionUsedInDropdown)
? getOperandsForFilterDefinition(filterDefinitionUsedInDropdown)
: [];
const handleOperandChange = (newOperand: ViewFilterOperand) => {
const isValuelessOperand = [

View File

@ -3,7 +3,7 @@ import { useRecoilValue } from 'recoil';
import { v4 } from 'uuid';
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { getSourceEnumOptions } from '@/object-record/object-filter-dropdown/utils/getSourceEnumOptions';
import { getActorSourceMultiSelectOptions } from '@/object-record/object-filter-dropdown/utils/getActorSourceMultiSelectOptions';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
import { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown';
import { SelectableItem } from '@/object-record/select/types/SelectableItem';
@ -55,7 +55,7 @@ export const ObjectFilterDropdownSourceSelect = ({
const selectedFilter = useRecoilValue(selectedFilterState);
const sourceTypes = getSourceEnumOptions(
const sourceTypes = getActorSourceMultiSelectOptions(
objectFilterDropdownSelectedRecordIds,
);

View File

@ -1,87 +0,0 @@
import styled from '@emotion/styled';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { ObjectFilterDropdownFilterSelectMenuItem } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem';
import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId';
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope';
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
export const StyledInput = styled.input`
background: transparent;
border: none;
border-top: none;
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
border-radius: 0;
border-top-left-radius: ${({ theme }) => theme.border.radius.md};
border-top-right-radius: ${({ theme }) => theme.border.radius.md};
color: ${({ theme }) => theme.font.color.primary};
margin: 0;
outline: none;
padding: ${({ theme }) => theme.spacing(2)};
height: 19px;
font-family: inherit;
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: inherit;
max-width: 100%;
overflow: hidden;
text-decoration: none;
&::placeholder {
color: ${({ theme }) => theme.font.color.light};
}
`;
type ObjectFilterSelectMenuProps = {
searchText: string;
setSearchText: (searchText: string) => void;
sortedAvailableFilterDefinitions: FilterDefinition[];
selectableListItemIds: string[];
handleEnter: (itemId: string) => void;
};
export const ObjectFilterSelectMenu = ({
searchText,
setSearchText,
sortedAvailableFilterDefinitions,
selectableListItemIds,
handleEnter,
}: ObjectFilterSelectMenuProps) => {
return (
<>
<StyledInput
value={searchText}
autoFocus
placeholder="Search fields"
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setSearchText(event.target.value)
}
/>
<SelectableList
hotkeyScope={FiltersHotkeyScope.ObjectFilterDropdownButton}
selectableItemIdArray={selectableListItemIds}
selectableListId={OBJECT_FILTER_DROPDOWN_ID}
onEnter={handleEnter}
>
<DropdownMenuItemsContainer>
{sortedAvailableFilterDefinitions.map(
(availableFilterDefinition: FilterDefinition, index: number) => (
<SelectableItem
key={`selectable-item-${availableFilterDefinition.fieldMetadataId}`}
itemId={availableFilterDefinition.fieldMetadataId}
>
<ObjectFilterDropdownFilterSelectMenuItem
key={`select-filter-${index}`}
filterDefinition={availableFilterDefinition}
/>
</SelectableItem>
),
)}
</DropdownMenuItemsContainer>
</SelectableList>
</>
);
};

View File

@ -1,102 +0,0 @@
import { StyledInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect';
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import {
currentParentFilterDefinitionState,
currentSubMenuState,
} from '@/object-record/object-filter-dropdown/states/subMenuStates';
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType';
import { getHeaderTitle } from '@/object-record/object-filter-dropdown/utils/getHeaderTitle';
import { getOperandsForFilterType } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType';
import { getSubMenuOptions } from '@/object-record/object-filter-dropdown/utils/getSubMenuOptions';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { IconChevronLeft, useIcons } from 'twenty-ui';
export const ObjectFilterSelectSubMenu = () => {
const [searchText, setSearchText] = useState('');
const { getIcon } = useIcons();
const [currentSubMenu, setCurrentSubMenu] =
useRecoilState(currentSubMenuState);
const currentParentFilterDefinition = useRecoilValue(
currentParentFilterDefinitionState,
);
const {
setFilterDefinitionUsedInDropdown,
setSelectedOperandInDropdown,
setObjectFilterDropdownSearchInput,
} = useFilterDropdown();
const setHotkeyScope = useSetHotkeyScope();
const handleSelectFilter = (definition: FilterDefinition | null) => {
if (definition !== null) {
setFilterDefinitionUsedInDropdown(definition);
if (definition.type === 'SOURCE') {
setHotkeyScope(RelationPickerHotkeyScope.RelationPicker);
}
setSelectedOperandInDropdown(
getOperandsForFilterType(definition.type)?.[0],
);
setObjectFilterDropdownSearchInput('');
}
};
return (
<>
<DropdownMenuHeader
StartIcon={IconChevronLeft}
onClick={() => {
setCurrentSubMenu(null);
}}
>
{getHeaderTitle(currentSubMenu)}
</DropdownMenuHeader>
<StyledInput
value={searchText}
autoFocus
placeholder="Search fields"
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setSearchText(event.target.value)
}
/>
<DropdownMenuItemsContainer>
{getSubMenuOptions(currentSubMenu)
.sort((a, b) => a.name.localeCompare(b.name))
.filter((item) =>
item.name
.toLocaleLowerCase()
.includes(searchText.toLocaleLowerCase()),
)
.map((menuOption, index) => (
<MenuItem
key={`select-filter-${index}`}
testId={`select-filter-${index}`}
onClick={() => {
currentParentFilterDefinition &&
handleSelectFilter({
...currentParentFilterDefinition,
label: menuOption.name,
type: menuOption.type as FilterType,
});
}}
text={menuOption.name}
LeftIcon={getIcon(
menuOption.icon || currentParentFilterDefinition?.iconName,
)}
/>
))}
</DropdownMenuItemsContainer>
</>
);
};

View File

@ -13,7 +13,7 @@ import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
import { getOperandsForFilterType } from '../utils/getOperandsForFilterType';
import { getOperandsForFilterDefinition } from '../utils/getOperandsForFilterType';
import { GenericEntityFilterChip } from './GenericEntityFilterChip';
import { ObjectFilterDropdownRecordSelect } from './ObjectFilterDropdownRecordSelect';
import { ObjectFilterDropdownSearchInput } from './ObjectFilterDropdownSearchInput';
@ -36,14 +36,16 @@ export const SingleEntityObjectFilterDropdownButton = ({
);
const selectedFilter = useRecoilValue(selectedFilterState);
const availableFilter = availableFilterDefinitions[0];
const availableFilterDefinition = availableFilterDefinitions[0];
React.useEffect(() => {
setFilterDefinitionUsedInDropdown(availableFilter);
const defaultOperand = getOperandsForFilterType(availableFilter?.type)[0];
setFilterDefinitionUsedInDropdown(availableFilterDefinition);
const defaultOperand = getOperandsForFilterDefinition(
availableFilterDefinition,
)[0];
setSelectedOperandInDropdown(defaultOperand);
}, [
availableFilter,
availableFilterDefinition,
setFilterDefinitionUsedInDropdown,
setSelectedOperandInDropdown,
]);
@ -62,7 +64,7 @@ export const SingleEntityObjectFilterDropdownButton = ({
filter={selectedFilter}
Icon={
selectedFilter.operand === ViewFilterOperand.IsNotNull
? availableFilter.SelectAllIcon
? availableFilterDefinition.SelectAllIcon
: undefined
}
/>

View File

@ -1,7 +1,7 @@
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue';
import { getOperandsForFilterType } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType';
import { getOperandsForFilterDefinition } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { v4 } from 'uuid';
@ -31,12 +31,12 @@ export const useSelectFilter = () => {
}
setSelectedOperandInDropdown(
getOperandsForFilterType(filterDefinition.type)?.[0],
getOperandsForFilterDefinition(filterDefinition)[0],
);
const { value, displayValue } = getInitialFilterValue(
filterDefinition.type,
getOperandsForFilterType(filterDefinition.type)?.[0],
getOperandsForFilterDefinition(filterDefinition)[0],
);
if (value !== '') {
@ -44,7 +44,7 @@ export const useSelectFilter = () => {
id: v4(),
fieldMetadataId: filterDefinition.fieldMetadataId,
displayValue,
operand: getOperandsForFilterType(filterDefinition.type)?.[0],
operand: getOperandsForFilterDefinition(filterDefinition)[0],
value,
definition: filterDefinition,
});

View File

@ -1,15 +0,0 @@
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType';
import { atom } from 'recoil';
export const currentSubMenuState = atom<FilterType | null>({
key: 'currentSubMenuState',
default: null,
});
export const currentParentFilterDefinitionState = atom<FilterDefinition | null>(
{
key: 'currentParentFilterDefinitionState',
default: null,
},
);

View File

@ -0,0 +1,5 @@
import { FilterableFieldType } from '@/object-record/object-filter-dropdown/types/FilterableFieldType';
import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType';
export type CompositeFilterableFieldType = FilterableFieldType &
CompositeFieldType;

View File

@ -1,15 +1,15 @@
import { IconComponent } from 'twenty-ui';
import { FilterType } from './FilterType';
import { FilterableFieldType } from './FilterableFieldType';
export type FilterDefinition = {
fieldMetadataId: string;
label: string;
iconName: string;
type: FilterType;
type: FilterableFieldType;
relationObjectMetadataNamePlural?: string;
relationObjectMetadataNameSingular?: string;
selectAllLabel?: string;
SelectAllIcon?: IconComponent;
subFieldType?: FilterType;
compositeFieldName?: string;
};

View File

@ -1,4 +1,8 @@
export type FilterType =
import { FieldType } from '@/settings/data-model/types/FieldType';
import { PickLiteral } from '~/types/PickLiteral';
export type FilterableFieldType = PickLiteral<
FieldType,
| 'TEXT'
| 'PHONE'
| 'PHONES'
@ -18,4 +22,4 @@ export type FilterType =
| 'MULTI_SELECT'
| 'ACTOR'
| 'ARRAY'
| 'SOURCE';
>;

View File

@ -1,7 +1,8 @@
import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { getOperandsForFilterType } from '../getOperandsForFilterType';
import { FilterableFieldType } from '@/object-record/object-filter-dropdown/types/FilterableFieldType';
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
import { getOperandsForFilterDefinition } from '../getOperandsForFilterType';
describe('getOperandsForFilterType', () => {
const emptyOperands = [
@ -51,7 +52,9 @@ describe('getOperandsForFilterType', () => {
testCases.forEach(([filterType, expectedOperands]) => {
it(`should return correct operands for FilterType.${filterType}`, () => {
const result = getOperandsForFilterType(filterType as FilterType);
const result = getOperandsForFilterDefinition({
type: filterType as FilterableFieldType,
} as FilterDefinition);
expect(result).toEqual(expectedOperands);
});
});

View File

@ -9,54 +9,54 @@ import {
IconUserCircle,
} from 'twenty-ui';
export const getSourceEnumOptions = (
selectedItemIds: string[],
export const getActorSourceMultiSelectOptions = (
selectedSourceNames: string[],
): SelectableItem[] => {
return [
{
id: 'MANUAL',
name: 'User',
isSelected: selectedItemIds.includes('MANUAL'),
isSelected: selectedSourceNames.includes('MANUAL'),
AvatarIcon: IconUserCircle,
isIconInverted: true,
},
{
id: 'IMPORT',
name: 'Import',
isSelected: selectedItemIds.includes('IMPORT'),
isSelected: selectedSourceNames.includes('IMPORT'),
AvatarIcon: IconCsv,
isIconInverted: true,
},
{
id: 'API',
name: 'Api',
isSelected: selectedItemIds.includes('API'),
isSelected: selectedSourceNames.includes('API'),
AvatarIcon: IconApi,
isIconInverted: true,
},
{
id: 'EMAIL',
name: 'Email',
isSelected: selectedItemIds.includes('EMAIL'),
isSelected: selectedSourceNames.includes('EMAIL'),
AvatarIcon: IconGmail,
},
{
id: 'CALENDAR',
name: 'Calendar',
isSelected: selectedItemIds.includes('CALENDAR'),
isSelected: selectedSourceNames.includes('CALENDAR'),
AvatarIcon: IconGoogleCalendar,
},
{
id: 'WORKFLOW',
name: 'Workflow',
isSelected: selectedItemIds.includes('WORKFLOW'),
isSelected: selectedSourceNames.includes('WORKFLOW'),
AvatarIcon: IconSettingsAutomation,
isIconInverted: true,
},
{
id: 'SYSTEM',
name: 'System',
isSelected: selectedItemIds.includes('SYSTEM'),
isSelected: selectedSourceNames.includes('SYSTEM'),
AvatarIcon: IconRobot,
isIconInverted: true,
},

View File

@ -0,0 +1,12 @@
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType';
export const getCompositeSubFieldLabel = (
compositeFieldType: CompositeFieldType,
subFieldName: (typeof SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS)[CompositeFieldType]['subFields'][number],
): string => {
return (
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[compositeFieldType]
.labelBySubField as any
)[subFieldName];
};

View File

@ -0,0 +1,8 @@
import { FilterableFieldType } from '@/object-record/object-filter-dropdown/types/FilterableFieldType';
import { SETTINGS_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsFieldTypeConfigs';
export const getFilterableFieldTypeLabel = (
filterableFieldType: FilterableFieldType,
) => {
return SETTINGS_FIELD_TYPE_CONFIGS[filterableFieldType].label;
};

View File

@ -1,14 +0,0 @@
import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType';
export const getHeaderTitle = (
subMenu: FilterType | null,
): string | undefined => {
switch (subMenu) {
case 'ACTOR':
return 'Actor';
case 'SOURCE':
return 'Creation Source';
default:
return undefined;
}
};

View File

@ -1,10 +1,10 @@
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType';
import { FilterableFieldType } from '@/object-record/object-filter-dropdown/types/FilterableFieldType';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { z } from 'zod';
export const getInitialFilterValue = (
newType: FilterType,
newType: FilterableFieldType,
newOperand: ViewFilterOperand,
oldValue?: string,
oldDisplayValue?: string,
@ -35,6 +35,7 @@ export const getInitialFilterValue = (
break;
}
}
return {
value: oldValue ?? '',
displayValue: oldDisplayValue ?? '',

View File

@ -1,9 +1,9 @@
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
import { isActorSourceCompositeFilter } from '@/object-record/object-filter-dropdown/utils/isActorSourceCompositeFilter';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { FilterType } from '../types/FilterType';
export const getOperandsForFilterType = (
filterType: FilterType | null | undefined,
export const getOperandsForFilterDefinition = (
filterDefinition: FilterDefinition,
): ViewFilterOperand[] => {
const emptyOperands = [
ViewFilterOperand.IsEmpty,
@ -12,7 +12,7 @@ export const getOperandsForFilterType = (
const relationOperands = [ViewFilterOperand.Is, ViewFilterOperand.IsNot];
switch (filterType) {
switch (filterDefinition.type) {
case 'TEXT':
case 'EMAIL':
case 'EMAILS':
@ -21,7 +21,6 @@ export const getOperandsForFilterType = (
case 'PHONE':
case 'LINK':
case 'LINKS':
case 'ACTOR':
case 'ARRAY':
case 'PHONES':
return [
@ -57,10 +56,23 @@ export const getOperandsForFilterType = (
];
case 'RELATION':
return [...relationOperands, ...emptyOperands];
case 'SOURCE':
return [...relationOperands];
case 'SELECT':
return [...relationOperands];
case 'ACTOR': {
if (isActorSourceCompositeFilter(filterDefinition)) {
return [
ViewFilterOperand.Is,
ViewFilterOperand.IsNot,
...emptyOperands,
];
}
return [
ViewFilterOperand.Contains,
ViewFilterOperand.DoesNotContain,
...emptyOperands,
];
}
default:
return [];
}

View File

@ -0,0 +1,10 @@
import { SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs';
import { SettingsNonCompositeFieldType } from '@/settings/data-model/types/SettingsNonCompositeFieldType';
export const getSettingsNonCompositeFieldTypeLabel = (
settingsNonCompositeFieldType: SettingsNonCompositeFieldType,
) => {
return SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS[
settingsNonCompositeFieldType
].label;
};

View File

@ -1,6 +1,6 @@
import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType';
import { FilterableFieldType } from '@/object-record/object-filter-dropdown/types/FilterableFieldType';
export const getSubMenuOptions = (subMenu: FilterType | null) => {
export const getSubMenuOptions = (subMenu: FilterableFieldType | null) => {
switch (subMenu) {
case 'ACTOR':
return [

View File

@ -1,3 +0,0 @@
import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType';
export const hasSubMenuFilter = (type: FilterType) => ['ACTOR'].includes(type);

View File

@ -0,0 +1,11 @@
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
import { FieldActorValue } from '@/object-record/record-field/types/FieldMetadata';
export const isActorSourceCompositeFilter = (
filterDefinition: FilterDefinition,
) => {
return (
filterDefinition.compositeFieldName ===
('source' satisfies keyof FieldActorValue)
);
};

View File

@ -0,0 +1,8 @@
import {
COMPOSITE_FIELD_TYPES,
CompositeFieldType,
} from '@/settings/data-model/types/CompositeFieldType';
import { FieldType } from '@/settings/data-model/types/FieldType';
export const isCompositeField = (type: FieldType): type is CompositeFieldType =>
COMPOSITE_FIELD_TYPES.includes(type as any);

View File

@ -177,7 +177,7 @@ export type FieldMetadata =
| FieldArrayMetadata;
export type FieldTextValue = string;
export type FieldUUidValue = string;
export type FieldUUidValue = string; // TODO: can we replace with a template literal type, or maybe overkill ?
export type FieldDateTimeValue = string | null;
export type FieldDateValue = string | null;
export type FieldNumberValue = number | null;
@ -225,6 +225,8 @@ export type FieldRelationValue<
export type Json = ZodHelperLiteral | { [key: string]: Json } | Json[];
export type FieldJsonValue = Record<string, Json> | Json[] | null;
export type FieldRichTextValue = Record<string, Json> | Json[] | null;
export type FieldActorValue = {
source: string;
workspaceMemberId?: string;

View File

@ -0,0 +1,339 @@
import {
ActorFilter,
AddressFilter,
CurrencyFilter,
DateFilter,
EmailsFilter,
FloatFilter,
RecordGqlOperationFilter,
RelationFilter,
StringFilter,
URLFilter,
UUIDFilter,
} from '@/object-record/graphql/types/RecordGqlOperationFilter';
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { isNonEmptyString } from '@sniptt/guards';
import { Field } from '~/generated/graphql';
import { generateILikeFiltersForCompositeFields } from '~/utils/array/generateILikeFiltersForCompositeFields';
// TODO: fix this
export const applyEmptyFilters = (
operand: ViewFilterOperand,
correspondingField: Pick<Field, 'id' | 'name'>,
objectRecordFilters: RecordGqlOperationFilter[],
definition: FilterDefinition,
) => {
let emptyRecordFilter: RecordGqlOperationFilter = {};
const compositeFieldName = definition.compositeFieldName;
const isCompositeField = isNonEmptyString(compositeFieldName);
switch (definition.type) {
case 'TEXT':
case 'EMAIL':
case 'PHONE':
emptyRecordFilter = {
or: [
{ [correspondingField.name]: { ilike: '' } as StringFilter },
{ [correspondingField.name]: { is: 'NULL' } as StringFilter },
],
};
break;
case 'PHONES': {
if (!isCompositeField) {
const phonesFilter = generateILikeFiltersForCompositeFields(
'',
correspondingField.name,
['primaryPhoneNumber', 'primaryPhoneCountryCode'],
true,
);
emptyRecordFilter = {
and: phonesFilter,
};
break;
} else {
emptyRecordFilter = {
or: [
{
[correspondingField.name]: {
[compositeFieldName]: { ilike: '' },
} as StringFilter,
},
{
[correspondingField.name]: {
[compositeFieldName]: { is: 'NULL' },
} as StringFilter,
},
],
};
break;
}
}
case 'CURRENCY':
emptyRecordFilter = {
or: [
{
[correspondingField.name]: {
amountMicros: { is: 'NULL' },
} as CurrencyFilter,
},
],
};
break;
case 'FULL_NAME': {
if (!isCompositeField) {
const fullNameFilters = generateILikeFiltersForCompositeFields(
'',
correspondingField.name,
['firstName', 'lastName'],
true,
);
emptyRecordFilter = {
and: fullNameFilters,
};
} else {
emptyRecordFilter = {
or: [
{
[correspondingField.name]: {
[compositeFieldName]: { ilike: '' },
},
},
{
[correspondingField.name]: {
[compositeFieldName]: { is: 'NULL' },
},
},
],
};
}
break;
}
case 'LINK':
emptyRecordFilter = {
or: [
{ [correspondingField.name]: { url: { ilike: '' } } as URLFilter },
{
[correspondingField.name]: { url: { is: 'NULL' } } as URLFilter,
},
],
};
break;
case 'LINKS': {
if (!isCompositeField) {
const linksFilters = generateILikeFiltersForCompositeFields(
'',
correspondingField.name,
['primaryLinkLabel', 'primaryLinkUrl'],
true,
);
emptyRecordFilter = {
and: linksFilters,
};
} else {
emptyRecordFilter = {
or: [
{
[correspondingField.name]: {
[compositeFieldName]: { ilike: '' },
} as URLFilter,
},
{
[correspondingField.name]: {
[compositeFieldName]: { is: 'NULL' },
} as URLFilter,
},
],
};
}
break;
}
case 'ADDRESS':
if (!isCompositeField) {
emptyRecordFilter = {
and: [
{
or: [
{
[correspondingField.name]: {
addressStreet1: { ilike: '' },
} as AddressFilter,
},
{
[correspondingField.name]: {
addressStreet1: { is: 'NULL' },
} as AddressFilter,
},
],
},
{
or: [
{
[correspondingField.name]: {
addressStreet2: { ilike: '' },
} as AddressFilter,
},
{
[correspondingField.name]: {
addressStreet2: { is: 'NULL' },
} as AddressFilter,
},
],
},
{
or: [
{
[correspondingField.name]: {
addressCity: { ilike: '' },
} as AddressFilter,
},
{
[correspondingField.name]: {
addressCity: { is: 'NULL' },
} as AddressFilter,
},
],
},
{
or: [
{
[correspondingField.name]: {
addressState: { ilike: '' },
} as AddressFilter,
},
{
[correspondingField.name]: {
addressState: { is: 'NULL' },
} as AddressFilter,
},
],
},
{
or: [
{
[correspondingField.name]: {
addressCountry: { ilike: '' },
} as AddressFilter,
},
{
[correspondingField.name]: {
addressCountry: { is: 'NULL' },
} as AddressFilter,
},
],
},
{
or: [
{
[correspondingField.name]: {
addressPostcode: { ilike: '' },
} as AddressFilter,
},
{
[correspondingField.name]: {
addressPostcode: { is: 'NULL' },
} as AddressFilter,
},
],
},
],
};
} else {
emptyRecordFilter = {
or: [
{
[correspondingField.name]: {
[compositeFieldName]: { ilike: '' },
} as AddressFilter,
},
{
[correspondingField.name]: {
[compositeFieldName]: { is: 'NULL' },
} as AddressFilter,
},
],
};
}
break;
case 'NUMBER':
emptyRecordFilter = {
[correspondingField.name]: { is: 'NULL' } as FloatFilter,
};
break;
case 'RATING':
emptyRecordFilter = {
[correspondingField.name]: { is: 'NULL' } as StringFilter,
};
break;
case 'DATE':
case 'DATE_TIME':
emptyRecordFilter = {
[correspondingField.name]: { is: 'NULL' } as DateFilter,
};
break;
case 'SELECT':
emptyRecordFilter = {
[correspondingField.name]: { is: 'NULL' } as UUIDFilter,
};
break;
case 'RELATION':
emptyRecordFilter = {
[correspondingField.name + 'Id']: { is: 'NULL' } as RelationFilter,
};
break;
case 'ACTOR':
emptyRecordFilter = {
or: [
{
[correspondingField.name]: {
name: { ilike: '' },
} as ActorFilter,
},
{
[correspondingField.name]: {
name: { is: 'NULL' },
} as ActorFilter,
},
],
};
break;
case 'EMAILS':
emptyRecordFilter = {
or: [
{
[correspondingField.name]: {
primaryEmail: { ilike: '' },
} as EmailsFilter,
},
{
[correspondingField.name]: {
primaryEmail: { is: 'NULL' },
} as EmailsFilter,
},
],
};
break;
default:
throw new Error(`Unsupported empty filter type ${definition.type}`);
}
switch (operand) {
case ViewFilterOperand.IsEmpty:
objectRecordFilters.push(emptyRecordFilter);
break;
case ViewFilterOperand.IsNotEmpty:
objectRecordFilters.push({
not: emptyRecordFilter,
});
break;
default:
throw new Error(
`Unknown operand ${operand} for ${definition.type} filter`,
);
}
};

View File

@ -10,9 +10,9 @@ import {
RecordGqlOperationFilter,
RelationFilter,
StringFilter,
URLFilter,
UUIDFilter,
} from '@/object-record/graphql/types/RecordGqlOperationFilter';
import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType';
import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { Field } from '~/generated/graphql';
@ -24,244 +24,15 @@ import {
convertLessThanRatingToArrayOfRatingValues,
convertRatingToRatingValue,
} from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput';
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
import { isActorSourceCompositeFilter } from '@/object-record/object-filter-dropdown/utils/isActorSourceCompositeFilter';
import { applyEmptyFilters } from '@/object-record/record-filter/utils/applyEmptyFilters';
import { resolveFilterValue } from '@/views/utils/view-filter-value/resolveFilterValue';
import { endOfDay, roundToNearestMinutes, startOfDay } from 'date-fns';
import { z } from 'zod';
import { Filter } from '../../object-filter-dropdown/types/Filter';
const applyEmptyFilters = (
operand: ViewFilterOperand,
correspondingField: Pick<Field, 'id' | 'name'>,
objectRecordFilters: RecordGqlOperationFilter[],
filterType: FilterType,
) => {
let emptyRecordFilter: RecordGqlOperationFilter = {};
switch (filterType) {
case 'TEXT':
emptyRecordFilter = {
or: [
{ [correspondingField.name]: { ilike: '' } as StringFilter },
{ [correspondingField.name]: { is: 'NULL' } as StringFilter },
],
};
break;
case 'PHONES': {
const phonesFilter = generateILikeFiltersForCompositeFields(
'',
correspondingField.name,
['primaryPhoneNumber', 'primaryPhoneCountryCode'],
true,
);
emptyRecordFilter = {
and: phonesFilter,
};
break;
}
case 'CURRENCY':
emptyRecordFilter = {
or: [
{
[correspondingField.name]: {
amountMicros: { is: 'NULL' },
} as CurrencyFilter,
},
],
};
break;
case 'FULL_NAME': {
const fullNameFilters = generateILikeFiltersForCompositeFields(
'',
correspondingField.name,
['firstName', 'lastName'],
true,
);
emptyRecordFilter = {
and: fullNameFilters,
};
break;
}
case 'LINKS': {
const linksFilters = generateILikeFiltersForCompositeFields(
'',
correspondingField.name,
['primaryLinkLabel', 'primaryLinkUrl'],
true,
);
emptyRecordFilter = {
and: linksFilters,
};
break;
}
case 'ADDRESS':
emptyRecordFilter = {
and: [
{
or: [
{
[correspondingField.name]: {
addressStreet1: { ilike: '' },
} as AddressFilter,
},
{
[correspondingField.name]: {
addressStreet1: { is: 'NULL' },
} as AddressFilter,
},
],
},
{
or: [
{
[correspondingField.name]: {
addressStreet2: { ilike: '' },
} as AddressFilter,
},
{
[correspondingField.name]: {
addressStreet2: { is: 'NULL' },
} as AddressFilter,
},
],
},
{
or: [
{
[correspondingField.name]: {
addressCity: { ilike: '' },
} as AddressFilter,
},
{
[correspondingField.name]: {
addressCity: { is: 'NULL' },
} as AddressFilter,
},
],
},
{
or: [
{
[correspondingField.name]: {
addressState: { ilike: '' },
} as AddressFilter,
},
{
[correspondingField.name]: {
addressState: { is: 'NULL' },
} as AddressFilter,
},
],
},
{
or: [
{
[correspondingField.name]: {
addressCountry: { ilike: '' },
} as AddressFilter,
},
{
[correspondingField.name]: {
addressCountry: { is: 'NULL' },
} as AddressFilter,
},
],
},
{
or: [
{
[correspondingField.name]: {
addressPostcode: { ilike: '' },
} as AddressFilter,
},
{
[correspondingField.name]: {
addressPostcode: { is: 'NULL' },
} as AddressFilter,
},
],
},
],
};
break;
case 'NUMBER':
emptyRecordFilter = {
[correspondingField.name]: { is: 'NULL' } as FloatFilter,
};
break;
case 'RATING':
emptyRecordFilter = {
[correspondingField.name]: { is: 'NULL' } as StringFilter,
};
break;
case 'DATE':
case 'DATE_TIME':
emptyRecordFilter = {
[correspondingField.name]: { is: 'NULL' } as DateFilter,
};
break;
case 'SELECT':
emptyRecordFilter = {
[correspondingField.name]: { is: 'NULL' } as UUIDFilter,
};
break;
case 'RELATION':
emptyRecordFilter = {
[correspondingField.name + 'Id']: { is: 'NULL' } as RelationFilter,
};
break;
case 'ACTOR':
emptyRecordFilter = {
or: [
{
[correspondingField.name]: {
name: { ilike: '' },
} as ActorFilter,
},
{
[correspondingField.name]: {
name: { is: 'NULL' },
} as ActorFilter,
},
],
};
break;
case 'EMAILS':
emptyRecordFilter = {
or: [
{
[correspondingField.name]: {
primaryEmail: { ilike: '' },
} as EmailsFilter,
},
{
[correspondingField.name]: {
primaryEmail: { is: 'NULL' },
} as EmailsFilter,
},
],
};
break;
default:
throw new Error(`Unsupported empty filter type ${filterType}`);
}
switch (operand) {
case ViewFilterOperand.IsEmpty:
objectRecordFilters.push(emptyRecordFilter);
break;
case ViewFilterOperand.IsNotEmpty:
objectRecordFilters.push({
not: emptyRecordFilter,
});
break;
default:
throw new Error(`Unknown operand ${operand} for ${filterType} filter`);
}
};
// TODO: break this down into smaller functions and make the whole thing immutable
// Especially applyEmptyFilters
export const turnObjectDropdownFilterIntoQueryFilter = (
rawUIFilters: Filter[],
fields: Pick<Field, 'id' | 'name'>[],
@ -273,7 +44,11 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
(field) => field.id === rawUIFilter.fieldMetadataId,
);
const isValuelessOperand = [
const compositeFieldName = rawUIFilter.definition.compositeFieldName;
const isCompositeFieldFiter = isNonEmptyString(compositeFieldName);
const isEmptyOperand = [
ViewFilterOperand.IsEmpty,
ViewFilterOperand.IsNotEmpty,
ViewFilterOperand.IsInPast,
@ -285,7 +60,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
continue;
}
if (!isValuelessOperand) {
if (!isEmptyOperand) {
if (!isDefined(rawUIFilter.value) || rawUIFilter.value === '') {
continue;
}
@ -316,7 +91,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
rawUIFilter.operand,
correspondingField,
objectRecordFilters,
rawUIFilter.definition.type,
rawUIFilter.definition,
);
break;
default:
@ -355,7 +130,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
rawUIFilter.operand,
correspondingField,
objectRecordFilters,
rawUIFilter.definition.type,
rawUIFilter.definition,
);
break;
}
@ -372,8 +147,9 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
operand: ViewFilterOperand.IsRelative,
});
if (!defaultDateRange)
if (!defaultDateRange) {
throw new Error('Failed to resolve default date range');
}
const { start, end } = dateRange ?? defaultDateRange;
@ -484,7 +260,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
rawUIFilter.operand,
correspondingField,
objectRecordFilters,
rawUIFilter.definition.type,
rawUIFilter.definition,
);
break;
default:
@ -515,7 +291,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
rawUIFilter.operand,
correspondingField,
objectRecordFilters,
rawUIFilter.definition.type,
rawUIFilter.definition,
);
break;
default:
@ -525,7 +301,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
}
break;
case 'RELATION': {
if (!isValuelessOperand) {
if (!isEmptyOperand) {
try {
JSON.parse(rawUIFilter.value);
} catch (e) {
@ -570,7 +346,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
rawUIFilter.operand,
correspondingField,
objectRecordFilters,
rawUIFilter.definition.type,
rawUIFilter.definition,
);
break;
default:
@ -603,7 +379,44 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
rawUIFilter.operand,
correspondingField,
objectRecordFilters,
rawUIFilter.definition.type,
rawUIFilter.definition,
);
break;
default:
throw new Error(
`Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`,
);
}
break;
case 'LINK':
switch (rawUIFilter.operand) {
case ViewFilterOperand.Contains:
objectRecordFilters.push({
[correspondingField.name]: {
url: {
ilike: `%${rawUIFilter.value}%`,
},
} as URLFilter,
});
break;
case ViewFilterOperand.DoesNotContain:
objectRecordFilters.push({
not: {
[correspondingField.name]: {
url: {
ilike: `%${rawUIFilter.value}%`,
},
} as URLFilter,
},
});
break;
case ViewFilterOperand.IsEmpty:
case ViewFilterOperand.IsNotEmpty:
applyEmptyFilters(
rawUIFilter.operand,
correspondingField,
objectRecordFilters,
rawUIFilter.definition,
);
break;
default:
@ -618,20 +431,43 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
correspondingField.name,
['primaryLinkLabel', 'primaryLinkUrl'],
);
switch (rawUIFilter.operand) {
case ViewFilterOperand.Contains:
objectRecordFilters.push({
or: linksFilters,
});
if (!isCompositeFieldFiter) {
objectRecordFilters.push({
or: linksFilters,
});
} else {
objectRecordFilters.push({
[correspondingField.name]: {
[compositeFieldName]: {
ilike: `%${rawUIFilter.value}%`,
},
},
});
}
break;
case ViewFilterOperand.DoesNotContain:
objectRecordFilters.push({
and: linksFilters.map((filter) => {
return {
not: filter,
};
}),
});
if (!isCompositeFieldFiter) {
objectRecordFilters.push({
and: linksFilters.map((filter) => {
return {
not: filter,
};
}),
});
} else {
objectRecordFilters.push({
not: {
[correspondingField.name]: {
[compositeFieldName]: {
ilike: `%${rawUIFilter.value}%`,
},
},
},
});
}
break;
case ViewFilterOperand.IsEmpty:
case ViewFilterOperand.IsNotEmpty:
@ -639,7 +475,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
rawUIFilter.operand,
correspondingField,
objectRecordFilters,
rawUIFilter.definition.type,
rawUIFilter.definition,
);
break;
default:
@ -657,18 +493,40 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
);
switch (rawUIFilter.operand) {
case ViewFilterOperand.Contains:
objectRecordFilters.push({
or: fullNameFilters,
});
if (!isCompositeFieldFiter) {
objectRecordFilters.push({
or: fullNameFilters,
});
} else {
objectRecordFilters.push({
[correspondingField.name]: {
[compositeFieldName]: {
ilike: `%${rawUIFilter.value}%`,
},
},
});
}
break;
case ViewFilterOperand.DoesNotContain:
objectRecordFilters.push({
and: fullNameFilters.map((filter) => {
return {
not: filter,
};
}),
});
if (!isCompositeFieldFiter) {
objectRecordFilters.push({
and: fullNameFilters.map((filter) => {
return {
not: filter,
};
}),
});
} else {
objectRecordFilters.push({
not: {
[correspondingField.name]: {
[compositeFieldName]: {
ilike: `%${rawUIFilter.value}%`,
},
},
},
});
}
break;
case ViewFilterOperand.IsEmpty:
case ViewFilterOperand.IsNotEmpty:
@ -676,7 +534,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
rawUIFilter.operand,
correspondingField,
objectRecordFilters,
rawUIFilter.definition.type,
rawUIFilter.definition,
);
break;
default:
@ -689,85 +547,107 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
case 'ADDRESS':
switch (rawUIFilter.operand) {
case ViewFilterOperand.Contains:
objectRecordFilters.push({
or: [
{
[correspondingField.name]: {
addressStreet1: {
ilike: `%${rawUIFilter.value}%`,
},
} as AddressFilter,
},
{
[correspondingField.name]: {
addressStreet2: {
ilike: `%${rawUIFilter.value}%`,
},
} as AddressFilter,
},
{
[correspondingField.name]: {
addressCity: {
ilike: `%${rawUIFilter.value}%`,
},
} as AddressFilter,
},
{
[correspondingField.name]: {
addressState: {
ilike: `%${rawUIFilter.value}%`,
},
} as AddressFilter,
},
{
[correspondingField.name]: {
addressCountry: {
ilike: `%${rawUIFilter.value}%`,
},
} as AddressFilter,
},
{
[correspondingField.name]: {
addressPostcode: {
ilike: `%${rawUIFilter.value}%`,
},
} as AddressFilter,
},
],
});
break;
case ViewFilterOperand.DoesNotContain:
objectRecordFilters.push({
and: [
{
not: {
if (!isCompositeFieldFiter) {
objectRecordFilters.push({
or: [
{
[correspondingField.name]: {
addressStreet1: {
ilike: `%${rawUIFilter.value}%`,
},
} as AddressFilter,
},
},
{
not: {
{
[correspondingField.name]: {
addressStreet2: {
ilike: `%${rawUIFilter.value}%`,
},
} as AddressFilter,
},
},
{
not: {
{
[correspondingField.name]: {
addressCity: {
ilike: `%${rawUIFilter.value}%`,
},
} as AddressFilter,
},
{
[correspondingField.name]: {
addressState: {
ilike: `%${rawUIFilter.value}%`,
},
} as AddressFilter,
},
{
[correspondingField.name]: {
addressCountry: {
ilike: `%${rawUIFilter.value}%`,
},
} as AddressFilter,
},
{
[correspondingField.name]: {
addressPostcode: {
ilike: `%${rawUIFilter.value}%`,
},
} as AddressFilter,
},
],
});
} else {
objectRecordFilters.push({
[correspondingField.name]: {
[compositeFieldName]: {
ilike: `%${rawUIFilter.value}%`,
} as AddressFilter,
},
],
});
});
}
break;
case ViewFilterOperand.DoesNotContain:
if (!isCompositeFieldFiter) {
objectRecordFilters.push({
and: [
{
not: {
[correspondingField.name]: {
addressStreet1: {
ilike: `%${rawUIFilter.value}%`,
},
} as AddressFilter,
},
},
{
not: {
[correspondingField.name]: {
addressStreet2: {
ilike: `%${rawUIFilter.value}%`,
},
} as AddressFilter,
},
},
{
not: {
[correspondingField.name]: {
addressCity: {
ilike: `%${rawUIFilter.value}%`,
},
} as AddressFilter,
},
},
],
});
} else {
objectRecordFilters.push({
not: {
[correspondingField.name]: {
[compositeFieldName]: {
ilike: `%${rawUIFilter.value}%`,
} as AddressFilter,
},
},
});
}
break;
case ViewFilterOperand.IsEmpty:
case ViewFilterOperand.IsNotEmpty:
@ -775,7 +655,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
rawUIFilter.operand,
correspondingField,
objectRecordFilters,
rawUIFilter.definition.type,
rawUIFilter.definition,
);
break;
default:
@ -785,12 +665,12 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
}
break;
case 'SELECT': {
if (isValuelessOperand) {
if (isEmptyOperand) {
applyEmptyFilters(
rawUIFilter.operand,
correspondingField,
objectRecordFilters,
rawUIFilter.definition.type,
rawUIFilter.definition,
);
break;
}
@ -836,41 +716,33 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
break;
}
case 'ACTOR':
if (rawUIFilter.definition.subFieldType !== undefined) {
if (isActorSourceCompositeFilter(rawUIFilter.definition)) {
const parsedRecordIds = JSON.parse(rawUIFilter.value) as string[];
switch (rawUIFilter.definition.subFieldType) {
case 'SOURCE':
switch (rawUIFilter.operand) {
case ViewFilterOperand.Is:
objectRecordFilters.push({
switch (rawUIFilter.operand) {
case ViewFilterOperand.Is:
objectRecordFilters.push({
[correspondingField.name]: {
source: {
in: parsedRecordIds,
} as RelationFilter,
},
});
break;
case ViewFilterOperand.IsNot:
if (parsedRecordIds.length > 0) {
objectRecordFilters.push({
not: {
[correspondingField.name]: {
source: {
in: parsedRecordIds,
} as RelationFilter,
},
});
break;
case ViewFilterOperand.IsNot:
if (parsedRecordIds.length > 0) {
objectRecordFilters.push({
not: {
[correspondingField.name]: {
[rawUIFilter.definition.subFieldType.toLowerCase()]: {
in: parsedRecordIds,
} as RelationFilter,
},
},
});
}
break;
default:
throw new Error(
`Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.subFieldType} filter`,
);
},
});
}
break;
}
} else {
switch (rawUIFilter.operand) {
@ -908,15 +780,14 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
rawUIFilter.operand,
correspondingField,
objectRecordFilters,
rawUIFilter.definition.type,
rawUIFilter.definition,
);
break;
default:
throw new Error(
`Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`,
`Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.label} filter`,
);
}
break;
}
break;
case 'EMAILS':
@ -955,7 +826,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
rawUIFilter.operand,
correspondingField,
objectRecordFilters,
rawUIFilter.definition.type,
rawUIFilter.definition,
);
break;
default:
@ -991,7 +862,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
rawUIFilter.operand,
correspondingField,
objectRecordFilters,
rawUIFilter.definition.type,
rawUIFilter.definition,
);
break;
default:

View File

@ -5,7 +5,8 @@ import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/u
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
import { getOperandsForFilterType } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType';
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
import { getOperandsForFilterDefinition } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
import { useUpsertCombinedViewFilters } from '@/views/hooks/useUpsertCombinedViewFilters';
import { isDefined } from '~/utils/isDefined';
@ -42,7 +43,15 @@ export const useHandleToggleColumnFilter = ({
correspondingColumnDefinition?.type,
);
const availableOperandsForFilter = getOperandsForFilterType(filterType);
const filterDefinition = {
label: correspondingColumnDefinition.label,
iconName: correspondingColumnDefinition.iconName,
fieldMetadataId,
type: filterType,
} satisfies FilterDefinition;
const availableOperandsForFilter =
getOperandsForFilterDefinition(filterDefinition);
const defaultOperand = availableOperandsForFilter[0];
@ -51,12 +60,7 @@ export const useHandleToggleColumnFilter = ({
fieldMetadataId,
operand: defaultOperand,
displayValue: '',
definition: {
label: correspondingColumnDefinition.label,
iconName: correspondingColumnDefinition.iconName,
fieldMetadataId,
type: filterType,
},
definition: filterDefinition,
value: '',
};