feat: modernize dashboard widgets
This commit is contained in:
1 parent
1a5e083fd0
commit
6f2cd7fc2b
270 files changed
+10189
-3544
No files matched your search
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Anchor, Card, Flex, Group, Stack, Text, Title, UnstyledButton } from "@mantine/core";
|
||||
import { Anchor, Card, Flex, Group, ScrollArea, SimpleGrid, Stack, Text, Title, UnstyledButton } from "@mantine/core";
|
||||
import combineClasses from "clsx";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
@@ -12,11 +12,11 @@ import { MaskedOrNormalImage } from "@homarr/ui";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import classes from "./bookmark.module.css";
|
||||
|
||||
export default function BookmarksWidget({ options, itemId }: WidgetComponentProps<"bookmarks">) {
|
||||
export default function BookmarksWidget({ options, itemId, width, displayMode }: WidgetComponentProps<"bookmarks">) {
|
||||
const board = useRequiredBoard();
|
||||
const { data = [] } = clientApi.app.byIds.useQuery(options.items, {
|
||||
select(data) {
|
||||
return data.toSorted((appA, appB) => options.items.indexOf(appA.id) - options.items.indexOf(appB.id));
|
||||
select(apps) {
|
||||
return apps.toSorted((appA, appB) => options.items.indexOf(appA.id) - options.items.indexOf(appB.id));
|
||||
},
|
||||
});
|
||||
|
||||
@@ -48,7 +48,16 @@ export default function BookmarksWidget({ options, itemId }: WidgetComponentProp
|
||||
{options.title}
|
||||
</Title>
|
||||
)}
|
||||
{(options.layout === "grid" || options.layout === "gridHorizontal") && (
|
||||
{displayMode === "advanced" && (
|
||||
<AdvancedBookmarksLayout
|
||||
data={data}
|
||||
width={width}
|
||||
openNewTab={options.openNewTab}
|
||||
withBorder={options.withBorder}
|
||||
hasIconColor={board.iconColor !== null}
|
||||
/>
|
||||
)}
|
||||
{displayMode !== "advanced" && (options.layout === "grid" || options.layout === "gridHorizontal") && (
|
||||
<GridLayout
|
||||
data={data}
|
||||
itemDirection={options.layout === "gridHorizontal" ? "horizontal" : "vertical"}
|
||||
@@ -60,7 +69,7 @@ export default function BookmarksWidget({ options, itemId }: WidgetComponentProp
|
||||
hasIconColor={board.iconColor !== null}
|
||||
/>
|
||||
)}
|
||||
{options.layout !== "grid" && options.layout !== "gridHorizontal" && (
|
||||
{displayMode !== "advanced" && options.layout !== "grid" && options.layout !== "gridHorizontal" && (
|
||||
<FlexLayout
|
||||
data={data}
|
||||
direction={options.layout}
|
||||
@@ -144,6 +153,77 @@ const FlexLayout = ({
|
||||
);
|
||||
};
|
||||
|
||||
interface AdvancedBookmarksLayoutProps {
|
||||
data: RouterOutputs["app"]["byIds"];
|
||||
width: number;
|
||||
openNewTab: boolean;
|
||||
withBorder: boolean;
|
||||
hasIconColor: boolean;
|
||||
}
|
||||
|
||||
export const getAdvancedBookmarkColumns = (width: number, itemCount: number): number =>
|
||||
Math.max(1, Math.min(itemCount || 1, Math.floor(width / 280)));
|
||||
|
||||
export const getBookmarkHostname = (href: string | null): string | undefined => {
|
||||
if (!href) return undefined;
|
||||
try {
|
||||
return new URL(href).hostname;
|
||||
} catch {
|
||||
return href;
|
||||
}
|
||||
};
|
||||
|
||||
const AdvancedBookmarksLayout = ({
|
||||
data,
|
||||
width,
|
||||
openNewTab,
|
||||
withBorder,
|
||||
hasIconColor,
|
||||
}: AdvancedBookmarksLayoutProps) => {
|
||||
const board = useRequiredBoard();
|
||||
|
||||
return (
|
||||
<ScrollArea h="100%" style={{ flex: 1 }}>
|
||||
<SimpleGrid cols={getAdvancedBookmarkColumns(width, data.length)} spacing="sm">
|
||||
{data.map((app) => (
|
||||
<UnstyledButton
|
||||
key={app.id}
|
||||
component="a"
|
||||
href={app.href ?? undefined}
|
||||
target={openNewTab ? "_blank" : "_self"}
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Card radius={board.itemRadius} className={classes.card} withBorder={withBorder} p="md" h="100%">
|
||||
<Group align="flex-start" wrap="nowrap">
|
||||
<MaskedOrNormalImage
|
||||
imageUrl={app.iconUrl}
|
||||
hasColor={hasIconColor}
|
||||
alt={app.name}
|
||||
className={classes.bookmarkIcon}
|
||||
style={{ width: 40, height: 40, flex: "0 0 auto" }}
|
||||
/>
|
||||
<Stack gap={3} miw={0}>
|
||||
<Text fw={700} size="sm" truncate="end">
|
||||
{app.name}
|
||||
</Text>
|
||||
<Anchor component="span" size="xs" truncate="end">
|
||||
{getBookmarkHostname(app.href)}
|
||||
</Anchor>
|
||||
{app.description && (
|
||||
<Text size="xs" c="dimmed" lineClamp={3}>
|
||||
{app.description}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Group>
|
||||
</Card>
|
||||
</UnstyledButton>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</ScrollArea>
|
||||
);
|
||||
};
|
||||
|
||||
interface GridLayoutProps {
|
||||
data: RouterOutputs["app"]["byIds"];
|
||||
hideTitle: boolean;
|
||||
@@ -247,7 +327,7 @@ const VerticalItem = ({
|
||||
)}
|
||||
{!hideHostname && (
|
||||
<Anchor ta="center" component="span" size="xs">
|
||||
{app.href ? new URL(app.href).hostname : undefined}
|
||||
{getBookmarkHostname(app.href)}
|
||||
</Anchor>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -294,7 +374,7 @@ const HorizontalItem = ({
|
||||
|
||||
{!hideHostname && (
|
||||
<Anchor component="span" size="xs">
|
||||
{app.href ? new URL(app.href).hostname : undefined}
|
||||
{getBookmarkHostname(app.href)}
|
||||
</Anchor>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -10,7 +10,8 @@ import { BookmarkAddButton } from "./add-button";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("bookmarks", {
|
||||
icon: IconBookmark,
|
||||
queryKeys: [[["app", "byIds"]]],
|
||||
supportsAdvancedFocus: true,
|
||||
queryKey: [["app", "byIds"]],
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
title: factory.text(),
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { getAdvancedBookmarkColumns, getBookmarkHostname } from "./component";
|
||||
|
||||
describe("bookmark display helpers", () => {
|
||||
test("uses bounded advanced columns", () => {
|
||||
expect(getAdvancedBookmarkColumns(900, 8)).toBe(3);
|
||||
expect(getAdvancedBookmarkColumns(120, 8)).toBe(1);
|
||||
expect(getAdvancedBookmarkColumns(900, 0)).toBe(1);
|
||||
});
|
||||
|
||||
test("does not crash on malformed bookmark URLs", () => {
|
||||
expect(getBookmarkHostname("https://homarr.dev/docs")).toBe("homarr.dev");
|
||||
expect(getBookmarkHostname("not a URL")).toBe("not a URL");
|
||||
expect(getBookmarkHostname(null)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user