open-swe/ui/src/components/AppShell.tsx

78 lines
2.3 KiB
TypeScript
Raw Normal View History

feat: refactor UI into sidebar layout (#1318) * feat(dashboard): refactor UI into Cursor-style sidebar layout Replace the top-bar AppHeader with a left sidebar that holds the four primary sections (My Settings, Cloud Agents, Open SWE Review, Integrations) and the user account menu at the bottom. Admin remains a hidden route reachable from the sidebar only when is_admin. UI: - AppShell + AppSidebar with bottom-anchored avatar/sign-out menu. - My Settings: profile (first/last name) + PR destination preference. - Cloud Agents: model/effort, default repo, base branch, branch prefix, PR toggles (auto-fix CI, create PRs, allow artifacts), Slack notifications. - Open SWE Review: trigger mode, draft reviews, PR summaries, autofix mode + severity threshold, plus the existing per-repo review-style prompts (was /review-styles). - Integrations: GitHub install status + Slack/Linear status rows. - New Switch and Menu primitives. Backend: - Extend ProfileUpdate with first_name, last_name, base_branch, branch_prefix, auto_fix_ci, create_prs, allow_artifacts, slack_notifications, preferred_pr_destination. - Add team_settings module + /team-settings GET (any session) / PUT (admin only) for the reviewer configuration. * fix(dashboard): replace base-ui Menu with plain dropdown, stabilise Selects - base-ui's Menu hit an "Invalid hook call / Cannot read properties of null (reading 'useRef')" crash inside fastComponent under Vite's dep optimisation. The sidebar account menu is the only consumer, so swap it for a useState-driven dropdown with click-outside + Esc handling and drop the unused Menu wrapper. - Initialise the Open SWE Review settings form with the same defaults the server returns instead of `null`, so the Selects don't switch from uncontrolled (`undefined`) to controlled on first data load. * fix(dashboard): address review feedback on admin overwrite + form reset - admin PUT /admin/profiles/{login} only sent model/effort/repo from the admin form. ProfileUpdate's Pydantic defaults then wrote auto_fix_ci / create_prs / slack_notifications etc back onto the target user's profile, clobbering whatever they had configured. Switch to model_dump(exclude_unset=True) and overlay the incoming fields on the user's existing stored profile so only sent fields change. - my-settings.tsx and cloud-agents.tsx initialised local state from profile.data on every change. Because useSaveProfile updates the cached profile on success, toggling any control would overwrite the user's typed-but-unsaved input in another field. Guard the initialisation effect with a ref so it runs once on first load. --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-05-20 14:13:23 -07:00
import type { ReactNode } from "react";
import type { SessionUser } from "@/lib/api";
import { AppSidebar } from "@/components/AppSidebar";
interface AppShellProps {
user: SessionUser;
title: string;
description?: string;
children: ReactNode;
}
export function AppShell({ user, title, description, children }: AppShellProps) {
return (
<div className="flex min-h-svh bg-background text-foreground">
<AppSidebar user={user} />
<main className="flex-1 overflow-y-auto">
<div className="mx-auto max-w-3xl px-8 py-10">
<header className="mb-8">
<h1 className="font-heading text-lg font-medium">{title}</h1>
{description && (
<p className="mt-1 text-xs text-muted-foreground">{description}</p>
)}
</header>
<div className="space-y-8">{children}</div>
</div>
</main>
</div>
);
}
interface SettingsSectionProps {
title: string;
description?: string;
action?: ReactNode;
children: ReactNode;
}
export function SettingsSection({ title, description, action, children }: SettingsSectionProps) {
return (
<section className="space-y-3">
<div className="flex items-start justify-between gap-4">
<div>
<h2 className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
{title}
</h2>
{description && (
<p className="mt-0.5 text-xs text-muted-foreground/80">{description}</p>
)}
</div>
{action}
</div>
<div className="rounded-lg border border-border bg-card">{children}</div>
</section>
);
}
interface SettingsRowProps {
label: string;
description?: string;
control: ReactNode;
htmlFor?: string;
}
export function SettingsRow({ label, description, control, htmlFor }: SettingsRowProps) {
return (
<div className="flex items-center justify-between gap-6 border-b border-border px-4 py-3 last:border-b-0">
<label className="flex flex-col gap-0.5" htmlFor={htmlFor}>
<span className="text-xs font-medium text-foreground">{label}</span>
{description && (
<span className="text-xs text-muted-foreground">{description}</span>
)}
</label>
<div className="shrink-0">{control}</div>
</div>
);
}