From 397ea3f4aaa8d126b9b4fa5b57a6b99eb4799441 Mon Sep 17 00:00:00 2001 From: hpkoh <53825802+hpkoh@users.noreply.github.com> Date: Mon, 31 Oct 2022 19:24:07 +0800 Subject: [PATCH] [questions][feat] add content search (#478) Co-authored-by: Jeff Sieu --- .../questions/QuestionSearchBar.tsx | 8 + .../card/question/BaseQuestionCard.tsx | 2 +- .../forms/CreateQuestionEncounterForm.tsx | 4 +- apps/portal/src/pages/questions/browse.tsx | 13 +- .../questions/questions-question-router.ts | 182 +++++++++++++++++- 5 files changed, 203 insertions(+), 6 deletions(-) diff --git a/apps/portal/src/components/questions/QuestionSearchBar.tsx b/apps/portal/src/components/questions/QuestionSearchBar.tsx index 917c342d..bafcb3cb 100644 --- a/apps/portal/src/components/questions/QuestionSearchBar.tsx +++ b/apps/portal/src/components/questions/QuestionSearchBar.tsx @@ -9,10 +9,14 @@ import SortOptionsSelect from './SortOptionsSelect'; export type QuestionSearchBarProps = SortOptionsSelectProps & { onFilterOptionsToggle: () => void; + onQueryChange: (query: string) => void; + query: string; }; export default function QuestionSearchBar({ onFilterOptionsToggle, + onQueryChange, + query, ...sortOptionsSelectProps }: QuestionSearchBarProps) { return ( @@ -24,6 +28,10 @@ export default function QuestionSearchBar({ placeholder="Search by content" startAddOn={MagnifyingGlassIcon} startAddOnType="icon" + value={query} + onChange={(value) => { + onQueryChange(value); + }} />
diff --git a/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx b/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx index 65a13054..7b50c1a9 100644 --- a/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx +++ b/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx @@ -218,7 +218,7 @@ export default function BaseQuestionCard({

{content} diff --git a/apps/portal/src/components/questions/forms/CreateQuestionEncounterForm.tsx b/apps/portal/src/components/questions/forms/CreateQuestionEncounterForm.tsx index b346b7b4..5e5f67c4 100644 --- a/apps/portal/src/components/questions/forms/CreateQuestionEncounterForm.tsx +++ b/apps/portal/src/components/questions/forms/CreateQuestionEncounterForm.tsx @@ -42,7 +42,9 @@ export default function CreateQuestionEncounterForm({ return (

-

I saw this question at

+

+ I saw this question {step <= 1 ? 'at' : step === 2 ? 'for' : 'on'} +

{step === 0 && (
cityId) .filter((id) => id !== undefined) as Array, companyIds: selectedCompanySlugs.map((slug) => slug.split('_')[0]), + content: query, countryIds: [], endDate: today, limit: 10, @@ -475,8 +478,8 @@ export default function QuestionsBrowsePage() {
-
-
+
+
{ const { cityId, countryId, stateId } = data.location; @@ -495,11 +498,15 @@ export default function QuestionsBrowsePage() {
{ setFilterDrawerOpen(!filterDrawerOpen); }} + onQueryChange={(newQuery) => { + setQuery(newQuery); + }} onSortOrderChange={setSortOrder} onSortTypeChange={setSortType} /> diff --git a/apps/portal/src/server/router/questions/questions-question-router.ts b/apps/portal/src/server/router/questions/questions-question-router.ts index a6821fb2..4d506b94 100644 --- a/apps/portal/src/server/router/questions/questions-question-router.ts +++ b/apps/portal/src/server/router/questions/questions-question-router.ts @@ -236,7 +236,8 @@ export const questionsQuestionRouter = createRouter() SELECT id FROM "QuestionsQuestion" WHERE to_tsvector("content") @@ to_tsquery('english', ${query}) - ORDER BY ts_rank_cd(to_tsvector("content"), to_tsquery('english', ${query}), 4) DESC; + ORDER BY ts_rank_cd(to_tsvector("content"), to_tsquery('english', ${query}), 4) DESC + LIMIT 3; `; const relatedQuestionsIdArray = relatedQuestionsId.map( @@ -281,4 +282,183 @@ export const questionsQuestionRouter = createRouter() return processedQuestionsData; }, + }) + .query('getQuestionsByFilterAndContent', { + input: z.object({ + cityIds: z.string().array(), + companyIds: z.string().array(), + content: z.string(), + countryIds: z.string().array(), + cursor: z.string().nullish(), + endDate: z.date().default(new Date()), + limit: z.number().min(1).default(50), + questionTypes: z.nativeEnum(QuestionsQuestionType).array(), + roles: z.string().array(), + sortOrder: z.nativeEnum(SortOrder), + sortType: z.nativeEnum(SortType), + startDate: z.date().optional(), + stateIds: z.string().array(), + }), + async resolve({ ctx, input }) { + const escapeChars = /[()|&:*!]/g; + + const query = input.content + .replace(escapeChars, ' ') + .trim() + .split(/\s+/) + .join(' | '); + + let relatedQuestionsId: Array<{ id: string }> = []; + + if (input.content !== "") { + relatedQuestionsId = await ctx.prisma + .$queryRaw` + SELECT id FROM "QuestionsQuestion" + WHERE + to_tsvector("content") @@ to_tsquery('english', ${query}) + ORDER BY ts_rank_cd(to_tsvector("content"), to_tsquery('english', ${query}), 4) DESC + LIMIT 3; + `; + } + + + + const relatedQuestionsIdArray = relatedQuestionsId.map( + (current) => current.id, + ); + + const { cursor } = input; + + const sortCondition = + input.sortType === SortType.TOP + ? [ + { + upvotes: input.sortOrder, + }, + { + id: input.sortOrder, + }, + ] + : [ + { + lastSeenAt: input.sortOrder, + }, + { + id: input.sortOrder, + }, + ]; + + const questionsData = await ctx.prisma.questionsQuestion.findMany({ + cursor: cursor ? { id: cursor } : undefined, + include: { + _count: { + select: { + answers: true, + comments: true, + }, + }, + encounters: { + select: { + city: true, + company: true, + country: true, + role: true, + seenAt: true, + state: true, + }, + }, + user: { + select: { + name: true, + }, + }, + votes: true, + }, + orderBy: sortCondition, + take: input.limit + 1, + where: { + id: input.content !== "" ? { + in: relatedQuestionsIdArray, + } : undefined, + ...(input.questionTypes.length > 0 + ? { + questionType: { + in: input.questionTypes, + }, + } + : {}), + encounters: { + some: { + seenAt: { + gte: input.startDate, + lte: input.endDate, + }, + ...(input.companyIds.length > 0 + ? { + company: { + id: { + in: input.companyIds, + }, + }, + } + : {}), + ...(input.cityIds.length > 0 + ? { + city: { + id: { + in: input.cityIds, + }, + }, + } + : {}), + ...(input.countryIds.length > 0 + ? { + country: { + id: { + in: input.countryIds, + }, + }, + } + : {}), + ...(input.stateIds.length > 0 + ? { + state: { + id: { + in: input.stateIds, + }, + }, + } + : {}), + ...(input.roles.length > 0 + ? { + role: { + in: input.roles, + }, + } + : {}), + }, + }, + }, + }); + + const processedQuestionsData = questionsData.map( + createQuestionWithAggregateData, + ); + + let nextCursor: typeof cursor | undefined = undefined; + + if (questionsData.length > input.limit) { + const nextItem = questionsData.pop()!; + processedQuestionsData.pop(); + + const nextIdCursor: string | undefined = nextItem.id; + + nextCursor = nextIdCursor; + } + + return { + data: processedQuestionsData, + nextCursor, + }; + }, });