Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Advanced filter #7700

Merged
merged 101 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from 86 commits
Commits
Show all changes
101 commits
Select commit Hold shift + click to select a range
e17bc6b
Add react-awesome-query-builder
ad-elias Sep 29, 2024
96a763d
Draft advanced filter files
ad-elias Sep 29, 2024
7116b23
Remove react-awesome-query-builder and refactor turnObjectDropdownFil…
ad-elias Oct 1, 2024
29d7f32
Add advanced filter button
ad-elias Oct 2, 2024
e06f1ab
Merge branch 'main' into feat/advanced-filter
ad-elias Oct 2, 2024
3271040
Finish merge
ad-elias Oct 2, 2024
78cb00a
Fix warnings
ad-elias Oct 2, 2024
c63d78d
Add todo
ad-elias Oct 4, 2024
214d882
Replace advanced filter type with view filter group entity
ad-elias Oct 5, 2024
c354da7
Merge branch 'main' into feat/advanced-filter
ad-elias Oct 5, 2024
1f1e6c8
Merge branch 'main' into feat/advanced-filter
ad-elias Oct 5, 2024
1a0cf4e
Expand view bar on advanced filter draft
ad-elias Oct 5, 2024
2c31acc
Show advanced filter dropdown button when drafting and add view filte…
ad-elias Oct 6, 2024
4641110
Draft advanced filter dropdown components
ad-elias Oct 6, 2024
bf77d99
Create view filter group in frontend when advanced filter editor is o…
ad-elias Oct 9, 2024
54ea309
Merge branch 'main' into feat/advanced-filter
ad-elias Oct 9, 2024
d092c80
Merge branch 'main' into feat/advanced-filter
ad-elias Oct 9, 2024
a5b457d
Add empty advanced filter rules
ad-elias Oct 9, 2024
1e2f540
Add nested view filter groups in frontend
ad-elias Oct 10, 2024
1f5cbe5
Save advanced filter rule position
ad-elias Oct 10, 2024
5b1c7d8
Advanced filter field, operand and value selection WiP
ad-elias Oct 11, 2024
a88a66d
Improve advanced filter dropdown component styling
ad-elias Oct 12, 2024
f4d2cb4
Extract add filter buttons to a component
ad-elias Oct 13, 2024
ad9c2cf
Merge branch 'main' into feat/advanced-filter
ad-elias Oct 13, 2024
1a0bc6a
Add filter or group dropdown
ad-elias Oct 13, 2024
709f7fa
Fix merge
ad-elias Oct 13, 2024
3b011dc
Support removing advanced filter
ad-elias Oct 13, 2024
596a728
Advanced filter rule field selection with a callback
ad-elias Oct 14, 2024
2f1a697
Add object filter dropdown scope provider
ad-elias Oct 14, 2024
db2ffd4
Show active advanced rule count
ad-elias Oct 14, 2024
7944b0f
Fix filter dropdown scope provider
ad-elias Oct 14, 2024
8da7ba1
Merge branch 'main' into feat/advanced-filter
ad-elias Oct 14, 2024
a42767a
Refactor turnObjectDropdownFilterIntoQueryFilter
ad-elias Oct 14, 2024
3342f85
Generate advanced GraphQL filter
ad-elias Oct 14, 2024
9daa5e0
Save filter groups to a view on view creation
ad-elias Oct 14, 2024
517efaf
Temporary change to refresh view on advanced filter value change
ad-elias Oct 14, 2024
392a991
Fix advanced filter persisting
ad-elias Oct 15, 2024
60fddfb
Close dropdown on select
ad-elias Oct 15, 2024
9ae9e33
Merge branch 'main' into feat/advanced-filter
ad-elias Oct 15, 2024
9747d5e
Set default operand on field change, improve typing
ad-elias Oct 15, 2024
18f5c9f
Fix selecting a hidden field
ad-elias Oct 16, 2024
301f060
Initialize value dropdown state
ad-elias Oct 16, 2024
c581bb0
Fix composite field selection
ad-elias Oct 16, 2024
893c6c3
Merge branch 'main' into feat/advanced-filter
ad-elias Oct 17, 2024
799a973
Support multiple advanced filter rules for same field
ad-elias Oct 17, 2024
6c53db1
Minimum one advanced filter in group
ad-elias Oct 17, 2024
e63f02c
Advanced filter layout styling fixes
ad-elias Oct 17, 2024
461d871
Merge branch 'main' into feat/advanced-filter
ad-elias Oct 18, 2024
4db9240
Fix operand and logical operator dropdowns
ad-elias Oct 18, 2024
109a283
Hide add advanced filter button
ad-elias Oct 18, 2024
cbb4b6b
Fix combining regular and advanced filters
ad-elias Oct 18, 2024
8c68ea7
Fix naming, add missing parameters to computeViewRecordGqlOperationFi…
ad-elias Oct 18, 2024
8e83080
Fix file name
ad-elias Oct 18, 2024
ffdca72
Set advanced view filter rule default value to label identifier.
ad-elias Oct 19, 2024
8cfcf86
Refactor
ad-elias Oct 19, 2024
105c9bb
Remove unused file
ad-elias Oct 19, 2024
36a019f
Refactor
ad-elias Oct 19, 2024
9f15d46
Refactor
ad-elias Oct 20, 2024
ca1bfe8
Replace props with useCurrentViewViewFilterGroup hook
ad-elias Oct 20, 2024
b25a3ca
Spread props
ad-elias Oct 20, 2024
d214753
Fix computeViewRecordGqlOperationFilter test
ad-elias Oct 20, 2024
77da6ae
Replace prop drilling with hooks
ad-elias Oct 20, 2024
fd5f7d4
Fix warnings
ad-elias Oct 20, 2024
a797f97
Clean up new position calculation
ad-elias Oct 20, 2024
d4feb15
Remove unused state
ad-elias Oct 20, 2024
a8fcadf
Readability improvements
ad-elias Oct 20, 2024
1c8d116
Extract constants to separate files
ad-elias Oct 20, 2024
4fca05f
Merge branch 'main' into feat/advanced-filter
ad-elias Oct 20, 2024
0ecb782
Rename file
ad-elias Oct 20, 2024
3c2caae
Fix import
ad-elias Oct 20, 2024
1426efa
Refactor: replace callback with useFilterDropdown
ad-elias Oct 20, 2024
6af8c1e
Fix advanced relative date filter
ad-elias Oct 20, 2024
1aeddd0
Fix persisting interdependent view filter groups. Fix recursive Graph…
ad-elias Oct 21, 2024
80db811
Extract filter replacement condition to a function
ad-elias Oct 21, 2024
e210406
Remove unused file
ad-elias Oct 21, 2024
69b0cf4
Fix rendering multiple view filter groups
ad-elias Oct 21, 2024
0c44d24
Add todo
ad-elias Oct 21, 2024
ca7d743
Delete group children
ad-elias Oct 21, 2024
8a82c13
Fix field selector showing on the first edit of a filter
ad-elias Oct 21, 2024
fe3ed15
Merge branch 'main' into feat/advanced-filter
ad-elias Oct 21, 2024
132fc1e
Fix logical operator cell position
ad-elias Oct 21, 2024
621ab4c
Fix merge
ad-elias Oct 21, 2024
1d2e088
Fix merge
ad-elias Oct 21, 2024
65682c7
Fix
lucasbordeau Oct 22, 2024
b2f590c
Fix
lucasbordeau Oct 22, 2024
215f9f5
Merge branch 'main' into feat/advanced-filter
lucasbordeau Oct 22, 2024
82a780a
Fix
lucasbordeau Oct 22, 2024
804a0b6
Fix
lucasbordeau Oct 22, 2024
75c4095
Fix
lucasbordeau Oct 22, 2024
5fd30d3
Fix
lucasbordeau Oct 22, 2024
a5c8d65
Merge branch 'main' into feat/advanced-filter
lucasbordeau Oct 22, 2024
5ae6b61
Fix lint
lucasbordeau Oct 23, 2024
5b343cb
Merge branch 'main' into feat/advanced-filter
lucasbordeau Oct 23, 2024
fd31ff3
Fix test
lucasbordeau Oct 23, 2024
47c522b
Fix test
lucasbordeau Oct 23, 2024
6e8d0e9
Merge branch 'main' into feat/advanced-filter
lucasbordeau Oct 24, 2024
4eb8ea3
Fix
lucasbordeau Oct 24, 2024
7d6df88
Fix bug
lucasbordeau Oct 24, 2024
381d35b
Merge branch 'main' into feat/advanced-filter
lucasbordeau Oct 24, 2024
c0dda92
Fix
lucasbordeau Oct 24, 2024
801dfb4
Fix coverage
lucasbordeau Oct 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleState';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter';
import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter';
import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables';

