Use view filters operands in step filters + migrate to twenty-shared (#13137)
Step operand will more or less be the same as view filter operand. This PR: - moves `ViewFilterOperand` to twenty-shared - use it as step operand - check what operand should be available based on the selected field type in filter action - rewrite the function that evaluates filters so it uses ViewFilterOperand instead ViewFilterOperand may be renamed in a future PR.
This commit is contained in:
@ -2,8 +2,8 @@ import { ActionLink } from '@/action-menu/actions/components/ActionLink';
|
||||
import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
|
||||
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
|
||||
export const SeeRunsWorkflowSingleRecordAction = () => {
|
||||
const recordId = useSelectedRecordIdOrThrow();
|
||||
|
||||
@ -2,8 +2,8 @@ import { ActionLink } from '@/action-menu/actions/components/ActionLink';
|
||||
import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
|
||||
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
|
||||
export const SeeVersionsWorkflowSingleRecordAction = () => {
|
||||
const recordId = useSelectedRecordIdOrThrow();
|
||||
|
||||
@ -3,9 +3,9 @@ import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions
|
||||
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
|
||||
export const SeeRunsWorkflowVersionSingleRecordAction = () => {
|
||||
const recordId = useSelectedRecordIdOrThrow();
|
||||
|
||||
@ -3,9 +3,9 @@ import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions
|
||||
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
|
||||
export const SeeVersionsWorkflowVersionSingleRecordAction = () => {
|
||||
const recordId = useSelectedRecordIdOrThrow();
|
||||
|
||||
@ -2,8 +2,8 @@ import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextS
|
||||
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
|
||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { RecordFilterValueDependencies } from '@/object-record/record-filter/types/RecordFilterValueDependencies';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { expect } from '@storybook/test';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
|
||||
describe('computeContextStoreFilters', () => {
|
||||
|
||||
@ -14,7 +14,7 @@ import { SelectableList } from '@/ui/layout/selectable-list/components/Selectabl
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
|
||||
type AdvancedFilterRecordFilterOperandSelectContentProps = {
|
||||
|
||||
@ -5,7 +5,6 @@ import { selectedOperandInDropdownComponentState } from '@/object-record/object-
|
||||
import { getRelativeDateDisplayValue } from '@/object-record/object-filter-dropdown/utils/getRelativeDateDisplayValue';
|
||||
import { DateTimePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { computeVariableDateViewFilterValue } from '@/views/view-filter-value/utils/computeVariableDateViewFilterValue';
|
||||
import {
|
||||
resolveDateViewFilterValue,
|
||||
@ -13,6 +12,7 @@ import {
|
||||
VariableDateViewFilterValueUnit,
|
||||
} from '@/views/view-filter-value/utils/resolveDateViewFilterValue';
|
||||
import { useState } from 'react';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import { ObjectFilterDropdownRecordSelect } from '@/object-record/object-filter-
|
||||
import { ObjectFilterDropdownSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { ViewBarFilterDropdownVectorSearchInput } from '@/views/components/ViewBarFilterDropdownVectorSearchInput';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
|
||||
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
|
||||
import { ObjectFilterDropdownBooleanSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownBooleanSelect';
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
|
||||
export const selectedOperandInDropdownComponentState =
|
||||
createComponentStateV2<ViewFilterOperand | null>({
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
import { getOperandLabel, getOperandLabelShort } from '../getOperandLabel';
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
|
||||
import { isFilterOperandExpectingValue } from '../isFilterOperandExpectingValue';
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
|
||||
export const configurableViewFilterOperands = new Set<ViewFilterOperand>([
|
||||
ViewFilterOperand.Is,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
|
||||
export const getOperandLabel = (
|
||||
operand: ViewFilterOperand | null | undefined,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
|
||||
export const isFilterOperandExpectingValue = (operand: ViewFilterOperand) => {
|
||||
switch (operand) {
|
||||
|
||||
@ -3,8 +3,8 @@ import { renderHook } from '@testing-library/react';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { act } from 'react';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
import { useRemoveRecordFilter } from '../useRemoveRecordFilter';
|
||||
|
||||
@ -4,7 +4,7 @@ import { act } from 'react';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
import { useUpsertRecordFilter } from '../useUpsertRecordFilter';
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { FilterableAndTSVectorFieldType } from '@/object-record/record-filter/types/FilterableFieldType';
|
||||
import { FILTER_OPERANDS_MAP } from '@/object-record/record-filter/utils/getRecordFilterOperands';
|
||||
import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
|
||||
export type RecordFilter = {
|
||||
id: string;
|
||||
|
||||
@ -1 +1 @@
|
||||
export { ViewFilterOperand as RecordFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
export { ViewFilterOperand as RecordFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
|
||||
@ -3,7 +3,7 @@ import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
|
||||
import { RecordFilterValueDependencies } from '@/object-record/record-filter/types/RecordFilterValueDependencies';
|
||||
import { computeRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeRecordGqlOperationFilter';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { getCompaniesMock } from '~/testing/mock-data/companies';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
|
||||
@ -19,8 +19,8 @@ import {
|
||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { computeEmptyGqlOperationFilterForEmails } from '@/object-record/record-filter/utils/compute-empty-record-gql-operation-filter/for-composite-field/computeEmptyGqlOperationFilterForEmails';
|
||||
import { computeEmptyGqlOperationFilterForLinks } from '@/object-record/record-filter/utils/compute-empty-record-gql-operation-filter/for-composite-field/computeEmptyGqlOperationFilterForLinks';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
import { Field } from '~/generated/graphql';
|
||||
import { generateILikeFiltersForCompositeFields } from '~/utils/array/generateILikeFiltersForCompositeFields';
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import {
|
||||
FilterableFieldType,
|
||||
} from '@/object-record/record-filter/types/FilterableFieldType';
|
||||
import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName';
|
||||
import { ViewFilterOperand as RecordFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { ViewFilterOperand as RecordFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
|
||||
@ -9,8 +9,8 @@ import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { isSoftDeleteFilterActiveComponentState } from '@/object-record/record-table/states/isSoftDeleteFilterActiveComponentState';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type UseHandleToggleTrashColumnFilterProps = {
|
||||
|
||||
@ -19,8 +19,8 @@ import { AppPath } from '@/types/AppPath';
|
||||
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
import { RelationType } from '~/generated-metadata/graphql';
|
||||
import { getAppPath } from '~/utils/navigation/getAppPath';
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { FilterableFieldType } from '@/object-record/record-filter/types/FilterableFieldType';
|
||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
import { RelationType } from '~/generated-metadata/graphql';
|
||||
import { buildValueFromFilter } from './buildRecordInputFromFilter';
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ import {
|
||||
RecordFilterToRecordInputOperand,
|
||||
} from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { FILTER_OPERANDS_MAP } from '@/object-record/record-filter/utils/getRecordFilterOperands';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
import { assertUnreachable, parseJson } from 'twenty-shared/utils';
|
||||
import { RelationType } from '~/generated-metadata/graphql';
|
||||
|
||||
|
||||
@ -9,10 +9,10 @@ import { prefetchViewsState } from '@/prefetch/states/prefetchViewsState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { View } from '@/views/types/View';
|
||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
|
||||
import { ViewType } from '@/views/types/ViewType';
|
||||
import { act } from 'react';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
|
||||
@ -5,11 +5,11 @@ import { currentRecordFiltersComponentState } from '@/object-record/record-filte
|
||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
import { useApplyViewFiltersToCurrentRecordFilters } from '../useApplyViewFiltersToCurrentRecordFilters';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const mockObjectMetadataItemNameSingular = 'company';
|
||||
|
||||
|
||||
@ -15,8 +15,8 @@ import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { generateFindManyRecordsQuery } from '@/object-record/utils/generateFindManyRecordsQuery';
|
||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { relationFilterValueSchemaObject } from '@/views/view-filter-value/validation-schemas/jsonRelationFilterValueSchema';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const filterQueryParamsSchema = z.object({
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
|
||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState';
|
||||
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
|
||||
export const useOpenVectorSearchFilter = (filterDropdownId?: string) => {
|
||||
const setSelectedOperandInDropdown = useSetRecoilComponentStateV2(
|
||||
|
||||
@ -3,7 +3,7 @@ import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRe
|
||||
import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter';
|
||||
import { isRecordFilterConsideredEmpty } from '@/object-record/record-filter/utils/isRecordFilterConsideredEmpty';
|
||||
import { useVectorSearchFieldInRecordIndexContextOrThrow } from '@/views/hooks/useVectorSearchFieldInRecordIndexContextOrThrow';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
import { useVectorSearchFilterState } from './useVectorSearchFilterState';
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName';
|
||||
import { ViewFilterOperand } from './ViewFilterOperand';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
|
||||
export type ViewFilter = {
|
||||
__typename: 'ViewFilter';
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
import { areViewFiltersEqual } from '../areViewFiltersEqual';
|
||||
|
||||
describe('areViewFiltersEqual', () => {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
import { getViewFiltersToCreate } from '../getViewFiltersToCreate';
|
||||
|
||||
describe('getViewFiltersToCreate', () => {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
import { getViewFiltersToDelete } from '../getViewFiltersToDelete';
|
||||
|
||||
describe('getViewFiltersToDelete', () => {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
import { getViewFiltersToUpdate } from '../getViewFiltersToUpdate';
|
||||
|
||||
describe('getViewFiltersToUpdate', () => {
|
||||
|
||||
@ -4,12 +4,12 @@ import { RecordSort } from '@/object-record/record-sort/types/RecordSort';
|
||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||
import { ViewField } from '@/views/types/ViewField';
|
||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { ViewSort } from '@/views/types/ViewSort';
|
||||
import { mapColumnDefinitionsToViewFields } from '@/views/utils/mapColumnDefinitionToViewField';
|
||||
import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToColumnDefinitions';
|
||||
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
|
||||
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
|
||||
export const isVectorSearchFilter = (filter: RecordFilter) => {
|
||||
return filter.operand === ViewFilterOperand.VectorSearch;
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import {
|
||||
addDays,
|
||||
addMonths,
|
||||
@ -19,6 +18,7 @@ import {
|
||||
subWeeks,
|
||||
subYears,
|
||||
} from 'date-fns';
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
StepFilter,
|
||||
StepFilterGroup,
|
||||
StepLogicalOperator,
|
||||
StepOperand,
|
||||
ViewFilterOperand,
|
||||
} from 'twenty-shared/src/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconLibraryPlus, IconPlus } from 'twenty-ui/display';
|
||||
@ -21,6 +21,17 @@ type WorkflowStepFilterAddFilterRuleSelectProps = {
|
||||
stepFilterGroup: StepFilterGroup;
|
||||
};
|
||||
|
||||
const BASE_NEW_STEP_FILTER = {
|
||||
id: v4(),
|
||||
type: 'unknown',
|
||||
label: '',
|
||||
value: '',
|
||||
operand: ViewFilterOperand.Is,
|
||||
displayValue: '',
|
||||
stepFilterGroupId: '',
|
||||
stepOutputKey: '',
|
||||
};
|
||||
|
||||
export const WorkflowStepFilterAddFilterRuleSelect = ({
|
||||
stepFilterGroup,
|
||||
}: WorkflowStepFilterAddFilterRuleSelectProps) => {
|
||||
@ -42,16 +53,10 @@ export const WorkflowStepFilterAddFilterRuleSelect = ({
|
||||
closeDropdown(dropdownId);
|
||||
|
||||
const newStepFilter = {
|
||||
id: v4(),
|
||||
type: 'text',
|
||||
label: 'New Filter',
|
||||
value: '',
|
||||
operand: StepOperand.EQ,
|
||||
displayValue: '',
|
||||
...BASE_NEW_STEP_FILTER,
|
||||
stepFilterGroupId: stepFilterGroup.id,
|
||||
stepOutputKey: '',
|
||||
positionInStepFilterGroup: newPositionInStepFilterGroup,
|
||||
};
|
||||
} satisfies StepFilter;
|
||||
|
||||
upsertStepFilterSettings({
|
||||
stepFilterToUpsert: newStepFilter,
|
||||
@ -71,15 +76,9 @@ export const WorkflowStepFilterAddFilterRuleSelect = ({
|
||||
};
|
||||
|
||||
const newStepFilter: StepFilter = {
|
||||
id: v4(),
|
||||
type: 'text',
|
||||
operand: StepOperand.EQ,
|
||||
value: '',
|
||||
displayValue: '',
|
||||
...BASE_NEW_STEP_FILTER,
|
||||
stepFilterGroupId: newStepFilterGroupId,
|
||||
positionInStepFilterGroup: 1,
|
||||
label: 'New Filter',
|
||||
stepOutputKey: '',
|
||||
};
|
||||
|
||||
upsertStepFilterSettings({
|
||||
|
||||
@ -8,7 +8,7 @@ import { extractRawVariableNamePart } from '@/workflow/workflow-variables/utils/
|
||||
import { searchVariableThroughOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughOutputSchema';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useRecoilCallback, useRecoilValue } from 'recoil';
|
||||
import { StepFilter } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
@ -30,6 +30,7 @@ export const WorkflowStepFilterFieldSelect = ({
|
||||
rawVariableName: stepFilter.stepOutputKey,
|
||||
part: 'stepId',
|
||||
});
|
||||
|
||||
const stepsOutputSchema = useRecoilValue(
|
||||
stepsOutputSchemaFamilySelector({
|
||||
workflowVersionId,
|
||||
@ -37,6 +38,41 @@ export const WorkflowStepFilterFieldSelect = ({
|
||||
}),
|
||||
);
|
||||
|
||||
const handleChange = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(variableName: string) => {
|
||||
const stepId = extractRawVariableNamePart({
|
||||
rawVariableName: variableName,
|
||||
part: 'stepId',
|
||||
});
|
||||
const currentStepOutputSchema = snapshot
|
||||
.getLoadable(
|
||||
stepsOutputSchemaFamilySelector({
|
||||
workflowVersionId,
|
||||
stepIds: [stepId],
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const { variableLabel, variableType } =
|
||||
searchVariableThroughOutputSchema({
|
||||
stepOutputSchema: currentStepOutputSchema?.[0],
|
||||
rawVariableName: variableName,
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
upsertStepFilterSettings({
|
||||
stepFilterToUpsert: {
|
||||
...stepFilter,
|
||||
stepOutputKey: variableName,
|
||||
displayValue: variableLabel ?? '',
|
||||
type: variableType ?? 'unknown',
|
||||
},
|
||||
});
|
||||
},
|
||||
[upsertStepFilterSettings, stepFilter, workflowVersionId],
|
||||
);
|
||||
|
||||
if (!isDefined(stepId)) {
|
||||
return null;
|
||||
}
|
||||
@ -50,16 +86,6 @@ export const WorkflowStepFilterFieldSelect = ({
|
||||
const isSelectedFieldNotFound = !isDefined(variableLabel);
|
||||
const label = isSelectedFieldNotFound ? t`No Field Selected` : variableLabel;
|
||||
|
||||
const handleChange = (variableName: string) => {
|
||||
upsertStepFilterSettings({
|
||||
stepFilterToUpsert: {
|
||||
...stepFilter,
|
||||
stepOutputKey: variableName,
|
||||
displayValue: label,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<WorkflowVariablesDropdown
|
||||
instanceId={`step-filter-field-${stepFilter.id}`}
|
||||
|
||||
@ -1,36 +1,31 @@
|
||||
import { DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET } from '@/object-record/advanced-filter/constants/DefaultAdvancedFilterDropdownOffset';
|
||||
import { getOperandLabel } from '@/object-record/object-filter-dropdown/utils/getOperandLabel';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { useUpsertStepFilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useUpsertStepFilterSettings';
|
||||
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
|
||||
import { getViewFilterOperands } from '@/workflow/workflow-steps/workflow-actions/filter-action/utils/getStepFilterOperands';
|
||||
import { useContext } from 'react';
|
||||
import { StepFilter, StepOperand } from 'twenty-shared/src/types';
|
||||
import { StepFilter, ViewFilterOperand } from 'twenty-shared/src/types';
|
||||
|
||||
type WorkflowStepFilterOperandSelectProps = {
|
||||
stepFilter: StepFilter;
|
||||
};
|
||||
|
||||
const STEP_OPERAND_OPTIONS = [
|
||||
{ value: StepOperand.EQ, label: 'Equals' },
|
||||
{ value: StepOperand.NE, label: 'Not equals' },
|
||||
{ value: StepOperand.GT, label: 'Greater than' },
|
||||
{ value: StepOperand.GTE, label: 'Greater than or equal' },
|
||||
{ value: StepOperand.LT, label: 'Less than' },
|
||||
{ value: StepOperand.LTE, label: 'Less than or equal' },
|
||||
{ value: StepOperand.LIKE, label: 'Contains' },
|
||||
{ value: StepOperand.ILIKE, label: 'Contains (case insensitive)' },
|
||||
{ value: StepOperand.IN, label: 'In' },
|
||||
{ value: StepOperand.IS, label: 'Is' },
|
||||
];
|
||||
|
||||
export const WorkflowStepFilterOperandSelect = ({
|
||||
stepFilter,
|
||||
}: WorkflowStepFilterOperandSelectProps) => {
|
||||
const { readonly } = useContext(WorkflowStepFilterContext);
|
||||
|
||||
const { upsertStepFilterSettings } = useUpsertStepFilterSettings();
|
||||
const operands = getViewFilterOperands({ filterType: stepFilter.type });
|
||||
|
||||
const handleChange = (operand: StepOperand) => {
|
||||
const options = operands.map((operand) => ({
|
||||
value: operand,
|
||||
label: getOperandLabel(operand),
|
||||
}));
|
||||
|
||||
const handleChange = (operand: ViewFilterOperand) => {
|
||||
upsertStepFilterSettings({
|
||||
stepFilterToUpsert: {
|
||||
...stepFilter,
|
||||
@ -45,7 +40,7 @@ export const WorkflowStepFilterOperandSelect = ({
|
||||
dropdownWidth={GenericDropdownContentWidth.Medium}
|
||||
dropdownId={`step-filter-operand-${stepFilter.id}`}
|
||||
value={stepFilter.operand}
|
||||
options={STEP_OPERAND_OPTIONS}
|
||||
options={options}
|
||||
onChange={handleChange}
|
||||
disabled={readonly}
|
||||
dropdownOffset={DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET}
|
||||
|
||||
@ -4,7 +4,7 @@ import {
|
||||
StepFilter,
|
||||
StepFilterGroup,
|
||||
StepLogicalOperator,
|
||||
StepOperand,
|
||||
ViewFilterOperand,
|
||||
} from 'twenty-shared/types';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
@ -28,7 +28,7 @@ const TEXT_STEP_FILTER: StepFilter = {
|
||||
type: 'text',
|
||||
label: 'Company Name',
|
||||
value: 'Acme',
|
||||
operand: StepOperand.LIKE,
|
||||
operand: ViewFilterOperand.Contains,
|
||||
};
|
||||
|
||||
const meta: Meta<typeof WorkflowStepFilterColumn> = {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { WorkflowStepFilterDecorator } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/decorators/WorkflowStepFilterDecorator';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { StepFilter, StepOperand } from 'twenty-shared/types';
|
||||
import { StepFilter, ViewFilterOperand } from 'twenty-shared/types';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
|
||||
@ -16,7 +16,7 @@ const DEFAULT_STEP_FILTER: StepFilter = {
|
||||
displayValue: '',
|
||||
type: 'text',
|
||||
label: 'New Filter',
|
||||
operand: StepOperand.EQ,
|
||||
operand: ViewFilterOperand.Is,
|
||||
value: '',
|
||||
positionInStepFilterGroup: 0,
|
||||
};
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import { WorkflowStepFilterDecorator } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/decorators/WorkflowStepFilterDecorator';
|
||||
import { WorkflowStepFilterOperandSelect } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterOperandSelect';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { expect, within } from '@storybook/test';
|
||||
import { StepFilter, StepOperand } from 'twenty-shared/types';
|
||||
import { StepFilter, ViewFilterOperand } from 'twenty-shared/types';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
|
||||
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
|
||||
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { WorkflowStepFilterOperandSelect } from '../WorkflowStepFilterOperandSelect';
|
||||
|
||||
const DEFAULT_STEP_FILTER: StepFilter = {
|
||||
id: 'filter-1',
|
||||
@ -17,23 +17,11 @@ const DEFAULT_STEP_FILTER: StepFilter = {
|
||||
displayValue: 'Company Name',
|
||||
type: 'text',
|
||||
label: 'Company Name',
|
||||
operand: StepOperand.EQ,
|
||||
operand: ViewFilterOperand.Contains,
|
||||
value: '',
|
||||
positionInStepFilterGroup: 0,
|
||||
};
|
||||
|
||||
const LIKE_OPERAND_FILTER: StepFilter = {
|
||||
id: 'filter-1',
|
||||
stepFilterGroupId: 'filter-group-1',
|
||||
stepOutputKey: 'company.name',
|
||||
displayValue: 'Company Name',
|
||||
type: 'text',
|
||||
label: 'Company Name',
|
||||
operand: StepOperand.LIKE,
|
||||
value: 'Acme',
|
||||
positionInStepFilterGroup: 0,
|
||||
};
|
||||
|
||||
const GREATER_THAN_FILTER: StepFilter = {
|
||||
id: 'filter-1',
|
||||
stepFilterGroupId: 'filter-group-1',
|
||||
@ -41,7 +29,7 @@ const GREATER_THAN_FILTER: StepFilter = {
|
||||
displayValue: 'Employee Count',
|
||||
type: 'number',
|
||||
label: 'Employee Count',
|
||||
operand: StepOperand.GT,
|
||||
operand: ViewFilterOperand.GreaterThanOrEqual,
|
||||
value: '100',
|
||||
positionInStepFilterGroup: 0,
|
||||
};
|
||||
@ -69,17 +57,6 @@ export default meta;
|
||||
type Story = StoryObj<typeof WorkflowStepFilterOperandSelect>;
|
||||
|
||||
export const Default: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await expect(await canvas.findByText('Equals')).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const WithLikeOperand: Story = {
|
||||
args: {
|
||||
stepFilter: LIKE_OPERAND_FILTER,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
@ -94,6 +71,8 @@ export const WithGreaterThanOperand: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await expect(await canvas.findByText('Greater than')).toBeVisible();
|
||||
await expect(
|
||||
await canvas.findByText('Greater than or equal'),
|
||||
).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { WorkflowStepFilterDecorator } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/decorators/WorkflowStepFilterDecorator';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { expect, within } from '@storybook/test';
|
||||
import { StepFilter, StepOperand } from 'twenty-shared/types';
|
||||
import { StepFilter, ViewFilterOperand } from 'twenty-shared/types';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
|
||||
@ -17,7 +17,7 @@ const TEXT_FILTER: StepFilter = {
|
||||
displayValue: 'Company Name',
|
||||
type: 'text',
|
||||
label: 'Company Name',
|
||||
operand: StepOperand.LIKE,
|
||||
operand: ViewFilterOperand.Contains,
|
||||
value: 'Acme',
|
||||
positionInStepFilterGroup: 0,
|
||||
};
|
||||
@ -29,7 +29,7 @@ const NUMBER_FILTER: StepFilter = {
|
||||
displayValue: 'Employee Count',
|
||||
type: 'number',
|
||||
label: 'Employee Count',
|
||||
operand: StepOperand.GT,
|
||||
operand: ViewFilterOperand.GreaterThanOrEqual,
|
||||
value: '100',
|
||||
positionInStepFilterGroup: 0,
|
||||
};
|
||||
|
||||
@ -11,7 +11,7 @@ import {
|
||||
StepFilter,
|
||||
StepFilterGroup,
|
||||
StepLogicalOperator,
|
||||
StepOperand,
|
||||
ViewFilterOperand,
|
||||
} from 'twenty-shared/types';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
@ -48,10 +48,10 @@ export const useAddRootStepFilter = () => {
|
||||
|
||||
const newStepFilter: StepFilter = {
|
||||
id: v4(),
|
||||
type: 'text',
|
||||
type: 'unknown',
|
||||
label: 'New Filter',
|
||||
value: '',
|
||||
operand: StepOperand.EQ,
|
||||
operand: ViewFilterOperand.Is,
|
||||
displayValue: '',
|
||||
stepFilterGroupId: newStepFilterGroup.id,
|
||||
stepOutputKey: '',
|
||||
|
||||
@ -0,0 +1,115 @@
|
||||
import { ViewFilterOperand } from 'twenty-shared/src/types';
|
||||
|
||||
const emptyOperands = [
|
||||
ViewFilterOperand.IsEmpty,
|
||||
ViewFilterOperand.IsNotEmpty,
|
||||
] as const;
|
||||
|
||||
const relationOperands = [
|
||||
ViewFilterOperand.Is,
|
||||
ViewFilterOperand.IsNot,
|
||||
] as const;
|
||||
|
||||
const defaultOperands = [
|
||||
ViewFilterOperand.Is,
|
||||
ViewFilterOperand.IsNot,
|
||||
ViewFilterOperand.Contains,
|
||||
ViewFilterOperand.DoesNotContain,
|
||||
ViewFilterOperand.GreaterThanOrEqual,
|
||||
ViewFilterOperand.LessThanOrEqual,
|
||||
...emptyOperands,
|
||||
] as const;
|
||||
|
||||
export const FILTER_OPERANDS_MAP = {
|
||||
TEXT: [
|
||||
ViewFilterOperand.Contains,
|
||||
ViewFilterOperand.DoesNotContain,
|
||||
...emptyOperands,
|
||||
],
|
||||
NUMBER: [
|
||||
ViewFilterOperand.GreaterThanOrEqual,
|
||||
ViewFilterOperand.LessThanOrEqual,
|
||||
...emptyOperands,
|
||||
],
|
||||
RAW_JSON: [
|
||||
ViewFilterOperand.Contains,
|
||||
ViewFilterOperand.DoesNotContain,
|
||||
...emptyOperands,
|
||||
],
|
||||
DATE_TIME: [
|
||||
ViewFilterOperand.Is,
|
||||
ViewFilterOperand.IsRelative,
|
||||
ViewFilterOperand.IsInPast,
|
||||
ViewFilterOperand.IsInFuture,
|
||||
ViewFilterOperand.IsToday,
|
||||
ViewFilterOperand.IsBefore,
|
||||
ViewFilterOperand.IsAfter,
|
||||
...emptyOperands,
|
||||
],
|
||||
DATE: [
|
||||
ViewFilterOperand.Is,
|
||||
ViewFilterOperand.IsRelative,
|
||||
ViewFilterOperand.IsInPast,
|
||||
ViewFilterOperand.IsInFuture,
|
||||
ViewFilterOperand.IsToday,
|
||||
ViewFilterOperand.IsBefore,
|
||||
ViewFilterOperand.IsAfter,
|
||||
...emptyOperands,
|
||||
],
|
||||
RATING: [ViewFilterOperand.Is, ViewFilterOperand.IsNot, ...emptyOperands],
|
||||
RELATION: [...relationOperands, ...emptyOperands],
|
||||
MULTI_SELECT: [
|
||||
ViewFilterOperand.Contains,
|
||||
ViewFilterOperand.DoesNotContain,
|
||||
...emptyOperands,
|
||||
],
|
||||
SELECT: [ViewFilterOperand.Is, ViewFilterOperand.IsNot, ...emptyOperands],
|
||||
ARRAY: [
|
||||
ViewFilterOperand.Contains,
|
||||
ViewFilterOperand.DoesNotContain,
|
||||
...emptyOperands,
|
||||
],
|
||||
BOOLEAN: [ViewFilterOperand.Is],
|
||||
UUID: [ViewFilterOperand.Is],
|
||||
NUMERIC: [
|
||||
ViewFilterOperand.GreaterThanOrEqual,
|
||||
ViewFilterOperand.LessThanOrEqual,
|
||||
...emptyOperands,
|
||||
],
|
||||
};
|
||||
|
||||
export const getViewFilterOperands = ({
|
||||
filterType,
|
||||
}: {
|
||||
filterType: string;
|
||||
}): readonly ViewFilterOperand[] => {
|
||||
switch (filterType) {
|
||||
case 'TEXT':
|
||||
return FILTER_OPERANDS_MAP.TEXT;
|
||||
case 'NUMBER':
|
||||
return FILTER_OPERANDS_MAP.NUMBER;
|
||||
case 'RAW_JSON':
|
||||
return FILTER_OPERANDS_MAP.RAW_JSON;
|
||||
case 'DATE_TIME':
|
||||
case 'DATE':
|
||||
return FILTER_OPERANDS_MAP.DATE_TIME;
|
||||
case 'RATING':
|
||||
return FILTER_OPERANDS_MAP.RATING;
|
||||
case 'RELATION':
|
||||
return FILTER_OPERANDS_MAP.RELATION;
|
||||
case 'MULTI_SELECT':
|
||||
return FILTER_OPERANDS_MAP.MULTI_SELECT;
|
||||
case 'SELECT':
|
||||
return FILTER_OPERANDS_MAP.SELECT;
|
||||
case 'ARRAY':
|
||||
return FILTER_OPERANDS_MAP.ARRAY;
|
||||
case 'BOOLEAN':
|
||||
return FILTER_OPERANDS_MAP.BOOLEAN;
|
||||
case 'UUID':
|
||||
return FILTER_OPERANDS_MAP.UUID;
|
||||
case 'NUMERIC':
|
||||
return FILTER_OPERANDS_MAP.NUMERIC;
|
||||
default:
|
||||
return defaultOperands;
|
||||
}
|
||||
};
|
||||
@ -76,6 +76,7 @@ describe('searchVariableThroughOutputSchema', () => {
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: 'Step 1 > undefined',
|
||||
variableType: 'unknown',
|
||||
});
|
||||
});
|
||||
|
||||
@ -89,6 +90,7 @@ describe('searchVariableThroughOutputSchema', () => {
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Name',
|
||||
variablePathLabel: 'Step 1 > Company > Name',
|
||||
variableType: 'unknown',
|
||||
});
|
||||
});
|
||||
|
||||
@ -102,6 +104,7 @@ describe('searchVariableThroughOutputSchema', () => {
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Email',
|
||||
variablePathLabel: 'Step 1 > Person > Email',
|
||||
variableType: 'unknown',
|
||||
});
|
||||
});
|
||||
|
||||
@ -115,6 +118,7 @@ describe('searchVariableThroughOutputSchema', () => {
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Company',
|
||||
variablePathLabel: 'Step 1 > Company > Company',
|
||||
variableType: 'unknown',
|
||||
});
|
||||
});
|
||||
|
||||
@ -128,6 +132,7 @@ describe('searchVariableThroughOutputSchema', () => {
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Person',
|
||||
variablePathLabel: 'Step 1 > Person > Person',
|
||||
variableType: 'unknown',
|
||||
});
|
||||
});
|
||||
|
||||
@ -141,6 +146,7 @@ describe('searchVariableThroughOutputSchema', () => {
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Simple Data',
|
||||
variablePathLabel: 'Step 1 > Simple Data',
|
||||
variableType: 'unknown',
|
||||
});
|
||||
});
|
||||
|
||||
@ -154,6 +160,7 @@ describe('searchVariableThroughOutputSchema', () => {
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Field 1',
|
||||
variablePathLabel: 'Step 1 > Nested Data > Field 1',
|
||||
variableType: 'unknown',
|
||||
});
|
||||
});
|
||||
|
||||
@ -167,6 +174,7 @@ describe('searchVariableThroughOutputSchema', () => {
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: 'Step 1 > undefined',
|
||||
variableType: 'unknown',
|
||||
});
|
||||
});
|
||||
|
||||
@ -180,6 +188,7 @@ describe('searchVariableThroughOutputSchema', () => {
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: 'Step 1 > undefined',
|
||||
variableType: 'unknown',
|
||||
});
|
||||
});
|
||||
|
||||
@ -208,6 +217,7 @@ describe('searchVariableThroughOutputSchema', () => {
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Field 1',
|
||||
variablePathLabel: 'Step 1 > Complex Field > Field 1',
|
||||
variableType: 'unknown',
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -274,6 +284,7 @@ describe('searchVariableThroughOutputSchema', () => {
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Name',
|
||||
variablePathLabel: 'Record is Created > Name',
|
||||
variableType: FieldMetadataType.TEXT,
|
||||
});
|
||||
});
|
||||
|
||||
@ -288,6 +299,7 @@ describe('searchVariableThroughOutputSchema', () => {
|
||||
expect(result).toEqual({
|
||||
variableLabel: ' Amount Micros',
|
||||
variablePathLabel: 'Record is Created > ARR > Amount Micros',
|
||||
variableType: FieldMetadataType.NUMERIC,
|
||||
});
|
||||
});
|
||||
|
||||
@ -301,6 +313,7 @@ describe('searchVariableThroughOutputSchema', () => {
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Company',
|
||||
variablePathLabel: 'Record is Created > Company',
|
||||
variableType: 'unknown',
|
||||
});
|
||||
});
|
||||
|
||||
@ -314,6 +327,7 @@ describe('searchVariableThroughOutputSchema', () => {
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: 'Record is Created > undefined',
|
||||
variableType: 'unknown',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
StepOutputSchema,
|
||||
} from '@/workflow/workflow-variables/types/StepOutputSchema';
|
||||
import { isBaseOutputSchema } from '@/workflow/workflow-variables/utils/isBaseOutputSchema';
|
||||
import { isLinkOutputSchema } from '@/workflow/workflow-variables/utils/isLinkOutputSchema';
|
||||
import { isRecordOutputSchema } from '@/workflow/workflow-variables/utils/isRecordOutputSchema';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
@ -30,6 +31,18 @@ const getDisplayedSubStepFieldLabel = (
|
||||
return;
|
||||
};
|
||||
|
||||
const getVariableType = (key: string, outputSchema: OutputSchema): string => {
|
||||
if (isRecordOutputSchema(outputSchema)) {
|
||||
return outputSchema.fields[key]?.type ?? 'unknown';
|
||||
}
|
||||
|
||||
if (isLinkOutputSchema(outputSchema)) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
return outputSchema[key]?.type ?? 'unknown';
|
||||
};
|
||||
|
||||
const searchCurrentStepOutputSchema = ({
|
||||
stepOutputSchema,
|
||||
path,
|
||||
@ -90,6 +103,7 @@ const searchCurrentStepOutputSchema = ({
|
||||
return {
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
variableType: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@ -102,6 +116,10 @@ const searchCurrentStepOutputSchema = ({
|
||||
currentSubStep,
|
||||
),
|
||||
variablePathLabel,
|
||||
variableType: getVariableType(
|
||||
isSelectedFieldInNextKey ? nextKey : selectedField,
|
||||
currentSubStep,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
@ -142,15 +160,17 @@ export const searchVariableThroughOutputSchema = ({
|
||||
};
|
||||
}
|
||||
|
||||
const { variableLabel, variablePathLabel } = searchCurrentStepOutputSchema({
|
||||
stepOutputSchema,
|
||||
path,
|
||||
isFullRecord,
|
||||
selectedField,
|
||||
});
|
||||
const { variableLabel, variablePathLabel, variableType } =
|
||||
searchCurrentStepOutputSchema({
|
||||
stepOutputSchema,
|
||||
path,
|
||||
isFullRecord,
|
||||
selectedField,
|
||||
});
|
||||
|
||||
return {
|
||||
variableLabel,
|
||||
variablePathLabel: `${variablePathLabel} > ${variableLabel}`,
|
||||
variableType,
|
||||
};
|
||||
};
|
||||
|
||||
@ -2,12 +2,32 @@ import {
|
||||
StepFilter,
|
||||
StepFilterGroup,
|
||||
StepLogicalOperator,
|
||||
StepOperand,
|
||||
ViewFilterOperand,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { evaluateFilterConditions } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/utils/evaluate-filter-conditions.util';
|
||||
|
||||
describe('evaluateFilterConditions', () => {
|
||||
type ResolvedFilter = Omit<StepFilter, 'value' | 'stepOutputKey'> & {
|
||||
rightOperand: unknown;
|
||||
leftOperand: unknown;
|
||||
};
|
||||
|
||||
const createFilter = (
|
||||
operand: ViewFilterOperand,
|
||||
leftOperand: unknown,
|
||||
rightOperand: unknown,
|
||||
): ResolvedFilter => ({
|
||||
id: 'filter1',
|
||||
type: 'text',
|
||||
label: 'Test Filter',
|
||||
rightOperand,
|
||||
operand,
|
||||
displayValue: String(rightOperand),
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand,
|
||||
});
|
||||
|
||||
describe('empty inputs', () => {
|
||||
it('should return true when no filters or groups are provided', () => {
|
||||
const result = evaluateFilterConditions({
|
||||
@ -26,190 +46,32 @@ describe('evaluateFilterConditions', () => {
|
||||
});
|
||||
|
||||
describe('single filter operands', () => {
|
||||
type ResolvedFilter = Omit<StepFilter, 'value' | 'stepOutputKey'> & {
|
||||
rightOperand: unknown;
|
||||
leftOperand: unknown;
|
||||
};
|
||||
|
||||
const createFilter = (
|
||||
operand: StepOperand,
|
||||
leftOperand: unknown,
|
||||
rightOperand: unknown,
|
||||
): ResolvedFilter => ({
|
||||
id: 'filter1',
|
||||
type: 'text',
|
||||
label: 'Test Filter',
|
||||
rightOperand,
|
||||
operand,
|
||||
displayValue: String(rightOperand),
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand,
|
||||
});
|
||||
|
||||
describe('eq operand', () => {
|
||||
describe('Is operand', () => {
|
||||
it('should return true when values are equal', () => {
|
||||
const filter = createFilter(StepOperand.EQ, 'John', 'John');
|
||||
const filter = createFilter(ViewFilterOperand.Is, 'John', 'John');
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when values are loosely equal', () => {
|
||||
const filter = createFilter(StepOperand.EQ, '123', 123);
|
||||
const filter = createFilter(ViewFilterOperand.Is, '123', 123);
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when values are not equal', () => {
|
||||
const filter = createFilter(StepOperand.EQ, 'John', 'Jane');
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ne operand', () => {
|
||||
it('should return false when values are equal', () => {
|
||||
const filter = createFilter(StepOperand.NE, 'John', 'John');
|
||||
const filter = createFilter(ViewFilterOperand.Is, 'John', 'Jane');
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when values are not equal', () => {
|
||||
const filter = createFilter(StepOperand.NE, 'John', 'Jane');
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('numeric operands', () => {
|
||||
it('should handle gt operand correctly', () => {
|
||||
const filter1 = createFilter(StepOperand.GT, 30, 25);
|
||||
const filter2 = createFilter(StepOperand.GT, 20, 25);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle gte operand correctly', () => {
|
||||
const filter1 = createFilter(StepOperand.GTE, 25, 25);
|
||||
const filter2 = createFilter(StepOperand.GTE, 30, 25);
|
||||
const filter3 = createFilter(StepOperand.GTE, 20, 25);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter3] })).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle lt operand correctly', () => {
|
||||
const filter1 = createFilter(StepOperand.LT, 20, 25);
|
||||
const filter2 = createFilter(StepOperand.LT, 30, 25);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle lte operand correctly', () => {
|
||||
const filter1 = createFilter(StepOperand.LTE, 25, 25);
|
||||
const filter2 = createFilter(StepOperand.LTE, 20, 25);
|
||||
const filter3 = createFilter(StepOperand.LTE, 30, 25);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter3] })).toBe(false);
|
||||
});
|
||||
|
||||
it('should convert string numbers for numeric comparisons', () => {
|
||||
const filter1 = createFilter(StepOperand.GT, '30', '25');
|
||||
const filter2 = createFilter(StepOperand.LT, '20', '25');
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('string operands', () => {
|
||||
it('should handle like operand correctly', () => {
|
||||
const filter1 = createFilter(StepOperand.LIKE, 'Hello World', 'World');
|
||||
const filter2 = createFilter(StepOperand.LIKE, 'Hello', 'World');
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle ilike operand correctly (case insensitive)', () => {
|
||||
const filter1 = createFilter(StepOperand.ILIKE, 'Hello World', 'WORLD');
|
||||
const filter2 = createFilter(StepOperand.ILIKE, 'Hello World', 'world');
|
||||
const filter3 = createFilter(StepOperand.ILIKE, 'Hello', 'WORLD');
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter3] })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('in operand', () => {
|
||||
it('should handle JSON array values', () => {
|
||||
const filter1 = createFilter(
|
||||
StepOperand.IN,
|
||||
'apple',
|
||||
'["apple", "banana", "cherry"]',
|
||||
);
|
||||
const filter2 = createFilter(
|
||||
StepOperand.IN,
|
||||
'grape',
|
||||
'["apple", "banana", "cherry"]',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle comma-separated string values when JSON parsing fails', () => {
|
||||
const filter1 = createFilter(
|
||||
StepOperand.IN,
|
||||
'apple',
|
||||
'apple, banana, cherry',
|
||||
);
|
||||
const filter2 = createFilter(
|
||||
StepOperand.IN,
|
||||
'grape',
|
||||
'apple, banana, cherry',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle comma-separated values with whitespace', () => {
|
||||
const filter = createFilter(
|
||||
StepOperand.IN,
|
||||
'apple',
|
||||
' apple , banana , cherry ',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter] })).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-array JSON values', () => {
|
||||
const filter = createFilter(
|
||||
StepOperand.IN,
|
||||
'apple',
|
||||
'{"key": "value"}',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter] })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('is operand', () => {
|
||||
it('should handle null checks', () => {
|
||||
const filter1 = createFilter(StepOperand.IS, null, 'null');
|
||||
const filter2 = createFilter(StepOperand.IS, undefined, 'NULL');
|
||||
const filter3 = createFilter(StepOperand.IS, 'value', 'null');
|
||||
const filter1 = createFilter(ViewFilterOperand.Is, null, 'null');
|
||||
const filter2 = createFilter(ViewFilterOperand.Is, undefined, 'NULL');
|
||||
const filter3 = createFilter(ViewFilterOperand.Is, 'value', 'null');
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
|
||||
@ -217,43 +79,213 @@ describe('evaluateFilterConditions', () => {
|
||||
});
|
||||
|
||||
it('should handle not null checks', () => {
|
||||
const filter1 = createFilter(StepOperand.IS, 'value', 'not null');
|
||||
const filter2 = createFilter(StepOperand.IS, 'value', 'NOT NULL');
|
||||
const filter3 = createFilter(StepOperand.IS, null, 'not null');
|
||||
const filter4 = createFilter(StepOperand.IS, undefined, 'not null');
|
||||
const filter1 = createFilter(ViewFilterOperand.Is, 'value', 'not null');
|
||||
const filter2 = createFilter(ViewFilterOperand.Is, 'value', 'NOT NULL');
|
||||
const filter3 = createFilter(ViewFilterOperand.Is, null, 'not null');
|
||||
const filter4 = createFilter(
|
||||
ViewFilterOperand.Is,
|
||||
undefined,
|
||||
'not null',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter3] })).toBe(false);
|
||||
expect(evaluateFilterConditions({ filters: [filter4] })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle exact value comparisons for non-null/not-null cases', () => {
|
||||
const filter1 = createFilter(StepOperand.IS, 'exact', 'exact');
|
||||
const filter2 = createFilter(StepOperand.IS, 'value', 'different');
|
||||
describe('IsNot operand', () => {
|
||||
it('should return false when values are equal', () => {
|
||||
const filter = createFilter(ViewFilterOperand.IsNot, 'John', 'John');
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when values are not equal', () => {
|
||||
const filter = createFilter(ViewFilterOperand.IsNot, 'John', 'Jane');
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('numeric operands', () => {
|
||||
it('should handle GreaterThanOrEqual operand correctly', () => {
|
||||
const filter1 = createFilter(
|
||||
ViewFilterOperand.GreaterThanOrEqual,
|
||||
25,
|
||||
25,
|
||||
);
|
||||
const filter2 = createFilter(
|
||||
ViewFilterOperand.GreaterThanOrEqual,
|
||||
30,
|
||||
25,
|
||||
);
|
||||
const filter3 = createFilter(
|
||||
ViewFilterOperand.GreaterThanOrEqual,
|
||||
20,
|
||||
25,
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter3] })).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle LessThanOrEqual operand correctly', () => {
|
||||
const filter1 = createFilter(ViewFilterOperand.LessThanOrEqual, 25, 25);
|
||||
const filter2 = createFilter(ViewFilterOperand.LessThanOrEqual, 20, 25);
|
||||
const filter3 = createFilter(ViewFilterOperand.LessThanOrEqual, 30, 25);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter3] })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('string and array operands', () => {
|
||||
it('should handle Contains operand with strings', () => {
|
||||
const filter1 = createFilter(
|
||||
ViewFilterOperand.Contains,
|
||||
'Hello World',
|
||||
'World',
|
||||
);
|
||||
const filter2 = createFilter(
|
||||
ViewFilterOperand.Contains,
|
||||
'Hello',
|
||||
'World',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle DoesNotContain operand with strings', () => {
|
||||
const filter1 = createFilter(
|
||||
ViewFilterOperand.DoesNotContain,
|
||||
'Hello World',
|
||||
'World',
|
||||
);
|
||||
const filter2 = createFilter(
|
||||
ViewFilterOperand.DoesNotContain,
|
||||
'Hello',
|
||||
'World',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(false);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle Contains operand with arrays', () => {
|
||||
const filter1 = createFilter(
|
||||
ViewFilterOperand.Contains,
|
||||
['apple', 'banana', 'cherry'],
|
||||
'apple',
|
||||
);
|
||||
const filter2 = createFilter(
|
||||
ViewFilterOperand.Contains,
|
||||
['apple', 'banana', 'cherry'],
|
||||
'grape',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle DoesNotContain operand with arrays', () => {
|
||||
const filter1 = createFilter(
|
||||
ViewFilterOperand.DoesNotContain,
|
||||
['apple', 'banana', 'cherry'],
|
||||
'apple',
|
||||
);
|
||||
const filter2 = createFilter(
|
||||
ViewFilterOperand.DoesNotContain,
|
||||
['apple', 'banana', 'cherry'],
|
||||
'grape',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(false);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('empty operands', () => {
|
||||
it('should handle IsEmpty operand correctly', () => {
|
||||
const filter1 = createFilter(ViewFilterOperand.IsEmpty, null, '');
|
||||
const filter2 = createFilter(ViewFilterOperand.IsEmpty, undefined, '');
|
||||
const filter3 = createFilter(ViewFilterOperand.IsEmpty, '', '');
|
||||
const filter4 = createFilter(ViewFilterOperand.IsEmpty, [], '');
|
||||
const filter5 = createFilter(
|
||||
ViewFilterOperand.IsEmpty,
|
||||
'not empty',
|
||||
'',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter3] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter4] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter5] })).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle IsNotEmpty operand correctly', () => {
|
||||
const filter1 = createFilter(
|
||||
ViewFilterOperand.IsNotEmpty,
|
||||
'not empty',
|
||||
'',
|
||||
);
|
||||
const filter2 = createFilter(
|
||||
ViewFilterOperand.IsNotEmpty,
|
||||
['item'],
|
||||
'',
|
||||
);
|
||||
const filter3 = createFilter(ViewFilterOperand.IsNotEmpty, null, '');
|
||||
const filter4 = createFilter(ViewFilterOperand.IsNotEmpty, '', '');
|
||||
const filter5 = createFilter(ViewFilterOperand.IsNotEmpty, [], '');
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter3] })).toBe(false);
|
||||
expect(evaluateFilterConditions({ filters: [filter4] })).toBe(false);
|
||||
expect(evaluateFilterConditions({ filters: [filter5] })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('date operands', () => {
|
||||
it('should handle date operands (returning false as placeholder)', () => {
|
||||
const dateOperands = [
|
||||
ViewFilterOperand.IsRelative,
|
||||
ViewFilterOperand.IsInPast,
|
||||
ViewFilterOperand.IsInFuture,
|
||||
ViewFilterOperand.IsToday,
|
||||
ViewFilterOperand.IsBefore,
|
||||
ViewFilterOperand.IsAfter,
|
||||
];
|
||||
|
||||
dateOperands.forEach((operand) => {
|
||||
const filter = createFilter(operand, new Date(), new Date());
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter] })).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('error cases', () => {
|
||||
it('should throw error for unknown operand', () => {
|
||||
const filter = createFilter('unknown' as StepOperand, 'value', 'value');
|
||||
|
||||
expect(() => evaluateFilterConditions({ filters: [filter] })).toThrow(
|
||||
'Unknown operand: unknown',
|
||||
const filter = createFilter(
|
||||
'unknown' as ViewFilterOperand,
|
||||
'value',
|
||||
'value',
|
||||
);
|
||||
|
||||
expect(() => evaluateFilterConditions({ filters: [filter] })).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple filters without groups', () => {
|
||||
type ResolvedFilter = Omit<StepFilter, 'value' | 'stepOutputKey'> & {
|
||||
rightOperand: unknown;
|
||||
leftOperand: unknown;
|
||||
};
|
||||
|
||||
it('should apply AND logic by default for multiple filters', () => {
|
||||
const filters: ResolvedFilter[] = [
|
||||
{
|
||||
@ -261,7 +293,7 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'text',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: StepOperand.EQ,
|
||||
operand: ViewFilterOperand.Is,
|
||||
displayValue: 'John',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 'John',
|
||||
@ -271,7 +303,7 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'number',
|
||||
label: 'Age Filter',
|
||||
rightOperand: 25,
|
||||
operand: StepOperand.GT,
|
||||
operand: ViewFilterOperand.GreaterThanOrEqual,
|
||||
displayValue: '25',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 30,
|
||||
@ -290,7 +322,7 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'text',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: StepOperand.EQ,
|
||||
operand: ViewFilterOperand.Is,
|
||||
displayValue: 'John',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 'John',
|
||||
@ -300,7 +332,7 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'number',
|
||||
label: 'Age Filter',
|
||||
rightOperand: 25,
|
||||
operand: StepOperand.GT,
|
||||
operand: ViewFilterOperand.GreaterThanOrEqual,
|
||||
displayValue: '25',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 20, // This will fail
|
||||
@ -314,11 +346,6 @@ describe('evaluateFilterConditions', () => {
|
||||
});
|
||||
|
||||
describe('filter groups', () => {
|
||||
type ResolvedFilter = Omit<StepFilter, 'value' | 'stepOutputKey'> & {
|
||||
rightOperand: unknown;
|
||||
leftOperand: unknown;
|
||||
};
|
||||
|
||||
describe('single group with AND logic', () => {
|
||||
it('should return true when all filters pass', () => {
|
||||
const filterGroups: StepFilterGroup[] = [
|
||||
@ -334,7 +361,7 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'text',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: StepOperand.EQ,
|
||||
operand: ViewFilterOperand.Is,
|
||||
displayValue: 'John',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 'John',
|
||||
@ -344,7 +371,7 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'number',
|
||||
label: 'Age Filter',
|
||||
rightOperand: 25,
|
||||
operand: StepOperand.GT,
|
||||
operand: ViewFilterOperand.GreaterThanOrEqual,
|
||||
displayValue: '25',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 30,
|
||||
@ -370,7 +397,7 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'text',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: StepOperand.EQ,
|
||||
operand: ViewFilterOperand.Is,
|
||||
displayValue: 'John',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 'Jane', // This will fail
|
||||
@ -380,7 +407,7 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'number',
|
||||
label: 'Age Filter',
|
||||
rightOperand: 25,
|
||||
operand: StepOperand.GT,
|
||||
operand: ViewFilterOperand.GreaterThanOrEqual,
|
||||
displayValue: '25',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 30,
|
||||
@ -408,7 +435,7 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'text',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: StepOperand.EQ,
|
||||
operand: ViewFilterOperand.Is,
|
||||
displayValue: 'John',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 'Jane', // This will fail
|
||||
@ -418,7 +445,7 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'number',
|
||||
label: 'Age Filter',
|
||||
rightOperand: 25,
|
||||
operand: StepOperand.GT,
|
||||
operand: ViewFilterOperand.GreaterThanOrEqual,
|
||||
displayValue: '25',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 30, // This will pass
|
||||
@ -444,7 +471,7 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'text',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: StepOperand.EQ,
|
||||
operand: ViewFilterOperand.Is,
|
||||
displayValue: 'John',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 'Jane', // This will fail
|
||||
@ -454,7 +481,7 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'number',
|
||||
label: 'Age Filter',
|
||||
rightOperand: 25,
|
||||
operand: StepOperand.GT,
|
||||
operand: ViewFilterOperand.GreaterThanOrEqual,
|
||||
displayValue: '25',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 20, // This will fail
|
||||
@ -467,8 +494,110 @@ describe('evaluateFilterConditions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple groups', () => {
|
||||
it('should handle multiple root groups with AND logic between them', () => {
|
||||
const filterGroups: StepFilterGroup[] = [
|
||||
{
|
||||
id: 'group1',
|
||||
logicalOperator: StepLogicalOperator.AND,
|
||||
},
|
||||
{
|
||||
id: 'group2',
|
||||
logicalOperator: StepLogicalOperator.OR,
|
||||
},
|
||||
];
|
||||
|
||||
const filters: ResolvedFilter[] = [
|
||||
{
|
||||
id: 'filter1',
|
||||
type: 'text',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: ViewFilterOperand.Is,
|
||||
displayValue: 'John',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 'John',
|
||||
},
|
||||
{
|
||||
id: 'filter2',
|
||||
type: 'number',
|
||||
label: 'Age Filter',
|
||||
rightOperand: 25,
|
||||
operand: ViewFilterOperand.GreaterThanOrEqual,
|
||||
displayValue: '25',
|
||||
stepFilterGroupId: 'group2',
|
||||
leftOperand: 30,
|
||||
},
|
||||
];
|
||||
|
||||
const result = evaluateFilterConditions({ filterGroups, filters });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('nested groups', () => {
|
||||
it('should handle nested filter groups correctly', () => {
|
||||
const filterGroups: StepFilterGroup[] = [
|
||||
{
|
||||
id: 'root',
|
||||
logicalOperator: StepLogicalOperator.AND,
|
||||
},
|
||||
{
|
||||
id: 'child1',
|
||||
logicalOperator: StepLogicalOperator.OR,
|
||||
parentStepFilterGroupId: 'root',
|
||||
positionInStepFilterGroup: 1,
|
||||
},
|
||||
{
|
||||
id: 'child2',
|
||||
logicalOperator: StepLogicalOperator.AND,
|
||||
parentStepFilterGroupId: 'root',
|
||||
positionInStepFilterGroup: 2,
|
||||
},
|
||||
];
|
||||
|
||||
const filters: ResolvedFilter[] = [
|
||||
{
|
||||
id: 'filter1',
|
||||
type: 'text',
|
||||
label: 'Filter 1',
|
||||
rightOperand: 'John',
|
||||
operand: ViewFilterOperand.Is,
|
||||
displayValue: 'John',
|
||||
stepFilterGroupId: 'child1',
|
||||
leftOperand: 'Jane', // This will fail
|
||||
},
|
||||
{
|
||||
id: 'filter2',
|
||||
type: 'text',
|
||||
label: 'Filter 2',
|
||||
rightOperand: 'Smith',
|
||||
operand: ViewFilterOperand.Is,
|
||||
displayValue: 'Smith',
|
||||
stepFilterGroupId: 'child1',
|
||||
leftOperand: 'Smith', // This will pass (OR group passes)
|
||||
},
|
||||
{
|
||||
id: 'filter3',
|
||||
type: 'number',
|
||||
label: 'Filter 3',
|
||||
rightOperand: 25,
|
||||
operand: ViewFilterOperand.GreaterThanOrEqual,
|
||||
displayValue: '25',
|
||||
stepFilterGroupId: 'child2',
|
||||
leftOperand: 30, // This will pass (AND group passes)
|
||||
},
|
||||
];
|
||||
|
||||
const result = evaluateFilterConditions({ filterGroups, filters });
|
||||
|
||||
expect(result).toBe(true); // child1 (OR) passes, child2 (AND) passes, root (AND) passes
|
||||
});
|
||||
});
|
||||
|
||||
describe('empty groups', () => {
|
||||
it('should return true for empty group', () => {
|
||||
it('should return true for empty filter groups', () => {
|
||||
const filterGroups: StepFilterGroup[] = [
|
||||
{
|
||||
id: 'group1',
|
||||
@ -482,254 +611,8 @@ describe('evaluateFilterConditions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('nested groups', () => {
|
||||
it('should handle nested groups correctly', () => {
|
||||
const filterGroups: StepFilterGroup[] = [
|
||||
{
|
||||
id: 'group1',
|
||||
logicalOperator: StepLogicalOperator.AND,
|
||||
},
|
||||
{
|
||||
id: 'group2',
|
||||
logicalOperator: StepLogicalOperator.OR,
|
||||
parentStepFilterGroupId: 'group1',
|
||||
positionInStepFilterGroup: 1,
|
||||
},
|
||||
];
|
||||
|
||||
const filters: ResolvedFilter[] = [
|
||||
// Filter in parent group
|
||||
{
|
||||
id: 'filter1',
|
||||
type: 'text',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: StepOperand.EQ,
|
||||
displayValue: 'John',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 'John', // This will pass
|
||||
},
|
||||
// Filters in child group with OR logic
|
||||
{
|
||||
id: 'filter2',
|
||||
type: 'number',
|
||||
label: 'Age Filter',
|
||||
rightOperand: 25,
|
||||
operand: StepOperand.GT,
|
||||
displayValue: '25',
|
||||
stepFilterGroupId: 'group2',
|
||||
leftOperand: 20, // This will fail
|
||||
},
|
||||
{
|
||||
id: 'filter3',
|
||||
type: 'text',
|
||||
label: 'City Filter',
|
||||
rightOperand: 'NYC',
|
||||
operand: StepOperand.EQ,
|
||||
displayValue: 'NYC',
|
||||
stepFilterGroupId: 'group2',
|
||||
leftOperand: 'NYC', // This will pass
|
||||
},
|
||||
];
|
||||
|
||||
// Parent group uses AND: filter1 (pass) AND group2 (pass because filter3 passes)
|
||||
const result = evaluateFilterConditions({ filterGroups, filters });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle multiple child groups with correct positioning', () => {
|
||||
const filterGroups: StepFilterGroup[] = [
|
||||
{
|
||||
id: 'group1',
|
||||
logicalOperator: StepLogicalOperator.AND,
|
||||
},
|
||||
{
|
||||
id: 'group2',
|
||||
logicalOperator: StepLogicalOperator.OR,
|
||||
parentStepFilterGroupId: 'group1',
|
||||
positionInStepFilterGroup: 1,
|
||||
},
|
||||
{
|
||||
id: 'group3',
|
||||
logicalOperator: StepLogicalOperator.AND,
|
||||
parentStepFilterGroupId: 'group1',
|
||||
positionInStepFilterGroup: 2,
|
||||
},
|
||||
];
|
||||
|
||||
const filters: ResolvedFilter[] = [
|
||||
// Group2 filters (OR logic)
|
||||
{
|
||||
id: 'filter1',
|
||||
type: 'text',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: StepOperand.EQ,
|
||||
displayValue: 'John',
|
||||
stepFilterGroupId: 'group2',
|
||||
leftOperand: 'Jane', // This will fail
|
||||
},
|
||||
{
|
||||
id: 'filter2',
|
||||
type: 'text',
|
||||
label: 'Status Filter',
|
||||
rightOperand: 'active',
|
||||
operand: StepOperand.EQ,
|
||||
displayValue: 'active',
|
||||
stepFilterGroupId: 'group2',
|
||||
leftOperand: 'active', // This will pass
|
||||
},
|
||||
// Group3 filters (AND logic)
|
||||
{
|
||||
id: 'filter3',
|
||||
type: 'number',
|
||||
label: 'Age Filter',
|
||||
rightOperand: 18,
|
||||
operand: StepOperand.GTE,
|
||||
displayValue: '18',
|
||||
stepFilterGroupId: 'group3',
|
||||
leftOperand: 25, // This will pass
|
||||
},
|
||||
{
|
||||
id: 'filter4',
|
||||
type: 'number',
|
||||
label: 'Score Filter',
|
||||
rightOperand: 80,
|
||||
operand: StepOperand.GT,
|
||||
displayValue: '80',
|
||||
stepFilterGroupId: 'group3',
|
||||
leftOperand: 85, // This will pass
|
||||
},
|
||||
];
|
||||
|
||||
// Group1 uses AND: group2 (pass) AND group3 (pass)
|
||||
const result = evaluateFilterConditions({ filterGroups, filters });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple root groups', () => {
|
||||
it('should combine multiple root groups with AND logic', () => {
|
||||
const filterGroups: StepFilterGroup[] = [
|
||||
{
|
||||
id: 'group1',
|
||||
logicalOperator: StepLogicalOperator.OR,
|
||||
},
|
||||
{
|
||||
id: 'group2',
|
||||
logicalOperator: StepLogicalOperator.AND,
|
||||
},
|
||||
];
|
||||
|
||||
const filters: ResolvedFilter[] = [
|
||||
// Group1 filters (OR logic)
|
||||
{
|
||||
id: 'filter1',
|
||||
type: 'text',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: StepOperand.EQ,
|
||||
displayValue: 'John',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 'John', // This will pass
|
||||
},
|
||||
{
|
||||
id: 'filter2',
|
||||
type: 'text',
|
||||
label: 'Status Filter',
|
||||
rightOperand: 'inactive',
|
||||
operand: StepOperand.EQ,
|
||||
displayValue: 'inactive',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 'active', // This will fail
|
||||
},
|
||||
// Group2 filters (AND logic)
|
||||
{
|
||||
id: 'filter3',
|
||||
type: 'number',
|
||||
label: 'Age Filter',
|
||||
rightOperand: 18,
|
||||
operand: StepOperand.GTE,
|
||||
displayValue: '18',
|
||||
stepFilterGroupId: 'group2',
|
||||
leftOperand: 25, // This will pass
|
||||
},
|
||||
{
|
||||
id: 'filter4',
|
||||
type: 'number',
|
||||
label: 'Score Filter',
|
||||
rightOperand: 80,
|
||||
operand: StepOperand.GT,
|
||||
displayValue: '80',
|
||||
stepFilterGroupId: 'group2',
|
||||
leftOperand: 85, // This will pass
|
||||
},
|
||||
];
|
||||
|
||||
// Root groups combined with AND: group1 (pass) AND group2 (pass)
|
||||
const result = evaluateFilterConditions({ filterGroups, filters });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when one root group fails', () => {
|
||||
const filterGroups: StepFilterGroup[] = [
|
||||
{
|
||||
id: 'group1',
|
||||
logicalOperator: StepLogicalOperator.OR,
|
||||
},
|
||||
{
|
||||
id: 'group2',
|
||||
logicalOperator: StepLogicalOperator.AND,
|
||||
},
|
||||
];
|
||||
|
||||
const filters: ResolvedFilter[] = [
|
||||
// Group1 filters (OR logic) - will pass
|
||||
{
|
||||
id: 'filter1',
|
||||
type: 'text',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: StepOperand.EQ,
|
||||
displayValue: 'John',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 'John', // This will pass
|
||||
},
|
||||
// Group2 filters (AND logic) - will fail
|
||||
{
|
||||
id: 'filter2',
|
||||
type: 'number',
|
||||
label: 'Age Filter',
|
||||
rightOperand: 30,
|
||||
operand: StepOperand.GT,
|
||||
displayValue: '30',
|
||||
stepFilterGroupId: 'group2',
|
||||
leftOperand: 25, // This will fail
|
||||
},
|
||||
{
|
||||
id: 'filter3',
|
||||
type: 'number',
|
||||
label: 'Score Filter',
|
||||
rightOperand: 80,
|
||||
operand: StepOperand.GT,
|
||||
displayValue: '80',
|
||||
stepFilterGroupId: 'group2',
|
||||
leftOperand: 85, // This will pass
|
||||
},
|
||||
];
|
||||
|
||||
// Root groups combined with AND: group1 (pass) AND group2 (fail)
|
||||
const result = evaluateFilterConditions({ filterGroups, filters });
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error cases', () => {
|
||||
it('should throw error when filter group is not found', () => {
|
||||
it('should throw error when filter references non-existent group', () => {
|
||||
const filterGroups: StepFilterGroup[] = [
|
||||
{
|
||||
id: 'group1',
|
||||
@ -743,42 +626,16 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'text',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: StepOperand.EQ,
|
||||
operand: ViewFilterOperand.Is,
|
||||
displayValue: 'John',
|
||||
stepFilterGroupId: 'nonexistent-group',
|
||||
stepFilterGroupId: 'nonexistent',
|
||||
leftOperand: 'John',
|
||||
},
|
||||
];
|
||||
|
||||
expect(() =>
|
||||
evaluateFilterConditions({ filterGroups, filters }),
|
||||
).toThrow('Filter group with id nonexistent-group not found');
|
||||
});
|
||||
|
||||
it('should throw error for unknown logical operator', () => {
|
||||
const filterGroups: StepFilterGroup[] = [
|
||||
{
|
||||
id: 'group1',
|
||||
logicalOperator: 'UNKNOWN' as StepLogicalOperator,
|
||||
},
|
||||
];
|
||||
|
||||
const filters: ResolvedFilter[] = [
|
||||
{
|
||||
id: 'filter1',
|
||||
type: 'text',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: StepOperand.EQ,
|
||||
displayValue: 'John',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 'John',
|
||||
},
|
||||
];
|
||||
|
||||
expect(() =>
|
||||
evaluateFilterConditions({ filterGroups, filters }),
|
||||
).toThrow('Unknown logical operator: UNKNOWN');
|
||||
).toThrow('Filter group with id nonexistent not found');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,4 +1,9 @@
|
||||
import { StepFilter, StepFilterGroup } from 'twenty-shared/types';
|
||||
import {
|
||||
StepFilter,
|
||||
StepFilterGroup,
|
||||
ViewFilterOperand,
|
||||
} from 'twenty-shared/types';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
type ResolvedFilter = Omit<StepFilter, 'value' | 'stepOutputKey'> & {
|
||||
rightOperand: unknown;
|
||||
@ -10,46 +15,7 @@ function evaluateFilter(filter: ResolvedFilter): boolean {
|
||||
const rightValue = filter.rightOperand;
|
||||
|
||||
switch (filter.operand) {
|
||||
case 'eq':
|
||||
return leftValue == rightValue;
|
||||
|
||||
case 'ne':
|
||||
return leftValue != rightValue;
|
||||
|
||||
case 'gt':
|
||||
return Number(leftValue) > Number(rightValue);
|
||||
|
||||
case 'gte':
|
||||
return Number(leftValue) >= Number(rightValue);
|
||||
|
||||
case 'lt':
|
||||
return Number(leftValue) < Number(rightValue);
|
||||
|
||||
case 'lte':
|
||||
return Number(leftValue) <= Number(rightValue);
|
||||
|
||||
case 'like':
|
||||
return String(leftValue).includes(String(rightValue));
|
||||
|
||||
case 'ilike':
|
||||
return String(leftValue)
|
||||
.toLowerCase()
|
||||
.includes(String(rightValue).toLowerCase());
|
||||
|
||||
case 'in':
|
||||
try {
|
||||
const values = JSON.parse(String(rightValue));
|
||||
|
||||
return Array.isArray(values) && values.includes(leftValue);
|
||||
} catch {
|
||||
const values = String(rightValue)
|
||||
.split(',')
|
||||
.map((v) => v.trim());
|
||||
|
||||
return values.includes(String(leftValue));
|
||||
}
|
||||
|
||||
case 'is':
|
||||
case ViewFilterOperand.Is:
|
||||
if (String(rightValue).toLowerCase() === 'null') {
|
||||
return leftValue === null || leftValue === undefined;
|
||||
}
|
||||
@ -57,16 +23,68 @@ function evaluateFilter(filter: ResolvedFilter): boolean {
|
||||
return leftValue !== null && leftValue !== undefined;
|
||||
}
|
||||
|
||||
return leftValue === rightValue;
|
||||
return leftValue == rightValue;
|
||||
|
||||
case ViewFilterOperand.IsNot:
|
||||
return leftValue != rightValue;
|
||||
|
||||
case ViewFilterOperand.GreaterThanOrEqual:
|
||||
return Number(leftValue) >= Number(rightValue);
|
||||
|
||||
case ViewFilterOperand.LessThanOrEqual:
|
||||
return Number(leftValue) <= Number(rightValue);
|
||||
|
||||
case ViewFilterOperand.Contains:
|
||||
if (Array.isArray(leftValue)) {
|
||||
return leftValue.includes(rightValue);
|
||||
}
|
||||
|
||||
return String(leftValue).includes(String(rightValue));
|
||||
|
||||
case ViewFilterOperand.DoesNotContain:
|
||||
if (Array.isArray(leftValue)) {
|
||||
return !leftValue.includes(rightValue);
|
||||
}
|
||||
|
||||
return !String(leftValue).includes(String(rightValue));
|
||||
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
return (
|
||||
leftValue === null ||
|
||||
leftValue === undefined ||
|
||||
leftValue === '' ||
|
||||
(Array.isArray(leftValue) && leftValue.length === 0)
|
||||
);
|
||||
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
return (
|
||||
leftValue !== null &&
|
||||
leftValue !== undefined &&
|
||||
leftValue !== '' &&
|
||||
(!Array.isArray(leftValue) || leftValue.length > 0)
|
||||
);
|
||||
|
||||
case ViewFilterOperand.IsNotNull:
|
||||
return leftValue !== null && leftValue !== undefined;
|
||||
|
||||
case ViewFilterOperand.IsRelative:
|
||||
case ViewFilterOperand.IsInPast:
|
||||
case ViewFilterOperand.IsInFuture:
|
||||
case ViewFilterOperand.IsToday:
|
||||
case ViewFilterOperand.IsBefore:
|
||||
case ViewFilterOperand.IsAfter:
|
||||
// Date/time operands - for now, return false as placeholder
|
||||
// These would need proper date logic implementation
|
||||
return false;
|
||||
|
||||
case ViewFilterOperand.VectorSearch:
|
||||
return false;
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown operand: ${filter.operand}`);
|
||||
assertUnreachable(filter.operand);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively evaluates a filter group and its children
|
||||
*/
|
||||
function evaluateFilterGroup(
|
||||
groupId: string,
|
||||
filterGroups: StepFilterGroup[],
|
||||
@ -78,7 +96,6 @@ function evaluateFilterGroup(
|
||||
throw new Error(`Filter group with id ${groupId} not found`);
|
||||
}
|
||||
|
||||
// Get all direct child groups
|
||||
const childGroups = filterGroups
|
||||
.filter((g) => g.parentStepFilterGroupId === groupId)
|
||||
.sort(
|
||||
|
||||
@ -1,21 +1,10 @@
|
||||
import { ViewFilterOperand } from './ViewFilterOperand';
|
||||
|
||||
export enum StepLogicalOperator {
|
||||
AND = 'AND',
|
||||
OR = 'OR',
|
||||
}
|
||||
|
||||
export enum StepOperand {
|
||||
EQ = 'eq',
|
||||
NE = 'ne',
|
||||
GT = 'gt',
|
||||
GTE = 'gte',
|
||||
LT = 'lt',
|
||||
LTE = 'lte',
|
||||
LIKE = 'like',
|
||||
ILIKE = 'ilike',
|
||||
IN = 'in',
|
||||
IS = 'is',
|
||||
}
|
||||
|
||||
export type StepFilterGroup = {
|
||||
id: string;
|
||||
logicalOperator: StepLogicalOperator;
|
||||
@ -28,7 +17,7 @@ export type StepFilter = {
|
||||
type: string;
|
||||
label: string;
|
||||
value: string;
|
||||
operand: StepOperand;
|
||||
operand: ViewFilterOperand;
|
||||
displayValue: string;
|
||||
stepFilterGroupId: string;
|
||||
stepOutputKey: string;
|
||||
|
||||
@ -14,4 +14,5 @@ export type { IsExactly } from './IsExactly';
|
||||
export type { ObjectRecordsPermissions } from './ObjectRecordsPermissions';
|
||||
export type { ObjectRecordsPermissionsByRoleId } from './ObjectRecordsPermissionsByRoleId';
|
||||
export type { StepFilterGroup, StepFilter } from './StepFilters';
|
||||
export { StepLogicalOperator, StepOperand } from './StepFilters';
|
||||
export { StepLogicalOperator } from './StepFilters';
|
||||
export { ViewFilterOperand } from './ViewFilterOperand';
|
||||
|
||||
Reference in New Issue
Block a user