diff --git a/moon/apps/web/components/MrView/MRComment.tsx b/moon/apps/web/components/MrView/MRComment.tsx index c8e5acdb6..6761b47c0 100644 --- a/moon/apps/web/components/MrView/MRComment.tsx +++ b/moon/apps/web/components/MrView/MRComment.tsx @@ -4,18 +4,24 @@ import type { MenuProps } from 'antd'; import { NotePlusIcon} from '@gitmono/ui/Icons' import { formatDistance, fromUnixTime } from 'date-fns'; import LexicalContent from './rich-editor/LexicalContent'; +import { useDeleteMrCommentDelete } from '@/hooks/useDeleteMrCommentDelete'; +import { Conversation } from '@/pages/[org]/mr/[id]'; -const Comment = ({ conv, fetchDetail }:any) => { +interface CommentProps { + conv: Conversation + id: string +} + +const Comment = ({ conv, id }: CommentProps) => { + + const { mutate: deleteComment } = useDeleteMrCommentDelete(id) + const handleDelete = () => { + deleteComment(conv.id) + } - const delete_comment = async () => { - await fetch(`/api/mr/comment/${conv.id}/delete`, { - method: 'POST', - }); - }; const handleMenuClick: MenuProps['onClick'] = ({ key }) => { if (key === '3') { - delete_comment() - fetchDetail() + handleDelete() } }; diff --git a/moon/apps/web/components/MrView/index.tsx b/moon/apps/web/components/MrView/index.tsx index 66e431e24..9e5b58b4f 100644 --- a/moon/apps/web/components/MrView/index.tsx +++ b/moon/apps/web/components/MrView/index.tsx @@ -1,12 +1,14 @@ 'use client' -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { List, PaginationProps, Tag, Tabs, TabsProps } from 'antd'; import { formatDistance, fromUnixTime } from 'date-fns'; -// import { MergeOutlined, PullRequestOutlined, CloseCircleOutlined } from '@ant-design/icons'; import { ChevronSelectIcon,AlarmIcon,ClockIcon} from '@gitmono/ui/Icons' import { Link } from '@gitmono/ui/Link' import { Heading } from './catalyst/heading'; +import { usePostMrList } from '@/hooks/usePostMrList'; +import { apiErrorToast } from '@/utils/apiErrorToast' +import { useScope } from '@/contexts/scope' interface MrInfoItem { link: string, @@ -22,63 +24,81 @@ export default function MrView() { const [numTotal, setNumTotal] = useState(0); const [pageSize] = useState(10); const [status, setStatus] = useState('open') + const [page, setPage] = useState(1); + const [isLoading, setIsLoading] = useState(false); + const { scope } = useScope() + const { mutate: fetchMrList } = usePostMrList(); - const fetchData = useCallback(async (page: number, per_page: number) => { - try { - const res = await fetch(`/api/mr/list`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', + const loadMrList = useCallback(() => { + setIsLoading(true); + fetchMrList( + { + data: { + pagination: { + page, + per_page: pageSize }, - body: JSON.stringify({ - pagination: { - page: page, - per_page: per_page - }, - additional: { - status: status - } - }), - }); - const response = await res.json(); - const data = response.data.data; - - setMrList(data.items); - setNumTotal(data.total) - } catch (error) { - // eslint-disable-next-line no-console - console.error('Error fetching data:', error); - } - }, [status]); + additional: { + status + } + } + }, + { + onSuccess: (response) => { + const data = response.data; + + setMrList( + data?.items?.map(item => ({ + ...item, + merge_timestamp: item.merge_timestamp ?? null + })) ?? [] + ); + setNumTotal(data?.total ?? 0); + }, + onError: apiErrorToast, + onSettled: () => setIsLoading(false) + } + ); + }, [page, pageSize, status, fetchMrList]); useEffect(() => { - fetchData(1, pageSize); - }, [pageSize, status, fetchData]); + loadMrList(); + }, [loadMrList]); const getStatusTag = (status: string) => { - switch (status) { + const normalizedStatus = status.toLowerCase(); + + switch (normalizedStatus) { case 'open': return open; case 'merged': return merged; case 'closed': return closed; + default: + return null; } }; const getStatusIcon = (status: string) => { - switch (status) { + const normalizedStatus = status.toLowerCase(); + + switch (normalizedStatus) { case 'open': return ; case 'closed': return ; case 'merged': return ; + default: + return null; } }; const getDescription = (item: MrInfoItem) => { - switch (item.status) { + const normalizedStatus = item.status.toLowerCase(); + + switch (normalizedStatus) { case 'open': return `MergeRequest opened by Admin ${formatDistance(fromUnixTime(item.open_timestamp), new Date(), { addSuffix: true })} `; case 'merged': @@ -88,12 +108,14 @@ export default function MrView() { return ""; } case 'closed': - return (`MR ${item.link} closed by Admin ${formatDistance(fromUnixTime(item.updated_at), new Date(), { addSuffix: true })}`) + return (`MR ${item.link} closed by Admin ${formatDistance(fromUnixTime(item.updated_at), new Date(), { addSuffix: true })}`); + default: + return null; } } - const onChange: PaginationProps['onChange'] = (current, pageSize) => { - fetchData(current, pageSize); + const onChange: PaginationProps['onChange'] = (current) => { + setPage(current); }; const tabsChange = (activeKey: string) => { @@ -104,7 +126,7 @@ export default function MrView() { } } - const tab_items: TabsProps['items'] = [ + const tab_items = useMemo(() => [ { key: '1', label: 'Open', @@ -113,9 +135,7 @@ export default function MrView() { key: '2', label: 'Closed', } - ]; - - + ], []) as TabsProps['items']; return (
@@ -127,13 +147,23 @@ export default function MrView() { className="w-full mt-2" pagination={{ align: "center", pageSize: pageSize, total: numTotal, onChange: onChange }} dataSource={mrList} + loading={isLoading} renderItem={(item) => ( {`${item.title}`} {getStatusTag(item.status)}} + title={ + + {`${item.title}`} {getStatusTag(item.status)} + + } description={getDescription(item)} /> diff --git a/moon/apps/web/hooks/useDeleteMrCommentDelete.ts b/moon/apps/web/hooks/useDeleteMrCommentDelete.ts new file mode 100644 index 000000000..6642db5e4 --- /dev/null +++ b/moon/apps/web/hooks/useDeleteMrCommentDelete.ts @@ -0,0 +1,18 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { legacyApiClient } from '@/utils/queryClient' +import type { DeleteApiMrCommentDeleteData, RequestParams } from '@gitmono/types' + +export function useDeleteMrCommentDelete(id: string, params?: RequestParams) { + const queryClient = useQueryClient() + + return useMutation({ + mutationKey: legacyApiClient.v1.deleteApiMrCommentDelete().baseKey, + mutationFn: (convId) => + legacyApiClient.v1.deleteApiMrCommentDelete().request(convId, params), + onSuccess: (_data, _convId) => { + queryClient.invalidateQueries({ + queryKey: legacyApiClient.v1.getApiMrDetail().requestKey(id), + }) + }, + }) +} \ No newline at end of file diff --git a/moon/apps/web/hooks/useGetMrDetail.ts b/moon/apps/web/hooks/useGetMrDetail.ts new file mode 100644 index 000000000..87d438e6c --- /dev/null +++ b/moon/apps/web/hooks/useGetMrDetail.ts @@ -0,0 +1,11 @@ +import { useQuery } from '@tanstack/react-query' +import { legacyApiClient } from '@/utils/queryClient' +import type { RequestParams } from '@gitmono/types' + +export function useGetMrDetail(id: string, params?: RequestParams) { + return useQuery({ + queryKey: legacyApiClient.v1.getApiMrDetail().requestKey(id), + queryFn: () => legacyApiClient.v1.getApiMrDetail().request(id, params), + enabled: !!id, + }) +} \ No newline at end of file diff --git a/moon/apps/web/hooks/useGetMrFilesChanged.ts b/moon/apps/web/hooks/useGetMrFilesChanged.ts new file mode 100644 index 000000000..d860f50f4 --- /dev/null +++ b/moon/apps/web/hooks/useGetMrFilesChanged.ts @@ -0,0 +1,12 @@ +import { useQuery } from '@tanstack/react-query' +import { legacyApiClient } from '@/utils/queryClient' +import type { RequestParams } from '@gitmono/types' +import type { GetApiMrFilesChangedData } from '@gitmono/types/generated' + +export function useGetMrFilesChanged(id: string, params?: RequestParams) { + return useQuery({ + queryKey: legacyApiClient.v1.getApiMrFilesChanged().requestKey(id), + queryFn: () => legacyApiClient.v1.getApiMrFilesChanged().request(id, params), + enabled: !!id, + }) +} \ No newline at end of file diff --git a/moon/apps/web/hooks/usePostMrClose.ts b/moon/apps/web/hooks/usePostMrClose.ts new file mode 100644 index 000000000..993bb5fe6 --- /dev/null +++ b/moon/apps/web/hooks/usePostMrClose.ts @@ -0,0 +1,17 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { legacyApiClient } from '@/utils/queryClient' +import type { PostApiMrCloseData, RequestParams } from '@gitmono/types' + +export function usePostMrClose(link: string, params?: RequestParams) { + const queryClient = useQueryClient() + + return useMutation({ + mutationKey: legacyApiClient.v1.postApiMrClose().requestKey(link), + mutationFn: () => legacyApiClient.v1.postApiMrClose().request(link, params), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: legacyApiClient.v1.getApiMrDetail().requestKey(link), + }) + }, + }) +} \ No newline at end of file diff --git a/moon/apps/web/hooks/usePostMrComment.ts b/moon/apps/web/hooks/usePostMrComment.ts new file mode 100644 index 000000000..55d56727c --- /dev/null +++ b/moon/apps/web/hooks/usePostMrComment.ts @@ -0,0 +1,18 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { legacyApiClient } from '@/utils/queryClient' +import type { SaveCommentRequest, PostApiMrCommentData, RequestParams } from '@gitmono/types' + +export function usePostMrComment(link: string, params?: RequestParams) { + const queryClient = useQueryClient() + + return useMutation({ + mutationKey: legacyApiClient.v1.postApiMrComment().requestKey(link), + mutationFn: (data) => + legacyApiClient.v1.postApiMrComment().request(link, data, params), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: legacyApiClient.v1.getApiMrDetail().requestKey(link), + }) + }, + }) +} \ No newline at end of file diff --git a/moon/apps/web/hooks/usePostMrList.ts b/moon/apps/web/hooks/usePostMrList.ts new file mode 100644 index 000000000..2fbe1d878 --- /dev/null +++ b/moon/apps/web/hooks/usePostMrList.ts @@ -0,0 +1,11 @@ +// hooks/usePostApiMrList.ts +import { useMutation } from '@tanstack/react-query' +import { legacyApiClient } from '@/utils/queryClient' +import { PageParamsMRStatusParams, RequestParams, PostApiMrListData } from '@gitmono/types' + +export function usePostMrList() { + return useMutation({ + mutationFn: ({ data, params }) => + legacyApiClient.v1.postApiMrList().request(data, params), + }) +} \ No newline at end of file diff --git a/moon/apps/web/hooks/usePostMrMerge.ts b/moon/apps/web/hooks/usePostMrMerge.ts new file mode 100644 index 000000000..1aed3b2b7 --- /dev/null +++ b/moon/apps/web/hooks/usePostMrMerge.ts @@ -0,0 +1,19 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { legacyApiClient } from '@/utils/queryClient' +import type { PostApiMrMergeData, RequestParams } from '@gitmono/types' + +export function usePostMrMerge(link: string, params?: RequestParams) { + const queryClient = useQueryClient() + + return useMutation({ + mutationKey: legacyApiClient.v1.postApiMrMerge().requestKey(link), + mutationFn: () => + legacyApiClient.v1.postApiMrMerge().request(link, params), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: legacyApiClient.v1.getApiMrDetail().requestKey(link), + }) + }, + }) +} + diff --git a/moon/apps/web/hooks/usePostMrReopen.ts b/moon/apps/web/hooks/usePostMrReopen.ts new file mode 100644 index 000000000..a8bf1a1e8 --- /dev/null +++ b/moon/apps/web/hooks/usePostMrReopen.ts @@ -0,0 +1,17 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { legacyApiClient } from '@/utils/queryClient' +import type { PostApiMrReopenData, RequestParams } from '@gitmono/types' + +export function usePostMrReopen(link: string, params?: RequestParams) { + const queryClient = useQueryClient() + + return useMutation({ + mutationKey: legacyApiClient.v1.postApiMrReopen().requestKey(link), + mutationFn: () => legacyApiClient.v1.postApiMrReopen().request(link, params), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: legacyApiClient.v1.getApiMrDetail().requestKey(link), + }) + }, + }) +} \ No newline at end of file diff --git a/moon/apps/web/pages/[org]/mr/[id].tsx b/moon/apps/web/pages/[org]/mr/[id].tsx index 751fb476a..2059aa385 100644 --- a/moon/apps/web/pages/[org]/mr/[id].tsx +++ b/moon/apps/web/pages/[org]/mr/[id].tsx @@ -1,13 +1,13 @@ 'use client' -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { Card, Tabs, TabsProps,Timeline,} from 'antd'; // import { CommentOutlined, MergeOutlined, CloseCircleOutlined, PullRequestOutlined } from '@ant-design/icons'; import { ChevronRightCircleIcon, ChevronSelectIcon,AlarmIcon,ClockIcon} from '@gitmono/ui/Icons' import { formatDistance, fromUnixTime } from 'date-fns'; import RichEditor from '@/components/MrView/rich-editor/RichEditor'; import MRComment from '@/components/MrView/MRComment'; -import { useParams, useRouter } from 'next/navigation'; +import { useRouter } from 'next/router'; import * as Diff2Html from 'diff2html'; import 'diff2html/bundles/css/diff2html.min.css'; import FilesChanged from '@/components/MrView/files-changed'; @@ -18,13 +18,19 @@ import { cn } from '@gitmono/ui/utils'; import AuthAppProviders from '@/components/Providers/AuthAppProviders'; import { AppLayout } from '@/components/Layout/AppLayout'; import { PageWithLayout } from '@/utils/types'; +import { useGetMrDetail } from '@/hooks/useGetMrDetail' +import { useGetMrFilesChanged } from '@/hooks/useGetMrFilesChanged'; +import { usePostMrComment } from '@/hooks/usePostMrComment' +import { usePostMrMerge } from '@/hooks/usePostMrMerge' +import { usePostMrReopen } from '@/hooks/usePostMrReopen'; +import { usePostMrClose } from '@/hooks/usePostMrClose'; interface MRDetail { status: string, conversations: Conversation[], title: string, } -interface Conversation { +export interface Conversation { id: number, user_id: number, conv_type: string, @@ -33,143 +39,71 @@ interface Conversation { } const MRDetailPage:PageWithLayout = () =>{ - // const { id } = React.use(params) const router = useRouter(); - const params = useParams(); - - const id = params?.id as string; + const { id : tempId, title } = router.query; const [editorState, setEditorState] = useState(""); - const [login, setLogin] = useState(false); - const [mrDetail, setMrDetail] = useState( - { - status: "", - conversations: [], - title: "", - } - ); - const [filedata, setFileData] = useState([]); - const [loadings, setLoadings] = useState([]); + const [login, _setLogin] = useState(false); const [outputHtml, setOutputHtml] = useState(''); - const checkLogin = async () => { - const res = await fetch(`/api/auth`); - - setLogin(res.ok); - }; - - const fetchDetail = useCallback(async () => { - const detail = await fetch(`/api/mr/${id}/detail`); - const detail_json = await detail.json(); - - setMrDetail(detail_json.data.data); - }, [id]); - - const fetchFileList = useCallback(async () => { - set_to_loading(2) - try { - const res = await fetch(`/api/mr/${id}/files`); - const result = await res.json(); - - setFileData(result.data.data); - } finally { - cancel_loading(2) - } - }, [id]); - - const get_diff_content = useCallback(async () => { - const detail = await fetch(`/api/mr/${id}/files-changed`); - const res = await detail.json(); - const diff = Diff2Html.html(res.data.data.content, { drawFileList: true, matching: 'lines' }) - - setOutputHtml(diff); - }, [id]) + const id = typeof tempId === 'string' ? tempId : ''; + const { data: MrDetailData } = useGetMrDetail(id) + const mrDetail = MrDetailData?.data as MRDetail | undefined - useEffect(() => { - fetchDetail() - fetchFileList(); - checkLogin(); - - }, [id, fetchDetail, fetchFileList]); - useEffect(()=>{ - // eslint-disable-next-line no-console - console.log(filedata); - },[filedata]) - - const set_to_loading = (index: number) => { - setLoadings((prevLoadings) => { - const newLoadings = [...prevLoadings]; - - newLoadings[index] = true; - return newLoadings; - }); + if (mrDetail && typeof mrDetail.status === 'string') { + mrDetail.status = mrDetail.status.toLowerCase(); } - const cancel_loading = (index: number) => { - setLoadings((prevLoadings) => { - const newLoadings = [...prevLoadings]; + const { data: MrFilesChangedData} = useGetMrFilesChanged(id) + const get_diff_content = useCallback(() => { + const content = MrFilesChangedData?.data?.content; - newLoadings[index] = false; - return newLoadings; + if (typeof content !== 'string') return; + const diff = Diff2Html.html(content, { + drawFileList: true, + matching: 'lines', }); + + setOutputHtml(diff); + }, [MrFilesChangedData]); + + const { mutate: approveMr, isPending : mrMergeIsPending } = usePostMrMerge(id) + const handleMrApprove = () => { + approveMr(undefined, { + onSuccess: () => { + router.push("/mr") + }, + }) } - async function approve_mr() { - set_to_loading(1); - const res = await fetch(`/api/mr/${id}/merge`, { - method: 'POST', - }); - - if (res) { - cancel_loading(1); - router.push( - "/mr" - ); - } - }; - - async function close_mr() { - set_to_loading(3); - const res = await fetch(`/api/mr/${id}/close`, { - method: 'POST', - }); - - if (res) { - cancel_loading(3); - router.push( - "/mr" - ); - } - }; - - async function reopen_mr() { - set_to_loading(3); - const res = await fetch(`/api/mr/${id}/reopen`, { - method: 'POST', - }); - - if (res) { - cancel_loading(3); - router.push( - "/mr" - ); - } - }; - + const { mutate: closeMr, isPending: mrCloseIsPending } = usePostMrClose(id) + const handleMrClose = () => { + closeMr(undefined, { + onSuccess: () => { + router.push("/mr") + }, + }) + } - async function save_comment(comment:any) { - set_to_loading(3); - const res = await fetch(`/api/mr/${id}/comment`, { - method: 'POST', - body: comment, - }); + const { mutate: reopenMr, isPending: mrReopenIsPending } = usePostMrReopen(id) + const handleMrReopen = () => { + reopenMr(undefined,{ + onSuccess: () => { + router.push("/mr") + }, + }) + } - if (res) { - setEditorState(""); - fetchDetail(); - cancel_loading(3); - } + const { mutate: postMrComment, isPending : mrCommentIsPending } = usePostMrComment(id) + const save_comment = () => { + postMrComment( + { content: editorState }, + { + onSuccess: () => { + setEditorState(""); + }, + }); } let conv_items = mrDetail?.conversations.map(conv => { @@ -177,7 +111,7 @@ const MRDetailPage:PageWithLayout = () =>{ let children; switch (conv.conv_type) { - case "Comment": icon = ; children = ; break + case "Comment": icon = ; children = ; break case "Merged": icon = ; children = "Merged via the queue into main " + formatDistance(fromUnixTime(conv.created_at), new Date(), { addSuffix: true }); break; case "Closed": icon = ; children = conv.comment; break; case "Reopen": icon = ; children = conv.comment; break; @@ -193,8 +127,6 @@ const MRDetailPage:PageWithLayout = () =>{ }); const onTabsChange = (key: string) => { - // eslint-disable-next-line no-console - console.log(key); if (key === '2') { get_diff_content() } @@ -214,33 +146,33 @@ const MRDetailPage:PageWithLayout = () =>{
{mrDetail && mrDetail.status === "open" && } {mrDetail && mrDetail.status === "closed" && }
@@ -254,15 +186,15 @@ const MRDetailPage:PageWithLayout = () =>{ ]; return ( - + {mrDetail && mrDetail.status === "open" && }