Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion moon/apps/web/components/Issues/IssueDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ export default function IssueDetailPage({ id }: { id: string }) {
<LoadingSpinner />
</div>
) : (
<TimelineItems mrDetail={issueDetail} id={id} />
<TimelineItems detail={issueDetail} id={id} type="issue"/>
)}

{info && info.status === 'open' && (
Expand Down
4 changes: 2 additions & 2 deletions moon/apps/web/components/MrView/CloseItem.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
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'
import HandleTime from './components/HandleTime'


interface CloseItemProps {
conv: Conversation
conv: ConversationItem
}
const CloseItem = ({ conv }: CloseItemProps) => {
const { data: member } = useGetOrganizationMember({ username: conv.username })
Expand Down
78 changes: 78 additions & 0 deletions moon/apps/web/components/MrView/CommentDropdownMenu.tsx
Original file line number Diff line number Diff line change
@@ -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: <CopyIcon />,
},
{
type: 'item',
label: 'Quote',
leftSlot: <QuoteIcon />,
},
{
type: 'item',
label: 'Reference',
leftSlot: <PreferenceIcon />,
},
{ type: 'separator' },
{
type: 'item',
label: 'Edit',
leftSlot: <PencilIcon />,
},
{
type: 'item',
label: 'Hide',
leftSlot: <EyeHideIcon />,
},
{
type: 'item',
label: 'Delete',
leftSlot: <TrashIcon isAnimated />,
destructive: true,
onSelect: () => handleDelete()
}
])



return (
<>
<DropdownMenu
items={items}
align='end'
trigger={<Button variant='plain' iconOnly={<DotsHorizontal />} accessibilityLabel='Comment actions dropdown' />}
Comment thread
Ceron-CSS marked this conversation as resolved.
/>
</>
)
}
36 changes: 27 additions & 9 deletions moon/apps/web/components/MrView/MRComment.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className='overflow-hidden rounded-lg border border-gray-300 bg-white'>
Expand Down Expand Up @@ -57,13 +61,27 @@ const Comment = ({ conv }: CommentProps) => {
<HandleTime created_at={conv.created_at}/>
</div>
</div>
<button className='hover:text-blue-700'>
<KebabHorizontalIcon />
</button>
<div className='flex items-center'>
<ReactionPicker
custom
align='end'
trigger={
<Button
variant='plain'
iconOnly={<FaceSmilePlusIcon />}
accessibilityLabel='Add reaction'
tooltip='Add reaction'
/>
}
onReactionSelect={handleReactionSelect}
/>
<CommentDropdownMenu id={id} Conversation={conv} CommentType={whoamI}/>
</div>
</div>

<div className='prose copyable-text p-3'>
<RichTextRenderer content={conv.comment} extensions={extensions} />
{ conv.comment && <RichTextRenderer content={conv.comment} extensions={extensions} /> }
<ReactionShow comment={conv} id={id} type={whoamI} />
</div>
</div>
)
Expand Down
4 changes: 2 additions & 2 deletions moon/apps/web/components/MrView/MergedItem.tsx
Original file line number Diff line number Diff line change
@@ -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) => {

Expand Down
4 changes: 2 additions & 2 deletions moon/apps/web/components/MrView/ReopenItem.tsx
Original file line number Diff line number Diff line change
@@ -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 })
Expand Down
11 changes: 4 additions & 7 deletions moon/apps/web/components/MrView/TimelineItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -54,20 +54,17 @@ const TimelineWrapper: React.FC<TimelineWrapperProps> = ({ 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 }) => {
Comment thread
Ceron-CSS marked this conversation as resolved.

const convItems: ConvItem[] = mrDetail?.conversations.map((conv) => {
const convItems: ConvItem[] = detail.conversations.map((conv: ConversationItem) => {
let icon;
let children;
let isOver = false;

switch (conv.conv_type) {
case 'Comment':
icon = <CommentIcon />;
children = <MRComment conv={conv} id={id} whoamI="mr" />;
children = <MRComment conv={conv} id={id} whoamI={type} />;
break;
case 'Merged':
icon = <FeedMergedIcon size={24} className="text-purple-500" />;
Expand Down
27 changes: 27 additions & 0 deletions moon/apps/web/components/MrView/hook/useHandleExpression.ts
Original file line number Diff line number Diff line change
@@ -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]
)
}
49 changes: 49 additions & 0 deletions moon/apps/web/components/Reactions/ReactionShow.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className='flex flex-col items-start gap-2 pt-1'>
<div className='-ml-1 flex flex-wrap items-center gap-1'>
<ReactionPicker
custom
trigger={
<Button
className='hover:text-primary text-tertiary px-0.5'
round
size='sm'
variant='plain'
iconOnly={<FaceSmilePlusIcon />}
accessibilityLabel='Add reaction'
/>
}
onReactionSelect={handleReactionSelect}
/>
<Reactions
reactions={comment.grouped_reactions as unknown as GroupedReaction[]}
onReactionSelect={handleReactionSelect}
getClasses={getClasses}
/>
</div>
</div>
)
}
35 changes: 35 additions & 0 deletions moon/apps/web/hooks/conversation/useDeleteConversationReaction.ts
Original file line number Diff line number Diff line change
@@ -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<DeleteApiConversationReactionsByIdData, Error, Props>({
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
}
}
})
}
39 changes: 39 additions & 0 deletions moon/apps/web/hooks/conversation/usePostConversationReactions.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
})
}
2 changes: 1 addition & 1 deletion moon/apps/web/hooks/issues/useGetIssueDetail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const getApiIssueDetail = legacyApiClient.v1.getApiIssueDetail()

export function useGetIssueDetail(id: string) {
return useQuery<detailRes, Error>({
queryKey: ['issueDetail', id],
queryKey: legacyApiClient.v1.getApiIssueDetail().requestKey(id),
queryFn: async () => {
const { err_message, data, req_result } = await getApiIssueDetail.request(id)

Expand Down
Loading