Feat: Advanced filter (#7700)
Design:  Not ready to be merged yet! --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -0,0 +1,161 @@
|
||||
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 { 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, LightButton } 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"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
@ -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
|
||||
disableBlur
|
||||
fullWidth
|
||||
dropdownId={`advanced-filter-logical-operator-${viewFilterGroup.id}`}
|
||||
value={viewFilterGroup.logicalOperator}
|
||||
onChange={handleChange}
|
||||
options={ADVANCED_FILTER_LOGICAL_OPERATOR_OPTIONS}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,78 @@
|
||||
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 AdvancedFilterRootLevelViewFilterGroupProps = {
|
||||
rootLevelViewFilterGroupId: string;
|
||||
};
|
||||
|
||||
export const AdvancedFilterRootLevelViewFilterGroup = ({
|
||||
rootLevelViewFilterGroupId,
|
||||
}: AdvancedFilterRootLevelViewFilterGroupProps) => {
|
||||
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>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,87 @@
|
||||
import { AdvancedFilterRuleOptionsDropdownButton } from '@/object-record/advanced-filter/components/AdvancedFilterRuleOptionsDropdownButton';
|
||||
import { useCurrentViewFilter } from '@/object-record/advanced-filter/hooks/useCurrentViewFilter';
|
||||
import { useCurrentViewViewFilterGroup } from '@/object-record/advanced-filter/hooks/useCurrentViewViewFilterGroup';
|
||||
import { useDeleteCombinedViewFilterGroup } from '@/object-record/advanced-filter/hooks/useDeleteCombinedViewFilterGroup';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { ADVANCED_FILTER_DROPDOWN_ID } from '@/views/constants/AdvancedFilterDropdownId';
|
||||
import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters';
|
||||
import { isDefined } 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 { deleteCombinedViewFilter } = useDeleteCombinedViewFilters();
|
||||
const { deleteCombinedViewFilterGroup } = useDeleteCombinedViewFilterGroup();
|
||||
|
||||
const { currentViewFilterGroup, childViewFiltersAndViewFilterGroups } =
|
||||
useCurrentViewViewFilterGroup({
|
||||
viewFilterGroupId,
|
||||
});
|
||||
|
||||
const currentViewFilter = useCurrentViewFilter({
|
||||
viewFilterId,
|
||||
});
|
||||
|
||||
const handleRemove = async () => {
|
||||
if (isDefined(viewFilterId)) {
|
||||
deleteCombinedViewFilter(viewFilterId);
|
||||
|
||||
const isOnlyViewFilterInGroup =
|
||||
childViewFiltersAndViewFilterGroups.length === 1;
|
||||
|
||||
if (
|
||||
isOnlyViewFilterInGroup &&
|
||||
isDefined(currentViewFilter?.viewFilterGroupId)
|
||||
) {
|
||||
deleteCombinedViewFilterGroup(currentViewFilter.viewFilterGroupId);
|
||||
}
|
||||
} else if (isDefined(currentViewFilterGroup)) {
|
||||
deleteCombinedViewFilterGroup(currentViewFilterGroup.id);
|
||||
|
||||
const childViewFilters = childViewFiltersAndViewFilterGroups.filter(
|
||||
(child) => child.__typename === 'ViewFilter',
|
||||
);
|
||||
|
||||
for (const childViewFilter of childViewFilters) {
|
||||
await deleteCombinedViewFilter(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
|
||||
disableBlur
|
||||
dropdownId={dropdownId}
|
||||
clickableComponent={
|
||||
<AdvancedFilterRuleOptionsDropdownButton dropdownId={dropdownId} />
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem text={removeButtonLabel} onClick={handleRemove} />
|
||||
</DropdownMenuItemsContainer>
|
||||
}
|
||||
dropdownHotkeyScope={{ scope: ADVANCED_FILTER_DROPDOWN_ID }}
|
||||
dropdownOffset={{ y: 8, x: 0 }}
|
||||
dropdownPlacement="bottom-start"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,25 @@
|
||||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,47 @@
|
||||
import { AdvancedFilterViewFilterFieldSelect } from '@/object-record/advanced-filter/components/AdvancedFilterViewFilterFieldSelect';
|
||||
import { AdvancedFilterViewFilterOperandSelect } from '@/object-record/advanced-filter/components/AdvancedFilterViewFilterOperandSelect';
|
||||
import { AdvancedFilterViewFilterValueInput } from '@/object-record/advanced-filter/components/AdvancedFilterViewFilterValueInput';
|
||||
import { useCurrentViewFilter } from '@/object-record/advanced-filter/hooks/useCurrentViewFilter';
|
||||
import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
|
||||
import { configurableViewFilterOperands } from '@/object-record/object-filter-dropdown/utils/configurableViewFilterOperands';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledValueDropdownContainer = styled.div`
|
||||
flex: 3;
|
||||
`;
|
||||
|
||||
const StyledRow = styled.div`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
type AdvancedFilterViewFilterProps = {
|
||||
viewFilterId: string;
|
||||
};
|
||||
|
||||
export const AdvancedFilterViewFilter = ({
|
||||
viewFilterId,
|
||||
}: AdvancedFilterViewFilterProps) => {
|
||||
const filter = useCurrentViewFilter({ viewFilterId });
|
||||
|
||||
if (!filter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ObjectFilterDropdownScope filterScopeId={filter.id}>
|
||||
<StyledRow>
|
||||
<AdvancedFilterViewFilterFieldSelect viewFilterId={filter.id} />
|
||||
<AdvancedFilterViewFilterOperandSelect viewFilterId={filter.id} />
|
||||
<StyledValueDropdownContainer>
|
||||
{configurableViewFilterOperands.has(filter.operand) && (
|
||||
<AdvancedFilterViewFilterValueInput viewFilterId={filter.id} />
|
||||
)}
|
||||
</StyledValueDropdownContainer>
|
||||
</StyledRow>
|
||||
</ObjectFilterDropdownScope>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,71 @@
|
||||
import { useAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterDropdown';
|
||||
import { useCurrentViewFilter } from '@/object-record/advanced-filter/hooks/useCurrentViewFilter';
|
||||
import { ObjectFilterDropdownFilterSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect';
|
||||
import { ObjectFilterDropdownFilterSelectCompositeFieldSubMenu } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu';
|
||||
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
||||
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
|
||||
import { SelectControl } from '@/ui/input/components/SelectControl';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
import { ADVANCED_FILTER_DROPDOWN_ID } from '@/views/constants/AdvancedFilterDropdownId';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
flex: 2;
|
||||
`;
|
||||
|
||||
type AdvancedFilterViewFilterFieldSelectProps = {
|
||||
viewFilterId: string;
|
||||
};
|
||||
|
||||
export const AdvancedFilterViewFilterFieldSelect = ({
|
||||
viewFilterId,
|
||||
}: AdvancedFilterViewFilterFieldSelectProps) => {
|
||||
const { advancedFilterDropdownId } = useAdvancedFilterDropdown(viewFilterId);
|
||||
|
||||
const filter = useCurrentViewFilter({ viewFilterId });
|
||||
|
||||
const selectedFieldLabel = filter?.definition.label ?? '';
|
||||
|
||||
const { setAdvancedFilterViewFilterGroupId, setAdvancedFilterViewFilterId } =
|
||||
useFilterDropdown();
|
||||
|
||||
const [objectFilterDropdownIsSelectingCompositeField] =
|
||||
useRecoilComponentStateV2(
|
||||
objectFilterDropdownIsSelectingCompositeFieldComponentState,
|
||||
);
|
||||
|
||||
const shouldShowCompositeSelectionSubMenu =
|
||||
objectFilterDropdownIsSelectingCompositeField;
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<Dropdown
|
||||
disableBlur
|
||||
dropdownId={advancedFilterDropdownId}
|
||||
clickableComponent={
|
||||
<SelectControl
|
||||
selectedOption={{
|
||||
label: selectedFieldLabel,
|
||||
value: null,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
onOpen={() => {
|
||||
setAdvancedFilterViewFilterId(filter?.id);
|
||||
setAdvancedFilterViewFilterGroupId(filter?.viewFilterGroupId);
|
||||
}}
|
||||
dropdownComponents={
|
||||
shouldShowCompositeSelectionSubMenu ? (
|
||||
<ObjectFilterDropdownFilterSelectCompositeFieldSubMenu />
|
||||
) : (
|
||||
<ObjectFilterDropdownFilterSelect />
|
||||
)
|
||||
}
|
||||
dropdownHotkeyScope={{ scope: ADVANCED_FILTER_DROPDOWN_ID }}
|
||||
dropdownOffset={{ y: 8, x: 0 }}
|
||||
dropdownPlacement="bottom-start"
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,67 @@
|
||||
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 { useCurrentViewViewFilterGroup } from '@/object-record/advanced-filter/hooks/useCurrentViewViewFilterGroup';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
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 = {
|
||||
viewFilterGroupId: string;
|
||||
};
|
||||
|
||||
export const AdvancedFilterViewFilterGroup = ({
|
||||
viewFilterGroupId,
|
||||
}: AdvancedFilterViewFilterGroupProps) => {
|
||||
const {
|
||||
currentViewFilterGroup,
|
||||
childViewFiltersAndViewFilterGroups,
|
||||
lastChildPosition,
|
||||
} = useCurrentViewViewFilterGroup({
|
||||
viewFilterGroupId,
|
||||
});
|
||||
|
||||
if (!currentViewFilterGroup) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer
|
||||
isGrayBackground={!!currentViewFilterGroup.parentViewFilterGroupId}
|
||||
>
|
||||
{childViewFiltersAndViewFilterGroups.map((child, i) => (
|
||||
<StyledRow key={child.id}>
|
||||
<AdvancedFilterLogicalOperatorCell
|
||||
index={i}
|
||||
viewFilterGroup={currentViewFilterGroup}
|
||||
/>
|
||||
<AdvancedFilterViewFilter viewFilterId={child.id} />
|
||||
<AdvancedFilterRuleOptionsDropdown viewFilterId={child.id} />
|
||||
</StyledRow>
|
||||
))}
|
||||
<AdvancedFilterAddFilterRuleSelect
|
||||
viewFilterGroup={currentViewFilterGroup}
|
||||
lastChildPosition={lastChildPosition}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,111 @@
|
||||
import { useCurrentViewFilter } from '@/object-record/advanced-filter/hooks/useCurrentViewFilter';
|
||||
import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue';
|
||||
import { getOperandLabel } from '@/object-record/object-filter-dropdown/utils/getOperandLabel';
|
||||
import { getOperandsForFilterDefinition } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType';
|
||||
import { SelectControl } from '@/ui/input/components/SelectControl';
|
||||
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 { ADVANCED_FILTER_DROPDOWN_ID } from '@/views/constants/AdvancedFilterDropdownId';
|
||||
import { useUpsertCombinedViewFilters } from '@/views/hooks/useUpsertCombinedViewFilters';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import styled from '@emotion/styled';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
type AdvancedFilterViewFilterOperandSelectProps = {
|
||||
viewFilterId: string;
|
||||
};
|
||||
|
||||
export const AdvancedFilterViewFilterOperandSelect = ({
|
||||
viewFilterId,
|
||||
}: AdvancedFilterViewFilterOperandSelectProps) => {
|
||||
const dropdownId = `advanced-filter-view-filter-operand-${viewFilterId}`;
|
||||
|
||||
const filter = useCurrentViewFilter({ viewFilterId });
|
||||
|
||||
const isDisabled = !filter?.fieldMetadataId;
|
||||
|
||||
const { closeDropdown } = useDropdown(dropdownId);
|
||||
|
||||
const { upsertCombinedViewFilter } = useUpsertCombinedViewFilters();
|
||||
|
||||
const handleOperandChange = (operand: ViewFilterOperand) => {
|
||||
closeDropdown();
|
||||
|
||||
if (!filter) {
|
||||
throw new Error('Filter is not defined');
|
||||
}
|
||||
|
||||
const { value, displayValue } = getInitialFilterValue(
|
||||
filter.definition.type,
|
||||
operand,
|
||||
filter.value,
|
||||
filter.displayValue,
|
||||
);
|
||||
|
||||
upsertCombinedViewFilter({
|
||||
...filter,
|
||||
operand,
|
||||
value,
|
||||
displayValue,
|
||||
});
|
||||
};
|
||||
|
||||
const operandsForFilterType = isDefined(filter?.definition)
|
||||
? getOperandsForFilterDefinition(filter.definition)
|
||||
: [];
|
||||
|
||||
if (isDisabled === true) {
|
||||
return (
|
||||
<SelectControl
|
||||
selectedOption={{
|
||||
label: filter?.operand
|
||||
? getOperandLabel(filter.operand)
|
||||
: 'Select operand',
|
||||
value: null,
|
||||
}}
|
||||
isDisabled
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<Dropdown
|
||||
disableBlur
|
||||
dropdownId={dropdownId}
|
||||
clickableComponent={
|
||||
<SelectControl
|
||||
selectedOption={{
|
||||
label: filter.operand
|
||||
? getOperandLabel(filter.operand)
|
||||
: 'Select operand',
|
||||
value: null,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownMenuItemsContainer>
|
||||
{operandsForFilterType.map((filterOperand, index) => (
|
||||
<MenuItem
|
||||
key={`select-filter-operand-${index}`}
|
||||
onClick={() => {
|
||||
handleOperandChange(filterOperand);
|
||||
}}
|
||||
text={getOperandLabel(filterOperand)}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
}
|
||||
dropdownHotkeyScope={{ scope: ADVANCED_FILTER_DROPDOWN_ID }}
|
||||
dropdownOffset={{ y: 8, x: 0 }}
|
||||
dropdownPlacement="bottom-start"
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,70 @@
|
||||
import { useCurrentViewFilter } from '@/object-record/advanced-filter/hooks/useCurrentViewFilter';
|
||||
import { ObjectFilterDropdownFilterInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput';
|
||||
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
||||
import { SelectControl } from '@/ui/input/components/SelectControl';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { ADVANCED_FILTER_DROPDOWN_ID } from '@/views/constants/AdvancedFilterDropdownId';
|
||||
|
||||
type AdvancedFilterViewFilterValueInputProps = {
|
||||
viewFilterId: string;
|
||||
};
|
||||
|
||||
export const AdvancedFilterViewFilterValueInput = ({
|
||||
viewFilterId,
|
||||
}: AdvancedFilterViewFilterValueInputProps) => {
|
||||
const dropdownId = `advanced-filter-view-filter-value-input-${viewFilterId}`;
|
||||
|
||||
const filter = useCurrentViewFilter({ viewFilterId });
|
||||
|
||||
const isDisabled = !filter?.fieldMetadataId || !filter.operand;
|
||||
|
||||
const {
|
||||
setFilterDefinitionUsedInDropdown,
|
||||
setSelectedOperandInDropdown,
|
||||
setIsObjectFilterDropdownOperandSelectUnfolded,
|
||||
setSelectedFilter,
|
||||
} = useFilterDropdown();
|
||||
|
||||
if (isDisabled) {
|
||||
return (
|
||||
<SelectControl
|
||||
isDisabled
|
||||
selectedOption={{
|
||||
label: filter?.displayValue ?? '',
|
||||
value: null,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
disableBlur
|
||||
dropdownId={dropdownId}
|
||||
clickableComponent={
|
||||
<SelectControl
|
||||
selectedOption={{
|
||||
label: filter?.displayValue ?? '',
|
||||
value: null,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
onOpen={() => {
|
||||
setFilterDefinitionUsedInDropdown(filter.definition);
|
||||
setSelectedOperandInDropdown(filter.operand);
|
||||
setIsObjectFilterDropdownOperandSelectUnfolded(true);
|
||||
setSelectedFilter(filter);
|
||||
}}
|
||||
dropdownComponents={
|
||||
<DropdownMenuItemsContainer>
|
||||
<ObjectFilterDropdownFilterInput />
|
||||
</DropdownMenuItemsContainer>
|
||||
}
|
||||
dropdownHotkeyScope={{ scope: ADVANCED_FILTER_DROPDOWN_ID }}
|
||||
dropdownOffset={{ y: 8, x: 0 }}
|
||||
dropdownPlacement="bottom-start"
|
||||
dropdownMenuWidth={280}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,12 @@
|
||||
import { ViewFilterGroupLogicalOperator } from '@/views/types/ViewFilterGroupLogicalOperator';
|
||||
|
||||
export const ADVANCED_FILTER_LOGICAL_OPERATOR_OPTIONS = [
|
||||
{
|
||||
value: ViewFilterGroupLogicalOperator.AND,
|
||||
label: 'And',
|
||||
},
|
||||
{
|
||||
value: ViewFilterGroupLogicalOperator.OR,
|
||||
label: 'Or',
|
||||
},
|
||||
];
|
||||
@ -0,0 +1,14 @@
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
|
||||
export const useAdvancedFilterDropdown = (viewFilterId?: string) => {
|
||||
const advancedFilterDropdownId = `advanced-filter-view-filter-field-${viewFilterId}`;
|
||||
|
||||
const { closeDropdown: closeAdvancedFilterDropdown } = useDropdown(
|
||||
advancedFilterDropdownId,
|
||||
);
|
||||
|
||||
return {
|
||||
closeAdvancedFilterDropdown,
|
||||
advancedFilterDropdownId,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,31 @@
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
||||
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
|
||||
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
|
||||
|
||||
export const useCurrentViewFilter = ({
|
||||
viewFilterId,
|
||||
}: {
|
||||
viewFilterId?: string;
|
||||
}) => {
|
||||
const availableFilterDefinitions = useRecoilComponentValueV2(
|
||||
availableFilterDefinitionsComponentState,
|
||||
);
|
||||
|
||||
const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView();
|
||||
|
||||
const viewFilter = currentViewWithCombinedFiltersAndSorts?.viewFilters.find(
|
||||
(viewFilter) => viewFilter.id === viewFilterId,
|
||||
);
|
||||
|
||||
if (!viewFilter) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [filter] = mapViewFiltersToFilters(
|
||||
[viewFilter],
|
||||
availableFilterDefinitions,
|
||||
);
|
||||
|
||||
return filter;
|
||||
};
|
||||
@ -0,0 +1,59 @@
|
||||
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { ViewFilterGroup } from '@/views/types/ViewFilterGroup';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const useCurrentViewViewFilterGroup = ({
|
||||
viewFilterGroupId,
|
||||
}: {
|
||||
viewFilterGroupId?: string;
|
||||
}) => {
|
||||
const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView();
|
||||
|
||||
const viewFilterGroup =
|
||||
currentViewWithCombinedFiltersAndSorts?.viewFilterGroups.find(
|
||||
(viewFilterGroup) => viewFilterGroup.id === viewFilterGroupId,
|
||||
);
|
||||
|
||||
if (!isDefined(viewFilterGroup)) {
|
||||
return {
|
||||
currentViewFilterGroup: undefined,
|
||||
childViewFiltersAndViewFilterGroups: [] as (
|
||||
| ViewFilter
|
||||
| ViewFilterGroup
|
||||
)[],
|
||||
};
|
||||
}
|
||||
|
||||
const childViewFilters =
|
||||
currentViewWithCombinedFiltersAndSorts?.viewFilters.filter(
|
||||
(viewFilterToFilter) =>
|
||||
viewFilterToFilter.viewFilterGroupId === viewFilterGroup.id,
|
||||
);
|
||||
|
||||
const childViewFilterGroups =
|
||||
currentViewWithCombinedFiltersAndSorts?.viewFilterGroups.filter(
|
||||
(viewFilterGroupToFilter) =>
|
||||
viewFilterGroupToFilter.parentViewFilterGroupId === viewFilterGroup.id,
|
||||
);
|
||||
|
||||
const childViewFiltersAndViewFilterGroups = [
|
||||
...(childViewFilterGroups ?? []),
|
||||
...(childViewFilters ?? []),
|
||||
].sort((a, b) => {
|
||||
const positionA = a.positionInViewFilterGroup ?? 0;
|
||||
const positionB = b.positionInViewFilterGroup ?? 0;
|
||||
return positionA - positionB;
|
||||
});
|
||||
|
||||
const lastChildPosition =
|
||||
childViewFiltersAndViewFilterGroups[
|
||||
childViewFiltersAndViewFilterGroups.length - 1
|
||||
]?.positionInViewFilterGroup ?? 0;
|
||||
|
||||
return {
|
||||
currentViewFilterGroup: viewFilterGroup,
|
||||
childViewFiltersAndViewFilterGroups,
|
||||
lastChildPosition,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,111 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { useGetViewFromCache } from '@/views/hooks/useGetViewFromCache';
|
||||
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
|
||||
import { unsavedToDeleteViewFilterGroupIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterGroupIdsComponentFamilyState';
|
||||
import { unsavedToUpsertViewFilterGroupsComponentFamilyState } from '@/views/states/unsavedToUpsertViewFilterGroupsComponentFamilyState';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const useDeleteCombinedViewFilterGroup = (
|
||||
viewBarComponentId?: string,
|
||||
) => {
|
||||
const unsavedToUpsertViewFilterGroupsCallbackState =
|
||||
useRecoilComponentCallbackStateV2(
|
||||
unsavedToUpsertViewFilterGroupsComponentFamilyState,
|
||||
viewBarComponentId,
|
||||
);
|
||||
|
||||
const unsavedToDeleteViewFilterGroupIdsCallbackState =
|
||||
useRecoilComponentCallbackStateV2(
|
||||
unsavedToDeleteViewFilterGroupIdsComponentFamilyState,
|
||||
viewBarComponentId,
|
||||
);
|
||||
|
||||
const currentViewIdCallbackState = useRecoilComponentCallbackStateV2(
|
||||
currentViewIdComponentState,
|
||||
viewBarComponentId,
|
||||
);
|
||||
|
||||
const { getViewFromCache } = useGetViewFromCache();
|
||||
|
||||
const deleteCombinedViewFilterGroup = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async (filterGroupId: string) => {
|
||||
const currentViewId = getSnapshotValue(
|
||||
snapshot,
|
||||
currentViewIdCallbackState,
|
||||
);
|
||||
|
||||
const unsavedToUpsertViewFilterGroups = getSnapshotValue(
|
||||
snapshot,
|
||||
unsavedToUpsertViewFilterGroupsCallbackState({
|
||||
viewId: currentViewId,
|
||||
}),
|
||||
);
|
||||
|
||||
const unsavedToDeleteViewFilterGroupIds = getSnapshotValue(
|
||||
snapshot,
|
||||
unsavedToDeleteViewFilterGroupIdsCallbackState({
|
||||
viewId: currentViewId,
|
||||
}),
|
||||
);
|
||||
|
||||
if (!currentViewId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentView = await getViewFromCache(currentViewId);
|
||||
|
||||
if (!currentView) {
|
||||
return;
|
||||
}
|
||||
|
||||
const matchingFilterGroupInCurrentView =
|
||||
currentView.viewFilterGroups?.find(
|
||||
(viewFilterGroup) => viewFilterGroup.id === filterGroupId,
|
||||
);
|
||||
|
||||
const matchingFilterGroupInUnsavedFilterGroups =
|
||||
unsavedToUpsertViewFilterGroups.find(
|
||||
(viewFilterGroup) => viewFilterGroup.id === filterGroupId,
|
||||
);
|
||||
|
||||
if (isDefined(matchingFilterGroupInUnsavedFilterGroups)) {
|
||||
set(
|
||||
unsavedToUpsertViewFilterGroupsCallbackState({
|
||||
viewId: currentViewId,
|
||||
}),
|
||||
unsavedToUpsertViewFilterGroups.filter(
|
||||
(viewFilterGroup) => viewFilterGroup.id !== filterGroupId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (isDefined(matchingFilterGroupInCurrentView)) {
|
||||
set(
|
||||
unsavedToDeleteViewFilterGroupIdsCallbackState({
|
||||
viewId: currentViewId,
|
||||
}),
|
||||
[
|
||||
...new Set([
|
||||
...unsavedToDeleteViewFilterGroupIds,
|
||||
matchingFilterGroupInCurrentView.id,
|
||||
]),
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
[
|
||||
currentViewIdCallbackState,
|
||||
getViewFromCache,
|
||||
unsavedToDeleteViewFilterGroupIdsCallbackState,
|
||||
unsavedToUpsertViewFilterGroupsCallbackState,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
deleteCombinedViewFilterGroup,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,54 @@
|
||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||
import { unsavedToUpsertViewFilterGroupsComponentFamilyState } from '@/views/states/unsavedToUpsertViewFilterGroupsComponentFamilyState';
|
||||
import { ViewFilterGroup } from '@/views/types/ViewFilterGroup';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
export const useUpsertCombinedViewFilterGroup = () => {
|
||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
ViewComponentInstanceContext,
|
||||
);
|
||||
|
||||
const unsavedToUpsertViewFilterGroupsCallbackState =
|
||||
useRecoilComponentCallbackStateV2(
|
||||
unsavedToUpsertViewFilterGroupsComponentFamilyState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const upsertCombinedViewFilterGroup = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(newViewFilterGroup: Omit<ViewFilterGroup, '__typename'>) => {
|
||||
const currentViewUnsavedToUpsertViewFilterGroups =
|
||||
unsavedToUpsertViewFilterGroupsCallbackState({
|
||||
viewId: newViewFilterGroup.viewId,
|
||||
});
|
||||
|
||||
const unsavedToUpsertViewFilterGroups = getSnapshotValue(
|
||||
snapshot,
|
||||
currentViewUnsavedToUpsertViewFilterGroups,
|
||||
);
|
||||
|
||||
const newViewFilterWithTypename: ViewFilterGroup = {
|
||||
...newViewFilterGroup,
|
||||
__typename: 'ViewFilterGroup',
|
||||
};
|
||||
|
||||
set(
|
||||
unsavedToUpsertViewFilterGroupsCallbackState({
|
||||
viewId: newViewFilterGroup.viewId,
|
||||
}),
|
||||
[
|
||||
...unsavedToUpsertViewFilterGroups.filter(
|
||||
(viewFilterGroup) => viewFilterGroup.id !== newViewFilterGroup.id,
|
||||
),
|
||||
newViewFilterWithTypename,
|
||||
],
|
||||
);
|
||||
},
|
||||
[unsavedToUpsertViewFilterGroupsCallbackState],
|
||||
);
|
||||
|
||||
return { upsertCombinedViewFilterGroup };
|
||||
};
|
||||
@ -0,0 +1,124 @@
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { useUpsertCombinedViewFilterGroup } from '@/object-record/advanced-filter/hooks/useUpsertCombinedViewFilterGroup';
|
||||
import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId';
|
||||
import { getOperandsForFilterDefinition } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { MenuItemLeftContent } from '@/ui/navigation/menu-item/internals/components/MenuItemLeftContent';
|
||||
import { StyledMenuItemBase } from '@/ui/navigation/menu-item/internals/components/StyledMenuItemBase';
|
||||
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 { ViewFilterGroupLogicalOperator } from '@/views/types/ViewFilterGroupLogicalOperator';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconFilter, Pill } from 'twenty-ui';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: ${({ theme }) => theme.spacing(1)};
|
||||
border-top: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
`;
|
||||
|
||||
export const StyledMenuItemSelect = styled(StyledMenuItemBase)`
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.background.transparent.light};
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledPill = styled(Pill)`
|
||||
background: ${({ theme }) => theme.color.blueAccent10};
|
||||
color: ${({ theme }) => theme.color.blue};
|
||||
`;
|
||||
|
||||
export const AdvancedFilterButton = () => {
|
||||
const advancedFilterQuerySubFilterCount = 0; // TODO
|
||||
|
||||
const { openDropdown: openAdvancedFilterDropdown } = useDropdown(
|
||||
ADVANCED_FILTER_DROPDOWN_ID,
|
||||
);
|
||||
|
||||
const { closeDropdown: closeObjectFilterDropdown } = useDropdown(
|
||||
OBJECT_FILTER_DROPDOWN_ID,
|
||||
);
|
||||
|
||||
const { currentViewId, currentViewWithCombinedFiltersAndSorts } =
|
||||
useGetCurrentView();
|
||||
|
||||
const { upsertCombinedViewFilterGroup } = useUpsertCombinedViewFilterGroup();
|
||||
|
||||
const { upsertCombinedViewFilter } = useUpsertCombinedViewFilters();
|
||||
|
||||
const objectMetadataId =
|
||||
currentViewWithCombinedFiltersAndSorts?.objectMetadataId;
|
||||
|
||||
if (!objectMetadataId) {
|
||||
throw new Error('Object metadata id is missing from current view');
|
||||
}
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItemById({
|
||||
objectId: objectMetadataId ?? null,
|
||||
});
|
||||
|
||||
const availableFilterDefinitions = useRecoilComponentValueV2(
|
||||
availableFilterDefinitionsComponentState,
|
||||
);
|
||||
|
||||
const handleClick = () => {
|
||||
if (!currentViewId) {
|
||||
throw new Error('Missing current view id');
|
||||
}
|
||||
|
||||
const alreadyHasAdvancedFilterGroup =
|
||||
(currentViewWithCombinedFiltersAndSorts?.viewFilterGroups?.length ?? 0) >
|
||||
0;
|
||||
|
||||
if (!alreadyHasAdvancedFilterGroup) {
|
||||
const newViewFilterGroup = {
|
||||
id: v4(),
|
||||
viewId: currentViewId,
|
||||
logicalOperator: ViewFilterGroupLogicalOperator.AND,
|
||||
};
|
||||
|
||||
upsertCombinedViewFilterGroup(newViewFilterGroup);
|
||||
|
||||
const defaultFilterDefinition =
|
||||
availableFilterDefinitions.find(
|
||||
(filterDefinition) =>
|
||||
filterDefinition.fieldMetadataId ===
|
||||
objectMetadataItem?.labelIdentifierFieldMetadataId,
|
||||
) ?? availableFilterDefinitions?.[0];
|
||||
|
||||
if (!defaultFilterDefinition) {
|
||||
throw new Error('Missing default filter definition');
|
||||
}
|
||||
|
||||
upsertCombinedViewFilter({
|
||||
id: v4(),
|
||||
fieldMetadataId: defaultFilterDefinition.fieldMetadataId,
|
||||
operand: getOperandsForFilterDefinition(defaultFilterDefinition)[0],
|
||||
definition: defaultFilterDefinition,
|
||||
value: '',
|
||||
displayValue: '',
|
||||
viewFilterGroupId: newViewFilterGroup.id,
|
||||
});
|
||||
}
|
||||
|
||||
openAdvancedFilterDropdown();
|
||||
closeObjectFilterDropdown();
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledMenuItemSelect onClick={handleClick}>
|
||||
<MenuItemLeftContent LeftIcon={IconFilter} text="Advanced filter" />
|
||||
{advancedFilterQuerySubFilterCount > 0 && (
|
||||
<StyledPill label={advancedFilterQuerySubFilterCount.toString()} />
|
||||
)}
|
||||
</StyledMenuItemSelect>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -1,7 +1,7 @@
|
||||
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
||||
|
||||
import { ObjectFilterDropdownFilterInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput';
|
||||
import { ObjectFilterDropdownFilterSelectCompositeFieldSubMenu } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu';
|
||||
import { ObjectFilterOperandSelectAndInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterOperandSelectAndInput';
|
||||
import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState';
|
||||
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
@ -48,11 +48,13 @@ export const MultipleFiltersDropdownContent = ({
|
||||
return (
|
||||
<StyledContainer>
|
||||
{shoudShowFilterInput ? (
|
||||
<ObjectFilterDropdownFilterInput filterDropdownId={filterDropdownId} />
|
||||
<ObjectFilterOperandSelectAndInput
|
||||
filterDropdownId={filterDropdownId}
|
||||
/>
|
||||
) : shouldShowCompositeSelectionSubMenu ? (
|
||||
<ObjectFilterDropdownFilterSelectCompositeFieldSubMenu />
|
||||
) : (
|
||||
<ObjectFilterDropdownFilterSelect />
|
||||
<ObjectFilterDropdownFilterSelect isAdvancedFilterButtonVisible />
|
||||
)}
|
||||
<MultipleFiltersDropdownFilterOnFilterChangedEffect
|
||||
filterDefinitionUsedInDropdownType={
|
||||
|
||||
@ -63,6 +63,7 @@ export const ObjectFilterDropdownDateInput = () => {
|
||||
: newDate.toLocaleDateString()
|
||||
: '',
|
||||
definition: filterDefinitionUsedInDropdown,
|
||||
viewFilterGroupId: selectedFilter?.viewFilterGroupId,
|
||||
});
|
||||
|
||||
setIsObjectFilterDropdownUnfolded(false);
|
||||
@ -92,6 +93,7 @@ export const ObjectFilterDropdownDateInput = () => {
|
||||
operand: selectedOperandInDropdown,
|
||||
displayValue: getRelativeDateDisplayValue(relativeDate),
|
||||
definition: filterDefinitionUsedInDropdown,
|
||||
viewFilterGroupId: selectedFilter?.viewFilterGroupId,
|
||||
});
|
||||
|
||||
setIsObjectFilterDropdownUnfolded(false);
|
||||
|
||||
@ -2,8 +2,6 @@ import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { ObjectFilterDropdownDateInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput';
|
||||
import { ObjectFilterDropdownNumberInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownNumberInput';
|
||||
import { ObjectFilterDropdownOperandButton } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandButton';
|
||||
import { ObjectFilterDropdownOperandSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandSelect';
|
||||
import { ObjectFilterDropdownOptionSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect';
|
||||
import { ObjectFilterDropdownRatingInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput';
|
||||
import { ObjectFilterDropdownRecordSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect';
|
||||
@ -14,19 +12,11 @@ import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/
|
||||
import { isActorSourceCompositeFilter } from '@/object-record/object-filter-dropdown/utils/isActorSourceCompositeFilter';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import styled from '@emotion/styled';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
const StyledOperandSelectContainer = styled.div`
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
box-shadow: ${({ theme }) => theme.boxShadow.light};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
left: 10px;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
`;
|
||||
import { DATE_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/DateFilterTypes';
|
||||
import { NUMBER_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/NumberFilterTypes';
|
||||
import { TEXT_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/TextFilterTypes';
|
||||
|
||||
type ObjectFilterDropdownFilterInputProps = {
|
||||
filterDropdownId?: string;
|
||||
@ -38,13 +28,8 @@ export const ObjectFilterDropdownFilterInput = ({
|
||||
const {
|
||||
filterDefinitionUsedInDropdownState,
|
||||
selectedOperandInDropdownState,
|
||||
isObjectFilterDropdownOperandSelectUnfoldedState,
|
||||
} = useFilterDropdown({ filterDropdownId });
|
||||
|
||||
const isObjectFilterDropdownOperandSelectUnfolded = useRecoilValue(
|
||||
isObjectFilterDropdownOperandSelectUnfoldedState,
|
||||
);
|
||||
|
||||
const filterDefinitionUsedInDropdown = useRecoilValue(
|
||||
filterDefinitionUsedInDropdownState,
|
||||
);
|
||||
@ -74,40 +59,21 @@ export const ObjectFilterDropdownFilterInput = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<ObjectFilterDropdownOperandButton />
|
||||
{isObjectFilterDropdownOperandSelectUnfolded && (
|
||||
<StyledOperandSelectContainer>
|
||||
<ObjectFilterDropdownOperandSelect />
|
||||
</StyledOperandSelectContainer>
|
||||
)}
|
||||
{isConfigurable && selectedOperandInDropdown && (
|
||||
<>
|
||||
{[
|
||||
'TEXT',
|
||||
'EMAIL',
|
||||
'EMAILS',
|
||||
'PHONE',
|
||||
'FULL_NAME',
|
||||
'LINK',
|
||||
'LINKS',
|
||||
'ADDRESS',
|
||||
'ACTOR',
|
||||
'ARRAY',
|
||||
'RAW_JSON',
|
||||
'PHONES',
|
||||
].includes(filterDefinitionUsedInDropdown.type) &&
|
||||
{TEXT_FILTER_TYPES.includes(filterDefinitionUsedInDropdown.type) &&
|
||||
!isActorSourceCompositeFilter(filterDefinitionUsedInDropdown) && (
|
||||
<ObjectFilterDropdownTextSearchInput />
|
||||
)}
|
||||
{['NUMBER', 'CURRENCY'].includes(
|
||||
{NUMBER_FILTER_TYPES.includes(
|
||||
filterDefinitionUsedInDropdown.type,
|
||||
) && <ObjectFilterDropdownNumberInput />}
|
||||
{filterDefinitionUsedInDropdown.type === 'RATING' && (
|
||||
<ObjectFilterDropdownRatingInput />
|
||||
)}
|
||||
{['DATE_TIME', 'DATE'].includes(
|
||||
filterDefinitionUsedInDropdown.type,
|
||||
) && <ObjectFilterDropdownDateInput />}
|
||||
{DATE_FILTER_TYPES.includes(filterDefinitionUsedInDropdown.type) && (
|
||||
<ObjectFilterDropdownDateInput />
|
||||
)}
|
||||
{filterDefinitionUsedInDropdown.type === 'RELATION' && (
|
||||
<>
|
||||
<ObjectFilterDropdownSearchInput />
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
import { ObjectFilterDropdownOperandButton } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandButton';
|
||||
import { ObjectFilterDropdownOperandSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandSelect';
|
||||
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
const StyledOperandSelectContainer = styled.div`
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
box-shadow: ${({ theme }) => theme.boxShadow.light};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
left: 10px;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
`;
|
||||
|
||||
export const ObjectFilterDropdownFilterOperandSelect = ({
|
||||
filterDropdownId,
|
||||
}: {
|
||||
filterDropdownId?: string;
|
||||
}) => {
|
||||
const { isObjectFilterDropdownOperandSelectUnfoldedState } =
|
||||
useFilterDropdown({ filterDropdownId });
|
||||
|
||||
const isObjectFilterDropdownOperandSelectUnfolded = useRecoilValue(
|
||||
isObjectFilterDropdownOperandSelectUnfoldedState,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ObjectFilterDropdownOperandButton />
|
||||
{isObjectFilterDropdownOperandSelectUnfolded && (
|
||||
<StyledOperandSelectContainer>
|
||||
<ObjectFilterDropdownOperandSelect />
|
||||
</StyledOperandSelectContainer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -3,6 +3,8 @@ import { useContext } from 'react';
|
||||
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
|
||||
import { useAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterDropdown';
|
||||
import { AdvancedFilterButton } from '@/object-record/object-filter-dropdown/components/AdvancedFilterButton';
|
||||
import { ObjectFilterDropdownFilterSelectMenuItem } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem';
|
||||
import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId';
|
||||
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
||||
@ -15,6 +17,7 @@ import { SelectableItem } from '@/ui/layout/selectable-list/components/Selectabl
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
||||
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
@ -45,12 +48,27 @@ export const StyledInput = styled.input`
|
||||
}
|
||||
`;
|
||||
|
||||
export const ObjectFilterDropdownFilterSelect = () => {
|
||||
type ObjectFilterDropdownFilterSelectProps = {
|
||||
isAdvancedFilterButtonVisible?: boolean;
|
||||
};
|
||||
|
||||
export const ObjectFilterDropdownFilterSelect = ({
|
||||
isAdvancedFilterButtonVisible,
|
||||
}: ObjectFilterDropdownFilterSelectProps) => {
|
||||
const {
|
||||
setObjectFilterDropdownSearchInput,
|
||||
objectFilterDropdownSearchInputState,
|
||||
advancedFilterViewFilterIdState,
|
||||
} = useFilterDropdown();
|
||||
|
||||
const advancedFilterViewFilterId = useRecoilValue(
|
||||
advancedFilterViewFilterIdState,
|
||||
);
|
||||
|
||||
const { closeAdvancedFilterDropdown } = useAdvancedFilterDropdown(
|
||||
advancedFilterViewFilterId,
|
||||
);
|
||||
|
||||
const objectFilterDropdownSearchInput = useRecoilValue(
|
||||
objectFilterDropdownSearchInputState,
|
||||
);
|
||||
@ -110,14 +128,22 @@ export const ObjectFilterDropdownFilterSelect = () => {
|
||||
}
|
||||
|
||||
resetSelectedItem();
|
||||
|
||||
selectFilter({ filterDefinition: selectedFilterDefinition });
|
||||
closeAdvancedFilterDropdown();
|
||||
};
|
||||
|
||||
const shoudShowSeparator =
|
||||
visibleColumnsFilterDefinitions.length > 0 &&
|
||||
hiddenColumnsFilterDefinitions.length > 0;
|
||||
|
||||
const { currentViewId, currentViewWithCombinedFiltersAndSorts } =
|
||||
useGetCurrentView();
|
||||
|
||||
const shouldShowAdvancedFilterButton =
|
||||
isDefined(currentViewId) &&
|
||||
isDefined(currentViewWithCombinedFiltersAndSorts?.objectMetadataId) &&
|
||||
isAdvancedFilterButtonVisible;
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledInput
|
||||
@ -164,6 +190,7 @@ export const ObjectFilterDropdownFilterSelect = () => {
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
</SelectableList>
|
||||
{shouldShowAdvancedFilterButton && <AdvancedFilterButton />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { useAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterDropdown';
|
||||
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
||||
import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState';
|
||||
import { objectFilterDropdownFirstLevelFilterDefinitionComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFirstLevelFilterDefinitionComponentState';
|
||||
@ -6,6 +7,7 @@ import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-rec
|
||||
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
|
||||
import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel';
|
||||
import { getFilterableFieldTypeLabel } from '@/object-record/object-filter-dropdown/utils/getFilterableFieldTypeLabel';
|
||||
import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue';
|
||||
import { getOperandsForFilterDefinition } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType';
|
||||
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
|
||||
@ -13,6 +15,7 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
import { useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { IconApps, IconChevronLeft, isDefined, useIcons } from 'twenty-ui';
|
||||
|
||||
export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => {
|
||||
@ -47,10 +50,46 @@ export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => {
|
||||
setFilterDefinitionUsedInDropdown,
|
||||
setSelectedOperandInDropdown,
|
||||
setObjectFilterDropdownSearchInput,
|
||||
selectFilter,
|
||||
advancedFilterViewFilterIdState,
|
||||
advancedFilterViewFilterGroupIdState,
|
||||
} = useFilterDropdown();
|
||||
|
||||
const advancedFilterViewFilterId = useRecoilValue(
|
||||
advancedFilterViewFilterIdState,
|
||||
);
|
||||
const advancedFilterViewFilterGroupId = useRecoilValue(
|
||||
advancedFilterViewFilterGroupIdState,
|
||||
);
|
||||
|
||||
const { closeAdvancedFilterDropdown } = useAdvancedFilterDropdown(
|
||||
advancedFilterViewFilterId,
|
||||
);
|
||||
|
||||
const handleSelectFilter = (definition: FilterDefinition | null) => {
|
||||
if (definition !== null) {
|
||||
if (
|
||||
isDefined(advancedFilterViewFilterId) &&
|
||||
isDefined(advancedFilterViewFilterGroupId)
|
||||
) {
|
||||
closeAdvancedFilterDropdown();
|
||||
|
||||
const operand = getOperandsForFilterDefinition(definition)[0];
|
||||
const { value, displayValue } = getInitialFilterValue(
|
||||
definition.type,
|
||||
operand,
|
||||
);
|
||||
selectFilter({
|
||||
id: advancedFilterViewFilterId,
|
||||
fieldMetadataId: definition.fieldMetadataId,
|
||||
value,
|
||||
operand,
|
||||
displayValue,
|
||||
definition,
|
||||
viewFilterGroupId: advancedFilterViewFilterGroupId,
|
||||
});
|
||||
}
|
||||
|
||||
setFilterDefinitionUsedInDropdown(definition);
|
||||
|
||||
setSelectedOperandInDropdown(
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { useAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterDropdown';
|
||||
import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId';
|
||||
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
||||
import { useSelectFilter } from '@/object-record/object-filter-dropdown/hooks/useSelectFilter';
|
||||
@ -59,11 +60,23 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({
|
||||
setFilterDefinitionUsedInDropdown,
|
||||
setSelectedOperandInDropdown,
|
||||
setObjectFilterDropdownSearchInput,
|
||||
advancedFilterViewFilterIdState,
|
||||
} = useFilterDropdown();
|
||||
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
|
||||
const advancedFilterViewFilterId = useRecoilValue(
|
||||
advancedFilterViewFilterIdState,
|
||||
);
|
||||
|
||||
const { closeAdvancedFilterDropdown } = useAdvancedFilterDropdown(
|
||||
advancedFilterViewFilterId,
|
||||
);
|
||||
|
||||
const handleSelectFilter = (availableFilterDefinition: FilterDefinition) => {
|
||||
closeAdvancedFilterDropdown();
|
||||
selectFilter({ filterDefinition: availableFilterDefinition });
|
||||
|
||||
setFilterDefinitionUsedInDropdown(availableFilterDefinition);
|
||||
|
||||
if (
|
||||
@ -87,8 +100,6 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({
|
||||
const handleClick = () => {
|
||||
resetSelectedItem();
|
||||
|
||||
selectFilter({ filterDefinition });
|
||||
|
||||
if (isACompositeField) {
|
||||
// TODO: create isCompositeFilterableFieldType type guard
|
||||
setObjectFilterDropdownSubMenuFieldType(
|
||||
|
||||
@ -56,6 +56,7 @@ export const ObjectFilterDropdownNumberInput = () => {
|
||||
operand: selectedOperandInDropdown,
|
||||
displayValue: newValue,
|
||||
definition: filterDefinitionUsedInDropdown,
|
||||
viewFilterGroupId: selectedFilter?.viewFilterGroupId,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -135,6 +135,7 @@ export const ObjectFilterDropdownOptionSelect = () => {
|
||||
displayValue: filterDisplayValue,
|
||||
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
|
||||
value: newFilterValue,
|
||||
viewFilterGroupId: selectedFilter?.viewFilterGroupId,
|
||||
});
|
||||
}
|
||||
resetSelectedItem();
|
||||
|
||||
@ -64,6 +64,7 @@ export const ObjectFilterDropdownRatingInput = () => {
|
||||
operand: selectedOperandInDropdown,
|
||||
displayValue: convertFieldRatingValueToNumber(newValue),
|
||||
definition: filterDefinitionUsedInDropdown,
|
||||
viewFilterGroupId: selectedFilter?.viewFilterGroupId,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -129,6 +129,7 @@ export const ObjectFilterDropdownRecordSelect = ({
|
||||
displayValue: filterDisplayValue,
|
||||
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
|
||||
value: newFilterValue,
|
||||
viewFilterGroupId: selectedFilter?.viewFilterGroupId,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -115,6 +115,7 @@ export const ObjectFilterDropdownSourceSelect = ({
|
||||
displayValue: filterDisplayValue,
|
||||
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
|
||||
value: newFilterValue,
|
||||
viewFilterGroupId: selectedFilter?.viewFilterGroupId,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -52,12 +52,13 @@ export const ObjectFilterDropdownTextSearchInput = () => {
|
||||
setObjectFilterDropdownSearchInput(event.target.value);
|
||||
|
||||
selectFilter?.({
|
||||
id: selectedFilter?.id ? selectedFilter.id : filterId,
|
||||
id: selectedFilter?.id ?? filterId,
|
||||
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
|
||||
value: event.target.value,
|
||||
operand: selectedOperandInDropdown,
|
||||
displayValue: event.target.value,
|
||||
definition: filterDefinitionUsedInDropdown,
|
||||
viewFilterGroupId: selectedFilter?.viewFilterGroupId,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
import { ObjectFilterDropdownFilterInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput';
|
||||
import { ObjectFilterDropdownFilterOperandSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterOperandSelect';
|
||||
|
||||
type ObjectFilterOperandSelectAndInputProps = {
|
||||
filterDropdownId?: string;
|
||||
};
|
||||
|
||||
export const ObjectFilterOperandSelectAndInput = ({
|
||||
filterDropdownId,
|
||||
}: ObjectFilterOperandSelectAndInputProps) => {
|
||||
return (
|
||||
<>
|
||||
<ObjectFilterDropdownFilterOperandSelect
|
||||
filterDropdownId={filterDropdownId}
|
||||
/>
|
||||
<ObjectFilterDropdownFilterInput filterDropdownId={filterDropdownId} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export const DATE_FILTER_TYPES = ['DATE_TIME', 'DATE'];
|
||||
@ -0,0 +1 @@
|
||||
export const NUMBER_FILTER_TYPES = ['NUMBER', 'CURRENCY'];
|
||||
@ -0,0 +1,14 @@
|
||||
export const TEXT_FILTER_TYPES = [
|
||||
'TEXT',
|
||||
'EMAIL',
|
||||
'EMAILS',
|
||||
'PHONE',
|
||||
'FULL_NAME',
|
||||
'LINK',
|
||||
'LINKS',
|
||||
'ADDRESS',
|
||||
'ACTOR',
|
||||
'ARRAY',
|
||||
'RAW_JSON',
|
||||
'PHONES',
|
||||
];
|
||||
@ -8,11 +8,24 @@ import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
|
||||
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
|
||||
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
|
||||
|
||||
const filterDropdownId = 'filterDropdownId';
|
||||
const renderHookConfig = {
|
||||
wrapper: RecoilRoot,
|
||||
wrapper: ({ children }: any) => (
|
||||
<RecoilRoot>
|
||||
<MockedProvider mocks={[]} addTypename={false}>
|
||||
<JestObjectMetadataItemSetter>
|
||||
<ViewComponentInstanceContext.Provider value={{ instanceId: 'test' }}>
|
||||
{children}
|
||||
</ViewComponentInstanceContext.Provider>
|
||||
</JestObjectMetadataItemSetter>
|
||||
</MockedProvider>
|
||||
</RecoilRoot>
|
||||
),
|
||||
};
|
||||
|
||||
const filterDefinitions: FilterDefinition[] = [
|
||||
@ -306,9 +319,10 @@ describe('useFilterDropdown', () => {
|
||||
|
||||
it('should reset filter', async () => {
|
||||
const { result } = renderHook(() => {
|
||||
const { selectFilter, resetFilter } = useFilterDropdown({
|
||||
const { resetFilter, selectFilter } = useFilterDropdown({
|
||||
filterDropdownId,
|
||||
});
|
||||
|
||||
const { selectedFilterState } = useFilterDropdownStates(filterDropdownId);
|
||||
|
||||
const [selectedFilter, setSelectedFilter] =
|
||||
|
||||
@ -7,11 +7,14 @@ import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotV
|
||||
import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState';
|
||||
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { useUpsertCombinedViewFilters } from '@/views/hooks/useUpsertCombinedViewFilters';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
import { ObjectFilterDropdownScopeInternalContext } from '../scopes/scope-internal-context/ObjectFilterDropdownScopeInternalContext';
|
||||
import { Filter } from '../types/Filter';
|
||||
|
||||
type UseFilterDropdownProps = {
|
||||
filterDropdownId?: string;
|
||||
advancedFilterViewFilterId?: string;
|
||||
};
|
||||
|
||||
export const useFilterDropdown = (props?: UseFilterDropdownProps) => {
|
||||
@ -30,17 +33,25 @@ export const useFilterDropdown = (props?: UseFilterDropdownProps) => {
|
||||
selectedFilterState,
|
||||
selectedOperandInDropdownState,
|
||||
onFilterSelectState,
|
||||
advancedFilterViewFilterGroupIdState,
|
||||
advancedFilterViewFilterIdState,
|
||||
} = useFilterDropdownStates(scopeId);
|
||||
|
||||
const { upsertCombinedViewFilter } = useUpsertCombinedViewFilters();
|
||||
|
||||
const selectFilter = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(filter: Filter | null) => {
|
||||
set(selectedFilterState, filter);
|
||||
const onFilterSelect = getSnapshotValue(snapshot, onFilterSelectState);
|
||||
|
||||
if (isDefined(filter)) {
|
||||
upsertCombinedViewFilter(filter);
|
||||
}
|
||||
|
||||
onFilterSelect?.(filter);
|
||||
},
|
||||
[selectedFilterState, onFilterSelectState],
|
||||
[selectedFilterState, onFilterSelectState, upsertCombinedViewFilter],
|
||||
);
|
||||
|
||||
const emptyFilterButKeepDefinition = useRecoilCallback(
|
||||
@ -117,6 +128,12 @@ export const useFilterDropdown = (props?: UseFilterDropdownProps) => {
|
||||
isObjectFilterDropdownUnfoldedState,
|
||||
);
|
||||
const setOnFilterSelect = useSetRecoilState(onFilterSelectState);
|
||||
const setAdvancedFilterViewFilterGroupId = useSetRecoilState(
|
||||
advancedFilterViewFilterGroupIdState,
|
||||
);
|
||||
const setAdvancedFilterViewFilterId = useSetRecoilState(
|
||||
advancedFilterViewFilterIdState,
|
||||
);
|
||||
|
||||
return {
|
||||
scopeId,
|
||||
@ -132,6 +149,8 @@ export const useFilterDropdown = (props?: UseFilterDropdownProps) => {
|
||||
setIsObjectFilterDropdownOperandSelectUnfolded,
|
||||
setIsObjectFilterDropdownUnfolded,
|
||||
setOnFilterSelect,
|
||||
setAdvancedFilterViewFilterGroupId,
|
||||
setAdvancedFilterViewFilterId,
|
||||
emptyFilterButKeepDefinition,
|
||||
filterDefinitionUsedInDropdownState,
|
||||
objectFilterDropdownSearchInputState,
|
||||
@ -143,5 +162,7 @@ export const useFilterDropdown = (props?: UseFilterDropdownProps) => {
|
||||
selectedFilterState,
|
||||
selectedOperandInDropdownState,
|
||||
onFilterSelectState,
|
||||
advancedFilterViewFilterGroupIdState,
|
||||
advancedFilterViewFilterIdState,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { advancedFilterViewFilterGroupIdComponentState } from '@/object-record/object-filter-dropdown/states/advancedFilterViewFilterGroupIdComponentState';
|
||||
import { advancedFilterViewFilterIdComponentState } from '@/object-record/object-filter-dropdown/states/advancedFilterViewFilterIdComponentState';
|
||||
import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState';
|
||||
import { isObjectFilterDropdownOperandSelectUnfoldedComponentState } from '@/object-record/object-filter-dropdown/states/isObjectFilterDropdownOperandSelectUnfoldedComponentState';
|
||||
import { isObjectFilterDropdownUnfoldedComponentState } from '@/object-record/object-filter-dropdown/states/isObjectFilterDropdownUnfoldedComponentState';
|
||||
@ -56,6 +58,16 @@ export const useFilterDropdownStates = (scopeId: string) => {
|
||||
scopeId,
|
||||
);
|
||||
|
||||
const advancedFilterViewFilterGroupIdState = extractComponentState(
|
||||
advancedFilterViewFilterGroupIdComponentState,
|
||||
scopeId,
|
||||
);
|
||||
|
||||
const advancedFilterViewFilterIdState = extractComponentState(
|
||||
advancedFilterViewFilterIdComponentState,
|
||||
scopeId,
|
||||
);
|
||||
|
||||
return {
|
||||
filterDefinitionUsedInDropdownState,
|
||||
objectFilterDropdownSearchInputState,
|
||||
@ -66,5 +78,7 @@ export const useFilterDropdownStates = (scopeId: string) => {
|
||||
selectedFilterState,
|
||||
selectedOperandInDropdownState,
|
||||
onFilterSelectState,
|
||||
advancedFilterViewFilterGroupIdState,
|
||||
advancedFilterViewFilterIdState,
|
||||
};
|
||||
};
|
||||
|
||||
@ -4,6 +4,8 @@ import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/ut
|
||||
import { getOperandsForFilterDefinition } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType';
|
||||
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
|
||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
type SelectFilterParams = {
|
||||
@ -16,8 +18,17 @@ export const useSelectFilter = () => {
|
||||
setSelectedOperandInDropdown,
|
||||
setObjectFilterDropdownSearchInput,
|
||||
selectFilter: filterDropdownSelectFilter,
|
||||
advancedFilterViewFilterGroupIdState,
|
||||
advancedFilterViewFilterIdState,
|
||||
} = useFilterDropdown();
|
||||
|
||||
const advancedFilterViewFilterId = useRecoilValue(
|
||||
advancedFilterViewFilterIdState,
|
||||
);
|
||||
const advancedFilterViewFilterGroupId = useRecoilValue(
|
||||
advancedFilterViewFilterGroupIdState,
|
||||
);
|
||||
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
|
||||
const selectFilter = ({ filterDefinition }: SelectFilterParams) => {
|
||||
@ -39,14 +50,17 @@ export const useSelectFilter = () => {
|
||||
getOperandsForFilterDefinition(filterDefinition)[0],
|
||||
);
|
||||
|
||||
if (value !== '') {
|
||||
const isAdvancedFilter = isDefined(advancedFilterViewFilterId);
|
||||
|
||||
if (isAdvancedFilter || value !== '') {
|
||||
filterDropdownSelectFilter({
|
||||
id: v4(),
|
||||
id: advancedFilterViewFilterId ?? v4(),
|
||||
fieldMetadataId: filterDefinition.fieldMetadataId,
|
||||
displayValue,
|
||||
operand: getOperandsForFilterDefinition(filterDefinition)[0],
|
||||
value,
|
||||
definition: filterDefinition,
|
||||
viewFilterGroupId: advancedFilterViewFilterGroupId,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
export const advancedFilterViewFilterGroupIdComponentState =
|
||||
createComponentState<string | undefined>({
|
||||
key: 'advancedFilterViewFilterGroupIdComponentState',
|
||||
defaultValue: undefined,
|
||||
});
|
||||
@ -0,0 +1,8 @@
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
export const advancedFilterViewFilterIdComponentState = createComponentState<
|
||||
string | undefined
|
||||
>({
|
||||
key: 'advancedFilterViewFilterIdComponentState',
|
||||
defaultValue: undefined,
|
||||
});
|
||||
@ -7,7 +7,9 @@ export type Filter = {
|
||||
fieldMetadataId: string;
|
||||
value: string;
|
||||
displayValue: string;
|
||||
viewFilterGroupId?: string;
|
||||
displayAvatarUrl?: string;
|
||||
operand: ViewFilterOperand;
|
||||
positionInViewFilterGroup?: number | null;
|
||||
definition: FilterDefinition;
|
||||
};
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
|
||||
|
||||
export type FilterDraft = Partial<Filter> &
|
||||
Omit<Filter, 'fieldMetadataId' | 'operand' | 'definition'>;
|
||||
@ -0,0 +1,14 @@
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
|
||||
export const configurableViewFilterOperands = new Set<ViewFilterOperand>([
|
||||
ViewFilterOperand.Is,
|
||||
ViewFilterOperand.IsNotNull,
|
||||
ViewFilterOperand.IsNot,
|
||||
ViewFilterOperand.LessThan,
|
||||
ViewFilterOperand.GreaterThan,
|
||||
ViewFilterOperand.IsBefore,
|
||||
ViewFilterOperand.IsAfter,
|
||||
ViewFilterOperand.Contains,
|
||||
ViewFilterOperand.DoesNotContain,
|
||||
ViewFilterOperand.IsRelative,
|
||||
]);
|
||||
@ -3,7 +3,7 @@ import { isActorSourceCompositeFilter } from '@/object-record/object-filter-drop
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
|
||||
export const getOperandsForFilterDefinition = (
|
||||
filterDefinition: FilterDefinition,
|
||||
filterDefinition: Pick<FilterDefinition, 'type' | 'compositeFieldName'>,
|
||||
): ViewFilterOperand[] => {
|
||||
const emptyOperands = [
|
||||
ViewFilterOperand.IsEmpty,
|
||||
|
||||
@ -2,7 +2,7 @@ import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/F
|
||||
import { FieldActorValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
|
||||
export const isActorSourceCompositeFilter = (
|
||||
filterDefinition: FilterDefinition,
|
||||
filterDefinition: Pick<FilterDefinition, 'compositeFieldName'>,
|
||||
) => {
|
||||
return (
|
||||
filterDefinition.compositeFieldName ===
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
|
||||
import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter';
|
||||
import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { getCompaniesMock } from '~/testing/mock-data/companies';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
@ -16,7 +16,7 @@ const personMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
|
||||
|
||||
jest.useFakeTimers().setSystemTime(new Date('2020-01-01'));
|
||||
|
||||
describe('turnFiltersIntoQueryFilter', () => {
|
||||
describe('computeViewRecordGqlOperationFilter', () => {
|
||||
it('should work as expected for single filter', () => {
|
||||
const companyMockNameFieldMetadataId =
|
||||
companyMockObjectMetadataItem.fields.find(
|
||||
@ -37,9 +37,10 @@ describe('turnFiltersIntoQueryFilter', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const result = turnFiltersIntoQueryFilter(
|
||||
const result = computeViewRecordGqlOperationFilter(
|
||||
[nameFilter],
|
||||
companyMockObjectMetadataItem.fields,
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
@ -88,9 +89,10 @@ describe('turnFiltersIntoQueryFilter', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const result = turnFiltersIntoQueryFilter(
|
||||
const result = computeViewRecordGqlOperationFilter(
|
||||
[nameFilter, employeesFilter],
|
||||
companyMockObjectMetadataItem.fields,
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
@ -173,7 +175,7 @@ describe('should work as expected for the different field types', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const result = turnFiltersIntoQueryFilter(
|
||||
const result = computeViewRecordGqlOperationFilter(
|
||||
[
|
||||
addressFilterContains,
|
||||
addressFilterDoesNotContain,
|
||||
@ -181,6 +183,7 @@ describe('should work as expected for the different field types', () => {
|
||||
addressFilterIsNotEmpty,
|
||||
],
|
||||
companyMockObjectMetadataItem.fields,
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
@ -554,7 +557,7 @@ describe('should work as expected for the different field types', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const result = turnFiltersIntoQueryFilter(
|
||||
const result = computeViewRecordGqlOperationFilter(
|
||||
[
|
||||
phonesFilterContains,
|
||||
phonesFilterDoesNotContain,
|
||||
@ -562,6 +565,7 @@ describe('should work as expected for the different field types', () => {
|
||||
phonesFilterIsNotEmpty,
|
||||
],
|
||||
personMockObjectMetadataItem.fields,
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
@ -754,7 +758,7 @@ describe('should work as expected for the different field types', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const result = turnFiltersIntoQueryFilter(
|
||||
const result = computeViewRecordGqlOperationFilter(
|
||||
[
|
||||
emailsFilterContains,
|
||||
emailsFilterDoesNotContain,
|
||||
@ -762,6 +766,7 @@ describe('should work as expected for the different field types', () => {
|
||||
emailsFilterIsNotEmpty,
|
||||
],
|
||||
personMockObjectMetadataItem.fields,
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
@ -908,7 +913,7 @@ describe('should work as expected for the different field types', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const result = turnFiltersIntoQueryFilter(
|
||||
const result = computeViewRecordGqlOperationFilter(
|
||||
[
|
||||
dateFilterIsAfter,
|
||||
dateFilterIsBefore,
|
||||
@ -917,6 +922,7 @@ describe('should work as expected for the different field types', () => {
|
||||
dateFilterIsNotEmpty,
|
||||
],
|
||||
companyMockObjectMetadataItem.fields,
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
@ -1023,7 +1029,7 @@ describe('should work as expected for the different field types', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const result = turnFiltersIntoQueryFilter(
|
||||
const result = computeViewRecordGqlOperationFilter(
|
||||
[
|
||||
employeesFilterIsGreaterThan,
|
||||
employeesFilterIsLessThan,
|
||||
@ -1031,6 +1037,7 @@ describe('should work as expected for the different field types', () => {
|
||||
employeesFilterIsNotEmpty,
|
||||
],
|
||||
companyMockObjectMetadataItem.fields,
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
@ -0,0 +1,903 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import {
|
||||
ActorFilter,
|
||||
AddressFilter,
|
||||
CurrencyFilter,
|
||||
DateFilter,
|
||||
EmailsFilter,
|
||||
FloatFilter,
|
||||
RawJsonFilter,
|
||||
RecordGqlOperationFilter,
|
||||
RelationFilter,
|
||||
StringFilter,
|
||||
UUIDFilter,
|
||||
} from '@/object-record/graphql/types/RecordGqlOperationFilter';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { Field } from '~/generated/graphql';
|
||||
import { generateILikeFiltersForCompositeFields } from '~/utils/array/generateILikeFiltersForCompositeFields';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import {
|
||||
convertGreaterThanRatingToArrayOfRatingValues,
|
||||
convertLessThanRatingToArrayOfRatingValues,
|
||||
convertRatingToRatingValue,
|
||||
} from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput';
|
||||
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
|
||||
import { getEmptyRecordGqlOperationFilter } from '@/object-record/record-filter/utils/getEmptyRecordGqlOperationFilter';
|
||||
import { ViewFilterGroup } from '@/views/types/ViewFilterGroup';
|
||||
import { ViewFilterGroupLogicalOperator } from '@/views/types/ViewFilterGroupLogicalOperator';
|
||||
import { resolveFilterValue } from '@/views/view-filter-value/utils/resolveFilterValue';
|
||||
import { endOfDay, roundToNearestMinutes, startOfDay } from 'date-fns';
|
||||
import { z } from 'zod';
|
||||
|
||||
const computeFilterRecordGqlOperationFilter = (
|
||||
filter: Filter,
|
||||
fields: Pick<Field, 'id' | 'name'>[],
|
||||
): RecordGqlOperationFilter | undefined => {
|
||||
const correspondingField = fields.find(
|
||||
(field) => field.id === filter.fieldMetadataId,
|
||||
);
|
||||
|
||||
const compositeFieldName = filter.definition.compositeFieldName;
|
||||
|
||||
const isCompositeFieldFiter = isNonEmptyString(compositeFieldName);
|
||||
|
||||
const isEmptyOperand = [
|
||||
ViewFilterOperand.IsEmpty,
|
||||
ViewFilterOperand.IsNotEmpty,
|
||||
ViewFilterOperand.IsInPast,
|
||||
ViewFilterOperand.IsInFuture,
|
||||
ViewFilterOperand.IsToday,
|
||||
].includes(filter.operand);
|
||||
|
||||
if (!correspondingField) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isEmptyOperand) {
|
||||
if (!isDefined(filter.value) || filter.value === '') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
switch (filter.definition.type) {
|
||||
case 'TEXT':
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.Contains:
|
||||
return {
|
||||
[correspondingField.name]: {
|
||||
ilike: `%${filter.value}%`,
|
||||
} as StringFilter,
|
||||
};
|
||||
case ViewFilterOperand.DoesNotContain:
|
||||
return {
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
ilike: `%${filter.value}%`,
|
||||
} as StringFilter,
|
||||
},
|
||||
};
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
return getEmptyRecordGqlOperationFilter(
|
||||
filter.operand,
|
||||
correspondingField,
|
||||
filter.definition,
|
||||
);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${filter.operand} for ${filter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
case 'RAW_JSON':
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.Contains:
|
||||
return {
|
||||
[correspondingField.name]: {
|
||||
like: `%${filter.value}%`,
|
||||
} as RawJsonFilter,
|
||||
};
|
||||
case ViewFilterOperand.DoesNotContain:
|
||||
return {
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
like: `%${filter.value}%`,
|
||||
} as RawJsonFilter,
|
||||
},
|
||||
};
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
return getEmptyRecordGqlOperationFilter(
|
||||
filter.operand,
|
||||
correspondingField,
|
||||
filter.definition,
|
||||
);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${filter.operand} for ${filter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
case 'DATE':
|
||||
case 'DATE_TIME': {
|
||||
const resolvedFilterValue = resolveFilterValue(filter);
|
||||
const now = roundToNearestMinutes(new Date());
|
||||
const date =
|
||||
resolvedFilterValue instanceof Date ? resolvedFilterValue : now;
|
||||
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.IsAfter: {
|
||||
return {
|
||||
[correspondingField.name]: {
|
||||
gt: date.toISOString(),
|
||||
} as DateFilter,
|
||||
};
|
||||
}
|
||||
case ViewFilterOperand.IsBefore: {
|
||||
return {
|
||||
[correspondingField.name]: {
|
||||
lt: date.toISOString(),
|
||||
} as DateFilter,
|
||||
};
|
||||
}
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty: {
|
||||
return getEmptyRecordGqlOperationFilter(
|
||||
filter.operand,
|
||||
correspondingField,
|
||||
filter.definition,
|
||||
);
|
||||
}
|
||||
case ViewFilterOperand.IsRelative: {
|
||||
const dateRange = z
|
||||
.object({ start: z.date(), end: z.date() })
|
||||
.safeParse(resolvedFilterValue).data;
|
||||
|
||||
const defaultDateRange = resolveFilterValue({
|
||||
value: 'PAST_1_DAY',
|
||||
definition: {
|
||||
type: 'DATE',
|
||||
},
|
||||
operand: ViewFilterOperand.IsRelative,
|
||||
});
|
||||
|
||||
if (!defaultDateRange) {
|
||||
throw new Error('Failed to resolve default date range');
|
||||
}
|
||||
|
||||
const { start, end } = dateRange ?? defaultDateRange;
|
||||
|
||||
return {
|
||||
and: [
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
gte: start.toISOString(),
|
||||
} as DateFilter,
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
lte: end.toISOString(),
|
||||
} as DateFilter,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
case ViewFilterOperand.Is: {
|
||||
const isValid = resolvedFilterValue instanceof Date;
|
||||
const date = isValid ? resolvedFilterValue : now;
|
||||
|
||||
return {
|
||||
and: [
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
lte: endOfDay(date).toISOString(),
|
||||
} as DateFilter,
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
gte: startOfDay(date).toISOString(),
|
||||
} as DateFilter,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
case ViewFilterOperand.IsInPast:
|
||||
return {
|
||||
[correspondingField.name]: {
|
||||
lte: now.toISOString(),
|
||||
} as DateFilter,
|
||||
};
|
||||
case ViewFilterOperand.IsInFuture:
|
||||
return {
|
||||
[correspondingField.name]: {
|
||||
gte: now.toISOString(),
|
||||
} as DateFilter,
|
||||
};
|
||||
case ViewFilterOperand.IsToday: {
|
||||
return {
|
||||
and: [
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
lte: endOfDay(now).toISOString(),
|
||||
} as DateFilter,
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
gte: startOfDay(now).toISOString(),
|
||||
} as DateFilter,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${filter.operand} for ${filter.definition.type} filter`, //
|
||||
);
|
||||
}
|
||||
}
|
||||
case 'RATING':
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.Is:
|
||||
return {
|
||||
[correspondingField.name]: {
|
||||
eq: convertRatingToRatingValue(parseFloat(filter.value)),
|
||||
} as StringFilter,
|
||||
};
|
||||
case ViewFilterOperand.GreaterThan:
|
||||
return {
|
||||
[correspondingField.name]: {
|
||||
in: convertGreaterThanRatingToArrayOfRatingValues(
|
||||
parseFloat(filter.value),
|
||||
),
|
||||
} as StringFilter,
|
||||
};
|
||||
case ViewFilterOperand.LessThan:
|
||||
return {
|
||||
[correspondingField.name]: {
|
||||
in: convertLessThanRatingToArrayOfRatingValues(
|
||||
parseFloat(filter.value),
|
||||
),
|
||||
} as StringFilter,
|
||||
};
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
return getEmptyRecordGqlOperationFilter(
|
||||
filter.operand,
|
||||
correspondingField,
|
||||
filter.definition,
|
||||
);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${filter.operand} for ${filter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
case 'NUMBER':
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.GreaterThan:
|
||||
return {
|
||||
[correspondingField.name]: {
|
||||
gte: parseFloat(filter.value),
|
||||
} as FloatFilter,
|
||||
};
|
||||
case ViewFilterOperand.LessThan:
|
||||
return {
|
||||
[correspondingField.name]: {
|
||||
lte: parseFloat(filter.value),
|
||||
} as FloatFilter,
|
||||
};
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
return getEmptyRecordGqlOperationFilter(
|
||||
filter.operand,
|
||||
correspondingField,
|
||||
filter.definition,
|
||||
);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${filter.operand} for ${filter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
case 'RELATION': {
|
||||
if (!isEmptyOperand) {
|
||||
try {
|
||||
JSON.parse(filter.value);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Cannot parse filter value for RELATION filter : "${filter.value}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const parsedRecordIds = JSON.parse(filter.value) as string[];
|
||||
|
||||
if (parsedRecordIds.length > 0) {
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.Is:
|
||||
return {
|
||||
[correspondingField.name + 'Id']: {
|
||||
in: parsedRecordIds,
|
||||
} as RelationFilter,
|
||||
};
|
||||
case ViewFilterOperand.IsNot:
|
||||
if (parsedRecordIds.length > 0) {
|
||||
return {
|
||||
not: {
|
||||
[correspondingField.name + 'Id']: {
|
||||
in: parsedRecordIds,
|
||||
} as RelationFilter,
|
||||
},
|
||||
};
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${filter.operand} for ${filter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
return getEmptyRecordGqlOperationFilter(
|
||||
filter.operand,
|
||||
correspondingField,
|
||||
filter.definition,
|
||||
);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown empty operand ${filter.operand} for ${filter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'CURRENCY':
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.GreaterThan:
|
||||
return {
|
||||
[correspondingField.name]: {
|
||||
amountMicros: { gte: parseFloat(filter.value) * 1000000 },
|
||||
} as CurrencyFilter,
|
||||
};
|
||||
case ViewFilterOperand.LessThan:
|
||||
return {
|
||||
[correspondingField.name]: {
|
||||
amountMicros: { lte: parseFloat(filter.value) * 1000000 },
|
||||
} as CurrencyFilter,
|
||||
};
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
return getEmptyRecordGqlOperationFilter(
|
||||
filter.operand,
|
||||
correspondingField,
|
||||
filter.definition,
|
||||
);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${filter.operand} for ${filter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
case 'LINKS': {
|
||||
const linksFilters = generateILikeFiltersForCompositeFields(
|
||||
filter.value,
|
||||
correspondingField.name,
|
||||
['primaryLinkLabel', 'primaryLinkUrl'],
|
||||
);
|
||||
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.Contains:
|
||||
if (!isCompositeFieldFiter) {
|
||||
return {
|
||||
or: linksFilters,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
[correspondingField.name]: {
|
||||
[compositeFieldName]: {
|
||||
ilike: `%${filter.value}%`,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case ViewFilterOperand.DoesNotContain:
|
||||
if (!isCompositeFieldFiter) {
|
||||
return {
|
||||
and: linksFilters.map((filter) => {
|
||||
return {
|
||||
not: filter,
|
||||
};
|
||||
}),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
[compositeFieldName]: {
|
||||
ilike: `%${filter.value}%`,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
return getEmptyRecordGqlOperationFilter(
|
||||
filter.operand,
|
||||
correspondingField,
|
||||
filter.definition,
|
||||
);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${filter.operand} for ${filter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
}
|
||||
case 'FULL_NAME': {
|
||||
const fullNameFilters = generateILikeFiltersForCompositeFields(
|
||||
filter.value,
|
||||
correspondingField.name,
|
||||
['firstName', 'lastName'],
|
||||
);
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.Contains:
|
||||
if (!isCompositeFieldFiter) {
|
||||
return {
|
||||
or: fullNameFilters,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
[correspondingField.name]: {
|
||||
[compositeFieldName]: {
|
||||
ilike: `%${filter.value}%`,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case ViewFilterOperand.DoesNotContain:
|
||||
if (!isCompositeFieldFiter) {
|
||||
return {
|
||||
and: fullNameFilters.map((filter) => {
|
||||
return {
|
||||
not: filter,
|
||||
};
|
||||
}),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
[compositeFieldName]: {
|
||||
ilike: `%${filter.value}%`,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
return getEmptyRecordGqlOperationFilter(
|
||||
filter.operand,
|
||||
correspondingField,
|
||||
filter.definition,
|
||||
);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${filter.operand} for ${filter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
}
|
||||
case 'ADDRESS':
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.Contains:
|
||||
if (!isCompositeFieldFiter) {
|
||||
return {
|
||||
or: [
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
addressStreet1: {
|
||||
ilike: `%${filter.value}%`,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
addressStreet2: {
|
||||
ilike: `%${filter.value}%`,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
addressCity: {
|
||||
ilike: `%${filter.value}%`,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
addressState: {
|
||||
ilike: `%${filter.value}%`,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
addressCountry: {
|
||||
ilike: `%${filter.value}%`,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
addressPostcode: {
|
||||
ilike: `%${filter.value}%`,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
[correspondingField.name]: {
|
||||
[compositeFieldName]: {
|
||||
ilike: `%${filter.value}%`,
|
||||
} as AddressFilter,
|
||||
},
|
||||
};
|
||||
}
|
||||
case ViewFilterOperand.DoesNotContain:
|
||||
if (!isCompositeFieldFiter) {
|
||||
return {
|
||||
and: [
|
||||
{
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
addressStreet1: {
|
||||
ilike: `%${filter.value}%`,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
},
|
||||
{
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
addressStreet2: {
|
||||
ilike: `%${filter.value}%`,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
},
|
||||
{
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
addressCity: {
|
||||
ilike: `%${filter.value}%`,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
[compositeFieldName]: {
|
||||
ilike: `%${filter.value}%`,
|
||||
} as AddressFilter,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
return getEmptyRecordGqlOperationFilter(
|
||||
filter.operand,
|
||||
correspondingField,
|
||||
filter.definition,
|
||||
);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${filter.operand} for ${filter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
case 'SELECT': {
|
||||
if (isEmptyOperand) {
|
||||
return getEmptyRecordGqlOperationFilter(
|
||||
filter.operand,
|
||||
correspondingField,
|
||||
filter.definition,
|
||||
);
|
||||
}
|
||||
const stringifiedSelectValues = filter.value;
|
||||
let parsedOptionValues: string[] = [];
|
||||
|
||||
if (!isNonEmptyString(stringifiedSelectValues)) {
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
parsedOptionValues = JSON.parse(stringifiedSelectValues);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Cannot parse filter value for SELECT filter : "${stringifiedSelectValues}"`,
|
||||
);
|
||||
}
|
||||
|
||||
if (parsedOptionValues.length > 0) {
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.Is:
|
||||
return {
|
||||
[correspondingField.name]: {
|
||||
in: parsedOptionValues,
|
||||
} as UUIDFilter,
|
||||
};
|
||||
case ViewFilterOperand.IsNot:
|
||||
return {
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
in: parsedOptionValues,
|
||||
} as UUIDFilter,
|
||||
},
|
||||
};
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${filter.operand} for ${filter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
// TODO: fix this with a new composite field in ViewFilter entity
|
||||
case 'ACTOR': {
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.Is: {
|
||||
const parsedRecordIds = JSON.parse(filter.value) as string[];
|
||||
|
||||
return {
|
||||
[correspondingField.name]: {
|
||||
source: {
|
||||
in: parsedRecordIds,
|
||||
} as RelationFilter,
|
||||
},
|
||||
};
|
||||
}
|
||||
case ViewFilterOperand.IsNot: {
|
||||
const parsedRecordIds = JSON.parse(filter.value) as string[];
|
||||
|
||||
if (parsedRecordIds.length > 0) {
|
||||
return {
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
source: {
|
||||
in: parsedRecordIds,
|
||||
} as RelationFilter,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ViewFilterOperand.Contains:
|
||||
return {
|
||||
or: [
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
name: {
|
||||
ilike: `%${filter.value}%`,
|
||||
},
|
||||
} as ActorFilter,
|
||||
},
|
||||
],
|
||||
};
|
||||
case ViewFilterOperand.DoesNotContain:
|
||||
return {
|
||||
and: [
|
||||
{
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
name: {
|
||||
ilike: `%${filter.value}%`,
|
||||
},
|
||||
} as ActorFilter,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
return getEmptyRecordGqlOperationFilter(
|
||||
filter.operand,
|
||||
correspondingField,
|
||||
filter.definition,
|
||||
);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${filter.operand} for ${filter.definition.label} filter`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'EMAILS':
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.Contains:
|
||||
return {
|
||||
or: [
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
primaryEmail: {
|
||||
ilike: `%${filter.value}%`,
|
||||
},
|
||||
} as EmailsFilter,
|
||||
},
|
||||
],
|
||||
};
|
||||
case ViewFilterOperand.DoesNotContain:
|
||||
return {
|
||||
and: [
|
||||
{
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
primaryEmail: {
|
||||
ilike: `%${filter.value}%`,
|
||||
},
|
||||
} as EmailsFilter,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
return getEmptyRecordGqlOperationFilter(
|
||||
filter.operand,
|
||||
correspondingField,
|
||||
filter.definition,
|
||||
);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${filter.operand} for ${filter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
case 'PHONES': {
|
||||
const phonesFilters = generateILikeFiltersForCompositeFields(
|
||||
filter.value,
|
||||
correspondingField.name,
|
||||
['primaryPhoneNumber', 'primaryPhoneCountryCode'],
|
||||
);
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.Contains:
|
||||
return {
|
||||
or: phonesFilters,
|
||||
};
|
||||
case ViewFilterOperand.DoesNotContain:
|
||||
return {
|
||||
and: phonesFilters.map((filter) => {
|
||||
return {
|
||||
not: filter,
|
||||
};
|
||||
}),
|
||||
};
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
return getEmptyRecordGqlOperationFilter(
|
||||
filter.operand,
|
||||
correspondingField,
|
||||
filter.definition,
|
||||
);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${filter.operand} for ${filter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
}
|
||||
default:
|
||||
throw new Error('Unknown filter type');
|
||||
}
|
||||
};
|
||||
|
||||
const computeViewFilterGroupRecordGqlOperationFilter = (
|
||||
filters: Filter[],
|
||||
fields: Pick<Field, 'id' | 'name'>[],
|
||||
viewFilterGroups: ViewFilterGroup[],
|
||||
currentViewFilterGroupId?: string,
|
||||
): RecordGqlOperationFilter | undefined => {
|
||||
const currentViewFilterGroup = viewFilterGroups.find(
|
||||
(viewFilterGroup) => viewFilterGroup.id === currentViewFilterGroupId,
|
||||
);
|
||||
|
||||
if (!currentViewFilterGroup) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const groupFilters = filters.filter(
|
||||
(filter) => filter.viewFilterGroupId === currentViewFilterGroupId,
|
||||
);
|
||||
|
||||
const groupRecordGqlOperationFilters = groupFilters
|
||||
.map((filter) => computeFilterRecordGqlOperationFilter(filter, fields))
|
||||
.filter(isDefined);
|
||||
|
||||
const subGroupRecordGqlOperationFilters = viewFilterGroups
|
||||
.filter(
|
||||
(viewFilterGroup) =>
|
||||
viewFilterGroup.parentViewFilterGroupId === currentViewFilterGroupId,
|
||||
)
|
||||
.map((subViewFilterGroup) =>
|
||||
computeViewFilterGroupRecordGqlOperationFilter(
|
||||
filters,
|
||||
fields,
|
||||
viewFilterGroups,
|
||||
subViewFilterGroup.id,
|
||||
),
|
||||
)
|
||||
.filter(isDefined);
|
||||
|
||||
if (
|
||||
currentViewFilterGroup.logicalOperator ===
|
||||
ViewFilterGroupLogicalOperator.AND
|
||||
) {
|
||||
return {
|
||||
and: [
|
||||
...groupRecordGqlOperationFilters,
|
||||
...subGroupRecordGqlOperationFilters,
|
||||
],
|
||||
};
|
||||
} else if (
|
||||
currentViewFilterGroup.logicalOperator === ViewFilterGroupLogicalOperator.OR
|
||||
) {
|
||||
return {
|
||||
or: [
|
||||
...groupRecordGqlOperationFilters,
|
||||
...subGroupRecordGqlOperationFilters,
|
||||
],
|
||||
};
|
||||
} else {
|
||||
throw new Error(
|
||||
`Unknown logical operator ${currentViewFilterGroup.logicalOperator}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const computeViewRecordGqlOperationFilter = (
|
||||
filters: Filter[],
|
||||
fields: Pick<Field, 'id' | 'name'>[],
|
||||
viewFilterGroups: ViewFilterGroup[],
|
||||
): RecordGqlOperationFilter => {
|
||||
const regularRecordGqlOperationFilter: RecordGqlOperationFilter[] = filters
|
||||
.filter((filter) => !filter.viewFilterGroupId)
|
||||
.map((regularFilter) =>
|
||||
computeFilterRecordGqlOperationFilter(regularFilter, fields),
|
||||
)
|
||||
.filter(isDefined);
|
||||
|
||||
const outermostFilterGroupId = viewFilterGroups.find(
|
||||
(viewFilterGroup) => !viewFilterGroup.parentViewFilterGroupId,
|
||||
)?.id;
|
||||
|
||||
const advancedRecordGqlOperationFilter =
|
||||
computeViewFilterGroupRecordGqlOperationFilter(
|
||||
filters,
|
||||
fields,
|
||||
viewFilterGroups,
|
||||
outermostFilterGroupId,
|
||||
);
|
||||
|
||||
const recordGqlOperationFilters = [
|
||||
...regularRecordGqlOperationFilter,
|
||||
advancedRecordGqlOperationFilter,
|
||||
].filter(isDefined);
|
||||
|
||||
if (recordGqlOperationFilters.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (recordGqlOperationFilters.length === 1) {
|
||||
return recordGqlOperationFilters[0];
|
||||
}
|
||||
|
||||
const recordGqlOperationFilter = {
|
||||
and: recordGqlOperationFilters,
|
||||
};
|
||||
|
||||
return recordGqlOperationFilter;
|
||||
};
|
||||
@ -19,11 +19,9 @@ import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { Field } from '~/generated/graphql';
|
||||
import { generateILikeFiltersForCompositeFields } from '~/utils/array/generateILikeFiltersForCompositeFields';
|
||||
|
||||
// TODO: fix this
|
||||
export const applyEmptyFilters = (
|
||||
export const getEmptyRecordGqlOperationFilter = (
|
||||
operand: ViewFilterOperand,
|
||||
correspondingField: Pick<Field, 'id' | 'name'>,
|
||||
objectRecordFilters: RecordGqlOperationFilter[],
|
||||
definition: FilterDefinition,
|
||||
) => {
|
||||
let emptyRecordFilter: RecordGqlOperationFilter = {};
|
||||
@ -332,13 +330,11 @@ export const applyEmptyFilters = (
|
||||
|
||||
switch (operand) {
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
objectRecordFilters.push(emptyRecordFilter);
|
||||
break;
|
||||
return emptyRecordFilter;
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
objectRecordFilters.push({
|
||||
return {
|
||||
not: emptyRecordFilter,
|
||||
});
|
||||
break;
|
||||
};
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${operand} for ${definition.type} filter`,
|
||||
@ -1,913 +0,0 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import {
|
||||
ActorFilter,
|
||||
AddressFilter,
|
||||
ArrayFilter,
|
||||
CurrencyFilter,
|
||||
DateFilter,
|
||||
EmailsFilter,
|
||||
FloatFilter,
|
||||
RawJsonFilter,
|
||||
RecordGqlOperationFilter,
|
||||
RelationFilter,
|
||||
StringFilter,
|
||||
UUIDFilter,
|
||||
} from '@/object-record/graphql/types/RecordGqlOperationFilter';
|
||||
import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { Field } from '~/generated/graphql';
|
||||
import { generateILikeFiltersForCompositeFields } from '~/utils/array/generateILikeFiltersForCompositeFields';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import {
|
||||
convertGreaterThanRatingToArrayOfRatingValues,
|
||||
convertLessThanRatingToArrayOfRatingValues,
|
||||
convertRatingToRatingValue,
|
||||
} from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput';
|
||||
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
|
||||
import { applyEmptyFilters } from '@/object-record/record-filter/utils/applyEmptyFilters';
|
||||
import { resolveFilterValue } from '@/views/view-filter-value/utils/resolveFilterValue';
|
||||
import { endOfDay, roundToNearestMinutes, startOfDay } from 'date-fns';
|
||||
import { z } from 'zod';
|
||||
|
||||
// TODO: break this down into smaller functions and make the whole thing immutable
|
||||
// Especially applyEmptyFilters
|
||||
export const turnFiltersIntoQueryFilter = (
|
||||
rawUIFilters: Filter[],
|
||||
fields: Pick<Field, 'id' | 'name'>[],
|
||||
): RecordGqlOperationFilter | undefined => {
|
||||
const objectRecordFilters: RecordGqlOperationFilter[] = [];
|
||||
|
||||
for (const rawUIFilter of rawUIFilters) {
|
||||
const correspondingField = fields.find(
|
||||
(field) => field.id === rawUIFilter.fieldMetadataId,
|
||||
);
|
||||
|
||||
const compositeFieldName = rawUIFilter.definition.compositeFieldName;
|
||||
|
||||
const isCompositeFieldFiter = isNonEmptyString(compositeFieldName);
|
||||
|
||||
const isEmptyOperand = [
|
||||
ViewFilterOperand.IsEmpty,
|
||||
ViewFilterOperand.IsNotEmpty,
|
||||
ViewFilterOperand.IsInPast,
|
||||
ViewFilterOperand.IsInFuture,
|
||||
ViewFilterOperand.IsToday,
|
||||
].includes(rawUIFilter.operand);
|
||||
|
||||
if (!correspondingField) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isEmptyOperand) {
|
||||
if (!isDefined(rawUIFilter.value) || rawUIFilter.value === '') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
switch (rawUIFilter.definition.type) {
|
||||
case 'TEXT':
|
||||
switch (rawUIFilter.operand) {
|
||||
case ViewFilterOperand.Contains:
|
||||
objectRecordFilters.push({
|
||||
[correspondingField.name]: {
|
||||
ilike: `%${rawUIFilter.value}%`,
|
||||
} as StringFilter,
|
||||
});
|
||||
break;
|
||||
case ViewFilterOperand.DoesNotContain:
|
||||
objectRecordFilters.push({
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
ilike: `%${rawUIFilter.value}%`,
|
||||
} as StringFilter,
|
||||
},
|
||||
});
|
||||
break;
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
applyEmptyFilters(
|
||||
rawUIFilter.operand,
|
||||
correspondingField,
|
||||
objectRecordFilters,
|
||||
rawUIFilter.definition,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'RAW_JSON':
|
||||
switch (rawUIFilter.operand) {
|
||||
case ViewFilterOperand.Contains:
|
||||
objectRecordFilters.push({
|
||||
[correspondingField.name]: {
|
||||
like: `%${rawUIFilter.value}%`,
|
||||
} as RawJsonFilter,
|
||||
});
|
||||
break;
|
||||
case ViewFilterOperand.DoesNotContain:
|
||||
objectRecordFilters.push({
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
like: `%${rawUIFilter.value}%`,
|
||||
} as RawJsonFilter,
|
||||
},
|
||||
});
|
||||
break;
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
applyEmptyFilters(
|
||||
rawUIFilter.operand,
|
||||
correspondingField,
|
||||
objectRecordFilters,
|
||||
rawUIFilter.definition,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'DATE':
|
||||
case 'DATE_TIME': {
|
||||
const resolvedFilterValue = resolveFilterValue(rawUIFilter);
|
||||
const now = roundToNearestMinutes(new Date());
|
||||
const date =
|
||||
resolvedFilterValue instanceof Date ? resolvedFilterValue : now;
|
||||
|
||||
switch (rawUIFilter.operand) {
|
||||
case ViewFilterOperand.IsAfter: {
|
||||
objectRecordFilters.push({
|
||||
[correspondingField.name]: {
|
||||
gt: date.toISOString(),
|
||||
} as DateFilter,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ViewFilterOperand.IsBefore: {
|
||||
objectRecordFilters.push({
|
||||
[correspondingField.name]: {
|
||||
lt: date.toISOString(),
|
||||
} as DateFilter,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty: {
|
||||
applyEmptyFilters(
|
||||
rawUIFilter.operand,
|
||||
correspondingField,
|
||||
objectRecordFilters,
|
||||
rawUIFilter.definition,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case ViewFilterOperand.IsRelative: {
|
||||
const dateRange = z
|
||||
.object({ start: z.date(), end: z.date() })
|
||||
.safeParse(resolvedFilterValue).data;
|
||||
|
||||
const defaultDateRange = resolveFilterValue({
|
||||
value: 'PAST_1_DAY',
|
||||
definition: {
|
||||
type: 'DATE',
|
||||
},
|
||||
operand: ViewFilterOperand.IsRelative,
|
||||
});
|
||||
|
||||
if (!defaultDateRange) {
|
||||
throw new Error('Failed to resolve default date range');
|
||||
}
|
||||
|
||||
const { start, end } = dateRange ?? defaultDateRange;
|
||||
|
||||
objectRecordFilters.push({
|
||||
and: [
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
gte: start.toISOString(),
|
||||
} as DateFilter,
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
lte: end.toISOString(),
|
||||
} as DateFilter,
|
||||
},
|
||||
],
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ViewFilterOperand.Is: {
|
||||
const isValid = resolvedFilterValue instanceof Date;
|
||||
const date = isValid ? resolvedFilterValue : now;
|
||||
|
||||
objectRecordFilters.push({
|
||||
and: [
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
lte: endOfDay(date).toISOString(),
|
||||
} as DateFilter,
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
gte: startOfDay(date).toISOString(),
|
||||
} as DateFilter,
|
||||
},
|
||||
],
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ViewFilterOperand.IsInPast:
|
||||
objectRecordFilters.push({
|
||||
[correspondingField.name]: {
|
||||
lte: now.toISOString(),
|
||||
} as DateFilter,
|
||||
});
|
||||
break;
|
||||
case ViewFilterOperand.IsInFuture:
|
||||
objectRecordFilters.push({
|
||||
[correspondingField.name]: {
|
||||
gte: now.toISOString(),
|
||||
} as DateFilter,
|
||||
});
|
||||
break;
|
||||
case ViewFilterOperand.IsToday: {
|
||||
objectRecordFilters.push({
|
||||
and: [
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
lte: endOfDay(now).toISOString(),
|
||||
} as DateFilter,
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
gte: startOfDay(now).toISOString(),
|
||||
} as DateFilter,
|
||||
},
|
||||
],
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`, //
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'RATING':
|
||||
switch (rawUIFilter.operand) {
|
||||
case ViewFilterOperand.Is:
|
||||
objectRecordFilters.push({
|
||||
[correspondingField.name]: {
|
||||
eq: convertRatingToRatingValue(parseFloat(rawUIFilter.value)),
|
||||
} as StringFilter,
|
||||
});
|
||||
break;
|
||||
case ViewFilterOperand.GreaterThan:
|
||||
objectRecordFilters.push({
|
||||
[correspondingField.name]: {
|
||||
in: convertGreaterThanRatingToArrayOfRatingValues(
|
||||
parseFloat(rawUIFilter.value),
|
||||
),
|
||||
} as StringFilter,
|
||||
});
|
||||
break;
|
||||
case ViewFilterOperand.LessThan:
|
||||
objectRecordFilters.push({
|
||||
[correspondingField.name]: {
|
||||
in: convertLessThanRatingToArrayOfRatingValues(
|
||||
parseFloat(rawUIFilter.value),
|
||||
),
|
||||
} as StringFilter,
|
||||
});
|
||||
break;
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
applyEmptyFilters(
|
||||
rawUIFilter.operand,
|
||||
correspondingField,
|
||||
objectRecordFilters,
|
||||
rawUIFilter.definition,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'NUMBER':
|
||||
switch (rawUIFilter.operand) {
|
||||
case ViewFilterOperand.GreaterThan:
|
||||
objectRecordFilters.push({
|
||||
[correspondingField.name]: {
|
||||
gte: parseFloat(rawUIFilter.value),
|
||||
} as FloatFilter,
|
||||
});
|
||||
break;
|
||||
case ViewFilterOperand.LessThan:
|
||||
objectRecordFilters.push({
|
||||
[correspondingField.name]: {
|
||||
lte: parseFloat(rawUIFilter.value),
|
||||
} as FloatFilter,
|
||||
});
|
||||
break;
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
applyEmptyFilters(
|
||||
rawUIFilter.operand,
|
||||
correspondingField,
|
||||
objectRecordFilters,
|
||||
rawUIFilter.definition,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'RELATION': {
|
||||
if (!isEmptyOperand) {
|
||||
try {
|
||||
JSON.parse(rawUIFilter.value);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Cannot parse filter value for RELATION filter : "${rawUIFilter.value}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const parsedRecordIds = JSON.parse(rawUIFilter.value) as string[];
|
||||
|
||||
if (parsedRecordIds.length > 0) {
|
||||
switch (rawUIFilter.operand) {
|
||||
case ViewFilterOperand.Is:
|
||||
objectRecordFilters.push({
|
||||
[correspondingField.name + 'Id']: {
|
||||
in: parsedRecordIds,
|
||||
} as RelationFilter,
|
||||
});
|
||||
break;
|
||||
case ViewFilterOperand.IsNot:
|
||||
if (parsedRecordIds.length > 0) {
|
||||
objectRecordFilters.push({
|
||||
not: {
|
||||
[correspondingField.name + 'Id']: {
|
||||
in: parsedRecordIds,
|
||||
} as RelationFilter,
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch (rawUIFilter.operand) {
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
applyEmptyFilters(
|
||||
rawUIFilter.operand,
|
||||
correspondingField,
|
||||
objectRecordFilters,
|
||||
rawUIFilter.definition,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown empty operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'CURRENCY':
|
||||
switch (rawUIFilter.operand) {
|
||||
case ViewFilterOperand.GreaterThan:
|
||||
objectRecordFilters.push({
|
||||
[correspondingField.name]: {
|
||||
amountMicros: { gte: parseFloat(rawUIFilter.value) * 1000000 },
|
||||
} as CurrencyFilter,
|
||||
});
|
||||
break;
|
||||
case ViewFilterOperand.LessThan:
|
||||
objectRecordFilters.push({
|
||||
[correspondingField.name]: {
|
||||
amountMicros: { lte: parseFloat(rawUIFilter.value) * 1000000 },
|
||||
} as CurrencyFilter,
|
||||
});
|
||||
break;
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
applyEmptyFilters(
|
||||
rawUIFilter.operand,
|
||||
correspondingField,
|
||||
objectRecordFilters,
|
||||
rawUIFilter.definition,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'LINKS': {
|
||||
const linksFilters = generateILikeFiltersForCompositeFields(
|
||||
rawUIFilter.value,
|
||||
correspondingField.name,
|
||||
['primaryLinkLabel', 'primaryLinkUrl'],
|
||||
);
|
||||
|
||||
switch (rawUIFilter.operand) {
|
||||
case ViewFilterOperand.Contains:
|
||||
if (!isCompositeFieldFiter) {
|
||||
objectRecordFilters.push({
|
||||
or: linksFilters,
|
||||
});
|
||||
} else {
|
||||
objectRecordFilters.push({
|
||||
[correspondingField.name]: {
|
||||
[compositeFieldName]: {
|
||||
ilike: `%${rawUIFilter.value}%`,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
case ViewFilterOperand.DoesNotContain:
|
||||
if (!isCompositeFieldFiter) {
|
||||
objectRecordFilters.push({
|
||||
and: linksFilters.map((filter) => {
|
||||
return {
|
||||
not: filter,
|
||||
};
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
objectRecordFilters.push({
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
[compositeFieldName]: {
|
||||
ilike: `%${rawUIFilter.value}%`,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
applyEmptyFilters(
|
||||
rawUIFilter.operand,
|
||||
correspondingField,
|
||||
objectRecordFilters,
|
||||
rawUIFilter.definition,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'FULL_NAME': {
|
||||
const fullNameFilters = generateILikeFiltersForCompositeFields(
|
||||
rawUIFilter.value,
|
||||
correspondingField.name,
|
||||
['firstName', 'lastName'],
|
||||
);
|
||||
switch (rawUIFilter.operand) {
|
||||
case ViewFilterOperand.Contains:
|
||||
if (!isCompositeFieldFiter) {
|
||||
objectRecordFilters.push({
|
||||
or: fullNameFilters,
|
||||
});
|
||||
} else {
|
||||
objectRecordFilters.push({
|
||||
[correspondingField.name]: {
|
||||
[compositeFieldName]: {
|
||||
ilike: `%${rawUIFilter.value}%`,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
case ViewFilterOperand.DoesNotContain:
|
||||
if (!isCompositeFieldFiter) {
|
||||
objectRecordFilters.push({
|
||||
and: fullNameFilters.map((filter) => {
|
||||
return {
|
||||
not: filter,
|
||||
};
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
objectRecordFilters.push({
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
[compositeFieldName]: {
|
||||
ilike: `%${rawUIFilter.value}%`,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
applyEmptyFilters(
|
||||
rawUIFilter.operand,
|
||||
correspondingField,
|
||||
objectRecordFilters,
|
||||
rawUIFilter.definition,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ADDRESS':
|
||||
switch (rawUIFilter.operand) {
|
||||
case ViewFilterOperand.Contains:
|
||||
if (!isCompositeFieldFiter) {
|
||||
objectRecordFilters.push({
|
||||
or: [
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
addressStreet1: {
|
||||
ilike: `%${rawUIFilter.value}%`,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
addressStreet2: {
|
||||
ilike: `%${rawUIFilter.value}%`,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
addressCity: {
|
||||
ilike: `%${rawUIFilter.value}%`,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
addressState: {
|
||||
ilike: `%${rawUIFilter.value}%`,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
addressCountry: {
|
||||
ilike: `%${rawUIFilter.value}%`,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
addressPostcode: {
|
||||
ilike: `%${rawUIFilter.value}%`,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
objectRecordFilters.push({
|
||||
[correspondingField.name]: {
|
||||
[compositeFieldName]: {
|
||||
ilike: `%${rawUIFilter.value}%`,
|
||||
} as AddressFilter,
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
case ViewFilterOperand.DoesNotContain:
|
||||
if (!isCompositeFieldFiter) {
|
||||
objectRecordFilters.push({
|
||||
and: [
|
||||
{
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
addressStreet1: {
|
||||
ilike: `%${rawUIFilter.value}%`,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
},
|
||||
{
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
addressStreet2: {
|
||||
ilike: `%${rawUIFilter.value}%`,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
},
|
||||
{
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
addressCity: {
|
||||
ilike: `%${rawUIFilter.value}%`,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
objectRecordFilters.push({
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
[compositeFieldName]: {
|
||||
ilike: `%${rawUIFilter.value}%`,
|
||||
} as AddressFilter,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
applyEmptyFilters(
|
||||
rawUIFilter.operand,
|
||||
correspondingField,
|
||||
objectRecordFilters,
|
||||
rawUIFilter.definition,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'SELECT': {
|
||||
if (isEmptyOperand) {
|
||||
applyEmptyFilters(
|
||||
rawUIFilter.operand,
|
||||
correspondingField,
|
||||
objectRecordFilters,
|
||||
rawUIFilter.definition,
|
||||
);
|
||||
break;
|
||||
}
|
||||
const stringifiedSelectValues = rawUIFilter.value;
|
||||
let parsedOptionValues: string[] = [];
|
||||
|
||||
if (!isNonEmptyString(stringifiedSelectValues)) {
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
parsedOptionValues = JSON.parse(stringifiedSelectValues);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Cannot parse filter value for SELECT filter : "${stringifiedSelectValues}"`,
|
||||
);
|
||||
}
|
||||
|
||||
if (parsedOptionValues.length > 0) {
|
||||
switch (rawUIFilter.operand) {
|
||||
case ViewFilterOperand.Is:
|
||||
objectRecordFilters.push({
|
||||
[correspondingField.name]: {
|
||||
in: parsedOptionValues,
|
||||
} as UUIDFilter,
|
||||
});
|
||||
break;
|
||||
case ViewFilterOperand.IsNot:
|
||||
objectRecordFilters.push({
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
in: parsedOptionValues,
|
||||
} as UUIDFilter,
|
||||
},
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
// TODO: fix this with a new composite field in ViewFilter entity
|
||||
case 'ACTOR': {
|
||||
switch (rawUIFilter.operand) {
|
||||
case ViewFilterOperand.Is: {
|
||||
const parsedRecordIds = JSON.parse(rawUIFilter.value) as string[];
|
||||
|
||||
objectRecordFilters.push({
|
||||
[correspondingField.name]: {
|
||||
source: {
|
||||
in: parsedRecordIds,
|
||||
} as RelationFilter,
|
||||
},
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
case ViewFilterOperand.IsNot: {
|
||||
const parsedRecordIds = JSON.parse(rawUIFilter.value) as string[];
|
||||
|
||||
if (parsedRecordIds.length > 0) {
|
||||
objectRecordFilters.push({
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
source: {
|
||||
in: parsedRecordIds,
|
||||
} as RelationFilter,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ViewFilterOperand.Contains:
|
||||
objectRecordFilters.push({
|
||||
or: [
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
name: {
|
||||
ilike: `%${rawUIFilter.value}%`,
|
||||
},
|
||||
} as ActorFilter,
|
||||
},
|
||||
],
|
||||
});
|
||||
break;
|
||||
case ViewFilterOperand.DoesNotContain:
|
||||
objectRecordFilters.push({
|
||||
and: [
|
||||
{
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
name: {
|
||||
ilike: `%${rawUIFilter.value}%`,
|
||||
},
|
||||
} as ActorFilter,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
break;
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
applyEmptyFilters(
|
||||
rawUIFilter.operand,
|
||||
correspondingField,
|
||||
objectRecordFilters,
|
||||
rawUIFilter.definition,
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.label} filter`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'EMAILS':
|
||||
switch (rawUIFilter.operand) {
|
||||
case ViewFilterOperand.Contains:
|
||||
objectRecordFilters.push({
|
||||
or: [
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
primaryEmail: {
|
||||
ilike: `%${rawUIFilter.value}%`,
|
||||
},
|
||||
} as EmailsFilter,
|
||||
},
|
||||
],
|
||||
});
|
||||
break;
|
||||
case ViewFilterOperand.DoesNotContain:
|
||||
objectRecordFilters.push({
|
||||
and: [
|
||||
{
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
primaryEmail: {
|
||||
ilike: `%${rawUIFilter.value}%`,
|
||||
},
|
||||
} as EmailsFilter,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
break;
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
applyEmptyFilters(
|
||||
rawUIFilter.operand,
|
||||
correspondingField,
|
||||
objectRecordFilters,
|
||||
rawUIFilter.definition,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'PHONES': {
|
||||
const phonesFilters = generateILikeFiltersForCompositeFields(
|
||||
rawUIFilter.value,
|
||||
correspondingField.name,
|
||||
['primaryPhoneNumber', 'primaryPhoneCountryCode'],
|
||||
);
|
||||
switch (rawUIFilter.operand) {
|
||||
case ViewFilterOperand.Contains:
|
||||
objectRecordFilters.push({
|
||||
or: phonesFilters,
|
||||
});
|
||||
break;
|
||||
case ViewFilterOperand.DoesNotContain:
|
||||
objectRecordFilters.push({
|
||||
and: phonesFilters.map((filter) => {
|
||||
return {
|
||||
not: filter,
|
||||
};
|
||||
}),
|
||||
});
|
||||
break;
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
applyEmptyFilters(
|
||||
rawUIFilter.operand,
|
||||
correspondingField,
|
||||
objectRecordFilters,
|
||||
rawUIFilter.definition,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ARRAY': {
|
||||
switch (rawUIFilter.operand) {
|
||||
case ViewFilterOperand.Contains: {
|
||||
objectRecordFilters.push({
|
||||
[correspondingField.name]: {
|
||||
contains: [`${rawUIFilter.value}`],
|
||||
} as ArrayFilter,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ViewFilterOperand.DoesNotContain: {
|
||||
objectRecordFilters.push({
|
||||
[correspondingField.name]: {
|
||||
not_contains: [`${rawUIFilter.value}`],
|
||||
} as ArrayFilter,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
applyEmptyFilters(
|
||||
rawUIFilter.operand,
|
||||
correspondingField,
|
||||
objectRecordFilters,
|
||||
rawUIFilter.definition,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.label} filter`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error('Unknown filter type');
|
||||
}
|
||||
}
|
||||
|
||||
return makeAndFilterVariables(objectRecordFilters);
|
||||
};
|
||||
@ -26,6 +26,7 @@ import { RecordIndexActionMenu } from '@/action-menu/components/RecordIndexActio
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
|
||||
import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState';
|
||||
import { recordIndexViewFilterGroupsState } from '@/object-record/record-index/states/recordIndexViewFilterGroupsState';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { ViewBar } from '@/views/components/ViewBar';
|
||||
@ -72,6 +73,9 @@ export const RecordIndexContainer = () => {
|
||||
const { columnDefinitions, filterDefinitions, sortDefinitions } =
|
||||
useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
|
||||
|
||||
const setRecordIndexViewFilterGroups = useSetRecoilState(
|
||||
recordIndexViewFilterGroupsState,
|
||||
);
|
||||
const setRecordIndexFilters = useSetRecoilState(recordIndexFiltersState);
|
||||
const setRecordIndexSorts = useSetRecoilState(recordIndexSortsState);
|
||||
const setRecordIndexIsCompactModeActive = useSetRecoilState(
|
||||
@ -81,7 +85,12 @@ export const RecordIndexContainer = () => {
|
||||
recordIndexKanbanFieldMetadataIdState,
|
||||
);
|
||||
|
||||
const { setTableFilters, setTableSorts, setTableColumns } = useRecordTable({
|
||||
const {
|
||||
setTableViewFilterGroups,
|
||||
setTableFilters,
|
||||
setTableSorts,
|
||||
setTableColumns,
|
||||
} = useRecordTable({
|
||||
recordTableId: recordIndexId,
|
||||
});
|
||||
|
||||
@ -164,12 +173,14 @@ export const RecordIndexContainer = () => {
|
||||
|
||||
onViewFieldsChange(view.viewFields);
|
||||
onViewGroupsChange(view.viewGroups);
|
||||
setTableViewFilterGroups(view.viewFilterGroups ?? []);
|
||||
setTableFilters(
|
||||
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
|
||||
);
|
||||
setRecordIndexFilters(
|
||||
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
|
||||
);
|
||||
setRecordIndexViewFilterGroups(view.viewFilterGroups ?? []);
|
||||
setContextStoreTargetedRecordsRule((prev) => ({
|
||||
...prev,
|
||||
filters: mapViewFiltersToFilters(
|
||||
|
||||
@ -5,13 +5,14 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
|
||||
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
|
||||
import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter';
|
||||
import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter';
|
||||
import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState';
|
||||
import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields';
|
||||
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
|
||||
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
|
||||
import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState';
|
||||
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
|
||||
import { recordIndexViewFilterGroupsState } from '@/object-record/record-index/states/recordIndexViewFilterGroupsState';
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useSetRecordCountInCurrentView } from '@/views/hooks/useSetRecordCountInCurrentView';
|
||||
@ -45,18 +46,24 @@ export const useLoadRecordIndexBoard = ({
|
||||
setFieldDefinitions(recordIndexFieldDefinitions);
|
||||
}, [recordIndexFieldDefinitions, setFieldDefinitions]);
|
||||
|
||||
const recordIndexViewFilterGroups = useRecoilValue(
|
||||
recordIndexViewFilterGroupsState,
|
||||
);
|
||||
|
||||
const recordIndexGroupDefinitions = useRecoilComponentValueV2(
|
||||
recordGroupDefinitionsComponentState,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setColumns(recordIndexGroupDefinitions);
|
||||
}, [recordIndexGroupDefinitions, setColumns]);
|
||||
|
||||
const recordIndexFilters = useRecoilValue(recordIndexFiltersState);
|
||||
const recordIndexSorts = useRecoilValue(recordIndexSortsState);
|
||||
const requestFilters = turnFiltersIntoQueryFilter(
|
||||
const requestFilters = computeViewRecordGqlOperationFilter(
|
||||
recordIndexFilters,
|
||||
objectMetadataItem?.fields ?? [],
|
||||
recordIndexViewFilterGroups,
|
||||
);
|
||||
const orderBy = turnSortsIntoOrderBy(objectMetadataItem, recordIndexSorts);
|
||||
|
||||
|
||||
@ -4,14 +4,15 @@ import { useRecoilValue } from 'recoil';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
|
||||
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
|
||||
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
|
||||
import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter';
|
||||
import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter';
|
||||
import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields';
|
||||
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
|
||||
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
|
||||
import { recordIndexViewFilterGroupsState } from '@/object-record/record-index/states/recordIndexViewFilterGroupsState';
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
|
||||
|
||||
type UseLoadRecordIndexBoardProps = {
|
||||
objectNameSingular: string;
|
||||
@ -33,12 +34,17 @@ export const useLoadRecordIndexBoardColumn = ({
|
||||
const { columnsFamilySelector } = useRecordBoardStates(recordBoardId);
|
||||
const { upsertRecords: upsertRecordsInStore } = useUpsertRecordsInStore();
|
||||
|
||||
const recordIndexViewFilterGroups = useRecoilValue(
|
||||
recordIndexViewFilterGroupsState,
|
||||
);
|
||||
const recordIndexFilters = useRecoilValue(recordIndexFiltersState);
|
||||
const recordIndexSorts = useRecoilValue(recordIndexSortsState);
|
||||
const columnDefinition = useRecoilValue(columnsFamilySelector(columnId));
|
||||
const requestFilters = turnFiltersIntoQueryFilter(
|
||||
|
||||
const requestFilters = computeViewRecordGqlOperationFilter(
|
||||
recordIndexFilters,
|
||||
objectMetadataItem?.fields ?? [],
|
||||
recordIndexViewFilterGroups,
|
||||
);
|
||||
const orderBy = turnSortsIntoOrderBy(objectMetadataItem, recordIndexSorts);
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
|
||||
import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter';
|
||||
import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter';
|
||||
import { useRecordTableRecordGqlFields } from '@/object-record/record-index/hooks/useRecordTableRecordGqlFields';
|
||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
@ -21,15 +21,17 @@ export const useFindManyParams = (
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { tableFiltersState, tableSortsState } =
|
||||
const { tableFiltersState, tableSortsState, tableViewFilterGroupsState } =
|
||||
useRecordTableStates(recordTableId);
|
||||
|
||||
const tableViewFilterGroups = useRecoilValue(tableViewFilterGroupsState);
|
||||
const tableFilters = useRecoilValue(tableFiltersState);
|
||||
const tableSorts = useRecoilValue(tableSortsState);
|
||||
|
||||
const filter = turnFiltersIntoQueryFilter(
|
||||
const filter = computeViewRecordGqlOperationFilter(
|
||||
tableFilters,
|
||||
objectMetadataItem?.fields ?? [],
|
||||
tableViewFilterGroups,
|
||||
);
|
||||
|
||||
const orderBy = turnSortsIntoOrderBy(objectMetadataItem, tableSorts);
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
import { ViewFilterGroup } from '@/views/types/ViewFilterGroup';
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
export const recordIndexViewFilterGroupsState = createState<ViewFilterGroup[]>({
|
||||
key: 'recordIndexViewFilterGroupsState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -27,6 +27,7 @@ import { tableFiltersComponentState } from '@/object-record/record-table/states/
|
||||
import { tableLastRowVisibleComponentState } from '@/object-record/record-table/states/tableLastRowVisibleComponentState';
|
||||
import { tableRowIdsComponentState } from '@/object-record/record-table/states/tableRowIdsComponentState';
|
||||
import { tableSortsComponentState } from '@/object-record/record-table/states/tableSortsComponentState';
|
||||
import { tableViewFilterGroupsComponentState } from '@/object-record/record-table/states/tableViewFilterGroupsComponentState';
|
||||
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||
import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId';
|
||||
import { extractComponentFamilyState } from '@/ui/utilities/state/component-state/utils/extractComponentFamilyState';
|
||||
@ -45,6 +46,10 @@ export const useRecordTableStates = (recordTableId?: string) => {
|
||||
availableTableColumnsComponentState,
|
||||
scopeId,
|
||||
),
|
||||
tableViewFilterGroupsState: extractComponentState(
|
||||
tableViewFilterGroupsComponentState,
|
||||
scopeId,
|
||||
),
|
||||
tableFiltersState: extractComponentState(
|
||||
tableFiltersComponentState,
|
||||
scopeId,
|
||||
|
||||
@ -34,6 +34,7 @@ export const useRecordTable = (props?: useRecordTableProps) => {
|
||||
const {
|
||||
scopeId,
|
||||
availableTableColumnsState,
|
||||
tableViewFilterGroupsState,
|
||||
tableFiltersState,
|
||||
tableSortsState,
|
||||
tableColumnsState,
|
||||
@ -67,6 +68,10 @@ export const useRecordTable = (props?: useRecordTableProps) => {
|
||||
|
||||
const setOnEntityCountChange = useSetRecoilState(onEntityCountChangeState);
|
||||
|
||||
const setTableViewFilterGroups = useSetRecoilState(
|
||||
tableViewFilterGroupsState,
|
||||
);
|
||||
|
||||
const setTableFilters = useSetRecoilState(tableFiltersState);
|
||||
|
||||
const setTableSorts = useSetRecoilState(tableSortsState);
|
||||
@ -203,6 +208,7 @@ export const useRecordTable = (props?: useRecordTableProps) => {
|
||||
scopeId,
|
||||
onColumnsChange,
|
||||
setAvailableTableColumns,
|
||||
setTableViewFilterGroups,
|
||||
setTableFilters,
|
||||
setTableSorts,
|
||||
setOnEntityCountChange,
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
import { ViewFilterGroup } from '@/views/types/ViewFilterGroup';
|
||||
|
||||
export const tableViewFilterGroupsComponentState = createComponentState<
|
||||
ViewFilterGroup[]
|
||||
>({
|
||||
key: 'tableViewFilterGroupsComponentState',
|
||||
defaultValue: [],
|
||||
});
|
||||
Reference in New Issue
Block a user