Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dashboard): Block navigations when unsaved changes are present #6874

Merged
merged 1 commit into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading