From 9b63c4add53229df3cf70d5357142e06f4d2c23d Mon Sep 17 00:00:00 2001 From: liuyangjuncong Date: Fri, 25 Jul 2025 17:51:01 +0800 Subject: [PATCH 1/2] fix(UI): fix assignees stay status --- .../web/components/Issues/IssueDetailPage.tsx | 130 +++++++++++++----- .../web/components/Issues/IssueNewPage.tsx | 34 ++++- .../web/components/Issues/IssuesContent.tsx | 6 +- .../web/components/MrView/TimelineItems.tsx | 76 +++++----- .../web/hooks/issues/useGetIssueDetail.ts | 57 +------- .../web/hooks/issues/usePostIssueAssignees.ts | 11 ++ .../[org]/issue/{ => [link]}/[id]/index.tsx | 7 +- 7 files changed, 186 insertions(+), 135 deletions(-) create mode 100644 moon/apps/web/hooks/issues/usePostIssueAssignees.ts rename moon/apps/web/pages/[org]/issue/{ => [link]}/[id]/index.tsx (84%) diff --git a/moon/apps/web/components/Issues/IssueDetailPage.tsx b/moon/apps/web/components/Issues/IssueDetailPage.tsx index 93ed4dbfa..96dbd1448 100644 --- a/moon/apps/web/components/Issues/IssueDetailPage.tsx +++ b/moon/apps/web/components/Issues/IssueDetailPage.tsx @@ -7,6 +7,7 @@ import { ItemInput } from '@primer/react/lib/deprecated/ActionList' import { useRouter } from 'next/router' import toast from 'react-hot-toast' +import { CommonResultIssueDetailRes } from '@gitmono/types' import { Button, LoadingSpinner, PicturePlusIcon } from '@gitmono/ui' import { EMPTY_HTML } from '@/atoms/markdown' @@ -14,6 +15,7 @@ import { useHandleBottomScrollOffset } from '@/components/NoteEditor/useHandleBo import { ComposerReactionPicker } from '@/components/Reactions/ComposerReactionPicker' import { SimpleNoteContent, SimpleNoteContentRef } from '@/components/SimpleNoteEditor/SimpleNoteContent' import { useGetIssueDetail } from '@/hooks/issues/useGetIssueDetail' +import { usePostIssueAssignees } from '@/hooks/issues/usePostIssueAssignees' import { usePostIssueClose } from '@/hooks/issues/usePostIssueClose' import { usePostIssueComment } from '@/hooks/issues/usePostIssueComment' import { usePostIssueReopen } from '@/hooks/issues/usePostIssueReopen' @@ -31,34 +33,22 @@ import { tags } from './utils/consts' import { extractTextArray } from './utils/extractText' import { pickWithReflect } from './utils/pickWithReflectDeep' -interface IssueDetail { - status: string - conversations: Conversation[] - title: string -} -interface Conversation { - id: number - conv_type: string - comment: string - created_at: number - updated_at: number - username: string -} - -interface detailRes { - err_message: string - data: IssueDetail - req_result: boolean -} +// interface IssueDetail { +// status: string +// conversations: Conversation[] +// title: string +// assignees?: string[] +// } let needComment = false -export default function IssueDetailPage({ id }: { id: string }) { +export default function IssueDetailPage({ link, id }: { link: string; id: number }) { const [login, setLogin] = useState(false) - const [info, setInfo] = useState({ + const [info, setInfo] = useState>({ status: '', conversations: [], - title: '' + title: '', + assignees: [] }) const [buttonLoading, setButtonLoading] = useState<{ [key: string]: boolean }>({ comment: false, @@ -78,17 +68,24 @@ export default function IssueDetailPage({ id }: { id: string }) { const { mutate: saveComment } = usePostIssueComment() - const { data: issueDetailObj, error, isError, refetch, isLoading: detailIsLoading } = useGetIssueDetail(id) + const { mutate: issueAssignees } = usePostIssueAssignees() + + const { data: issueDetailObj, error, isError, refetch, isLoading: detailIsLoading } = useGetIssueDetail(link) + + const issueDetail = issueDetailObj?.data as CommonResultIssueDetailRes['data'] | undefined - const issueDetail = issueDetailObj?.data as IssueDetail | undefined + let selectRef = useRef([]) - const applyDetailData = (detail: detailRes | undefined) => { - if (!detail || !detail.req_result) return + const applyDetailData = (detail: CommonResultIssueDetailRes | undefined) => { + if (!detail || !detail.req_result || !detail.data) return setInfo({ title: detail.data.title, status: detail.data.status, - conversations: detail.data.conversations + conversations: detail.data.conversations, + assignees: detail.data.assignees }) + + selectRef.current = detail.data.assignees } const fetchDetail = useCallback(() => { @@ -99,7 +96,7 @@ export default function IssueDetailPage({ id }: { id: string }) { useEffect(() => { fetchDetail() - }, [fetchDetail, id]) + }, [fetchDetail, link]) const [_loadings, setLoadings] = useState([]) const router = useRouter() @@ -132,7 +129,7 @@ export default function IssueDetailPage({ id }: { id: string }) { setLoading('comment', true) set_to_loading(3) saveComment( - { link: id, data: { content: currentContentHTML } }, + { link, data: { content: currentContentHTML } }, { onSuccess: async () => { editorRef.current?.clearAndBlur() @@ -146,7 +143,7 @@ export default function IssueDetailPage({ id }: { id: string }) { onSettled: () => setLoading('comment', false) } ) - }, [id, refetch, saveComment]) + }, [link, refetch, saveComment]) const close_issue = useCallback(() => { if (closeHint === 'Close with comment') { @@ -156,7 +153,7 @@ export default function IssueDetailPage({ id }: { id: string }) { setLoading('close', true) set_to_loading(3) closeIssue( - { link: id }, + { link }, { onSuccess: () => { router.push(`/${router.query.org}/issue`) @@ -166,7 +163,7 @@ export default function IssueDetailPage({ id }: { id: string }) { onSettled: () => setLoading('close', false) } ) - }, [id, router, closeIssue, closeHint, save_comment]) + }, [link, router, closeIssue, closeHint, save_comment]) const reopen_issue = useCallback(() => { setLoading('reopen', true) @@ -176,7 +173,7 @@ export default function IssueDetailPage({ id }: { id: string }) { needComment = false } reopenIssue( - { link: id }, + { link }, { onSuccess: () => { router.push(`/${router.query.org}/issue`) @@ -185,7 +182,7 @@ export default function IssueDetailPage({ id }: { id: string }) { onSettled: () => setLoading('reopen', false) } ) - }, [id, router, reopenIssue, save_comment]) + }, [link, router, reopenIssue, save_comment]) const editorRef = useRef(null) const onKeyDownScrollHandler = useHandleBottomScrollOffset({ @@ -281,10 +278,65 @@ export default function IssueDetailPage({ id }: { id: string }) { } } + let selects: string[] = info?.assignees as string[] + + const shouldFetch = useRef(false) + + const handleAssignees = (selected: ItemInput[]) => { + selects = [...selected.map((i) => i.text).filter((t): t is string => typeof t === 'string')] + } + + const [open, setOpen] = useState(false) + + const handleOpneChange = (open: boolean) => { + if (selectRef.current.length !== selects.length) { + shouldFetch.current = true + } else { + const set = new Set(selects) + + for (let i = 0; i < selectRef.current.length; i++) { + if (!set.has(selectRef.current[i])) { + shouldFetch.current = true + break + } + } + } + + setOpen(open) + if (!open && shouldFetch.current) { + selectRef.current = selects + issueAssignees( + { + data: { + assignees: selectRef.current, + item_id: Number(id), + link + } + }, + { + onSuccess: async () => { + editorRef.current?.clearAndBlur() + const { data: issueDetailObj } = await refetch({ throwOnError: true }) + + applyDetailData(issueDetailObj) + }, + onError: apiErrorToast, + onSettled: () => (shouldFetch.current = false) + } + ) + } + } + + const fetchSelected = useMemo(() => { + const set = new Set(selectRef.current.length ? selectRef.current : info?.assignees) + + return avatars.filter((user) => set.has(user.text as string)) + }, [selectRef, avatars, info]) + return ( <>
- {info.title && ( + {info?.title && (
{info.title}
{info.status === 'open' ? ( @@ -315,7 +367,7 @@ export default function IssueDetailPage({ id }: { id: string }) {
) : ( - + )} {info && info.status === 'open' && ( @@ -431,6 +483,10 @@ export default function IssueDetailPage({ id }: { id: string }) { selectPannelProps={{ title: 'Assign up to 10 people to this issue' }} items={avatars} title='Assignees' + handleGroup={(selected) => handleAssignees(selected)} + open={open} + onOpenChange={(open) => handleOpneChange(open)} + selected={fetchSelected} > {(el) => { const names = Array.from(new Set(splitFun(el))) @@ -486,4 +542,4 @@ export default function IssueDetailPage({ id }: { id: string }) {
) -} \ No newline at end of file +} diff --git a/moon/apps/web/components/Issues/IssueNewPage.tsx b/moon/apps/web/components/Issues/IssueNewPage.tsx index 05a04b5f5..ef3534b4f 100644 --- a/moon/apps/web/components/Issues/IssueNewPage.tsx +++ b/moon/apps/web/components/Issues/IssueNewPage.tsx @@ -1,6 +1,6 @@ 'use client' -import React, { useCallback, useMemo, useRef, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { ActionList, FormControl, SelectPanel, Stack, Text, TextInput } from '@primer/react' import { ItemInput } from '@primer/react/lib/deprecated/ActionList' import { useRouter } from 'next/router' @@ -315,7 +315,10 @@ export const BadgeItem = ({ selectPannelProps, items, children, - handleGroup + handleGroup, + open, + onOpenChange, + selected }: { title: string selectPannelProps?: Omit, SelectPanelExcludedProps> & { @@ -324,13 +327,34 @@ export const BadgeItem = ({ items: ItemInput[] children?: (el: React.ReactNode) => React.ReactNode handleGroup?: (selected: ItemInput[]) => void + open?: boolean + onOpenChange?: (open: boolean) => void + selected?: ItemInput[] }) => { + // const { open, onOpenChange } = selectPannelProps ?? {} + // open有传,则是受控组件,没传就是非受控组件(内部管理) + const isControl = open !== undefined + const [control, setControl] = useState(false) + const currentOpen = isControl ? open : control + const [chose, setChose] = useState([]) const [filter, setFilter] = React.useState('') + useEffect(() => { + selected && selected?.length !== 0 ? setChose([...selected]) : setChose([]) + }, [selected]) + + const handleOpenChange = (nextOpen: boolean, _gesture?: any) => { + // 有onOpenChange就传递 + onOpenChange?.(nextOpen) + if (!isControl) { + setControl(nextOpen) + } + } + const filteredItems = items.filter( (item) => // design guidelines say to always show selected item in the list @@ -359,8 +383,8 @@ export const BadgeItem = ({ ) }} - open={control} - onOpenChange={setControl} + open={currentOpen} + onOpenChange={(open, gesture) => handleOpenChange(open, gesture)} items={filteredItems} selected={chose} onSelectedChange={(selected: ItemInput[]) => { @@ -385,4 +409,4 @@ export const SideBarItem = ({ emptyState }: { emptyState: string }) => { ) -} \ No newline at end of file +} diff --git a/moon/apps/web/components/Issues/IssuesContent.tsx b/moon/apps/web/components/Issues/IssuesContent.tsx index 2c6f5f24a..5d825df01 100644 --- a/moon/apps/web/components/Issues/IssuesContent.tsx +++ b/moon/apps/web/components/Issues/IssuesContent.tsx @@ -99,8 +99,6 @@ export function IssuesContent({ searching }: Props) { const router = useRouter() - - const [openCurrent, setopenCurrent] = useAtom(issueOpenCurrentPage) const [closeCurrent, setcloseCurrent] = useAtom(issueCloseCurrentPage) @@ -416,7 +414,7 @@ export function IssuesContent({ searching }: Props) { title={i.title} leftIcon={getStatusIcon(i.status)} rightIcon={} - onClick={() => router.push(`/${scope}/issue/${i.link}`)} + onClick={() => router.push(`/${scope}/issue/${i.link}/${i.id}`)} >
{i.link} · {i.author} {i.status}{' '} @@ -480,4 +478,4 @@ export const RightAvatar = ({ item }: { item: ItemsType[number] }) => {
) -} \ No newline at end of file +} diff --git a/moon/apps/web/components/MrView/TimelineItems.tsx b/moon/apps/web/components/MrView/TimelineItems.tsx index 2f2c50fe3..7d16684d2 100644 --- a/moon/apps/web/components/MrView/TimelineItems.tsx +++ b/moon/apps/web/components/MrView/TimelineItems.tsx @@ -1,13 +1,18 @@ import React from 'react' + import '@primer/primitives/dist/css/functional/themes/light.css' + +import { CommentIcon, FeedMergedIcon, FeedPullRequestClosedIcon, FeedPullRequestOpenIcon } from '@primer/octicons-react' import { BaseStyles, ThemeProvider, Timeline } from '@primer/react' -import { FeedPullRequestClosedIcon, CommentIcon, FeedMergedIcon, FeedPullRequestOpenIcon } from '@primer/octicons-react' -import MRComment from '@/components/MrView/MRComment'; -import CloseItem from './CloseItem'; -import ReopenItem from './ReopenItem'; -import MergedItem from './MergedItem'; + import { ConversationItem } from '@gitmono/types/generated' +import MRComment from '@/components/MrView/MRComment' + +import CloseItem from './CloseItem' +import MergedItem from './MergedItem' +import ReopenItem from './ReopenItem' + interface TimelineItemProps { badge?: React.ReactNode children?: React.ReactNode @@ -18,14 +23,13 @@ interface ConvItem { id: number badge?: React.ReactNode children?: React.ReactNode - isOver: boolean; + isOver: boolean } interface TimelineWrapperProps { - convItems?: ConvItem[]; + convItems?: ConvItem[] } - const TimelineItem = ({ badge, children, isOver }: TimelineItemProps) => { return ( <> @@ -51,42 +55,44 @@ const TimelineWrapper: React.FC = ({ convItems = [] }) => - ); -}; - -const TimelineItems: React.FC<{ detail: any, id: string, type: string }> = ({ detail, id, type }) => { + ) +} +const TimelineItems: React.FC<{ detail: any; id: string; type: string }> = ({ detail, id, type }) => { const convItems: ConvItem[] = detail.conversations.map((conv: ConversationItem) => { - let icon; - let children; - let isOver = false; + let icon + let children + let isOver = false switch (conv.conv_type) { case 'Comment': - icon = ; - children = ; - break; + icon = + children = + break case 'Merged': - icon = ; - children = ; - isOver = true; - break; + icon = + children = + isOver = true + break case 'Closed': - icon = ; - children = ; - isOver = true; - break; + icon = + children = + isOver = true + break case 'Reopen': - icon = - children = ; - break; + icon = + children = + break + case 'Assignee': + icon = + children = + break } - return { badge: icon, children, isOver, id: conv.id }; - }); + return { badge: icon, children, isOver, id: conv.id } + }) - return ; -}; + return +} -export default TimelineItems; \ No newline at end of file +export default TimelineItems diff --git a/moon/apps/web/hooks/issues/useGetIssueDetail.ts b/moon/apps/web/hooks/issues/useGetIssueDetail.ts index 4d79b1d25..83045814e 100644 --- a/moon/apps/web/hooks/issues/useGetIssueDetail.ts +++ b/moon/apps/web/hooks/issues/useGetIssueDetail.ts @@ -1,58 +1,13 @@ import { useQuery } from '@tanstack/react-query' -import { legacyApiClient } from '@/utils/queryClient' - -interface conversations { - id: number - conv_type: string - comment: string - created_at: number - updated_at: number - username: string -} +import { CommonResultIssueDetailRes, RequestParams } from '@gitmono/types' -interface raw { - id: number - link: string - title: string - status: string - open_timestamp: number - conversations: conversations[] -} - -interface issueDetail { - status: string - conversations: conversations[] - title: string -} - -interface detailRes { - err_message: string - data: issueDetail - req_result: boolean -} -const getApiIssueDetail = legacyApiClient.v1.getApiIssueDetail() - -export function useGetIssueDetail(id: string) { - return useQuery({ - queryKey: legacyApiClient.v1.getApiIssueDetail().requestKey(id), - queryFn: async () => { - const { err_message, data, req_result } = await getApiIssueDetail.request(id) - - if (!req_result) throw new Error(err_message || 'fetching failed') - const rawData = data as unknown as raw - const converted: issueDetail = { - title: rawData.title, - conversations: rawData.conversations, - status: rawData.status - } +import { legacyApiClient } from '@/utils/queryClient' - return { - err_message, - data: converted, - req_result - } - }, +export function useGetIssueDetail(id: string, params?: RequestParams) { + return useQuery({ + queryKey: [legacyApiClient.v1.getApiIssueDetail().requestKey(id), params], + queryFn: () => legacyApiClient.v1.getApiIssueDetail().request(id, params), enabled: !!id }) diff --git a/moon/apps/web/hooks/issues/usePostIssueAssignees.ts b/moon/apps/web/hooks/issues/usePostIssueAssignees.ts new file mode 100644 index 000000000..ac8a40dce --- /dev/null +++ b/moon/apps/web/hooks/issues/usePostIssueAssignees.ts @@ -0,0 +1,11 @@ +import { useMutation } from '@tanstack/react-query' + +import { AssigneeUpdatePayload, PostApiIssueAssigneesData, RequestParams } from '@gitmono/types/generated' + +import { legacyApiClient } from '@/utils/queryClient' + +export function usePostIssueAssignees() { + return useMutation({ + mutationFn: ({ data, params }) => legacyApiClient.v1.postApiIssueAssignees().request(data, params) + }) +} diff --git a/moon/apps/web/pages/[org]/issue/[id]/index.tsx b/moon/apps/web/pages/[org]/issue/[link]/[id]/index.tsx similarity index 84% rename from moon/apps/web/pages/[org]/issue/[id]/index.tsx rename to moon/apps/web/pages/[org]/issue/[link]/[id]/index.tsx index b3c5edb8e..d7062ac57 100644 --- a/moon/apps/web/pages/[org]/issue/[id]/index.tsx +++ b/moon/apps/web/pages/[org]/issue/[link]/[id]/index.tsx @@ -17,17 +17,18 @@ export const getServerSideProps: GetServerSideProps = async ({ query }) => { } return { props: { - id: query.id + id: query.id, + link: query.link } } } -const OrganizationIssueDetailPage: PageWithLayout = ({ id }) => { +const OrganizationIssueDetailPage: PageWithLayout = ({ link, id }) => { return ( <> - + From 3659e6dd0d671851845fe758a2dbadafeacf8bfd Mon Sep 17 00:00:00 2001 From: liuyangjuncong Date: Sat, 26 Jul 2025 16:10:43 +0800 Subject: [PATCH 2/2] chore: adjust some logic --- .../web/components/Issues/IssueDetailPage.tsx | 189 +++------ .../web/components/Issues/IssueNewPage.tsx | 55 +-- .../web/components/Issues/IssuesContent.tsx | 63 ++- .../components/Issues/utils/sideEffect.tsx | 153 ++++++++ .../web/components/MemberAvatar/index.tsx | 2 +- moon/apps/web/components/MrView/index.tsx | 116 +++--- .../web/hooks/issues/usePostMRAssignees.ts | 11 + moon/apps/web/pages/[org]/mr/[id].tsx | 259 ------------- moon/apps/web/pages/[org]/mr/[link]/[id].tsx | 358 ++++++++++++++++++ 9 files changed, 692 insertions(+), 514 deletions(-) create mode 100644 moon/apps/web/components/Issues/utils/sideEffect.tsx create mode 100644 moon/apps/web/hooks/issues/usePostMRAssignees.ts delete mode 100644 moon/apps/web/pages/[org]/mr/[id].tsx create mode 100644 moon/apps/web/pages/[org]/mr/[link]/[id].tsx diff --git a/moon/apps/web/components/Issues/IssueDetailPage.tsx b/moon/apps/web/components/Issues/IssueDetailPage.tsx index 96dbd1448..aabfb2b6c 100644 --- a/moon/apps/web/components/Issues/IssueDetailPage.tsx +++ b/moon/apps/web/components/Issues/IssueDetailPage.tsx @@ -3,7 +3,6 @@ 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' @@ -21,7 +20,6 @@ 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 { trimHtml } from '@/utils/trimHtml' @@ -29,9 +27,16 @@ 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' +import { + splitFun, + useAssigneesSelector, + useAvatars, + useChange, + useLabelMap, + useLabels, + useMemberMap +} from './utils/sideEffect' // interface IssueDetail { // status: string @@ -40,7 +45,7 @@ import { pickWithReflect } from './utils/pickWithReflectDeep' // assignees?: string[] // } -let needComment = false +// let needComment = false export default function IssueDetailPage({ link, id }: { link: string; id: number }) { const [login, setLogin] = useState(false) @@ -60,7 +65,8 @@ export default function IssueDetailPage({ link, id }: { link: string; id: number setButtonLoading((prev) => ({ ...prev, [key]: value })) } - const [closeHint, setCloseHint] = useState('Close issue') + const { closeHint, needComment, handleChange, handleCloseChange } = useChange({}) + // const [closeHint, setCloseHint] = useState('Close issue') const { mutate: closeIssue } = usePostIssueClose() @@ -74,8 +80,6 @@ export default function IssueDetailPage({ link, id }: { link: string; id: number const issueDetail = issueDetailObj?.data as CommonResultIssueDetailRes['data'] | undefined - let selectRef = useRef([]) - const applyDetailData = (detail: CommonResultIssueDetailRes | undefined) => { if (!detail || !detail.req_result || !detail.data) return setInfo({ @@ -85,7 +89,7 @@ export default function IssueDetailPage({ link, id }: { link: string; id: number assignees: detail.data.assignees }) - selectRef.current = detail.data.assignees + // selectRef.current = detail.data.assignees } const fetchDetail = useCallback(() => { @@ -168,9 +172,9 @@ export default function IssueDetailPage({ link, id }: { link: string; id: number const reopen_issue = useCallback(() => { setLoading('reopen', true) set_to_loading(3) - if (needComment) { + if (needComment.current) { save_comment() - needComment = false + needComment.current = false } reopenIssue( { link }, @@ -182,7 +186,7 @@ export default function IssueDetailPage({ link, id }: { link: string; id: number onSettled: () => setLoading('reopen', false) } ) - }, [link, router, reopenIssue, save_comment]) + }, [link, router, reopenIssue, save_comment, needComment]) const editorRef = useRef(null) const onKeyDownScrollHandler = useHandleBottomScrollOffset({ @@ -210,105 +214,37 @@ export default function IssueDetailPage({ link, id }: { link: string; id: number } }, [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() + const avatars = useAvatars() - 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') - } - } - - const handleCloseChange = (html: string) => { - if (html && html === '

') { - needComment = false - } else { - needComment = true - } - } - - let selects: string[] = info?.assignees as string[] + const labels = useLabels() - const shouldFetch = useRef(false) + const memberMap = useMemberMap() - const handleAssignees = (selected: ItemInput[]) => { - selects = [...selected.map((i) => i.text).filter((t): t is string => typeof t === 'string')] - } + const labelMap = useLabelMap() - const [open, setOpen] = useState(false) + // const handleChange = (html: string) => { + // if (html && html === '

') { + // setCloseHint('Close issue') + // } else { + // setCloseHint('Close with comment') + // } + // } - const handleOpneChange = (open: boolean) => { - if (selectRef.current.length !== selects.length) { - shouldFetch.current = true - } else { - const set = new Set(selects) - - for (let i = 0; i < selectRef.current.length; i++) { - if (!set.has(selectRef.current[i])) { - shouldFetch.current = true - break - } - } - } + // const handleCloseChange = (html: string) => { + // if (html && html === '

') { + // needComment = false + // } else { + // needComment = true + // } + // } - setOpen(open) - if (!open && shouldFetch.current) { - selectRef.current = selects + const { open, handleAssignees, handleOpenChange, fetchSelected } = useAssigneesSelector({ + assignees: info?.assignees ?? [], + assignRequest: (selected) => issueAssignees( { data: { - assignees: selectRef.current, + assignees: selected, item_id: Number(id), link } @@ -320,18 +256,11 @@ export default function IssueDetailPage({ link, id }: { link: string; id: number applyDetailData(issueDetailObj) }, - onError: apiErrorToast, - onSettled: () => (shouldFetch.current = false) + onError: apiErrorToast } - ) - } - } - - const fetchSelected = useMemo(() => { - const set = new Set(selectRef.current.length ? selectRef.current : info?.assignees) - - return avatars.filter((user) => set.has(user.text as string)) - }, [selectRef, avatars, info]) + ), + avatars + }) return ( <> @@ -360,7 +289,7 @@ export default function IssueDetailPage({ link, id }: { link: string; id: number {avatarUser && } -
+
{detailIsLoading ? (
@@ -375,7 +304,7 @@ export default function IssueDetailPage({ link, id }: { link: string; id: number

Add a comment

-
+

Add a comment

-
+
handleCloseChange(html)} /> -
@@ -485,7 +416,7 @@ export default function IssueDetailPage({ link, id }: { link: string; id: number title='Assignees' handleGroup={(selected) => handleAssignees(selected)} open={open} - onOpenChange={(open) => handleOpneChange(open)} + onOpenChange={(open) => handleOpenChange(open)} selected={fetchSelected} > {(el) => { diff --git a/moon/apps/web/components/Issues/IssueNewPage.tsx b/moon/apps/web/components/Issues/IssueNewPage.tsx index ef3534b4f..a3238fdde 100644 --- a/moon/apps/web/components/Issues/IssueNewPage.tsx +++ b/moon/apps/web/components/Issues/IssueNewPage.tsx @@ -18,15 +18,13 @@ import { useScope } from '@/contexts/scope' import { usePostIssueSubmit } from '@/hooks/issues/usePostIssueSubmit' 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 { trimHtml } from '@/utils/trimHtml' import { MemberAvatar } from '../MemberAvatar' -import { tags } from './utils/consts' -import { extractTextArray } from './utils/extractText' import { pickWithReflect } from './utils/pickWithReflectDeep' +import { splitFun, useAvatars, useLabelMap, useLabels, useMemberMap } from './utils/sideEffect' // import { OrganizationMember } from '@gitmono/types/generated' @@ -54,39 +52,10 @@ export default function IssueNewPage() { } }, [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 avatars = useAvatars() // const [avatarTems, setAvatarItems] = useState(avatars) - const labels: ItemInput[] = useMemo( - () => - tags.map((i) => ({ - text: i.description, - leadingVisual: () => ( -
- ) - })), - [] - ) + const labels = useLabels() const [isReactionPickerOpen, setIsReactionPickerOpen] = useState(false) const set_to_loading = (index: number) => { @@ -137,23 +106,9 @@ export default function IssueNewPage() { upload: editorRef.current?.uploadAndAppendAttachments }) - const memberMap = useMemo(() => { - const map = new Map() + const memberMap = useMemberMap() - 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 labelMap = useLabelMap() // const createGroup = (selected: ItemInput[]) => { // setAvatarItems((prev) => { diff --git a/moon/apps/web/components/Issues/IssuesContent.tsx b/moon/apps/web/components/Issues/IssuesContent.tsx index 5d825df01..0e60d57f7 100644 --- a/moon/apps/web/components/Issues/IssuesContent.tsx +++ b/moon/apps/web/components/Issues/IssuesContent.tsx @@ -424,21 +424,25 @@ export function IssuesContent({ searching }: Props) { )) }} - {numTotal > 10 && status === 'open' && ( - handlePageChange(page)} - /> + {status === 'open' && ( +
+ handlePageChange(page)} + /> +
)} - {numTotal > 10 && status === 'closed' && ( - handlePageChange(page)} - /> + {status === 'closed' && ( +
+ handlePageChange(page)} + /> +
)} )} @@ -458,17 +462,34 @@ function IssueSearchList(_props: { searchIssueList?: Item[]; hideProject?: boole export const RightAvatar = ({ item }: { item: ItemsType[number] }) => { const shouldFetch = item.assignees.length > 0 - const { data } = useGetOrganizationMember({ username: item.assignees[0], org: 'mega', enabled: shouldFetch }) + const [index, setIndex] = useState(0) + + const { data, error } = useGetOrganizationMember({ + username: item.assignees[index], + org: 'mega', + enabled: shouldFetch + }) + + useEffect(() => { + if (index >= item.assignees.length) return + if (error) { + setIndex((prev) => prev + 1) + } + }, [error, index, item.assignees]) return ( <>
- {item.comment_num !== 0 && ( -
- - {item.comment_num} -
- )} +
+ + {item.comment_num} +
+ {data && ( //
展示头像
diff --git a/moon/apps/web/components/Issues/utils/sideEffect.tsx b/moon/apps/web/components/Issues/utils/sideEffect.tsx new file mode 100644 index 000000000..72c74797d --- /dev/null +++ b/moon/apps/web/components/Issues/utils/sideEffect.tsx @@ -0,0 +1,153 @@ +import { useMemo, useRef, useState } from 'react' +import { ItemInput } from '@primer/react/lib/SelectPanel/types' + +import { MemberAvatar } from '@/components/MemberAvatar' +import { useSyncedMembers } from '@/hooks/useSyncedMembers' + +import { tags } from './consts' +import { extractTextArray } from './extractText' + +export const useAvatars = () => { + const { members } = useSyncedMembers() + + return useMemo( + () => + members?.map((i) => ({ + groupId: 'end', + text: i.user.username, + leadingVisual: () => + })) || [], + [members] + ) +} + +export const splitFun = (el: React.ReactNode): string[] => { + return extractTextArray(el) + .flatMap((name) => name.split(',').map((n) => n.trim())) + .filter((n) => n.length > 0) +} + +export const useMemberMap = () => { + const { members } = useSyncedMembers() + + return useMemo(() => { + const map = new Map() + + members?.forEach((i) => { + map.set(i.user.username, i) + }) + return map + }, [members]) +} + +export const useLabels = () => { + return useMemo( + () => + tags.map((i) => ({ + text: i.description, + leadingVisual: () => ( +
+ ) + })), + [] + ) +} + +export const useLabelMap = () => { + return useMemo(() => { + const map = new Map() + + tags.map((i) => { + map.set(i.description, i) + }) + return map + }, []) +} + +// assignees逻辑 + +export const useAssigneesSelector = ({ + assignees, + assignRequest, + avatars +}: { + assignees: string[] + assignRequest: (selected: string[]) => void + avatars: ReturnType +}) => { + const selectRef = useRef([]) + let selects: string[] = assignees as string[] + const shouldFetch = useRef(false) + const [open, setOpen] = useState(false) + + const handleAssignees = (selected: ItemInput[]) => { + selects = [...selected.map((i) => i.text).filter((t): t is string => typeof t === 'string')] + } + + const handleOpenChange = (open: boolean) => { + if (selectRef.current.length !== selects.length) { + shouldFetch.current = true + } else { + const set = new Set(selects) + + for (let i = 0; i < selectRef.current.length; i++) { + if (!set.has(selectRef.current[i])) { + shouldFetch.current = true + break + } + } + } + + setOpen(open) + if (!open && shouldFetch.current) { + selectRef.current = selects + assignRequest(selectRef.current) + setTimeout(() => (shouldFetch.current = false), 0) + } + } + + const fetchSelected = useMemo(() => { + const set = new Set(selectRef.current.length ? selectRef.current : assignees) + + return avatars.filter((user) => set.has(user.text as string)) + }, [selectRef, avatars, assignees]) + + return { + open, + handleAssignees, + handleOpenChange, + fetchSelected + } +} + +export const useChange = ({ title = 'Close issue' }: { title?: string }) => { + const [closeHint, setCloseHint] = useState(title) + + const needComment = useRef(false) + const handleChange = (html: string) => { + if (html && html === '

') { + setCloseHint(title) + } else { + setCloseHint('Close with comment') + } + } + + const handleCloseChange = (html: string) => { + if (html && html === '

') { + needComment.current = false + } else { + needComment.current = true + } + } + + return { + closeHint, + needComment, + handleChange, + handleCloseChange + } +} diff --git a/moon/apps/web/components/MemberAvatar/index.tsx b/moon/apps/web/components/MemberAvatar/index.tsx index afd130d63..0a4cf97d8 100644 --- a/moon/apps/web/components/MemberAvatar/index.tsx +++ b/moon/apps/web/components/MemberAvatar/index.tsx @@ -17,7 +17,7 @@ export function MemberAvatar({ }: { displayStatus?: boolean; member: { deactivated?: boolean; user: MemberAvatarUser } } & ComponentProps< typeof Avatar >) { - const isOnline = useUserIsOnline(member.user.id) + const isOnline = useUserIsOnline(member?.user?.id) || true return ( +
Merge Request
- } - > - {(p) => ListHeaderItem(p)} - - } - > - {(issueList) => { - return issueList.map((i) => ( - router.push(`/${scope}/mr/${i.link}`)} - title={i.title} - leftIcon={getStatusIcon(i.status)} - rightIcon={} +
+ } > -
- {i.link} {i.status} {getDescription(i)} -
- - )) - }} -
- {numTotal > 10 && status === 'open' && ( - handlePageChange(page)} - /> - )} - {numTotal > 10 && status === 'closed' && ( - handlePageChange(page)} - /> - )} + {(p) => ListHeaderItem(p)} + + } + > + {(issueList) => { + return issueList.map((i) => ( + router.push(`/${scope}/mr/${i.link}/${i.id}`)} + title={i.title} + leftIcon={getStatusIcon(i.status)} + rightIcon={} + > +
+ {i.link} {i.status} {getDescription(i)} +
+
+ )) + }} + +
+ {status === 'open' && ( +
+ handlePageChange(page)} + /> +
+ )} + {status === 'closed' && ( +
+ handlePageChange(page)} + /> +
+ )} +
+
) -} \ No newline at end of file +} diff --git a/moon/apps/web/hooks/issues/usePostMRAssignees.ts b/moon/apps/web/hooks/issues/usePostMRAssignees.ts new file mode 100644 index 000000000..73553e113 --- /dev/null +++ b/moon/apps/web/hooks/issues/usePostMRAssignees.ts @@ -0,0 +1,11 @@ +import { useMutation } from '@tanstack/react-query' + +import {AssigneeUpdatePayload, PostApiMrAssigneesData, RequestParams } from '@gitmono/types/generated' + +import { legacyApiClient } from '@/utils/queryClient' + +export function usePostMRAssignees() { + return useMutation({ + mutationFn: ({ data, params }) => legacyApiClient.v1.postApiMrAssignees().request(data, params) + }) +} diff --git a/moon/apps/web/pages/[org]/mr/[id].tsx b/moon/apps/web/pages/[org]/mr/[id].tsx deleted file mode 100644 index 9a0180172..000000000 --- a/moon/apps/web/pages/[org]/mr/[id].tsx +++ /dev/null @@ -1,259 +0,0 @@ -'use client' - -import React, { useRef, useState } from 'react' -import { ChecklistIcon, CommentDiscussionIcon, FileDiffIcon } from '@primer/octicons-react' -import { useRouter } from 'next/router' -import { toast } from 'react-hot-toast' - -import { ConversationItem } from '@gitmono/types/generated' -import { Button, LoadingSpinner, UIText } from '@gitmono/ui' -import { PicturePlusIcon } from '@gitmono/ui/Icons' -import { cn } from '@gitmono/ui/utils' - -import { EMPTY_HTML } from '@/atoms/markdown' -import FileDiff from '@/components/DiffView/FileDiff' -import { AppLayout } from '@/components/Layout/AppLayout' -import TimelineItems from '@/components/MrView/TimelineItems' -import { useHandleBottomScrollOffset } from '@/components/NoteEditor/useHandleBottomScrollOffset' -import AuthAppProviders from '@/components/Providers/AuthAppProviders' -import { ComposerReactionPicker } from '@/components/Reactions/ComposerReactionPicker' -import { SimpleNoteContent, SimpleNoteContentRef } from '@/components/SimpleNoteEditor/SimpleNoteContent' -import { useScope } from '@/contexts/scope' -import { useGetMrDetail } from '@/hooks/useGetMrDetail' -import { useGetMrFilesChanged } from '@/hooks/useGetMrFilesChanged' -import { usePostMrClose } from '@/hooks/usePostMrClose' -import { usePostMrComment } from '@/hooks/usePostMrComment' -import { usePostMrMerge } from '@/hooks/usePostMrMerge' -import { usePostMrReopen } from '@/hooks/usePostMrReopen' -import { useUploadHelpers } from '@/hooks/useUploadHelpers' -import { trimHtml } from '@/utils/trimHtml' -import { PageWithLayout } from '@/utils/types' - -const { UnderlinePanels } = require('@primer/react/experimental') - -export interface MRDetail { - status: string - conversations: ConversationItem[] - title: string -} - -let needComment = false - -const MRDetailPage: PageWithLayout = () => { - const router = useRouter() - const { id: tempId } = router.query - const { scope } = useScope() - const [login, _setLogin] = useState(true) - const [isReactionPickerOpen, setIsReactionPickerOpen] = useState(false) - const id = typeof tempId === 'string' ? tempId : '' - const { data: MrDetailData, isLoading: detailIsLoading } = useGetMrDetail(id) - const mrDetail = MrDetailData?.data as MRDetail | undefined - const [closeHint, setCloseHint] = useState('Close Merge Request') - - if (mrDetail && typeof mrDetail.status === 'string') { - mrDetail.status = mrDetail.status.toLowerCase() - } - - const { data: MrFilesChangedData, isLoading: fileChgIsLoading } = useGetMrFilesChanged(id) - - const { mutate: approveMr, isPending: mrMergeIsPending } = usePostMrMerge(id) - const handleMrApprove = () => { - approveMr(undefined, { - onSuccess: () => { - router.push(`/${scope}/mr`) - } - }) - } - - const { mutate: closeMr, isPending: mrCloseIsPending } = usePostMrClose(id) - const handleMrClose = () => { - if (closeHint === 'Close with comment') { - send_comment() - } - closeMr(undefined, { - onSuccess: () => { - router.push(`/${scope}/mr`) - } - }) - } - - const { mutate: reopenMr, isPending: mrReopenIsPending } = usePostMrReopen(id) - const handleMrReopen = () => { - if (needComment) { - send_comment() - needComment = false - } - reopenMr(undefined, { - onSuccess: () => { - router.push(`/${scope}/mr`) - } - }) - } - - const { mutate: postMrComment, isPending: mrCommentIsPending } = usePostMrComment(id) - - const send_comment = () => { - const currentContentHTML = editorRef.current?.editor?.getHTML() ?? '

'; - 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' - const editorRef = useRef(null) - const onKeyDownScrollHandler = useHandleBottomScrollOffset({ - editor: editorRef.current?.editor - }) - const { dropzone } = useUploadHelpers({ - upload: editorRef.current?.uploadAndAppendAttachments - }) - - const handleChange = (html: string) => { - if (html && html === '

') { - setCloseHint('Close Merge Request') - needComment = false - } else { - setCloseHint('Close with comment') - needComment = true - } - } - - return ( -
-
- - {`${mrDetail?.title || ''}#${id}`} - -
-
- - Conversation - Checks - Files Changed - -
- {detailIsLoading ? ( -
- -
- ) : ( - mrDetail && - )} -
-
- {mrDetail && mrDetail.status === 'open' && ( - - )} -
-

Add a comment

- -
- handleChange(html)} - /> -
-
- {mrDetail && mrDetail.status === 'open' && ( - - )} - {mrDetail && mrDetail.status === 'closed' && ( - - )} - -
-
-
-
- -
Checks
-
- - {fileChgIsLoading ? ( -
- -
- ) : MrFilesChangedData?.data?.content ? ( - - ) : ( -
No files changed
- )} -
-
-
-
- ) -} - -MRDetailPage.getProviders = (page, pageProps) => { - return ( - - {page} - - ) -} - -export default MRDetailPage \ No newline at end of file diff --git a/moon/apps/web/pages/[org]/mr/[link]/[id].tsx b/moon/apps/web/pages/[org]/mr/[link]/[id].tsx new file mode 100644 index 000000000..33b21d751 --- /dev/null +++ b/moon/apps/web/pages/[org]/mr/[link]/[id].tsx @@ -0,0 +1,358 @@ +'use client' + +import React, { useRef, useState } from 'react' +import { ChecklistIcon, CommentDiscussionIcon, FileDiffIcon } from '@primer/octicons-react' +import { BaseStyles, ThemeProvider } from '@primer/react' +import { useRouter } from 'next/router' +import { toast } from 'react-hot-toast' + +import { ConversationItem } from '@gitmono/types/generated' +import { Button, LoadingSpinner, UIText } from '@gitmono/ui' +import { PicturePlusIcon } from '@gitmono/ui/Icons' +import { cn } from '@gitmono/ui/utils' + +import { EMPTY_HTML } from '@/atoms/markdown' +import FileDiff from '@/components/DiffView/FileDiff' +import { BadgeItem } from '@/components/Issues/IssueNewPage' +import { + splitFun, + useAssigneesSelector, + useAvatars, + useChange, + useLabelMap, + useLabels, + useMemberMap +} from '@/components/Issues/utils/sideEffect' +import { AppLayout } from '@/components/Layout/AppLayout' +import { MemberAvatar } from '@/components/MemberAvatar' +import TimelineItems from '@/components/MrView/TimelineItems' +import { useHandleBottomScrollOffset } from '@/components/NoteEditor/useHandleBottomScrollOffset' +import AuthAppProviders from '@/components/Providers/AuthAppProviders' +import { ComposerReactionPicker } from '@/components/Reactions/ComposerReactionPicker' +import { SimpleNoteContent, SimpleNoteContentRef } from '@/components/SimpleNoteEditor/SimpleNoteContent' +import { useScope } from '@/contexts/scope' +import { usePostMRAssignees } from '@/hooks/issues/usePostMRAssignees' +import { useGetMrDetail } from '@/hooks/useGetMrDetail' +import { useGetMrFilesChanged } from '@/hooks/useGetMrFilesChanged' +import { usePostMrClose } from '@/hooks/usePostMrClose' +import { usePostMrComment } from '@/hooks/usePostMrComment' +import { usePostMrMerge } from '@/hooks/usePostMrMerge' +import { usePostMrReopen } from '@/hooks/usePostMrReopen' +import { useUploadHelpers } from '@/hooks/useUploadHelpers' +import { apiErrorToast } from '@/utils/apiErrorToast' +import { trimHtml } from '@/utils/trimHtml' +import { PageWithLayout } from '@/utils/types' + +const { UnderlinePanels } = require('@primer/react/experimental') + +export interface MRDetail { + status: string + conversations: ConversationItem[] + title: string +} + +const MRDetailPage: PageWithLayout = () => { + const router = useRouter() + const { link: tempId, id: item_id } = router.query + const { scope } = useScope() + const [login, _setLogin] = useState(true) + const [isReactionPickerOpen, setIsReactionPickerOpen] = useState(false) + const id = typeof tempId === 'string' ? tempId : '' + const { data: MrDetailData, isLoading: detailIsLoading, refetch } = useGetMrDetail(id) + const mrDetail = MrDetailData?.data as MRDetail | undefined + const { closeHint, needComment, handleChange } = useChange({ title: 'Close Merge Request' }) + const { mutate: mrAssgnees } = usePostMRAssignees() + + if (mrDetail && typeof mrDetail.status === 'string') { + mrDetail.status = mrDetail.status.toLowerCase() + } + + const { data: MrFilesChangedData, isLoading: fileChgIsLoading } = useGetMrFilesChanged(id) + + const { mutate: approveMr, isPending: mrMergeIsPending } = usePostMrMerge(id) + const handleMrApprove = () => { + approveMr(undefined, { + onSuccess: () => { + router.push(`/${scope}/mr`) + } + }) + } + + const { mutate: closeMr, isPending: mrCloseIsPending } = usePostMrClose(id) + const handleMrClose = () => { + if (closeHint === 'Close with comment') { + send_comment() + } + closeMr(undefined, { + onSuccess: () => { + router.push(`/${scope}/mr`) + } + }) + } + + const { mutate: reopenMr, isPending: mrReopenIsPending } = usePostMrReopen(id) + const handleMrReopen = () => { + if (needComment.current) { + send_comment() + needComment.current = false + } + reopenMr(undefined, { + onSuccess: () => { + router.push(`/${scope}/mr`) + } + }) + } + + const { mutate: postMrComment, isPending: mrCommentIsPending } = usePostMrComment(id) + + const send_comment = () => { + const currentContentHTML = editorRef.current?.editor?.getHTML() ?? '

' + 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' + const editorRef = useRef(null) + const onKeyDownScrollHandler = useHandleBottomScrollOffset({ + editor: editorRef.current?.editor + }) + const { dropzone } = useUploadHelpers({ + upload: editorRef.current?.uploadAndAppendAttachments + }) + + const avatars = useAvatars() + + const memberMap = useMemberMap() + + const labels = useLabels() + + const labelMap = useLabelMap() + + const { open, handleAssignees, handleOpenChange, fetchSelected } = useAssigneesSelector({ + assignees: MrDetailData?.data?.assignees ?? [], + assignRequest: (selected) => + mrAssgnees( + { + data: { + link: id, + item_id: Number(item_id), + assignees: selected + } + }, + { + onSuccess: async () => { + editorRef.current?.clearAndBlur() + await refetch({ throwOnError: true }) + }, + onError: apiErrorToast + } + ), + avatars + }) + + return ( +
+
+ + {`${mrDetail?.title || ''}#${id}`} + +
+
+ + Conversation + Checks + Files Changed + +
+
+ {detailIsLoading ? ( +
+ +
+ ) : ( + mrDetail && + )} +
+
+ {mrDetail && mrDetail.status === 'open' && ( + + )} +
+

Add a comment

+ +
+ handleChange(html)} + /> +
+
+ {mrDetail && mrDetail.status === 'open' && ( + + )} + {mrDetail && mrDetail.status === 'closed' && ( + + )} + +
+
+
+ {/* */} +
+ handleAssignees(selected)} + open={open} + // eslint-disable-next-line react-hooks/rules-of-hooks + onOpenChange={(open) => handleOpenChange(open)} + selected={fetchSelected} + > + {(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 +
+
+ {label.name} +
+
+ ) + })} +
+ + ) + }} +
+ + + +
+
+
+ +
Checks
+
+ + {fileChgIsLoading ? ( +
+ +
+ ) : MrFilesChangedData?.data?.content ? ( + + ) : ( +
No files changed
+ )} +
+
+
+
+ ) +} + +MRDetailPage.getProviders = (page, pageProps) => { + return ( + + + + {page} + + + + ) +} + +export default MRDetailPage