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<{
|
||||
searchInput: Scalars['String'];
|
||||
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 GetClientConfigQueryResult = Apollo.QueryResult<GetClientConfigQuery, GetClientConfigQueryVariables>;
|
||||
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'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
||||
@ -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
|
||||
? {
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export const MAX_SEARCH_RESULTS = 30;
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
@ -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);
|
||||
|
||||
@ -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',
|
||||
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');
|
||||
|
||||
|
||||
@ -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 { 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 (
|
||||
<MultipleRecordPickerMenuItemContent
|
||||
record={record}
|
||||
searchRecord={searchRecord}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
@ -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 (
|
||||
<StyledSelectableItem itemId={record.id} key={record.id}>
|
||||
<StyledSelectableItem
|
||||
itemId={searchRecord.recordId}
|
||||
key={searchRecord.recordId}
|
||||
>
|
||||
<MenuItemMultiSelectAvatar
|
||||
onSelectChange={(isSelected) => handleSelectChange(isSelected)}
|
||||
isKeySelected={isSelectedByKeyboard}
|
||||
selected={isRecordSelectedWithObjectItem}
|
||||
avatar={
|
||||
<Avatar
|
||||
avatarUrl={recordIdentifier.avatarUrl}
|
||||
placeholderColorSeed={record.id}
|
||||
placeholder={recordIdentifier.name}
|
||||
avatarUrl={searchRecord.imageUrl}
|
||||
placeholderColorSeed={searchRecord.recordId}
|
||||
placeholder={searchRecord.label}
|
||||
size="md"
|
||||
type={recordIdentifier.avatarType ?? 'rounded'}
|
||||
type={getAvatarType(objectMetadataItem.nameSingular) ?? 'rounded'}
|
||||
/>
|
||||
}
|
||||
text={recordIdentifier.name}
|
||||
text={searchRecord.label}
|
||||
/>
|
||||
</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 { 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<object>;
|
||||
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<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[];
|
||||
}) => {
|
||||
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<CombinedFindManyRecordsQueryResult>({
|
||||
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,
|
||||
];
|
||||
};
|
||||
|
||||
@ -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,
|
||||
}}
|
||||
>
|
||||
<ActivityTargetsInlineCell
|
||||
activityObjectNameSingular={
|
||||
objectNameSingular as
|
||||
| CoreObjectNameSingular.Note
|
||||
| CoreObjectNameSingular.Task
|
||||
}
|
||||
activityRecordId={objectRecordId}
|
||||
showLabel={true}
|
||||
maxWidth={200}
|
||||
/>
|
||||
<RecordFieldComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: objectRecordId + fieldMetadataItem.id,
|
||||
}}
|
||||
>
|
||||
<ActivityTargetsInlineCell
|
||||
activityObjectNameSingular={
|
||||
objectNameSingular as
|
||||
| CoreObjectNameSingular.Note
|
||||
| CoreObjectNameSingular.Task
|
||||
}
|
||||
activityRecordId={objectRecordId}
|
||||
showLabel={true}
|
||||
maxWidth={200}
|
||||
/>
|
||||
</RecordFieldComponentInstanceContext.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 { 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<SingleRecordPickerRecord> => {
|
||||
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 ||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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<HTMLInputElement>) => {
|
||||
setSearchFilter(event.target.value);
|
||||
|
||||
@ -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) => (
|
||||
<MenuItemAvatar
|
||||
key={workspaceMember.id}
|
||||
key={workspaceMember.recordId}
|
||||
onClick={() => 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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user