Skip to content

Commit

Permalink
feat(dashboard): Block navigations when unsaved changes are present (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
desiprisg authored Nov 6, 2024
1 parent 2415da8 commit 3573b1d
Show file tree
Hide file tree
Showing 4 changed files with 1,027 additions and 703 deletions.
1 change: 1 addition & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@novu/react": "workspace:*",
"@novu/shared": "workspace:*",
"@radix-ui/react-accordion": "^1.2.1",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.2",
Expand Down
108 changes: 108 additions & 0 deletions apps/dashboard/src/components/primitives/alert-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import * as React from 'react';
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';

import { cn } from '@/utils/ui';
import { buttonVariants } from '@/components/primitives/button';

const AlertDialog = AlertDialogPrimitive.Root;

const AlertDialogTrigger = AlertDialogPrimitive.Trigger;

const AlertDialogPortal = AlertDialogPrimitive.Portal;

const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/20',
className
)}
{...props}
ref={ref}
/>
));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;

const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] border shadow-lg duration-200 sm:rounded-3xl',
className
)}
{...props}
/>
</AlertDialogPortal>
));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;

const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn('flex flex-col gap-2 p-5 text-center sm:text-left', className)} {...props} />
);
AlertDialogHeader.displayName = 'AlertDialogHeader';

const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn('flex flex-col-reverse px-5 py-4 sm:flex-row sm:justify-end sm:gap-2', className)} {...props} />
);
AlertDialogFooter.displayName = 'AlertDialogFooter';

const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title ref={ref} className={cn('text-lg font-semibold', className)} {...props} />
));
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;

const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn('text-foreground-600 text-sm font-normal', className)}
{...props}
/>
));
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;

const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
));
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;

const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(buttonVariants({ variant: 'outline' }), 'mt-2 sm:mt-0', className)}
{...props}
/>
));
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;

export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ReactNode, useMemo, useCallback, useRef, useLayoutEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useBlocker, useNavigate, useParams } from 'react-router-dom';
import { useForm, useFieldArray } from 'react-hook-form';
// eslint-disable-next-line
// @ts-ignore
Expand All @@ -18,6 +18,19 @@ import { showToast } from '../primitives/sonner-helpers';
import { ToastIcon } from '../primitives/sonner';
import { handleValidationIssues } from '@/utils/handleValidationIssues';
import { WorkflowOriginEnum } from '@novu/shared';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/primitives/alert-dialog';
import { Button, buttonVariants } from '@/components/primitives/button';
import { RiAlertFill } from 'react-icons/ri';
import { Separator } from '@/components/primitives/separator';

const STEP_NAME_BY_TYPE: Record<StepTypeEnum, string> = {
email: 'Email Step',
Expand Down Expand Up @@ -69,7 +82,7 @@ export const WorkflowEditorProvider = ({ children }: { children: ReactNode }) =>
reset({ ...workflow, steps: workflow.steps.map((step) => ({ ...step })) });
}, [workflow, error, navigate, reset, currentEnvironment]);

const { updateWorkflow } = useUpdateWorkflow({
const { updateWorkflow, isPending } = useUpdateWorkflow({
onSuccess: (data) => {
reset({ ...data, steps: data.steps.map((step) => ({ ...step })) });

Expand Down Expand Up @@ -103,6 +116,8 @@ export const WorkflowEditorProvider = ({ children }: { children: ReactNode }) =>
},
});

const blocker = useBlocker(form.formState.isDirty || isPending);

useFormAutoSave({
form,
onSubmit: async (data: z.infer<typeof workflowSchema>) => {
Expand Down Expand Up @@ -149,6 +164,33 @@ export const WorkflowEditorProvider = ({ children }: { children: ReactNode }) =>

return (
<WorkflowEditorContext.Provider value={value}>
<AlertDialog open={blocker.state === 'blocked'}>
<AlertDialogContent>
<AlertDialogHeader className="flex flex-row items-start gap-4">
<div className="bg-warning/10 rounded-lg p-3">
<RiAlertFill className="text-warning size-6" />
</div>
<div className="space-y-1">
<AlertDialogTitle>You might lose your progress</AlertDialogTitle>
<AlertDialogDescription>
This workflow has some unsaved changes. Save progress before you leave.
</AlertDialogDescription>
</div>
</AlertDialogHeader>

<Separator />

<AlertDialogFooter>
<AlertDialogCancel onClick={() => blocker.reset?.()}>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => blocker.proceed?.()}
className={buttonVariants({ variant: 'destructive' })}
>
Proceed anyway
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<Form {...form}>
<form className="h-full">{children}</form>
</Form>
Expand Down
Loading

0 comments on commit 3573b1d

Please sign in to comment.