From 62c7955e48ca62b4bf1baf26ac6a12847795ac18 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Fri, 28 Mar 2025 17:44:11 +0100 Subject: [PATCH] fix(import): autofix missing shapes for sidebars and some sections as well (#2723) --- packages/old-import/src/import-error.ts | 8 +-- .../import/collections/board-collection.ts | 33 +++++++++- .../src/import/test/board-collection.spec.ts | 53 ++++++++++++++++ .../src/move-widgets-and-apps-merge.ts | 63 ++++++++----------- 4 files changed, 113 insertions(+), 44 deletions(-) create mode 100644 packages/old-import/src/import/test/board-collection.spec.ts diff --git a/packages/old-import/src/import-error.ts b/packages/old-import/src/import-error.ts index 8485aa3b9..15efdecfb 100644 --- a/packages/old-import/src/import-error.ts +++ b/packages/old-import/src/import-error.ts @@ -1,4 +1,4 @@ -import type { BoardSize, OldmarrConfig } from "@homarr/old-schema"; +import type { OldmarrConfig } from "@homarr/old-schema"; export class OldHomarrImportError extends Error { 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}`); - } -} diff --git a/packages/old-import/src/import/collections/board-collection.ts b/packages/old-import/src/import/collections/board-collection.ts index a3259ce08..2fd2a5142 100644 --- a/packages/old-import/src/import/collections/board-collection.ts +++ b/packages/old-import/src/import/collections/board-collection.ts @@ -3,11 +3,12 @@ import { isWidgetRestricted } from "@homarr/auth/shared"; import { createId } from "@homarr/db"; import { createDbInsertCollectionForTransaction } from "@homarr/db/collection"; 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 { widgetImports } from "../../../../widgets/src"; import { fixSectionIssues } from "../../fix-section-issues"; +import { OldHomarrImportError } from "../../import-error"; import { mapBoard } from "../../mappers/map-board"; import { mapBreakpoint } from "../../mappers/map-breakpoint"; 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}`); 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 { apps, widgets } = moveWidgetsAndAppsIfMerge(board.config, wrapperIdsToMerge, { ...settings, @@ -130,3 +138,26 @@ export const createBoardInsertCollection = ( return insertCollection; }; + +export const hasEnoughItemShapes = (config: { + apps: Pick[]; + widgets: Pick[]; +}) => { + 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; +}; diff --git a/packages/old-import/src/import/test/board-collection.spec.ts b/packages/old-import/src/import/test/board-collection.spec.ts new file mode 100644 index 000000000..0933b0de7 --- /dev/null +++ b/packages/old-import/src/import/test/board-collection.spec.ts @@ -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>; diff --git a/packages/old-import/src/move-widgets-and-apps-merge.ts b/packages/old-import/src/move-widgets-and-apps-merge.ts index ac3989172..5b77fa925 100644 --- a/packages/old-import/src/move-widgets-and-apps-merge.ts +++ b/packages/old-import/src/move-widgets-and-apps-merge.ts @@ -3,7 +3,6 @@ import { logger } from "@homarr/log"; import type { BoardSize, OldmarrApp, OldmarrConfig, OldmarrWidget } from "@homarr/old-schema"; import { boardSizes } from "@homarr/old-schema"; -import { OldHomarrScreenSizeError } from "./import-error"; import { mapColumnCount } from "./mappers/map-column-count"; import type { OldmarrImportConfiguration } from "./settings"; @@ -60,7 +59,7 @@ export const moveWidgetsAndAppsIfMerge = ( for (const screenSize of boardSizes) { const screenSizeShape = app.shape[screenSize]; if (!screenSizeShape) { - throw new OldHomarrScreenSizeError("app", app.id, screenSize); + continue; } // Find the highest widget in the wrapper to increase the offset accordingly @@ -81,7 +80,7 @@ export const moveWidgetsAndAppsIfMerge = ( for (const screenSize of boardSizes) { const screenSizeShape = widget.shape[screenSize]; if (!screenSizeShape) { - throw new OldHomarrScreenSizeError("widget", widget.id, screenSize); + continue; } // Find the highest widget in the wrapper to increase the offset accordingly @@ -145,15 +144,6 @@ const moveWidgetsAndAppsInLeftSidebar = ( item.area.properties.location === "left" && (columnCount >= 2 || item.shape[screenSize]?.location.x === 0), 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 = { type: "wrapper", 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; }, }); @@ -184,11 +182,6 @@ const moveWidgetsAndAppsInLeftSidebar = ( item.area.properties.location === "left" && item.shape[screenSize]?.location.x === 1, update: (item) => { - const screenSizeShape = item.shape[screenSize]; - if (!screenSizeShape) { - throw new OldHomarrScreenSizeError("kind" in item ? "widget" : "app", item.id, screenSize); - } - item.area = { type: "wrapper", properties: { @@ -196,6 +189,9 @@ const moveWidgetsAndAppsInLeftSidebar = ( }, }; + const screenSizeShape = item.shape[screenSize]; + if (!screenSizeShape) return; + screenSizeShape.location.x = 0; screenSizeShape.location.y += offset; }, @@ -222,16 +218,6 @@ const moveWidgetsAndAppsInRightSidebar = ( item.area.properties.location === "right" && (columnCount >= 2 || item.shape[screenSize]?.location.x === 0), 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 = { type: "wrapper", 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.x += xOffsetDelta; }, @@ -260,11 +254,6 @@ const moveWidgetsAndAppsInRightSidebar = ( item.area.properties.location === "left" && item.shape[screenSize]?.location.x === 1, update: (item) => { - const screenSizeShape = item.shape[screenSize]; - if (!screenSizeShape) { - throw new OldHomarrScreenSizeError("kind" in item ? "widget" : "app", item.id, screenSize); - } - item.area = { type: "wrapper", properties: { @@ -272,6 +261,9 @@ const moveWidgetsAndAppsInRightSidebar = ( }, }; + const screenSizeShape = item.shape[screenSize]; + if (!screenSizeShape) return; + screenSizeShape.location.x = 0; screenSizeShape.location.y += offset; }, @@ -312,16 +304,15 @@ const updateItems = (options: { for (const item of items) { const before = createItemSnapshot(item, options.screenSize); + options.update(item); + const screenSizeShape = item.shape[options.screenSize]; - if (!screenSizeShape) { - throw new OldHomarrScreenSizeError("kind" in item ? "widget" : "app", item.id, options.screenSize); - } + if (!screenSizeShape) return requiredHeight; if (screenSizeShape.location.y + screenSizeShape.size.height > requiredHeight) { requiredHeight = screenSizeShape.location.y + screenSizeShape.size.height; } - options.update(item); const after = createItemSnapshot(item, options.screenSize); logger.debug(