mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
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:
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user