feat: update comments to use tiptap editor
This commit is contained in:
@@ -387,46 +387,54 @@ export interface SlashNodeAttrs {
|
|||||||
label?: string | null;
|
label?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CommandItems: SlashCommandItem[] = [
|
const getCommandItems = (disableHeadings: boolean): SlashCommandItem[] => {
|
||||||
{
|
const headingCommands: SlashCommandItem[] = disableHeadings
|
||||||
title: "Heading 1",
|
? []
|
||||||
icon: <HiH1 />,
|
: [
|
||||||
command: ({ editor }) =>
|
{
|
||||||
editor.chain().focus().setHeading({ level: 1 }).run(),
|
title: "Heading 1",
|
||||||
},
|
icon: <HiH1 />,
|
||||||
{
|
command: ({ editor }) =>
|
||||||
title: "Heading 2",
|
editor.chain().focus().setHeading({ level: 1 }).run(),
|
||||||
icon: <HiH2 />,
|
},
|
||||||
command: ({ editor }) =>
|
{
|
||||||
editor.chain().focus().setHeading({ level: 2 }).run(),
|
title: "Heading 2",
|
||||||
},
|
icon: <HiH2 />,
|
||||||
{
|
command: ({ editor }) =>
|
||||||
title: "Heading 3",
|
editor.chain().focus().setHeading({ level: 2 }).run(),
|
||||||
icon: <HiH3 />,
|
},
|
||||||
command: ({ editor }) =>
|
{
|
||||||
editor.chain().focus().setHeading({ level: 3 }).run(),
|
title: "Heading 3",
|
||||||
},
|
icon: <HiH3 />,
|
||||||
{
|
command: ({ editor }) =>
|
||||||
title: "Bullet List",
|
editor.chain().focus().setHeading({ level: 3 }).run(),
|
||||||
icon: <HiOutlineListBullet />,
|
},
|
||||||
command: ({ editor }) => editor.chain().focus().toggleBulletList().run(),
|
];
|
||||||
},
|
|
||||||
{
|
return [
|
||||||
title: "Ordered List",
|
...headingCommands,
|
||||||
icon: <HiOutlineNumberedList />,
|
{
|
||||||
command: ({ editor }) => editor.chain().focus().toggleOrderedList().run(),
|
title: "Bullet List",
|
||||||
},
|
icon: <HiOutlineListBullet />,
|
||||||
{
|
command: ({ editor }) => editor.chain().focus().toggleBulletList().run(),
|
||||||
title: "Blockquote",
|
},
|
||||||
icon: <HiOutlineChatBubbleLeftEllipsis />,
|
{
|
||||||
command: ({ editor }) => editor.chain().focus().toggleBlockquote().run(),
|
title: "Ordered List",
|
||||||
},
|
icon: <HiOutlineNumberedList />,
|
||||||
{
|
command: ({ editor }) => editor.chain().focus().toggleOrderedList().run(),
|
||||||
title: "Code Block",
|
},
|
||||||
icon: <HiOutlineCodeBracketSquare />,
|
{
|
||||||
command: ({ editor }) => editor.chain().focus().toggleCodeBlock().run(),
|
title: "Blockquote",
|
||||||
},
|
icon: <HiOutlineChatBubbleLeftEllipsis />,
|
||||||
];
|
command: ({ editor }) => editor.chain().focus().toggleBlockquote().run(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Code Block",
|
||||||
|
icon: <HiOutlineCodeBracketSquare />,
|
||||||
|
command: ({ editor }) => editor.chain().focus().toggleCodeBlock().run(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
export default function Editor({
|
export default function Editor({
|
||||||
content,
|
content,
|
||||||
@@ -435,6 +443,8 @@ export default function Editor({
|
|||||||
readOnly = false,
|
readOnly = false,
|
||||||
workspaceMembers,
|
workspaceMembers,
|
||||||
enableYouTubeEmbed = true,
|
enableYouTubeEmbed = true,
|
||||||
|
placeholder,
|
||||||
|
disableHeadings = false,
|
||||||
}: {
|
}: {
|
||||||
content: string | null;
|
content: string | null;
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
@@ -442,18 +452,23 @@ export default function Editor({
|
|||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
workspaceMembers: WorkspaceMember[];
|
workspaceMembers: WorkspaceMember[];
|
||||||
enableYouTubeEmbed?: boolean;
|
enableYouTubeEmbed?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
disableHeadings?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const editor = useEditor(
|
const editor = useEditor(
|
||||||
{
|
{
|
||||||
extensions: [
|
extensions: [
|
||||||
StarterKit,
|
StarterKit.configure({
|
||||||
|
heading: disableHeadings ? false : undefined,
|
||||||
|
}),
|
||||||
Markdown,
|
Markdown,
|
||||||
Placeholder.configure({
|
Placeholder.configure({
|
||||||
placeholder: readOnly
|
placeholder: readOnly
|
||||||
? ""
|
? ""
|
||||||
: t`Add description... (type '/' to open commands or '@' to mention)`,
|
: placeholder ??
|
||||||
|
t`Add description... (type '/' to open commands or '@' to mention)`,
|
||||||
}),
|
}),
|
||||||
Link.configure({
|
Link.configure({
|
||||||
openOnClick: true,
|
openOnClick: true,
|
||||||
@@ -466,10 +481,10 @@ export default function Editor({
|
|||||||
autolink: true,
|
autolink: true,
|
||||||
}),
|
}),
|
||||||
SlashCommands.configure({
|
SlashCommands.configure({
|
||||||
commandItems: CommandItems,
|
commandItems: getCommandItems(disableHeadings),
|
||||||
suggestion: {
|
suggestion: {
|
||||||
items: ({ query }: { query: string }) =>
|
items: ({ query }: { query: string }) =>
|
||||||
filterSlashCommandItems(CommandItems, query),
|
filterSlashCommandItems(getCommandItems(disableHeadings), query),
|
||||||
startOfLine: true,
|
startOfLine: true,
|
||||||
char: "/",
|
char: "/",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
import { formatDistanceToNow } from "date-fns";
|
import { formatDistanceToNow } from "date-fns";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import ContentEditable from "react-contenteditable";
|
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { HiEllipsisHorizontal, HiPencil, HiTrash } from "react-icons/hi2";
|
import { HiEllipsisHorizontal, HiPencil, HiTrash } from "react-icons/hi2";
|
||||||
|
|
||||||
import Avatar from "~/components/Avatar";
|
import Avatar from "~/components/Avatar";
|
||||||
import Button from "~/components/Button";
|
import Button from "~/components/Button";
|
||||||
|
import Editor from "~/components/Editor";
|
||||||
|
import type { WorkspaceMember } from "~/components/Editor";
|
||||||
import Dropdown from "~/components/Dropdown";
|
import Dropdown from "~/components/Dropdown";
|
||||||
import { usePermissions } from "~/hooks/usePermissions";
|
import { usePermissions } from "~/hooks/usePermissions";
|
||||||
import { useModal } from "~/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
@@ -29,7 +30,6 @@ const Comment = ({
|
|||||||
createdAt,
|
createdAt,
|
||||||
comment,
|
comment,
|
||||||
isAuthor,
|
isAuthor,
|
||||||
isAdmin,
|
|
||||||
isEdited = false,
|
isEdited = false,
|
||||||
isViewOnly = false,
|
isViewOnly = false,
|
||||||
}: {
|
}: {
|
||||||
@@ -42,7 +42,6 @@ const Comment = ({
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
comment: string | undefined;
|
comment: string | undefined;
|
||||||
isAuthor: boolean;
|
isAuthor: boolean;
|
||||||
isAdmin: boolean;
|
|
||||||
isEdited: boolean;
|
isEdited: boolean;
|
||||||
isViewOnly: boolean;
|
isViewOnly: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -57,6 +56,30 @@ const Comment = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: cardData } = api.card.byId.useQuery(
|
||||||
|
{
|
||||||
|
cardPublicId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!cardPublicId && cardPublicId.length >= 12,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const workspaceMembers: WorkspaceMember[] =
|
||||||
|
cardData?.list.board.workspace.members
|
||||||
|
.filter((member) => member.email)
|
||||||
|
.map((member) => ({
|
||||||
|
publicId: member.publicId,
|
||||||
|
email: member.email,
|
||||||
|
user: member.user
|
||||||
|
? {
|
||||||
|
id: member.user.id,
|
||||||
|
name: member.user.name ?? null,
|
||||||
|
image: member.user.image ?? null,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
})) ?? [];
|
||||||
|
|
||||||
if (!publicId) return null;
|
if (!publicId) return null;
|
||||||
|
|
||||||
const updateCommentMutation = api.card.updateComment.useMutation({
|
const updateCommentMutation = api.card.updateComment.useMutation({
|
||||||
@@ -142,21 +165,28 @@ const Comment = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!isEditing ? (
|
{!isEditing ? (
|
||||||
<ContentEditable
|
<div className="mt-2">
|
||||||
html={comment ?? ""}
|
<Editor
|
||||||
disabled={true}
|
content={comment ?? null}
|
||||||
className="break-anywhere mt-2 text-sm"
|
readOnly={true}
|
||||||
/>
|
workspaceMembers={workspaceMembers}
|
||||||
|
enableYouTubeEmbed={false}
|
||||||
|
disableHeadings={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<ContentEditable
|
<div className="mt-2">
|
||||||
placeholder={t`Add a comment...`}
|
<Editor
|
||||||
html={watch("comment")}
|
content={watch("comment")}
|
||||||
disabled={false}
|
onChange={(value) => setValue("comment", value)}
|
||||||
onChange={(e) => setValue("comment", e.target.value)}
|
workspaceMembers={workspaceMembers}
|
||||||
className="block w-full max-w-[800px] border-0 bg-transparent py-1.5 text-sm text-light-900 focus-visible:outline-none dark:text-dark-1000 sm:text-sm sm:leading-6"
|
enableYouTubeEmbed={false}
|
||||||
/>
|
placeholder={t`Add comment... (type '/' to open commands or '@' to mention)`}
|
||||||
<div className="flex justify-end space-x-2">
|
disableHeadings={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end space-x-2 mt-2">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
import ContentEditable from "react-contenteditable";
|
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { HiOutlineArrowUp } from "react-icons/hi2";
|
import { HiOutlineArrowUp } from "react-icons/hi2";
|
||||||
|
|
||||||
|
import Editor from "~/components/Editor";
|
||||||
|
import type { WorkspaceMember } from "~/components/Editor";
|
||||||
import LoadingSpinner from "~/components/LoadingSpinner";
|
import LoadingSpinner from "~/components/LoadingSpinner";
|
||||||
import { usePermissions } from "~/hooks/usePermissions";
|
import { usePermissions } from "~/hooks/usePermissions";
|
||||||
import { usePopup } from "~/providers/popup";
|
import { usePopup } from "~/providers/popup";
|
||||||
@@ -13,7 +14,13 @@ interface FormValues {
|
|||||||
comment: string;
|
comment: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NewCommentForm = ({ cardPublicId }: { cardPublicId: string }) => {
|
const NewCommentForm = ({
|
||||||
|
cardPublicId,
|
||||||
|
workspaceMembers,
|
||||||
|
}: {
|
||||||
|
cardPublicId: string;
|
||||||
|
workspaceMembers: WorkspaceMember[];
|
||||||
|
}) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { showPopup } = usePopup();
|
const { showPopup } = usePopup();
|
||||||
const { canCreateComment } = usePermissions();
|
const { canCreateComment } = usePermissions();
|
||||||
@@ -23,10 +30,6 @@ const NewCommentForm = ({ cardPublicId }: { cardPublicId: string }) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const queryParams = {
|
|
||||||
cardPublicId,
|
|
||||||
};
|
|
||||||
|
|
||||||
const addCommentMutation = api.card.addComment.useMutation({
|
const addCommentMutation = api.card.addComment.useMutation({
|
||||||
onError: (_error, _newList) => {
|
onError: (_error, _newList) => {
|
||||||
showPopup({
|
showPopup({
|
||||||
@@ -57,18 +60,13 @@ const NewCommentForm = ({ cardPublicId }: { cardPublicId: string }) => {
|
|||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
className="flex w-full max-w-[800px] flex-col rounded-xl border border-light-600 bg-light-100 p-4 text-light-900 focus-visible:outline-none dark:border-dark-400 dark:bg-dark-100 dark:text-dark-1000 sm:text-sm sm:leading-6"
|
className="flex w-full max-w-[800px] flex-col rounded-xl border border-light-600 bg-light-100 p-4 text-light-900 focus-visible:outline-none dark:border-dark-400 dark:bg-dark-100 dark:text-dark-1000 sm:text-sm sm:leading-6"
|
||||||
>
|
>
|
||||||
<ContentEditable
|
<Editor
|
||||||
placeholder={t`Add a comment...`}
|
content={watch("comment")}
|
||||||
html={watch("comment")}
|
onChange={(value) => setValue("comment", value)}
|
||||||
disabled={false}
|
workspaceMembers={workspaceMembers}
|
||||||
onChange={(e) => setValue("comment", e.target.value)}
|
enableYouTubeEmbed={false}
|
||||||
className="block w-full border-0 bg-transparent py-1.5 text-light-900 focus-visible:outline-none dark:text-dark-1000 sm:text-sm sm:leading-6"
|
placeholder={t`Add comment... (type '/' to open commands or '@' to mention)`}
|
||||||
onKeyDown={async (e) => {
|
disableHeadings={true}
|
||||||
if (e.key === "Enter" && e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
await handleSubmit(onSubmit)();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -200,6 +200,21 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
|
|||||||
const workspaceMembers = board?.workspace.members;
|
const workspaceMembers = board?.workspace.members;
|
||||||
const boardId = board?.publicId;
|
const boardId = board?.publicId;
|
||||||
|
|
||||||
|
const editorWorkspaceMembers =
|
||||||
|
workspaceMembers
|
||||||
|
?.filter((member) => member.email)
|
||||||
|
.map((member) => ({
|
||||||
|
publicId: member.publicId,
|
||||||
|
email: member.email,
|
||||||
|
user: member.user
|
||||||
|
? {
|
||||||
|
id: member.user.id,
|
||||||
|
name: member.user.name ?? null,
|
||||||
|
image: member.user.image ?? null,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
})) ?? [];
|
||||||
|
|
||||||
const updateCard = api.card.update.useMutation({
|
const updateCard = api.card.update.useMutation({
|
||||||
onError: () => {
|
onError: () => {
|
||||||
showPopup({
|
showPopup({
|
||||||
@@ -432,7 +447,10 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
|
|||||||
</div>
|
</div>
|
||||||
{!isTemplate && (
|
{!isTemplate && (
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<NewCommentForm cardPublicId={cardId} />
|
<NewCommentForm
|
||||||
|
cardPublicId={cardId}
|
||||||
|
workspaceMembers={editorWorkspaceMembers}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user