Skip to content

Commit

Permalink
feat(dashboard): Save workflow on blur (#6990)
Browse files Browse the repository at this point in the history
  • Loading branch information
desiprisg authored Nov 14, 2024
1 parent f2200a0 commit d51a862
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 44 deletions.
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<StepTypeEnum, string> = {
email: 'Email Step',
Expand Down Expand Up @@ -150,17 +151,27 @@ export const WorkflowEditorProvider = ({ children }: { children: ReactNode }) =>

const blocker = useBlocker(isDirty || isPending);

useFormAutoSave({
form,
onSubmit: async (data: z.infer<typeof workflowSchema>) => {
if (!workflow) {
const onSubmit = useCallback(
async (data: z.infer<typeof workflowSchema>) => {
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;

Expand Down Expand Up @@ -231,7 +242,9 @@ export const WorkflowEditorProvider = ({ children }: { children: ReactNode }) =>
</AlertDialogContent>
</AlertDialog>
<Form {...form}>
<form className="h-full">{children}</form>
<form className="h-full" onBlur={debouncedSave.flush}>
{children}
</form>
</Form>
</WorkflowEditorContext.Provider>
);
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/src/hooks/use-debounce.ts
Original file line number Diff line number Diff line change
@@ -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 = <Arguments extends any[]>(callback: (...args: Arguments) => void, ms = 0) => {
Expand Down
31 changes: 10 additions & 21 deletions apps/dashboard/src/hooks/use-form-autosave.ts
Original file line number Diff line number Diff line change
@@ -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 = <T extends FieldValues>({
onSubmit,
debouncedSave,
form,
enabled = true,
shouldSaveImmediately,
shouldFlush,
}: {
onSubmit: SubmitHandler<T>;
debouncedSave: ReturnType<typeof useDebounce>;
form: UseFormReturn<T>;
enabled?: boolean;
shouldSaveImmediately?: (
shouldFlush?: (
watchedData: DeepPartialSkipArrayKey<T>,
previousWatchedData: DeepPartialSkipArrayKey<T> | 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<T>({
control,
});

const debouncedSave = useDebounce(saveRef.current, 800);

const previousWatchedData = useRef<DeepPartialSkipArrayKey<T> | null>(null);

useDeepCompareEffect(() => {
Expand All @@ -40,14 +29,14 @@ export const useFormAutoSave = <T extends FieldValues>({
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]);
};

0 comments on commit d51a862

Please sign in to comment.