mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 15:36:33 +00:00
fix(maps): let users and admins enter a CARTO API key (#2060)
CARTO stamps an "API KEY REQUIRED" watermark onto every basemap tile fetched without a key since 26.08.2026, and both CartoDB presets plus the built-in fallback are CARTO. The key is free and needs no account, so add it as a setting rather than dropping the provider. carto_api_key sits next to mapbox_access_token: per user in Settings/Map, instance-wide under Admin/User Defaults, encrypted at rest, injected from CARTO_API_KEY on managed instances. Unlike Mapbox it never switches the map provider, since a raster key says nothing about the renderer. The key is appended as ?key= at render time instead of being stored in the template, so a saved URL stays portable and survives a rotation. That is also what reaches the four maps that pinned CARTO and ignored the user's tile setting: atlas, collections, the public trip share and the public journey. The two share pages read it from their token-gated payload, the same way baseCurrency already travels. Also lifts the release notice above page chrome. Its overlay sat at z-index 50, under the Vacay mode toolbar at 61, so the dialog was neither on top nor modal. Adds a six-step layer scale to index.css and moves the three notice shells onto it. Closes #2054 Closes #2052
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// FE-COMP-MAP-001 to FE-COMP-MAP-030
|
||||
// FE-COMP-MAP-001 to FE-COMP-MAP-035
|
||||
import { render, screen, waitFor, within, fireEvent } from '../../../tests/helpers/render';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
@@ -350,3 +350,72 @@ describe('MapSettingsTab – GL providers', () => {
|
||||
expect(screen.getByDisplayValue('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// ── CARTO key (031–035) ─────────────────────────────────────────────
|
||||
|
||||
const CARTO_DARK_TILES = 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png';
|
||||
|
||||
/** The key input sits under its label inside the field's own wrapper. */
|
||||
function cartoInput(): HTMLInputElement {
|
||||
return within(screen.getByText('CARTO API key').closest('div') as HTMLElement).getByRole('textbox');
|
||||
}
|
||||
|
||||
describe('MapSettingsTab – CARTO key', () => {
|
||||
it('FE-COMP-MAP-031: the key field belongs to Leaflet and goes away with the GL providers', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<MapSettingsTab />);
|
||||
|
||||
expect(screen.getByText('CARTO API key')).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByText('Mapbox GL'));
|
||||
|
||||
expect(screen.queryByText('CARTO API key')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-MAP-032: a managed instance brings its own key, so the field is hidden', () => {
|
||||
seedStore(useAuthStore, { user: buildUser(), isAuthenticated: true, managed: true });
|
||||
render(<MapSettingsTab />);
|
||||
|
||||
expect(screen.queryByText('CARTO API key')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-MAP-033: the typed key is part of the save patch', async () => {
|
||||
const user = userEvent.setup();
|
||||
const updateSettings = vi.fn().mockResolvedValue(undefined);
|
||||
seedStore(useSettingsStore, {
|
||||
settings: buildSettings({ map_tile_url: CARTO_DARK_TILES }),
|
||||
updateSettings,
|
||||
});
|
||||
render(<MapSettingsTab />);
|
||||
|
||||
await user.type(cartoInput(), 'demo-key');
|
||||
await user.click(screen.getByText('Save Map'));
|
||||
|
||||
expect(updateSettings).toHaveBeenCalledWith(expect.objectContaining({ carto_api_key: 'demo-key' }));
|
||||
});
|
||||
|
||||
it('FE-COMP-MAP-034: a CARTO template without a key explains the watermark until one is typed', async () => {
|
||||
const user = userEvent.setup();
|
||||
seedStore(useSettingsStore, {
|
||||
settings: buildSettings({ map_tile_url: CARTO_DARK_TILES }),
|
||||
updateSettings: vi.fn().mockResolvedValue(undefined),
|
||||
});
|
||||
render(<MapSettingsTab />);
|
||||
|
||||
expect(screen.getByText(/API KEY REQUIRED/)).toBeInTheDocument();
|
||||
|
||||
await user.type(cartoInput(), 'demo-key');
|
||||
|
||||
expect(screen.queryByText(/API KEY REQUIRED/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-MAP-035: a template on another host never gets that notice', () => {
|
||||
seedStore(useSettingsStore, {
|
||||
settings: buildSettings({ map_tile_url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png' }),
|
||||
updateSettings: vi.fn().mockResolvedValue(undefined),
|
||||
});
|
||||
render(<MapSettingsTab />);
|
||||
|
||||
expect(screen.queryByText(/API KEY REQUIRED/)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -164,6 +164,7 @@ export default function MapSettingsTab(): React.ReactElement {
|
||||
const [mapTileUrl, setMapTileUrl] = useState<string>(settings.map_tile_url || '')
|
||||
const managed = useAuthStore((s) => s.managed)
|
||||
const [mapboxToken, setMapboxToken] = useState<string>(settings.mapbox_access_token || '')
|
||||
const [cartoKey, setCartoKey] = useState<string>(settings.carto_api_key || '')
|
||||
const [mapboxStyle, setMapboxStyle] = useState<string>(styleForProvider(initialProvider, slotStyle(initialProvider, settings)))
|
||||
const [mapbox3d, setMapbox3d] = useState<boolean>(settings.mapbox_3d_enabled !== false)
|
||||
const [mapboxQuality, setMapboxQuality] = useState<boolean>(settings.mapbox_quality_mode === true)
|
||||
@@ -175,6 +176,7 @@ export default function MapSettingsTab(): React.ReactElement {
|
||||
setProvider(nextProvider)
|
||||
setMapTileUrl(settings.map_tile_url || '')
|
||||
setMapboxToken(settings.mapbox_access_token || '')
|
||||
setCartoKey(settings.carto_api_key || '')
|
||||
setMapboxStyle(styleForProvider(nextProvider, slotStyle(nextProvider, settings)))
|
||||
setMapbox3d(settings.mapbox_3d_enabled !== false)
|
||||
setMapboxQuality(settings.mapbox_quality_mode === true)
|
||||
@@ -209,6 +211,7 @@ export default function MapSettingsTab(): React.ReactElement {
|
||||
map_provider: provider,
|
||||
map_tile_url: mapTileUrl,
|
||||
mapbox_access_token: mapboxToken,
|
||||
carto_api_key: cartoKey,
|
||||
...stylePatch,
|
||||
mapbox_3d_enabled: mapbox3d,
|
||||
mapbox_quality_mode: mapboxQuality,
|
||||
@@ -230,6 +233,8 @@ export default function MapSettingsTab(): React.ReactElement {
|
||||
setProvider(nextProvider)
|
||||
if (nextProvider !== 'leaflet') setMapboxStyle(styleForProvider(nextProvider, mapboxStyle))
|
||||
}
|
||||
// Only CARTO burns a watermark into keyless tiles, so the nudge is scoped to its hosts.
|
||||
const cartoNeedsKey = mapTileUrl.includes('basemaps.cartocdn.com') && !cartoKey.trim()
|
||||
|
||||
return (
|
||||
<Section title={t('settings.map')} icon={Map}>
|
||||
@@ -321,6 +326,30 @@ export default function MapSettingsTab(): React.ReactElement {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Same deal as the Mapbox token: a managed install brings its own key. */}
|
||||
{provider === 'leaflet' && !managed && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1.5">{t('settings.mapCartoKey')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={cartoKey}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setCartoKey(e.target.value)}
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm font-mono focus:ring-2 focus:ring-slate-400 focus:border-transparent"
|
||||
/>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
{t('settings.mapCartoKeyHint')}{' '}
|
||||
<a href="https://carto.com/basemaps/apikey/" target="_blank" rel="noreferrer" className="underline">
|
||||
{t('settings.mapCartoKeyLink')}
|
||||
</a>
|
||||
</p>
|
||||
{cartoNeedsKey && (
|
||||
<p className="text-xs text-amber-600 dark:text-amber-400 mt-1">{t('settings.mapCartoKeyMissing')}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* GL settings */}
|
||||
{provider !== 'leaflet' && (
|
||||
<div className="space-y-3">
|
||||
|
||||
Reference in New Issue
Block a user