Skip to content

Commit

Permalink
feat(components): gs-location-filter: show counts in autocomplete opt…
Browse files Browse the repository at this point in the history
…ions

resolves #665
  • Loading branch information
fengelniederhammer committed Jan 23, 2025
1 parent ef9be92 commit 7b59411
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 45 deletions.
4 changes: 2 additions & 2 deletions components/src/preact/components/downshift-combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,15 +127,15 @@ export function DownshiftCombobox<Item>({
{items.length > 0 ? (
items.map((item, index) => (
<li
className={`${highlightedIndex === index ? 'bg-blue-300' : ''} ${selectedItem !== null && itemToString(selectedItem) === itemToString(item) ? 'font-bold' : ''} py-2 px-3 shadow-sm flex flex-col`}
className={`${highlightedIndex === index ? 'bg-blue-300' : ''} ${selectedItem !== null && itemToString(selectedItem) === itemToString(item) ? 'font-bold' : ''} py-2 px-3 shadow-sm`}
key={itemToString(item)}
{...getItemProps({ item, index })}
>
{formatItemInList(item)}
</li>
))
) : (
<li className='py-2 px-3 shadow-sm flex flex-col'>No elements to select.</li>
<li className='py-2 px-3 shadow-sm'>No elements to select.</li>
)}
</ul>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ describe('fetchAutocompletionList', () => {
{ fields, country: 'Germany' },
{
data: [
{ count: 0, region: 'region1', country: 'country1_1', division: 'division1_1_1' },
{ count: 0, region: 'region1', country: 'country1_1', division: 'division1_1_2' },
{ count: 0, region: 'region1', country: 'country1_1', division: null },
{ count: 0, region: 'region1', country: 'country1_2', division: 'division1_2_1' },
{ count: 0, region: 'region2', country: 'country2_1', division: null },
{ count: 1, region: 'region1', country: 'country1_1', division: 'division1_1_1' },
{ count: 2, region: 'region1', country: 'country1_1', division: 'division1_1_2' },
{ count: 3, region: 'region1', country: 'country1_1', division: null },
{ count: 4, region: 'region1', country: 'country1_2', division: 'division1_2_1' },
{ count: 5, region: 'region2', country: 'country2_1', division: null },
],
},
);
Expand All @@ -27,14 +27,14 @@ describe('fetchAutocompletionList', () => {
});

expect(result).to.deep.equal([
{ region: 'region1', country: undefined, division: undefined },
{ region: 'region1', country: 'country1_1', division: undefined },
{ region: 'region1', country: 'country1_1', division: 'division1_1_1' },
{ region: 'region1', country: 'country1_1', division: 'division1_1_2' },
{ region: 'region1', country: 'country1_2', division: undefined },
{ region: 'region1', country: 'country1_2', division: 'division1_2_1' },
{ region: 'region2', country: undefined, division: undefined },
{ region: 'region2', country: 'country2_1', division: undefined },
{ value: { region: 'region1', country: undefined, division: undefined }, count: 10 },
{ value: { region: 'region1', country: 'country1_1', division: undefined }, count: 6 },
{ value: { region: 'region1', country: 'country1_1', division: 'division1_1_1' }, count: 1 },
{ value: { region: 'region1', country: 'country1_1', division: 'division1_1_2' }, count: 2 },
{ value: { region: 'region1', country: 'country1_2', division: undefined }, count: 4 },
{ value: { region: 'region1', country: 'country1_2', division: 'division1_2_1' }, count: 4 },
{ value: { region: 'region2', country: undefined, division: undefined }, count: 5 },
{ value: { region: 'region2', country: 'country2_1', division: undefined }, count: 5 },
]);
});
});
65 changes: 49 additions & 16 deletions components/src/preact/locationFilter/fetchAutocompletionList.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { FetchAggregatedOperator } from '../../operator/FetchAggregatedOperator';
import type { LapisFilter } from '../../types';
import type { LapisFilter, LapisLocationFilter } from '../../types';

export type LocationEntry = {
value: LapisLocationFilter;
count: number;
};

