From e34ac2967c588f9672d36405ec977f853cd0b2ed Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Wed, 23 Jul 2025 14:50:03 +0200 Subject: [PATCH] Add any field filter requests (#13336) This PR adds any field filter request generation utils with its unit test. It also calls this new util in the relevant requests for table and board. This PR also adds a new corresponding state in context store so that the filter is handled in command menu and actions. We also add this new filter to the aggregate queries. The RecordShowPage story was also fixed. --- .../DeleteMultipleRecordsAction.tsx | 6 + .../DestroyMultipleRecordsAction.tsx | 6 + .../RestoreMultipleRecordsAction.tsx | 6 + .../useSetGlobalCommandMenuContext.test.tsx | 10 + .../useCopyContextStoreAndActionMenuStates.ts | 16 + .../hooks/useResetContextStoreStates.ts | 8 + .../hooks/useSetGlobalCommandMenuContext.ts | 8 + ...seFindManyRecordsSelectedInContextStore.ts | 10 +- ...tStoreAnyFieldFilterValueComponentState.ts | 9 + .../computeContextStoreFilters.test.ts | 4 + .../utils/computeContextStoreFilters.ts | 10 + ...bjectFilterDropdownAnyFieldSearchInput.tsx | 10 +- .../meta-types/hooks/useActorFieldDisplay.ts | 4 +- .../anyFieldFilterValueComponentState.ts | 10 + ...nAnyFieldFilterIntoRecordGqlFilter.test.ts | 603 ++++++++++++++++++ .../turnRecordFilterIntoGqlOperationFilter.ts | 6 + ...reateAnyFieldRecordFilterBaseProperties.ts | 22 + .../filterSelectOptionsOfFieldMetadataItem.ts | 25 + .../turnAnyFieldFilterIntoRecordGqlFilter.ts | 236 +++++++ ...textStoreNumberOfSelectedRecordsEffect.tsx | 6 + ...RecordIndexFiltersToContextStoreEffect.tsx | 19 + .../hooks/useRecordIndexLazyFetchRecords.ts | 6 + .../useFindManyRecordIndexTableParams.ts | 16 +- .../hooks/useLoadRecordIndexBoardColumn.ts | 13 + .../hooks/useAggregateRecordsForHeader.ts | 14 +- ...egateRecordsForRecordTableColumnFooter.tsx | 14 +- .../views/components/AnyFieldSearchChip.tsx | 15 +- .../views/components/ViewBarDetails.tsx | 8 +- .../internal/useGetRecordIndexTotalCount.ts | 14 +- .../useOpenAnyFieldSearchFilterFromViewBar.ts | 29 +- .../viewAnyFieldSearchValueComponentState.ts | 9 - .../__stories__/RecordShowPage.stories.tsx | 3 +- 32 files changed, 1136 insertions(+), 39 deletions(-) create mode 100644 packages/twenty-front/src/modules/context-store/states/contextStoreAnyFieldFilterValueComponentState.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/states/anyFieldFilterValueComponentState.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/turnAnyFieldFilterIntoRecordGqlFilter.test.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/utils/createAnyFieldRecordFilterBaseProperties.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/utils/filterSelectOptionsOfFieldMetadataItem.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/utils/turnAnyFieldFilterIntoRecordGqlFilter.ts delete mode 100644 packages/twenty-front/src/modules/views/states/viewAnyFieldSearchValueComponentState.ts diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/DeleteMultipleRecordsAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/DeleteMultipleRecordsAction.tsx index 22ee31805..7d6e4cbfe 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/DeleteMultipleRecordsAction.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/DeleteMultipleRecordsAction.tsx @@ -1,5 +1,6 @@ import { ActionModal } from '@/action-menu/actions/components/ActionModal'; import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow'; +import { contextStoreAnyFieldFilterValueComponentState } from '@/context-store/states/contextStoreAnyFieldFilterValueComponentState'; import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; @@ -43,6 +44,10 @@ export const DeleteMultipleRecordsAction = () => { contextStoreFiltersComponentState, ); + const contextStoreAnyFieldFilterValue = useRecoilComponentValueV2( + contextStoreAnyFieldFilterValueComponentState, + ); + const { filterValueDependencies } = useFilterValueDependencies(); const graphqlFilter = computeContextStoreFilters( @@ -50,6 +55,7 @@ export const DeleteMultipleRecordsAction = () => { contextStoreFilters, objectMetadataItem, filterValueDependencies, + contextStoreAnyFieldFilterValue, ); const { fetchAllRecords: fetchAllRecordIds } = useLazyFetchAllRecords({ diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/DestroyMultipleRecordsAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/DestroyMultipleRecordsAction.tsx index 1f9b5c871..a41b7adcd 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/DestroyMultipleRecordsAction.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/DestroyMultipleRecordsAction.tsx @@ -1,5 +1,6 @@ import { ActionModal } from '@/action-menu/actions/components/ActionModal'; import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow'; +import { contextStoreAnyFieldFilterValueComponentState } from '@/context-store/states/contextStoreAnyFieldFilterValueComponentState'; import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; @@ -43,6 +44,10 @@ export const DestroyMultipleRecordsAction = () => { contextStoreFiltersComponentState, ); + const contextStoreAnyFieldFilterValue = useRecoilComponentValueV2( + contextStoreAnyFieldFilterValueComponentState, + ); + const { filterValueDependencies } = useFilterValueDependencies(); const deletedAtFilter: RecordGqlOperationFilter = { @@ -54,6 +59,7 @@ export const DestroyMultipleRecordsAction = () => { contextStoreFilters, objectMetadataItem, filterValueDependencies, + contextStoreAnyFieldFilterValue, ), ...deletedAtFilter, }; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/RestoreMultipleRecordsAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/RestoreMultipleRecordsAction.tsx index 867d08772..d970c35b0 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/RestoreMultipleRecordsAction.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/RestoreMultipleRecordsAction.tsx @@ -1,5 +1,6 @@ import { ActionModal } from '@/action-menu/actions/components/ActionModal'; import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow'; +import { contextStoreAnyFieldFilterValueComponentState } from '@/context-store/states/contextStoreAnyFieldFilterValueComponentState'; import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; @@ -43,6 +44,10 @@ export const RestoreMultipleRecordsAction = () => { contextStoreFiltersComponentState, ); + const contextStoreAnyFieldFilterValue = useRecoilComponentValueV2( + contextStoreAnyFieldFilterValueComponentState, + ); + const { filterValueDependencies } = useFilterValueDependencies(); const deletedAtFilter: RecordGqlOperationFilter = { @@ -55,6 +60,7 @@ export const RestoreMultipleRecordsAction = () => { contextStoreFilters, objectMetadataItem, filterValueDependencies, + contextStoreAnyFieldFilterValue, ), ...deletedAtFilter, }; diff --git a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useSetGlobalCommandMenuContext.test.tsx b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useSetGlobalCommandMenuContext.test.tsx index c4a2e8d9d..8a3bad901 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useSetGlobalCommandMenuContext.test.tsx +++ b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useSetGlobalCommandMenuContext.test.tsx @@ -7,6 +7,7 @@ import { COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/command-menu/cons import { useSetGlobalCommandMenuContext } from '@/command-menu/hooks/useSetGlobalCommandMenuContext'; import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState'; import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState'; +import { contextStoreAnyFieldFilterValueComponentState } from '@/context-store/states/contextStoreAnyFieldFilterValueComponentState'; import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState'; import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState'; import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; @@ -80,6 +81,12 @@ describe('useSetGlobalCommandMenuContext', () => { }), ); + const anyFieldFilterValue = useRecoilValue( + contextStoreAnyFieldFilterValueComponentState.atomFamily({ + instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID, + }), + ); + const currentViewType = useRecoilValue( contextStoreCurrentViewTypeComponentState.atomFamily({ instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID, @@ -100,6 +107,7 @@ describe('useSetGlobalCommandMenuContext', () => { currentViewType, commandMenuPageInfo, hasUserSelectedCommand, + anyFieldFilterValue, }; }, { @@ -113,6 +121,7 @@ describe('useSetGlobalCommandMenuContext', () => { }); expect(result.current.numberOfSelectedRecords).toBe(2); expect(result.current.filters).toEqual([]); + expect(result.current.anyFieldFilterValue).toEqual(''); expect(result.current.currentViewType).toBe(ContextStoreViewType.Table); expect(result.current.commandMenuPageInfo).toEqual({ title: undefined, @@ -131,6 +140,7 @@ describe('useSetGlobalCommandMenuContext', () => { }); expect(result.current.numberOfSelectedRecords).toBe(0); expect(result.current.filters).toEqual([]); + expect(result.current.anyFieldFilterValue).toEqual(''); expect(result.current.currentViewType).toBe(ContextStoreViewType.Table); expect(result.current.commandMenuPageInfo).toEqual({ title: undefined, diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCopyContextStoreAndActionMenuStates.ts b/packages/twenty-front/src/modules/command-menu/hooks/useCopyContextStoreAndActionMenuStates.ts index 15ff88349..b4c1a1cf9 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useCopyContextStoreAndActionMenuStates.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCopyContextStoreAndActionMenuStates.ts @@ -1,3 +1,4 @@ +import { contextStoreAnyFieldFilterValueComponentState } from '@/context-store/states/contextStoreAnyFieldFilterValueComponentState'; import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState'; import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState'; @@ -76,6 +77,21 @@ export const useCopyContextStoreStates = () => { contextStoreFilters, ); + const contextStoreAnyFieldFilterValue = snapshot + .getLoadable( + contextStoreAnyFieldFilterValueComponentState.atomFamily({ + instanceId: instanceIdToCopyFrom, + }), + ) + .getValue(); + + set( + contextStoreAnyFieldFilterValueComponentState.atomFamily({ + instanceId: instanceIdToCopyTo, + }), + contextStoreAnyFieldFilterValue, + ); + const contextStoreCurrentViewId = snapshot .getLoadable( contextStoreCurrentViewIdComponentState.atomFamily({ diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useResetContextStoreStates.ts b/packages/twenty-front/src/modules/command-menu/hooks/useResetContextStoreStates.ts index 0b49009f6..eee7c7fa4 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useResetContextStoreStates.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useResetContextStoreStates.ts @@ -1,3 +1,4 @@ +import { contextStoreAnyFieldFilterValueComponentState } from '@/context-store/states/contextStoreAnyFieldFilterValueComponentState'; import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState'; import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState'; @@ -39,6 +40,13 @@ export const useResetContextStoreStates = () => { [], ); + set( + contextStoreAnyFieldFilterValueComponentState.atomFamily({ + instanceId, + }), + '', + ); + set( contextStoreCurrentViewIdComponentState.atomFamily({ instanceId, diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useSetGlobalCommandMenuContext.ts b/packages/twenty-front/src/modules/command-menu/hooks/useSetGlobalCommandMenuContext.ts index 26a000592..57a6e4b35 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useSetGlobalCommandMenuContext.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useSetGlobalCommandMenuContext.ts @@ -3,6 +3,7 @@ import { COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/command-menu/cons import { useCopyContextStoreStates } from '@/command-menu/hooks/useCopyContextStoreAndActionMenuStates'; import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState'; import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState'; +import { contextStoreAnyFieldFilterValueComponentState } from '@/context-store/states/contextStoreAnyFieldFilterValueComponentState'; import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState'; import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState'; import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; @@ -45,6 +46,13 @@ export const useSetGlobalCommandMenuContext = () => { [], ); + set( + contextStoreAnyFieldFilterValueComponentState.atomFamily({ + instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID, + }), + '', + ); + set( contextStoreCurrentViewTypeComponentState.atomFamily({ instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID, diff --git a/packages/twenty-front/src/modules/context-store/hooks/useFindManyRecordsSelectedInContextStore.ts b/packages/twenty-front/src/modules/context-store/hooks/useFindManyRecordsSelectedInContextStore.ts index 4730f463e..c30ce792f 100644 --- a/packages/twenty-front/src/modules/context-store/hooks/useFindManyRecordsSelectedInContextStore.ts +++ b/packages/twenty-front/src/modules/context-store/hooks/useFindManyRecordsSelectedInContextStore.ts @@ -1,3 +1,4 @@ +import { contextStoreAnyFieldFilterValueComponentState } from '@/context-store/states/contextStoreAnyFieldFilterValueComponentState'; import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState'; import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; @@ -36,6 +37,11 @@ export const useFindManyRecordsSelectedInContextStore = ({ instanceId, ); + const contextStoreAnyFieldFilterValue = useRecoilComponentValueV2( + contextStoreAnyFieldFilterValueComponentState, + instanceId, + ); + const { filterValueDependencies } = useFilterValueDependencies(); const objectMetadataItems = useRecoilValue(objectMetadataItemsState); @@ -58,9 +64,9 @@ export const useFindManyRecordsSelectedInContextStore = ({ const queryFilter = computeContextStoreFilters( contextStoreTargetedRecordsRule, contextStoreFilters, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - objectMetadataItem!, + objectMetadataItem, filterValueDependencies, + contextStoreAnyFieldFilterValue, ); const { records, loading, totalCount } = useFindManyRecords({ diff --git a/packages/twenty-front/src/modules/context-store/states/contextStoreAnyFieldFilterValueComponentState.ts b/packages/twenty-front/src/modules/context-store/states/contextStoreAnyFieldFilterValueComponentState.ts new file mode 100644 index 000000000..dff10d24d --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/states/contextStoreAnyFieldFilterValueComponentState.ts @@ -0,0 +1,9 @@ +import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +export const contextStoreAnyFieldFilterValueComponentState = + createComponentStateV2({ + key: 'contextStoreAnyFieldFilterValueComponentState', + defaultValue: '', + componentInstanceContext: ContextStoreComponentInstanceContext, + }); 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 bfffc8b72..1ad7ec844 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 @@ -26,10 +26,12 @@ describe('computeContextStoreFilters', () => { [], personObjectMetadataItem, mockFilterValueDependencies, + '', ); expect(filters).toEqual({ and: [ + {}, { id: { in: ['1', '2', '3'], @@ -67,10 +69,12 @@ describe('computeContextStoreFilters', () => { contextStoreFilters, personObjectMetadataItem, mockFilterValueDependencies, + '', ); expect(filters).toEqual({ and: [ + {}, { or: [ { diff --git a/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts b/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts index 7842a1c8e..bbdb606c4 100644 --- a/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts +++ b/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts @@ -4,6 +4,7 @@ import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGq import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; import { RecordFilterValueDependencies } from '@/object-record/record-filter/types/RecordFilterValueDependencies'; import { computeRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeRecordGqlOperationFilter'; +import { turnAnyFieldFilterIntoRecordGqlFilter } from '@/object-record/record-filter/utils/turnAnyFieldFilterIntoRecordGqlFilter'; import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; export const computeContextStoreFilters = ( @@ -11,11 +12,19 @@ export const computeContextStoreFilters = ( contextStoreFilters: RecordFilter[], objectMetadataItem: ObjectMetadataItem, filterValueDependencies: RecordFilterValueDependencies, + anyFieldFilterValue: string, ) => { let queryFilter: RecordGqlOperationFilter | undefined; + const { recordGqlOperationFilter: recordGqlFilterForAnyFieldFilter } = + turnAnyFieldFilterIntoRecordGqlFilter({ + filterValue: anyFieldFilterValue, + objectMetadataItem, + }); + if (contextStoreTargetedRecordsRule.mode === 'exclusion') { queryFilter = makeAndFilterVariables([ + recordGqlFilterForAnyFieldFilter, computeRecordGqlOperationFilter({ filterValueDependencies, fields: objectMetadataItem?.fields ?? [], @@ -35,6 +44,7 @@ export const computeContextStoreFilters = ( } if (contextStoreTargetedRecordsRule.mode === 'selection') { queryFilter = makeAndFilterVariables([ + recordGqlFilterForAnyFieldFilter, contextStoreTargetedRecordsRule.selectedRecordIds.length > 0 ? { id: { diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownAnyFieldSearchInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownAnyFieldSearchInput.tsx index a4d12c023..432d5472d 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownAnyFieldSearchInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownAnyFieldSearchInput.tsx @@ -1,25 +1,25 @@ +import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; -import { viewAnyFieldSearchValueComponentState } from '@/views/states/viewAnyFieldSearchValueComponentState'; import { useLingui } from '@lingui/react/macro'; export const ObjectFilterDropdownAnyFieldSearchInput = () => { const { t } = useLingui(); - const [viewAnyFieldSearchValue, setViewAnyFieldSearchValue] = - useRecoilComponentStateV2(viewAnyFieldSearchValueComponentState); + const [anyFieldFilterSearchValue, setAnyFieldFilterSearchValue] = + useRecoilComponentStateV2(anyFieldFilterValueComponentState); const handleSearchChange = (event: React.ChangeEvent) => { const inputValue = event.target.value; - setViewAnyFieldSearchValue(inputValue); + setAnyFieldFilterSearchValue(inputValue); }; return ( diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useActorFieldDisplay.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useActorFieldDisplay.ts index e678ce180..69da44df9 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useActorFieldDisplay.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useActorFieldDisplay.ts @@ -30,8 +30,8 @@ export const useActorFieldDisplay = (): ActorFieldDisplayValue | undefined => { } const relatedWorkspaceMember = [ - ...currentWorkspaceDeletedMembers, - ...currentWorkspaceMembers, + ...(currentWorkspaceDeletedMembers ?? []), + ...(currentWorkspaceMembers ?? []), ].find( (workspaceMember) => workspaceMember.id === fieldValue.workspaceMemberId, ); diff --git a/packages/twenty-front/src/modules/object-record/record-filter/states/anyFieldFilterValueComponentState.ts b/packages/twenty-front/src/modules/object-record/record-filter/states/anyFieldFilterValueComponentState.ts new file mode 100644 index 000000000..46d33dcbb --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/states/anyFieldFilterValueComponentState.ts @@ -0,0 +1,10 @@ +import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +export const anyFieldFilterValueComponentState = createComponentStateV2( + { + key: 'anyFieldFilterValueComponentState', + defaultValue: '', + componentInstanceContext: RecordFiltersComponentInstanceContext, + }, +); diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/turnAnyFieldFilterIntoRecordGqlFilter.test.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/turnAnyFieldFilterIntoRecordGqlFilter.test.ts new file mode 100644 index 000000000..4e0c9bba4 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/turnAnyFieldFilterIntoRecordGqlFilter.test.ts @@ -0,0 +1,603 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { filterSelectOptionsOfFieldMetadataItem } from '@/object-record/record-filter/utils/filterSelectOptionsOfFieldMetadataItem'; +import { turnAnyFieldFilterIntoRecordGqlFilter } from '@/object-record/record-filter/utils/turnAnyFieldFilterIntoRecordGqlFilter'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; + +const baseFieldMetadataItem: FieldMetadataItem = { + id: 'base-field-metadata-item-id', + createdAt: new Date().toISOString(), + label: 'Test', + name: 'test', + type: FieldMetadataType.TEXT, + updatedAt: new Date().toISOString(), +}; + +const textFieldMetadataItem: FieldMetadataItem = { + ...baseFieldMetadataItem, + id: 'text-field-id', + name: 'textField', +}; + +const addressFieldMetadataItem: FieldMetadataItem = { + ...baseFieldMetadataItem, + id: 'address-field-id', + type: FieldMetadataType.ADDRESS, + name: 'addressField', +}; + +const linksFieldMetadataItem: FieldMetadataItem = { + ...baseFieldMetadataItem, + id: 'links-field-id', + type: FieldMetadataType.LINKS, + name: 'linksField', +}; + +const fullNameFieldMetadataItem: FieldMetadataItem = { + ...baseFieldMetadataItem, + id: 'full-name-field-id', + type: FieldMetadataType.FULL_NAME, + name: 'fullNameField', +}; + +const arrayFieldMetadataItem: FieldMetadataItem = { + ...baseFieldMetadataItem, + id: 'array-field-id', + type: FieldMetadataType.ARRAY, + name: 'arrayField', +}; + +const emailsFieldMetadataItem: FieldMetadataItem = { + ...baseFieldMetadataItem, + id: 'emails-field-id', + type: FieldMetadataType.EMAILS, + name: 'emailsField', +}; + +const phonesFieldMetadataItem: FieldMetadataItem = { + ...baseFieldMetadataItem, + id: 'phones-field-id', + type: FieldMetadataType.PHONES, + name: 'phonesField', +}; + +const numberFieldMetadataItem: FieldMetadataItem = { + ...baseFieldMetadataItem, + id: 'number-field-id', + type: FieldMetadataType.NUMBER, + name: 'numberField', +}; + +const currencyFieldMetadataItem: FieldMetadataItem = { + ...baseFieldMetadataItem, + id: 'currency-field-id', + type: FieldMetadataType.CURRENCY, + name: 'currencyField', +}; + +const selectFieldMetadataItem: FieldMetadataItem = { + ...baseFieldMetadataItem, + id: 'select-field-id', + type: FieldMetadataType.SELECT, + name: 'selectField', + options: [ + { + color: 'blue', + id: '1', + label: 'blue', + position: 1, + value: 'BLUE', + }, + { + color: 'red', + id: '2', + label: 'red', + position: 2, + value: 'RED', + }, + ], +}; + +const multiSelectFieldMetadataItem: FieldMetadataItem = { + ...baseFieldMetadataItem, + id: 'multi-select-field-id', + type: FieldMetadataType.MULTI_SELECT, + name: 'multiSelect', + options: [ + { + color: 'blue', + id: '1', + label: 'blue', + position: 1, + value: 'BLUE', + }, + { + color: 'red', + id: '2', + label: 'red', + position: 2, + value: 'RED', + }, + ], +}; + +const mockObjectMetadataItem: ObjectMetadataItem = { + id: 'mock-object-metadata-item', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + indexMetadatas: [], + isActive: true, + isCustom: true, + isLabelSyncedWithName: true, + isRemote: false, + isSearchable: true, + isSystem: false, + labelIdentifierFieldMetadataId: 'mock-id', + labelPlural: 'Tests', + labelSingular: 'Test', + nameSingular: 'test', + namePlural: 'tests', + fields: [], +}; + +const mockObjectMetadataItemWithAllFields: ObjectMetadataItem = { + ...mockObjectMetadataItem, + fields: [ + textFieldMetadataItem, + addressFieldMetadataItem, + linksFieldMetadataItem, + fullNameFieldMetadataItem, + arrayFieldMetadataItem, + emailsFieldMetadataItem, + phonesFieldMetadataItem, + numberFieldMetadataItem, + currencyFieldMetadataItem, + selectFieldMetadataItem, + multiSelectFieldMetadataItem, + ], +}; + +describe('turnAnyFieldFilterIntoRecordGqlFilter', () => { + describe('TEXT field type', () => { + it('should generate correct filter for text field', () => { + const filterValue = 'test'; + + const result = turnAnyFieldFilterIntoRecordGqlFilter({ + filterValue, + objectMetadataItem: { + ...mockObjectMetadataItem, + fields: [textFieldMetadataItem], + }, + }); + + expect(result.recordGqlOperationFilter.or).toContainEqual({ + [textFieldMetadataItem.name]: { + ilike: `%${filterValue}%`, + }, + }); + }); + }); + + describe('ADDRESS field type', () => { + it('should generate correct filter for address field', () => { + const filterValue = 'New York'; + + const result = turnAnyFieldFilterIntoRecordGqlFilter({ + filterValue, + objectMetadataItem: { + ...mockObjectMetadataItem, + fields: [addressFieldMetadataItem], + }, + }); + + expect(result.recordGqlOperationFilter.or).toContainEqual({ + or: [ + { + [addressFieldMetadataItem.name]: { + addressStreet1: { + ilike: `%${filterValue}%`, + }, + }, + }, + { + [addressFieldMetadataItem.name]: { + addressStreet2: { + ilike: `%${filterValue}%`, + }, + }, + }, + { + [addressFieldMetadataItem.name]: { + addressCity: { + ilike: `%${filterValue}%`, + }, + }, + }, + { + [addressFieldMetadataItem.name]: { + addressState: { + ilike: `%${filterValue}%`, + }, + }, + }, + { + [addressFieldMetadataItem.name]: { + addressCountry: { + ilike: `%${filterValue}%`, + }, + }, + }, + { + [addressFieldMetadataItem.name]: { + addressPostcode: { + ilike: `%${filterValue}%`, + }, + }, + }, + ], + }); + }); + }); + + describe('LINKS field type', () => { + it('should generate correct filter for links field', () => { + const filterValue = 'test'; + + const result = turnAnyFieldFilterIntoRecordGqlFilter({ + filterValue, + objectMetadataItem: { + ...mockObjectMetadataItem, + fields: [linksFieldMetadataItem], + }, + }); + + expect(result.recordGqlOperationFilter.or).toContainEqual({ + or: [ + { + [linksFieldMetadataItem.name]: { + primaryLinkUrl: { + ilike: `%${filterValue}%`, + }, + }, + }, + { + [linksFieldMetadataItem.name]: { + primaryLinkLabel: { + ilike: `%${filterValue}%`, + }, + }, + }, + { + [linksFieldMetadataItem.name]: { + secondaryLinks: { + like: `%${filterValue}%`, + }, + }, + }, + ], + }); + }); + }); + + describe('FULL_NAME field type', () => { + it('should generate correct filter for full name field', () => { + const filterValue = 'test'; + + const result = turnAnyFieldFilterIntoRecordGqlFilter({ + filterValue, + objectMetadataItem: { + ...mockObjectMetadataItem, + fields: [fullNameFieldMetadataItem], + }, + }); + + expect(result.recordGqlOperationFilter.or).toContainEqual({ + or: [ + { + [fullNameFieldMetadataItem.name]: { + firstName: { + ilike: `%${filterValue}%`, + }, + }, + }, + { + [fullNameFieldMetadataItem.name]: { + lastName: { + ilike: `%${filterValue}%`, + }, + }, + }, + ], + }); + }); + }); + + describe('ARRAY field type', () => { + it('should generate correct filter for array field', () => { + const filterValue = 'test'; + + const result = turnAnyFieldFilterIntoRecordGqlFilter({ + filterValue, + objectMetadataItem: { + ...mockObjectMetadataItem, + fields: [arrayFieldMetadataItem], + }, + }); + + expect(result.recordGqlOperationFilter.or).toContainEqual({ + [arrayFieldMetadataItem.name]: { + containsIlike: `%${filterValue}%`, + }, + }); + }); + }); + + describe('EMAILS field type', () => { + it('should generate correct filter for emails field', () => { + const filterValue = 'test'; + + const result = turnAnyFieldFilterIntoRecordGqlFilter({ + filterValue, + objectMetadataItem: { + ...mockObjectMetadataItem, + fields: [emailsFieldMetadataItem], + }, + }); + + expect(result.recordGqlOperationFilter.or).toContainEqual({ + or: [ + { + [emailsFieldMetadataItem.name]: { + primaryEmail: { + ilike: `%${filterValue}%`, + }, + }, + }, + { + [emailsFieldMetadataItem.name]: { + additionalEmails: { + like: `%${filterValue}%`, + }, + }, + }, + ], + }); + }); + }); + + describe('PHONES field type', () => { + it('should generate correct filter for phones field', () => { + const filterValue = '123'; + + const result = turnAnyFieldFilterIntoRecordGqlFilter({ + filterValue, + objectMetadataItem: { + ...mockObjectMetadataItem, + fields: [phonesFieldMetadataItem], + }, + }); + + expect(result.recordGqlOperationFilter.or).toContainEqual({ + or: [ + { + [phonesFieldMetadataItem.name]: { + primaryPhoneNumber: { + ilike: `%${filterValue}%`, + }, + }, + }, + { + [phonesFieldMetadataItem.name]: { + primaryPhoneCallingCode: { + ilike: `%${filterValue}%`, + }, + }, + }, + { + [phonesFieldMetadataItem.name]: { + additionalPhones: { + like: `%${filterValue}%`, + }, + }, + }, + ], + }); + }); + }); + + describe('NUMBER field type', () => { + it('should generate correct filter for number field with numeric value', () => { + const filterValue = '123.1'; + + const result = turnAnyFieldFilterIntoRecordGqlFilter({ + filterValue, + objectMetadataItem: { + ...mockObjectMetadataItem, + fields: [numberFieldMetadataItem], + }, + }); + + expect(result.recordGqlOperationFilter.or).toContainEqual({ + [numberFieldMetadataItem.name]: { + eq: 123.1, + }, + }); + }); + + it('should not generate filter for number field with non-numeric value', () => { + const filterValue = 'not a number'; + + const result = turnAnyFieldFilterIntoRecordGqlFilter({ + filterValue, + objectMetadataItem: { + ...mockObjectMetadataItem, + fields: [numberFieldMetadataItem], + }, + }); + + expect(result.recordGqlOperationFilter).toEqual({}); + }); + }); + + describe('CURRENCY field type', () => { + it('should generate correct filter for currency field with numeric value', () => { + const filterValue = '123'; + + const result = turnAnyFieldFilterIntoRecordGqlFilter({ + filterValue, + objectMetadataItem: { + ...mockObjectMetadataItem, + fields: [currencyFieldMetadataItem], + }, + }); + + expect(result.recordGqlOperationFilter.or).toContainEqual({ + [currencyFieldMetadataItem.name]: { + amountMicros: { + eq: 123000000, + }, + }, + }); + }); + + it('should generate correct filter for currency field with currency code', () => { + const filterValue = 'USD'; + + const result = turnAnyFieldFilterIntoRecordGqlFilter({ + filterValue, + objectMetadataItem: { + ...mockObjectMetadataItem, + fields: [currencyFieldMetadataItem], + }, + }); + + expect( + (result.recordGqlOperationFilter.or as any)?.[0][ + currencyFieldMetadataItem.name + ].currencyCode.in.includes(filterValue), + ).toBe(true); + }); + }); + + describe('SELECT field type', () => { + it('should generate correct filter for select field with matching option', () => { + const filterValue = 'r'; + + const result = turnAnyFieldFilterIntoRecordGqlFilter({ + filterValue, + objectMetadataItem: { + ...mockObjectMetadataItem, + fields: [selectFieldMetadataItem], + }, + }); + + const { foundCorrespondingSelectOptions: expectedOptions } = + filterSelectOptionsOfFieldMetadataItem({ + fieldMetadataItem: selectFieldMetadataItem, + filterValue, + }); + + expect(result.recordGqlOperationFilter.or).toContainEqual({ + [selectFieldMetadataItem.name]: { + in: expectedOptions?.map((option) => option.value), + }, + }); + }); + + it('should not generate filter for select field with non-matching value', () => { + const filterValue = 'not-found'; + + const result = turnAnyFieldFilterIntoRecordGqlFilter({ + filterValue, + objectMetadataItem: { + ...mockObjectMetadataItem, + fields: [selectFieldMetadataItem], + }, + }); + + expect(result.recordGqlOperationFilter).toEqual({}); + }); + }); + + describe('MULTI_SELECT field type', () => { + it('should generate correct filter for multi-select field with matching option', () => { + const filterValue = 'r'; + + const result = turnAnyFieldFilterIntoRecordGqlFilter({ + filterValue, + objectMetadataItem: { + ...mockObjectMetadataItem, + fields: [multiSelectFieldMetadataItem], + }, + }); + + const { foundCorrespondingSelectOptions: expectedOptions } = + filterSelectOptionsOfFieldMetadataItem({ + fieldMetadataItem: multiSelectFieldMetadataItem, + filterValue, + }); + + expect(result.recordGqlOperationFilter.or).toContainEqual({ + [multiSelectFieldMetadataItem.name]: { + containsAny: expectedOptions?.map((option) => option.value), + }, + }); + }); + + it('should not generate filter for multi-select field with non-matching value', () => { + const filterValue = 'not-found'; + + const result = turnAnyFieldFilterIntoRecordGqlFilter({ + filterValue, + objectMetadataItem: { + ...mockObjectMetadataItem, + fields: [multiSelectFieldMetadataItem], + }, + }); + + expect(result.recordGqlOperationFilter).toEqual({}); + }); + }); + + describe('combined field filters', () => { + it('should generate OR filter combining all matching field types', () => { + const filterValue = 'a'; + + const result = turnAnyFieldFilterIntoRecordGqlFilter({ + filterValue, + objectMetadataItem: mockObjectMetadataItemWithAllFields, + }); + + expect(result.recordGqlOperationFilter).toHaveProperty('or'); + expect(Array.isArray(result.recordGqlOperationFilter.or)).toBe(true); + expect( + (result.recordGqlOperationFilter.or as any[])?.length, + ).toBeGreaterThan(0); + }); + + it('should handle empty filter value', () => { + const filterValue = ''; + + const result = turnAnyFieldFilterIntoRecordGqlFilter({ + filterValue, + objectMetadataItem: mockObjectMetadataItemWithAllFields, + }); + + expect(result.recordGqlOperationFilter).toEqual({}); + }); + + it('should handle object with no fields', () => { + const emptyObjectMetadata = { + ...mockObjectMetadataItem, + fields: [], + }; + + const result = turnAnyFieldFilterIntoRecordGqlFilter({ + filterValue: 'test', + objectMetadataItem: emptyObjectMetadata, + }); + + expect(result.recordGqlOperationFilter).toEqual({}); + }); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/turnRecordFilterIntoGqlOperationFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/turnRecordFilterIntoGqlOperationFilter.ts index c12457e11..e509ea43a 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/turnRecordFilterIntoGqlOperationFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/turnRecordFilterIntoGqlOperationFilter.ts @@ -303,6 +303,12 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ lte: parseFloat(recordFilter.value), } as FloatFilter, }; + case RecordFilterOperand.Is: + return { + [correspondingFieldMetadataItem.name]: { + eq: parseFloat(recordFilter.value), + } as FloatFilter, + }; default: throw new Error( `Unknown operand ${recordFilter.operand} for ${filterType} filter`, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/createAnyFieldRecordFilterBaseProperties.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/createAnyFieldRecordFilterBaseProperties.ts new file mode 100644 index 000000000..b151e174a --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/createAnyFieldRecordFilterBaseProperties.ts @@ -0,0 +1,22 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; +import { v4 } from 'uuid'; + +export const createAnyFieldRecordFilterBaseProperties = ({ + filterValue, + fieldMetadataItem, +}: { + filterValue: string; + fieldMetadataItem: FieldMetadataItem; +}): Pick< + RecordFilter, + 'id' | 'value' | 'displayValue' | 'label' | 'fieldMetadataId' +> => { + return { + id: v4(), + value: filterValue, + displayValue: '', + label: '', + fieldMetadataId: fieldMetadataItem.id, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/filterSelectOptionsOfFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/filterSelectOptionsOfFieldMetadataItem.ts new file mode 100644 index 000000000..ca0c9910d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/filterSelectOptionsOfFieldMetadataItem.ts @@ -0,0 +1,25 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; + +export const filterSelectOptionsOfFieldMetadataItem = ({ + fieldMetadataItem, + filterValue, +}: { + fieldMetadataItem: FieldMetadataItem; + filterValue: string; +}) => { + const selectOptions = fieldMetadataItem.options; + + const foundCorrespondingSelectOptions = selectOptions?.filter( + (selectOption) => + selectOption.value + .toLocaleLowerCase() + .includes(filterValue.toLocaleLowerCase()) || + selectOption.label + .toLocaleLowerCase() + .includes(filterValue.toLocaleLowerCase()), + ); + + return { + foundCorrespondingSelectOptions, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/turnAnyFieldFilterIntoRecordGqlFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/turnAnyFieldFilterIntoRecordGqlFilter.ts new file mode 100644 index 000000000..c16754e4b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/turnAnyFieldFilterIntoRecordGqlFilter.ts @@ -0,0 +1,236 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter'; +import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; +import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand'; +import { turnRecordFilterIntoRecordGqlOperationFilter } from '@/object-record/record-filter/utils/compute-record-gql-operation-filter/turnRecordFilterIntoGqlOperationFilter'; +import { createAnyFieldRecordFilterBaseProperties } from '@/object-record/record-filter/utils/createAnyFieldRecordFilterBaseProperties'; +import { filterSelectOptionsOfFieldMetadataItem } from '@/object-record/record-filter/utils/filterSelectOptionsOfFieldMetadataItem'; +import { CURRENCIES } from '@/settings/data-model/constants/Currencies'; +import { isNonEmptyString } from '@sniptt/guards'; +import { isDefined } from 'twenty-shared/utils'; +import { z } from 'zod'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { isNonEmptyArray } from '~/utils/isNonEmptyArray'; + +export const turnAnyFieldFilterIntoRecordGqlFilter = ({ + filterValue, + objectMetadataItem, +}: { + filterValue: string; + objectMetadataItem: ObjectMetadataItem; +}) => { + const fieldMetadataItems = objectMetadataItem.fields; + + const anyFieldRecordFilters: RecordFilter[] = []; + + const isFilterValueANumber = z.coerce.number().safeParse(filterValue).success; + + for (const fieldMetadataItem of fieldMetadataItems) { + switch (fieldMetadataItem.type) { + case FieldMetadataType.TEXT: { + anyFieldRecordFilters.push({ + ...createAnyFieldRecordFilterBaseProperties({ + filterValue, + fieldMetadataItem, + }), + operand: RecordFilterOperand.Contains, + type: 'TEXT', + } satisfies RecordFilter); + break; + } + case FieldMetadataType.ADDRESS: { + anyFieldRecordFilters.push({ + ...createAnyFieldRecordFilterBaseProperties({ + filterValue, + fieldMetadataItem, + }), + operand: RecordFilterOperand.Contains, + type: 'ADDRESS', + } satisfies RecordFilter); + break; + } + case FieldMetadataType.LINKS: { + anyFieldRecordFilters.push({ + ...createAnyFieldRecordFilterBaseProperties({ + filterValue, + fieldMetadataItem, + }), + operand: RecordFilterOperand.Contains, + type: 'LINKS', + } satisfies RecordFilter); + break; + } + case FieldMetadataType.FULL_NAME: { + anyFieldRecordFilters.push({ + ...createAnyFieldRecordFilterBaseProperties({ + filterValue, + fieldMetadataItem, + }), + operand: RecordFilterOperand.Contains, + type: 'FULL_NAME', + } satisfies RecordFilter); + break; + } + case FieldMetadataType.ARRAY: { + anyFieldRecordFilters.push({ + ...createAnyFieldRecordFilterBaseProperties({ + filterValue, + fieldMetadataItem, + }), + operand: RecordFilterOperand.Contains, + type: 'ARRAY', + } satisfies RecordFilter); + break; + } + case FieldMetadataType.EMAILS: { + anyFieldRecordFilters.push({ + ...createAnyFieldRecordFilterBaseProperties({ + filterValue, + fieldMetadataItem, + }), + operand: RecordFilterOperand.Contains, + type: 'EMAILS', + } satisfies RecordFilter); + break; + } + case FieldMetadataType.PHONES: { + anyFieldRecordFilters.push({ + ...createAnyFieldRecordFilterBaseProperties({ + filterValue, + fieldMetadataItem, + }), + operand: RecordFilterOperand.Contains, + type: 'PHONES', + } satisfies RecordFilter); + break; + } + case FieldMetadataType.NUMBER: { + if (isFilterValueANumber) { + anyFieldRecordFilters.push({ + ...createAnyFieldRecordFilterBaseProperties({ + filterValue, + fieldMetadataItem, + }), + operand: RecordFilterOperand.Is, + type: 'NUMBER', + } satisfies RecordFilter); + } + break; + } + case FieldMetadataType.CURRENCY: { + if (isFilterValueANumber) { + anyFieldRecordFilters.push({ + ...createAnyFieldRecordFilterBaseProperties({ + filterValue, + fieldMetadataItem, + }), + operand: RecordFilterOperand.Is, + type: 'CURRENCY', + subFieldName: 'amountMicros', + } satisfies RecordFilter); + } + + if (isNonEmptyString(filterValue)) { + const foundCorrespondingCurrencies = CURRENCIES.filter( + (currency) => + currency.label.includes(filterValue) || + currency.value.includes(filterValue), + ); + + if (isNonEmptyArray(foundCorrespondingCurrencies)) { + const arrayOfCurrenciesStringified = JSON.stringify( + foundCorrespondingCurrencies.map((currency) => currency.value), + ); + + anyFieldRecordFilters.push({ + ...createAnyFieldRecordFilterBaseProperties({ + filterValue: arrayOfCurrenciesStringified, + fieldMetadataItem, + }), + operand: RecordFilterOperand.Is, + type: 'CURRENCY', + subFieldName: 'currencyCode', + } satisfies RecordFilter); + } + } + break; + } + case FieldMetadataType.SELECT: { + if (isNonEmptyString(filterValue)) { + const { foundCorrespondingSelectOptions } = + filterSelectOptionsOfFieldMetadataItem({ + fieldMetadataItem, + filterValue, + }); + + if (isNonEmptyArray(foundCorrespondingSelectOptions)) { + const arrayOfSelectValues = JSON.stringify( + foundCorrespondingSelectOptions.map( + (selectOption) => selectOption.value, + ), + ); + + anyFieldRecordFilters.push({ + ...createAnyFieldRecordFilterBaseProperties({ + fieldMetadataItem, + filterValue: arrayOfSelectValues, + }), + operand: RecordFilterOperand.Is, + type: 'SELECT', + } satisfies RecordFilter); + } + } + break; + } + case FieldMetadataType.MULTI_SELECT: { + if (isNonEmptyString(filterValue)) { + const { foundCorrespondingSelectOptions } = + filterSelectOptionsOfFieldMetadataItem({ + fieldMetadataItem, + filterValue, + }); + + if (isNonEmptyArray(foundCorrespondingSelectOptions)) { + const arrayOfSelectValues = JSON.stringify( + foundCorrespondingSelectOptions.map( + (selectOption) => selectOption.value, + ), + ); + + anyFieldRecordFilters.push({ + ...createAnyFieldRecordFilterBaseProperties({ + fieldMetadataItem, + filterValue: arrayOfSelectValues, + }), + operand: RecordFilterOperand.Contains, + type: 'MULTI_SELECT', + } satisfies RecordFilter); + } + } + break; + } + } + } + + const baseRecordGqlOperationFilters = anyFieldRecordFilters + .map((recordFilter) => + turnRecordFilterIntoRecordGqlOperationFilter({ + filterValueDependencies: {}, + fieldMetadataItems: objectMetadataItem.fields, + recordFilter, + }), + ) + .filter(isDefined); + + const recordGqlOperationFilter: RecordGqlOperationFilter = { + or: baseRecordGqlOperationFilters, + }; + + if (baseRecordGqlOperationFilters.length === 0) { + return { recordGqlOperationFilter: {} }; + } + + return { + recordGqlOperationFilter, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx index 3859e5b79..8b470236d 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx @@ -1,3 +1,4 @@ +import { contextStoreAnyFieldFilterValueComponentState } from '@/context-store/states/contextStoreAnyFieldFilterValueComponentState'; import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState'; import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; @@ -40,6 +41,10 @@ export const RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect = contextStoreFiltersComponentState, ); + const contextStoreAnyFieldFilterValue = useRecoilComponentValueV2( + contextStoreAnyFieldFilterValueComponentState, + ); + const { filterValueDependencies } = useFilterValueDependencies(); const { totalCount } = useFindManyRecords({ @@ -52,6 +57,7 @@ export const RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect = contextStoreFilters, objectMetadataItem, filterValueDependencies, + contextStoreAnyFieldFilterValue, ), limit: 1, skip: contextStoreTargetedRecordsRule.mode === 'selection', diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect.tsx index 325cf8935..a240a9ad6 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect.tsx @@ -1,7 +1,9 @@ import { useEffect } from 'react'; +import { contextStoreAnyFieldFilterValueComponentState } from '@/context-store/states/contextStoreAnyFieldFilterValueComponentState'; import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; +import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { hasUserSelectedAllRowsComponentState } from '@/object-record/record-table/record-table-row/states/hasUserSelectedAllRowsFamilyState'; @@ -74,5 +76,22 @@ export const RecordIndexFiltersToContextStoreEffect = () => { }; }, [recordIndexFilters, setContextStoreFilters]); + const setContextStoreAnyFieldFilterValue = useSetRecoilComponentStateV2( + contextStoreAnyFieldFilterValueComponentState, + ); + + const anyFieldFilterValue = useRecoilComponentValueV2( + anyFieldFilterValueComponentState, + recordIndexId, + ); + + useEffect(() => { + setContextStoreAnyFieldFilterValue(anyFieldFilterValue); + + return () => { + setContextStoreAnyFieldFilterValue(''); + }; + }, [anyFieldFilterValue, setContextStoreAnyFieldFilterValue]); + return <>; }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useRecordIndexLazyFetchRecords.ts b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useRecordIndexLazyFetchRecords.ts index 88d5b57d7..f8194b861 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useRecordIndexLazyFetchRecords.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useRecordIndexLazyFetchRecords.ts @@ -2,6 +2,7 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata' import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { contextStoreAnyFieldFilterValueComponentState } from '@/context-store/states/contextStoreAnyFieldFilterValueComponentState'; import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; @@ -72,6 +73,10 @@ export const useRecordIndexLazyFetchRecords = ({ contextStoreFiltersComponentState, ); + const contextStoreAnyFieldFilterValue = useRecoilComponentValueV2( + contextStoreAnyFieldFilterValueComponentState, + ); + const { filterValueDependencies } = useFilterValueDependencies(); const findManyRecordsParams = useFindManyRecordIndexTableParams( @@ -83,6 +88,7 @@ export const useRecordIndexLazyFetchRecords = ({ contextStoreFilters, objectMetadataItem, filterValueDependencies, + contextStoreAnyFieldFilterValue, ); const finalColumns = [ diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useFindManyRecordIndexTableParams.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useFindManyRecordIndexTableParams.ts index f0b60f091..c08fa3bf4 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useFindManyRecordIndexTableParams.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useFindManyRecordIndexTableParams.ts @@ -2,9 +2,11 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; +import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; import { combineFilters } from '@/object-record/record-filter/utils/combineFilters'; import { computeRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeRecordGqlOperationFilter'; +import { turnAnyFieldFilterIntoRecordGqlFilter } from '@/object-record/record-filter/utils/turnAnyFieldFilterIntoRecordGqlFilter'; import { useCurrentRecordGroupDefinition } from '@/object-record/record-group/hooks/useCurrentRecordGroupDefinition'; import { useRecordGroupFilter } from '@/object-record/record-group/hooks/useRecordGroupFilter'; import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState'; @@ -37,18 +39,28 @@ export const useFindManyRecordIndexTableParams = ( const { filterValueDependencies } = useFilterValueDependencies(); - const stateFilter = computeRecordGqlOperationFilter({ + const currentFilters = computeRecordGqlOperationFilter({ fields: objectMetadataItem?.fields ?? [], filterValueDependencies, recordFilterGroups: currentRecordFilterGroups, recordFilters: currentRecordFilters, }); + const anyFieldFilterValue = useRecoilComponentValueV2( + anyFieldFilterValueComponentState, + ); + + const { recordGqlOperationFilter: anyFieldFilter } = + turnAnyFieldFilterIntoRecordGqlFilter({ + objectMetadataItem, + filterValue: anyFieldFilterValue, + }); + const orderBy = turnSortsIntoOrderBy(objectMetadataItem, currentRecordSorts); return { objectNameSingular, - filter: combineFilters([stateFilter, recordGroupFilter]), + filter: combineFilters([currentFilters, recordGroupFilter, anyFieldFilter]), orderBy, // If we have a current record group definition, we only want to fetch 8 records by page ...(currentRecordGroupDefinition ? { limit: 8 } : {}), diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts index 4d7d48ae2..f49bd607c 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts @@ -15,7 +15,9 @@ import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hook import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState'; +import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState'; import { combineFilters } from '@/object-record/record-filter/utils/combineFilters'; +import { turnAnyFieldFilterIntoRecordGqlFilter } from '@/object-record/record-filter/utils/turnAnyFieldFilterIntoRecordGqlFilter'; import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { isDefined } from 'twenty-shared/utils'; @@ -64,6 +66,16 @@ export const useLoadRecordIndexBoardColumn = ({ fields: objectMetadataItem.fields, }); + const anyFieldFilterValue = useRecoilComponentValueV2( + anyFieldFilterValueComponentState, + ); + + const { recordGqlOperationFilter: anyFieldFilter } = + turnAnyFieldFilterIntoRecordGqlFilter({ + objectMetadataItem, + filterValue: anyFieldFilterValue, + }); + const orderBy = turnSortsIntoOrderBy(objectMetadataItem, currentRecordSorts); const recordGqlFields = useRecordBoardRecordGqlFields({ @@ -78,6 +90,7 @@ export const useLoadRecordIndexBoardColumn = ({ : { is: 'NULL' }; const combinedFilters = combineFilters([ + anyFieldFilter, requestFilters, { [kanbanFieldMetadataItem.name]: recordIndexKanbanFieldMetadataFilterValue, diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useAggregateRecordsForHeader.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useAggregateRecordsForHeader.ts index 3f0e1dc57..4a328637a 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useAggregateRecordsForHeader.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useAggregateRecordsForHeader.ts @@ -4,8 +4,10 @@ import { buildRecordGqlFieldsAggregateForView } from '@/object-record/record-boa import { computeAggregateValueAndLabel } from '@/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel'; import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; +import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; import { computeRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeRecordGqlOperationFilter'; +import { turnAnyFieldFilterIntoRecordGqlFilter } from '@/object-record/record-filter/utils/turnAnyFieldFilterIntoRecordGqlFilter'; import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { UserContext } from '@/users/contexts/UserContext'; @@ -54,10 +56,20 @@ export const useAggregateRecordsForHeader = ({ recordIndexKanbanAggregateOperation, }); + const anyFieldFilterValue = useRecoilComponentValueV2( + anyFieldFilterValueComponentState, + ); + + const { recordGqlOperationFilter: anyFieldFilter } = + turnAnyFieldFilterIntoRecordGqlFilter({ + objectMetadataItem, + filterValue: anyFieldFilterValue, + }); + const { data } = useAggregateRecords({ objectNameSingular: objectMetadataItem.nameSingular, recordGqlFieldsAggregate, - filter: { ...requestFilters, ...additionalFilters }, + filter: { ...requestFilters, ...additionalFilters, ...anyFieldFilter }, }); const { value, labelWithFieldName } = computeAggregateValueAndLabel({ diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter.tsx index 1e81ec74d..50afe1ffe 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter.tsx @@ -2,8 +2,10 @@ import { useAggregateRecords } from '@/object-record/hooks/useAggregateRecords'; import { computeAggregateValueAndLabel } from '@/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel'; import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; +import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; import { computeRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeRecordGqlOperationFilter'; +import { turnAnyFieldFilterIntoRecordGqlFilter } from '@/object-record/record-filter/utils/turnAnyFieldFilterIntoRecordGqlFilter'; import { useRecordGroupFilter } from '@/object-record/record-group/hooks/useRecordGroupFilter'; import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; @@ -85,10 +87,20 @@ export const useAggregateRecordsForRecordTableColumnFooter = ( } : {}; + const anyFieldFilterValue = useRecoilComponentValueV2( + anyFieldFilterValueComponentState, + ); + + const { recordGqlOperationFilter: anyFieldFilter } = + turnAnyFieldFilterIntoRecordGqlFilter({ + objectMetadataItem, + filterValue: anyFieldFilterValue, + }); + const { data, loading } = useAggregateRecords({ objectNameSingular: objectMetadataItem.nameSingular, recordGqlFieldsAggregate, - filter: { ...requestFilters, ...recordGroupFilter }, + filter: { ...requestFilters, ...recordGroupFilter, ...anyFieldFilter }, skip: !isDefined(aggregateOperationForViewField), }); diff --git a/packages/twenty-front/src/modules/views/components/AnyFieldSearchChip.tsx b/packages/twenty-front/src/modules/views/components/AnyFieldSearchChip.tsx index c088d4bbb..03ddcbaf4 100644 --- a/packages/twenty-front/src/modules/views/components/AnyFieldSearchChip.tsx +++ b/packages/twenty-front/src/modules/views/components/AnyFieldSearchChip.tsx @@ -1,26 +1,29 @@ +import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState'; import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; import { SortOrFilterChip } from '@/views/components/SortOrFilterChip'; import { ADVANCED_FILTER_DROPDOWN_ID } from '@/views/constants/AdvancedFilterDropdownId'; -import { viewAnyFieldSearchValueComponentState } from '@/views/states/viewAnyFieldSearchValueComponentState'; +import { useLingui } from '@lingui/react/macro'; import { IconFilter } from 'twenty-ui/display'; export const AnyFieldSearchChip = () => { + const { t } = useLingui(); + const { closeDropdown } = useCloseDropdown(); - const [viewAnyFieldSearchValue, setViewAnyFieldSearchValue] = - useRecoilComponentStateV2(viewAnyFieldSearchValueComponentState); + const [anyFieldFilterValue, setAnyFieldFilterValue] = + useRecoilComponentStateV2(anyFieldFilterValueComponentState); const handleRemoveClick = () => { closeDropdown(); - setViewAnyFieldSearchValue(''); + setAnyFieldFilterValue(''); }; return ( { fields: objectMetadataItem.fields, }); + const anyFieldFilterValue = useRecoilComponentValueV2( + anyFieldFilterValueComponentState, + ); + + const { recordGqlOperationFilter: anyFieldFilter } = + turnAnyFieldFilterIntoRecordGqlFilter({ + objectMetadataItem, + filterValue: anyFieldFilterValue, + }); + const { data, loading } = useAggregateRecords<{ id: { COUNT: number }; }>({ objectNameSingular: objectMetadataItem.nameSingular, - filter, + filter: { ...filter, ...anyFieldFilter }, recordGqlFieldsAggregate: { id: [AggregateOperations.COUNT], }, diff --git a/packages/twenty-front/src/modules/views/hooks/useOpenAnyFieldSearchFilterFromViewBar.ts b/packages/twenty-front/src/modules/views/hooks/useOpenAnyFieldSearchFilterFromViewBar.ts index 091c174fd..ae5bad902 100644 --- a/packages/twenty-front/src/modules/views/hooks/useOpenAnyFieldSearchFilterFromViewBar.ts +++ b/packages/twenty-front/src/modules/views/hooks/useOpenAnyFieldSearchFilterFromViewBar.ts @@ -1,15 +1,19 @@ import { objectFilterDropdownAnyFieldSearchIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownAnyFieldSearchIsSelectedComponentState'; import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState'; +import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState'; +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; -import { viewAnyFieldSearchValueComponentState } from '@/views/states/viewAnyFieldSearchValueComponentState'; +import { t } from '@lingui/core/macro'; import { isNonEmptyString } from '@sniptt/guards'; export const useOpenAnyFieldSearchFilterFromViewBar = () => { - const setViewAnyFieldSearchValueComponentState = useSetRecoilComponentStateV2( - viewAnyFieldSearchValueComponentState, + const setAnyFieldFilterValue = useSetRecoilComponentStateV2( + anyFieldFilterValueComponentState, ); + const { objectMetadataItem } = useRecordIndexContextOrThrow(); + const setObjectFilterDropdownAnyFieldSearchIsSelectedComponentState = useSetRecoilComponentStateV2( objectFilterDropdownAnyFieldSearchIsSelectedComponentState, @@ -19,14 +23,29 @@ export const useOpenAnyFieldSearchFilterFromViewBar = () => { objectFilterDropdownSearchInputComponentState, ); + const translatedLabel = t`Search any field`; + const openAnyFieldSearchFilterFromViewBar = () => { const userHasAlreadyEnteredSearchInputForObjectDropdownSearch = isNonEmptyString(objectFilterDropdownSearchInput); - if (userHasAlreadyEnteredSearchInputForObjectDropdownSearch) { + const userInputIsMatchingAListMenuItem = + objectMetadataItem.fields.some((fieldMetadataItem) => + fieldMetadataItem.label + .toLocaleLowerCase() + .includes(objectFilterDropdownSearchInput.toLocaleLowerCase()), + ) || + translatedLabel + .toLocaleLowerCase() + .includes(objectFilterDropdownSearchInput.toLocaleLowerCase()); + + if ( + userHasAlreadyEnteredSearchInputForObjectDropdownSearch && + !userInputIsMatchingAListMenuItem + ) { const filterValue = objectFilterDropdownSearchInput; - setViewAnyFieldSearchValueComponentState(filterValue); + setAnyFieldFilterValue(filterValue); } setObjectFilterDropdownAnyFieldSearchIsSelectedComponentState(true); diff --git a/packages/twenty-front/src/modules/views/states/viewAnyFieldSearchValueComponentState.ts b/packages/twenty-front/src/modules/views/states/viewAnyFieldSearchValueComponentState.ts deleted file mode 100644 index 57a450ef1..000000000 --- a/packages/twenty-front/src/modules/views/states/viewAnyFieldSearchValueComponentState.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; -import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; - -export const viewAnyFieldSearchValueComponentState = - createComponentStateV2({ - key: 'viewAnyFieldSearchValueComponentState', - defaultValue: '', - componentInstanceContext: ViewComponentInstanceContext, - }); diff --git a/packages/twenty-front/src/pages/object-record/__stories__/RecordShowPage.stories.tsx b/packages/twenty-front/src/pages/object-record/__stories__/RecordShowPage.stories.tsx index 16570cdd0..633a30ae1 100644 --- a/packages/twenty-front/src/pages/object-record/__stories__/RecordShowPage.stories.tsx +++ b/packages/twenty-front/src/pages/object-record/__stories__/RecordShowPage.stories.tsx @@ -14,6 +14,7 @@ import { } from '~/testing/mock-data/people'; import { mockedWorkspaceMemberData } from '~/testing/mock-data/users'; +import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator'; import { RecordShowPage } from '../RecordShowPage'; const personRecord = allMockPersonRecords[0]; @@ -62,7 +63,7 @@ export type Story = StoryObj; export const Default: Story = { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - decorators: [PageDecorator], + decorators: [PageDecorator, ContextStoreDecorator], play: async ({ canvasElement }) => { const canvas = within(canvasElement);