Skip to content

Commit

Permalink
Ability to filter by composite's subfields (#6832)
Browse files Browse the repository at this point in the history
# This PR

- Fix #6425 

See #7188 because there's some
more work to do.

---------

Co-authored-by: Lucas Bordeau <[email protected]>
  • Loading branch information
pacyL2K19 and lucasbordeau authored Oct 8, 2024
1 parent af4f3ce commit 4156d78
Show file tree
Hide file tree
Showing 57 changed files with 1,425 additions and 973 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 />}
Expand All @@ -116,7 +118,7 @@ export const MultipleFiltersDropdownContent = ({
<ObjectFilterDropdownRecordSelect />
</>
)}
{filterDefinitionUsedInDropdown.type === 'SOURCE' && (
{isActorSourceCompositeFilter(filterDefinitionUsedInDropdown) && (
<>
<DropdownMenuSeparator />
<ObjectFilterDropdownSourceSelect />
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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(
Expand All @@ -76,21 +101,96 @@ export const ObjectFilterDropdownFilterSelect = () => {
selectFilter({ filterDefinition: selectedFilterDefinition });
};

useEffect(() => {
return () => {
setCurrentSubMenu(null);
};
}, [setCurrentSubMenu]);

return !currentSubMenu ? (
<ObjectFilterSelectMenu
searchText={searchText}
setSearchText={setSearchText}
sortedAvailableFilterDefinitions={sortedAvailableFilterDefinitions}
selectableListItemIds={selectableListItemIds}
handleEnter={handleEnter}
/>
) : (
<ObjectFilterSelectSubMenu />
const setHotkeyScope = useSetHotkeyScope();
const { getIcon } = useIcons();

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}
/>
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -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>
</>
);
};
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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 (
Expand All @@ -55,7 +39,6 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({
onClick={handleClick}
LeftIcon={getIcon(filterDefinition.iconName)}
text={filterDefinition.label}
hasSubMenu={hasSubMenu}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 = [
Expand Down
Loading

0 comments on commit 4156d78

Please sign in to comment.