Skip to content

Commit d037d31

Browse files
authored
feat(dashboard): add repeat menu description component and integrate with email step (#8070)
1 parent a1e6b01 commit d037d31

File tree

6 files changed

+111
-86
lines changed

6 files changed

+111
-86
lines changed

apps/dashboard/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"@codemirror/autocomplete": "^6.18.3",
2525
"@hookform/resolvers": "^3.9.0",
2626
"@lezer/highlight": "^1.2.1",
27-
"@maily-to/core": "github:novuhq/maily.to.git#release/v0.2.7-novu.4-core&path:/packages/core",
27+
"@maily-to/core": "github:novuhq/maily.to.git#release/v0.2.7-novu.6-core&path:/packages/core",
2828
"@novu/api": "workspace:*",
2929
"@novu/framework": "workspace:*",
3030
"@novu/js": "workspace:*",

apps/dashboard/src/components/workflow-editor/steps/email/email-editor.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const EmailEditor = (props: EmailEditorProps) => {
2424
</EmailPreviewHeader>
2525
{getComponentByType({ component: subject.component })}
2626
</div>
27-
<div className="relative flex-1 overflow-y-auto bg-neutral-50 px-16 pt-8">
27+
<div className="relative flex-1 overflow-y-visible bg-neutral-50 px-16 pt-8">
2828
{getComponentByType({ component: body.component })}
2929
</div>
3030
</div>

apps/dashboard/src/components/workflow-editor/steps/email/maily.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useTelemetry } from '@/hooks/use-telemetry';
99
import { cn } from '@/utils/ui';
1010
import { createEditorBlocks, createExtensions, DEFAULT_EDITOR_CONFIG, MAILY_EMAIL_WIDTH } from './maily-config';
1111
import { calculateVariables, VariableFrom } from './variables/variables';
12+
import { RepeatMenuDescription } from './views/repeat-menu-description';
1213

1314
type MailyProps = HTMLAttributes<HTMLDivElement> & {
1415
value: string;
@@ -102,6 +103,9 @@ export const Maily = ({ value, onChange, className, ...rest }: MailyProps) => {
102103
onChange(JSON.stringify(editor.getJSON()));
103104
}
104105
}}
106+
repeatMenuConfig={{
107+
description: (editor) => <RepeatMenuDescription editor={editor} />,
108+
}}
105109
/>
106110
</div>
107111
</>

apps/dashboard/src/components/workflow-editor/steps/email/views/for-view.tsx

+2-68
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,15 @@
1-
import { Editor, NodeViewContent, NodeViewProps, NodeViewWrapper } from '@tiptap/react';
2-
import { Lightbulb, Repeat2 } from 'lucide-react';
3-
import { AnimatePresence, motion } from 'motion/react';
4-
import { useEffect, useState } from 'react';
5-
6-
function TooltipContent({ forNodeEachKey, currentProperty }: { forNodeEachKey: string; currentProperty: string }) {
7-
return (
8-
<p className="mly-top-1/2 mly-left-1/2 -mly-translate-x-1/2 -mly-translate-y-1/2 mly-text-gray-400 mly-shadow-sm absolute z-[1] flex items-center gap-2 rounded-md bg-white px-3 py-1.5">
9-
<Lightbulb className="size-3.5 stroke-[2] text-gray-400" />
10-
Access each 'repeat' key via{' '}
11-
<code className="mly-px-1 mly-py-0.5 mly-bg-gray-50 mly-rounded mly-font-mono mly-text-gray-400">
12-
{`{{ ${forNodeEachKey}`}
13-
<span className="inline-block pr-1">
14-
<AnimatePresence mode="wait">
15-
<motion.span
16-
key={currentProperty}
17-
initial={{ opacity: 0, y: 10 }}
18-
animate={{ opacity: 1, y: 0 }}
19-
exit={{ opacity: 0, y: -10 }}
20-
transition={{ duration: 0.3 }}
21-
className="inline-block"
22-
>
23-
{currentProperty}
24-
</motion.span>
25-
</AnimatePresence>
26-
</span>
27-
</code>
28-
</p>
29-
);
30-
}
1+
import { NodeViewContent, NodeViewProps, NodeViewWrapper } from '@tiptap/react';
2+
import { Repeat2 } from 'lucide-react';
313

