diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 744d044ff..555c215a9 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -2418,7 +2418,9 @@ export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typ export type GlobalSearchQueryVariables = Exact<{ searchInput: Scalars['String']; limit: Scalars['Int']; - excludedObjectNameSingulars: Array | Scalars['String']; + excludedObjectNameSingulars?: InputMaybe | Scalars['String']>; + includedObjectNameSingulars?: InputMaybe | Scalars['String']>; + filter?: InputMaybe; }>; @@ -4095,11 +4097,13 @@ export type GetClientConfigQueryHookResult = ReturnType; export type GetClientConfigQueryResult = Apollo.QueryResult; export const GlobalSearchDocument = gql` - query GlobalSearch($searchInput: String!, $limit: Int!, $excludedObjectNameSingulars: [String!]!) { + query GlobalSearch($searchInput: String!, $limit: Int!, $excludedObjectNameSingulars: [String!], $includedObjectNameSingulars: [String!], $filter: ObjectRecordFilterInput) { globalSearch( searchInput: $searchInput limit: $limit excludedObjectNameSingulars: $excludedObjectNameSingulars + includedObjectNameSingulars: $includedObjectNameSingulars + filter: $filter ) { recordId objectSingularName @@ -4126,6 +4130,8 @@ export const GlobalSearchDocument = gql` * searchInput: // value for 'searchInput' * limit: // value for 'limit' * excludedObjectNameSingulars: // value for 'excludedObjectNameSingulars' + * includedObjectNameSingulars: // value for 'includedObjectNameSingulars' + * filter: // value for 'filter' * }, * }); */ diff --git a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useUpdateActivityTargetFromInlineCell.ts b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useUpdateActivityTargetFromInlineCell.ts index d96caaffc..46a6e6aec 100644 --- a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useUpdateActivityTargetFromInlineCell.ts +++ b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useUpdateActivityTargetFromInlineCell.ts @@ -6,6 +6,7 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; +import { searchRecordStoreComponentFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState'; import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { isNull } from '@sniptt/guards'; @@ -47,6 +48,7 @@ export const useUpdateActivityTargetFromInlineCell = ({ async ({ morphItem, activityTargetWithTargetRecords, + recordPickerInstanceId, }: UpdateActivityTargetFromInlineCellProps) => { const targetObjectName = activityObjectNameSingular === CoreObjectNameSingular.Task @@ -92,11 +94,16 @@ export const useUpdateActivityTargetFromInlineCell = ({ ); } } else { - const targetRecord = snapshot - .getLoadable(recordStoreFamilyState(morphItem.recordId)) + const searchRecord = snapshot + .getLoadable( + searchRecordStoreComponentFamilyState.atomFamily({ + instanceId: recordPickerInstanceId, + familyKey: morphItem.recordId, + }), + ) .getValue(); - if (!isDefined(targetRecord)) { + if (!isDefined(searchRecord) || !isDefined(searchRecord?.record)) { return; } @@ -104,6 +111,8 @@ export const useUpdateActivityTargetFromInlineCell = ({ return; } + const targetRecord = searchRecord.record; + const activityTarget = activityObjectNameSingular === CoreObjectNameSingular.Task ? { diff --git a/packages/twenty-front/src/modules/command-menu/constants/MaxSearchResults.ts b/packages/twenty-front/src/modules/command-menu/constants/MaxSearchResults.ts new file mode 100644 index 000000000..9441c9c1d --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/constants/MaxSearchResults.ts @@ -0,0 +1 @@ +export const MAX_SEARCH_RESULTS = 30; diff --git a/packages/twenty-front/src/modules/command-menu/graphql/queries/globalSearch.ts b/packages/twenty-front/src/modules/command-menu/graphql/queries/globalSearch.ts index 66f79b4e3..4407502b0 100644 --- a/packages/twenty-front/src/modules/command-menu/graphql/queries/globalSearch.ts +++ b/packages/twenty-front/src/modules/command-menu/graphql/queries/globalSearch.ts @@ -4,12 +4,16 @@ export const globalSearch = gql` query GlobalSearch( $searchInput: String! $limit: Int! - $excludedObjectNameSingulars: [String!]! + $excludedObjectNameSingulars: [String!] + $includedObjectNameSingulars: [String!] + $filter: ObjectRecordFilterInput ) { globalSearch( searchInput: $searchInput limit: $limit excludedObjectNameSingulars: $excludedObjectNameSingulars + includedObjectNameSingulars: $includedObjectNameSingulars + filter: $filter ) { recordId objectSingularName diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useSearchRecords.tsx b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuSearchRecords.tsx similarity index 95% rename from packages/twenty-front/src/modules/command-menu/hooks/useSearchRecords.tsx rename to packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuSearchRecords.tsx index 22690b1f1..9a26d15ae 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useSearchRecords.tsx +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuSearchRecords.tsx @@ -1,3 +1,4 @@ +import { MAX_SEARCH_RESULTS } from '@/command-menu/constants/MaxSearchResults'; import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu'; import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; @@ -9,9 +10,7 @@ import { Avatar } from 'twenty-ui'; import { useDebounce } from 'use-debounce'; import { useGlobalSearchQuery } from '~/generated/graphql'; -const MAX_SEARCH_RESULTS = 30; - -export const useSearchRecords = () => { +export const useCommandMenuSearchRecords = () => { const commandMenuSearch = useRecoilValue(commandMenuSearchState); const [deferredCommandMenuSearch] = useDebounce(commandMenuSearch, 300); diff --git a/packages/twenty-front/src/modules/command-menu/pages/search/components/CommandMenuSearchRecordsPage.tsx b/packages/twenty-front/src/modules/command-menu/pages/search/components/CommandMenuSearchRecordsPage.tsx index 75e17e437..7d6590f0b 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/search/components/CommandMenuSearchRecordsPage.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/search/components/CommandMenuSearchRecordsPage.tsx @@ -1,9 +1,9 @@ import { CommandMenuList } from '@/command-menu/components/CommandMenuList'; -import { useSearchRecords } from '@/command-menu/hooks/useSearchRecords'; +import { useCommandMenuSearchRecords } from '@/command-menu/hooks/useCommandMenuSearchRecords'; import { useMemo } from 'react'; export const CommandMenuSearchRecordsPage = () => { - const { commandGroups, loading, noResults } = useSearchRecords(); + const { commandGroups, loading, noResults } = useCommandMenuSearchRecords(); const selectableItemIds = useMemo(() => { return commandGroups.flatMap((group) => group.items).map((item) => item.id); diff --git a/packages/twenty-front/src/modules/object-metadata/utils/formatGlobalSearchRecordAsSingleRecordPickerRecord.ts b/packages/twenty-front/src/modules/object-metadata/utils/formatGlobalSearchRecordAsSingleRecordPickerRecord.ts new file mode 100644 index 000000000..1d38d6a78 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/utils/formatGlobalSearchRecordAsSingleRecordPickerRecord.ts @@ -0,0 +1,23 @@ +import { getAvatarType } from '@/object-metadata/utils/getAvatarType'; +import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage'; +import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord'; +import { GlobalSearchRecord } from '~/generated/graphql'; + +export const formatGlobalSearchRecordAsSingleRecordPickerRecord = ( + searchRecord: GlobalSearchRecord, +): SingleRecordPickerRecord => { + return { + id: searchRecord.recordId, + name: searchRecord.label, + avatarUrl: searchRecord.imageUrl ?? undefined, + avatarType: getAvatarType(searchRecord.objectSingularName), + linkToShowPage: + getBasePathToShowPage({ + objectNameSingular: searchRecord.objectSingularName, + }) + searchRecord.recordId, + record: { + id: searchRecord.recordId, + __typename: searchRecord.objectSingularName, + }, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useObjectRecordSearchRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useObjectRecordSearchRecords.ts new file mode 100644 index 000000000..579d2b5ec --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/useObjectRecordSearchRecords.ts @@ -0,0 +1,81 @@ +import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; +import { MAX_SEARCH_RESULTS } from '@/command-menu/constants/MaxSearchResults'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; +import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { WatchQueryFetchPolicy } from '@apollo/client'; +import { useMemo } from 'react'; +import { useRecoilValue } from 'recoil'; +import { isDefined } from 'twenty-shared'; +import { + ObjectRecordFilterInput, + useGlobalSearchQuery, +} from '~/generated/graphql'; +import { logError } from '~/utils/logError'; + +export type UseSearchRecordsParams = ObjectMetadataItemIdentifier & { + limit?: number; + onError?: (error?: Error) => void; + skip?: boolean; + fetchPolicy?: WatchQueryFetchPolicy; + searchInput?: string; + filter?: ObjectRecordFilterInput; +}; + +export const useObjectRecordSearchRecords = ({ + objectNameSingular, + searchInput, + limit, + skip, + filter, + fetchPolicy, +}: UseSearchRecordsParams) => { + const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + + const { enqueueSnackBar } = useSnackBar(); + + const { data, loading, error, previousData } = useGlobalSearchQuery({ + skip: + skip || + !objectMetadataItem || + !currentWorkspaceMember || + !isDefined(searchInput), + variables: { + searchInput: searchInput ?? '', + limit: limit ?? MAX_SEARCH_RESULTS, + filter: filter ?? {}, + includedObjectNameSingulars: [objectNameSingular], + }, + fetchPolicy: fetchPolicy, + onError: (error) => { + logError( + `useGlobalSearchRecords for "${objectMetadataItem.namePlural}" error : ` + + error, + ); + enqueueSnackBar( + `Error during useGlobalSearchRecords for "${objectMetadataItem.namePlural}", ${error.message}`, + { + variant: SnackBarVariant.Error, + }, + ); + }, + }); + + const effectiveData = loading ? previousData : data; + + const searchRecords = useMemo( + () => effectiveData?.globalSearch || [], + [effectiveData], + ); + + return { + objectMetadataItem, + searchRecords, + loading, + error, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useSearchRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useSearchRecords.ts deleted file mode 100644 index 1e8dae6bf..000000000 --- a/packages/twenty-front/src/modules/object-record/hooks/useSearchRecords.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; -import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; -import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; -import { RecordGqlOperationSearchResult } from '@/object-record/graphql/types/RecordGqlOperationSearchResult'; -import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables'; -import { useSearchRecordsQuery } from '@/object-record/hooks/useSearchRecordsQuery'; -import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { getSearchRecordsQueryResponseField } from '@/object-record/utils/getSearchRecordsQueryResponseField'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; -import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { useQuery, WatchQueryFetchPolicy } from '@apollo/client'; -import { useMemo } from 'react'; -import { useRecoilValue } from 'recoil'; -import { isDefined } from 'twenty-shared'; -import { logError } from '~/utils/logError'; - -export type UseSearchRecordsParams = ObjectMetadataItemIdentifier & - Pick & { - onError?: (error?: Error) => void; - skip?: boolean; - recordGqlFields?: RecordGqlOperationGqlRecordFields; - fetchPolicy?: WatchQueryFetchPolicy; - searchInput?: string; - }; - -export const useSearchRecords = ({ - objectNameSingular, - searchInput, - limit, - skip, - filter, - recordGqlFields, - fetchPolicy, -}: UseSearchRecordsParams) => { - const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); - const { objectMetadataItem } = useObjectMetadataItem({ - objectNameSingular, - }); - const { searchRecordsQuery } = useSearchRecordsQuery({ - objectNameSingular, - recordGqlFields, - }); - - const { enqueueSnackBar } = useSnackBar(); - const { data, loading, error, previousData } = - useQuery(searchRecordsQuery, { - skip: - skip || - !objectMetadataItem || - !currentWorkspaceMember || - !isDefined(searchInput), - variables: { - search: searchInput, - limit: limit, - filter: filter, - }, - fetchPolicy: fetchPolicy, - onError: (error) => { - logError( - `useSearchRecords for "${objectMetadataItem.namePlural}" error : ` + - error, - ); - enqueueSnackBar( - `Error during useSearchRecords for "${objectMetadataItem.namePlural}", ${error.message}`, - { - variant: SnackBarVariant.Error, - }, - ); - }, - }); - - const effectiveData = loading ? previousData : data; - - const queryResponseField = getSearchRecordsQueryResponseField( - objectMetadataItem.namePlural, - ); - - const result = effectiveData?.[queryResponseField]; - - const records = useMemo( - () => - result - ? (getRecordsFromRecordConnection({ - recordConnection: result, - }) as T[]) - : [], - [result], - ); - - return { - objectMetadataItem, - records: records, - loading, - error, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useSearchRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/hooks/useSearchRecordsQuery.ts deleted file mode 100644 index 6cc3972ca..000000000 --- a/packages/twenty-front/src/modules/object-record/hooks/useSearchRecordsQuery.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { useRecoilValue } from 'recoil'; - -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; -import { generateSearchRecordsQuery } from '@/object-record/utils/generateSearchRecordsQuery'; - -export const useSearchRecordsQuery = ({ - objectNameSingular, - recordGqlFields, - computeReferences, -}: { - objectNameSingular: string; - recordGqlFields?: RecordGqlOperationGqlRecordFields; - computeReferences?: boolean; -}) => { - const { objectMetadataItem } = useObjectMetadataItem({ - objectNameSingular, - }); - - const objectMetadataItems = useRecoilValue(objectMetadataItemsState); - - const searchRecordsQuery = generateSearchRecordsQuery({ - objectMetadataItem, - objectMetadataItems, - recordGqlFields, - computeReferences, - }); - - return { - searchRecordsQuery, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useGenerateCombinedSearchRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useGenerateCombinedSearchRecordsQuery.ts deleted file mode 100644 index 4a1cbab8b..000000000 --- a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useGenerateCombinedSearchRecordsQuery.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useRecoilValue } from 'recoil'; - -import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature'; -import { generateCombinedSearchRecordsQuery } from '@/object-record/multiple-objects/utils/generateCombinedSearchRecordsQuery'; -import { isNonEmptyArray } from '~/utils/isNonEmptyArray'; - -export const useGenerateCombinedSearchRecordsQuery = ({ - operationSignatures, -}: { - operationSignatures: RecordGqlOperationSignature[]; -}) => { - const objectMetadataItems = useRecoilValue(objectMetadataItemsState); - - if (!isNonEmptyArray(operationSignatures)) { - return null; - } - - return generateCombinedSearchRecordsQuery({ - objectMetadataItems, - operationSignatures, - }); -}; diff --git a/packages/twenty-front/src/modules/object-record/multiple-objects/utils/generateCombinedSearchRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/multiple-objects/utils/generateCombinedSearchRecordsQuery.ts deleted file mode 100644 index 96b0edba5..000000000 --- a/packages/twenty-front/src/modules/object-record/multiple-objects/utils/generateCombinedSearchRecordsQuery.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; -import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature'; -import { getSearchRecordsQueryResponseField } from '@/object-record/utils/getSearchRecordsQueryResponseField'; -import { isUndefined } from '@sniptt/guards'; -import gql from 'graphql-tag'; -import { capitalize } from 'twenty-shared'; - -export const generateCombinedSearchRecordsQuery = ({ - objectMetadataItems, - operationSignatures, -}: { - objectMetadataItems: ObjectMetadataItem[]; - operationSignatures: RecordGqlOperationSignature[]; -}) => { - const filterPerMetadataItemArray = operationSignatures - .map( - ({ objectNameSingular }) => - `$filter${capitalize(objectNameSingular)}: ${capitalize( - objectNameSingular, - )}FilterInput`, - ) - .join(', '); - - const limitPerMetadataItemArray = operationSignatures - .map( - ({ objectNameSingular }) => - `$limit${capitalize(objectNameSingular)}: Int`, - ) - .join(', '); - - const queryKeyWithObjectMetadataItemArray = operationSignatures.map( - (queryKey) => { - const objectMetadataItem = objectMetadataItems.find( - (objectMetadataItem) => - objectMetadataItem.nameSingular === queryKey.objectNameSingular, - ); - - if (isUndefined(objectMetadataItem)) { - throw new Error( - `Object metadata item not found for object name singular: ${queryKey.objectNameSingular}`, - ); - } - - return { ...queryKey, objectMetadataItem }; - }, - ); - - const filteredQueryKeyWithObjectMetadataItemArray = - queryKeyWithObjectMetadataItemArray.filter( - ({ objectMetadataItem }) => objectMetadataItem.isSearchable, - ); - - return gql` - query CombinedSearchRecords( - ${filterPerMetadataItemArray}, - ${limitPerMetadataItemArray}, - $search: String, - ) { - ${filteredQueryKeyWithObjectMetadataItemArray - .map( - ({ objectMetadataItem }) => - `${getSearchRecordsQueryResponseField(objectMetadataItem.namePlural)}(filter: $filter${capitalize( - objectMetadataItem.nameSingular, - )}, - limit: $limit${capitalize(objectMetadataItem.nameSingular)}, - searchInput: $search - ){ - edges { - node ${mapObjectMetadataToGraphQLQuery({ - objectMetadataItems: objectMetadataItems, - objectMetadataItem, - })} - cursor - } - totalCount - }`, - ) - .join('\n')} - } - `; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx index 31da0fa6c..ab3083ec7 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx @@ -77,9 +77,8 @@ const RelationToOneFieldInputWithContext = ({ iconName: 'IconLink', metadata: { fieldName: 'Relation', - relationObjectMetadataNamePlural: 'workspaceMembers', - relationObjectMetadataNameSingular: - CoreObjectNameSingular.WorkspaceMember, + relationObjectMetadataNamePlural: 'companies', + relationObjectMetadataNameSingular: CoreObjectNameSingular.Company, objectMetadataNameSingular: 'person', relationFieldMetadataId: '20202020-8c37-4163-ba06-1dada334ce3e', }, @@ -149,7 +148,7 @@ export const Submit: Story = { expect(submitJestFn).toHaveBeenCalledTimes(0); - const item = await canvas.findByText('John Wick', undefined, { + const item = await canvas.findByText('Linkedin', undefined, { timeout: 3000, }); @@ -167,7 +166,7 @@ export const Cancel: Story = { const canvas = within(getCanvasElementForDropdownTesting()); expect(cancelJestFn).toHaveBeenCalledTimes(0); - await canvas.findByText('John Wick', undefined, { timeout: 3000 }); + await canvas.findByText('Linkedin', undefined, { timeout: 3000 }); const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div'); diff --git a/packages/twenty-front/src/modules/object-record/record-picker/hooks/useRecordPickerGetRecordAndObjectMetadataItemFromRecordId.ts b/packages/twenty-front/src/modules/object-record/record-picker/hooks/useRecordPickerGetRecordAndObjectMetadataItemFromRecordId.ts deleted file mode 100644 index c0c53b30e..000000000 --- a/packages/twenty-front/src/modules/object-record/record-picker/hooks/useRecordPickerGetRecordAndObjectMetadataItemFromRecordId.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; -import { multipleRecordPickerSinglePickableMorphItemComponentFamilySelector } from '@/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerSinglePickableMorphItemComponentFamilySelector'; -import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; -import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; -import { useRecoilValue } from 'recoil'; -import { isDefined } from 'twenty-shared'; - -type UseRecordPickerGetRecordAndObjectMetadataItemFromRecordIdProps = { - recordId: string; -}; - -export const useRecordPickerGetRecordAndObjectMetadataItemFromRecordId = ({ - recordId, -}: UseRecordPickerGetRecordAndObjectMetadataItemFromRecordIdProps) => { - const { objectMetadataItems } = useObjectMetadataItems(); - - const pickableMorphItem = useRecoilComponentFamilyValueV2( - multipleRecordPickerSinglePickableMorphItemComponentFamilySelector, - recordId, - ); - - const record = useRecoilValue(recordStoreFamilyState(recordId)); - - if (!isDefined(pickableMorphItem)) { - return { record: null, objectMetadataItem: null }; - } - - const objectMetadataItem = objectMetadataItems.find( - (objectMetadataItem) => - objectMetadataItem.id === pickableMorphItem.objectMetadataId, - ); - - if (!isDefined(objectMetadataItem)) { - return { record: null, objectMetadataItem: null }; - } - - return { record, objectMetadataItem }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/hooks/useRecordPickerGetSearchRecordAndObjectMetadataItemFromRecordId.ts b/packages/twenty-front/src/modules/object-record/record-picker/hooks/useRecordPickerGetSearchRecordAndObjectMetadataItemFromRecordId.ts new file mode 100644 index 000000000..223d80f1e --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/hooks/useRecordPickerGetSearchRecordAndObjectMetadataItemFromRecordId.ts @@ -0,0 +1,41 @@ +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { searchRecordStoreComponentFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState'; +import { multipleRecordPickerSinglePickableMorphItemComponentFamilySelector } from '@/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerSinglePickableMorphItemComponentFamilySelector'; +import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; +import { isDefined } from 'twenty-shared'; + +type UseRecordPickerGetRecordAndObjectMetadataItemFromRecordIdProps = { + recordId: string; +}; + +export const useRecordPickerGetSearchRecordAndObjectMetadataItemFromRecordId = + ({ + recordId, + }: UseRecordPickerGetRecordAndObjectMetadataItemFromRecordIdProps) => { + const { objectMetadataItems } = useObjectMetadataItems(); + + const pickableMorphItem = useRecoilComponentFamilyValueV2( + multipleRecordPickerSinglePickableMorphItemComponentFamilySelector, + recordId, + ); + + const searchRecord = useRecoilComponentFamilyValueV2( + searchRecordStoreComponentFamilyState, + recordId, + ); + + if (!isDefined(pickableMorphItem) || !isDefined(searchRecord)) { + return { searchRecord: null, objectMetadataItem: null }; + } + + const objectMetadataItem = objectMetadataItems.find( + (objectMetadataItem) => + objectMetadataItem.id === pickableMorphItem.objectMetadataId, + ); + + if (!isDefined(objectMetadataItem)) { + return { searchRecord: null, objectMetadataItem: null }; + } + + return { searchRecord, objectMetadataItem }; + }; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItem.tsx b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItem.tsx index 433939b42..4ebea4114 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItem.tsx +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItem.tsx @@ -1,6 +1,6 @@ import styled from '@emotion/styled'; -import { useRecordPickerGetRecordAndObjectMetadataItemFromRecordId } from '@/object-record/record-picker/hooks/useRecordPickerGetRecordAndObjectMetadataItemFromRecordId'; +import { useRecordPickerGetSearchRecordAndObjectMetadataItemFromRecordId } from '@/object-record/record-picker/hooks/useRecordPickerGetSearchRecordAndObjectMetadataItemFromRecordId'; import { MultipleRecordPickerMenuItemContent } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItemContent'; import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem'; import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem'; @@ -20,18 +20,18 @@ export const MultipleRecordPickerMenuItem = ({ recordId, onChange, }: MultipleRecordPickerMenuItemProps) => { - const { record, objectMetadataItem } = - useRecordPickerGetRecordAndObjectMetadataItemFromRecordId({ + const { searchRecord, objectMetadataItem } = + useRecordPickerGetSearchRecordAndObjectMetadataItemFromRecordId({ recordId, }); - if (!isDefined(record) || !isDefined(objectMetadataItem)) { + if (!isDefined(searchRecord) || !isDefined(objectMetadataItem)) { return null; } return ( diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItemContent.tsx b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItemContent.tsx index 1f78460af..45711b25c 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItemContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItemContent.tsx @@ -3,17 +3,16 @@ import { useRecoilValue } from 'recoil'; import { Avatar, MenuItemMultiSelectAvatar } from 'twenty-ui'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier'; +import { getAvatarType } from '@/object-metadata/utils/getAvatarType'; import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext'; import { multipleRecordPickerIsSelectedComponentFamilySelector } from '@/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerIsSelectedComponentFamilySelector'; import { getMultipleRecordPickerSelectableListId } from '@/object-record/record-picker/multiple-record-picker/utils/getMultipleRecordPickerSelectableListId'; import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem'; -import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; -import { isDefined } from 'twenty-shared'; +import { GlobalSearchRecord } from '~/generated-metadata/graphql'; export const StyledSelectableItem = styled(SelectableItem)` height: 100%; @@ -21,13 +20,13 @@ export const StyledSelectableItem = styled(SelectableItem)` `; type MultipleRecordPickerMenuItemContentProps = { - record: ObjectRecord; + searchRecord: GlobalSearchRecord; objectMetadataItem: ObjectMetadataItem; onChange: (morphItem: RecordPickerPickableMorphItem) => void; }; export const MultipleRecordPickerMenuItemContent = ({ - record, + searchRecord, objectMetadataItem, onChange, }: MultipleRecordPickerMenuItemContentProps) => { @@ -43,49 +42,43 @@ export const MultipleRecordPickerMenuItemContent = ({ ); const isSelectedByKeyboard = useRecoilValue( - isSelectedItemIdSelector(record.id), + isSelectedItemIdSelector(searchRecord.recordId), ); const isRecordSelectedWithObjectItem = useRecoilComponentFamilyValueV2( multipleRecordPickerIsSelectedComponentFamilySelector, - record.id, + searchRecord.recordId, componentInstanceId, ); const handleSelectChange = (isSelected: boolean) => { onChange({ - recordId: record.id, + recordId: searchRecord.recordId, objectMetadataId: objectMetadataItem.id, isSelected, isMatchingSearchFilter: true, }); }; - const recordIdentifier = getObjectRecordIdentifier({ - objectMetadataItem, - record, - }); - - if (!isDefined(recordIdentifier)) { - return null; - } - return ( - + handleSelectChange(isSelected)} isKeySelected={isSelectedByKeyboard} selected={isRecordSelectedWithObjectItem} avatar={ } - text={recordIdentifier.name} + text={searchRecord.label} /> ); diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch.ts b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch.ts index cae27861c..bf9cdd7c6 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch.ts +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch.ts @@ -1,21 +1,24 @@ +import { MAX_SEARCH_RESULTS } from '@/command-menu/constants/MaxSearchResults'; +import { globalSearch } from '@/command-menu/graphql/queries/globalSearch'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { CombinedFindManyRecordsQueryResult } from '@/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult'; -import { generateCombinedSearchRecordsQuery } from '@/object-record/multiple-objects/utils/generateCombinedSearchRecordsQuery'; -import { getLimitPerMetadataItem } from '@/object-record/multiple-objects/utils/getLimitPerMetadataItem'; +import { usePerformCombinedFindManyRecords } from '@/object-record/multiple-objects/hooks/usePerformCombinedFindManyRecords'; import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState'; import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState'; import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState'; -import { multipleRecordPickerformatQueryResultAsRecordsWithObjectMetadataId } from '@/object-record/record-picker/multiple-record-picker/utils/multipleRecordPickerformatQueryResultAsRecordWithObjectMetadataId'; +import { searchRecordStoreComponentFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState'; import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem'; -import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { ApolloClient, useApolloClient } from '@apollo/client'; import { isNonEmptyArray } from '@sniptt/guards'; import { useRecoilCallback } from 'recoil'; import { capitalize, isDefined } from 'twenty-shared'; +import { GlobalSearchRecord } from '~/generated-metadata/graphql'; export const useMultipleRecordPickerPerformSearch = () => { const client = useApolloClient(); + const { performCombinedFindManyRecords } = + usePerformCombinedFindManyRecords(); + const performSearch = useRecoilCallback( ({ snapshot, set }) => async ({ @@ -29,57 +32,52 @@ export const useMultipleRecordPickerPerformSearch = () => { forceSearchableObjectMetadataItems?: ObjectMetadataItem[]; forcePickableMorphItems?: RecordPickerPickableMorphItem[]; }) => { - const recordPickerSearchFilter = snapshot - .getLoadable( - multipleRecordPickerSearchFilterComponentState.atomFamily({ - instanceId: multipleRecordPickerInstanceId, - }), - ) - .getValue(); + const { getLoadable } = snapshot; + + const recordPickerSearchFilter = getLoadable( + multipleRecordPickerSearchFilterComponentState.atomFamily({ + instanceId: multipleRecordPickerInstanceId, + }), + ).getValue(); const searchFilter = forceSearchFilter ?? recordPickerSearchFilter; - const recordPickerSearchableObjectMetadataItems = snapshot - .getLoadable( - multipleRecordPickerSearchableObjectMetadataItemsComponentState.atomFamily( - { instanceId: multipleRecordPickerInstanceId }, - ), - ) - .getValue(); + const recordPickerSearchableObjectMetadataItems = getLoadable( + multipleRecordPickerSearchableObjectMetadataItemsComponentState.atomFamily( + { instanceId: multipleRecordPickerInstanceId }, + ), + ).getValue(); const searchableObjectMetadataItems = forceSearchableObjectMetadataItems.length > 0 ? forceSearchableObjectMetadataItems : recordPickerSearchableObjectMetadataItems; - const recordPickerPickableMorphItems = snapshot - .getLoadable( - multipleRecordPickerPickableMorphItemsComponentState.atomFamily({ - instanceId: multipleRecordPickerInstanceId, - }), - ) - .getValue(); + const recordPickerPickableMorphItems = getLoadable( + multipleRecordPickerPickableMorphItemsComponentState.atomFamily({ + instanceId: multipleRecordPickerInstanceId, + }), + ).getValue(); const pickableMorphItems = forcePickableMorphItems.length > 0 ? forcePickableMorphItems : recordPickerPickableMorphItems; + const selectedPickableMorphItems = pickableMorphItems.filter( + ({ isSelected }) => isSelected, + ); - const recordsWithObjectMetadataIdFilteredOnPickedRecords = - await performSearchForPickedRecords({ - client, - searchFilter, - searchableObjectMetadataItems, - pickableMorphItems, - }); - - const recordsWithObjectMetadataIdExcludingPickedRecords = - await performSearchExcludingPickedRecords({ - client, - searchFilter, - searchableObjectMetadataItems, - pickableMorphItems, - }); + const [ + searchRecordsFilteredOnPickedRecords, + searchRecordsExcludingPickedRecords, + ] = await performSearchQueries({ + client, + searchFilter, + searchableObjectMetadataItems, + pickedRecordIds: selectedPickableMorphItems.map( + ({ recordId }) => recordId, + ), + }); const pickedMorphItems = pickableMorphItems.filter( ({ isSelected }) => isSelected, @@ -87,10 +85,9 @@ export const useMultipleRecordPickerPerformSearch = () => { // We update the existing pickedMorphItems to be matching the search filter const updatedPickedMorphItems = pickedMorphItems.map((morphItem) => { - const record = - recordsWithObjectMetadataIdFilteredOnPickedRecords.find( - ({ record }) => record.id === morphItem.recordId, - ); + const record = searchRecordsFilteredOnPickedRecords.find( + ({ recordId }) => recordId === morphItem.recordId, + ); return { ...morphItem, @@ -98,40 +95,47 @@ export const useMultipleRecordPickerPerformSearch = () => { }; }); - const recordsWithObjectMetadataIdFilteredOnPickedRecordsWithoutDuplicates = - recordsWithObjectMetadataIdFilteredOnPickedRecords.filter( - ({ record }) => + const searchRecordsFilteredOnPickedRecordsWithoutDuplicates = + searchRecordsFilteredOnPickedRecords.filter( + (searchRecord) => !updatedPickedMorphItems.some( - ({ recordId }) => recordId === record.id, + ({ recordId }) => recordId === searchRecord.recordId, ), ); - const recordsWithObjectMetadataIdExcludingPickedRecordsWithoutDuplicates = - recordsWithObjectMetadataIdExcludingPickedRecords.filter( - ({ record }) => - !recordsWithObjectMetadataIdFilteredOnPickedRecords.some( - ({ record: recordFilteredOnPickedRecords }) => - recordFilteredOnPickedRecords.id === record.id, + const searchRecordsExcludingPickedRecordsWithoutDuplicates = + searchRecordsExcludingPickedRecords.filter( + (searchRecord) => + !searchRecordsFilteredOnPickedRecords.some( + ({ recordId }) => recordId === searchRecord.recordId, ) && - !pickedMorphItems.some(({ recordId }) => recordId === record.id), + !pickedMorphItems.some( + ({ recordId }) => recordId === searchRecord.recordId, + ), ); const morphItems = [ ...updatedPickedMorphItems, - ...recordsWithObjectMetadataIdFilteredOnPickedRecordsWithoutDuplicates.map( - ({ record, objectMetadataItem }) => ({ + ...searchRecordsFilteredOnPickedRecordsWithoutDuplicates.map( + ({ recordId, objectSingularName }) => ({ isMatchingSearchFilter: true, isSelected: true, - objectMetadataId: objectMetadataItem.id, - recordId: record.id, + objectMetadataId: searchableObjectMetadataItems.find( + (objectMetadata) => + objectMetadata.nameSingular === objectSingularName, + )?.id, + recordId, }), ), - ...recordsWithObjectMetadataIdExcludingPickedRecordsWithoutDuplicates.map( - ({ record, objectMetadataItem }) => ({ + ...searchRecordsExcludingPickedRecordsWithoutDuplicates.map( + ({ recordId, objectSingularName }) => ({ isMatchingSearchFilter: true, isSelected: false, - objectMetadataId: objectMetadataItem.id, - recordId: record.id, + objectMetadataId: searchableObjectMetadataItems.find( + (objectMetadata) => + objectMetadata.nameSingular === objectSingularName, + )?.id, + recordId, }), ), ]; @@ -143,192 +147,153 @@ export const useMultipleRecordPickerPerformSearch = () => { morphItems, ); - [ - ...recordsWithObjectMetadataIdFilteredOnPickedRecords, - ...recordsWithObjectMetadataIdExcludingPickedRecordsWithoutDuplicates, - ].forEach(({ record }) => { - set(recordStoreFamilyState(record.id), record); + const searchRecords = [ + ...searchRecordsFilteredOnPickedRecords, + ...searchRecordsExcludingPickedRecordsWithoutDuplicates, + ]; + + searchRecords.forEach((searchRecord) => { + set( + searchRecordStoreComponentFamilyState.atomFamily({ + instanceId: multipleRecordPickerInstanceId, + familyKey: searchRecord.recordId, + }), + searchRecord, + ); }); + + if (searchRecords.length > 0) { + const filterPerMetadataItemFilteredOnRecordId = Object.fromEntries( + searchableObjectMetadataItems + .map(({ nameSingular }) => { + const recordIdsForMetadataItem = searchRecords + .filter( + ({ objectSingularName }) => + objectSingularName === nameSingular, + ) + .map(({ recordId }) => recordId); + + if (!isNonEmptyArray(recordIdsForMetadataItem)) { + return null; + } + + return [ + `filter${capitalize(nameSingular)}`, + { + id: { + in: recordIdsForMetadataItem, + }, + }, + ]; + }) + .filter(isDefined), + ); + + const operationSignatures = searchableObjectMetadataItems + .filter(({ nameSingular }) => + isDefined( + filterPerMetadataItemFilteredOnRecordId[ + `filter${capitalize(nameSingular)}` + ], + ), + ) + .map((objectMetadataItem) => ({ + objectNameSingular: objectMetadataItem.nameSingular, + variables: { + filter: + filterPerMetadataItemFilteredOnRecordId[ + `filter${capitalize(objectMetadataItem.nameSingular)}` + ], + }, + })); + + performCombinedFindManyRecords({ operationSignatures }).then( + ({ result }) => { + Object.values(result) + .flat() + .forEach((objectRecord) => { + const searchRecord = searchRecords.find( + ({ recordId }) => recordId === objectRecord.id, + ); + + if (!searchRecord) { + return; + } + + set( + searchRecordStoreComponentFamilyState.atomFamily({ + instanceId: multipleRecordPickerInstanceId, + familyKey: objectRecord.id, + }), + { + ...searchRecord, + record: objectRecord, + }, + ); + }); + }, + ); + } }, - [client], + [client, performCombinedFindManyRecords], ); return { performSearch }; }; -const performSearchForPickedRecords = async ({ +const performSearchQueries = async ({ client, searchFilter, searchableObjectMetadataItems, - pickableMorphItems, + pickedRecordIds, }: { client: ApolloClient; searchFilter: string; searchableObjectMetadataItems: ObjectMetadataItem[]; - pickableMorphItems: RecordPickerPickableMorphItem[]; -}) => { - const pickedMorphItems = pickableMorphItems.filter( - ({ isSelected }) => isSelected, - ); - - const filterPerMetadataItemFilteredOnPickedRecordId = Object.fromEntries( - searchableObjectMetadataItems - .map(({ id, nameSingular }) => { - const pickedRecordIdsForMetadataItem = pickedMorphItems - .filter( - ({ objectMetadataId, isSelected }) => - objectMetadataId === id && isSelected, - ) - .map(({ recordId }) => recordId); - - if (!isNonEmptyArray(pickedRecordIdsForMetadataItem)) { - return null; - } - - return [ - `filter${capitalize(nameSingular)}`, - { - id: { - in: pickedRecordIdsForMetadataItem, - }, - }, - ]; - }) - .filter(isDefined), - ); - - const searchableObjectMetadataItemsFilteredOnPickedRecordId = - searchableObjectMetadataItems.filter(({ nameSingular }) => - isDefined( - filterPerMetadataItemFilteredOnPickedRecordId[ - `filter${capitalize(nameSingular)}` - ], - ), - ); - - if (!isNonEmptyArray(searchableObjectMetadataItemsFilteredOnPickedRecordId)) { - return []; - } - - const combinedSearchRecordsQueryFilteredOnPickedRecords = - generateCombinedSearchRecordsQuery({ - objectMetadataItems: - searchableObjectMetadataItemsFilteredOnPickedRecordId, - operationSignatures: - searchableObjectMetadataItemsFilteredOnPickedRecordId.map( - (objectMetadataItem) => ({ - objectNameSingular: objectMetadataItem.nameSingular, - variables: {}, - }), - ), - }); - - const limitPerMetadataItem = getLimitPerMetadataItem( - searchableObjectMetadataItemsFilteredOnPickedRecordId, - 10, - ); - - const { data: combinedSearchRecordFilteredOnPickedRecordsQueryResult } = - await client.query({ - query: combinedSearchRecordsQueryFilteredOnPickedRecords, - variables: { - search: searchFilter, - ...limitPerMetadataItem, - ...filterPerMetadataItemFilteredOnPickedRecordId, - }, - }); - - const { - recordsWithObjectMetadataId: - recordsWithObjectMetadataIdFilteredOnPickedRecords, - } = multipleRecordPickerformatQueryResultAsRecordsWithObjectMetadataId({ - objectMetadataItems: searchableObjectMetadataItems, - searchQueryResult: combinedSearchRecordFilteredOnPickedRecordsQueryResult, - }); - - return recordsWithObjectMetadataIdFilteredOnPickedRecords; -}; - -const performSearchExcludingPickedRecords = async ({ - client, - searchFilter, - searchableObjectMetadataItems, - pickableMorphItems, -}: { - client: ApolloClient; - searchFilter: string; - searchableObjectMetadataItems: ObjectMetadataItem[]; - pickableMorphItems: RecordPickerPickableMorphItem[]; -}) => { + pickedRecordIds: string[]; +}): Promise<[GlobalSearchRecord[], GlobalSearchRecord[]]> => { if (searchableObjectMetadataItems.length === 0) { - return []; + return [[], []]; } - const pickedMorphItems = pickableMorphItems.filter( - ({ isSelected }) => isSelected, - ); - - const filterPerMetadataItemExcludingPickedRecordId = Object.fromEntries( - searchableObjectMetadataItems - .map(({ id, nameSingular }) => { - const pickedRecordIdsForMetadataItem = pickedMorphItems - .filter( - ({ objectMetadataId, isSelected }) => - objectMetadataId === id && isSelected, - ) - .map(({ recordId }) => recordId); - - if (!isNonEmptyArray(pickedRecordIdsForMetadataItem)) { - return null; - } - - return [ - `filter${capitalize(nameSingular)}`, - { - not: { - id: { - in: pickedRecordIdsForMetadataItem, - }, - }, - }, - ]; - }) - .filter(isDefined), - ); - - const combinedSearchRecordsQueryExcludingPickedRecords = - generateCombinedSearchRecordsQuery({ - objectMetadataItems: searchableObjectMetadataItems, - operationSignatures: searchableObjectMetadataItems.map( - (objectMetadataItem) => ({ - objectNameSingular: objectMetadataItem.nameSingular, - variables: {}, - }), - ), - }); - - const limitPerMetadataItem = getLimitPerMetadataItem( - searchableObjectMetadataItems, - 10, - ); - - const { data: combinedSearchRecordExcludingPickedRecordsQueryResult } = - await client.query({ - query: combinedSearchRecordsQueryExcludingPickedRecords, + const searchRecords = async (filter: any) => { + const { data } = await client.query({ + query: globalSearch, variables: { - search: searchFilter, - ...limitPerMetadataItem, - ...filterPerMetadataItemExcludingPickedRecordId, + searchInput: searchFilter, + includedObjectNameSingulars: searchableObjectMetadataItems.map( + ({ nameSingular }) => nameSingular, + ), + filter, + limit: MAX_SEARCH_RESULTS, }, }); + return data.globalSearch; + }; - const { - recordsWithObjectMetadataId: - recordsWithObjectMetadataIdExcludingPickedRecords, - } = multipleRecordPickerformatQueryResultAsRecordsWithObjectMetadataId({ - objectMetadataItems: searchableObjectMetadataItems, - searchQueryResult: combinedSearchRecordExcludingPickedRecordsQueryResult, - }); + const searchRecordsExcludingPickedRecords = await searchRecords( + pickedRecordIds.length > 0 + ? { + not: { + id: { + in: pickedRecordIds, + }, + }, + } + : undefined, + ); - return recordsWithObjectMetadataIdExcludingPickedRecords; + const searchRecordsIncludingPickedRecords = + pickedRecordIds.length > 0 + ? await searchRecords({ + id: { + in: pickedRecordIds, + }, + }) + : []; + + return [ + searchRecordsIncludingPickedRecords, + searchRecordsExcludingPickedRecords, + ]; }; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState.ts new file mode 100644 index 000000000..6dfebda23 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState.ts @@ -0,0 +1,14 @@ +import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2'; +import { GlobalSearchRecord } from '~/generated-metadata/graphql'; + +export const searchRecordStoreComponentFamilyState = + createComponentFamilyStateV2< + (GlobalSearchRecord & { record?: ObjectRecord }) | undefined, + string + >({ + key: 'searchRecordStoreComponentFamilyState', + defaultValue: undefined, + componentInstanceContext: MultipleRecordPickerComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx index 22a0b3868..d77ff9af9 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx @@ -110,16 +110,22 @@ export const FieldsCard = ({ hotkeyScope: InlineCellHotkeyScope.InlineCell, }} > - + + + ), )} diff --git a/packages/twenty-front/src/modules/object-record/utils/generateSearchRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/utils/generateSearchRecordsQuery.ts deleted file mode 100644 index e4faeac66..000000000 --- a/packages/twenty-front/src/modules/object-record/utils/generateSearchRecordsQuery.ts +++ /dev/null @@ -1,42 +0,0 @@ -import gql from 'graphql-tag'; - -import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; -import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; -import { getSearchRecordsQueryResponseField } from '@/object-record/utils/getSearchRecordsQueryResponseField'; -import { capitalize } from 'twenty-shared'; - -export type QueryCursorDirection = 'before' | 'after'; - -export const generateSearchRecordsQuery = ({ - objectMetadataItem, - objectMetadataItems, - recordGqlFields, - computeReferences, -}: { - objectMetadataItem: ObjectMetadataItem; - objectMetadataItems: ObjectMetadataItem[]; - recordGqlFields?: RecordGqlOperationGqlRecordFields; - computeReferences?: boolean; -}) => gql` - query Search${capitalize(objectMetadataItem.namePlural)}($search: String, $limit: Int, $filter: ${capitalize( - objectMetadataItem.nameSingular, - )}FilterInput) { - ${getSearchRecordsQueryResponseField(objectMetadataItem.namePlural)}(searchInput: $search, limit: $limit, filter: $filter){ - edges { - node ${mapObjectMetadataToGraphQLQuery({ - objectMetadataItems, - objectMetadataItem, - recordGqlFields, - computeReferences, - })} - } - pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor - } - } -} -`; diff --git a/packages/twenty-front/src/modules/object-record/utils/getSearchRecordsQueryResponseField.ts b/packages/twenty-front/src/modules/object-record/utils/getSearchRecordsQueryResponseField.ts deleted file mode 100644 index 2801bfce1..000000000 --- a/packages/twenty-front/src/modules/object-record/utils/getSearchRecordsQueryResponseField.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { capitalize } from 'twenty-shared'; - -export const getSearchRecordsQueryResponseField = (objectNamePlural: string) => - `search${capitalize(objectNamePlural)}`; diff --git a/packages/twenty-front/src/modules/search/hooks/useFilteredSearchRecordQuery.ts b/packages/twenty-front/src/modules/search/hooks/useFilteredSearchRecordQuery.ts index 84a98447d..071e0ca14 100644 --- a/packages/twenty-front/src/modules/search/hooks/useFilteredSearchRecordQuery.ts +++ b/packages/twenty-front/src/modules/search/hooks/useFilteredSearchRecordQuery.ts @@ -1,9 +1,7 @@ -import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapToObjectRecordIdentifier'; +import { formatGlobalSearchRecordAsSingleRecordPickerRecord } from '@/object-metadata/utils/formatGlobalSearchRecordAsSingleRecordPickerRecord'; import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit'; -import { useSearchRecords } from '@/object-record/hooks/useSearchRecords'; -import { MultipleRecordPickerRecords } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerRecords'; +import { useObjectRecordSearchRecords } from '@/object-record/hooks/useObjectRecordSearchRecords'; import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord'; -import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { isDefined } from 'twenty-shared'; export const useFilteredSearchRecordQuery = ({ @@ -18,19 +16,16 @@ export const useFilteredSearchRecordQuery = ({ excludedRecordIds?: string[]; objectNameSingular: string; searchFilter?: string; -}): MultipleRecordPickerRecords => { - const { mapToObjectRecordIdentifier } = useMapToObjectRecordIdentifier({ - objectNameSingular, - }); - - const mappingFunction = (record: ObjectRecord) => ({ - ...mapToObjectRecordIdentifier(record), - record, - }); +}): { + selectedRecords: SingleRecordPickerRecord[]; + filteredSelectedRecords: SingleRecordPickerRecord[]; + recordsToSelect: SingleRecordPickerRecord[]; + loading: boolean; +} => { const selectedIdsFilter = { id: { in: selectedIds } }; - const { loading: selectedRecordsLoading, records: selectedRecords } = - useSearchRecords({ + const { loading: selectedRecordsLoading, searchRecords: selectedRecords } = + useObjectRecordSearchRecords({ objectNameSingular, filter: selectedIdsFilter, skip: !selectedIds.length, @@ -39,8 +34,8 @@ export const useFilteredSearchRecordQuery = ({ const { loading: filteredSelectedRecordsLoading, - records: filteredSelectedRecords, - } = useSearchRecords({ + searchRecords: filteredSelectedRecords, + } = useObjectRecordSearchRecords({ objectNameSingular, filter: selectedIdsFilter, skip: !selectedIds.length, @@ -51,8 +46,8 @@ export const useFilteredSearchRecordQuery = ({ const notFilter = notFilterIds.length ? { not: { id: { in: notFilterIds } } } : undefined; - const { loading: recordsToSelectLoading, records: recordsToSelect } = - useSearchRecords({ + const { loading: recordsToSelectLoading, searchRecords: recordsToSelect } = + useObjectRecordSearchRecords({ objectNameSingular, filter: notFilter, limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT, @@ -61,11 +56,15 @@ export const useFilteredSearchRecordQuery = ({ }); return { - selectedRecords: selectedRecords.map(mappingFunction).filter(isDefined), - filteredSelectedRecords: filteredSelectedRecords - .map(mappingFunction) + selectedRecords: selectedRecords + .map(formatGlobalSearchRecordAsSingleRecordPickerRecord) + .filter(isDefined), + filteredSelectedRecords: filteredSelectedRecords + .map(formatGlobalSearchRecordAsSingleRecordPickerRecord) + .filter(isDefined), + recordsToSelect: recordsToSelect + .map(formatGlobalSearchRecordAsSingleRecordPickerRecord) .filter(isDefined), - recordsToSelect: recordsToSelect.map(mappingFunction).filter(isDefined), loading: recordsToSelectLoading || filteredSelectedRecordsLoading || diff --git a/packages/twenty-front/src/modules/settings/roles/role-assignment/components/RoleAssignment.tsx b/packages/twenty-front/src/modules/settings/roles/role-assignment/components/RoleAssignment.tsx index 287d8e58d..696e7e4c1 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-assignment/components/RoleAssignment.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-assignment/components/RoleAssignment.tsx @@ -21,7 +21,11 @@ import { Section, TooltipDelay, } from 'twenty-ui'; -import { Role, WorkspaceMember } from '~/generated-metadata/graphql'; +import { + GlobalSearchRecord, + Role, + WorkspaceMember, +} from '~/generated-metadata/graphql'; import { GetRolesDocument, useGetRolesQuery, @@ -129,14 +133,18 @@ export const RoleAssignment = ({ role }: RoleAssignmentProps) => { setSelectedWorkspaceMember(null); }; - const handleSelectWorkspaceMember = (workspaceMember: WorkspaceMember) => { - const existingRole = workspaceMemberRoleMap.get(workspaceMember.id); + const handleSelectWorkspaceMember = ( + workspaceMemberSearchRecord: GlobalSearchRecord, + ) => { + const existingRole = workspaceMemberRoleMap.get( + workspaceMemberSearchRecord.recordId, + ); setSelectedWorkspaceMember({ - id: workspaceMember.id, - name: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`, + id: workspaceMemberSearchRecord.recordId, + name: `${workspaceMemberSearchRecord.label}`, role: existingRole, - avatarUrl: workspaceMember.avatarUrl, + avatarUrl: workspaceMemberSearchRecord.imageUrl, }); setConfirmationModalIsOpen(true); closeDropdown(); diff --git a/packages/twenty-front/src/modules/settings/roles/role-assignment/components/RoleAssignmentWorkspaceMemberPickerDropdown.tsx b/packages/twenty-front/src/modules/settings/roles/role-assignment/components/RoleAssignmentWorkspaceMemberPickerDropdown.tsx index 995ad0858..d4398fea6 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-assignment/components/RoleAssignmentWorkspaceMemberPickerDropdown.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-assignment/components/RoleAssignmentWorkspaceMemberPickerDropdown.tsx @@ -1,16 +1,16 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useSearchRecords } from '@/object-record/hooks/useSearchRecords'; +import { useObjectRecordSearchRecords } from '@/object-record/hooks/useObjectRecordSearchRecords'; import { RoleAssignmentWorkspaceMemberPickerDropdownContent } from '@/settings/roles/role-assignment/components/RoleAssignmentWorkspaceMemberPickerDropdownContent'; import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { ChangeEvent, useState } from 'react'; -import { WorkspaceMember } from '~/generated-metadata/graphql'; +import { GlobalSearchRecord } from '~/generated-metadata/graphql'; type RoleAssignmentWorkspaceMemberPickerDropdownProps = { excludedWorkspaceMemberIds: string[]; - onSelect: (workspaceMember: WorkspaceMember) => void; + onSelect: (workspaceMemberSearchRecord: GlobalSearchRecord) => void; }; export const RoleAssignmentWorkspaceMemberPickerDropdown = ({ @@ -19,15 +19,17 @@ export const RoleAssignmentWorkspaceMemberPickerDropdown = ({ }: RoleAssignmentWorkspaceMemberPickerDropdownProps) => { const [searchFilter, setSearchFilter] = useState(''); - const { loading, records: workspaceMembers } = useSearchRecords({ - objectNameSingular: CoreObjectNameSingular.WorkspaceMember, - searchInput: searchFilter, - }); + const { loading, searchRecords: workspaceMembers } = + useObjectRecordSearchRecords({ + objectNameSingular: CoreObjectNameSingular.WorkspaceMember, + searchInput: searchFilter, + }); - const filteredWorkspaceMembers = (workspaceMembers?.filter( - (workspaceMember) => - !excludedWorkspaceMemberIds.includes(workspaceMember.id), - ) ?? []) as WorkspaceMember[]; + const filteredWorkspaceMembers = + workspaceMembers?.filter( + (workspaceMember) => + !excludedWorkspaceMemberIds.includes(workspaceMember.recordId), + ) ?? []; const handleSearchFilterChange = (event: ChangeEvent) => { setSearchFilter(event.target.value); diff --git a/packages/twenty-front/src/modules/settings/roles/role-assignment/components/RoleAssignmentWorkspaceMemberPickerDropdownContent.tsx b/packages/twenty-front/src/modules/settings/roles/role-assignment/components/RoleAssignmentWorkspaceMemberPickerDropdownContent.tsx index e5814b563..812e76ab1 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-assignment/components/RoleAssignmentWorkspaceMemberPickerDropdownContent.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-assignment/components/RoleAssignmentWorkspaceMemberPickerDropdownContent.tsx @@ -1,12 +1,12 @@ import { t } from '@lingui/core/macro'; import { MenuItem, MenuItemAvatar } from 'twenty-ui'; -import { WorkspaceMember } from '~/generated-metadata/graphql'; +import { GlobalSearchRecord } from '~/generated-metadata/graphql'; type RoleAssignmentWorkspaceMemberPickerDropdownContentProps = { loading: boolean; searchFilter: string; - filteredWorkspaceMembers: WorkspaceMember[]; - onSelect: (workspaceMember: WorkspaceMember) => void; + filteredWorkspaceMembers: GlobalSearchRecord[]; + onSelect: (workspaceMemberSearchRecord: GlobalSearchRecord) => void; }; export const RoleAssignmentWorkspaceMemberPickerDropdownContent = ({ @@ -27,15 +27,15 @@ export const RoleAssignmentWorkspaceMemberPickerDropdownContent = ({ <> {filteredWorkspaceMembers.map((workspaceMember) => ( onSelect(workspaceMember)} avatar={{ type: 'rounded', size: 'md', - placeholder: workspaceMember.name.firstName ?? '', - placeholderColorSeed: workspaceMember.id, + placeholder: workspaceMember.label ?? '', + placeholderColorSeed: workspaceMember.recordId, }} - text={`${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`} + text={workspaceMember.label} /> ))}