diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterGroupOptionsDropdown.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterGroupOptionsDropdown.tsx index 337f4d3ca..36b15a8c7 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterGroupOptionsDropdown.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterGroupOptionsDropdown.tsx @@ -1,5 +1,6 @@ import { useChildRecordFiltersAndRecordFilterGroups } from '@/object-record/advanced-filter/hooks/useChildRecordFiltersAndRecordFilterGroups'; import { useRemoveRecordFilterGroup } from '@/object-record/record-filter-group/hooks/useRemoveRecordFilterGroup'; +import { useRemoveRootRecordFilterGroupIfEmpty } from '@/object-record/record-filter-group/hooks/useRemoveRootRecordFilterGroupIfEmpty'; import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; @@ -20,6 +21,8 @@ export const AdvancedFilterRecordFilterGroupOptionsDropdown = ({ const { removeRecordFilter } = useRemoveRecordFilter(); const { removeRecordFilterGroup } = useRemoveRecordFilterGroup(); + const { removeRootRecordFilterGroupIfEmpty } = + useRemoveRootRecordFilterGroupIfEmpty(); const { childRecordFilters } = useChildRecordFiltersAndRecordFilterGroups({ recordFilterGroupId, @@ -32,6 +35,8 @@ export const AdvancedFilterRecordFilterGroupOptionsDropdown = ({ removeRecordFilterGroup(recordFilterGroupId); + removeRootRecordFilterGroupIfEmpty(); + closeDropdown(); }; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterOptionsDropdown.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterOptionsDropdown.tsx index bbec255b8..390015f69 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterOptionsDropdown.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRecordFilterOptionsDropdown.tsx @@ -1,5 +1,6 @@ import { useChildRecordFiltersAndRecordFilterGroups } from '@/object-record/advanced-filter/hooks/useChildRecordFiltersAndRecordFilterGroups'; import { useRemoveRecordFilterGroup } from '@/object-record/record-filter-group/hooks/useRemoveRecordFilterGroup'; +import { useRemoveRootRecordFilterGroupIfEmpty } from '@/object-record/record-filter-group/hooks/useRemoveRootRecordFilterGroupIfEmpty'; import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; @@ -38,6 +39,9 @@ export const AdvancedFilterRecordFilterOptionsDropdown = ({ recordFilterGroupId: currentRecordFilter?.recordFilterGroupId, }); + const { removeRootRecordFilterGroupIfEmpty } = + useRemoveRootRecordFilterGroupIfEmpty(); + const handleRemove = async () => { closeDropdown(); @@ -51,6 +55,8 @@ export const AdvancedFilterRecordFilterOptionsDropdown = ({ } removeRecordFilter({ recordFilterId: recordFilterId }); + + removeRootRecordFilterGroupIfEmpty(); }; return ( diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRootLevelViewFilterGroup.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRootLevelViewFilterGroup.tsx index a7f6fe0c9..8c694ea7c 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRootLevelViewFilterGroup.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRootLevelViewFilterGroup.tsx @@ -5,8 +5,8 @@ import { AdvancedFilterRecordFilterGroupChildOptionsDropdown } from '@/object-re import { AdvancedFilterRecordFilterGroup } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterGroup'; import { AdvancedFilterViewFilter } from '@/object-record/advanced-filter/components/AdvancedFilterViewFilter'; import { useChildRecordFiltersAndRecordFilterGroups } from '@/object-record/advanced-filter/hooks/useChildRecordFiltersAndRecordFilterGroups'; +import { rootLevelRecordFilterGroupComponentSelector } from '@/object-record/advanced-filter/states/rootLevelRecordFilterGroupComponentSelector'; import { isRecordFilterGroupChildARecordFilterGroup } from '@/object-record/advanced-filter/utils/isRecordFilterGroupChildARecordFilterGroup'; -import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import styled from '@emotion/styled'; import { isDefined } from 'twenty-shared'; @@ -32,20 +32,14 @@ const StyledContainer = styled.div<{ isGrayBackground?: boolean }>` `; export const AdvancedFilterRootLevelViewFilterGroup = () => { - const currentRecordFilterGroups = useRecoilComponentValueV2( - currentRecordFilterGroupsComponentState, + const rootLevelRecordFilterGroup = useRecoilComponentValueV2( + rootLevelRecordFilterGroupComponentSelector, ); - const rootRecordFilterGroupId = currentRecordFilterGroups.find( - (recordFilterGroup) => !recordFilterGroup.parentRecordFilterGroupId, - )?.id; - - const { - currentRecordFilterGroup: rootLevelRecordFilterGroup, - childRecordFiltersAndRecordFilterGroups, - } = useChildRecordFiltersAndRecordFilterGroups({ - recordFilterGroupId: rootRecordFilterGroupId, - }); + const { childRecordFiltersAndRecordFilterGroups } = + useChildRecordFiltersAndRecordFilterGroups({ + recordFilterGroupId: rootLevelRecordFilterGroup?.id, + }); if (!isDefined(rootLevelRecordFilterGroup)) { return null; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useChildRecordFiltersAndRecordFilterGroups.ts b/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useChildRecordFiltersAndRecordFilterGroups.ts index 13552ab83..0bdc1257b 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useChildRecordFiltersAndRecordFilterGroups.ts +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/hooks/useChildRecordFiltersAndRecordFilterGroups.ts @@ -1,5 +1,7 @@ import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; +import { RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; +import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { isDefined } from 'twenty-shared'; @@ -23,9 +25,11 @@ export const useChildRecordFiltersAndRecordFilterGroups = ({ if (!isDefined(currentRecordFilterGroup)) { return { currentRecordFilterGroup: undefined, - childRecordFiltersAndRecordFilterGroups: [], - childRecordFilters: [], - childRecordFilterGroups: [], + childRecordFiltersAndRecordFilterGroups: [] as Array< + RecordFilter | RecordFilterGroup + >, + childRecordFilters: [] as RecordFilter[], + childRecordFilterGroups: [] as RecordFilterGroup[], lastChildPosition: 0, }; } diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/states/rootLevelRecordFilterGroupComponentSelector.ts b/packages/twenty-front/src/modules/object-record/advanced-filter/states/rootLevelRecordFilterGroupComponentSelector.ts new file mode 100644 index 000000000..2a86b65ab --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/states/rootLevelRecordFilterGroupComponentSelector.ts @@ -0,0 +1,24 @@ +import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext'; +import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; +import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; +import { isDefined } from 'twenty-shared'; + +export const rootLevelRecordFilterGroupComponentSelector = + createComponentSelectorV2({ + key: 'rootLevelRecordFilterGroupComponentSelector', + get: + ({ instanceId }) => + ({ get }) => { + const currentRecordFilterGroups = get( + currentRecordFilterGroupsComponentState.atomFamily({ instanceId }), + ); + + const rootLevelRecordFilterGroup = currentRecordFilterGroups.find( + (recordFilterGroup) => + !isDefined(recordFilterGroup.parentRecordFilterGroupId), + ); + + return rootLevelRecordFilterGroup; + }, + componentInstanceContext: RecordFilterGroupsComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandDropdown.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandDropdown.tsx index d422773ab..681d4566c 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandDropdown.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandDropdown.tsx @@ -35,7 +35,7 @@ export const ObjectFilterDropdownOperandDropdown = ({ clickableComponent={ } > {getOperandLabel(selectedOperandInDropdown)} diff --git a/packages/twenty-front/src/modules/object-record/record-filter-group/hooks/useRemoveRootRecordFilterGroupIfEmpty.ts b/packages/twenty-front/src/modules/object-record/record-filter-group/hooks/useRemoveRootRecordFilterGroupIfEmpty.ts new file mode 100644 index 000000000..d7fed5bc4 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter-group/hooks/useRemoveRootRecordFilterGroupIfEmpty.ts @@ -0,0 +1,71 @@ +import { useRemoveRecordFilterGroup } from '@/object-record/record-filter-group/hooks/useRemoveRecordFilterGroup'; +import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; +import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-shared'; + +export const useRemoveRootRecordFilterGroupIfEmpty = () => { + const currentRecordFilterGroupsCallbackState = + useRecoilComponentCallbackStateV2(currentRecordFilterGroupsComponentState); + + const currentRecordFiltersCallbackState = useRecoilComponentCallbackStateV2( + currentRecordFiltersComponentState, + ); + + const { removeRecordFilterGroup } = useRemoveRecordFilterGroup(); + + const removeRootRecordFilterGroupIfEmpty = useRecoilCallback( + ({ snapshot }) => + () => { + const currentRecordFilterGroups = getSnapshotValue( + snapshot, + currentRecordFilterGroupsCallbackState, + ); + + const currentRecordFilters = getSnapshotValue( + snapshot, + currentRecordFiltersCallbackState, + ); + + const rootRecordFilterGroup = currentRecordFilterGroups.find( + (existingRecordFilterGroup) => + !isDefined(existingRecordFilterGroup.parentRecordFilterGroupId), + ); + + if (isDefined(rootRecordFilterGroup)) { + const recordFilterGroupsInRootRecordFilterGroup = + currentRecordFilterGroups.filter( + (recordFilterGroupToFilter) => + recordFilterGroupToFilter.parentRecordFilterGroupId === + rootRecordFilterGroup.id, + ); + + const recordFiltersInRootRecordFilterGroup = + currentRecordFilters.filter( + (recordFilterToFilter) => + recordFilterToFilter.recordFilterGroupId === + rootRecordFilterGroup.id, + ); + + const rootRecordFilterGroupIsEmpty = + recordFilterGroupsInRootRecordFilterGroup.length === 0 && + recordFiltersInRootRecordFilterGroup.length === 0; + + if (rootRecordFilterGroupIsEmpty) { + removeRecordFilterGroup(rootRecordFilterGroup.id); + } + } + }, + [ + removeRecordFilterGroup, + currentRecordFilterGroupsCallbackState, + currentRecordFiltersCallbackState, + ], + ); + + return { + removeRootRecordFilterGroupIfEmpty, + }; +}; diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader.tsx index 760f5e10d..dc39b99ee 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader.tsx @@ -1,6 +1,5 @@ import styled from '@emotion/styled'; import { ComponentProps, MouseEvent } from 'react'; -import { IconComponent } from 'twenty-ui'; const StyledHeader = styled.li` align-items: center; @@ -44,13 +43,13 @@ const StyledEndComponent = styled.div` `; type DropdownMenuHeaderProps = ComponentProps<'li'> & { - EndIcon?: IconComponent; onClick?: (event: MouseEvent) => void; testId?: string; className?: string; StartComponent?: React.ReactNode; EndComponent?: React.ReactNode; }; + export const DropdownMenuHeader = ({ children, StartComponent, diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/__stories__/DropdownMenuHeader.stories.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/__stories__/DropdownMenuHeader.stories.tsx index 3ed73e189..012ebc9be 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/__stories__/DropdownMenuHeader.stories.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/__stories__/DropdownMenuHeader.stories.tsx @@ -1,7 +1,7 @@ import { Meta, StoryObj } from '@storybook/react'; import { - Avatar, AVATAR_URL_MOCK, + Avatar, ComponentDecorator, IconChevronLeft, IconChevronRight, @@ -9,11 +9,11 @@ import { MenuItem, } from 'twenty-ui'; -import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; import { SelectHotkeyScope } from '@/ui/input/types/SelectHotkeyScope'; -import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; +import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; const meta: Meta = { title: 'UI/Layout/Dropdown/DropdownMenuHeader', @@ -40,7 +40,7 @@ export const StartIcon: Story = { export const StartAndEndIcon: Story = { args: { StartComponent: , - EndIcon: IconChevronRight, + EndComponent: , children: 'Start and End Icon', }, }; diff --git a/packages/twenty-front/src/modules/views/components/AdvancedFilterChip.tsx b/packages/twenty-front/src/modules/views/components/AdvancedFilterChip.tsx index 915ac7c99..9e0cb5dc9 100644 --- a/packages/twenty-front/src/modules/views/components/AdvancedFilterChip.tsx +++ b/packages/twenty-front/src/modules/views/components/AdvancedFilterChip.tsx @@ -1,17 +1,20 @@ import { IconFilterCog } from 'twenty-ui'; import { useRemoveRecordFilterGroup } from '@/object-record/record-filter-group/hooks/useRemoveRecordFilterGroup'; +import { useRemoveRootRecordFilterGroupIfEmpty } from '@/object-record/record-filter-group/hooks/useRemoveRootRecordFilterGroupIfEmpty'; import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; +import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { SortOrFilterChip } from '@/views/components/SortOrFilterChip'; import { ADVANCED_FILTER_DROPDOWN_ID } from '@/views/constants/AdvancedFilterDropdownId'; import { plural } from 'pluralize'; import { isDefined } from 'twenty-shared'; -import { isNonEmptyArray } from '~/utils/isNonEmptyArray'; export const AdvancedFilterChip = () => { + const { closeDropdown } = useDropdown(ADVANCED_FILTER_DROPDOWN_ID); + const currentRecordFilterGroups = useRecoilComponentValueV2( currentRecordFilterGroupsComponentState, ); @@ -27,10 +30,11 @@ export const AdvancedFilterChip = () => { const { removeRecordFilter } = useRemoveRecordFilter(); const { removeRecordFilterGroup } = useRemoveRecordFilterGroup(); + const { removeRootRecordFilterGroupIfEmpty } = + useRemoveRootRecordFilterGroupIfEmpty(); + const handleRemoveClick = () => { - if (!isNonEmptyArray(advancedRecordFilterIds)) { - throw new Error('No advanced view filters to remove'); - } + closeDropdown(); const viewFilterGroupIds = currentRecordFilterGroups.map( (recordFilterGroup) => recordFilterGroup.id, @@ -43,6 +47,8 @@ export const AdvancedFilterChip = () => { for (const recordFilterId of advancedRecordFilterIds) { removeRecordFilter({ recordFilterId }); } + + removeRootRecordFilterGroupIfEmpty(); }; const advancedFilterCount = advancedRecordFilterIds.length; diff --git a/packages/twenty-front/src/modules/views/components/AdvancedFilterDropdownButton.tsx b/packages/twenty-front/src/modules/views/components/AdvancedFilterDropdownButton.tsx index 59729a31a..8ecb837cc 100644 --- a/packages/twenty-front/src/modules/views/components/AdvancedFilterDropdownButton.tsx +++ b/packages/twenty-front/src/modules/views/components/AdvancedFilterDropdownButton.tsx @@ -1,21 +1,18 @@ import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { AdvancedFilterRootLevelViewFilterGroup } from '@/object-record/advanced-filter/components/AdvancedFilterRootLevelViewFilterGroup'; -import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; +import { rootLevelRecordFilterGroupComponentSelector } from '@/object-record/advanced-filter/states/rootLevelRecordFilterGroupComponentSelector'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { AdvancedFilterChip } from '@/views/components/AdvancedFilterChip'; import { ADVANCED_FILTER_DROPDOWN_ID } from '@/views/constants/AdvancedFilterDropdownId'; +import { isDefined } from 'twenty-shared'; export const AdvancedFilterDropdownButton = () => { - const currentRecordFilterGroups = useRecoilComponentValueV2( - currentRecordFilterGroupsComponentState, + const rootLevelRecordFilterGroup = useRecoilComponentValueV2( + rootLevelRecordFilterGroupComponentSelector, ); - const outermostRecordFilterGroupId = currentRecordFilterGroups.find( - (recordFilterGroup) => !recordFilterGroup.parentRecordFilterGroupId, - )?.id; - - if (!outermostRecordFilterGroupId) { + if (!isDefined(rootLevelRecordFilterGroup)) { return null; }