Skip to content

Group-then-type selection in the POI/line/area forms — implementation plan

Source: user request ("improve our poi type selection — first select the group, then the type, to avoid an overly long list").

Today, PoiForm, LineForm, and AreaForm in project-map.tsx each render a single flat Select listing every PoiTypeDefinition/LineTypeDefinition/ AreaTypeDefinition on the project. Each type already carries an optional group?: string (see poi.ts, line.ts, area.ts), and project.tsx already groups and sorts by this field for the settings tabs via a local groupByField helper (line 107). This plan adds a second, preceding Select in each of the three sidebar forms that narrows the type list to one group at a time, reusing that same grouping logic instead of duplicating it.

No data model or server changes: group is already populated end-to-end and needs no new field, migration, or action changes.


Decisions

Decision Choice Why
UI shape A group Select stacked above the existing type Select in each form Confirmed with user
Ungrouped types Bucketed together under one synthetic group Confirmed with user; label it "Ungrouped", not "Other" — matches the label groupByField/project.tsx's settings tabs already use for the exact same bucket, so the same concept reads identically in both places
Grouping logic Move the existing groupByField helper out of project.tsx into app/lib/utils.ts and import it from both project.tsx and project-map.tsx It already does exactly what's needed (bucket by group, alphabetical, "Ungrouped" last); duplicating a second copy inside project-map.tsx would just be the same 25 lines twice
Group select options groupByField(types).map(g => g.group) — only groups that have at least one type; no empty groups shown Mirrors groupByField's existing output shape, nothing new to compute
Type select options Filtered to the types in the currently-selected group's bucket Matches the "two stacked Selects, second filtered by first" shape confirmed with user
Group/type state Both become local useState in each form (group, type), Select switches from defaultValue-only (uncontrolled) to value/onValueChange (controlled) The type list must react when the group changes; defaultValue alone can't do that
Changing the group Resets the selected type to the first type in the newly-chosen group's bucket Simplest deterministic rule; avoids leaving type pointing at a type from the group the user just left
Preselecting on edit group initializes from defaultValues.type's owning type's group (or "Ungrouped" if that type has no group or isn't found) Confirmed with user — editing must land on the correct group already selected
Unknown-type fallback item Unchanged in behavior: still rendered as a SelectItem for defaultValues.type when that slug isn't found in the project's types, but now only reachable while type === defaultValues.type (i.e. before the user changes the group) Once the user picks a different group deliberately, the unknown value is intentionally replaced by that group's first real type, per the reset rule above
Submitted form field Only the type Select keeps name="type"; the group Select has no name and is never submitted Confirmed acceptance criterion — no change to the action's payload shape

Out of scope

  • Any change to poiconfig.json, PoiTypeDefinition/LineTypeDefinition/AreaTypeDefinition, projects.server.ts, or the project-map.tsx action.
  • FeatureControl's existing grouped visibility checkboxes (already grouped, unrelated code path).
  • project.tsx's settings tabs beyond the one-line import change needed to reuse groupByField from its new location.
  • Managing types/groups themselves (creating/renaming/deleting a group).

Step 1 — Move groupByField into app/lib/utils.ts

Goal: groupByField is exported from app/lib/utils.ts instead of being a local function in project.tsx, with no behavior change.

// app/lib/utils.ts
export function groupByField<T extends { group?: string }>(
  items: T[]
): { group: string; items: T[] }[]
  • Delete the local groupByField in project.tsx; import it from ~/lib/utils at its three call sites (POI/line/area settings tabs) instead.
  • distinctGroups (used for the settings forms' Combobox suggestions) stays where it is — it isn't needed by this plan.

Acceptance criteria - [ ] app/lib/utils.ts exports groupByField; project.tsx no longer defines it locally. - [ ] Project settings tabs render identically to before (same grouping, same "Ungrouped" label, same sort order) — pure move, no behavior change. - [ ] pnpm typecheck passes.

Step 2 — Group-then-type selection in PoiForm

Goal: PoiForm's "Type" field is preceded by a "Group" field; picking a group filters the type options to that group.

  • Add local state: type (renamed from being purely defaultValue-driven) and group, both initialized once from defaultValues/poiTypes via groupByField.
  • Add a Field/FieldLabel/Select for "Group" above the existing "Type" field, options from groupByField(poiTypes), controlled by group/setGroup.
  • On group change, set type to the first slug in that group's bucket.
  • Existing "Type" Select becomes controlled (value={type} / onValueChange={setType}), options limited to the selected group's bucket, keeping the existing unknown-type SelectItem fallback gated on type === defaultValues?.type.
  • No change to hiddenFields, name="type", or the submitted shape.

Acceptance criteria - [ ] Adding a POI: group defaults to the first available group (or "Ungrouped" if poiTypes has no grouped entries); type defaults to the first type in that group. - [ ] Editing a POI: group preselects the group owning the POI's current type; type preselects the POI's current type. - [ ] Changing the group updates the type options and selects the first type in the new group. - [ ] Editing a POI whose type matches nothing in project.poiTypes still shows the "Unknown" fallback item and keeps it selected until the group is changed. - [ ] Submitting the form still posts only type (the slug) — verified by reading the network payload or the existing action's behavior being unaffected. - [ ] pnpm typecheck passes.

Step 3 — Group-then-type selection in LineForm

Goal: Same change as Step 2, applied to LineForm and project.lineTypes.

Acceptance criteria - [ ] Same five behavioral criteria as Step 2, adapted to lines (add-line / update-line, LineTypeDefinition, lineTypes). - [ ] pnpm typecheck passes.

Step 4 — Group-then-type selection in AreaForm

Goal: Same change as Step 2, applied to AreaForm and project.areaTypes.

Acceptance criteria - [ ] Same five behavioral criteria as Step 2, adapted to areas (add-area / update-area, AreaTypeDefinition, areaTypes). - [ ] pnpm typecheck passes.

Step 5 — Verification pass

Goal: Confirm the three forms behave consistently and nothing else regressed.

  • Manually exercise, per kind (POI/line/area): add with default group/type, switch groups before saving, edit an existing item and confirm group preselection, edit an item with an unknown/deleted type.
  • Confirm project.tsx's settings tabs still render unaffected by the Step 1 move.

Acceptance criteria - [ ] pnpm typecheck passes. - [ ] pnpm build passes. - [ ] Manual checks above pass with no console errors.