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:
@@ -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 { Outlet, useLocation } from 'react-router-dom';
|
||||||
import { Moon, Sun, Globe, Menu } from 'lucide-react';
|
import { Moon, Sun, Globe, Menu } from 'lucide-react';
|
||||||
import { User } from '../types';
|
import { User } from '../types';
|
||||||
@@ -8,8 +8,11 @@ import NotificationDropdown from '../components/NotificationDropdown';
|
|||||||
import LanguageSelector from '../components/LanguageSelector';
|
import LanguageSelector from '../components/LanguageSelector';
|
||||||
import TicketModal from '../components/TicketModal';
|
import TicketModal from '../components/TicketModal';
|
||||||
import HelpButton from '../components/HelpButton';
|
import HelpButton from '../components/HelpButton';
|
||||||
|
import MasqueradeBanner from '../components/MasqueradeBanner';
|
||||||
import { useTicket } from '../hooks/useTickets';
|
import { useTicket } from '../hooks/useTickets';
|
||||||
import { useScrollToTop } from '../hooks/useScrollToTop';
|
import { useScrollToTop } from '../hooks/useScrollToTop';
|
||||||
|
import { useStopMasquerade } from '../hooks/useAuth';
|
||||||
|
import { MasqueradeStackEntry } from '../api/auth';
|
||||||
|
|
||||||
interface PlatformLayoutProps {
|
interface PlatformLayoutProps {
|
||||||
user: User;
|
user: User;
|
||||||
@@ -30,6 +33,34 @@ const PlatformLayout: React.FC<PlatformLayoutProps> = ({ user, darkMode, toggleT
|
|||||||
|
|
||||||
useScrollToTop(mainContentRef);
|
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
|
// Fetch ticket data when modal is opened from notification
|
||||||
const { data: ticketFromNotification } = useTicket(ticketModalId && ticketModalId !== 'undefined' ? ticketModalId : undefined);
|
const { data: ticketFromNotification } = useTicket(ticketModalId && ticketModalId !== 'undefined' ? ticketModalId : undefined);
|
||||||
|
|
||||||
@@ -56,6 +87,16 @@ const PlatformLayout: React.FC<PlatformLayoutProps> = ({ user, darkMode, toggleT
|
|||||||
|
|
||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
<div className="flex flex-col flex-1 min-w-0 overflow-hidden">
|
<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 */}
|
{/* 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">
|
<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">
|
<div className="flex items-center gap-4">
|
||||||
|
|||||||
@@ -92,6 +92,23 @@ vi.mock('../../hooks/useScrollToTop', () => ({
|
|||||||
useScrollToTop: vi.fn(),
|
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
|
// Mock react-router-dom Outlet
|
||||||
vi.mock('react-router-dom', async () => {
|
vi.mock('react-router-dom', async () => {
|
||||||
const actual = await vi.importActual('react-router-dom');
|
const actual = await vi.importActual('react-router-dom');
|
||||||
|
|||||||
Reference in New Issue
Block a user