fix(custom-widgets): simplify error sharing
This commit is contained in:
10 files changed
+65
-115
No files matched your search
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Button, Card, Group, Stack, Text, TextInput, Textarea } from "@mantine/core";
|
||||
import { IconCopy, IconSparkles } from "@tabler/icons-react";
|
||||
import { IconSparkles } from "@tabler/icons-react";
|
||||
|
||||
import type { HomarrCustomWidgetV2 } from "@homarr/custom-widgets/core";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
@@ -15,7 +15,6 @@ interface AiCardProps {
|
||||
documentationUrl: string;
|
||||
onDocumentationUrlChange(value: string): void;
|
||||
onPaste(): void;
|
||||
onCopyDiagnostics(): void;
|
||||
}
|
||||
|
||||
export function CustomWidgetAiCard(props: AiCardProps) {
|
||||
@@ -48,9 +47,6 @@ export function CustomWidgetAiCard(props: AiCardProps) {
|
||||
<Button type="button" variant="light" leftSection={<IconSparkles size={16} />} onClick={props.onPaste}>
|
||||
{t("paste")}
|
||||
</Button>
|
||||
<Button type="button" variant="subtle" leftSection={<IconCopy size={16} />} onClick={props.onCopyDiagnostics}>
|
||||
{t("copyDiagnostics")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Alert, Box, Button, List, Paper, SegmentedControl, Stack, Text, TextInput, Textarea } from "@mantine/core";
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
CopyButton,
|
||||
Paper,
|
||||
SegmentedControl,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Textarea,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconAlertCircle,
|
||||
IconApi,
|
||||
IconBraces,
|
||||
IconCheck,
|
||||
IconCode,
|
||||
IconCopy,
|
||||
IconDatabase,
|
||||
IconEye,
|
||||
IconSettings,
|
||||
@@ -119,21 +132,17 @@ export function CustomWidgetForm({ mode, initialValues, definitionId }: CustomWi
|
||||
[candidate, requestDiagnostics, templateDiagnostics],
|
||||
);
|
||||
|
||||
const { save, runPreview, pasteAiResponse, copyDiagnostics, saveIssues, savePending, previewPending } =
|
||||
useCustomWidgetFormActions({
|
||||
mode,
|
||||
definitionId,
|
||||
form,
|
||||
candidate,
|
||||
templateDiagnostics,
|
||||
requestDiagnostics,
|
||||
preview,
|
||||
setPreview,
|
||||
setMobilePane,
|
||||
setOptionsSnapshot,
|
||||
request,
|
||||
documentationUrl,
|
||||
});
|
||||
const { save, runPreview, pasteAiResponse, saveIssues, savePending, previewPending } = useCustomWidgetFormActions({
|
||||
mode,
|
||||
definitionId,
|
||||
form,
|
||||
candidate,
|
||||
preview,
|
||||
setPreview,
|
||||
setMobilePane,
|
||||
setOptionsSnapshot,
|
||||
});
|
||||
const saveIssueText = saveIssues.map((issue) => `${issue.path ? `${issue.path}: ` : ""}${issue.message}`).join("\n");
|
||||
|
||||
useEffect(() => {
|
||||
const beforeUnload = (event: BeforeUnloadEvent) => {
|
||||
@@ -169,14 +178,29 @@ export function CustomWidgetForm({ mode, initialValues, definitionId }: CustomWi
|
||||
<Text size="sm" mb="xs">
|
||||
{w("saveError.description")}
|
||||
</Text>
|
||||
<List size="sm" spacing={4}>
|
||||
{saveIssues.map((issue, index) => (
|
||||
<List.Item key={`${issue.path ?? "widget"}-${index}`}>
|
||||
{issue.path ? `${issue.path}: ` : ""}
|
||||
{issue.message}
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
<Textarea
|
||||
value={saveIssueText}
|
||||
readOnly
|
||||
autosize
|
||||
minRows={2}
|
||||
maxRows={10}
|
||||
aria-label={w("saveError.errors")}
|
||||
mb="xs"
|
||||
/>
|
||||
<CopyButton value={saveIssueText} timeout={2_000}>
|
||||
{({ copied, copy }) => (
|
||||
<Button
|
||||
type="button"
|
||||
size="compact-sm"
|
||||
variant="light"
|
||||
color={copied ? "green" : "red"}
|
||||
leftSection={copied ? <IconCheck size={14} /> : <IconCopy size={14} />}
|
||||
onClick={copy}
|
||||
>
|
||||
{copied ? w("saveError.copied") : w("saveError.copy")}
|
||||
</Button>
|
||||
)}
|
||||
</CopyButton>
|
||||
</Alert>
|
||||
)}
|
||||
<nav className={classes.sectionNav} aria-label={w("sectionNavigation")}>
|
||||
@@ -265,7 +289,6 @@ export function CustomWidgetForm({ mode, initialValues, definitionId }: CustomWi
|
||||
documentationUrl={documentationUrl}
|
||||
onDocumentationUrlChange={setDocumentationUrl}
|
||||
onPaste={() => void pasteAiResponse()}
|
||||
onCopyDiagnostics={() => void copyDiagnostics()}
|
||||
/>
|
||||
<Paper p="md" className={classes.mobileSaveBar} shadow="sm">
|
||||
<SaveActions
|
||||
|
||||
+2
-35
@@ -5,14 +5,13 @@ import type { Dispatch, SetStateAction } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { UseFormReturnType } from "@mantine/form";
|
||||
|
||||
import { clientApi, fetchApi } from "@homarr/api/client";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import {
|
||||
buildCustomWidgetFixPrompt,
|
||||
customWidgetDefinitionSchema,
|
||||
formatCustomWidgetImportIssues,
|
||||
parseCustomWidgetAiResponse,
|
||||
} from "@homarr/custom-widgets/core";
|
||||
import type { EditorDiagnostic, CustomWidgetFormValues } from "@homarr/custom-widgets/workbench";
|
||||
import type { CustomWidgetFormValues } from "@homarr/custom-widgets/workbench";
|
||||
import { showErrorNotification, showSuccessNotification, showWarningNotification } from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
@@ -26,14 +25,10 @@ interface FormActionsInput {
|
||||
definitionId?: string;
|
||||
form: UseFormReturnType<CustomWidgetFormValues>;
|
||||
candidate: ReturnType<typeof buildDefinition>;
|
||||
templateDiagnostics: EditorDiagnostic[];
|
||||
requestDiagnostics: EditorDiagnostic[];
|
||||
preview: PreviewState;
|
||||
setPreview: Dispatch<SetStateAction<PreviewState>>;
|
||||
setMobilePane: Dispatch<SetStateAction<"configure" | "preview">>;
|
||||
setOptionsSnapshot: Dispatch<SetStateAction<Record<string, unknown>>>;
|
||||
request: string;
|
||||
documentationUrl: string;
|
||||
}
|
||||
|
||||
export function useCustomWidgetFormActions(input: FormActionsInput) {
|
||||
@@ -44,7 +39,6 @@ export function useCustomWidgetFormActions(input: FormActionsInput) {
|
||||
const createMutation = clientApi.customWidget.create.useMutation();
|
||||
const updateMutation = clientApi.customWidget.update.useMutation();
|
||||
const previewMutation = clientApi.customWidget.previewCreate.useMutation();
|
||||
const [importIssues, setImportIssues] = useState<string[]>([]);
|
||||
const [saveIssues, setSaveIssues] = useState<CustomWidgetSaveIssue[]>([]);
|
||||
const [previewPending, setPreviewPending] = useState(false);
|
||||
|
||||
@@ -169,10 +163,8 @@ export function useCustomWidgetFormActions(input: FormActionsInput) {
|
||||
try {
|
||||
const result = parseCustomWidgetAiResponse(await navigator.clipboard.readText());
|
||||
if (!result.success) {
|
||||
setImportIssues(result.issues.map((issue) => `${issue.path?.join(".") ?? "widget"}: ${issue.message}`));
|
||||
throw new Error(formatCustomWidgetImportIssues(result.issues));
|
||||
}
|
||||
setImportIssues([]);
|
||||
applyDefinition(input.form, customWidgetDefinitionSchema.parse(result.widget));
|
||||
input.setPreview({ data: {}, status: {}, session: null, outcome: "idle" });
|
||||
showSuccessNotification({
|
||||
@@ -191,35 +183,10 @@ export function useCustomWidgetFormActions(input: FormActionsInput) {
|
||||
}
|
||||
};
|
||||
|
||||
const copyDiagnostics = async () => {
|
||||
try {
|
||||
const diagnostics = [...input.templateDiagnostics, ...input.requestDiagnostics]
|
||||
.map((entry) => `${entry.severity.toUpperCase()} ${entry.code}: ${entry.value ?? ""}`)
|
||||
.concat(importIssues.map((issue) => `IMPORT: ${issue}`))
|
||||
.join("\n");
|
||||
const journal = input.preview.session
|
||||
? await fetchApi.customWidget.previewJournal.query({ sessionId: input.preview.session.id }).catch(() => [])
|
||||
: [];
|
||||
await navigator.clipboard.writeText(
|
||||
buildCustomWidgetFixPrompt({
|
||||
currentConfig: input.candidate.success ? input.candidate.data : input.form.values,
|
||||
request: input.request,
|
||||
documentationUrl: input.documentationUrl,
|
||||
diagnostics,
|
||||
journal,
|
||||
}),
|
||||
);
|
||||
showSuccessNotification({ title: w("ai.diagnostics"), message: w("ai.diagnosticsCopied") });
|
||||
} catch {
|
||||
showErrorNotification({ title: w("ai.diagnostics"), message: t("notification.aiPromptCopyError") });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
save,
|
||||
runPreview,
|
||||
pasteAiResponse,
|
||||
copyDiagnostics,
|
||||
saveIssues,
|
||||
savePending: createMutation.isPending || updateMutation.isPending,
|
||||
previewPending,
|
||||
|
||||
Reference in new issue
Block a user