Feat: Advanced filter (#7700)
Design:  Not ready to be merged yet! --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -25,9 +25,9 @@ const jestConfig: JestConfigWithTsJest = {
|
|||||||
extensionsToTreatAsEsm: ['.ts', '.tsx'],
|
extensionsToTreatAsEsm: ['.ts', '.tsx'],
|
||||||
coverageThreshold: {
|
coverageThreshold: {
|
||||||
global: {
|
global: {
|
||||||
statements: 59,
|
statements: 58,
|
||||||
lines: 55,
|
lines: 55,
|
||||||
functions: 48,
|
functions: 47,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
collectCoverageFrom: ['<rootDir>/src/**/*.ts'],
|
collectCoverageFrom: ['<rootDir>/src/**/*.ts'],
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
|
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';
|
import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables';
|
||||||
|
|
||||||
export const computeContextStoreFilters = (
|
export const computeContextStoreFilters = (
|
||||||
@ -12,9 +12,10 @@ export const computeContextStoreFilters = (
|
|||||||
|
|
||||||
if (contextStoreTargetedRecordsRule.mode === 'exclusion') {
|
if (contextStoreTargetedRecordsRule.mode === 'exclusion') {
|
||||||
queryFilter = makeAndFilterVariables([
|
queryFilter = makeAndFilterVariables([
|
||||||
turnFiltersIntoQueryFilter(
|
computeViewRecordGqlOperationFilter(
|
||||||
contextStoreTargetedRecordsRule.filters,
|
contextStoreTargetedRecordsRule.filters,
|
||||||
objectMetadataItem?.fields ?? [],
|
objectMetadataItem?.fields ?? [],
|
||||||
|
[],
|
||||||
),
|
),
|
||||||
contextStoreTargetedRecordsRule.excludedRecordIds.length > 0
|
contextStoreTargetedRecordsRule.excludedRecordIds.length > 0
|
||||||
? {
|
? {
|
||||||
|
|||||||
@ -24,6 +24,7 @@ export enum CoreObjectNameSingular {
|
|||||||
View = 'view',
|
View = 'view',
|
||||||
ViewField = 'viewField',
|
ViewField = 'viewField',
|
||||||
ViewFilter = 'viewFilter',
|
ViewFilter = 'viewFilter',
|
||||||
|
ViewFilterGroup = 'viewFilterGroup',
|
||||||
ViewSort = 'viewSort',
|
ViewSort = 'viewSort',
|
||||||
ViewGroup = 'viewGroup',
|
ViewGroup = 'viewGroup',
|
||||||
Webhook = 'webhook',
|
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 { 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 { 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 { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState';
|
||||||
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
|
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
|
||||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||||
@ -48,11 +48,13 @@ export const MultipleFiltersDropdownContent = ({
|
|||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
{shoudShowFilterInput ? (
|
{shoudShowFilterInput ? (
|
||||||
<ObjectFilterDropdownFilterInput filterDropdownId={filterDropdownId} />
|
<ObjectFilterOperandSelectAndInput
|
||||||
|
filterDropdownId={filterDropdownId}
|
||||||
|
/>
|
||||||
) : shouldShowCompositeSelectionSubMenu ? (
|
) : shouldShowCompositeSelectionSubMenu ? (
|
||||||
<ObjectFilterDropdownFilterSelectCompositeFieldSubMenu />
|
<ObjectFilterDropdownFilterSelectCompositeFieldSubMenu />
|
||||||
) : (
|
) : (
|
||||||
<ObjectFilterDropdownFilterSelect />
|
<ObjectFilterDropdownFilterSelect isAdvancedFilterButtonVisible />
|
||||||
)}
|
)}
|
||||||
<MultipleFiltersDropdownFilterOnFilterChangedEffect
|
<MultipleFiltersDropdownFilterOnFilterChangedEffect
|
||||||
filterDefinitionUsedInDropdownType={
|
filterDefinitionUsedInDropdownType={
|
||||||
|
|||||||
@ -63,6 +63,7 @@ export const ObjectFilterDropdownDateInput = () => {
|
|||||||
: newDate.toLocaleDateString()
|
: newDate.toLocaleDateString()
|
||||||
: '',
|
: '',
|
||||||
definition: filterDefinitionUsedInDropdown,
|
definition: filterDefinitionUsedInDropdown,
|
||||||
|
viewFilterGroupId: selectedFilter?.viewFilterGroupId,
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsObjectFilterDropdownUnfolded(false);
|
setIsObjectFilterDropdownUnfolded(false);
|
||||||
@ -92,6 +93,7 @@ export const ObjectFilterDropdownDateInput = () => {
|
|||||||
operand: selectedOperandInDropdown,
|
operand: selectedOperandInDropdown,
|
||||||
displayValue: getRelativeDateDisplayValue(relativeDate),
|
displayValue: getRelativeDateDisplayValue(relativeDate),
|
||||||
definition: filterDefinitionUsedInDropdown,
|
definition: filterDefinitionUsedInDropdown,
|
||||||
|
viewFilterGroupId: selectedFilter?.viewFilterGroupId,
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsObjectFilterDropdownUnfolded(false);
|
setIsObjectFilterDropdownUnfolded(false);
|
||||||
|
|||||||
@ -2,8 +2,6 @@ import { useRecoilValue } from 'recoil';
|
|||||||
|
|
||||||
import { ObjectFilterDropdownDateInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput';
|
import { ObjectFilterDropdownDateInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput';
|
||||||
import { ObjectFilterDropdownNumberInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownNumberInput';
|
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 { ObjectFilterDropdownOptionSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect';
|
||||||
import { ObjectFilterDropdownRatingInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput';
|
import { ObjectFilterDropdownRatingInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput';
|
||||||
import { ObjectFilterDropdownRecordSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect';
|
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 { isActorSourceCompositeFilter } from '@/object-record/object-filter-dropdown/utils/isActorSourceCompositeFilter';
|
||||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { isDefined } from 'twenty-ui';
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
const StyledOperandSelectContainer = styled.div`
|
import { DATE_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/DateFilterTypes';
|
||||||
background: ${({ theme }) => theme.background.secondary};
|
import { NUMBER_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/NumberFilterTypes';
|
||||||
box-shadow: ${({ theme }) => theme.boxShadow.light};
|
import { TEXT_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/TextFilterTypes';
|
||||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
|
||||||
left: 10px;
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 1000;
|
|
||||||
`;
|
|
||||||
|
|
||||||
type ObjectFilterDropdownFilterInputProps = {
|
type ObjectFilterDropdownFilterInputProps = {
|
||||||
filterDropdownId?: string;
|
filterDropdownId?: string;
|
||||||
@ -38,13 +28,8 @@ export const ObjectFilterDropdownFilterInput = ({
|
|||||||
const {
|
const {
|
||||||
filterDefinitionUsedInDropdownState,
|
filterDefinitionUsedInDropdownState,
|
||||||
selectedOperandInDropdownState,
|
selectedOperandInDropdownState,
|
||||||
isObjectFilterDropdownOperandSelectUnfoldedState,
|
|
||||||
} = useFilterDropdown({ filterDropdownId });
|
} = useFilterDropdown({ filterDropdownId });
|
||||||
|
|
||||||
const isObjectFilterDropdownOperandSelectUnfolded = useRecoilValue(
|
|
||||||
isObjectFilterDropdownOperandSelectUnfoldedState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const filterDefinitionUsedInDropdown = useRecoilValue(
|
const filterDefinitionUsedInDropdown = useRecoilValue(
|
||||||
filterDefinitionUsedInDropdownState,
|
filterDefinitionUsedInDropdownState,
|
||||||
);
|
);
|
||||||
@ -74,40 +59,21 @@ export const ObjectFilterDropdownFilterInput = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ObjectFilterDropdownOperandButton />
|
|
||||||
{isObjectFilterDropdownOperandSelectUnfolded && (
|
|
||||||
<StyledOperandSelectContainer>
|
|
||||||
<ObjectFilterDropdownOperandSelect />
|
|
||||||
</StyledOperandSelectContainer>
|
|
||||||
)}
|
|
||||||
{isConfigurable && selectedOperandInDropdown && (
|
{isConfigurable && selectedOperandInDropdown && (
|
||||||
<>
|
<>
|
||||||
{[
|
{TEXT_FILTER_TYPES.includes(filterDefinitionUsedInDropdown.type) &&
|
||||||
'TEXT',
|
|
||||||
'EMAIL',
|
|
||||||
'EMAILS',
|
|
||||||
'PHONE',
|
|
||||||
'FULL_NAME',
|
|
||||||
'LINK',
|
|
||||||
'LINKS',
|
|
||||||
'ADDRESS',
|
|
||||||
'ACTOR',
|
|
||||||
'ARRAY',
|
|
||||||
'RAW_JSON',
|
|
||||||
'PHONES',
|
|
||||||
].includes(filterDefinitionUsedInDropdown.type) &&
|
|
||||||
!isActorSourceCompositeFilter(filterDefinitionUsedInDropdown) && (
|
!isActorSourceCompositeFilter(filterDefinitionUsedInDropdown) && (
|
||||||
<ObjectFilterDropdownTextSearchInput />
|
<ObjectFilterDropdownTextSearchInput />
|
||||||
)}
|
)}
|
||||||
{['NUMBER', 'CURRENCY'].includes(
|
{NUMBER_FILTER_TYPES.includes(
|
||||||
filterDefinitionUsedInDropdown.type,
|
filterDefinitionUsedInDropdown.type,
|
||||||
) && <ObjectFilterDropdownNumberInput />}
|
) && <ObjectFilterDropdownNumberInput />}
|
||||||
{filterDefinitionUsedInDropdown.type === 'RATING' && (
|
{filterDefinitionUsedInDropdown.type === 'RATING' && (
|
||||||
<ObjectFilterDropdownRatingInput />
|
<ObjectFilterDropdownRatingInput />
|
||||||
)}
|
)}
|
||||||
{['DATE_TIME', 'DATE'].includes(
|
{DATE_FILTER_TYPES.includes(filterDefinitionUsedInDropdown.type) && (
|
||||||
filterDefinitionUsedInDropdown.type,
|
<ObjectFilterDropdownDateInput />
|
||||||
) && <ObjectFilterDropdownDateInput />}
|
)}
|
||||||
{filterDefinitionUsedInDropdown.type === 'RELATION' && (
|
{filterDefinitionUsedInDropdown.type === 'RELATION' && (
|
||||||
<>
|
<>
|
||||||
<ObjectFilterDropdownSearchInput />
|
<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 { 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 { ObjectFilterDropdownFilterSelectMenuItem } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem';
|
||||||
import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId';
|
import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId';
|
||||||
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
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 { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
|
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
||||||
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
|
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { isDefined } from 'twenty-ui';
|
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 {
|
const {
|
||||||
setObjectFilterDropdownSearchInput,
|
setObjectFilterDropdownSearchInput,
|
||||||
objectFilterDropdownSearchInputState,
|
objectFilterDropdownSearchInputState,
|
||||||
|
advancedFilterViewFilterIdState,
|
||||||
} = useFilterDropdown();
|
} = useFilterDropdown();
|
||||||
|
|
||||||
|
const advancedFilterViewFilterId = useRecoilValue(
|
||||||
|
advancedFilterViewFilterIdState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { closeAdvancedFilterDropdown } = useAdvancedFilterDropdown(
|
||||||
|
advancedFilterViewFilterId,
|
||||||
|
);
|
||||||
|
|
||||||
const objectFilterDropdownSearchInput = useRecoilValue(
|
const objectFilterDropdownSearchInput = useRecoilValue(
|
||||||
objectFilterDropdownSearchInputState,
|
objectFilterDropdownSearchInputState,
|
||||||
);
|
);
|
||||||
@ -110,14 +128,22 @@ export const ObjectFilterDropdownFilterSelect = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resetSelectedItem();
|
resetSelectedItem();
|
||||||
|
|
||||||
selectFilter({ filterDefinition: selectedFilterDefinition });
|
selectFilter({ filterDefinition: selectedFilterDefinition });
|
||||||
|
closeAdvancedFilterDropdown();
|
||||||
};
|
};
|
||||||
|
|
||||||
const shoudShowSeparator =
|
const shoudShowSeparator =
|
||||||
visibleColumnsFilterDefinitions.length > 0 &&
|
visibleColumnsFilterDefinitions.length > 0 &&
|
||||||
hiddenColumnsFilterDefinitions.length > 0;
|
hiddenColumnsFilterDefinitions.length > 0;
|
||||||
|
|
||||||
|
const { currentViewId, currentViewWithCombinedFiltersAndSorts } =
|
||||||
|
useGetCurrentView();
|
||||||
|
|
||||||
|
const shouldShowAdvancedFilterButton =
|
||||||
|
isDefined(currentViewId) &&
|
||||||
|
isDefined(currentViewWithCombinedFiltersAndSorts?.objectMetadataId) &&
|
||||||
|
isAdvancedFilterButtonVisible;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledInput
|
<StyledInput
|
||||||
@ -164,6 +190,7 @@ export const ObjectFilterDropdownFilterSelect = () => {
|
|||||||
)}
|
)}
|
||||||
</DropdownMenuItemsContainer>
|
</DropdownMenuItemsContainer>
|
||||||
</SelectableList>
|
</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 { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
||||||
import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState';
|
import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState';
|
||||||
import { objectFilterDropdownFirstLevelFilterDefinitionComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFirstLevelFilterDefinitionComponentState';
|
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 { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
|
||||||
import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel';
|
import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel';
|
||||||
import { getFilterableFieldTypeLabel } from '@/object-record/object-filter-dropdown/utils/getFilterableFieldTypeLabel';
|
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 { getOperandsForFilterDefinition } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType';
|
||||||
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
|
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
|
||||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
|
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 { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
import { IconApps, IconChevronLeft, isDefined, useIcons } from 'twenty-ui';
|
import { IconApps, IconChevronLeft, isDefined, useIcons } from 'twenty-ui';
|
||||||
|
|
||||||
export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => {
|
export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => {
|
||||||
@ -47,10 +50,46 @@ export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => {
|
|||||||
setFilterDefinitionUsedInDropdown,
|
setFilterDefinitionUsedInDropdown,
|
||||||
setSelectedOperandInDropdown,
|
setSelectedOperandInDropdown,
|
||||||
setObjectFilterDropdownSearchInput,
|
setObjectFilterDropdownSearchInput,
|
||||||
|
selectFilter,
|
||||||
|
advancedFilterViewFilterIdState,
|
||||||
|
advancedFilterViewFilterGroupIdState,
|
||||||
} = useFilterDropdown();
|
} = useFilterDropdown();
|
||||||
|
|
||||||
|
const advancedFilterViewFilterId = useRecoilValue(
|
||||||
|
advancedFilterViewFilterIdState,
|
||||||
|
);
|
||||||
|
const advancedFilterViewFilterGroupId = useRecoilValue(
|
||||||
|
advancedFilterViewFilterGroupIdState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { closeAdvancedFilterDropdown } = useAdvancedFilterDropdown(
|
||||||
|
advancedFilterViewFilterId,
|
||||||
|
);
|
||||||
|
|
||||||
const handleSelectFilter = (definition: FilterDefinition | null) => {
|
const handleSelectFilter = (definition: FilterDefinition | null) => {
|
||||||
if (definition !== 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);
|
setFilterDefinitionUsedInDropdown(definition);
|
||||||
|
|
||||||
setSelectedOperandInDropdown(
|
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 { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId';
|
||||||
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
||||||
import { useSelectFilter } from '@/object-record/object-filter-dropdown/hooks/useSelectFilter';
|
import { useSelectFilter } from '@/object-record/object-filter-dropdown/hooks/useSelectFilter';
|
||||||
@ -59,11 +60,23 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({
|
|||||||
setFilterDefinitionUsedInDropdown,
|
setFilterDefinitionUsedInDropdown,
|
||||||
setSelectedOperandInDropdown,
|
setSelectedOperandInDropdown,
|
||||||
setObjectFilterDropdownSearchInput,
|
setObjectFilterDropdownSearchInput,
|
||||||
|
advancedFilterViewFilterIdState,
|
||||||
} = useFilterDropdown();
|
} = useFilterDropdown();
|
||||||
|
|
||||||
const setHotkeyScope = useSetHotkeyScope();
|
const setHotkeyScope = useSetHotkeyScope();
|
||||||
|
|
||||||
|
const advancedFilterViewFilterId = useRecoilValue(
|
||||||
|
advancedFilterViewFilterIdState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { closeAdvancedFilterDropdown } = useAdvancedFilterDropdown(
|
||||||
|
advancedFilterViewFilterId,
|
||||||
|
);
|
||||||
|
|
||||||
const handleSelectFilter = (availableFilterDefinition: FilterDefinition) => {
|
const handleSelectFilter = (availableFilterDefinition: FilterDefinition) => {
|
||||||
|
closeAdvancedFilterDropdown();
|
||||||
|
selectFilter({ filterDefinition: availableFilterDefinition });
|
||||||
|
|
||||||
setFilterDefinitionUsedInDropdown(availableFilterDefinition);
|
setFilterDefinitionUsedInDropdown(availableFilterDefinition);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -87,8 +100,6 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({
|
|||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
resetSelectedItem();
|
resetSelectedItem();
|
||||||
|
|
||||||
selectFilter({ filterDefinition });
|
|
||||||
|
|
||||||
if (isACompositeField) {
|
if (isACompositeField) {
|
||||||
// TODO: create isCompositeFilterableFieldType type guard
|
// TODO: create isCompositeFilterableFieldType type guard
|
||||||
setObjectFilterDropdownSubMenuFieldType(
|
setObjectFilterDropdownSubMenuFieldType(
|
||||||
|
|||||||
@ -56,6 +56,7 @@ export const ObjectFilterDropdownNumberInput = () => {
|
|||||||
operand: selectedOperandInDropdown,
|
operand: selectedOperandInDropdown,
|
||||||
displayValue: newValue,
|
displayValue: newValue,
|
||||||
definition: filterDefinitionUsedInDropdown,
|
definition: filterDefinitionUsedInDropdown,
|
||||||
|
viewFilterGroupId: selectedFilter?.viewFilterGroupId,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -135,6 +135,7 @@ export const ObjectFilterDropdownOptionSelect = () => {
|
|||||||
displayValue: filterDisplayValue,
|
displayValue: filterDisplayValue,
|
||||||
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
|
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
|
||||||
value: newFilterValue,
|
value: newFilterValue,
|
||||||
|
viewFilterGroupId: selectedFilter?.viewFilterGroupId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
resetSelectedItem();
|
resetSelectedItem();
|
||||||
|
|||||||
@ -64,6 +64,7 @@ export const ObjectFilterDropdownRatingInput = () => {
|
|||||||
operand: selectedOperandInDropdown,
|
operand: selectedOperandInDropdown,
|
||||||
displayValue: convertFieldRatingValueToNumber(newValue),
|
displayValue: convertFieldRatingValueToNumber(newValue),
|
||||||
definition: filterDefinitionUsedInDropdown,
|
definition: filterDefinitionUsedInDropdown,
|
||||||
|
viewFilterGroupId: selectedFilter?.viewFilterGroupId,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -129,6 +129,7 @@ export const ObjectFilterDropdownRecordSelect = ({
|
|||||||
displayValue: filterDisplayValue,
|
displayValue: filterDisplayValue,
|
||||||
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
|
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
|
||||||
value: newFilterValue,
|
value: newFilterValue,
|
||||||
|
viewFilterGroupId: selectedFilter?.viewFilterGroupId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -115,6 +115,7 @@ export const ObjectFilterDropdownSourceSelect = ({
|
|||||||
displayValue: filterDisplayValue,
|
displayValue: filterDisplayValue,
|
||||||
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
|
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
|
||||||
value: newFilterValue,
|
value: newFilterValue,
|
||||||
|
viewFilterGroupId: selectedFilter?.viewFilterGroupId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -52,12 +52,13 @@ export const ObjectFilterDropdownTextSearchInput = () => {
|
|||||||
setObjectFilterDropdownSearchInput(event.target.value);
|
setObjectFilterDropdownSearchInput(event.target.value);
|
||||||
|
|
||||||
selectFilter?.({
|
selectFilter?.({
|
||||||
id: selectedFilter?.id ? selectedFilter.id : filterId,
|
id: selectedFilter?.id ?? filterId,
|
||||||
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
|
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
|
||||||
value: event.target.value,
|
value: event.target.value,
|
||||||
operand: selectedOperandInDropdown,
|
operand: selectedOperandInDropdown,
|
||||||
displayValue: event.target.value,
|
displayValue: event.target.value,
|
||||||
definition: filterDefinitionUsedInDropdown,
|
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 { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
|
||||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||||
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
|
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
|
||||||
|
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
|
||||||
|
|
||||||
const filterDropdownId = 'filterDropdownId';
|
const filterDropdownId = 'filterDropdownId';
|
||||||
const renderHookConfig = {
|
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[] = [
|
const filterDefinitions: FilterDefinition[] = [
|
||||||
@ -306,9 +319,10 @@ describe('useFilterDropdown', () => {
|
|||||||
|
|
||||||
it('should reset filter', async () => {
|
it('should reset filter', async () => {
|
||||||
const { result } = renderHook(() => {
|
const { result } = renderHook(() => {
|
||||||
const { selectFilter, resetFilter } = useFilterDropdown({
|
const { resetFilter, selectFilter } = useFilterDropdown({
|
||||||
filterDropdownId,
|
filterDropdownId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { selectedFilterState } = useFilterDropdownStates(filterDropdownId);
|
const { selectedFilterState } = useFilterDropdownStates(filterDropdownId);
|
||||||
|
|
||||||
const [selectedFilter, setSelectedFilter] =
|
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 { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState';
|
||||||
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
|
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
|
||||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
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 { ObjectFilterDropdownScopeInternalContext } from '../scopes/scope-internal-context/ObjectFilterDropdownScopeInternalContext';
|
||||||
import { Filter } from '../types/Filter';
|
import { Filter } from '../types/Filter';
|
||||||
|
|
||||||
type UseFilterDropdownProps = {
|
type UseFilterDropdownProps = {
|
||||||
filterDropdownId?: string;
|
filterDropdownId?: string;
|
||||||
|
advancedFilterViewFilterId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useFilterDropdown = (props?: UseFilterDropdownProps) => {
|
export const useFilterDropdown = (props?: UseFilterDropdownProps) => {
|
||||||
@ -30,17 +33,25 @@ export const useFilterDropdown = (props?: UseFilterDropdownProps) => {
|
|||||||
selectedFilterState,
|
selectedFilterState,
|
||||||
selectedOperandInDropdownState,
|
selectedOperandInDropdownState,
|
||||||
onFilterSelectState,
|
onFilterSelectState,
|
||||||
|
advancedFilterViewFilterGroupIdState,
|
||||||
|
advancedFilterViewFilterIdState,
|
||||||
} = useFilterDropdownStates(scopeId);
|
} = useFilterDropdownStates(scopeId);
|
||||||
|
|
||||||
|
const { upsertCombinedViewFilter } = useUpsertCombinedViewFilters();
|
||||||
|
|
||||||
const selectFilter = useRecoilCallback(
|
const selectFilter = useRecoilCallback(
|
||||||
({ set, snapshot }) =>
|
({ set, snapshot }) =>
|
||||||
(filter: Filter | null) => {
|
(filter: Filter | null) => {
|
||||||
set(selectedFilterState, filter);
|
set(selectedFilterState, filter);
|
||||||
const onFilterSelect = getSnapshotValue(snapshot, onFilterSelectState);
|
const onFilterSelect = getSnapshotValue(snapshot, onFilterSelectState);
|
||||||
|
|
||||||
|
if (isDefined(filter)) {
|
||||||
|
upsertCombinedViewFilter(filter);
|
||||||
|
}
|
||||||
|
|
||||||
onFilterSelect?.(filter);
|
onFilterSelect?.(filter);
|
||||||
},
|
},
|
||||||
[selectedFilterState, onFilterSelectState],
|
[selectedFilterState, onFilterSelectState, upsertCombinedViewFilter],
|
||||||
);
|
);
|
||||||
|
|
||||||
const emptyFilterButKeepDefinition = useRecoilCallback(
|
const emptyFilterButKeepDefinition = useRecoilCallback(
|
||||||
@ -117,6 +128,12 @@ export const useFilterDropdown = (props?: UseFilterDropdownProps) => {
|
|||||||
isObjectFilterDropdownUnfoldedState,
|
isObjectFilterDropdownUnfoldedState,
|
||||||
);
|
);
|
||||||
const setOnFilterSelect = useSetRecoilState(onFilterSelectState);
|
const setOnFilterSelect = useSetRecoilState(onFilterSelectState);
|
||||||
|
const setAdvancedFilterViewFilterGroupId = useSetRecoilState(
|
||||||
|
advancedFilterViewFilterGroupIdState,
|
||||||
|
);
|
||||||
|
const setAdvancedFilterViewFilterId = useSetRecoilState(
|
||||||
|
advancedFilterViewFilterIdState,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
scopeId,
|
scopeId,
|
||||||
@ -132,6 +149,8 @@ export const useFilterDropdown = (props?: UseFilterDropdownProps) => {
|
|||||||
setIsObjectFilterDropdownOperandSelectUnfolded,
|
setIsObjectFilterDropdownOperandSelectUnfolded,
|
||||||
setIsObjectFilterDropdownUnfolded,
|
setIsObjectFilterDropdownUnfolded,
|
||||||
setOnFilterSelect,
|
setOnFilterSelect,
|
||||||
|
setAdvancedFilterViewFilterGroupId,
|
||||||
|
setAdvancedFilterViewFilterId,
|
||||||
emptyFilterButKeepDefinition,
|
emptyFilterButKeepDefinition,
|
||||||
filterDefinitionUsedInDropdownState,
|
filterDefinitionUsedInDropdownState,
|
||||||
objectFilterDropdownSearchInputState,
|
objectFilterDropdownSearchInputState,
|
||||||
@ -143,5 +162,7 @@ export const useFilterDropdown = (props?: UseFilterDropdownProps) => {
|
|||||||
selectedFilterState,
|
selectedFilterState,
|
||||||
selectedOperandInDropdownState,
|
selectedOperandInDropdownState,
|
||||||
onFilterSelectState,
|
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 { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState';
|
||||||
import { isObjectFilterDropdownOperandSelectUnfoldedComponentState } from '@/object-record/object-filter-dropdown/states/isObjectFilterDropdownOperandSelectUnfoldedComponentState';
|
import { isObjectFilterDropdownOperandSelectUnfoldedComponentState } from '@/object-record/object-filter-dropdown/states/isObjectFilterDropdownOperandSelectUnfoldedComponentState';
|
||||||
import { isObjectFilterDropdownUnfoldedComponentState } from '@/object-record/object-filter-dropdown/states/isObjectFilterDropdownUnfoldedComponentState';
|
import { isObjectFilterDropdownUnfoldedComponentState } from '@/object-record/object-filter-dropdown/states/isObjectFilterDropdownUnfoldedComponentState';
|
||||||
@ -56,6 +58,16 @@ export const useFilterDropdownStates = (scopeId: string) => {
|
|||||||
scopeId,
|
scopeId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const advancedFilterViewFilterGroupIdState = extractComponentState(
|
||||||
|
advancedFilterViewFilterGroupIdComponentState,
|
||||||
|
scopeId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const advancedFilterViewFilterIdState = extractComponentState(
|
||||||
|
advancedFilterViewFilterIdComponentState,
|
||||||
|
scopeId,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filterDefinitionUsedInDropdownState,
|
filterDefinitionUsedInDropdownState,
|
||||||
objectFilterDropdownSearchInputState,
|
objectFilterDropdownSearchInputState,
|
||||||
@ -66,5 +78,7 @@ export const useFilterDropdownStates = (scopeId: string) => {
|
|||||||
selectedFilterState,
|
selectedFilterState,
|
||||||
selectedOperandInDropdownState,
|
selectedOperandInDropdownState,
|
||||||
onFilterSelectState,
|
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 { getOperandsForFilterDefinition } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType';
|
||||||
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
|
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
|
||||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { isDefined } from 'twenty-ui';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
type SelectFilterParams = {
|
type SelectFilterParams = {
|
||||||
@ -16,8 +18,17 @@ export const useSelectFilter = () => {
|
|||||||
setSelectedOperandInDropdown,
|
setSelectedOperandInDropdown,
|
||||||
setObjectFilterDropdownSearchInput,
|
setObjectFilterDropdownSearchInput,
|
||||||
selectFilter: filterDropdownSelectFilter,
|
selectFilter: filterDropdownSelectFilter,
|
||||||
|
advancedFilterViewFilterGroupIdState,
|
||||||
|
advancedFilterViewFilterIdState,
|
||||||
} = useFilterDropdown();
|
} = useFilterDropdown();
|
||||||
|
|
||||||
|
const advancedFilterViewFilterId = useRecoilValue(
|
||||||
|
advancedFilterViewFilterIdState,
|
||||||
|
);
|
||||||
|
const advancedFilterViewFilterGroupId = useRecoilValue(
|
||||||
|
advancedFilterViewFilterGroupIdState,
|
||||||
|
);
|
||||||
|
|
||||||
const setHotkeyScope = useSetHotkeyScope();
|
const setHotkeyScope = useSetHotkeyScope();
|
||||||
|
|
||||||
const selectFilter = ({ filterDefinition }: SelectFilterParams) => {
|
const selectFilter = ({ filterDefinition }: SelectFilterParams) => {
|
||||||
@ -39,14 +50,17 @@ export const useSelectFilter = () => {
|
|||||||
getOperandsForFilterDefinition(filterDefinition)[0],
|
getOperandsForFilterDefinition(filterDefinition)[0],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (value !== '') {
|
const isAdvancedFilter = isDefined(advancedFilterViewFilterId);
|
||||||
|
|
||||||
|
if (isAdvancedFilter || value !== '') {
|
||||||
filterDropdownSelectFilter({
|
filterDropdownSelectFilter({
|
||||||
id: v4(),
|
id: advancedFilterViewFilterId ?? v4(),
|
||||||
fieldMetadataId: filterDefinition.fieldMetadataId,
|
fieldMetadataId: filterDefinition.fieldMetadataId,
|
||||||
displayValue,
|
displayValue,
|
||||||
operand: getOperandsForFilterDefinition(filterDefinition)[0],
|
operand: getOperandsForFilterDefinition(filterDefinition)[0],
|
||||||
value,
|
value,
|
||||||
definition: filterDefinition,
|
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;
|
fieldMetadataId: string;
|
||||||
value: string;
|
value: string;
|
||||||
displayValue: string;
|
displayValue: string;
|
||||||
|
viewFilterGroupId?: string;
|
||||||
displayAvatarUrl?: string;
|
displayAvatarUrl?: string;
|
||||||
operand: ViewFilterOperand;
|
operand: ViewFilterOperand;
|
||||||
|
positionInViewFilterGroup?: number | null;
|
||||||
definition: FilterDefinition;
|
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';
|
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||||
|
|
||||||
export const getOperandsForFilterDefinition = (
|
export const getOperandsForFilterDefinition = (
|
||||||
filterDefinition: FilterDefinition,
|
filterDefinition: Pick<FilterDefinition, 'type' | 'compositeFieldName'>,
|
||||||
): ViewFilterOperand[] => {
|
): ViewFilterOperand[] => {
|
||||||
const emptyOperands = [
|
const emptyOperands = [
|
||||||
ViewFilterOperand.IsEmpty,
|
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';
|
import { FieldActorValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
|
||||||
export const isActorSourceCompositeFilter = (
|
export const isActorSourceCompositeFilter = (
|
||||||
filterDefinition: FilterDefinition,
|
filterDefinition: Pick<FilterDefinition, 'compositeFieldName'>,
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
filterDefinition.compositeFieldName ===
|
filterDefinition.compositeFieldName ===
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
|
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 { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||||
import { getCompaniesMock } from '~/testing/mock-data/companies';
|
import { getCompaniesMock } from '~/testing/mock-data/companies';
|
||||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||||
@ -16,7 +16,7 @@ const personMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
|
|||||||
|
|
||||||
jest.useFakeTimers().setSystemTime(new Date('2020-01-01'));
|
jest.useFakeTimers().setSystemTime(new Date('2020-01-01'));
|
||||||
|
|
||||||
describe('turnFiltersIntoQueryFilter', () => {
|
describe('computeViewRecordGqlOperationFilter', () => {
|
||||||
it('should work as expected for single filter', () => {
|
it('should work as expected for single filter', () => {
|
||||||
const companyMockNameFieldMetadataId =
|
const companyMockNameFieldMetadataId =
|
||||||
companyMockObjectMetadataItem.fields.find(
|
companyMockObjectMetadataItem.fields.find(
|
||||||
@ -37,9 +37,10 @@ describe('turnFiltersIntoQueryFilter', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = turnFiltersIntoQueryFilter(
|
const result = computeViewRecordGqlOperationFilter(
|
||||||
[nameFilter],
|
[nameFilter],
|
||||||
companyMockObjectMetadataItem.fields,
|
companyMockObjectMetadataItem.fields,
|
||||||
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
@ -88,9 +89,10 @@ describe('turnFiltersIntoQueryFilter', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = turnFiltersIntoQueryFilter(
|
const result = computeViewRecordGqlOperationFilter(
|
||||||
[nameFilter, employeesFilter],
|
[nameFilter, employeesFilter],
|
||||||
companyMockObjectMetadataItem.fields,
|
companyMockObjectMetadataItem.fields,
|
||||||
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
@ -173,7 +175,7 @@ describe('should work as expected for the different field types', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = turnFiltersIntoQueryFilter(
|
const result = computeViewRecordGqlOperationFilter(
|
||||||
[
|
[
|
||||||
addressFilterContains,
|
addressFilterContains,
|
||||||
addressFilterDoesNotContain,
|
addressFilterDoesNotContain,
|
||||||
@ -181,6 +183,7 @@ describe('should work as expected for the different field types', () => {
|
|||||||
addressFilterIsNotEmpty,
|
addressFilterIsNotEmpty,
|
||||||
],
|
],
|
||||||
companyMockObjectMetadataItem.fields,
|
companyMockObjectMetadataItem.fields,
|
||||||
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
@ -554,7 +557,7 @@ describe('should work as expected for the different field types', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = turnFiltersIntoQueryFilter(
|
const result = computeViewRecordGqlOperationFilter(
|
||||||
[
|
[
|
||||||
phonesFilterContains,
|
phonesFilterContains,
|
||||||
phonesFilterDoesNotContain,
|
phonesFilterDoesNotContain,
|
||||||
@ -562,6 +565,7 @@ describe('should work as expected for the different field types', () => {
|
|||||||
phonesFilterIsNotEmpty,
|
phonesFilterIsNotEmpty,
|
||||||
],
|
],
|
||||||
personMockObjectMetadataItem.fields,
|
personMockObjectMetadataItem.fields,
|
||||||
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
@ -754,7 +758,7 @@ describe('should work as expected for the different field types', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = turnFiltersIntoQueryFilter(
|
const result = computeViewRecordGqlOperationFilter(
|
||||||
[
|
[
|
||||||
emailsFilterContains,
|
emailsFilterContains,
|
||||||
emailsFilterDoesNotContain,
|
emailsFilterDoesNotContain,
|
||||||
@ -762,6 +766,7 @@ describe('should work as expected for the different field types', () => {
|
|||||||
emailsFilterIsNotEmpty,
|
emailsFilterIsNotEmpty,
|
||||||
],
|
],
|
||||||
personMockObjectMetadataItem.fields,
|
personMockObjectMetadataItem.fields,
|
||||||
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
@ -908,7 +913,7 @@ describe('should work as expected for the different field types', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = turnFiltersIntoQueryFilter(
|
const result = computeViewRecordGqlOperationFilter(
|
||||||
[
|
[
|
||||||
dateFilterIsAfter,
|
dateFilterIsAfter,
|
||||||
dateFilterIsBefore,
|
dateFilterIsBefore,
|
||||||
@ -917,6 +922,7 @@ describe('should work as expected for the different field types', () => {
|
|||||||
dateFilterIsNotEmpty,
|
dateFilterIsNotEmpty,
|
||||||
],
|
],
|
||||||
companyMockObjectMetadataItem.fields,
|
companyMockObjectMetadataItem.fields,
|
||||||
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
@ -1023,7 +1029,7 @@ describe('should work as expected for the different field types', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = turnFiltersIntoQueryFilter(
|
const result = computeViewRecordGqlOperationFilter(
|
||||||
[
|
[
|
||||||
employeesFilterIsGreaterThan,
|
employeesFilterIsGreaterThan,
|
||||||
employeesFilterIsLessThan,
|
employeesFilterIsLessThan,
|
||||||
@ -1031,6 +1037,7 @@ describe('should work as expected for the different field types', () => {
|
|||||||
employeesFilterIsNotEmpty,
|
employeesFilterIsNotEmpty,
|
||||||
],
|
],
|
||||||
companyMockObjectMetadataItem.fields,
|
companyMockObjectMetadataItem.fields,
|
||||||
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).toEqual({
|
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 { Field } from '~/generated/graphql';
|
||||||
import { generateILikeFiltersForCompositeFields } from '~/utils/array/generateILikeFiltersForCompositeFields';
|
import { generateILikeFiltersForCompositeFields } from '~/utils/array/generateILikeFiltersForCompositeFields';
|
||||||
|
|
||||||
// TODO: fix this
|
export const getEmptyRecordGqlOperationFilter = (
|
||||||
export const applyEmptyFilters = (
|
|
||||||
operand: ViewFilterOperand,
|
operand: ViewFilterOperand,
|
||||||
correspondingField: Pick<Field, 'id' | 'name'>,
|
correspondingField: Pick<Field, 'id' | 'name'>,
|
||||||
objectRecordFilters: RecordGqlOperationFilter[],
|
|
||||||
definition: FilterDefinition,
|
definition: FilterDefinition,
|
||||||
) => {
|
) => {
|
||||||
let emptyRecordFilter: RecordGqlOperationFilter = {};
|
let emptyRecordFilter: RecordGqlOperationFilter = {};
|
||||||
@ -332,13 +330,11 @@ export const applyEmptyFilters = (
|
|||||||
|
|
||||||
switch (operand) {
|
switch (operand) {
|
||||||
case ViewFilterOperand.IsEmpty:
|
case ViewFilterOperand.IsEmpty:
|
||||||
objectRecordFilters.push(emptyRecordFilter);
|
return emptyRecordFilter;
|
||||||
break;
|
|
||||||
case ViewFilterOperand.IsNotEmpty:
|
case ViewFilterOperand.IsNotEmpty:
|
||||||
objectRecordFilters.push({
|
return {
|
||||||
not: emptyRecordFilter,
|
not: emptyRecordFilter,
|
||||||
});
|
};
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unknown operand ${operand} for ${definition.type} filter`,
|
`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 { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||||
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
|
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
|
||||||
import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState';
|
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 { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||||
import { ViewBar } from '@/views/components/ViewBar';
|
import { ViewBar } from '@/views/components/ViewBar';
|
||||||
@ -72,6 +73,9 @@ export const RecordIndexContainer = () => {
|
|||||||
const { columnDefinitions, filterDefinitions, sortDefinitions } =
|
const { columnDefinitions, filterDefinitions, sortDefinitions } =
|
||||||
useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
|
useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
|
||||||
|
|
||||||
|
const setRecordIndexViewFilterGroups = useSetRecoilState(
|
||||||
|
recordIndexViewFilterGroupsState,
|
||||||
|
);
|
||||||
const setRecordIndexFilters = useSetRecoilState(recordIndexFiltersState);
|
const setRecordIndexFilters = useSetRecoilState(recordIndexFiltersState);
|
||||||
const setRecordIndexSorts = useSetRecoilState(recordIndexSortsState);
|
const setRecordIndexSorts = useSetRecoilState(recordIndexSortsState);
|
||||||
const setRecordIndexIsCompactModeActive = useSetRecoilState(
|
const setRecordIndexIsCompactModeActive = useSetRecoilState(
|
||||||
@ -81,7 +85,12 @@ export const RecordIndexContainer = () => {
|
|||||||
recordIndexKanbanFieldMetadataIdState,
|
recordIndexKanbanFieldMetadataIdState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { setTableFilters, setTableSorts, setTableColumns } = useRecordTable({
|
const {
|
||||||
|
setTableViewFilterGroups,
|
||||||
|
setTableFilters,
|
||||||
|
setTableSorts,
|
||||||
|
setTableColumns,
|
||||||
|
} = useRecordTable({
|
||||||
recordTableId: recordIndexId,
|
recordTableId: recordIndexId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -164,12 +173,14 @@ export const RecordIndexContainer = () => {
|
|||||||
|
|
||||||
onViewFieldsChange(view.viewFields);
|
onViewFieldsChange(view.viewFields);
|
||||||
onViewGroupsChange(view.viewGroups);
|
onViewGroupsChange(view.viewGroups);
|
||||||
|
setTableViewFilterGroups(view.viewFilterGroups ?? []);
|
||||||
setTableFilters(
|
setTableFilters(
|
||||||
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
|
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
|
||||||
);
|
);
|
||||||
setRecordIndexFilters(
|
setRecordIndexFilters(
|
||||||
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
|
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
|
||||||
);
|
);
|
||||||
|
setRecordIndexViewFilterGroups(view.viewFilterGroups ?? []);
|
||||||
setContextStoreTargetedRecordsRule((prev) => ({
|
setContextStoreTargetedRecordsRule((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
filters: mapViewFiltersToFilters(
|
filters: mapViewFiltersToFilters(
|
||||||
|
|||||||
@ -5,13 +5,14 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
|
|||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||||
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
|
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
|
||||||
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
|
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 { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState';
|
||||||
import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields';
|
import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields';
|
||||||
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
|
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
|
||||||
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
|
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
|
||||||
import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState';
|
import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState';
|
||||||
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
|
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 { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { useSetRecordCountInCurrentView } from '@/views/hooks/useSetRecordCountInCurrentView';
|
import { useSetRecordCountInCurrentView } from '@/views/hooks/useSetRecordCountInCurrentView';
|
||||||
@ -45,18 +46,24 @@ export const useLoadRecordIndexBoard = ({
|
|||||||
setFieldDefinitions(recordIndexFieldDefinitions);
|
setFieldDefinitions(recordIndexFieldDefinitions);
|
||||||
}, [recordIndexFieldDefinitions, setFieldDefinitions]);
|
}, [recordIndexFieldDefinitions, setFieldDefinitions]);
|
||||||
|
|
||||||
|
const recordIndexViewFilterGroups = useRecoilValue(
|
||||||
|
recordIndexViewFilterGroupsState,
|
||||||
|
);
|
||||||
|
|
||||||
const recordIndexGroupDefinitions = useRecoilComponentValueV2(
|
const recordIndexGroupDefinitions = useRecoilComponentValueV2(
|
||||||
recordGroupDefinitionsComponentState,
|
recordGroupDefinitionsComponentState,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setColumns(recordIndexGroupDefinitions);
|
setColumns(recordIndexGroupDefinitions);
|
||||||
}, [recordIndexGroupDefinitions, setColumns]);
|
}, [recordIndexGroupDefinitions, setColumns]);
|
||||||
|
|
||||||
const recordIndexFilters = useRecoilValue(recordIndexFiltersState);
|
const recordIndexFilters = useRecoilValue(recordIndexFiltersState);
|
||||||
const recordIndexSorts = useRecoilValue(recordIndexSortsState);
|
const recordIndexSorts = useRecoilValue(recordIndexSortsState);
|
||||||
const requestFilters = turnFiltersIntoQueryFilter(
|
const requestFilters = computeViewRecordGqlOperationFilter(
|
||||||
recordIndexFilters,
|
recordIndexFilters,
|
||||||
objectMetadataItem?.fields ?? [],
|
objectMetadataItem?.fields ?? [],
|
||||||
|
recordIndexViewFilterGroups,
|
||||||
);
|
);
|
||||||
const orderBy = turnSortsIntoOrderBy(objectMetadataItem, recordIndexSorts);
|
const orderBy = turnSortsIntoOrderBy(objectMetadataItem, recordIndexSorts);
|
||||||
|
|
||||||
|
|||||||
@ -4,14 +4,15 @@ import { useRecoilValue } from 'recoil';
|
|||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||||
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
|
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 { 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 { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields';
|
||||||
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
|
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
|
||||||
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
|
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 { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
|
|
||||||
|
|
||||||
type UseLoadRecordIndexBoardProps = {
|
type UseLoadRecordIndexBoardProps = {
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
@ -33,12 +34,17 @@ export const useLoadRecordIndexBoardColumn = ({
|
|||||||
const { columnsFamilySelector } = useRecordBoardStates(recordBoardId);
|
const { columnsFamilySelector } = useRecordBoardStates(recordBoardId);
|
||||||
const { upsertRecords: upsertRecordsInStore } = useUpsertRecordsInStore();
|
const { upsertRecords: upsertRecordsInStore } = useUpsertRecordsInStore();
|
||||||
|
|
||||||
|
const recordIndexViewFilterGroups = useRecoilValue(
|
||||||
|
recordIndexViewFilterGroupsState,
|
||||||
|
);
|
||||||
const recordIndexFilters = useRecoilValue(recordIndexFiltersState);
|
const recordIndexFilters = useRecoilValue(recordIndexFiltersState);
|
||||||
const recordIndexSorts = useRecoilValue(recordIndexSortsState);
|
const recordIndexSorts = useRecoilValue(recordIndexSortsState);
|
||||||
const columnDefinition = useRecoilValue(columnsFamilySelector(columnId));
|
const columnDefinition = useRecoilValue(columnsFamilySelector(columnId));
|
||||||
const requestFilters = turnFiltersIntoQueryFilter(
|
|
||||||
|
const requestFilters = computeViewRecordGqlOperationFilter(
|
||||||
recordIndexFilters,
|
recordIndexFilters,
|
||||||
objectMetadataItem?.fields ?? [],
|
objectMetadataItem?.fields ?? [],
|
||||||
|
recordIndexViewFilterGroups,
|
||||||
);
|
);
|
||||||
const orderBy = turnSortsIntoOrderBy(objectMetadataItem, recordIndexSorts);
|
const orderBy = turnSortsIntoOrderBy(objectMetadataItem, recordIndexSorts);
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
|||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||||
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
|
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 { useRecordTableRecordGqlFields } from '@/object-record/record-index/hooks/useRecordTableRecordGqlFields';
|
||||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||||
@ -21,15 +21,17 @@ export const useFindManyParams = (
|
|||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { tableFiltersState, tableSortsState } =
|
const { tableFiltersState, tableSortsState, tableViewFilterGroupsState } =
|
||||||
useRecordTableStates(recordTableId);
|
useRecordTableStates(recordTableId);
|
||||||
|
|
||||||
|
const tableViewFilterGroups = useRecoilValue(tableViewFilterGroupsState);
|
||||||
const tableFilters = useRecoilValue(tableFiltersState);
|
const tableFilters = useRecoilValue(tableFiltersState);
|
||||||
const tableSorts = useRecoilValue(tableSortsState);
|
const tableSorts = useRecoilValue(tableSortsState);
|
||||||
|
|
||||||
const filter = turnFiltersIntoQueryFilter(
|
const filter = computeViewRecordGqlOperationFilter(
|
||||||
tableFilters,
|
tableFilters,
|
||||||
objectMetadataItem?.fields ?? [],
|
objectMetadataItem?.fields ?? [],
|
||||||
|
tableViewFilterGroups,
|
||||||
);
|
);
|
||||||
|
|
||||||
const orderBy = turnSortsIntoOrderBy(objectMetadataItem, tableSorts);
|
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 { tableLastRowVisibleComponentState } from '@/object-record/record-table/states/tableLastRowVisibleComponentState';
|
||||||
import { tableRowIdsComponentState } from '@/object-record/record-table/states/tableRowIdsComponentState';
|
import { tableRowIdsComponentState } from '@/object-record/record-table/states/tableRowIdsComponentState';
|
||||||
import { tableSortsComponentState } from '@/object-record/record-table/states/tableSortsComponentState';
|
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 { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||||
import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId';
|
import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId';
|
||||||
import { extractComponentFamilyState } from '@/ui/utilities/state/component-state/utils/extractComponentFamilyState';
|
import { extractComponentFamilyState } from '@/ui/utilities/state/component-state/utils/extractComponentFamilyState';
|
||||||
@ -45,6 +46,10 @@ export const useRecordTableStates = (recordTableId?: string) => {
|
|||||||
availableTableColumnsComponentState,
|
availableTableColumnsComponentState,
|
||||||
scopeId,
|
scopeId,
|
||||||
),
|
),
|
||||||
|
tableViewFilterGroupsState: extractComponentState(
|
||||||
|
tableViewFilterGroupsComponentState,
|
||||||
|
scopeId,
|
||||||
|
),
|
||||||
tableFiltersState: extractComponentState(
|
tableFiltersState: extractComponentState(
|
||||||
tableFiltersComponentState,
|
tableFiltersComponentState,
|
||||||
scopeId,
|
scopeId,
|
||||||
|
|||||||
@ -34,6 +34,7 @@ export const useRecordTable = (props?: useRecordTableProps) => {
|
|||||||
const {
|
const {
|
||||||
scopeId,
|
scopeId,
|
||||||
availableTableColumnsState,
|
availableTableColumnsState,
|
||||||
|
tableViewFilterGroupsState,
|
||||||
tableFiltersState,
|
tableFiltersState,
|
||||||
tableSortsState,
|
tableSortsState,
|
||||||
tableColumnsState,
|
tableColumnsState,
|
||||||
@ -67,6 +68,10 @@ export const useRecordTable = (props?: useRecordTableProps) => {
|
|||||||
|
|
||||||
const setOnEntityCountChange = useSetRecoilState(onEntityCountChangeState);
|
const setOnEntityCountChange = useSetRecoilState(onEntityCountChangeState);
|
||||||
|
|
||||||
|
const setTableViewFilterGroups = useSetRecoilState(
|
||||||
|
tableViewFilterGroupsState,
|
||||||
|
);
|
||||||
|
|
||||||
const setTableFilters = useSetRecoilState(tableFiltersState);
|
const setTableFilters = useSetRecoilState(tableFiltersState);
|
||||||
|
|
||||||
const setTableSorts = useSetRecoilState(tableSortsState);
|
const setTableSorts = useSetRecoilState(tableSortsState);
|
||||||
@ -203,6 +208,7 @@ export const useRecordTable = (props?: useRecordTableProps) => {
|
|||||||
scopeId,
|
scopeId,
|
||||||
onColumnsChange,
|
onColumnsChange,
|
||||||
setAvailableTableColumns,
|
setAvailableTableColumns,
|
||||||
|
setTableViewFilterGroups,
|
||||||
setTableFilters,
|
setTableFilters,
|
||||||
setTableSorts,
|
setTableSorts,
|
||||||
setOnEntityCountChange,
|
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,
|
icon: true,
|
||||||
key: true,
|
key: true,
|
||||||
viewFilters: true,
|
viewFilters: true,
|
||||||
|
viewFilterGroups: true,
|
||||||
viewSorts: true,
|
viewSorts: true,
|
||||||
viewFields: true,
|
viewFields: true,
|
||||||
viewGroups: true,
|
viewGroups: true,
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { useTheme } from '@emotion/react';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { MouseEvent, useMemo, useRef, useState } from 'react';
|
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 { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
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 { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||||
|
|
||||||
|
import { SelectControl } from '@/ui/input/components/SelectControl';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay';
|
|
||||||
import { SelectHotkeyScope } from '../types/SelectHotkeyScope';
|
import { SelectHotkeyScope } from '../types/SelectHotkeyScope';
|
||||||
|
|
||||||
export type SelectOption<Value extends string | number | null> = {
|
export type SelectOption<Value extends string | number | null> = {
|
||||||
@ -47,22 +46,6 @@ const StyledContainer = styled.div<{ fullWidth?: boolean }>`
|
|||||||
width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')};
|
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`
|
const StyledLabel = styled.span`
|
||||||
color: ${({ theme }) => theme.font.color.light};
|
color: ${({ theme }) => theme.font.color.light};
|
||||||
display: block;
|
display: block;
|
||||||
@ -71,20 +54,6 @@ const StyledLabel = styled.span`
|
|||||||
margin-bottom: ${({ theme }) => theme.spacing(1)};
|
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>({
|
export const Select = <Value extends string | number | null>({
|
||||||
className,
|
className,
|
||||||
disabled: disabledFromProps,
|
disabled: disabledFromProps,
|
||||||
@ -103,7 +72,6 @@ export const Select = <Value extends string | number | null>({
|
|||||||
}: SelectProps<Value>) => {
|
}: SelectProps<Value>) => {
|
||||||
const selectContainerRef = useRef<HTMLDivElement>(null);
|
const selectContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const theme = useTheme();
|
|
||||||
const [searchInputValue, setSearchInputValue] = useState('');
|
const [searchInputValue, setSearchInputValue] = useState('');
|
||||||
|
|
||||||
const selectedOption =
|
const selectedOption =
|
||||||
@ -126,24 +94,6 @@ export const Select = <Value extends string | number | null>({
|
|||||||
|
|
||||||
const { closeDropdown } = useDropdown(dropdownId);
|
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 (
|
return (
|
||||||
<StyledContainer
|
<StyledContainer
|
||||||
className={className}
|
className={className}
|
||||||
@ -154,13 +104,21 @@ export const Select = <Value extends string | number | null>({
|
|||||||
>
|
>
|
||||||
{!!label && <StyledLabel>{label}</StyledLabel>}
|
{!!label && <StyledLabel>{label}</StyledLabel>}
|
||||||
{isDisabled ? (
|
{isDisabled ? (
|
||||||
selectControl
|
<SelectControl
|
||||||
|
selectedOption={selectedOption}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
dropdownId={dropdownId}
|
dropdownId={dropdownId}
|
||||||
dropdownMenuWidth={dropdownWidth}
|
dropdownMenuWidth={dropdownWidth}
|
||||||
dropdownPlacement="bottom-start"
|
dropdownPlacement="bottom-start"
|
||||||
clickableComponent={selectControl}
|
clickableComponent={
|
||||||
|
<SelectControl
|
||||||
|
selectedOption={selectedOption}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
/>
|
||||||
|
}
|
||||||
disableBlur={disableBlur}
|
disableBlur={disableBlur}
|
||||||
dropdownComponents={
|
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 { 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 { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
||||||
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
|
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
|
||||||
import { FilterOperand } from '@/object-record/object-filter-dropdown/types/FilterOperand';
|
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 { EditableFilterChip } from '@/views/components/EditableFilterChip';
|
||||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||||
|
|
||||||
|
import { ObjectFilterOperandSelectAndInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterOperandSelectAndInput';
|
||||||
import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters';
|
import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters';
|
||||||
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
|
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
@ -98,7 +98,7 @@ export const EditableFilterDropdownButton = ({
|
|||||||
<EditableFilterChip viewFilter={viewFilter} onRemove={handleRemove} />
|
<EditableFilterChip viewFilter={viewFilter} onRemove={handleRemove} />
|
||||||
}
|
}
|
||||||
dropdownComponents={
|
dropdownComponents={
|
||||||
<MultipleFiltersDropdownContent
|
<ObjectFilterOperandSelectAndInput
|
||||||
filterDropdownId={viewFilterDropdownId}
|
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 { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
||||||
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
|
import { AdvancedFilterDropdownButton } from '@/views/components/AdvancedFilterDropdownButton';
|
||||||
import { EditableFilterDropdownButton } from '@/views/components/EditableFilterDropdownButton';
|
import { EditableFilterDropdownButton } from '@/views/components/EditableFilterDropdownButton';
|
||||||
import { EditableSortChip } from '@/views/components/EditableSortChip';
|
import { EditableSortChip } from '@/views/components/EditableSortChip';
|
||||||
import { ViewBarFilterEffect } from '@/views/components/ViewBarFilterEffect';
|
import { ViewBarFilterEffect } from '@/views/components/ViewBarFilterEffect';
|
||||||
@ -137,11 +138,16 @@ export const ViewBarDetails = ({
|
|||||||
|
|
||||||
const otherViewFilters =
|
const otherViewFilters =
|
||||||
currentViewWithCombinedFiltersAndSorts.viewFilters.filter(
|
currentViewWithCombinedFiltersAndSorts.viewFilters.filter(
|
||||||
(viewFilter) => viewFilter.variant && viewFilter.variant !== 'default',
|
(viewFilter) =>
|
||||||
|
viewFilter.variant &&
|
||||||
|
viewFilter.variant !== 'default' &&
|
||||||
|
!viewFilter.viewFilterGroupId,
|
||||||
);
|
);
|
||||||
const defaultViewFilters =
|
const defaultViewFilters =
|
||||||
currentViewWithCombinedFiltersAndSorts.viewFilters.filter(
|
currentViewWithCombinedFiltersAndSorts.viewFilters.filter(
|
||||||
(viewFilter) => !viewFilter.variant || viewFilter.variant === 'default',
|
(viewFilter) =>
|
||||||
|
(!viewFilter.variant || viewFilter.variant === 'default') &&
|
||||||
|
!viewFilter.viewFilterGroupId,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -166,6 +172,10 @@ export const ViewBarDetails = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showAdvancedFilterDropdownButton =
|
||||||
|
currentViewWithCombinedFiltersAndSorts?.viewFilterGroups &&
|
||||||
|
currentViewWithCombinedFiltersAndSorts?.viewFilterGroups.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledBar>
|
<StyledBar>
|
||||||
<StyledFilterContainer>
|
<StyledFilterContainer>
|
||||||
@ -199,6 +209,7 @@ export const ViewBarDetails = ({
|
|||||||
<StyledSeperator />
|
<StyledSeperator />
|
||||||
</StyledSeperatorContainer>
|
</StyledSeperatorContainer>
|
||||||
)}
|
)}
|
||||||
|
{showAdvancedFilterDropdownButton && <AdvancedFilterDropdownButton />}
|
||||||
{mapViewFiltersToFilters(
|
{mapViewFiltersToFilters(
|
||||||
defaultViewFilters,
|
defaultViewFilters,
|
||||||
availableFilterDefinitions,
|
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,
|
mutation: createOneRecordMutation,
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
|
id: viewFilter.id,
|
||||||
fieldMetadataId: viewFilter.fieldMetadataId,
|
fieldMetadataId: viewFilter.fieldMetadataId,
|
||||||
viewId: view.id,
|
viewId: view.id,
|
||||||
value: viewFilter.value,
|
value: viewFilter.value,
|
||||||
displayValue: viewFilter.displayValue,
|
displayValue: viewFilter.displayValue,
|
||||||
operand: viewFilter.operand,
|
operand: viewFilter.operand,
|
||||||
|
viewFilterGroupId: viewFilter.viewFilterGroupId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
update: (cache, { data }) => {
|
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 { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||||
import { usePersistViewFieldRecords } from '@/views/hooks/internal/usePersistViewFieldRecords';
|
import { usePersistViewFieldRecords } from '@/views/hooks/internal/usePersistViewFieldRecords';
|
||||||
|
import { usePersistViewFilterGroupRecords } from '@/views/hooks/internal/usePersistViewFilterGroupRecords';
|
||||||
import { usePersistViewFilterRecords } from '@/views/hooks/internal/usePersistViewFilterRecords';
|
import { usePersistViewFilterRecords } from '@/views/hooks/internal/usePersistViewFilterRecords';
|
||||||
import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroupRecords';
|
import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroupRecords';
|
||||||
import { usePersistViewSortRecords } from '@/views/hooks/internal/usePersistViewSortRecords';
|
import { usePersistViewSortRecords } from '@/views/hooks/internal/usePersistViewSortRecords';
|
||||||
|
import { useGetViewFilterGroupsCombined } from '@/views/hooks/useGetCombinedViewFilterGroups';
|
||||||
import { useGetViewFiltersCombined } from '@/views/hooks/useGetCombinedViewFilters';
|
import { useGetViewFiltersCombined } from '@/views/hooks/useGetCombinedViewFilters';
|
||||||
import { useGetViewSortsCombined } from '@/views/hooks/useGetCombinedViewSorts';
|
import { useGetViewSortsCombined } from '@/views/hooks/useGetCombinedViewSorts';
|
||||||
import { useGetViewFromCache } from '@/views/hooks/useGetViewFromCache';
|
import { useGetViewFromCache } from '@/views/hooks/useGetViewFromCache';
|
||||||
@ -45,6 +47,8 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
|
|||||||
const { getViewSortsCombined } = useGetViewSortsCombined(viewBarComponentId);
|
const { getViewSortsCombined } = useGetViewSortsCombined(viewBarComponentId);
|
||||||
const { getViewFiltersCombined } =
|
const { getViewFiltersCombined } =
|
||||||
useGetViewFiltersCombined(viewBarComponentId);
|
useGetViewFiltersCombined(viewBarComponentId);
|
||||||
|
const { getViewFilterGroupsCombined } =
|
||||||
|
useGetViewFilterGroupsCombined(viewBarComponentId);
|
||||||
|
|
||||||
const { createViewSortRecords } = usePersistViewSortRecords();
|
const { createViewSortRecords } = usePersistViewSortRecords();
|
||||||
|
|
||||||
@ -52,6 +56,8 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
|
|||||||
|
|
||||||
const { createViewFilterRecords } = usePersistViewFilterRecords();
|
const { createViewFilterRecords } = usePersistViewFilterRecords();
|
||||||
|
|
||||||
|
const { createViewFilterGroupRecords } = usePersistViewFilterGroupRecords();
|
||||||
|
|
||||||
const { objectMetadataItem } = useContext(RecordIndexRootPropsContext);
|
const { objectMetadataItem } = useContext(RecordIndexRootPropsContext);
|
||||||
|
|
||||||
const createViewFromCurrentView = useRecoilCallback(
|
const createViewFromCurrentView = useRecoilCallback(
|
||||||
@ -143,11 +149,18 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (shouldCopyFiltersAndSorts === true) {
|
if (shouldCopyFiltersAndSorts === true) {
|
||||||
|
const sourceViewCombinedFilterGroups = getViewFilterGroupsCombined(
|
||||||
|
view.id,
|
||||||
|
);
|
||||||
const sourceViewCombinedFilters = getViewFiltersCombined(view.id);
|
const sourceViewCombinedFilters = getViewFiltersCombined(view.id);
|
||||||
const sourceViewCombinedSorts = getViewSortsCombined(view.id);
|
const sourceViewCombinedSorts = getViewSortsCombined(view.id);
|
||||||
|
|
||||||
await createViewSortRecords(sourceViewCombinedSorts, view);
|
await createViewSortRecords(sourceViewCombinedSorts, view);
|
||||||
await createViewFilterRecords(sourceViewCombinedFilters, view);
|
await createViewFilterRecords(sourceViewCombinedFilters, view);
|
||||||
|
await createViewFilterGroupRecords(
|
||||||
|
sourceViewCombinedFilterGroups,
|
||||||
|
view,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
set(isPersistingViewFieldsCallbackState, false);
|
set(isPersistingViewFieldsCallbackState, false);
|
||||||
@ -160,10 +173,12 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
|
|||||||
createViewFieldRecords,
|
createViewFieldRecords,
|
||||||
getViewSortsCombined,
|
getViewSortsCombined,
|
||||||
getViewFiltersCombined,
|
getViewFiltersCombined,
|
||||||
|
getViewFilterGroupsCombined,
|
||||||
currentViewIdCallbackState,
|
currentViewIdCallbackState,
|
||||||
getViewFromCache,
|
getViewFromCache,
|
||||||
isPersistingViewFieldsCallbackState,
|
isPersistingViewFieldsCallbackState,
|
||||||
createViewGroupRecords,
|
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 { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||||
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
|
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
|
||||||
import { isCurrentViewKeyIndexComponentState } from '@/views/states/isCurrentViewIndexComponentState';
|
import { isCurrentViewKeyIndexComponentState } from '@/views/states/isCurrentViewIndexComponentState';
|
||||||
|
import { unsavedToDeleteViewFilterGroupIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterGroupIdsComponentFamilyState';
|
||||||
import { unsavedToDeleteViewFilterIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterIdsComponentFamilyState';
|
import { unsavedToDeleteViewFilterIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterIdsComponentFamilyState';
|
||||||
import { unsavedToDeleteViewSortIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewSortIdsComponentFamilyState';
|
import { unsavedToDeleteViewSortIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewSortIdsComponentFamilyState';
|
||||||
|
import { unsavedToUpsertViewFilterGroupsComponentFamilyState } from '@/views/states/unsavedToUpsertViewFilterGroupsComponentFamilyState';
|
||||||
import { unsavedToUpsertViewFiltersComponentFamilyState } from '@/views/states/unsavedToUpsertViewFiltersComponentFamilyState';
|
import { unsavedToUpsertViewFiltersComponentFamilyState } from '@/views/states/unsavedToUpsertViewFiltersComponentFamilyState';
|
||||||
import { unsavedToUpsertViewSortsComponentFamilyState } from '@/views/states/unsavedToUpsertViewSortsComponentFamilyState';
|
import { unsavedToUpsertViewSortsComponentFamilyState } from '@/views/states/unsavedToUpsertViewSortsComponentFamilyState';
|
||||||
import { viewObjectMetadataIdComponentState } from '@/views/states/viewObjectMetadataIdComponentState';
|
import { viewObjectMetadataIdComponentState } from '@/views/states/viewObjectMetadataIdComponentState';
|
||||||
import { View } from '@/views/types/View';
|
import { View } from '@/views/types/View';
|
||||||
|
import { getCombinedViewFilterGroups } from '@/views/utils/getCombinedViewFilterGroups';
|
||||||
import { getCombinedViewFilters } from '@/views/utils/getCombinedViewFilters';
|
import { getCombinedViewFilters } from '@/views/utils/getCombinedViewFilters';
|
||||||
import { getCombinedViewSorts } from '@/views/utils/getCombinedViewSorts';
|
import { getCombinedViewSorts } from '@/views/utils/getCombinedViewSorts';
|
||||||
import { getObjectMetadataItemViews } from '@/views/utils/getObjectMetadataItemViews';
|
import { getObjectMetadataItemViews } from '@/views/utils/getObjectMetadataItemViews';
|
||||||
@ -70,6 +73,12 @@ export const useGetCurrentView = (viewBarInstanceId?: string) => {
|
|||||||
instanceId,
|
instanceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const unsavedToUpsertViewFilterGroups = useRecoilComponentFamilyValueV2(
|
||||||
|
unsavedToUpsertViewFilterGroupsComponentFamilyState,
|
||||||
|
{ viewId },
|
||||||
|
instanceId,
|
||||||
|
);
|
||||||
|
|
||||||
const unsavedToUpsertViewSorts = useRecoilComponentFamilyValueV2(
|
const unsavedToUpsertViewSorts = useRecoilComponentFamilyValueV2(
|
||||||
unsavedToUpsertViewSortsComponentFamilyState,
|
unsavedToUpsertViewSortsComponentFamilyState,
|
||||||
{ viewId },
|
{ viewId },
|
||||||
@ -82,6 +91,12 @@ export const useGetCurrentView = (viewBarInstanceId?: string) => {
|
|||||||
instanceId,
|
instanceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const unsavedToDeleteViewFilterGroupIds = useRecoilComponentFamilyValueV2(
|
||||||
|
unsavedToDeleteViewFilterGroupIdsComponentFamilyState,
|
||||||
|
{ viewId },
|
||||||
|
instanceId,
|
||||||
|
);
|
||||||
|
|
||||||
const unsavedToDeleteViewSortIds = useRecoilComponentFamilyValueV2(
|
const unsavedToDeleteViewSortIds = useRecoilComponentFamilyValueV2(
|
||||||
unsavedToDeleteViewSortIdsComponentFamilyState,
|
unsavedToDeleteViewSortIdsComponentFamilyState,
|
||||||
{ viewId },
|
{ viewId },
|
||||||
@ -104,6 +119,11 @@ export const useGetCurrentView = (viewBarInstanceId?: string) => {
|
|||||||
unsavedToUpsertViewFilters,
|
unsavedToUpsertViewFilters,
|
||||||
unsavedToDeleteViewFilterIds,
|
unsavedToDeleteViewFilterIds,
|
||||||
),
|
),
|
||||||
|
viewFilterGroups: getCombinedViewFilterGroups(
|
||||||
|
currentView.viewFilterGroups ?? [],
|
||||||
|
unsavedToUpsertViewFilterGroups,
|
||||||
|
unsavedToDeleteViewFilterGroupIds,
|
||||||
|
),
|
||||||
viewSorts: getCombinedViewSorts(
|
viewSorts: getCombinedViewSorts(
|
||||||
currentView.viewSorts,
|
currentView.viewSorts,
|
||||||
unsavedToUpsertViewSorts,
|
unsavedToUpsertViewSorts,
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||||
|
import { unsavedToDeleteViewFilterGroupIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterGroupIdsComponentFamilyState';
|
||||||
import { unsavedToDeleteViewFilterIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterIdsComponentFamilyState';
|
import { unsavedToDeleteViewFilterIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterIdsComponentFamilyState';
|
||||||
import { unsavedToDeleteViewSortIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewSortIdsComponentFamilyState';
|
import { unsavedToDeleteViewSortIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewSortIdsComponentFamilyState';
|
||||||
|
import { unsavedToUpsertViewFilterGroupsComponentFamilyState } from '@/views/states/unsavedToUpsertViewFilterGroupsComponentFamilyState';
|
||||||
import { unsavedToUpsertViewFiltersComponentFamilyState } from '@/views/states/unsavedToUpsertViewFiltersComponentFamilyState';
|
import { unsavedToUpsertViewFiltersComponentFamilyState } from '@/views/states/unsavedToUpsertViewFiltersComponentFamilyState';
|
||||||
import { unsavedToUpsertViewSortsComponentFamilyState } from '@/views/states/unsavedToUpsertViewSortsComponentFamilyState';
|
import { unsavedToUpsertViewSortsComponentFamilyState } from '@/views/states/unsavedToUpsertViewSortsComponentFamilyState';
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
@ -18,6 +20,12 @@ export const useResetUnsavedViewStates = (viewBarInstanceId?: string) => {
|
|||||||
viewBarInstanceId,
|
viewBarInstanceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const unsavedToDeleteViewFilterGroupIdsCallbackState =
|
||||||
|
useRecoilComponentCallbackStateV2(
|
||||||
|
unsavedToDeleteViewFilterGroupIdsComponentFamilyState,
|
||||||
|
viewBarInstanceId,
|
||||||
|
);
|
||||||
|
|
||||||
const setUnsavedToUpsertViewFiltersCallbackState =
|
const setUnsavedToUpsertViewFiltersCallbackState =
|
||||||
useRecoilComponentCallbackStateV2(
|
useRecoilComponentCallbackStateV2(
|
||||||
unsavedToUpsertViewFiltersComponentFamilyState,
|
unsavedToUpsertViewFiltersComponentFamilyState,
|
||||||
@ -30,19 +38,29 @@ export const useResetUnsavedViewStates = (viewBarInstanceId?: string) => {
|
|||||||
viewBarInstanceId,
|
viewBarInstanceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const unsavedToUpsertViewFilterGroupsCallbackState =
|
||||||
|
useRecoilComponentCallbackStateV2(
|
||||||
|
unsavedToUpsertViewFilterGroupsComponentFamilyState,
|
||||||
|
viewBarInstanceId,
|
||||||
|
);
|
||||||
|
|
||||||
const resetUnsavedViewStates = useRecoilCallback(
|
const resetUnsavedViewStates = useRecoilCallback(
|
||||||
({ set }) =>
|
({ set }) =>
|
||||||
(viewId: string) => {
|
(viewId: string) => {
|
||||||
|
set(unsavedToDeleteViewFilterGroupIdsCallbackState({ viewId }), []);
|
||||||
set(setUnsavedToDeleteViewFilterIdsCallbackState({ viewId }), []);
|
set(setUnsavedToDeleteViewFilterIdsCallbackState({ viewId }), []);
|
||||||
set(setUnsavedToDeleteViewSortIdsCallbackState({ viewId }), []);
|
set(setUnsavedToDeleteViewSortIdsCallbackState({ viewId }), []);
|
||||||
|
set(unsavedToUpsertViewFilterGroupsCallbackState({ viewId }), []);
|
||||||
set(setUnsavedToUpsertViewFiltersCallbackState({ viewId }), []);
|
set(setUnsavedToUpsertViewFiltersCallbackState({ viewId }), []);
|
||||||
set(unsavedToUpsertViewSortsCallbackState({ viewId }), []);
|
set(unsavedToUpsertViewSortsCallbackState({ viewId }), []);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
unsavedToUpsertViewSortsCallbackState,
|
unsavedToUpsertViewSortsCallbackState,
|
||||||
setUnsavedToUpsertViewFiltersCallbackState,
|
setUnsavedToUpsertViewFiltersCallbackState,
|
||||||
|
unsavedToUpsertViewFilterGroupsCallbackState,
|
||||||
setUnsavedToDeleteViewSortIdsCallbackState,
|
setUnsavedToDeleteViewSortIdsCallbackState,
|
||||||
setUnsavedToDeleteViewFilterIdsCallbackState,
|
setUnsavedToDeleteViewFilterIdsCallbackState,
|
||||||
|
unsavedToDeleteViewFilterGroupIdsCallbackState,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -2,13 +2,16 @@ import { useRecoilCallback } from 'recoil';
|
|||||||
|
|
||||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
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 { usePersistViewFilterRecords } from '@/views/hooks/internal/usePersistViewFilterRecords';
|
||||||
import { usePersistViewSortRecords } from '@/views/hooks/internal/usePersistViewSortRecords';
|
import { usePersistViewSortRecords } from '@/views/hooks/internal/usePersistViewSortRecords';
|
||||||
import { useGetViewFromCache } from '@/views/hooks/useGetViewFromCache';
|
import { useGetViewFromCache } from '@/views/hooks/useGetViewFromCache';
|
||||||
import { useResetUnsavedViewStates } from '@/views/hooks/useResetUnsavedViewStates';
|
import { useResetUnsavedViewStates } from '@/views/hooks/useResetUnsavedViewStates';
|
||||||
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
|
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
|
||||||
|
import { unsavedToDeleteViewFilterGroupIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterGroupIdsComponentFamilyState';
|
||||||
import { unsavedToDeleteViewFilterIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterIdsComponentFamilyState';
|
import { unsavedToDeleteViewFilterIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterIdsComponentFamilyState';
|
||||||
import { unsavedToDeleteViewSortIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewSortIdsComponentFamilyState';
|
import { unsavedToDeleteViewSortIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewSortIdsComponentFamilyState';
|
||||||
|
import { unsavedToUpsertViewFilterGroupsComponentFamilyState } from '@/views/states/unsavedToUpsertViewFilterGroupsComponentFamilyState';
|
||||||
import { unsavedToUpsertViewFiltersComponentFamilyState } from '@/views/states/unsavedToUpsertViewFiltersComponentFamilyState';
|
import { unsavedToUpsertViewFiltersComponentFamilyState } from '@/views/states/unsavedToUpsertViewFiltersComponentFamilyState';
|
||||||
import { unsavedToUpsertViewSortsComponentFamilyState } from '@/views/states/unsavedToUpsertViewSortsComponentFamilyState';
|
import { unsavedToUpsertViewSortsComponentFamilyState } from '@/views/states/unsavedToUpsertViewSortsComponentFamilyState';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
@ -48,6 +51,18 @@ export const useSaveCurrentViewFiltersAndSorts = (
|
|||||||
viewBarComponentId,
|
viewBarComponentId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const unsavedToUpsertViewFilterGroupsCallbackState =
|
||||||
|
useRecoilComponentCallbackStateV2(
|
||||||
|
unsavedToUpsertViewFilterGroupsComponentFamilyState,
|
||||||
|
viewBarComponentId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const unsavedToDeleteViewFilterGroupIdsCallbackState =
|
||||||
|
useRecoilComponentCallbackStateV2(
|
||||||
|
unsavedToDeleteViewFilterGroupIdsComponentFamilyState,
|
||||||
|
viewBarComponentId,
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
createViewSortRecords,
|
createViewSortRecords,
|
||||||
updateViewSortRecords,
|
updateViewSortRecords,
|
||||||
@ -60,6 +75,12 @@ export const useSaveCurrentViewFiltersAndSorts = (
|
|||||||
deleteViewFilterRecords,
|
deleteViewFilterRecords,
|
||||||
} = usePersistViewFilterRecords();
|
} = usePersistViewFilterRecords();
|
||||||
|
|
||||||
|
const {
|
||||||
|
createViewFilterGroupRecords,
|
||||||
|
deleteViewFilterGroupRecords,
|
||||||
|
updateViewFilterGroupRecords,
|
||||||
|
} = usePersistViewFilterGroupRecords();
|
||||||
|
|
||||||
const { resetUnsavedViewStates } =
|
const { resetUnsavedViewStates } =
|
||||||
useResetUnsavedViewStates(viewBarComponentId);
|
useResetUnsavedViewStates(viewBarComponentId);
|
||||||
|
|
||||||
@ -131,14 +152,14 @@ export const useSaveCurrentViewFiltersAndSorts = (
|
|||||||
const viewFiltersToCreate = unsavedToUpsertViewFilters.filter(
|
const viewFiltersToCreate = unsavedToUpsertViewFilters.filter(
|
||||||
(viewFilter) =>
|
(viewFilter) =>
|
||||||
!view.viewFilters.some(
|
!view.viewFilters.some(
|
||||||
(vf) => vf.fieldMetadataId === viewFilter.fieldMetadataId,
|
(viewFilterToFilter) => viewFilterToFilter.id === viewFilter.id,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const viewFiltersToUpdate = unsavedToUpsertViewFilters.filter(
|
const viewFiltersToUpdate = unsavedToUpsertViewFilters.filter(
|
||||||
(viewFilter) =>
|
(viewFilter) =>
|
||||||
view.viewFilters.some(
|
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(
|
const saveCurrentViewFilterAndSorts = useRecoilCallback(
|
||||||
({ snapshot }) =>
|
({ snapshot }) =>
|
||||||
async (viewIdFromProps?: string) => {
|
async (viewIdFromProps?: string) => {
|
||||||
@ -169,6 +239,7 @@ export const useSaveCurrentViewFiltersAndSorts = (
|
|||||||
|
|
||||||
const viewId = viewIdFromProps ?? currentViewId;
|
const viewId = viewIdFromProps ?? currentViewId;
|
||||||
|
|
||||||
|
await saveViewFilterGroups(viewId);
|
||||||
await saveViewFilters(viewId);
|
await saveViewFilters(viewId);
|
||||||
await saveViewSorts(viewId);
|
await saveViewSorts(viewId);
|
||||||
|
|
||||||
@ -179,6 +250,7 @@ export const useSaveCurrentViewFiltersAndSorts = (
|
|||||||
resetUnsavedViewStates,
|
resetUnsavedViewStates,
|
||||||
saveViewFilters,
|
saveViewFilters,
|
||||||
saveViewSorts,
|
saveViewSorts,
|
||||||
|
saveViewFilterGroups,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { currentViewIdComponentState } from '@/views/states/currentViewIdCompone
|
|||||||
import { unsavedToDeleteViewFilterIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterIdsComponentFamilyState';
|
import { unsavedToDeleteViewFilterIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterIdsComponentFamilyState';
|
||||||
import { unsavedToUpsertViewFiltersComponentFamilyState } from '@/views/states/unsavedToUpsertViewFiltersComponentFamilyState';
|
import { unsavedToUpsertViewFiltersComponentFamilyState } from '@/views/states/unsavedToUpsertViewFiltersComponentFamilyState';
|
||||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||||
|
import { shouldReplaceFilter } from '@/views/utils/shouldReplaceFilter';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
export const useUpsertCombinedViewFilters = (viewBarComponentId?: string) => {
|
export const useUpsertCombinedViewFilters = (viewBarComponentId?: string) => {
|
||||||
@ -59,19 +60,16 @@ export const useUpsertCombinedViewFilters = (viewBarComponentId?: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const matchingFilterInCurrentView = currentView.viewFilters.find(
|
const matchingFilterInCurrentView = currentView.viewFilters.find(
|
||||||
(viewFilter) =>
|
(viewFilter) => shouldReplaceFilter(viewFilter, upsertedFilter),
|
||||||
viewFilter.fieldMetadataId === upsertedFilter.fieldMetadataId,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const matchingFilterInUnsavedFilters = unsavedToUpsertViewFilters.find(
|
const matchingFilterInUnsavedFilters = unsavedToUpsertViewFilters.find(
|
||||||
(viewFilter) =>
|
(viewFilter) => shouldReplaceFilter(viewFilter, upsertedFilter),
|
||||||
viewFilter.fieldMetadataId === upsertedFilter.fieldMetadataId,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isDefined(matchingFilterInUnsavedFilters)) {
|
if (isDefined(matchingFilterInUnsavedFilters)) {
|
||||||
const updatedFilters = unsavedToUpsertViewFilters.map((viewFilter) =>
|
const updatedFilters = unsavedToUpsertViewFilters.map((viewFilter) =>
|
||||||
viewFilter.fieldMetadataId ===
|
shouldReplaceFilter(viewFilter, matchingFilterInUnsavedFilters)
|
||||||
matchingFilterInUnsavedFilters.fieldMetadataId
|
|
||||||
? { ...viewFilter, ...upsertedFilter, id: viewFilter.id }
|
? { ...viewFilter, ...upsertedFilter, id: viewFilter.id }
|
||||||
: viewFilter,
|
: 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 { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2';
|
||||||
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||||
import { ViewFilter } from '../types/ViewFilter';
|
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||||
|
|
||||||
export const unsavedToUpsertViewFiltersComponentFamilyState =
|
export const unsavedToUpsertViewFiltersComponentFamilyState =
|
||||||
createComponentFamilyStateV2<ViewFilter[], { viewId?: string }>({
|
createComponentFamilyStateV2<ViewFilter[], { viewId?: string }>({
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { ViewField } from '@/views/types/ViewField';
|
import { ViewField } from '@/views/types/ViewField';
|
||||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||||
|
import { ViewFilterGroup } from '@/views/types/ViewFilterGroup';
|
||||||
import { ViewGroup } from '@/views/types/ViewGroup';
|
import { ViewGroup } from '@/views/types/ViewGroup';
|
||||||
import { ViewKey } from '@/views/types/ViewKey';
|
import { ViewKey } from '@/views/types/ViewKey';
|
||||||
import { ViewSort } from '@/views/types/ViewSort';
|
import { ViewSort } from '@/views/types/ViewSort';
|
||||||
@ -15,6 +16,7 @@ export type GraphQLView = {
|
|||||||
isCompact: boolean;
|
isCompact: boolean;
|
||||||
viewFields: ViewField[];
|
viewFields: ViewField[];
|
||||||
viewFilters: ViewFilter[];
|
viewFilters: ViewFilter[];
|
||||||
|
viewFilterGroups?: ViewFilterGroup[];
|
||||||
viewSorts: ViewSort[];
|
viewSorts: ViewSort[];
|
||||||
viewGroups: ViewGroup[];
|
viewGroups: ViewGroup[];
|
||||||
position: number;
|
position: number;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { ViewField } from '@/views/types/ViewField';
|
import { ViewField } from '@/views/types/ViewField';
|
||||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||||
|
import { ViewFilterGroup } from '@/views/types/ViewFilterGroup';
|
||||||
import { ViewGroup } from '@/views/types/ViewGroup';
|
import { ViewGroup } from '@/views/types/ViewGroup';
|
||||||
import { ViewKey } from '@/views/types/ViewKey';
|
import { ViewKey } from '@/views/types/ViewKey';
|
||||||
import { ViewSort } from '@/views/types/ViewSort';
|
import { ViewSort } from '@/views/types/ViewSort';
|
||||||
@ -15,6 +16,7 @@ export type View = {
|
|||||||
viewFields: ViewField[];
|
viewFields: ViewField[];
|
||||||
viewGroups: ViewGroup[];
|
viewGroups: ViewGroup[];
|
||||||
viewFilters: ViewFilter[];
|
viewFilters: ViewFilter[];
|
||||||
|
viewFilterGroups?: ViewFilterGroup[];
|
||||||
viewSorts: ViewSort[];
|
viewSorts: ViewSort[];
|
||||||
kanbanFieldMetadataId: string;
|
kanbanFieldMetadataId: string;
|
||||||
position: number;
|
position: number;
|
||||||
|
|||||||
@ -12,5 +12,7 @@ export type ViewFilter = {
|
|||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
viewId?: string;
|
viewId?: string;
|
||||||
|
viewFilterGroupId?: string;
|
||||||
|
positionInViewFilterGroup?: number | null;
|
||||||
definition?: FilterDefinition;
|
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(
|
const toCreateViewFilters = toUpsertViewFilters.filter(
|
||||||
(toUpsertViewFilter) =>
|
(toUpsertViewFilter) =>
|
||||||
!viewFilters.some(
|
!viewFilters.some(
|
||||||
(viewFilter) =>
|
(viewFilter) => viewFilter.id === toUpsertViewFilter.id,
|
||||||
viewFilter.fieldMetadataId === toUpsertViewFilter.fieldMetadataId,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const toUpdateViewFilters = toUpsertViewFilters.filter((toUpsertViewFilter) =>
|
const toUpdateViewFilters = toUpsertViewFilters.filter((toUpsertViewFilter) =>
|
||||||
viewFilters.some(
|
viewFilters.some((viewFilter) => viewFilter.id === toUpsertViewFilter.id),
|
||||||
(viewFilter) =>
|
|
||||||
viewFilter.fieldMetadataId === toUpsertViewFilter.fieldMetadataId,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const combinedViewFilters = viewFilters
|
const combinedViewFilters = viewFilters
|
||||||
.filter((viewFilter) => !toDeleteViewFilterIds.includes(viewFilter.id))
|
.filter((viewFilter) => !toDeleteViewFilterIds.includes(viewFilter.id))
|
||||||
.map((viewFilter) => {
|
.map((viewFilter) => {
|
||||||
const toUpdateViewFilter = toUpdateViewFilters.find(
|
const toUpdateViewFilter = toUpdateViewFilters.find(
|
||||||
(toUpdateViewFilter) =>
|
(toUpdateViewFilter) => toUpdateViewFilter.id === viewFilter.id,
|
||||||
toUpdateViewFilter.fieldMetadataId === viewFilter.fieldMetadataId,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return toUpdateViewFilter ?? viewFilter;
|
return toUpdateViewFilter ?? viewFilter;
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
|||||||
import { formatFieldMetadataItemsAsFilterDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
|
import { formatFieldMetadataItemsAsFilterDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
|
||||||
import { formatFieldMetadataItemsAsSortDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions';
|
import { formatFieldMetadataItemsAsSortDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions';
|
||||||
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
|
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 { View } from '@/views/types/View';
|
||||||
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
|
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
|
||||||
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
|
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({
|
const filterDefinitions = formatFieldMetadataItemsAsFilterDefinitions({
|
||||||
fields: fieldMetadataItems,
|
fields: fieldMetadataItems,
|
||||||
@ -38,9 +38,10 @@ export const getQueryVariablesFromView = ({
|
|||||||
fields: fieldMetadataItems,
|
fields: fieldMetadataItems,
|
||||||
});
|
});
|
||||||
|
|
||||||
const filter = turnFiltersIntoQueryFilter(
|
const filter = computeViewRecordGqlOperationFilter(
|
||||||
mapViewFiltersToFilters(viewFilters, filterDefinitions),
|
mapViewFiltersToFilters(viewFilters, filterDefinitions),
|
||||||
objectMetadataItem?.fields ?? [],
|
objectMetadataItem?.fields ?? [],
|
||||||
|
viewFilterGroups ?? [],
|
||||||
);
|
);
|
||||||
|
|
||||||
const orderBy = turnSortsIntoOrderBy(
|
const orderBy = turnSortsIntoOrderBy(
|
||||||
|
|||||||
@ -23,6 +23,8 @@ export const mapViewFiltersToFilters = (
|
|||||||
value: viewFilter.value,
|
value: viewFilter.value,
|
||||||
displayValue: viewFilter.displayValue,
|
displayValue: viewFilter.displayValue,
|
||||||
operand: viewFilter.operand,
|
operand: viewFilter.operand,
|
||||||
|
viewFilterGroupId: viewFilter.viewFilterGroupId,
|
||||||
|
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
|
||||||
definition: viewFilter.definition ?? availableFilterDefinition,
|
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',
|
value: '20202020-1e55-4a1e-a1d2-fefb86a5fce5',
|
||||||
displayValue: '20202020-1270-4ebf-9018-c0ec10d5038e',
|
displayValue: '20202020-1270-4ebf-9018-c0ec10d5038e',
|
||||||
view: '20202020-4f5b-487e-829c-3d881c163611',
|
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 = {
|
export const VIEW_SORT_STANDARD_FIELD_IDS = {
|
||||||
@ -402,6 +411,7 @@ export const VIEW_STANDARD_FIELD_IDS = {
|
|||||||
viewFields: '20202020-542b-4bdc-b177-b63175d48edf',
|
viewFields: '20202020-542b-4bdc-b177-b63175d48edf',
|
||||||
viewGroups: '20202020-e1a1-419f-ac81-1986a5ea59a8',
|
viewGroups: '20202020-e1a1-419f-ac81-1986a5ea59a8',
|
||||||
viewFilters: '20202020-ff23-4154-b63c-21fb36cd0967',
|
viewFilters: '20202020-ff23-4154-b63c-21fb36cd0967',
|
||||||
|
viewFilterGroups: '20202020-0318-474a-84a1-bac895ceaa5a',
|
||||||
viewSorts: '20202020-891b-45c3-9fe1-80a75b4aa043',
|
viewSorts: '20202020-891b-45c3-9fe1-80a75b4aa043',
|
||||||
favorites: '20202020-c818-4a86-8284-9ec0ef0a59a5',
|
favorites: '20202020-c818-4a86-8284-9ec0ef0a59a5',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -37,6 +37,7 @@ export const STANDARD_OBJECT_IDS = {
|
|||||||
viewField: '20202020-4d19-4655-95bf-b2a04cf206d4',
|
viewField: '20202020-4d19-4655-95bf-b2a04cf206d4',
|
||||||
viewGroup: '20202020-725f-47a4-8008-4255f9519f70',
|
viewGroup: '20202020-725f-47a4-8008-4255f9519f70',
|
||||||
viewFilter: '20202020-6fb6-4631-aded-b7d67e952ec8',
|
viewFilter: '20202020-6fb6-4631-aded-b7d67e952ec8',
|
||||||
|
viewFilterGroup: '20202020-b920-4b11-92aa-9b07d878e542',
|
||||||
viewSort: '20202020-e46a-47a8-939a-e5d911f83531',
|
viewSort: '20202020-e46a-47a8-939a-e5d911f83531',
|
||||||
view: '20202020-722e-4739-8e2c-0c372d661f49',
|
view: '20202020-722e-4739-8e2c-0c372d661f49',
|
||||||
webhook: '20202020-be4d-4e08-811d-0fffcd13ffd4',
|
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 { BehavioralEventWorkspaceEntity } from 'src/modules/timeline/standard-objects/behavioral-event.workspace-entity';
|
||||||
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.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 { 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 { ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity';
|
||||||
import { ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.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';
|
import { ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity';
|
||||||
@ -59,6 +60,7 @@ export const standardObjectMetadataDefinitions = [
|
|||||||
ViewFieldWorkspaceEntity,
|
ViewFieldWorkspaceEntity,
|
||||||
ViewGroupWorkspaceEntity,
|
ViewGroupWorkspaceEntity,
|
||||||
ViewFilterWorkspaceEntity,
|
ViewFilterWorkspaceEntity,
|
||||||
|
ViewFilterGroupWorkspaceEntity,
|
||||||
ViewSortWorkspaceEntity,
|
ViewSortWorkspaceEntity,
|
||||||
ViewWorkspaceEntity,
|
ViewWorkspaceEntity,
|
||||||
WebhookWorkspaceEntity,
|
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')
|
@WorkspaceJoinColumn('view')
|
||||||
viewId: string | null;
|
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 { 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 { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
|
||||||
import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.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 { ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity';
|
||||||
import { ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.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';
|
import { ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity';
|
||||||
@ -138,6 +139,18 @@ export class ViewWorkspaceEntity extends BaseWorkspaceEntity {
|
|||||||
@WorkspaceIsNullable()
|
@WorkspaceIsNullable()
|
||||||
viewFilters: Relation<ViewFilterWorkspaceEntity[]>;
|
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({
|
@WorkspaceRelation({
|
||||||
standardId: VIEW_STANDARD_FIELD_IDS.viewSorts,
|
standardId: VIEW_STANDARD_FIELD_IDS.viewSorts,
|
||||||
type: RelationMetadataType.ONE_TO_MANY,
|
type: RelationMetadataType.ONE_TO_MANY,
|
||||||
|
|||||||
@ -126,6 +126,7 @@ export {
|
|||||||
IconFileZip,
|
IconFileZip,
|
||||||
IconFilter,
|
IconFilter,
|
||||||
IconFilterOff,
|
IconFilterOff,
|
||||||
|
IconFilterCog,
|
||||||
IconFocusCentered,
|
IconFocusCentered,
|
||||||
IconForbid,
|
IconForbid,
|
||||||
IconFunction,
|
IconFunction,
|
||||||
@ -151,6 +152,7 @@ export {
|
|||||||
IconLayoutSidebarLeftCollapse,
|
IconLayoutSidebarLeftCollapse,
|
||||||
IconLayoutSidebarRightCollapse,
|
IconLayoutSidebarRightCollapse,
|
||||||
IconLayoutSidebarRightExpand,
|
IconLayoutSidebarRightExpand,
|
||||||
|
IconLibraryPlus,
|
||||||
IconLink,
|
IconLink,
|
||||||
IconLinkOff,
|
IconLinkOff,
|
||||||
IconList,
|
IconList,
|
||||||
|
|||||||
@ -3,7 +3,6 @@ export * from './components';
|
|||||||
export * from './display';
|
export * from './display';
|
||||||
export * from './feedback';
|
export * from './feedback';
|
||||||
export * from './input';
|
export * from './input';
|
||||||
export * from './feedback';
|
|
||||||
export * from './layout';
|
export * from './layout';
|
||||||
export * from './navigation';
|
export * from './navigation';
|
||||||
export * from './testing';
|
export * from './testing';
|
||||||
|
|||||||
Reference in New Issue
Block a user