export const computeContextStoreFilters = (
Expand All @@ -12,9 +12,10 @@ export const computeContextStoreFilters = (

if (contextStoreTargetedRecordsRule.mode === 'exclusion') {
queryFilter = makeAndFilterVariables([
turnFiltersIntoQueryFilter(
computeViewRecordGqlOperationFilter(
contextStoreTargetedRecordsRule.filters,
objectMetadataItem?.fields ?? [],
[],
),
contextStoreTargetedRecordsRule.excludedRecordIds.length > 0
? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export enum CoreObjectNameSingular {
View = 'view',
ViewField = 'viewField',
ViewFilter = 'viewFilter',
ViewFilterGroup = 'viewFilterGroup',
FelixMalfait marked this conversation as resolved.
Show resolved Hide resolved
ViewSort = 'viewSort',
Webhook = 'webhook',
WorkspaceMember = 'workspaceMember',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { useUpsertCombinedViewFilterGroup } from '@/object-record/advanced-filter/hooks/useUpsertCombinedViewFilterGroup';
import { getOperandsForFilterDefinition } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType';
import { LightButton } from '@/ui/input/button/components/LightButton';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ADVANCED_FILTER_DROPDOWN_ID } from '@/views/constants/AdvancedFilterDropdownId';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { useUpsertCombinedViewFilters } from '@/views/hooks/useUpsertCombinedViewFilters';
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
import { ViewFilterGroup } from '@/views/types/ViewFilterGroup';
import { ViewFilterGroupLogicalOperator } from '@/views/types/ViewFilterGroupLogicalOperator';
import { useCallback } from 'react';
import { IconLibraryPlus, IconPlus, isDefined } from 'twenty-ui';
import { v4 } from 'uuid';

type AdvancedFilterAddFilterRuleSelectProps = {
viewFilterGroup: ViewFilterGroup;
lastChildPosition?: number;
};

export const AdvancedFilterAddFilterRuleSelect = ({
viewFilterGroup,
lastChildPosition = 0,
}: AdvancedFilterAddFilterRuleSelectProps) => {
const dropdownId = `advanced-filter-add-filter-rule-${viewFilterGroup.id}`;

const { currentViewId } = useGetCurrentView();

const { upsertCombinedViewFilterGroup } = useUpsertCombinedViewFilterGroup();
const { upsertCombinedViewFilter } = useUpsertCombinedViewFilters();

const newPositionInViewFilterGroup = lastChildPosition + 1;

const { closeDropdown } = useDropdown(dropdownId);

const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView();

const objectMetadataId =
currentViewWithCombinedFiltersAndSorts?.objectMetadataId;

if (!objectMetadataId) {
throw new Error('Object metadata id is missing from current view');
}

const { objectMetadataItem } = useObjectMetadataItemById({
objectId: objectMetadataId,
});

const availableFilterDefinitions = useRecoilComponentValueV2(
availableFilterDefinitionsComponentState,
);

const getDefaultFilterDefinition = useCallback(() => {
const defaultFilterDefinition =
availableFilterDefinitions.find(
(filterDefinition) =>
filterDefinition.fieldMetadataId ===
objectMetadataItem?.labelIdentifierFieldMetadataId,
) ?? availableFilterDefinitions?.[0];

if (!defaultFilterDefinition) {
throw new Error('Missing default filter definition');
}

return defaultFilterDefinition;
}, [availableFilterDefinitions, objectMetadataItem]);

const handleAddFilter = () => {
closeDropdown();

const defaultFilterDefinition = getDefaultFilterDefinition();

upsertCombinedViewFilter({
id: v4(),
fieldMetadataId: defaultFilterDefinition.fieldMetadataId,
operand: getOperandsForFilterDefinition(defaultFilterDefinition)[0],
definition: defaultFilterDefinition,
value: '',
displayValue: '',
viewFilterGroupId: viewFilterGroup.id,
positionInViewFilterGroup: newPositionInViewFilterGroup,
});
};

const handleAddFilterGroup = () => {
closeDropdown();

if (!currentViewId) {
throw new Error('Missing view id');
}

const newViewFilterGroup = {
id: v4(),
viewId: currentViewId,
logicalOperator: ViewFilterGroupLogicalOperator.AND,
parentViewFilterGroupId: viewFilterGroup.id,
positionInViewFilterGroup: newPositionInViewFilterGroup,
};

upsertCombinedViewFilterGroup(newViewFilterGroup);

const defaultFilterDefinition = getDefaultFilterDefinition();

upsertCombinedViewFilter({
id: v4(),
fieldMetadataId: defaultFilterDefinition.fieldMetadataId,
operand: getOperandsForFilterDefinition(defaultFilterDefinition)[0],
definition: defaultFilterDefinition,
value: '',
displayValue: '',
viewFilterGroupId: newViewFilterGroup.id,
positionInViewFilterGroup: newPositionInViewFilterGroup,
});
};

const isFilterRuleGroupOptionVisible = !isDefined(
viewFilterGroup.parentViewFilterGroupId,
);

if (!isFilterRuleGroupOptionVisible) {
return (
<LightButton
Icon={IconPlus}
title="Add filter rule"
onClick={handleAddFilter}
/>
);
}

return (
<Dropdown
disableBlur
dropdownId={dropdownId}
clickableComponent={
<LightButton Icon={IconPlus} title="Add filter rule" />
}
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem
LeftIcon={IconPlus}
text="Add rule"
onClick={handleAddFilter}
/>
{isFilterRuleGroupOptionVisible && (
<MenuItem
LeftIcon={IconLibraryPlus}
text="Add rule group"
onClick={handleAddFilterGroup}
/>
)}
</DropdownMenuItemsContainer>
}
dropdownHotkeyScope={{ scope: ADVANCED_FILTER_DROPDOWN_ID }}
dropdownOffset={{ y: 8, x: 0 }}
dropdownPlacement="bottom-start"
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { AdvancedFilterLogicalOperatorDropdown } from '@/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorDropdown';
import { ViewFilterGroup } from '@/views/types/ViewFilterGroup';
import styled from '@emotion/styled';
import { capitalize } from '~/utils/string/capitalize';

const StyledText = styled.div`
height: ${({ theme }) => theme.spacing(8)};
display: flex;
align-items: center;
`;

const StyledContainer = styled.div`
align-items: start;
display: flex;
min-width: ${({ theme }) => theme.spacing(20)};
color: ${({ theme }) => theme.font.color.tertiary};
`;

type AdvancedFilterLogicalOperatorCellProps = {
index: number;
viewFilterGroup: ViewFilterGroup;
};

export const AdvancedFilterLogicalOperatorCell = ({
index,
viewFilterGroup,
}: AdvancedFilterLogicalOperatorCellProps) => (
<StyledContainer>
{index === 0 ? (
<StyledText>Where</StyledText>
) : index === 1 ? (
<AdvancedFilterLogicalOperatorDropdown
viewFilterGroup={viewFilterGroup}
/>
) : (
<StyledText>
{capitalize(viewFilterGroup.logicalOperator.toLowerCase())}
</StyledText>
)}
Comment on lines +29 to +39
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider extracting this conditional rendering logic into a separate function for better readability

</StyledContainer>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ADVANCED_FILTER_LOGICAL_OPERATOR_OPTIONS } from '@/object-record/advanced-filter/constants/AdvancedFilterLogicalOperatorOptions';
import { useUpsertCombinedViewFilterGroup } from '@/object-record/advanced-filter/hooks/useUpsertCombinedViewFilterGroup';
import { Select } from '@/ui/input/components/Select';
import { ViewFilterGroup } from '@/views/types/ViewFilterGroup';
import { ViewFilterGroupLogicalOperator } from '@/views/types/ViewFilterGroupLogicalOperator';

type AdvancedFilterLogicalOperatorDropdownProps = {
viewFilterGroup: ViewFilterGroup;
};

export const AdvancedFilterLogicalOperatorDropdown = ({
viewFilterGroup,
}: AdvancedFilterLogicalOperatorDropdownProps) => {
const { upsertCombinedViewFilterGroup } = useUpsertCombinedViewFilterGroup();

const handleChange = (value: ViewFilterGroupLogicalOperator) => {
upsertCombinedViewFilterGroup({
...viewFilterGroup,
logicalOperator: value,
});
};

return (
<Select
ad-elias marked this conversation as resolved.
Show resolved Hide resolved
disableBlur
fullWidth
dropdownId={`advanced-filter-logical-operator-${viewFilterGroup.id}`}
value={viewFilterGroup.logicalOperator}
onChange={handleChange}
options={ADVANCED_FILTER_LOGICAL_OPERATOR_OPTIONS}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { AdvancedFilterAddFilterRuleSelect } from '@/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect';
import { AdvancedFilterLogicalOperatorCell } from '@/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorCell';
import { AdvancedFilterRuleOptionsDropdown } from '@/object-record/advanced-filter/components/AdvancedFilterRuleOptionsDropdown';
import { AdvancedFilterViewFilter } from '@/object-record/advanced-filter/components/AdvancedFilterViewFilter';
import { AdvancedFilterViewFilterGroup } from '@/object-record/advanced-filter/components/AdvancedFilterViewFilterGroup';
import { useCurrentViewViewFilterGroup } from '@/object-record/advanced-filter/hooks/useCurrentViewViewFilterGroup';
import styled from '@emotion/styled';
import { isDefined } from 'twenty-ui';

const StyledRow = styled.div`
display: flex;
width: 100%;
gap: ${({ theme }) => theme.spacing(2)};
`;

const StyledContainer = styled.div<{ isGrayBackground?: boolean }>`
align-items: start;
background-color: ${({ theme, isGrayBackground }) =>
isGrayBackground ? theme.background.transparent.lighter : 'transparent'};
border: ${({ theme }) => `1px solid ${theme.border.color.medium}`};
border-radius: ${({ theme }) => theme.border.radius.md};
display: flex;
flex: 1;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(2)};
overflow: hidden;
`;

type AdvancedFilterViewFilterGroupProps = {
rootLevelViewFilterGroupId: string;
};

export const AdvancedFilterRootLevelViewFilterGroup = ({
rootLevelViewFilterGroupId,
}: AdvancedFilterViewFilterGroupProps) => {
const { currentViewFilterGroup: rootLevelViewFilterGroup, childViewFiltersAndViewFilterGroups, lastChildPosition } =
useCurrentViewViewFilterGroup({
viewFilterGroupId: rootLevelViewFilterGroupId,
});

if (!isDefined(rootLevelViewFilterGroup)) {
return null;
}

return (
<StyledContainer>
{childViewFiltersAndViewFilterGroups.map((child, i) =>
child.__typename === 'ViewFilterGroup' ? (
<StyledRow key={child.id}>
<AdvancedFilterLogicalOperatorCell
index={i}
viewFilterGroup={rootLevelViewFilterGroup}
/>
<AdvancedFilterViewFilterGroup viewFilterGroupId={child.id} />
<AdvancedFilterRuleOptionsDropdown viewFilterGroupId={child.id} />
</StyledRow>
) : (
<StyledRow key={child.id}>
<AdvancedFilterLogicalOperatorCell
index={i}
viewFilterGroup={rootLevelViewFilterGroup}
/>
<AdvancedFilterViewFilter viewFilterId={child.id} />
<AdvancedFilterRuleOptionsDropdown viewFilterId={child.id} />
</StyledRow>
),
)}
<AdvancedFilterAddFilterRuleSelect
viewFilterGroup={rootLevelViewFilterGroup}
lastChildPosition={lastChildPosition}
/>
</StyledContainer>
);
};
Loading
Loading