feat(dashboard): add click-outside dismissal for autocomplete dropdown
Closes bd-3ny. Added mousedown listener that dismisses the dropdown when clicking outside both the dropdown and textarea. Uses early return to avoid registering listeners when dropdown is already closed.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { html, useState, useEffect, useCallback, useMemo, useRef } from '../lib/preact.js';
|
||||
import { API_STATE, API_STREAM, API_DISMISS, API_RESPOND, API_CONVERSATION, POLL_MS, fetchWithTimeout } from '../utils/api.js';
|
||||
import { API_STATE, API_STREAM, API_DISMISS, API_DISMISS_DEAD, API_RESPOND, API_CONVERSATION, POLL_MS, fetchWithTimeout } from '../utils/api.js';
|
||||
import { groupSessionsByProject } from '../utils/status.js';
|
||||
import { Sidebar } from './Sidebar.js';
|
||||
import { SessionCard } from './SessionCard.js';
|
||||
@@ -17,6 +17,7 @@ export function App() {
|
||||
const [error, setError] = useState(null);
|
||||
const [selectedProject, setSelectedProject] = useState(null);
|
||||
const [sseConnected, setSseConnected] = useState(false);
|
||||
const [deadSessionsCollapsed, setDeadSessionsCollapsed] = useState(true);
|
||||
|
||||
// Background conversation refresh with error tracking
|
||||
const refreshConversationSilent = useCallback(async (sessionId, projectDir, agent = 'claude') => {
|
||||
@@ -209,6 +210,21 @@ export function App() {
|
||||
}
|
||||
}, [fetchState]);
|
||||
|
||||
// Dismiss all dead sessions
|
||||
const dismissDeadSessions = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch(API_DISMISS_DEAD, { method: 'POST' });
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
fetchState();
|
||||
} else {
|
||||
trackError('dismiss-dead', `Failed to clear completed sessions: ${data.error || 'Unknown error'}`);
|
||||
}
|
||||
} catch (err) {
|
||||
trackError('dismiss-dead', `Error clearing completed sessions: ${err.message}`);
|
||||
}
|
||||
}, [fetchState]);
|
||||
|
||||
// Subscribe to live state updates via SSE
|
||||
useEffect(() => {
|
||||
let eventSource = null;
|
||||
@@ -296,6 +312,22 @@ export function App() {
|
||||
return projectGroups.filter(g => g.projectDir === selectedProject);
|
||||
}, [projectGroups, selectedProject]);
|
||||
|
||||
// Split sessions into active and dead
|
||||
const { activeSessions, deadSessions } = useMemo(() => {
|
||||
const active = [];
|
||||
const dead = [];
|
||||
for (const group of filteredGroups) {
|
||||
for (const session of group.sessions) {
|
||||
if (session.is_dead) {
|
||||
dead.push(session);
|
||||
} else {
|
||||
active.push(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { activeSessions: active, deadSessions: dead };
|
||||
}, [filteredGroups]);
|
||||
|
||||
// Handle card click - open modal and fetch conversation if not cached
|
||||
const handleCardClick = useCallback(async (session) => {
|
||||
modalSessionRef.current = session.session_id;
|
||||
@@ -394,10 +426,10 @@ export function App() {
|
||||
` : filteredGroups.length === 0 ? html`
|
||||
<${EmptyState} />
|
||||
` : html`
|
||||
<!-- Sessions Grid (no project grouping header since sidebar shows selection) -->
|
||||
<div class="flex flex-wrap gap-4">
|
||||
${filteredGroups.flatMap(group =>
|
||||
group.sessions.map(session => html`
|
||||
<!-- Active Sessions Grid -->
|
||||
${activeSessions.length > 0 ? html`
|
||||
<div class="flex flex-wrap gap-4">
|
||||
${activeSessions.map(session => html`
|
||||
<${SessionCard}
|
||||
key=${session.session_id}
|
||||
session=${session}
|
||||
@@ -407,9 +439,64 @@ export function App() {
|
||||
onRespond=${respondToSession}
|
||||
onDismiss=${dismissSession}
|
||||
/>
|
||||
`)
|
||||
)}
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
` : deadSessions.length > 0 ? html`
|
||||
<div class="glass-panel flex items-center justify-center rounded-xl py-12 mb-6">
|
||||
<div class="text-center">
|
||||
<p class="font-display text-lg text-dim">No active sessions</p>
|
||||
<p class="mt-1 font-mono text-micro text-dim/70">All sessions have completed</p>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Completed Sessions (Dead) - Collapsible -->
|
||||
${deadSessions.length > 0 && html`
|
||||
<div class="mt-8">
|
||||
<button
|
||||
onClick=${() => setDeadSessionsCollapsed(!deadSessionsCollapsed)}
|
||||
class="group flex w-full items-center gap-3 rounded-lg border border-selection/50 bg-surface/50 px-4 py-3 text-left transition-colors hover:border-selection hover:bg-surface/80"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4 text-dim transition-transform ${deadSessionsCollapsed ? '' : 'rotate-90'}"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
<span class="font-display text-sm font-medium text-dim">
|
||||
Completed Sessions
|
||||
</span>
|
||||
<span class="rounded-full bg-done/15 px-2 py-0.5 font-mono text-micro tabular-nums text-done/70">
|
||||
${deadSessions.length}
|
||||
</span>
|
||||
<div class="flex-1"></div>
|
||||
<button
|
||||
onClick=${(e) => { e.stopPropagation(); dismissDeadSessions(); }}
|
||||
class="rounded-lg border border-selection/80 bg-bg/40 px-3 py-1.5 font-mono text-micro text-dim transition-colors hover:border-done/40 hover:bg-done/10 hover:text-bright"
|
||||
>
|
||||
Clear All
|
||||
</button>
|
||||
</button>
|
||||
|
||||
${!deadSessionsCollapsed && html`
|
||||
<div class="mt-4 flex flex-wrap gap-4">
|
||||
${deadSessions.map(session => html`
|
||||
<${SessionCard}
|
||||
key=${session.session_id}
|
||||
session=${session}
|
||||
onClick=${handleCardClick}
|
||||
conversation=${conversations[session.session_id]}
|
||||
onFetchConversation=${fetchConversation}
|
||||
onRespond=${respondToSession}
|
||||
onDismiss=${dismissSession}
|
||||
/>
|
||||
`)}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`}
|
||||
`}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user