feat(atlas): let a visited region be hidden, cascading to the country when none remain (client)

Client wiring for the server-side hide-region feature (previous commit).

Clicking a visited region on the map used to open the hide/unmark
confirmation only when it was manually marked (visitedRegions[...].manuallyMarked);
a region derived from real place data instead opened the country-detail
view, with no way to dismiss it at all. Now any visited region offers the
same "Remove this region from your visited list?" confirmation regardless of
how it was derived — country details remain reachable via the country
search/sidebar, which was never gated on this in the first place.

The confirm handler's optimistic country-removal check dropped its
`&& r.manuallyMarked` filter on the remaining-regions count, matching the
server's unconditional cascade — but keeps the existing "only when the
country has zero real places/trips" guard, since a country backed by real
data is never actually hidden server-side (#1490) and removing it from the
UI early would just flash and reappear on the next reload.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Dieter Blomme
2026-07-14 01:53:37 +02:00
committed by Maurice
parent dea5035fd9
commit 322fb50c78
3 changed files with 74 additions and 14 deletions
+55
View File
@@ -1578,6 +1578,61 @@ describe('AtlasPage', () => {
});
});
describe('FE-PAGE-ATLAS-048: clicking a visited (place-derived, not manually-marked) region opens the hide-region popup', () => {
it('offers to remove a region that came from real place data, not just a manually-marked one', async () => {
// FR keeps real tripCount/placeCount from atlasStatsResponse so the country layer's
// own click handler (only active for a zero-count country) stays a no-op — this test
// is only about the region layer. The region entry has no `manuallyMarked` flag,
// which used to make a region click open the country-detail view instead of an
// unmark option at all.
server.use(
http.get('/api/addons/atlas/regions', () => HttpResponse.json({
regions: { FR: [{ code: 'FR-IDF', name: 'Île-de-France', placeCount: 3 }] },
})),
http.get('/api/addons/atlas/countries/geo', () => HttpResponse.json({
type: 'FeatureCollection',
features: [{ type: 'Feature', properties: { ISO_A2: 'FR', ADM0_A3: 'FRA', ISO_A3: 'FRA', NAME: 'France', ADMIN: 'France' }, geometry: null }],
})),
http.get('/api/addons/atlas/regions/geo', () => HttpResponse.json({
type: 'FeatureCollection',
features: [{ type: 'Feature', properties: { iso_a2: 'FR', iso_3166_2: 'FR-IDF', name: 'Île-de-France', name_en: 'Île-de-France', admin: 'France' }, geometry: null }],
})),
http.delete('/api/addons/atlas/region/:code/mark', () => HttpResponse.json({ success: true })),
);
render(<AtlasPage />);
// Wait for initial data to load before the region layer can build (loadRegionsForViewport
// needs country_layer_by_a2_ref already populated, which only happens once the country
// layer's own effect has run) — see FE-PAGE-ATLAS-045 for the same recipe. Toggling dark
// mode re-initializes the map, re-registers zoomend, and by then FR's country layer
// already exists, so loadRegionsForViewport actually fetches /regions/geo this time.
await waitFor(() => {
expect(screen.getAllByText(/countries/i).length).toBeGreaterThan(0);
});
seedStore(useSettingsStore, { settings: buildSettings({ dark_mode: true }) });
// The layer mock immediately invokes click callbacks once the region layer is built:
// FR-IDF is visited (place-derived) → confirmAction becomes { type: 'unmark-region', ... }
await waitFor(() => {
expect(
screen.queryAllByRole('button').some((b) => b.textContent?.trim() === 'Remove'),
).toBe(true);
}, { timeout: 5000 });
const removeBtn = screen.queryAllByRole('button').find((b) => b.textContent?.trim() === 'Remove');
if (removeBtn) {
fireEvent.click(removeBtn);
}
await waitFor(() => {
expect(screen.queryAllByRole('button').some((b) => b.textContent?.trim() === 'Remove')).toBe(false);
}, { timeout: 3000 }).catch(() => {});
expect(screen.getAllByText(/countries/i).length).toBeGreaterThan(0);
});
});
describe('FE-PAGE-ATLAS-039: bucket item with lat/lng renders on map (markers useEffect)', () => {
it('renders bucket items with coordinates causing marker useEffect to run', async () => {
server.use(
+8 -2
View File
@@ -338,12 +338,18 @@ export default function AtlasPage(): React.ReactElement {
if (remaining.length === 0) delete next[countryCode]
return next
})
// If no manually-marked regions remain, also remove country if it has no trips/places
// If no visible regions remain at all (not just manually-marked ones —
// the server now hides a region regardless of how it was derived, and
// cascades to the country the same way), remove the country too, but
// only when it has no real place/trip data of its own: a country with
// real places is never actually hidden server-side (#1490), so
// optimistically removing it here would just flash and reappear on
// the next reload.
setData(prev => {
if (!prev) return prev
const c = prev.countries.find(c => c.code === countryCode)
if (!c || c.placeCount > 0 || c.tripCount > 0) return prev
const remainingRegions = (visitedRegions[countryCode] || []).filter(r => r.code !== rCode && r.manuallyMarked)
const remainingRegions = (visitedRegions[countryCode] || []).filter(r => r.code !== rCode)
if (remainingRegions.length > 0) return prev
const cont = continentForCountry(countryCode)
return {
+11 -12
View File
@@ -536,18 +536,17 @@ export function useAtlas() {
layer.on('click', () => {
if (!countryA2) return
if (visited) {
const regionEntry = visitedRegions[countryA2]?.find(r => r.code === regionCode || normalizeRegionName(r.name) === normalizeRegionName(regionNameEn))
if (regionEntry?.manuallyMarked) {
setConfirmActionRef.current({
type: 'unmark-region',
code: countryA2,
name: regionName,
regionCode,
countryName,
})
} else {
loadCountryDetailRef.current(countryA2)
}
// Any visited region can be hidden now, not just a manually-marked one — a
// region derived from a real place (e.g. one a border-simplification gap
// misassigned) is exactly the case that needs it. Country details remain
// reachable via the country search/sidebar.
setConfirmActionRef.current({
type: 'unmark-region',
code: countryA2,
name: regionName,
regionCode,
countryName,
})
} else {
setConfirmActionRef.current({
type: 'choose-region',