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 theproject-map.tsxaction. FeatureControl's existing grouped visibility checkboxes (already grouped, unrelated code path).project.tsx's settings tabs beyond the one-line import change needed to reusegroupByFieldfrom 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
groupByFieldinproject.tsx; import it from~/lib/utilsat its three call sites (POI/line/area settings tabs) instead. distinctGroups(used for the settings forms'Comboboxsuggestions) 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 purelydefaultValue-driven) andgroup, both initialized once fromdefaultValues/poiTypesviagroupByField. - Add a
Field/FieldLabel/Selectfor "Group" above the existing "Type" field, options fromgroupByField(poiTypes), controlled bygroup/setGroup. - On group change, set
typeto the first slug in that group's bucket. - Existing "Type"
Selectbecomes controlled (value={type}/onValueChange={setType}), options limited to the selected group's bucket, keeping the existing unknown-typeSelectItemfallback gated ontype === 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.