[questions][fix] fix expanded typeaheads (#516)

pull/521/head
Jeff Sieu 2 years ago committed by GitHub
parent c10aa15347
commit 6792e20f0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -186,7 +186,7 @@ export default function BaseQuestionCard({
)}
<div className="flex flex-1 flex-col items-start gap-2">
<div className="flex items-baseline justify-between self-stretch">
<div className="flex items-center gap-2 text-slate-500">
<div className="flex flex-wrap items-center gap-2 text-slate-500">
{showAggregateStatistics && (
<>
<QuestionTypeBadge type={type} />

@ -6,8 +6,13 @@ import { Button } from '@tih/ui';
import type { Month } from '~/components/shared/MonthYearPicker';
import MonthYearPicker from '~/components/shared/MonthYearPicker';
import useCompanyOptions from '~/utils/shared/useCompanyOptions';
import useJobTitleOptions from '~/utils/shared/useJobTitleOptions';
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 +48,10 @@ export default function CreateQuestionEncounterForm({
startOfMonth(new Date()),
);
const { data: allCompanyOptions } = useCompanyOptions('');
const { data: allLocationOptions } = useLocationOptions('');
const allRoleOptions = useJobTitleOptions('');
if (submitted) {
return (
<div className="font-md flex items-center gap-1 rounded-full border bg-slate-50 py-1 pl-2 pr-3 text-sm text-slate-500">
@ -53,7 +62,7 @@ export default function CreateQuestionEncounterForm({
}
return (
<div className="flex items-center gap-2">
<div className="flex flex-wrap items-center gap-2">
<p className="text-md text-slate-600">
I saw this question {step <= 1 ? 'at' : step === 2 ? 'for' : 'on'}
</p>
@ -62,8 +71,8 @@ export default function CreateQuestionEncounterForm({
<CompanyTypeahead
isLabelHidden={true}
placeholder="Company"
// TODO: Fix suggestions and set count back to 3
suggestedCount={0}
suggestedCount={3}
suggestedOptions={allCompanyOptions}
onSelect={({ value: company }) => {
setSelectedCompany(company);
}}
@ -79,7 +88,8 @@ export default function CreateQuestionEncounterForm({
<LocationTypeahead
isLabelHidden={true}
placeholder="Location"
suggestedCount={0}
suggestedCount={3}
suggestedOptions={allLocationOptions}
onSelect={(location) => {
setSelectedLocation(location);
}}
@ -95,7 +105,8 @@ export default function CreateQuestionEncounterForm({
<RoleTypeahead
isLabelHidden={true}
placeholder="Role"
suggestedCount={0}
suggestedCount={3}
suggestedOptions={allRoleOptions}
onSelect={({ value: role }) => {
setSelectedRole(role);
}}

@ -1,6 +1,6 @@
import { useMemo, useState } from 'react';
import { useState } from 'react';
import { trpc } from '~/utils/trpc';
import useCompanyOptions from '~/utils/shared/useCompanyOptions';
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 (
<ExpandedTypeahead

@ -13,11 +13,12 @@ export type ExpandedTypeaheadProps = Omit<
'nullable' | 'onSelect'
> &
RequireAllOrNone<{
clearOnSelect?: boolean;
filterOption: (option: TypeaheadOption) => boolean;
onSuggestionClick: (option: TypeaheadOption) => void;
suggestedCount: number;
suggestedOptions: Array<TypeaheadOption>;
}> & {
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 (
<div className="flex flex-wrap gap-x-2">
<div className="flex flex-wrap gap-2">
{suggestions.map((suggestion) => (
<Button
key={suggestion.id}
label={suggestion.label}
variant="tertiary"
onClick={() => {
onSuggestionClick?.(suggestion);
}}
/>
<div key={suggestion.id} className="hidden lg:block">
<Button
label={suggestion.label}
variant="tertiary"
onClick={() => {
onSuggestionClick?.(suggestion);
}}
/>
</div>
))}
<div className="flex-1">
<Typeahead

@ -16,14 +16,8 @@ export type LocationTypeaheadProps = Omit<
onSuggestionClick?: (option: Location) => 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 (
<ExpandedTypeahead
isLoading={isLoading}

@ -1,31 +1,25 @@
import { useState } from 'react';
import { JobTitleLabels } from '~/components/shared/JobTitles';
import useJobTitleOptions from '~/utils/shared/useJobTitleOptions';
import type { ExpandedTypeaheadProps } from './ExpandedTypeahead';
import ExpandedTypeahead from './ExpandedTypeahead';
import type { FilterChoices } from '../filter/FilterSection';
export type RoleTypeaheadProps = Omit<
ExpandedTypeaheadProps,
'label' | 'onQueryChange' | 'options'
>;
const ROLES: FilterChoices = Object.entries(JobTitleLabels).map(
([slug, { label }]) => ({
id: slug,
label,
value: slug,
}),
);
export default function RoleTypeahead(props: RoleTypeaheadProps) {
const [query, setQuery] = useState('');
const roleOptions = useJobTitleOptions(query);
return (
<ExpandedTypeahead
{...(props as ExpandedTypeaheadProps)}
label="Role"
options={ROLES.filter((option) =>
options={roleOptions.filter((option) =>
option.label
.toLocaleLowerCase()
.includes(query.trim().toLocaleLowerCase()),

@ -22,6 +22,25 @@ type Props = BaseProps &
value?: TypeaheadOption | null;
}>;
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 (
<Typeahead
@ -45,13 +58,7 @@ export default function CitiesTypeahead({
minQueryLength={3}
noResultsMessage="No cities found"
nullable={true}
options={
data?.map(({ id, name, state }) => ({
id,
label: `${name}, ${state?.name}, ${state?.country?.name}`,
value: id,
})) ?? []
}
options={cityOptions}
value={value}
onQueryChange={setQuery}
onSelect={onSelect}

@ -3,7 +3,7 @@ import { useState } from 'react';
import type { TypeaheadOption } from '@tih/ui';
import { Typeahead } from '@tih/ui';
import { trpc } from '~/utils/trpc';
import useCompanyOptions from '~/utils/shared/useCompanyOptions';
type BaseProps = Pick<
ComponentProps<typeof Typeahead>,
@ -27,14 +27,8 @@ export default function CompaniesTypeahead({
...props
}: Props) {
const [query, setQuery] = useState('');
const companies = trpc.useQuery([
'companies.list',
{
name: query,
},
]);
const { data, isLoading } = companies;
const { data: companyOptions, isLoading } = useCompanyOptions(query);
return (
<Typeahead
@ -42,13 +36,7 @@ export default function CompaniesTypeahead({
label="Company"
noResultsMessage="No companies found"
nullable={true}
options={
data?.map(({ id, name }) => ({
id,
label: name,
value: id,
})) ?? []
}
options={companyOptions}
value={value}
onQueryChange={setQuery}
onSelect={onSelect}

@ -3,7 +3,7 @@ import { useState } from 'react';
import type { TypeaheadOption } from '@tih/ui';
import { Typeahead } from '@tih/ui';
import { trpc } from '~/utils/trpc';
import useCountryOptions from '~/utils/shared/useCountryOptions';
type BaseProps = Pick<
ComponentProps<typeof Typeahead>,
@ -23,16 +23,6 @@ type Props = BaseProps &
value?: TypeaheadOption | null;
}>;
function stringPositionComparator(a: string, b: string, query: string): number {
const normalizedQueryString = query.trim().toLocaleLowerCase();
const positionA = a.toLocaleLowerCase().indexOf(normalizedQueryString);
const positionB = b.toLocaleLowerCase().indexOf(normalizedQueryString);
return (
(positionA === -1 ? 9999 : positionA) -
(positionB === -1 ? 9999 : positionB)
);
}
export default function CountriesTypeahead({
excludedValues,
label = 'Country',
@ -41,14 +31,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 (
<Typeahead
@ -56,34 +39,9 @@ export default function CountriesTypeahead({
label={label}
noResultsMessage="No countries found"
nullable={true}
options={(data ?? [])
// Client-side sorting by position of query string appearing
// in the country name since we can't do that in Prisma.
.sort((a, b) => {
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}

@ -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 ?? '';
}

@ -3,7 +3,7 @@ import { useState } from 'react';
import type { TypeaheadOption } from '@tih/ui';
import { Typeahead } from '@tih/ui';
import { JobTitleLabels } from './JobTitles';
import useJobTitleOptions from '~/utils/shared/useJobTitleOptions';
type BaseProps = Pick<
ComponentProps<typeof Typeahead>,
@ -33,18 +33,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 (
<Typeahead

@ -0,0 +1,22 @@
import { trpc } from '../trpc';
export default function useCompanyOptions(query: string) {
const companies = trpc.useQuery([
'companies.list',
{
name: query,
},
]);
const { data, ...restQuery } = companies;
return {
data:
data?.map(({ id, name }) => ({
id,
label: name,
value: id,
})) ?? [],
...restQuery,
};
}

@ -0,0 +1,55 @@
import type { Country } from '@prisma/client';
import { trpc } from '../trpc';
function stringPositionComparator(a: string, b: string, query: string): number {
const normalizedQueryString = query.trim().toLocaleLowerCase();
const positionA = a.toLocaleLowerCase().indexOf(normalizedQueryString);
const positionB = b.toLocaleLowerCase().indexOf(normalizedQueryString);
return (
(positionA === -1 ? 9999 : positionA) -
(positionB === -1 ? 9999 : positionB)
);
}
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 default 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,
};
}

@ -0,0 +1,18 @@
import { JobTitleLabels } from '~/components/shared/JobTitles';
const sortedJobTitleOptions = Object.entries(JobTitleLabels)
.map(([slug, { label, ranking }]) => ({
id: slug,
label,
ranking,
value: slug,
}))
.sort((a, b) => b.ranking - a.ranking);
export default function useJobTitleOptions(query: string) {
const jobTitles = sortedJobTitleOptions.filter(({ label }) =>
label.toLocaleLowerCase().includes(query.trim().toLocaleLowerCase()),
);
return jobTitles;
}
Loading…
Cancel
Save