From 4e16a6d6b57f59eceaa784aa2aade87752b7a016 Mon Sep 17 00:00:00 2001 From: Jeff Sieu Date: Sun, 6 Nov 2022 14:07:06 +0800 Subject: [PATCH] [questions][fix] fix expanded typeaheads --- .../card/question/BaseQuestionCard.tsx | 2 +- .../forms/CreateQuestionEncounterForm.tsx | 22 +++-- .../questions/typeahead/CompanyTypeahead.tsx | 21 +---- .../questions/typeahead/ExpandedTypeahead.tsx | 29 ++++--- .../questions/typeahead/LocationTypeahead.tsx | 25 ++++-- .../src/components/shared/CitiesTypeahead.tsx | 35 ++++---- .../components/shared/CompaniesTypeahead.tsx | 37 +++++---- .../components/shared/CountriesTypeahead.tsx | 83 +++++++++++-------- .../portal/src/components/shared/JobTitles.ts | 2 +- .../components/shared/JobTitlesTypeahead.tsx | 33 +++++--- 10 files changed, 166 insertions(+), 123 deletions(-) diff --git a/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx b/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx index ee3b0630..f938169c 100644 --- a/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx +++ b/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx @@ -185,7 +185,7 @@ export default function BaseQuestionCard({ )}
-
+
{showAggregateStatistics && ( <> diff --git a/apps/portal/src/components/questions/forms/CreateQuestionEncounterForm.tsx b/apps/portal/src/components/questions/forms/CreateQuestionEncounterForm.tsx index 6f65bf22..aa91b6ff 100644 --- a/apps/portal/src/components/questions/forms/CreateQuestionEncounterForm.tsx +++ b/apps/portal/src/components/questions/forms/CreateQuestionEncounterForm.tsx @@ -3,11 +3,15 @@ import { useState } from 'react'; import { CheckIcon } from '@heroicons/react/20/solid'; import { Button } from '@tih/ui'; +import { useCompanyOptions } from '~/components/shared/CompaniesTypeahead'; +import { useJobTitleOptions } from '~/components/shared/JobTitlesTypeahead'; import type { Month } from '~/components/shared/MonthYearPicker'; import MonthYearPicker from '~/components/shared/MonthYearPicker'; import CompanyTypeahead from '../typeahead/CompanyTypeahead'; -import LocationTypeahead from '../typeahead/LocationTypeahead'; +import LocationTypeahead, { + useLocationOptions, +} from '../typeahead/LocationTypeahead'; import RoleTypeahead from '../typeahead/RoleTypeahead'; import type { Location } from '~/types/questions'; @@ -43,6 +47,10 @@ export default function CreateQuestionEncounterForm({ startOfMonth(new Date()), ); + const { data: allCompanyOptions } = useCompanyOptions(''); + const { data: allLocationOptions } = useLocationOptions(''); + const allRoleOptions = useJobTitleOptions(''); + if (submitted) { return (
@@ -53,7 +61,7 @@ export default function CreateQuestionEncounterForm({ } return ( -
+

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

@@ -62,8 +70,8 @@ export default function CreateQuestionEncounterForm({ { setSelectedCompany(company); }} @@ -79,7 +87,8 @@ export default function CreateQuestionEncounterForm({ { setSelectedLocation(location); }} @@ -95,7 +104,8 @@ export default function CreateQuestionEncounterForm({ { setSelectedRole(role); }} diff --git a/apps/portal/src/components/questions/typeahead/CompanyTypeahead.tsx b/apps/portal/src/components/questions/typeahead/CompanyTypeahead.tsx index e3cbaa98..bf950a49 100644 --- a/apps/portal/src/components/questions/typeahead/CompanyTypeahead.tsx +++ b/apps/portal/src/components/questions/typeahead/CompanyTypeahead.tsx @@ -1,6 +1,6 @@ -import { useMemo, useState } from 'react'; +import { useState } from 'react'; -import { trpc } from '~/utils/trpc'; +import { useCompanyOptions } from '~/components/shared/CompaniesTypeahead'; import type { ExpandedTypeaheadProps } from './ExpandedTypeahead'; import ExpandedTypeahead from './ExpandedTypeahead'; @@ -13,22 +13,7 @@ export type CompanyTypeaheadProps = Omit< export default function CompanyTypeahead(props: CompanyTypeaheadProps) { const [query, setQuery] = useState(''); - const { data: companies, isLoading } = trpc.useQuery([ - 'companies.list', - { - name: query, - }, - ]); - - const companyOptions = useMemo(() => { - return ( - companies?.map(({ id, name }) => ({ - id, - label: name, - value: id, - })) ?? [] - ); - }, [companies]); + const { data: companyOptions, isLoading } = useCompanyOptions(query); return ( & RequireAllOrNone<{ - clearOnSelect?: boolean; - filterOption: (option: TypeaheadOption) => boolean; onSuggestionClick: (option: TypeaheadOption) => void; suggestedCount: number; + suggestedOptions: Array; }> & { + clearOnSelect?: boolean; + filterOption?: (option: TypeaheadOption) => boolean; onChange?: unknown; // Workaround: This prop is here just to absorb the onChange returned react-hook-form onSelect: (option: TypeaheadOption) => void; }; @@ -25,6 +26,7 @@ export type ExpandedTypeaheadProps = Omit< export default function ExpandedTypeahead({ suggestedCount = 0, onSuggestionClick, + suggestedOptions = [], filterOption = () => true, clearOnSelect = false, options, @@ -37,21 +39,22 @@ export default function ExpandedTypeahead({ return options.filter(filterOption); }, [options, filterOption]); const suggestions = useMemo( - () => filteredOptions.slice(0, suggestedCount), - [filteredOptions, suggestedCount], + () => suggestedOptions.slice(0, suggestedCount), + [suggestedOptions, suggestedCount], ); return ( -
+
{suggestions.map((suggestion) => ( -
))}
void; }; -export default function LocationTypeahead({ - onSelect, - onSuggestionClick, - ...restProps -}: LocationTypeaheadProps) { - const [query, setQuery] = useState(''); - - const { data: locations, isLoading } = trpc.useQuery([ +export function useLocationOptions(query: string) { + const { data: locations, ...restQuery } = trpc.useQuery([ 'locations.cities.list', { name: query, @@ -43,6 +37,21 @@ export default function LocationTypeahead({ ); }, [locations]); + return { + data: locationOptions, + ...restQuery, + }; +} + +export default function LocationTypeahead({ + onSelect, + onSuggestionClick, + ...restProps +}: LocationTypeaheadProps) { + const [query, setQuery] = useState(''); + + const { data: locationOptions, isLoading } = useLocationOptions(query); + return ( ; +export function useCityOptions(query: string) { + const { data, ...restQuery } = trpc.useQuery([ + 'locations.cities.list', + { + name: query, + }, + ]); + + return { + data: + data?.map(({ id, name, state }) => ({ + id, + label: `${name}, ${state?.name}, ${state?.country?.name}`, + value: id, + })) ?? [], + ...restQuery, + }; +} + export default function CitiesTypeahead({ label = 'City', onSelect, @@ -29,14 +48,8 @@ export default function CitiesTypeahead({ ...props }: Props) { const [query, setQuery] = useState(''); - const cities = trpc.useQuery([ - 'locations.cities.list', - { - name: query, - }, - ]); - const { data, isLoading } = cities; + const { data: cityOptions, isLoading } = useCityOptions(query); return ( ({ - id, - label: `${name}, ${state?.name}, ${state?.country?.name}`, - value: id, - })) ?? [] - } + options={cityOptions} value={value} onQueryChange={setQuery} onSelect={onSelect} diff --git a/apps/portal/src/components/shared/CompaniesTypeahead.tsx b/apps/portal/src/components/shared/CompaniesTypeahead.tsx index d7a43b14..f51ba8d7 100644 --- a/apps/portal/src/components/shared/CompaniesTypeahead.tsx +++ b/apps/portal/src/components/shared/CompaniesTypeahead.tsx @@ -21,12 +21,7 @@ type Props = BaseProps & value?: TypeaheadOption | null; }>; -export default function CompaniesTypeahead({ - onSelect, - value, - ...props -}: Props) { - const [query, setQuery] = useState(''); +export function useCompanyOptions(query: string) { const companies = trpc.useQuery([ 'companies.list', { @@ -34,7 +29,27 @@ export default function CompaniesTypeahead({ }, ]); - const { data, isLoading } = companies; + const { data, ...restQuery } = companies; + + return { + data: + data?.map(({ id, name }) => ({ + id, + label: name, + value: id, + })) ?? [], + ...restQuery, + }; +} + +export default function CompaniesTypeahead({ + onSelect, + value, + ...props +}: Props) { + const [query, setQuery] = useState(''); + + const { data: companyOptions, isLoading } = useCompanyOptions(query); return ( ({ - id, - label: name, - value: id, - })) ?? [] - } + options={companyOptions} value={value} onQueryChange={setQuery} onSelect={onSelect} diff --git a/apps/portal/src/components/shared/CountriesTypeahead.tsx b/apps/portal/src/components/shared/CountriesTypeahead.tsx index 7784d7f4..2c6b73d0 100644 --- a/apps/portal/src/components/shared/CountriesTypeahead.tsx +++ b/apps/portal/src/components/shared/CountriesTypeahead.tsx @@ -1,5 +1,6 @@ import type { ComponentProps } from 'react'; import { useState } from 'react'; +import type { Country } from '@prisma/client'; import type { TypeaheadOption } from '@tih/ui'; import { Typeahead } from '@tih/ui'; @@ -33,6 +34,48 @@ function stringPositionComparator(a: string, b: string, query: string): number { ); } +export function useCompareCountry(query: string) { + return (a: Country, b: Country) => { + const normalizedQueryString = query.trim().toLocaleLowerCase(); + if ( + a.code.toLocaleLowerCase() === normalizedQueryString || + b.code.toLocaleLowerCase() === normalizedQueryString + ) { + return stringPositionComparator(a.code, b.code, normalizedQueryString); + } + + return stringPositionComparator(a.name, b.name, normalizedQueryString); + }; +} + +export function useCountryOptions(query: string) { + const countries = trpc.useQuery([ + 'locations.countries.list', + { + name: query, + }, + ]); + + const { data, ...restQuery } = countries; + + const compareCountry = useCompareCountry(query); + + const countryOptions = (data ?? []) + // Client-side sorting by position of query string appearing + // in the country name since we can't do that in Prisma. + .sort(compareCountry) + .map(({ id, name }) => ({ + id, + label: name, + value: id, + })); + + return { + ...restQuery, + data: countryOptions, + }; +} + export default function CountriesTypeahead({ excludedValues, label = 'Country', @@ -41,14 +84,7 @@ export default function CountriesTypeahead({ ...props }: Props) { const [query, setQuery] = useState(''); - const countries = trpc.useQuery([ - 'locations.countries.list', - { - name: query, - }, - ]); - - const { data, isLoading } = countries; + const { data: countryOptions, isLoading } = useCountryOptions(query); return ( { - const normalizedQueryString = query.trim().toLocaleLowerCase(); - if ( - a.code.toLocaleLowerCase() === normalizedQueryString || - b.code.toLocaleLowerCase() === normalizedQueryString - ) { - return stringPositionComparator( - a.code, - b.code, - normalizedQueryString, - ); - } - - return stringPositionComparator( - a.name, - b.name, - normalizedQueryString, - ); - }) - .map(({ id, name }) => ({ - id, - label: name, - value: id, - })) - .filter((option) => !excludedValues?.has(option.value))} + options={countryOptions.filter( + (option) => !excludedValues?.has(option.value), + )} value={value} onQueryChange={setQuery} onSelect={onSelect} diff --git a/apps/portal/src/components/shared/JobTitles.ts b/apps/portal/src/components/shared/JobTitles.ts index d992a79d..f11cbce3 100644 --- a/apps/portal/src/components/shared/JobTitles.ts +++ b/apps/portal/src/components/shared/JobTitles.ts @@ -78,5 +78,5 @@ export const JobTitleLabels: JobTitleData = { export type JobTitleType = keyof typeof JobTitleLabels; export function getLabelForJobTitleType(jobTitle: JobTitleType): string { - return JobTitleLabels[jobTitle].label; + return JobTitleLabels[jobTitle]?.label ?? ''; } diff --git a/apps/portal/src/components/shared/JobTitlesTypeahead.tsx b/apps/portal/src/components/shared/JobTitlesTypeahead.tsx index 9f560fce..165d0077 100644 --- a/apps/portal/src/components/shared/JobTitlesTypeahead.tsx +++ b/apps/portal/src/components/shared/JobTitlesTypeahead.tsx @@ -24,6 +24,23 @@ type Props = BaseProps & value?: TypeaheadOption | null; }>; +const sortedJobTitleOptions = Object.entries(JobTitleLabels) + .map(([slug, { label, ranking }]) => ({ + id: slug, + label, + ranking, + value: slug, + })) + .sort((a, b) => b.ranking - a.ranking); + +export function useJobTitleOptions(query: string) { + const jobTitles = sortedJobTitleOptions.filter(({ label }) => + label.toLocaleLowerCase().includes(query.trim().toLocaleLowerCase()), + ); + + return jobTitles; +} + export default function JobTitlesTypeahead({ excludedValues, label: labelProp = 'Job Title', @@ -33,18 +50,10 @@ export default function JobTitlesTypeahead({ ...props }: Props) { const [query, setQuery] = useState(''); - const options = Object.entries(JobTitleLabels) - .map(([slug, { label, ranking }]) => ({ - id: slug, - label, - ranking, - value: slug, - })) - .filter(({ label }) => - label.toLocaleLowerCase().includes(query.trim().toLocaleLowerCase()), - ) - .filter((option) => !excludedValues?.has(option.value)) - .sort((a, b) => b.ranking - a.ranking); + const jobTitleOptions = useJobTitleOptions(query); + const options = jobTitleOptions.filter( + (option) => !excludedValues?.has(option.value), + ); return (