fix(admin): graceful logo-img fallback + show sidebar widgets during perm hydration (#523 follow-up 2)
Two issues Rekoo-PS hit immediately after upgrading to v3.60.3-beta.0:
1. **Broken logo URL rendered the browser's broken-image icon + alt
text.** Their `<img src={resolvedLogoUrl}>` had no `onError` handler,
so a 404 / slow logo URL produced the default broken-image rendering
— which uses the `alt` attribute (`companyName`) as text. Visually it
looked like the wordmark span had unexpectedly re-appeared on phone,
even though the actual `<span>` was correctly hidden by the existing
`wordmarkVisibilityClass` logic.
Fix:
- `useState` tracks `logoLoadError` (first failure) and
`fallbackLoadError` (second failure). On a configured-URL miss the
`<img>` swaps to the bundled `/picpeak-kamera-transparent.png`; on
a second miss the `<img>` is removed from the DOM entirely.
- `useEffect([resolvedLogoUrl])` resets both flags when the URL
changes, so a dark-mode toggle that flips `lightLogo ↔ darkLogo`
gets a fresh attempt instead of being permanently sad.
- `wordmarkVisibilityClass` now derives from `logoEffectivelyVisible`
(showLogo && !fallbackLoadError) — when both the configured URL
AND the bundled fallback have failed, the wordmark un-hides on <sm
so the phone header isn't completely empty.
2. **Sidebar VersionInfo + StorageInfo vanished during the
permission-hydration window.** The bottom block was gated on
`hasPermission('settings.view')` directly, which returns `false`
while `PermissionsContext.isLoading` is still resolving (a few
hundred ms right after a deploy when the auth context bootstraps).
Net effect: the whole "Version / Storage" block was absent on first
paint, then re-appeared once permissions hydrated — Rekoo-PS read
that flash as "backend version + storage missing".
Fix: gate on `permissionsLoading || hasPermission('settings.view')`.
Optimistic render during hydration; permitted users see the widgets
immediately (with each widget's own internal loading state), denied
users still see nothing once the permission state lands as `false`.
Side benefit: the `<img>` fallback chain also covers the broader "logo
hosted on a flaky CDN" case for self-hosters, not just the one-time
post-upgrade asset-cache hiccup. Pure resilience polish — no behaviour
change when everything works.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Menu, User, LogOut, Settings, Bell, Lock, CheckCircle, Trash2, Sun, Moon, Globe, ChevronDown } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -51,11 +51,31 @@ export const AdminHeader: React.FC<AdminHeaderProps> = ({ onMenuClick }) => {
|
||||
? (logoUrl.startsWith('http') ? logoUrl : buildResourceUrl(logoUrl))
|
||||
: '/picpeak-kamera-transparent.png';
|
||||
|
||||
// #523 follow-up 2: graceful fallback when the configured logo URL
|
||||
// 404s or stalls. Without an error handler, the <img> failure draws
|
||||
// the browser's default broken-image-icon + alt text rendering — see
|
||||
// Rekoo-PS's 3.60.3-beta.0 screenshot where "Arkan Studio" appeared
|
||||
// as the alt text of a broken icon, not the real wordmark span. The
|
||||
// chain:
|
||||
// 1. configured URL fails → try the bundled picpeak fallback
|
||||
// 2. bundled fallback fails → hide the image entirely, let the
|
||||
// wordmark carry the brand
|
||||
// Reset on URL change so a dark-mode toggle (which can flip lightLogo
|
||||
// ↔ darkLogo) retries the new URL instead of being permanently sad.
|
||||
const [logoLoadError, setLogoLoadError] = useState(false);
|
||||
const [fallbackLoadError, setFallbackLoadError] = useState(false);
|
||||
useEffect(() => {
|
||||
setLogoLoadError(false);
|
||||
setFallbackLoadError(false);
|
||||
}, [resolvedLogoUrl]);
|
||||
const logoImgSrc = logoLoadError ? '/picpeak-kamera-transparent.png' : resolvedLogoUrl;
|
||||
|
||||
// Renders the logo + wordmark block per the current logo_display_mode.
|
||||
// Re-used in left / center / right slots below so all three positions
|
||||
// produce visually identical brand chrome.
|
||||
const showLogo = !logoInSidebar && (logoDisplayMode === 'logo_only' || logoDisplayMode === 'logo_and_text');
|
||||
const showText = logoDisplayMode === 'text_only' || logoDisplayMode === 'logo_and_text';
|
||||
const logoEffectivelyVisible = showLogo && !fallbackLoadError;
|
||||
// On <sm the wordmark hides when the logo carries the brand identity
|
||||
// (logo_and_text). Same pattern LanguageSelector uses for its language
|
||||
// name (#527). Without this, even with truncate, a phone-width admin
|
||||
@@ -64,7 +84,12 @@ export const AdminHeader: React.FC<AdminHeaderProps> = ({ onMenuClick }) => {
|
||||
// cluster it overlaps the LanguageSelector button (#523 follow-up,
|
||||
// Rekoo-PS's "Arkan Studio" screenshot in v3.59.0-beta.0). text_only
|
||||
// mode keeps the wordmark on every width — nothing else would render.
|
||||
const wordmarkVisibilityClass = showLogo ? 'hidden sm:inline' : 'inline';
|
||||
//
|
||||
// #523 follow-up 2: when both the configured URL AND the bundled
|
||||
// fallback have failed (fallbackLoadError → logoEffectivelyVisible
|
||||
// false), unhide the wordmark on <sm too — otherwise the phone header
|
||||
// shows nothing at all for the brand block.
|
||||
const wordmarkVisibilityClass = logoEffectivelyVisible ? 'hidden sm:inline' : 'inline';
|
||||
const renderBrandBlock = () => {
|
||||
// Skeleton placeholder while `usePublicSettings()` is in flight (#523
|
||||
// follow-up — Rekoo-PS's "logo took some time to load" screenshot in
|
||||
@@ -88,8 +113,19 @@ export const AdminHeader: React.FC<AdminHeaderProps> = ({ onMenuClick }) => {
|
||||
// action buttons on narrow mobile widths (#523 regression).
|
||||
return (
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
{showLogo && (
|
||||
<img src={resolvedLogoUrl} alt={companyName} className="h-8 w-auto object-contain flex-shrink-0" />
|
||||
{logoEffectivelyVisible && (
|
||||
<img
|
||||
src={logoImgSrc}
|
||||
alt={companyName}
|
||||
className="h-8 w-auto object-contain flex-shrink-0"
|
||||
onError={() => {
|
||||
// First failure: configured URL → try the bundled fallback.
|
||||
// Second failure: bundled fallback → hide entirely, let
|
||||
// the wordmark carry the brand (#523 follow-up 2).
|
||||
if (!logoLoadError) setLogoLoadError(true);
|
||||
else setFallbackLoadError(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showText && (
|
||||
<span className={`${wordmarkVisibilityClass} text-xl sm:text-2xl truncate`} style={{ fontFamily: 'Poppins, sans-serif', fontWeight: 600, color: '#145346' }}>{companyName}</span>
|
||||
|
||||
@@ -101,7 +101,7 @@ const navigation: NavItem[] = [
|
||||
export const AdminSidebar: React.FC<AdminSidebarProps> = ({ isOpen, onClose, collapsed = false, onToggleCollapse }) => {
|
||||
const location = useLocation();
|
||||
const { t } = useTranslation();
|
||||
const { hasPermission } = usePermissions();
|
||||
const { hasPermission, isLoading: permissionsLoading } = usePermissions();
|
||||
const { flags } = useFeatureFlags();
|
||||
// Branding lookup for the "logo_position = sidepanel" mode — when
|
||||
// chosen, the logo replaces the "PicPeak Admin" text in the brand
|
||||
@@ -283,8 +283,18 @@ export const AdminSidebar: React.FC<AdminSidebarProps> = ({ isOpen, onClose, col
|
||||
|
||||
{/* Bottom section - sticky to bottom (only for users with settings.view permission).
|
||||
Hidden on desktop when collapsed since these widgets don't fit in the icon rail;
|
||||
mobile keeps them visible because mobile width is always w-64. */}
|
||||
{hasPermission('settings.view') && (
|
||||
mobile keeps them visible because mobile width is always w-64.
|
||||
|
||||
#523 follow-up 2: render OPTIMISTICALLY while permissions are
|
||||
still hydrating from the auth context (Rekoo-PS's 3.60.3-beta.0
|
||||
screenshot showed the whole bottom block missing on first paint
|
||||
right after a deploy — `hasPermission` returns false during the
|
||||
~hundreds-of-ms hydration window, the widgets vanish entirely,
|
||||
then re-appear). Only HIDE the block when we definitively know
|
||||
the user lacks the permission. VersionInfo + StorageInfo each
|
||||
have their own loading states so admins see "—" / a spinner
|
||||
instead of nothing during the actual data fetch. */}
|
||||
{(permissionsLoading || hasPermission('settings.view')) && (
|
||||
<div className={`flex-shrink-0 ${collapsed ? 'lg:hidden' : ''}`}>
|
||||
{/* Version Info */}
|
||||
<VersionInfo />
|
||||
|
||||
Reference in New Issue
Block a user