fix(custom-widgets): fix v2 editor crash and definition-not-found race (#6377)

This commit is contained in:
Thomas authored and ajnart committed 2026-08-13 19:47:16 +02:00
1 parent f0963405ca
commit 3a1245cbc5
8 files changed
+194 -21

No files matched your search

@@ -0,0 +1,54 @@
"use client";
import type { ErrorInfo, ReactNode } from "react";
import { Component } from "react";
import { Alert, Button, Stack, Text } from "@mantine/core";
import { IconAlertTriangle } from "@tabler/icons-react";
import { useScopedI18n } from "@homarr/translation/client";
interface FormErrorBoundaryState {
error: Error | null;
}
interface FormErrorBoundaryProps {
children: ReactNode;
}
export class FormErrorBoundary extends Component<FormErrorBoundaryProps, FormErrorBoundaryState> {
public state: FormErrorBoundaryState = { error: null };
public static getDerivedStateFromError(error: Error): FormErrorBoundaryState {
return { error };
}
public componentDidCatch(error: Error, _info: ErrorInfo) {
console.error("Custom widget form error:", error);
}
public render() {
if (this.state.error) {
return <FormErrorFallback error={this.state.error} reset={() => this.setState({ error: null })} />;
}
return this.props.children;
}
}
function FormErrorFallback({ error, reset }: { error: Error; reset: () => void }) {
const t = useScopedI18n("customWidget");
return (
<Alert color="red" variant="light" icon={<IconAlertTriangle size={16} />} p="md">
<Stack gap="xs">
<Text size="sm" fw={600}>
{t("editor.errorBoundary.title")}
</Text>
<Text size="xs" c="dimmed" style={{ fontFamily: "monospace" }}>
{error.message}
</Text>
<Button size="xs" variant="light" color="red" onClick={reset}>
{t("editor.errorBoundary.retry")}
</Button>
</Stack>
</Alert>
);
}
@@ -9,6 +9,7 @@ import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { catchTrpcNotFound } from "~/errors/trpc-catch-error";
import { CustomWidgetBetaBanner } from "../../_beta-banner";
import { CustomWidgetForm } from "../../_custom-widget-form";
import { FormErrorBoundary } from "../../_form-error-boundary";
const authTypeExpectedSecrets: Record<string, string[]> = {
bearer: ["apiKey"],
@@ -52,22 +53,24 @@ export default async function EditCustomWidgetPage(props: EditCustomWidgetPagePr
<Stack>
<Title>{definition.name}</Title>
<CustomWidgetBetaBanner />
<CustomWidgetForm
mode="edit"
definitionId={params.id}
initialValues={{
name: definition.name,
description: definition.description ?? "",
iconUrl: definition.iconUrl ?? "",
url: definition.url,
authType: definition.authType,
headerName: definition.headerName ?? "",
method: definition.method,
requestBody: definition.requestBody ?? "",
...displayValues,
secrets: buildInitialSecrets(definition.authType, definition.secrets),
}}
/>
<FormErrorBoundary>
<CustomWidgetForm
mode="edit"
definitionId={params.id}
initialValues={{
name: definition.name,
description: definition.description ?? "",
iconUrl: definition.iconUrl ?? "",
url: definition.url,
authType: definition.authType,
headerName: definition.headerName ?? "",
method: definition.method,
requestBody: definition.requestBody ?? "",
...displayValues,
secrets: buildInitialSecrets(definition.authType, definition.secrets),
}}
/>
</FormErrorBoundary>
</Stack>
</Container>
</>
@@ -7,6 +7,7 @@ import { getScopedI18n } from "@homarr/translation/server";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { CustomWidgetBetaBanner } from "../_beta-banner";
import { CustomWidgetForm } from "../_custom-widget-form";
import { FormErrorBoundary } from "../_form-error-boundary";
export default async function NewCustomWidgetPage() {
const session = await auth();
@@ -28,7 +29,9 @@ export default async function NewCustomWidgetPage() {
</Text>
</div>
<CustomWidgetBetaBanner />
<CustomWidgetForm mode="create" />
<FormErrorBoundary>
<CustomWidgetForm mode="create" />
</FormErrorBoundary>
</Stack>
</Container>
</>