diff --git a/eslint-suppressions.json b/eslint-suppressions.json index fdcf848a354..c704beaefc3 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -2110,11 +2110,6 @@ "count": 1 } }, - "public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx": { - "no-restricted-syntax": { - "count": 4 - } - }, "public/app/features/dashboard-scene/saving/SaveDashboardForm.tsx": { "no-restricted-syntax": { "count": 1 diff --git a/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx b/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx index 8f0a383d80f..896c2fa3474 100644 --- a/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx +++ b/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx @@ -1,5 +1,4 @@ -import debounce from 'debounce-promise'; -import { ChangeEvent, useState, useEffect } from 'react'; +import { ChangeEvent, useCallback, useEffect, useRef, useState } from 'react'; import { UseFormSetValue, useForm } from 'react-hook-form'; import { selectors } from '@grafana/e2e-selectors'; @@ -43,19 +42,48 @@ export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) { }, }); - const { errors, isValid, validatingFields } = formState; + const { errors, isValid } = formState; const formValues = watch(); const { state, onSaveDashboard } = useSaveDashboard(false); const [contentSent, setContentSent] = useState<{ title?: string; folderUid?: string }>({}); + const validationTimeoutRef = useRef(); + // Validate title on form mount to catch invalid default values useEffect(() => { trigger('title'); }, [trigger]); + // Cleanup timeout on unmount + useEffect(() => { + return () => { + clearTimeout(validationTimeoutRef.current); + }; + }, []); + + const handleTitleChange = useCallback( + (e: ChangeEvent) => { + setValue('title', e.target.value, { shouldDirty: true }); + clearTimeout(validationTimeoutRef.current); + validationTimeoutRef.current = setTimeout(() => { + trigger('title'); + }, 400); + }, + [setValue, trigger] + ); + const onSave = async (overwrite: boolean) => { + clearTimeout(validationTimeoutRef.current); + + const isTitleValid = await trigger('title'); + + // This prevents the race between the new input and old validation state + if (!isTitleValid) { + return; + } + const data = getValues(); const result = await onSaveDashboard(dashboard, { @@ -88,16 +116,7 @@ export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) { ); const saveButton = (overwrite: boolean) => { - const isTitleValidating = !!validatingFields.title; - - return ( - - ); + return ; }; function renderFooter(error?: Error) { const formValuesMatchContentSent = @@ -128,23 +147,27 @@ export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) { return (
onSave(false))}> - } invalid={!!errors.title} error={errors.title?.message}> + } + invalid={!!errors.title} + error={errors.title?.message} + > ) => { - setValue('title', e.target.value, { shouldValidate: true }); - }, 400)} /> } invalid={!!errors.description} error={errors.description?.message} @@ -159,7 +182,7 @@ export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) { /> - + { setValue('folder', { uid, title }); @@ -177,7 +200,7 @@ export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) { /> {!changeInfo.isNew && ( - + )}