export async function fetchAutocompletionList({
fields,
Expand All @@ -11,8 +16,8 @@ export async function fetchAutocompletionList({
lapis: string;
lapisFilter?: LapisFilter;
signal?: AbortSignal;
}): Promise<Record<string, string | undefined>[]> {
const toAncestorInHierarchyOverwriteValues = Array(fields.length - 1)
}): Promise<LocationEntry[]> {
const toAncestorInHierarchyOverwriteValues = Array(fields.length)
.fill(0)
.map((_, i) => i + 1)
.map((i) => fields.slice(i).reduce((acc, field) => ({ ...acc, [field]: null }), {}));
Expand All @@ -25,26 +30,54 @@ export async function fetchAutocompletionList({
const data = (await fetchAggregatedOperator.evaluate(lapis, signal)).content;

const locationValues = data
.map((entry) => fields.reduce((acc, field) => ({ ...acc, [field]: entry[field] }), {}))
.reduce<Set<string>>((setOfAllHierarchies, entry) => {
setOfAllHierarchies.add(JSON.stringify(entry));
toAncestorInHierarchyOverwriteValues.forEach((overwriteValues) => {
setOfAllHierarchies.add(JSON.stringify({ ...entry, ...overwriteValues }));
});
return setOfAllHierarchies;
}, new Set());
.map((entry) => ({
value: fields.reduce((acc, field) => ({ ...acc, [field]: entry[field] }), {}),
count: entry.count,
}))
.reduce((mapOfAllHierarchiesAndCounts, entry) => {
return addValueAndAllAncestors(entry, toAncestorInHierarchyOverwriteValues, mapOfAllHierarchiesAndCounts);
}, new Map<string, number>());

return [...locationValues]
.map((json) => JSON.parse(json))
.map<EntryWithNullValues>(([json, count]) => ({
value: JSON.parse(json),
count,
}))
.sort(compareLocationEntries(fields))
.map((entry) => fields.reduce((acc, field) => ({ ...acc, [field]: entry[field] ?? undefined }), {}));
.map(({ value, count }) => ({
value: fields.reduce((acc, field) => ({ ...acc, [field]: value[field] ?? undefined }), {}),
count,
}));
}

function addValueAndAllAncestors(
{ value, count }: LocationEntry,
toAncestorInHierarchyOverwriteValues: Record<string, null>[],
mapOfAllHierarchiesAndCounts: Map<string, number>,
) {
const keysOfAllHierarchyLevels = new Set(
toAncestorInHierarchyOverwriteValues
.map((overwriteValues) => ({ ...value, ...overwriteValues }))
.map((value) => JSON.stringify(value)),
);

for (const key of keysOfAllHierarchyLevels) {
mapOfAllHierarchiesAndCounts.set(key, (mapOfAllHierarchiesAndCounts.get(key) ?? 0) + count);
}

return mapOfAllHierarchiesAndCounts;
}

type EntryWithNullValues = {
value: Record<string, string | null>;
count: number;
};

function compareLocationEntries(fields: string[]) {
return (a: Record<string, string | null>, b: Record<string, string | null>) => {
return (a: EntryWithNullValues, b: EntryWithNullValues) => {
for (const field of fields) {
const valueA = a[field];
const valueB = b[field];
const valueA = a.value[field];
const valueB = b.value[field];
if (valueA === valueB) {
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export const Primary: StoryObj<LocationFilterProps> = {
const input = await inputField(canvas);
await userEvent.clear(input);
await userEvent.type(input, 'Germany');
await userEvent.click(canvas.getByRole('option', { name: 'Germany Europe / Germany' }));
await userEvent.click(canvas.getByRole('option', { name: /Germany\(\d+\) Europe \/ Germany/ }));

await waitFor(() => {
return expect(locationChangedListenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
Expand Down
30 changes: 18 additions & 12 deletions components/src/preact/locationFilter/location-filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { type FunctionComponent } from 'preact';
import { useContext, useMemo } from 'preact/hooks';
import z from 'zod';

import { fetchAutocompletionList } from './fetchAutocompletionList';
import { fetchAutocompletionList, type LocationEntry } from './fetchAutocompletionList';
import { LapisUrlContext } from '../LapisUrlContext';
import { LocationChangedEvent } from './LocationChangedEvent';
import { lapisFilterSchema, type LapisLocationFilter, lapisLocationFilterSchema } from '../../types';
Expand Down Expand Up @@ -61,6 +61,7 @@ type SelectItem = {
lapisFilter: LapisLocationFilter;
label: string | null | undefined;
description: string;
count: number;
};

const LocationSelector = ({
Expand All @@ -69,7 +70,7 @@ const LocationSelector = ({
placeholderText,
locationData,
}: LocationSelectorProps & {
locationData: LapisLocationFilter[];
locationData: LocationEntry[];
}) => {
const allItems = useMemo(() => {
return locationData
Expand All @@ -78,8 +79,10 @@ const LocationSelector = ({
}, [fields, locationData]);

const selectedItem = useMemo(() => {
return value !== undefined ? toSelectItem(value, fields) : undefined;
}, [fields, value]);
return value !== undefined
? allItems.find((item) => item.description == concatenateLocation(value, fields))
: undefined;
}, [fields, value, allItems]);

return (
<DownshiftCombobox
Expand All @@ -91,14 +94,15 @@ const LocationSelector = ({
}
itemToString={(item: SelectItem | undefined | null) => item?.label ?? ''}
placeholderText={placeholderText}
formatItemInList={(item: SelectItem) => {
return (
<>
formatItemInList={(item: SelectItem) => (
<>
<p>
<span>{item.label}</span>
<span className='text-sm text-gray-500'>{item.description}</span>
</>
);
}}
<span className='ml-2 text-gray-500'>({item.count})</span>
</p>
<span className='text-sm text-gray-500'>{item.description}</span>
</>
)}
/>
);
};
Expand All @@ -113,7 +117,8 @@ function filterByInputValue(item: SelectItem, inputValue: string | undefined | n
);
}

function toSelectItem(locationFilter: LapisLocationFilter, fields: string[]): SelectItem | undefined {
function toSelectItem(locationEntry: LocationEntry, fields: string[]): SelectItem | undefined {
const locationFilter = locationEntry.value;
const concatenatedLocation = concatenateLocation(locationFilter, fields);

const lastNonUndefinedField = [...fields]
Expand All @@ -128,6 +133,7 @@ function toSelectItem(locationFilter: LapisLocationFilter, fields: string[]): Se
lapisFilter: locationFilter,
label: locationFilter[lastNonUndefinedField],
description: concatenatedLocation,
count: locationEntry.count,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ export const FiresEvent: StoryObj<LocationFilterProps> = {

await step('Select Asia', async () => {
await userEvent.type(inputField(), 'Asia');
await userEvent.click(canvas.getByRole('option', { name: 'Asia Asia' }));
await userEvent.click(canvas.getByRole('option', { name: /^Asia.*Asia$/ }));

await waitFor(() => {
return expect(listenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
Expand Down

0 comments on commit 7b59411

Please sign in to comment.