Fix advanced filter creation of group rule (#10690)

This PR improves advanced filter code and fixes the bug that prevented
the creation of a filter group.

On the debugging side : 
- Adding an advanced filter rule to create a group now works

On the refactoring side : 
- We now use AdvancedFilterRecordFilterGroupChildOptionsDropdown to
clarify the code that show the option dropdown of a group.
- Refacatored useCurrentViewViewFilterGroup to
useChildRecordFiltersAndRecordFilterGroups. It is now using only
RecordFilter and RecordFilterGroup type instead of view types. It also
exports recordFilters and recordFilterGroups alone, when they are
children of a group, so we don't have to extract them from the merged
array that is typed RecordFilter | RecordFilterGroup, which is necessary
for displaying a group.
- Two typeguards have been introduced to help discern RecordFilter from
RecordFilterGroup : isRecordFilterGroupChildARecordFilterGroup and
isRecordFilterGroupChildARecordFilter, this allows to remove any typing
on child processing.
- Renaming from view to record (but there are still some left)
This commit is contained in:
Lucas Bordeau
2025-03-06 17:57:23 +01:00
committed by GitHub
parent cb5f4820d7
commit 777c12dd06
13 changed files with 293 additions and 223 deletions

View File

@ -1,6 +1,5 @@
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { availableFieldMetadataItemsForFilterFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector';
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { useDefaultFieldMetadataItemForFilter } from '@/object-record/advanced-filter/hooks/useDefaultFieldMetadataItemForFilter';
import { getAdvancedFilterAddFilterRuleSelectDropdownId } from '@/object-record/advanced-filter/utils/getAdvancedFilterAddFilterRuleSelectDropdownId'; import { getAdvancedFilterAddFilterRuleSelectDropdownId } from '@/object-record/advanced-filter/utils/getAdvancedFilterAddFilterRuleSelectDropdownId';
import { useUpsertRecordFilterGroup } from '@/object-record/record-filter-group/hooks/useUpsertRecordFilterGroup'; import { useUpsertRecordFilterGroup } from '@/object-record/record-filter-group/hooks/useUpsertRecordFilterGroup';
import { RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup'; import { RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup';
@ -12,8 +11,6 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { ADVANCED_FILTER_DROPDOWN_ID } from '@/views/constants/AdvancedFilterDropdownId'; import { ADVANCED_FILTER_DROPDOWN_ID } from '@/views/constants/AdvancedFilterDropdownId';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly'; import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { useCallback } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared'; import { isDefined } from 'twenty-shared';
import { IconLibraryPlus, IconPlus, LightButton, MenuItem } from 'twenty-ui'; import { IconLibraryPlus, IconPlus, LightButton, MenuItem } from 'twenty-ui';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
@ -41,55 +38,23 @@ export const AdvancedFilterAddFilterRuleSelect = ({
const { closeDropdown } = useDropdown(dropdownId); const { closeDropdown } = useDropdown(dropdownId);
const objectMetadataId = currentView?.objectMetadataId; const { defaultFieldMetadataItemForFilter } =
useDefaultFieldMetadataItemForFilter();
if (!isDefined(objectMetadataId)) {
throw new Error('Object metadata id is missing from current view');
}
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: objectMetadataId,
});
const availableFieldMetadataItemsForFilter = useRecoilValue(
availableFieldMetadataItemsForFilterFamilySelector({
objectMetadataItemId: objectMetadataId,
}),
);
const getDefaultFieldMetadataItem = useCallback(() => {
const defaultFieldMetadataItem =
availableFieldMetadataItemsForFilter.find(
(fieldMetadataItem) =>
fieldMetadataItem.id ===
objectMetadataItem?.labelIdentifierFieldMetadataId,
) ?? availableFieldMetadataItemsForFilter[0];
if (!isDefined(defaultFieldMetadataItem)) {
throw new Error(
`Could not find default field metadata item for object ${objectMetadataId}`,
);
}
return defaultFieldMetadataItem;
}, [
availableFieldMetadataItemsForFilter,
objectMetadataItem,
objectMetadataId,
]);
const handleAddFilter = () => { const handleAddFilter = () => {
if (!isDefined(defaultFieldMetadataItemForFilter)) {
throw new Error('Missing default field metadata item for filter');
}
closeDropdown(); closeDropdown();
const defaultFieldMetadataItem = getDefaultFieldMetadataItem();
const filterType = getFilterTypeFromFieldType( const filterType = getFilterTypeFromFieldType(
defaultFieldMetadataItem.type, defaultFieldMetadataItemForFilter.type,
); );
upsertRecordFilter({ upsertRecordFilter({
id: v4(), id: v4(),
fieldMetadataId: defaultFieldMetadataItem.id, fieldMetadataId: defaultFieldMetadataItemForFilter.id,
type: filterType, type: filterType,
operand: getRecordFilterOperands({ operand: getRecordFilterOperands({
filterType, filterType,
@ -98,13 +63,17 @@ export const AdvancedFilterAddFilterRuleSelect = ({
displayValue: '', displayValue: '',
recordFilterGroupId: recordFilterGroup.id, recordFilterGroupId: recordFilterGroup.id,
positionInRecordFilterGroup: newPositionInRecordFilterGroup, positionInRecordFilterGroup: newPositionInRecordFilterGroup,
label: defaultFieldMetadataItem.label, label: defaultFieldMetadataItemForFilter.label,
}); });
}; };
const handleAddFilterGroup = () => { const handleAddFilterGroup = () => {
closeDropdown(); closeDropdown();
if (!isDefined(defaultFieldMetadataItemForFilter)) {
throw new Error('Missing default field metadata item for filter');
}
if (!isDefined(currentView)) { if (!isDefined(currentView)) {
throw new Error('Missing view'); throw new Error('Missing view');
} }
@ -120,15 +89,13 @@ export const AdvancedFilterAddFilterRuleSelect = ({
upsertRecordFilterGroup(newRecordFilterGroup); upsertRecordFilterGroup(newRecordFilterGroup);
const defaultFieldMetadataItem = getDefaultFieldMetadataItem();
const filterType = getFilterTypeFromFieldType( const filterType = getFilterTypeFromFieldType(
defaultFieldMetadataItem.type, defaultFieldMetadataItemForFilter.type,
); );
upsertRecordFilter({ upsertRecordFilter({
id: v4(), id: v4(),
fieldMetadataId: defaultFieldMetadataItem.id, fieldMetadataId: defaultFieldMetadataItemForFilter.id,
type: filterType, type: filterType,
operand: getRecordFilterOperands({ operand: getRecordFilterOperands({
filterType, filterType,
@ -136,8 +103,8 @@ export const AdvancedFilterAddFilterRuleSelect = ({
value: '', value: '',
displayValue: '', displayValue: '',
recordFilterGroupId: newRecordFilterGroupId, recordFilterGroupId: newRecordFilterGroupId,
positionInRecordFilterGroup: newPositionInRecordFilterGroup, positionInRecordFilterGroup: 1,
label: defaultFieldMetadataItem.label, label: defaultFieldMetadataItemForFilter.label,
}); });
}; };

View File

@ -1,9 +1,11 @@
import { AdvancedFilterAddFilterRuleSelect } from '@/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect'; import { AdvancedFilterAddFilterRuleSelect } from '@/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect';
import { AdvancedFilterLogicalOperatorCell } from '@/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorCell'; import { AdvancedFilterLogicalOperatorCell } from '@/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorCell';
import { AdvancedFilterRuleOptionsDropdown } from '@/object-record/advanced-filter/components/AdvancedFilterRuleOptionsDropdown'; import { AdvancedFilterRecordFilterGroupChildOptionsDropdown } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterGroupChildOptionsDropdown';
import { AdvancedFilterViewFilter } from '@/object-record/advanced-filter/components/AdvancedFilterViewFilter'; import { AdvancedFilterViewFilter } from '@/object-record/advanced-filter/components/AdvancedFilterViewFilter';
import { useCurrentViewViewFilterGroup } from '@/object-record/advanced-filter/hooks/useCurrentViewViewFilterGroup'; import { useChildRecordFiltersAndRecordFilterGroups } from '@/object-record/advanced-filter/hooks/useChildRecordFiltersAndRecordFilterGroups';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { isDefined } from 'twenty-shared';
const StyledRow = styled.div` const StyledRow = styled.div`
display: flex; display: flex;
@ -25,41 +27,45 @@ const StyledContainer = styled.div<{ isGrayBackground?: boolean }>`
overflow: hidden; overflow: hidden;
`; `;
type AdvancedFilterViewFilterGroupProps = { type AdvancedFilterRecordFilterGroupProps = {
viewFilterGroupId: string; recordFilterGroupId: string;
}; };
export const AdvancedFilterViewFilterGroup = ({ export const AdvancedFilterRecordFilterGroup = ({
viewFilterGroupId, recordFilterGroupId,
}: AdvancedFilterViewFilterGroupProps) => { }: AdvancedFilterRecordFilterGroupProps) => {
const { const {
currentViewFilterGroup, currentRecordFilterGroup,
childViewFiltersAndViewFilterGroups, childRecordFiltersAndRecordFilterGroups,
lastChildPosition, lastChildPosition,
} = useCurrentViewViewFilterGroup({ } = useChildRecordFiltersAndRecordFilterGroups({
recordFilterGroupId: viewFilterGroupId, recordFilterGroupId,
}); });
if (!currentViewFilterGroup) { if (!currentRecordFilterGroup) {
return null; return null;
} }
const hasParentRecordFilterGroup = isDefined(
currentRecordFilterGroup.parentRecordFilterGroupId,
);
return ( return (
<StyledContainer <StyledContainer isGrayBackground={hasParentRecordFilterGroup}>
isGrayBackground={!!currentViewFilterGroup.parentRecordFilterGroupId} {childRecordFiltersAndRecordFilterGroups.map((child, i) => (
>
{childViewFiltersAndViewFilterGroups.map((child, i) => (
<StyledRow key={child.id}> <StyledRow key={child.id}>
<AdvancedFilterLogicalOperatorCell <AdvancedFilterLogicalOperatorCell
index={i} index={i}
recordFilterGroup={currentViewFilterGroup} recordFilterGroup={currentRecordFilterGroup}
/> />
<AdvancedFilterViewFilter viewFilterId={child.id} /> <AdvancedFilterViewFilter viewFilterId={child.id} />
<AdvancedFilterRuleOptionsDropdown viewFilterId={child.id} /> <AdvancedFilterRecordFilterGroupChildOptionsDropdown
recordFilterGroupChild={child}
/>
</StyledRow> </StyledRow>
))} ))}
<AdvancedFilterAddFilterRuleSelect <AdvancedFilterAddFilterRuleSelect
recordFilterGroup={currentViewFilterGroup} recordFilterGroup={currentRecordFilterGroup}
lastChildPosition={lastChildPosition} lastChildPosition={lastChildPosition}
/> />
</StyledContainer> </StyledContainer>

View File

@ -0,0 +1,27 @@
import { AdvancedFilterRecordFilterGroupOptionsDropdown } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterGroupOptionsDropdown';
import { AdvancedFilterRecordFilterOptionsDropdown } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterOptionsDropdown';
import { isRecordFilterGroupChildARecordFilter } from '@/object-record/advanced-filter/utils/isRecordFilterGroupChildARecordFilter';
import { RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
type AdvancedFilterRecordFilterGroupChildOptionsDropdownProps = {
recordFilterGroupChild: RecordFilter | RecordFilterGroup;
};
export const AdvancedFilterRecordFilterGroupChildOptionsDropdown = ({
recordFilterGroupChild,
}: AdvancedFilterRecordFilterGroupChildOptionsDropdownProps) => {
const isRecordFilter = isRecordFilterGroupChildARecordFilter(
recordFilterGroupChild,
);
return isRecordFilter ? (
<AdvancedFilterRecordFilterOptionsDropdown
recordFilterId={recordFilterGroupChild.id}
/>
) : (
<AdvancedFilterRecordFilterGroupOptionsDropdown
recordFilterGroupId={recordFilterGroupChild.id}
/>
);
};

View File

@ -0,0 +1,53 @@
import { useChildRecordFiltersAndRecordFilterGroups } from '@/object-record/advanced-filter/hooks/useChildRecordFiltersAndRecordFilterGroups';
import { useRemoveRecordFilterGroup } from '@/object-record/record-filter-group/hooks/useRemoveRecordFilterGroup';
import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { IconButton, IconDotsVertical, MenuItem } from 'twenty-ui';
type AdvancedFilterRecordFilterGroupOptionsDropdownProps = {
recordFilterGroupId: string;
};
export const AdvancedFilterRecordFilterGroupOptionsDropdown = ({
recordFilterGroupId,
}: AdvancedFilterRecordFilterGroupOptionsDropdownProps) => {
const dropdownId = `advanced-filter-record-filter-group-options-${recordFilterGroupId}`;
const { removeRecordFilter } = useRemoveRecordFilter();
const { removeRecordFilterGroup } = useRemoveRecordFilterGroup();
const { childRecordFilters } = useChildRecordFiltersAndRecordFilterGroups({
recordFilterGroupId,
});
const handleRemove = () => {
for (const childRecordFilter of childRecordFilters ?? []) {
removeRecordFilter({ recordFilterId: childRecordFilter.id });
}
removeRecordFilterGroup(recordFilterGroupId);
};
return (
<Dropdown
dropdownId={dropdownId}
clickableComponent={
<IconButton
aria-label="Filter group rule options"
variant="tertiary"
Icon={IconDotsVertical}
/>
}
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem text="Remove rule group" onClick={handleRemove} />
</DropdownMenuItemsContainer>
}
dropdownHotkeyScope={{ scope: dropdownId }}
dropdownOffset={{ y: 8, x: 0 }}
dropdownPlacement="bottom-start"
/>
);
};

View File

@ -0,0 +1,71 @@
import { useChildRecordFiltersAndRecordFilterGroups } from '@/object-record/advanced-filter/hooks/useChildRecordFiltersAndRecordFilterGroups';
import { useRemoveRecordFilterGroup } from '@/object-record/record-filter-group/hooks/useRemoveRecordFilterGroup';
import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isDefined } from 'twenty-shared';
import { IconButton, IconDotsVertical, MenuItem } from 'twenty-ui';
type AdvancedFilterRecordFilterOptionsDropdownProps = {
recordFilterId: string;
};
export const AdvancedFilterRecordFilterOptionsDropdown = ({
recordFilterId,
}: AdvancedFilterRecordFilterOptionsDropdownProps) => {
const dropdownId = `advanced-filter-record-filter-options-${recordFilterId}`;
const { removeRecordFilter } = useRemoveRecordFilter();
const { removeRecordFilterGroup } = useRemoveRecordFilterGroup();
const currentRecordFilters = useRecoilComponentValueV2(
currentRecordFiltersComponentState,
);
const currentRecordFilter = currentRecordFilters.find(
(recordFilter) => recordFilter.id === recordFilterId,
);
const { childRecordFiltersAndRecordFilterGroups } =
useChildRecordFiltersAndRecordFilterGroups({
recordFilterGroupId: currentRecordFilter?.recordFilterGroupId,
});
const handleRemove = async () => {
removeRecordFilter({ recordFilterId: recordFilterId });
if (isDefined(currentRecordFilter?.recordFilterGroupId)) {
const isOnlyViewFilterInGroup =
childRecordFiltersAndRecordFilterGroups?.length === 1;
if (isOnlyViewFilterInGroup) {
removeRecordFilterGroup(currentRecordFilter.recordFilterGroupId);
}
}
};
return (
<Dropdown
dropdownId={dropdownId}
clickableComponent={
<IconButton
aria-label="Record filter rule options"
variant="tertiary"
Icon={IconDotsVertical}
/>
}
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem text="Remove rule" onClick={handleRemove} />
</DropdownMenuItemsContainer>
}
dropdownHotkeyScope={{ scope: dropdownId }}
dropdownOffset={{ y: 8, x: 0 }}
dropdownPlacement="bottom-start"
/>
);
};

View File

@ -1,9 +1,11 @@
import { AdvancedFilterAddFilterRuleSelect } from '@/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect'; import { AdvancedFilterAddFilterRuleSelect } from '@/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect';
import { AdvancedFilterLogicalOperatorCell } from '@/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorCell'; import { AdvancedFilterLogicalOperatorCell } from '@/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorCell';
import { AdvancedFilterRuleOptionsDropdown } from '@/object-record/advanced-filter/components/AdvancedFilterRuleOptionsDropdown'; import { AdvancedFilterRecordFilterGroupChildOptionsDropdown } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterGroupChildOptionsDropdown';
import { AdvancedFilterRecordFilterGroup } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterGroup';
import { AdvancedFilterViewFilter } from '@/object-record/advanced-filter/components/AdvancedFilterViewFilter'; import { AdvancedFilterViewFilter } from '@/object-record/advanced-filter/components/AdvancedFilterViewFilter';
import { AdvancedFilterViewFilterGroup } from '@/object-record/advanced-filter/components/AdvancedFilterViewFilterGroup'; import { useChildRecordFiltersAndRecordFilterGroups } from '@/object-record/advanced-filter/hooks/useChildRecordFiltersAndRecordFilterGroups';
import { useCurrentViewViewFilterGroup } from '@/object-record/advanced-filter/hooks/useCurrentViewViewFilterGroup'; import { isRecordFilterGroupChildARecordFilterGroup } from '@/object-record/advanced-filter/utils/isRecordFilterGroupChildARecordFilterGroup';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { isDefined } from 'twenty-shared'; import { isDefined } from 'twenty-shared';
@ -28,49 +30,53 @@ const StyledContainer = styled.div<{ isGrayBackground?: boolean }>`
`; `;
type AdvancedFilterRootLevelViewFilterGroupProps = { type AdvancedFilterRootLevelViewFilterGroupProps = {
rootLevelViewFilterGroupId: string; rootLevelRecordFilterGroupId: string;
}; };
export const AdvancedFilterRootLevelViewFilterGroup = ({ export const AdvancedFilterRootLevelViewFilterGroup = ({
rootLevelViewFilterGroupId, rootLevelRecordFilterGroupId,
}: AdvancedFilterRootLevelViewFilterGroupProps) => { }: AdvancedFilterRootLevelViewFilterGroupProps) => {
const { const {
currentViewFilterGroup: rootLevelViewFilterGroup, currentRecordFilterGroup: rootLevelRecordFilterGroup,
childViewFiltersAndViewFilterGroups, childRecordFiltersAndRecordFilterGroups,
lastChildPosition, lastChildPosition,
} = useCurrentViewViewFilterGroup({ } = useChildRecordFiltersAndRecordFilterGroups({
recordFilterGroupId: rootLevelViewFilterGroupId, recordFilterGroupId: rootLevelRecordFilterGroupId,
}); });
if (!isDefined(rootLevelViewFilterGroup)) { if (!isDefined(rootLevelRecordFilterGroup)) {
return null; return null;
} }
return ( return (
<StyledContainer> <StyledContainer>
{childViewFiltersAndViewFilterGroups.map((child, i) => {childRecordFiltersAndRecordFilterGroups.map((child, i) =>
(child as any).__typename === 'ViewFilterGroup' ? ( isRecordFilterGroupChildARecordFilterGroup(child) ? (
<StyledRow key={child.id}> <StyledRow key={child.id}>
<AdvancedFilterLogicalOperatorCell <AdvancedFilterLogicalOperatorCell
index={i} index={i}
recordFilterGroup={rootLevelViewFilterGroup} recordFilterGroup={rootLevelRecordFilterGroup}
/>
<AdvancedFilterRecordFilterGroup recordFilterGroupId={child.id} />
<AdvancedFilterRecordFilterGroupChildOptionsDropdown
recordFilterGroupChild={child}
/> />
<AdvancedFilterViewFilterGroup viewFilterGroupId={child.id} />
<AdvancedFilterRuleOptionsDropdown viewFilterGroupId={child.id} />
</StyledRow> </StyledRow>
) : ( ) : (
<StyledRow key={child.id}> <StyledRow key={child.id}>
<AdvancedFilterLogicalOperatorCell <AdvancedFilterLogicalOperatorCell
index={i} index={i}
recordFilterGroup={rootLevelViewFilterGroup} recordFilterGroup={rootLevelRecordFilterGroup}
/> />
<AdvancedFilterViewFilter viewFilterId={child.id} /> <AdvancedFilterViewFilter viewFilterId={child.id} />
<AdvancedFilterRuleOptionsDropdown viewFilterId={child.id} /> <AdvancedFilterRecordFilterGroupChildOptionsDropdown
recordFilterGroupChild={child}
/>
</StyledRow> </StyledRow>
), ),
)} )}
<AdvancedFilterAddFilterRuleSelect <AdvancedFilterAddFilterRuleSelect
recordFilterGroup={rootLevelViewFilterGroup} recordFilterGroup={rootLevelRecordFilterGroup}
lastChildPosition={lastChildPosition} lastChildPosition={lastChildPosition}
/> />
</StyledContainer> </StyledContainer>

View File

@ -1,93 +0,0 @@
import { AdvancedFilterRuleOptionsDropdownButton } from '@/object-record/advanced-filter/components/AdvancedFilterRuleOptionsDropdownButton';
import { useCurrentViewViewFilterGroup } from '@/object-record/advanced-filter/hooks/useCurrentViewViewFilterGroup';
import { useRemoveRecordFilterGroup } from '@/object-record/record-filter-group/hooks/useRemoveRecordFilterGroup';
import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isDefined } from 'twenty-shared';
import { MenuItem } from 'twenty-ui';
type AdvancedFilterRuleOptionsDropdownProps =
| {
viewFilterId: string;
viewFilterGroupId?: never;
}
| {
viewFilterId?: never;
viewFilterGroupId: string;
};
export const AdvancedFilterRuleOptionsDropdown = ({
viewFilterId,
viewFilterGroupId,
}: AdvancedFilterRuleOptionsDropdownProps) => {
const dropdownId = `advanced-filter-rule-options-${viewFilterId ?? viewFilterGroupId}`;
const { removeRecordFilter } = useRemoveRecordFilter();
const { removeRecordFilterGroup } = useRemoveRecordFilterGroup();
const { currentViewFilterGroup, childViewFiltersAndViewFilterGroups } =
useCurrentViewViewFilterGroup({
recordFilterGroupId: viewFilterGroupId,
});
const currentRecordFilters = useRecoilComponentValueV2(
currentRecordFiltersComponentState,
);
const currentRecordFilter = currentRecordFilters.find(
(recordFilter) => recordFilter.id === viewFilterId,
);
const handleRemove = async () => {
if (isDefined(viewFilterId)) {
removeRecordFilter({ recordFilterId: viewFilterId });
const isOnlyViewFilterInGroup =
childViewFiltersAndViewFilterGroups.length === 1;
if (
isOnlyViewFilterInGroup &&
isDefined(currentRecordFilter?.recordFilterGroupId)
) {
removeRecordFilterGroup(currentRecordFilter.recordFilterGroupId);
}
} else if (isDefined(currentViewFilterGroup)) {
removeRecordFilterGroup(currentViewFilterGroup.id);
// TODO: This is a temporary fix view filter group will be removed soon.
const childViewFilters = childViewFiltersAndViewFilterGroups.filter(
(child) => (child as any).__typename === 'ViewFilter',
);
for (const childViewFilter of childViewFilters) {
removeRecordFilter({ recordFilterId: childViewFilter.id });
}
} else {
throw new Error('No view filter or view filter group to remove');
}
};
const removeButtonLabel = viewFilterId ? 'Remove rule' : 'Remove rule group';
return (
<Dropdown
dropdownId={dropdownId}
clickableComponent={
<AdvancedFilterRuleOptionsDropdownButton dropdownId={dropdownId} />
}
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem text={removeButtonLabel} onClick={handleRemove} />
</DropdownMenuItemsContainer>
}
dropdownHotkeyScope={{ scope: dropdownId }}
dropdownOffset={{ y: 8, x: 0 }}
dropdownPlacement="bottom-start"
/>
);
};

View File

@ -1,25 +0,0 @@
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { IconButton, IconDotsVertical } from 'twenty-ui';
type AdvancedFilterRuleOptionsDropdownButtonProps = {
dropdownId: string;
};
export const AdvancedFilterRuleOptionsDropdownButton = ({
dropdownId,
}: AdvancedFilterRuleOptionsDropdownButtonProps) => {
const { toggleDropdown } = useDropdown(dropdownId);
const handleClick = () => {
toggleDropdown();
};
return (
<IconButton
aria-label="Filter rule options"
variant="tertiary"
Icon={IconDotsVertical}
onClick={handleClick}
/>
);
};

View File

@ -1,11 +1,9 @@
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewFilter } from '@/views/types/ViewFilter';
import { ViewFilterGroup } from '@/views/types/ViewFilterGroup';
import { isDefined } from 'twenty-shared'; import { isDefined } from 'twenty-shared';
export const useCurrentViewViewFilterGroup = ({ export const useChildRecordFiltersAndRecordFilterGroups = ({
recordFilterGroupId, recordFilterGroupId,
}: { }: {
recordFilterGroupId?: string; recordFilterGroupId?: string;
@ -24,11 +22,11 @@ export const useCurrentViewViewFilterGroup = ({
if (!isDefined(currentRecordFilterGroup)) { if (!isDefined(currentRecordFilterGroup)) {
return { return {
currentViewFilterGroup: undefined, currentRecordFilterGroup: undefined,
childViewFiltersAndViewFilterGroups: [] as ( childRecordFiltersAndRecordFilterGroups: [],
| ViewFilter childRecordFilters: [],
| ViewFilterGroup childRecordFilterGroups: [],
)[], lastChildPosition: 0,
}; };
} }
@ -37,15 +35,15 @@ export const useCurrentViewViewFilterGroup = ({
recordFilterToFilter.recordFilterGroupId === currentRecordFilterGroup.id, recordFilterToFilter.recordFilterGroupId === currentRecordFilterGroup.id,
); );
const childViewFilterGroups = currentRecordFilterGroups.filter( const childRecordFilterGroups = currentRecordFilterGroups.filter(
(currentRecordGroupToFilter) => (currentRecordGroupToFilter) =>
currentRecordGroupToFilter.parentRecordFilterGroupId === currentRecordGroupToFilter.parentRecordFilterGroupId ===
currentRecordFilterGroup.id, currentRecordFilterGroup.id,
); );
const childViewFiltersAndViewFilterGroups = [ const childRecordFiltersAndRecordFilterGroups = [
...(childViewFilterGroups ?? []), ...childRecordFilterGroups,
...(childRecordFilters ?? []), ...childRecordFilters,
].sort((a, b) => { ].sort((a, b) => {
const positionA = a.positionInRecordFilterGroup ?? 0; const positionA = a.positionInRecordFilterGroup ?? 0;
const positionB = b.positionInRecordFilterGroup ?? 0; const positionB = b.positionInRecordFilterGroup ?? 0;
@ -53,13 +51,15 @@ export const useCurrentViewViewFilterGroup = ({
}); });
const lastChildPosition = const lastChildPosition =
childViewFiltersAndViewFilterGroups[ childRecordFiltersAndRecordFilterGroups[
childViewFiltersAndViewFilterGroups.length - 1 childRecordFiltersAndRecordFilterGroups.length - 1
]?.positionInRecordFilterGroup ?? 0; ]?.positionInRecordFilterGroup ?? 0;
return { return {
currentViewFilterGroup: currentRecordFilterGroup, currentRecordFilterGroup,
childViewFiltersAndViewFilterGroups, childRecordFiltersAndRecordFilterGroups,
childRecordFilters,
childRecordFilterGroups,
lastChildPosition, lastChildPosition,
}; };
}; };

View File

@ -0,0 +1,42 @@
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { availableFieldMetadataItemsForFilterFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared';
export const useDefaultFieldMetadataItemForFilter = () => {
const { currentView } = useGetCurrentViewOnly();
const objectMetadataId = currentView?.objectMetadataId;
if (!isDefined(objectMetadataId)) {
throw new Error('Object metadata id is missing from current view');
}
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: objectMetadataId,
});
const availableFieldMetadataItemsForFilter = useRecoilValue(
availableFieldMetadataItemsForFilterFamilySelector({
objectMetadataItemId: objectMetadataId,
}),
);
const fieldMetadataItemForLabelIdentifier =
availableFieldMetadataItemsForFilter.find(
(fieldMetadataItem) =>
fieldMetadataItem.id ===
objectMetadataItem?.labelIdentifierFieldMetadataId,
);
const firstFieldMetadataItem = availableFieldMetadataItemsForFilter?.[0] as
| FieldMetadataItem
| undefined;
const defaultFieldMetadataItemForFilter =
fieldMetadataItemForLabelIdentifier ?? firstFieldMetadataItem;
return { defaultFieldMetadataItemForFilter };
};

View File

@ -0,0 +1,8 @@
import { RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
export const isRecordFilterGroupChildARecordFilter = (
child: RecordFilter | RecordFilterGroup,
): child is RecordFilter => {
return ('fieldMetadataId' satisfies keyof RecordFilter) in child;
};

View File

@ -0,0 +1,8 @@
import { RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup';
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
export const isRecordFilterGroupChildARecordFilterGroup = (
child: RecordFilter | RecordFilterGroup,
): child is RecordFilterGroup => {
return ('logicalOperator' satisfies keyof RecordFilterGroup) in child;
};

View File

@ -72,7 +72,7 @@ export const AdvancedFilterDropdownButton = () => {
} }
dropdownComponents={ dropdownComponents={
<AdvancedFilterRootLevelViewFilterGroup <AdvancedFilterRootLevelViewFilterGroup
rootLevelViewFilterGroupId={outermostRecordFilterGroupId} rootLevelRecordFilterGroupId={outermostRecordFilterGroupId}
/> />
} }
dropdownHotkeyScope={{ scope: ADVANCED_FILTER_DROPDOWN_ID }} dropdownHotkeyScope={{ scope: ADVANCED_FILTER_DROPDOWN_ID }}