diff --git a/packages/twenty-front/src/modules/context-store/utils/__tests__/computeContextStoreFilters.test.ts b/packages/twenty-front/src/modules/context-store/utils/__tests__/computeContextStoreFilters.test.ts index ec8f07171..1321cd044 100644 --- a/packages/twenty-front/src/modules/context-store/utils/__tests__/computeContextStoreFilters.test.ts +++ b/packages/twenty-front/src/modules/context-store/utils/__tests__/computeContextStoreFilters.test.ts @@ -73,9 +73,22 @@ describe('computeContextStoreFilters', () => { expect(filters).toEqual({ and: [ { - name: { - ilike: '%John%', - }, + or: [ + { + name: { + firstName: { + ilike: '%John%', + }, + }, + }, + { + name: { + lastName: { + ilike: '%John%', + }, + }, + }, + ], }, { not: { diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromFieldMetadata.test.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromFieldMetadata.test.ts index 90e76e6d4..65b3de83d 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromFieldMetadata.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromFieldMetadata.test.ts @@ -9,11 +9,13 @@ import { SubscriptionStatus, WorkspaceActivationStatus, } from '~/generated/graphql'; -import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; +import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; -const Wrapper = getJestMetadataAndApolloMocksWrapper({ +const Wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({ apolloMocks: [], + componentInstanceId: 'instanceId', + contextStoreCurrentObjectMetadataNameSingular: 'company', onInitializeRecoilSnapshot: ({ set }) => { set(currentWorkspaceState, { id: '1', diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata.ts index 09bf1aaf6..07fc72adc 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata.ts @@ -6,10 +6,8 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata' import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { FeatureFlagKey } from '~/generated/graphql'; +import { useFilterableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext'; import { formatFieldMetadataItemAsColumnDefinition } from '../utils/formatFieldMetadataItemAsColumnDefinition'; -import { formatFieldMetadataItemsAsFilterDefinitions } from '../utils/formatFieldMetadataItemsAsFilterDefinitions'; import { formatFieldMetadataItemsAsSortDefinitions } from '../utils/formatFieldMetadataItemsAsSortDefinitions'; export const useColumnDefinitionsFromFieldMetadata = ( @@ -25,14 +23,8 @@ export const useColumnDefinitionsFromFieldMetadata = ( [objectMetadataItem], ); - const isJsonFilterEnabled = useIsFeatureEnabled( - FeatureFlagKey.IsJsonFilterEnabled, - ); - - const filterDefinitions = formatFieldMetadataItemsAsFilterDefinitions({ - fields: activeFieldMetadataItems, - isJsonFilterEnabled, - }); + const { filterableFieldMetadataItems } = + useFilterableFieldMetadataItemsInRecordIndexContext(); const sortDefinitions = formatFieldMetadataItemsAsSortDefinitions({ fields: activeFieldMetadataItems, @@ -51,9 +43,11 @@ export const useColumnDefinitionsFromFieldMetadata = ( ) .filter(filterAvailableTableColumns) .map((column) => { - const existsInFilterDefinitions = filterDefinitions.some( - (filter) => filter.fieldMetadataId === column.fieldMetadataId, - ); + const existsInFilterDefinitions = + filterableFieldMetadataItems.some( + (fieldMetadataItem) => + fieldMetadataItem.id === column.fieldMetadataId, + ); const existsInSortDefinitions = sortDefinitions.some( (sort) => sort.fieldMetadataId === column.fieldMetadataId, @@ -67,16 +61,15 @@ export const useColumnDefinitionsFromFieldMetadata = ( }) : [], [ + filterableFieldMetadataItems, activeFieldMetadataItems, objectMetadataItem, - filterDefinitions, sortDefinitions, ], ); return { columnDefinitions, - filterDefinitions, sortDefinitions, }; }; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterViewFilterOperandSelect.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterViewFilterOperandSelect.tsx index 8daba133c..2c149f68b 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterViewFilterOperandSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterViewFilterOperandSelect.tsx @@ -1,7 +1,9 @@ +import { useGetFieldMetadataItemById } from '@/object-metadata/hooks/useGetFieldMetadataItemById'; +import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; 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 { getRecordFilterOperandsForRecordFilterDefinition } from '@/object-record/record-filter/utils/getRecordFilterOperandsForRecordFilterDefinition'; +import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands'; import { SelectControl } from '@/ui/input/components/SelectControl'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; @@ -28,6 +30,8 @@ export const AdvancedFilterViewFilterOperandSelect = ({ const filter = useCurrentViewFilter({ viewFilterId }); + const { getFieldMetadataItemById } = useGetFieldMetadataItemById(); + const isDisabled = !filter?.fieldMetadataId; const { closeDropdown } = useDropdown(dropdownId); @@ -41,8 +45,16 @@ export const AdvancedFilterViewFilterOperandSelect = ({ throw new Error('Filter is not defined'); } + const fieldMetadataItem = getFieldMetadataItemById(filter.fieldMetadataId); + + if (!isDefined(fieldMetadataItem)) { + throw new Error('Field metadata item is not defined'); + } + + const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type); + const { value, displayValue } = getInitialFilterValue( - filter.definition.type, + filterType, operand, filter.value, filter.displayValue, @@ -56,8 +68,12 @@ export const AdvancedFilterViewFilterOperandSelect = ({ }); }; - const operandsForFilterType = isDefined(filter?.definition) - ? getRecordFilterOperandsForRecordFilterDefinition(filter.definition) + const filterType = filter?.type; + + const operandsForFilterType = isDefined(filterType) + ? getRecordFilterOperands({ + filterType, + }) : []; if (isDisabled === true) { diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/AdvancedFilterButton.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/AdvancedFilterButton.tsx index 8ea9ce944..816706fac 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/AdvancedFilterButton.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/AdvancedFilterButton.tsx @@ -1,9 +1,13 @@ import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; import { availableFieldMetadataItemsForFilterFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector'; -import { formatFieldMetadataItemAsFilterDefinition } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; +import { + formatFieldMetadataItemAsFilterDefinition, + getFilterTypeFromFieldType, +} from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; import { useUpsertCombinedViewFilterGroup } from '@/object-record/advanced-filter/hooks/useUpsertCombinedViewFilterGroup'; import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId'; -import { getRecordFilterOperandsForRecordFilterDefinition } from '@/object-record/record-filter/utils/getRecordFilterOperandsForRecordFilterDefinition'; +import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands'; + import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { ADVANCED_FILTER_DROPDOWN_ID } from '@/views/constants/AdvancedFilterDropdownId'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; @@ -110,11 +114,18 @@ export const AdvancedFilterButton = () => { field: defaultFieldMetadataItem, }); + const filterType = getFilterTypeFromFieldType( + defaultFieldMetadataItem.type, + ); + + const firstOperand = getRecordFilterOperands({ + filterType, + })[0]; + upsertCombinedViewFilter({ id: v4(), fieldMetadataId: defaultFieldMetadataItem.id, - operand: - getRecordFilterOperandsForRecordFilterDefinition(filterDefinition)[0], + operand: firstOperand, definition: filterDefinition, value: '', displayValue: '', diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx index 08c3d8b06..67b613264 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx @@ -1,10 +1,8 @@ import { ObjectFilterDropdownFilterSelectCompositeFieldSubMenu } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu'; import { ObjectFilterOperandSelectAndInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterOperandSelectAndInput'; -import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState'; import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState'; import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState'; import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { MultipleFiltersDropdownFilterOnFilterChangedEffect } from './MultipleFiltersDropdownFilterOnFilterChangedEffect'; import { ObjectFilterDropdownFilterSelect } from './ObjectFilterDropdownFilterSelect'; @@ -15,11 +13,6 @@ type MultipleFiltersDropdownContentProps = { export const MultipleFiltersDropdownContent = ({ filterDropdownId, }: MultipleFiltersDropdownContentProps) => { - const filterDefinitionUsedInDropdown = useRecoilComponentValueV2( - filterDefinitionUsedInDropdownComponentState, - filterDropdownId, - ); - const [objectFilterDropdownIsSelectingCompositeField] = useRecoilComponentStateV2( objectFilterDropdownIsSelectingCompositeFieldComponentState, @@ -47,11 +40,7 @@ export const MultipleFiltersDropdownContent = ({ ) : ( )} - + ); }; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownFilterOnFilterChangedEffect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownFilterOnFilterChangedEffect.tsx index 5b8d12162..06b434804 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownFilterOnFilterChangedEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownFilterOnFilterChangedEffect.tsx @@ -1,16 +1,28 @@ import { useEffect } from 'react'; +import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; +import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { isDefined } from 'twenty-shared'; -export const MultipleFiltersDropdownFilterOnFilterChangedEffect = ({ - filterDefinitionUsedInDropdownType, -}: { - filterDefinitionUsedInDropdownType: string | undefined; -}) => { +export const MultipleFiltersDropdownFilterOnFilterChangedEffect = () => { const { setDropdownWidth } = useDropdown(); + const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2( + fieldMetadataItemUsedInDropdownComponentSelector, + ); + useEffect(() => { - switch (filterDefinitionUsedInDropdownType) { + if (!isDefined(fieldMetadataItemUsedInDropdown)) { + return; + } + + const filterType = getFilterTypeFromFieldType( + fieldMetadataItemUsedInDropdown.type, + ); + + switch (filterType) { case 'DATE': case 'DATE_TIME': setDropdownWidth(280); @@ -18,7 +30,7 @@ export const MultipleFiltersDropdownFilterOnFilterChangedEffect = ({ default: setDropdownWidth(200); } - }, [filterDefinitionUsedInDropdownType, setDropdownWidth]); + }, [fieldMetadataItemUsedInDropdown, setDropdownWidth]); return null; }; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput.tsx index 891521ee6..3121c2dd6 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput.tsx @@ -6,17 +6,19 @@ import { ObjectFilterDropdownRecordSelect } from '@/object-record/object-filter- import { ObjectFilterDropdownSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput'; import { ObjectFilterDropdownSourceSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect'; import { ObjectFilterDropdownTextSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextSearchInput'; -import { isActorSourceCompositeFilter } from '@/object-record/object-filter-dropdown/utils/isActorSourceCompositeFilter'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { isDefined } from 'twenty-shared'; +import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; import { ObjectFilterDropdownBooleanSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownBooleanSelect'; import { DATE_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/DateFilterTypes'; import { NUMBER_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/NumberFilterTypes'; import { TEXT_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/TextFilterTypes'; -import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState'; +import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector'; import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; +import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState'; +import { isFilterOnActorSourceSubField } from '@/object-record/object-filter-dropdown/utils/isFilterOnActorSourceSubField'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; type ObjectFilterDropdownFilterInputProps = { @@ -26,10 +28,16 @@ type ObjectFilterDropdownFilterInputProps = { export const ObjectFilterDropdownFilterInput = ({ filterDropdownId, }: ObjectFilterDropdownFilterInputProps) => { - const filterDefinitionUsedInDropdown = useRecoilComponentValueV2( - filterDefinitionUsedInDropdownComponentState, + const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2( + fieldMetadataItemUsedInDropdownComponentSelector, filterDropdownId, ); + + const subFieldNameUsedInDropdown = useRecoilComponentValueV2( + subFieldNameUsedInDropdownComponentState, + filterDropdownId, + ); + const selectedOperandInDropdown = useRecoilComponentValueV2( selectedOperandInDropdownComponentState, filterDropdownId, @@ -50,52 +58,54 @@ export const ObjectFilterDropdownFilterInput = ({ ViewFilterOperand.IsRelative, ].includes(selectedOperandInDropdown); - if (!isDefined(filterDefinitionUsedInDropdown)) { + if (!isDefined(fieldMetadataItemUsedInDropdown)) { return null; } + const filterType = getFilterTypeFromFieldType( + fieldMetadataItemUsedInDropdown.type, + ); + + const isActorSourceCompositeFilter = isFilterOnActorSourceSubField( + subFieldNameUsedInDropdown, + ); + return ( <> {isConfigurable && selectedOperandInDropdown && ( <> - {TEXT_FILTER_TYPES.includes(filterDefinitionUsedInDropdown.type) && - !isActorSourceCompositeFilter(filterDefinitionUsedInDropdown) && ( + {TEXT_FILTER_TYPES.includes(filterType) && + !isActorSourceCompositeFilter && ( )} - {NUMBER_FILTER_TYPES.includes( - filterDefinitionUsedInDropdown.type, - ) && } - {filterDefinitionUsedInDropdown.type === 'RATING' && ( - + {NUMBER_FILTER_TYPES.includes(filterType) && ( + )} - {DATE_FILTER_TYPES.includes(filterDefinitionUsedInDropdown.type) && ( + {filterType === 'RATING' && } + {DATE_FILTER_TYPES.includes(filterType) && ( )} - {filterDefinitionUsedInDropdown.type === 'RELATION' && ( + {filterType === 'RELATION' && ( <> )} - {isActorSourceCompositeFilter(filterDefinitionUsedInDropdown) && ( + {isActorSourceCompositeFilter && ( <> )} - {['SELECT', 'MULTI_SELECT'].includes( - filterDefinitionUsedInDropdown.type, - ) && ( + {['SELECT', 'MULTI_SELECT'].includes(filterType) && ( <> )} - {filterDefinitionUsedInDropdown.type === 'BOOLEAN' && ( - - )} + {filterType === 'BOOLEAN' && } )} diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx index 6e4603772..9ea449a6b 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx @@ -91,16 +91,15 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({ setFilterDefinitionUsedInDropdown(filterDefinition); - if ( - filterDefinition.type === 'RELATION' || - filterDefinition.type === 'SELECT' - ) { + const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type); + + if (filterType === 'RELATION' || filterType === 'SELECT') { setHotkeyScope(RelationPickerHotkeyScope.RelationPicker); } setSelectedOperandInDropdown( getRecordFilterOperands({ - filterType: filterDefinition.type, + filterType, })[0], ); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/SingleEntityObjectFilterDropdownButtonEffect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/SingleEntityObjectFilterDropdownButtonEffect.tsx index 58c9ba456..be0c30e8d 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/SingleEntityObjectFilterDropdownButtonEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/SingleEntityObjectFilterDropdownButtonEffect.tsx @@ -1,9 +1,12 @@ -import { formatFieldMetadataItemAsFilterDefinition } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; +import { + formatFieldMetadataItemAsFilterDefinition, + getFilterTypeFromFieldType, +} from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState'; import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState'; import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; import { useFilterableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext'; -import { getRecordFilterOperandsForRecordFilterDefinition } from '@/object-record/record-filter/utils/getRecordFilterOperandsForRecordFilterDefinition'; +import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useEffect } from 'react'; @@ -30,14 +33,16 @@ export const SingleEntityObjectFilterDropdownButtonEffect = () => { }); useEffect(() => { - setFieldMetadataItemIdUsedInDropdown(firstFieldDefinition.fieldMetadataId); + setFieldMetadataItemIdUsedInDropdown(firstFieldMetadataItem.id); setFilterDefinitionUsedInDropdown(firstFieldDefinition); - const defaultOperand = - getRecordFilterOperandsForRecordFilterDefinition(firstFieldDefinition)[0]; + const filterType = getFilterTypeFromFieldType(firstFieldMetadataItem.type); + + const defaultOperand = getRecordFilterOperands({ filterType })[0]; setSelectedOperandInDropdown(defaultOperand); }, [ + firstFieldMetadataItem, firstFieldDefinition, setFilterDefinitionUsedInDropdown, setSelectedOperandInDropdown, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilterDefinitionUsedInDropdown.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilterDefinitionUsedInDropdown.ts index db3d2255f..71ba160d8 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilterDefinitionUsedInDropdown.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilterDefinitionUsedInDropdown.ts @@ -6,7 +6,8 @@ import { selectedOperandInDropdownComponentState } from '@/object-record/object- import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue'; import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter'; import { RecordFilterDefinition } from '@/object-record/record-filter/types/RecordFilterDefinition'; -import { getRecordFilterOperandsForRecordFilterDefinition } from '@/object-record/record-filter/utils/getRecordFilterOperandsForRecordFilterDefinition'; +import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands'; + import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; @@ -62,13 +63,15 @@ export const useSelectFilterDefinitionUsedInDropdown = ( setHotkeyScope(RelationPickerHotkeyScope.RelationPicker); } - setSelectedOperandInDropdown( - getRecordFilterOperandsForRecordFilterDefinition(filterDefinition)[0], - ); + const firstOperand = getRecordFilterOperands({ + filterType: filterDefinition.type, + })[0]; + + setSelectedOperandInDropdown(firstOperand); const { value, displayValue } = getInitialFilterValue( filterDefinition.type, - getRecordFilterOperandsForRecordFilterDefinition(filterDefinition)[0], + firstOperand, ); const isAdvancedFilter = isDefined(advancedFilterViewFilterId); @@ -78,8 +81,7 @@ export const useSelectFilterDefinitionUsedInDropdown = ( id: advancedFilterViewFilterId ?? v4(), fieldMetadataId: filterDefinition.fieldMetadataId, displayValue, - operand: - getRecordFilterOperandsForRecordFilterDefinition(filterDefinition)[0], + operand: firstOperand, value, definition: filterDefinition, viewFilterGroupId: advancedFilterViewFilterGroupId, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.ts index b4f8b4597..dbe5c921f 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.ts @@ -1,7 +1,6 @@ import { FilterableFieldType } from '@/object-record/record-filter/types/FilterableFieldType'; -import { RecordFilterDefinition } from '@/object-record/record-filter/types/RecordFilterDefinition'; import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand'; -import { getRecordFilterOperandsForRecordFilterDefinition } from '../../../record-filter/utils/getRecordFilterOperandsForRecordFilterDefinition'; +import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands'; describe('getOperandsForFilterType', () => { const emptyOperands = [ @@ -49,9 +48,9 @@ describe('getOperandsForFilterType', () => { testCases.forEach(([filterType, expectedOperands]) => { it(`should return correct operands for FilterType.${filterType}`, () => { - const result = getRecordFilterOperandsForRecordFilterDefinition({ - type: filterType as FilterableFieldType, - } as RecordFilterDefinition); + const result = getRecordFilterOperands({ + filterType: filterType as FilterableFieldType, + }); expect(result).toEqual(expectedOperands); }); }); diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useObjectOptionsForBoard.test.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useObjectOptionsForBoard.test.tsx index 055e62271..875b65bc7 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useObjectOptionsForBoard.test.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useObjectOptionsForBoard.test.tsx @@ -3,7 +3,8 @@ import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/s import { DropResult, ResponderProvided } from '@hello-pangea/dnd'; import { renderHook } from '@testing-library/react'; import { act } from 'react'; -import { RecoilRoot } from 'recoil'; +import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; jest.mock('@/views/hooks/useSaveCurrentViewFields', () => ({ useSaveCurrentViewFields: jest.fn(() => ({ @@ -17,53 +18,67 @@ jest.mock('@/views/hooks/useUpdateCurrentView', () => ({ })), })); -jest.mock('@/object-metadata/hooks/useObjectMetadataItem', () => ({ - useObjectMetadataItem: jest.fn(() => ({ - objectMetadataItem: { - fields: [ - { - id: 'field1', - name: 'field1', - label: 'Field 1', - isVisible: true, - position: 0, - }, - { - id: 'field2', - name: 'field2', - label: 'Field 2', - isVisible: true, - position: 1, - }, - ], - }, - })), -})); +const objectNameSingular = 'company'; describe('useObjectOptionsForBoard', () => { + const mockObjectMetadataItem = generatedMockObjectMetadataItems.find( + (objectMetadataItem) => + objectMetadataItem.nameSingular === objectNameSingular, + ); + + if (!mockObjectMetadataItem) { + throw new Error('Mock object metadata item not found'); + } + + const mockFieldMetadataItem1 = mockObjectMetadataItem.fields.find( + (field) => field.name === 'name', + ); + + if (!mockFieldMetadataItem1) { + throw new Error('Mock field metadata item not found for "name"'); + } + + const mockFieldMetadataItem2 = mockObjectMetadataItem.fields.find( + (field) => field.name === 'createdAt', + ); + + if (!mockFieldMetadataItem2) { + throw new Error('Mock field metadata item not found for "createdAt"'); + } + const initialRecoilState = [ - { fieldMetadataId: 'field1', isVisible: true, position: 0 }, - { fieldMetadataId: 'field2', isVisible: true, position: 1 }, + { + fieldMetadataId: mockFieldMetadataItem1.id, + isVisible: true, + position: 0, + }, + { + fieldMetadataId: mockFieldMetadataItem2.id, + isVisible: true, + position: 1, + }, ]; const renderWithRecoil = () => renderHook( () => useObjectOptionsForBoard({ - objectNameSingular: 'object', + objectNameSingular, recordBoardId: 'boardId', viewBarId: 'viewBarId', }), { - wrapper: ({ children }) => ( - { - set(recordIndexFieldDefinitionsState, initialRecoilState as any); - }} - > - {children} - - ), + wrapper: getJestMetadataAndApolloMocksAndActionMenuWrapper({ + apolloMocks: [], + onInitializeRecoilSnapshot: (snapshot) => { + snapshot.set( + recordIndexFieldDefinitionsState, + initialRecoilState as any, + ); + }, + componentInstanceId: 'test', + contextStoreCurrentObjectMetadataNameSingular: objectNameSingular, + }), }, ); @@ -73,7 +88,7 @@ describe('useObjectOptionsForBoard', () => { const dropResult: DropResult = { source: { droppableId: 'droppable', index: 1 }, destination: { droppableId: 'droppable', index: 2 }, - draggableId: 'field1', + draggableId: mockFieldMetadataItem1.id, type: 'TYPE', mode: 'FLUID', reason: 'DROP', @@ -90,12 +105,12 @@ describe('useObjectOptionsForBoard', () => { expect(result.current.visibleBoardFields).toEqual([ { - fieldMetadataId: 'field2', + fieldMetadataId: mockFieldMetadataItem2.id, isVisible: true, position: 0, }, { - fieldMetadataId: 'field1', + fieldMetadataId: mockFieldMetadataItem1.id, isVisible: true, position: 1, }, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/computeViewRecordGqlOperationFilter.test.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/computeViewRecordGqlOperationFilter.test.ts index e4b7b4985..18a8b2d26 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/computeViewRecordGqlOperationFilter.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/computeViewRecordGqlOperationFilter.test.ts @@ -1,4 +1,5 @@ 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 { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; @@ -33,7 +34,9 @@ describe('computeViewRecordGqlOperationFilter', () => { value: companiesMock[0].name, fieldMetadataId: companyMockNameFieldMetadataId?.id, displayValue: companiesMock[0].name, - operand: ViewFilterOperand.Contains, + operand: RecordFilterOperand.Contains, + type: 'TEXT', + label: 'Name', definition: { type: 'TEXT', fieldMetadataId: companyMockNameFieldMetadataId?.id, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/computeViewRecordGqlOperationFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/computeViewRecordGqlOperationFilter.ts index 5e2200182..bf7f219c2 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/computeViewRecordGqlOperationFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/computeViewRecordGqlOperationFilter.ts @@ -22,6 +22,7 @@ import { isDefined } from 'twenty-shared'; import { Field } from '~/generated/graphql'; import { generateILikeFiltersForCompositeFields } from '~/utils/array/generateILikeFiltersForCompositeFields'; +import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; import { convertGreaterThanRatingToArrayOfRatingValues, convertLessThanRatingToArrayOfRatingValues, @@ -40,22 +41,35 @@ import { simpleRelationFilterValueSchema } from '@/views/view-filter-value/valid import { endOfDay, roundToNearestMinutes, startOfDay } from 'date-fns'; import { z } from 'zod'; -const computeFilterRecordGqlOperationFilter = ( - filterValueDependencies: RecordFilterValueDependencies, - filter: RecordFilter, - fields: Pick[], -): RecordGqlOperationFilter | undefined => { +import { FilterableFieldType } from '@/object-record/record-filter/types/FilterableFieldType'; + +type ComputeFilterRecordGqlOperationFilterParams = { + filterValueDependencies: RecordFilterValueDependencies; + filter: RecordFilter; + fieldMetadataItems: Pick[]; +}; + +export const computeFilterRecordGqlOperationFilter = ({ + filterValueDependencies, + filter, + fieldMetadataItems: fields, +}: ComputeFilterRecordGqlOperationFilterParams): + | RecordGqlOperationFilter + | undefined => { const correspondingField = fields.find( (field) => field.id === filter.fieldMetadataId, ); - const compositeFieldName = filter.definition.compositeFieldName; + const compositeFieldName = filter.subFieldName; const isCompositeFieldFiter = isNonEmptyString(compositeFieldName); - const isEmptyOperand = [ + const isEmptinessOperand = [ RecordFilterOperand.IsEmpty, RecordFilterOperand.IsNotEmpty, + ].includes(filter.operand); + + const isDateOperandWithoutValue = [ RecordFilterOperand.IsInPast, RecordFilterOperand.IsInFuture, RecordFilterOperand.IsToday, @@ -65,13 +79,35 @@ const computeFilterRecordGqlOperationFilter = ( return; } - if (!isEmptyOperand) { - if (!isDefined(filter.value) || filter.value === '') { - return; - } + const filterType = getFilterTypeFromFieldType(correspondingField.type); + + const isFilterValueEmpty = !isDefined(filter.value) || filter.value === ''; + + const shouldSkipFiltering = + !isEmptinessOperand && !isDateOperandWithoutValue && isFilterValueEmpty; + + if (shouldSkipFiltering) { + return; } - switch (filter.definition.type) { + const filterTypesThatHaveNoEmptinessOperand: FilterableFieldType[] = [ + 'BOOLEAN', + ]; + + const filterHasEmptinessOperands = + !filterTypesThatHaveNoEmptinessOperand.includes(filterType); + + if (filterHasEmptinessOperands && isEmptinessOperand) { + const emptyOperationFilter = getEmptyRecordGqlOperationFilter({ + operand: filter.operand, + correspondingField, + recordFilter: filter, + }); + + return emptyOperationFilter; + } + + switch (filterType) { case 'TEXT': switch (filter.operand) { case RecordFilterOperand.Contains: @@ -88,16 +124,9 @@ const computeFilterRecordGqlOperationFilter = ( } as StringFilter, }, }; - case RecordFilterOperand.IsEmpty: - case RecordFilterOperand.IsNotEmpty: - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); default: throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, + `Unknown operand ${filter.operand} for ${filterType} filter`, ); } case 'RAW_JSON': @@ -116,16 +145,9 @@ const computeFilterRecordGqlOperationFilter = ( } as RawJsonFilter, }, }; - case RecordFilterOperand.IsEmpty: - case RecordFilterOperand.IsNotEmpty: - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); default: throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, + `Unknown operand ${filter.operand} for ${filterType} filter`, ); } case 'DATE': @@ -150,14 +172,6 @@ const computeFilterRecordGqlOperationFilter = ( } as DateFilter, }; } - case RecordFilterOperand.IsEmpty: - case RecordFilterOperand.IsNotEmpty: { - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); - } case RecordFilterOperand.IsRelative: { const dateRange = z .object({ start: z.date(), end: z.date() }) @@ -238,7 +252,7 @@ const computeFilterRecordGqlOperationFilter = ( } default: throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, // + `Unknown operand ${filter.operand} for ${filterType} filter`, // ); } } @@ -266,16 +280,10 @@ const computeFilterRecordGqlOperationFilter = ( ), } as RatingFilter, }; - case RecordFilterOperand.IsEmpty: - case RecordFilterOperand.IsNotEmpty: - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); + default: throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, + `Unknown operand ${filter.operand} for ${filterType} filter`, ); } case 'NUMBER': @@ -292,83 +300,61 @@ const computeFilterRecordGqlOperationFilter = ( lte: parseFloat(filter.value), } as FloatFilter, }; - case RecordFilterOperand.IsEmpty: - case RecordFilterOperand.IsNotEmpty: - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); default: throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, + `Unknown operand ${filter.operand} for ${filterType} filter`, ); } case 'RELATION': { - if (!isEmptyOperand) { - const { isCurrentWorkspaceMemberSelected, selectedRecordIds } = - jsonRelationFilterValueSchema - .catch({ - isCurrentWorkspaceMemberSelected: false, - selectedRecordIds: simpleRelationFilterValueSchema.parse( - filter.value, - ), - }) - .parse(filter.value); + const { isCurrentWorkspaceMemberSelected, selectedRecordIds } = + jsonRelationFilterValueSchema + .catch({ + isCurrentWorkspaceMemberSelected: false, + selectedRecordIds: simpleRelationFilterValueSchema.parse( + filter.value, + ), + }) + .parse(filter.value); - const recordIds = isCurrentWorkspaceMemberSelected - ? [ - ...selectedRecordIds, - filterValueDependencies.currentWorkspaceMemberId, - ] - : selectedRecordIds; + const recordIds = isCurrentWorkspaceMemberSelected + ? [ + ...selectedRecordIds, + filterValueDependencies.currentWorkspaceMemberId, + ] + : selectedRecordIds; - if (recordIds.length === 0) return; - switch (filter.operand) { - case RecordFilterOperand.Is: - return { - [correspondingField.name + 'Id']: { - in: recordIds, - } as RelationFilter, - }; - case RecordFilterOperand.IsNot: { - if (recordIds.length === 0) return; - return { - or: [ - { - not: { - [correspondingField.name + 'Id']: { - in: recordIds, - } as RelationFilter, - }, - }, - { + if (recordIds.length === 0) return; + + switch (filter.operand) { + case RecordFilterOperand.Is: + return { + [correspondingField.name + 'Id']: { + in: recordIds, + } as RelationFilter, + }; + case RecordFilterOperand.IsNot: { + if (recordIds.length === 0) return; + return { + or: [ + { + not: { [correspondingField.name + 'Id']: { - is: 'NULL', + in: recordIds, } as RelationFilter, }, - ], - }; - } - default: - throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, - ); - } - } else { - switch (filter.operand) { - case RecordFilterOperand.IsEmpty: - case RecordFilterOperand.IsNotEmpty: - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); - default: - throw new Error( - `Unknown empty operand ${filter.operand} for ${filter.definition.type} filter`, - ); + }, + { + [correspondingField.name + 'Id']: { + is: 'NULL', + } as RelationFilter, + }, + ], + }; } + default: + throw new Error( + `Unknown operand ${filter.operand} for ${filterType} filter`, + ); } } case 'CURRENCY': @@ -385,16 +371,9 @@ const computeFilterRecordGqlOperationFilter = ( amountMicros: { lte: parseFloat(filter.value) * 1000000 }, } as CurrencyFilter, }; - case RecordFilterOperand.IsEmpty: - case RecordFilterOperand.IsNotEmpty: - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); default: throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, + `Unknown operand ${filter.operand} for ${filterType} filter`, ); } case 'LINKS': { @@ -439,16 +418,9 @@ const computeFilterRecordGqlOperationFilter = ( }, }; } - case RecordFilterOperand.IsEmpty: - case RecordFilterOperand.IsNotEmpty: - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); default: throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, + `Unknown operand ${filter.operand} for ${filterType} filter`, ); } } @@ -493,16 +465,9 @@ const computeFilterRecordGqlOperationFilter = ( }, }; } - case RecordFilterOperand.IsEmpty: - case RecordFilterOperand.IsNotEmpty: - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); default: throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, + `Unknown operand ${filter.operand} for ${filterType} filter`, ); } } @@ -609,27 +574,12 @@ const computeFilterRecordGqlOperationFilter = ( }, }; } - case RecordFilterOperand.IsEmpty: - case RecordFilterOperand.IsNotEmpty: - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); default: throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, + `Unknown operand ${filter.operand} for ${filterType} filter`, ); } case 'MULTI_SELECT': { - if (isEmptyOperand) { - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); - } - const options = resolveSelectViewFilterValue(filter); if (options.length === 0) return; @@ -665,18 +615,11 @@ const computeFilterRecordGqlOperationFilter = ( }; default: throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, + `Unknown operand ${filter.operand} for ${filterType} filter`, ); } } case 'SELECT': { - if (isEmptyOperand) { - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); - } const options = resolveSelectViewFilterValue(filter); if (options.length === 0) return; @@ -698,7 +641,7 @@ const computeFilterRecordGqlOperationFilter = ( }; default: throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, + `Unknown operand ${filter.operand} for ${filterType} filter`, ); } } @@ -718,16 +661,9 @@ const computeFilterRecordGqlOperationFilter = ( } as ArrayFilter, }, }; - case RecordFilterOperand.IsEmpty: - case RecordFilterOperand.IsNotEmpty: - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); default: throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, + `Unknown operand ${filter.operand} for ${filterType} filter`, ); } } @@ -786,13 +722,6 @@ const computeFilterRecordGqlOperationFilter = ( }, ], }; - case RecordFilterOperand.IsEmpty: - case RecordFilterOperand.IsNotEmpty: - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); default: throw new Error( `Unknown operand ${filter.operand} for ${filter.label} filter`, @@ -827,16 +756,9 @@ const computeFilterRecordGqlOperationFilter = ( }, ], }; - case RecordFilterOperand.IsEmpty: - case RecordFilterOperand.IsNotEmpty: - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); default: throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, + `Unknown operand ${filter.operand} for ${filterType} filter`, ); } case 'PHONES': { @@ -869,16 +791,9 @@ const computeFilterRecordGqlOperationFilter = ( }, ], }; - case RecordFilterOperand.IsEmpty: - case RecordFilterOperand.IsNotEmpty: - return getEmptyRecordGqlOperationFilter( - filter.operand, - correspondingField, - filter.definition, - ); default: throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, + `Unknown operand ${filter.operand} for ${filterType} filter`, ); } } @@ -897,7 +812,7 @@ const computeFilterRecordGqlOperationFilter = ( const computeViewFilterGroupRecordGqlOperationFilter = ( filterValueDependencies: RecordFilterValueDependencies, filters: RecordFilter[], - fields: Pick[], + fields: Pick[], viewFilterGroups: ViewFilterGroup[], currentViewFilterGroupId?: string, ): RecordGqlOperationFilter | undefined => { @@ -915,11 +830,11 @@ const computeViewFilterGroupRecordGqlOperationFilter = ( const groupRecordGqlOperationFilters = groupFilters .map((filter) => - computeFilterRecordGqlOperationFilter( + computeFilterRecordGqlOperationFilter({ filterValueDependencies, filter, - fields, - ), + fieldMetadataItems: fields, + }), ) .filter(isDefined); @@ -968,17 +883,17 @@ const computeViewFilterGroupRecordGqlOperationFilter = ( export const computeViewRecordGqlOperationFilter = ( filterValueDependencies: RecordFilterValueDependencies, filters: RecordFilter[], - fields: Pick[], + fields: Pick[], viewFilterGroups: ViewFilterGroup[], ): RecordGqlOperationFilter => { const regularRecordGqlOperationFilter: RecordGqlOperationFilter[] = filters .filter((filter) => !filter.viewFilterGroupId) .map((regularFilter) => - computeFilterRecordGqlOperationFilter( + computeFilterRecordGqlOperationFilter({ filterValueDependencies, - regularFilter, - fields, - ), + filter: regularFilter, + fieldMetadataItems: fields, + }), ) .filter(isDefined); diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/getEmptyRecordGqlOperationFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/getEmptyRecordGqlOperationFilter.ts index cabfc67d7..f5f3d2cb3 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/getEmptyRecordGqlOperationFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/getEmptyRecordGqlOperationFilter.ts @@ -1,3 +1,4 @@ +import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; import { ActorFilter, AddressFilter, @@ -15,24 +16,32 @@ import { StringFilter, URLFilter, } from '@/object-record/graphql/types/RecordGqlOperationFilter'; -import { RecordFilterDefinition } from '@/object-record/record-filter/types/RecordFilterDefinition'; +import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { isNonEmptyString } from '@sniptt/guards'; import { Field } from '~/generated/graphql'; import { generateILikeFiltersForCompositeFields } from '~/utils/array/generateILikeFiltersForCompositeFields'; -export const getEmptyRecordGqlOperationFilter = ( - operand: ViewFilterOperand, - correspondingField: Pick, - definition: RecordFilterDefinition, -) => { +type GetEmptyRecordGqlOperationFilterParams = { + operand: ViewFilterOperand; + correspondingField: Pick; + recordFilter: RecordFilter; +}; + +export const getEmptyRecordGqlOperationFilter = ({ + operand, + correspondingField, + recordFilter, +}: GetEmptyRecordGqlOperationFilterParams) => { let emptyRecordFilter: RecordGqlOperationFilter = {}; - const compositeFieldName = definition.compositeFieldName; + const compositeFieldName = recordFilter.subFieldName; const isCompositeField = isNonEmptyString(compositeFieldName); - switch (definition.type) { + const filterType = getFilterTypeFromFieldType(correspondingField.type); + + switch (filterType) { case 'TEXT': emptyRecordFilter = { or: [ @@ -344,7 +353,7 @@ export const getEmptyRecordGqlOperationFilter = ( }; break; default: - throw new Error(`Unsupported empty filter type ${definition.type}`); + throw new Error(`Unsupported empty filter type ${filterType}`); } switch (operand) { @@ -355,8 +364,6 @@ export const getEmptyRecordGqlOperationFilter = ( not: emptyRecordFilter, }; default: - throw new Error( - `Unknown operand ${operand} for ${definition.type} filter`, - ); + throw new Error(`Unknown operand ${operand} for ${filterType} filter`); } }; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterOperandsForRecordFilterDefinition.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterOperandsForRecordFilterDefinition.ts deleted file mode 100644 index 9effb7202..000000000 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterOperandsForRecordFilterDefinition.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { isActorSourceCompositeFilter } from '@/object-record/object-filter-dropdown/utils/isActorSourceCompositeFilter'; -import { RecordFilterDefinition } from '@/object-record/record-filter/types/RecordFilterDefinition'; -import { ViewFilterOperand as RecordFilterOperand } from '@/views/types/ViewFilterOperand'; - -export const getRecordFilterOperandsForRecordFilterDefinition = ( - filterDefinition: Pick, -): RecordFilterOperand[] => { - const emptyOperands = [ - RecordFilterOperand.IsEmpty, - RecordFilterOperand.IsNotEmpty, - ]; - - const relationOperands = [RecordFilterOperand.Is, RecordFilterOperand.IsNot]; - - switch (filterDefinition.type) { - case 'TEXT': - case 'EMAILS': - case 'FULL_NAME': - case 'ADDRESS': - case 'LINKS': - case 'PHONES': - return [ - RecordFilterOperand.Contains, - RecordFilterOperand.DoesNotContain, - ...emptyOperands, - ]; - case 'CURRENCY': - case 'NUMBER': - return [ - RecordFilterOperand.GreaterThan, - RecordFilterOperand.LessThan, - ...emptyOperands, - ]; - case 'RAW_JSON': - return [ - RecordFilterOperand.Contains, - RecordFilterOperand.DoesNotContain, - ...emptyOperands, - ]; - case 'DATE_TIME': - case 'DATE': - return [ - RecordFilterOperand.Is, - RecordFilterOperand.IsRelative, - RecordFilterOperand.IsInPast, - RecordFilterOperand.IsInFuture, - RecordFilterOperand.IsToday, - RecordFilterOperand.IsBefore, - RecordFilterOperand.IsAfter, - ...emptyOperands, - ]; - case 'RATING': - return [ - RecordFilterOperand.Is, - RecordFilterOperand.GreaterThan, - RecordFilterOperand.LessThan, - ...emptyOperands, - ]; - case 'RELATION': - return [...relationOperands, ...emptyOperands]; - case 'MULTI_SELECT': - return [ - RecordFilterOperand.Contains, - RecordFilterOperand.DoesNotContain, - ...emptyOperands, - ]; - case 'SELECT': - return [ - RecordFilterOperand.Is, - RecordFilterOperand.IsNot, - ...emptyOperands, - ]; - case 'ACTOR': { - if (isActorSourceCompositeFilter(filterDefinition)) { - return [ - RecordFilterOperand.Is, - RecordFilterOperand.IsNot, - ...emptyOperands, - ]; - } - - return [ - RecordFilterOperand.Contains, - RecordFilterOperand.DoesNotContain, - ...emptyOperands, - ]; - } - case 'ARRAY': - return [ - RecordFilterOperand.Contains, - RecordFilterOperand.DoesNotContain, - ...emptyOperands, - ]; - case 'BOOLEAN': - return [RecordFilterOperand.Is]; - default: - return []; - } -}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleColumnFilter.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleColumnFilter.ts index d9614843b..ef5e2f0b3 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleColumnFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleColumnFilter.ts @@ -5,12 +5,15 @@ import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/u import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { availableFieldMetadataItemsForFilterFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector'; -import { formatFieldMetadataItemAsFilterDefinition } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; +import { + formatFieldMetadataItemAsFilterDefinition, + getFilterTypeFromFieldType, +} from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; import { useSelectFilterDefinitionUsedInDropdown } from '@/object-record/object-filter-dropdown/hooks/useSelectFilterDefinitionUsedInDropdown'; import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState'; import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter'; import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; -import { getRecordFilterOperandsForRecordFilterDefinition } from '@/object-record/record-filter/utils/getRecordFilterOperandsForRecordFilterDefinition'; +import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands'; import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious'; import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; @@ -115,8 +118,11 @@ export const useHandleToggleColumnFilter = ({ throw new Error('Filter definition not found'); } - const availableOperandsForFilter = - getRecordFilterOperandsForRecordFilterDefinition(filterDefinition); + const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type); + + const availableOperandsForFilter = getRecordFilterOperands({ + filterType, + }); const defaultOperand = availableOperandsForFilter[0]; diff --git a/packages/twenty-front/src/modules/views/hooks/__tests__/useApplyCurrentViewFiltersToCurrentRecordFilters.test.tsx b/packages/twenty-front/src/modules/views/hooks/__tests__/useApplyCurrentViewFiltersToCurrentRecordFilters.test.tsx index 327d3a000..149237b5c 100644 --- a/packages/twenty-front/src/modules/views/hooks/__tests__/useApplyCurrentViewFiltersToCurrentRecordFilters.test.tsx +++ b/packages/twenty-front/src/modules/views/hooks/__tests__/useApplyCurrentViewFiltersToCurrentRecordFilters.test.tsx @@ -13,15 +13,17 @@ import { currentViewIdComponentState } from '@/views/states/currentViewIdCompone import { ViewFilter } from '@/views/types/ViewFilter'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { isDefined } from 'twenty-shared'; -import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; +import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { useApplyCurrentViewFiltersToCurrentRecordFilters } from '../useApplyCurrentViewFiltersToCurrentRecordFilters'; jest.mock('@/prefetch/hooks/usePrefetchedData'); +const mockObjectMetadataItemNameSingular = 'company'; + describe('useApplyCurrentViewFiltersToCurrentRecordFilters', () => { const mockObjectMetadataItem = generatedMockObjectMetadataItems.find( - (item) => item.nameSingular === 'company', + (item) => item.nameSingular === mockObjectMetadataItemNameSingular, ); if (!isDefined(mockObjectMetadataItem)) { @@ -76,7 +78,11 @@ describe('useApplyCurrentViewFiltersToCurrentRecordFilters', () => { }; }, { - wrapper: getJestMetadataAndApolloMocksWrapper({ + wrapper: getJestMetadataAndApolloMocksAndActionMenuWrapper({ + apolloMocks: [], + componentInstanceId: 'instanceId', + contextStoreCurrentObjectMetadataNameSingular: + mockObjectMetadataItemNameSingular, onInitializeRecoilSnapshot: (snapshot) => { snapshot.set( currentViewIdComponentState.atomFamily({ @@ -129,7 +135,11 @@ describe('useApplyCurrentViewFiltersToCurrentRecordFilters', () => { }; }, { - wrapper: getJestMetadataAndApolloMocksWrapper({ + wrapper: getJestMetadataAndApolloMocksAndActionMenuWrapper({ + apolloMocks: [], + componentInstanceId: 'instanceId', + contextStoreCurrentObjectMetadataNameSingular: + mockObjectMetadataItemNameSingular, onInitializeRecoilSnapshot: (snapshot) => { snapshot.set( currentViewIdComponentState.atomFamily({ @@ -174,7 +184,11 @@ describe('useApplyCurrentViewFiltersToCurrentRecordFilters', () => { }; }, { - wrapper: getJestMetadataAndApolloMocksWrapper({ + wrapper: getJestMetadataAndApolloMocksAndActionMenuWrapper({ + apolloMocks: [], + componentInstanceId: 'instanceId', + contextStoreCurrentObjectMetadataNameSingular: + mockObjectMetadataItemNameSingular, onInitializeRecoilSnapshot: (snapshot) => { snapshot.set( currentViewIdComponentState.atomFamily({ diff --git a/packages/twenty-front/src/modules/views/hooks/__tests__/useApplyViewFiltersToCurrentRecordFilters.test.tsx b/packages/twenty-front/src/modules/views/hooks/__tests__/useApplyViewFiltersToCurrentRecordFilters.test.tsx index b17767c93..7b972a08e 100644 --- a/packages/twenty-front/src/modules/views/hooks/__tests__/useApplyViewFiltersToCurrentRecordFilters.test.tsx +++ b/packages/twenty-front/src/modules/views/hooks/__tests__/useApplyViewFiltersToCurrentRecordFilters.test.tsx @@ -11,18 +11,20 @@ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/ import { ViewFilter } from '@/views/types/ViewFilter'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { isDefined } from 'twenty-shared'; -import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; +import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { useApplyViewFiltersToCurrentRecordFilters } from '../useApplyViewFiltersToCurrentRecordFilters'; +const mockObjectMetadataItemNameSingular = 'company'; + describe('useApplyViewFiltersToCurrentRecordFilters', () => { const mockObjectMetadataItem = generatedMockObjectMetadataItems.find( - (item) => item.nameSingular === 'company', + (item) => item.nameSingular === mockObjectMetadataItemNameSingular, ); if (!isDefined(mockObjectMetadataItem)) { throw new Error( - 'Missing mock object metadata item with name singular "company"', + `Missing mock object metadata item with name singular ${mockObjectMetadataItemNameSingular}`, ); } @@ -58,7 +60,12 @@ describe('useApplyViewFiltersToCurrentRecordFilters', () => { return { applyViewFiltersToCurrentRecordFilters, currentFilters }; }, { - wrapper: getJestMetadataAndApolloMocksWrapper({}), + wrapper: getJestMetadataAndApolloMocksAndActionMenuWrapper({ + apolloMocks: [], + componentInstanceId: 'instanceId', + contextStoreCurrentObjectMetadataNameSingular: + mockObjectMetadataItemNameSingular, + }), }, ); @@ -95,7 +102,12 @@ describe('useApplyViewFiltersToCurrentRecordFilters', () => { return { applyViewFiltersToCurrentRecordFilters, currentFilters }; }, { - wrapper: getJestMetadataAndApolloMocksWrapper({}), + wrapper: getJestMetadataAndApolloMocksAndActionMenuWrapper({ + apolloMocks: [], + componentInstanceId: 'instanceId', + contextStoreCurrentObjectMetadataNameSingular: + mockObjectMetadataItemNameSingular, + }), }, ); diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm.tsx b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm.tsx index 3ef5e6172..9612ddd80 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm.tsx @@ -121,7 +121,7 @@ export const WorkflowEditTriggerCronForm = ({ const cronValidator = cron(newPattern); - if (cronValidator.isError()) { + if (cronValidator.isError() === true) { setErrorMessages({ CUSTOM: `Invalid cron pattern, ${cronValidator .getError()[0] diff --git a/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper.tsx b/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper.tsx index b01c98be4..aa7eaa19d 100644 --- a/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper.tsx +++ b/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper.tsx @@ -1,14 +1,17 @@ import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext'; +import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext'; import { MockedResponse } from '@apollo/client/testing'; import { ReactNode } from 'react'; import { MutableSnapshot } from 'recoil'; +import { isDefined } from 'twenty-shared'; import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; import { JestContextStoreSetter, JestContextStoreSetterMocks, } from '~/testing/jest/JestContextStoreSetter'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; export type GetJestMetadataAndApolloMocksAndActionMenuWrapperProps = { apolloMocks: @@ -32,6 +35,18 @@ export const getJestMetadataAndApolloMocksAndActionMenuWrapper = ({ onInitializeRecoilSnapshot, }); + const mockObjectMetadataItem = generatedMockObjectMetadataItems.find( + (objectMetadataItem) => + objectMetadataItem.nameSingular === + contextStoreCurrentObjectMetadataNameSingular, + ); + + if (!isDefined(mockObjectMetadataItem)) { + throw new Error( + `Mock object metadata item ${contextStoreCurrentObjectMetadataNameSingular} not found`, + ); + } + return ({ children }: { children: ReactNode }) => ( - 'indexIdentifierUrl', + onIndexRecordsLoaded: () => {}, + objectNamePlural: mockObjectMetadataItem.namePlural, + objectNameSingular: mockObjectMetadataItem.nameSingular, + objectMetadataItem: mockObjectMetadataItem, + recordIndexId: 'recordIndexId', + }} > - {children} - + + {children} + + diff --git a/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksWrapper.tsx b/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksWrapper.tsx index 4a838c24b..41f286708 100644 --- a/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksWrapper.tsx +++ b/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksWrapper.tsx @@ -3,12 +3,10 @@ import { ReactNode } from 'react'; import { MutableSnapshot, RecoilRoot } from 'recoil'; import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext'; -import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext'; import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; import { InMemoryCache } from '@apollo/client'; import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; export const getJestMetadataAndApolloMocksWrapper = ({ apolloMocks, @@ -25,31 +23,17 @@ export const getJestMetadataAndApolloMocksWrapper = ({ - 'indexIdentifierUrl', - onIndexRecordsLoaded: () => {}, - objectNamePlural: 'objectNamePlural', - objectNameSingular: 'objectNameSingular', - objectMetadataItem: - generatedMockObjectMetadataItems.find( - (item) => item.nameSingular === 'company', - ) ?? generatedMockObjectMetadataItems[0], - recordIndexId: 'recordIndexId', - }} + - - - - {children} - - - - + + {children} + + +