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);