mirror of
https://github.com/twentyhq/twenty.git
synced 2024-11-25 20:00:34 +03:00
Feat: Advanced filter (#7700)
Design: ![twenty-advanced-filters-design](https://github.com/user-attachments/assets/7d99971c-9ee1-4a78-a2fb-7ae5a9b3a836) Not ready to be merged yet! --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
parent
1dfeba39eb
commit
315820ec86
@ -25,9 +25,9 @@ const jestConfig: JestConfigWithTsJest = {
|
||||
extensionsToTreatAsEsm: ['.ts', '.tsx'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
statements: 59,
|
||||
statements: 58,
|
||||
lines: 55,
|
||||
functions: 48,
|
||||
functions: 47,
|
||||
},
|
||||
},
|
||||
collectCoverageFrom: ['<rootDir>/src/**/*.ts'],
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
|
||||
import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter';
|
||||
import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter';
|
||||
import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables';
|
||||
|
||||
export const computeContextStoreFilters = (
|
||||
@ -12,9 +12,10 @@ export const computeContextStoreFilters = (
|
||||
|
||||
if (contextStoreTargetedRecordsRule.mode === 'exclusion') {
|
||||
queryFilter = makeAndFilterVariables([
|
||||
turnFiltersIntoQueryFilter(
|
||||
computeViewRecordGqlOperationFilter(
|
||||
contextStoreTargetedRecordsRule.filters,
|
||||
objectMetadataItem?.fields ?? [],
|
||||
[],
|
||||
),
|
||||
contextStoreTargetedRecordsRule.excludedRecordIds.length > 0
|
||||
? {
|
||||
|
@ -24,6 +24,7 @@ export enum CoreObjectNameSingular {
|
||||
View = 'view',
|
||||
ViewField = 'viewField',
|
||||
ViewFilter = 'viewFilter',
|
||||
ViewFilterGroup = 'viewFilterGroup',
|
||||
ViewSort = 'viewSort',
|
||||
ViewGroup = 'viewGroup',
|
||||
Webhook = 'webhook',
|
||||
|
@ -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: [],
|
||||
});
|
@ -18,6 +18,7 @@ export const findAllViewsOperationSignatureFactory: RecordGqlOperationSignatureF
|
||||
icon: true,
|
||||
key: true,
|
||||
viewFilters: true,
|
||||
viewFilterGroups: true,
|
||||
viewSorts: true,
|
||||
viewFields: true,
|
||||
viewGroups: true,
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { MouseEvent, useMemo, useRef, useState } from 'react';
|
||||
import { IconChevronDown, IconComponent } from 'twenty-ui';
|
||||
import { IconComponent } from 'twenty-ui';
|
||||
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
@ -10,8 +9,8 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
|
||||
import { SelectControl } from '@/ui/input/components/SelectControl';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay';
|
||||
import { SelectHotkeyScope } from '../types/SelectHotkeyScope';
|
||||
|
||||
export type SelectOption<Value extends string | number | null> = {
|
||||
@ -47,22 +46,6 @@ const StyledContainer = styled.div<{ fullWidth?: boolean }>`
|
||||
width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')};
|
||||
`;
|
||||
|
||||
const StyledControlContainer = styled.div<{ disabled?: boolean }>`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
box-sizing: border-box;
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
color: ${({ disabled, theme }) =>
|
||||
disabled ? theme.font.color.tertiary : theme.font.color.primary};
|
||||
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
height: ${({ theme }) => theme.spacing(8)};
|
||||
justify-content: space-between;
|
||||
padding: 0 ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledLabel = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
display: block;
|
||||
@ -71,20 +54,6 @@ const StyledLabel = styled.span`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledControlLabel = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledIconChevronDown = styled(IconChevronDown)<{
|
||||
disabled?: boolean;
|
||||
}>`
|
||||
color: ${({ disabled, theme }) =>
|
||||
disabled ? theme.font.color.extraLight : theme.font.color.tertiary};
|
||||
`;
|
||||
|
||||
export const Select = <Value extends string | number | null>({
|
||||
className,
|
||||
disabled: disabledFromProps,
|
||||
@ -103,7 +72,6 @@ export const Select = <Value extends string | number | null>({
|
||||
}: SelectProps<Value>) => {
|
||||
const selectContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const theme = useTheme();
|
||||
const [searchInputValue, setSearchInputValue] = useState('');
|
||||
|
||||
const selectedOption =
|
||||
@ -126,24 +94,6 @@ export const Select = <Value extends string | number | null>({
|
||||
|
||||
const { closeDropdown } = useDropdown(dropdownId);
|
||||
|
||||
const selectControl = (
|
||||
<StyledControlContainer disabled={isDisabled}>
|
||||
<StyledControlLabel>
|
||||
{!!selectedOption?.Icon && (
|
||||
<selectedOption.Icon
|
||||
color={
|
||||
isDisabled ? theme.font.color.light : theme.font.color.primary
|
||||
}
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
)}
|
||||
<EllipsisDisplay> {selectedOption?.label} </EllipsisDisplay>
|
||||
</StyledControlLabel>
|
||||
<StyledIconChevronDown disabled={isDisabled} size={theme.icon.size.md} />
|
||||
</StyledControlContainer>
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledContainer
|
||||
className={className}
|
||||
@ -154,13 +104,21 @@ export const Select = <Value extends string | number | null>({
|
||||
>
|
||||
{!!label && <StyledLabel>{label}</StyledLabel>}
|
||||
{isDisabled ? (
|
||||
selectControl
|
||||
<SelectControl
|
||||
selectedOption={selectedOption}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
) : (
|
||||
<Dropdown
|
||||
dropdownId={dropdownId}
|
||||
dropdownMenuWidth={dropdownWidth}
|
||||
dropdownPlacement="bottom-start"
|
||||
clickableComponent={selectControl}
|
||||
clickableComponent={
|
||||
<SelectControl
|
||||
selectedOption={selectedOption}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
}
|
||||
disableBlur={disableBlur}
|
||||
dropdownComponents={
|
||||
<>
|
||||
|
@ -0,0 +1,63 @@
|
||||
import { SelectOption } from '@/ui/input/components/Select';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconChevronDown } from 'twenty-ui';
|
||||
|
||||
const StyledControlContainer = styled.div<{ disabled?: boolean }>`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
box-sizing: border-box;
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
color: ${({ disabled, theme }) =>
|
||||
disabled ? theme.font.color.tertiary : theme.font.color.primary};
|
||||
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
height: ${({ theme }) => theme.spacing(8)};
|
||||
justify-content: space-between;
|
||||
padding: 0 ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledControlLabel = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledIconChevronDown = styled(IconChevronDown)<{
|
||||
disabled?: boolean;
|
||||
}>`
|
||||
color: ${({ disabled, theme }) =>
|
||||
disabled ? theme.font.color.extraLight : theme.font.color.tertiary};
|
||||
`;
|
||||
|
||||
type SelectControlProps = {
|
||||
selectedOption: SelectOption<string | number | null>;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
export const SelectControl = ({
|
||||
selectedOption,
|
||||
isDisabled,
|
||||
}: SelectControlProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<StyledControlContainer disabled={isDisabled}>
|
||||
<StyledControlLabel>
|
||||
{!!selectedOption?.Icon && (
|
||||
<selectedOption.Icon
|
||||
color={
|
||||
isDisabled ? theme.font.color.light : theme.font.color.primary
|
||||
}
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
)}
|
||||
{selectedOption?.label}
|
||||
</StyledControlLabel>
|
||||
<StyledIconChevronDown disabled={isDisabled} size={theme.icon.size.md} />
|
||||
</StyledControlContainer>
|
||||
);
|
||||
};
|
@ -0,0 +1,27 @@
|
||||
import { IconFilterCog } from 'twenty-ui';
|
||||
|
||||
import { SortOrFilterChip } from '@/views/components/SortOrFilterChip';
|
||||
import { ADVANCED_FILTER_DROPDOWN_ID } from '@/views/constants/AdvancedFilterDropdownId';
|
||||
import { plural } from 'pluralize';
|
||||
|
||||
type AdvancedFilterChipProps = {
|
||||
onRemove: () => void;
|
||||
advancedFilterCount?: number;
|
||||
};
|
||||
|
||||
export const AdvancedFilterChip = ({
|
||||
onRemove,
|
||||
advancedFilterCount,
|
||||
}: AdvancedFilterChipProps) => {
|
||||
const labelText = 'advanced rule';
|
||||
const chipLabel = `${advancedFilterCount ?? 0} ${advancedFilterCount === 1 ? labelText : plural(labelText)}`;
|
||||
return (
|
||||
<SortOrFilterChip
|
||||
testId={ADVANCED_FILTER_DROPDOWN_ID}
|
||||
labelKey={chipLabel}
|
||||
labelValue=""
|
||||
Icon={IconFilterCog}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,83 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
|
||||
import { AdvancedFilterRootLevelViewFilterGroup } from '@/object-record/advanced-filter/components/AdvancedFilterRootLevelViewFilterGroup';
|
||||
import { useDeleteCombinedViewFilterGroup } from '@/object-record/advanced-filter/hooks/useDeleteCombinedViewFilterGroup';
|
||||
import { AdvancedFilterChip } from '@/views/components/AdvancedFilterChip';
|
||||
import { ADVANCED_FILTER_DROPDOWN_ID } from '@/views/constants/AdvancedFilterDropdownId';
|
||||
import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters';
|
||||
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const AdvancedFilterDropdownButton = () => {
|
||||
const { deleteCombinedViewFilter } = useDeleteCombinedViewFilters();
|
||||
const { deleteCombinedViewFilterGroup } = useDeleteCombinedViewFilterGroup();
|
||||
|
||||
const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView();
|
||||
|
||||
const advancedViewFilterIds =
|
||||
currentViewWithCombinedFiltersAndSorts?.viewFilters
|
||||
.filter((viewFilter) => isDefined(viewFilter.viewFilterGroupId))
|
||||
.map((viewFilter) => viewFilter.id);
|
||||
|
||||
const handleDropdownClickOutside = useCallback(() => {}, []);
|
||||
|
||||
const handleDropdownClose = () => {};
|
||||
|
||||
const removeAdvancedFilter = useCallback(async () => {
|
||||
if (!advancedViewFilterIds) {
|
||||
throw new Error('No advanced view filters to remove');
|
||||
}
|
||||
|
||||
const viewFilterGroupIds =
|
||||
currentViewWithCombinedFiltersAndSorts?.viewFilterGroups?.map(
|
||||
(viewFilter) => viewFilter.id,
|
||||
) ?? [];
|
||||
|
||||
for (const viewFilterGroupId of viewFilterGroupIds) {
|
||||
await deleteCombinedViewFilterGroup(viewFilterGroupId);
|
||||
}
|
||||
|
||||
for (const viewFilterId of advancedViewFilterIds) {
|
||||
await deleteCombinedViewFilter(viewFilterId);
|
||||
}
|
||||
}, [
|
||||
advancedViewFilterIds,
|
||||
deleteCombinedViewFilter,
|
||||
deleteCombinedViewFilterGroup,
|
||||
currentViewWithCombinedFiltersAndSorts?.viewFilterGroups,
|
||||
]);
|
||||
|
||||
const outermostViewFilterGroupId =
|
||||
currentViewWithCombinedFiltersAndSorts?.viewFilterGroups.find(
|
||||
(viewFilterGroup) => !viewFilterGroup.parentViewFilterGroupId,
|
||||
)?.id;
|
||||
|
||||
if (!outermostViewFilterGroupId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
dropdownId={ADVANCED_FILTER_DROPDOWN_ID}
|
||||
clickableComponent={
|
||||
<AdvancedFilterChip
|
||||
onRemove={removeAdvancedFilter}
|
||||
advancedFilterCount={advancedViewFilterIds?.length}
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<AdvancedFilterRootLevelViewFilterGroup
|
||||
rootLevelViewFilterGroupId={outermostViewFilterGroupId}
|
||||
/>
|
||||
}
|
||||
dropdownHotkeyScope={{ scope: ADVANCED_FILTER_DROPDOWN_ID }}
|
||||
dropdownOffset={{ y: 8, x: 0 }}
|
||||
dropdownPlacement="bottom-start"
|
||||
dropdownMenuWidth={800}
|
||||
onClickOutside={handleDropdownClickOutside}
|
||||
onClose={handleDropdownClose}
|
||||
/>
|
||||
);
|
||||
};
|
@ -1,6 +1,5 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { MultipleFiltersDropdownContent } from '@/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent';
|
||||
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
||||
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
|
||||
import { FilterOperand } from '@/object-record/object-filter-dropdown/types/FilterOperand';
|
||||
@ -11,6 +10,7 @@ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/
|
||||
import { EditableFilterChip } from '@/views/components/EditableFilterChip';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
|
||||
import { ObjectFilterOperandSelectAndInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterOperandSelectAndInput';
|
||||
import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters';
|
||||
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
@ -98,7 +98,7 @@ export const EditableFilterDropdownButton = ({
|
||||
<EditableFilterChip viewFilter={viewFilter} onRemove={handleRemove} />
|
||||
}
|
||||
dropdownComponents={
|
||||
<MultipleFiltersDropdownContent
|
||||
<ObjectFilterOperandSelectAndInput
|
||||
filterDropdownId={viewFilterDropdownId}
|
||||
/>
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
|
||||
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
||||
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { AdvancedFilterDropdownButton } from '@/views/components/AdvancedFilterDropdownButton';
|
||||
import { EditableFilterDropdownButton } from '@/views/components/EditableFilterDropdownButton';
|
||||
import { EditableSortChip } from '@/views/components/EditableSortChip';
|
||||
import { ViewBarFilterEffect } from '@/views/components/ViewBarFilterEffect';
|
||||
@ -137,11 +138,16 @@ export const ViewBarDetails = ({
|
||||
|
||||
const otherViewFilters =
|
||||
currentViewWithCombinedFiltersAndSorts.viewFilters.filter(
|
||||
(viewFilter) => viewFilter.variant && viewFilter.variant !== 'default',
|
||||
(viewFilter) =>
|
||||
viewFilter.variant &&
|
||||
viewFilter.variant !== 'default' &&
|
||||
!viewFilter.viewFilterGroupId,
|
||||
);
|
||||
const defaultViewFilters =
|
||||
currentViewWithCombinedFiltersAndSorts.viewFilters.filter(
|
||||
(viewFilter) => !viewFilter.variant || viewFilter.variant === 'default',
|
||||
(viewFilter) =>
|
||||
(!viewFilter.variant || viewFilter.variant === 'default') &&
|
||||
!viewFilter.viewFilterGroupId,
|
||||
);
|
||||
|
||||
return {
|
||||
@ -166,6 +172,10 @@ export const ViewBarDetails = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const showAdvancedFilterDropdownButton =
|
||||
currentViewWithCombinedFiltersAndSorts?.viewFilterGroups &&
|
||||
currentViewWithCombinedFiltersAndSorts?.viewFilterGroups.length > 0;
|
||||
|
||||
return (
|
||||
<StyledBar>
|
||||
<StyledFilterContainer>
|
||||
@ -199,6 +209,7 @@ export const ViewBarDetails = ({
|
||||
<StyledSeperator />
|
||||
</StyledSeperatorContainer>
|
||||
)}
|
||||
{showAdvancedFilterDropdownButton && <AdvancedFilterDropdownButton />}
|
||||
{mapViewFiltersToFilters(
|
||||
defaultViewFilters,
|
||||
availableFilterDefinitions,
|
||||
|
@ -0,0 +1 @@
|
||||
export const ADVANCED_FILTER_DROPDOWN_ID = 'advanced-filter';
|
@ -0,0 +1,214 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
|
||||
import { triggerDestroyRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDestroyRecordsOptimisticEffect';
|
||||
import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
|
||||
import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation';
|
||||
import { useDestroyOneRecordMutation } from '@/object-record/hooks/useDestroyOneRecordMutation';
|
||||
import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { GraphQLView } from '@/views/types/GraphQLView';
|
||||
import { ViewFilterGroup } from '@/views/types/ViewFilterGroup';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const usePersistViewFilterGroupRecords = () => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.ViewFilterGroup,
|
||||
});
|
||||
|
||||
const getRecordFromCache = useGetRecordFromCache({
|
||||
objectNameSingular: CoreObjectNameSingular.ViewFilterGroup,
|
||||
});
|
||||
|
||||
const { destroyOneRecordMutation } = useDestroyOneRecordMutation({
|
||||
objectNameSingular: CoreObjectNameSingular.ViewFilterGroup,
|
||||
});
|
||||
|
||||
const { createOneRecordMutation } = useCreateOneRecordMutation({
|
||||
objectNameSingular: CoreObjectNameSingular.ViewFilterGroup,
|
||||
});
|
||||
|
||||
const { updateOneRecordMutation } = useUpdateOneRecordMutation({
|
||||
objectNameSingular: CoreObjectNameSingular.ViewFilterGroup,
|
||||
});
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const createViewFilterGroupRecord = useCallback(
|
||||
async (viewFilterGroup: ViewFilterGroup, view: GraphQLView) => {
|
||||
const result = await apolloClient.mutate<{
|
||||
createViewFilterGroup: ViewFilterGroup;
|
||||
}>({
|
||||
mutation: createOneRecordMutation,
|
||||
variables: {
|
||||
input: {
|
||||
id: viewFilterGroup.id,
|
||||
viewId: view.id,
|
||||
parentViewFilterGroupId: viewFilterGroup.parentViewFilterGroupId,
|
||||
logicalOperator: viewFilterGroup.logicalOperator,
|
||||
positionInViewFilterGroup:
|
||||
viewFilterGroup.positionInViewFilterGroup,
|
||||
},
|
||||
},
|
||||
update: (cache, { data }) => {
|
||||
const record = data?.createViewFilterGroup;
|
||||
if (!record) return;
|
||||
|
||||
triggerCreateRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
recordsToCreate: [record],
|
||||
objectMetadataItems,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (!result.data) {
|
||||
throw new Error('Failed to create view filter group');
|
||||
}
|
||||
|
||||
return { newRecordId: result.data.createViewFilterGroup.id };
|
||||
},
|
||||
[
|
||||
apolloClient,
|
||||
createOneRecordMutation,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
],
|
||||
);
|
||||
|
||||
const createViewFilterGroupRecords = useCallback(
|
||||
async (viewFilterGroupsToCreate: ViewFilterGroup[], view: GraphQLView) => {
|
||||
if (!viewFilterGroupsToCreate.length) return [];
|
||||
|
||||
const oldToNewId = new Map<string, string>();
|
||||
|
||||
for (const viewFilterGroupToCreate of viewFilterGroupsToCreate) {
|
||||
const newParentViewFilterGroupId = isDefined(
|
||||
viewFilterGroupToCreate.parentViewFilterGroupId,
|
||||
)
|
||||
? (oldToNewId.get(viewFilterGroupToCreate.parentViewFilterGroupId) ??
|
||||
viewFilterGroupToCreate.parentViewFilterGroupId)
|
||||
: undefined;
|
||||
|
||||
const { newRecordId } = await createViewFilterGroupRecord(
|
||||
{
|
||||
...viewFilterGroupToCreate,
|
||||
parentViewFilterGroupId: newParentViewFilterGroupId,
|
||||
},
|
||||
view,
|
||||
);
|
||||
|
||||
oldToNewId.set(viewFilterGroupToCreate.id, newRecordId);
|
||||
}
|
||||
|
||||
const newRecordIds = viewFilterGroupsToCreate.map((viewFilterGroup) => {
|
||||
const newId = oldToNewId.get(viewFilterGroup.id);
|
||||
if (!newId) {
|
||||
throw new Error('Failed to create view filter group');
|
||||
}
|
||||
return newId;
|
||||
});
|
||||
|
||||
return newRecordIds;
|
||||
},
|
||||
[createViewFilterGroupRecord],
|
||||
);
|
||||
|
||||
const updateViewFilterGroupRecords = useCallback(
|
||||
(viewFilterGroupsToUpdate: ViewFilterGroup[]) => {
|
||||
if (!viewFilterGroupsToUpdate.length) return;
|
||||
return Promise.all(
|
||||
viewFilterGroupsToUpdate.map((viewFilterGroup) =>
|
||||
apolloClient.mutate<{ updateViewFilterGroup: ViewFilterGroup }>({
|
||||
mutation: updateOneRecordMutation,
|
||||
variables: {
|
||||
idToUpdate: viewFilterGroup.id,
|
||||
input: {
|
||||
parentViewFilterGroupId:
|
||||
viewFilterGroup.parentViewFilterGroupId,
|
||||
logicalOperator: viewFilterGroup.logicalOperator,
|
||||
positionInViewFilterGroup:
|
||||
viewFilterGroup.positionInViewFilterGroup,
|
||||
},
|
||||
},
|
||||
update: (cache, { data }) => {
|
||||
const record = data?.updateViewFilterGroup;
|
||||
if (!record) return;
|
||||
const cachedRecord = getRecordFromCache<ObjectRecord>(record.id);
|
||||
|
||||
if (!cachedRecord) return;
|
||||
|
||||
triggerUpdateRecordOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
currentRecord: cachedRecord,
|
||||
updatedRecord: record,
|
||||
objectMetadataItems,
|
||||
});
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
[
|
||||
apolloClient,
|
||||
getRecordFromCache,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
updateOneRecordMutation,
|
||||
],
|
||||
);
|
||||
|
||||
const deleteViewFilterGroupRecords = useCallback(
|
||||
(viewFilterGroupIdsToDelete: string[]) => {
|
||||
if (!viewFilterGroupIdsToDelete.length) return;
|
||||
return Promise.all(
|
||||
viewFilterGroupIdsToDelete.map((viewFilterGroupId) =>
|
||||
apolloClient.mutate<{ destroyViewFilterGroup: ViewFilterGroup }>({
|
||||
mutation: destroyOneRecordMutation,
|
||||
variables: {
|
||||
idToDestroy: viewFilterGroupId,
|
||||
},
|
||||
update: (cache, { data }) => {
|
||||
const record = data?.destroyViewFilterGroup;
|
||||
|
||||
if (!record) return;
|
||||
|
||||
const cachedRecord = getRecordFromCache(record.id, cache);
|
||||
|
||||
if (!cachedRecord) return;
|
||||
|
||||
triggerDestroyRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
recordsToDestroy: [cachedRecord],
|
||||
objectMetadataItems,
|
||||
});
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
[
|
||||
apolloClient,
|
||||
destroyOneRecordMutation,
|
||||
getRecordFromCache,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
createViewFilterGroupRecords,
|
||||
updateViewFilterGroupRecords,
|
||||
deleteViewFilterGroupRecords,
|
||||
};
|
||||
};
|
@ -50,11 +50,13 @@ export const usePersistViewFilterRecords = () => {
|
||||
mutation: createOneRecordMutation,
|
||||
variables: {
|
||||
input: {
|
||||
id: viewFilter.id,
|
||||
fieldMetadataId: viewFilter.fieldMetadataId,
|
||||
viewId: view.id,
|
||||
value: viewFilter.value,
|
||||
displayValue: viewFilter.displayValue,
|
||||
operand: viewFilter.operand,
|
||||
viewFilterGroupId: viewFilter.viewFilterGroupId,
|
||||
},
|
||||
},
|
||||
update: (cache, { data }) => {
|
||||
|
@ -4,9 +4,11 @@ import { RecordIndexRootPropsContext } from '@/object-record/record-index/contex
|
||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { usePersistViewFieldRecords } from '@/views/hooks/internal/usePersistViewFieldRecords';
|
||||
import { usePersistViewFilterGroupRecords } from '@/views/hooks/internal/usePersistViewFilterGroupRecords';
|
||||
import { usePersistViewFilterRecords } from '@/views/hooks/internal/usePersistViewFilterRecords';
|
||||
import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroupRecords';
|
||||
import { usePersistViewSortRecords } from '@/views/hooks/internal/usePersistViewSortRecords';
|
||||
import { useGetViewFilterGroupsCombined } from '@/views/hooks/useGetCombinedViewFilterGroups';
|
||||
import { useGetViewFiltersCombined } from '@/views/hooks/useGetCombinedViewFilters';
|
||||
import { useGetViewSortsCombined } from '@/views/hooks/useGetCombinedViewSorts';
|
||||
import { useGetViewFromCache } from '@/views/hooks/useGetViewFromCache';
|
||||
@ -45,6 +47,8 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
|
||||
const { getViewSortsCombined } = useGetViewSortsCombined(viewBarComponentId);
|
||||
const { getViewFiltersCombined } =
|
||||
useGetViewFiltersCombined(viewBarComponentId);
|
||||
const { getViewFilterGroupsCombined } =
|
||||
useGetViewFilterGroupsCombined(viewBarComponentId);
|
||||
|
||||
const { createViewSortRecords } = usePersistViewSortRecords();
|
||||
|
||||
@ -52,6 +56,8 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
|
||||
|
||||
const { createViewFilterRecords } = usePersistViewFilterRecords();
|
||||
|
||||
const { createViewFilterGroupRecords } = usePersistViewFilterGroupRecords();
|
||||
|
||||
const { objectMetadataItem } = useContext(RecordIndexRootPropsContext);
|
||||
|
||||
const createViewFromCurrentView = useRecoilCallback(
|
||||
@ -143,11 +149,18 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
|
||||
}
|
||||
|
||||
if (shouldCopyFiltersAndSorts === true) {
|
||||
const sourceViewCombinedFilterGroups = getViewFilterGroupsCombined(
|
||||
view.id,
|
||||
);
|
||||
const sourceViewCombinedFilters = getViewFiltersCombined(view.id);
|
||||
const sourceViewCombinedSorts = getViewSortsCombined(view.id);
|
||||
|
||||
await createViewSortRecords(sourceViewCombinedSorts, view);
|
||||
await createViewFilterRecords(sourceViewCombinedFilters, view);
|
||||
await createViewFilterGroupRecords(
|
||||
sourceViewCombinedFilterGroups,
|
||||
view,
|
||||
);
|
||||
}
|
||||
|
||||
set(isPersistingViewFieldsCallbackState, false);
|
||||
@ -160,10 +173,12 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
|
||||
createViewFieldRecords,
|
||||
getViewSortsCombined,
|
||||
getViewFiltersCombined,
|
||||
getViewFilterGroupsCombined,
|
||||
currentViewIdCallbackState,
|
||||
getViewFromCache,
|
||||
isPersistingViewFieldsCallbackState,
|
||||
createViewGroupRecords,
|
||||
createViewFilterGroupRecords,
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -0,0 +1,67 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
|
||||
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
|
||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { unsavedToDeleteViewFilterGroupIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterGroupIdsComponentFamilyState';
|
||||
import { unsavedToUpsertViewFilterGroupsComponentFamilyState } from '@/views/states/unsavedToUpsertViewFilterGroupsComponentFamilyState';
|
||||
import { View } from '@/views/types/View';
|
||||
import { getCombinedViewFilterGroups } from '@/views/utils/getCombinedViewFilterGroups';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const useGetViewFilterGroupsCombined = (viewBarComponentId?: string) => {
|
||||
const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews);
|
||||
|
||||
const unsavedToUpsertViewFilterGroupsCallbackState =
|
||||
useRecoilComponentCallbackStateV2(
|
||||
unsavedToUpsertViewFilterGroupsComponentFamilyState,
|
||||
viewBarComponentId,
|
||||
);
|
||||
|
||||
const unsavedToDeleteViewFilterGroupIdsCallbackState =
|
||||
useRecoilComponentCallbackStateV2(
|
||||
unsavedToDeleteViewFilterGroupIdsComponentFamilyState,
|
||||
viewBarComponentId,
|
||||
);
|
||||
|
||||
const getViewFilterGroupsCombined = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(viewId: string) => {
|
||||
const view = views.find((view) => view.id === viewId);
|
||||
|
||||
if (!isDefined(view)) {
|
||||
throw new Error(
|
||||
`Cannot get view with id ${viewId}, because it cannot be found in client cache data.`,
|
||||
);
|
||||
}
|
||||
|
||||
const unsavedToUpsertViewFilterGroups = getSnapshotValue(
|
||||
snapshot,
|
||||
unsavedToUpsertViewFilterGroupsCallbackState({ viewId: view.id }),
|
||||
);
|
||||
|
||||
const unsavedToDeleteViewFilterGroupIds = getSnapshotValue(
|
||||
snapshot,
|
||||
unsavedToDeleteViewFilterGroupIdsCallbackState({ viewId: view.id }),
|
||||
);
|
||||
|
||||
const combinedViewFilterGroups = getCombinedViewFilterGroups(
|
||||
view.viewFilterGroups ?? [],
|
||||
unsavedToUpsertViewFilterGroups,
|
||||
unsavedToDeleteViewFilterGroupIds,
|
||||
);
|
||||
|
||||
return combinedViewFilterGroups;
|
||||
},
|
||||
[
|
||||
views,
|
||||
unsavedToDeleteViewFilterGroupIdsCallbackState,
|
||||
unsavedToUpsertViewFilterGroupsCallbackState,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
getViewFilterGroupsCombined,
|
||||
};
|
||||
};
|
@ -9,12 +9,15 @@ import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-sta
|
||||
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
|
||||
import { isCurrentViewKeyIndexComponentState } from '@/views/states/isCurrentViewIndexComponentState';
|
||||
import { unsavedToDeleteViewFilterGroupIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterGroupIdsComponentFamilyState';
|
||||
import { unsavedToDeleteViewFilterIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterIdsComponentFamilyState';
|
||||
import { unsavedToDeleteViewSortIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewSortIdsComponentFamilyState';
|
||||
import { unsavedToUpsertViewFilterGroupsComponentFamilyState } from '@/views/states/unsavedToUpsertViewFilterGroupsComponentFamilyState';
|
||||
import { unsavedToUpsertViewFiltersComponentFamilyState } from '@/views/states/unsavedToUpsertViewFiltersComponentFamilyState';
|
||||
import { unsavedToUpsertViewSortsComponentFamilyState } from '@/views/states/unsavedToUpsertViewSortsComponentFamilyState';
|
||||
import { viewObjectMetadataIdComponentState } from '@/views/states/viewObjectMetadataIdComponentState';
|
||||
import { View } from '@/views/types/View';
|
||||
import { getCombinedViewFilterGroups } from '@/views/utils/getCombinedViewFilterGroups';
|
||||
import { getCombinedViewFilters } from '@/views/utils/getCombinedViewFilters';
|
||||
import { getCombinedViewSorts } from '@/views/utils/getCombinedViewSorts';
|
||||
import { getObjectMetadataItemViews } from '@/views/utils/getObjectMetadataItemViews';
|
||||
@ -70,6 +73,12 @@ export const useGetCurrentView = (viewBarInstanceId?: string) => {
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const unsavedToUpsertViewFilterGroups = useRecoilComponentFamilyValueV2(
|
||||
unsavedToUpsertViewFilterGroupsComponentFamilyState,
|
||||
{ viewId },
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const unsavedToUpsertViewSorts = useRecoilComponentFamilyValueV2(
|
||||
unsavedToUpsertViewSortsComponentFamilyState,
|
||||
{ viewId },
|
||||
@ -82,6 +91,12 @@ export const useGetCurrentView = (viewBarInstanceId?: string) => {
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const unsavedToDeleteViewFilterGroupIds = useRecoilComponentFamilyValueV2(
|
||||
unsavedToDeleteViewFilterGroupIdsComponentFamilyState,
|
||||
{ viewId },
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const unsavedToDeleteViewSortIds = useRecoilComponentFamilyValueV2(
|
||||
unsavedToDeleteViewSortIdsComponentFamilyState,
|
||||
{ viewId },
|
||||
@ -104,6 +119,11 @@ export const useGetCurrentView = (viewBarInstanceId?: string) => {
|
||||
unsavedToUpsertViewFilters,
|
||||
unsavedToDeleteViewFilterIds,
|
||||
),
|
||||
viewFilterGroups: getCombinedViewFilterGroups(
|
||||
currentView.viewFilterGroups ?? [],
|
||||
unsavedToUpsertViewFilterGroups,
|
||||
unsavedToDeleteViewFilterGroupIds,
|
||||
),
|
||||
viewSorts: getCombinedViewSorts(
|
||||
currentView.viewSorts,
|
||||
unsavedToUpsertViewSorts,
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { unsavedToDeleteViewFilterGroupIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterGroupIdsComponentFamilyState';
|
||||
import { unsavedToDeleteViewFilterIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterIdsComponentFamilyState';
|
||||
import { unsavedToDeleteViewSortIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewSortIdsComponentFamilyState';
|
||||
import { unsavedToUpsertViewFilterGroupsComponentFamilyState } from '@/views/states/unsavedToUpsertViewFilterGroupsComponentFamilyState';
|
||||
import { unsavedToUpsertViewFiltersComponentFamilyState } from '@/views/states/unsavedToUpsertViewFiltersComponentFamilyState';
|
||||
import { unsavedToUpsertViewSortsComponentFamilyState } from '@/views/states/unsavedToUpsertViewSortsComponentFamilyState';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
@ -18,6 +20,12 @@ export const useResetUnsavedViewStates = (viewBarInstanceId?: string) => {
|
||||
viewBarInstanceId,
|
||||
);
|
||||
|
||||
const unsavedToDeleteViewFilterGroupIdsCallbackState =
|
||||
useRecoilComponentCallbackStateV2(
|
||||
unsavedToDeleteViewFilterGroupIdsComponentFamilyState,
|
||||
viewBarInstanceId,
|
||||
);
|
||||
|
||||
const setUnsavedToUpsertViewFiltersCallbackState =
|
||||
useRecoilComponentCallbackStateV2(
|
||||
unsavedToUpsertViewFiltersComponentFamilyState,
|
||||
@ -30,19 +38,29 @@ export const useResetUnsavedViewStates = (viewBarInstanceId?: string) => {
|
||||
viewBarInstanceId,
|
||||
);
|
||||
|
||||
const unsavedToUpsertViewFilterGroupsCallbackState =
|
||||
useRecoilComponentCallbackStateV2(
|
||||
unsavedToUpsertViewFilterGroupsComponentFamilyState,
|
||||
viewBarInstanceId,
|
||||
);
|
||||
|
||||
const resetUnsavedViewStates = useRecoilCallback(
|
||||
({ set }) =>
|
||||
(viewId: string) => {
|
||||
set(unsavedToDeleteViewFilterGroupIdsCallbackState({ viewId }), []);
|
||||
set(setUnsavedToDeleteViewFilterIdsCallbackState({ viewId }), []);
|
||||
set(setUnsavedToDeleteViewSortIdsCallbackState({ viewId }), []);
|
||||
set(unsavedToUpsertViewFilterGroupsCallbackState({ viewId }), []);
|
||||
set(setUnsavedToUpsertViewFiltersCallbackState({ viewId }), []);
|
||||
set(unsavedToUpsertViewSortsCallbackState({ viewId }), []);
|
||||
},
|
||||
[
|
||||
unsavedToUpsertViewSortsCallbackState,
|
||||
setUnsavedToUpsertViewFiltersCallbackState,
|
||||
unsavedToUpsertViewFilterGroupsCallbackState,
|
||||
setUnsavedToDeleteViewSortIdsCallbackState,
|
||||
setUnsavedToDeleteViewFilterIdsCallbackState,
|
||||
unsavedToDeleteViewFilterGroupIdsCallbackState,
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -2,13 +2,16 @@ import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { usePersistViewFilterGroupRecords } from '@/views/hooks/internal/usePersistViewFilterGroupRecords';
|
||||
import { usePersistViewFilterRecords } from '@/views/hooks/internal/usePersistViewFilterRecords';
|
||||
import { usePersistViewSortRecords } from '@/views/hooks/internal/usePersistViewSortRecords';
|
||||
import { useGetViewFromCache } from '@/views/hooks/useGetViewFromCache';
|
||||
import { useResetUnsavedViewStates } from '@/views/hooks/useResetUnsavedViewStates';
|
||||
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
|
||||
import { unsavedToDeleteViewFilterGroupIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterGroupIdsComponentFamilyState';
|
||||
import { unsavedToDeleteViewFilterIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterIdsComponentFamilyState';
|
||||
import { unsavedToDeleteViewSortIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewSortIdsComponentFamilyState';
|
||||
import { unsavedToUpsertViewFilterGroupsComponentFamilyState } from '@/views/states/unsavedToUpsertViewFilterGroupsComponentFamilyState';
|
||||
import { unsavedToUpsertViewFiltersComponentFamilyState } from '@/views/states/unsavedToUpsertViewFiltersComponentFamilyState';
|
||||
import { unsavedToUpsertViewSortsComponentFamilyState } from '@/views/states/unsavedToUpsertViewSortsComponentFamilyState';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
@ -48,6 +51,18 @@ export const useSaveCurrentViewFiltersAndSorts = (
|
||||
viewBarComponentId,
|
||||
);
|
||||
|
||||
const unsavedToUpsertViewFilterGroupsCallbackState =
|
||||
useRecoilComponentCallbackStateV2(
|
||||
unsavedToUpsertViewFilterGroupsComponentFamilyState,
|
||||
viewBarComponentId,
|
||||
);
|
||||
|
||||
const unsavedToDeleteViewFilterGroupIdsCallbackState =
|
||||
useRecoilComponentCallbackStateV2(
|
||||
unsavedToDeleteViewFilterGroupIdsComponentFamilyState,
|
||||
viewBarComponentId,
|
||||
);
|
||||
|
||||
const {
|
||||
createViewSortRecords,
|
||||
updateViewSortRecords,
|
||||
@ -60,6 +75,12 @@ export const useSaveCurrentViewFiltersAndSorts = (
|
||||
deleteViewFilterRecords,
|
||||
} = usePersistViewFilterRecords();
|
||||
|
||||
const {
|
||||
createViewFilterGroupRecords,
|
||||
deleteViewFilterGroupRecords,
|
||||
updateViewFilterGroupRecords,
|
||||
} = usePersistViewFilterGroupRecords();
|
||||
|
||||
const { resetUnsavedViewStates } =
|
||||
useResetUnsavedViewStates(viewBarComponentId);
|
||||
|
||||
@ -131,14 +152,14 @@ export const useSaveCurrentViewFiltersAndSorts = (
|
||||
const viewFiltersToCreate = unsavedToUpsertViewFilters.filter(
|
||||
(viewFilter) =>
|
||||
!view.viewFilters.some(
|
||||
(vf) => vf.fieldMetadataId === viewFilter.fieldMetadataId,
|
||||
(viewFilterToFilter) => viewFilterToFilter.id === viewFilter.id,
|
||||
),
|
||||
);
|
||||
|
||||
const viewFiltersToUpdate = unsavedToUpsertViewFilters.filter(
|
||||
(viewFilter) =>
|
||||
view.viewFilters.some(
|
||||
(vf) => vf.fieldMetadataId === viewFilter.fieldMetadataId,
|
||||
(viewFilterToFilter) => viewFilterToFilter.id === viewFilter.id,
|
||||
),
|
||||
);
|
||||
|
||||
@ -156,6 +177,55 @@ export const useSaveCurrentViewFiltersAndSorts = (
|
||||
],
|
||||
);
|
||||
|
||||
const saveViewFilterGroups = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
async (viewId: string) => {
|
||||
const unsavedToDeleteViewFilterGroupIds = getSnapshotValue(
|
||||
snapshot,
|
||||
unsavedToDeleteViewFilterGroupIdsCallbackState({ viewId }),
|
||||
);
|
||||
|
||||
const unsavedToUpsertViewFilterGroups = getSnapshotValue(
|
||||
snapshot,
|
||||
unsavedToUpsertViewFilterGroupsCallbackState({ viewId }),
|
||||
);
|
||||
|
||||
const view = await getViewFromCache(viewId);
|
||||
|
||||
if (isUndefinedOrNull(view)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewFilterGroupsToCreate = unsavedToUpsertViewFilterGroups.filter(
|
||||
(viewFilterGroup) =>
|
||||
!view.viewFilterGroups?.some(
|
||||
(viewFilterGroupToFilter) =>
|
||||
viewFilterGroupToFilter.id === viewFilterGroup.id,
|
||||
),
|
||||
);
|
||||
|
||||
const viewFilterGroupsToUpdate = unsavedToUpsertViewFilterGroups.filter(
|
||||
(viewFilterGroup) =>
|
||||
view.viewFilterGroups?.some(
|
||||
(viewFilterGroupToFilter) =>
|
||||
viewFilterGroupToFilter.id === viewFilterGroup.id,
|
||||
),
|
||||
);
|
||||
|
||||
await createViewFilterGroupRecords(viewFilterGroupsToCreate, view);
|
||||
await updateViewFilterGroupRecords(viewFilterGroupsToUpdate);
|
||||
await deleteViewFilterGroupRecords(unsavedToDeleteViewFilterGroupIds);
|
||||
},
|
||||
[
|
||||
getViewFromCache,
|
||||
createViewFilterGroupRecords,
|
||||
deleteViewFilterGroupRecords,
|
||||
unsavedToDeleteViewFilterGroupIdsCallbackState,
|
||||
unsavedToUpsertViewFilterGroupsCallbackState,
|
||||
updateViewFilterGroupRecords,
|
||||
],
|
||||
);
|
||||
|
||||
const saveCurrentViewFilterAndSorts = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
async (viewIdFromProps?: string) => {
|
||||
@ -169,6 +239,7 @@ export const useSaveCurrentViewFiltersAndSorts = (
|
||||
|
||||
const viewId = viewIdFromProps ?? currentViewId;
|
||||
|
||||
await saveViewFilterGroups(viewId);
|
||||
await saveViewFilters(viewId);
|
||||
await saveViewSorts(viewId);
|
||||
|
||||
@ -179,6 +250,7 @@ export const useSaveCurrentViewFiltersAndSorts = (
|
||||
resetUnsavedViewStates,
|
||||
saveViewFilters,
|
||||
saveViewSorts,
|
||||
saveViewFilterGroups,
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -8,6 +8,7 @@ import { currentViewIdComponentState } from '@/views/states/currentViewIdCompone
|
||||
import { unsavedToDeleteViewFilterIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterIdsComponentFamilyState';
|
||||
import { unsavedToUpsertViewFiltersComponentFamilyState } from '@/views/states/unsavedToUpsertViewFiltersComponentFamilyState';
|
||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { shouldReplaceFilter } from '@/views/utils/shouldReplaceFilter';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const useUpsertCombinedViewFilters = (viewBarComponentId?: string) => {
|
||||
@ -59,19 +60,16 @@ export const useUpsertCombinedViewFilters = (viewBarComponentId?: string) => {
|
||||
}
|
||||
|
||||
const matchingFilterInCurrentView = currentView.viewFilters.find(
|
||||
(viewFilter) =>
|
||||
viewFilter.fieldMetadataId === upsertedFilter.fieldMetadataId,
|
||||
(viewFilter) => shouldReplaceFilter(viewFilter, upsertedFilter),
|
||||
);
|
||||
|
||||
const matchingFilterInUnsavedFilters = unsavedToUpsertViewFilters.find(
|
||||
(viewFilter) =>
|
||||
viewFilter.fieldMetadataId === upsertedFilter.fieldMetadataId,
|
||||
(viewFilter) => shouldReplaceFilter(viewFilter, upsertedFilter),
|
||||
);
|
||||
|
||||
if (isDefined(matchingFilterInUnsavedFilters)) {
|
||||
const updatedFilters = unsavedToUpsertViewFilters.map((viewFilter) =>
|
||||
viewFilter.fieldMetadataId ===
|
||||
matchingFilterInUnsavedFilters.fieldMetadataId
|
||||
shouldReplaceFilter(viewFilter, matchingFilterInUnsavedFilters)
|
||||
? { ...viewFilter, ...upsertedFilter, id: viewFilter.id }
|
||||
: viewFilter,
|
||||
);
|
||||
|
@ -0,0 +1,9 @@
|
||||
import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2';
|
||||
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||
|
||||
export const unsavedToDeleteViewFilterGroupIdsComponentFamilyState =
|
||||
createComponentFamilyStateV2<string[], { viewId?: string }>({
|
||||
key: 'unsavedToDeleteViewFilterGroupIdsComponentFamilyState',
|
||||
defaultValue: [],
|
||||
componentInstanceContext: ViewComponentInstanceContext,
|
||||
});
|
@ -0,0 +1,10 @@
|
||||
import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2';
|
||||
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||
import { ViewFilterGroup } from '@/views/types/ViewFilterGroup';
|
||||
|
||||
export const unsavedToUpsertViewFilterGroupsComponentFamilyState =
|
||||
createComponentFamilyStateV2<ViewFilterGroup[], { viewId?: string }>({
|
||||
key: 'unsavedToUpsertViewFilterGroupsComponentFamilyState',
|
||||
defaultValue: [],
|
||||
componentInstanceContext: ViewComponentInstanceContext,
|
||||
});
|
@ -1,6 +1,6 @@
|
||||
import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2';
|
||||
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||
import { ViewFilter } from '../types/ViewFilter';
|
||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||
|
||||
export const unsavedToUpsertViewFiltersComponentFamilyState =
|
||||
createComponentFamilyStateV2<ViewFilter[], { viewId?: string }>({
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ViewField } from '@/views/types/ViewField';
|
||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { ViewFilterGroup } from '@/views/types/ViewFilterGroup';
|
||||
import { ViewGroup } from '@/views/types/ViewGroup';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
import { ViewSort } from '@/views/types/ViewSort';
|
||||
@ -15,6 +16,7 @@ export type GraphQLView = {
|
||||
isCompact: boolean;
|
||||
viewFields: ViewField[];
|
||||
viewFilters: ViewFilter[];
|
||||
viewFilterGroups?: ViewFilterGroup[];
|
||||
viewSorts: ViewSort[];
|
||||
viewGroups: ViewGroup[];
|
||||
position: number;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ViewField } from '@/views/types/ViewField';
|
||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { ViewFilterGroup } from '@/views/types/ViewFilterGroup';
|
||||
import { ViewGroup } from '@/views/types/ViewGroup';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
import { ViewSort } from '@/views/types/ViewSort';
|
||||
@ -15,6 +16,7 @@ export type View = {
|
||||
viewFields: ViewField[];
|
||||
viewGroups: ViewGroup[];
|
||||
viewFilters: ViewFilter[];
|
||||
viewFilterGroups?: ViewFilterGroup[];
|
||||
viewSorts: ViewSort[];
|
||||
kanbanFieldMetadataId: string;
|
||||
position: number;
|
||||
|
@ -12,5 +12,7 @@ export type ViewFilter = {
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
viewId?: string;
|
||||
viewFilterGroupId?: string;
|
||||
positionInViewFilterGroup?: number | null;
|
||||
definition?: FilterDefinition;
|
||||
};
|
||||
|
@ -0,0 +1,10 @@
|
||||
import { ViewFilterGroupLogicalOperator } from '@/views/types/ViewFilterGroupLogicalOperator';
|
||||
|
||||
export type ViewFilterGroup = {
|
||||
__typename: 'ViewFilterGroup';
|
||||
id: string;
|
||||
viewId: string;
|
||||
parentViewFilterGroupId?: string | null;
|
||||
logicalOperator: ViewFilterGroupLogicalOperator;
|
||||
positionInViewFilterGroup?: number | null;
|
||||
};
|
@ -0,0 +1,4 @@
|
||||
export enum ViewFilterGroupLogicalOperator {
|
||||
AND = 'AND',
|
||||
OR = 'OR',
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
import { ViewFilterGroup } from '@/views/types/ViewFilterGroup';
|
||||
|
||||
export const getCombinedViewFilterGroups = (
|
||||
viewFilterGroups: ViewFilterGroup[],
|
||||
unsavedToUpsertViewFilterGroups: ViewFilterGroup[],
|
||||
unsavedToDeleteViewFilterGroupIds: string[],
|
||||
): ViewFilterGroup[] => {
|
||||
const toCreateViewFilterGroups = unsavedToUpsertViewFilterGroups.filter(
|
||||
(toUpsertViewFilterGroup) =>
|
||||
!viewFilterGroups.some(
|
||||
(viewFilterGroup) => viewFilterGroup.id === toUpsertViewFilterGroup.id,
|
||||
),
|
||||
);
|
||||
|
||||
const toUpdateViewFilterGroups = unsavedToUpsertViewFilterGroups.filter(
|
||||
(toUpsertViewFilterGroup) =>
|
||||
viewFilterGroups.some(
|
||||
(viewFilterGroup) => viewFilterGroup.id === toUpsertViewFilterGroup.id,
|
||||
),
|
||||
);
|
||||
|
||||
const combinedViewFilterGroups = viewFilterGroups
|
||||
.filter(
|
||||
(viewFilterGroup) =>
|
||||
!unsavedToDeleteViewFilterGroupIds.includes(viewFilterGroup.id),
|
||||
)
|
||||
.map((viewFilterGroup) => {
|
||||
const toUpdateViewFilterGroup = toUpdateViewFilterGroups.find(
|
||||
(toUpdateViewFilterGroup) =>
|
||||
toUpdateViewFilterGroup.id === viewFilterGroup.id,
|
||||
);
|
||||
|
||||
return toUpdateViewFilterGroup ?? viewFilterGroup;
|
||||
})
|
||||
.concat(toCreateViewFilterGroups);
|
||||
|
||||
return combinedViewFilterGroups;
|
||||
};
|
@ -8,24 +8,19 @@ export const getCombinedViewFilters = (
|
||||
const toCreateViewFilters = toUpsertViewFilters.filter(
|
||||
(toUpsertViewFilter) =>
|
||||
!viewFilters.some(
|
||||
(viewFilter) =>
|
||||
viewFilter.fieldMetadataId === toUpsertViewFilter.fieldMetadataId,
|
||||
(viewFilter) => viewFilter.id === toUpsertViewFilter.id,
|
||||
),
|
||||
);
|
||||
|
||||
const toUpdateViewFilters = toUpsertViewFilters.filter((toUpsertViewFilter) =>
|
||||
viewFilters.some(
|
||||
(viewFilter) =>
|
||||
viewFilter.fieldMetadataId === toUpsertViewFilter.fieldMetadataId,
|
||||
),
|
||||
viewFilters.some((viewFilter) => viewFilter.id === toUpsertViewFilter.id),
|
||||
);
|
||||
|
||||
const combinedViewFilters = viewFilters
|
||||
.filter((viewFilter) => !toDeleteViewFilterIds.includes(viewFilter.id))
|
||||
.map((viewFilter) => {
|
||||
const toUpdateViewFilter = toUpdateViewFilters.find(
|
||||
(toUpdateViewFilter) =>
|
||||
toUpdateViewFilter.fieldMetadataId === viewFilter.fieldMetadataId,
|
||||
(toUpdateViewFilter) => toUpdateViewFilter.id === viewFilter.id,
|
||||
);
|
||||
|
||||
return toUpdateViewFilter ?? viewFilter;
|
||||
|
@ -3,7 +3,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { formatFieldMetadataItemsAsFilterDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
|
||||
import { formatFieldMetadataItemsAsSortDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions';
|
||||
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 { View } from '@/views/types/View';
|
||||
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
|
||||
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
|
||||
@ -27,7 +27,7 @@ export const getQueryVariablesFromView = ({
|
||||
};
|
||||
}
|
||||
|
||||
const { viewFilters, viewSorts } = view;
|
||||
const { viewFilterGroups, viewFilters, viewSorts } = view;
|
||||
|
||||
const filterDefinitions = formatFieldMetadataItemsAsFilterDefinitions({
|
||||
fields: fieldMetadataItems,
|
||||
@ -38,9 +38,10 @@ export const getQueryVariablesFromView = ({
|
||||
fields: fieldMetadataItems,
|
||||
});
|
||||
|
||||
const filter = turnFiltersIntoQueryFilter(
|
||||
const filter = computeViewRecordGqlOperationFilter(
|
||||
mapViewFiltersToFilters(viewFilters, filterDefinitions),
|
||||
objectMetadataItem?.fields ?? [],
|
||||
viewFilterGroups ?? [],
|
||||
);
|
||||
|
||||
const orderBy = turnSortsIntoOrderBy(
|
||||
|
@ -23,6 +23,8 @@ export const mapViewFiltersToFilters = (
|
||||
value: viewFilter.value,
|
||||
displayValue: viewFilter.displayValue,
|
||||
operand: viewFilter.operand,
|
||||
viewFilterGroupId: viewFilter.viewFilterGroupId,
|
||||
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
|
||||
definition: viewFilter.definition ?? availableFilterDefinition,
|
||||
};
|
||||
})
|
||||
|
@ -0,0 +1,18 @@
|
||||
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const shouldReplaceFilter = (
|
||||
oldFilter: Pick<Filter, 'id' | 'fieldMetadataId' | 'viewFilterGroupId'>,
|
||||
newFilter: Pick<Filter, 'id' | 'fieldMetadataId' | 'viewFilterGroupId'>,
|
||||
) => {
|
||||
const isNewFilterAdvancedFilter = isDefined(newFilter.viewFilterGroupId);
|
||||
|
||||
if (isNewFilterAdvancedFilter) {
|
||||
return newFilter.id === oldFilter.id;
|
||||
} else {
|
||||
return (
|
||||
newFilter.fieldMetadataId === oldFilter.fieldMetadataId &&
|
||||
!oldFilter.viewFilterGroupId
|
||||
);
|
||||
}
|
||||
};
|
@ -0,0 +1,15 @@
|
||||
import { ViewFilterGroup } from '@/views/types/ViewFilterGroup';
|
||||
|
||||
export const sortViewFilterGroupsOutermostFirst = (
|
||||
viewFilterGroups: ViewFilterGroup[],
|
||||
parentViewFilterGroupId?: string,
|
||||
): ViewFilterGroup[] => {
|
||||
const childGroups = viewFilterGroups.filter(
|
||||
(group) => group.parentViewFilterGroupId === parentViewFilterGroupId,
|
||||
);
|
||||
|
||||
return childGroups.flatMap((group) => [
|
||||
group,
|
||||
...sortViewFilterGroupsOutermostFirst(viewFilterGroups, group.id),
|
||||
]);
|
||||
};
|
@ -382,6 +382,15 @@ export const VIEW_FILTER_STANDARD_FIELD_IDS = {
|
||||
value: '20202020-1e55-4a1e-a1d2-fefb86a5fce5',
|
||||
displayValue: '20202020-1270-4ebf-9018-c0ec10d5038e',
|
||||
view: '20202020-4f5b-487e-829c-3d881c163611',
|
||||
viewFilterGroupId: '20202020-2580-420a-8328-cab1635c0296',
|
||||
positionInViewFilterGroup: '20202020-3bb0-4f66-a537-a46fe0dc468f',
|
||||
};
|
||||
|
||||
export const VIEW_FILTER_GROUP_STANDARD_FIELD_IDS = {
|
||||
view: '20202020-ff7a-4b54-8be5-aa0249047b74',
|
||||
parentViewFilterGroupId: '20202020-edbf-4929-8ede-64f48d6bf2a7',
|
||||
logicalOperator: '20202020-64d9-4bc5-85ba-c250796ce9aa',
|
||||
positionInViewFilterGroup: '20202020-90d6-4299-ad87-d05ddd3a0a3f',
|
||||
};
|
||||
|
||||
export const VIEW_SORT_STANDARD_FIELD_IDS = {
|
||||
@ -402,6 +411,7 @@ export const VIEW_STANDARD_FIELD_IDS = {
|
||||
viewFields: '20202020-542b-4bdc-b177-b63175d48edf',
|
||||
viewGroups: '20202020-e1a1-419f-ac81-1986a5ea59a8',
|
||||
viewFilters: '20202020-ff23-4154-b63c-21fb36cd0967',
|
||||
viewFilterGroups: '20202020-0318-474a-84a1-bac895ceaa5a',
|
||||
viewSorts: '20202020-891b-45c3-9fe1-80a75b4aa043',
|
||||
favorites: '20202020-c818-4a86-8284-9ec0ef0a59a5',
|
||||
};
|
||||
|
@ -37,6 +37,7 @@ export const STANDARD_OBJECT_IDS = {
|
||||
viewField: '20202020-4d19-4655-95bf-b2a04cf206d4',
|
||||
viewGroup: '20202020-725f-47a4-8008-4255f9519f70',
|
||||
viewFilter: '20202020-6fb6-4631-aded-b7d67e952ec8',
|
||||
viewFilterGroup: '20202020-b920-4b11-92aa-9b07d878e542',
|
||||
viewSort: '20202020-e46a-47a8-939a-e5d911f83531',
|
||||
view: '20202020-722e-4739-8e2c-0c372d661f49',
|
||||
webhook: '20202020-be4d-4e08-811d-0fffcd13ffd4',
|
||||
|
@ -27,6 +27,7 @@ import { AuditLogWorkspaceEntity } from 'src/modules/timeline/standard-objects/a
|
||||
import { BehavioralEventWorkspaceEntity } from 'src/modules/timeline/standard-objects/behavioral-event.workspace-entity';
|
||||
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
||||
import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
|
||||
import { ViewFilterGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter-group.workspace-entity';
|
||||
import { ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity';
|
||||
import { ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity';
|
||||
import { ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity';
|
||||
@ -59,6 +60,7 @@ export const standardObjectMetadataDefinitions = [
|
||||
ViewFieldWorkspaceEntity,
|
||||
ViewGroupWorkspaceEntity,
|
||||
ViewFilterWorkspaceEntity,
|
||||
ViewFilterGroupWorkspaceEntity,
|
||||
ViewSortWorkspaceEntity,
|
||||
ViewWorkspaceEntity,
|
||||
WebhookWorkspaceEntity,
|
||||
|
@ -0,0 +1,95 @@
|
||||
import { Relation } from 'typeorm';
|
||||
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||
import { VIEW_FILTER_GROUP_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||
|
||||
export enum ViewFilterGroupLogicalOperator {
|
||||
AND = 'AND',
|
||||
OR = 'OR',
|
||||
NOT = 'NOT',
|
||||
}
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.viewFilterGroup,
|
||||
namePlural: 'viewFilterGroups',
|
||||
labelSingular: 'View Filter Group',
|
||||
labelPlural: 'View Filter Groups',
|
||||
description: '(System) View Filter Groups',
|
||||
icon: 'IconFilterBolt',
|
||||
})
|
||||
@WorkspaceIsNotAuditLogged()
|
||||
@WorkspaceIsSystem()
|
||||
export class ViewFilterGroupWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
@WorkspaceRelation({
|
||||
standardId: VIEW_FILTER_GROUP_STANDARD_FIELD_IDS.view,
|
||||
type: RelationMetadataType.MANY_TO_ONE,
|
||||
label: 'View',
|
||||
description: 'View',
|
||||
inverseSideTarget: () => ViewWorkspaceEntity,
|
||||
inverseSideFieldKey: 'viewFilterGroups',
|
||||
})
|
||||
view: Relation<ViewWorkspaceEntity>;
|
||||
|
||||
@WorkspaceJoinColumn('view')
|
||||
viewId: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_FILTER_GROUP_STANDARD_FIELD_IDS.parentViewFilterGroupId,
|
||||
type: FieldMetadataType.UUID,
|
||||
label: 'Parent View Filter Group Id',
|
||||
description: 'Parent View Filter Group',
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
parentViewFilterGroupId: string | null;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_FILTER_GROUP_STANDARD_FIELD_IDS.logicalOperator,
|
||||
type: FieldMetadataType.SELECT,
|
||||
label: 'Logical Operator',
|
||||
description: 'Logical operator for the filter group',
|
||||
options: [
|
||||
{
|
||||
value: ViewFilterGroupLogicalOperator.AND,
|
||||
label: 'AND',
|
||||
position: 0,
|
||||
color: 'blue',
|
||||
},
|
||||
{
|
||||
value: ViewFilterGroupLogicalOperator.OR,
|
||||
label: 'OR',
|
||||
position: 1,
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
value: ViewFilterGroupLogicalOperator.NOT,
|
||||
label: 'NOT',
|
||||
position: 2,
|
||||
color: 'red',
|
||||
},
|
||||
],
|
||||
defaultValue: `'${ViewFilterGroupLogicalOperator.NOT}'`,
|
||||
})
|
||||
logicalOperator: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_FILTER_GROUP_STANDARD_FIELD_IDS.positionInViewFilterGroup,
|
||||
type: FieldMetadataType.POSITION,
|
||||
label: 'Position in view filter group',
|
||||
description: 'Position in the parent view filter group',
|
||||
icon: 'IconHierarchy2',
|
||||
})
|
||||
@WorkspaceIsSystem()
|
||||
@WorkspaceIsNullable()
|
||||
positionInViewFilterGroup: number | null;
|
||||
}
|
@ -72,4 +72,24 @@ export class ViewFilterWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
|
||||
@WorkspaceJoinColumn('view')
|
||||
viewId: string | null;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_FILTER_STANDARD_FIELD_IDS.viewFilterGroupId,
|
||||
type: FieldMetadataType.UUID,
|
||||
label: 'View Filter Group Id',
|
||||
description: 'View Filter Group',
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
viewFilterGroupId: string | null;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_FILTER_STANDARD_FIELD_IDS.positionInViewFilterGroup,
|
||||
type: FieldMetadataType.POSITION,
|
||||
label: 'Position in view filter group',
|
||||
description: 'Position in the view filter group',
|
||||
icon: 'IconHierarchy2',
|
||||
})
|
||||
@WorkspaceIsSystem()
|
||||
@WorkspaceIsNullable()
|
||||
positionInViewFilterGroup: number | null;
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import { VIEW_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
|
||||
import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
|
||||
import { ViewFilterGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter-group.workspace-entity';
|
||||
import { ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity';
|
||||
import { ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity';
|
||||
import { ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity';
|
||||
@ -138,6 +139,18 @@ export class ViewWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
@WorkspaceIsNullable()
|
||||
viewFilters: Relation<ViewFilterWorkspaceEntity[]>;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: VIEW_STANDARD_FIELD_IDS.viewFilterGroups,
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
label: 'View Filter Groups',
|
||||
description: 'View Filter Groups',
|
||||
icon: 'IconFilterBolt',
|
||||
inverseSideTarget: () => ViewFilterGroupWorkspaceEntity,
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
viewFilterGroups: Relation<ViewFilterGroupWorkspaceEntity[]>;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: VIEW_STANDARD_FIELD_IDS.viewSorts,
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
|
@ -126,6 +126,7 @@ export {
|
||||
IconFileZip,
|
||||
IconFilter,
|
||||
IconFilterOff,
|
||||
IconFilterCog,
|
||||
IconFocusCentered,
|
||||
IconForbid,
|
||||
IconFunction,
|
||||
@ -151,6 +152,7 @@ export {
|
||||
IconLayoutSidebarLeftCollapse,
|
||||
IconLayoutSidebarRightCollapse,
|
||||
IconLayoutSidebarRightExpand,
|
||||
IconLibraryPlus,
|
||||
IconLink,
|
||||
IconLinkOff,
|
||||
IconList,
|
||||
|
@ -3,7 +3,6 @@ export * from './components';
|
||||
export * from './display';
|
||||
export * from './feedback';
|
||||
export * from './input';
|
||||
export * from './feedback';
|
||||
export * from './layout';
|
||||
export * from './navigation';
|
||||
export * from './testing';
|
||||
|
Loading…
Reference in New Issue
Block a user