From d84640d48068b51bb40eb240ad0ec6ba9643b7dc Mon Sep 17 00:00:00 2001 From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Date: Sat, 11 Jan 2025 11:19:31 -0500 Subject: [PATCH] Generalize setting up groups in dropdown (#306) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Screenshot 2025-01-11 at 11 00 25 AM --- packages/ct/src/js/dropdownGroups.ts | 13 ++++++ packages/ct/src/js/main.ts | 2 + packages/primary/src/js/dropdownGroups.ts | 29 ++++++++++++ packages/primary/src/js/main.ts | 2 + packages/primary/tests/communityMaps.test.ts | 21 --------- packages/primary/tests/dropdownGroups.test.ts | 35 ++++++++++++++ packages/shared/src/js/bootstrap.ts | 4 +- packages/shared/src/js/city-ui/dropdown.ts | 46 +++---------------- .../shared/src/js/city-ui/dropdownUtils.ts | 27 ++++++++++- packages/shared/tests/dropdownUtils.test.ts | 33 ++++++++++++- 10 files changed, 149 insertions(+), 63 deletions(-) create mode 100644 packages/ct/src/js/dropdownGroups.ts create mode 100644 packages/primary/src/js/dropdownGroups.ts delete mode 100644 packages/primary/tests/communityMaps.test.ts create mode 100644 packages/primary/tests/dropdownGroups.test.ts diff --git a/packages/ct/src/js/dropdownGroups.ts b/packages/ct/src/js/dropdownGroups.ts new file mode 100644 index 0000000..c86263c --- /dev/null +++ b/packages/ct/src/js/dropdownGroups.ts @@ -0,0 +1,13 @@ +import { DropdownGroup } from "@prn-parking-lots/shared/src/js/city-ui/dropdownUtils"; +import { CityStatsCollection } from "@prn-parking-lots/shared/src/js/model/types"; + +export default function createDropdownGroups( + data: CityStatsCollection, +): DropdownGroup[] { + return [ + { + label: "Group 1", + cities: Object.entries(data).map(([id, { name }]) => ({ id, name })), + }, + ]; +} diff --git a/packages/ct/src/js/main.ts b/packages/ct/src/js/main.ts index c382dea..3dd6e0a 100644 --- a/packages/ct/src/js/main.ts +++ b/packages/ct/src/js/main.ts @@ -5,6 +5,7 @@ import { CITY_BOUNDARIES_GEOJSON, PARKING_LOT_GEOJSON_MODULES, } from "./data"; +import createDropdownGroups from "./dropdownGroups"; export default async function initApp(): Promise { await bootstrapApp({ @@ -13,6 +14,7 @@ export default async function initApp(): Promise { boundaries: CITY_BOUNDARIES_GEOJSON, parkingLots: PARKING_LOT_GEOJSON_MODULES, }, + dropdownGroups: createDropdownGroups(CITY_STATS_DATA), initialCity: "hartford", }); } diff --git a/packages/primary/src/js/dropdownGroups.ts b/packages/primary/src/js/dropdownGroups.ts new file mode 100644 index 0000000..a20e594 --- /dev/null +++ b/packages/primary/src/js/dropdownGroups.ts @@ -0,0 +1,29 @@ +import { + DropdownGroup, + DropdownChoiceId, +} from "@prn-parking-lots/shared/src/js/city-ui/dropdownUtils"; +import { CityStatsCollection } from "@prn-parking-lots/shared/src/js/model/types"; + +export default function createDropdownGroups( + data: CityStatsCollection, +): DropdownGroup[] { + const official: DropdownChoiceId[] = []; + const community: DropdownChoiceId[] = []; + Object.entries(data).forEach(([id, { name, contribution }]) => { + if (contribution) { + community.push({ id, name }); + } else { + official.push({ id, name }); + } + }); + return [ + { + label: "Official maps", + cities: official, + }, + { + label: "Community maps", + cities: community, + }, + ]; +} diff --git a/packages/primary/src/js/main.ts b/packages/primary/src/js/main.ts index 1116100..b951a47 100644 --- a/packages/primary/src/js/main.ts +++ b/packages/primary/src/js/main.ts @@ -5,6 +5,7 @@ import { CITY_BOUNDARIES_GEOJSON, PARKING_LOT_GEOJSON_MODULES, } from "./data"; +import createDropdownGroups from "./dropdownGroups"; export default async function initApp(): Promise { await bootstrapApp({ @@ -13,6 +14,7 @@ export default async function initApp(): Promise { boundaries: CITY_BOUNDARIES_GEOJSON, parkingLots: PARKING_LOT_GEOJSON_MODULES, }, + dropdownGroups: createDropdownGroups(CITY_STATS_DATA), initialCity: "atlanta-ga", }); } diff --git a/packages/primary/tests/communityMaps.test.ts b/packages/primary/tests/communityMaps.test.ts deleted file mode 100644 index e0b7bc2..0000000 --- a/packages/primary/tests/communityMaps.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { expect, test } from "@playwright/test"; - -test("there are exactly 103 official city maps", async ({ page }) => { - await page.goto("/"); - await page.waitForSelector(".choices"); - await page.locator(".choices").click(); - await page.waitForSelector(".is-active"); - - const toggleValues = await page.$eval( - ".choices__list [role='listbox']", - (element: HTMLElement) => - Array.from(element.children).map( - (child: Element) => (child as HTMLElement).innerText, - ), - ); - - const expectedCities = 103; - const labelOffset = 1; - const communityMapIndex = toggleValues.indexOf("Community maps"); - expect(communityMapIndex).toEqual(expectedCities + labelOffset); -}); diff --git a/packages/primary/tests/dropdownGroups.test.ts b/packages/primary/tests/dropdownGroups.test.ts new file mode 100644 index 0000000..d9337a8 --- /dev/null +++ b/packages/primary/tests/dropdownGroups.test.ts @@ -0,0 +1,35 @@ +import { expect, test } from "@playwright/test"; +import createDropdownGroups from "../src/js/dropdownGroups"; + +test("createDropdownGroups", () => { + const common = { + percentage: "", + cityType: "", + population: "", + urbanizedAreaPopulation: "", + parkingScore: null, + reforms: "", + url: "", + }; + const input = { + "city1-ny": { + ...common, + name: "City 1, NY", + }, + "city2-ny": { + ...common, + name: "City 2, NY", + contribution: "some-email@web.com", + }, + }; + expect(createDropdownGroups(input)).toEqual([ + { + label: "Official maps", + cities: [{ id: "city1-ny", name: "City 1, NY" }], + }, + { + label: "Community maps", + cities: [{ id: "city2-ny", name: "City 2, NY" }], + }, + ]); +}); diff --git a/packages/shared/src/js/bootstrap.ts b/packages/shared/src/js/bootstrap.ts index 7dae5a0..b5a1781 100644 --- a/packages/shared/src/js/bootstrap.ts +++ b/packages/shared/src/js/bootstrap.ts @@ -1,5 +1,6 @@ import subscribeScorecard from "./city-ui/scorecard"; import initDropdown from "./city-ui/dropdown"; +import type { DropdownGroup } from "./city-ui/dropdownUtils"; import initAbout from "./layout/about"; import initIcons from "./layout/fontAwesome"; @@ -23,6 +24,7 @@ import { initViewState } from "./state/ViewState"; interface Args { data: DataSet; initialCity: CityId; + dropdownGroups: DropdownGroup[]; } export default async function bootstrapApp(args: Args): Promise { @@ -45,7 +47,7 @@ export default async function bootstrapApp(args: Args): Promise { args.initialCity, ); - initDropdown(args.data.stats, viewState); + initDropdown(args.dropdownGroups, viewState); subscribeScorecard(viewState, cityEntries); subscribeShareLink(viewState); subscribeSnapToCity(viewState, map, cityEntries); diff --git a/packages/shared/src/js/city-ui/dropdown.ts b/packages/shared/src/js/city-ui/dropdown.ts index 811aa30..5819597 100644 --- a/packages/shared/src/js/city-ui/dropdown.ts +++ b/packages/shared/src/js/city-ui/dropdown.ts @@ -1,10 +1,9 @@ import ChoicesJS from "choices.js"; -import { DropdownChoice, createChoice } from "./dropdownUtils"; -import type { CityStatsCollection } from "../model/types"; +import { DropdownGroup, convertToChoicesGroups } from "./dropdownUtils"; import { ViewStateObservable } from "../state/ViewState"; -function createDropdown(cityStatsData: CityStatsCollection): ChoicesJS { +function createDropdown(groups: DropdownGroup[]): ChoicesJS { const dropdown = new ChoicesJS("#city-dropdown", { position: "bottom", allowHTML: false, @@ -12,57 +11,26 @@ function createDropdown(cityStatsData: CityStatsCollection): ChoicesJS { searchEnabled: true, searchResultLimit: 6, searchFields: ["customProperties.city", "customProperties.context"], - // Disabling this option allows us to properly handle search groups. + // Disabling this option allows us to properly handle groups. // We already sort entries in the JSON file. shouldSort: false, }); - - const officialCities: DropdownChoice[] = []; - const communityCities: DropdownChoice[] = []; - Object.entries(cityStatsData).forEach(([id, { name, contribution }]) => { - const entry = createChoice(id, name); - if (contribution) { - communityCities.push(entry); - } else { - officialCities.push(entry); - } - }); - - dropdown.setChoices([ - { - value: "Official maps", - label: "Official maps", - disabled: false, - choices: officialCities, - }, - ]); - - if (communityCities.length > 0) { - dropdown.setChoices([ - { - value: "Community maps", - label: "Community maps", - disabled: false, - choices: communityCities, - }, - ]); - } - + dropdown.setChoices(convertToChoicesGroups(groups)); return dropdown; } export default function initDropdown( - cityStatsData: CityStatsCollection, + groups: DropdownGroup[], viewState: ViewStateObservable, ): void { - const dropdown = createDropdown(cityStatsData); + const dropdown = createDropdown(groups); viewState.subscribe( ({ cityId }) => dropdown.setChoiceByValue(cityId), "set dropdown to city", ); - // Bind user-changes in the dropdown to update the state in CitySelectionObservable. + // Bind user changes in the dropdown to update the view state. // Note that `change` only triggers for user-driven changes, not programmatic updates. const selectElement = dropdown.passedElement.element as HTMLSelectElement; selectElement.addEventListener("change", () => { diff --git a/packages/shared/src/js/city-ui/dropdownUtils.ts b/packages/shared/src/js/city-ui/dropdownUtils.ts index 0d8a340..cbd95e5 100644 --- a/packages/shared/src/js/city-ui/dropdownUtils.ts +++ b/packages/shared/src/js/city-ui/dropdownUtils.ts @@ -1,6 +1,8 @@ +import type { Group as ChoicesJSGroup } from "choices.js"; + import type { CityId } from "../model/types"; -export interface DropdownChoice { +interface DropdownChoice { value: string; label: string; customProperties: { @@ -10,6 +12,16 @@ export interface DropdownChoice { }; } +export interface DropdownChoiceId { + id: CityId; + name: string; +} + +export interface DropdownGroup { + label: string; + cities: Array; +} + export function createChoice(id: CityId, name: string): DropdownChoice { const [city, context] = name.split(/,\s|\s-\s/); return { @@ -21,3 +33,16 @@ export function createChoice(id: CityId, name: string): DropdownChoice { }, }; } + +export function convertToChoicesGroups( + groups: DropdownGroup[], +): ChoicesJSGroup[] { + return groups + .filter(({ cities }) => cities.length > 0) + .map(({ label, cities }) => ({ + label, + value: label, + disabled: false, + choices: cities.map(({ id, name }) => createChoice(id, name)), + })); +} diff --git a/packages/shared/tests/dropdownUtils.test.ts b/packages/shared/tests/dropdownUtils.test.ts index 6f3921a..da89604 100644 --- a/packages/shared/tests/dropdownUtils.test.ts +++ b/packages/shared/tests/dropdownUtils.test.ts @@ -1,5 +1,8 @@ import { expect, test } from "@playwright/test"; -import { createChoice } from "../src/js/city-ui/dropdownUtils"; +import { + createChoice, + convertToChoicesGroups, +} from "../src/js/city-ui/dropdownUtils"; test.describe("createChoice()", () => { test("city and state", () => { @@ -26,3 +29,31 @@ test.describe("createChoice()", () => { }); }); }); + +test("convertToChoicesGroups()", () => { + const city1 = { id: "city1", name: "City 1" }; + const city2 = { id: "city2", name: "City 2" }; + const city3 = { id: "city2", name: "City 3" }; + const input = [ + { label: "group 1", cities: [city1] }, + { label: "hidden", cities: [] }, + { label: "group 2", cities: [city2, city3] }, + ]; + expect(convertToChoicesGroups(input)).toEqual([ + { + value: "group 1", + label: "group 1", + disabled: false, + choices: [createChoice(city1.id, city1.name)], + }, + { + value: "group 2", + label: "group 2", + disabled: false, + choices: [ + createChoice(city2.id, city2.name), + createChoice(city3.id, city3.name), + ], + }, + ]); +});