fix: impersonation, dashboard counter, logo lightbox, submission config, auth logs
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m12s
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m12s
- Fix impersonation by bypassing useSession().update() loading gate with direct session POST - Fix dashboard account counter defaulting to latest round with PASSED projects - Add clickToEnlarge lightbox for project logos on admin detail page - Remove submission eligibility config (all passed projects must upload) - Suppress CredentialsSignin auth errors in production (minified name check) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,71 +3,19 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
type SubmissionConfigProps = {
|
||||
config: Record<string, unknown>
|
||||
onChange: (config: Record<string, unknown>) => void
|
||||
}
|
||||
|
||||
const STATUSES = [
|
||||
{ value: 'PENDING', label: 'Pending', color: 'bg-gray-100 text-gray-700' },
|
||||
{ value: 'IN_PROGRESS', label: 'In Progress', color: 'bg-blue-100 text-blue-700' },
|
||||
{ value: 'PASSED', label: 'Passed', color: 'bg-emerald-100 text-emerald-700' },
|
||||
{ value: 'REJECTED', label: 'Rejected', color: 'bg-red-100 text-red-700' },
|
||||
{ value: 'COMPLETED', label: 'Completed', color: 'bg-purple-100 text-purple-700' },
|
||||
{ value: 'WITHDRAWN', label: 'Withdrawn', color: 'bg-amber-100 text-amber-700' },
|
||||
]
|
||||
|
||||
export function SubmissionConfig({ config, onChange }: SubmissionConfigProps) {
|
||||
const update = (key: string, value: unknown) => {
|
||||
onChange({ ...config, [key]: value })
|
||||
}
|
||||
|
||||
const eligible = (config.eligibleStatuses as string[]) ?? ['PASSED']
|
||||
|
||||
const toggleStatus = (status: string) => {
|
||||
const current = [...eligible]
|
||||
const idx = current.indexOf(status)
|
||||
if (idx >= 0) {
|
||||
current.splice(idx, 1)
|
||||
} else {
|
||||
current.push(status)
|
||||
}
|
||||
update('eligibleStatuses', current)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Submission Eligibility</CardTitle>
|
||||
<CardDescription>
|
||||
Which project states from the previous round are eligible to submit documents in this round
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Eligible Project Statuses</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Projects with these statuses from the previous round can submit
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{STATUSES.map((s) => (
|
||||
<Badge
|
||||
key={s.value}
|
||||
variant={eligible.includes(s.value) ? 'default' : 'outline'}
|
||||
className="cursor-pointer select-none"
|
||||
onClick={() => toggleStatus(s.value)}
|
||||
>
|
||||
{s.label}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Notifications & Locking</CardTitle>
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { Route } from 'next'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
import { directSessionUpdate } from '@/lib/session-update'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -71,7 +72,7 @@ function getRoleHomePath(role: string): string {
|
||||
export function UserActions({ userId, userEmail, userStatus, userRole, userRoles, currentUserRole }: UserActionsProps) {
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||
const [isSending, setIsSending] = useState(false)
|
||||
const { data: session, update } = useSession()
|
||||
const { data: session } = useSession()
|
||||
const router = useRouter()
|
||||
|
||||
const utils = trpc.useUtils()
|
||||
@@ -125,9 +126,12 @@ export function UserActions({ userId, userEmail, userStatus, userRole, userRoles
|
||||
const handleImpersonate = async () => {
|
||||
try {
|
||||
const result = await startImpersonation.mutateAsync({ targetUserId: userId })
|
||||
await update({ impersonate: userId })
|
||||
// Full page navigation to ensure the updated JWT cookie is sent
|
||||
// (router.push can race with cookie propagation)
|
||||
// Direct POST to session endpoint — bypasses useSession().update()'s loading gate
|
||||
const ok = await directSessionUpdate({ impersonate: userId })
|
||||
if (!ok) {
|
||||
toast.error('Failed to update session for impersonation')
|
||||
return
|
||||
}
|
||||
window.location.href = getRoleHomePath(result.targetRole)
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to start impersonation')
|
||||
@@ -279,7 +283,7 @@ export function UserMobileActions({
|
||||
currentUserRole,
|
||||
}: UserMobileActionsProps) {
|
||||
const [isSending, setIsSending] = useState(false)
|
||||
const { data: session, update } = useSession()
|
||||
const { data: session } = useSession()
|
||||
const router = useRouter()
|
||||
const utils = trpc.useUtils()
|
||||
const sendInvitation = trpc.user.sendInvitation.useMutation()
|
||||
@@ -301,7 +305,11 @@ export function UserMobileActions({
|
||||
const handleImpersonateMobile = async () => {
|
||||
try {
|
||||
const result = await startImpersonation.mutateAsync({ targetUserId: userId })
|
||||
await update({ impersonate: userId })
|
||||
const ok = await directSessionUpdate({ impersonate: userId })
|
||||
if (!ok) {
|
||||
toast.error('Failed to update session for impersonation')
|
||||
return
|
||||
}
|
||||
window.location.href = getRoleHomePath(result.targetRole)
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to start impersonation')
|
||||
|
||||
@@ -8,6 +8,7 @@ import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { UserAvatar } from '@/components/shared/user-avatar'
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
import { directSessionUpdate } from '@/lib/session-update'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -85,7 +86,7 @@ export function RoleNav({ navigation, roleName, user, basePath, statusBadge, edi
|
||||
if (isImpersonating) {
|
||||
try {
|
||||
await endImpersonation.mutateAsync()
|
||||
await updateSession({ endImpersonation: true })
|
||||
await directSessionUpdate({ endImpersonation: true })
|
||||
window.location.href = '/admin/members'
|
||||
} catch {
|
||||
// Fallback: just sign out completely
|
||||
|
||||
@@ -12,6 +12,7 @@ type ProjectLogoWithUrlProps = {
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
fallback?: 'icon' | 'initials'
|
||||
className?: string
|
||||
clickToEnlarge?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -23,6 +24,7 @@ export function ProjectLogoWithUrl({
|
||||
size = 'md',
|
||||
fallback = 'icon',
|
||||
className,
|
||||
clickToEnlarge,
|
||||
}: ProjectLogoWithUrlProps) {
|
||||
const { data: logoUrl } = trpc.logo.getUrl.useQuery(
|
||||
{ projectId: project.id },
|
||||
@@ -39,6 +41,7 @@ export function ProjectLogoWithUrl({
|
||||
size={size}
|
||||
fallback={fallback}
|
||||
className={className}
|
||||
clickToEnlarge={clickToEnlarge}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { cn, getInitials } from '@/lib/utils'
|
||||
import { ClipboardList } from 'lucide-react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { VisuallyHidden } from '@radix-ui/react-visually-hidden'
|
||||
|
||||
type ProjectLogoProps = {
|
||||
project: {
|
||||
@@ -13,6 +19,8 @@ type ProjectLogoProps = {
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
fallback?: 'icon' | 'initials'
|
||||
className?: string
|
||||
/** When true, clicking the logo opens a lightbox */
|
||||
clickToEnlarge?: boolean
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
@@ -39,8 +47,10 @@ export function ProjectLogo({
|
||||
size = 'md',
|
||||
fallback = 'icon',
|
||||
className,
|
||||
clickToEnlarge,
|
||||
}: ProjectLogoProps) {
|
||||
const [imageError, setImageError] = useState(false)
|
||||
const [lightboxOpen, setLightboxOpen] = useState(false)
|
||||
const initials = getInitials(project.title)
|
||||
|
||||
// Reset error state when logoUrl changes
|
||||
@@ -49,28 +59,26 @@ export function ProjectLogo({
|
||||
}, [logoUrl])
|
||||
|
||||
const showImage = logoUrl && !imageError
|
||||
const canEnlarge = clickToEnlarge && showImage
|
||||
|
||||
if (showImage) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative overflow-hidden rounded-lg bg-muted',
|
||||
sizeClasses[size],
|
||||
className
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src={logoUrl}
|
||||
alt={`${project.title} logo`}
|
||||
className="h-full w-full object-cover"
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Fallback
|
||||
return (
|
||||
const logoElement = showImage ? (
|
||||
<div
|
||||
className={cn(
|
||||
'relative overflow-hidden rounded-lg bg-muted',
|
||||
sizeClasses[size],
|
||||
canEnlarge && 'cursor-pointer ring-offset-background transition-all hover:ring-2 hover:ring-primary/30',
|
||||
className
|
||||
)}
|
||||
onClick={canEnlarge ? () => setLightboxOpen(true) : undefined}
|
||||
>
|
||||
<img
|
||||
src={logoUrl}
|
||||
alt={`${project.title} logo`}
|
||||
className="h-full w-full object-cover"
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-center rounded-lg bg-muted',
|
||||
@@ -87,4 +95,22 @@ export function ProjectLogo({
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{logoElement}
|
||||
{canEnlarge && (
|
||||
<Dialog open={lightboxOpen} onOpenChange={setLightboxOpen}>
|
||||
<DialogContent className="max-w-lg p-2">
|
||||
<VisuallyHidden><DialogTitle>{project.title} logo</DialogTitle></VisuallyHidden>
|
||||
<img
|
||||
src={logoUrl!}
|
||||
alt={`${project.title} logo`}
|
||||
className="w-full rounded-lg object-contain"
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user