From 97ec720d2c292e9fe4c930c839e1150b5d584469 Mon Sep 17 00:00:00 2001 From: Cheng Yin <34277382+wadeKeith@users.noreply.github.com> Date: Mon, 4 May 2026 23:08:30 +0800 Subject: [PATCH] fix: show active advanced filter count badge in dropdown button (#20229) ## Summary Replaces the hardcoded `0` in `ViewBarFilterDropdownAdvancedFilterButton` with the actual count of active advanced filter rules, matching the behavior of `AdvancedFilterChip` in the view bar. ## What changed In `packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownAdvancedFilterButton.tsx`: - Imported `useAtomComponentSelectorValue` and `rootLevelRecordFilterGroupComponentSelector` - Imported `useChildRecordFiltersAndRecordFilterGroups` - Replaced `const advancedFilterQuerySubFilterCount = 0; // TODO` with the real computed count via the same hook pattern used in `AdvancedFilterChip.tsx` The pill badge will now appear on the "Advanced filter" dropdown menu item showing the number of active advanced filter rules (e.g. "2" when two rules are active). ## References - Fixes #20207 --------- Co-authored-by: wadeKeith Co-authored-by: Charles Bochet Co-authored-by: Cursor --- ...wBarFilterDropdownAdvancedFilterButton.tsx | 27 ++++- .../ViewBarFilterDropdown.stories.tsx | 103 +++++++++++++++++- 2 files changed, 122 insertions(+), 8 deletions(-) diff --git a/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownAdvancedFilterButton.tsx b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownAdvancedFilterButton.tsx index 3fc1c12d1b..80196d25cc 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownAdvancedFilterButton.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownAdvancedFilterButton.tsx @@ -7,9 +7,12 @@ import { SelectableListItem } from '@/ui/layout/selectable-list/components/Selec import { isSelectedItemIdComponentFamilyState } from '@/ui/layout/selectable-list/states/isSelectedItemIdComponentFamilyState'; import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue'; import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue'; +import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue'; import { VIEW_BAR_FILTER_BOTTOM_MENU_ITEM_IDS } from '@/views/constants/ViewBarFilterBottomMenuItemIds'; +import { useChildRecordFiltersAndRecordFilterGroups } from '@/object-record/advanced-filter/hooks/useChildRecordFiltersAndRecordFilterGroups'; import { useSetRecordFilterUsedInAdvancedFilterDropdownRow } from '@/object-record/advanced-filter/hooks/useSetRecordFilterUsedInAdvancedFilterDropdownRow'; +import { rootLevelRecordFilterGroupComponentSelector } from '@/object-record/advanced-filter/states/rootLevelRecordFilterGroupComponentSelector'; import { useCreateEmptyRecordFilterFromFieldMetadataItem } from '@/object-record/record-filter/hooks/useCreateEmptyRecordFilterFromFieldMetadataItem'; import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown'; @@ -34,7 +37,17 @@ const StyledPillContainer = styled.span` `; export const ViewBarFilterDropdownAdvancedFilterButton = () => { - const advancedFilterQuerySubFilterCount = 0; // TODO + const rootRecordFilterGroup = useAtomComponentSelectorValue( + rootLevelRecordFilterGroupComponentSelector, + ); + + const { childRecordFiltersAndRecordFilterGroups } = + useChildRecordFiltersAndRecordFilterGroups({ + recordFilterGroupId: rootRecordFilterGroup?.id, + }); + + const advancedFilterQuerySubFilterCount = + childRecordFiltersAndRecordFilterGroups.length; const { t } = useLingui(); @@ -134,12 +147,14 @@ export const ViewBarFilterDropdownAdvancedFilterButton = () => { onClick={handleClick} LeftIcon={IconFilter} focused={isSelectedItemId} + RightComponent={ + advancedFilterQuerySubFilterCount > 0 ? ( + + + + ) : undefined + } /> - {advancedFilterQuerySubFilterCount > 0 && ( - - - - )} ); }; diff --git a/packages/twenty-front/src/modules/views/components/__stories__/ViewBarFilterDropdown.stories.tsx b/packages/twenty-front/src/modules/views/components/__stories__/ViewBarFilterDropdown.stories.tsx index d4181ac703..c0a954efb5 100644 --- a/packages/twenty-front/src/modules/views/components/__stories__/ViewBarFilterDropdown.stories.tsx +++ b/packages/twenty-front/src/modules/views/components/__stories__/ViewBarFilterDropdown.stories.tsx @@ -9,16 +9,22 @@ import { RecordTableComponentInstanceContext } from '@/object-record/record-tabl import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState'; import { ViewBarFilterDropdown } from '@/views/components/ViewBarFilterDropdown'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; -import { CoreObjectNameSingular } from 'twenty-shared/types'; +import { + CoreObjectNameSingular, + RecordFilterGroupLogicalOperator, + ViewFilterOperand, +} from 'twenty-shared/types'; import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper'; import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; import { type RecordField } from '@/object-record/record-field/types/RecordField'; +import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; +import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; import { useRecordIndexFieldMetadataDerivedStates } from '@/object-record/record-index/hooks/useRecordIndexFieldMetadataDerivedStates'; import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore'; import { ViewBarFilterDropdownIds } from '@/views/constants/ViewBarFilterDropdownIds'; -import { userEvent, within } from 'storybook/test'; +import { expect, userEvent, within } from 'storybook/test'; import { ComponentDecorator } from 'twenty-ui/testing'; import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator'; import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator'; @@ -191,3 +197,96 @@ export const Number: Story = { await userEvent.click(dateFilter); }, }; + +const MOCK_ROOT_FILTER_GROUP_ID = 'test-root-filter-group-id'; + +export const AdvancedFilterCountBadge: Story = { + decorators: [ + (Story) => { + const companyObjectMetadataItem = + getTestEnrichedObjectMetadataItemsMock().find( + (item) => item.nameSingular === CoreObjectNameSingular.Company, + )!; + const instanceId = companyObjectMetadataItem.id; + + const setCurrentRecordFilterGroups = useSetAtomComponentState( + currentRecordFilterGroupsComponentState, + instanceId, + ); + + const setCurrentRecordFilters = useSetAtomComponentState( + currentRecordFiltersComponentState, + instanceId, + ); + + const firstFieldMetadataItem = companyObjectMetadataItem.fields[0]; + + useEffect(() => { + setCurrentRecordFilterGroups([ + { + id: MOCK_ROOT_FILTER_GROUP_ID, + logicalOperator: RecordFilterGroupLogicalOperator.AND, + positionInRecordFilterGroup: 0, + }, + ]); + + setCurrentRecordFilters([ + { + id: 'filter-1', + fieldMetadataId: firstFieldMetadataItem.id, + value: 'test-value-1', + displayValue: 'Test 1', + type: 'TEXT', + operand: ViewFilterOperand.CONTAINS, + label: firstFieldMetadataItem.label, + recordFilterGroupId: MOCK_ROOT_FILTER_GROUP_ID, + positionInRecordFilterGroup: 0, + }, + { + id: 'filter-2', + fieldMetadataId: firstFieldMetadataItem.id, + value: 'test-value-2', + displayValue: 'Test 2', + type: 'TEXT', + operand: ViewFilterOperand.CONTAINS, + label: firstFieldMetadataItem.label, + recordFilterGroupId: MOCK_ROOT_FILTER_GROUP_ID, + positionInRecordFilterGroup: 1, + }, + { + id: 'filter-3', + fieldMetadataId: firstFieldMetadataItem.id, + value: 'test-value-3', + displayValue: 'Test 3', + type: 'TEXT', + operand: ViewFilterOperand.CONTAINS, + label: firstFieldMetadataItem.label, + recordFilterGroupId: MOCK_ROOT_FILTER_GROUP_ID, + positionInRecordFilterGroup: 2, + }, + ]); + }, [ + setCurrentRecordFilterGroups, + setCurrentRecordFilters, + firstFieldMetadataItem, + ]); + + return ; + }, + ], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement.ownerDocument.body); + + const filterButton = await canvas.findByText('Filter'); + + await userEvent.click(filterButton); + + const advancedFilterButton = await canvas.findByText('Advanced filter'); + + expect(advancedFilterButton).toBeVisible(); + + const pillBadge = await canvas.findByText('3'); + + expect(pillBadge).toBeVisible(); + }, +};