import { Document } from '@tiptap/extension-document';
import { HardBreak } from '@tiptap/extension-hard-break';
import { History } from '@tiptap/extension-history';
import { MentionNodeAttrs, Mention } from '@tiptap/extension-mention';
import { Paragraph } from '@tiptap/extension-paragraph';
import { Placeholder } from '@tiptap/extension-placeholder';
import { Text } from '@tiptap/extension-text';
import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { inputClass } from '@/components/ui/input';
import { stepsHooks } from '@/features/pieces/lib/steps-hooks';
import { cn } from '@/lib/utils';
import { flowStructureUtil, isNil } from '@activepieces/shared';
import { useBuilderStateContext } from '../../builder-hooks';
import { textMentionUtils } from './text-input-utils';
type TextInputWithMentionsProps = {
className?: string;
initialValue?: unknown;
onChange: (value: string) => void;
placeholder?: string;
disabled?: boolean;
enableMarkdown?: boolean;
};
const extensions = ({
enableMarkdown,
placeholder,
}: {
placeholder?: string;
enableMarkdown?: boolean;
}) => {
const baseExtensions = [
Placeholder.configure({
placeholder: placeholder,
emptyNodeClass: 'before:text-muted-foreground opacity-75',
}),
Mention.configure({
suggestion: {
char: '',
},
deleteTriggerWithBackspace: true,
renderHTML({ node }) {
const mentionAttrs = node.attrs as unknown as MentionNodeAttrs;
return textMentionUtils.generateMentionHtmlElement(mentionAttrs);
},
}),
];
if (enableMarkdown) {
return [
...baseExtensions,
StarterKit.configure({
heading: {
levels: [1, 2, 3],
},
}),
];
}
return [
...baseExtensions,
Document,
History,
HardBreak,
Text,
Paragraph.configure({
HTMLAttributes: {},
}),
];
};
function convertToText(value: unknown): string {
if (isNil(value)) {
return '';
}
if (typeof value === 'string') {
return value;
}
if (typeof value === 'number') {
return value.toString();
}
return JSON.stringify(value);
}
export const TextInputWithMentions = ({
className,
initialValue,
onChange,
disabled,
placeholder,
enableMarkdown,
}: TextInputWithMentionsProps) => {
const steps = useBuilderStateContext((state) =>
flowStructureUtil.getAllSteps(state.flowVersion.trigger),
);
const stepsMetadata = stepsHooks
.useStepsMetadata(steps)
.map(({ data: metadata }, index) => {
if (metadata) {
return {
...metadata,
stepDisplayName: steps[index].displayName,
};
}
return undefined;
});
const setInsertMentionHandler = useBuilderStateContext(
(state) => state.setInsertMentionHandler,
);
const insertMention = (propertyPath: string) => {
const mentionNode = textMentionUtils.createMentionNodeFromText(
`{{${propertyPath}}}`,
steps,
stepsMetadata,
);
editor?.chain().focus().insertContent(mentionNode).run();
};
const editor = useEditor({
editable: !disabled,
extensions: extensions({ placeholder, enableMarkdown }),
content: {
type: 'doc',
content: textMentionUtils.convertTextToTipTapJsonContent(
convertToText(initialValue),
steps,
stepsMetadata,
),
},
editorProps: {
attributes: {
class: cn(
className ?? cn(inputClass, 'py-2 h-[unset] block min-h-9 '),
textMentionUtils.inputWithMentionsCssClass,
{
'cursor-not-allowed opacity-50': disabled,
},
),
},
},
onUpdate: ({ editor }) => {
const editorContent = editor.getJSON();
const textResult =
textMentionUtils.convertTiptapJsonToText(editorContent);
if (onChange) {
onChange(textResult);
}
},
onFocus: () => {
setInsertMentionHandler(insertMention);
},
});
if (!editor) {
return null;
}
return (
<div className="w-full">
<EditorContent editor={editor} />
</div>
);
};