fix(ui): collapse session lists and scope selection controls to panels
This commit is contained in:
@@ -314,7 +314,11 @@ default, hover, focus, active, disabled, loading, and error behavior where those
|
|||||||
tooltip and accessible description retain the translated exact state. Do not
|
tooltip and accessible description retain the translated exact state. Do not
|
||||||
add a separate readiness text row or change the backend readiness gate.
|
add a separate readiness text row or change the backend readiness gate.
|
||||||
- **Session groups:** one accessible multi-open accordion contains Active sessions
|
- **Session groups:** one accessible multi-open accordion contains Active sessions
|
||||||
(initially open) and Archive (initially closed). Preserve the existing archived
|
and Archive, both initially closed. Below the scope tabs, show only their
|
||||||
|
adjacent section headers, without a redundant Sessions heading. Selection and
|
||||||
|
bulk-delete controls belong inside each panel and only appear for nonempty
|
||||||
|
lists. Select all affects that list only, preserves the other list's selection,
|
||||||
|
and exposes a mixed state for partial selection. Preserve the existing archived
|
||||||
flag as the grouping rule, independent of whether a Pi process is running.
|
flag as the grouping rule, independent of whether a Pi process is running.
|
||||||
Open sections share the rail's remaining height; each list scrolls independently
|
Open sections share the rail's remaining height; each list scrolls independently
|
||||||
with a cap of `min(18rem, 35dvh)`, while its trigger remains outside that scroll
|
with a cap of `min(18rem, 35dvh)`, while its trigger remains outside that scroll
|
||||||
|
|||||||
+6
-3
@@ -47,18 +47,21 @@ successful, current `ready` response; checking, unavailable and other states are
|
|||||||
red, with the state exposed through the tooltip and accessible description.
|
red, with the state exposed through the tooltip and accessible description.
|
||||||
The backend readiness gate is unchanged.
|
The backend readiness gate is unchanged.
|
||||||
|
|
||||||
Active sessions (non-archived) start expanded; Archive starts collapsed. Both
|
Active sessions (non-archived) and Archive both start collapsed. Their adjacent
|
||||||
|
headers are the only visible content below the scope tabs: no Sessions heading
|
||||||
|
or external selection toolbar. Each nonempty panel owns its Select all and
|
||||||
|
bulk-delete controls, scoped to that list and preserving the other's selection. Both
|
||||||
accordion sections can remain open and share the remaining sidebar height,
|
accordion sections can remain open and share the remaining sidebar height,
|
||||||
with independently scrolling content capped at `min(18rem, 35dvh)`. The mobile
|
with independently scrolling content capped at `min(18rem, 35dvh)`. The mobile
|
||||||
navigation dialog also provides a bounded height. Keyboard controls and labels
|
navigation dialog also provides a bounded height. Keyboard controls and labels
|
||||||
are retained. See `DESIGN.md` and `docs/guida-utente.md` for the UI contract.
|
are retained. See `DESIGN.md` and `docs/guida-utente.md` for the UI contract.
|
||||||
|
|
||||||
Verification: 763 frontend unit tests, 20 browser scenarios (including 80 mocked
|
Verification: 766 frontend unit tests, 20 browser scenarios (including 80 mocked
|
||||||
sessions at 390/1280px), TypeScript and the production frontend build passed.
|
sessions at 390/1280px), TypeScript and the production frontend build passed.
|
||||||
Only the Mac frontend was recreated; it is healthy at `127.0.0.1:8080`.
|
Only the Mac frontend was recreated; it is healthy at `127.0.0.1:8080`.
|
||||||
Core, catalog, Qdrant and embedding containers were not changed. The prior
|
Core, catalog, Qdrant and embedding containers were not changed. The prior
|
||||||
frontend image is retained as
|
frontend image is retained as
|
||||||
`thothii-frontend:before-navigation-accordions-20260913` for rollback.
|
`thothii-frontend:before-scoped-session-selection-20260913` for rollback.
|
||||||
|
|
||||||
### Agreed Omics delivery route — 2026-09-13
|
### Agreed Omics delivery route — 2026-09-13
|
||||||
|
|
||||||
|
|||||||
@@ -63,8 +63,11 @@ In Administration, the dot beside **Workspace** is green when readiness is
|
|||||||
confirmed and red otherwise. Hover the button for the exact state; assistive
|
confirmed and red otherwise. Hover the button for the exact state; assistive
|
||||||
technology receives the same description. Select Workspace to inspect preparation.
|
technology receives the same description. Select Workspace to inspect preparation.
|
||||||
|
|
||||||
The session sidebar has two accordion sections: **Active sessions** starts open,
|
The session sidebar has two accordion sections: **Active sessions** and
|
||||||
**Archive** starts closed. Both can be open together; long lists scroll inside
|
**Archive**, both initially closed. Only their headers appear below the scope tabs.
|
||||||
|
Inside each nonempty list, **Select all** selects only that list; its delete action
|
||||||
|
also applies only to the selected sessions in that list. The other list's selection
|
||||||
|
is preserved. Both sections can be open together; long lists scroll inside
|
||||||
their own panels. Here active means not archived, not necessarily a running model
|
their own panels. Here active means not archived, not necessarily a running model
|
||||||
process. Existing groups and session actions remain inside those sections.
|
process. Existing groups and session actions remain inside those sections.
|
||||||
|
|
||||||
|
|||||||
@@ -125,11 +125,19 @@ for (const width of [390, 1280]) {
|
|||||||
const green = await workspaceButton.getByRole("img").evaluate(el => getComputedStyle(el).backgroundColor);
|
const green = await workspaceButton.getByRole("img").evaluate(el => getComputedStyle(el).backgroundColor);
|
||||||
const active = page.getByRole("button", { name: "Active sessions", exact: true });
|
const active = page.getByRole("button", { name: "Active sessions", exact: true });
|
||||||
const archive = page.getByRole("button", { name: "Archive (40)", exact: true });
|
const archive = page.getByRole("button", { name: "Archive (40)", exact: true });
|
||||||
await expect(active).toHaveAttribute("aria-expanded", "true");
|
await expect(active).toHaveAttribute("aria-expanded", "false");
|
||||||
await expect(archive).toHaveAttribute("aria-expanded", "false");
|
await expect(archive).toHaveAttribute("aria-expanded", "false");
|
||||||
|
await expect(page.getByRole("checkbox", { name: "Select all" })).toHaveCount(0);
|
||||||
|
await expect(page.getByText("Sessions", { exact: true })).toHaveCount(0);
|
||||||
|
const activeBox = await active.boundingBox();
|
||||||
|
const archiveBox = await archive.boundingBox();
|
||||||
|
expect(archiveBox!.y - activeBox!.y - activeBox!.height).toBeLessThan(8);
|
||||||
|
await page.screenshot({ path: testInfo.outputPath("session-accordions-closed.png"), animations: "disabled" });
|
||||||
|
await active.click();
|
||||||
await archive.click();
|
await archive.click();
|
||||||
for (const name of ["Active sessions", "Archive (40)"]) {
|
for (const name of ["Active sessions", "Archive (40)"]) {
|
||||||
const panel = page.getByRole("region", { name, exact: true });
|
const panel = page.getByRole("region", { name, exact: true });
|
||||||
|
await expect(panel.getByRole("checkbox", { name: "Select all" })).toBeVisible();
|
||||||
await expect(panel).toHaveCSS("overflow-y", "auto");
|
await expect(panel).toHaveCSS("overflow-y", "auto");
|
||||||
const dimensions = await panel.evaluate(el => ({ height: el.getBoundingClientRect().height, scroll: el.scrollHeight, client: el.clientHeight }));
|
const dimensions = await panel.evaluate(el => ({ height: el.getBoundingClientRect().height, scroll: el.scrollHeight, client: el.clientHeight }));
|
||||||
expect(dimensions.height).toBeLessThanOrEqual(288);
|
expect(dimensions.height).toBeLessThanOrEqual(288);
|
||||||
|
|||||||
@@ -222,6 +222,7 @@ test("Resume binds the pinned workspace immediately and leaves the global model
|
|||||||
);
|
);
|
||||||
renderShell();
|
renderShell();
|
||||||
await screen.findByRole("textbox", { name: /new question/i });
|
await screen.findByRole("textbox", { name: /new question/i });
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: "Active sessions" }));
|
||||||
await userEvent.click(await screen.findByTestId("session-item-in-beta"));
|
await userEvent.click(await screen.findByTestId("session-item-in-beta"));
|
||||||
await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1));
|
await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1));
|
||||||
expect(workspacePreferences.load()).toMatchObject({ workspaceId: "beta", provider: "test", model: "test" });
|
expect(workspacePreferences.load()).toMatchObject({ workspaceId: "beta", provider: "test", model: "test" });
|
||||||
|
|||||||
@@ -320,6 +320,7 @@ describe("authenticated shell permissions", () => {
|
|||||||
}
|
}
|
||||||
renderUserShell();
|
renderUserShell();
|
||||||
|
|
||||||
|
await userEvent.click(await screen.findByRole("button", { name: "Active sessions" }));
|
||||||
await userEvent.click(await screen.findByTestId("session-item-panel-a"));
|
await userEvent.click(await screen.findByTestId("session-item-panel-a"));
|
||||||
expect(await screen.findByText("A-private-document")).toBeInTheDocument();
|
expect(await screen.findByText("A-private-document")).toBeInTheDocument();
|
||||||
act(() => useSessionStore.getState().applyEvent({ type: "text_delta", text: "A-private-transcript" }));
|
act(() => useSessionStore.getState().applyEvent({ type: "text_delta", text: "A-private-transcript" }));
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ test("keeps a live core session connected while returning from database manageme
|
|||||||
renderShell();
|
renderShell();
|
||||||
await expandAdministration();
|
await expandAdministration();
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: "Active sessions" }));
|
||||||
const session = await screen.findByTestId("session-item-s1");
|
const session = await screen.findByTestId("session-item-s1");
|
||||||
await userEvent.click(session);
|
await userEvent.click(session);
|
||||||
await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1));
|
await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1));
|
||||||
|
|||||||
@@ -20,10 +20,13 @@ const adminUser: AuthenticatedUser = {
|
|||||||
permissions: ["session.use", "session.read_all", "workspace.manage", "workspace.secrets.manage", "database.manage", "pi.manage"] as const, isAdmin: true,
|
permissions: ["session.use", "session.read_all", "workspace.manage", "workspace.secrets.manage", "database.manage", "pi.manage"] as const, isAdmin: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
function wrap(user: AuthenticatedUser = regularUser) {
|
function wrap(user: AuthenticatedUser = regularUser, openActive = true) {
|
||||||
if (user !== regularUser) setAuthState(user);
|
if (user !== regularUser) setAuthState(user);
|
||||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||||
return render(<QueryClientProvider client={client}><AppShell canLogout={false} /></QueryClientProvider>);
|
const view = render(<QueryClientProvider client={client}><AppShell canLogout={false} /></QueryClientProvider>);
|
||||||
|
// Session-operation tests explicitly open the list before using its rows.
|
||||||
|
if (openActive) fireEvent.click(screen.getByRole("button", { name: "Active sessions" }));
|
||||||
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LIST = [
|
const LIST = [
|
||||||
@@ -424,12 +427,17 @@ test("Archive accordion expands to reveal archived sessions", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("session accordion exposes labelled panels and supports independent keyboard toggles", async () => {
|
test("session accordion exposes labelled panels and supports independent keyboard toggles", async () => {
|
||||||
wrap();
|
wrap(regularUser, false);
|
||||||
await screen.findByText("Attiva uno");
|
await screen.findByRole("button", { name: "Archive (1)" });
|
||||||
const active = screen.getByRole("button", { name: "Active sessions" });
|
const active = screen.getByRole("button", { name: "Active sessions" });
|
||||||
const archive = screen.getByRole("button", { name: "Archive (1)" });
|
const archive = screen.getByRole("button", { name: "Archive (1)" });
|
||||||
expect(active).toHaveAttribute("aria-expanded", "true");
|
expect(active).toHaveAttribute("aria-expanded", "false");
|
||||||
expect(archive).toHaveAttribute("aria-expanded", "false");
|
expect(archive).toHaveAttribute("aria-expanded", "false");
|
||||||
|
expect(screen.queryByText("Sessions", { exact: true })).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole("checkbox", { name: "Select all" })).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole("region", { name: "Active sessions" })).not.toBeInTheDocument();
|
||||||
|
active.focus();
|
||||||
|
await userEvent.keyboard("{Enter}");
|
||||||
expect(screen.getByRole("region", { name: "Active sessions" })).toHaveClass("thot-session-accordion__panel");
|
expect(screen.getByRole("region", { name: "Active sessions" })).toHaveClass("thot-session-accordion__panel");
|
||||||
archive.focus();
|
archive.focus();
|
||||||
await userEvent.keyboard("{Enter}");
|
await userEvent.keyboard("{Enter}");
|
||||||
@@ -443,6 +451,44 @@ test("session accordion exposes labelled panels and supports independent keyboar
|
|||||||
expect(screen.getByText("Archiviata due")).toBeInTheDocument();
|
expect(screen.getByText("Archiviata due")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("each list selects only its own sessions and preserves selection in the other list", async () => {
|
||||||
|
server.use(http.get("/api/sessions", () => HttpResponse.json([
|
||||||
|
...LIST, { ...LIST[0], id: "s3", question: "Attiva tre" },
|
||||||
|
])));
|
||||||
|
wrap();
|
||||||
|
await screen.findByText("Attiva tre");
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: "Archive (1)" }));
|
||||||
|
const active = within(screen.getByRole("region", { name: "Active sessions" }));
|
||||||
|
const archive = within(screen.getByRole("region", { name: "Archive (1)" }));
|
||||||
|
const activeAll = active.getByRole("checkbox", { name: "Select all" });
|
||||||
|
const archiveAll = archive.getByRole("checkbox", { name: "Select all" });
|
||||||
|
await userEvent.click(active.getByRole("checkbox", { name: "Select Attiva uno" }));
|
||||||
|
expect(activeAll).toBePartiallyChecked();
|
||||||
|
await userEvent.click(activeAll);
|
||||||
|
expect(activeAll).toBeChecked();
|
||||||
|
expect(active.getByRole("checkbox", { name: "Select Attiva tre" })).toBeChecked();
|
||||||
|
expect(archiveAll).not.toBeChecked();
|
||||||
|
await userEvent.click(archiveAll);
|
||||||
|
await userEvent.click(activeAll);
|
||||||
|
expect(activeAll).not.toBeChecked();
|
||||||
|
expect(active.queryByRole("button", { name: /Delete.*selected sessions/ })).not.toBeInTheDocument();
|
||||||
|
expect(archive.getByRole("checkbox", { name: "Select Archiviata due" })).toBeChecked();
|
||||||
|
expect(archive.getByRole("button", { name: "Delete 1 selected sessions" })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([false, true])("empty accordion list has no selection controls (archived=%s)", async (archived) => {
|
||||||
|
server.use(http.get("/api/sessions", () => HttpResponse.json([
|
||||||
|
{ ...LIST[0], archived, group: null },
|
||||||
|
])));
|
||||||
|
wrap(regularUser, false);
|
||||||
|
await screen.findByRole("button", { name: `Archive (${archived ? 1 : 0})` });
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: "Active sessions" }));
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: /^Archive/ }));
|
||||||
|
const emptyPanel = screen.getByRole("region", { name: archived ? "Active sessions" : "Archive (0)" });
|
||||||
|
expect(within(emptyPanel).queryByRole("checkbox")).not.toBeInTheDocument();
|
||||||
|
expect(screen.getAllByRole("checkbox", { name: "Select all" })).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
test("Resume from the panel activates the session and closes the panel", async () => {
|
test("Resume from the panel activates the session and closes the panel", async () => {
|
||||||
let resumed: string | null = null;
|
let resumed: string | null = null;
|
||||||
useSessionStore.setState({
|
useSessionStore.setState({
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ test("a held stop for s1 cannot reset the newer active s2 session", async () =>
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
renderShell();
|
renderShell();
|
||||||
|
await userEvent.click(await screen.findByRole("button", { name: "Active sessions" }));
|
||||||
await userEvent.click(await screen.findByTestId("session-item-s1"));
|
await userEvent.click(await screen.findByTestId("session-item-s1"));
|
||||||
await waitFor(() => expect(FakeEventSource.instances.at(-1)?.url).toContain("/sessions/s1/events"));
|
await waitFor(() => expect(FakeEventSource.instances.at(-1)?.url).toContain("/sessions/s1/events"));
|
||||||
await userEvent.click(screen.getByRole("button", { name: /stop and save session/i }));
|
await userEvent.click(screen.getByRole("button", { name: /stop and save session/i }));
|
||||||
@@ -97,6 +98,7 @@ test("a held new-session completion cannot replace the newer active s2 target",
|
|||||||
await userEvent.click(screen.getByRole("button", { name: /send/i }));
|
await userEvent.click(screen.getByRole("button", { name: /send/i }));
|
||||||
await createStarted.promise;
|
await createStarted.promise;
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: "Active sessions" }));
|
||||||
await userEvent.click(screen.getByTestId("session-item-s2"));
|
await userEvent.click(screen.getByTestId("session-item-s2"));
|
||||||
await waitFor(() => expect(FakeEventSource.instances.at(-1)?.url).toContain("/sessions/s2/events"));
|
await waitFor(() => expect(FakeEventSource.instances.at(-1)?.url).toContain("/sessions/s2/events"));
|
||||||
act(() => useSessionStore.setState({ currentPhase: "F2" }));
|
act(() => useSessionStore.setState({ currentPhase: "F2" }));
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ export function AppShell({ canLogout }: AppShellProps) {
|
|||||||
const contextLocked = interactionBusy || creatingSession || administrationBusy
|
const contextLocked = interactionBusy || creatingSession || administrationBusy
|
||||||
|| preprocessingQuery.data?.state === "running";
|
|| preprocessingQuery.data?.state === "running";
|
||||||
const [adminNavigationValue, setAdminNavigationValue] = useState<string[]>([]);
|
const [adminNavigationValue, setAdminNavigationValue] = useState<string[]>([]);
|
||||||
const [activeOpen, setActiveOpen] = useState(true);
|
const [activeOpen, setActiveOpen] = useState(false);
|
||||||
const [archiveOpen, setArchiveOpen] = useState(false);
|
const [archiveOpen, setArchiveOpen] = useState(false);
|
||||||
const [renameTarget, setRenameTarget] = useState<SessionSummary | null>(null);
|
const [renameTarget, setRenameTarget] = useState<SessionSummary | null>(null);
|
||||||
const [deleteTargets, setDeleteTargets] = useState<SessionSummary[]>([]);
|
const [deleteTargets, setDeleteTargets] = useState<SessionSummary[]>([]);
|
||||||
@@ -326,8 +326,39 @@ export function AppShell({ canLogout }: AppShellProps) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAllSessions(selected: boolean) {
|
function sessionSelectionControls(list: SessionSummary[]) {
|
||||||
setSelectedSessionIds(selected ? new Set(sessions.map((session) => session.id)) : new Set());
|
if (list.length === 0) return null;
|
||||||
|
const selected = list.filter((session) => selectedSessionIds.has(session.id));
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between gap-2 px-2 pb-2">
|
||||||
|
<label className="flex cursor-pointer items-center gap-2 text-xs font-medium text-muted-foreground hover:text-foreground">
|
||||||
|
<Checkbox
|
||||||
|
checked={selected.length === list.length}
|
||||||
|
indeterminate={selected.length > 0 && selected.length < list.length}
|
||||||
|
onCheckedChange={(checked) => setSelectedSessionIds((current) => {
|
||||||
|
const next = new Set(current);
|
||||||
|
for (const session of list) {
|
||||||
|
if (checked) next.add(session.id);
|
||||||
|
else next.delete(session.id);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<span>{t("Select all")}</span>
|
||||||
|
</label>
|
||||||
|
{selected.length > 0 && (
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="xs"
|
||||||
|
aria-label={t("Delete {count} selected sessions", { count: selected.length })}
|
||||||
|
onClick={() => requestDelete(selected)}
|
||||||
|
>
|
||||||
|
<Trash2 />
|
||||||
|
{t("Delete ({count})", { count: selected.length })}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSessionScopeTabKeyDown(
|
function handleSessionScopeTabKeyDown(
|
||||||
@@ -1124,42 +1155,11 @@ export function AppShell({ canLogout }: AppShellProps) {
|
|||||||
className="flex min-h-0 flex-1 flex-col"
|
className="flex min-h-0 flex-1 flex-col"
|
||||||
>
|
>
|
||||||
{canReadAllSessions && showingAllSessions && (
|
{canReadAllSessions && showingAllSessions && (
|
||||||
<p className="mx-4 mb-2 mt-2 text-xs font-medium text-muted-foreground">
|
<p className="sr-only">
|
||||||
{t("Administrator view: all sessions")}
|
{t("Administrator view: all sessions")}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* L1 — rail title */}
|
|
||||||
<div className="px-4 pb-1.5 pt-1">
|
|
||||||
<span className="text-sm font-semibold text-foreground">
|
|
||||||
{t("Sessions")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between px-4 pb-2">
|
|
||||||
<label className="flex cursor-pointer items-center gap-2 text-xs font-medium text-muted-foreground hover:text-foreground">
|
|
||||||
<Checkbox
|
|
||||||
checked={allSessionsSelected}
|
|
||||||
aria-label={t("Select all sessions")}
|
|
||||||
disabled={sessions.length === 0}
|
|
||||||
onCheckedChange={(selected) => toggleAllSessions(selected === true)}
|
|
||||||
/>
|
|
||||||
<span>{t("Select all")}</span>
|
|
||||||
</label>
|
|
||||||
{selectedSessions.length > 0 && (
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
size="xs"
|
|
||||||
aria-label={t("Delete {count} selected sessions", { count: selectedSessions.length })}
|
|
||||||
onClick={() => {
|
|
||||||
requestDelete(selectedSessions);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trash2 />
|
|
||||||
{t("Delete ({count})", { count: selectedSessions.length })}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Accordion.Root multiple
|
<Accordion.Root multiple
|
||||||
value={[...(activeOpen ? ["active"] : []), ...(archiveOpen ? ["archive"] : [])]}
|
value={[...(activeOpen ? ["active"] : []), ...(archiveOpen ? ["archive"] : [])]}
|
||||||
onValueChange={value => { setActiveOpen(value.includes("active")); setArchiveOpen(value.includes("archive")); }}
|
onValueChange={value => { setActiveOpen(value.includes("active")); setArchiveOpen(value.includes("archive")); }}
|
||||||
@@ -1172,6 +1172,7 @@ export function AppShell({ canLogout }: AppShellProps) {
|
|||||||
</Accordion.Trigger>
|
</Accordion.Trigger>
|
||||||
</Accordion.Header>
|
</Accordion.Header>
|
||||||
<Accordion.Panel className="thot-session-accordion__panel" tabIndex={0}>
|
<Accordion.Panel className="thot-session-accordion__panel" tabIndex={0}>
|
||||||
|
{sessionSelectionControls(activeList)}
|
||||||
<div className="flex flex-col gap-3 pb-2">
|
<div className="flex flex-col gap-3 pb-2">
|
||||||
{groups.map((g) => (
|
{groups.map((g) => (
|
||||||
<div key={g}>
|
<div key={g}>
|
||||||
@@ -1233,6 +1234,7 @@ export function AppShell({ canLogout }: AppShellProps) {
|
|||||||
</Accordion.Trigger>
|
</Accordion.Trigger>
|
||||||
</Accordion.Header>
|
</Accordion.Header>
|
||||||
<Accordion.Panel className="thot-session-accordion__panel" tabIndex={0}>
|
<Accordion.Panel className="thot-session-accordion__panel" tabIndex={0}>
|
||||||
|
{sessionSelectionControls(archivedList)}
|
||||||
<NavSessions
|
<NavSessions
|
||||||
sessions={archivedList}
|
sessions={archivedList}
|
||||||
activeSessionId={activeSessionId}
|
activeSessionId={activeSessionId}
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ test("reload restores another user's selected session from the authorized All se
|
|||||||
);
|
);
|
||||||
const first = render(<App />);
|
const first = render(<App />);
|
||||||
await userEvent.click(await screen.findByRole("tab", { name: "All sessions" }));
|
await userEvent.click(await screen.findByRole("tab", { name: "All sessions" }));
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: "Active sessions" }));
|
||||||
await userEvent.click(await screen.findByText("Another analyst's work"));
|
await userEvent.click(await screen.findByText("Another analyst's work"));
|
||||||
expect(await screen.findByRole("button", { name: "Resume" })).toBeInTheDocument();
|
expect(await screen.findByRole("button", { name: "Resume" })).toBeInTheDocument();
|
||||||
expect(readRememberedSession(admin)).toBe("foreign");
|
expect(readRememberedSession(admin)).toBe("foreign");
|
||||||
|
|||||||
Reference in New Issue
Block a user