From 315820ec86abf9d805a4450507c1b9116929881e Mon Sep 17 00:00:00 2001 From: ad-elias Date: Thu, 24 Oct 2024 16:59:59 +0200 Subject: [PATCH] Feat: Advanced filter (#7700) Design: ![twenty-advanced-filters-design](https://github.com/user-attachments/assets/7d99971c-9ee1-4a78-a2fb-7ae5a9b3a836) Not ready to be merged yet! --------- Co-authored-by: Lucas Bordeau --- packages/twenty-front/jest.config.ts | 4 +- .../utils/computeContextStoreFilters.ts | 5 +- .../types/CoreObjectNameSingular.ts | 1 + .../AdvancedFilterAddFilterRuleSelect.tsx | 161 +++ .../AdvancedFilterLogicalOperatorCell.tsx | 41 + .../AdvancedFilterLogicalOperatorDropdown.tsx | 33 + ...AdvancedFilterRootLevelViewFilterGroup.tsx | 78 ++ .../AdvancedFilterRuleOptionsDropdown.tsx | 87 ++ ...dvancedFilterRuleOptionsDropdownButton.tsx | 25 + .../components/AdvancedFilterViewFilter.tsx | 47 + .../AdvancedFilterViewFilterFieldSelect.tsx | 71 ++ .../AdvancedFilterViewFilterGroup.tsx | 67 ++ .../AdvancedFilterViewFilterOperandSelect.tsx | 111 +++ .../AdvancedFilterViewFilterValueInput.tsx | 70 ++ .../AdvancedFilterLogicalOperatorOptions.ts | 12 + .../hooks/useAdvancedFilterDropdown.ts | 14 + .../hooks/useCurrentViewFilter.ts | 31 + .../hooks/useCurrentViewViewFilterGroup.ts | 59 ++ .../hooks/useDeleteCombinedViewFilterGroup.ts | 111 +++ .../hooks/useUpsertCombinedViewFilterGroup.ts | 54 ++ .../components/AdvancedFilterButton.tsx | 124 +++ .../MultipleFiltersDropdownContent.tsx | 8 +- .../ObjectFilterDropdownDateInput.tsx | 2 + .../ObjectFilterDropdownFilterInput.tsx | 50 +- ...bjectFilterDropdownFilterOperandSelect.tsx | 40 + .../ObjectFilterDropdownFilterSelect.tsx | 31 +- ...pdownFilterSelectCompositeFieldSubMenu.tsx | 39 + ...jectFilterDropdownFilterSelectMenuItem.tsx | 15 +- .../ObjectFilterDropdownNumberInput.tsx | 1 + .../ObjectFilterDropdownOptionSelect.tsx | 1 + .../ObjectFilterDropdownRatingInput.tsx | 1 + .../ObjectFilterDropdownRecordSelect.tsx | 1 + .../ObjectFilterDropdownSourceSelect.tsx | 1 + .../ObjectFilterDropdownTextSearchInput.tsx | 3 +- .../ObjectFilterOperandSelectAndInput.tsx | 19 + .../constants/DateFilterTypes.ts | 1 + .../constants/NumberFilterTypes.ts | 1 + .../constants/TextFilterTypes.ts | 14 + .../__tests__/useFilterDropdown.test.tsx | 18 +- .../hooks/useFilterDropdown.ts | 23 +- .../hooks/useFilterDropdownStates.ts | 14 + .../hooks/useSelectFilter.ts | 18 +- ...edFilterViewFilterGroupIdComponentState.ts | 7 + ...dvancedFilterViewFilterIdComponentState.ts | 8 + .../object-filter-dropdown/types/Filter.ts | 2 + .../types/FilterDraft.ts | 4 + .../utils/configurableViewFilterOperands.ts | 14 + .../utils/getOperandsForFilterType.ts | 2 +- .../utils/isActorSourceCompositeFilter.ts | 2 +- ...mputeViewRecordGqlOperationFilter.test.ts} | 25 +- .../computeViewRecordGqlOperationFilter.ts | 903 +++++++++++++++++ ...ts => getEmptyRecordGqlOperationFilter.ts} | 12 +- .../utils/turnFiltersIntoQueryFilter.ts | 913 ------------------ .../components/RecordIndexContainer.tsx | 13 +- .../hooks/useLoadRecordIndexBoard.ts | 11 +- .../hooks/useLoadRecordIndexBoardColumn.ts | 12 +- .../hooks/useLoadRecordIndexTable.ts | 8 +- .../recordIndexViewFilterGroupsState.ts | 7 + .../hooks/internal/useRecordTableStates.ts | 5 + .../record-table/hooks/useRecordTable.ts | 6 + .../tableViewFilterGroupsComponentState.ts | 9 + .../findAllViewsOperationSignatureFactory.ts | 1 + .../modules/ui/input/components/Select.tsx | 66 +- .../ui/input/components/SelectControl.tsx | 63 ++ .../views/components/AdvancedFilterChip.tsx | 27 + .../AdvancedFilterDropdownButton.tsx | 83 ++ .../EditableFilterDropdownButton.tsx | 4 +- .../views/components/ViewBarDetails.tsx | 15 +- .../constants/AdvancedFilterDropdownId.ts | 1 + .../usePersistViewFilterGroupRecords.ts | 214 ++++ .../internal/usePersistViewFilterRecords.ts | 2 + .../hooks/useCreateViewFromCurrentView.ts | 15 + .../hooks/useGetCombinedViewFilterGroups.ts | 67 ++ .../modules/views/hooks/useGetCurrentView.ts | 20 + .../views/hooks/useResetUnsavedViewStates.ts | 18 + .../useSaveCurrentViewFiltersAndSorts.ts | 76 +- .../hooks/useUpsertCombinedViewFilters.ts | 10 +- ...eViewFilterGroupIdsComponentFamilyState.ts | 9 + ...ertViewFilterGroupsComponentFamilyState.ts | 10 + ...ToUpsertViewFiltersComponentFamilyState.ts | 2 +- .../src/modules/views/types/GraphQLView.ts | 2 + .../src/modules/views/types/View.ts | 2 + .../src/modules/views/types/ViewFilter.ts | 2 + .../modules/views/types/ViewFilterGroup.ts | 10 + .../types/ViewFilterGroupLogicalOperator.ts | 4 + .../utils/getCombinedViewFilterGroups.ts | 38 + .../views/utils/getCombinedViewFilters.ts | 11 +- .../views/utils/getQueryVariablesFromView.ts | 7 +- .../views/utils/mapViewFiltersToFilters.ts | 2 + .../views/utils/shouldReplaceFilter.ts | 18 + .../sortViewFilterGroupsOutermostFirst.ts | 15 + .../constants/standard-field-ids.ts | 10 + .../constants/standard-object-ids.ts | 1 + .../standard-objects/index.ts | 2 + .../view-filter-group.workspace-entity.ts | 95 ++ .../view-filter.workspace-entity.ts | 20 + .../standard-objects/view.workspace-entity.ts | 13 + .../display/icon/components/TablerIcons.ts | 2 + packages/twenty-ui/src/index.ts | 1 - 99 files changed, 3349 insertions(+), 1079 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect.tsx create mode 100644 packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorCell.tsx create mode 100644 packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorDropdown.tsx create mode 100644 packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRootLevelViewFilterGroup.tsx create mode 100644 packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRuleOptionsDropdown.tsx create mode 100644 packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRuleOptionsDropdownButton.tsx create mode 100644 packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterViewFilter.tsx create mode 100644 packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterViewFilterFieldSelect.tsx create mode 100644 packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterViewFilterGroup.tsx create mode 100644 packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterViewFilterOperandSelect.tsx create mode 100644 packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterViewFilterValueInput.tsx create mode 100644 packages/twenty-front/src/modules/object-record/advanced-filter/constants/AdvancedFilterLogicalOperatorOptions.ts create mode 100644 packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useAdvancedFilterDropdown.ts create mode 100644 packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useCurrentViewFilter.ts create mode 100644 packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useCurrentViewViewFilterGroup.ts create mode 100644 packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useDeleteCombinedViewFilterGroup.ts create mode 100644 packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useUpsertCombinedViewFilterGroup.ts create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/AdvancedFilterButton.tsx create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterOperandSelect.tsx create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterOperandSelectAndInput.tsx create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/constants/DateFilterTypes.ts create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/constants/NumberFilterTypes.ts create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/constants/TextFilterTypes.ts create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/advancedFilterViewFilterGroupIdComponentState.ts create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/advancedFilterViewFilterIdComponentState.ts create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterDraft.ts create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/configurableViewFilterOperands.ts rename packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/{turnFiltersIntoQueryFilter.test.ts => computeViewRecordGqlOperationFilter.test.ts} (97%) create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/utils/computeViewRecordGqlOperationFilter.ts rename packages/twenty-front/src/modules/object-record/record-filter/utils/{applyEmptyFilters.ts => getEmptyRecordGqlOperationFilter.ts} (97%) delete mode 100644 packages/twenty-front/src/modules/object-record/record-filter/utils/turnFiltersIntoQueryFilter.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-index/states/recordIndexViewFilterGroupsState.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-table/states/tableViewFilterGroupsComponentState.ts create mode 100644 packages/twenty-front/src/modules/ui/input/components/SelectControl.tsx create mode 100644 packages/twenty-front/src/modules/views/components/AdvancedFilterChip.tsx create mode 100644 packages/twenty-front/src/modules/views/components/AdvancedFilterDropdownButton.tsx create mode 100644 packages/twenty-front/src/modules/views/constants/AdvancedFilterDropdownId.ts create mode 100644 packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilterGroupRecords.ts create mode 100644 packages/twenty-front/src/modules/views/hooks/useGetCombinedViewFilterGroups.ts create mode 100644 packages/twenty-front/src/modules/views/states/unsavedToDeleteViewFilterGroupIdsComponentFamilyState.ts create mode 100644 packages/twenty-front/src/modules/views/states/unsavedToUpsertViewFilterGroupsComponentFamilyState.ts create mode 100644 packages/twenty-front/src/modules/views/types/ViewFilterGroup.ts create mode 100644 packages/twenty-front/src/modules/views/types/ViewFilterGroupLogicalOperator.ts create mode 100644 packages/twenty-front/src/modules/views/utils/getCombinedViewFilterGroups.ts create mode 100644 packages/twenty-front/src/modules/views/utils/shouldReplaceFilter.ts create mode 100644 packages/twenty-front/src/modules/views/utils/sortViewFilterGroupsOutermostFirst.ts create mode 100644 packages/twenty-server/src/modules/view/standard-objects/view-filter-group.workspace-entity.ts diff --git a/packages/twenty-front/jest.config.ts b/packages/twenty-front/jest.config.ts index 5cb6b789c850..ecf046e155e8 100644 --- a/packages/twenty-front/jest.config.ts +++ b/packages/twenty-front/jest.config.ts @@ -25,9 +25,9 @@ const jestConfig: JestConfigWithTsJest = { extensionsToTreatAsEsm: ['.ts', '.tsx'], coverageThreshold: { global: { - statements: 59, + statements: 58, lines: 55, - functions: 48, + functions: 47, }, }, collectCoverageFrom: ['/src/**/*.ts'], diff --git a/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts b/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts index edc4e173360d..5126ae272ccb 100644 --- a/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts +++ b/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts @@ -1,7 +1,7 @@ import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter'; -import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter'; +import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; export const computeContextStoreFilters = ( @@ -12,9 +12,10 @@ export const computeContextStoreFilters = ( if (contextStoreTargetedRecordsRule.mode === 'exclusion') { queryFilter = makeAndFilterVariables([ - turnFiltersIntoQueryFilter( + computeViewRecordGqlOperationFilter( contextStoreTargetedRecordsRule.filters, objectMetadataItem?.fields ?? [], + [], ), contextStoreTargetedRecordsRule.excludedRecordIds.length > 0 ? { diff --git a/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts b/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts index 03164e28ac27..bf872fafbc12 100644 --- a/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts +++ b/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts @@ -24,6 +24,7 @@ export enum CoreObjectNameSingular { View = 'view', ViewField = 'viewField', ViewFilter = 'viewFilter', + ViewFilterGroup = 'viewFilterGroup', ViewSort = 'viewSort', ViewGroup = 'viewGroup', Webhook = 'webhook', diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect.tsx new file mode 100644 index 000000000000..2e82b72fc859 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect.tsx @@ -0,0 +1,161 @@ +import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; +import { useUpsertCombinedViewFilterGroup } from '@/object-record/advanced-filter/hooks/useUpsertCombinedViewFilterGroup'; +import { getOperandsForFilterDefinition } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType'; +import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; +import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { ADVANCED_FILTER_DROPDOWN_ID } from '@/views/constants/AdvancedFilterDropdownId'; +import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; +import { useUpsertCombinedViewFilters } from '@/views/hooks/useUpsertCombinedViewFilters'; +import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState'; +import { ViewFilterGroup } from '@/views/types/ViewFilterGroup'; +import { ViewFilterGroupLogicalOperator } from '@/views/types/ViewFilterGroupLogicalOperator'; +import { useCallback } from 'react'; +import { IconLibraryPlus, IconPlus, isDefined, LightButton } from 'twenty-ui'; +import { v4 } from 'uuid'; + +type AdvancedFilterAddFilterRuleSelectProps = { + viewFilterGroup: ViewFilterGroup; + lastChildPosition?: number; +}; + +export const AdvancedFilterAddFilterRuleSelect = ({ + viewFilterGroup, + lastChildPosition = 0, +}: AdvancedFilterAddFilterRuleSelectProps) => { + const dropdownId = `advanced-filter-add-filter-rule-${viewFilterGroup.id}`; + + const { currentViewId } = useGetCurrentView(); + + const { upsertCombinedViewFilterGroup } = useUpsertCombinedViewFilterGroup(); + const { upsertCombinedViewFilter } = useUpsertCombinedViewFilters(); + + const newPositionInViewFilterGroup = lastChildPosition + 1; + + const { closeDropdown } = useDropdown(dropdownId); + + const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView(); + + const objectMetadataId = + currentViewWithCombinedFiltersAndSorts?.objectMetadataId; + + if (!objectMetadataId) { + throw new Error('Object metadata id is missing from current view'); + } + + const { objectMetadataItem } = useObjectMetadataItemById({ + objectId: objectMetadataId, + }); + + const availableFilterDefinitions = useRecoilComponentValueV2( + availableFilterDefinitionsComponentState, + ); + + const getDefaultFilterDefinition = useCallback(() => { + const defaultFilterDefinition = + availableFilterDefinitions.find( + (filterDefinition) => + filterDefinition.fieldMetadataId === + objectMetadataItem?.labelIdentifierFieldMetadataId, + ) ?? availableFilterDefinitions?.[0]; + + if (!defaultFilterDefinition) { + throw new Error('Missing default filter definition'); + } + + return defaultFilterDefinition; + }, [availableFilterDefinitions, objectMetadataItem]); + + const handleAddFilter = () => { + closeDropdown(); + + const defaultFilterDefinition = getDefaultFilterDefinition(); + + upsertCombinedViewFilter({ + id: v4(), + fieldMetadataId: defaultFilterDefinition.fieldMetadataId, + operand: getOperandsForFilterDefinition(defaultFilterDefinition)[0], + definition: defaultFilterDefinition, + value: '', + displayValue: '', + viewFilterGroupId: viewFilterGroup.id, + positionInViewFilterGroup: newPositionInViewFilterGroup, + }); + }; + + const handleAddFilterGroup = () => { + closeDropdown(); + + if (!currentViewId) { + throw new Error('Missing view id'); + } + + const newViewFilterGroup = { + id: v4(), + viewId: currentViewId, + logicalOperator: ViewFilterGroupLogicalOperator.AND, + parentViewFilterGroupId: viewFilterGroup.id, + positionInViewFilterGroup: newPositionInViewFilterGroup, + }; + + upsertCombinedViewFilterGroup(newViewFilterGroup); + + const defaultFilterDefinition = getDefaultFilterDefinition(); + + upsertCombinedViewFilter({ + id: v4(), + fieldMetadataId: defaultFilterDefinition.fieldMetadataId, + operand: getOperandsForFilterDefinition(defaultFilterDefinition)[0], + definition: defaultFilterDefinition, + value: '', + displayValue: '', + viewFilterGroupId: newViewFilterGroup.id, + positionInViewFilterGroup: newPositionInViewFilterGroup, + }); + }; + + const isFilterRuleGroupOptionVisible = !isDefined( + viewFilterGroup.parentViewFilterGroupId, + ); + + if (!isFilterRuleGroupOptionVisible) { + return ( + + ); + } + + return ( + + } + dropdownComponents={ + + + {isFilterRuleGroupOptionVisible && ( + + )} + + } + dropdownHotkeyScope={{ scope: ADVANCED_FILTER_DROPDOWN_ID }} + dropdownOffset={{ y: 8, x: 0 }} + dropdownPlacement="bottom-start" + /> + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorCell.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorCell.tsx new file mode 100644 index 000000000000..b4e70550672b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorCell.tsx @@ -0,0 +1,41 @@ +import { AdvancedFilterLogicalOperatorDropdown } from '@/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorDropdown'; +import { ViewFilterGroup } from '@/views/types/ViewFilterGroup'; +import styled from '@emotion/styled'; +import { capitalize } from '~/utils/string/capitalize'; + +const StyledText = styled.div` + height: ${({ theme }) => theme.spacing(8)}; + display: flex; + align-items: center; +`; + +const StyledContainer = styled.div` + align-items: start; + display: flex; + min-width: ${({ theme }) => theme.spacing(20)}; + color: ${({ theme }) => theme.font.color.tertiary}; +`; + +type AdvancedFilterLogicalOperatorCellProps = { + index: number; + viewFilterGroup: ViewFilterGroup; +}; + +export const AdvancedFilterLogicalOperatorCell = ({ + index, + viewFilterGroup, +}: AdvancedFilterLogicalOperatorCellProps) => ( + + {index === 0 ? ( + Where + ) : index === 1 ? ( + + ) : ( + + {capitalize(viewFilterGroup.logicalOperator.toLowerCase())} + + )} + +); diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorDropdown.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorDropdown.tsx new file mode 100644 index 000000000000..68fa9f8763a4 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorDropdown.tsx @@ -0,0 +1,33 @@ +import { ADVANCED_FILTER_LOGICAL_OPERATOR_OPTIONS } from '@/object-record/advanced-filter/constants/AdvancedFilterLogicalOperatorOptions'; +import { useUpsertCombinedViewFilterGroup } from '@/object-record/advanced-filter/hooks/useUpsertCombinedViewFilterGroup'; +import { Select } from '@/ui/input/components/Select'; +import { ViewFilterGroup } from '@/views/types/ViewFilterGroup'; +import { ViewFilterGroupLogicalOperator } from '@/views/types/ViewFilterGroupLogicalOperator'; + +type AdvancedFilterLogicalOperatorDropdownProps = { + viewFilterGroup: ViewFilterGroup; +}; + +export const AdvancedFilterLogicalOperatorDropdown = ({ + viewFilterGroup, +}: AdvancedFilterLogicalOperatorDropdownProps) => { + const { upsertCombinedViewFilterGroup } = useUpsertCombinedViewFilterGroup(); + + const handleChange = (value: ViewFilterGroupLogicalOperator) => { + upsertCombinedViewFilterGroup({ + ...viewFilterGroup, + logicalOperator: value, + }); + }; + + return ( +