fix: resolve admin issue review findings and record verification
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
/** Horizontal tab lists activate and focus the same item when navigating by keyboard. */
|
||||
export function nextTabIndex(key: string, current: number, count: number): number | undefined {
|
||||
if (count === 0) return undefined;
|
||||
switch (key) {
|
||||
case "ArrowRight": return (current + 1) % count;
|
||||
case "ArrowLeft": return (current + count - 1) % count;
|
||||
case "Home": return 0;
|
||||
case "End": return count - 1;
|
||||
default: return undefined;
|
||||
}
|
||||
}
|
||||
@@ -806,7 +806,6 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
<div style={{ display: activeSurface === "database-management" ? "contents" : "none" }}>
|
||||
<DatabaseManagementPage
|
||||
key={selectedWorkspaceId}
|
||||
initialWorkspaceId={selectedWorkspaceId}
|
||||
canManage={canManageDatabase}
|
||||
canManageSecrets={canManageWorkspaceSecrets}
|
||||
onNavigationStateChange={updateDatabaseNavigationState}
|
||||
|
||||
@@ -90,14 +90,12 @@ function renderPage({
|
||||
canManageSecrets = true,
|
||||
onNavigationStateChange,
|
||||
presentation = "legacy",
|
||||
initialWorkspaceId,
|
||||
}: {
|
||||
rows?: CatalogDatabase[] | (() => CatalogDatabase[]);
|
||||
canManage?: boolean;
|
||||
canManageSecrets?: boolean;
|
||||
onNavigationStateChange?: (state: { dirty: boolean; busy: boolean }) => void;
|
||||
presentation?: "legacy" | "fleet";
|
||||
initialWorkspaceId?: string;
|
||||
} = {}) {
|
||||
server.use(http.get("/api/catalog/databases", () => HttpResponse.json(
|
||||
typeof rows === "function" ? rows() : rows,
|
||||
@@ -106,7 +104,6 @@ function renderPage({
|
||||
const view = render(
|
||||
<QueryClientProvider client={client}>
|
||||
<DatabaseManagementPage
|
||||
initialWorkspaceId={initialWorkspaceId}
|
||||
canManage={canManage}
|
||||
canManageSecrets={canManageSecrets}
|
||||
onNavigationStateChange={onNavigationStateChange}
|
||||
@@ -119,7 +116,7 @@ function renderPage({
|
||||
}
|
||||
|
||||
test("fleet entry opens the database list without workspace preparation or an automatic editor", async () => {
|
||||
renderPage({ presentation: "fleet", initialWorkspaceId: "psd-clinical" });
|
||||
renderPage({ presentation: "fleet" });
|
||||
expect(await screen.findByRole("button", { name: "View tables for Policlinico San Donato" })).toBeVisible();
|
||||
expect(screen.getByText("Research laboratory")).toBeVisible();
|
||||
expect(screen.queryByRole("button", { name: "Back to list" })).not.toBeInTheDocument();
|
||||
@@ -127,6 +124,18 @@ test("fleet entry opens the database list without workspace preparation or an au
|
||||
expect(screen.queryByRole("link", { name: "Open workspace preprocessing" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test.each(["tables", "relationships"])("fresh entry ignores a previously persisted %s screen", async kind => {
|
||||
vi.stubEnv("MODE", "production");
|
||||
localStorage.setItem("thothii:database-management-screen", JSON.stringify({ kind, workspaceId: "psd-clinical" }));
|
||||
try {
|
||||
renderPage({ presentation: "fleet" });
|
||||
expect(await screen.findByRole("button", { name: "View tables for Policlinico San Donato" })).toBeVisible();
|
||||
expect(screen.queryByRole("button", { name: "Back to databases" })).not.toBeInTheDocument();
|
||||
} finally {
|
||||
vi.unstubAllEnvs();
|
||||
}
|
||||
});
|
||||
|
||||
function makeSyncRun(
|
||||
scope: CatalogSyncRun["scope"],
|
||||
tableIds: string[] = [],
|
||||
|
||||
@@ -92,23 +92,7 @@ const SYNC_STARTED_MESSAGES: Record<CatalogSyncScope, string> = {
|
||||
relationships: "Relationship synchronization started",
|
||||
all: "Full schema synchronization started",
|
||||
};
|
||||
const DATABASE_SCREEN_STORAGE_KEY = "thothii:database-management-screen";
|
||||
|
||||
function readPersistedDatabaseScreen(): DatabaseScreen {
|
||||
if (import.meta.env.MODE === "test") return { kind: "list" };
|
||||
try {
|
||||
const parsed = JSON.parse(window.localStorage.getItem(DATABASE_SCREEN_STORAGE_KEY) ?? "null") as { kind?: unknown; workspaceId?: unknown } | null;
|
||||
if ((parsed?.kind === "tables" || parsed?.kind === "relationships") && typeof parsed.workspaceId === "string") {
|
||||
return { kind: parsed.kind, workspaceId: parsed.workspaceId };
|
||||
}
|
||||
} catch {
|
||||
// Ignore malformed persisted navigation and return to the safe list view.
|
||||
}
|
||||
return { kind: "list" };
|
||||
}
|
||||
|
||||
interface Props {
|
||||
initialWorkspaceId?: string;
|
||||
canManage: boolean;
|
||||
canManageSecrets: boolean;
|
||||
onNavigationStateChange?: (state: DatabaseNavigationState) => void;
|
||||
@@ -214,7 +198,7 @@ export function DatabaseManagementPage({
|
||||
(run) => run.status === "running",
|
||||
);
|
||||
|
||||
const [screen, setScreen] = useState<DatabaseScreen>(readPersistedDatabaseScreen);
|
||||
const [screen, setScreen] = useState<DatabaseScreen>({ kind: "list" });
|
||||
const [interactionPreferences, setInteractionPreferences] = useState(() => workspacePreferences.load());
|
||||
useEffect(() => workspacePreferences.subscribe(setInteractionPreferences), []);
|
||||
const selectedMetadataModel = interactionPreferences.provider && interactionPreferences.model
|
||||
@@ -254,12 +238,6 @@ export function DatabaseManagementPage({
|
||||
suggestions: SensitivityReviewItem[];
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (import.meta.env.MODE === "test") return;
|
||||
const persisted = screen.kind === "tables" || screen.kind === "relationships" ? screen : { kind: "list" };
|
||||
window.localStorage.setItem(DATABASE_SCREEN_STORAGE_KEY, JSON.stringify(persisted));
|
||||
}, [screen]);
|
||||
|
||||
const originRef = useRef<HTMLElement | null>(null);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const formHeadingRef = useRef<HTMLHeadingElement>(null);
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "../api/pi-management";
|
||||
import { listModels } from "../api/models";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { nextTabIndex } from "../components/ui/tab-navigation";
|
||||
import { WorkAreaPanel } from "./WorkAreaPanel";
|
||||
|
||||
type Feedback = { tone: "success" | "error"; message: string } | undefined;
|
||||
@@ -214,18 +215,10 @@ function PiPlatformInstructions({ hostPlatform }: { hostPlatform?: PiPlatform })
|
||||
|
||||
function handleTabKeyDown(event: React.KeyboardEvent<HTMLButtonElement>, platform: PiPlatform) {
|
||||
const index = piPlatforms.findIndex((item) => item.id === platform);
|
||||
const nextPlatform = event.key === "ArrowRight"
|
||||
? piPlatforms[(index + 1) % piPlatforms.length]
|
||||
: event.key === "ArrowLeft"
|
||||
? piPlatforms[(index - 1 + piPlatforms.length) % piPlatforms.length]
|
||||
: event.key === "Home"
|
||||
? piPlatforms[0]
|
||||
: event.key === "End"
|
||||
? piPlatforms[piPlatforms.length - 1]
|
||||
: undefined;
|
||||
if (!nextPlatform) return;
|
||||
const next = nextTabIndex(event.key, index, piPlatforms.length);
|
||||
if (next === undefined) return;
|
||||
event.preventDefault();
|
||||
activateAndFocus(nextPlatform.id);
|
||||
activateAndFocus(piPlatforms[next].id);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
type AuthOperationGuard,
|
||||
} from "../auth/authOperation";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { nextTabIndex } from "../components/ui/tab-navigation";
|
||||
import { WorkAreaPanel } from "./WorkAreaPanel";
|
||||
import { WorkspacePreparation } from "./administration/WorkspacePreparation";
|
||||
|
||||
@@ -556,9 +557,7 @@ export function WorkspaceManager({
|
||||
tabIndex={activeArea === area ? 0 : -1}
|
||||
onClick={() => setActiveArea(area)}
|
||||
onKeyDown={event => {
|
||||
const next = event.key === "ArrowRight" ? (index + 1) % workspaceAreas.length
|
||||
: event.key === "ArrowLeft" ? (index + workspaceAreas.length - 1) % workspaceAreas.length
|
||||
: event.key === "Home" ? 0 : event.key === "End" ? workspaceAreas.length - 1 : undefined;
|
||||
const next = nextTabIndex(event.key, index, workspaceAreas.length);
|
||||
if (next === undefined) return;
|
||||
event.preventDefault();
|
||||
setActiveArea(workspaceAreas[next]);
|
||||
|
||||
Reference in New Issue
Block a user