Add masquerade banner to PlatformLayout

When masquerading as a platform staff member, the orange banner now
appears at the top allowing the user to stop masquerading.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-21 23:52:08 -05:00
parent 33e07fe64f
commit 961dbf0a96
2 changed files with 59 additions and 1 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState, useRef } from 'react';
import React, { useState, useRef, useEffect } from 'react';
import { Outlet, useLocation } from 'react-router-dom';
import { Moon, Sun, Globe, Menu } from 'lucide-react';
import { User } from '../types';
@@ -8,8 +8,11 @@ import NotificationDropdown from '../components/NotificationDropdown';
import LanguageSelector from '../components/LanguageSelector';
import TicketModal from '../components/TicketModal';
import HelpButton from '../components/HelpButton';
import MasqueradeBanner from '../components/MasqueradeBanner';
import { useTicket } from '../hooks/useTickets';
import { useScrollToTop } from '../hooks/useScrollToTop';
import { useStopMasquerade } from '../hooks/useAuth';
import { MasqueradeStackEntry } from '../api/auth';
interface PlatformLayoutProps {
user: User;
@@ -30,6 +33,34 @@ const PlatformLayout: React.FC<PlatformLayoutProps> = ({ user, darkMode, toggleT
useScrollToTop(mainContentRef);
// Masquerade logic
const [masqueradeStack, setMasqueradeStack] = useState<MasqueradeStackEntry[]>([]);
const stopMasqueradeMutation = useStopMasquerade();
useEffect(() => {
const stackJson = localStorage.getItem('masquerade_stack');
if (stackJson) {
try {
setMasqueradeStack(JSON.parse(stackJson));
} catch (e) {
console.error('Failed to parse masquerade stack data', e);
}
}
}, [user.id]);
const handleStopMasquerade = () => {
stopMasqueradeMutation.mutate();
};
const originalUser = masqueradeStack.length > 0
? {
id: masqueradeStack[0].user_id,
username: masqueradeStack[0].username,
name: masqueradeStack[0].username,
role: masqueradeStack[0].role,
} as User
: null;
// Fetch ticket data when modal is opened from notification
const { data: ticketFromNotification } = useTicket(ticketModalId && ticketModalId !== 'undefined' ? ticketModalId : undefined);
@@ -56,6 +87,16 @@ const PlatformLayout: React.FC<PlatformLayoutProps> = ({ user, darkMode, toggleT
{/* Main Content Area */}
<div className="flex flex-col flex-1 min-w-0 overflow-hidden">
{/* Masquerade Banner */}
{originalUser && (
<MasqueradeBanner
effectiveUser={user}
originalUser={originalUser}
previousUser={null}
onStop={handleStopMasquerade}
/>
)}
{/* Platform Top Bar */}
<header className="flex items-center justify-between h-16 px-4 sm:px-8 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-4">

View File

@@ -92,6 +92,23 @@ vi.mock('../../hooks/useScrollToTop', () => ({
useScrollToTop: vi.fn(),
}));
vi.mock('../../hooks/useAuth', () => ({
useStopMasquerade: () => ({
mutate: vi.fn(),
isPending: false,
}),
}));
vi.mock('../../components/MasqueradeBanner', () => ({
default: ({ effectiveUser, originalUser, onStop }: any) => (
<div data-testid="masquerade-banner">
<span data-testid="masquerade-effective-user">{effectiveUser.name}</span>
<span data-testid="masquerade-original-user">{originalUser.name}</span>
<button onClick={onStop} data-testid="stop-masquerade-btn">Stop</button>
</div>
),
}));
// Mock react-router-dom Outlet
vi.mock('react-router-dom', async () => {
const actual = await vi.importActual('react-router-dom');