From d83e9b2611215d42bc3cb878e34d45b2191bd793 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Thu, 28 May 2026 14:51:23 +0200 Subject: [PATCH 1/7] feat(widgets): add embedded app edit form for in-place app editing Add EmbeddedAppEditForm component that allows editing app properties (name, icon, URL, description) directly from the widget edit modal without navigating to the management pages. --- .../src/modals/embedded-app-edit-form.tsx | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 packages/widgets/src/modals/embedded-app-edit-form.tsx diff --git a/packages/widgets/src/modals/embedded-app-edit-form.tsx b/packages/widgets/src/modals/embedded-app-edit-form.tsx new file mode 100644 index 000000000..20d0125af --- /dev/null +++ b/packages/widgets/src/modals/embedded-app-edit-form.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { useImperativeHandle, useRef } from "react"; +import { Alert, Center, Loader, Stack } from "@mantine/core"; +import { IconInfoCircle } from "@tabler/icons-react"; + +import { clientApi } from "@homarr/api/client"; +import type { AppFormHandle } from "@homarr/forms-collection"; +import { AppForm } from "@homarr/forms-collection"; +import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; +import { useI18n } from "@homarr/translation/client"; + +export interface EmbeddedAppEditFormHandle { + submitIfDirty: () => void; +} + +interface EmbeddedAppEditFormProps { + appId: string; + handleRef: React.Ref; +} + +export const EmbeddedAppEditForm = ({ appId, handleRef }: EmbeddedAppEditFormProps) => { + const t = useI18n(); + const appFormRef = useRef(null); + const { data: app, isPending: isLoadingApp } = clientApi.app.byId.useQuery({ id: appId }); + + const { mutate, isPending: isMutating } = clientApi.app.update.useMutation({ + onSuccess: () => { + showSuccessNotification({ + title: t("app.page.edit.notification.success.title"), + message: t("app.page.edit.notification.success.message"), + }); + }, + onError: () => { + showErrorNotification({ + title: t("app.page.edit.notification.error.title"), + message: t("app.page.edit.notification.error.message"), + }); + }, + }); + + useImperativeHandle( + handleRef, + () => ({ + submitIfDirty: () => { + if (appFormRef.current?.isDirty()) { + appFormRef.current.submit(); + } + }, + }), + [], + ); + + if (isLoadingApp || !app) { + return ( +
+ +
+ ); + } + + return ( + + } color="blue" variant="light"> + {t("item.edit.app.propagationNotice")} + + { + mutate({ id: appId, ...values }); + }} + isPending={isMutating} + /> + + ); +}; From 3d1bb2f0badfe1f19ca7ad4a63b1748302162453 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Thu, 28 May 2026 14:55:15 +0200 Subject: [PATCH 2/7] feat(widgets): wire up tabbed app editing in widget edit modal - Add hideButtons/formRef props to AppForm for headless embedding - Add Mantine Tabs to WidgetEditModal for app widgets (permission-gated) - Pass appId from BoardItemMenu to the modal - Add translation keys for tab labels and propagation notice --- .../src/components/board/items/item-menu.tsx | 1 + .../forms-collection/src/new-app/_form.tsx | 78 +++++--- packages/translation/src/lang/en.json | 7 + .../widgets/src/modals/widget-edit-modal.tsx | 174 +++++++++++------- 4 files changed, 167 insertions(+), 93 deletions(-) diff --git a/apps/nextjs/src/components/board/items/item-menu.tsx b/apps/nextjs/src/components/board/items/item-menu.tsx index a7d870a28..b8296c1c3 100644 --- a/apps/nextjs/src/components/board/items/item-menu.tsx +++ b/apps/nextjs/src/components/board/items/item-menu.tsx @@ -85,6 +85,7 @@ const BoardItemMenuInner = ({ offset, item, resetErrorBoundary }: BoardItemMenuP ), integrationSupport: "supportedIntegrations" in currentDefinition, settings, + appId: item.kind === "app" ? (item.options.appId as string | undefined) : undefined, }, { title(t) { diff --git a/packages/forms-collection/src/new-app/_form.tsx b/packages/forms-collection/src/new-app/_form.tsx index 05c7e3f6f..9973ccb61 100644 --- a/packages/forms-collection/src/new-app/_form.tsx +++ b/packages/forms-collection/src/new-app/_form.tsx @@ -1,7 +1,7 @@ "use client"; import type { ChangeEventHandler } from "react"; -import { useEffect, useRef } from "react"; +import { useEffect, useImperativeHandle, useRef } from "react"; import { Button, Checkbox, Collapse, Group, Stack, Textarea, TextInput } from "@mantine/core"; import { useDebouncedValue, useDisclosure } from "@mantine/hooks"; import type { z } from "zod/v4"; @@ -17,6 +17,11 @@ import { findBestIconMatch } from "./icon-matcher"; type FormType = z.infer; +export interface AppFormHandle { + submit: () => void; + isDirty: () => boolean; +} + interface AppFormProps { showBackToOverview: boolean; buttonLabels: { @@ -26,6 +31,8 @@ interface AppFormProps { initialValues?: FormType; handleSubmit: (values: FormType, redirect: boolean, afterSuccess?: () => void) => void; isPending: boolean; + hideButtons?: boolean; + formRef?: React.Ref; } export const AppForm = ({ @@ -34,6 +41,8 @@ export const AppForm = ({ handleSubmit: originalHandleSubmit, initialValues, isPending, + hideButtons, + formRef, }: AppFormProps) => { const t = useI18n(); @@ -62,6 +71,15 @@ export const AppForm = ({ originalHandleSubmit(values, redirect, afterSuccess); }; + useImperativeHandle( + formRef, + () => ({ + submit: () => form.onSubmit(handleSubmit)(), + isDirty: () => form.isDirty(), + }), + [form, handleSubmit], + ); + const [opened, { open, close }] = useDisclosure((initialValues?.pingUrl?.length ?? 0) > 0); const handleClickDifferentUrlPing: ChangeEventHandler = () => { @@ -92,32 +110,32 @@ export const AppForm = ({ } }, [debouncedName, iconsData]); - return ( -
- - - -