From 0d0f7e67a67848c3c70009f2397338d110bccdf4 Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Tue, 19 Nov 2024 14:11:38 +0100 Subject: [PATCH] Add custom objects to command menu search + use ilike for notes search (#8564) In this PR - Re-introduce previously used search based on "ILIKE" queries for search on notes since the tsvector search with json text is not working correctly (@charlesBochet) - Add search on custom objects in Command Menu bar (closes https://github.com/twentyhq/twenty/issues/8522) https://github.com/user-attachments/assets/0cc064cf-889d-4f2c-8747-6d8670f35a39 --- .../command-menu/components/CommandMenu.tsx | 424 +++++++++++------- .../src/modules/command-menu/types/Command.ts | 2 +- .../types/CoreObjectNamePlural.ts | 37 ++ ...EditModeMultiRecordsSearchFilterEffect.tsx | 22 +- ...ResultFormattedAsObjectRecordsMap.test.tsx | 97 ++++ ...FilterQuery.ts => useMultiObjectSearch.ts} | 15 +- ...hQueryResultFormattedAsObjectRecordsMap.ts | 61 +++ .../twenty-front/src/testing/graphqlMocks.ts | 39 ++ 8 files changed, 522 insertions(+), 175 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-metadata/types/CoreObjectNamePlural.ts create mode 100644 packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap.test.tsx rename packages/twenty-front/src/modules/object-record/relation-picker/hooks/{useMultiObjectSearchMatchesSearchFilterQuery.ts => useMultiObjectSearch.ts} (79%) create mode 100644 packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap.ts diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx index a7b56427e0..0e3d2bdce7 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx @@ -2,6 +2,7 @@ import { useOpenCopilotRightDrawer } from '@/activities/copilot/right-drawer/hoo import { copilotQueryState } from '@/activities/copilot/right-drawer/states/copilotQueryState'; import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer'; import { Note } from '@/activities/types/Note'; +import { Task } from '@/activities/types/Task'; import { CommandGroup } from '@/command-menu/components/CommandGroup'; import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; @@ -12,11 +13,13 @@ import { Command, CommandType } from '@/command-menu/types/Command'; import { Company } from '@/companies/types/Company'; import { mainContextStoreComponentInstanceIdState } from '@/context-store/states/mainContextStoreComponentInstanceId'; import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu'; +import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { getCompanyDomainName } from '@/object-metadata/utils/getCompanyDomainName'; -import { useSearchRecords } from '@/object-record/hooks/useSearchRecords'; -import { Opportunity } from '@/opportunities/types/Opportunity'; -import { Person } from '@/people/types/Person'; +import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { useMultiObjectSearch } from '@/object-record/relation-picker/hooks/useMultiObjectSearch'; +import { useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap } from '@/object-record/relation-picker/hooks/useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap'; +import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables'; import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem'; import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; @@ -27,11 +30,14 @@ import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import styled from '@emotion/styled'; import { isNonEmptyString } from '@sniptt/guards'; +import isEmpty from 'lodash.isempty'; import { useMemo, useRef } from 'react'; import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { Key } from 'ts-key-enum'; import { Avatar, + IconCheckbox, + IconComponent, IconNotes, IconSparkles, IconX, @@ -40,11 +46,25 @@ import { } from 'twenty-ui'; import { useDebounce } from 'use-debounce'; import { getLogoUrlFromDomainName } from '~/utils'; +import { capitalize } from '~/utils/string/capitalize'; const SEARCH_BAR_HEIGHT = 56; const SEARCH_BAR_PADDING = 3; const MOBILE_NAVIGATION_BAR_HEIGHT = 64; +type CommandGroupConfig = { + heading: string; + items?: any[]; + renderItem: (item: any) => { + id: string; + Icon?: IconComponent; + label: string; + to?: string; + onClick?: () => void; + key?: string; + }; +}; + const StyledCommandMenu = styled.div` background: ${({ theme }) => theme.background.secondary}; border-left: 1px solid ${({ theme }) => theme.border.color.medium}; @@ -170,37 +190,68 @@ export const CommandMenu = () => { [closeCommandMenu], ); - const { loading: isPeopleLoading, records: people } = - useSearchRecords({ - skip: !isCommandMenuOpened, - objectNameSingular: CoreObjectNameSingular.Person, - limit: 3, - searchInput: deferredCommandMenuSearch ?? undefined, - }); - - const { loading: isCompaniesLoading, records: companies } = - useSearchRecords({ - skip: !isCommandMenuOpened, - objectNameSingular: CoreObjectNameSingular.Company, - limit: 3, - searchInput: deferredCommandMenuSearch ?? undefined, - }); - - const { loading: isNotesLoading, records: notes } = useSearchRecords({ - skip: !isCommandMenuOpened, - objectNameSingular: CoreObjectNameSingular.Note, + const { + matchesSearchFilterObjectRecordsQueryResult, + matchesSearchFilterObjectRecordsLoading: loading, + } = useMultiObjectSearch({ + excludedObjects: [CoreObjectNameSingular.Task, CoreObjectNameSingular.Note], + searchFilterValue: deferredCommandMenuSearch ?? undefined, limit: 3, - searchInput: deferredCommandMenuSearch ?? undefined, }); - const { loading: isOpportunitiesLoading, records: opportunities } = - useSearchRecords({ - skip: !isCommandMenuOpened, - objectNameSingular: CoreObjectNameSingular.Opportunity, - limit: 3, - searchInput: deferredCommandMenuSearch ?? undefined, + const { objectRecordsMap: matchesSearchFilterObjectRecords } = + useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap({ + multiObjectRecordsQueryResult: + matchesSearchFilterObjectRecordsQueryResult, }); + const { loading: isNotesLoading, records: notes } = useFindManyRecords({ + skip: !isCommandMenuOpened, + objectNameSingular: CoreObjectNameSingular.Note, + filter: deferredCommandMenuSearch + ? makeOrFilterVariables([ + { title: { ilike: `%${deferredCommandMenuSearch}%` } }, + { body: { ilike: `%${deferredCommandMenuSearch}%` } }, + ]) + : undefined, + limit: 3, + }); + + const { loading: isTasksLoading, records: tasks } = useFindManyRecords({ + skip: !isCommandMenuOpened, + objectNameSingular: CoreObjectNameSingular.Task, + filter: deferredCommandMenuSearch + ? makeOrFilterVariables([ + { title: { ilike: `%${deferredCommandMenuSearch}%` } }, + { body: { ilike: `%${deferredCommandMenuSearch}%` } }, + ]) + : undefined, + limit: 3, + }); + + const people = matchesSearchFilterObjectRecords.people?.map( + (people) => people.record, + ); + const companies = matchesSearchFilterObjectRecords.companies?.map( + (companies) => companies.record, + ); + const opportunities = matchesSearchFilterObjectRecords.opportunities?.map( + (opportunities) => opportunities.record, + ); + + const customObjectRecordsMap = useMemo(() => { + return Object.fromEntries( + Object.entries(matchesSearchFilterObjectRecords).filter( + ([namePlural, records]) => + ![ + CoreObjectNamePlural.Person, + CoreObjectNamePlural.Opportunity, + CoreObjectNamePlural.Company, + ].includes(namePlural as CoreObjectNamePlural) && !isEmpty(records), + ), + ); + }, [matchesSearchFilterObjectRecords]); + const peopleCommands = useMemo( () => people?.map(({ id, name: { firstName, lastName } }) => ({ @@ -242,6 +293,32 @@ export const CommandMenu = () => { [notes, openActivityRightDrawer], ); + const tasksCommands = useMemo( + () => + tasks?.map((task) => ({ + id: task.id, + label: task.title ?? '', + to: '', + onCommandClick: () => openActivityRightDrawer(task.id), + })), + [tasks, openActivityRightDrawer], + ); + + const customObjectCommands = useMemo(() => { + const customObjectCommandsArray: Command[] = []; + Object.values(customObjectRecordsMap).forEach((objectRecords) => { + customObjectCommandsArray.push( + ...objectRecords.map((objectRecord) => ({ + id: objectRecord.record.id, + label: objectRecord.recordIdentifier.name, + to: `object/${objectRecord.objectMetadataItem.nameSingular}/${objectRecord.record.id}`, + })), + ); + }); + + return customObjectCommandsArray; + }, [customObjectRecordsMap]); + const otherCommands = useMemo(() => { const commandsArray: Command[] = []; if (peopleCommands?.length > 0) { @@ -256,8 +333,21 @@ export const CommandMenu = () => { if (noteCommands?.length > 0) { commandsArray.push(...(noteCommands as Command[])); } + if (tasksCommands?.length > 0) { + commandsArray.push(...(tasksCommands as Command[])); + } + if (customObjectCommands?.length > 0) { + commandsArray.push(...(customObjectCommands as Command[])); + } return commandsArray; - }, [peopleCommands, companyCommands, noteCommands, opportunityCommands]); + }, [ + peopleCommands, + companyCommands, + opportunityCommands, + noteCommands, + customObjectCommands, + tasksCommands, + ]); const checkInShortcuts = (cmd: Command, search: string) => { return (cmd.firstHotKey + (cmd.secondHotKey ?? '')) @@ -335,7 +425,15 @@ export const CommandMenu = () => { .concat(people?.map((person) => person.id)) .concat(companies?.map((company) => company.id)) .concat(opportunities?.map((opportunity) => opportunity.id)) - .concat(notes?.map((note) => note.id)); + .concat(notes?.map((note) => note.id)) + .concat(tasks?.map((task) => task.id)) + .concat( + Object.values(customObjectRecordsMap) + ?.map((objectRecords) => + objectRecords.map((objectRecord) => objectRecord.record.id), + ) + .flat() ?? [], + ); const isNoResults = !matchingStandardActionCommands.length && @@ -345,18 +443,133 @@ export const CommandMenu = () => { !people?.length && !companies?.length && !notes?.length && - !opportunities?.length; + !tasks?.length && + !opportunities?.length && + isEmpty(customObjectRecordsMap); - const isLoading = - isPeopleLoading || - isNotesLoading || - isOpportunitiesLoading || - isCompaniesLoading; + const isLoading = loading || isNotesLoading || isTasksLoading; const mainContextStoreComponentInstanceId = useRecoilValue( mainContextStoreComponentInstanceIdState, ); + const commandGroups: CommandGroupConfig[] = [ + { + heading: 'Navigate', + items: matchingNavigateCommand, + renderItem: (command) => ({ + id: command.id, + Icon: command.Icon, + label: command.label, + to: command.to, + onClick: command.onCommandClick, + }), + }, + { + heading: 'Other', + items: matchingCreateCommand, + renderItem: (command) => ({ + id: command.id, + Icon: command.Icon, + label: command.label, + to: command.to, + onClick: command.onCommandClick, + }), + }, + { + heading: 'People', + items: people, + renderItem: (person) => ({ + id: person.id, + label: `${person.name.firstName} ${person.name.lastName}`, + to: `object/person/${person.id}`, + Icon: () => ( + + ), + }), + }, + { + heading: 'Companies', + items: companies, + renderItem: (company) => ({ + id: company.id, + label: company.name, + to: `object/company/${company.id}`, + Icon: () => ( + + ), + }), + }, + { + heading: 'Opportunities', + items: opportunities, + renderItem: (opportunity) => ({ + id: opportunity.id, + label: opportunity.name ?? '', + to: `object/opportunity/${opportunity.id}`, + Icon: () => ( + + ), + }), + }, + { + heading: 'Notes', + items: notes, + renderItem: (note) => ({ + id: note.id, + Icon: IconNotes, + label: note.title ?? '', + onClick: () => openActivityRightDrawer(note.id), + }), + }, + { + heading: 'Tasks', + items: tasks, + renderItem: (task) => ({ + id: task.id, + Icon: IconCheckbox, + label: task.title ?? '', + onClick: () => openActivityRightDrawer(task.id), + }), + }, + ...Object.entries(customObjectRecordsMap).map( + ([customObjectNamePlural, objectRecords]): CommandGroupConfig => ({ + heading: capitalize(customObjectNamePlural), + items: objectRecords, + renderItem: (objectRecord) => ({ + key: objectRecord.record.id, + id: objectRecord.record.id, + label: objectRecord.recordIdentifier.name, + to: `object/${objectRecord.objectMetadataItem.nameSingular}/${objectRecord.record.id}`, + Icon: () => ( + + ), + }), + }), + ), + ]; + return ( <> {isCommandMenuOpened && ( @@ -457,121 +670,28 @@ export const CommandMenu = () => { )} - - {matchingNavigateCommand.map((cmd) => ( - - - - ))} - - - {matchingCreateCommand.map((cmd) => ( - - - - ))} - - - {people?.map((person) => ( - - ( - - )} - /> - - ))} - - - {companies?.map((company) => ( - - ( - - )} - /> - - ))} - - - {opportunities?.map((opportunity) => ( - - ( - - )} - /> - - ))} - - - {notes?.map((note) => ( - - openActivityRightDrawer(note.id)} - /> - - ))} - + {commandGroups.map(({ heading, items, renderItem }) => + items?.length ? ( + + {items.map((item) => { + const { id, Icon, label, to, onClick, key } = + renderItem(item); + return ( + + + + ); + })} + + ) : null, + )} diff --git a/packages/twenty-front/src/modules/command-menu/types/Command.ts b/packages/twenty-front/src/modules/command-menu/types/Command.ts index 181ec43918..7394669033 100644 --- a/packages/twenty-front/src/modules/command-menu/types/Command.ts +++ b/packages/twenty-front/src/modules/command-menu/types/Command.ts @@ -11,7 +11,7 @@ export type Command = { id: string; to?: string; label: string; - type: + type?: | CommandType.Navigate | CommandType.Create | CommandType.StandardAction diff --git a/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNamePlural.ts b/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNamePlural.ts new file mode 100644 index 0000000000..c5a92ed2f0 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNamePlural.ts @@ -0,0 +1,37 @@ +export enum CoreObjectNamePlural { + Activity = 'activities', + ActivityTarget = 'activityTargets', + ApiKey = 'apiKeys', + Attachment = 'attachments', + Blocklist = 'blocklists', + CalendarChannel = 'calendarChannels', + CalendarEvent = 'calendarEvents', + Comment = 'comments', + Company = 'companies', + ConnectedAccount = 'connectedAccounts', + TimelineActivity = 'timelineActivities', + Favorite = 'favorites', + Message = 'messages', + MessageChannel = 'messageChannels', + MessageParticipant = 'messageParticipants', + MessageThread = 'messageThreads', + Note = 'notes', + NoteTarget = 'noteTargets', + Opportunity = 'opportunities', + Person = 'people', + Task = 'tasks', + TaskTarget = 'taskTargets', + View = 'views', + ViewField = 'viewFields', + ViewFilter = 'viewFilters', + ViewFilterGroup = 'viewFilterGroups', + ViewSort = 'viewSorts', + ViewGroup = 'viewGroups', + Webhook = 'webhooks', + WorkspaceMember = 'workspaceMembers', + MessageThreadSubscriber = 'messageThreadSubscribers', + Workflow = 'workflows', + MessageChannelMessageAssociation = 'messageChannelMessageAssociations', + WorkflowVersion = 'workflowVersions', + WorkflowRun = 'workflowRuns', +} diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect.tsx index c216122c14..347bc81b79 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/components/ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect.tsx @@ -4,7 +4,8 @@ import { useRecoilValue, useSetRecoilState } from 'recoil'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { objectRecordMultiSelectMatchesFilterRecordsIdsComponentState } from '@/object-record/record-field/states/objectRecordMultiSelectMatchesFilterRecordsIdsComponentState'; import { useRelationPickerScopedStates } from '@/object-record/relation-picker/hooks/internal/useRelationPickerScopedStates'; -import { useMultiObjectSearchMatchesSearchFilterQuery } from '@/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterQuery'; +import { useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray } from '@/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray'; +import { useMultiObjectSearch } from '@/object-record/relation-picker/hooks/useMultiObjectSearch'; import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext'; import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; @@ -31,8 +32,8 @@ export const ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect = relationPickerSearchFilterState, ); - const { matchesSearchFilterObjectRecords } = - useMultiObjectSearchMatchesSearchFilterQuery({ + const { matchesSearchFilterObjectRecordsQueryResult } = + useMultiObjectSearch({ excludedObjects: [ CoreObjectNameSingular.Task, CoreObjectNameSingular.Note, @@ -41,14 +42,15 @@ export const ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect = limit: 10, }); + const { objectRecordForSelectArray } = + useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray({ + multiObjectRecordsQueryResult: + matchesSearchFilterObjectRecordsQueryResult, + }); + useEffect(() => { - setRecordMultiSelectMatchesFilterRecords( - matchesSearchFilterObjectRecords, - ); - }, [ - setRecordMultiSelectMatchesFilterRecords, - matchesSearchFilterObjectRecords, - ]); + setRecordMultiSelectMatchesFilterRecords(objectRecordForSelectArray); + }, [setRecordMultiSelectMatchesFilterRecords, objectRecordForSelectArray]); return <>; }; diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap.test.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap.test.tsx new file mode 100644 index 0000000000..53bd5e2cbf --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap.test.tsx @@ -0,0 +1,97 @@ +import { act, renderHook } from '@testing-library/react'; +import { RecoilRoot, useSetRecoilState } from 'recoil'; + +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap } from '@/object-record/relation-picker/hooks/useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap'; +import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; + +const scopeId = 'scopeId'; +const Wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + +); + +const opportunityId = 'cb702502-4b1d-488e-9461-df3fb096ebf6'; +const personId = 'ab091fd9-1b81-4dfd-bfdb-564ffee032a2'; + +describe('useMultiObjectRecordsQueryResultFormattedAsObjectRecordsMap', () => { + it('should return object formatted from objectMetadataItemsState', async () => { + const { result } = renderHook( + () => { + return { + formattedRecord: + useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap({ + multiObjectRecordsQueryResult: { + opportunities: { + edges: [ + { + node: { + id: opportunityId, + pointOfContactId: + 'e992bda7-d797-4e12-af04-9b427f42244c', + updatedAt: '2023-11-30T11:13:15.308Z', + createdAt: '2023-11-30T11:13:15.308Z', + __typename: 'Opportunity', + }, + cursor: 'cursor', + __typename: 'OpportunityEdge', + }, + ], + pageInfo: {}, + }, + people: { + edges: [ + { + node: { + id: personId, + updatedAt: '2023-11-30T11:13:15.308Z', + createdAt: '2023-11-30T11:13:15.308Z', + __typename: 'Person', + }, + cursor: 'cursor', + __typename: 'PersonEdge', + }, + ], + pageInfo: {}, + }, + }, + }), + setObjectMetadata: useSetRecoilState(objectMetadataItemsState), + }; + }, + { + wrapper: Wrapper, + }, + ); + act(() => { + result.current.setObjectMetadata(generatedMockObjectMetadataItems); + }); + + expect( + Object.values(result.current.formattedRecord.objectRecordsMap).flat() + .length, + ).toBe(2); + + const opportunityObjectRecords = + result.current.formattedRecord.objectRecordsMap.opportunities; + + const personObjectRecords = + result.current.formattedRecord.objectRecordsMap.people; + + expect(opportunityObjectRecords[0].objectMetadataItem.namePlural).toBe( + 'opportunities', + ); + expect(opportunityObjectRecords[0].record.id).toBe(opportunityId); + expect(opportunityObjectRecords[0].recordIdentifier.linkToShowPage).toBe( + `/object/opportunity/${opportunityId}`, + ); + + expect(personObjectRecords[0].objectMetadataItem.namePlural).toBe('people'); + expect(personObjectRecords[0].record.id).toBe(personId); + expect(personObjectRecords[0].recordIdentifier.linkToShowPage).toBe( + `/object/person/${personId}`, + ); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterQuery.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearch.ts similarity index 79% rename from packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterQuery.ts rename to packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearch.ts index a8bdfec354..07d5e0d9cd 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterQuery.ts +++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearch.ts @@ -6,14 +6,11 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery'; import { useGenerateCombinedSearchRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedSearchRecordsQuery'; import { useLimitPerMetadataItem } from '@/object-record/relation-picker/hooks/useLimitPerMetadataItem'; -import { - MultiObjectRecordQueryResult, - useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray, -} from '@/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray'; +import { MultiObjectRecordQueryResult } from '@/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray'; import { isObjectMetadataItemSearchableInCombinedRequest } from '@/object-record/utils/isObjectMetadataItemSearchableInCombinedRequest'; import { isDefined } from '~/utils/isDefined'; -export const useMultiObjectSearchMatchesSearchFilterQuery = ({ +export const useMultiObjectSearch = ({ searchFilterValue, limit, excludedObjects, @@ -62,14 +59,8 @@ export const useMultiObjectSearchMatchesSearchFilterQuery = ({ }, ); - const { objectRecordForSelectArray: matchesSearchFilterObjectRecords } = - useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray({ - multiObjectRecordsQueryResult: - matchesSearchFilterObjectRecordsQueryResult, - }); - return { matchesSearchFilterObjectRecordsLoading, - matchesSearchFilterObjectRecords, + matchesSearchFilterObjectRecordsQueryResult, }; }; diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap.ts new file mode 100644 index 0000000000..5945af71d3 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap.ts @@ -0,0 +1,61 @@ +import { useMemo } from 'react'; +import { useRecoilValue } from 'recoil'; + +import { objectMetadataItemsByNamePluralMapSelector } from '@/object-metadata/states/objectMetadataItemsByNamePluralMapSelector'; +import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier'; +import { MultiObjectRecordQueryResult } from '@/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray'; +import { formatMultiObjectRecordSearchResults } from '@/object-record/relation-picker/utils/formatMultiObjectRecordSearchResults'; +import { ObjectRecordForSelect } from '@/object-record/types/ObjectRecordForSelect'; +import { isDefined } from '~/utils/isDefined'; + +export const useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap = ({ + multiObjectRecordsQueryResult, +}: { + multiObjectRecordsQueryResult: + | MultiObjectRecordQueryResult + | null + | undefined; +}) => { + const objectMetadataItemsByNamePluralMap = useRecoilValue( + objectMetadataItemsByNamePluralMapSelector, + ); + + const formattedMultiObjectRecordsQueryResult = useMemo(() => { + return formatMultiObjectRecordSearchResults(multiObjectRecordsQueryResult); + }, [multiObjectRecordsQueryResult]); + + const objectRecordsMap = useMemo(() => { + const recordsByNamePlural: { [key: string]: ObjectRecordForSelect[] } = {}; + Object.entries(formattedMultiObjectRecordsQueryResult ?? {}).forEach( + ([namePlural, objectRecordConnection]) => { + const objectMetadataItem = + objectMetadataItemsByNamePluralMap.get(namePlural); + + if (!isDefined(objectMetadataItem)) return []; + if (!isDefined(recordsByNamePlural[namePlural])) { + recordsByNamePlural[namePlural] = []; + } + + objectRecordConnection.edges.forEach(({ node }) => { + const record = { + objectMetadataItem, + record: node, + recordIdentifier: getObjectRecordIdentifier({ + objectMetadataItem, + record: node, + }), + } as ObjectRecordForSelect; + recordsByNamePlural[namePlural].push(record); + }); + }, + ); + return recordsByNamePlural; + }, [ + formattedMultiObjectRecordsQueryResult, + objectMetadataItemsByNamePluralMap, + ]); + + return { + objectRecordsMap, + }; +}; diff --git a/packages/twenty-front/src/testing/graphqlMocks.ts b/packages/twenty-front/src/testing/graphqlMocks.ts index c3f66bc1eb..da49b618de 100644 --- a/packages/twenty-front/src/testing/graphqlMocks.ts +++ b/packages/twenty-front/src/testing/graphqlMocks.ts @@ -114,6 +114,45 @@ export const graphqlMocks = { }, }); }), + graphql.query('CombinedSearchRecords', () => { + return HttpResponse.json({ + data: { + searchOpportunities: { + edges: [], + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + startCursor: null, + endCursor: null, + }, + }, + searchCompanies: { + edges: companiesMock.slice(0, 3).map((company) => ({ + node: company, + cursor: null, + })), + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + startCursor: null, + endCursor: null, + }, + }, + searchPeople: { + edges: peopleMock.slice(0, 3).map((person) => ({ + node: person, + cursor: null, + })), + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + startCursor: null, + endCursor: null, + }, + }, + }, + }); + }), graphql.query('FindManyViews', ({ variables }) => { const objectMetadataId = variables.filter?.objectMetadataId?.eq; const viewType = variables.filter?.type?.eq;