Skip to content

Commit

Permalink
feat(dashboard): in-app editor form fields based on codemirror (#6809)
Browse files Browse the repository at this point in the history
  • Loading branch information
LetItRock authored Nov 1, 2024
1 parent f222249 commit 96889c1
Show file tree
Hide file tree
Showing 8 changed files with 422 additions and 45 deletions.
3 changes: 2 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -690,7 +690,8 @@
"xyflow",
"Sonner",
"sonner",
"cmdk"
"cmdk",
"Keymap"
],
"flagWords": [],
"patterns": [
Expand Down
6 changes: 5 additions & 1 deletion apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
"@monaco-editor/react": "^4.6.0",
"@novu/react": "workspace:*",
"@novu/shared": "workspace:*",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-accordion": "^1.2.1",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-icons": "^1.3.0",
Expand All @@ -43,10 +43,14 @@
"@segment/analytics-next": "^1.73.0",
"@sentry/react": "^8.35.0",
"@tanstack/react-query": "^5.59.6",
"@uiw/codemirror-theme-white": "^4.23.6",
"@uiw/codemirror-themes": "^4.23.6",
"@uiw/react-codemirror": "^4.23.6",
"@xyflow/react": "^12.3.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "1.0.0",
"codemirror-lang-liquid": "^1.0.0",
"date-fns": "^4.1.0",
"framer-motion": "^11.3.19",
"lodash.debounce": "^4.0.8",
Expand Down
64 changes: 64 additions & 0 deletions apps/dashboard/src/components/primitives/editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useEffect, useRef } from 'react';
import { useCodeMirror, EditorView } from '@uiw/react-codemirror';
import { LiquidHTML } from 'codemirror-lang-liquid';
import { cva, VariantProps } from 'class-variance-authority';
import createTheme from '@uiw/codemirror-themes';

const editorVariants = cva('-mx-1 -mt-[2px] h-full w-full flex-1 [&_.cm-focused]:outline-none', {
variants: {
size: {
default: 'text-xs [&_.cm-editor]:py-1',
md: 'text-sm [&_.cm-editor]:py-2',
},
},
defaultVariants: {
size: 'default',
},
});

const theme = createTheme({
theme: 'light',
styles: [],
settings: {
background: 'transparent',
lineHighlight: 'transparent',
},
});

type EditorProps = {
value: string;
placeholder?: string;
className?: string;
height?: string;
onChange: (val: string) => void;
} & VariantProps<typeof editorVariants>;

export const Editor = ({ value, placeholder, className, height, size, onChange }: EditorProps) => {
const editor = useRef<HTMLDivElement>(null);
const { setContainer } = useCodeMirror({
extensions: [LiquidHTML({}), EditorView.lineWrapping],
height,
placeholder,
basicSetup: {
lineNumbers: false,
foldGutter: false,
defaultKeymap: false,
highlightActiveLine: false,
highlightActiveLineGutter: false,
indentOnInput: false,
searchKeymap: false,
},
container: editor.current,
value,
onChange,
theme,
});

useEffect(() => {
if (editor.current) {
setContainer(editor.current);
}
}, [setContainer]);

return <div ref={editor} className={editorVariants({ size, className })} />;
};
37 changes: 24 additions & 13 deletions apps/dashboard/src/components/primitives/url-input.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
'use client';

import { forwardRef } from 'react';
import { RedirectTargetEnum } from '@novu/shared';
import { Input, InputField, InputFieldProps, InputProps } from '@/components/primitives/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/primitives/select';
import { RedirectTargetEnum } from '@novu/shared';
import { forwardRef } from 'react';
import { Editor } from './editor';

