replace search resolvers by global search in front (#11086)
Done - Replace global search in multi record picker and single record picker To do - refactor SingleRecordPicker to match MultipleRecordPicker - next 1:1 - items in this issue https://github.com/twentyhq/core-team-issues/issues/643 closes https://github.com/twentyhq/core-team-issues/issues/535
This commit is contained in:
@ -2418,7 +2418,9 @@ export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typ
|
|||||||
export type GlobalSearchQueryVariables = Exact<{
|
export type GlobalSearchQueryVariables = Exact<{
|
||||||
searchInput: Scalars['String'];
|
searchInput: Scalars['String'];
|
||||||
limit: Scalars['Int'];
|
limit: Scalars['Int'];
|
||||||
excludedObjectNameSingulars: Array<Scalars['String']> | Scalars['String'];
|
excludedObjectNameSingulars?: InputMaybe<Array<Scalars['String']> | Scalars['String']>;
|
||||||
|
includedObjectNameSingulars?: InputMaybe<Array<Scalars['String']> | Scalars['String']>;
|
||||||
|
filter?: InputMaybe<ObjectRecordFilterInput>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
@ -4095,11 +4097,13 @@ export type GetClientConfigQueryHookResult = ReturnType<typeof useGetClientConfi
|
|||||||
export type GetClientConfigLazyQueryHookResult = ReturnType<typeof useGetClientConfigLazyQuery>;
|
export type GetClientConfigLazyQueryHookResult = ReturnType<typeof useGetClientConfigLazyQuery>;
|
||||||
export type GetClientConfigQueryResult = Apollo.QueryResult<GetClientConfigQuery, GetClientConfigQueryVariables>;
|
export type GetClientConfigQueryResult = Apollo.QueryResult<GetClientConfigQuery, GetClientConfigQueryVariables>;
|
||||||
export const GlobalSearchDocument = gql`
|
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(
|
globalSearch(
|
||||||
searchInput: $searchInput
|
searchInput: $searchInput
|
||||||
limit: $limit
|
limit: $limit
|
||||||
excludedObjectNameSingulars: $excludedObjectNameSingulars
|
excludedObjectNameSingulars: $excludedObjectNameSingulars
|
||||||
|
includedObjectNameSingulars: $includedObjectNameSingulars
|
||||||
|
filter: $filter
|
||||||
) {
|
) {
|
||||||
recordId
|
recordId
|
||||||
objectSingularName
|
objectSingularName
|
||||||
@ -4126,6 +4130,8 @@ export const GlobalSearchDocument = gql`
|
|||||||
* searchInput: // value for 'searchInput'
|
* searchInput: // value for 'searchInput'
|
||||||
* limit: // value for 'limit'
|
* limit: // value for 'limit'
|
||||||
* excludedObjectNameSingulars: // value for 'excludedObjectNameSingulars'
|
* excludedObjectNameSingulars: // value for 'excludedObjectNameSingulars'
|
||||||
|
* includedObjectNameSingulars: // value for 'includedObjectNameSingulars'
|
||||||
|
* filter: // value for 'filter'
|
||||||
* },
|
* },
|
||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat
|
|||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||||
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
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 { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
import { isNull } from '@sniptt/guards';
|
import { isNull } from '@sniptt/guards';
|
||||||
@ -47,6 +48,7 @@ export const useUpdateActivityTargetFromInlineCell = ({
|
|||||||
async ({
|
async ({
|
||||||
morphItem,
|
morphItem,
|
||||||
activityTargetWithTargetRecords,
|
activityTargetWithTargetRecords,
|
||||||
|
recordPickerInstanceId,
|
||||||
}: UpdateActivityTargetFromInlineCellProps) => {
|
}: UpdateActivityTargetFromInlineCellProps) => {
|
||||||
const targetObjectName =
|
const targetObjectName =
|
||||||
activityObjectNameSingular === CoreObjectNameSingular.Task
|
activityObjectNameSingular === CoreObjectNameSingular.Task
|
||||||
@ -92,11 +94,16 @@ export const useUpdateActivityTargetFromInlineCell = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const targetRecord = snapshot
|
const searchRecord = snapshot
|
||||||
.getLoadable(recordStoreFamilyState(morphItem.recordId))
|
.getLoadable(
|
||||||
|
searchRecordStoreComponentFamilyState.atomFamily({
|
||||||
|
instanceId: recordPickerInstanceId,
|
||||||
|
familyKey: morphItem.recordId,
|
||||||
|
}),
|
||||||
|
)
|
||||||
.getValue();
|
.getValue();
|
||||||
|
|
||||||
if (!isDefined(targetRecord)) {
|
if (!isDefined(searchRecord) || !isDefined(searchRecord?.record)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,6 +111,8 @@ export const useUpdateActivityTargetFromInlineCell = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const targetRecord = searchRecord.record;
|
||||||
|
|
||||||
const activityTarget =
|
const activityTarget =
|
||||||
activityObjectNameSingular === CoreObjectNameSingular.Task
|
activityObjectNameSingular === CoreObjectNameSingular.Task
|
||||||
? {
|
? {
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export const MAX_SEARCH_RESULTS = 30;
|
||||||
@ -4,12 +4,16 @@ export const globalSearch = gql`
|
|||||||
query GlobalSearch(
|
query GlobalSearch(
|
||||||
$searchInput: String!
|
$searchInput: String!
|
||||||
$limit: Int!
|
$limit: Int!
|
||||||
$excludedObjectNameSingulars: [String!]!
|
$excludedObjectNameSingulars: [String!]
|
||||||
|
$includedObjectNameSingulars: [String!]
|
||||||
|
$filter: ObjectRecordFilterInput
|
||||||
) {
|
) {
|
||||||
globalSearch(
|
globalSearch(
|
||||||
searchInput: $searchInput
|
searchInput: $searchInput
|
||||||
limit: $limit
|
limit: $limit
|
||||||
excludedObjectNameSingulars: $excludedObjectNameSingulars
|
excludedObjectNameSingulars: $excludedObjectNameSingulars
|
||||||
|
includedObjectNameSingulars: $includedObjectNameSingulars
|
||||||
|
filter: $filter
|
||||||
) {
|
) {
|
||||||
recordId
|
recordId
|
||||||
objectSingularName
|
objectSingularName
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { MAX_SEARCH_RESULTS } from '@/command-menu/constants/MaxSearchResults';
|
||||||
import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu';
|
import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu';
|
||||||
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
@ -9,9 +10,7 @@ import { Avatar } from 'twenty-ui';
|
|||||||
import { useDebounce } from 'use-debounce';
|
import { useDebounce } from 'use-debounce';
|
||||||
import { useGlobalSearchQuery } from '~/generated/graphql';
|
import { useGlobalSearchQuery } from '~/generated/graphql';
|
||||||
|
|
||||||
const MAX_SEARCH_RESULTS = 30;
|
export const useCommandMenuSearchRecords = () => {
|
||||||
|
|
||||||
export const useSearchRecords = () => {
|
|
||||||
const commandMenuSearch = useRecoilValue(commandMenuSearchState);
|
const commandMenuSearch = useRecoilValue(commandMenuSearchState);
|
||||||
|
|
||||||
const [deferredCommandMenuSearch] = useDebounce(commandMenuSearch, 300);
|
const [deferredCommandMenuSearch] = useDebounce(commandMenuSearch, 300);
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
|
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';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
export const CommandMenuSearchRecordsPage = () => {
|
export const CommandMenuSearchRecordsPage = () => {
|
||||||
const { commandGroups, loading, noResults } = useSearchRecords();
|
const { commandGroups, loading, noResults } = useCommandMenuSearchRecords();
|
||||||
|
|
||||||
const selectableItemIds = useMemo(() => {
|
const selectableItemIds = useMemo(() => {
|
||||||
return commandGroups.flatMap((group) => group.items).map((item) => item.id);
|
return commandGroups.flatMap((group) => group.items).map((item) => item.id);
|
||||||
|
|||||||
@ -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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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<RecordGqlOperationVariables, 'filter' | 'limit'> & {
|
|
||||||
onError?: (error?: Error) => void;
|
|
||||||
skip?: boolean;
|
|
||||||
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
|
||||||
fetchPolicy?: WatchQueryFetchPolicy;
|
|
||||||
searchInput?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useSearchRecords = <T extends ObjectRecord = ObjectRecord>({
|
|
||||||
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<RecordGqlOperationSearchResult>(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,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -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,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -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,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -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')}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
};
|
|
||||||
@ -77,9 +77,8 @@ const RelationToOneFieldInputWithContext = ({
|
|||||||
iconName: 'IconLink',
|
iconName: 'IconLink',
|
||||||
metadata: {
|
metadata: {
|
||||||
fieldName: 'Relation',
|
fieldName: 'Relation',
|
||||||
relationObjectMetadataNamePlural: 'workspaceMembers',
|
relationObjectMetadataNamePlural: 'companies',
|
||||||
relationObjectMetadataNameSingular:
|
relationObjectMetadataNameSingular: CoreObjectNameSingular.Company,
|
||||||
CoreObjectNameSingular.WorkspaceMember,
|
|
||||||
objectMetadataNameSingular: 'person',
|
objectMetadataNameSingular: 'person',
|
||||||
relationFieldMetadataId: '20202020-8c37-4163-ba06-1dada334ce3e',
|
relationFieldMetadataId: '20202020-8c37-4163-ba06-1dada334ce3e',
|
||||||
},
|
},
|
||||||
@ -149,7 +148,7 @@ export const Submit: Story = {
|
|||||||
|
|
||||||
expect(submitJestFn).toHaveBeenCalledTimes(0);
|
expect(submitJestFn).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
const item = await canvas.findByText('John Wick', undefined, {
|
const item = await canvas.findByText('Linkedin', undefined, {
|
||||||
timeout: 3000,
|
timeout: 3000,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -167,7 +166,7 @@ export const Cancel: Story = {
|
|||||||
const canvas = within(getCanvasElementForDropdownTesting());
|
const canvas = within(getCanvasElementForDropdownTesting());
|
||||||
|
|
||||||
expect(cancelJestFn).toHaveBeenCalledTimes(0);
|
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');
|
const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div');
|
||||||
|
|
||||||
|
|||||||
@ -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 };
|
|
||||||
};
|
|
||||||
@ -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 };
|
||||||
|
};
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import styled from '@emotion/styled';
|
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 { MultipleRecordPickerMenuItemContent } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItemContent';
|
||||||
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||||
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
||||||
@ -20,18 +20,18 @@ export const MultipleRecordPickerMenuItem = ({
|
|||||||
recordId,
|
recordId,
|
||||||
onChange,
|
onChange,
|
||||||
}: MultipleRecordPickerMenuItemProps) => {
|
}: MultipleRecordPickerMenuItemProps) => {
|
||||||
const { record, objectMetadataItem } =
|
const { searchRecord, objectMetadataItem } =
|
||||||
useRecordPickerGetRecordAndObjectMetadataItemFromRecordId({
|
useRecordPickerGetSearchRecordAndObjectMetadataItemFromRecordId({
|
||||||
recordId,
|
recordId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isDefined(record) || !isDefined(objectMetadataItem)) {
|
if (!isDefined(searchRecord) || !isDefined(objectMetadataItem)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MultipleRecordPickerMenuItemContent
|
<MultipleRecordPickerMenuItemContent
|
||||||
record={record}
|
searchRecord={searchRecord}
|
||||||
objectMetadataItem={objectMetadataItem}
|
objectMetadataItem={objectMetadataItem}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -3,17 +3,16 @@ import { useRecoilValue } from 'recoil';
|
|||||||
import { Avatar, MenuItemMultiSelectAvatar } from 'twenty-ui';
|
import { Avatar, MenuItemMultiSelectAvatar } from 'twenty-ui';
|
||||||
|
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
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 { 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 { 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 { getMultipleRecordPickerSelectableListId } from '@/object-record/record-picker/multiple-record-picker/utils/getMultipleRecordPickerSelectableListId';
|
||||||
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
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 { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
||||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||||
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
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)`
|
export const StyledSelectableItem = styled(SelectableItem)`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -21,13 +20,13 @@ export const StyledSelectableItem = styled(SelectableItem)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
type MultipleRecordPickerMenuItemContentProps = {
|
type MultipleRecordPickerMenuItemContentProps = {
|
||||||
record: ObjectRecord;
|
searchRecord: GlobalSearchRecord;
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
onChange: (morphItem: RecordPickerPickableMorphItem) => void;
|
onChange: (morphItem: RecordPickerPickableMorphItem) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MultipleRecordPickerMenuItemContent = ({
|
export const MultipleRecordPickerMenuItemContent = ({
|
||||||
record,
|
searchRecord,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
onChange,
|
onChange,
|
||||||
}: MultipleRecordPickerMenuItemContentProps) => {
|
}: MultipleRecordPickerMenuItemContentProps) => {
|
||||||
@ -43,49 +42,43 @@ export const MultipleRecordPickerMenuItemContent = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isSelectedByKeyboard = useRecoilValue(
|
const isSelectedByKeyboard = useRecoilValue(
|
||||||
isSelectedItemIdSelector(record.id),
|
isSelectedItemIdSelector(searchRecord.recordId),
|
||||||
);
|
);
|
||||||
|
|
||||||
const isRecordSelectedWithObjectItem = useRecoilComponentFamilyValueV2(
|
const isRecordSelectedWithObjectItem = useRecoilComponentFamilyValueV2(
|
||||||
multipleRecordPickerIsSelectedComponentFamilySelector,
|
multipleRecordPickerIsSelectedComponentFamilySelector,
|
||||||
record.id,
|
searchRecord.recordId,
|
||||||
componentInstanceId,
|
componentInstanceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSelectChange = (isSelected: boolean) => {
|
const handleSelectChange = (isSelected: boolean) => {
|
||||||
onChange({
|
onChange({
|
||||||
recordId: record.id,
|
recordId: searchRecord.recordId,
|
||||||
objectMetadataId: objectMetadataItem.id,
|
objectMetadataId: objectMetadataItem.id,
|
||||||
isSelected,
|
isSelected,
|
||||||
isMatchingSearchFilter: true,
|
isMatchingSearchFilter: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const recordIdentifier = getObjectRecordIdentifier({
|
|
||||||
objectMetadataItem,
|
|
||||||
record,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isDefined(recordIdentifier)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledSelectableItem itemId={record.id} key={record.id}>
|
<StyledSelectableItem
|
||||||
|
itemId={searchRecord.recordId}
|
||||||
|
key={searchRecord.recordId}
|
||||||
|
>
|
||||||
<MenuItemMultiSelectAvatar
|
<MenuItemMultiSelectAvatar
|
||||||
onSelectChange={(isSelected) => handleSelectChange(isSelected)}
|
onSelectChange={(isSelected) => handleSelectChange(isSelected)}
|
||||||
isKeySelected={isSelectedByKeyboard}
|
isKeySelected={isSelectedByKeyboard}
|
||||||
selected={isRecordSelectedWithObjectItem}
|
selected={isRecordSelectedWithObjectItem}
|
||||||
avatar={
|
avatar={
|
||||||
<Avatar
|
<Avatar
|
||||||
avatarUrl={recordIdentifier.avatarUrl}
|
avatarUrl={searchRecord.imageUrl}
|
||||||
placeholderColorSeed={record.id}
|
placeholderColorSeed={searchRecord.recordId}
|
||||||
placeholder={recordIdentifier.name}
|
placeholder={searchRecord.label}
|
||||||
size="md"
|
size="md"
|
||||||
type={recordIdentifier.avatarType ?? 'rounded'}
|
type={getAvatarType(objectMetadataItem.nameSingular) ?? 'rounded'}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
text={recordIdentifier.name}
|
text={searchRecord.label}
|
||||||
/>
|
/>
|
||||||
</StyledSelectableItem>
|
</StyledSelectableItem>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { CombinedFindManyRecordsQueryResult } from '@/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult';
|
import { usePerformCombinedFindManyRecords } from '@/object-record/multiple-objects/hooks/usePerformCombinedFindManyRecords';
|
||||||
import { generateCombinedSearchRecordsQuery } from '@/object-record/multiple-objects/utils/generateCombinedSearchRecordsQuery';
|
|
||||||
import { getLimitPerMetadataItem } from '@/object-record/multiple-objects/utils/getLimitPerMetadataItem';
|
|
||||||
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
|
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 { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState';
|
||||||
import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState';
|
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 { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
|
||||||
import { ApolloClient, useApolloClient } from '@apollo/client';
|
import { ApolloClient, useApolloClient } from '@apollo/client';
|
||||||
import { isNonEmptyArray } from '@sniptt/guards';
|
import { isNonEmptyArray } from '@sniptt/guards';
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
import { capitalize, isDefined } from 'twenty-shared';
|
import { capitalize, isDefined } from 'twenty-shared';
|
||||||
|
import { GlobalSearchRecord } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
export const useMultipleRecordPickerPerformSearch = () => {
|
export const useMultipleRecordPickerPerformSearch = () => {
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
|
|
||||||
|
const { performCombinedFindManyRecords } =
|
||||||
|
usePerformCombinedFindManyRecords();
|
||||||
|
|
||||||
const performSearch = useRecoilCallback(
|
const performSearch = useRecoilCallback(
|
||||||
({ snapshot, set }) =>
|
({ snapshot, set }) =>
|
||||||
async ({
|
async ({
|
||||||
@ -29,57 +32,52 @@ export const useMultipleRecordPickerPerformSearch = () => {
|
|||||||
forceSearchableObjectMetadataItems?: ObjectMetadataItem[];
|
forceSearchableObjectMetadataItems?: ObjectMetadataItem[];
|
||||||
forcePickableMorphItems?: RecordPickerPickableMorphItem[];
|
forcePickableMorphItems?: RecordPickerPickableMorphItem[];
|
||||||
}) => {
|
}) => {
|
||||||
const recordPickerSearchFilter = snapshot
|
const { getLoadable } = snapshot;
|
||||||
.getLoadable(
|
|
||||||
multipleRecordPickerSearchFilterComponentState.atomFamily({
|
const recordPickerSearchFilter = getLoadable(
|
||||||
instanceId: multipleRecordPickerInstanceId,
|
multipleRecordPickerSearchFilterComponentState.atomFamily({
|
||||||
}),
|
instanceId: multipleRecordPickerInstanceId,
|
||||||
)
|
}),
|
||||||
.getValue();
|
).getValue();
|
||||||
|
|
||||||
const searchFilter = forceSearchFilter ?? recordPickerSearchFilter;
|
const searchFilter = forceSearchFilter ?? recordPickerSearchFilter;
|
||||||
|
|
||||||
const recordPickerSearchableObjectMetadataItems = snapshot
|
const recordPickerSearchableObjectMetadataItems = getLoadable(
|
||||||
.getLoadable(
|
multipleRecordPickerSearchableObjectMetadataItemsComponentState.atomFamily(
|
||||||
multipleRecordPickerSearchableObjectMetadataItemsComponentState.atomFamily(
|
{ instanceId: multipleRecordPickerInstanceId },
|
||||||
{ instanceId: multipleRecordPickerInstanceId },
|
),
|
||||||
),
|
).getValue();
|
||||||
)
|
|
||||||
.getValue();
|
|
||||||
|
|
||||||
const searchableObjectMetadataItems =
|
const searchableObjectMetadataItems =
|
||||||
forceSearchableObjectMetadataItems.length > 0
|
forceSearchableObjectMetadataItems.length > 0
|
||||||
? forceSearchableObjectMetadataItems
|
? forceSearchableObjectMetadataItems
|
||||||
: recordPickerSearchableObjectMetadataItems;
|
: recordPickerSearchableObjectMetadataItems;
|
||||||
|
|
||||||
const recordPickerPickableMorphItems = snapshot
|
const recordPickerPickableMorphItems = getLoadable(
|
||||||
.getLoadable(
|
multipleRecordPickerPickableMorphItemsComponentState.atomFamily({
|
||||||
multipleRecordPickerPickableMorphItemsComponentState.atomFamily({
|
instanceId: multipleRecordPickerInstanceId,
|
||||||
instanceId: multipleRecordPickerInstanceId,
|
}),
|
||||||
}),
|
).getValue();
|
||||||
)
|
|
||||||
.getValue();
|
|
||||||
|
|
||||||
const pickableMorphItems =
|
const pickableMorphItems =
|
||||||
forcePickableMorphItems.length > 0
|
forcePickableMorphItems.length > 0
|
||||||
? forcePickableMorphItems
|
? forcePickableMorphItems
|
||||||
: recordPickerPickableMorphItems;
|
: recordPickerPickableMorphItems;
|
||||||
|
const selectedPickableMorphItems = pickableMorphItems.filter(
|
||||||
|
({ isSelected }) => isSelected,
|
||||||
|
);
|
||||||
|
|
||||||
const recordsWithObjectMetadataIdFilteredOnPickedRecords =
|
const [
|
||||||
await performSearchForPickedRecords({
|
searchRecordsFilteredOnPickedRecords,
|
||||||
client,
|
searchRecordsExcludingPickedRecords,
|
||||||
searchFilter,
|
] = await performSearchQueries({
|
||||||
searchableObjectMetadataItems,
|
client,
|
||||||
pickableMorphItems,
|
searchFilter,
|
||||||
});
|
searchableObjectMetadataItems,
|
||||||
|
pickedRecordIds: selectedPickableMorphItems.map(
|
||||||
const recordsWithObjectMetadataIdExcludingPickedRecords =
|
({ recordId }) => recordId,
|
||||||
await performSearchExcludingPickedRecords({
|
),
|
||||||
client,
|
});
|
||||||
searchFilter,
|
|
||||||
searchableObjectMetadataItems,
|
|
||||||
pickableMorphItems,
|
|
||||||
});
|
|
||||||
|
|
||||||
const pickedMorphItems = pickableMorphItems.filter(
|
const pickedMorphItems = pickableMorphItems.filter(
|
||||||
({ isSelected }) => isSelected,
|
({ isSelected }) => isSelected,
|
||||||
@ -87,10 +85,9 @@ export const useMultipleRecordPickerPerformSearch = () => {
|
|||||||
|
|
||||||
// We update the existing pickedMorphItems to be matching the search filter
|
// We update the existing pickedMorphItems to be matching the search filter
|
||||||
const updatedPickedMorphItems = pickedMorphItems.map((morphItem) => {
|
const updatedPickedMorphItems = pickedMorphItems.map((morphItem) => {
|
||||||
const record =
|
const record = searchRecordsFilteredOnPickedRecords.find(
|
||||||
recordsWithObjectMetadataIdFilteredOnPickedRecords.find(
|
({ recordId }) => recordId === morphItem.recordId,
|
||||||
({ record }) => record.id === morphItem.recordId,
|
);
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...morphItem,
|
...morphItem,
|
||||||
@ -98,40 +95,47 @@ export const useMultipleRecordPickerPerformSearch = () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const recordsWithObjectMetadataIdFilteredOnPickedRecordsWithoutDuplicates =
|
const searchRecordsFilteredOnPickedRecordsWithoutDuplicates =
|
||||||
recordsWithObjectMetadataIdFilteredOnPickedRecords.filter(
|
searchRecordsFilteredOnPickedRecords.filter(
|
||||||
({ record }) =>
|
(searchRecord) =>
|
||||||
!updatedPickedMorphItems.some(
|
!updatedPickedMorphItems.some(
|
||||||
({ recordId }) => recordId === record.id,
|
({ recordId }) => recordId === searchRecord.recordId,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const recordsWithObjectMetadataIdExcludingPickedRecordsWithoutDuplicates =
|
const searchRecordsExcludingPickedRecordsWithoutDuplicates =
|
||||||
recordsWithObjectMetadataIdExcludingPickedRecords.filter(
|
searchRecordsExcludingPickedRecords.filter(
|
||||||
({ record }) =>
|
(searchRecord) =>
|
||||||
!recordsWithObjectMetadataIdFilteredOnPickedRecords.some(
|
!searchRecordsFilteredOnPickedRecords.some(
|
||||||
({ record: recordFilteredOnPickedRecords }) =>
|
({ recordId }) => recordId === searchRecord.recordId,
|
||||||
recordFilteredOnPickedRecords.id === record.id,
|
|
||||||
) &&
|
) &&
|
||||||
!pickedMorphItems.some(({ recordId }) => recordId === record.id),
|
!pickedMorphItems.some(
|
||||||
|
({ recordId }) => recordId === searchRecord.recordId,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const morphItems = [
|
const morphItems = [
|
||||||
...updatedPickedMorphItems,
|
...updatedPickedMorphItems,
|
||||||
...recordsWithObjectMetadataIdFilteredOnPickedRecordsWithoutDuplicates.map(
|
...searchRecordsFilteredOnPickedRecordsWithoutDuplicates.map(
|
||||||
({ record, objectMetadataItem }) => ({
|
({ recordId, objectSingularName }) => ({
|
||||||
isMatchingSearchFilter: true,
|
isMatchingSearchFilter: true,
|
||||||
isSelected: true,
|
isSelected: true,
|
||||||
objectMetadataId: objectMetadataItem.id,
|
objectMetadataId: searchableObjectMetadataItems.find(
|
||||||
recordId: record.id,
|
(objectMetadata) =>
|
||||||
|
objectMetadata.nameSingular === objectSingularName,
|
||||||
|
)?.id,
|
||||||
|
recordId,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
...recordsWithObjectMetadataIdExcludingPickedRecordsWithoutDuplicates.map(
|
...searchRecordsExcludingPickedRecordsWithoutDuplicates.map(
|
||||||
({ record, objectMetadataItem }) => ({
|
({ recordId, objectSingularName }) => ({
|
||||||
isMatchingSearchFilter: true,
|
isMatchingSearchFilter: true,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
objectMetadataId: objectMetadataItem.id,
|
objectMetadataId: searchableObjectMetadataItems.find(
|
||||||
recordId: record.id,
|
(objectMetadata) =>
|
||||||
|
objectMetadata.nameSingular === objectSingularName,
|
||||||
|
)?.id,
|
||||||
|
recordId,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
@ -143,192 +147,153 @@ export const useMultipleRecordPickerPerformSearch = () => {
|
|||||||
morphItems,
|
morphItems,
|
||||||
);
|
);
|
||||||
|
|
||||||
[
|
const searchRecords = [
|
||||||
...recordsWithObjectMetadataIdFilteredOnPickedRecords,
|
...searchRecordsFilteredOnPickedRecords,
|
||||||
...recordsWithObjectMetadataIdExcludingPickedRecordsWithoutDuplicates,
|
...searchRecordsExcludingPickedRecordsWithoutDuplicates,
|
||||||
].forEach(({ record }) => {
|
];
|
||||||
set(recordStoreFamilyState(record.id), record);
|
|
||||||
|
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 };
|
return { performSearch };
|
||||||
};
|
};
|
||||||
|
|
||||||
const performSearchForPickedRecords = async ({
|
const performSearchQueries = async ({
|
||||||
client,
|
client,
|
||||||
searchFilter,
|
searchFilter,
|
||||||
searchableObjectMetadataItems,
|
searchableObjectMetadataItems,
|
||||||
pickableMorphItems,
|
pickedRecordIds,
|
||||||
}: {
|
}: {
|
||||||
client: ApolloClient<object>;
|
client: ApolloClient<object>;
|
||||||
searchFilter: string;
|
searchFilter: string;
|
||||||
searchableObjectMetadataItems: ObjectMetadataItem[];
|
searchableObjectMetadataItems: ObjectMetadataItem[];
|
||||||
pickableMorphItems: RecordPickerPickableMorphItem[];
|
pickedRecordIds: string[];
|
||||||
}) => {
|
}): Promise<[GlobalSearchRecord[], GlobalSearchRecord[]]> => {
|
||||||
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<CombinedFindManyRecordsQueryResult>({
|
|
||||||
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<object>;
|
|
||||||
searchFilter: string;
|
|
||||||
searchableObjectMetadataItems: ObjectMetadataItem[];
|
|
||||||
pickableMorphItems: RecordPickerPickableMorphItem[];
|
|
||||||
}) => {
|
|
||||||
if (searchableObjectMetadataItems.length === 0) {
|
if (searchableObjectMetadataItems.length === 0) {
|
||||||
return [];
|
return [[], []];
|
||||||
}
|
}
|
||||||
|
|
||||||
const pickedMorphItems = pickableMorphItems.filter(
|
const searchRecords = async (filter: any) => {
|
||||||
({ isSelected }) => isSelected,
|
const { data } = await client.query({
|
||||||
);
|
query: globalSearch,
|
||||||
|
|
||||||
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<CombinedFindManyRecordsQueryResult>({
|
|
||||||
query: combinedSearchRecordsQueryExcludingPickedRecords,
|
|
||||||
variables: {
|
variables: {
|
||||||
search: searchFilter,
|
searchInput: searchFilter,
|
||||||
...limitPerMetadataItem,
|
includedObjectNameSingulars: searchableObjectMetadataItems.map(
|
||||||
...filterPerMetadataItemExcludingPickedRecordId,
|
({ nameSingular }) => nameSingular,
|
||||||
|
),
|
||||||
|
filter,
|
||||||
|
limit: MAX_SEARCH_RESULTS,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
return data.globalSearch;
|
||||||
|
};
|
||||||
|
|
||||||
const {
|
const searchRecordsExcludingPickedRecords = await searchRecords(
|
||||||
recordsWithObjectMetadataId:
|
pickedRecordIds.length > 0
|
||||||
recordsWithObjectMetadataIdExcludingPickedRecords,
|
? {
|
||||||
} = multipleRecordPickerformatQueryResultAsRecordsWithObjectMetadataId({
|
not: {
|
||||||
objectMetadataItems: searchableObjectMetadataItems,
|
id: {
|
||||||
searchQueryResult: combinedSearchRecordExcludingPickedRecordsQueryResult,
|
in: pickedRecordIds,
|
||||||
});
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
|
||||||
return recordsWithObjectMetadataIdExcludingPickedRecords;
|
const searchRecordsIncludingPickedRecords =
|
||||||
|
pickedRecordIds.length > 0
|
||||||
|
? await searchRecords({
|
||||||
|
id: {
|
||||||
|
in: pickedRecordIds,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
searchRecordsIncludingPickedRecords,
|
||||||
|
searchRecordsExcludingPickedRecords,
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
|
});
|
||||||
@ -110,16 +110,22 @@ export const FieldsCard = ({
|
|||||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ActivityTargetsInlineCell
|
<RecordFieldComponentInstanceContext.Provider
|
||||||
activityObjectNameSingular={
|
value={{
|
||||||
objectNameSingular as
|
instanceId: objectRecordId + fieldMetadataItem.id,
|
||||||
| CoreObjectNameSingular.Note
|
}}
|
||||||
| CoreObjectNameSingular.Task
|
>
|
||||||
}
|
<ActivityTargetsInlineCell
|
||||||
activityRecordId={objectRecordId}
|
activityObjectNameSingular={
|
||||||
showLabel={true}
|
objectNameSingular as
|
||||||
maxWidth={200}
|
| CoreObjectNameSingular.Note
|
||||||
/>
|
| CoreObjectNameSingular.Task
|
||||||
|
}
|
||||||
|
activityRecordId={objectRecordId}
|
||||||
|
showLabel={true}
|
||||||
|
maxWidth={200}
|
||||||
|
/>
|
||||||
|
</RecordFieldComponentInstanceContext.Provider>
|
||||||
</FieldContext.Provider>
|
</FieldContext.Provider>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
import { capitalize } from 'twenty-shared';
|
|
||||||
|
|
||||||
export const getSearchRecordsQueryResponseField = (objectNamePlural: string) =>
|
|
||||||
`search${capitalize(objectNamePlural)}`;
|
|
||||||
@ -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 { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit';
|
||||||
import { useSearchRecords } from '@/object-record/hooks/useSearchRecords';
|
import { useObjectRecordSearchRecords } from '@/object-record/hooks/useObjectRecordSearchRecords';
|
||||||
import { MultipleRecordPickerRecords } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerRecords';
|
|
||||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
|
||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
|
|
||||||
export const useFilteredSearchRecordQuery = ({
|
export const useFilteredSearchRecordQuery = ({
|
||||||
@ -18,19 +16,16 @@ export const useFilteredSearchRecordQuery = ({
|
|||||||
excludedRecordIds?: string[];
|
excludedRecordIds?: string[];
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
searchFilter?: string;
|
searchFilter?: string;
|
||||||
}): MultipleRecordPickerRecords<SingleRecordPickerRecord> => {
|
}): {
|
||||||
const { mapToObjectRecordIdentifier } = useMapToObjectRecordIdentifier({
|
selectedRecords: SingleRecordPickerRecord[];
|
||||||
objectNameSingular,
|
filteredSelectedRecords: SingleRecordPickerRecord[];
|
||||||
});
|
recordsToSelect: SingleRecordPickerRecord[];
|
||||||
|
loading: boolean;
|
||||||
const mappingFunction = (record: ObjectRecord) => ({
|
} => {
|
||||||
...mapToObjectRecordIdentifier(record),
|
|
||||||
record,
|
|
||||||
});
|
|
||||||
const selectedIdsFilter = { id: { in: selectedIds } };
|
const selectedIdsFilter = { id: { in: selectedIds } };
|
||||||
|
|
||||||
const { loading: selectedRecordsLoading, records: selectedRecords } =
|
const { loading: selectedRecordsLoading, searchRecords: selectedRecords } =
|
||||||
useSearchRecords({
|
useObjectRecordSearchRecords({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
filter: selectedIdsFilter,
|
filter: selectedIdsFilter,
|
||||||
skip: !selectedIds.length,
|
skip: !selectedIds.length,
|
||||||
@ -39,8 +34,8 @@ export const useFilteredSearchRecordQuery = ({
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
loading: filteredSelectedRecordsLoading,
|
loading: filteredSelectedRecordsLoading,
|
||||||
records: filteredSelectedRecords,
|
searchRecords: filteredSelectedRecords,
|
||||||
} = useSearchRecords({
|
} = useObjectRecordSearchRecords({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
filter: selectedIdsFilter,
|
filter: selectedIdsFilter,
|
||||||
skip: !selectedIds.length,
|
skip: !selectedIds.length,
|
||||||
@ -51,8 +46,8 @@ export const useFilteredSearchRecordQuery = ({
|
|||||||
const notFilter = notFilterIds.length
|
const notFilter = notFilterIds.length
|
||||||
? { not: { id: { in: notFilterIds } } }
|
? { not: { id: { in: notFilterIds } } }
|
||||||
: undefined;
|
: undefined;
|
||||||
const { loading: recordsToSelectLoading, records: recordsToSelect } =
|
const { loading: recordsToSelectLoading, searchRecords: recordsToSelect } =
|
||||||
useSearchRecords({
|
useObjectRecordSearchRecords({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
filter: notFilter,
|
filter: notFilter,
|
||||||
limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT,
|
limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT,
|
||||||
@ -61,11 +56,15 @@ export const useFilteredSearchRecordQuery = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectedRecords: selectedRecords.map(mappingFunction).filter(isDefined),
|
selectedRecords: selectedRecords
|
||||||
filteredSelectedRecords: filteredSelectedRecords
|
.map(formatGlobalSearchRecordAsSingleRecordPickerRecord)
|
||||||
.map(mappingFunction)
|
.filter(isDefined),
|
||||||
|
filteredSelectedRecords: filteredSelectedRecords
|
||||||
|
.map(formatGlobalSearchRecordAsSingleRecordPickerRecord)
|
||||||
|
.filter(isDefined),
|
||||||
|
recordsToSelect: recordsToSelect
|
||||||
|
.map(formatGlobalSearchRecordAsSingleRecordPickerRecord)
|
||||||
.filter(isDefined),
|
.filter(isDefined),
|
||||||
recordsToSelect: recordsToSelect.map(mappingFunction).filter(isDefined),
|
|
||||||
loading:
|
loading:
|
||||||
recordsToSelectLoading ||
|
recordsToSelectLoading ||
|
||||||
filteredSelectedRecordsLoading ||
|
filteredSelectedRecordsLoading ||
|
||||||
|
|||||||
@ -21,7 +21,11 @@ import {
|
|||||||
Section,
|
Section,
|
||||||
TooltipDelay,
|
TooltipDelay,
|
||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
import { Role, WorkspaceMember } from '~/generated-metadata/graphql';
|
import {
|
||||||
|
GlobalSearchRecord,
|
||||||
|
Role,
|
||||||
|
WorkspaceMember,
|
||||||
|
} from '~/generated-metadata/graphql';
|
||||||
import {
|
import {
|
||||||
GetRolesDocument,
|
GetRolesDocument,
|
||||||
useGetRolesQuery,
|
useGetRolesQuery,
|
||||||
@ -129,14 +133,18 @@ export const RoleAssignment = ({ role }: RoleAssignmentProps) => {
|
|||||||
setSelectedWorkspaceMember(null);
|
setSelectedWorkspaceMember(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectWorkspaceMember = (workspaceMember: WorkspaceMember) => {
|
const handleSelectWorkspaceMember = (
|
||||||
const existingRole = workspaceMemberRoleMap.get(workspaceMember.id);
|
workspaceMemberSearchRecord: GlobalSearchRecord,
|
||||||
|
) => {
|
||||||
|
const existingRole = workspaceMemberRoleMap.get(
|
||||||
|
workspaceMemberSearchRecord.recordId,
|
||||||
|
);
|
||||||
|
|
||||||
setSelectedWorkspaceMember({
|
setSelectedWorkspaceMember({
|
||||||
id: workspaceMember.id,
|
id: workspaceMemberSearchRecord.recordId,
|
||||||
name: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`,
|
name: `${workspaceMemberSearchRecord.label}`,
|
||||||
role: existingRole,
|
role: existingRole,
|
||||||
avatarUrl: workspaceMember.avatarUrl,
|
avatarUrl: workspaceMemberSearchRecord.imageUrl,
|
||||||
});
|
});
|
||||||
setConfirmationModalIsOpen(true);
|
setConfirmationModalIsOpen(true);
|
||||||
closeDropdown();
|
closeDropdown();
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
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 { RoleAssignmentWorkspaceMemberPickerDropdownContent } from '@/settings/roles/role-assignment/components/RoleAssignmentWorkspaceMemberPickerDropdownContent';
|
||||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||||
import { ChangeEvent, useState } from 'react';
|
import { ChangeEvent, useState } from 'react';
|
||||||
import { WorkspaceMember } from '~/generated-metadata/graphql';
|
import { GlobalSearchRecord } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
type RoleAssignmentWorkspaceMemberPickerDropdownProps = {
|
type RoleAssignmentWorkspaceMemberPickerDropdownProps = {
|
||||||
excludedWorkspaceMemberIds: string[];
|
excludedWorkspaceMemberIds: string[];
|
||||||
onSelect: (workspaceMember: WorkspaceMember) => void;
|
onSelect: (workspaceMemberSearchRecord: GlobalSearchRecord) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RoleAssignmentWorkspaceMemberPickerDropdown = ({
|
export const RoleAssignmentWorkspaceMemberPickerDropdown = ({
|
||||||
@ -19,15 +19,17 @@ export const RoleAssignmentWorkspaceMemberPickerDropdown = ({
|
|||||||
}: RoleAssignmentWorkspaceMemberPickerDropdownProps) => {
|
}: RoleAssignmentWorkspaceMemberPickerDropdownProps) => {
|
||||||
const [searchFilter, setSearchFilter] = useState('');
|
const [searchFilter, setSearchFilter] = useState('');
|
||||||
|
|
||||||
const { loading, records: workspaceMembers } = useSearchRecords({
|
const { loading, searchRecords: workspaceMembers } =
|
||||||
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
|
useObjectRecordSearchRecords({
|
||||||
searchInput: searchFilter,
|
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
|
||||||
});
|
searchInput: searchFilter,
|
||||||
|
});
|
||||||
|
|
||||||
const filteredWorkspaceMembers = (workspaceMembers?.filter(
|
const filteredWorkspaceMembers =
|
||||||
(workspaceMember) =>
|
workspaceMembers?.filter(
|
||||||
!excludedWorkspaceMemberIds.includes(workspaceMember.id),
|
(workspaceMember) =>
|
||||||
) ?? []) as WorkspaceMember[];
|
!excludedWorkspaceMemberIds.includes(workspaceMember.recordId),
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
const handleSearchFilterChange = (event: ChangeEvent<HTMLInputElement>) => {
|
const handleSearchFilterChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
setSearchFilter(event.target.value);
|
setSearchFilter(event.target.value);
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { MenuItem, MenuItemAvatar } from 'twenty-ui';
|
import { MenuItem, MenuItemAvatar } from 'twenty-ui';
|
||||||
import { WorkspaceMember } from '~/generated-metadata/graphql';
|
import { GlobalSearchRecord } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
type RoleAssignmentWorkspaceMemberPickerDropdownContentProps = {
|
type RoleAssignmentWorkspaceMemberPickerDropdownContentProps = {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
searchFilter: string;
|
searchFilter: string;
|
||||||
filteredWorkspaceMembers: WorkspaceMember[];
|
filteredWorkspaceMembers: GlobalSearchRecord[];
|
||||||
onSelect: (workspaceMember: WorkspaceMember) => void;
|
onSelect: (workspaceMemberSearchRecord: GlobalSearchRecord) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RoleAssignmentWorkspaceMemberPickerDropdownContent = ({
|
export const RoleAssignmentWorkspaceMemberPickerDropdownContent = ({
|
||||||
@ -27,15 +27,15 @@ export const RoleAssignmentWorkspaceMemberPickerDropdownContent = ({
|
|||||||
<>
|
<>
|
||||||
{filteredWorkspaceMembers.map((workspaceMember) => (
|
{filteredWorkspaceMembers.map((workspaceMember) => (
|
||||||
<MenuItemAvatar
|
<MenuItemAvatar
|
||||||
key={workspaceMember.id}
|
key={workspaceMember.recordId}
|
||||||
onClick={() => onSelect(workspaceMember)}
|
onClick={() => onSelect(workspaceMember)}
|
||||||
avatar={{
|
avatar={{
|
||||||
type: 'rounded',
|
type: 'rounded',
|
||||||
size: 'md',
|
size: 'md',
|
||||||
placeholder: workspaceMember.name.firstName ?? '',
|
placeholder: workspaceMember.label ?? '',
|
||||||
placeholderColorSeed: workspaceMember.id,
|
placeholderColorSeed: workspaceMember.recordId,
|
||||||
}}
|
}}
|
||||||
text={`${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`}
|
text={workspaceMember.label}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user