324
/**
335
* @see https://github.com/arikchakma/maily.to/blob/d7ea26e6b28201fc66c241200adaebc689018b03/packages/core/src/editor/nodes/for/for-view.tsx
346
*/
357
export function ForView(props: NodeViewProps) {
368
const { editor, getPos } = props;
379

38-
const pos = getPos();
39-
const cursorPos = editor.state.selection.from;
40-
41-
const forNode = editor.state.doc.nodeAt(pos);
42-
const forNodeEndPos = pos + (forNode?.nodeSize ?? 0);
43-
const forNodeEachKey = forNode?.attrs.each;
44-
45-
const isCursorInForNode = cursorPos >= pos && cursorPos <= forNodeEndPos;
46-
const isOnEmptyForNodeLine = isOnEmptyLine(editor, cursorPos) && isCursorInForNode;
47-
48-
const [currentProperty, setCurrentProperty] = useState('\u00A0}}');
49-
50-
function isOnEmptyLine(editor: Editor, cursorPos: number) {
51-
const currentLineContent = editor.state.doc
52-
.textBetween(
53-
Math.max(0, editor.state.doc.resolve(cursorPos).start()),
54-
Math.min(editor.state.doc.content.size, editor.state.doc.resolve(cursorPos).end())
55-
)
56-
.trim();
57-
58-
return currentLineContent === '';
59-
}
60-
61-
useEffect(() => {
62-
const properties = ['\u00A0}}', '.foo }}', '.bar }}', '.attr }}'];
63-
let currentIndex = 0;
64-
65-
const interval = setInterval(() => {
66-
currentIndex = (currentIndex + 1) % properties.length;
67-
setCurrentProperty(properties[currentIndex]);
68-
}, 2000);
69-
70-
return () => clearInterval(interval);
71-
}, []);
72-
7310
return (
7411
<NodeViewWrapper draggable="true" data-drag-handle="" data-type="repeat" className="mly-relative">
7512
<NodeViewContent className="is-editable" />
76-
{isOnEmptyForNodeLine && forNodeEachKey && (
77-
<TooltipContent forNodeEachKey={forNodeEachKey} currentProperty={currentProperty} />
78-
)}
7913
<div
8014
role="button"
8115
data-repeat-indicator=""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { useEffect, useState } from 'react';
2+
import { Editor, useEditorState } from '@tiptap/react';
3+
import { AnimatePresence, motion } from 'motion/react';
4+
import { Lightbulb } from 'lucide-react';
5+
import { Separator } from '@/components/primitives/separator';
6+
7+
export function RepeatMenuDescription({ editor }: { editor: Editor }) {
8+
const [currentProperty, setCurrentProperty] = useState('\u00A0}}');
9+
const state = useEditorState({
10+
editor,
11+
selector: (ctx) => {
12+
return {
13+
each: ctx.editor.getAttributes('repeat')?.each,
14+
currentShowIfKey: ctx.editor.getAttributes('repeat')?.showIfKey || '',
15+
16+
isSectionActive: ctx.editor.isActive('section'),
17+
};
18+
},
19+
});
20+
const forNodeEachKey = state.each;
21+
22+
function isOnEmptyLine(editor: Editor, cursorPos: number) {
23+
const currentLineContent = editor.state.doc
24+
.textBetween(
25+
Math.max(0, editor.state.doc.resolve(cursorPos).start()),
26+
Math.min(editor.state.doc.content.size, editor.state.doc.resolve(cursorPos).end())
27+
)
28+
.trim();
29+
30+
return currentLineContent === '';
31+
}
32+
33+
useEffect(() => {
34+
const properties = ['\u00A0}}', '.foo }}', '.bar }}', '.attr }}'];
35+
let currentIndex = 0;
36+
37+
const interval = setInterval(() => {
38+
currentIndex = (currentIndex + 1) % properties.length;
39+
setCurrentProperty(properties[currentIndex]);
40+
}, 2000);
41+
42+
return () => clearInterval(interval);
43+
}, []);
44+
45+
const shouldShow = isOnEmptyLine(editor, editor.state.selection.from);
46+
47+
return (
48+
<AnimatePresence mode="wait">
49+
{shouldShow && (
50+
<motion.div
51+
key="repeat-menu"
52+
initial={{ opacity: 0, height: 0 }}
53+
animate={{ opacity: 1, height: 'auto' }}
54+
exit={{ opacity: 0, height: 0 }}
55+
transition={{ duration: 0.25, ease: 'easeInOut' }}
56+
className="mly-shadow-sm mly-text-gray-400 overflow-hidden text-xs"
57+
>
58+
<Separator className="mt-0.5" />
59+
<div className="flex items-start gap-1 px-1 py-1.5">
60+
<Lightbulb className="mt-0.5 size-3.5 stroke-[2] text-gray-400" />
61+
<div>
62+
<div>Access each 'repeat' key via</div>
63+
<span>
64+
<code className="mly-py-0.5 mly-bg-gray-50 mly-rounded mly-font-mono mly-text-gray-400">
65+
{`{{ ${forNodeEachKey}`}
66+
<span className="inline-block pr-1">
67+
<AnimatePresence mode="wait">
68+
<motion.span
69+
key={currentProperty}
70+
initial={{ opacity: 0, y: 10 }}
71+
animate={{ opacity: 1, y: 0 }}
72+
exit={{ opacity: 0, y: -10 }}
73+
transition={{ duration: 0.3 }}
74+
className="inline-block"
75+
>
76+
{currentProperty}
77+
</motion.span>
78+
</AnimatePresence>
79+
</span>
80+
</code>
81+
</span>
82+
</div>
83+
</div>
84+
</motion.div>
85+
)}
86+
</AnimatePresence>
87+
);
88+
}

pnpm-lock.yaml

+15-16
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)