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:
@ -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,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -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 };
|
||||||
|
};
|
||||||
@ -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;
|
||||||
|
};
|
||||||
@ -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;
|
||||||
|
};
|
||||||
@ -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 }}
|
||||||
|
|||||||
Reference in New Issue
Block a user