import { ADDITIONAL_PROPERTY_FLAG, FormContextType, getUiOptions, RJSFSchema, StrictRJSFSchema, WrapIfAdditionalTemplateProps, } from "@rjsf/utils"; import { useEffect, useMemo, useState, type FocusEvent } from "react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { cn } from "@/lib/utils"; import { useTranslation } from "react-i18next"; import { LuTrash2 } from "react-icons/lu"; import type { ConfigFormContext } from "@/types/configForm"; export function WrapIfAdditionalTemplate< T = unknown, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = FormContextType, >(props: WrapIfAdditionalTemplateProps) { const { classNames, style, children, disabled, id, label, displayLabel, onRemoveProperty, onKeyRenameBlur, readonly, required, registry, schema, uiSchema, } = props; const { t } = useTranslation(["views/settings"]); const additional = ADDITIONAL_PROPERTY_FLAG in schema; const uiOptions = getUiOptions(uiSchema); const keyIsReadonly = uiOptions.additionalPropertyKeyReadonly === true; const keyLabelKey = typeof uiOptions.additionalPropertyKeyLabel === "string" ? uiOptions.additionalPropertyKeyLabel : undefined; const keyPlaceholderKey = typeof uiOptions.additionalPropertyKeyPlaceholder === "string" ? uiOptions.additionalPropertyKeyPlaceholder : undefined; const keyPattern = typeof uiOptions.additionalPropertyKeyPattern === "string" ? uiOptions.additionalPropertyKeyPattern : undefined; const preventKeyRename = uiOptions.preventKeyRename === true; const formContext = registry?.formContext as ConfigFormContext | undefined; // optionally, lock the key once it's been saved const baseline = formContext?.baselineFormData; const keyLocked = preventKeyRename && typeof label === "string" && !!baseline && Object.prototype.hasOwnProperty.call(baseline, label); // controlled key value so we can validate live and block invalid renames. const [keyValue, setKeyValue] = useState(label ?? ""); useEffect(() => { setKeyValue(label ?? ""); }, [label]); const keyRegex = useMemo( () => (keyPattern ? new RegExp(keyPattern) : undefined), [keyPattern], ); const keyError = useMemo(() => { if (!keyRegex || keyLocked) return null; if (!keyRegex.test(keyValue)) { return t("configForm.additionalProperties.keyPatternError", { ns: "views/settings", defaultValue: "Use only letters, numbers, hyphens, and underscores (no spaces)", }); } return null; }, [keyRegex, keyLocked, keyValue, t]); if (!additional) { return (
{children}
); } const keyId = `${id}-key`; const keyLabel = keyLabelKey ? t(keyLabelKey, { ns: "views/settings" }) : t("configForm.additionalProperties.keyLabel", { ns: "views/settings" }); const valueLabel = t("configForm.additionalProperties.valueLabel", { ns: "views/settings", }); const keyPlaceholder = keyPlaceholderKey ? t(keyPlaceholderKey, { ns: "views/settings" }) : t("configForm.additionalProperties.keyPlaceholder", { ns: "views/settings", }); const removeLabel = t("configForm.additionalProperties.remove", { ns: "views/settings", }); const commitKeyRename = (e: FocusEvent) => { if (readonly) return; if (keyError) return; onKeyRenameBlur?.(e); }; return (
{!keyIsReadonly && (
{displayLabel && } {keyLocked ? (
{label}
) : ( <> setKeyValue(e.target.value)} onBlur={!readonly ? commitKeyRename : undefined} aria-invalid={keyError ? true : undefined} /> {keyError && (

{keyError}

)} )}
)}
{!keyIsReadonly && displayLabel && ( )}
{children}
{!keyIsReadonly && (
)}
); } export default WrapIfAdditionalTemplate;