diff --git a/moon/apps/web/components/Issues/IssueDetailPage.tsx b/moon/apps/web/components/Issues/IssueDetailPage.tsx
index 3c1e7fe4d..7b35d6b3e 100644
--- a/moon/apps/web/components/Issues/IssueDetailPage.tsx
+++ b/moon/apps/web/components/Issues/IssueDetailPage.tsx
@@ -1,25 +1,36 @@
'use client'
-import React, { useCallback, useEffect, useState, useRef } from 'react'
-import { Card, Flex, Space, Tabs, TabsProps, Timeline } from 'antd'
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import { IssueClosedIcon, IssueOpenedIcon, IssueReopenedIcon } from '@primer/octicons-react'
+import { Stack } from '@primer/react'
+import { ItemInput } from '@primer/react/lib/deprecated/ActionList'
import { useRouter } from 'next/router'
import toast from 'react-hot-toast'
-import { CanvasCommentIcon, ResolveCommentIcon, PicturePlusIcon, Button } from '@gitmono/ui'
+import { Button, LoadingSpinner, PicturePlusIcon } from '@gitmono/ui'
-import Comment from '@/components/MrView/MRComment'
+import { EMPTY_HTML } from '@/atoms/markdown'
+import { useHandleBottomScrollOffset } from '@/components/NoteEditor/useHandleBottomScrollOffset'
+import { ComposerReactionPicker } from '@/components/Reactions/ComposerReactionPicker'
+import { SimpleNoteContent, SimpleNoteContentRef } from '@/components/SimpleNoteEditor/SimpleNoteContent'
import { useGetIssueDetail } from '@/hooks/issues/useGetIssueDetail'
import { usePostIssueClose } from '@/hooks/issues/usePostIssueClose'
import { usePostIssueComment } from '@/hooks/issues/usePostIssueComment'
import { usePostIssueReopen } from '@/hooks/issues/usePostIssueReopen'
+import { useGetCurrentUser } from '@/hooks/useGetCurrentUser'
+import { useGetOrganizationMember } from '@/hooks/useGetOrganizationMember'
+import { useSyncedMembers } from '@/hooks/useSyncedMembers'
+import { useUploadHelpers } from '@/hooks/useUploadHelpers'
import { apiErrorToast } from '@/utils/apiErrorToast'
-import { SimpleNoteContent, SimpleNoteContentRef } from '@/components/SimpleNoteEditor/SimpleNoteContent';
-import { EMPTY_HTML } from '@/atoms/markdown'
-import { useHandleBottomScrollOffset } from '@/components/NoteEditor/useHandleBottomScrollOffset'
-import { useUploadHelpers } from '@/hooks/useUploadHelpers';
-import { ComposerReactionPicker } from '@/components/Reactions/ComposerReactionPicker';
import { trimHtml } from '@/utils/trimHtml'
+import { MemberAvatar } from '../MemberAvatar'
+import TimelineItems from '../MrView/TimelineItems'
+import { BadgeItem } from './IssueNewPage'
+import { tags } from './utils/consts'
+import { extractTextArray } from './utils/extractText'
+import { pickWithReflect } from './utils/pickWithReflectDeep'
+
interface IssueDetail {
status: string
conversations: Conversation[]
@@ -29,9 +40,9 @@ interface Conversation {
id: number
conv_type: string
comment: string
- created_at: number,
- updated_at: number,
- username: string,
+ created_at: number
+ updated_at: number
+ username: string
}
interface detailRes {
@@ -57,13 +68,17 @@ export default function IssueDetailPage({ id }: { id: string }) {
setButtonLoading((prev) => ({ ...prev, [key]: value }))
}
+ const [closeHint, setCloseHint] = useState('Close issue')
+
const { mutate: closeIssue } = usePostIssueClose()
const { mutate: reopenIssue } = usePostIssueReopen()
const { mutate: saveComment } = usePostIssueComment()
- const { data: issueDetailObj, error, isError, refetch } = useGetIssueDetail(id)
+ const { data: issueDetailObj, error, isError, refetch, isLoading: detailIsLoading } = useGetIssueDetail(id)
+
+ const issueDetail = issueDetailObj?.data as IssueDetail | undefined
const applyDetailData = (detail: detailRes | undefined) => {
if (!detail || !detail.req_result) return
@@ -105,7 +120,37 @@ export default function IssueDetailPage({ id }: { id: string }) {
})
}
+ const save_comment = useCallback(() => {
+ const currentContentHTML = editorRef.current?.editor?.getHTML() ?? '
'
+
+ if (trimHtml(currentContentHTML) === '') {
+ toast.error('comment can not be empty!')
+ return
+ }
+ setLoading('comment', true)
+ set_to_loading(3)
+ saveComment(
+ { link: id, data: { content: currentContentHTML } },
+ {
+ onSuccess: async () => {
+ editorRef.current?.clearAndBlur()
+ const { data: issueDetailObj } = await refetch({ throwOnError: true })
+
+ applyDetailData(issueDetailObj)
+ cancel_loading(3)
+ toast.success('comment successfully!')
+ },
+ onError: apiErrorToast,
+ onSettled: () => setLoading('comment', false)
+ }
+ )
+ }, [id, refetch, saveComment])
+
const close_issue = useCallback(() => {
+ if (closeHint === 'Close with comment') {
+ save_comment()
+ }
+
setLoading('close', true)
set_to_loading(3)
closeIssue(
@@ -119,7 +164,7 @@ export default function IssueDetailPage({ id }: { id: string }) {
onSettled: () => setLoading('close', false)
}
)
- }, [id, router, closeIssue])
+ }, [id, router, closeIssue, closeHint, save_comment])
const reopen_issue = useCallback(() => {
setLoading('reopen', true)
@@ -136,58 +181,7 @@ export default function IssueDetailPage({ id }: { id: string }) {
)
}, [id, router, reopenIssue])
- const save_comment = useCallback(
- () => {
- const currentContentHTML = editorRef.current?.editor?.getHTML() ?? '';
-
- if (trimHtml(currentContentHTML) === '') {
- toast.error('comment can not be empty!')
- return
- }
- setLoading('comment', true)
- set_to_loading(3)
- saveComment(
- { link: id, data: { content: currentContentHTML } },
- {
- onSuccess: async () => {
- editorRef.current?.clearAndBlur()
- const { data: issueDetailObj } = await refetch({ throwOnError: true })
-
- applyDetailData(issueDetailObj)
- cancel_loading(3)
- toast.success('comment successfully!')
- },
- onError: apiErrorToast,
- onSettled: () => setLoading('comment', false)
- }
- )
- },
- [id, refetch, saveComment]
- )
-
- const conv_items = info?.conversations.map((conv) => {
- let icon
- let children
-
- switch (conv.conv_type) {
- case 'Comment':
- icon =
- children =
- break
- case 'Closed':
- icon =
- children = conv.comment
- }
-
- const element = {
- dot: icon,
- children: children
- }
-
- return element
- })
-
- const editorRef = useRef(null);
+ const editorRef = useRef(null)
const onKeyDownScrollHandler = useHandleBottomScrollOffset({
editor: editorRef.current?.editor
})
@@ -195,75 +189,286 @@ export default function IssueDetailPage({ id }: { id: string }) {
upload: editorRef.current?.uploadAndAppendAttachments
})
- const tab_items: TabsProps['items'] = [
- {
- key: '1',
- label: 'Conversation',
- children: (
-
-
- {info && info.status === 'open' && (
- <>
-
-
Add a comment
-
-
-
- }
- accessibilityLabel='Add files'
- onClick={dropzone.open}
- tooltip='Add files'
- />
-
-
-
-
-
-
-
- >
- )}
- {info && info.status === 'closed' && (
-
-
-
- )}
-
- )
+ const { data } = useGetCurrentUser()
+ const { data: user } = useGetOrganizationMember({ username: data?.username })
+
+ const avatarUser = useMemo(() => {
+ if (!user) return undefined
+ return {
+ deactivated: user['deactivated'],
+ user: pickWithReflect(user?.user ?? {}, [
+ 'id',
+ 'display_name',
+ 'username',
+ 'avatar_urls',
+ 'notifications_paused',
+ 'integration'
+ ])
}
- ]
+ }, [user])
+
+ const splitFun = (el: React.ReactNode): string[] => {
+ return extractTextArray(el)
+ .flatMap((name) => name.split(',').map((n) => n.trim()))
+ .filter((n) => n.length > 0)
+ }
+
+ const { members } = useSyncedMembers()
+
+ const avatars: ItemInput[] = useMemo(
+ () =>
+ members?.map((i) => ({
+ groupId: 'end',
+ text: i.user.display_name,
+ leadingVisual: () =>
+ })) || [],
+ [members]
+ )
+ // const [avatarTems, setAvatarItems] = useState(avatars)
+
+ const labels: ItemInput[] = useMemo(
+ () =>
+ tags.map((i) => ({
+ text: i.description,
+ leadingVisual: () => (
+
+ )
+ })),
+ []
+ )
+
+ const memberMap = useMemo(() => {
+ const map = new Map()
+
+ members?.forEach((i) => {
+ map.set(i.user.display_name, i)
+ })
+ return map
+ }, [members])
+
+ const labelMap = useMemo(() => {
+ const map = new Map()
+
+ tags.map((i) => {
+ map.set(i.description, i)
+ })
+ return map
+ }, [])
+
+ const handleChange = (html: string) => {
+ if (html && html === '') {
+ setCloseHint('Close issue')
+ } else {
+ setCloseHint('Close with comment')
+ }
+ }
return (
-
-
-
+ <>
+
+ {info.title && (
+
+
{info.title}
+ {info.status === 'open' ? (
+ <>
+
+
+ Open
+
+ >
+ ) : (
+ <>
+
+
+ Closed
+
+ >
+ )}
+
+ )}
+
+ {avatarUser &&
}
+
+
+
+
+ {detailIsLoading ? (
+
+
+
+ ) : (
+
+ )}
+
+ {info && info.status === 'open' && (
+ <>
+
+
Add a comment
+
+
+ handleChange(html)}
+ />
+ }
+ accessibilityLabel='Add files'
+ onClick={dropzone.open}
+ tooltip='Add files'
+ />
+
+
+
+
+
+
+
+ >
+ )}
+
+ {info && info.status === 'closed' && (
+ <>
+
+
Add a comment
+
+
+
+ }
+ accessibilityLabel='Add files'
+ onClick={dropzone.open}
+ tooltip='Add files'
+ />
+
+
+
+
+
+
+
+ >
+ )}
+
+ {/*
*/}
+
+
+ {(el) => {
+ const names = Array.from(new Set(splitFun(el)))
+
+ return (
+ <>
+ {names.map((i, index) => (
+ // eslint-disable-next-line react/no-array-index-key
+
+
+ {i}
+
+ ))}
+ >
+ )
+ }}
+
+
+ {(el) => {
+ const names = splitFun(el)
+
+ return (
+ <>
+
+ {names.map((i, index) => {
+ const label = labelMap.get(i) ?? {}
+
+ return (
+ // eslint-disable-next-line react/no-array-index-key
+
+ )
+ })}
+
+ >
+ )
+ }}
+
+
+
+
+
+
+
+
+
+ >
)
}
diff --git a/moon/apps/web/components/Issues/IssueList.tsx b/moon/apps/web/components/Issues/IssueList.tsx
index ab1339eef..1876f4ee6 100644
--- a/moon/apps/web/components/Issues/IssueList.tsx
+++ b/moon/apps/web/components/Issues/IssueList.tsx
@@ -192,6 +192,40 @@ export const DropdownOrder = ({
>
)
}
+export const DropdownReview = ({
+ name,
+ dropdownArr,
+ dropdownItem,
+ onOpen,
+ open
+}: {
+ name: string
+ dropdownArr: MenuItem[]
+ dropdownItem?: MenuItem[]
+ onOpen?: (open: boolean) => void
+ open?: boolean
+ inside?: React.ReactNode
+}) => {
+ return (
+ <>
+
+
+ {name}
+
+
+ }
+ />
+ >
+ )
+}
// dropdownArr是不一样的,其他一样
export const Dropdown = ({
diff --git a/moon/apps/web/components/Issues/IssueNewPage.tsx b/moon/apps/web/components/Issues/IssueNewPage.tsx
index 970ebda68..fb66475e3 100644
--- a/moon/apps/web/components/Issues/IssueNewPage.tsx
+++ b/moon/apps/web/components/Issues/IssueNewPage.tsx
@@ -310,7 +310,7 @@ type SelectPanelExcludedProps =
| 'renderAnchor'
| 'variant'
-const BadgeItem = ({
+export const BadgeItem = ({
title,
selectPannelProps,
items,
@@ -376,7 +376,7 @@ const BadgeItem = ({
)
}
-const SideBarItem = ({ emptyState }: { emptyState: string }) => {
+export const SideBarItem = ({ emptyState }: { emptyState: string }) => {
return (
<>
diff --git a/moon/apps/web/components/Issues/IssuesContent.tsx b/moon/apps/web/components/Issues/IssuesContent.tsx
index 2f573c550..51ad6d0d7 100644
--- a/moon/apps/web/components/Issues/IssuesContent.tsx
+++ b/moon/apps/web/components/Issues/IssuesContent.tsx
@@ -430,7 +430,7 @@ export const RightAvatar = ({ member, commentNum }: { member?: Member; commentNu
- {commentNum}
+ {commentNum !== 0 && {commentNum}}
{member && (
diff --git a/moon/apps/web/components/Issues/utils/consts.ts b/moon/apps/web/components/Issues/utils/consts.ts
index c73f282ac..8a696131c 100644
--- a/moon/apps/web/components/Issues/utils/consts.ts
+++ b/moon/apps/web/components/Issues/utils/consts.ts
@@ -39,6 +39,8 @@ export const tags: LabelItem[] = [
export const orderTags = ['Created on', 'Last updated', 'Total comments', 'Best match', 'Oldest', 'Newest']
+export const reviewTags = ['No reviews', 'Review required', 'Approved review', 'Changes requested']
+
export const searchList: SearchType = [
{
type: 'item',
diff --git a/moon/apps/web/components/MrView/index.tsx b/moon/apps/web/components/MrView/index.tsx
index f9d619546..e3455dfa7 100644
--- a/moon/apps/web/components/MrView/index.tsx
+++ b/moon/apps/web/components/MrView/index.tsx
@@ -16,6 +16,7 @@ import {
DropdownItemwithAvatar,
DropdownItemwithLabel,
DropdownOrder,
+ DropdownReview,
ListBanner,
ListItem as MrItem,
IssueList as MrList
@@ -29,7 +30,7 @@ import { atomWithWebStorage } from '@/utils/atomWithWebStorage'
import { IndexPageContainer, IndexPageContent } from '../IndexPages/components'
import { AdditionType, RightAvatar } from '../Issues/IssuesContent'
import { Pagination } from '../Issues/Pagenation'
-import { orderTags, tags } from '../Issues/utils/consts'
+import { orderTags, reviewTags, tags } from '../Issues/utils/consts'
import { generateAllMenuItems, MenuConfig } from '../Issues/utils/generateAllMenuItems'
import { filterAtom, sortAtom } from '../Issues/utils/store'
import { Heading } from './catalyst/heading'
@@ -63,12 +64,16 @@ export default function MrView() {
[scope]
)
+ const reviewAtom = useMemo(() => atomWithWebStorage(`${scope}:mr-review`, ''), [scope])
+
const labelAtom = useMemo(() => atomWithWebStorage(`${scope}:mr-label`, []), [scope])
const [order, setOrder] = useAtom(orderAtom)
const [label, setLabel] = useAtom(labelAtom)
+ const [review, setReview] = useAtom(reviewAtom)
+
const additions = useCallback(
(labels: number[]): AdditionType => {
const additional: AdditionType = { status, asc: false }
@@ -240,6 +245,29 @@ export default function MrView() {
}
]
+ const ReviewConfig: MenuConfig[] = [
+ {
+ key: 'Review',
+ isChosen: () => true,
+
+ onSelectFactory: (item) => (e: Event) => {
+ e.preventDefault()
+ if (item === review) {
+ setReview('')
+ } else {
+ setReview(item)
+ }
+ },
+ className: 'overflow-hidden',
+ labelFactory: (item) => (
+
+
{review === item && }
+
{item}
+
+ )
+ }
+ ]
+
const OrderConfig: MenuConfig[] = [
{
key: 'Order',
@@ -292,6 +320,8 @@ export default function MrView() {
const orders = generateAllMenuItems(orderTags, OrderConfig)
+ const reviews = generateAllMenuItems(reviewTags, ReviewConfig)
+
const ListHeaderItem = (p: string) => {
switch (p) {
case 'Author':
@@ -314,6 +344,15 @@ export default function MrView() {
dropdownItem={member?.get('Assignees').chosen}
/>
)
+ case 'Reviews':
+ return (
+
+ )
case 'Labels':
return (
}
>
{(p) => ListHeaderItem(p)}
diff --git a/moon/apps/web/components/SimpleNoteEditor/SimpleNoteContent.tsx b/moon/apps/web/components/SimpleNoteEditor/SimpleNoteContent.tsx
index e35f9c718..bf3b30460 100644
--- a/moon/apps/web/components/SimpleNoteEditor/SimpleNoteContent.tsx
+++ b/moon/apps/web/components/SimpleNoteEditor/SimpleNoteContent.tsx
@@ -1,4 +1,14 @@
-import { DragEvent, forwardRef, KeyboardEvent, memo, MouseEvent, useImperativeHandle, useRef, useState } from 'react'
+import {
+ DragEvent,
+ forwardRef,
+ KeyboardEvent,
+ memo,
+ MouseEvent,
+ useEffect,
+ useImperativeHandle,
+ useRef,
+ useState
+} from 'react'
import { Editor as TTEditor } from '@tiptap/core'
import { EditorContent } from '@tiptap/react'
@@ -28,6 +38,7 @@ interface Props {
content: string
onBlurAtTop?: BlurAtTopOptions['onBlur']
onKeyDown?: (event: KeyboardEvent) => void
+ onChange?: (html: string) => void
}
export interface SimpleNoteContentRef {
@@ -42,7 +53,7 @@ export interface SimpleNoteContentRef {
export const SimpleNoteContent = memo(
forwardRef((props, ref) => {
- const { commentId, editable = 'viewer', autofocus = false, onBlurAtTop, content } = props
+ const { commentId, editable = 'viewer', autofocus = false, onBlurAtTop, content, onChange } = props
const [activeComment, setActiveComment] = useState(null)
const [hoverComment, setHoverComment] = useState(null)
@@ -120,6 +131,22 @@ export const SimpleNoteContent = memo(
enabled: true
})
+ useEffect(() => {
+ if (!editor || !onChange) return
+
+ const handleUpdate = () => {
+ const html = editor.getHTML()
+
+ onChange?.(html)
+ }
+
+ editor.on('update', handleUpdate)
+
+ return () => {
+ editor.off('update', handleUpdate)
+ }
+ }, [editor, onChange])
+
return (
{
const OrganizationIssueDetailPage: PageWithLayout = ({ id }) => {
return (
<>
-
+
+
+
+
+
>
)
}
diff --git a/moon/apps/web/pages/[org]/issue/new/index.tsx b/moon/apps/web/pages/[org]/issue/new/index.tsx
index 8b3fd3ebd..9f2d7080c 100644
--- a/moon/apps/web/pages/[org]/issue/new/index.tsx
+++ b/moon/apps/web/pages/[org]/issue/new/index.tsx
@@ -12,10 +12,10 @@ const OrganizationIssueNewPage: PageWithLayout = () => {
return (
<>
-
-
-
-
+
+
+
+
>
)
}