open-swe/ui/src/lib/profile.ts

74 lines
1.9 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 { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ApiError, api } from "./api";
import { useSession } from "./session";
import type {Profile, ProfileUpdate} from "./api";
export function useProfile() {
const session = useSession();
return useQuery({
queryKey: ["profile"],
queryFn: api.profile,
enabled: !!session.data,
});
}
export function useOptions() {
const session = useSession();
return useQuery({
queryKey: ["options"],
queryFn: api.options,
enabled: !!session.data,
});
}
export function useRepos() {
const session = useSession();
return useQuery({
queryKey: ["repos"],
queryFn: async () => {
try {
return await api.repos();
} catch (e) {
if (e instanceof ApiError && e.status === 401)
return { installations: [], repositories: [] };
throw e;
}
},
enabled: !!session.data,
});
}
export function useSaveProfile() {
const qc = useQueryClient();
return useMutation({
mutationFn: (body: ProfileUpdate) => api.saveProfile(body),
onSuccess: (saved) => {
qc.setQueryData(["profile"], saved);
},
});
}
/**
* Build a ProfileUpdate body from the current cached profile plus a patch,
* so individual pages can mutate one field without losing values managed by
* other pages.
*/
export function buildProfileUpdate(
current: Profile | undefined,
patch: Partial<ProfileUpdate>,
fallbackModel: string,
fallbackEffort: string,
): ProfileUpdate {
return {
default_model: current?.default_model ?? fallbackModel,
reasoning_effort: current?.reasoning_effort ?? fallbackEffort,
default_repo: current?.default_repo ?? null,
base_branch: current?.base_branch ?? null,
branch_prefix: current?.branch_prefix ?? null,
auto_fix_ci: current?.auto_fix_ci ?? true,
create_prs: current?.create_prs ?? true,
...patch,
};
}