fix release v2 review issues

This commit is contained in:
ajnart committed 2026-08-14 02:18:46 +02:00
1 parent ef2ed6c602
commit ca856f9f54
29 files changed
+508 -131

No files matched your search

@@ -36,11 +36,16 @@ export const createItemCallback =
section.id === targetSectionId && (section.kind === "empty" || section.kind === "container"),
)
: undefined;
const targetSection = requestedSection ?? firstCanvasSection;
let targetSection = requestedSection ?? firstCanvasSection;
if (!targetSection) return previous;
const itemLayouts = createItemLayouts(previous, targetSection, kind);
let itemLayouts = createItemLayouts(previous, targetSection, kind);
if (itemLayouts.length === 0 && firstCanvasSection && targetSection.id !== firstCanvasSection.id) {
targetSection = firstCanvasSection;
itemLayouts = createItemLayouts(previous, targetSection, kind);
}
if (itemLayouts.length === 0) return previous;
const widget = {
id: id ?? createId(),
kind,
@@ -73,15 +78,16 @@ const createItemLayouts = (
const layouts = getBoardLayouts(board);
const itemSize = getWidgetItemSize(kind);
return layouts.map((layoutId) => {
return layouts.flatMap((layoutId) => {
const boardLayout = board.layouts.find((layout) => layout.id === layoutId);
const elements = getSectionElements(board, { sectionId: currentSection.id, layoutId });
const containerLayout =
currentSection.kind === "container"
? currentSection.layouts.find((layout) => layout.layoutId === layoutId)
: undefined;
const columnCount =
containerLayout?.width ?? (boardLayout ? getBoardLaneColumnCount(boardLayout, "main") : itemSize.width);
const lane = currentSection.kind === "empty" ? getRootSectionLane(currentSection.xOffset) : "main";
const columnCount = containerLayout?.width ?? (boardLayout ? getBoardLaneColumnCount(boardLayout, lane) : 0);
if (columnCount === 0) return [];
const size = {
width: Math.min(itemSize.width, columnCount),
height: itemSize.height,
@@ -95,12 +101,14 @@ const createItemLayouts = (
throw new Error("Your board is full");
}
return {
width: size.width,
height: size.height,
...emptyPosition,
sectionId: currentSection.id,
layoutId,
};
return [
{
width: size.width,
height: size.height,
...emptyPosition,
sectionId: currentSection.id,
layoutId,
},
];
});
};
@@ -169,6 +169,36 @@ describe("item actions create-item", () => {
]);
});
test("sizes a new item for its requested rail and omits layouts where that rail is hidden", () => {
const board = new BoardMockBuilder()
.addLayout({ id: "mobile", name: "Mobile", role: "mobile", columnCount: 3, breakpoint: 0 })
.addEmptySection({ id: "canvas" })
.addEmptySection({ id: "left-rail", xOffset: -1 })
.build();
const baseLayout = board.layouts.find((layout) => layout.role === "base");
if (!baseLayout) throw new Error("Expected a base layout");
baseLayout.leftGutterColumnCount = 2;
const result = createItemCallback({ id: "missing", kind: "mediaMissing", targetSectionId: "left-rail" })(board);
expect(result.items.find((item) => item.id === "missing")?.layouts).toEqual([
expect.objectContaining({ layoutId: baseLayout.id, sectionId: "left-rail", width: 2 }),
]);
});
test("falls back to the main canvas instead of creating a ghost item for a disabled rail", () => {
const board = new BoardMockBuilder()
.addEmptySection({ id: "canvas" })
.addEmptySection({ id: "left-rail", xOffset: -1 })
.build();
const result = createItemCallback({ id: "clock", kind: "clock", targetSectionId: "left-rail" })(board);
expect(result.items.find((item) => item.id === "clock")?.layouts).toEqual([
expect.objectContaining({ sectionId: "canvas" }),
]);
});
test("falls back to the main canvas when a requested destination no longer exists", () => {
const board = new BoardMockBuilder().addEmptySection({ id: "canvas" }).build();
const layout = board.layouts.at(0);
@@ -1,14 +1,16 @@
import { useMemo, useRef, useState } from "react";
import { Avatar, Badge, Box, Button, Center, Divider, Group, Image, Select, Stack, Text, Tooltip } from "@mantine/core";
import { notifications } from "@mantine/notifications";
import { IconApi } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { useSession } from "@homarr/auth/client";
import { useRequiredBoard } from "@homarr/boards/context";
import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context";
import { createId } from "@homarr/common";
import {
getIconUrl,
getBoardLaneColumnCount,
getIntegrationName,
getRootSectionLane,
widgetIntegrationSupport,
@@ -61,6 +63,8 @@ const ItemSelectModalContent = ({
const [search, setSearch] = useState(initialSearch);
const t = useI18n();
const board = useRequiredBoard();
const currentLayoutId = useCurrentLayout();
const currentLayout = board.layouts.find((layout) => layout.id === currentLayoutId);
const mainCanvasSection = board.sections
.filter(
(section): section is EmptySection => section.kind === "empty" && getRootSectionLane(section.xOffset) === "main",
@@ -73,7 +77,7 @@ const ItemSelectModalContent = ({
...board.sections.flatMap((section) => {
if (section.kind !== "empty") return [];
const lane = getRootSectionLane(section.xOffset);
if (lane === "main") return [];
if (lane === "main" || !currentLayout || getBoardLaneColumnCount(currentLayout, lane) === 0) return [];
return [
{
value: section.id,
@@ -88,7 +92,7 @@ const ItemSelectModalContent = ({
label: section.options.title.trim() || t("section.container.untitled"),
})),
],
[board.sections, mainCanvasSection, t],
[board.sections, currentLayout, mainCanvasSection, t],
);
const [targetSectionId, setTargetSectionId] = useState<string | null>(mainCanvasSection?.id ?? null);
const [loadingSelection, setLoadingSelection] = useState<string | null>(null);
@@ -149,12 +153,21 @@ const ItemSelectModalContent = ({
};
const notifyCreated = (itemId: string, name: string) => {
const notificationId = `board-item-created:${itemId}`;
showSuccessNotification({
id: notificationId,
title: t("item.create.notification.success.title"),
message: (
<Group justify="space-between" wrap="nowrap">
<Text size="sm">{t("item.create.notification.success.message", { name })}</Text>
<Button variant="subtle" size="compact-xs" onClick={() => removeItem({ itemId })}>
<Button
variant="subtle"
size="compact-xs"
onClick={() => {
removeItem({ itemId });
notifications.hide(notificationId);
}}
>
{t("common.action.undo")}
</Button>
</Group>