From 1615f88a2201c013155a2e683453653001089e38 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Fri, 21 Aug 2026 00:28:03 +0200 Subject: [PATCH] feat(widgets): allow creating and auto-selecting supported integrations directly from widget edit modal --- .../board/items/lazy-widget-edit-modal.tsx | 40 ++++++++++++++++++- .../widgets/src/modals/widget-edit-modal.tsx | 12 ++++++ .../widgets/src/widget-integration-select.tsx | 20 +++++++--- 3 files changed, 65 insertions(+), 7 deletions(-) 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 c745aeafd..0a6f448b2 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 @@ -1,16 +1,19 @@ "use client"; -import { Suspense, use } from "react"; +import { Suspense, use, useCallback, useMemo, useState } from "react"; import { Center, Loader } from "@mantine/core"; import { ErrorBoundary } from "react-error-boundary"; import type { FallbackProps } from "react-error-boundary"; +import { clientApi } from "@homarr/api/client"; import type { WidgetKind } from "@homarr/definitions"; -import { createModal, modalSizeForm } from "@homarr/modals"; +import { createModal, modalSizeForm, useModalAction } from "@homarr/modals"; import { WidgetError } from "@homarr/widgets/errors"; import type * as WidgetModalsModule from "@homarr/widgets/modals"; import type { WidgetEditModalProps } from "@homarr/widgets/modals"; +import type { IntegrationSelectOption } from "@homarr/widgets/widget-integration-select"; +import { IntegrationSelectModal } from "~/components/integration/integration-select-modal"; import type * as IntegrationEditFormModule from "~/components/integration/embedded-integration-edit-form"; import type { EmbeddedIntegrationEditFormProps } from "~/components/integration/embedded-integration-edit-form"; @@ -80,13 +83,46 @@ interface LazyWidgetEditModalContentProps { const LazyWidgetEditModalContent = (props: LazyWidgetEditModalContentProps) => { const { WidgetEditModal } = use(loadWidgetEditModal()); + const { openModal: openIntegrationModal } = useModalAction(IntegrationSelectModal); const Component = WidgetEditModal.component; + const utils = clientApi.useUtils(); + const [createdIntegrations, setCreatedIntegrations] = useState([]); + + const supportedKinds = useMemo(() => { + if (!("supportedIntegrations" in props.innerProps.definition)) return []; + return (props.innerProps.definition.supportedIntegrations ?? []).filter((k) => k !== "mock"); + }, [props.innerProps.definition]); + + const handleOpenNewIntegration = useCallback( + (onCreated?: (id: string) => void) => { + openIntegrationModal({ + allowedKinds: supportedKinds, + onSuccess: (result) => { + if (result?.integration) { + setCreatedIntegrations((prev) => [...prev, result.integration]); + void utils.integration.all.invalidate(); + onCreated?.(result.integration.id); + } + }, + }); + }, + [openIntegrationModal, supportedKinds, utils], + ); + + const combinedIntegrationData = useMemo(() => { + const existing = props.innerProps.integrationData; + const additional = createdIntegrations.filter((c) => !existing.some((e) => e.id === c.id)); + return [...existing, ...additional]; + }, [props.innerProps.integrationData, createdIntegrations]); + return ( 0 ? handleOpenNewIntegration : undefined, }} /> ); diff --git a/packages/widgets/src/modals/widget-edit-modal.tsx b/packages/widgets/src/modals/widget-edit-modal.tsx index 5de65c5a5..1bdf03f4c 100644 --- a/packages/widgets/src/modals/widget-edit-modal.tsx +++ b/packages/widgets/src/modals/widget-edit-modal.tsx @@ -58,6 +58,7 @@ export interface WidgetEditModalProps { appId?: string; integrationEditForm?: ComponentType; onIntegrationSaved?: () => void; + onOpenNewIntegration?: (onCreated?: (id: string) => void) => void; } export const getSelectedWidgetIntegrations = ( @@ -217,6 +218,16 @@ export const WidgetEditModal = createModal>(({ handleSubmit(event); }; + const handleOpenNewIntegration = innerProps.onOpenNewIntegration + ? () => { + innerProps.onOpenNewIntegration?.((newId) => { + if (newId) { + form.setFieldValue("integrationIds", [...(maxIntegrations > 1 ? form.values.integrationIds : []), newId]); + } + }); + } + : undefined; + const widgetFormContent = ( {canConfigureWidget && innerProps.integrationSupport && ( @@ -225,6 +236,7 @@ export const WidgetEditModal = createModal>(({ data={innerProps.integrationData} canSelectMultiple={maxIntegrations > 1} withAsterisk={integrationsRequired} + onOpenNewIntegration={handleOpenNewIntegration} {...form.getInputProps("integrationIds")} /> )} diff --git a/packages/widgets/src/widget-integration-select.tsx b/packages/widgets/src/widget-integration-select.tsx index ed82c4099..e24418395 100644 --- a/packages/widgets/src/widget-integration-select.tsx +++ b/packages/widgets/src/widget-integration-select.tsx @@ -32,6 +32,7 @@ interface WidgetIntegrationSelectProps { canSelectMultiple?: boolean; data: IntegrationSelectOption[]; withAsterisk?: boolean; + onOpenNewIntegration?: () => void; } export const WidgetIntegrationSelect = ({ data, @@ -40,6 +41,7 @@ export const WidgetIntegrationSelect = ({ label, canSelectMultiple = true, withAsterisk = false, + onOpenNewIntegration, ...props }: WidgetIntegrationSelectProps) => { const t = useI18n(); @@ -109,11 +111,19 @@ export const WidgetIntegrationSelect = ({ - - {t("widget.common.integration.manage")} - - + onOpenNewIntegration ? ( + + + {t("integration.action.create")} + + + ) : ( + + + {t("widget.common.integration.manage")} + + + ) } pointer onClick={() => combobox.openDropdown()}