Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ffeeed91b |
No files matched your search
@@ -1,38 +1,17 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { ComponentProps, ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { Suspense, use, useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Box, Loader, UnstyledButton, useMantineColorScheme } from "@mantine/core";
|
import { UnstyledButton, useMantineColorScheme } from "@mantine/core";
|
||||||
import { useHotkeys } from "@mantine/hooks";
|
import { useHotkeys } from "@mantine/hooks";
|
||||||
import { ErrorBoundary } from "react-error-boundary";
|
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { useSession } from "@homarr/auth/client";
|
import { useSession } from "@homarr/auth/client";
|
||||||
import { hotkeys } from "@homarr/definitions";
|
import { hotkeys } from "@homarr/definitions";
|
||||||
|
|
||||||
|
import { UserAvatarMenu } from "~/components/user-avatar-menu";
|
||||||
import { UpdateIndicator } from "./update";
|
import { UpdateIndicator } from "./update";
|
||||||
|
|
||||||
type UserAvatarMenuModule = typeof import("~/components/user-avatar-menu");
|
|
||||||
type UserAvatarMenuProps = ComponentProps<UserAvatarMenuModule["UserAvatarMenu"]>;
|
|
||||||
|
|
||||||
let userAvatarMenuPromise: Promise<UserAvatarMenuModule> | undefined;
|
|
||||||
const loadUserAvatarMenu = () => {
|
|
||||||
if (userAvatarMenuPromise) return userAvatarMenuPromise;
|
|
||||||
|
|
||||||
const promise = import("~/components/user-avatar-menu");
|
|
||||||
userAvatarMenuPromise = promise;
|
|
||||||
void promise.catch(() => {
|
|
||||||
if (userAvatarMenuPromise === promise) userAvatarMenuPromise = undefined;
|
|
||||||
});
|
|
||||||
return promise;
|
|
||||||
};
|
|
||||||
const preloadUserAvatarMenu = () => void loadUserAvatarMenu().catch(() => undefined);
|
|
||||||
|
|
||||||
const LazyUserAvatarMenu = (props: UserAvatarMenuProps) => {
|
|
||||||
const { UserAvatarMenu } = use(loadUserAvatarMenu());
|
|
||||||
return <UserAvatarMenu {...props} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface UserButtonClientProps {
|
interface UserButtonClientProps {
|
||||||
avatar: ReactNode;
|
avatar: ReactNode;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
@@ -41,8 +20,6 @@ interface UserButtonClientProps {
|
|||||||
|
|
||||||
export const UserButtonClient = ({ avatar, isAdmin, isDockerEnabled }: UserButtonClientProps) => {
|
export const UserButtonClient = ({ avatar, isAdmin, isDockerEnabled }: UserButtonClientProps) => {
|
||||||
const [canCheckForUpdates, setCanCheckForUpdates] = useState(false);
|
const [canCheckForUpdates, setCanCheckForUpdates] = useState(false);
|
||||||
const [isMenuMounted, setIsMenuMounted] = useState(false);
|
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
const { toggleColorScheme } = useMantineColorScheme();
|
const { toggleColorScheme } = useMantineColorScheme();
|
||||||
useHotkeys([[hotkeys.toggleColorScheme, toggleColorScheme]]);
|
useHotkeys([[hotkeys.toggleColorScheme, toggleColorScheme]]);
|
||||||
@@ -66,60 +43,14 @@ export const UserButtonClient = ({ avatar, isAdmin, isDockerEnabled }: UserButto
|
|||||||
staleTime: 60 * 60 * 1_000,
|
staleTime: 60 * 60 * 1_000,
|
||||||
});
|
});
|
||||||
const visibleUpdates = isCurrentSessionAdmin ? availableUpdates : undefined;
|
const visibleUpdates = isCurrentSessionAdmin ? availableUpdates : undefined;
|
||||||
const openMenu = () => {
|
|
||||||
setIsMenuMounted(true);
|
|
||||||
setIsMenuOpen(true);
|
|
||||||
};
|
|
||||||
const renderButton = (onClick: () => void, isLoading = false) => (
|
|
||||||
<UnstyledButton
|
|
||||||
onClick={onClick}
|
|
||||||
onFocus={preloadUserAvatarMenu}
|
|
||||||
onMouseEnter={preloadUserAvatarMenu}
|
|
||||||
onPointerDown={preloadUserAvatarMenu}
|
|
||||||
aria-busy={isLoading || undefined}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
<UpdateIndicator availableUpdates={visibleUpdates} disabled={!isCurrentSessionAdmin}>
|
|
||||||
<Box pos="relative" style={{ display: "inline-flex" }}>
|
|
||||||
<Box opacity={isLoading ? 0.35 : 1}>{avatar}</Box>
|
|
||||||
{isLoading && (
|
|
||||||
<Loader
|
|
||||||
aria-label="Loading user menu"
|
|
||||||
size={18}
|
|
||||||
pos="absolute"
|
|
||||||
top="50%"
|
|
||||||
left="50%"
|
|
||||||
style={{ transform: "translate(-50%, -50%)" }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</UpdateIndicator>
|
|
||||||
</UnstyledButton>
|
|
||||||
);
|
|
||||||
const button = renderButton(openMenu);
|
|
||||||
const loadingButton = renderButton(() => undefined, true);
|
|
||||||
|
|
||||||
if (!isMenuMounted) return button;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary
|
<UserAvatarMenu availableUpdates={visibleUpdates} isDockerEnabled={isDockerEnabled}>
|
||||||
fallbackRender={({ resetErrorBoundary }) =>
|
<UnstyledButton>
|
||||||
renderButton(() => {
|
<UpdateIndicator availableUpdates={visibleUpdates} disabled={!isCurrentSessionAdmin}>
|
||||||
resetErrorBoundary();
|
{avatar}
|
||||||
openMenu();
|
</UpdateIndicator>
|
||||||
})
|
</UnstyledButton>
|
||||||
}
|
</UserAvatarMenu>
|
||||||
>
|
|
||||||
<Suspense fallback={loadingButton}>
|
|
||||||
<LazyUserAvatarMenu
|
|
||||||
availableUpdates={visibleUpdates}
|
|
||||||
isDockerEnabled={isDockerEnabled}
|
|
||||||
opened={isMenuOpen}
|
|
||||||
onOpenChange={setIsMenuOpen}
|
|
||||||
>
|
|
||||||
{button}
|
|
||||||
</LazyUserAvatarMenu>
|
|
||||||
</Suspense>
|
|
||||||
</ErrorBoundary>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -21,17 +21,9 @@ interface UserAvatarMenuProps {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
availableUpdates?: RouterOutputs["updateChecker"]["getAvailableUpdates"];
|
availableUpdates?: RouterOutputs["updateChecker"]["getAvailableUpdates"];
|
||||||
isDockerEnabled?: boolean;
|
isDockerEnabled?: boolean;
|
||||||
opened: boolean;
|
|
||||||
onOpenChange: (opened: boolean) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UserAvatarMenu = ({
|
export const UserAvatarMenu = ({ children, availableUpdates, isDockerEnabled }: UserAvatarMenuProps) => {
|
||||||
children,
|
|
||||||
availableUpdates,
|
|
||||||
isDockerEnabled,
|
|
||||||
opened,
|
|
||||||
onOpenChange,
|
|
||||||
}: UserAvatarMenuProps) => {
|
|
||||||
const t = useScopedI18n("common.userAvatar.menu");
|
const t = useScopedI18n("common.userAvatar.menu");
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
|
|
||||||
@@ -48,7 +40,7 @@ export const UserAvatarMenu = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
// We use keepMounted so we can add event listeners to prevent navigating away without saving the board
|
// We use keepMounted so we can add event listeners to prevent navigating away without saving the board
|
||||||
<Menu width={300} withinPortal keepMounted opened={opened} onChange={onOpenChange}>
|
<Menu width={300} withinPortal keepMounted>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<AvailableUpdatesMenuItem availableUpdates={availableUpdates} />
|
<AvailableUpdatesMenuItem availableUpdates={availableUpdates} />
|
||||||
<Menu.Item component={Link} href="/boards" leftSection={<IconHome size="1rem" />}>
|
<Menu.Item component={Link} href="/boards" leftSection={<IconHome size="1rem" />}>
|
||||||
|
|||||||
Reference in new issue
Block a user