feat(widgets): allow creating and auto-selecting supported integrations directly from widget edit modal

This commit is contained in:
Thomas Camlong committed 2026-08-21 00:28:03 +02:00
1 parent 81ea412721
commit 1615f88a22
3 files changed
+65 -7

No files matched your search

@@ -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<IntegrationSelectOption[]>([]);
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 (
<Component
{...props}
innerProps={{
...props.innerProps,
integrationData: combinedIntegrationData,
integrationEditForm: LazyIntegrationEditForm,
onOpenNewIntegration: supportedKinds.length > 0 ? handleOpenNewIntegration : undefined,
}}
/>
);
@@ -58,6 +58,7 @@ export interface WidgetEditModalProps<TSort extends WidgetKind> {
appId?: string;
integrationEditForm?: ComponentType<EmbeddedIntegrationEditFormProps>;
onIntegrationSaved?: () => void;
onOpenNewIntegration?: (onCreated?: (id: string) => void) => void;
}
export const getSelectedWidgetIntegrations = (
@@ -217,6 +218,16 @@ export const WidgetEditModal = createModal<WidgetEditModalProps<WidgetKind>>(({
handleSubmit(event);
};
const handleOpenNewIntegration = innerProps.onOpenNewIntegration
? () => {
innerProps.onOpenNewIntegration?.((newId) => {
if (newId) {
form.setFieldValue("integrationIds", [...(maxIntegrations > 1 ? form.values.integrationIds : []), newId]);
}
});
}
: undefined;
const widgetFormContent = (
<Stack>
{canConfigureWidget && innerProps.integrationSupport && (
@@ -225,6 +236,7 @@ export const WidgetEditModal = createModal<WidgetEditModalProps<WidgetKind>>(({
data={innerProps.integrationData}
canSelectMultiple={maxIntegrations > 1}
withAsterisk={integrationsRequired}
onOpenNewIntegration={handleOpenNewIntegration}
{...form.getInputProps("integrationIds")}
/>
)}
@@ -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 = ({
<PillsInput
inputWrapperOrder={["label", "input", "description", "error"]}
description={
<Text size="xs" span>
<Anchor size="xs" component={Link} target="_blank" href="/manage/integrations">
{t("widget.common.integration.manage")}
</Anchor>
</Text>
onOpenNewIntegration ? (
<Text size="xs" span>
<Anchor size="xs" component="button" type="button" onClick={onOpenNewIntegration}>
{t("integration.action.create")}
</Anchor>
</Text>
) : (
<Text size="xs" span>
<Anchor size="xs" component={Link} target="_blank" href="/manage/integrations">
{t("widget.common.integration.manage")}
</Anchor>
</Text>
)
}
pointer
onClick={() => combobox.openDropdown()}