From c43836e85a01ce8e78130290117b45e3ef589b73 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Sun, 12 Jul 2026 14:46:17 +0200 Subject: [PATCH] refactor(custom-widgets): simplify authoring flow --- .../_custom-widget-form.module.css | 34 ++ .../custom-widgets/_custom-widget-form.tsx | 342 +++++++++--------- .../custom-widgets/_custom-widget-preview.tsx | 34 -- packages/translation/src/lang/en.json | 3 +- 4 files changed, 209 insertions(+), 204 deletions(-) diff --git a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-form.module.css b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-form.module.css index 54bff70b6..3d65cc0a9 100644 --- a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-form.module.css +++ b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-form.module.css @@ -48,6 +48,40 @@ background: var(--mantine-color-body); } +.formFlow { + --flow-marker-size: 2.5rem; +} + +.formSection { + position: relative; + padding: 0 0 var(--mantine-spacing-xl) calc(var(--flow-marker-size) + var(--mantine-spacing-lg)); + scroll-margin-top: 5rem; + outline: none; +} + +.formSection:not(:last-child)::before { + position: absolute; + top: var(--flow-marker-size); + bottom: 0; + left: calc(var(--flow-marker-size) / 2); + width: 1px; + background: var(--mantine-color-default-border); + content: ""; +} + +.formSection:focus-visible { + border-radius: var(--mantine-radius-md); + outline: 2px solid var(--mantine-primary-color-filled); + outline-offset: 4px; +} + +.sectionMarker { + position: absolute; + top: 0; + left: 0; + z-index: 1; +} + .displayTypeOption { display: flex; align-items: flex-start; diff --git a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-form.tsx b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-form.tsx index 2a3599b17..50b3f4049 100644 --- a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-form.tsx +++ b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-form.tsx @@ -17,7 +17,6 @@ import { Select, SimpleGrid, Stack, - Stepper, Switch, Text, TextInput, @@ -30,7 +29,6 @@ import { IconActivityHeartbeat, IconAlertTriangle, IconBraces, - IconCheck, IconCode, IconHash, IconLayoutGrid, @@ -655,8 +653,16 @@ export function CustomWidgetForm({ mode, initialValues, definitionId }: CustomWi const [previewJson, setPreviewJson] = useState(null); const [previewFetchResult, setPreviewFetchResult] = useState(null); const [lastPreviewFingerprint, setLastPreviewFingerprint] = useState(null); - const [activeStep, setActiveStep] = useState(0); const [mobilePane, setMobilePane] = useState<"configure" | "preview">("configure"); + const formatSectionRef = useRef(null); + const connectionSectionRef = useRef(null); + const configureSectionRef = useRef(null); + const scrollToSection = useCallback((section: number) => { + const element = [formatSectionRef.current, connectionSectionRef.current, configureSectionRef.current][section]; + if (!element) return; + element.scrollIntoView({ behavior: "smooth", block: "start" }); + window.setTimeout(() => element.focus({ preventScroll: true }), 250); + }, []); const hasTestedRef = useRef(false); const mergedInitialValues = { ...defaultCreateValues, ...initialValues }; @@ -771,7 +777,7 @@ export function CustomWidgetForm({ mode, initialValues, definitionId }: CustomWi const focusStepForErrors = (errors: Record) => { const fields = Object.keys(errors); if (fields.some((field) => ["name", "description", "iconUrl", "displayType"].includes(field.split(".")[0] ?? ""))) { - setActiveStep(0); + scrollToSection(0); return; } if ( @@ -779,10 +785,10 @@ export function CustomWidgetForm({ mode, initialValues, definitionId }: CustomWi ["url", "method", "authType", "headerName", "requestBody", "secrets"].includes(field.split(".")[0] ?? ""), ) ) { - setActiveStep(1); + scrollToSection(1); return; } - setActiveStep(2); + scrollToSection(2); }; const handleSubmit = form.onSubmit(async (values) => { @@ -986,25 +992,12 @@ export function CustomWidgetForm({ mode, initialValues, definitionId }: CustomWi setLastPreviewFingerprint(null); }, []); - const NEXT_STEP_LABELS: Record = { - 0: t("steps.connection.label"), - 1: t("steps.configure.label"), - }; - - const STEP_FIELDS: Record = { - 0: ["name"], - 1: ["url"], - }; - - const handleNextStep = () => { - const fieldsToValidate = STEP_FIELDS[activeStep]; - if (!fieldsToValidate) { - setActiveStep((step) => Math.min(2, step + 1)); + const continueToSection = (currentSection: number, field: string, nextSection: number) => { + if (form.validateField(field).hasError) { + scrollToSection(currentSection); return; } - const hasErrors = fieldsToValidate.some((field) => form.validateField(field).hasError); - if (hasErrors) return; - setActiveStep((step) => Math.min(2, step + 1)); + scrollToSection(nextSection); }; return ( @@ -1021,26 +1014,20 @@ export function CustomWidgetForm({ mode, initialValues, definitionId }: CustomWi />
- { - if (target <= activeStep) { - setActiveStep(target); - return; - } - for (let step = activeStep; step < target; step++) { - const fields = STEP_FIELDS[step]; - if (fields?.some((field) => form.validateField(field).hasError)) return; - } - setActiveStep(target); - }} - allowNextStepsSelect - keepMounted - size="sm" - completedIcon={} - > - - + +
+ + + + +
+ + {t("steps.format.label")} + + + {t("steps.format.description")} + +
@@ -1065,141 +1052,158 @@ export function CustomWidgetForm({ mode, initialValues, definitionId }: CustomWi />
+ + +
- +
- -
- - - handleAuthTypeChange(value ?? "none")} - allowDeselect={false} - aria-label={t("field.authType")} - /> - -
- {showHeaderName[form.values.authType] && ( - - )} - {secretFields.map((field) => { - const secretIndex = form.values.secrets.findIndex((s) => s.kind === field.kind); - if (secretIndex === -1) return null; - const secret = form.values.secrets[secretIndex]; - if (!secret) return null; - const placeholder = - secret.hasValue && !secret.value ? t("secret.savedPlaceholder" as never) : undefined; - return field.isPassword ? ( - + + + + +
+ + {t("steps.connection.label")} + + + {t("steps.connection.description")} + +
+
+ + +