mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #951 from omnivore-app/feature/typeahead-search
feature/typeahead search
This commit is contained in:
commit
ab687ca832
13 changed files with 393 additions and 15 deletions
|
|
@ -548,3 +548,57 @@ export const deletePagesByParam = async <K extends keyof ParamSet>(
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const searchAsYouType = async (
|
||||
userId: string,
|
||||
query: string,
|
||||
size = 5
|
||||
): Promise<Page[]> => {
|
||||
try {
|
||||
const { body } = await client.search<SearchResponse<Page>>({
|
||||
index: INDEX_ALIAS,
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
term: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
{
|
||||
multi_match: {
|
||||
query,
|
||||
type: 'bool_prefix',
|
||||
fields: [
|
||||
'title',
|
||||
'title._2gram',
|
||||
'title._3gram',
|
||||
'siteName',
|
||||
'siteName._2gram',
|
||||
'siteName._3gram',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
_source: ['title', 'slug', 'siteName'],
|
||||
size,
|
||||
},
|
||||
})
|
||||
|
||||
if (body.hits.total.value === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
return body.hits.hits.map((hit: { _source: Page; _id: string }) => ({
|
||||
...hit._source,
|
||||
id: hit._id,
|
||||
}))
|
||||
} catch (e) {
|
||||
console.error('failed to search as you type in elastic', e)
|
||||
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1280,6 +1280,7 @@ export type Query = {
|
|||
sendInstallInstructions: SendInstallInstructionsResult;
|
||||
sharedArticle: SharedArticleResult;
|
||||
subscriptions: SubscriptionsResult;
|
||||
typeaheadSearch: TypeaheadSearchResult;
|
||||
user: UserResult;
|
||||
users: UsersResult;
|
||||
validateUsername: Scalars['Boolean'];
|
||||
|
|
@ -1351,6 +1352,12 @@ export type QuerySubscriptionsArgs = {
|
|||
};
|
||||
|
||||
|
||||
export type QueryTypeaheadSearchArgs = {
|
||||
first?: InputMaybe<Scalars['Int']>;
|
||||
query: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryUserArgs = {
|
||||
userId?: InputMaybe<Scalars['ID']>;
|
||||
username?: InputMaybe<Scalars['String']>;
|
||||
|
|
@ -1922,6 +1929,30 @@ export type SubscriptionsSuccess = {
|
|||
subscriptions: Array<Subscription>;
|
||||
};
|
||||
|
||||
export type TypeaheadSearchError = {
|
||||
__typename?: 'TypeaheadSearchError';
|
||||
errorCodes: Array<TypeaheadSearchErrorCode>;
|
||||
};
|
||||
|
||||
export enum TypeaheadSearchErrorCode {
|
||||
Unauthorized = 'UNAUTHORIZED'
|
||||
}
|
||||
|
||||
export type TypeaheadSearchItem = {
|
||||
__typename?: 'TypeaheadSearchItem';
|
||||
id: Scalars['ID'];
|
||||
siteName?: Maybe<Scalars['String']>;
|
||||
slug: Scalars['String'];
|
||||
title: Scalars['String'];
|
||||
};
|
||||
|
||||
export type TypeaheadSearchResult = TypeaheadSearchError | TypeaheadSearchSuccess;
|
||||
|
||||
export type TypeaheadSearchSuccess = {
|
||||
__typename?: 'TypeaheadSearchSuccess';
|
||||
items: Array<TypeaheadSearchItem>;
|
||||
};
|
||||
|
||||
export type UnsubscribeError = {
|
||||
__typename?: 'UnsubscribeError';
|
||||
errorCodes: Array<UnsubscribeErrorCode>;
|
||||
|
|
@ -2664,6 +2695,11 @@ export type ResolversTypes = {
|
|||
SubscriptionsErrorCode: SubscriptionsErrorCode;
|
||||
SubscriptionsResult: ResolversTypes['SubscriptionsError'] | ResolversTypes['SubscriptionsSuccess'];
|
||||
SubscriptionsSuccess: ResolverTypeWrapper<SubscriptionsSuccess>;
|
||||
TypeaheadSearchError: ResolverTypeWrapper<TypeaheadSearchError>;
|
||||
TypeaheadSearchErrorCode: TypeaheadSearchErrorCode;
|
||||
TypeaheadSearchItem: ResolverTypeWrapper<TypeaheadSearchItem>;
|
||||
TypeaheadSearchResult: ResolversTypes['TypeaheadSearchError'] | ResolversTypes['TypeaheadSearchSuccess'];
|
||||
TypeaheadSearchSuccess: ResolverTypeWrapper<TypeaheadSearchSuccess>;
|
||||
UnsubscribeError: ResolverTypeWrapper<UnsubscribeError>;
|
||||
UnsubscribeErrorCode: UnsubscribeErrorCode;
|
||||
UnsubscribeResult: ResolversTypes['UnsubscribeError'] | ResolversTypes['UnsubscribeSuccess'];
|
||||
|
|
@ -2961,6 +2997,10 @@ export type ResolversParentTypes = {
|
|||
SubscriptionsError: SubscriptionsError;
|
||||
SubscriptionsResult: ResolversParentTypes['SubscriptionsError'] | ResolversParentTypes['SubscriptionsSuccess'];
|
||||
SubscriptionsSuccess: SubscriptionsSuccess;
|
||||
TypeaheadSearchError: TypeaheadSearchError;
|
||||
TypeaheadSearchItem: TypeaheadSearchItem;
|
||||
TypeaheadSearchResult: ResolversParentTypes['TypeaheadSearchError'] | ResolversParentTypes['TypeaheadSearchSuccess'];
|
||||
TypeaheadSearchSuccess: TypeaheadSearchSuccess;
|
||||
UnsubscribeError: UnsubscribeError;
|
||||
UnsubscribeResult: ResolversParentTypes['UnsubscribeError'] | ResolversParentTypes['UnsubscribeSuccess'];
|
||||
UnsubscribeSuccess: UnsubscribeSuccess;
|
||||
|
|
@ -3782,6 +3822,7 @@ export type QueryResolvers<ContextType = ResolverContext, ParentType extends Res
|
|||
sendInstallInstructions?: Resolver<ResolversTypes['SendInstallInstructionsResult'], ParentType, ContextType>;
|
||||
sharedArticle?: Resolver<ResolversTypes['SharedArticleResult'], ParentType, ContextType, RequireFields<QuerySharedArticleArgs, 'slug' | 'username'>>;
|
||||
subscriptions?: Resolver<ResolversTypes['SubscriptionsResult'], ParentType, ContextType, Partial<QuerySubscriptionsArgs>>;
|
||||
typeaheadSearch?: Resolver<ResolversTypes['TypeaheadSearchResult'], ParentType, ContextType, RequireFields<QueryTypeaheadSearchArgs, 'query'>>;
|
||||
user?: Resolver<ResolversTypes['UserResult'], ParentType, ContextType, Partial<QueryUserArgs>>;
|
||||
users?: Resolver<ResolversTypes['UsersResult'], ParentType, ContextType>;
|
||||
validateUsername?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<QueryValidateUsernameArgs, 'username'>>;
|
||||
|
|
@ -4138,6 +4179,28 @@ export type SubscriptionsSuccessResolvers<ContextType = ResolverContext, ParentT
|
|||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type TypeaheadSearchErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['TypeaheadSearchError'] = ResolversParentTypes['TypeaheadSearchError']> = {
|
||||
errorCodes?: Resolver<Array<ResolversTypes['TypeaheadSearchErrorCode']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type TypeaheadSearchItemResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['TypeaheadSearchItem'] = ResolversParentTypes['TypeaheadSearchItem']> = {
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
siteName?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
slug?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
title?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type TypeaheadSearchResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['TypeaheadSearchResult'] = ResolversParentTypes['TypeaheadSearchResult']> = {
|
||||
__resolveType: TypeResolveFn<'TypeaheadSearchError' | 'TypeaheadSearchSuccess', ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type TypeaheadSearchSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['TypeaheadSearchSuccess'] = ResolversParentTypes['TypeaheadSearchSuccess']> = {
|
||||
items?: Resolver<Array<ResolversTypes['TypeaheadSearchItem']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type UnsubscribeErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UnsubscribeError'] = ResolversParentTypes['UnsubscribeError']> = {
|
||||
errorCodes?: Resolver<Array<ResolversTypes['UnsubscribeErrorCode']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
|
|
@ -4574,6 +4637,10 @@ export type Resolvers<ContextType = ResolverContext> = {
|
|||
SubscriptionsError?: SubscriptionsErrorResolvers<ContextType>;
|
||||
SubscriptionsResult?: SubscriptionsResultResolvers<ContextType>;
|
||||
SubscriptionsSuccess?: SubscriptionsSuccessResolvers<ContextType>;
|
||||
TypeaheadSearchError?: TypeaheadSearchErrorResolvers<ContextType>;
|
||||
TypeaheadSearchItem?: TypeaheadSearchItemResolvers<ContextType>;
|
||||
TypeaheadSearchResult?: TypeaheadSearchResultResolvers<ContextType>;
|
||||
TypeaheadSearchSuccess?: TypeaheadSearchSuccessResolvers<ContextType>;
|
||||
UnsubscribeError?: UnsubscribeErrorResolvers<ContextType>;
|
||||
UnsubscribeResult?: UnsubscribeResultResolvers<ContextType>;
|
||||
UnsubscribeSuccess?: UnsubscribeSuccessResolvers<ContextType>;
|
||||
|
|
|
|||
|
|
@ -924,6 +924,7 @@ type Query {
|
|||
sendInstallInstructions: SendInstallInstructionsResult!
|
||||
sharedArticle(selectedHighlightId: String, slug: String!, username: String!): SharedArticleResult!
|
||||
subscriptions(sort: SortParams): SubscriptionsResult!
|
||||
typeaheadSearch(first: Int, query: String!): TypeaheadSearchResult!
|
||||
user(userId: ID, username: String): UserResult!
|
||||
users: UsersResult!
|
||||
validateUsername(username: String!): Boolean!
|
||||
|
|
@ -1443,6 +1444,27 @@ type SubscriptionsSuccess {
|
|||
subscriptions: [Subscription!]!
|
||||
}
|
||||
|
||||
type TypeaheadSearchError {
|
||||
errorCodes: [TypeaheadSearchErrorCode!]!
|
||||
}
|
||||
|
||||
enum TypeaheadSearchErrorCode {
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
type TypeaheadSearchItem {
|
||||
id: ID!
|
||||
siteName: String
|
||||
slug: String!
|
||||
title: String!
|
||||
}
|
||||
|
||||
union TypeaheadSearchResult = TypeaheadSearchError | TypeaheadSearchSuccess
|
||||
|
||||
type TypeaheadSearchSuccess {
|
||||
items: [TypeaheadSearchItem!]!
|
||||
}
|
||||
|
||||
type UnsubscribeError {
|
||||
errorCodes: [UnsubscribeErrorCode!]!
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import {
|
|||
QueryArticleArgs,
|
||||
QueryArticlesArgs,
|
||||
QuerySearchArgs,
|
||||
QueryTypeaheadSearchArgs,
|
||||
ResolverFn,
|
||||
SaveArticleReadingProgressError,
|
||||
SaveArticleReadingProgressErrorCode,
|
||||
|
|
@ -35,6 +36,9 @@ import {
|
|||
SetShareArticleError,
|
||||
SetShareArticleErrorCode,
|
||||
SetShareArticleSuccess,
|
||||
TypeaheadSearchError,
|
||||
TypeaheadSearchErrorCode,
|
||||
TypeaheadSearchSuccess,
|
||||
} from '../../generated/graphql'
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Merge } from '../../util'
|
||||
|
|
@ -82,6 +86,7 @@ import {
|
|||
deletePage,
|
||||
getPageById,
|
||||
getPageByParam,
|
||||
searchAsYouType,
|
||||
searchPages,
|
||||
updatePage,
|
||||
} from '../../elastic/pages'
|
||||
|
|
@ -889,3 +894,25 @@ export const searchResolver = authorized<
|
|||
},
|
||||
}
|
||||
})
|
||||
|
||||
export const typeaheadSearchResolver = authorized<
|
||||
TypeaheadSearchSuccess,
|
||||
TypeaheadSearchError,
|
||||
QueryTypeaheadSearchArgs
|
||||
>(async (_obj, { query, first }, { claims }) => {
|
||||
if (!claims?.uid) {
|
||||
return { errorCodes: [TypeaheadSearchErrorCode.Unauthorized] }
|
||||
}
|
||||
|
||||
analytics.track({
|
||||
userId: claims.uid,
|
||||
event: 'typeahead',
|
||||
properties: {
|
||||
env: env.server.apiEnv,
|
||||
query,
|
||||
first,
|
||||
},
|
||||
})
|
||||
|
||||
return { items: await searchAsYouType(claims.uid, query, first || undefined) }
|
||||
})
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ import {
|
|||
signupResolver,
|
||||
subscribeResolver,
|
||||
subscriptionsResolver,
|
||||
typeaheadSearchResolver,
|
||||
unsubscribeResolver,
|
||||
updateHighlightResolver,
|
||||
updateLabelResolver,
|
||||
|
|
@ -188,6 +189,7 @@ export const functionResolvers = {
|
|||
webhooks: webhooksResolver,
|
||||
webhook: webhookResolver,
|
||||
apiKeys: apiKeysResolver,
|
||||
typeaheadSearch: typeaheadSearchResolver,
|
||||
},
|
||||
User: {
|
||||
async sharedArticles(
|
||||
|
|
@ -589,4 +591,5 @@ export const functionResolvers = {
|
|||
...resultResolveTypeResolver('ApiKeys'),
|
||||
...resultResolveTypeResolver('RevokeApiKey'),
|
||||
...resultResolveTypeResolver('DeleteAccount'),
|
||||
...resultResolveTypeResolver('TypeaheadSearch'),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1763,6 +1763,27 @@ const schema = gql`
|
|||
labelIds: [ID!]!
|
||||
}
|
||||
|
||||
union TypeaheadSearchResult = TypeaheadSearchSuccess | TypeaheadSearchError
|
||||
|
||||
type TypeaheadSearchSuccess {
|
||||
items: [TypeaheadSearchItem!]!
|
||||
}
|
||||
|
||||
type TypeaheadSearchError {
|
||||
errorCodes: [TypeaheadSearchErrorCode!]!
|
||||
}
|
||||
|
||||
enum TypeaheadSearchErrorCode {
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
type TypeaheadSearchItem {
|
||||
id: ID!
|
||||
title: String!
|
||||
slug: String!
|
||||
siteName: String
|
||||
}
|
||||
|
||||
# Mutations
|
||||
type Mutation {
|
||||
googleLogin(input: GoogleLoginInput!): LoginResult!
|
||||
|
|
@ -1876,6 +1897,7 @@ const schema = gql`
|
|||
webhooks: WebhooksResult!
|
||||
webhook(id: ID!): WebhookResult!
|
||||
apiKeys: ApiKeysResult!
|
||||
typeaheadSearch(query: String!, first: Int): TypeaheadSearchResult!
|
||||
}
|
||||
`
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
deletePagesByParam,
|
||||
getPageById,
|
||||
getPageByParam,
|
||||
searchAsYouType,
|
||||
searchPages,
|
||||
updatePage,
|
||||
} from '../../src/elastic/pages'
|
||||
|
|
@ -339,4 +340,39 @@ describe('elastic api', () => {
|
|||
expect(deleted).to.be.true
|
||||
})
|
||||
})
|
||||
|
||||
describe('searchAsYouType', () => {
|
||||
before(async () => {
|
||||
// create a testing page
|
||||
await createPage(
|
||||
{
|
||||
content: '',
|
||||
createdAt: new Date(),
|
||||
hash: '',
|
||||
id: '',
|
||||
pageType: PageType.Article,
|
||||
readingProgressAnchorIndex: 0,
|
||||
readingProgressPercent: 0,
|
||||
savedAt: new Date(),
|
||||
slug: '',
|
||||
state: ArticleSavingRequestStatus.Succeeded,
|
||||
title: 'search as you type',
|
||||
url: '',
|
||||
userId,
|
||||
},
|
||||
ctx
|
||||
)
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
// delete the testing page
|
||||
await deletePagesByParam({ userId }, ctx)
|
||||
})
|
||||
|
||||
it('searches pages', async () => {
|
||||
const searchResults = await searchAsYouType(userId, 'search')
|
||||
expect(searchResults).to.have.lengthOf(1)
|
||||
expect(searchResults[0].title).to.eq('search as you type')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -317,6 +317,25 @@ const saveArticleReadingProgressQuery = (
|
|||
`
|
||||
}
|
||||
|
||||
const typeaheadSearchQuery = (keyword: string) => {
|
||||
return `
|
||||
query {
|
||||
typeaheadSearch(query: "${keyword}") {
|
||||
... on TypeaheadSearchSuccess {
|
||||
items {
|
||||
id
|
||||
slug
|
||||
title
|
||||
}
|
||||
}
|
||||
... on TypeaheadSearchError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
describe('Article API', () => {
|
||||
const username = 'fakeUser'
|
||||
let authToken: string
|
||||
|
|
@ -1069,4 +1088,54 @@ describe('Article API', () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('TypeaheadSearch API', () => {
|
||||
const pages: Page[] = []
|
||||
|
||||
let query = ''
|
||||
let keyword = 'typeahead'
|
||||
|
||||
before(async () => {
|
||||
// Create some test pages
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const page: Page = {
|
||||
id: '',
|
||||
hash: '',
|
||||
userId: user.id,
|
||||
pageType: PageType.Article,
|
||||
title: 'typeahead search page',
|
||||
content: '',
|
||||
slug: '',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
readingProgressPercent: 0,
|
||||
readingProgressAnchorIndex: 0,
|
||||
url: '',
|
||||
savedAt: new Date(),
|
||||
state: ArticleSavingRequestStatus.Succeeded,
|
||||
}
|
||||
const pageId = await createPage(page, ctx)
|
||||
if (!pageId) {
|
||||
expect.fail('Failed to create page')
|
||||
}
|
||||
page.id = pageId
|
||||
pages.push(page)
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
query = typeaheadSearchQuery(keyword)
|
||||
})
|
||||
|
||||
it('should return pages with typeahead prefix', async () => {
|
||||
const res = await graphqlRequest(query, authToken).expect(200)
|
||||
|
||||
expect(res.body.data.search.edges.length).to.eql(5)
|
||||
expect(res.body.data.search.edges[0].node.id).to.eq(pages[4].id)
|
||||
expect(res.body.data.search.edges[1].node.id).to.eq(pages[3].id)
|
||||
expect(res.body.data.search.edges[2].node.id).to.eq(pages[2].id)
|
||||
expect(res.body.data.search.edges[3].node.id).to.eq(pages[1].id)
|
||||
expect(res.body.data.search.edges[4].node.id).to.eq(pages[0].id)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
"type": "search_as_you_type"
|
||||
},
|
||||
"author": {
|
||||
"type": "text"
|
||||
|
|
@ -130,7 +130,7 @@
|
|||
"type": "date"
|
||||
},
|
||||
"siteName": {
|
||||
"type": "text"
|
||||
"type": "search_as_you_type"
|
||||
},
|
||||
"subscription": {
|
||||
"type": "keyword",
|
||||
|
|
|
|||
|
|
@ -14,13 +14,14 @@ export const searchStyle = {
|
|||
}
|
||||
|
||||
export const animatorStyle = {
|
||||
maxWidth: '600px',
|
||||
width: '100%',
|
||||
backgroundColor: theme.colors.grayBase.toString(),
|
||||
color: theme.colors.grayTextContrast.toString(),
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0px 6px 20px rgba(0, 0, 0, 0.2)',
|
||||
maxWidth: '600px',
|
||||
borderRadius: '8px',
|
||||
color: theme.colors.grayTextContrast.toString(),
|
||||
backgroundColor: theme.colors.grayBase.toString(),
|
||||
boxShadow: theme.shadows.cardBoxShadow.toString(),
|
||||
border: `1px solid ${theme.colors.grayBorder.toString()}`,
|
||||
}
|
||||
|
||||
const groupNameStyle = {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import { EditTitleModal } from './EditTitleModal'
|
|||
import { useGetUserPreferences } from '../../../lib/networking/queries/useGetUserPreferences'
|
||||
import { searchQuery } from '../../../lib/networking/queries/search'
|
||||
import debounce from 'lodash/debounce'
|
||||
import { SearchItem, TypeaheadSearchItemsData, typeaheadSearchQuery } from '../../../lib/networking/queries/typeaheadSearch'
|
||||
|
||||
export type LayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT'
|
||||
|
||||
|
|
@ -66,7 +67,7 @@ const SAVED_SEARCHES: Record<string, string> = {
|
|||
|
||||
const fetchSearchResults = async (query: string, cb: any) => {
|
||||
if (!query.startsWith('#')) return
|
||||
const res = await searchQuery({ limit: 10, searchQuery: query.substring(1)})
|
||||
const res = await typeaheadSearchQuery({ limit: 10, searchQuery: query.substring(1)})
|
||||
cb(res);
|
||||
};
|
||||
|
||||
|
|
@ -80,7 +81,7 @@ export function HomeFeedContainer(): JSX.Element {
|
|||
const { viewerData } = useGetViewerQuery()
|
||||
const router = useRouter()
|
||||
const { queryValue } = useKBar((state) => ({queryValue: state.searchQuery}));
|
||||
const [searchResults, setSearchResults] = useState<LibraryItem[]>([]);
|
||||
const [searchResults, setSearchResults] = useState<SearchItem[]>([]);
|
||||
|
||||
const defaultQuery = {
|
||||
limit: 10,
|
||||
|
|
@ -121,8 +122,8 @@ export function HomeFeedContainer(): JSX.Element {
|
|||
|
||||
useEffect(() => {
|
||||
if (queryValue.startsWith('#')) {
|
||||
debouncedFetchSearchResults(queryValue, (data: LibraryItemsData) => {
|
||||
setSearchResults(data?.search.edges || [])
|
||||
debouncedFetchSearchResults(queryValue, (data: TypeaheadSearchItemsData) => {
|
||||
setSearchResults(data?.typeaheadSearch.items || [])
|
||||
})
|
||||
}
|
||||
else setSearchResults([])
|
||||
|
|
@ -506,11 +507,17 @@ export function HomeFeedContainer(): JSX.Element {
|
|||
]
|
||||
|
||||
useRegisterActions(searchResults.map(link => ({
|
||||
id: link.node.id,
|
||||
id: link.id,
|
||||
section: 'Search Results',
|
||||
name: link.node.title,
|
||||
keywords: '#' + link.node.title,
|
||||
perform: () => handleCardAction('showDetail', link),
|
||||
name: link.title,
|
||||
keywords: '#' + link.title,
|
||||
perform: () => {
|
||||
const username = viewerData?.me?.profile.username
|
||||
if (username) {
|
||||
setActiveCardId(link.id)
|
||||
router.push(`/${username}/${link.slug}`)
|
||||
}
|
||||
},
|
||||
})), [searchResults])
|
||||
|
||||
useRegisterActions(activeCardId ? [...ACTIVE_ACTIONS, ...UNACTIVE_ACTIONS] : UNACTIVE_ACTIONS, [activeCardId, activeItem]);
|
||||
|
|
|
|||
58
packages/web/lib/networking/queries/typeaheadSearch.tsx
Normal file
58
packages/web/lib/networking/queries/typeaheadSearch.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
|
||||
export type LibraryItemsQueryInput = {
|
||||
limit?: number
|
||||
searchQuery?: string
|
||||
}
|
||||
|
||||
export type TypeaheadSearchItemsData = {
|
||||
typeaheadSearch: SearchItems
|
||||
}
|
||||
|
||||
export type SearchItems = {
|
||||
items: SearchItem[]
|
||||
}
|
||||
|
||||
export type SearchItem = {
|
||||
id: string
|
||||
title: string
|
||||
slug: string
|
||||
siteName?: string
|
||||
}
|
||||
|
||||
export async function typeaheadSearchQuery({
|
||||
limit = 10,
|
||||
searchQuery,
|
||||
}: LibraryItemsQueryInput): Promise<TypeaheadSearchItemsData | undefined> {
|
||||
const query = gql`
|
||||
query TypeaheadSearch($query: String!, $first: Int) {
|
||||
typeaheadSearch(query: $query, first: $first) {
|
||||
... on TypeaheadSearchSuccess {
|
||||
items {
|
||||
id
|
||||
title
|
||||
slug
|
||||
siteName
|
||||
}
|
||||
}
|
||||
... on TypeaheadSearchError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const variables = {
|
||||
first: limit,
|
||||
query: searchQuery,
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await gqlFetcher(query, { ...variables })
|
||||
return (data as TypeaheadSearchItemsData) || undefined
|
||||
} catch (error) {
|
||||
console.log('search error', error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
12
yarn.lock
12
yarn.lock
|
|
@ -7678,6 +7678,13 @@
|
|||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/graphql-fields@^1.3.4":
|
||||
version "1.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/graphql-fields/-/graphql-fields-1.3.4.tgz#868ffe444ba8027ea1eccb0909f9c331d1bd620a"
|
||||
integrity sha512-McLJaAaqY7lk9d9y7E61iQrj0AwcEjSb8uHlPh7KgYV+XX1MSLlSt/alhd5k2BPRE8gy/f4lnkLGb5ke3iG66Q==
|
||||
dependencies:
|
||||
graphql "^15.3.0"
|
||||
|
||||
"@types/hast@^2.0.0":
|
||||
version "2.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.4.tgz#8aa5ef92c117d20d974a82bdfb6a648b08c0bafc"
|
||||
|
|
@ -14649,6 +14656,11 @@ graphql-config@^4.1.0:
|
|||
minimatch "3.0.4"
|
||||
string-env-interpolation "1.0.1"
|
||||
|
||||
graphql-fields@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/graphql-fields/-/graphql-fields-2.0.3.tgz#5e68dff7afbb202be4f4f40623e983b22c96ab8f"
|
||||
integrity sha512-x3VE5lUcR4XCOxPIqaO4CE+bTK8u6gVouOdpQX9+EKHr+scqtK5Pp/l8nIGqIpN1TUlkKE6jDCCycm/WtLRAwA==
|
||||
|
||||
graphql-middleware@^6.0.10:
|
||||
version "6.1.4"
|
||||
resolved "https://registry.yarnpkg.com/graphql-middleware/-/graphql-middleware-6.1.4.tgz#1b4dd66195477046282acc8937cb5fca32b6bfd5"
|
||||
|
|
|
|||
Loading…
Reference in a new issue