type URLValue = {
type: string;
Expand All @@ -14,23 +13,35 @@ type URLInputProps = Omit<InputProps, 'value' | 'onChange' | 'size'> & {
options: string[];
value: URLValue;
onChange: (value: URLValue) => void;
asEditor?: boolean;
} & Pick<InputFieldProps, 'size'>;

export const URLInput = forwardRef<HTMLInputElement, URLInputProps>((props, ref) => {
const { options, value, onChange, size, ...rest } = props;
const { options, value, onChange, size = 'default', asEditor = false, placeholder, ...rest } = props;

return (
<div className="flex items-center justify-between space-x-2">
<div className="relative flex-grow">
<InputField className="pr-0" size={size}>
<Input
ref={ref}
type="text"
className="min-w-[20ch]"
value={value.url}
onChange={(e) => onChange({ ...value, url: e.target.value })}
{...rest}
/>
{asEditor ? (
<Editor
size={size}
placeholder={placeholder}
value={value.url}
onChange={(val) => onChange({ ...value, url: val })}
height={size === 'md' ? '38px' : '30px'}
/>
) : (
<Input
ref={ref}
type="text"
className="min-w-[20ch]"
value={value.url}
placeholder={placeholder}
onChange={(e) => onChange({ ...value, url: e.target.value })}
{...rest}
/>
)}
<Select value={value.type} onValueChange={(val: RedirectTargetEnum) => onChange({ ...value, type: val })}>
<SelectTrigger className="h-full max-w-24 rounded-l-none border-0 border-l">
<SelectValue />
Expand Down
20 changes: 11 additions & 9 deletions apps/dashboard/src/components/workflow-editor/action-picker.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import { ComponentProps } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { RiEdit2Line, RiExpandUpDownLine, RiForbid2Line } from 'react-icons/ri';
import { z } from 'zod';
import { RedirectTargetEnum } from '@novu/shared';
import { Button, buttonVariants } from '@/components/primitives/button';
import {
DropdownMenu,
Expand All @@ -6,18 +12,13 @@ import {
DropdownMenuTrigger,
} from '@/components/primitives/dropdown-menu';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/primitives/form/form';
import { Input, InputField } from '@/components/primitives/input';
import { InputField } from '@/components/primitives/input';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/primitives/popover';
import { Separator } from '@/components/primitives/separator';
import { URLInput } from '@/components/primitives/url-input';
import { cn } from '@/utils/ui';
import { urlTargetTypes } from '@/utils/url';
import { zodResolver } from '@hookform/resolvers/zod';
import { RedirectTargetEnum } from '@novu/shared';
import { ComponentProps } from 'react';
import { useForm } from 'react-hook-form';
import { RiEdit2Line, RiExpandUpDownLine, RiForbid2Line } from 'react-icons/ri';
import { z } from 'zod';
import { Editor } from '../primitives/editor';

type Action = {
label: string;
Expand Down Expand Up @@ -177,7 +178,7 @@ const ConfigureActionPopover = (
}}
>
<PopoverTrigger {...rest} />
<PopoverContent>
<PopoverContent className="max-w-72">
<Form {...form}>
<form className="space-y-4">
<div className="space-y-2">
Expand All @@ -195,7 +196,7 @@ const ConfigureActionPopover = (
</div>
<FormControl>
<InputField>
<Input {...field} />
<Editor placeholder="Button text" value={field.value} onChange={field.onChange} height="30px" />
</InputField>
</FormControl>
<FormMessage />
Expand All @@ -217,6 +218,7 @@ const ConfigureActionPopover = (
options={urlTargetTypes}
value={field.value}
onChange={(val) => field.onChange(val)}
asEditor
/>
</FormControl>
<FormMessage />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ import { StepEditor } from './step-editor';
const transitionSetting = { ease: [0.29, 0.83, 0.57, 0.99], duration: 0.4 };

export const EditStepSidebar = () => {
const { workflowId = '', stepId = '' } = useParams<{ workflowId: string; stepId: string }>();
const { workflowSlug = '', stepId = '' } = useParams<{ workflowSlug: string; stepId: string }>();
const navigate = useNavigate();
const form = useForm<z.infer<typeof workflowSchema>>({ mode: 'onSubmit', resolver: zodResolver(workflowSchema) });
const { reset, setError } = form;

const { workflow, error } = useFetchWorkflow({
workflowSlug: workflowId,
workflowSlug,
});

const step = useMemo(() => workflow?.steps.find((el) => el._id === stepId), [stepId, workflow]);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
import { useState } from 'react';
import { RiEdit2Line, RiInformationFill, RiPencilRuler2Line } from 'react-icons/ri';
import { Cross2Icon } from '@radix-ui/react-icons';
import { useNavigate } from 'react-router-dom';
import { useFormContext } from 'react-hook-form';
import * as z from 'zod';
import { RedirectTargetEnum } from '@novu/shared';

import { Button } from '@/components/primitives/button';
import { Separator } from '@/components/primitives/separator';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/primitives/tabs';
import { Notification5Fill } from '@/components/icons';
import { AvatarPicker } from '@/components/primitives/form/avatar-picker';
import { Input, InputField } from '@/components/primitives/input';
import { Textarea } from '@/components/primitives/textarea';
import { InputField } from '@/components/primitives/input';
import { workflowSchema } from '../schema';
import { ActionPicker } from '../action-picker';
import { URLInput } from '@/components/primitives/url-input';
import { urlTargetTypes } from '@/utils/url';
import { RedirectTargetEnum } from '@novu/shared';
import { Editor } from '@/components/primitives/editor';

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

export const InAppEditor = () => {
const navigate = useNavigate();
const { formState } = useFormContext<z.infer<typeof workflowSchema>>();

const [subject, setSubject] = useState('');
const [body, setBody] = useState('');

return (
<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">
Expand Down Expand Up @@ -63,11 +67,13 @@ export const InAppEditor = () => {
<div className="flex flex-col gap-1 rounded-xl border border-neutral-100 p-1">
<div className="flex gap-1">
<AvatarPicker />
<InputField size="md">
<Input type="text" name="subject" placeholder="Subject" />
<InputField size="md" className="px-1">
<Editor placeholder="Subject" size="md" value={subject} onChange={setSubject} height="38px" />
</InputField>
</div>
<Textarea placeholder="Body" className="h-24" />
<InputField size="md" className="h-24 px-1">
<Editor placeholder="Body" size="md" value={body} onChange={setBody} />
</InputField>
<div className="mt-1 flex items-center gap-1">
<RiInformationFill className="text-foreground-400 size-4 p-0.5" />
<span className="text-foreground-600 text-xs font-normal">
Expand All @@ -89,7 +95,9 @@ export const InAppEditor = () => {
options={urlTargetTypes}
value={{ type: RedirectTargetEnum.BLANK, url: '' }}
onChange={(val) => console.log(val)}
placeholder="Redirect URL"
size="md"
asEditor
/>
</div>
</div>
Expand Down
Loading

0 comments on commit 96889c1

Please sign in to comment.