diff --git a/frontend/src/shell/AppShell.session-mgmt.test.tsx b/frontend/src/shell/AppShell.session-mgmt.test.tsx new file mode 100644 index 00000000..3b10eb7f --- /dev/null +++ b/frontend/src/shell/AppShell.session-mgmt.test.tsx @@ -0,0 +1,45 @@ +import { render, screen, waitFor, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { http, HttpResponse } from "msw"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { server } from "../test/msw"; +import { AppShell } from "./AppShell"; + +function wrap() { + const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + return render(); +} + +const LIST = [ + { id: "s1", status: "open", question: "Attiva uno", summary: null, created_at: "2026-01-02T00:00:00Z", updated_at: null, author: null, name: null, group: "Aritmologia", archived: false }, + { id: "s2", status: "finalized", question: "Archiviata due", summary: null, created_at: "2026-01-01T00:00:00Z", updated_at: null, author: null, name: null, group: null, archived: true }, +]; + +beforeEach(() => { + server.use( + http.get("http://localhost:8787/sessions", () => HttpResponse.json(LIST)), + http.get("http://localhost:8787/sessions/:id/documents", () => HttpResponse.json([ + { phase: "—", key: "question", title: "Domanda originale", format: "text", content: "Attiva uno" }, + ])), + http.post("http://localhost:8787/sessions/:id/archive", () => new HttpResponse(null, { status: 204 })), + ); +}); + +test("active list shows group header and hides archived sessions", async () => { + wrap(); + expect(await screen.findByText("Attiva uno")).toBeInTheDocument(); + expect(screen.getByText("Aritmologia")).toBeInTheDocument(); // group header + expect(screen.queryByText("Archiviata due")).not.toBeInTheDocument(); // archived hidden +}); + +test("opening the panel shows the session documents", async () => { + wrap(); + await userEvent.click(await screen.findByText("Attiva uno")); + expect(await screen.findByText("Domanda originale")).toBeInTheDocument(); +}); + +test("Archive toggle reveals archived sessions", async () => { + wrap(); + await userEvent.click(await screen.findByRole("button", { name: /archive/i })); + expect(await screen.findByText("Archiviata due")).toBeInTheDocument(); +}); diff --git a/frontend/src/shell/AppShell.tsx b/frontend/src/shell/AppShell.tsx index 1b7b1eca..61675c9d 100644 --- a/frontend/src/shell/AppShell.tsx +++ b/frontend/src/shell/AppShell.tsx @@ -3,14 +3,21 @@ import { useSessionStore } from "../store/sessionStore"; import { WidgetHost } from "./WidgetHost"; import { Transcript } from "./Transcript"; import { NavSessions } from "./NavSessions"; +import { SessionMenu } from "./SessionMenu"; +import { SessionDocumentsPanel } from "./SessionDocumentsPanel"; +import { RenameDialog } from "./RenameDialog"; +import { DeleteConfirmDialog } from "./DeleteConfirmDialog"; import { SteerInput, ComposerFooter } from "./SteerInput"; import { WorkflowBar } from "./WorkflowBar"; import { Button } from "../components/ui/button"; import { Toaster } from "../components/ui/sonner"; -import { closeSession, listSessions, resumeSession } from "../api/sessions"; +import { + closeSession, listSessions, resumeSession, + renameSession, setSessionGroup, archiveSession, unarchiveSession, deleteSession, +} from "../api/sessions"; import type { SessionSummary } from "../api/types"; -import { useQuery } from "@tanstack/react-query"; -import { useRef, useState } from "react"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { useMemo, useRef, useState } from "react"; /** * The page is designed to live INSIDE the Omics Portal chrome (its left sidebar @@ -25,6 +32,55 @@ export function AppShell() { }); const composerRef = useRef(null); + const queryClient = useQueryClient(); + const [panelSession, setPanelSession] = useState(null); + const [showArchive, setShowArchive] = useState(false); + const [renameTarget, setRenameTarget] = useState(null); + const [deleteTarget, setDeleteTarget] = useState(null); + + const groups = useMemo( + () => [...new Set(sessions.map((s) => s.group).filter((g): g is string => !!g))].sort(), + [sessions], + ); + const activeList = sessions.filter((s) => !s.archived); + const archivedList = sessions.filter((s) => s.archived); + const refresh = () => queryClient.invalidateQueries({ queryKey: ["sessions"] }); + + function openPanel(id: string) { + const s = sessions.find((x) => x.id === id); + if (s) setPanelSession(s); + } + async function doResume(id: string) { + await resumeSession(id); + setPanelSession(null); + setActiveSessionId(id); + } + async function move(s: SessionSummary, group: string) { await setSessionGroup(s.id, group); refresh(); } + async function newGroup(s: SessionSummary) { + const name = window.prompt("New group:"); + if (name && name.trim()) { await setSessionGroup(s.id, name.trim()); refresh(); } + } + async function toggleArchive(s: SessionSummary) { + await (s.archived ? unarchiveSession(s.id) : archiveSession(s.id)); + if (panelSession?.id === s.id) setPanelSession(null); + refresh(); + } + + function menuFor(s: SessionSummary) { + return ( + setPanelSession(s)} + onRename={() => setRenameTarget(s)} + onMove={(g) => move(s, g)} + onNewGroup={() => newGroup(s)} + onArchiveToggle={() => toggleArchive(s)} + onDelete={() => setDeleteTarget(s)} + /> + ); + } + // The harness "holds the ball" whenever a session is live and no widget is // waiting on the human; once a widget appears, input is back in the user's court. const pendingWidget = useSessionStore((s) => s.pendingWidget); @@ -51,6 +107,14 @@ export function AppShell() { return (
+ {panelSession && ( + setPanelSession(null)} + onResume={doResume} + /> + )} + {/* Conversation column */}
{activeSessionId && ( @@ -125,16 +189,66 @@ export function AppShell() {
- { await resumeSession(id); setActiveSessionId(id); }} - menuFor={() => null} - /> + {showArchive ? ( + + ) : ( +
+ {groups.map((g) => ( +
+

{g}

+ s.group === g)} + activeSessionId={activeSessionId} + onOpenPanel={openPanel} + menuFor={menuFor} + /> +
+ ))} +
+ {groups.length > 0 && ( +

No group

+ )} + !s.group)} + activeSessionId={activeSessionId} + onOpenPanel={openPanel} + menuFor={menuFor} + /> +
+
+ )} +
+ + {renameTarget && ( + { if (!o) setRenameTarget(null); }} + onSubmit={async (name) => { await renameSession(renameTarget.id, name); setRenameTarget(null); refresh(); }} + /> + )} + {deleteTarget && ( + { if (!o) setDeleteTarget(null); }} + onConfirm={async () => { + await deleteSession(deleteTarget.id); + if (panelSession?.id === deleteTarget.id) setPanelSession(null); + if (activeSessionId === deleteTarget.id) { resetSession(); setActiveSessionId(null); } + setDeleteTarget(null); refresh(); + }} + /> + )}
); }