Skip to content

Commit

Permalink
feat(dashboard): email 'for' block visual cues (#7581)
Browse files Browse the repository at this point in the history
  • Loading branch information
ChmaraX authored Jan 27, 2025
1 parent 5308023 commit a9ea45e
Show file tree
Hide file tree
Showing 6 changed files with 696 additions and 254 deletions.
1 change: 1 addition & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"@segment/analytics-next": "^1.73.0",
"@sentry/react": "^8.35.0",
"@tanstack/react-query": "^5.59.6",
"@tiptap/react": "^2.6.6",
"@types/js-cookie": "^3.0.6",
"@types/lodash.isequal": "^4.5.8",
"@uiw/codemirror-extensions-langs": "^4.23.6",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Editor, NodeViewContent, NodeViewProps, NodeViewWrapper } from '@tiptap/react';
import { Lightbulb, Repeat2 } from 'lucide-react';
import { AnimatePresence, motion } from 'motion/react';
import { useEffect, useState } from 'react';

function TooltipContent({ forNodeEachKey, currentProperty }: { forNodeEachKey: string; currentProperty: string }) {
return (
<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">
<Lightbulb className="size-3.5 stroke-[2] text-gray-400" />
Access 'for' loop items using{' '}
<code className="mly-px-1 mly-py-0.5 mly-bg-gray-50 mly-rounded mly-font-mono mly-text-gray-400">
{`{{ ${forNodeEachKey}`}
<span className="inline-block pr-1">
<AnimatePresence mode="wait">
<motion.span
key={currentProperty}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3 }}
className="inline-block"
>
{currentProperty}
</motion.span>
</AnimatePresence>
</span>
</code>
</p>
);
}

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

const pos = getPos();
const cursorPos = editor.state.selection.from;

const forNode = editor.state.doc.nodeAt(pos);
const forNodeEndPos = pos + (forNode?.nodeSize ?? 0);
const forNodeEachKey = forNode?.attrs.each;

const isCursorInForNode = cursorPos >= pos && cursorPos <= forNodeEndPos;
const isOnEmptyForNodeLine = isOnEmptyLine(editor, cursorPos) && isCursorInForNode;

const [currentProperty, setCurrentProperty] = useState('\u00A0}}');

function isOnEmptyLine(editor: Editor, cursorPos: number) {
const currentLineContent = editor.state.doc
.textBetween(
Math.max(0, editor.state.doc.resolve(cursorPos).start()),
Math.min(editor.state.doc.content.size, editor.state.doc.resolve(cursorPos).end())
)
.trim();

return currentLineContent === '';
}

useEffect(() => {
const properties = ['\u00A0}}', '.foo }}', '.bar }}', '.attr }}'];
let currentIndex = 0;

const interval = setInterval(() => {
currentIndex = (currentIndex + 1) % properties.length;
setCurrentProperty(properties[currentIndex]);
}, 2000);

return () => clearInterval(interval);
}, []);

