Skip to content

Commit

Permalink
feat(dashboard): in-app editor unsaved changes modal plus handling es…
Browse files Browse the repository at this point in the history
…c and clicking on drawer gray overlay (#6958)
  • Loading branch information
LetItRock authored Nov 12, 2024
1 parent e17502f commit aabc1df
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 91 deletions.
1 change: 1 addition & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.3",
"@radix-ui/react-visually-hidden": "^1.1.0",
"@rjsf/core": "^5.22.3",
"@rjsf/utils": "^5.20.0",
"@rjsf/validator-ajv8": "^5.17.1",
Expand Down
3 changes: 3 additions & 0 deletions apps/dashboard/src/components/primitives/sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const SheetClose = SheetPrimitive.Close;

const SheetPortal = SheetPrimitive.Portal;

const SheetContentBase = SheetPrimitive.Content;

const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
Expand Down Expand Up @@ -104,6 +106,7 @@ export {
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContentBase,
SheetContent,
SheetHeader,
SheetFooter,
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/src/components/primitives/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const tabsContentVariants = cva('focus-visible:outline-none', {
variants: {
variant: {
default: 'ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
default: '',
regular: 'mt-2',
},
},
Expand Down
5 changes: 5 additions & 0 deletions apps/dashboard/src/components/primitives/visually-hidden.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as VisuallyHiddenAll from '@radix-ui/react-visually-hidden';

const VisuallyHidden = VisuallyHiddenAll.Root;

export { VisuallyHidden };
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import { useMemo } from 'react';
import { motion } from 'framer-motion';
import { useParams } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router-dom';

import { Sheet, SheetOverlay, SheetPortal } from '@/components/primitives/sheet';
import {
Sheet,
SheetContentBase,
SheetDescription,
SheetOverlay,
SheetPortal,
SheetTitle,
} from '@/components/primitives/sheet';
import { useFetchWorkflow } from '@/hooks/use-fetch-workflow';
import { StepEditor } from '@/components/workflow-editor/steps/step-editor';
import { useFetchStep } from '@/hooks/use-fetch-step';
import { VisuallyHidden } from '@/components/primitives/visually-hidden';

const transitionSetting = { ease: [0.29, 0.83, 0.57, 0.99], duration: 0.4 };

export const EditStepSidebar = () => {
const { workflowSlug = '', stepSlug = '' } = useParams<{ workflowSlug: string; stepSlug: string }>();
const navigate = useNavigate();

const { workflow } = useFetchWorkflow({
workflowSlug,
Expand All @@ -19,6 +28,10 @@ export const EditStepSidebar = () => {
const { step } = useFetchStep({ workflowSlug, stepSlug });
const stepType = useMemo(() => workflow?.steps.find((el) => el.slug === stepSlug)?.type, [stepSlug, workflow]);

const handleCloseSidebar = () => {
navigate('../', { relative: 'path' });
};

return (
<Sheet open>
<SheetPortal>
Expand All @@ -36,24 +49,30 @@ export const EditStepSidebar = () => {
transition={transitionSetting}
/>
</SheetOverlay>
<motion.div
initial={{
x: '100%',
}}
animate={{
x: 0,
}}
exit={{
x: '100%',
}}
transition={transitionSetting}
className={
'bg-background fixed inset-y-0 right-0 z-50 flex h-full w-3/4 flex-col border-l shadow-lg sm:max-w-[600px]'
}
>
{/* TODO: show loading indicator */}
{workflow && step && stepType && <StepEditor workflow={workflow} step={step} stepType={stepType} />}
</motion.div>
<SheetContentBase asChild onInteractOutside={handleCloseSidebar} onEscapeKeyDown={handleCloseSidebar}>
<motion.div
initial={{
x: '100%',
}}
animate={{
x: 0,
}}
exit={{
x: '100%',
}}
transition={transitionSetting}
className={
'bg-background fixed inset-y-0 right-0 z-50 flex h-full w-3/4 flex-col border-l shadow-lg outline-none sm:max-w-[600px]'
}
>
<VisuallyHidden>
<SheetTitle />
<SheetDescription />
</VisuallyHidden>
{/* TODO: show loading indicator */}
{workflow && step && stepType && <StepEditor workflow={workflow} step={step} stepType={stepType} />}
</motion.div>
</SheetContentBase>
</SheetPortal>
</Sheet>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { useState, useEffect, useMemo } from 'react';
import { RiEdit2Line, RiPencilRuler2Line } from 'react-icons/ri';
import { RiAlertFill, RiEdit2Line, RiPencilRuler2Line } from 'react-icons/ri';
import { Cross2Icon } from '@radix-ui/react-icons';
import { useNavigate, useParams } from 'react-router-dom';
import { useBlocker, useNavigate, useParams } from 'react-router-dom';
import { FieldValues, useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { type WorkflowResponseDto, type StepDataDto, type StepUpdateDto } from '@novu/shared';

import { Form } from '@/components/primitives/form/form';
import { Notification5Fill } from '@/components/icons';
import { Button } from '@/components/primitives/button';
import { Button, buttonVariants } from '@/components/primitives/button';
import { Separator } from '@/components/primitives/separator';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/primitives/tabs';
import { InAppEditorPreview } from '@/components/workflow-editor/steps/in-app/in-app-editor-preview';
Expand All @@ -23,6 +23,16 @@ import { CustomStepControls } from '../controls/custom-step-controls';
import { useStep } from '../use-step';
import { flattenIssues } from '../../step-utils';
import { useWorkflowEditorContext } from '../../hooks';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/primitives/alert-dialog';

const tabsContentClassName = 'h-full w-full px-3 py-3.5';

Expand Down Expand Up @@ -55,7 +65,7 @@ export const InAppTabs = ({ workflow, step }: { workflow: WorkflowResponseDto; s
}, [controlErrors, setError]);

const { previewStep, data: previewData } = usePreviewStep();
const { updateWorkflow } = useUpdateWorkflow({
const { isPending, updateWorkflow } = useUpdateWorkflow({
onSuccess: (data) => {
resetWorkflowForm(data);
showToast({
Expand Down Expand Up @@ -128,75 +138,112 @@ export const InAppTabs = ({ workflow, step }: { workflow: WorkflowResponseDto; s
[formValues]
);

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

return (
<Form {...form}>
<form
id="save-step"
className="flex h-full flex-col"
onSubmit={(event) => {
event.preventDefault();
event.stopPropagation();
form.handleSubmit(onSubmit)(event);
}}
>
<Tabs defaultValue="editor" className="flex h-full flex-1 flex-col">
<header className="flex flex-row items-center gap-3 px-3 py-1.5">
<div className="mr-auto flex items-center gap-2.5 text-sm font-medium">
<RiEdit2Line className="size-4" />
<span>Configure Template</span>
<>
<Form {...form}>
<form
id="save-step"
className="flex h-full flex-col"
onSubmit={(event) => {
event.preventDefault();
event.stopPropagation();
form.handleSubmit(onSubmit)(event);
}}
>
<Tabs defaultValue="editor" className="flex h-full flex-1 flex-col">
<header className="flex flex-row items-center gap-3 px-3 py-1.5">
<div className="mr-auto flex items-center gap-2.5 text-sm font-medium">
<RiEdit2Line className="size-4" />
<span>Configure Template</span>
</div>
<TabsList className="w-min">
<TabsTrigger value="editor" className="gap-1.5">
<RiPencilRuler2Line className="size-5 p-0.5" />
<span>Editor</span>
</TabsTrigger>
<TabsTrigger value="preview" className="gap-1.5">
<Notification5Fill className="size-5 p-0.5" />
<span>Preview</span>
</TabsTrigger>
</TabsList>

<Button
variant="ghost"
size="xs"
className="size-6"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
navigate('../', { relative: 'path' });
}}
>
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</Button>
</header>
<Separator />
<TabsContent value="editor" className={tabsContentClassName}>
<InAppEditor uiSchema={uiSchema} />
<CustomStepControls dataSchema={dataSchema} origin={workflow.origin} />
</TabsContent>
<TabsContent value="preview" className={tabsContentClassName}>
<InAppEditorPreview
value={editorValue}
onChange={setEditorValue}
previewData={previewData}
applyPreview={() => {
previewStep({
stepSlug,
workflowSlug,
data: { controlValues: form.getValues() as FieldValues, previewPayload: JSON.parse(editorValue) },
});
}}
/>
</TabsContent>
<Separator />
<footer className="flex justify-end px-3 py-3.5">
<Button
className="ml-auto"
variant="default"
type="submit"
form="save-step"
disabled={!formState.isDirty}
>
Save step
</Button>
</footer>
</Tabs>
</form>
</Form>
<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>
<TabsList className="w-min">
<TabsTrigger value="editor" className="gap-1.5">
<RiPencilRuler2Line className="size-5 p-0.5" />
<span>Editor</span>
</TabsTrigger>
<TabsTrigger value="preview" className="gap-1.5">
<Notification5Fill className="size-5 p-0.5" />
<span>Preview</span>
</TabsTrigger>
</TabsList>
<div className="space-y-1">
<AlertDialogTitle>You might lose your progress</AlertDialogTitle>
<AlertDialogDescription>
This editor form has some unsaved changes. Save progress before you leave.
</AlertDialogDescription>
</div>
</AlertDialogHeader>

<Button
variant="ghost"
size="xs"
className="size-6"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
navigate('../', { relative: 'path' });
}}
>
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</Button>
</header>
<Separator />
<TabsContent value="editor" className={tabsContentClassName}>
<InAppEditor uiSchema={uiSchema} />
<CustomStepControls dataSchema={dataSchema} origin={workflow.origin} />
</TabsContent>
<TabsContent value="preview" className={tabsContentClassName}>
<InAppEditorPreview
value={editorValue}
onChange={setEditorValue}
previewData={previewData}
applyPreview={() => {
previewStep({
stepSlug,
workflowSlug,
data: { controlValues: form.getValues() as FieldValues, previewPayload: JSON.parse(editorValue) },
});
}}
/>
</TabsContent>
<Separator />
<footer className="flex justify-end px-3 py-3.5">
<Button className="ml-auto" variant="default" type="submit" form="save-step" disabled={!formState.isDirty}>
Save step
</Button>
</footer>
</Tabs>
</form>
</Form>

<AlertDialogFooter>
<AlertDialogCancel onClick={() => blocker.reset?.()}>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => blocker.proceed?.()}
className={buttonVariants({ variant: 'destructive' })}
>
Proceed anyway
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
};
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit aabc1df

Please sign in to comment.