fix: refine mobile board preview
This commit is contained in:
1 parent
866b3ab78b
commit
dec04da806
5 files changed
+51
-15
No files matched your search
@@ -125,7 +125,7 @@ describe("createMobileBoardPreviewItems", () => {
|
||||
expect(createMobileBoardPreviewItems(board, desktopLayoutId)).toEqual([]);
|
||||
});
|
||||
|
||||
test("uses the production mobile order, footprint, kind, and custom title", () => {
|
||||
test("uses the production mobile order, footprint, kind, app ID, and custom title", () => {
|
||||
const board = new BoardMockBuilder().build();
|
||||
const desktopLayoutId = board.layouts.at(0)?.id;
|
||||
if (!desktopLayoutId) throw new Error("Expected a desktop layout");
|
||||
@@ -135,17 +135,20 @@ describe("createMobileBoardPreviewItems", () => {
|
||||
new ItemMockBuilder({ id: "wide", kind: "notebook" })
|
||||
.addLayout({ layoutId: desktopLayoutId, sectionId: rootSection.id, width: 8, height: 6, yOffset: 1 })
|
||||
.build(),
|
||||
new ItemMockBuilder({ id: "first", kind: "clock" })
|
||||
new ItemMockBuilder({ id: "first", kind: "app" })
|
||||
.addLayout({ layoutId: desktopLayoutId, sectionId: rootSection.id, width: 1, height: 1, yOffset: 0 })
|
||||
.build(),
|
||||
);
|
||||
const appItem = board.items.find((item) => item.id === "first");
|
||||
if (!appItem || appItem.kind !== "app") throw new Error("Expected app board item");
|
||||
appItem.options.appId = "actual-app";
|
||||
const wideItem = board.items.find((item) => item.id === "wide");
|
||||
if (!wideItem) throw new Error("Expected wide board item");
|
||||
wideItem.advancedOptions.title = "Notes";
|
||||
|
||||
expect(createMobileBoardPreviewItems(board, desktopLayoutId)).toEqual([
|
||||
{ id: "first", kind: "clock", width: 1, height: 1, title: null },
|
||||
{ id: "wide", kind: "notebook", width: 2, height: 3, title: "Notes" },
|
||||
{ id: "first", kind: "app", appId: "actual-app", width: 1, height: 1, title: null },
|
||||
{ id: "wide", kind: "notebook", appId: null, width: 2, height: 3, title: "Notes" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -120,6 +120,7 @@ export const createMobileBoardPreviewItems = (board: Board, desktopLayoutId: str
|
||||
createMobileBoardItems(board, desktopLayoutId).map((item) => ({
|
||||
id: item.id,
|
||||
kind: item.kind,
|
||||
appId: item.kind === "app" && typeof item.options.appId === "string" ? item.options.appId : null,
|
||||
width: item.width,
|
||||
height: item.height,
|
||||
title: item.advancedOptions.title,
|
||||
|
||||
@@ -30,6 +30,11 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.appIcon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.panel,
|
||||
.item {
|
||||
|
||||
@@ -5,10 +5,12 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import { ActionIcon, Affix, Button, Center, Group, Paper, ScrollArea, Stack, Text, ThemeIcon } from "@mantine/core";
|
||||
import { IconDeviceMobile, IconX } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { getDesktopLayout, useRequiredBoard } from "@homarr/boards/context";
|
||||
import { useEditMode } from "@homarr/boards/edit-mode";
|
||||
import { useSettings } from "@homarr/settings";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { MaskedOrNormalImage } from "@homarr/ui";
|
||||
import { widgetCatalogIcons } from "@homarr/widgets/catalog";
|
||||
|
||||
import { useIsMobileBoard } from "../use-mobile-board";
|
||||
@@ -31,7 +33,7 @@ export const MobileBoardPreview = () => {
|
||||
if (!isEditMode || isMobileBoard || !enableAutomaticMobileLayout) return null;
|
||||
|
||||
return (
|
||||
<Affix position={{ bottom: "var(--mantine-spacing-md)", right: "var(--mantine-spacing-md)" }} zIndex={200}>
|
||||
<Affix position={{ bottom: "var(--mantine-spacing-md)", left: "var(--mantine-spacing-md)" }} zIndex={200}>
|
||||
{isExpanded ? (
|
||||
<Paper className={classes.panel} radius="md" shadow="md" data-mobile-board-preview>
|
||||
<Group justify="space-between" wrap="nowrap" p="sm">
|
||||
@@ -67,6 +69,12 @@ const MobileBoardPreviewMap = () => {
|
||||
const board = useRequiredBoard();
|
||||
const desktopLayout = getDesktopLayout(board);
|
||||
const items = useMemo(() => createMobileBoardPreviewItems(board, desktopLayout.id), [board, desktopLayout.id]);
|
||||
const appIds = useMemo(
|
||||
() => [...new Set(items.flatMap((item) => (item.appId === null ? [] : [item.appId])))],
|
||||
[items],
|
||||
);
|
||||
const { data: apps = [] } = clientApi.app.byIds.useQuery(appIds, { enabled: appIds.length > 0 });
|
||||
const appsById = useMemo(() => new Map(apps.map((app) => [app.id, app])), [apps]);
|
||||
const t = useI18n();
|
||||
|
||||
return (
|
||||
@@ -88,7 +96,8 @@ const MobileBoardPreviewMap = () => {
|
||||
>
|
||||
{items.map((item) => {
|
||||
const WidgetIcon = widgetCatalogIcons[item.kind];
|
||||
const label = item.title?.trim() || t(`widget.${item.kind}.name`);
|
||||
const app = item.appId === null ? undefined : appsById.get(item.appId);
|
||||
const label = app?.name ?? (item.title?.trim() || t(`widget.${item.kind}.name`));
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -98,8 +107,12 @@ const MobileBoardPreviewMap = () => {
|
||||
data-mobile-board-preview-item={item.id}
|
||||
>
|
||||
<Group className={classes.itemContent} gap={6} wrap="nowrap" px="xs">
|
||||
<ThemeIcon variant="light" size="sm">
|
||||
<WidgetIcon size={14} aria-hidden />
|
||||
<ThemeIcon variant="light" size="sm" data-mobile-board-preview-app-icon={app?.id}>
|
||||
{app ? (
|
||||
<MaskedOrNormalImage imageUrl={app.iconUrl} hasColor={false} alt="" className={classes.appIcon} />
|
||||
) : (
|
||||
<WidgetIcon size={14} aria-hidden />
|
||||
)}
|
||||
</ThemeIcon>
|
||||
<Text size="xs" fw={500} truncate title={label}>
|
||||
{label}
|
||||
|
||||
@@ -25,7 +25,8 @@ const seedBoardAsync = async (db: SqliteDatabase, userId: string) => {
|
||||
const sectionId = createId();
|
||||
const emptySectionId = createId();
|
||||
const notebookId = createId();
|
||||
const clockId = createId();
|
||||
const appItemId = createId();
|
||||
const appId = createId();
|
||||
|
||||
await db.insert(sqliteSchema.boards).values({
|
||||
id: boardId,
|
||||
@@ -65,6 +66,11 @@ const seedBoardAsync = async (db: SqliteDatabase, userId: string) => {
|
||||
yOffset: 1,
|
||||
},
|
||||
]);
|
||||
await db.insert(sqliteSchema.apps).values({
|
||||
id: appId,
|
||||
name: "Media Server",
|
||||
iconUrl: "/favicon.ico",
|
||||
});
|
||||
await db.insert(sqliteSchema.items).values([
|
||||
{
|
||||
id: notebookId,
|
||||
@@ -74,10 +80,10 @@ const seedBoardAsync = async (db: SqliteDatabase, userId: string) => {
|
||||
advancedOptions: stringify({}),
|
||||
},
|
||||
{
|
||||
id: clockId,
|
||||
id: appItemId,
|
||||
boardId,
|
||||
kind: "clock",
|
||||
options: stringify({}),
|
||||
kind: "app",
|
||||
options: stringify({ appId }),
|
||||
advancedOptions: stringify({}),
|
||||
},
|
||||
]);
|
||||
@@ -92,7 +98,7 @@ const seedBoardAsync = async (db: SqliteDatabase, userId: string) => {
|
||||
height: 2,
|
||||
},
|
||||
{
|
||||
itemId: clockId,
|
||||
itemId: appItemId,
|
||||
layoutId: desktopLayoutId,
|
||||
sectionId,
|
||||
xOffset: 2,
|
||||
@@ -110,7 +116,7 @@ const seedBoardAsync = async (db: SqliteDatabase, userId: string) => {
|
||||
height: 4,
|
||||
},
|
||||
{
|
||||
itemId: clockId,
|
||||
itemId: appItemId,
|
||||
layoutId: mobileLayoutId,
|
||||
sectionId,
|
||||
xOffset: 0,
|
||||
@@ -127,6 +133,8 @@ const seedBoardAsync = async (db: SqliteDatabase, userId: string) => {
|
||||
completedBoardTour: true,
|
||||
})
|
||||
.where(eq(sqliteSchema.users.id, userId));
|
||||
|
||||
return { appId };
|
||||
};
|
||||
|
||||
describe("Automatic mobile board", () => {
|
||||
@@ -155,7 +163,7 @@ describe("Automatic mobile board", () => {
|
||||
test("squeezes unchanged widgets into a read-only two-column grid", async () => {
|
||||
const { db, localMountPath } = await createSqliteDbFileAsync();
|
||||
const { userId } = await seedAdminUserAsync(db, credentials);
|
||||
await seedBoardAsync(db, userId);
|
||||
const { appId } = await seedBoardAsync(db, userId);
|
||||
await db.insert(sqliteSchema.serverSettings).values({
|
||||
settingKey: "board",
|
||||
value: stringify({
|
||||
@@ -191,9 +199,15 @@ describe("Automatic mobile board", () => {
|
||||
await loginPage.getByRole("button", { name: "Toggle board edit mode", exact: true }).click();
|
||||
const previewToggle = loginPage.getByRole("button", { name: "Mobile preview", exact: true });
|
||||
await expect(previewToggle).toBeVisible();
|
||||
expect(await previewToggle.evaluate((element) => element.getBoundingClientRect().left)).toBeLessThan(32);
|
||||
await previewToggle.click();
|
||||
await expect(loginPage.locator("[data-mobile-board-preview]")).toBeVisible();
|
||||
await expect(loginPage.locator("[data-mobile-board-preview-item]")).toHaveCount(2);
|
||||
await expect(loginPage.getByText("Media Server", { exact: true })).toBeVisible();
|
||||
await expect(loginPage.locator(`[data-mobile-board-preview-app-icon="${appId}"] img`)).toHaveAttribute(
|
||||
"src",
|
||||
"/favicon.ico",
|
||||
);
|
||||
await loginPage
|
||||
.locator("header button")
|
||||
.filter({ has: loginPage.locator("svg.tabler-icon-plus") })
|
||||
|
||||
Reference in new issue
Block a user