fix(ui): expand session dialogs and repeat confirmation actions
Publish documentation / publish (push) Successful in 24s
Publish documentation / publish (push) Successful in 24s
This commit is contained in:
@@ -273,7 +273,7 @@ test("administrator confirms before deleting a same-named user's session", async
|
||||
await userEvent.click(screen.getByRole("button", { name: "Delete 1 selected sessions" }));
|
||||
expect(deletes).toBe(0);
|
||||
expect(await screen.findByRole("heading", { name: "Delete permanently" })).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByRole("button", { name: "Delete" }));
|
||||
await userEvent.click(screen.getAllByRole("button", { name: "Delete" })[0]);
|
||||
await waitFor(() => expect(deletes).toBe(1));
|
||||
});
|
||||
|
||||
@@ -1253,7 +1253,7 @@ test("renaming a group reassigns its members via setSessionGroup", async () => {
|
||||
const input = await screen.findByLabelText(/name/i, { selector: "input" });
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, "Cardiologia");
|
||||
await userEvent.click(screen.getByRole("button", { name: /save/i }));
|
||||
await userEvent.click(screen.getAllByRole("button", { name: /save/i })[0]);
|
||||
await waitFor(() => expect(groupSets).toEqual([{ id: "s1", group: "Cardiologia" }]));
|
||||
});
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ test("a held stop for s1 cannot reset the newer active s2 session", async () =>
|
||||
await userEvent.click(await screen.findByTestId("session-item-s1"));
|
||||
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(await screen.findByRole("button", { name: "Stop & save" }));
|
||||
await userEvent.click((await screen.findAllByRole("button", { name: "Stop & save" }))[0]);
|
||||
await closeStarted.promise;
|
||||
|
||||
await userEvent.click(screen.getByTestId("session-item-s2"));
|
||||
|
||||
@@ -117,8 +117,7 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
resizing: sessionResizing,
|
||||
separatorProps: sessionSeparatorProps,
|
||||
} = useSessionPanelResize(containerRef, panelSession !== null);
|
||||
// Publish the app area's horizontal geometry so viewport-fixed dialogs center on
|
||||
// the application area rather than the whole browser window.
|
||||
// Publish the visible app bounds for dialogs, including embedded portal layouts.
|
||||
useEffect(() => {
|
||||
const el = containerRef.current;
|
||||
if (!el) return;
|
||||
@@ -127,16 +126,23 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
const r = el.getBoundingClientRect();
|
||||
doc.style.setProperty("--app-area-left", `${r.left}px`);
|
||||
doc.style.setProperty("--app-area-width", `${r.width}px`);
|
||||
const top = Math.max(0, r.top);
|
||||
doc.style.setProperty("--app-area-top", `${top}px`);
|
||||
doc.style.setProperty("--app-area-height", `${Math.max(0, Math.min(window.innerHeight, r.bottom) - top)}px`);
|
||||
};
|
||||
publish();
|
||||
const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(publish);
|
||||
observer?.observe(el);
|
||||
window.addEventListener("resize", publish);
|
||||
window.addEventListener("scroll", publish, true);
|
||||
return () => {
|
||||
observer?.disconnect();
|
||||
window.removeEventListener("resize", publish);
|
||||
window.removeEventListener("scroll", publish, true);
|
||||
doc.style.removeProperty("--app-area-left");
|
||||
doc.style.removeProperty("--app-area-width");
|
||||
doc.style.removeProperty("--app-area-top");
|
||||
doc.style.removeProperty("--app-area-height");
|
||||
};
|
||||
}, [containerRef]);
|
||||
const panelWidthsStyle = {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { SessionDialogContent } from "../components/SessionDialogContent";
|
||||
import { useId } from "react";
|
||||
import { useI18n } from "../i18n";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "../components/ui/dialog";
|
||||
import { Dialog, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "../components/ui/dialog";
|
||||
import { Button } from "../components/ui/button";
|
||||
|
||||
interface Props {
|
||||
@@ -13,22 +15,26 @@ interface Props {
|
||||
|
||||
export function DeleteConfirmDialog({ open, labels, label, onOpenChange, onConfirm }: Props) {
|
||||
const { t: translate } = useI18n();
|
||||
const cancelId = useId();
|
||||
const targets = labels ?? (label ? [label] : []);
|
||||
const description = targets.length > 1
|
||||
? translate("{count} sessions will be permanently deleted, including all of their documents. This action cannot be undone.", { count: targets.length })
|
||||
: translate('"{name}" will be permanently deleted, including all of its documents. This action cannot be undone.', { name: targets[0] ?? "" });
|
||||
const confirmAction = (<Button type="button" variant="destructive" size="sm" onClick={() => { onConfirm(); onOpenChange(false); }}>{translate("Delete")}</Button>);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<SessionDialogContent initialFocus={() => document.getElementById(cancelId)}>
|
||||
<DialogHeader><DialogTitle>{translate("Delete permanently")}</DialogTitle></DialogHeader>
|
||||
<div>{confirmAction}</div>
|
||||
<DialogDescription>
|
||||
{description}
|
||||
</DialogDescription>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => onOpenChange(false)}>{translate("Cancel")}</Button>
|
||||
<Button type="button" variant="destructive" size="sm" onClick={() => { onConfirm(); onOpenChange(false); }}>{translate("Delete")}</Button>
|
||||
<Button id={cancelId} type="button" variant="outline" size="sm" onClick={() => onOpenChange(false)}>{translate("Cancel")}</Button>
|
||||
{confirmAction}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</SessionDialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,8 +92,8 @@ export function ModelActivityPanel({
|
||||
<div className="flex items-center justify-between gap-2 border-b border-border/60 px-3 py-2">
|
||||
<h2 className="font-heading text-sm font-semibold text-foreground">{translate("Model activity")}</h2>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button size="sm" variant="ghost" aria-label={translate("Workspace management")} onClick={onOpenWorkspaceManager}>
|
||||
<Settings2 className="size-3.5" />{translate("Workspace management")}</Button>
|
||||
<Button size="sm" variant="ghost" aria-label={translate("To Administration")} onClick={onOpenWorkspaceManager}>
|
||||
<Settings2 className="size-3.5" />{translate("To Administration")}</Button>
|
||||
<Button size="icon-sm" variant="ghost" aria-label={translate("Close model activity")} onClick={onClose}>
|
||||
<X className="size-4" />
|
||||
</Button>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { SessionDialogContent } from "../components/SessionDialogContent";
|
||||
import { useI18n } from "../i18n";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "../components/ui/dialog";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Dialog, DialogHeader, DialogTitle, DialogFooter } from "../components/ui/dialog";
|
||||
import { Button } from "../components/ui/button";
|
||||
|
||||
interface Props {
|
||||
@@ -13,6 +14,7 @@ interface Props {
|
||||
|
||||
export function RenameDialog({ open, initial, onOpenChange, onSubmit, title }: Props) {
|
||||
const { t: translate } = useI18n();
|
||||
const nameRef = useRef<HTMLInputElement>(null);
|
||||
const [name, setName] = useState(initial);
|
||||
useEffect(() => { if (open) setName(initial); }, [open, initial]);
|
||||
|
||||
@@ -25,12 +27,14 @@ export function RenameDialog({ open, initial, onOpenChange, onSubmit, title }: P
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<SessionDialogContent initialFocus={nameRef}>
|
||||
<DialogHeader><DialogTitle>{title ?? translate("Rename session")}</DialogTitle></DialogHeader>
|
||||
<form onSubmit={submit} className="flex flex-col gap-3">
|
||||
<form onSubmit={submit} className="flex flex-col gap-4">
|
||||
<div><Button type="submit" size="sm">{translate("Save")}</Button></div>
|
||||
<div>
|
||||
<label className="mb-1.5 block text-xs font-medium text-muted-foreground" htmlFor="rename-name">{translate("Name")}</label>
|
||||
<input
|
||||
ref={nameRef}
|
||||
id="rename-name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
@@ -43,7 +47,7 @@ export function RenameDialog({ open, initial, onOpenChange, onSubmit, title }: P
|
||||
<Button type="submit" size="sm">{translate("Save")}</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</SessionDialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,30 +4,30 @@ import { vi } from "vitest";
|
||||
import { RenameDialog } from "./RenameDialog";
|
||||
import { DeleteConfirmDialog } from "./DeleteConfirmDialog";
|
||||
|
||||
test("RenameDialog submits the edited name", async () => {
|
||||
test.each([0, 1])("RenameDialog submits the edited name (position %s)", async (position) => {
|
||||
const onSubmit = vi.fn();
|
||||
render(<RenameDialog open initial="Vecchio" onOpenChange={vi.fn()} onSubmit={onSubmit} />);
|
||||
const input = screen.getByDisplayValue("Vecchio");
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, "Nuovo");
|
||||
await userEvent.click(screen.getByRole("button", { name: /save/i }));
|
||||
await userEvent.click(screen.getAllByRole("button", { name: /save/i })[position]);
|
||||
expect(onSubmit).toHaveBeenCalledWith("Nuovo");
|
||||
});
|
||||
|
||||
test("DeleteConfirmDialog confirms only on the destructive button", async () => {
|
||||
test.each([0, 1])("DeleteConfirmDialog confirms only on the destructive button (position %s)", async (position) => {
|
||||
const onConfirm = vi.fn();
|
||||
render(<DeleteConfirmDialog open label="Sessione X" onOpenChange={vi.fn()} onConfirm={onConfirm} />);
|
||||
expect(screen.getByText(/Sessione X/)).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByRole("button", { name: /delete/i }));
|
||||
await userEvent.click(screen.getAllByRole("button", { name: /delete/i })[position]);
|
||||
expect(onConfirm).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("RenameDialog does not submit an empty name", async () => {
|
||||
test.each([0, 1])("RenameDialog does not submit an empty name (position %s)", async (position) => {
|
||||
const onSubmit = vi.fn();
|
||||
render(<RenameDialog open initial="Old" onOpenChange={vi.fn()} onSubmit={onSubmit} />);
|
||||
const input = screen.getByDisplayValue("Old");
|
||||
await userEvent.clear(input);
|
||||
await userEvent.click(screen.getByRole("button", { name: /save/i }));
|
||||
await userEvent.click(screen.getAllByRole("button", { name: /save/i })[position]);
|
||||
expect(onSubmit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { SessionDialogContent } from "../components/SessionDialogContent";
|
||||
import { useId } from "react";
|
||||
import { useI18n } from "../i18n";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "../components/ui/dialog";
|
||||
import { Dialog, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "../components/ui/dialog";
|
||||
import { Button } from "../components/ui/button";
|
||||
|
||||
interface Props {
|
||||
@@ -10,17 +12,21 @@ interface Props {
|
||||
|
||||
export function StopConfirmDialog({ open, onOpenChange, onConfirm }: Props) {
|
||||
const { t: translate } = useI18n();
|
||||
const cancelId = useId();
|
||||
const confirmAction = (<Button type="button" variant="destructive" size="sm" onClick={() => { onConfirm(); onOpenChange(false); }}>{translate("Stop & save")}</Button>);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<SessionDialogContent initialFocus={() => document.getElementById(cancelId)}>
|
||||
<DialogHeader><DialogTitle>{translate("Stop the session?")}</DialogTitle></DialogHeader>
|
||||
<div>{confirmAction}</div>
|
||||
<DialogDescription>
|
||||
{translate("The running process will be interrupted. Progress so far is saved, and you can resume the session later.")}</DialogDescription>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => onOpenChange(false)}>{translate("Cancel")}</Button>
|
||||
<Button type="button" variant="destructive" size="sm" onClick={() => { onConfirm(); onOpenChange(false); }}>{translate("Stop & save")}</Button>
|
||||
<Button id={cancelId} type="button" variant="outline" size="sm" onClick={() => onOpenChange(false)}>{translate("Cancel")}</Button>
|
||||
{confirmAction}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</SessionDialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,17 +66,17 @@ test("keeps answer drafts protected during a failed POST and retry, then clears
|
||||
expect(reloadIsBlocked()).toBe(false);
|
||||
await userEvent.type(screen.getByRole("textbox"), "Unsaved answer");
|
||||
expect(reloadIsBlocked()).toBe(true);
|
||||
await userEvent.click(screen.getByRole("button", { name: "Send" }));
|
||||
await userEvent.click(screen.getAllByRole("button", { name: "Send" })[0]);
|
||||
expect(screen.getByRole("status")).toHaveTextContent("Sending response");
|
||||
try {
|
||||
expect(reloadIsBlocked()).toBe(true);
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
await waitFor(() => expect(screen.getByRole("button", { name: "Send" })).toBeEnabled());
|
||||
await waitFor(() => expect(screen.getAllByRole("button", { name: "Send" })[0]).toBeEnabled());
|
||||
expect(screen.getByRole("textbox")).toHaveValue("Unsaved answer");
|
||||
expect(reloadIsBlocked()).toBe(true);
|
||||
await userEvent.click(screen.getByRole("button", { name: "Send" }));
|
||||
await userEvent.click(screen.getAllByRole("button", { name: "Send" })[0]);
|
||||
await waitFor(() => expect(screen.queryByRole("textbox")).not.toBeInTheDocument());
|
||||
expect(requests).toBe(2);
|
||||
expect(reloadIsBlocked()).toBe(false);
|
||||
|
||||
Reference in New Issue
Block a user