diff --git a/moon/apps/web/components/MarkdownEditor/MentionList.tsx b/moon/apps/web/components/MarkdownEditor/MentionList.tsx index 4a93c6508..fb1acf3c6 100644 --- a/moon/apps/web/components/MarkdownEditor/MentionList.tsx +++ b/moon/apps/web/components/MarkdownEditor/MentionList.tsx @@ -1,16 +1,18 @@ import { ComponentPropsWithoutRef } from 'react' import { uniqBy } from 'remeda' - -import { Mention, LinkIssue } from '@gitmono/editor/extensions' +import { ChatBubblePlusIcon } from '@gitmono/ui/Icons' +import { LinkIssue, Mention } from '@gitmono/editor/extensions' import { OrganizationMember, SyncOrganizationMember } from '@gitmono/types/generated' import { GitCommitIcon, UIText } from '@gitmono/ui' import { AppBadge } from '@/components/AppBadge' import { GuestBadge } from '@/components/GuestBadge' import { MemberAvatar } from '@/components/MemberAvatar' -import { SuggestionItem, SuggestionRoot, useSuggestionEmpty } from '@/components/SuggestionList' +import { SuggestionItem, SuggestionRoot, useSuggestionEmpty, useSuggestionQuery } from '@/components/SuggestionList' +import { useGetIssueIssueSuggester } from '@/hooks/issues/useGetIssueIssueSuggester' import { useGetOauthApplications } from '@/hooks/useGetOauthApplications' import { useSyncedMembers } from '@/hooks/useSyncedMembers' +import { useDebounce } from 'use-debounce' type Props = Pick, 'editor'> & { defaultMentions?: (OrganizationMember | SyncOrganizationMember)[] @@ -18,6 +20,8 @@ type Props = Pick, 'editor'> & { } export function MentionList({ editor, defaultMentions, modal }: Props) { + useGetIssueIssueSuggester({ query: '' }); + return ( <> @@ -118,44 +123,63 @@ function InnerMentionList({ editor, defaultMentions }: Pick) { - const items = [ - { - role: 'app', - issue: { - hash: 'OCN2RX13', - name: 'MR/Issue open close icon 需要替换' - } - }, - { - role: 'app', - issue: { - hash: 'DVUAVF2J', - name: 'cccc' - } - } - ] + const query = useSuggestionQuery(); + const [debouncedQuery] = useDebounce(query, 400); - return items.map((member) => ( + const { data: issueSuggestions } = useGetIssueIssueSuggester( + { query: debouncedQuery } + ); + + if (!issueSuggestions?.data || issueSuggestions.data.length === 0) { + return ( +
+ No results +
+ ); + } + + return issueSuggestions.data.map((item) => { + + let suggestionType = '' + + switch (item.type) { + case 'issue_closed': + case 'issue_open': + suggestionType = 'issue' + break + case 'merge_request_closed': + case 'merge_request': + suggestionType = 'merge_request' + break + default: + break + } + + return ( editor.commands.insertIssue({ range, - id: member.issue.hash, - label: member.issue.hash + id: item.link, + label: item.link, + suggestionType: suggestionType }) } > - + + {suggestionType === 'merge_request' + ? + : } +
- {member.issue.name} - {member.issue.hash} + {item.title} + {item.link}
- )) -} + ) + }) +} \ No newline at end of file diff --git a/moon/apps/web/components/Post/Notes/SlashCommand.tsx b/moon/apps/web/components/Post/Notes/SlashCommand.tsx index 295854ce3..c41b94d3a 100644 --- a/moon/apps/web/components/Post/Notes/SlashCommand.tsx +++ b/moon/apps/web/components/Post/Notes/SlashCommand.tsx @@ -12,6 +12,7 @@ import { Heading3Icon, HorizontalRuleIcon, KeyboardShortcut, + LinkIcon, OrderedListIcon, PlayIcon, PostIcon, @@ -170,6 +171,13 @@ const COMMANDS: CommandItemProps[] = [ command: ({ editor, range }: CommandProps) => editor.chain().focus().deleteRange(range).insertContent('@').run(), kbd: '@' }, + { + title: 'Link Issues/PRs', + searchTerms: ['issue', 'link', 'reference'], + icon: , + command: ({ editor, range }: CommandProps) => editor.chain().focus().deleteRange(range).insertContent('$').run(), + kbd: '$' + }, { title: 'Reference', searchTerms: ['link', 'reference', 'post', 'call', 'doc'], diff --git a/moon/apps/web/components/RichTextRenderer/handlers/LinkIssue.tsx b/moon/apps/web/components/RichTextRenderer/handlers/LinkIssue.tsx index 20dd5174e..7c0d5cf39 100644 --- a/moon/apps/web/components/RichTextRenderer/handlers/LinkIssue.tsx +++ b/moon/apps/web/components/RichTextRenderer/handlers/LinkIssue.tsx @@ -9,12 +9,23 @@ export const LinkIssue: NodeHandler = ({ node, children }) => { return {children} } - const issueId = node.attrs?.id - const label = node.attrs?.label || issueId - const issueUrl = `/${scope}/issue/${issueId}` + const id = node.attrs?.id + const label = node.attrs?.label || id + let url = '/404' + + switch (node.attrs?.suggestionType) { + case 'issue': + url = `/${scope}/issue/${id}` + break + case 'merge_request': + url = `/${scope}/mr/${id}` + break + default: + break + } return ( - + ${label} ) diff --git a/moon/apps/web/components/SuggestionList.tsx b/moon/apps/web/components/SuggestionList.tsx index 4b0a6e3a3..05f7e9bb4 100644 --- a/moon/apps/web/components/SuggestionList.tsx +++ b/moon/apps/web/components/SuggestionList.tsx @@ -49,6 +49,8 @@ type SuggestionProps = React.PropsWithChildren & char: string // allow controlled query changes. providing this value will disable cmdk filtering. onControlledQueryChange?: (query: string) => void + // disable the default "No results" empty state + disableEmptyState?: boolean contentClassName?: string listClassName?: string } @@ -76,6 +78,7 @@ export function SuggestionRoot(props: SuggestionProps) { modal = true, filter, onControlledQueryChange, + disableEmptyState, minScore, ...etc } = props @@ -310,17 +313,19 @@ export function SuggestionRoot(props: SuggestionProps) { {children} - { - close() - // clicking will steal focus, so return it - editor.commands.focus() - }} - > - No results - Dismiss - + {!disableEmptyState && ( + { + close() + // clicking will steal focus, so return it + editor.commands.focus() + }} + > + No results + Dismiss + + )} diff --git a/moon/apps/web/hooks/issues/useGetIssueIssueSuggester.ts b/moon/apps/web/hooks/issues/useGetIssueIssueSuggester.ts new file mode 100644 index 000000000..eaee74563 --- /dev/null +++ b/moon/apps/web/hooks/issues/useGetIssueIssueSuggester.ts @@ -0,0 +1,13 @@ +import { keepPreviousData, useQuery } from '@tanstack/react-query' + +import { GetApiIssueIssueSuggesterParams } from '@gitmono/types' + +import { legacyApiClient } from '@/utils/queryClient' + +export function useGetIssueIssueSuggester(query: GetApiIssueIssueSuggesterParams) { + return useQuery({ + queryKey: legacyApiClient.v1.getApiIssueIssueSuggester().requestKey(query), + queryFn: () => legacyApiClient.v1.getApiIssueIssueSuggester().request(query), + placeholderData: keepPreviousData + }) +} diff --git a/moon/packages/editor/src/extensions/LinkIssue.ts b/moon/packages/editor/src/extensions/LinkIssue.ts index 293e069bc..fec25d894 100644 --- a/moon/packages/editor/src/extensions/LinkIssue.ts +++ b/moon/packages/editor/src/extensions/LinkIssue.ts @@ -9,6 +9,7 @@ declare module '@tiptap/core' { insertIssue: (props: { id: string label: string + suggestionType: string range: Range }) => ReturnType } @@ -77,6 +78,20 @@ export const LinkIssue = Node.create({ 'data-label': attributes.label } } + }, + + suggestionType: { + default: null, + parseHTML: (element) => element.getAttribute('data-suggestionType'), + renderHTML: (attributes) => { + if (!attributes.suggestionType) { + return {} + } + + return { + 'data-suggestionType': attributes.suggestionType + } + } } } },