Response:
diff --git a/apps/app/components/issues/comment/add-comment.tsx b/apps/app/components/issues/comment/add-comment.tsx
index b7504d932..3b3cd21b0 100644
--- a/apps/app/components/issues/comment/add-comment.tsx
+++ b/apps/app/components/issues/comment/add-comment.tsx
@@ -18,23 +18,23 @@ import type { ICurrentUserResponse, IIssueComment } from "types";
// fetch-keys
import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
-const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
- ssr: false,
- loading: () => (
-
-
-
- ),
-});
-import { IRemirrorRichTextEditor } from "components/rich-text-editor";
-
-const WrappedRemirrorRichTextEditor = React.forwardRef<
- IRemirrorRichTextEditor,
- IRemirrorRichTextEditor
->((props, ref) =>
);
-
-WrappedRemirrorRichTextEditor.displayName = "WrappedRemirrorRichTextEditor";
-
+// const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
+// ssr: false,
+// loading: () => (
+//
+//
+//
+// ),
+// });
+// import { IRemirrorRichTextEditor } from "components/rich-text-editor";
+//
+// const WrappedRemirrorRichTextEditor = React.forwardRef<
+// IRemirrorRichTextEditor,
+// IRemirrorRichTextEditor
+// >((props, ref) =>
);
+//
+// WrappedRemirrorRichTextEditor.displayName = "WrappedRemirrorRichTextEditor";
+//
const defaultValues: Partial
= {
comment_json: "",
comment_html: "",
@@ -98,19 +98,19 @@ export const AddComment: React.FC = ({ issueId, user, disabled = false })
-
+ {/* */}
diff --git a/apps/app/components/issues/main-content.tsx b/apps/app/components/issues/main-content.tsx
index 316d39e8a..7c29f0810 100644
--- a/apps/app/components/issues/main-content.tsx
+++ b/apps/app/components/issues/main-content.tsx
@@ -50,11 +50,11 @@ export const IssueMainContent: React.FC
= ({
workspaceSlug && projectId && issueDetails?.parent ? SUB_ISSUES(issueDetails.parent) : null,
workspaceSlug && projectId && issueDetails?.parent
? () =>
- issuesService.subIssues(
- workspaceSlug as string,
- projectId as string,
- issueDetails.parent ?? ""
- )
+ issuesService.subIssues(
+ workspaceSlug as string,
+ projectId as string,
+ issueDetails.parent ?? ""
+ )
: null
);
const siblingIssuesList = siblingIssues?.sub_issues.filter((i) => i.id !== issueDetails.id);
@@ -97,9 +97,8 @@ export const IssueMainContent: React.FC = ({
@@ -130,30 +129,30 @@ export const IssueMainContent: React.FC = ({
isAllowed={memberRole.isMember || memberRole.isOwner || !uneditable}
/>
-
-
-
-
-
-
-
-
-
Comments/Activity
-
-
+ {/*
*/}
+ {/**/}
+ {/*
*/}
+ {/* */}
+ {/*
*/}
+ {/*
*/}
+ {/* */}
+ {/*
Attachments
*/}
+ {/*
*/}
+ {/* */}
+ {/* */}
+ {/*
*/}
+ {/*
*/}
+ {/* */}
+ {/*
Comments/Activity
*/}
+ {/*
*/}
+ {/*
*/}
>
);
diff --git a/apps/app/components/pages/create-update-block-inline.tsx b/apps/app/components/pages/create-update-block-inline.tsx
index 5e2dcb43c..3b73e041b 100644
--- a/apps/app/components/pages/create-update-block-inline.tsx
+++ b/apps/app/components/pages/create-update-block-inline.tsx
@@ -39,22 +39,22 @@ const defaultValues = {
description_html: null,
};
-const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
- ssr: false,
- loading: () => (
-
-
-
- ),
-});
-import { IRemirrorRichTextEditor } from "components/rich-text-editor";
-
-const WrappedRemirrorRichTextEditor = React.forwardRef<
- IRemirrorRichTextEditor,
- IRemirrorRichTextEditor
->((props, ref) => );
-
-WrappedRemirrorRichTextEditor.displayName = "WrappedRemirrorRichTextEditor";
+// const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
+// ssr: false,
+// loading: () => (
+//
+//
+//
+// ),
+// });
+// import { IRemirrorRichTextEditor } from "components/rich-text-editor";
+//
+// const WrappedRemirrorRichTextEditor = React.forwardRef<
+// IRemirrorRichTextEditor,
+// IRemirrorRichTextEditor
+// >((props, ref) => );
+//
+// WrappedRemirrorRichTextEditor.displayName = "WrappedRemirrorRichTextEditor";
export const CreateUpdateBlockInline: React.FC = ({
handleClose,
@@ -296,51 +296,51 @@ export const CreateUpdateBlockInline: React.FC = ({
/>
Date: Wed, 2 Aug 2023 23:04:55 +0530
Subject: [PATCH 02/27] styles migrated for remirror to tiptap transition
---
.../components/issues/description-form.tsx | 75 +++++++++++--------
apps/app/components/issues/form.tsx | 64 ++++++++--------
apps/app/components/issues/props.tsx | 7 ++
apps/app/components/issues/tiptap.tsx | 43 +++++++++++
apps/app/styles/editor.css | 8 ++
5 files changed, 132 insertions(+), 65 deletions(-)
create mode 100644 apps/app/components/issues/props.tsx
create mode 100644 apps/app/components/issues/tiptap.tsx
diff --git a/apps/app/components/issues/description-form.tsx b/apps/app/components/issues/description-form.tsx
index e81c8c1b3..fb7d574ee 100644
--- a/apps/app/components/issues/description-form.tsx
+++ b/apps/app/components/issues/description-form.tsx
@@ -8,14 +8,16 @@ import { Controller, useForm } from "react-hook-form";
import useReloadConfirmations from "hooks/use-reload-confirmation";
// components
import { Loader, TextArea } from "components/ui";
-const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
- ssr: false,
- loading: () => (
-
-
-
- ),
-});
+// const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
+// ssr: false,
+// loading: () => (
+//
+//
+//
+// ),
+// });
+
+import Tiptap from "./tiptap";
// types
import { IIssue } from "types";
@@ -106,9 +108,8 @@ export const IssueDescriptionForm: FC = ({
{characterLimit && (
255 ? "text-red-500" : ""
- }`}
+ className={`${watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : ""
+ }`}
>
{watch("name").length}
@@ -125,31 +126,39 @@ export const IssueDescriptionForm: FC = ({
if (!value && !watch("description_html")) return <>>;
return (
- {
- setShowAlert(true);
- setValue("description", jsonValue);
- }}
- onHTMLChange={(htmlValue) => {
- setShowAlert(true);
- setValue("description_html", htmlValue);
- }}
- onBlur={() => {
- setIsSubmitting(true);
- handleSubmit(handleDescriptionFormSubmit)()
- .then(() => setShowAlert(false))
- .finally(() => setIsSubmitting(false));
- }}
- placeholder="Description"
- editable={isAllowed}
+ ? watch("description_html")
+ : value
+ }
/>
+ // {
+ // setShowAlert(true);
+ // setValue("description", jsonValue);
+ // }}
+ // onHTMLChange={(htmlValue) => {
+ // setShowAlert(true);
+ // setValue("description_html", htmlValue);
+ // }}
+ // onBlur={() => {
+ // setIsSubmitting(true);
+ // handleSubmit(handleDescriptionFormSubmit)()
+ // .then(() => setShowAlert(false))
+ // .finally(() => setIsSubmitting(false));
+ // }}
+ // placeholder="Description"
+ // editable={isAllowed}
+ // />
);
}}
/>
diff --git a/apps/app/components/issues/form.tsx b/apps/app/components/issues/form.tsx
index e6fc11ae2..e16bd317f 100644
--- a/apps/app/components/issues/form.tsx
+++ b/apps/app/components/issues/form.tsx
@@ -37,23 +37,23 @@ import { SparklesIcon, XMarkIcon } from "@heroicons/react/24/outline";
// types
import type { ICurrentUserResponse, IIssue, ISearchIssueResponse } from "types";
// rich-text-editor
-const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
- ssr: false,
- loading: () => (
-
-
-
- ),
-});
+// const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
+// ssr: false,
+// loading: () => (
+//
+//
+//
+// ),
+// });
-import { IRemirrorRichTextEditor } from "components/rich-text-editor";
+// import { IRemirrorRichTextEditor } from "components/rich-text-editor";
-const WrappedRemirrorRichTextEditor = React.forwardRef<
- IRemirrorRichTextEditor,
- IRemirrorRichTextEditor
->((props, ref) => );
-
-WrappedRemirrorRichTextEditor.displayName = "WrappedRemirrorRichTextEditor";
+// const WrappedRemirrorRichTextEditor = React.forwardRef<
+// IRemirrorRichTextEditor,
+// IRemirrorRichTextEditor
+// >((props, ref) => );
+//
+// WrappedRemirrorRichTextEditor.displayName = "WrappedRemirrorRichTextEditor";
const defaultValues: Partial = {
project: "",
@@ -362,23 +362,23 @@ export const IssueForm: FC = ({
AI
- (
- setValue("description", jsonValue)}
- onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)}
- placeholder="Description"
- ref={editorRef}
- />
- )}
- />
+ {/* ( */}
+ {/* setValue("description", jsonValue)} */}
+ {/* onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)} */}
+ {/* placeholder="Description" */}
+ {/* ref={editorRef} */}
+ {/* /> */}
+ {/* )} */}
+ {/* /> */}
{
diff --git a/apps/app/components/issues/props.tsx b/apps/app/components/issues/props.tsx
new file mode 100644
index 000000000..54306429c
--- /dev/null
+++ b/apps/app/components/issues/props.tsx
@@ -0,0 +1,7 @@
+import { EditorProps } from "@tiptap/pm/view";
+
+export const TiptapEditorProps: EditorProps = {
+ attributes: {
+ class: `prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none`,
+ }
+};
diff --git a/apps/app/components/issues/tiptap.tsx b/apps/app/components/issues/tiptap.tsx
new file mode 100644
index 000000000..d3f554d4c
--- /dev/null
+++ b/apps/app/components/issues/tiptap.tsx
@@ -0,0 +1,43 @@
+import Placeholder from '@tiptap/extension-placeholder';
+import { useEditor, EditorContent } from '@tiptap/react';
+import StarterKit from '@tiptap/starter-kit';
+import { EditorBubbleMenu } from './EditorBubbleMenu';
+import { TiptapEditorProps } from "./props";
+
+type TiptapProps = {
+ value: string;
+ noBorder?: boolean;
+ borderOnFocus?: boolean;
+ customClassName?: string;
+}
+
+const Tiptap = ({ value, noBorder, borderOnFocus, customClassName }: TiptapProps) => {
+ const editor = useEditor({
+ editorProps: TiptapEditorProps,
+ extensions: [
+ StarterKit,
+ Placeholder.configure({
+ placeholder: 'Description...',
+ })
+ ],
+ content: value,
+ });
+
+ const editorClassNames = `mt-2 p-3 relative focus:outline-none rounded-md focus:border-custom-border-200
+ ${noBorder ? '' : 'border border-custom-border-200'
+ } ${borderOnFocus ? 'focus:border border-custom-border-200' : 'focus:border-0'
+ } ${customClassName}`;
+
+ return (
+ {
+ editor?.chain().focus().run();
+ }}
+ className={`tiptap-editor-container relative min-h-[150px] ${editorClassNames}`}
+ >
+
+
+ );
+};
+
+export default Tiptap;
diff --git a/apps/app/styles/editor.css b/apps/app/styles/editor.css
index ea4b2e601..293a017b2 100644
--- a/apps/app/styles/editor.css
+++ b/apps/app/styles/editor.css
@@ -8,6 +8,14 @@
margin-left: 1px;
}
+.ProseMirror p.is-editor-empty:first-child::before {
+ color: rgb(var(--color-text-400));
+ content: attr(data-placeholder);
+ float: left;
+ height: 0;
+ pointer-events: none;
+}
+
.ProseMirror {
position: relative;
word-wrap: break-word;
From b078e24d828c68782302a52b111d5ce6900b9445 Mon Sep 17 00:00:00 2001
From: Palanikannan1437 <73993394+Palanikannan1437@users.noreply.github.com>
Date: Thu, 3 Aug 2023 00:15:02 +0530
Subject: [PATCH 03/27] added bubblemenu support with extensions
---
.../components/issues/EditorBubbleMenu.tsx | 116 +++++++++++++++
apps/app/components/issues/extensions.tsx | 127 ++++++++++++++++
apps/app/components/issues/link-selector.tsx | 78 ++++++++++
apps/app/components/issues/node-selector.tsx | 135 ++++++++++++++++++
apps/app/components/issues/props.tsx | 13 +-
apps/app/components/issues/tiptap.tsx | 15 +-
apps/app/components/issues/utils.ts | 6 +
apps/app/package.json | 12 ++
apps/app/styles/editor.css | 91 +++++++++++-
9 files changed, 584 insertions(+), 9 deletions(-)
create mode 100644 apps/app/components/issues/EditorBubbleMenu.tsx
create mode 100644 apps/app/components/issues/extensions.tsx
create mode 100644 apps/app/components/issues/link-selector.tsx
create mode 100644 apps/app/components/issues/node-selector.tsx
create mode 100644 apps/app/components/issues/utils.ts
diff --git a/apps/app/components/issues/EditorBubbleMenu.tsx b/apps/app/components/issues/EditorBubbleMenu.tsx
new file mode 100644
index 000000000..7945c4589
--- /dev/null
+++ b/apps/app/components/issues/EditorBubbleMenu.tsx
@@ -0,0 +1,116 @@
+import { BubbleMenu, BubbleMenuProps } from "@tiptap/react";
+import { FC, useState } from "react";
+import {
+ BoldIcon,
+ ItalicIcon,
+ UnderlineIcon,
+ StrikethroughIcon,
+ CodeIcon,
+} from "lucide-react";
+
+import { NodeSelector } from "./node-selector";
+import { LinkSelector } from "./link-selector";
+import { cn } from "./utils";
+
+export interface BubbleMenuItem {
+ name: string;
+ isActive: () => boolean;
+ command: () => void;
+ icon: typeof BoldIcon;
+}
+
+type EditorBubbleMenuProps = Omit;
+
+export const EditorBubbleMenu: FC = (props) => {
+ const items: BubbleMenuItem[] = [
+ {
+ name: "bold",
+ isActive: () => props.editor.isActive("bold"),
+ command: () => props.editor.chain().focus().toggleBold().run(),
+ icon: BoldIcon,
+ },
+ {
+ name: "italic",
+ isActive: () => props.editor.isActive("italic"),
+ command: () => props.editor.chain().focus().toggleItalic().run(),
+ icon: ItalicIcon,
+ },
+ {
+ name: "underline",
+ isActive: () => props.editor.isActive("underline"),
+ command: () => props.editor.chain().focus().toggleUnderline().run(),
+ icon: UnderlineIcon,
+ },
+ {
+ name: "strike",
+ isActive: () => props.editor.isActive("strike"),
+ command: () => props.editor.chain().focus().toggleStrike().run(),
+ icon: StrikethroughIcon,
+ },
+ {
+ name: "code",
+ isActive: () => props.editor.isActive("code"),
+ command: () => props.editor.chain().focus().toggleCode().run(),
+ icon: CodeIcon,
+ },
+ ];
+
+ const bubbleMenuProps: EditorBubbleMenuProps = {
+ ...props,
+ shouldShow: ({ editor }) => {
+ if (editor.isActive("image")) {
+ return false;
+ }
+ return editor.view.state.selection.content().size > 0;
+ },
+ tippyOptions: {
+ moveTransition: "transform 0.15s ease-out",
+ onHidden: () => {
+ setIsNodeSelectorOpen(false);
+ setIsLinkSelectorOpen(false);
+ },
+ },
+ };
+
+ const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false);
+ const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false);
+
+ return (
+
+ {
+ setIsNodeSelectorOpen(!isNodeSelectorOpen);
+ setIsLinkSelectorOpen(false);
+ }}
+ />
+ {
+ setIsLinkSelectorOpen(!isLinkSelectorOpen);
+ setIsNodeSelectorOpen(false);
+ }}
+ />
+
+ {items.map((item, index) => (
+
+ ))}
+
+
+ );
+};
diff --git a/apps/app/components/issues/extensions.tsx b/apps/app/components/issues/extensions.tsx
new file mode 100644
index 000000000..7e5a2d6a6
--- /dev/null
+++ b/apps/app/components/issues/extensions.tsx
@@ -0,0 +1,127 @@
+import StarterKit from "@tiptap/starter-kit";
+import HorizontalRule from "@tiptap/extension-horizontal-rule";
+import TiptapLink from "@tiptap/extension-link";
+import TiptapImage from "@tiptap/extension-image";
+import Placeholder from "@tiptap/extension-placeholder";
+import TiptapUnderline from "@tiptap/extension-underline";
+import TextStyle from "@tiptap/extension-text-style";
+import { Color } from "@tiptap/extension-color";
+import TaskItem from "@tiptap/extension-task-item";
+import TaskList from "@tiptap/extension-task-list";
+import { Markdown } from "tiptap-markdown";
+import Highlight from "@tiptap/extension-highlight";
+
+// import SlashCommand from "./slash-command";
+import { InputRule } from "@tiptap/core";
+
+export const TiptapExtensions = [
+ StarterKit.configure({
+ bulletList: {
+ HTMLAttributes: {
+ class: "list-disc list-outside leading-3 -mt-2",
+ },
+ },
+ orderedList: {
+ HTMLAttributes: {
+ class: "list-decimal list-outside leading-3 -mt-2",
+ },
+ },
+ listItem: {
+ HTMLAttributes: {
+ class: "leading-normal -mb-2",
+ },
+ },
+ blockquote: {
+ HTMLAttributes: {
+ class: "border-l-4 border-stone-700",
+ },
+ },
+ codeBlock: {
+ HTMLAttributes: {
+ class:
+ "rounded-sm bg-stone-100 p-5 font-mono font-medium text-stone-800",
+ },
+ },
+ code: {
+ HTMLAttributes: {
+ class:
+ "rounded-md bg-stone-200 px-1.5 py-1 font-mono font-medium text-stone-900",
+ spellcheck: "false",
+ },
+ },
+ horizontalRule: false,
+ dropcursor: {
+ color: "#DBEAFE",
+ width: 4,
+ },
+ gapcursor: false,
+ }),
+ HorizontalRule.extend({
+ addInputRules() {
+ return [
+ new InputRule({
+ find: /^(?:---|—-|___\s|\*\*\*\s)$/,
+ handler: ({ state, range }) => {
+ const attributes = {};
+
+ const { tr } = state;
+ const start = range.from;
+ const end = range.to;
+
+ tr.insert(start - 1, this.type.create(attributes)).delete(
+ tr.mapping.map(start),
+ tr.mapping.map(end),
+ );
+ },
+ }),
+ ];
+ },
+ }).configure({
+ HTMLAttributes: {
+ class: "mt-4 mb-6 border-t border-stone-300",
+ },
+ }),
+ TiptapLink.configure({
+ HTMLAttributes: {
+ class:
+ "text-stone-400 underline underline-offset-[3px] hover:text-stone-600 transition-colors cursor-pointer",
+ },
+ }),
+ TiptapImage.configure({
+ allowBase64: true,
+ HTMLAttributes: {
+ class: "rounded-lg border border-stone-200",
+ },
+ }),
+ Placeholder.configure({
+ placeholder: ({ node }) => {
+ if (node.type.name === "heading") {
+ return `Heading ${node.attrs.level}`;
+ }
+ return "Press '/' for commands, or '++' for AI autocomplete...";
+ },
+ includeChildren: true,
+ }),
+ // SlashCommand,
+ TiptapUnderline,
+ TextStyle,
+ Color,
+ Highlight.configure({
+ multicolor: true,
+ }),
+ TaskList.configure({
+ HTMLAttributes: {
+ class: "not-prose pl-2",
+ },
+ }),
+ TaskItem.configure({
+ HTMLAttributes: {
+ class: "flex items-start my-4",
+ },
+ nested: true,
+ }),
+ Markdown.configure({
+ html: false,
+ transformCopiedText: true,
+ }),
+];
diff --git a/apps/app/components/issues/link-selector.tsx b/apps/app/components/issues/link-selector.tsx
new file mode 100644
index 000000000..7bc3ea2e9
--- /dev/null
+++ b/apps/app/components/issues/link-selector.tsx
@@ -0,0 +1,78 @@
+import { Editor } from "@tiptap/core";
+import { Check, Trash } from "lucide-react";
+import { Dispatch, FC, SetStateAction, useEffect, useRef } from "react";
+import { cn } from './utils';
+
+interface LinkSelectorProps {
+ editor: Editor;
+ isOpen: boolean;
+ setIsOpen: Dispatch>;
+}
+
+export const LinkSelector: FC = ({
+ editor,
+ isOpen,
+ setIsOpen,
+}) => {
+ const inputRef = useRef(null);
+
+ // Autofocus on input by default
+ useEffect(() => {
+ inputRef.current && inputRef.current?.focus();
+ });
+
+ return (
+
+
+ {isOpen && (
+
+ )}
+
+ );
+};
+
diff --git a/apps/app/components/issues/node-selector.tsx b/apps/app/components/issues/node-selector.tsx
new file mode 100644
index 000000000..4591e8707
--- /dev/null
+++ b/apps/app/components/issues/node-selector.tsx
@@ -0,0 +1,135 @@
+import { Editor } from "@tiptap/core";
+import {
+ Check,
+ ChevronDown,
+ Heading1,
+ Heading2,
+ Heading3,
+ TextQuote,
+ ListOrdered,
+ TextIcon,
+ Code,
+ CheckSquare,
+} from "lucide-react";
+import { Dispatch, FC, SetStateAction } from "react";
+
+import { BubbleMenuItem } from "./EditorBubbleMenu";
+
+interface NodeSelectorProps {
+ editor: Editor;
+ isOpen: boolean;
+ setIsOpen: Dispatch>;
+}
+
+export const NodeSelector: FC = ({
+ editor,
+ isOpen,
+ setIsOpen,
+}) => {
+ const items: BubbleMenuItem[] = [
+ {
+ name: "Text",
+ icon: TextIcon,
+ command: () =>
+ editor.chain().focus().toggleNode("paragraph", "paragraph").run(),
+ // I feel like there has to be a more efficient way to do this – feel free to PR if you know how!
+ isActive: () =>
+ editor.isActive("paragraph") &&
+ !editor.isActive("bulletList") &&
+ !editor.isActive("orderedList"),
+ },
+ {
+ name: "Heading 1",
+ icon: Heading1,
+ command: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
+ isActive: () => editor.isActive("heading", { level: 1 }),
+ },
+ {
+ name: "Heading 2",
+ icon: Heading2,
+ command: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
+ isActive: () => editor.isActive("heading", { level: 2 }),
+ },
+ {
+ name: "Heading 3",
+ icon: Heading3,
+ command: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
+ isActive: () => editor.isActive("heading", { level: 3 }),
+ },
+ {
+ name: "To-do List",
+ icon: CheckSquare,
+ command: () => editor.chain().focus().toggleTaskList().run(),
+ isActive: () => editor.isActive("taskItem"),
+ },
+ {
+ name: "Bullet List",
+ icon: ListOrdered,
+ command: () => editor.chain().focus().toggleBulletList().run(),
+ isActive: () => editor.isActive("bulletList"),
+ },
+ {
+ name: "Numbered List",
+ icon: ListOrdered,
+ command: () => editor.chain().focus().toggleOrderedList().run(),
+ isActive: () => editor.isActive("orderedList"),
+ },
+ {
+ name: "Quote",
+ icon: TextQuote,
+ command: () =>
+ editor
+ .chain()
+ .focus()
+ .toggleNode("paragraph", "paragraph")
+ .toggleBlockquote()
+ .run(),
+ isActive: () => editor.isActive("blockquote"),
+ },
+ {
+ name: "Code",
+ icon: Code,
+ command: () => editor.chain().focus().toggleCodeBlock().run(),
+ isActive: () => editor.isActive("codeBlock"),
+ },
+ ];
+
+ const activeItem = items.filter((item) => item.isActive()).pop() ?? {
+ name: "Multiple",
+ };
+
+ return (
+
+
+
+ {isOpen && (
+
+ {items.map((item, index) => (
+
+ ))}
+
+ )}
+
+ );
+};
diff --git a/apps/app/components/issues/props.tsx b/apps/app/components/issues/props.tsx
index 54306429c..6470b37c1 100644
--- a/apps/app/components/issues/props.tsx
+++ b/apps/app/components/issues/props.tsx
@@ -3,5 +3,16 @@ import { EditorProps } from "@tiptap/pm/view";
export const TiptapEditorProps: EditorProps = {
attributes: {
class: `prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none`,
- }
+ },
+ handleDOMEvents: {
+ keydown: (_view, event) => {
+ // prevent default event listeners from firing when slash command is active
+ if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) {
+ const slashCommand = document.querySelector("#slash-command");
+ if (slashCommand) {
+ return true;
+ }
+ }
+ },
+ },
};
diff --git a/apps/app/components/issues/tiptap.tsx b/apps/app/components/issues/tiptap.tsx
index d3f554d4c..9e298ef3c 100644
--- a/apps/app/components/issues/tiptap.tsx
+++ b/apps/app/components/issues/tiptap.tsx
@@ -2,6 +2,7 @@ import Placeholder from '@tiptap/extension-placeholder';
import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { EditorBubbleMenu } from './EditorBubbleMenu';
+import { TiptapExtensions } from './extensions';
import { TiptapEditorProps } from "./props";
type TiptapProps = {
@@ -14,12 +15,13 @@ type TiptapProps = {
const Tiptap = ({ value, noBorder, borderOnFocus, customClassName }: TiptapProps) => {
const editor = useEditor({
editorProps: TiptapEditorProps,
- extensions: [
- StarterKit,
- Placeholder.configure({
- placeholder: 'Description...',
- })
- ],
+ extensions: TiptapExtensions,
+ // extensions: [
+ // StarterKit,
+ // Placeholder.configure({
+ // placeholder: 'Description...',
+ // })
+ // ],
content: value,
});
@@ -35,6 +37,7 @@ const Tiptap = ({ value, noBorder, borderOnFocus, customClassName }: TiptapProps
}}
className={`tiptap-editor-container relative min-h-[150px] ${editorClassNames}`}
>
+ {editor && }
);
diff --git a/apps/app/components/issues/utils.ts b/apps/app/components/issues/utils.ts
new file mode 100644
index 000000000..a5ef19350
--- /dev/null
+++ b/apps/app/components/issues/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/apps/app/package.json b/apps/app/package.json
index 8c8c0d1f7..eb8bbaae0 100644
--- a/apps/app/package.json
+++ b/apps/app/package.json
@@ -27,17 +27,27 @@
"@nivo/scatterplot": "0.80.0",
"@sentry/nextjs": "^7.36.0",
"@tailwindcss/typography": "^0.5.9",
+ "@tiptap/extension-color": "^2.0.4",
+ "@tiptap/extension-highlight": "^2.0.4",
+ "@tiptap/extension-image": "^2.0.4",
+ "@tiptap/extension-link": "^2.0.4",
"@tiptap/extension-placeholder": "^2.0.4",
+ "@tiptap/extension-task-item": "^2.0.4",
+ "@tiptap/extension-task-list": "^2.0.4",
+ "@tiptap/extension-text-style": "^2.0.4",
+ "@tiptap/extension-underline": "^2.0.4",
"@tiptap/pm": "^2.0.4",
"@tiptap/react": "^2.0.4",
"@tiptap/starter-kit": "^2.0.4",
"@types/lodash.debounce": "^4.0.7",
"@types/react-datepicker": "^4.8.0",
"axios": "^1.1.3",
+ "clsx": "^2.0.0",
"cmdk": "^0.2.0",
"dotenv": "^16.0.3",
"js-cookie": "^3.0.1",
"lodash.debounce": "^4.0.8",
+ "lucide-react": "^0.263.1",
"next": "12.3.2",
"next-pwa": "^5.6.0",
"next-themes": "^0.2.1",
@@ -51,6 +61,8 @@
"react-hook-form": "^7.38.0",
"react-markdown": "^8.0.7",
"swr": "^2.1.3",
+ "tailwind-merge": "^1.14.0",
+ "tiptap-markdown": "^0.8.2",
"tlds": "^1.238.0",
"uuid": "^9.0.0"
},
diff --git a/apps/app/styles/editor.css b/apps/app/styles/editor.css
index 293a017b2..6f45ecc2a 100644
--- a/apps/app/styles/editor.css
+++ b/apps/app/styles/editor.css
@@ -9,13 +9,100 @@
}
.ProseMirror p.is-editor-empty:first-child::before {
- color: rgb(var(--color-text-400));
content: attr(data-placeholder);
float: left;
- height: 0;
+ color: rgb(var(--color-text-400));
pointer-events: none;
+ height: 0;
}
+.ProseMirror .is-empty::before {
+ content: attr(data-placeholder);
+ float: left;
+ color: rgb(var(--color-text-400));
+ pointer-events: none;
+ height: 0;
+}
+
+/* Custom image styles */
+
+.ProseMirror img {
+ transition: filter 0.1s ease-in-out;
+
+ &:hover {
+ cursor: pointer;
+ filter: brightness(90%);
+ }
+
+ &.ProseMirror-selectednode {
+ outline: 3px solid #5abbf7;
+ filter: brightness(90%);
+ }
+}
+
+/* Custom TODO list checkboxes – shoutout to this awesome tutorial: https://moderncss.dev/pure-css-custom-checkbox-style/ */
+
+ul[data-type="taskList"] li > label {
+ margin-right: 0.2rem;
+ user-select: none;
+}
+
+@media screen and (max-width: 768px) {
+ ul[data-type="taskList"] li > label {
+ margin-right: 0.5rem;
+ }
+}
+
+ul[data-type="taskList"] li > label input[type="checkbox"] {
+ -webkit-appearance: none;
+ appearance: none;
+ background-color: var(--novel-white);
+ margin: 0;
+ cursor: pointer;
+ width: 1.2em;
+ height: 1.2em;
+ position: relative;
+ top: 5px;
+ border: 2px solid var(--novel-stone-900);
+ margin-right: 0.3rem;
+ display: grid;
+ place-content: center;
+
+ &:hover {
+ background-color: var(--novel-stone-50);
+ }
+
+ &:active {
+ background-color: var(--novel-stone-200);
+ }
+
+ &::before {
+ content: "";
+ width: 0.65em;
+ height: 0.65em;
+ transform: scale(0);
+ transition: 120ms transform ease-in-out;
+ box-shadow: inset 1em 1em;
+ transform-origin: center;
+ clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
+ }
+
+ &:checked::before {
+ transform: scale(1);
+ }
+}
+
+ul[data-type="taskList"] li[data-checked="true"] > div > p {
+ color: var(--novel-stone-400);
+ text-decoration: line-through;
+ text-decoration-thickness: 2px;
+}
+
+/* Overwrite tippy-box original max-width */
+
+.tippy-box {
+ max-width: 400px !important;
+}
.ProseMirror {
position: relative;
word-wrap: break-word;
From 727570e34742408bf3d83a38b5f695192a5827e0 Mon Sep 17 00:00:00 2001
From: Palanikannan1437 <73993394+Palanikannan1437@users.noreply.github.com>
Date: Sat, 5 Aug 2023 04:50:18 +0530
Subject: [PATCH 04/27] fixed css for task lists and code with syntax
highlighting
---
apps/app/postcss.config.js | 1 +
apps/app/styles/editor.css | 50 +++++++++++++++++++++----------------
apps/app/tailwind.config.js | 5 +++-
3 files changed, 33 insertions(+), 23 deletions(-)
diff --git a/apps/app/postcss.config.js b/apps/app/postcss.config.js
index 12a703d90..cbfea5ea2 100644
--- a/apps/app/postcss.config.js
+++ b/apps/app/postcss.config.js
@@ -1,5 +1,6 @@
module.exports = {
plugins: {
+ "tailwindcss/nesting": {},
tailwindcss: {},
autoprefixer: {},
},
diff --git a/apps/app/styles/editor.css b/apps/app/styles/editor.css
index 6f45ecc2a..f8f91ca34 100644
--- a/apps/app/styles/editor.css
+++ b/apps/app/styles/editor.css
@@ -1,13 +1,3 @@
-.empty-node::after {
- content: attr(data-placeholder);
- color: rgb(var(--color-text-400));
-
- position: absolute;
- pointer-events: none;
- top: 15px;
- margin-left: 1px;
-}
-
.ProseMirror p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left;
@@ -42,38 +32,37 @@
/* Custom TODO list checkboxes – shoutout to this awesome tutorial: https://moderncss.dev/pure-css-custom-checkbox-style/ */
-ul[data-type="taskList"] li > label {
+ul[data-type="taskList"] li>label {
margin-right: 0.2rem;
user-select: none;
}
@media screen and (max-width: 768px) {
- ul[data-type="taskList"] li > label {
+ ul[data-type="taskList"] li>label {
margin-right: 0.5rem;
}
}
-ul[data-type="taskList"] li > label input[type="checkbox"] {
+ul[data-type="taskList"] li>label input[type="checkbox"] {
-webkit-appearance: none;
appearance: none;
- background-color: var(--novel-white);
+ background-color: rgb(var(--color-background-100));
margin: 0;
cursor: pointer;
- width: 1.2em;
- height: 1.2em;
+ width: 1.2rem;
+ height: 1.2rem;
position: relative;
- top: 5px;
- border: 2px solid var(--novel-stone-900);
+ border: 2px solid rgb(var(--color-text-100));
margin-right: 0.3rem;
display: grid;
place-content: center;
&:hover {
- background-color: var(--novel-stone-50);
+ background-color: rgb(var(--color-background-80));
}
&:active {
- background-color: var(--novel-stone-200);
+ background-color: rgb(var(--color-background-90));
}
&::before {
@@ -92,8 +81,8 @@ ul[data-type="taskList"] li > label input[type="checkbox"] {
}
}
-ul[data-type="taskList"] li[data-checked="true"] > div > p {
- color: var(--novel-stone-400);
+ul[data-type="taskList"] li[data-checked="true"]>div>p {
+ color: rgb(var(--color-text-200));
text-decoration: line-through;
text-decoration-thickness: 2px;
}
@@ -103,6 +92,7 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
.tippy-box {
max-width: 400px !important;
}
+
.ProseMirror {
position: relative;
word-wrap: break-word;
@@ -126,6 +116,22 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
-moz-appearance: textfield;
}
+.ProseMirror pre {
+ background: #121212;
+ border-radius: 0.375rem;
+ border-color: rgba(var(--color-background-100));
+ border: 0.5px solid;
+ font-family: "JetBrainsMono", monospace;
+ padding: 0.75rem 1rem;
+}
+
+.ProseMirror pre code {
+ background: none;
+ color: inherit;
+ font-size: 0.8rem;
+ padding: 0;
+}
+
.ProseMirror-icon {
display: inline-block;
line-height: 0.8;
diff --git a/apps/app/tailwind.config.js b/apps/app/tailwind.config.js
index fbe0994b4..11e1946a5 100644
--- a/apps/app/tailwind.config.js
+++ b/apps/app/tailwind.config.js
@@ -182,5 +182,8 @@ module.exports = {
custom: ["Inter", "sans-serif"],
},
},
- plugins: [require("@tailwindcss/typography")],
+ plugins: [
+ require("tailwindcss-animate"),
+ require("@tailwindcss/typography")
+ ],
};
From 60f1b7346d21465d343b60162b263d433fb5d057 Mon Sep 17 00:00:00 2001
From: Palanikannan1437 <73993394+Palanikannan1437@users.noreply.github.com>
Date: Sat, 5 Aug 2023 04:50:59 +0530
Subject: [PATCH 05/27] added support for slash command
---
apps/app/components/issues/slash-command.tsx | 350 +++++++++++++++++++
1 file changed, 350 insertions(+)
create mode 100644 apps/app/components/issues/slash-command.tsx
diff --git a/apps/app/components/issues/slash-command.tsx b/apps/app/components/issues/slash-command.tsx
new file mode 100644
index 000000000..d869001cd
--- /dev/null
+++ b/apps/app/components/issues/slash-command.tsx
@@ -0,0 +1,350 @@
+import React, {
+ useState,
+ useEffect,
+ useCallback,
+ ReactNode,
+ useRef,
+ useLayoutEffect,
+} from "react";
+import { Editor, Range, Extension } from "@tiptap/core";
+import Suggestion from "@tiptap/suggestion";
+import { ReactRenderer } from "@tiptap/react";
+import tippy from "tippy.js";
+import {
+ Heading1,
+ Heading2,
+ Heading3,
+ List,
+ ListOrdered,
+ Text,
+ TextQuote,
+ Code,
+ MinusSquare,
+ CheckSquare,
+} from "lucide-react";
+
+interface CommandItemProps {
+ title: string;
+ description: string;
+ icon: ReactNode;
+}
+
+interface CommandProps {
+ editor: Editor;
+ range: Range;
+}
+
+const Command = Extension.create({
+ name: "slash-command",
+ addOptions() {
+ return {
+ suggestion: {
+ char: "/",
+ command: ({
+ editor,
+ range,
+ props,
+ }: {
+ editor: Editor;
+ range: Range;
+ props: any;
+ }) => {
+ props.command({ editor, range });
+ },
+ },
+ };
+ },
+ addProseMirrorPlugins() {
+ return [
+ Suggestion({
+ editor: this.editor,
+ ...this.options.suggestion,
+ }),
+ ];
+ },
+});
+
+const getSuggestionItems = ({ query }: { query: string }) =>
+ [
+ {
+ title: "Text",
+ description: "Just start typing with plain text.",
+ searchTerms: ["p", "paragraph"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ editor
+ .chain()
+ .focus()
+ .deleteRange(range)
+ .toggleNode("paragraph", "paragraph")
+ .run();
+ },
+ },
+ {
+ title: "Heading 1",
+ description: "Big section heading.",
+ searchTerms: ["title", "big", "large"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ editor
+ .chain()
+ .focus()
+ .deleteRange(range)
+ .setNode("heading", { level: 1 })
+ .run();
+ },
+ },
+ {
+ title: "Heading 2",
+ description: "Medium section heading.",
+ searchTerms: ["subtitle", "medium"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ editor
+ .chain()
+ .focus()
+ .deleteRange(range)
+ .setNode("heading", { level: 2 })
+ .run();
+ },
+ },
+ {
+ title: "Heading 3",
+ description: "Small section heading.",
+ searchTerms: ["subtitle", "small"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ editor
+ .chain()
+ .focus()
+ .deleteRange(range)
+ .setNode("heading", { level: 3 })
+ .run();
+ },
+ },
+ {
+ title: "To-do List",
+ description: "Track tasks with a to-do list.",
+ searchTerms: ["todo", "task", "list", "check", "checkbox"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ editor.chain().focus().deleteRange(range).toggleTaskList().run();
+ },
+ },
+ {
+ title: "Bullet List",
+ description: "Create a simple bullet list.",
+ searchTerms: ["unordered", "point"],
+ icon:
,
+ command: ({ editor, range }: CommandProps) => {
+ editor.chain().focus().deleteRange(range).toggleBulletList().run();
+ },
+ },
+ {
+ title: "Divider",
+ description: "Visually divide blocks",
+ searchTerms: ["line", "divider", "horizontal", "rule", "separate"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ editor.chain().focus().deleteRange(range).setHorizontalRule().run()
+ },
+ },
+ {
+ title: "Numbered List",
+ description: "Create a list with numbering.",
+ searchTerms: ["ordered"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ editor.chain().focus().deleteRange(range).toggleOrderedList().run();
+ },
+ },
+ {
+ title: "Quote",
+ description: "Capture a quote.",
+ searchTerms: ["blockquote"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) =>
+ editor
+ .chain()
+ .focus()
+ .deleteRange(range)
+ .toggleNode("paragraph", "paragraph")
+ .toggleBlockquote()
+ .run(),
+ },
+ {
+ title: "Code",
+ description: "Capture a code snippet.",
+ searchTerms: ["codeblock"],
+ icon:
,
+ command: ({ editor, range }: CommandProps) =>
+ editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
+ },
+ ].filter((item) => {
+ if (typeof query === "string" && query.length > 0) {
+ const search = query.toLowerCase();
+ return (
+ item.title.toLowerCase().includes(search) ||
+ item.description.toLowerCase().includes(search) ||
+ (item.searchTerms &&
+ item.searchTerms.some((term: string) => term.includes(search)))
+ );
+ }
+ return true;
+ });;
+
+export const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
+ const containerHeight = container.offsetHeight;
+ const itemHeight = item ? item.offsetHeight : 0;
+
+ const top = item.offsetTop;
+ const bottom = top + itemHeight;
+
+ if (top < container.scrollTop) {
+ container.scrollTop -= container.scrollTop - top + 5;
+ } else if (bottom > containerHeight + container.scrollTop) {
+ container.scrollTop += bottom - containerHeight - container.scrollTop + 5;
+ }
+};
+
+const CommandList = ({
+ items,
+ command,
+ editor,
+ range,
+}: {
+ items: CommandItemProps[];
+ command: any;
+ editor: any;
+ range: any;
+}) => {
+ const [selectedIndex, setSelectedIndex] = useState(0);
+
+ const selectItem = useCallback(
+ (index: number) => {
+ const item = items[index];
+ if (item) {
+ command(item);
+ }
+ },
+ [command, items],
+ );
+
+ useEffect(() => {
+ const navigationKeys = ["ArrowUp", "ArrowDown", "Enter"];
+ const onKeyDown = (e: KeyboardEvent) => {
+ if (navigationKeys.includes(e.key)) {
+ e.preventDefault();
+ if (e.key === "ArrowUp") {
+ setSelectedIndex((selectedIndex + items.length - 1) % items.length);
+ return true;
+ }
+ if (e.key === "ArrowDown") {
+ setSelectedIndex((selectedIndex + 1) % items.length);
+ return true;
+ }
+ if (e.key === "Enter") {
+ selectItem(selectedIndex);
+ return true;
+ }
+ return false;
+ }
+ };
+ document.addEventListener("keydown", onKeyDown);
+ return () => {
+ document.removeEventListener("keydown", onKeyDown);
+ };
+ }, [items, selectedIndex, setSelectedIndex, selectItem]);
+
+ useEffect(() => {
+ setSelectedIndex(0);
+ }, [items]);
+
+ const commandListContainer = useRef(null);
+
+ useLayoutEffect(() => {
+ const container = commandListContainer?.current;
+
+ const item = container?.children[selectedIndex] as HTMLElement;
+
+ if (item && container) updateScrollView(container, item);
+ }, [selectedIndex]);
+
+ return items.length > 0 ? (
+
+ {items.map((item: CommandItemProps, index: number) =>
+
+ )}
+
+ ) : null;
+};
+
+const renderItems = () => {
+ let component: ReactRenderer | null = null;
+ let popup: any | null = null;
+
+ return {
+ onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
+ component = new ReactRenderer(CommandList, {
+ props,
+ editor: props.editor,
+ });
+
+ // @ts-ignore
+ popup = tippy("body", {
+ getReferenceClientRect: props.clientRect,
+ appendTo: () => document.body,
+ content: component.element,
+ showOnCreate: true,
+ interactive: true,
+ trigger: "manual",
+ placement: "bottom-start",
+ });
+ },
+ onUpdate: (props: { editor: Editor; clientRect: DOMRect }) => {
+ component?.updateProps(props);
+
+ popup &&
+ popup[0].setProps({
+ getReferenceClientRect: props.clientRect,
+ });
+ },
+ onKeyDown: (props: { event: KeyboardEvent }) => {
+ if (props.event.key === "Escape") {
+ popup?.[0].hide();
+
+ return true;
+ }
+
+ // @ts-ignore
+ return component?.ref?.onKeyDown(props);
+ },
+ onExit: () => {
+ popup?.[0].destroy();
+ component?.destroy();
+ },
+ };
+};
+
+const SlashCommand = Command.configure({
+ suggestion: {
+ items: getSuggestionItems,
+ render: renderItems,
+ },
+});
+
+export default SlashCommand;
From 50e7c5924c906bf521748ebc4623fcec3217f20a Mon Sep 17 00:00:00 2001
From: Palanikannan1437 <73993394+Palanikannan1437@users.noreply.github.com>
Date: Sat, 5 Aug 2023 04:51:58 +0530
Subject: [PATCH 06/27] fixed bubble menu to match styles and added better
seperation in UI
---
.../components/issues/EditorBubbleMenu.tsx | 4 +--
apps/app/components/issues/extensions.tsx | 30 +++++++++++--------
apps/app/components/issues/link-selector.tsx | 11 ++++---
apps/app/components/issues/node-selector.tsx | 21 ++++++-------
apps/app/package.json | 8 +++++
5 files changed, 44 insertions(+), 30 deletions(-)
diff --git a/apps/app/components/issues/EditorBubbleMenu.tsx b/apps/app/components/issues/EditorBubbleMenu.tsx
index 7945c4589..32ad3f2a9 100644
--- a/apps/app/components/issues/EditorBubbleMenu.tsx
+++ b/apps/app/components/issues/EditorBubbleMenu.tsx
@@ -78,7 +78,7 @@ export const EditorBubbleMenu: FC = (props) => {
return (
= (props) => {