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:
committed by
GitHub
parent
af4f3cebb0
commit
4156d7821c
@ -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 />
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 = [
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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
|
||||
}
|
||||
/>
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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,
|
||||
},
|
||||
);
|
||||
@ -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;
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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';
|
||||
>;
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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,
|
||||
},
|
||||
@ -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];
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
};
|
||||
@ -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 ?? '',
|
||||
|
||||
@ -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 [];
|
||||
}
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -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 [
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType';
|
||||
|
||||
export const hasSubMenuFilter = (type: FilterType) => ['ACTOR'].includes(type);
|
||||
@ -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)
|
||||
);
|
||||
};
|
||||
@ -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);
|
||||
@ -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;
|
||||
|
||||
@ -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`,
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -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:
|
||||
|
||||
@ -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: '',
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user