import { type FormEvent, useEffect, useId, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import * as Dialog from "@/components/dialog/Dialog"; import { Input } from "@/components/inputs/Input"; import { Button } from "@/components/buttons/Button"; import { DialogActions } from "@/components/dialog/DialogActions"; import { Label } from "@/components/typography/Label"; import { HelpText } from "@/components/typography/HelpText"; import { ManagementServerSwitch } from "@/components/ManagementServerSwitch"; import { CLOUD_MANAGEMENT_URL, ManagementMode, checkManagementUrlReachable, isValidManagementUrl, normalizeManagementUrl, } from "@/hooks/useManagementUrl"; import { useRestrictions } from "@/contexts/RestrictionsContext.tsx"; export type ProfileFormInitial = { name: string; managementUrl: string; }; type Props = { open: boolean; onOpenChange: (open: boolean) => void; onSubmit: (name: string, managementUrl: string) => void | Promise; initial?: ProfileFormInitial; }; const MAX_PROFILE_NAME_LEN = 128; export const ProfileCreationModal = ({ open, onOpenChange, onSubmit, initial }: Props) => { const { t } = useTranslation(); const { mdm } = useRestrictions(); const managedManagementUrl = mdm.managementURL; const nameId = useId(); const urlId = useId(); const isEdit = !!initial; const initialModeFromUrl = (u: string): ManagementMode => u && u !== CLOUD_MANAGEMENT_URL ? ManagementMode.SelfHosted : ManagementMode.Cloud; const initialSelfHostedUrl = (u: string): string => (u && u !== CLOUD_MANAGEMENT_URL ? u : ""); const [name, setName] = useState(initial?.name ?? ""); const [nameError, setNameError] = useState(null); const nameRef = useRef(null); const [mode, setMode] = useState( initial ? initialModeFromUrl(initial.managementUrl) : ManagementMode.Cloud, ); const [url, setUrl] = useState(initial ? initialSelfHostedUrl(initial.managementUrl) : ""); const [urlError, setUrlError] = useState(null); const [unreachable, setUnreachable] = useState(false); const [checking, setChecking] = useState(false); const urlRef = useRef(null); useEffect(() => { if (open) { setName(initial?.name ?? ""); setMode(initial ? initialModeFromUrl(initial.managementUrl) : ManagementMode.Cloud); setUrl(initial ? initialSelfHostedUrl(initial.managementUrl) : ""); setNameError(null); setUrlError(null); setUnreachable(false); setChecking(false); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [open, initial?.name, initial?.managementUrl]); const initialModeRef = useRef(ManagementMode.Cloud); useEffect(() => { if (!open) return; initialModeRef.current = mode; const id = globalThis.setTimeout(() => { nameRef.current?.focus(); nameRef.current?.select(); }, 0); return () => globalThis.clearTimeout(id); // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); // When the user toggles to Self-hosted inside the dialog (not on initial // open), move focus to the URL input so they can start typing immediately. useEffect(() => { if (!open) return; if (mode === initialModeRef.current) return; if (mode !== ManagementMode.SelfHosted) return; urlRef.current?.focus(); }, [open, mode]); useEffect(() => { setUrlError(null); setUnreachable(false); }, [url, mode]); const resolveTargetUrl = (): { url: string; needsReachCheck: boolean } | null => { if (managedManagementUrl) { return { url: managedManagementUrl, needsReachCheck: false }; } if (mode === ManagementMode.Cloud) { return { url: CLOUD_MANAGEMENT_URL, needsReachCheck: false }; } const trimmed = url.trim(); if (!trimmed || !isValidManagementUrl(trimmed)) { setUrlError(t("settings.general.management.urlError")); urlRef.current?.focus(); return null; } const target = normalizeManagementUrl(trimmed); const unchanged = target === initial?.managementUrl; return { url: target, needsReachCheck: !unchanged }; }; const handleSubmit = async (e: FormEvent) => { e.preventDefault(); if (checking) return; const sanitized = name.trim(); if (sanitized.length === 0) { setNameError(t("profile.dialog.required")); nameRef.current?.focus(); return; } const target = resolveTargetUrl(); if (!target) return; if (target.needsReachCheck) { setChecking(true); const reachable = await checkManagementUrlReachable(target.url); setChecking(false); if (!reachable && !unreachable) { setUnreachable(true); return; } } await onSubmit(sanitized, target.url); onOpenChange(false); }; const handleNameChange = (value: string) => { setName(value); if (nameError) setNameError(null); }; const trimmedUrl = url.trim(); const showUrlSyntaxError = mode === ManagementMode.SelfHosted && trimmedUrl !== "" && !isValidManagementUrl(trimmedUrl); const urlInputError = showUrlSyntaxError ? t("settings.general.management.urlError") : (urlError ?? undefined); const urlInputWarning = !urlInputError && unreachable ? t("profile.dialog.urlUnreachable") : undefined; return ( { e.preventDefault(); // Focus + select-all so editing an existing name is one // keystroke away from overwriting it. nameRef.current?.focus(); nameRef.current?.select(); }} >
{t("profile.dialog.description")}
handleNameChange(e.target.value)} error={nameError ?? undefined} maxLength={MAX_PROFILE_NAME_LEN} spellCheck={false} autoComplete={"off"} autoCapitalize={"off"} />
{!managedManagementUrl && (
{t("profile.dialog.managementHelp")}
{mode === ManagementMode.SelfHosted && ( setUrl(e.target.value)} error={urlInputError} warning={urlInputWarning} spellCheck={false} autoComplete={"off"} autoCorrect={"off"} autoCapitalize={"off"} /> )}
)}
); };