remove "Loading..." from profile dropdown and show skeleton

This commit is contained in:
Eduard Gert
2026-06-16 12:09:49 +02:00
parent 44156b6256
commit 64c9551a4d
2 changed files with 29 additions and 7 deletions

View File

@@ -4,6 +4,7 @@ import {
useContext,
useEffect,
useMemo,
useRef,
useState,
type ReactNode,
} from "react";
@@ -42,8 +43,13 @@ export const ProfileProvider = ({ children }: { children: ReactNode }) => {
const [activeProfile, setActiveProfile] = useState("");
const [profiles, setProfiles] = useState<Profile[]>([]);
const [loaded, setLoaded] = useState(false);
const retryRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const refresh = useCallback(async () => {
if (retryRef.current) {
clearTimeout(retryRef.current);
retryRef.current = null;
}
try {
const u = await ProfilesSvc.Username();
const [active, list] = await Promise.all([
@@ -53,23 +59,28 @@ export const ProfileProvider = ({ children }: { children: ReactNode }) => {
setUsername(u);
setActiveProfile(active.profileName || "default");
setProfiles(list);
setLoaded(true);
} catch (e) {
// Daemon-down is already surfaced by DaemonUnavailableOverlay; swallow it here.
const msg = e instanceof Error ? e.message : String(e);
if (msg.includes("code = Unavailable")) {
retryRef.current = setTimeout(() => {
void refresh();
}, 1000);
return;
}
setLoaded(true);
await errorDialog({
Title: i18next.t("profile.error.loadTitle"),
Message: formatErrorMessage(e),
});
} finally {
setLoaded(true);
}
}, []);
useEffect(() => {
refresh().catch((err: unknown) => console.error("[ProfileContext] refresh failed", err));
return () => {
if (retryRef.current) clearTimeout(retryRef.current);
};
}, [refresh]);
useEffect(() => {

View File

@@ -19,7 +19,7 @@ const MANAGE_VALUE = "__manage_profiles__";
export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => {
const { t } = useTranslation();
const { activeProfile, profiles, switchProfile } = useProfile();
const { activeProfile, profiles, switchProfile, loaded } = useProfile();
const [open, setOpen] = useState(false);
const [busy, setBusy] = useState(false);
@@ -55,12 +55,15 @@ export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => {
onManageProfiles?.();
};
const displayName = activeProfile || t("profile.selector.loading");
if (!loaded) return <ProfileTriggerSkeleton />;
const hasProfile = !!activeProfile;
const displayName = hasProfile ? activeProfile : t("profile.selector.noProfile");
return (
<Popover.Root open={open} onOpenChange={setOpen}>
<Popover.Trigger asChild className={"wails-no-draggable"}>
<ProfileTriggerButton name={displayName} />
<Popover.Trigger asChild className={"wails-no-draggable"} disabled={!hasProfile}>
<ProfileTriggerButton name={displayName} disabled={!hasProfile} />
</Popover.Trigger>
<Popover.Portal>
<Popover.Content
@@ -134,6 +137,13 @@ export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => {
);
};
const ProfileTriggerSkeleton = () => (
<div className="h-10 flex items-center gap-2 px-3 rounded-lg select-none wails-no-draggable">
<div className="size-4 rounded-full bg-nb-gray-900 animate-pulse shrink-0" />
<div className="h-4 w-24 rounded bg-nb-gray-900 animate-pulse" />
</div>
);
type ProfileTriggerButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
name: string;
};
@@ -149,6 +159,7 @@ const ProfileTriggerButton = forwardRef<HTMLButtonElement, ProfileTriggerButtonP
"h-10 flex items-center gap-2 px-3 rounded-lg outline-none cursor-default select-none wails-no-draggable",
"text-nb-gray-200 hover:bg-nb-gray-900",
"data-[state=open]:bg-nb-gray-900",
"disabled:opacity-50 disabled:hover:bg-transparent",
"transition-colors duration-150 wails-no-draggable",
className,
)}