fix(import): autofix missing shapes for sidebars and some sections as well (#2723)

This commit is contained in:
Meier Lukas
2025-03-28 17:44:11 +01:00
committed by GitHub
parent 217818573e
commit 62c7955e48
4 changed files with 113 additions and 44 deletions

View File

@@ -1,4 +1,4 @@
import type { BoardSize, OldmarrConfig } from "@homarr/old-schema"; import type { OldmarrConfig } from "@homarr/old-schema";
export class OldHomarrImportError extends Error { export class OldHomarrImportError extends Error {
constructor(oldConfig: OldmarrConfig, cause: unknown) { constructor(oldConfig: OldmarrConfig, cause: unknown) {
@@ -7,9 +7,3 @@ export class OldHomarrImportError extends Error {
}); });
} }
} }
export class OldHomarrScreenSizeError extends Error {
constructor(type: "app" | "widget", id: string, screenSize: BoardSize) {
super(`Screen size not found for type=${type} id=${id} screenSize=${screenSize}`);
}
}

View File

@@ -3,11 +3,12 @@ import { isWidgetRestricted } from "@homarr/auth/shared";
import { createId } from "@homarr/db"; import { createId } from "@homarr/db";
import { createDbInsertCollectionForTransaction } from "@homarr/db/collection"; import { createDbInsertCollectionForTransaction } from "@homarr/db/collection";
import { logger } from "@homarr/log"; import { logger } from "@homarr/log";
import type { BoardSize } from "@homarr/old-schema"; import type { BoardSize, OldmarrConfig } from "@homarr/old-schema";
import { boardSizes, getBoardSizeName } from "@homarr/old-schema"; import { boardSizes, getBoardSizeName } from "@homarr/old-schema";
import { widgetImports } from "../../../../widgets/src"; import { widgetImports } from "../../../../widgets/src";
import { fixSectionIssues } from "../../fix-section-issues"; import { fixSectionIssues } from "../../fix-section-issues";
import { OldHomarrImportError } from "../../import-error";
import { mapBoard } from "../../mappers/map-board"; import { mapBoard } from "../../mappers/map-board";
import { mapBreakpoint } from "../../mappers/map-breakpoint"; import { mapBreakpoint } from "../../mappers/map-breakpoint";
import { mapColumnCount } from "../../mappers/map-column-count"; import { mapColumnCount } from "../../mappers/map-column-count";
@@ -61,6 +62,13 @@ export const createBoardInsertCollection = (
logger.debug(`Added apps to board insert collection count=${insertCollection.apps.length}`); logger.debug(`Added apps to board insert collection count=${insertCollection.apps.length}`);
preparedBoards.forEach((board) => { preparedBoards.forEach((board) => {
if (!hasEnoughItemShapes(board.config)) {
throw new OldHomarrImportError(
board.config,
new Error("Your config contains items without shapes for all board sizes."),
);
}
const { wrappers, categories, wrapperIdsToMerge } = fixSectionIssues(board.config); const { wrappers, categories, wrapperIdsToMerge } = fixSectionIssues(board.config);
const { apps, widgets } = moveWidgetsAndAppsIfMerge(board.config, wrapperIdsToMerge, { const { apps, widgets } = moveWidgetsAndAppsIfMerge(board.config, wrapperIdsToMerge, {
...settings, ...settings,
@@ -130,3 +138,26 @@ export const createBoardInsertCollection = (
return insertCollection; return insertCollection;
}; };
export const hasEnoughItemShapes = (config: {
apps: Pick<OldmarrConfig["apps"][number], "shape">[];
widgets: Pick<OldmarrConfig["widgets"][number], "shape">[];
}) => {
const invalidSizes: BoardSize[] = [];
for (const size of boardSizes) {
if (invalidSizes.includes(size)) continue;
if (config.apps.some((app) => app.shape[size] === undefined)) {
invalidSizes.push(size);
}
if (invalidSizes.includes(size)) continue;
if (config.widgets.some((widget) => widget.shape[size] === undefined)) {
invalidSizes.push(size);
}
}
return invalidSizes.length <= 2;
};

View File

@@ -0,0 +1,53 @@
import { describe, expect, test } from "vitest";
import type { BoardSize } from "@homarr/old-schema";
import { hasEnoughItemShapes } from "../collections/board-collection";
const defaultShape = {
location: {
x: 0,
y: 0,
},
size: {
width: 1,
height: 1,
},
};
describe("hasEnoughItemShapes should check if there are more than one shape available for automatic reconstruction", () => {
test.each([
[true, [], []], // no items, so nothing to check
[true, [{ lg: true }], []], // lg always exists
[true, [], [{ md: true }]], // md always exists
[true, [{ md: true, sm: true }], [{ md: true, lg: true }]], // md always exists
[true, [{ md: true }], [{ md: true }]], // md always exists
[false, [{ md: true }, { md: true }], [{ lg: true }]], // md is missing for widgets
[false, [{ md: true }], [{ lg: true }]], // md is missing for widgets
[false, [{ md: true }], [{ md: true, lg: true }, { lg: true }]], // md is missing for 2. widget
] as [boolean, Shape[], Shape[]][])(
"should return %s if there are more than one shape available",
(returnValue, appShapes, widgetShapes) => {
const result = hasEnoughItemShapes({
apps: appShapes.map((shapes) => ({
shape: {
sm: shapes.sm ? defaultShape : undefined,
md: shapes.md ? defaultShape : undefined,
lg: shapes.lg ? defaultShape : undefined,
},
})),
widgets: widgetShapes.map((shapes) => ({
shape: {
sm: shapes.sm ? defaultShape : undefined,
md: shapes.md ? defaultShape : undefined,
lg: shapes.lg ? defaultShape : undefined,
},
})),
});
expect(result).toBe(returnValue);
},
);
});
type Shape = Partial<Record<BoardSize, true>>;

View File

@@ -3,7 +3,6 @@ import { logger } from "@homarr/log";
import type { BoardSize, OldmarrApp, OldmarrConfig, OldmarrWidget } from "@homarr/old-schema"; import type { BoardSize, OldmarrApp, OldmarrConfig, OldmarrWidget } from "@homarr/old-schema";
import { boardSizes } from "@homarr/old-schema"; import { boardSizes } from "@homarr/old-schema";
import { OldHomarrScreenSizeError } from "./import-error";
import { mapColumnCount } from "./mappers/map-column-count"; import { mapColumnCount } from "./mappers/map-column-count";
import type { OldmarrImportConfiguration } from "./settings"; import type { OldmarrImportConfiguration } from "./settings";
@@ -60,7 +59,7 @@ export const moveWidgetsAndAppsIfMerge = (
for (const screenSize of boardSizes) { for (const screenSize of boardSizes) {
const screenSizeShape = app.shape[screenSize]; const screenSizeShape = app.shape[screenSize];
if (!screenSizeShape) { if (!screenSizeShape) {
throw new OldHomarrScreenSizeError("app", app.id, screenSize); continue;
} }
// Find the highest widget in the wrapper to increase the offset accordingly // Find the highest widget in the wrapper to increase the offset accordingly
@@ -81,7 +80,7 @@ export const moveWidgetsAndAppsIfMerge = (
for (const screenSize of boardSizes) { for (const screenSize of boardSizes) {
const screenSizeShape = widget.shape[screenSize]; const screenSizeShape = widget.shape[screenSize];
if (!screenSizeShape) { if (!screenSizeShape) {
throw new OldHomarrScreenSizeError("widget", widget.id, screenSize); continue;
} }
// Find the highest widget in the wrapper to increase the offset accordingly // Find the highest widget in the wrapper to increase the offset accordingly
@@ -145,15 +144,6 @@ const moveWidgetsAndAppsInLeftSidebar = (
item.area.properties.location === "left" && item.area.properties.location === "left" &&
(columnCount >= 2 || item.shape[screenSize]?.location.x === 0), (columnCount >= 2 || item.shape[screenSize]?.location.x === 0),
update: (item) => { update: (item) => {
const screenSizeShape = item.shape[screenSize];
if (!screenSizeShape) {
throw new OldHomarrScreenSizeError("kind" in item ? "widget" : "app", item.id, screenSize);
}
// Reduce width to one if column count is one
if (screenSizeShape.size.width > columnCount) {
screenSizeShape.size.width = columnCount;
}
item.area = { item.area = {
type: "wrapper", type: "wrapper",
properties: { properties: {
@@ -161,6 +151,14 @@ const moveWidgetsAndAppsInLeftSidebar = (
}, },
}; };
const screenSizeShape = item.shape[screenSize];
if (!screenSizeShape) return;
// Reduce width to one if column count is one
if (screenSizeShape.size.width > columnCount) {
screenSizeShape.size.width = columnCount;
}
screenSizeShape.location.y += offset; screenSizeShape.location.y += offset;
}, },
}); });
@@ -184,11 +182,6 @@ const moveWidgetsAndAppsInLeftSidebar = (
item.area.properties.location === "left" && item.area.properties.location === "left" &&
item.shape[screenSize]?.location.x === 1, item.shape[screenSize]?.location.x === 1,
update: (item) => { update: (item) => {
const screenSizeShape = item.shape[screenSize];
if (!screenSizeShape) {
throw new OldHomarrScreenSizeError("kind" in item ? "widget" : "app", item.id, screenSize);
}
item.area = { item.area = {
type: "wrapper", type: "wrapper",
properties: { properties: {
@@ -196,6 +189,9 @@ const moveWidgetsAndAppsInLeftSidebar = (
}, },
}; };
const screenSizeShape = item.shape[screenSize];
if (!screenSizeShape) return;
screenSizeShape.location.x = 0; screenSizeShape.location.x = 0;
screenSizeShape.location.y += offset; screenSizeShape.location.y += offset;
}, },
@@ -222,16 +218,6 @@ const moveWidgetsAndAppsInRightSidebar = (
item.area.properties.location === "right" && item.area.properties.location === "right" &&
(columnCount >= 2 || item.shape[screenSize]?.location.x === 0), (columnCount >= 2 || item.shape[screenSize]?.location.x === 0),
update: (item) => { update: (item) => {
const screenSizeShape = item.shape[screenSize];
if (!screenSizeShape) {
throw new OldHomarrScreenSizeError("kind" in item ? "widget" : "app", item.id, screenSize);
}
// Reduce width to one if column count is one
if (screenSizeShape.size.width > columnCount) {
screenSizeShape.size.width = columnCount;
}
item.area = { item.area = {
type: "wrapper", type: "wrapper",
properties: { properties: {
@@ -239,6 +225,14 @@ const moveWidgetsAndAppsInRightSidebar = (
}, },
}; };
const screenSizeShape = item.shape[screenSize];
if (!screenSizeShape) return;
// Reduce width to one if column count is one
if (screenSizeShape.size.width > columnCount) {
screenSizeShape.size.width = columnCount;
}
screenSizeShape.location.y += offset; screenSizeShape.location.y += offset;
screenSizeShape.location.x += xOffsetDelta; screenSizeShape.location.x += xOffsetDelta;
}, },
@@ -260,11 +254,6 @@ const moveWidgetsAndAppsInRightSidebar = (
item.area.properties.location === "left" && item.area.properties.location === "left" &&
item.shape[screenSize]?.location.x === 1, item.shape[screenSize]?.location.x === 1,
update: (item) => { update: (item) => {
const screenSizeShape = item.shape[screenSize];
if (!screenSizeShape) {
throw new OldHomarrScreenSizeError("kind" in item ? "widget" : "app", item.id, screenSize);
}
item.area = { item.area = {
type: "wrapper", type: "wrapper",
properties: { properties: {
@@ -272,6 +261,9 @@ const moveWidgetsAndAppsInRightSidebar = (
}, },
}; };
const screenSizeShape = item.shape[screenSize];
if (!screenSizeShape) return;
screenSizeShape.location.x = 0; screenSizeShape.location.x = 0;
screenSizeShape.location.y += offset; screenSizeShape.location.y += offset;
}, },
@@ -312,16 +304,15 @@ const updateItems = (options: {
for (const item of items) { for (const item of items) {
const before = createItemSnapshot(item, options.screenSize); const before = createItemSnapshot(item, options.screenSize);
options.update(item);
const screenSizeShape = item.shape[options.screenSize]; const screenSizeShape = item.shape[options.screenSize];
if (!screenSizeShape) { if (!screenSizeShape) return requiredHeight;
throw new OldHomarrScreenSizeError("kind" in item ? "widget" : "app", item.id, options.screenSize);
}
if (screenSizeShape.location.y + screenSizeShape.size.height > requiredHeight) { if (screenSizeShape.location.y + screenSizeShape.size.height > requiredHeight) {
requiredHeight = screenSizeShape.location.y + screenSizeShape.size.height; requiredHeight = screenSizeShape.location.y + screenSizeShape.size.height;
} }
options.update(item);
const after = createItemSnapshot(item, options.screenSize); const after = createItemSnapshot(item, options.screenSize);
logger.debug( logger.debug(