feat: add Share Progress popover to TreeNavigationPage
Replace the single "Copy for Ticket" button with a "Share Progress" popover that offers three actions: Copy Progress Summary (existing PSA export flow), Copy Share Link (auto-creates account-only share if needed), and Manage Share Links (opens ShareSessionModal). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useParams, useNavigate, useLocation } from 'react-router-dom'
|
import { useParams, useNavigate, useLocation } from 'react-router-dom'
|
||||||
import { treesApi } from '@/api/trees'
|
import { treesApi } from '@/api/trees'
|
||||||
import { sessionsApi } from '@/api/sessions'
|
import { sessionsApi } from '@/api/sessions'
|
||||||
@@ -10,9 +10,11 @@ import { cn, safeGetItem, safeSetItem } from '@/lib/utils'
|
|||||||
import { MarkdownContent } from '@/components/ui/MarkdownContent'
|
import { MarkdownContent } from '@/components/ui/MarkdownContent'
|
||||||
import { CustomStepModal } from '@/components/step-library/CustomStepModal'
|
import { CustomStepModal } from '@/components/step-library/CustomStepModal'
|
||||||
import { PostStepActionModal, ContinuationModal, ForkTreeModal, ScratchpadSidebar, SessionOutcomeModal } from '@/components/session'
|
import { PostStepActionModal, ContinuationModal, ForkTreeModal, ScratchpadSidebar, SessionOutcomeModal } from '@/components/session'
|
||||||
import { Plus, CheckCircle, ArrowRight, Clock, Terminal, Clipboard, Check, Copy, HelpCircle } from 'lucide-react'
|
import { Plus, CheckCircle, ArrowRight, Clock, Terminal, Clipboard, Check, Copy, HelpCircle, Link2, ChevronDown, Settings } from 'lucide-react'
|
||||||
import { toast } from '@/lib/toast'
|
import { toast } from '@/lib/toast'
|
||||||
import { Modal } from '@/components/common/Modal'
|
import { Modal } from '@/components/common/Modal'
|
||||||
|
import { ShareSessionModal } from '@/components/session/ShareSessionModal'
|
||||||
|
import { buildSessionShareUrl, getLatestActiveShareForSession } from '@/lib/sessionShare'
|
||||||
|
|
||||||
interface LocationState {
|
interface LocationState {
|
||||||
sessionId?: string
|
sessionId?: string
|
||||||
@@ -48,6 +50,11 @@ export function TreeNavigationPage() {
|
|||||||
const [selectingOption, setSelectingOption] = useState<string | null>(null)
|
const [selectingOption, setSelectingOption] = useState<string | null>(null)
|
||||||
const [copiedForTicket, setCopiedForTicket] = useState(false)
|
const [copiedForTicket, setCopiedForTicket] = useState(false)
|
||||||
const [isCopyingForTicket, setIsCopyingForTicket] = useState(false)
|
const [isCopyingForTicket, setIsCopyingForTicket] = useState(false)
|
||||||
|
const [showSharePopover, setShowSharePopover] = useState(false)
|
||||||
|
const [showShareModal, setShowShareModal] = useState(false)
|
||||||
|
const [copiedShareLink, setCopiedShareLink] = useState(false)
|
||||||
|
const [isCopyingShareLink, setIsCopyingShareLink] = useState(false)
|
||||||
|
const sharePopoverRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const handleCopyCommand = (text: string) => {
|
const handleCopyCommand = (text: string) => {
|
||||||
navigator.clipboard.writeText(text)
|
navigator.clipboard.writeText(text)
|
||||||
@@ -78,6 +85,55 @@ export function TreeNavigationPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleCopyShareLink = async () => {
|
||||||
|
if (!session || isCopyingShareLink) return
|
||||||
|
setIsCopyingShareLink(true)
|
||||||
|
try {
|
||||||
|
const allShares = await sessionsApi.listMyShares()
|
||||||
|
const existingShare = getLatestActiveShareForSession(allShares, session.id)
|
||||||
|
let shareUrl: string
|
||||||
|
if (existingShare) {
|
||||||
|
shareUrl = buildSessionShareUrl(existingShare)
|
||||||
|
} else {
|
||||||
|
const newShare = await sessionsApi.createShare(session.id, { visibility: 'account' })
|
||||||
|
shareUrl = buildSessionShareUrl(newShare)
|
||||||
|
}
|
||||||
|
await navigator.clipboard.writeText(shareUrl)
|
||||||
|
setCopiedShareLink(true)
|
||||||
|
toast.success('Share link copied to clipboard')
|
||||||
|
setTimeout(() => setCopiedShareLink(false), 2000)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Copy share link failed:', err)
|
||||||
|
toast.error('Failed to copy share link')
|
||||||
|
} finally {
|
||||||
|
setIsCopyingShareLink(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close share popover on outside click
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showSharePopover) return
|
||||||
|
const handleMouseDown = (e: MouseEvent) => {
|
||||||
|
if (sharePopoverRef.current && !sharePopoverRef.current.contains(e.target as Node)) {
|
||||||
|
setShowSharePopover(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('mousedown', handleMouseDown)
|
||||||
|
return () => document.removeEventListener('mousedown', handleMouseDown)
|
||||||
|
}, [showSharePopover])
|
||||||
|
|
||||||
|
// Close share popover on Escape key
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showSharePopover) return
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
setShowSharePopover(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
|
return () => document.removeEventListener('keydown', handleKeyDown)
|
||||||
|
}, [showSharePopover])
|
||||||
|
|
||||||
// Session metadata (prefill from Repeat Last Session)
|
// Session metadata (prefill from Repeat Last Session)
|
||||||
const [ticketNumber, setTicketNumber] = useState<string>(locationState?.prefillTicketNumber || '')
|
const [ticketNumber, setTicketNumber] = useState<string>(locationState?.prefillTicketNumber || '')
|
||||||
const [clientName, setClientName] = useState<string>(locationState?.prefillClientName || '')
|
const [clientName, setClientName] = useState<string>(locationState?.prefillClientName || '')
|
||||||
@@ -576,18 +632,70 @@ export function TreeNavigationPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
{/* Share Progress Popover */}
|
||||||
onClick={handleCopyForTicket}
|
<div className="relative" ref={sharePopoverRef}>
|
||||||
disabled={isCopyingForTicket}
|
<button
|
||||||
title="Copy progress notes for ticket"
|
onClick={() => setShowSharePopover(!showSharePopover)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-1.5 rounded-md border border-white/10 px-3 py-1.5 text-xs font-medium text-white/60',
|
'flex items-center gap-1.5 rounded-md border border-white/10 px-3 py-1.5 text-xs font-medium text-white/60',
|
||||||
'hover:bg-white/10 hover:text-white transition-colors disabled:opacity-50'
|
'hover:bg-white/10 hover:text-white transition-colors'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Copy className="h-3.5 w-3.5" />
|
||||||
|
Share Progress
|
||||||
|
<ChevronDown className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
{showSharePopover && (
|
||||||
|
<div className="absolute right-0 mt-1 z-50 glass-card rounded-lg p-1 min-w-[220px]">
|
||||||
|
{/* Copy Progress Summary */}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
handleCopyForTicket()
|
||||||
|
setShowSharePopover(false)
|
||||||
|
}}
|
||||||
|
disabled={isCopyingForTicket}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 rounded-md px-3 py-2 text-sm text-white/70 w-full text-left cursor-pointer transition-colors',
|
||||||
|
'hover:bg-white/10 hover:text-white disabled:opacity-50'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{copiedForTicket ? <Check className="h-4 w-4 text-emerald-400" /> : <Clipboard className="h-4 w-4" />}
|
||||||
|
{copiedForTicket ? 'Copied!' : 'Copy Progress Summary'}
|
||||||
|
</button>
|
||||||
|
{/* Copy Share Link */}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
handleCopyShareLink()
|
||||||
|
setShowSharePopover(false)
|
||||||
|
}}
|
||||||
|
disabled={isCopyingShareLink}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 rounded-md px-3 py-2 text-sm text-white/70 w-full text-left cursor-pointer transition-colors',
|
||||||
|
'hover:bg-white/10 hover:text-white disabled:opacity-50'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{copiedShareLink ? <Check className="h-4 w-4 text-emerald-400" /> : <Link2 className="h-4 w-4" />}
|
||||||
|
{isCopyingShareLink ? 'Loading...' : copiedShareLink ? 'Copied!' : 'Copy Share Link'}
|
||||||
|
</button>
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="border-t border-white/[0.06] my-1" />
|
||||||
|
{/* Manage Share Links */}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowSharePopover(false)
|
||||||
|
setShowShareModal(true)
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 rounded-md px-3 py-2 text-sm text-white/70 w-full text-left cursor-pointer transition-colors',
|
||||||
|
'hover:bg-white/10 hover:text-white'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Settings className="h-4 w-4" />
|
||||||
|
Manage Share Links...
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
>
|
</div>
|
||||||
{copiedForTicket ? <Check className="h-3.5 w-3.5 text-emerald-400" /> : <Copy className="h-3.5 w-3.5" />}
|
|
||||||
{copiedForTicket ? 'Copied!' : 'Copy for Ticket'}
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/sessions')}
|
onClick={() => navigate('/sessions')}
|
||||||
className="rounded-md px-3 py-1.5 text-sm text-white/50 hover:bg-white/[0.06] hover:text-white"
|
className="rounded-md px-3 py-1.5 text-sm text-white/50 hover:bg-white/[0.06] hover:text-white"
|
||||||
@@ -1092,6 +1200,16 @@ export function TreeNavigationPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
{/* Share Session Modal */}
|
||||||
|
{session && (
|
||||||
|
<ShareSessionModal
|
||||||
|
sessionId={session.id}
|
||||||
|
sessionLabel={ticketNumber || tree?.name || 'Session'}
|
||||||
|
isOpen={showShareModal}
|
||||||
|
onClose={() => setShowShareModal(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user