From 35788af351e886b87df7792f66ff61953b947c6a Mon Sep 17 00:00:00 2001 From: "gitstart-app[bot]" <57568882+gitstart-app[bot]@users.noreply.github.com> Date: Wed, 2 Oct 2024 17:56:09 +0200 Subject: [PATCH] TWNTY-6808 - Ability to Filter by Creation Source (#7078) ### Description - Ability to Filter by Creation Source ### Demo LOOM: ### Refs #6808 Fixes #6808 --------- Co-authored-by: gitstart-twenty Co-authored-by: gitstart-twenty <140154534+gitstart-twenty@users.noreply.github.com> Co-authored-by: bosiraphael --- .../graphql/types/RecordGqlOperationFilter.ts | 1 + .../MultipleFiltersDropdownContent.tsx | 13 +- .../ObjectFilterDropdownFilterSelect.tsx | 63 +++----- ...jectFilterDropdownFilterSelectMenuItem.tsx | 22 ++- .../ObjectFilterDropdownRecordSelect.tsx | 16 +- .../ObjectFilterDropdownSourceSelect.tsx | 137 ++++++++++++++++++ .../components/ObjectFilterSelectMenu.tsx | 87 +++++++++++ .../components/ObjectFilterSelectSubMenu.tsx | 102 +++++++++++++ .../states/subMenuStates.ts | 15 ++ .../types/FilterDefinition.ts | 1 + .../types/FilterType.ts | 3 +- .../utils/getHeaderTitle.ts | 14 ++ .../utils/getOperandsForFilterType.ts | 2 + .../utils/getSourceEnumOptions.ts | 56 +++++++ .../utils/getSubMenuOptions.ts | 21 +++ .../utils/hasSubMenuFilter.ts | 3 + ...turnObjectDropdownFilterIntoQueryFilter.ts | 117 +++++++++------ ...ropdown.tsx => MultipleSelectDropdown.tsx} | 100 +++++++------ .../select/hooks/useRecordsForSelect.ts | 8 +- .../select/types/SelectableItem.ts | 11 ++ .../select/types/SelectableRecord.ts | 10 -- .../components/MenuItemMultiSelectAvatar.tsx | 2 +- .../menu-item/components/MenuItemSelect.tsx | 10 +- .../src/modules/views/types/ViewFilter.ts | 2 + .../utils/getFilterDefinitionForViewFilter.ts | 17 +++ .../views/utils/mapViewFiltersToFilters.ts | 6 +- .../src/display/chip/components/Chip.tsx | 2 + 27 files changed, 686 insertions(+), 155 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect.tsx create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterSelectMenu.tsx create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterSelectSubMenu.tsx create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/subMenuStates.ts create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getHeaderTitle.ts create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getSourceEnumOptions.ts create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getSubMenuOptions.ts create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/hasSubMenuFilter.ts rename packages/twenty-front/src/modules/object-record/select/components/{MultipleRecordSelectDropdown.tsx => MultipleSelectDropdown.tsx} (55%) create mode 100644 packages/twenty-front/src/modules/object-record/select/types/SelectableItem.ts delete mode 100644 packages/twenty-front/src/modules/object-record/select/types/SelectableRecord.ts create mode 100644 packages/twenty-front/src/modules/views/utils/getFilterDefinitionForViewFilter.ts diff --git a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts index 72573ea2133e..6c1615b17afa 100644 --- a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts +++ b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts @@ -92,6 +92,7 @@ export type LinksFilter = { export type ActorFilter = { name?: StringFilter; + source?: IsFilter; }; export type EmailsFilter = { diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx index da02a28a6731..28f82a4e7c73 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx @@ -2,6 +2,11 @@ import { ObjectFilterDropdownRatingInput } from '@/object-record/object-filter-d import { ObjectFilterDropdownSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; 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 { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import styled from '@emotion/styled'; import { useRecoilValue } from 'recoil'; import { MultipleFiltersDropdownFilterOnFilterChangedEffect } from './MultipleFiltersDropdownFilterOnFilterChangedEffect'; @@ -11,8 +16,6 @@ import { ObjectFilterDropdownNumberInput } from './ObjectFilterDropdownNumberInp import { ObjectFilterDropdownOperandButton } from './ObjectFilterDropdownOperandButton'; import { ObjectFilterDropdownOperandSelect } from './ObjectFilterDropdownOperandSelect'; import { ObjectFilterDropdownOptionSelect } from './ObjectFilterDropdownOptionSelect'; -import { ObjectFilterDropdownRecordSelect } from './ObjectFilterDropdownRecordSelect'; -import { ObjectFilterDropdownTextSearchInput } from './ObjectFilterDropdownTextSearchInput'; const StyledContainer = styled.div` position: relative; @@ -113,6 +116,12 @@ export const MultipleFiltersDropdownContent = ({ )} + {filterDefinitionUsedInDropdown.type === 'SOURCE' && ( + <> + + + + )} {filterDefinitionUsedInDropdown.type === 'SELECT' && ( <> diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx index f0c2d66cdc7e..15465b67df7d 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx @@ -1,17 +1,15 @@ import styled from '@emotion/styled'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; -import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; - -import { ObjectFilterDropdownFilterSelectMenuItem } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem'; +import { ObjectFilterSelectMenu } from '@/object-record/object-filter-dropdown/components/ObjectFilterSelectMenu'; +import { ObjectFilterSelectSubMenu } from '@/object-record/object-filter-dropdown/components/ObjectFilterSelectSubMenu'; import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId'; import { useSelectFilter } from '@/object-record/object-filter-dropdown/hooks/useSelectFilter'; -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'; +import { currentSubMenuState } from '@/object-record/object-filter-dropdown/states/subMenuStates'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; 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'; export const StyledInput = styled.input` @@ -47,6 +45,9 @@ export const ObjectFilterDropdownFilterSelect = () => { availableFilterDefinitionsComponentState, ); + const [currentSubMenu, setCurrentSubMenu] = + useRecoilState(currentSubMenuState); + const sortedAvailableFilterDefinitions = [...availableFilterDefinitions] .sort((a, b) => a.label.localeCompare(b.label)) .filter((item) => @@ -75,37 +76,21 @@ export const ObjectFilterDropdownFilterSelect = () => { selectFilter({ filterDefinition: selectedFilterDefinition }); }; - return ( - <> - ) => - setSearchText(event.target.value) - } - /> - - - {sortedAvailableFilterDefinitions.map( - (availableFilterDefinition, index) => ( - - - - ), - )} - - - + useEffect(() => { + return () => { + setCurrentSubMenu(null); + }; + }, [setCurrentSubMenu]); + + return !currentSubMenu ? ( + + ) : ( + ); }; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx index eb04750c9b8c..da2127ee08bb 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx @@ -1,9 +1,14 @@ 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 } from 'recoil'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; import { useIcons } from 'twenty-ui'; export type ObjectFilterDropdownFilterSelectMenuItemProps = { @@ -23,12 +28,24 @@ 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(); - selectFilter({ filterDefinition }); + if (hasSubMenu) { + setCurrentSubMenu(filterDefinition.type); + setCurrentParentFilterDefinition(filterDefinition); + } else { + selectFilter({ filterDefinition }); + } }; return ( @@ -38,6 +55,7 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({ onClick={handleClick} LeftIcon={getIcon(filterDefinition.iconName)} text={filterDefinition.label} + hasSubMenu={hasSubMenu} /> ); }; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx index ddaaf2e6ad92..c55496ee61d3 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx @@ -4,9 +4,9 @@ import { v4 } from 'uuid'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; -import { MultipleRecordSelectDropdown } from '@/object-record/select/components/MultipleRecordSelectDropdown'; +import { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown'; import { useRecordsForSelect } from '@/object-record/select/hooks/useRecordsForSelect'; -import { SelectableRecord } from '@/object-record/select/types/SelectableRecord'; +import { SelectableItem } from '@/object-record/select/types/SelectableItem'; import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { isDefined } from '~/utils/isDefined'; @@ -66,7 +66,7 @@ export const ObjectFilterDropdownRecordSelect = ({ }); const handleMultipleRecordSelectChange = ( - recordToSelect: SelectableRecord, + recordToSelect: SelectableItem, newSelectedValue: boolean, ) => { if (loading) { @@ -134,15 +134,15 @@ export const ObjectFilterDropdownRecordSelect = ({ }; return ( - ); }; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect.tsx new file mode 100644 index 000000000000..b0acad0bf247 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect.tsx @@ -0,0 +1,137 @@ +import { useState } from 'react'; +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 { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; +import { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown'; +import { SelectableItem } from '@/object-record/select/types/SelectableItem'; +import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters'; +import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; +import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; +import { isDefined } from '~/utils/isDefined'; + +export const EMPTY_FILTER_VALUE = '[]'; +export const MAX_ITEMS_TO_DISPLAY = 3; + +type ObjectFilterDropdownSourceSelectProps = { + viewComponentId?: string; +}; + +export const ObjectFilterDropdownSourceSelect = ({ + viewComponentId, +}: ObjectFilterDropdownSourceSelectProps) => { + const { + filterDefinitionUsedInDropdownState, + objectFilterDropdownSearchInputState, + selectedOperandInDropdownState, + selectedFilterState, + setObjectFilterDropdownSelectedRecordIds, + objectFilterDropdownSelectedRecordIdsState, + selectFilter, + emptyFilterButKeepDefinition, + } = useFilterDropdown(); + + const { deleteCombinedViewFilter } = + useDeleteCombinedViewFilters(viewComponentId); + + const { currentViewWithCombinedFiltersAndSorts } = + useGetCurrentView(viewComponentId); + + const filterDefinitionUsedInDropdown = useRecoilValue( + filterDefinitionUsedInDropdownState, + ); + const objectFilterDropdownSearchInput = useRecoilValue( + objectFilterDropdownSearchInputState, + ); + const selectedOperandInDropdown = useRecoilValue( + selectedOperandInDropdownState, + ); + const objectFilterDropdownSelectedRecordIds = useRecoilValue( + objectFilterDropdownSelectedRecordIdsState, + ); + const [fieldId] = useState(v4()); + + const selectedFilter = useRecoilValue(selectedFilterState); + + const sourceTypes = getSourceEnumOptions( + objectFilterDropdownSelectedRecordIds, + ); + + const filteredSelectedItems = sourceTypes.filter((option) => + objectFilterDropdownSelectedRecordIds.includes(option.id), + ); + + const handleMultipleItemSelectChange = ( + itemToSelect: SelectableItem, + newSelectedValue: boolean, + ) => { + const newSelectedItemIds = newSelectedValue + ? [...objectFilterDropdownSelectedRecordIds, itemToSelect.id] + : objectFilterDropdownSelectedRecordIds.filter( + (id) => id !== itemToSelect.id, + ); + + if (newSelectedItemIds.length === 0) { + emptyFilterButKeepDefinition(); + deleteCombinedViewFilter(fieldId); + return; + } + + setObjectFilterDropdownSelectedRecordIds(newSelectedItemIds); + + const selectedItemNames = sourceTypes + .filter((option) => newSelectedItemIds.includes(option.id)) + .map((option) => option.name); + + const filterDisplayValue = + selectedItemNames.length > MAX_ITEMS_TO_DISPLAY + ? `${selectedItemNames.length} source types` + : selectedItemNames.join(', '); + + if ( + isDefined(filterDefinitionUsedInDropdown) && + isDefined(selectedOperandInDropdown) + ) { + const newFilterValue = + newSelectedItemIds.length > 0 + ? JSON.stringify(newSelectedItemIds) + : EMPTY_FILTER_VALUE; + + const viewFilter = + currentViewWithCombinedFiltersAndSorts?.viewFilters.find( + (viewFilter) => + viewFilter.fieldMetadataId === + filterDefinitionUsedInDropdown.fieldMetadataId, + ); + + const filterId = viewFilter?.id ?? fieldId; + + selectFilter({ + id: selectedFilter?.id ? selectedFilter.id : filterId, + definition: filterDefinitionUsedInDropdown, + operand: selectedOperandInDropdown || ViewFilterOperand.Is, + displayValue: filterDisplayValue, + fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId, + value: newFilterValue, + }); + } + }; + + return ( + + !filteredSelectedItems.some((selected) => selected.id === item.id), + )} + filteredSelectedItems={filteredSelectedItems} + selectedItems={filteredSelectedItems} + onChange={handleMultipleItemSelectChange} + searchFilter={objectFilterDropdownSearchInput} + loadingItems={false} + /> + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterSelectMenu.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterSelectMenu.tsx new file mode 100644 index 000000000000..ad5d8bc6a5fd --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterSelectMenu.tsx @@ -0,0 +1,87 @@ +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 ( + <> + ) => + setSearchText(event.target.value) + } + /> + + + {sortedAvailableFilterDefinitions.map( + (availableFilterDefinition: FilterDefinition, index: number) => ( + + + + ), + )} + + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterSelectSubMenu.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterSelectSubMenu.tsx new file mode 100644 index 000000000000..205ca425e77b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterSelectSubMenu.tsx @@ -0,0 +1,102 @@ +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 ( + <> + { + setCurrentSubMenu(null); + }} + > + {getHeaderTitle(currentSubMenu)} + + ) => + setSearchText(event.target.value) + } + /> + + {getSubMenuOptions(currentSubMenu) + .sort((a, b) => a.name.localeCompare(b.name)) + .filter((item) => + item.name + .toLocaleLowerCase() + .includes(searchText.toLocaleLowerCase()), + ) + .map((menuOption, index) => ( + { + currentParentFilterDefinition && + handleSelectFilter({ + ...currentParentFilterDefinition, + label: menuOption.name, + type: menuOption.type as FilterType, + }); + }} + text={menuOption.name} + LeftIcon={getIcon( + menuOption.icon || currentParentFilterDefinition?.iconName, + )} + /> + ))} + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/subMenuStates.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/subMenuStates.ts new file mode 100644 index 000000000000..5f0469a2a678 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/subMenuStates.ts @@ -0,0 +1,15 @@ +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({ + key: 'currentSubMenuState', + default: null, +}); + +export const currentParentFilterDefinitionState = atom( + { + key: 'currentParentFilterDefinitionState', + default: null, + }, +); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterDefinition.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterDefinition.ts index 954562f4fed3..7f7cb678a5f1 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterDefinition.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterDefinition.ts @@ -11,4 +11,5 @@ export type FilterDefinition = { relationObjectMetadataNameSingular?: string; selectAllLabel?: string; SelectAllIcon?: IconComponent; + subFieldType?: FilterType; }; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterType.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterType.ts index 1803a88a54e4..cec0407c5177 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterType.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterType.ts @@ -17,4 +17,5 @@ export type FilterType = | 'RATING' | 'MULTI_SELECT' | 'ACTOR' - | 'ARRAY'; + | 'ARRAY' + | 'SOURCE'; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getHeaderTitle.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getHeaderTitle.ts new file mode 100644 index 000000000000..dcffda68384d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getHeaderTitle.ts @@ -0,0 +1,14 @@ +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; + } +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts index d1066e2a5491..80ac33bc660a 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts @@ -57,6 +57,8 @@ export const getOperandsForFilterType = ( ]; case 'RELATION': return [...relationOperands, ...emptyOperands]; + case 'SOURCE': + return [...relationOperands]; case 'SELECT': return [...relationOperands]; default: diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getSourceEnumOptions.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getSourceEnumOptions.ts new file mode 100644 index 000000000000..ee886da633e0 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getSourceEnumOptions.ts @@ -0,0 +1,56 @@ +import { SelectableItem } from '@/object-record/select/types/SelectableItem'; +import { + IconApi, + IconCsv, + IconGmail, + IconGoogleCalendar, + IconSettingsAutomation, + IconUserCircle, +} from 'twenty-ui'; + +export const getSourceEnumOptions = ( + selectedItemIds: string[], +): SelectableItem[] => { + return [ + { + id: 'MANUAL', + name: 'User', + isSelected: selectedItemIds.includes('MANUAL'), + AvatarIcon: IconUserCircle, + isIconInverted: true, + }, + { + id: 'IMPORT', + name: 'Import', + isSelected: selectedItemIds.includes('IMPORT'), + AvatarIcon: IconCsv, + isIconInverted: true, + }, + { + id: 'API', + name: 'Api', + isSelected: selectedItemIds.includes('API'), + AvatarIcon: IconApi, + isIconInverted: true, + }, + { + id: 'EMAIL', + name: 'Email', + isSelected: selectedItemIds.includes('EMAIL'), + AvatarIcon: IconGmail, + }, + { + id: 'CALENDAR', + name: 'Calendar', + isSelected: selectedItemIds.includes('CALENDAR'), + AvatarIcon: IconGoogleCalendar, + }, + { + id: 'WORKFLOW', + name: 'Workflow', + isSelected: selectedItemIds.includes('WORKFLOW'), + AvatarIcon: IconSettingsAutomation, + isIconInverted: true, + }, + ]; +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getSubMenuOptions.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getSubMenuOptions.ts new file mode 100644 index 000000000000..dde8aedbde3b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getSubMenuOptions.ts @@ -0,0 +1,21 @@ +import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType'; + +export const getSubMenuOptions = (subMenu: FilterType | null) => { + switch (subMenu) { + case 'ACTOR': + return [ + { + name: 'Creation Source', + icon: 'IconPlug', + type: 'SOURCE', + }, + { + name: 'Creator Name', + icon: 'IconId', + type: 'ACTOR', + }, + ]; + default: + return []; + } +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/hasSubMenuFilter.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/hasSubMenuFilter.ts new file mode 100644 index 000000000000..897670457d3f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/hasSubMenuFilter.ts @@ -0,0 +1,3 @@ +import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType'; + +export const hasSubMenuFilter = (type: FilterType) => ['ACTOR'].includes(type); diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts index 58cbaca25e9d..14584485370e 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts @@ -30,12 +30,6 @@ import { endOfDay, roundToNearestMinutes, startOfDay } from 'date-fns'; import { z } from 'zod'; import { Filter } from '../../object-filter-dropdown/types/Filter'; -export type ObjectDropdownFilter = Omit & { - definition: { - type: Filter['definition']['type']; - }; -}; - const applyEmptyFilters = ( operand: ViewFilterOperand, correspondingField: Pick, @@ -282,7 +276,7 @@ const applyEmptyFilters = ( }; export const turnObjectDropdownFilterIntoQueryFilter = ( - rawUIFilters: ObjectDropdownFilter[], + rawUIFilters: Filter[], fields: Pick[], ): RecordGqlOperationFilter | undefined => { const objectRecordFilters: RecordGqlOperationFilter[] = []; @@ -894,48 +888,87 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( break; } case 'ACTOR': - switch (rawUIFilter.operand) { - case ViewFilterOperand.Contains: - objectRecordFilters.push({ - or: [ - { - [correspondingField.name]: { - name: { - ilike: `%${rawUIFilter.value}%`, + if (rawUIFilter.definition.subFieldType !== undefined) { + const parsedRecordIds = JSON.parse(rawUIFilter.value) as string[]; + switch (rawUIFilter.definition.subFieldType) { + case 'SOURCE': + switch (rawUIFilter.operand) { + case ViewFilterOperand.Is: + objectRecordFilters.push({ + [correspondingField.name]: { + source: { + in: parsedRecordIds, + } as RelationFilter, }, - } as ActorFilter, - }, - ], - }); - break; - case ViewFilterOperand.DoesNotContain: - objectRecordFilters.push({ - and: [ - { - not: { + }); + + 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`, + ); + } + } + } else { + switch (rawUIFilter.operand) { + case ViewFilterOperand.Contains: + objectRecordFilters.push({ + or: [ + { [correspondingField.name]: { name: { ilike: `%${rawUIFilter.value}%`, }, } as ActorFilter, }, - }, - ], - }); - break; - case ViewFilterOperand.IsEmpty: - case ViewFilterOperand.IsNotEmpty: - applyEmptyFilters( - rawUIFilter.operand, - correspondingField, - objectRecordFilters, - rawUIFilter.definition.type, - ); - break; - default: - throw new Error( - `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`, - ); + ], + }); + break; + case ViewFilterOperand.DoesNotContain: + objectRecordFilters.push({ + and: [ + { + not: { + [correspondingField.name]: { + name: { + ilike: `%${rawUIFilter.value}%`, + }, + } as ActorFilter, + }, + }, + ], + }); + break; + case ViewFilterOperand.IsEmpty: + case ViewFilterOperand.IsNotEmpty: + applyEmptyFilters( + rawUIFilter.operand, + correspondingField, + objectRecordFilters, + rawUIFilter.definition.type, + ); + break; + default: + throw new Error( + `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`, + ); + } + break; } break; case 'EMAILS': diff --git a/packages/twenty-front/src/modules/object-record/select/components/MultipleRecordSelectDropdown.tsx b/packages/twenty-front/src/modules/object-record/select/components/MultipleSelectDropdown.tsx similarity index 55% rename from packages/twenty-front/src/modules/object-record/select/components/MultipleRecordSelectDropdown.tsx rename to packages/twenty-front/src/modules/object-record/select/components/MultipleSelectDropdown.tsx index 3914a41633fe..fba130110ddc 100644 --- a/packages/twenty-front/src/modules/object-record/select/components/MultipleRecordSelectDropdown.tsx +++ b/packages/twenty-front/src/modules/object-record/select/components/MultipleSelectDropdown.tsx @@ -1,9 +1,10 @@ +import styled from '@emotion/styled'; import { useEffect, useState } from 'react'; import { useRecoilValue } from 'recoil'; import { Key } from 'ts-key-enum'; -import { Avatar } from 'twenty-ui'; +import { AvatarChip } from 'twenty-ui'; -import { SelectableRecord } from '@/object-record/select/types/SelectableRecord'; +import { SelectableItem } from '@/object-record/select/types/SelectableItem'; import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; @@ -14,26 +15,36 @@ import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { MenuItemMultiSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; -export const MultipleRecordSelectDropdown = ({ +const StyledAvatarChip = styled(AvatarChip)` + &.avatar-icon-container { + color: ${({ theme }) => theme.font.color.secondary}; + gap: ${({ theme }) => theme.spacing(2)}; + padding-left: 0px; + padding-right: 0px; + font-size: ${({ theme }) => theme.font.size.md}; + } +`; + +export const MultipleSelectDropdown = ({ selectableListId, hotkeyScope, - recordsToSelect, - loadingRecords, - filteredSelectedRecords, + itemsToSelect, + loadingItems, + filteredSelectedItems, onChange, searchFilter, }: { selectableListId: string; hotkeyScope: string; - recordsToSelect: SelectableRecord[]; - filteredSelectedRecords: SelectableRecord[]; - selectedRecords: SelectableRecord[]; + itemsToSelect: SelectableItem[]; + filteredSelectedItems: SelectableItem[]; + selectedItems: SelectableItem[]; searchFilter: string; onChange: ( - changedRecordToSelect: SelectableRecord, + changedItemToSelect: SelectableItem, newSelectedValue: boolean, ) => void; - loadingRecords: boolean; + loadingItems: boolean; }) => { const { closeDropdown } = useDropdown(); const { selectedItemIdState } = useSelectableListStates({ @@ -44,32 +55,32 @@ export const MultipleRecordSelectDropdown = ({ const selectedItemId = useRecoilValue(selectedItemIdState); - const handleRecordSelectChange = ( - recordToSelect: SelectableRecord, + const handleItemSelectChange = ( + itemToSelect: SelectableItem, newSelectedValue: boolean, ) => { onChange( { - ...recordToSelect, + ...itemToSelect, isSelected: newSelectedValue, }, newSelectedValue, ); }; - const [recordsInDropdown, setRecordInDropdown] = useState([ - ...(filteredSelectedRecords ?? []), - ...(recordsToSelect ?? []), + const [itemsInDropdown, setItemInDropdown] = useState([ + ...(filteredSelectedItems ?? []), + ...(itemsToSelect ?? []), ]); useEffect(() => { - if (!loadingRecords) { - setRecordInDropdown([ - ...(filteredSelectedRecords ?? []), - ...(recordsToSelect ?? []), + if (!loadingItems) { + setItemInDropdown([ + ...(filteredSelectedItems ?? []), + ...(itemsToSelect ?? []), ]); } - }, [recordsToSelect, filteredSelectedRecords, loadingRecords]); + }, [itemsToSelect, filteredSelectedItems, loadingItems]); useScopedHotkeys( [Key.Escape], @@ -82,12 +93,12 @@ export const MultipleRecordSelectDropdown = ({ ); const showNoResult = - recordsToSelect?.length === 0 && + itemsToSelect?.length === 0 && searchFilter !== '' && - filteredSelectedRecords?.length === 0 && - !loadingRecords; + filteredSelectedItems?.length === 0 && + !loadingItems; - const selectableItemIds = recordsInDropdown.map((record) => record.id); + const selectableItemIds = itemsInDropdown.map((item) => item.id); return ( { - const record = recordsInDropdown.findIndex( + const item = itemsInDropdown.findIndex( (entity) => entity.id === itemId, ); - const recordIsSelectedInDropwdown = filteredSelectedRecords.find( + const itemIsSelectedInDropwdown = filteredSelectedItems.find( (entity) => entity.id === itemId, ); - handleRecordSelectChange( - recordsInDropdown[record], - !recordIsSelectedInDropwdown, + handleItemSelectChange( + itemsInDropdown[item], + !itemIsSelectedInDropwdown, ); resetSelectedItem(); }} > - {recordsInDropdown?.map((record) => { + {itemsInDropdown?.map((item) => { return ( { resetSelectedItem(); - handleRecordSelectChange(record, newCheckedValue); + handleItemSelectChange(item, newCheckedValue); }} avatar={ - } - text={record.name} /> ); })} {showNoResult && } - {loadingRecords && } + {loadingItems && } ); diff --git a/packages/twenty-front/src/modules/object-record/select/hooks/useRecordsForSelect.ts b/packages/twenty-front/src/modules/object-record/select/hooks/useRecordsForSelect.ts index 905d4fd8f138..73bacf2e7cf6 100644 --- a/packages/twenty-front/src/modules/object-record/select/hooks/useRecordsForSelect.ts +++ b/packages/twenty-front/src/modules/object-record/select/hooks/useRecordsForSelect.ts @@ -5,7 +5,7 @@ import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapTo import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; -import { SelectableRecord } from '@/object-record/select/types/SelectableRecord'; +import { SelectableItem } from '@/object-record/select/types/SelectableItem'; import { getObjectFilterFields } from '@/object-record/select/utils/getObjectFilterFields'; import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables'; @@ -109,19 +109,19 @@ export const useRecordsForSelect = ({ .map((record) => ({ ...record, isSelected: true, - })) as SelectableRecord[], + })) as SelectableItem[], filteredSelectedRecords: filteredSelectedRecordsData .map(mapToObjectRecordIdentifier) .map((record) => ({ ...record, isSelected: true, - })) as SelectableRecord[], + })) as SelectableItem[], recordsToSelect: recordsToSelectData .map(mapToObjectRecordIdentifier) .map((record) => ({ ...record, isSelected: false, - })) as SelectableRecord[], + })) as SelectableItem[], loading: recordsToSelectLoading || filteredSelectedRecordsLoading || diff --git a/packages/twenty-front/src/modules/object-record/select/types/SelectableItem.ts b/packages/twenty-front/src/modules/object-record/select/types/SelectableItem.ts new file mode 100644 index 000000000000..57122ebe702b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/select/types/SelectableItem.ts @@ -0,0 +1,11 @@ +import { AvatarType, IconComponent } from 'twenty-ui'; + +export type SelectableItem = T & { + id: string; + name: string; + avatarUrl?: string; + avatarType?: AvatarType; + AvatarIcon?: IconComponent; + isSelected: boolean; + isIconInverted?: boolean; +}; diff --git a/packages/twenty-front/src/modules/object-record/select/types/SelectableRecord.ts b/packages/twenty-front/src/modules/object-record/select/types/SelectableRecord.ts deleted file mode 100644 index ff08ac70b8e3..000000000000 --- a/packages/twenty-front/src/modules/object-record/select/types/SelectableRecord.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { AvatarType } from 'twenty-ui'; - -export type SelectableRecord = { - id: string; - name: string; - avatarUrl?: string; - avatarType?: AvatarType; - record: any; - isSelected: boolean; -}; diff --git a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar.tsx b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar.tsx index 7e8dff27ecfc..8086e4ad0a3c 100644 --- a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar.tsx @@ -22,7 +22,7 @@ type MenuItemMultiSelectAvatarProps = { avatar?: ReactNode; selected: boolean; isKeySelected?: boolean; - text: string; + text?: string; className?: string; onSelectChange?: (selected: boolean) => void; }; diff --git a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemSelect.tsx b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemSelect.tsx index 5f6dee4cd9d1..7a5a976709f0 100644 --- a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemSelect.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemSelect.tsx @@ -1,6 +1,6 @@ import { css, useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { IconCheck, IconComponent } from 'twenty-ui'; +import { IconCheck, IconChevronRight, IconComponent } from 'twenty-ui'; import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent'; import { StyledMenuItemBase } from '../internals/components/StyledMenuItemBase'; @@ -45,6 +45,7 @@ type MenuItemSelectProps = { onClick?: () => void; disabled?: boolean; hovered?: boolean; + hasSubMenu?: boolean; }; export const MenuItemSelect = ({ @@ -55,6 +56,7 @@ export const MenuItemSelect = ({ onClick, disabled, hovered, + hasSubMenu = false, }: MenuItemSelectProps) => { const theme = useTheme(); @@ -68,6 +70,12 @@ export const MenuItemSelect = ({ > {selected && } + {hasSubMenu && ( + + )} ); }; diff --git a/packages/twenty-front/src/modules/views/types/ViewFilter.ts b/packages/twenty-front/src/modules/views/types/ViewFilter.ts index 608e13918550..f5175cf41433 100644 --- a/packages/twenty-front/src/modules/views/types/ViewFilter.ts +++ b/packages/twenty-front/src/modules/views/types/ViewFilter.ts @@ -1,3 +1,4 @@ +import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; import { ViewFilterOperand } from './ViewFilterOperand'; export type ViewFilter = { @@ -11,4 +12,5 @@ export type ViewFilter = { createdAt?: string; updatedAt?: string; viewId?: string; + definition?: FilterDefinition; }; diff --git a/packages/twenty-front/src/modules/views/utils/getFilterDefinitionForViewFilter.ts b/packages/twenty-front/src/modules/views/utils/getFilterDefinitionForViewFilter.ts new file mode 100644 index 000000000000..d66b6f26b6fd --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/getFilterDefinitionForViewFilter.ts @@ -0,0 +1,17 @@ +import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; +import { hasSubMenuFilter } from '@/object-record/object-filter-dropdown/utils/hasSubMenuFilter'; +import { ViewFilter } from '../types/ViewFilter'; + +export const getFilterDefinitionForViewFilter = ( + viewFilter: ViewFilter, + availableFilterDefinition: FilterDefinition, +): FilterDefinition => { + return { + ...availableFilterDefinition, + subFieldType: + hasSubMenuFilter(availableFilterDefinition.type) && + viewFilter.definition?.type !== availableFilterDefinition.type + ? viewFilter.definition?.type + : undefined, + }; +}; diff --git a/packages/twenty-front/src/modules/views/utils/mapViewFiltersToFilters.ts b/packages/twenty-front/src/modules/views/utils/mapViewFiltersToFilters.ts index 104ba6afdaae..f3a4fcd223ab 100644 --- a/packages/twenty-front/src/modules/views/utils/mapViewFiltersToFilters.ts +++ b/packages/twenty-front/src/modules/views/utils/mapViewFiltersToFilters.ts @@ -2,6 +2,7 @@ import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; import { isDefined } from '~/utils/isDefined'; +import { getFilterDefinitionForViewFilter } from '@/views/utils/getFilterDefinitionForViewFilter'; import { ViewFilter } from '../types/ViewFilter'; export const mapViewFiltersToFilters = ( @@ -23,7 +24,10 @@ export const mapViewFiltersToFilters = ( value: viewFilter.value, displayValue: viewFilter.displayValue, operand: viewFilter.operand, - definition: availableFilterDefinition, + definition: getFilterDefinitionForViewFilter( + viewFilter, + availableFilterDefinition, + ), }; }) .filter(isDefined); diff --git a/packages/twenty-ui/src/display/chip/components/Chip.tsx b/packages/twenty-ui/src/display/chip/components/Chip.tsx index 8ff16e9a9db1..48795fd42551 100644 --- a/packages/twenty-ui/src/display/chip/components/Chip.tsx +++ b/packages/twenty-ui/src/display/chip/components/Chip.tsx @@ -127,6 +127,7 @@ export const Chip = ({ rightComponent, accent = ChipAccent.TextPrimary, onClick, + className, }: ChipProps) => { return ( {leftComponent}