Skip to content

Commit d3d4ddd

Browse files
authored
refactor(UI): The rich text editing box uses the tiptap component (#1139)
* refactor(UI): The rich text editing box uses the tiptap component * fix(UI):fix class name errors and button disabling
1 parent 9ecaca8 commit d3d4ddd

File tree

4 files changed

+375
-27
lines changed

4 files changed

+375
-27
lines changed

moon/apps/web/components/MrView/MRComment.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@ import { NotePlusIcon } from '@gitmono/ui/Icons'
33
import type { MenuProps } from 'antd'
44
import { Card, Dropdown } from 'antd/lib'
55
import { formatDistance, fromUnixTime } from 'date-fns'
6-
6+
import { getMarkdownExtensions } from '@gitmono/editor'
77
import { useDeleteIssueComment } from '@/hooks/issues/useDeleteIssueComment'
88
import { useDeleteMrCommentDelete } from '@/hooks/useDeleteMrCommentDelete'
99
import { Conversation } from '@/pages/[org]/mr/[id]'
10-
11-
import LexicalContent from './rich-editor/LexicalContent'
12-
10+
import { RichTextRenderer } from '@/components/RichTextRenderer'
11+
import { useMemo } from 'react'
1312
interface CommentProps {
1413
conv: Conversation
1514
id: string
@@ -64,6 +63,7 @@ const Comment = ({ conv, id, whoamI }: CommentProps) => {
6463
}
6564

6665
const time = formatDistance(fromUnixTime(conv.created_at), new Date(), { addSuffix: true })
66+
const extensions = useMemo(() => getMarkdownExtensions({ linkUnfurl: {} }), [])
6767

6868
return (
6969
<Card
@@ -76,7 +76,9 @@ const Comment = ({ conv, id, whoamI }: CommentProps) => {
7676
</Dropdown>
7777
}
7878
>
79-
<LexicalContent lexicalJson={conv.comment} />
79+
<div className='prose'>
80+
<RichTextRenderer content={conv.comment} extensions={extensions} />
81+
</div>
8082
</Card>
8183
)
8284
}
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import {
2+
DragEvent,
3+
forwardRef,
4+
KeyboardEvent,
5+
memo,
6+
MouseEvent,
7+
useImperativeHandle,
8+
useRef,
9+
useState
10+
} from 'react'
11+
import { Editor as TTEditor } from '@tiptap/core'
12+
import { EditorContent } from '@tiptap/react'
13+
14+
import { ActiveEditorComment, BlurAtTopOptions } from '@gitmono/editor'
15+
import { LayeredHotkeys } from '@gitmono/ui'
16+
17+
import { AttachmentLightbox } from '@/components/AttachmentLightbox'
18+
import { MentionList } from '@/components/MarkdownEditor/MentionList'
19+
import { ReactionList } from '@/components/MarkdownEditor/ReactionList'
20+
import { ResourceMentionList } from '@/components/MarkdownEditor/ResourceMentionList'
21+
import { ADD_ATTACHMENT_SHORTCUT, SlashCommand } from '@/components/Post/Notes/SlashCommand'
22+
import { useAutoScroll } from '@/hooks/useAutoScroll'
23+
import { EMPTY_HTML } from '@/atoms/markdown'
24+
import { CodeBlockLanguagePicker } from '@/components/CodeBlockLanguagePicker'
25+
import { EditorBubbleMenu } from '@/components/EditorBubbleMenu'
26+
import { MentionInteractivity } from '@/components/InlinePost/MemberHovercard'
27+
import { DropProps, useEditorFileHandlers } from '@/components/MarkdownEditor/useEditorFileHandlers'
28+
import { HighlightCommentPopover } from '@/components/NoteComments/HighlightCommentPopover'
29+
import { useUploadNoteAttachments } from '@/components/Post/Notes/Attachments/useUploadAttachments'
30+
import { NoteCommentPreview } from '@/components/Post/Notes/CommentRenderer'
31+
import { useSimpleNoteEditor } from '@/components/SimpleNoteEditor/useSimpleNoteEditor'
32+
33+
interface Props {
34+
commentId: string
35+
editable?: 'all' | 'viewer'
36+
autofocus?: boolean
37+
content: string
38+
onBlurAtTop?: BlurAtTopOptions['onBlur']
39+
onKeyDown?: (event: KeyboardEvent) => void
40+
}
41+
42+
export interface SimpleNoteContentRef {
43+
focus(pos: 'start' | 'end' | 'restore' | 'start-newline' | MouseEvent): void
44+
handleDrop(props: DropProps): void
45+
handleDragOver(isOver: boolean, event: DragEvent): void
46+
editor: TTEditor | null
47+
clearAndBlur(): void
48+
}
49+
50+
export const SimpleNoteContent = memo(
51+
forwardRef<SimpleNoteContentRef, Props>((props, ref) => {
52+
const { commentId, editable = 'viewer', autofocus = false, onBlurAtTop, content } = props
53+
54+
const [activeComment, setActiveComment] = useState<ActiveEditorComment | null>(null)
55+
const [hoverComment, setHoverComment] = useState<ActiveEditorComment | null>(null)
56+
const [openAttachmentId, setOpenAttachmentId] = useState<string | undefined>()
57+
58+
const canUploadAttachments = editable === 'all'
59+
const upload = useUploadNoteAttachments({ noteId: commentId, enabled: canUploadAttachments })
60+
61+
const editor = useSimpleNoteEditor({
62+
content,
63+
autofocus,
64+
editable: editable,
65+
onHoverComment: setHoverComment,
66+
onActiveComment: setActiveComment,
67+
onOpenAttachment: setOpenAttachmentId,
68+
onBlurAtTop
69+
})
70+
71+
const { onDrop, onPaste, imperativeHandlers } = useEditorFileHandlers({
72+
enabled: canUploadAttachments,
73+
upload,
74+
editor
75+
})
76+
77+
// these functions allow us to call editorRef?.current?.handleDrop() etc. on the parent container
78+
useImperativeHandle(
79+
ref,
80+
() => ({
81+
clearAndBlur: () => editor.chain().setContent(EMPTY_HTML).blur().run(),
82+
focus: (pos) => {
83+
if (pos === 'start') {
84+
editor.commands.focus('start')
85+
} else if (pos === 'start-newline') {
86+
editor.commands.focus('start')
87+
editor.commands.insertContent('\n')
88+
} else if (pos === 'end') {
89+
editor.commands.focus('end')
90+
} else if (pos === 'restore') {
91+
editor.commands.focus()
92+
} else if ('clientX' in pos && 'clientY' in pos && 'target' in pos) {
93+
if (editor.view.dom.contains(pos.target as Node)) {
94+
return
95+
}
96+
97+
const { left, right, top } = editor.view.dom.getBoundingClientRect()
98+
const isRight = pos.clientX > right
99+
const editorPos = editor.view.posAtCoords({
100+
left: isRight ? right : left,
101+
top: pos.clientY
102+
})
103+
104+
if (editorPos) {
105+
const posAdjustment = isRight && editor.view.coordsAtPos(editorPos.pos).left === left ? -1 : 0
106+
107+
editor.commands.focus(editorPos.pos + posAdjustment)
108+
} else if (pos.clientY < top) {
109+
editor.commands.focus('start')
110+
} else {
111+
editor.commands.focus('end')
112+
}
113+
}
114+
},
115+
...imperativeHandlers,
116+
editor
117+
}),
118+
[editor, imperativeHandlers]
119+
)
120+
121+
const containerRef = useRef<HTMLDivElement>(null)
122+
123+
useAutoScroll({
124+
ref: containerRef,
125+
enabled: true
126+
})
127+
128+
return (
129+
<div ref={containerRef} className="relative min-h-[160px]">
130+
<LayeredHotkeys
131+
keys={ADD_ATTACHMENT_SHORTCUT}
132+
callback={() => {
133+
if (!editor.isFocused) return
134+
135+
const input = document.createElement('input')
136+
137+
input.type = 'file'
138+
input.onchange = async () => {
139+
if (input.files?.length) {
140+
upload({
141+
files: Array.from(input.files),
142+
editor
143+
})
144+
}
145+
}
146+
input.click()
147+
}}
148+
options={{ enableOnContentEditable: true, enableOnFormTags: true }}
149+
/>
150+
151+
<NoteCommentPreview
152+
onExpand={() => {
153+
if (hoverComment) {
154+
setHoverComment(null)
155+
setActiveComment(hoverComment)
156+
}
157+
}}
158+
previewComment={activeComment ? null : hoverComment}
159+
editor={editor}
160+
noteId={commentId}
161+
/>
162+
<MentionInteractivity container={containerRef} />
163+
<CodeBlockLanguagePicker editor={editor} />
164+
<SlashCommand editor={editor} upload={upload} />
165+
<MentionList editor={editor} />
166+
<ResourceMentionList editor={editor} />
167+
<ReactionList editor={editor} />
168+
169+
<AttachmentLightbox
170+
selectedAttachmentId={openAttachmentId}
171+
onClose={() => setOpenAttachmentId(undefined)}
172+
onSelectAttachment={({ id }) => setOpenAttachmentId(id)}
173+
/>
174+
175+
<HighlightCommentPopover
176+
activeComment={activeComment}
177+
editor={editor}
178+
noteId={commentId}
179+
onCommentDeactivated={() => setActiveComment(null)}
180+
/>
181+
182+
<EditorBubbleMenu editor={editor} canComment />
183+
184+
<EditorContent editor={editor} onKeyDown={props.onKeyDown} onPaste={onPaste} onDrop={onDrop} />
185+
</div>
186+
)
187+
})
188+
)
189+
190+
SimpleNoteContent.displayName = 'SimpleNoteContent'
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { useEffect, useMemo } from 'react'
2+
import { EditorOptions, ReactNodeViewRenderer, useEditor } from '@tiptap/react'
3+
4+
import { ActiveEditorComment, BlurAtTopOptions, getNoteExtensions, PostNoteAttachmentOptions } from '@gitmono/editor'
5+
6+
import { InlineResourceMentionRenderer } from '@/components/InlineResourceMentionRenderer'
7+
import { useControlClickLink } from '@/components/MarkdownEditor/ControlClickLink'
8+
import { MediaGalleryRenderer } from '@/components/Post/MediaGalleryRenderer'
9+
import { InlineRelativeTimeRenderer } from '@/components/RichTextRenderer/handlers/RelativeTime'
10+
import { useCurrentUserOrOrganizationHasFeature } from '@/hooks/useCurrentUserOrOrganizationHasFeature'
11+
import { notEmpty } from '@/utils/notEmpty'
12+
13+
import { LinkUnfurlRenderer } from '@/components/Post/LinkUnfurlRenderer'
14+
import { NoteAttachmentRenderer } from '@/components/Post/Notes/Attachments/NoteAttachmentRenderer'
15+
import { DragAndDrop } from '@/components/Post/Notes/DragAndDrop'
16+
17+
interface SimpleNoteEditorOptions {
18+
content: string
19+
onOpenAttachment?: PostNoteAttachmentOptions['onOpenAttachment']
20+
autofocus?: boolean
21+
editable?: 'all' | 'viewer' | 'none'
22+
editorProps?: EditorOptions['editorProps']
23+
onHoverComment?(comment: ActiveEditorComment | null): void
24+
onActiveComment?(comment: ActiveEditorComment | null): void
25+
onBlurAtTop?: BlurAtTopOptions['onBlur']
26+
}
27+
28+
export function useSimpleNoteEditor({
29+
content,
30+
autofocus,
31+
editable,
32+
editorProps,
33+
onHoverComment,
34+
onActiveComment,
35+
onOpenAttachment,
36+
onBlurAtTop,
37+
}: SimpleNoteEditorOptions) {
38+
const linkOptions = useControlClickLink()
39+
const hasRelativeTime = useCurrentUserOrOrganizationHasFeature('relative_time')
40+
41+
const extensions = useMemo(() => {
42+
return [
43+
...getNoteExtensions({
44+
history: {
45+
enabled: true
46+
},
47+
dropcursor: {
48+
class: 'text-blue-500',
49+
width: 2
50+
},
51+
link: linkOptions,
52+
linkUnfurl: {
53+
addNodeView() {
54+
return ReactNodeViewRenderer(LinkUnfurlRenderer)
55+
}
56+
},
57+
taskItem: {
58+
canEdit() {
59+
return editable !== 'none'
60+
},
61+
onReadOnlyChecked() {
62+
return editable === 'viewer'
63+
}
64+
},
65+
postNoteAttachment: {
66+
onOpenAttachment,
67+
addNodeView() {
68+
return ReactNodeViewRenderer(NoteAttachmentRenderer)
69+
}
70+
},
71+
mediaGallery: {
72+
onOpenAttachment,
73+
addNodeView() {
74+
return ReactNodeViewRenderer(MediaGalleryRenderer)
75+
}
76+
},
77+
resourceMention: {
78+
addNodeView() {
79+
return ReactNodeViewRenderer(InlineResourceMentionRenderer, { contentDOMElementTag: 'span' })
80+
}
81+
},
82+
comment: {
83+
enabled: true,
84+
onCommentHovered: onHoverComment,
85+
onCommentActivated: onActiveComment
86+
},
87+
codeBlockHighlighted: {
88+
highlight: true
89+
},
90+
blurAtTop: {
91+
enabled: !!onBlurAtTop,
92+
onBlur: onBlurAtTop
93+
},
94+
relativeTime: {
95+
disabled: !hasRelativeTime,
96+
addNodeView() {
97+
return ReactNodeViewRenderer(InlineRelativeTimeRenderer, { contentDOMElementTag: 'span' })
98+
}
99+
}
100+
}),
101+
DragAndDrop
102+
].filter(notEmpty)
103+
}, [editable, linkOptions, onActiveComment, onBlurAtTop, onHoverComment, onOpenAttachment, hasRelativeTime])
104+
105+
const allEditable = editable === 'all'
106+
107+
const editor = useEditor(
108+
{
109+
immediatelyRender: true,
110+
shouldRerenderOnTransaction: false,
111+
editorProps: {
112+
attributes: {
113+
class:
114+
'new-posts prose select-text focus:outline-none w-full relative note min-w-full px-4',
115+
style: "overflow-anchor: ''"
116+
},
117+
...editorProps
118+
},
119+
extensions,
120+
autofocus: !!autofocus,
121+
content,
122+
editable: allEditable
123+
},
124+
[extensions]
125+
)
126+
127+
useEffect(() => {
128+
if (editor.isEditable !== allEditable) {
129+
editor.setEditable(allEditable)
130+
}
131+
}, [editor, allEditable])
132+
133+
return editor
134+
}

0 commit comments

Comments
 (0)