From 6bfa9c33d022ac6579d926cab263174f749aa8a8 Mon Sep 17 00:00:00 2001 From: ajnart Date: Thu, 13 Aug 2026 20:00:04 +0200 Subject: [PATCH] chore: reconcile grid layer after stack rebase --- PRODUCT.md | 4 + .../boards/(content)/_board-add-menu.tsx | 86 ------------------- .../boards/(content)/_edit-actions.tsx | 4 +- .../board/items/lazy-item-move-modal.tsx | 67 --------------- .../board/sections/grid/grid-portal-host.tsx | 10 ++- .../grid/test/grid-portal-host.spec.ts | 16 +++- .../board/sections/use-open-section-apps.ts | 26 +++++- packages/boards/src/layout-selection.ts | 5 +- .../migrations/mysql/meta/0042_snapshot.json | 2 +- 9 files changed, 54 insertions(+), 166 deletions(-) delete mode 100644 apps/nextjs/src/app/[locale]/boards/(content)/_board-add-menu.tsx delete mode 100644 apps/nextjs/src/components/board/items/lazy-item-move-modal.tsx diff --git a/PRODUCT.md b/PRODUCT.md index adcf7f24e..8b4a6c01a 100644 --- a/PRODUCT.md +++ b/PRODUCT.md @@ -1,5 +1,9 @@ # Product +## Register + +product + ## Users Homarr is used by self-hosters and administrators who assemble dashboards for services running across home labs and private infrastructure. They need dense, reliable controls and status at a glance, with enough guidance to configure advanced features without hiding how they work. diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_board-add-menu.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_board-add-menu.tsx deleted file mode 100644 index 0448e1097..000000000 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_board-add-menu.tsx +++ /dev/null @@ -1,86 +0,0 @@ -"use client"; - -import { useCallback } from "react"; -import { Group, Menu } from "@mantine/core"; -import { IconBox, IconBoxAlignTop, IconChevronDown, IconPlug, IconPlus, IconResize } from "@tabler/icons-react"; - -import { useSession } from "@homarr/auth/client"; -import { useRequiredBoard } from "@homarr/boards/context"; -import { useModalAction } from "@homarr/modals"; -import { AppSelectModal } from "@homarr/modals-collection"; -import { useI18n } from "@homarr/translation/client"; - -import { useItemActions } from "~/components/board/items/item-actions"; -import { ItemSelectModal } from "~/components/board/items/item-select-modal"; -import { useCategoryActions } from "~/components/board/sections/category/category-actions"; -import { CategoryEditModal } from "~/components/board/sections/category/category-edit-modal"; -import { useDynamicSectionActions } from "~/components/board/sections/dynamic/dynamic-actions"; -import { IntegrationSelectModal } from "~/components/integration/integration-select-modal"; -import { HeaderButton } from "~/components/layout/header/button"; - -export const BoardAddMenu = () => { - const { data: session } = useSession(); - const board = useRequiredBoard(); - const { openModal: openCategoryEditModal } = useModalAction(CategoryEditModal); - const { openModal: openItemSelectModal } = useModalAction(ItemSelectModal); - const { openModal: openAppSelectModal } = useModalAction(AppSelectModal); - const { openModal: openIntegrationSelectModal } = useModalAction(IntegrationSelectModal); - const { addCategoryToEnd } = useCategoryActions(); - const { addDynamicSection } = useDynamicSectionActions(); - const { createItem } = useItemActions(); - const t = useI18n(); - - const handleAddCategory = useCallback( - () => - openCategoryEditModal( - { - category: { id: "new", name: "" }, - onSuccess({ name }) { - addCategoryToEnd({ name }); - }, - submitLabel: t("section.category.create.submit"), - }, - { title: (translate) => translate("section.category.create.title") }, - ), - [addCategoryToEnd, openCategoryEditModal, t], - ); - - return ( - - - - - - - - - - - } onClick={() => openItemSelectModal({ boardId: board.id })}> - {t("item.action.create")} - - } - onClick={() => - openAppSelectModal({ - onSelect: (app) => createItem({ kind: "app", options: { appId: app.id } }), - withCreate: session?.user.permissions.includes("app-create") ?? false, - }) - } - > - {t("app.action.add")} - - } onClick={() => openIntegrationSelectModal({})}> - {t("integration.action.create")} - - - } onClick={handleAddCategory}> - {t("section.category.action.create")} - - } onClick={addDynamicSection}> - {t("section.dynamic.action.create")} - - - - ); -}; diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_edit-actions.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_edit-actions.tsx index c869e0d62..ca2990680 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_edit-actions.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_edit-actions.tsx @@ -4,6 +4,7 @@ import { Group, Menu } from "@mantine/core"; import { IconBox, IconChevronDown, IconLayoutGridAdd, IconPlug, IconPlus, IconResize } from "@tabler/icons-react"; import { useSession } from "@homarr/auth/client"; +import { useRequiredBoard } from "@homarr/boards/context"; import { useModalAction } from "@homarr/modals"; import { AppSelectModal } from "@homarr/modals-collection"; import { useI18n } from "@homarr/translation/client"; @@ -20,6 +21,7 @@ export default function BoardEditActions() { const AddMenu = () => { const { data: session } = useSession(); + const board = useRequiredBoard(); const { openModal: openItemSelectModal } = useModalAction(ItemSelectModal); const { openModal: openAppSelectModal } = useModalAction(AppSelectModal); const { openModal: openIntegrationSelectModal } = useModalAction(IntegrationSelectModal); @@ -28,7 +30,7 @@ const AddMenu = () => { const t = useI18n(); const handleSelectItem = () => { - openItemSelectModal(); + openItemSelectModal({ boardId: board.id }); }; const handleSelectApp = () => { diff --git a/apps/nextjs/src/components/board/items/lazy-item-move-modal.tsx b/apps/nextjs/src/components/board/items/lazy-item-move-modal.tsx deleted file mode 100644 index 836017bc1..000000000 --- a/apps/nextjs/src/components/board/items/lazy-item-move-modal.tsx +++ /dev/null @@ -1,67 +0,0 @@ -"use client"; - -import { Suspense, use } from "react"; -import { Center, Loader } from "@mantine/core"; -import { ErrorBoundary } from "react-error-boundary"; - -import { createModal, modalSizeForm } from "@homarr/modals"; -import { WidgetError } from "@homarr/widgets/errors"; - -import type { ItemMoveModalProps } from "./item-move-modal"; - -let itemMoveModalPromise: Promise | undefined; - -const loadItemMoveModal = () => { - if (itemMoveModalPromise) return itemMoveModalPromise; - - const currentPromise = import("./item-move-modal"); - itemMoveModalPromise = currentPromise; - void currentPromise.catch(() => { - if (itemMoveModalPromise === currentPromise) itemMoveModalPromise = undefined; - }); - return currentPromise; -}; - -export const preloadItemMoveModal = () => void loadItemMoveModal().catch(() => undefined); - -interface LazyItemMoveModalContentProps { - actions: { closeModal: () => void }; - innerProps: ItemMoveModalProps; -} - -const LazyItemMoveModalContent = (props: LazyItemMoveModalContentProps) => { - const { ItemMoveModal } = use(loadItemMoveModal()); - const Component = ItemMoveModal.component; - return ; -}; - -export const LazyItemMoveModal = createModal((props) => ( - ( -
- { - itemMoveModalPromise = undefined; - resetErrorBoundary(); - }} - /> -
- )} - > - - - - } - > - - -
-)).withOptions({ - defaultTitle(t) { - return t("item.moveResize.title"); - }, - size: modalSizeForm, -}); diff --git a/apps/nextjs/src/components/board/sections/grid/grid-portal-host.tsx b/apps/nextjs/src/components/board/sections/grid/grid-portal-host.tsx index 5e5e9a297..f08afa600 100644 --- a/apps/nextjs/src/components/board/sections/grid/grid-portal-host.tsx +++ b/apps/nextjs/src/components/board/sections/grid/grid-portal-host.tsx @@ -5,8 +5,10 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, use import { createPortal } from "react-dom"; import type { RouterOutputs } from "@homarr/api"; -import { useIntegrations } from "@homarr/auth/client"; +import { clientApi } from "@homarr/api/client"; +import { useSession } from "@homarr/auth/client"; import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context"; +import { useEditMode } from "@homarr/boards/edit-mode"; import { getRootSectionLane } from "@homarr/definitions"; import type { ContainerSectionItem, Section } from "~/app/[locale]/boards/_types"; @@ -33,7 +35,11 @@ const GridPortalHostContext = createContext(n */ export const BoardGridPortalHost = ({ children }: PropsWithChildren) => { const board = useRequiredBoard(); - const integrations = useIntegrations(); + const { data: session } = useSession(); + const [isEditMode] = useEditMode(); + const { data: integrations } = clientApi.integration.all.useQuery(undefined, { + enabled: Boolean(session) && isEditMode, + }); const containersRef = useRef>(new Map()); const [containers, setContainers] = useState>(() => new Map()); const [announcement, setAnnouncement] = useState({ id: 0, message: "" }); diff --git a/apps/nextjs/src/components/board/sections/grid/test/grid-portal-host.spec.ts b/apps/nextjs/src/components/board/sections/grid/test/grid-portal-host.spec.ts index f5b95d969..d38ea78de 100644 --- a/apps/nextjs/src/components/board/sections/grid/test/grid-portal-host.spec.ts +++ b/apps/nextjs/src/components/board/sections/grid/test/grid-portal-host.spec.ts @@ -12,8 +12,18 @@ const mocks = vi.hoisted(() => ({ }, })); +vi.mock("@homarr/api/client", () => ({ + clientApi: { + integration: { + all: { + useQuery: () => ({ data: undefined }), + }, + }, + }, +})); + vi.mock("@homarr/auth/client", () => ({ - useIntegrations: () => undefined, + useSession: () => ({ data: { user: { id: "user" } } }), })); vi.mock("@homarr/boards/context", () => ({ @@ -21,6 +31,10 @@ vi.mock("@homarr/boards/context", () => ({ useRequiredBoard: () => mocks.board, })); +vi.mock("@homarr/boards/edit-mode", () => ({ + useEditMode: () => [true], +})); + describe("board grid portal host", () => { let container: HTMLDivElement; let root: Root; diff --git a/apps/nextjs/src/components/board/sections/use-open-section-apps.ts b/apps/nextjs/src/components/board/sections/use-open-section-apps.ts index 2a373dedc..b8f7bb64f 100644 --- a/apps/nextjs/src/components/board/sections/use-open-section-apps.ts +++ b/apps/nextjs/src/components/board/sections/use-open-section-apps.ts @@ -1,13 +1,14 @@ -import { useCallback, useMemo } from "react"; +import { use, useCallback, useMemo } from "react"; import { clientApi } from "@homarr/api/client"; import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context"; import { useConfirmModal } from "@homarr/modals"; import { useSettings } from "@homarr/settings"; import { useI18n } from "@homarr/translation/client"; +import type { WidgetComponentProps } from "@homarr/widgets/definition"; +import { loadWidgetDefinition, reduceWidgetOptionsWithDefinition } from "@homarr/widgets/manifest"; import type { Item } from "~/app/[locale]/boards/_types"; -import { filterByItemKind } from "./filter-by-item-kind"; interface SectionItemLayout { layoutId: string; @@ -79,10 +80,27 @@ export const useOpenSectionApps = (sectionId: string, enabled: boolean) => { const settings = useSettings(); const { openConfirmModal } = useConfirmModal(); const t = useI18n(); + const appDefinition = use(loadWidgetDefinition("app")); const appIds = useMemo(() => { const items: Item[] = getSectionItemsForLayout(board, sectionId, currentLayoutId); - return Array.from(new Set(filterByItemKind(items, settings, "app").map((item) => item.options.appId))); - }, [board, currentLayoutId, sectionId, settings]); + return Array.from( + new Set( + items + .filter((item) => item.kind === "app") + .map( + (item) => + ( + reduceWidgetOptionsWithDefinition( + appDefinition, + settings, + item.options, + ) as WidgetComponentProps<"app">["options"] + ).appId, + ) + .filter((appId): appId is string => typeof appId === "string"), + ), + ); + }, [appDefinition, board, currentLayoutId, sectionId, settings]); const { data: apps = [], isLoading } = clientApi.app.byIds.useQuery(appIds, { enabled: enabled && appIds.length > 0, staleTime: 30_000, diff --git a/packages/boards/src/layout-selection.ts b/packages/boards/src/layout-selection.ts index 20503188f..ec6a87f04 100644 --- a/packages/boards/src/layout-selection.ts +++ b/packages/boards/src/layout-selection.ts @@ -5,10 +5,7 @@ interface ResponsiveLayout { breakpoint: number; } -export const getLayoutIdForViewportWidth = ( - layouts: readonly ResponsiveLayout[], - viewportWidth: number, -): string => { +export const getLayoutIdForViewportWidth = (layouts: readonly ResponsiveLayout[], viewportWidth: number): string => { if (layouts.length === 0) { throw new Error("Expected the board to contain a layout"); } diff --git a/packages/db/migrations/mysql/meta/0042_snapshot.json b/packages/db/migrations/mysql/meta/0042_snapshot.json index dd26483d7..ba6c70fa5 100644 --- a/packages/db/migrations/mysql/meta/0042_snapshot.json +++ b/packages/db/migrations/mysql/meta/0042_snapshot.json @@ -2347,7 +2347,7 @@ } }, "uniqueConstraints": {}, - "checkConstraints": {} + "checkConstraint": {} } }, "views": {},