return (
<NodeViewWrapper draggable="true" data-drag-handle="" data-type="for" className="mly-relative">
<NodeViewContent className="is-editable" />
{isOnEmptyForNodeLine && forNodeEachKey && (
<TooltipContent forNodeEachKey={forNodeEachKey} currentProperty={currentProperty} />
)}
<div
role="button"
data-repeat-indicator=""
contentEditable={false}
onClick={() => {
editor.commands.setNodeSelection(getPos());
}}
className="mly-inline-flex mly-items-center mly-gap-1 mly-px-1.5 mly-py-0.5 mly-rounded-sm mly-bg-rose-50 mly-text-xs mly-absolute mly-top-0 mly-right-0 mly-translate-y-[-50%] mly-cursor-grab"
>
<Repeat2 className="mly-size-3 mly-stroke-[2.5]" />
<span className="mly-font-medium">Repeat</span>
</div>
<div className="mly-bg-rose-50 absolute right-0 top-0 h-full w-[2px]" />
<div
className="mly-bg-rose-50 absolute right-0 top-0 h-[2px] w-[25%]"
style={{ background: 'linear-gradient(to left, rgb(255 241 242) 60%, transparent)' }}
/>
<div
className="mly-bg-rose-50 absolute bottom-0 right-0 h-[2px] w-[25%]"
style={{ background: 'linear-gradient(to left, rgb(255 241 242) 60%, transparent)' }}
/>
</NodeViewWrapper>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { Extension, mergeAttributes, Node } from '@tiptap/core';
import { ReactNodeViewRenderer } from '@tiptap/react';
import { ForView } from './for-view';
import { updateAttributes } from './update-attributes';

const DEFAULT_SECTION_SHOW_IF_KEY = null;

type ForAttributes = {
each: string;
isUpdatingKey: boolean;
showIfKey: string;
};

declare module '@tiptap/core' {
interface Commands<ReturnType> {
for: {
setFor: () => ReturnType;
updateFor: (attrs: Partial<ForAttributes>) => ReturnType;
};
}
}

/**
* @see https://github.com/arikchakma/maily.to/blob/d7ea26e6b28201fc66c241200adaebc689018b03/packages/core/src/editor/nodes/for/for.ts
*/
export const ForExtension = Node.create({
name: 'for',
group: 'block',
content: '(block|columns)+',
draggable: true,
isolating: true,

addAttributes() {
return {
each: {
default: 'payload.items',
parseHTML: (element) => {
return element.getAttribute('each') || '';
},
renderHTML: (attributes) => {
if (!attributes.each) {
return {};
}

return {
each: attributes.each,
};
},
},
isUpdatingKey: {
default: false,
},
showIfKey: {
default: DEFAULT_SECTION_SHOW_IF_KEY,
parseHTML: (element) => {
return element.getAttribute('data-show-if-key') || DEFAULT_SECTION_SHOW_IF_KEY;
},
renderHTML(attributes) {
if (!attributes.showIfKey) {
return {};
}

return {
'data-show-if-key': attributes.showIfKey,
};
},
},
};
},

parseHTML() {
return [
{
tag: `div[data-type="${this.name}"]`,
},
];
},

renderHTML({ HTMLAttributes }) {
return [
'div',
mergeAttributes(HTMLAttributes, {
'data-type': this.name,
}),
0,
];
},

addCommands() {
return {
setFor:
() =>
({ commands }) => {
return commands.insertContent({
type: this.name,
attrs: {},
content: [
{
type: 'paragraph',
},
],
});
},
updateFor: (attrs) => updateAttributes(this.name, attrs),
};
},

addNodeView() {
return ReactNodeViewRenderer(ForView, {
contentDOMElementTag: 'div',
className: 'mly-relative',
});
},
}) as Extension;
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Command } from '@tiptap/core';

/**
*
* @see https://github.com/arikchakma/maily.to/blob/d7ea26e6b28201fc66c241200adaebc689018b03/packages/core/src/editor/utils/update-attribute.ts
*/
export function updateAttributes(type: string, attrs: Record<string, any>): Command {
return ({ commands }) =>
commands.command(({ tr, state, dispatch }) => {
if (dispatch) {
let lastPos = null;

tr.selection.ranges.forEach((range) => {
state.doc.nodesBetween(range.$from.pos, range.$to.pos, (node, pos) => {
if (node.type.name === type) {
lastPos = pos;
}
});
});

if (lastPos !== null) {
const node = state.doc.nodeAt(lastPos);
if (node) {
tr.setNodeMarkup(lastPos, null, {
...node.attrs,
...attrs,
});
} else {
for (const [key, value] of Object.entries(attrs)) {
tr.setNodeAttribute(lastPos, key, value);
}
}
}

if (type === 'button') {
tr.setSelection(tr.selection);
}
}

return true;
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Editor } from '@maily-to/core';
import { FeatureFlagsKeysEnum } from '@novu/shared';
import type { Editor as TiptapEditor } from '@tiptap/core';
import { HTMLAttributes, useMemo, useState } from 'react';
import { ForExtension } from './extensions/for';
import { DEFAULT_EDITOR_CONFIG, getEditorBlocks } from './maily-config';

type MailyProps = HTMLAttributes<HTMLDivElement> & {
Expand Down Expand Up @@ -52,6 +53,7 @@ export const Maily = ({ value, onChange, className, ...rest }: MailyProps) => {
key={isForBlockEnabled ? 'for-block-enabled' : 'for-block-disabled'}
config={DEFAULT_EDITOR_CONFIG}
blocks={getEditorBlocks(isForBlockEnabled)}
extensions={[ForExtension]}
variableTriggerCharacter="{{"
variables={({ query, editor, from }) => {
const queryWithoutSuffix = query.replace(/}+$/, '');
Expand Down
Loading

0 comments on commit a9ea45e

Please sign in to comment.