diff --git a/apps/dashboard/src/components/workflow-editor/workflow-editor-provider.tsx b/apps/dashboard/src/components/workflow-editor/workflow-editor-provider.tsx index 4f34b88e707..f21d3bac19a 100644 --- a/apps/dashboard/src/components/workflow-editor/workflow-editor-provider.tsx +++ b/apps/dashboard/src/components/workflow-editor/workflow-editor-provider.tsx @@ -1,23 +1,12 @@ -import { ReactNode, useMemo, useCallback, useLayoutEffect, useState } from 'react'; +import { ReactNode, useCallback, useLayoutEffect, useMemo, useState } from 'react'; +import { useFieldArray, useForm } from 'react-hook-form'; import { useBlocker, useNavigate, useParams } from 'react-router-dom'; -import { useForm, useFieldArray } from 'react-hook-form'; // eslint-disable-next-line // @ts-ignore import { zodResolver } from '@hookform/resolvers/zod'; -import * as z from 'zod'; import { WorkflowOriginEnum, WorkflowResponseDto } from '@novu/shared'; +import * as z from 'zod'; -import { WorkflowEditorContext } from './workflow-editor-context'; -import { StepTypeEnum } from '@/utils/enums'; -import { Form } from '../primitives/form/form'; -import { buildRoute, ROUTES } from '@/utils/routes'; -import { useEnvironment } from '@/context/environment/hooks'; -import { workflowSchema } from './schema'; -import { useFetchWorkflow, useUpdateWorkflow, useFormAutoSave } from '@/hooks'; -import { Step } from '@/utils/types'; -import { showToast } from '../primitives/sonner-helpers'; -import { ToastIcon } from '../primitives/sonner'; -import { handleValidationIssues } from '@/utils/handleValidationIssues'; import { AlertDialog, AlertDialogAction, @@ -29,8 +18,20 @@ import { AlertDialogTitle, } from '@/components/primitives/alert-dialog'; import { buttonVariants } from '@/components/primitives/button'; -import { RiAlertFill } from 'react-icons/ri'; import { Separator } from '@/components/primitives/separator'; +import { useEnvironment } from '@/context/environment/hooks'; +import { useFetchWorkflow, useFormAutoSave, useUpdateWorkflow } from '@/hooks'; +import { StepTypeEnum } from '@/utils/enums'; +import { handleValidationIssues } from '@/utils/handleValidationIssues'; +import { buildRoute, ROUTES } from '@/utils/routes'; +import { Step } from '@/utils/types'; +import debounce from 'lodash.debounce'; +import { RiAlertFill } from 'react-icons/ri'; +import { Form } from '../primitives/form/form'; +import { ToastIcon } from '../primitives/sonner'; +import { showToast } from '../primitives/sonner-helpers'; +import { workflowSchema } from './schema'; +import { WorkflowEditorContext } from './workflow-editor-context'; const STEP_NAME_BY_TYPE: Record = { email: 'Email Step', @@ -150,17 +151,27 @@ export const WorkflowEditorProvider = ({ children }: { children: ReactNode }) => const blocker = useBlocker(isDirty || isPending); - useFormAutoSave({ - form, - onSubmit: async (data: z.infer) => { - if (!workflow) { + const onSubmit = useCallback( + async (data: z.infer) => { + if (!workflow || !form.formState.isDirty || isReadOnly) { return; } updateWorkflow({ id: workflow._id, workflow: { ...workflow, ...data } as any }); }, - enabled: !isReadOnly, - shouldSaveImmediately: (previousData, data) => { + [workflow, form.formState.isDirty, isReadOnly, updateWorkflow] + ); + + const debouncedSave = useCallback(debounce(form.handleSubmit(onSubmit), 800), [ + form.handleSubmit, + onSubmit, + debounce, + ]); + + useFormAutoSave({ + form, + debouncedSave, + shouldFlush: (previousData, data) => { const currentStepsLength = data?.steps?.length ?? 0; const wasStepsLengthAltered = previousData.steps != null && currentStepsLength !== previousData.steps?.length; @@ -231,7 +242,9 @@ export const WorkflowEditorProvider = ({ children }: { children: ReactNode }) =>
- {children}
+
+ {children} +
); diff --git a/apps/dashboard/src/hooks/use-debounce.ts b/apps/dashboard/src/hooks/use-debounce.ts index 23e53f8bf9e..2be95b592bd 100644 --- a/apps/dashboard/src/hooks/use-debounce.ts +++ b/apps/dashboard/src/hooks/use-debounce.ts @@ -1,5 +1,5 @@ -import { useCallback, useEffect } from 'react'; import debounce from 'lodash.debounce'; +import { useCallback, useEffect } from 'react'; import { useDataRef } from './use-data-ref'; export const useDebounce = (callback: (...args: Arguments) => void, ms = 0) => { diff --git a/apps/dashboard/src/hooks/use-form-autosave.ts b/apps/dashboard/src/hooks/use-form-autosave.ts index dd46e1b1d8d..91f2c0ff0fb 100644 --- a/apps/dashboard/src/hooks/use-form-autosave.ts +++ b/apps/dashboard/src/hooks/use-form-autosave.ts @@ -1,37 +1,26 @@ -import { DeepPartialSkipArrayKey, FieldValues, SubmitHandler, UseFormReturn, useWatch } from 'react-hook-form'; +import { useRef } from 'react'; +import { DeepPartialSkipArrayKey, FieldValues, UseFormReturn, useWatch } from 'react-hook-form'; import useDeepCompareEffect from 'use-deep-compare-effect'; import { useDebounce } from './use-debounce'; -import { useDataRef } from './use-data-ref'; -import { useRef } from 'react'; export const useFormAutoSave = ({ - onSubmit, + debouncedSave, form, - enabled = true, - shouldSaveImmediately, + shouldFlush, }: { - onSubmit: SubmitHandler; + debouncedSave: ReturnType; form: UseFormReturn; - enabled?: boolean; - shouldSaveImmediately?: ( + shouldFlush?: ( watchedData: DeepPartialSkipArrayKey, previousWatchedData: DeepPartialSkipArrayKey | null ) => boolean; }) => { - const onSubmitRef = useDataRef(onSubmit); - const saveRef = useDataRef(() => { - if (enabled) { - handleSubmit(onSubmitRef.current)(); - } - }); - const { formState, control, handleSubmit } = form; + const { formState, control } = form; const watchedData = useWatch({ control, }); - const debouncedSave = useDebounce(saveRef.current, 800); - const previousWatchedData = useRef | null>(null); useDeepCompareEffect(() => { @@ -40,14 +29,14 @@ export const useFormAutoSave = ({ return; } - const immediateSave = shouldSaveImmediately?.(watchedData, previousWatchedData.current) || false; + const immediateSave = shouldFlush?.(watchedData, previousWatchedData.current) || false; if (immediateSave) { - saveRef.current(); + debouncedSave.flush(); } else { debouncedSave(); } previousWatchedData.current = watchedData; - }, [watchedData]); + }, [watchedData, formState.isDirty]); };