-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use search in multi object pickers #7909
Changes from 9 commits
c095808
b8d65c9
6d1f7b9
45f26a3
39ad8ef
3fa3d90
cdcead6
79a576a
dbd3d3b
e3aea54
8fa9fc5
129950c
9237e78
aae71fb
2e9d262
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import { gql } from '@apollo/client'; | ||
import { isUndefined } from '@sniptt/guards'; | ||
import { useRecoilValue } from 'recoil'; | ||
|
||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; | ||
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; | ||
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature'; | ||
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; | ||
import { isObjectMetadataItemSearchable } from '@/object-record/multiple-objects/hooks/utils/isObjectMetadataItemSearchable'; | ||
import { getSearchRecordsQueryResponseField } from '@/object-record/utils/getSearchRecordsQueryResponseField'; | ||
import { isNonEmptyArray } from '~/utils/isNonEmptyArray'; | ||
import { capitalize } from '~/utils/string/capitalize'; | ||
|
||
export const useGenerateCombinedSearchRecordsQuery = ({ | ||
operationSignatures, | ||
}: { | ||
operationSignatures: RecordGqlOperationSignature[]; | ||
}) => { | ||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState); | ||
|
||
if (!isNonEmptyArray(operationSignatures)) { | ||
return null; | ||
} | ||
|
||
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 }) => | ||
isObjectMetadataItemSearchable(objectMetadataItem), | ||
); | ||
|
||
return gql` | ||
query CombinedSearchRecords( | ||
${filterPerMetadataItemArray}, | ||
${limitPerMetadataItemArray}, | ||
$search: String, | ||
) { | ||
${filteredQueryKeyWithObjectMetadataItemArray | ||
.map( | ||
({ objectMetadataItem, fields }) => | ||
`${getSearchRecordsQueryResponseField(objectMetadataItem.namePlural)}(filter: $filter${capitalize( | ||
objectMetadataItem.nameSingular, | ||
)}, | ||
limit: $limit${capitalize(objectMetadataItem.nameSingular)}, | ||
searchInput: $search | ||
){ | ||
edges { | ||
node ${mapObjectMetadataToGraphQLQuery({ | ||
objectMetadataItems: objectMetadataItems, | ||
objectMetadataItem, | ||
recordGqlFields: | ||
fields ?? | ||
generateDepthOneRecordGqlFields({ | ||
objectMetadataItem, | ||
}), | ||
})} | ||
cursor | ||
} | ||
totalCount | ||
}`, | ||
) | ||
.join('\n')} | ||
} | ||
`; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; | ||
|
||
const SEARCHABLE_STANDARD_OBJECTS_NAMES_PLURAL = [ | ||
'companies', | ||
'people', | ||
'opportunities', | ||
]; | ||
export const isObjectMetadataItemSearchable = ( | ||
objectMetadataItem: ObjectMetadataItem, | ||
) => { | ||
return ( | ||
objectMetadataItem.isCustom || | ||
SEARCHABLE_STANDARD_OBJECTS_NAMES_PLURAL.includes( | ||
objectMetadataItem.namePlural, | ||
) | ||
); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: Function looks correct, but consider adding a comment explaining the logic behind determining searchability |
||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,17 +5,32 @@ import { useRecoilValue } from 'recoil'; | |
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; | ||
import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery'; | ||
import { useGenerateCombinedFindManyRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery'; | ||
import { useGenerateCombinedSearchRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedSearchRecordsQuery'; | ||
import { useLimitPerMetadataItem } from '@/object-record/relation-picker/hooks/useLimitPerMetadataItem'; | ||
import { | ||
MultiObjectRecordQueryResult, | ||
useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray, | ||
} from '@/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray'; | ||
import { SelectedObjectRecordId } from '@/object-record/relation-picker/hooks/useMultiObjectSearch'; | ||
import { useOrderByFieldPerMetadataItem } from '@/object-record/relation-picker/hooks/useOrderByFieldPerMetadataItem'; | ||
import { useSearchFilterPerMetadataItem } from '@/object-record/relation-picker/hooks/useSearchFilterPerMetadataItem'; | ||
import { useMemo } from 'react'; | ||
import { isDefined } from '~/utils/isDefined'; | ||
import { capitalize } from '~/utils/string/capitalize'; | ||
|
||
export const formatSearchResults = ( | ||
searchResults: MultiObjectRecordQueryResult | undefined, | ||
): MultiObjectRecordQueryResult => { | ||
if (!searchResults) { | ||
return {}; | ||
} | ||
|
||
return Object.entries(searchResults).reduce((acc, [key, value]) => { | ||
let newKey = key.replace(/^search/, ''); | ||
newKey = newKey.charAt(0).toLowerCase() + newKey.slice(1); | ||
acc[newKey] = value; | ||
return acc; | ||
}, {} as MultiObjectRecordQueryResult); | ||
}; | ||
Comment on lines
+18
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: consider moving this utility function to a separate file for better code organization |
||
|
||
export const useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery = ({ | ||
selectedObjectRecordIds, | ||
searchFilterValue, | ||
|
@@ -27,18 +42,14 @@ export const useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery = ({ | |
}) => { | ||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState); | ||
|
||
const { searchFilterPerMetadataItemNameSingular } = | ||
useSearchFilterPerMetadataItem({ | ||
objectMetadataItems, | ||
searchFilterValue, | ||
}); | ||
|
||
const objectMetadataItemsUsedInSelectedIdsQuery = objectMetadataItems.filter( | ||
({ nameSingular }) => { | ||
return selectedObjectRecordIds.some(({ objectNameSingular }) => { | ||
return objectNameSingular === nameSingular; | ||
}); | ||
}, | ||
const objectMetadataItemsUsedInSelectedIdsQuery = useMemo( | ||
() => | ||
objectMetadataItems.filter(({ nameSingular }) => { | ||
return selectedObjectRecordIds.some(({ objectNameSingular }) => { | ||
return objectNameSingular === nameSingular; | ||
}); | ||
}), | ||
[objectMetadataItems, selectedObjectRecordIds], | ||
); | ||
|
||
const selectedAndMatchesSearchFilterTextFilterPerMetadataItem = | ||
|
@@ -53,31 +64,18 @@ export const useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery = ({ | |
|
||
if (!isNonEmptyArray(selectedIds)) return null; | ||
|
||
const searchFilter = | ||
searchFilterPerMetadataItemNameSingular[nameSingular] ?? {}; | ||
return [ | ||
`filter${capitalize(nameSingular)}`, | ||
{ | ||
and: [ | ||
{ | ||
...searchFilter, | ||
}, | ||
{ | ||
id: { | ||
in: selectedIds, | ||
}, | ||
}, | ||
], | ||
id: { | ||
in: selectedIds, | ||
}, | ||
}, | ||
]; | ||
}) | ||
.filter(isDefined), | ||
); | ||
|
||
const { orderByFieldPerMetadataItem } = useOrderByFieldPerMetadataItem({ | ||
objectMetadataItems: objectMetadataItemsUsedInSelectedIdsQuery, | ||
}); | ||
|
||
const { limitPerMetadataItem } = useLimitPerMetadataItem({ | ||
objectMetadataItems: objectMetadataItemsUsedInSelectedIdsQuery, | ||
limit, | ||
|
@@ -93,15 +91,25 @@ export const useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery = ({ | |
), | ||
}); | ||
|
||
const multiSelectSearchQueryForSelectedIds = | ||
useGenerateCombinedSearchRecordsQuery({ | ||
operationSignatures: objectMetadataItemsUsedInSelectedIdsQuery.map( | ||
(objectMetadataItem) => ({ | ||
objectNameSingular: objectMetadataItem.nameSingular, | ||
variables: {}, | ||
}), | ||
), | ||
}); | ||
|
||
const { | ||
loading: selectedAndMatchesSearchFilterObjectRecordsLoading, | ||
data: selectedAndMatchesSearchFilterObjectRecordsQueryResult, | ||
} = useQuery<MultiObjectRecordQueryResult>( | ||
multiSelectQueryForSelectedIds ?? EMPTY_QUERY, | ||
multiSelectSearchQueryForSelectedIds ?? EMPTY_QUERY, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: multiSelectSearchQueryForSelectedIds is used here, but multiSelectQueryForSelectedIds is used in the skip condition. This inconsistency could lead to unexpected behavior |
||
{ | ||
variables: { | ||
search: searchFilterValue, | ||
...selectedAndMatchesSearchFilterTextFilterPerMetadataItem, | ||
...orderByFieldPerMetadataItem, | ||
...limitPerMetadataItem, | ||
}, | ||
skip: !isDefined(multiSelectQueryForSelectedIds), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: ensure multiSelectQueryForSelectedIds is still needed, or if it should be replaced with multiSelectSearchQueryForSelectedIds |
||
|
@@ -111,8 +119,9 @@ export const useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery = ({ | |
const { | ||
objectRecordForSelectArray: selectedAndMatchesSearchFilterObjectRecords, | ||
} = useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray({ | ||
multiObjectRecordsQueryResult: | ||
multiObjectRecordsQueryResult: formatSearchResults( | ||
selectedAndMatchesSearchFilterObjectRecordsQueryResult, | ||
), | ||
}); | ||
|
||
return { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Consider adding a non-null constraint to the $search parameter if it's required