fix: address board review and CI feedback
This commit is contained in:
1 parent
f133e09dc6
commit
e2f9fa871a
7 files changed
+197
-96
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import type { JSX, PropsWithChildren } from "react";
|
||||
import { headers } from "next/headers";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { AppShellMain } from "@mantine/core";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -6,8 +7,10 @@ import { TRPCError } from "@trpc/server";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { BoardProvider } from "@homarr/boards/context";
|
||||
import { EditModeProvider } from "@homarr/boards/edit-mode";
|
||||
import { userAgent } from "@homarr/common/server";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
|
||||
import { MobileBoardViewportProvider } from "~/components/board/use-mobile-board";
|
||||
import { MainHeader } from "~/components/layout/header";
|
||||
import { BoardLogoWithTitle } from "~/components/layout/logo/board-logo";
|
||||
import { ClientShell } from "~/components/layout/shell";
|
||||
@@ -61,28 +64,31 @@ export const createBoardLayout = <TParams extends Params>({
|
||||
|
||||
throw error;
|
||||
});
|
||||
const colorScheme = await getCurrentColorSchemeAsync();
|
||||
const [colorScheme, requestHeaders] = await Promise.all([getCurrentColorSchemeAsync(), headers()]);
|
||||
const initialIsMobile = userAgent(new Headers(requestHeaders)).device.type === "mobile";
|
||||
|
||||
return (
|
||||
<BoardProvider initialBoard={initialBoard}>
|
||||
<BoardReadyProvider>
|
||||
<EditModeProvider>
|
||||
<BoardMantineProvider defaultColorScheme={colorScheme}>
|
||||
<CustomCss />
|
||||
<BoardTourWrapper hasSession={withTour && !!session}>
|
||||
<ClientShell hasNavigation={false}>
|
||||
<MainHeader
|
||||
logo={<BoardLogoWithTitle size="md" hideTitleOnMobile />}
|
||||
actions={headerActions}
|
||||
hasNavigation={false}
|
||||
/>
|
||||
<AppShellMain>{children}</AppShellMain>
|
||||
</ClientShell>
|
||||
</BoardTourWrapper>
|
||||
</BoardMantineProvider>
|
||||
</EditModeProvider>
|
||||
</BoardReadyProvider>
|
||||
</BoardProvider>
|
||||
<MobileBoardViewportProvider initialIsMobile={initialIsMobile}>
|
||||
<BoardProvider initialBoard={initialBoard}>
|
||||
<BoardReadyProvider>
|
||||
<EditModeProvider>
|
||||
<BoardMantineProvider defaultColorScheme={colorScheme}>
|
||||
<CustomCss />
|
||||
<BoardTourWrapper hasSession={withTour && !!session}>
|
||||
<ClientShell hasNavigation={false}>
|
||||
<MainHeader
|
||||
logo={<BoardLogoWithTitle size="md" hideTitleOnMobile />}
|
||||
actions={headerActions}
|
||||
hasNavigation={false}
|
||||
/>
|
||||
<AppShellMain>{children}</AppShellMain>
|
||||
</ClientShell>
|
||||
</BoardTourWrapper>
|
||||
</BoardMantineProvider>
|
||||
</EditModeProvider>
|
||||
</BoardReadyProvider>
|
||||
</BoardProvider>
|
||||
</MobileBoardViewportProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(var(--mobile-column-count), minmax(0, 1fr));
|
||||
grid-auto-flow: row;
|
||||
grid-auto-rows: min(calc((100vw - 42px) / 2), 12rem);
|
||||
gap: 10px;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import type { RefObject } from "react";
|
||||
import type { CSSProperties, RefObject } from "react";
|
||||
import { useMemo, useRef } from "react";
|
||||
import { Box } from "@mantine/core";
|
||||
|
||||
@@ -11,19 +11,22 @@ import type { GridItemHTMLElement, GridStack as GridStackInstance } from "@homar
|
||||
import { BoardItemContent } from "../items/item-content";
|
||||
import { SectionProvider } from "../sections/section-context";
|
||||
import classes from "./mobile-board.module.css";
|
||||
import { createMobileBoardItems } from "./mobile-layout";
|
||||
import { createMobileBoardItems, mobileColumnCount } from "./mobile-layout";
|
||||
|
||||
export const MobileBoard = () => {
|
||||
const board = useRequiredBoard();
|
||||
const desktopLayout = getDesktopLayout(board);
|
||||
const items = useMemo(() => createMobileBoardItems(board, desktopLayout.id), [board, desktopLayout.id]);
|
||||
const rootSection = board.sections.find((section) => section.kind !== "dynamic");
|
||||
const rootSection = board.sections.find((section) => section.kind !== "dynamic") ?? {
|
||||
id: "mobile",
|
||||
kind: "empty" as const,
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
};
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||
const itemRefs = useRef<Record<string, RefObject<GridItemHTMLElement | null>>>({});
|
||||
const gridstackRef = useRef<GridStackInstance | null>(null);
|
||||
|
||||
if (!rootSection) return null;
|
||||
|
||||
return (
|
||||
<ReadOnlyEditModeProvider>
|
||||
<SectionProvider
|
||||
@@ -34,7 +37,11 @@ export const MobileBoard = () => {
|
||||
refs: { wrapper: wrapperRef, items: itemRefs, gridstack: gridstackRef },
|
||||
}}
|
||||
>
|
||||
<Box className={classes.grid} data-mobile-board>
|
||||
<Box
|
||||
className={classes.grid}
|
||||
style={{ "--mobile-column-count": mobileColumnCount } as CSSProperties}
|
||||
data-mobile-board
|
||||
>
|
||||
{items.map((item) => (
|
||||
<Box
|
||||
key={item.id}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Board, DynamicSectionItem, ItemLayout, SectionItem } from "~/app/[locale]/boards/_types";
|
||||
|
||||
const mobileColumnCount = 2;
|
||||
export const mobileColumnCount = 2;
|
||||
const mobileMaxHeight = 3;
|
||||
|
||||
type PositionedElement = DynamicSectionItem | SectionItem;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useMediaQuery } from "@mantine/hooks";
|
||||
|
||||
export const mobileBoardMediaQuery = "(max-width: 48em)";
|
||||
|
||||
export const useIsMobileBoard = () => useMediaQuery(mobileBoardMediaQuery);
|
||||
@@ -0,0 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { createContext, useContext } from "react";
|
||||
import { useMediaQuery } from "@mantine/hooks";
|
||||
|
||||
export const mobileBoardMediaQuery = "(max-width: 48em)";
|
||||
|
||||
const InitialMobileBoardContext = createContext<boolean | undefined>(undefined);
|
||||
|
||||
export const MobileBoardViewportProvider = ({
|
||||
initialIsMobile,
|
||||
children,
|
||||
}: PropsWithChildren<{ initialIsMobile: boolean }>) => (
|
||||
<InitialMobileBoardContext.Provider value={initialIsMobile}>{children}</InitialMobileBoardContext.Provider>
|
||||
);
|
||||
|
||||
export const useIsMobileBoard = () => {
|
||||
const initialIsMobile = useContext(InitialMobileBoardContext);
|
||||
return useMediaQuery(mobileBoardMediaQuery, initialIsMobile, { getInitialValueInEffect: true });
|
||||
};
|
||||
@@ -682,73 +682,147 @@ export const boardRouter = createTRPCRouter({
|
||||
|
||||
return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
|
||||
}),
|
||||
saveLayout: protectedProcedure.input(boardSaveLayoutSchema).mutation(async ({ ctx, input }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
|
||||
saveLayout: protectedProcedure
|
||||
.meta({
|
||||
mcp: {
|
||||
enabled: true,
|
||||
description:
|
||||
"Set a board's desktop column count and remove legacy responsive layouts. Requires modify permission. REQUIRED: id (board ID), columnCount (1-24)",
|
||||
},
|
||||
})
|
||||
.input(boardSaveLayoutSchema)
|
||||
.output(z.void())
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
|
||||
|
||||
const board = await getFullBoardWithWhereAsync(ctx.db, eq(boards.id, input.id), ctx.session.user.id);
|
||||
const desktopLayout = board.layouts
|
||||
.toSorted(
|
||||
(layoutA, layoutB) => layoutB.breakpoint - layoutA.breakpoint || layoutB.columnCount - layoutA.columnCount,
|
||||
)
|
||||
.at(0);
|
||||
const board = await getFullBoardWithWhereAsync(ctx.db, eq(boards.id, input.id), ctx.session.user.id);
|
||||
const desktopLayout = board.layouts
|
||||
.toSorted(
|
||||
(layoutA, layoutB) => layoutB.breakpoint - layoutA.breakpoint || layoutB.columnCount - layoutA.columnCount,
|
||||
)
|
||||
.at(0);
|
||||
|
||||
if (!desktopLayout) {
|
||||
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Board must have a layout" });
|
||||
}
|
||||
if (!desktopLayout) {
|
||||
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Board must have a layout" });
|
||||
}
|
||||
|
||||
if (desktopLayout.columnCount !== input.columnCount) {
|
||||
const updatedBoardLayout = getUpdatedBoardLayout(board, {
|
||||
previous: {
|
||||
layoutId: desktopLayout.id,
|
||||
columnCount: desktopLayout.columnCount,
|
||||
const updatedBoardLayout =
|
||||
desktopLayout.columnCount === input.columnCount
|
||||
? null
|
||||
: getUpdatedBoardLayout(board, {
|
||||
previous: {
|
||||
layoutId: desktopLayout.id,
|
||||
columnCount: desktopLayout.columnCount,
|
||||
},
|
||||
current: {
|
||||
layoutId: desktopLayout.id,
|
||||
columnCount: input.columnCount,
|
||||
},
|
||||
});
|
||||
|
||||
await handleTransactionsAsync(ctx.db, {
|
||||
async handleAsync(db, schema) {
|
||||
await db.transaction(async (transaction) => {
|
||||
for (const itemSectionLayout of updatedBoardLayout?.itemSectionLayouts ?? []) {
|
||||
await transaction
|
||||
.update(schema.itemLayouts)
|
||||
.set({
|
||||
height: itemSectionLayout.height,
|
||||
width: itemSectionLayout.width,
|
||||
xOffset: itemSectionLayout.xOffset,
|
||||
yOffset: itemSectionLayout.yOffset,
|
||||
sectionId: itemSectionLayout.sectionId,
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(schema.itemLayouts.itemId, itemSectionLayout.itemId),
|
||||
eq(schema.itemLayouts.layoutId, itemSectionLayout.layoutId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
for (const sectionLayout of updatedBoardLayout?.sectionLayouts ?? []) {
|
||||
await transaction
|
||||
.update(schema.sectionLayouts)
|
||||
.set({
|
||||
height: sectionLayout.height,
|
||||
width: sectionLayout.width,
|
||||
xOffset: sectionLayout.xOffset,
|
||||
yOffset: sectionLayout.yOffset,
|
||||
parentSectionId: sectionLayout.parentSectionId,
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(schema.sectionLayouts.sectionId, sectionLayout.sectionId),
|
||||
eq(schema.sectionLayouts.layoutId, sectionLayout.layoutId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await transaction
|
||||
.update(schema.layouts)
|
||||
.set({ name: "Base", columnCount: input.columnCount, breakpoint: 0 })
|
||||
.where(eq(schema.layouts.id, desktopLayout.id));
|
||||
|
||||
await transaction
|
||||
.delete(schema.layouts)
|
||||
.where(and(eq(schema.layouts.boardId, board.id), not(eq(schema.layouts.id, desktopLayout.id))));
|
||||
});
|
||||
},
|
||||
current: {
|
||||
layoutId: desktopLayout.id,
|
||||
columnCount: input.columnCount,
|
||||
handleSync(db) {
|
||||
db.transaction((transaction) => {
|
||||
for (const itemSectionLayout of updatedBoardLayout?.itemSectionLayouts ?? []) {
|
||||
transaction
|
||||
.update(itemLayouts)
|
||||
.set({
|
||||
height: itemSectionLayout.height,
|
||||
width: itemSectionLayout.width,
|
||||
xOffset: itemSectionLayout.xOffset,
|
||||
yOffset: itemSectionLayout.yOffset,
|
||||
sectionId: itemSectionLayout.sectionId,
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(itemLayouts.itemId, itemSectionLayout.itemId),
|
||||
eq(itemLayouts.layoutId, itemSectionLayout.layoutId),
|
||||
),
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
for (const sectionLayout of updatedBoardLayout?.sectionLayouts ?? []) {
|
||||
transaction
|
||||
.update(sectionLayouts)
|
||||
.set({
|
||||
height: sectionLayout.height,
|
||||
width: sectionLayout.width,
|
||||
xOffset: sectionLayout.xOffset,
|
||||
yOffset: sectionLayout.yOffset,
|
||||
parentSectionId: sectionLayout.parentSectionId,
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(sectionLayouts.sectionId, sectionLayout.sectionId),
|
||||
eq(sectionLayouts.layoutId, sectionLayout.layoutId),
|
||||
),
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
transaction
|
||||
.update(layouts)
|
||||
.set({ name: "Base", columnCount: input.columnCount, breakpoint: 0 })
|
||||
.where(eq(layouts.id, desktopLayout.id))
|
||||
.run();
|
||||
|
||||
transaction
|
||||
.delete(layouts)
|
||||
.where(and(eq(layouts.boardId, board.id), not(eq(layouts.id, desktopLayout.id))))
|
||||
.run();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
for (const itemSectionLayout of updatedBoardLayout.itemSectionLayouts) {
|
||||
await ctx.db
|
||||
.update(itemLayouts)
|
||||
.set({
|
||||
height: itemSectionLayout.height,
|
||||
width: itemSectionLayout.width,
|
||||
xOffset: itemSectionLayout.xOffset,
|
||||
yOffset: itemSectionLayout.yOffset,
|
||||
sectionId: itemSectionLayout.sectionId,
|
||||
})
|
||||
.where(
|
||||
and(eq(itemLayouts.itemId, itemSectionLayout.itemId), eq(itemLayouts.layoutId, itemSectionLayout.layoutId)),
|
||||
);
|
||||
}
|
||||
|
||||
for (const sectionLayout of updatedBoardLayout.sectionLayouts) {
|
||||
await ctx.db
|
||||
.update(sectionLayouts)
|
||||
.set({
|
||||
height: sectionLayout.height,
|
||||
width: sectionLayout.width,
|
||||
xOffset: sectionLayout.xOffset,
|
||||
yOffset: sectionLayout.yOffset,
|
||||
parentSectionId: sectionLayout.parentSectionId,
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(sectionLayouts.sectionId, sectionLayout.sectionId),
|
||||
eq(sectionLayouts.layoutId, sectionLayout.layoutId),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await ctx.db
|
||||
.update(layouts)
|
||||
.set({ name: "Base", columnCount: input.columnCount, breakpoint: 0 })
|
||||
.where(eq(layouts.id, desktopLayout.id));
|
||||
|
||||
await ctx.db.delete(layouts).where(and(eq(layouts.boardId, board.id), not(eq(layouts.id, desktopLayout.id))));
|
||||
}),
|
||||
}),
|
||||
savePartialBoardSettings: protectedProcedure
|
||||
.meta({
|
||||
openapi: { method: "PATCH", path: "/api/boards/{id}/settings", tags: ["boards"], protect: true },
|
||||
|
||||
Reference in new issue
Block a user