Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35649eaaa5 | ||
|
|
e31dffb42d | ||
|
|
1c089dd906 | ||
|
|
ad0d6e0498 |
No files matched your search
@@ -361,6 +361,7 @@ const LoadedBoardItemContent = ({
|
||||
widgetStateRef={widgetStateRef}
|
||||
widgetRuntimeRef={widgetRuntimeRef}
|
||||
sourceRef={sourceRef}
|
||||
disabled={isAdvanced}
|
||||
>
|
||||
<Box
|
||||
ref={sourceRef}
|
||||
|
||||
@@ -44,6 +44,7 @@ interface WidgetContextMenuProps {
|
||||
widgetStateRef: MutableRefObject<Record<string, unknown> | null>;
|
||||
widgetRuntimeRef: WidgetRuntimeRef;
|
||||
sourceRef: RefObject<HTMLElement | null>;
|
||||
disabled?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
@@ -53,6 +54,7 @@ export const WidgetContextMenu = ({
|
||||
previewDimensions,
|
||||
widgetRuntimeRef,
|
||||
sourceRef,
|
||||
disabled = false,
|
||||
children,
|
||||
}: WidgetContextMenuProps) => {
|
||||
const { data: session } = useSession();
|
||||
@@ -162,6 +164,7 @@ export const WidgetContextMenu = ({
|
||||
}) ?? [];
|
||||
|
||||
const openEditModal = useCallback(() => {
|
||||
setMenuOpened(false);
|
||||
openModal(
|
||||
{
|
||||
kind: item.kind,
|
||||
@@ -230,6 +233,7 @@ export const WidgetContextMenu = ({
|
||||
|
||||
const handleOpenAdvancedFocus = useCallback(() => {
|
||||
if (!sourceRef.current) return;
|
||||
setMenuOpened(false);
|
||||
openAdvancedFocus(item.id, sourceRef.current, {
|
||||
restoreFocusTarget:
|
||||
sourceRef.current.querySelector<HTMLElement>("[data-advanced-focus-trigger]") ?? sourceRef.current,
|
||||
@@ -256,7 +260,7 @@ export const WidgetContextMenu = ({
|
||||
opened={menuOpened}
|
||||
onChange={setMenuOpened}
|
||||
>
|
||||
<Menu.ContextMenu>{children}</Menu.ContextMenu>
|
||||
<Menu.ContextMenu disabled={disabled}>{children}</Menu.ContextMenu>
|
||||
<WidgetContextMenuDropdown opened={menuOpened} onClose={handleCloseMenu} title={getWidgetName(item.kind, t)}>
|
||||
{canOpenAdvancedFocus && (
|
||||
<>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header[data-desktop-visible="false"] {
|
||||
.header[data-visible="false"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -73,39 +73,38 @@
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.userMenu {
|
||||
display: flex;
|
||||
padding-inline: rem(6px);
|
||||
}
|
||||
|
||||
.mobileContent {
|
||||
display: none;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--mantine-spacing-xs);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-inline: var(--mantine-spacing-xs);
|
||||
}
|
||||
|
||||
.mobileIdentity,
|
||||
.mobileActions {
|
||||
.mobileZone {
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.mobileIdentity {
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
.mobileZone::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobileIdentity > :last-child {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
.mobileZone[data-zone="left"] {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.mobileIdentity h2 {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.mobileActions {
|
||||
flex: 0 0 auto;
|
||||
.mobileZone[data-zone="right"] {
|
||||
justify-content: flex-end;
|
||||
max-width: 72vw;
|
||||
}
|
||||
|
||||
.floatingControls {
|
||||
@@ -155,21 +154,12 @@
|
||||
}
|
||||
|
||||
@media (max-width: $mantine-breakpoint-sm) {
|
||||
.header[data-desktop-visible="false"] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.desktopContent {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobileContent {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.floatingControls,
|
||||
.floatingControlsCorner {
|
||||
display: none;
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,15 +3,7 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { AppShellHeader, Avatar, Group, Indicator, Loader, Tooltip, useMantineColorScheme } from "@mantine/core";
|
||||
import {
|
||||
IconBrandDocker,
|
||||
IconHome,
|
||||
IconLayoutDashboard,
|
||||
IconReplace,
|
||||
IconRobot,
|
||||
IconSettings,
|
||||
IconSunMoon,
|
||||
} from "@tabler/icons-react";
|
||||
import { IconBrandDocker, IconHome, IconReplace, IconRobot, IconSettings, IconSunMoon } from "@tabler/icons-react";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
@@ -81,7 +73,10 @@ export const ConfigurableHeader = ({
|
||||
return (
|
||||
<BoardSwitcher>
|
||||
{(boardSwitcher) => {
|
||||
const renderItems = (items: HeaderItem[]) =>
|
||||
const renderItems = (
|
||||
items: HeaderItem[],
|
||||
display: { search: "input" | "icon"; logo: "logo" | "logoAndText" },
|
||||
) =>
|
||||
items.map((item) => (
|
||||
<HeaderItem
|
||||
key={getHeaderItemKey(item)}
|
||||
@@ -95,8 +90,8 @@ export const ConfigurableHeader = ({
|
||||
isAdmin={isAdmin}
|
||||
isDockerEnabled={isDockerEnabled}
|
||||
board={item.type === "board" ? boardsById.get(item.boardId) : undefined}
|
||||
searchDisplay={headerPreferences.searchDisplay}
|
||||
logoDisplay={headerPreferences.logoDisplay}
|
||||
searchDisplay={display.search}
|
||||
logoDisplay={display.logo}
|
||||
boardSwitcher={boardSwitcher}
|
||||
assistant={assistant}
|
||||
openDockerModal={openDockerModal}
|
||||
@@ -106,13 +101,19 @@ export const ConfigurableHeader = ({
|
||||
/>
|
||||
));
|
||||
|
||||
const desktopDisplay = {
|
||||
search: headerPreferences.searchDisplay,
|
||||
logo: headerPreferences.logoDisplay,
|
||||
};
|
||||
const mobileDisplay = { search: "icon", logo: "logo" } as const;
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppShellHeader
|
||||
maw="100vw"
|
||||
zIndex="var(--homarr-z-index-board-header)"
|
||||
className={classes.header}
|
||||
data-desktop-visible={headerPreferences.visible}
|
||||
data-visible={headerPreferences.visible}
|
||||
data-advanced-focus-background
|
||||
data-app-shell-header
|
||||
>
|
||||
@@ -120,37 +121,26 @@ export const ConfigurableHeader = ({
|
||||
<div className={classes.content}>
|
||||
<div className={classes.zone} data-zone="left">
|
||||
{hasNavigation ? <ClientBurger /> : null}
|
||||
{renderItems(headerPreferences.zones.left)}
|
||||
{renderItems(headerPreferences.zones.left, desktopDisplay)}
|
||||
</div>
|
||||
<div className={classes.zone} data-zone="center">
|
||||
{renderItems(headerPreferences.zones.center)}
|
||||
{renderItems(headerPreferences.zones.center, desktopDisplay)}
|
||||
</div>
|
||||
<div className={classes.zone} data-zone="right">
|
||||
{renderItems(headerPreferences.zones.right)}
|
||||
{renderItems(headerPreferences.zones.right, desktopDisplay)}
|
||||
{actions ? <Group className={classes.contextActions}>{actions}</Group> : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.mobileContent}>
|
||||
<Group className={classes.mobileIdentity} gap="xs" wrap="nowrap">
|
||||
<Group className={classes.mobileZone} data-zone="left" gap="xs" wrap="nowrap">
|
||||
{hasNavigation ? <ClientBurger /> : null}
|
||||
<HeaderLogo display="logo" logo={logo} logoWithTitle={logoWithTitle} label={t("items.logo")} />
|
||||
{renderItems(headerPreferences.zones.left, mobileDisplay)}
|
||||
</Group>
|
||||
<Group className={classes.mobileActions} gap="xs" wrap="nowrap">
|
||||
{actions}
|
||||
{boardEditAction}
|
||||
{boardSettingsAction}
|
||||
<TourTarget id="board-search">
|
||||
<MobileSearchButton alwaysVisible />
|
||||
</TourTarget>
|
||||
<TourTarget id="board-user-menu">
|
||||
<UserButtonClient
|
||||
avatar={avatar}
|
||||
isAdmin={isAdmin}
|
||||
isDockerEnabled={isDockerEnabled}
|
||||
boardSwitcher={boardSwitcher}
|
||||
/>
|
||||
</TourTarget>
|
||||
<Group className={classes.mobileZone} data-zone="right" gap="xs" wrap="nowrap">
|
||||
{renderItems(headerPreferences.zones.center, mobileDisplay)}
|
||||
{renderItems(headerPreferences.zones.right, mobileDisplay)}
|
||||
{actions ? <Group className={classes.contextActions}>{actions}</Group> : null}
|
||||
</Group>
|
||||
</div>
|
||||
</AppShellHeader>
|
||||
@@ -298,7 +288,9 @@ const HeaderItem = ({
|
||||
return (
|
||||
<Tooltip label={boardUnavailableLabel}>
|
||||
<HeaderButton aria-label={boardUnavailableLabel} disabled>
|
||||
<IconLayoutDashboard size={20} stroke={1.5} />
|
||||
<Avatar size={22} radius="sm">
|
||||
?
|
||||
</Avatar>
|
||||
</HeaderButton>
|
||||
</Tooltip>
|
||||
);
|
||||
@@ -416,14 +408,19 @@ const HeaderItem = ({
|
||||
|
||||
return (
|
||||
<TourTarget id="board-user-menu">
|
||||
<UserButtonClient
|
||||
avatar={avatar}
|
||||
isAdmin={isAdmin}
|
||||
isDockerEnabled={isDockerEnabled}
|
||||
boardSwitcher={boardSwitcher}
|
||||
/>
|
||||
<div className={classes.userMenu}>
|
||||
<UserButtonClient
|
||||
avatar={avatar}
|
||||
isAdmin={isAdmin}
|
||||
isDockerEnabled={isDockerEnabled}
|
||||
boardSwitcher={boardSwitcher}
|
||||
/>
|
||||
</div>
|
||||
</TourTarget>
|
||||
);
|
||||
};
|
||||
|
||||
const getBoardInitial = (name: string) => Array.from(name.trim())[0]?.toLocaleUpperCase() ?? "?";
|
||||
const getBoardInitial = (name: string) => {
|
||||
const firstCharacter = Array.from(name.trim())[0] ?? "?";
|
||||
return Array.from(firstCharacter.toUpperCase())[0] ?? "?";
|
||||
};
|
||||
@@ -22,7 +22,7 @@ export const ClientShell = ({
|
||||
const collapsed = useAtomValue(navigationCollapsedAtom);
|
||||
const backgroundProps = useOptionalBackgroundProps();
|
||||
const { headerPreferences } = useSettings();
|
||||
const headerHeight = headerPreferences.visible ? appShellHeaderHeight : { base: appShellHeaderHeight, sm: 0 };
|
||||
const headerHeight = headerPreferences.visible ? appShellHeaderHeight : 0;
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Badge, Indicator, Kbd, Loader, Menu, Text } from "@mantine/core";
|
||||
import {
|
||||
IconBrandDocker,
|
||||
IconHome,
|
||||
IconLayoutDashboard,
|
||||
IconLogin,
|
||||
IconLogout,
|
||||
IconReplace,
|
||||
@@ -16,6 +17,8 @@ import {
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { signOut, useSession } from "@homarr/auth/client";
|
||||
import { constructBoardPermissions } from "@homarr/auth/shared";
|
||||
import { useOptionalBoard } from "@homarr/boards/context";
|
||||
import { hotkeys, invariantTechnicalLabels } from "@homarr/definitions";
|
||||
import { useModalAction } from "@homarr/modals";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
@@ -46,6 +49,8 @@ export const UserAvatarMenu = ({ children, availableUpdates, isDockerEnabled, bo
|
||||
const t = useI18n("common.userAvatar.menu");
|
||||
const tBoard = useI18n("board");
|
||||
const session = useSession();
|
||||
const board = useOptionalBoard();
|
||||
const boardPermissions = board && constructBoardPermissions(board, session.data ?? null);
|
||||
|
||||
const { logoutUrl } = useAuthContext();
|
||||
const { openModal: openDockerModal } = useModalAction(DockerQuickAccessModal);
|
||||
@@ -67,6 +72,15 @@ export const UserAvatarMenu = ({ children, availableUpdates, isDockerEnabled, bo
|
||||
<Menu.Item component={Link} href="/boards" leftSection={<IconHome size="1rem" />}>
|
||||
{t("homeBoard")}
|
||||
</Menu.Item>
|
||||
{board && boardPermissions?.hasChangeAccess && (
|
||||
<Menu.Item
|
||||
component={Link}
|
||||
href={`/boards/${encodeURIComponent(board.name)}/settings`}
|
||||
leftSection={<IconLayoutDashboard size="1rem" />}
|
||||
>
|
||||
{tBoard("action.settings")}
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item
|
||||
leftSection={<IconReplace size="1rem" />}
|
||||
rightSection={<Kbd size="xs">{formatHotkeyLabel(boardSwitcher.hotkey, t("modifier"))}</Kbd>}
|
||||
|
||||
@@ -49,8 +49,7 @@ export const BoardCreateForm = ({ onCancel }: BoardCreateFormProps) => {
|
||||
message: tBoard("action.create.notification.success.message", { name: values.name }),
|
||||
});
|
||||
const name = encodeURIComponent(result.name);
|
||||
const layoutId = encodeURIComponent(result.layoutId);
|
||||
router.push(`/boards/${name}?layout=${layoutId}&edit=true&add=true`);
|
||||
router.push(`/boards/${name}`);
|
||||
},
|
||||
onError() {
|
||||
showErrorNotification({
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
interface ModalFormFooterProps {
|
||||
onCancel: () => void;
|
||||
form?: string;
|
||||
submitLabel?: string;
|
||||
cancelLabel?: string;
|
||||
loading?: boolean;
|
||||
@@ -18,24 +19,38 @@ const ActionButtons = ({
|
||||
submitLabel,
|
||||
cancelLabel,
|
||||
loading,
|
||||
}: Pick<ModalFormFooterProps, "onCancel" | "submitLabel" | "cancelLabel" | "loading">) => {
|
||||
const t = useI18n();
|
||||
form,
|
||||
}: Pick<ModalFormFooterProps, "onCancel" | "submitLabel" | "cancelLabel" | "loading" | "form">) => {
|
||||
const t = useI18n("common.action");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={onCancel} variant="subtle" color="gray">
|
||||
{cancelLabel ?? t("common.action.cancel")}
|
||||
{cancelLabel ?? t("cancel")}
|
||||
</Button>
|
||||
<Button type="submit" loading={loading}>
|
||||
{submitLabel ?? t("common.action.saveChanges")}
|
||||
<Button type="submit" form={form} loading={loading}>
|
||||
{submitLabel ?? t("saveChanges")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ModalFormFooter = ({ onCancel, submitLabel, cancelLabel, loading, leftSection }: ModalFormFooterProps) => {
|
||||
export const ModalFormFooter = ({
|
||||
onCancel,
|
||||
form,
|
||||
submitLabel,
|
||||
cancelLabel,
|
||||
loading,
|
||||
leftSection,
|
||||
}: ModalFormFooterProps) => {
|
||||
const actionButtons = (
|
||||
<ActionButtons onCancel={onCancel} submitLabel={submitLabel} cancelLabel={cancelLabel} loading={loading} />
|
||||
<ActionButtons
|
||||
onCancel={onCancel}
|
||||
form={form}
|
||||
submitLabel={submitLabel}
|
||||
cancelLabel={cancelLabel}
|
||||
loading={loading}
|
||||
/>
|
||||
);
|
||||
|
||||
if (leftSection) {
|
||||
|
||||
@@ -16,7 +16,7 @@ export const headerBuiltinItemIds = [
|
||||
"user",
|
||||
] as const;
|
||||
|
||||
export const requiredHeaderBuiltinItemIds = ["boardEdit", "boardSettings", "user"] as const;
|
||||
export const requiredHeaderBuiltinItemIds = ["user"] as const;
|
||||
|
||||
export const headerPreferenceItemIds = headerBuiltinItemIds;
|
||||
export const headerItemIds = headerBuiltinItemIds;
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
margin-inline: calc(var(--mantine-spacing-md) * -1);
|
||||
margin-bottom: calc(var(--mantine-spacing-md) * -1);
|
||||
padding: var(--mantine-spacing-sm) var(--mantine-spacing-md);
|
||||
padding-bottom: max(var(--mantine-spacing-sm), env(safe-area-inset-bottom));
|
||||
border-top: 1px solid var(--mantine-color-default-border);
|
||||
background: color-mix(in srgb, var(--mantine-color-body) 94%, transparent);
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
@@ -36,7 +36,7 @@ import { objectEntries } from "@homarr/common";
|
||||
import { IntegrationProvider, useSession } from "@homarr/auth/client";
|
||||
import { useOptionalBoard } from "@homarr/boards/context";
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import { createModal, modalSizeForm } from "@homarr/modals";
|
||||
import { createModal, ModalFormFooter, modalSizeForm } from "@homarr/modals";
|
||||
import type { SettingsContextProps } from "@homarr/settings/creator";
|
||||
import { SpotlightProvider } from "@homarr/spotlight";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
@@ -674,16 +674,7 @@ export const WidgetEditModal = createModal<WidgetEditModalProps<WidgetKind>>(({
|
||||
|
||||
const footer = (
|
||||
<Box className={classes.footer}>
|
||||
<Group justify="end" wrap="nowrap">
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<Button onClick={actions.closeModal} variant="subtle" color="gray">
|
||||
{tCommon("action.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" form={widgetFormId} loading={isSubmitting}>
|
||||
{tCommon("action.saveChanges")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
<ModalFormFooter onCancel={actions.closeModal} form={widgetFormId} loading={isSubmitting} />
|
||||
</Box>
|
||||
);
|
||||
|
||||
|
||||
Reference in new issue
Block a user