From c6d6f43c85e3989a6ce72ced93b48ec6e9c227f7 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Mon, 17 Aug 2026 16:21:44 +0200 Subject: [PATCH] feat(board): remove live widget preview, streamline integration editing - Drop the narrow live preview panel from the widget picker (it kept rendering oversized widgets with errors) along with its translations - The Edit integration tab now opens the connection editor directly when a widget has exactly one integration - The connection editor closes on outside click - Keep the save/cancel footer floating at the bottom of the editor --- .../board/items/item-select-modal.tsx | 43 ------ .../board/items/lazy-widget-edit-modal.tsx | 2 +- .../board/items/widget-preview-panel.tsx | 138 ------------------ .../embedded-integration-edit-modal.tsx | 2 +- packages/translation/src/lang/en.json | 4 - .../widgets/src/modals/widget-edit-modal.tsx | 44 ++++-- 6 files changed, 36 insertions(+), 197 deletions(-) delete mode 100644 apps/nextjs/src/components/board/items/widget-preview-panel.tsx diff --git a/apps/nextjs/src/components/board/items/item-select-modal.tsx b/apps/nextjs/src/components/board/items/item-select-modal.tsx index f81c3fb21..2d163c89d 100644 --- a/apps/nextjs/src/components/board/items/item-select-modal.tsx +++ b/apps/nextjs/src/components/board/items/item-select-modal.tsx @@ -44,7 +44,6 @@ import { loadWidgetDefinition, reduceWidgetOptionsWithDefinition } from "@homarr import { IntegrationSelectModal } from "~/components/integration/integration-select-modal"; import { useSetupAnalytics } from "~/components/create/setup-analytics"; import type { EmptySection } from "~/app/[locale]/boards/_types"; -import { WidgetPreviewPanel } from "./widget-preview-panel"; import { useItemActions } from "./item-actions"; import type { WidgetConnectionStatus } from "./item-select-data"; import { @@ -65,8 +64,6 @@ interface ItemSelectModalContentProps { initialSearch?: string; } -type PreviewState = { kind: WidgetKind; options?: Record; label?: string } | null; - const ItemSelectModalContent = ({ actions, integrationData, @@ -112,7 +109,6 @@ const ItemSelectModalContent = ({ ); const [targetSectionId, setTargetSectionId] = useState(placementOptions[0]?.value ?? null); const [loadingSelection, setLoadingSelection] = useState(null); - const [preview, setPreview] = useState(null); const selectionLock = useRef(false); const { createItem, removeItem } = useItemActions(); @@ -132,33 +128,6 @@ const ItemSelectModalContent = ({ [integrationData], ); - const previewIntegrationIds = useMemo(() => { - if (!preview?.kind) return []; - const supported = (widgetIntegrationSupport[preview.kind] ?? []).filter((integration) => integration !== "mock"); - return (integrationData ?? []) - .filter(({ permissions }) => permissions.hasUseAccess) - .filter(({ kind }) => (supported as readonly string[]).includes(kind)) - .map(({ id }) => id); - }, [integrationData, preview]); - - const handlePreviewCustomWidget = (customWidgetDefinition: NonNullable[number]) => { - void loadWidgetDefinition("customApi") - .then((definition) => { - const defaultOptions = reduceWidgetOptionsWithDefinition(definition, settings); - setPreview({ - kind: "customApi", - label: customWidgetDefinition.name, - options: { - ...defaultOptions, - definitionId: customWidgetDefinition.id, - configuration: customWidgetDefinition.defaultOptions, - configurationVersion: customWidgetDefinition.updatedAt.getTime(), - }, - }); - }) - .catch(() => undefined); - }; - const items = useMemo( () => widgetKinds @@ -442,12 +411,10 @@ const ItemSelectModalContent = ({ onFocus={() => { void loadWidgetDefinition(item.kind).catch(() => undefined); preloadWidgetEditModal(); - setPreview({ kind: item.kind }); }} onPointerEnter={() => { void loadWidgetDefinition(item.kind).catch(() => undefined); preloadWidgetEditModal(); - setPreview({ kind: item.kind }); }} disabled={loadingSelection !== null} loading={loadingSelection === item.kind} @@ -478,11 +445,9 @@ const ItemSelectModalContent = ({ onSelect={() => void handleAddCustomWidget(def)} onFocus={() => { void loadWidgetDefinition("customApi").catch(() => undefined); - handlePreviewCustomWidget(def); }} onPointerEnter={() => { void loadWidgetDefinition("customApi").catch(() => undefined); - handlePreviewCustomWidget(def); }} > @@ -528,14 +493,6 @@ const ItemSelectModalContent = ({ )} - - - ); }; diff --git a/apps/nextjs/src/components/board/items/lazy-widget-edit-modal.tsx b/apps/nextjs/src/components/board/items/lazy-widget-edit-modal.tsx index 719a5716e..8dc014b80 100644 --- a/apps/nextjs/src/components/board/items/lazy-widget-edit-modal.tsx +++ b/apps/nextjs/src/components/board/items/lazy-widget-edit-modal.tsx @@ -79,7 +79,7 @@ const LazyIntegrationEditModal = createModal( defaultTitle: (t) => t("item.edit.tab.integration"), size: modalSizeForm, presentation: "inspector", - closeOnClickOutside: false, + closeOnClickOutside: true, }); interface LazyWidgetEditModalContentProps { diff --git a/apps/nextjs/src/components/board/items/widget-preview-panel.tsx b/apps/nextjs/src/components/board/items/widget-preview-panel.tsx deleted file mode 100644 index 6438783d8..000000000 --- a/apps/nextjs/src/components/board/items/widget-preview-panel.tsx +++ /dev/null @@ -1,138 +0,0 @@ -"use client"; - -import { Suspense, use } from "react"; -import { Center, Loader, Paper, Stack, Text, ThemeIcon } from "@mantine/core"; -import { IconZoomIn } from "@tabler/icons-react"; -import { QueryErrorResetBoundary } from "@tanstack/react-query"; -import { ErrorBoundary } from "react-error-boundary"; - -import type { WidgetKind } from "@homarr/definitions"; -import { widgetDefaultSizes } from "@homarr/definitions"; -import { useSettings } from "@homarr/settings"; -import { useI18n } from "@homarr/translation/client"; -import { WidgetError } from "@homarr/widgets/errors"; -import { loadWidgetResources, reduceWidgetOptionsWithDefinition } from "@homarr/widgets/manifest"; - -const PANEL_WIDTH = 320; - -const MAX_PREVIEW_HEIGHT = 260; - -const fitInBounds = (minHeight: number, width: number, height: number, maxWidth: number, maxHeight: number) => { - const scale = Math.min(maxWidth / width, maxHeight / height, 1); - const scaledWidth = Math.min(Math.max(width * scale, 160), maxWidth); - const scaledHeight = Math.min(Math.max(height * scale, minHeight), maxHeight); - return { width: scaledWidth, height: scaledHeight }; -}; - -export const getPreviewSize = (kind: WidgetKind) => { - const base = widgetDefaultSizes[kind] ?? { width: 1, height: 1 }; - return fitInBounds(140, base.width * 56, base.height * 56, PANEL_WIDTH - 32, MAX_PREVIEW_HEIGHT); -}; - -interface WidgetPreviewPanelProps { - kind: WidgetKind | null; - integrationIds: string[]; - options?: Record; - label?: string; -} - -export const WidgetPreviewPanel = ({ kind, integrationIds, options, label }: WidgetPreviewPanelProps) => { - const t = useI18n(); - - return ( - - - {label ?? t("item.create.preview.label")} - - {kind ? : } - - ); -}; - -const Placeholder = () => { - const t = useI18n(); - - return ( -
- - - - - - {t("item.create.preview.placeholder")} - - -
- ); -}; - -const PreviewContent = ({ - kind, - integrationIds, - options, -}: { - kind: WidgetKind; - integrationIds: string[]; - options?: Record; -}) => { - const { width, height } = getPreviewSize(kind); - - return ( -
- - -
- } - > - - - - ); -}; - -const InnerPreview = ({ - kind, - integrationIds, - options, - width, - height, -}: { - kind: WidgetKind; - integrationIds: string[]; - options?: Record; - width: number; - height: number; -}) => { - const settings = useSettings(); - const { definition, Component } = use(loadWidgetResources(kind)); - const defaultOptions = reduceWidgetOptionsWithDefinition(definition, settings, {}); - const mergedOptions = { ...defaultOptions, ...options }; - - return ( - - {({ reset }) => ( - ( - - )} - > - - undefined} - /> - - - )} - - ); -}; diff --git a/apps/nextjs/src/components/integration/embedded-integration-edit-modal.tsx b/apps/nextjs/src/components/integration/embedded-integration-edit-modal.tsx index 10849c67d..7efd63297 100644 --- a/apps/nextjs/src/components/integration/embedded-integration-edit-modal.tsx +++ b/apps/nextjs/src/components/integration/embedded-integration-edit-modal.tsx @@ -82,5 +82,5 @@ export const EmbeddedIntegrationEditModal = createModal t("item.edit.tab.integration"), size: modalSizeForm, presentation: "inspector", - closeOnClickOutside: false, + closeOnClickOutside: true, }); diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 4c68d23da..731d1850d 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -2762,10 +2762,6 @@ "title": "Unable to add item", "message": "The item could not be added to this board" } - }, - "preview": { - "label": "Live preview", - "placeholder": "Select a widget to preview its appearance" } }, "moveResize": { diff --git a/packages/widgets/src/modals/widget-edit-modal.tsx b/packages/widgets/src/modals/widget-edit-modal.tsx index 459bd93e3..d840b3076 100644 --- a/packages/widgets/src/modals/widget-edit-modal.tsx +++ b/packages/widgets/src/modals/widget-edit-modal.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react"; import type { FormEvent } from "react"; -import { Button, Group, Stack, Tabs, Text } from "@mantine/core"; +import { Box, Button, Group, Stack, Tabs, Text } from "@mantine/core"; import { schemaResolver } from "@mantine/form"; import { IconPencil } from "@tabler/icons-react"; import { z } from "zod/v4"; @@ -127,6 +127,17 @@ export const WidgetEditModal = createModal>(({ if (activeTab === "app" && !showAppTab) setActiveTab("widget"); }, [activeTab, showAppTab, showIntegrationTab]); + const handleTabChange = (value: string | null) => { + if (value === "integration" && selectedIntegrations.length === 1) { + const integration = selectedIntegrations[0]; + if (integration) { + innerProps.onEditIntegration?.(integration.id); + return; + } + } + setActiveTab(value); + }; + const handleSubmit = form.onSubmit(async (values) => { setIsSubmitting(true); try { @@ -229,7 +240,7 @@ export const WidgetEditModal = createModal>(({ {showResourceTabs ? ( - + {t("item.edit.tab.widget")} {showAppTab && {t("item.edit.tab.app")}} @@ -286,14 +297,27 @@ export const WidgetEditModal = createModal>(({ )} - - - - + + + + + + ) : ( widgetFormContent