From 05394644b9176785a22cfbce1a89e404f904d5d3 Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 9 Oct 2026 10:15:41 +0200 Subject: [PATCH] refactor(admin): share the default user settings logic between desktop and phone Loading, saving and resetting the defaults for users and the map field handling live in one useDefaultUserSettings hook that reads the presets and style rules from mapSettingsModel. Each shell passes its own error text and preview stamp. --- .../Admin/DefaultUserSettingsTab.tsx | 175 ++------------- .../Admin/useDefaultUserSettings.test.ts | 155 +++++++++++++ .../Admin/useDefaultUserSettings.ts | 204 ++++++++++++++++++ .../admin/MAdminDefaultUserSettings.tsx | 171 ++------------- 4 files changed, 394 insertions(+), 311 deletions(-) create mode 100644 client/src/components/Admin/useDefaultUserSettings.test.ts create mode 100644 client/src/components/Admin/useDefaultUserSettings.ts diff --git a/client/src/components/Admin/DefaultUserSettingsTab.tsx b/client/src/components/Admin/DefaultUserSettingsTab.tsx index e7d9a7269..1a08f61ff 100644 --- a/client/src/components/Admin/DefaultUserSettingsTab.tsx +++ b/client/src/components/Admin/DefaultUserSettingsTab.tsx @@ -1,8 +1,6 @@ -import React, { useEffect, useMemo, useState } from 'react' +import React from 'react' import { Loader2, Map as MapIcon, Settings2 } from 'lucide-react' -import { adminApi } from '../../api/client' import { useTranslation } from '../../i18n' -import { useToast } from '../shared/Toast' import Section from '../Settings/Section' import { SettingRows, SettingsHint } from '../Settings/settingsKit' import { fs } from '../shared/DialogShell' @@ -10,64 +8,13 @@ import { EditorField, INPUT, Segmented } from '../shared/dialogParts' import CustomSelect from '../shared/CustomSelect' import { MapView } from '../Map/MapView' import { SYMBOLS, currenciesWith } from '../Budget/BudgetPanel.constants' -import type { DistanceUnit, Place, WeekStart } from '../../types' +import type { DistanceUnit, WeekStart } from '../../types' import { weekStartOptions } from '../../utils/calendarWeek' -import { normalizeTileUrl, withTileApiKey } from '../../utils/tileUrl' -import { - MAPBOX_DEFAULT_STYLE, - defaultStyleForProvider, - getStylePresets, - isOpenFreeMapStyle, - normalizeStyleForProvider, - styleSettingKey, - type GlMapProvider, -} from '../Map/glProviders' -import { useAuthStore } from '../../store/authStore' -import RoutingInstanceFields, { type RoutingDefaults } from './RoutingInstanceFields' - -const MAP_PRESETS = [ - { name: 'OpenStreetMap', url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png' }, - { name: 'OpenStreetMap DE', url: 'https://tile.openstreetmap.de/{z}/{x}/{y}.png' }, - // The app default, and a vector style rather than a {z}/{x}/{y} template: no - // key, no registration, no request limits. - { name: 'OpenFreeMap Positron', url: 'https://tiles.openfreemap.org/styles/positron' }, - { name: 'OpenFreeMap Bright', url: 'https://tiles.openfreemap.org/styles/bright' }, - // CARTO watermarks keyless tiles since 26.08.2026 and issues keys by mail, so - // these two need one; without it the map falls back to the default (#2054). - { name: 'CartoDB Light', url: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png' }, - { name: 'CartoDB Dark', url: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png' }, - { name: 'Stadia Smooth', url: 'https://tiles.stadiamaps.com/tiles/alidade_smooth/{z}/{x}/{y}{r}.png' }, -] - -type Defaults = RoutingDefaults & { - temperature_unit?: string - distance_unit?: DistanceUnit - dark_mode?: string | boolean - time_format?: string - week_start?: WeekStart - default_currency?: string - blur_booking_codes?: boolean - map_tile_url?: string - carto_api_key?: string - map_provider?: string - mapbox_access_token?: string - mapbox_style?: string - maplibre_style?: string - mapbox_3d_enabled?: boolean - mapbox_quality_mode?: boolean -} - -type MapProvider = 'leaflet' | GlMapProvider - -function normalizeProvider(value: unknown): MapProvider { - return value === 'mapbox-gl' || value === 'maplibre-gl' ? value : 'leaflet' -} - -/** Only the GL providers keep a style — Leaflet is handled by its callers. */ -function styleForProvider(provider: GlMapProvider, style?: string | null): string { - if (provider === 'mapbox-gl' && isOpenFreeMapStyle(style)) return MAPBOX_DEFAULT_STYLE - return normalizeStyleForProvider(provider, style) -} +import { withTileApiKey } from '../../utils/tileUrl' +import { defaultStyleForProvider } from '../Map/glProviders' +import { DEFAULT_MAP_PRESETS as MAP_PRESETS, type MapProvider } from '../Settings/mapSettingsModel' +import RoutingInstanceFields from './RoutingInstanceFields' +import { useDefaultUserSettings, type Defaults } from './useDefaultUserSettings' /** * One default as a row of the card's white box: its name (with the reset link @@ -114,57 +61,18 @@ function colorModeValue(value: string | boolean | undefined): string { return value ?? '' } +/** A failed save names the error it threw, or falls back to the generic one. */ +function errorText(err: unknown, fallback: string): string { + return err instanceof Error ? err.message : fallback +} + export default function DefaultUserSettingsTab(): React.ReactElement { const { t, locale } = useTranslation() - const toast = useToast() - const [defaults, setDefaults] = useState({}) - const [loaded, setLoaded] = useState(false) - const [mapTileUrl, setMapTileUrl] = useState('') - const managed = useAuthStore((s) => s.managed) - const [mapboxToken, setMapboxToken] = useState('') - const [cartoKey, setCartoKey] = useState('') - const [mapboxStyle, setMapboxStyle] = useState('') - - useEffect(() => { - adminApi.getDefaultUserSettings().then((data: Defaults) => { - const provider = normalizeProvider(data.map_provider) - setDefaults(data) - setMapTileUrl(normalizeTileUrl(data.map_tile_url || '')) - setMapboxToken(data.mapbox_access_token || '') - setCartoKey(data.carto_api_key || '') - setMapboxStyle(provider === 'leaflet' ? (data.mapbox_style || '') : styleForProvider(provider, provider === 'maplibre-gl' ? data.maplibre_style : data.mapbox_style)) - setLoaded(true) - }).catch(() => setLoaded(true)) - }, []) - - const save = async (patch: Partial) => { - try { - const updated = await adminApi.updateDefaultUserSettings(patch as Record) - setDefaults(updated) - toast.success(t('admin.defaultSettings.saved')) - } catch (err: unknown) { - toast.error(err instanceof Error ? err.message : t('common.error')) - } - } - - const reset = async (key: keyof Defaults) => { - try { - const updated = await adminApi.updateDefaultUserSettings({ [key]: null }) - setDefaults(updated) - if (key === 'map_tile_url') setMapTileUrl('') - if (key === 'mapbox_access_token') setMapboxToken('') - if (key === 'carto_api_key') setCartoKey('') - if (key === 'mapbox_style' || key === 'maplibre_style') { - const provider = normalizeProvider(defaults.map_provider) - setMapboxStyle(provider === 'leaflet' ? '' : defaultStyleForProvider(provider)) - } - toast.success(t('admin.defaultSettings.reset')) - } catch (err: unknown) { - toast.error(err instanceof Error ? err.message : t('common.error')) - } - } - - const isSet = (key: keyof Defaults) => defaults[key] !== undefined + const { + defaults, loaded, managed, isSet, save, reset, mapTileUrl, setMapTileUrl, mapboxToken, setMapboxToken, + cartoKey, setCartoKey, mapboxStyle, setMapboxStyle, mapPreviewPlaces, mapProvider, glStylePresets, styleKey, + saveMapProvider, pickTilePreset, pickStylePreset, commitStyle, + } = useDefaultUserSettings({ errorMessage: errorText, previewCreatedAt: () => String(new Date()) }) // Inside the row's own