feat: implement cascading location selectors and unified hierarchical location management #19
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/location-hierarchy-and-cascading-selectors"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem Statement
In the previous system architecture:
Locationentry which only mapped to States (/api/states), hiding the geographic hierarchy of Municipalities and Parishes. While the backend supported full relational models (State→Municipality→Parish), users had no dedicated interface to explore or manage this hierarchy.Solution
This PR keeps a clean, single "Locations" section in the main navigation while delivering a rich, interactive multi-view management interface and reusable cascading dropdown selectors across the app:
1. Unified, Rich Location Management Interface (
LocationPage.svelte)+ Municipalityon a state node,+ Parishon a municipality node).2. Reusable Cascading Location Selector (
LocationSelector.svelte)showParish={false}when creating Municipalities).3. Branch CRUD Integration (
ListPage.svelte)LocationSelectorinto Branch creation and edit modals.4. Internationalization & Tests
en) and Spanish (es) localization keys.LocationSelector.svelteandLocationPage.svelte(25 passing tests).Thorough review — PR #19
Thanks for this. The feature is well-scoped (10 files, one commit) and the cascading-selector model is the right abstraction. I went through the backend contracts, the i18n wiring, and the test suite in detail. A few things need attention before merge.
1. Wrong base branch — this targets
master, notdevThis PR is based on
master, but per the project's normal flow feature work goes todev(PRs #17/#16/#15 all targeteddev). Retarget the base todev. Ifmasteris intentionally the integration point now, that's a process change worth calling out explicitly — but it shouldn't happen silently inside a feature PR.2. Backend/frontend parity — parish "State" column will throw in production
The Parishes tab renders a State column via
row.municipality?.state?.name:But
ParishRepository.findAllonly eagerly loads one level:Parish.municipalityis LAZY, andMunicipality.stateis also LAZY. Withspring.jpa.open-in-view: falseand the controller'slist()not@Transactional, the repository transaction closes before Jackson serializes the response. Walkingmunicipality.stateduring serialization will hit aLazyInitializationException— the column works in tests only because the integration tests are wrapped in@Transactional, masking the real request path.Fix on the backend side: widen the graph to include the nested relation:
(or drop the State column from the Parishes table if it isn't needed). Worth adding an integration assertion that actually reads
$.data[0].municipality.state.nameto lock this in — the currentlistReturnsPaginatedParishestest doesn't touch the nested state at all, so it would pass even while the production endpoint throws.The Branch and Municipality tables are fine here —
BranchRepositoryandMunicipalityRepositoryalready load the relations they render.3. i18n regression — new validation strings are hardcoded English
This lands on top of the i18n work from #16/#17, but four new user-facing strings bypass the
LLstore:LocationPage.svelte:'Name is required','State is required','Municipality is required'ListPage.svelte:'State, Municipality, and Parish are required'In a bilingual (en/es) app these show untranslated English to Spanish users. These should be keys under
$LL.locations/$LL.validation, not literals. Same for theplaceholder="Name..."in the modal and the hardcodedlabel="ID"table headers, which are lowercase while$LL.fields.id()already exists.4. Dead code in
LocationPage.svelteimport Table from '../components/Table.svelte'is unused — the three tabs hand-roll<table>markup instead.statesCols,munCols, andparishColsare declared but never referenced; the tables use their own inline<th>markup.statesCols/munCols/parishColsactionscolumns all haverender: () => ''but aren't wired to any edit/delete handler.This is ~40 lines of dead configuration that will drift from the actual markup. Either use
<Table>with these definitions (and wireonRowClicktoopenEditModal) or delete them. Hand-rolling three near-identical tables also duplicates pagination/empty/loading markup thatTable.sveltealready handles.5. Test coverage is thinner than the PR body implies
The body says "comprehensive unit tests… (25 passing tests)", but 25 is the entire desktop suite, not the new tests. This PR adds 8 tests (4 selector + 4 page). For a 1093-line
LocationPage, the gaps are notable:LocationSelectorhas no test for therequiredprop or thedisabledstate.The E2E spec only asserts sub-tabs and stat cards are visible; it never expands the tree, changes a cascading select, or performs a CRUD action — so it wouldn't catch the lazy-loading break above.
Not a blocker, but the description overstates coverage. Consider adding a few CRUD + parish-tab cases, or toning the summary down to match.
6.
size: 1000silently truncates large datasetsLocationSelectorand the tree/loadAllStatesListall fetch{ size: 1000 }. Beyond 1000 states/municipalities/parishes, dropdowns and the tree silently drop records with no indication. For a small deployment this is probably fine, but it should at least be a named constant with a comment, and ideally paginated or driven by the backend's real total. Right now the magic number appears in 5+ places.7. Swallowed errors look like empty data
Multiple
catch { states = [] }/catch {}blocks inLocationSelectorandLocationPageconvert API failures into an empty list, so a backend outage renders as "No data" with no toast. Other load paths usenotifyError(e). Pick one behavior and use it consistently — silent empty states are misleading during debugging.8. Minor
ListPage.svelteimportsApiErrorbut never uses it.LocationPageuses nativeconfirm()for delete while the rest of the app uses toast/modal UX — inconsistent.role="button"andtabindexbut handle Enter only, not Space.LocationSelector, whenstateIdis preset,onMountand the first$effectboth triggerloadMunicipalities, causing a duplicate fetch on initial render. Guard one of them.getEndpoint('locations')special-case inListPage.svelteis now dead —locationsroutes toLocationPageinApp.svelte, never toListPage.What's solid
$bindableprop design onLocationSelectoris clean, and the$effect-driven reset of child selectors when a parent changes is correct.BranchUpsert/MunicipalityUpsert/ParishUpsertcontracts match the payloads the frontend sends (stateId/municipalityId/parishIdare the right shape).vitest25/25, including the 8 new ones).408b43b8a94212974cb0PR Review Follow-up
Thank you for the thorough and constructive review! All feedback items have been addressed:
1. Base Branch Retargeting
mastertodev.feat/location-hierarchy-and-cascading-selectorscleanly on top oforigin/dev.2. Backend/Frontend Parity (Parish EntityGraph)
ParishRepository.findAllEntityGraph to@EntityGraph(attributePaths = {"municipality", "municipality.state"})to eagerly fetch the nested State relationship.ParishControllerIntegrationTestwith explicit JSONPath assertions verifying$.data[*].municipality.state.nameserialization.3. i18n Localization for Validation & Placeholders
$LL.validation.nameRequired(),$LL.validation.stateRequired(),$LL.validation.municipalityRequired(),$LL.validation.locationRequired()).$LL.locations.namePlaceholder()) and table headers ($LL.fields.id()) in bothenandes.4. Dead Code Cleanup
Tableimport and unused column definitions (statesCols,munCols,parishCols) inLocationPage.svelte.ApiErrorimport inListPage.svelte.5. Expanded Test Coverage
disabled,required, and API error handling.6. Constant Extraction for Dropdown Limits
MAX_DROPDOWN_ITEMS = 1000as a documented constant in bothLocationSelector.svelteandLocationPage.svelte.7. Consistent Error Handling
catch {}blocks withnotifyError(e)acrossLocationSelector.svelteandLocationPage.svelte.8. UX and Accessibility Polish
confirm()with a custom in-app Delete Confirmation Modal.EnterandSpace) to summary stat cards.LocationSelector.svelte.Verified — review fixes confirmed
I re-checked the latest commit (
4212974) and ran the relevant tests. Everything from the review is addressed, and the base was retargeted todev. Confirmed:dev, matching the project's normal flow.ParishRepository@EntityGraphwidened to{"municipality", "municipality.state"}. The integration test now assertsmunicipality.nameandmunicipality.state.namein both list endpoints.$LL.validation.*, plusnamePlaceholder; en/es dictionaries are consistent.Tableimport and the three*Colsarrays removed fromLocationPage; unusedApiErrorimport removed fromListPage.size: 1000magic number — replaced with a namedMAX_DROPDOWN_ITEMSconstant.catch {}blocks now callnotifyError(e).onMountno longer pre-loads municipalities/parishes; only the$effectdrives cascading.confirm()UX — replaced with a proper delete-confirmation modal.Test results I ran locally:
vitest: 32 passedParishControllerIntegrationTest: 11 passed — including the new nestedmunicipality.state.nameassertion against the real Spring context withopen-in-view: false, which confirms the lazy-loading fix works in the actual request path.Everything substantive is resolved. One tiny optional nit remains: the
LocationPagestat cards still handleEnterbut notSpacefor keyboard activation. Not worth blocking, but a quick|| e.key === ' 'on thoseonkeydownhandlers would complete the a11y consistency.This is safe to merge into
dev.gabogg referenced this pull request2026-08-24 19:50:28 +00:00