From f903abcd293b77adce74f0d786b6fa462ff4ef3b Mon Sep 17 00:00:00 2001 From: Tautvydas Derzinskas Date: Fri, 11 Sep 2026 14:29:17 +0200 Subject: [PATCH] fix: profile menu chips --- backend/src/routes/settings.ts | 11 ++--- frontend/src/components/Layout/TopBar.tsx | 1 - frontend/src/components/Layout/UserMenu.tsx | 49 +++++++++++++++++---- frontend/src/i18n/locales/en.json | 4 +- frontend/src/i18n/locales/lt.json | 4 +- 5 files changed, 53 insertions(+), 16 deletions(-) diff --git a/backend/src/routes/settings.ts b/backend/src/routes/settings.ts index bdb9385..a279793 100644 --- a/backend/src/routes/settings.ts +++ b/backend/src/routes/settings.ts @@ -108,13 +108,14 @@ router.post( }), ); -// Admin-only end to end, like storage settings: the token is a shared credential for the whole -// instance's Thingiverse imports, not a per-user preference. GET never echoes the token itself -// back (write-only, like any other API secret) -- only whether one is currently configured, so -// the admin UI can show "configured" / "not configured" without re-displaying the value. +// Read is open to every user -- UserMenu's Thingiverse connection chip needs to know whether +// imports will actually work, same as /settings/previews above. Only admins can change it: the +// token is a shared credential for the whole instance's Thingiverse imports, not a per-user +// preference. GET never echoes the token itself back (write-only, like any other API secret) -- +// only whether one is currently configured, so the admin UI can show "configured" / "not +// configured" without re-displaying the value. router.get( "/settings/thingiverse", - requireAdmin, asyncHandler(async (_req, res) => { res.json({ configured: Boolean(await getThingiverseAccessToken()) }); }), diff --git a/frontend/src/components/Layout/TopBar.tsx b/frontend/src/components/Layout/TopBar.tsx index a9e431b..d27c28a 100644 --- a/frontend/src/components/Layout/TopBar.tsx +++ b/frontend/src/components/Layout/TopBar.tsx @@ -74,7 +74,6 @@ export default function TopBar({ onThemeChange={onThemeChange} onOpenProfile={onOpenProfile} onLogout={onLogout} - makerworldCookie={makerworldCookie} /> diff --git a/frontend/src/components/Layout/UserMenu.tsx b/frontend/src/components/Layout/UserMenu.tsx index b86faa4..46aebea 100644 --- a/frontend/src/components/Layout/UserMenu.tsx +++ b/frontend/src/components/Layout/UserMenu.tsx @@ -18,14 +18,18 @@ import type { AuthUser } from "../../api/auth"; import type { ThemeSelection } from "../../constants/settingsOptions"; import { IMPORT_PROVIDER_INFO } from "../../constants/importProviders"; import { useGravatarUrl } from "../../hooks/useGravatarUrl"; +import { settingsApi } from "../../api/settings"; const THEME_MODES: ThemeSelection[] = ["light", "dark"]; type ServiceChipDef = { - key: "makerworld"; + key: "makerworld" | "thingiverse" | "printables"; label: string; color: string; connected: boolean; + /** Shown in the tooltip only when connected is false -- explains what's missing (a per-user + * credential the viewer can fix themselves, vs. an instance-wide one only an admin can). */ + disabledReason?: string; }; type Props = { @@ -34,19 +38,30 @@ type Props = { onThemeChange: (theme: ThemeSelection) => void; onOpenProfile: () => void; onLogout: () => void; - makerworldCookie: string; }; /** Avatar (Gravatar, from the account email) that opens identity + quick settings, mirroring * the youtube-mp3-vault UserMenu: name/email/logout up top, Profile and a light/dark Theme - * submenu below, then at-a-glance MakerWorld cookie status. Thingiverse import auth is an - * admin-configured, instance-wide Access Token now (see AdminSettingsPage/ThingiverseSection), - * not a per-user credential, so it has no chip here. */ -export function UserMenu({ user, theme, onThemeChange, onOpenProfile, onLogout, makerworldCookie }: Props) { + * submenu below, then at-a-glance import-provider connection chips (MakerWorld, Thingiverse, + * Printables). Each provider's "configured" status is fetched from the backend once on mount -- + * never from localStorage, which only mirrors what this one browser last saved and can easily + * disagree with the DB (e.g. the cookie was set from a different device, or localStorage was + * cleared) -- see MakerworldCookieSection, which had the same DB-truth fetch already for its + * own chip; this just brings UserMenu's chip in line with it. */ +export function UserMenu({ user, theme, onThemeChange, onOpenProfile, onLogout }: Props) { const { t } = useTranslation(["app", "common"]); const avatarUrl = useGravatarUrl(user?.email, 128); const [anchorEl, setAnchorEl] = React.useState(null); const [themeAnchorEl, setThemeAnchorEl] = React.useState(null); + const [makerworldConfigured, setMakerworldConfigured] = React.useState(false); + const [thingiverseConfigured, setThingiverseConfigured] = React.useState(false); + + React.useEffect(() => { + let active = true; + settingsApi.getMakerworld().then(res => { if (active) setMakerworldConfigured(res.configured); }).catch(() => undefined); + settingsApi.getThingiverse().then(res => { if (active) setThingiverseConfigured(res.configured); }).catch(() => undefined); + return () => { active = false; }; + }, []); const closeMenu = () => setAnchorEl(null); const closeThemeMenu = () => setThemeAnchorEl(null); @@ -58,7 +73,21 @@ export function UserMenu({ user, theme, onThemeChange, onOpenProfile, onLogout, }; const services: ServiceChipDef[] = [ - { key: "makerworld", ...IMPORT_PROVIDER_INFO.makerworld, connected: Boolean(makerworldCookie.trim()) }, + { + key: "makerworld", + ...IMPORT_PROVIDER_INFO.makerworld, + connected: makerworldConfigured, + disabledReason: t("userMenu.makerworldDisabledReason"), + }, + { + key: "thingiverse", + ...IMPORT_PROVIDER_INFO.thingiverse, + connected: thingiverseConfigured, + disabledReason: t("userMenu.thingiverseDisabledReason"), + }, + // No configuration to check -- Printables' public API answers unauthenticated requests + // directly (see backend's printablesApi.ts), so imports from it always work. + { key: "printables", ...IMPORT_PROVIDER_INFO.printables, connected: true }, ]; return ( @@ -104,7 +133,11 @@ export function UserMenu({ user, theme, onThemeChange, onOpenProfile, onLogout, {services.map(svc => (