"use client"; import dynamic from "next/dynamic"; import { useEffect, useMemo, useState } from "react"; import { json } from "@codemirror/lang-json"; import { Accordion, Alert, ColorInput, Fieldset, MultiSelect, NumberInput, Select, Slider, Stack, Switch, Text, Textarea, TextInput, } from "@mantine/core"; import { DateInput, TimeInput } from "@mantine/dates"; import { IconAlertTriangle, IconBraces } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; import { useOptionalBoard } from "@homarr/boards/context"; import { resolveCustomWidgetOptionsBinding, validateCustomWidgetOptions } from "@homarr/custom-widgets/core"; import { IconPicker } from "@homarr/forms-collection"; import { useScopedI18n } from "@homarr/translation/client"; import type { CommonWidgetInputProps } from "./common"; import { useFormContext } from "./form"; const CodeMirror = dynamic(() => import("@uiw/react-codemirror").then((module) => module.default), { ssr: false }); type Schema = Record; export const WidgetCustomWidgetConfigurationInput = ({ property, }: CommonWidgetInputProps<"customWidgetConfiguration">) => { const form = useFormContext(); const labels = useScopedI18n("widget.customApi.configuration"); const board = useOptionalBoard(); const definitionId = typeof form.values.options.definitionId === "string" ? form.values.options.definitionId : ""; const configurationValue = form.values.options[property]; const configuration = useMemo(() => (isRecord(configurationValue) ? configurationValue : {}), [configurationValue]); const available = clientApi.customWidget.available.useQuery( { boardId: board?.id ?? "", currentId: definitionId || undefined }, { enabled: board !== null && Boolean(definitionId) }, ); const definition = available.data?.find((candidate) => candidate.id === definitionId); const schema = isRecord(definition?.optionsSchema) ? definition.optionsSchema : null; const issues = useMemo( () => (schema ? validateCustomWidgetOptions(schema, configuration) : []), [configuration, schema], ); useEffect(() => { const path = `options.${property}`; if (issues[0]) form.setFieldError(path, `${issues[0].path}: ${issues[0].message}`); else form.clearFieldError(path); }, [form, issues, property]); if (!definitionId || !schema) return null; const properties = getEffectiveProperties(schema, configuration); const required = getEffectiveRequired(schema, configuration); const entries = sortOptionEntries(properties); const regular = entries.filter(([, child]) => !isAdvancedOption(child)); const advanced = entries.filter(([, child]) => isAdvancedOption(child)); return ( {issues.length > 0 && ( }> {labels("needsAttention", { message: issues[0]?.message ?? "" })} )} {regular.map(([name, candidate]) => isRecord(candidate) ? ( ) : null, )} {advanced.length > 0 && ( {labels("advancedOptions")} {advanced.map(([name, candidate]) => isRecord(candidate) ? ( ) : null, )} )} form.setFieldValue(`options.${property}`, value)} /> ); }; function OptionField({ name, schema, path, configuration, required = false, }: { name: string; schema: Schema; path: string; configuration: Record; required?: boolean; }) { const form = useFormContext(); const presentation = isRecord(schema["x-homarr"]) ? schema["x-homarr"] : {}; const label = typeof schema.title === "string" ? schema.title : humanize(name); const description = typeof schema.description === "string" ? schema.description : undefined; const placeholder = typeof presentation.placeholder === "string" ? presentation.placeholder : undefined; const control = typeof presentation.control === "string" ? presentation.control : inferControl(schema); const input = form.getInputProps(path, { type: control === "switch" ? "checkbox" : "input" }); if (control === "json") { return ( form.setFieldValue(path, value)} /> ); } if (schema.type === "object" && isRecord(schema.properties)) { const value = isRecord(input.value) ? input.value : {}; const properties = getEffectiveProperties(schema, value); const nestedRequired = getEffectiveRequired(schema, value); return (
{sortOptionEntries(properties).map(([childName, child]) => isRecord(child) ? ( ) : null, )}
); } const choices = getChoices(control === "multi-select" && isRecord(schema.items) ? schema.items : schema); if (isRecord(presentation.optionsSource)) { return ( ); } if (control === "switch") return ; if (control === "number") return ; if (control === "duration") return ; if (control === "slider") { return ( {label} {required ? " *" : ""} {description && ( {description} )} ); } if (control === "select") { const numeric = schema.type === "number" || schema.type === "integer"; return ( ); } if (control === "textarea") return (