diff --git a/moon/apps/web/components/MarkdownEditor/MentionList.tsx b/moon/apps/web/components/MarkdownEditor/MentionList.tsx index a416d2b11..4a93c6508 100644 --- a/moon/apps/web/components/MarkdownEditor/MentionList.tsx +++ b/moon/apps/web/components/MarkdownEditor/MentionList.tsx @@ -1,7 +1,7 @@ import { ComponentPropsWithoutRef } from 'react' import { uniqBy } from 'remeda' -import { Mention } from '@gitmono/editor/extensions' +import { Mention, LinkIssue } from '@gitmono/editor/extensions' import { OrganizationMember, SyncOrganizationMember } from '@gitmono/types/generated' import { GitCommitIcon, UIText } from '@gitmono/ui' @@ -43,7 +43,7 @@ export function MentionList({ editor, defaultMentions, modal }: Props) { char='$' allow={({ state, range }) => { const $from = state.doc.resolve(range.from) - const type = state.schema.nodes[Mention.name] + const type = state.schema.nodes[LinkIssue.name] const allow = !!$from.parent.type.contentMatch.matchType(type) return allow @@ -122,15 +122,15 @@ function InnerIssueList({ editor }: Pick) { { role: 'app', issue: { - hash: '#1234', - name: 'This is an issue example.' + hash: 'OCN2RX13', + name: 'MR/Issue open close icon 需要替换' } }, { role: 'app', issue: { - hash: '#5678', - name: 'Another issue example.' + hash: 'DVUAVF2J', + name: 'cccc' } } ] @@ -144,12 +144,10 @@ function InnerIssueList({ editor }: Pick) { // downrank guest members scoreModifier={member.role === 'guest' ? 0.3 : 1} onSelect={({ editor, range }) => - editor.commands.insertMention({ + editor.commands.insertIssue({ range, id: member.issue.hash, - label: member.issue.hash, - username: '', - role: member.role === 'app' ? 'app' : 'member' + label: member.issue.hash }) } > diff --git a/moon/apps/web/components/MrView/MRComment.tsx b/moon/apps/web/components/MrView/MRComment.tsx index 5cb5f2e28..4310d9d03 100644 --- a/moon/apps/web/components/MrView/MRComment.tsx +++ b/moon/apps/web/components/MrView/MRComment.tsx @@ -1,4 +1,4 @@ -import { getMarkdownExtensions } from '@gitmono/editor' +import { getNoteExtensions } from '@gitmono/editor' import { RichTextRenderer } from '@/components/RichTextRenderer' import { useMemo } from 'react' import { Button, ConditionalWrap, FaceSmilePlusIcon, UIText } from '@gitmono/ui' @@ -22,7 +22,7 @@ interface CommentProps { const Comment = ({ conv, id, whoamI }: CommentProps) => { const { data: member } = useGetOrganizationMember({ username: conv.username }) - const extensions = useMemo(() => getMarkdownExtensions({ linkUnfurl: {} }), []) + const extensions = useMemo(() => getNoteExtensions({ linkUnfurl: {} }), []) const handleReactionSelect = useHandleExpression({ conv, id, type: whoamI }) return ( diff --git a/moon/apps/web/components/RichTextRenderer/handlers/LinkIssue.tsx b/moon/apps/web/components/RichTextRenderer/handlers/LinkIssue.tsx new file mode 100644 index 000000000..20dd5174e --- /dev/null +++ b/moon/apps/web/components/RichTextRenderer/handlers/LinkIssue.tsx @@ -0,0 +1,21 @@ +import { Link } from '@gitmono/ui/Link' +import { NodeHandler } from '.' +import { useScope } from '@/contexts/scope' + +export const LinkIssue: NodeHandler = ({ node, children }) => { + const { scope } = useScope() + + if (!node.attrs || !node.attrs.id) { + return {children} + } + + const issueId = node.attrs?.id + const label = node.attrs?.label || issueId + const issueUrl = `/${scope}/issue/${issueId}` + + return ( + + ${label} + + ) +} \ No newline at end of file diff --git a/moon/apps/web/components/RichTextRenderer/index.tsx b/moon/apps/web/components/RichTextRenderer/index.tsx index 36f1cfce9..b2aa8e005 100644 --- a/moon/apps/web/components/RichTextRenderer/index.tsx +++ b/moon/apps/web/components/RichTextRenderer/index.tsx @@ -8,6 +8,7 @@ import { Blockquote } from '@/components/RichTextRenderer/handlers/Blockquote' import { CodeBlock } from '@/components/RichTextRenderer/handlers/CodeBlock' import { Hardbreak } from '@/components/RichTextRenderer/handlers/Hardbreak' import { InlineResourceMention } from '@/components/RichTextRenderer/handlers/InlineResourceMention' +import { LinkIssue } from '@/components/RichTextRenderer/handlers/LinkIssue' import { PostNoteAttachment } from '@/components/RichTextRenderer/handlers/PostNoteAttachment' import { RelativeTime } from '@/components/RichTextRenderer/handlers/RelativeTime' @@ -103,6 +104,8 @@ function RenderBlock({ return case 'mention': return + case 'linkIssue': + return case 'postNoteAttachment': return case 'reaction': diff --git a/moon/apps/web/components/SimpleNoteEditor/SimpleNoteContent.tsx b/moon/apps/web/components/SimpleNoteEditor/SimpleNoteContent.tsx index bf3b30460..89d56af1f 100644 --- a/moon/apps/web/components/SimpleNoteEditor/SimpleNoteContent.tsx +++ b/moon/apps/web/components/SimpleNoteEditor/SimpleNoteContent.tsx @@ -49,6 +49,7 @@ export interface SimpleNoteContentRef { clearAndBlur(): void insertReaction: TTEditor['commands']['insertReaction'] uploadAndAppendAttachments: (files: File[]) => Promise + getLinkedIssues(): string[] } export const SimpleNoteContent = memo( @@ -117,6 +118,17 @@ export const SimpleNoteContent = memo( } } }, + getLinkedIssues: () => { + const issues: string[] = [] + + editor.state.doc.descendants((node) => { + if (node.type.name === 'linkIssue' && node.attrs.id) { + issues.push(node.attrs.id) + } + }) + + return Array.from(new Set(issues)) + }, uploadAndAppendAttachments, ...imperativeHandlers, editor diff --git a/moon/apps/web/pages/[org]/mr/[id].tsx b/moon/apps/web/pages/[org]/mr/[id].tsx index 0a9fe747e..54132c883 100644 --- a/moon/apps/web/pages/[org]/mr/[id].tsx +++ b/moon/apps/web/pages/[org]/mr/[id].tsx @@ -82,19 +82,23 @@ const MRDetailPage:PageWithLayout = () =>{ const send_comment = () => { const currentContentHTML = editorRef.current?.editor?.getHTML() ?? '

'; - - if (trimHtml(currentContentHTML) === '') { - toast.error('Please enter the content.') - }else { - postMrComment( - { content: currentContentHTML }, - { - onSuccess: () =>{ - editorRef.current?.clearAndBlur() + const issues = editorRef.current?.getLinkedIssues() || [] + + /* eslint-disable-next-line no-console */ + console.log('commentIssues:',issues); + + if (trimHtml(currentContentHTML) === '') { + toast.error('Please enter the content.') + } else { + postMrComment( + { content: currentContentHTML }, + { + onSuccess: () =>{ + editorRef.current?.clearAndBlur() + } } - } - ); - } + ); + } } const buttonClasses= 'cursor-pointer'; diff --git a/moon/apps/web/styles/prose.css b/moon/apps/web/styles/prose.css index 0d0fd5b2b..0dbff3ec0 100644 --- a/moon/apps/web/styles/prose.css +++ b/moon/apps/web/styles/prose.css @@ -85,6 +85,10 @@ @apply text-primary cursor-pointer font-semibold; } + .link-issue { + @apply text-blue-500 border-b border-blue-500 cursor-pointer; + } + :where(ul, ol):not(:where([class~='not-prose']) *) { /* Erase margins in nested lists */ ul, diff --git a/moon/packages/editor/src/extensions/LinkIssue.ts b/moon/packages/editor/src/extensions/LinkIssue.ts new file mode 100644 index 000000000..293e069bc --- /dev/null +++ b/moon/packages/editor/src/extensions/LinkIssue.ts @@ -0,0 +1,126 @@ +import { mergeAttributes, Node, Range } from '@tiptap/core' + +import { handleRestoreMentionBackspace } from '../utils/handleRestoreMentionBackspace' +import { insertContent } from '../utils/insertContent' + +declare module '@tiptap/core' { + interface Commands { + issue: { + insertIssue: (props: { + id: string + label: string + range: Range + }) => ReturnType + } + } +} + +export interface IssueNodeAttrs { + /** + * The identifier for the selected issue, stored as a `data-id` attribute. + */ + id: string | null + /** + * The label to be rendered by the editor as the displayed text for this issue. + */ + label?: string | null +} + +export type IssueOptions = { + HTMLAttributes: Record +} + +export const LinkIssue = Node.create({ + name: 'linkIssue', + + addOptions() { + return { + HTMLAttributes: { + class: 'link-issue' + } + } + }, + + group: 'inline', + + inline: true, + + selectable: false, + + atom: true, + + addAttributes() { + return { + id: { + default: null, + parseHTML: (element) => element.getAttribute('data-id'), + renderHTML: (attributes) => { + if (!attributes.id) { + return {} + } + + return { + 'data-id': attributes.id + } + } + }, + + label: { + default: null, + parseHTML: (element) => element.getAttribute('data-label'), + renderHTML: (attributes) => { + if (!attributes.label) { + return {} + } + + return { + 'data-label': attributes.label + } + } + } + } + }, + + parseHTML() { + return [ + { + tag: `span[data-type="${this.name}"]` + } + ] + }, + + renderHTML({ node, HTMLAttributes }) { + return [ + 'span', + mergeAttributes(this.options.HTMLAttributes, { 'data-type': this.name }, HTMLAttributes), + `$${node.attrs.label ?? node.attrs.id}` + ] + }, + + renderText({ node }) { + return `$${node.attrs.label ?? node.attrs.id}` + }, + + addCommands() { + return { + insertIssue: + ({ range, ...attrs }) => + ({ chain, state }) => + insertContent({ + chain, + range, + state, + content: { type: this.name, attrs } + }) + } + }, + + addKeyboardShortcuts() { + return { + Backspace: () => + this.editor.commands.command(({ tr, state }) => + handleRestoreMentionBackspace({ transaction: tr, state, nodeName: this.name, char: '$' }) + ) + } + } +}) \ No newline at end of file diff --git a/moon/packages/editor/src/extensions/index.ts b/moon/packages/editor/src/extensions/index.ts index 1f2e63751..4485b404e 100644 --- a/moon/packages/editor/src/extensions/index.ts +++ b/moon/packages/editor/src/extensions/index.ts @@ -20,6 +20,7 @@ export * from './Italic' export * from './Kbd' export * from './Link' export * from './LinkUnfurl' +export * from './LinkIssue' export * from './ListItem' export * from './ListKeyMap' export * from './MediaGallery' diff --git a/moon/packages/editor/src/note.ts b/moon/packages/editor/src/note.ts index e706ab290..c64114174 100644 --- a/moon/packages/editor/src/note.ts +++ b/moon/packages/editor/src/note.ts @@ -118,6 +118,7 @@ export function getNoteExtensions(options?: GetNoteExtensionsOptions) { E.TaskItem.configure(options?.taskItem), E.TaskList, E.Mention.configure(options?.mention), + E.LinkIssue, E.Reaction, E.Details.configure(options?.details),