diff --git a/moon/apps/web/components/Issues/IssueDetailPage.tsx b/moon/apps/web/components/Issues/IssueDetailPage.tsx
index 7b35d6b3e..e77a101d8 100644
--- a/moon/apps/web/components/Issues/IssueDetailPage.tsx
+++ b/moon/apps/web/components/Issues/IssueDetailPage.tsx
@@ -301,7 +301,7 @@ export default function IssueDetailPage({ id }: { id: string }) {
) : (
-
+
)}
{info && info.status === 'open' && (
diff --git a/moon/apps/web/components/MrView/CloseItem.tsx b/moon/apps/web/components/MrView/CloseItem.tsx
index 8fa6f075f..32ee8319a 100644
--- a/moon/apps/web/components/MrView/CloseItem.tsx
+++ b/moon/apps/web/components/MrView/CloseItem.tsx
@@ -1,6 +1,6 @@
import { ConditionalWrap } from '@gitmono/ui'
import { useGetOrganizationMember } from '@/hooks/useGetOrganizationMember'
-import { Conversation } from '@/pages/[org]/mr/[id]'
+import { ConversationItem } from '@gitmono/types/generated';
import { MemberHovercard } from '../InlinePost/MemberHovercard'
import { MemberAvatar } from '../MemberAvatar'
import { UserLinkByName } from './components/UserLinkByName'
@@ -8,7 +8,7 @@ import HandleTime from './components/HandleTime'
interface CloseItemProps {
- conv: Conversation
+ conv: ConversationItem
}
const CloseItem = ({ conv }: CloseItemProps) => {
const { data: member } = useGetOrganizationMember({ username: conv.username })
diff --git a/moon/apps/web/components/MrView/CommentDropdownMenu.tsx b/moon/apps/web/components/MrView/CommentDropdownMenu.tsx
new file mode 100644
index 000000000..35bd5469a
--- /dev/null
+++ b/moon/apps/web/components/MrView/CommentDropdownMenu.tsx
@@ -0,0 +1,78 @@
+import { Button, CopyIcon, DotsHorizontal, PreferenceIcon, PencilIcon, QuoteIcon, TrashIcon, EyeHideIcon } from '@gitmono/ui'
+import { DropdownMenu } from '@gitmono/ui/DropdownMenu'
+import { buildMenuItems } from '@gitmono/ui/Menu'
+import { ConversationItem } from '@gitmono/types/generated';
+import { useDeleteIssueComment } from '@/hooks/issues/useDeleteIssueComment'
+import { useDeleteMrCommentDelete } from '@/hooks/useDeleteMrCommentDelete'
+
+interface CommentDropdownMenuProps {
+ id: string
+ Conversation: ConversationItem
+ CommentType: 'mr' | 'issue' | (string & {})
+}
+
+export function CommentDropdownMenu({ Conversation, id, CommentType }: CommentDropdownMenuProps) {
+ const { mutate: deleteComment } = useDeleteMrCommentDelete(id)
+ const { mutate: deleteIssueComment } = useDeleteIssueComment(id)
+
+ const handleDelete = () => {
+ switch (CommentType) {
+ case 'issue':
+ deleteIssueComment(Conversation.id)
+ break
+ case 'mr':
+ deleteComment(Conversation.id)
+ break
+ default:
+ return
+ }
+ }
+
+ const items = buildMenuItems([
+ {
+ type: 'item',
+ label: 'Copy',
+ leftSlot: ,
+ },
+ {
+ type: 'item',
+ label: 'Quote',
+ leftSlot: ,
+ },
+ {
+ type: 'item',
+ label: 'Reference',
+ leftSlot: ,
+ },
+ { type: 'separator' },
+ {
+ type: 'item',
+ label: 'Edit',
+ leftSlot: ,
+ },
+ {
+ type: 'item',
+ label: 'Hide',
+ leftSlot: ,
+ },
+ {
+ type: 'item',
+ label: 'Delete',
+ leftSlot: ,
+ destructive: true,
+ onSelect: () => handleDelete()
+ }
+ ])
+
+
+
+ return (
+ <>
+ } accessibilityLabel='Comment actions dropdown' />}
+ />
+ >
+ )
+}
diff --git a/moon/apps/web/components/MrView/MRComment.tsx b/moon/apps/web/components/MrView/MRComment.tsx
index 24d2b8dac..5cb5f2e28 100644
--- a/moon/apps/web/components/MrView/MRComment.tsx
+++ b/moon/apps/web/components/MrView/MRComment.tsx
@@ -1,25 +1,29 @@
import { getMarkdownExtensions } from '@gitmono/editor'
-import { Conversation } from '@/pages/[org]/mr/[id]'
import { RichTextRenderer } from '@/components/RichTextRenderer'
import { useMemo } from 'react'
-import { ConditionalWrap, UIText } from '@gitmono/ui'
+import { Button, ConditionalWrap, FaceSmilePlusIcon, UIText } from '@gitmono/ui'
import { MemberHovercard } from '../InlinePost/MemberHovercard'
import { MemberAvatar } from '../MemberAvatar'
import { useGetOrganizationMember } from '@/hooks/useGetOrganizationMember'
-import { KebabHorizontalIcon } from '@primer/octicons-react'
import { UserLinkByName } from './components/UserLinkByName'
import HandleTime from './components/HandleTime'
+import { ReactionPicker } from '../Reactions/ReactionPicker'
+import { useHandleExpression } from './hook/useHandleExpression'
+import { CommentDropdownMenu } from './CommentDropdownMenu'
+import { ConversationItem } from '@gitmono/types/generated'
+import { ReactionShow } from '../Reactions/ReactionShow'
interface CommentProps {
- conv: Conversation
+ conv: ConversationItem
id: string
whoamI: string
}
-const Comment = ({ conv }: CommentProps) => {
+const Comment = ({ conv, id, whoamI }: CommentProps) => {
const { data: member } = useGetOrganizationMember({ username: conv.username })
const extensions = useMemo(() => getMarkdownExtensions({ linkUnfurl: {} }), [])
+ const handleReactionSelect = useHandleExpression({ conv, id, type: whoamI })
return (
@@ -57,13 +61,27 @@ const Comment = ({ conv }: CommentProps) => {
-
+
+ }
+ accessibilityLabel='Add reaction'
+ tooltip='Add reaction'
+ />
+ }
+ onReactionSelect={handleReactionSelect}
+ />
+
+
-
+ { conv.comment && }
+
)
diff --git a/moon/apps/web/components/MrView/MergedItem.tsx b/moon/apps/web/components/MrView/MergedItem.tsx
index 83d5ac79c..724dd9bfc 100644
--- a/moon/apps/web/components/MrView/MergedItem.tsx
+++ b/moon/apps/web/components/MrView/MergedItem.tsx
@@ -1,8 +1,8 @@
-import { Conversation } from '@/pages/[org]/mr/[id]'
+import { ConversationItem } from '@gitmono/types/generated';
import HandleTime from './components/HandleTime'
interface MergedItemProps {
- conv: Conversation
+ conv: ConversationItem
}
const MergedItem = ({ conv }: MergedItemProps) => {
diff --git a/moon/apps/web/components/MrView/ReopenItem.tsx b/moon/apps/web/components/MrView/ReopenItem.tsx
index 804c4c7a2..906c1d8c6 100644
--- a/moon/apps/web/components/MrView/ReopenItem.tsx
+++ b/moon/apps/web/components/MrView/ReopenItem.tsx
@@ -1,14 +1,14 @@
import { ConditionalWrap } from '@gitmono/ui'
import { useGetOrganizationMember } from '@/hooks/useGetOrganizationMember'
-import { Conversation } from '@/pages/[org]/mr/[id]'
import { MemberHovercard } from '../InlinePost/MemberHovercard'
import { MemberAvatar } from '../MemberAvatar'
import { UserLinkByName } from './components/UserLinkByName'
import HandleTime from './components/HandleTime'
+import { ConversationItem } from '@gitmono/types/generated'
interface ReopenItemProps {
- conv: Conversation
+ conv: ConversationItem
}
const ReopenItem = ({ conv }: ReopenItemProps) => {
const { data: member } = useGetOrganizationMember({ username: conv.username })
diff --git a/moon/apps/web/components/MrView/TimelineItems.tsx b/moon/apps/web/components/MrView/TimelineItems.tsx
index 93bc6e43f..2f2c50fe3 100644
--- a/moon/apps/web/components/MrView/TimelineItems.tsx
+++ b/moon/apps/web/components/MrView/TimelineItems.tsx
@@ -3,10 +3,10 @@ import '@primer/primitives/dist/css/functional/themes/light.css'
import { BaseStyles, ThemeProvider, Timeline } from '@primer/react'
import { FeedPullRequestClosedIcon, CommentIcon, FeedMergedIcon, FeedPullRequestOpenIcon } from '@primer/octicons-react'
import MRComment from '@/components/MrView/MRComment';
-import { MRDetail } from '@/pages/[org]/mr/[id]';
import CloseItem from './CloseItem';
import ReopenItem from './ReopenItem';
import MergedItem from './MergedItem';
+import { ConversationItem } from '@gitmono/types/generated'
interface TimelineItemProps {
badge?: React.ReactNode
@@ -54,12 +54,9 @@ const TimelineWrapper: React.FC = ({ convItems = [] }) =>
);
};
-const TimelineItems: React.FC<{ mrDetail?: MRDetail, id: string }> = ({ mrDetail, id }) => {
- if (!mrDetail) {
- return null;
- }
+const TimelineItems: React.FC<{ detail: any, id: string, type: string }> = ({ detail, id, type }) => {
- const convItems: ConvItem[] = mrDetail?.conversations.map((conv) => {
+ const convItems: ConvItem[] = detail.conversations.map((conv: ConversationItem) => {
let icon;
let children;
let isOver = false;
@@ -67,7 +64,7 @@ const TimelineItems: React.FC<{ mrDetail?: MRDetail, id: string }> = ({ mrDetail
switch (conv.conv_type) {
case 'Comment':
icon = ;
- children = ;
+ children = ;
break;
case 'Merged':
icon = ;
diff --git a/moon/apps/web/components/MrView/hook/useHandleExpression.ts b/moon/apps/web/components/MrView/hook/useHandleExpression.ts
new file mode 100644
index 000000000..e5cfd88a9
--- /dev/null
+++ b/moon/apps/web/components/MrView/hook/useHandleExpression.ts
@@ -0,0 +1,27 @@
+import { useCallback } from 'react'
+import { SyncCustomReaction } from '@gitmono/types'
+import { findGroupedReaction, StandardReaction } from '@/utils/reactions'
+import { ConversationItem, GroupedReaction } from '@gitmono/types/generated';
+import { usePostConversationReactions } from '@/hooks/conversation/usePostConversationReactions'
+import { useDeleteConversationReaction } from '@/hooks/conversation/useDeleteConversationReaction'
+
+
+export function useHandleExpression({ conv, id, type }: { conv: ConversationItem, id: string, type: string}) {
+ const { mutate: postConversationReactions } = usePostConversationReactions(conv.id, id, type)
+ const { mutate: deleteConversationReaction } = useDeleteConversationReaction(id, type)
+
+ return useCallback(
+ (reaction: StandardReaction | SyncCustomReaction) => {
+ if (!conv) return
+
+ const groupedReaction = findGroupedReaction(conv.grouped_reactions as unknown as GroupedReaction[], reaction)
+
+ if (groupedReaction?.viewer_reaction_id) {
+ deleteConversationReaction({ reactionId: groupedReaction.viewer_reaction_id })
+ } else {
+ postConversationReactions({ reaction })
+ }
+ },
+ [conv, deleteConversationReaction, postConversationReactions]
+ )
+}
diff --git a/moon/apps/web/components/Reactions/ReactionShow.tsx b/moon/apps/web/components/Reactions/ReactionShow.tsx
new file mode 100644
index 000000000..4c2b4e9b1
--- /dev/null
+++ b/moon/apps/web/components/Reactions/ReactionShow.tsx
@@ -0,0 +1,49 @@
+import { ConversationItem, GroupedReaction } from '@gitmono/types/generated'
+import { Button, cn, FaceSmilePlusIcon } from '@gitmono/ui'
+
+import { Reactions } from '.'
+import { useHandleExpression } from '../MrView/hook/useHandleExpression'
+import { ReactionPicker } from './ReactionPicker'
+
+export function ReactionShow({ comment, id, type }: { comment: ConversationItem, id: string, type: string }) {
+ const handleReactionSelect = useHandleExpression({ conv: comment, id: id, type: type })
+
+ if (!comment.grouped_reactions.length) return null
+
+ function getClasses(hasReacted: boolean) {
+ return cn(
+ 'flex gap-[5px] pointer-events-auto items-center p-0.5 pl-1.5 pr-2 justify-center group h-6.5 rounded-full text-xs font-semibold ring-1 min-w-[32px]',
+ {
+ 'bg-blue-50 dark:bg-blue-900/40 hover:bg-blue-100 dark:hover:bg-blue-900/60 text-blue-900 dark:text-blue-400':
+ hasReacted,
+ 'bg-tertiary hover:bg-quaternary': !hasReacted
+ }
+ )
+ }
+
+ return (
+
+
+ }
+ accessibilityLabel='Add reaction'
+ />
+ }
+ onReactionSelect={handleReactionSelect}
+ />
+
+
+
+ )
+}
diff --git a/moon/apps/web/hooks/conversation/useDeleteConversationReaction.ts b/moon/apps/web/hooks/conversation/useDeleteConversationReaction.ts
new file mode 100644
index 000000000..bde9e26be
--- /dev/null
+++ b/moon/apps/web/hooks/conversation/useDeleteConversationReaction.ts
@@ -0,0 +1,35 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+
+import type { DeleteApiConversationReactionsByIdData, RequestParams } from '@gitmono/types'
+
+import { legacyApiClient } from '@/utils/queryClient'
+
+type Props = {
+ reactionId: string
+ params?: RequestParams
+}
+
+export function useDeleteConversationReaction(id: string, type: string) {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: async ({ reactionId, params }) =>
+ legacyApiClient.v1.deleteApiConversationReactionsById().request(reactionId, { ...params }),
+ onSuccess: () => {
+ switch (type) {
+ case 'issue':
+ queryClient.invalidateQueries({
+ queryKey: legacyApiClient.v1.getApiIssueDetail().requestKey(id)
+ })
+ break
+ case 'mr':
+ queryClient.invalidateQueries({
+ queryKey: legacyApiClient.v1.getApiMrDetail().requestKey(id)
+ })
+ break
+ default:
+ return
+ }
+ }
+ })
+}
diff --git a/moon/apps/web/hooks/conversation/usePostConversationReactions.ts b/moon/apps/web/hooks/conversation/usePostConversationReactions.ts
new file mode 100644
index 000000000..eef1fd772
--- /dev/null
+++ b/moon/apps/web/hooks/conversation/usePostConversationReactions.ts
@@ -0,0 +1,39 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+
+import type { ReactionRequest } from '@gitmono/types'
+import { SyncCustomReaction } from '@gitmono/types'
+
+import { legacyApiClient } from '@/utils/queryClient'
+import { getStandardReaction, StandardReaction } from '@/utils/reactions'
+
+type Props = {
+ reaction: StandardReaction | SyncCustomReaction
+}
+
+export function usePostConversationReactions(commentId: number, id: string, type: string) {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: ({ reaction }: Props) =>
+ legacyApiClient.v1.postApiConversationReactions().request(commentId, {
+ content: getStandardReaction(reaction)?.native,
+ comment_type: type
+ } as ReactionRequest),
+ onSuccess: () => {
+ switch (type) {
+ case 'issue':
+ queryClient.invalidateQueries({
+ queryKey: legacyApiClient.v1.getApiIssueDetail().requestKey(id)
+ })
+ break
+ case 'mr':
+ queryClient.invalidateQueries({
+ queryKey: legacyApiClient.v1.getApiMrDetail().requestKey(id)
+ })
+ break
+ default:
+ return
+ }
+ }
+ })
+}
diff --git a/moon/apps/web/hooks/issues/useGetIssueDetail.ts b/moon/apps/web/hooks/issues/useGetIssueDetail.ts
index 815cfec72..4d79b1d25 100644
--- a/moon/apps/web/hooks/issues/useGetIssueDetail.ts
+++ b/moon/apps/web/hooks/issues/useGetIssueDetail.ts
@@ -35,7 +35,7 @@ const getApiIssueDetail = legacyApiClient.v1.getApiIssueDetail()
export function useGetIssueDetail(id: string) {
return useQuery({
- queryKey: ['issueDetail', id],
+ queryKey: legacyApiClient.v1.getApiIssueDetail().requestKey(id),
queryFn: async () => {
const { err_message, data, req_result } = await getApiIssueDetail.request(id)
diff --git a/moon/apps/web/pages/[org]/mr/[id].tsx b/moon/apps/web/pages/[org]/mr/[id].tsx
index a9ca0dc19..0a9fe747e 100644
--- a/moon/apps/web/pages/[org]/mr/[id].tsx
+++ b/moon/apps/web/pages/[org]/mr/[id].tsx
@@ -25,22 +25,15 @@ import { ComposerReactionPicker } from '@/components/Reactions/ComposerReactionP
import { useUploadHelpers } from '@/hooks/useUploadHelpers';
import { CommentDiscussionIcon, FileDiffIcon, ChecklistIcon } from '@primer/octicons-react'
import TimelineItems from '@/components/MrView/TimelineItems';
+import { ConversationItem } from '@gitmono/types/generated';
const { UnderlinePanels } = require('@primer/react/experimental')
export interface MRDetail {
status: string,
- conversations: Conversation[],
+ conversations: ConversationItem[],
title: string,
}
-export interface Conversation {
- id: number,
- conv_type: string,
- comment: string,
- created_at: number,
- updated_at: number,
- username: string,
-}
const MRDetailPage:PageWithLayout = () =>{
const router = useRouter();
@@ -132,7 +125,7 @@ const MRDetailPage:PageWithLayout = () =>{
) : (
-
+ mrDetail &&
)}
diff --git a/moon/apps/web/styles/global.css b/moon/apps/web/styles/global.css
index 218b08678..3765ed363 100644
--- a/moon/apps/web/styles/global.css
+++ b/moon/apps/web/styles/global.css
@@ -235,6 +235,16 @@ body.grabbing * {
.copyable-text {
@apply select-text;
}
+
+ [aria-haspopup="dialog"]::after,
+ [aria-haspopup="menu"]::after {
+ @apply opacity-0 border-0 ring-0 !important;
+ }
+
+ button:focus-visible,
+ a:focus-visible {
+ @apply border-transparent outline-none;
+ }
}
@layer components {