([])
+ const router = useRouter()
+ const { mutate: submitNewIssue } = usePostIssueSubmit()
+
+ const set_to_loading = (index: number) => {
+ setLoadings((prevLoadings) => {
+ const newLoadings = [...prevLoadings]
+
+ newLoadings[index] = true
+ return newLoadings
+ })
+ }
+
+ const cancel_loading = (index: number) => {
+ setLoadings((prevLoadings) => {
+ const newLoadings = [...prevLoadings]
+
+ newLoadings[index] = false
+ return newLoadings
+ })
+ }
+
+ const submit = useCallback(
+ (description: string) => {
+ if (JSON.parse(description).root.children[0].children.length === 0 || !title) {
+ toast.error('please fill the issue list first!')
+ return
+ }
+ set_to_loading(3)
+ submitNewIssue(
+ { data: { title, description } },
+ {
+ onSuccess: (_response) => {
+ setEditorState('')
+ cancel_loading(3)
+ toast.success('success')
+ router.push(`/${router.query.org}/issue`)
+ },
+ onError: () => apiErrorToast
+ }
+ )
+ },
+ [router, title, submitNewIssue]
+ )
+
+ return (
+ <>
+
+
+
+ Add a title
+ setTitle(e.target.value)}
+ >
+
+
+
+ Add a description
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/moon/apps/web/components/Issues/IssuePage.tsx b/moon/apps/web/components/Issues/IssuePage.tsx
new file mode 100644
index 000000000..eb96871d4
--- /dev/null
+++ b/moon/apps/web/components/Issues/IssuePage.tsx
@@ -0,0 +1,151 @@
+'use client'
+
+import React, { useCallback, useEffect, useState } from 'react'
+// import { CheckCircleOutlined, ExclamationCircleOutlined } from '@ant-design/icons'
+import { Link } from '@gitmono/ui'
+import { Button, Flex, List, PaginationProps, Tabs, TabsProps, Tag } from 'antd'
+import { formatDistance, fromUnixTime } from 'date-fns'
+import { useRouter } from 'next/router'
+
+import { Heading } from '@/components/Catalyst/Heading'
+import { useGetIssueLists } from '@/hooks/issues/useGetIssueLists'
+import { apiErrorToast } from '@/utils/apiErrorToast'
+
+interface Item {
+ closed_at?: number | null
+ link: string
+ owner: number
+ title: string
+ status: string
+ open_timestamp: number
+ updated_at: number
+}
+
+export default function IssuePage() {
+ const router = useRouter()
+ const [itemList, setItemList] = useState- ([])
+ const [numTotal, setNumTotal] = useState(0)
+ const [pageSize, _setPageSize] = useState(10)
+ const [loading, setLoading] = useState(false)
+ const [status, setStatus] = useState('open')
+ const { mutate: issueLists } = useGetIssueLists()
+
+ const fetchData = useCallback(
+ (page: number, per_page: number) => {
+ setLoading(true)
+
+ issueLists(
+ {
+ data: { pagination: { page, per_page }, additional: { status } }
+ },
+ {
+ onSuccess: (response) => {
+ const data = response.data
+
+ setItemList(data?.items ?? [])
+ setNumTotal(data?.total ?? 0)
+ },
+ onError: apiErrorToast,
+ onSettled: () => setLoading(false)
+ }
+ )
+ },
+
+ [status, issueLists]
+ )
+
+ useEffect(() => {
+ fetchData(1, pageSize)
+ }, [pageSize, fetchData])
+
+ const getStatusTag = (status: string) => {
+ switch (status) {
+ case 'open':
+ return open
+ case 'closed':
+ return closed
+ }
+ }
+
+ // const getStatusIcon = (status: string) => {
+ // switch (status) {
+ // case 'open':
+ // // return
+ // case 'closed':
+ // // return
+ // }
+ // }
+
+ const getDescription = (item: Item) => {
+ switch (item.status) {
+ case 'open':
+ return `Issue opened by Admin ${formatDistance(fromUnixTime(item.open_timestamp), new Date(), { addSuffix: true })} `
+ case 'closed':
+ return `Issue ${item.link} closed by Admin ${formatDistance(fromUnixTime(item.updated_at), new Date(), { addSuffix: true })}`
+ }
+ }
+
+ const onChange: PaginationProps['onChange'] = (current, pageSize) => {
+ fetchData(current, pageSize)
+ }
+
+ const tabsChange = (activeKey: string) => {
+ if (activeKey === '1') {
+ setStatus('open')
+ } else {
+ setStatus('closed')
+ }
+ }
+
+ const tab_items: TabsProps['items'] = [
+ {
+ key: '1',
+ label: 'Open'
+ },
+ {
+ key: '2',
+ label: 'Closed'
+ }
+ ]
+
+ return (
+ <>
+
+ Issues
+
+
+
+
+
+
+ (
+
+
+ // getStatusIcon(item.status)
+ // }
+ title={
+
+ {item.title} {getStatusTag(item.status)}
+
+ }
+ description={getDescription(item)}
+ />
+
+ )}
+ />
+
+ >
+ )
+}
diff --git a/moon/apps/web/components/MrView/MRComment.tsx b/moon/apps/web/components/MrView/MRComment.tsx
index 6761b47c0..9aece062b 100644
--- a/moon/apps/web/components/MrView/MRComment.tsx
+++ b/moon/apps/web/components/MrView/MRComment.tsx
@@ -1,67 +1,84 @@
-import { Card, Dropdown } from 'antd/lib';
-import type { MenuProps } from 'antd';
// import { MoreOutlined } from '@ant-design/icons';
-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]';
+import { NotePlusIcon } from '@gitmono/ui/Icons'
+import type { MenuProps } from 'antd'
+import { Card, Dropdown } from 'antd/lib'
+import { formatDistance, fromUnixTime } from 'date-fns'
+
+import { useDeleteIssueComment } from '@/hooks/issues/useDeleteIssueComment'
+import { useDeleteMrCommentDelete } from '@/hooks/useDeleteMrCommentDelete'
+import { Conversation } from '@/pages/[org]/mr/[id]'
+
+import LexicalContent from './rich-editor/LexicalContent'
interface CommentProps {
- conv: Conversation
+ conv: Conversation
id: string
+ whoamI: string
}
-const Comment = ({ conv, id }: CommentProps) => {
-
+const Comment = ({ conv, id, whoamI }: CommentProps) => {
const { mutate: deleteComment } = useDeleteMrCommentDelete(id)
+ const { mutate: deleteIssueComment } = useDeleteIssueComment(id)
const handleDelete = () => {
- deleteComment(conv.id)
+ switch (whoamI) {
+ case 'issue':
+ deleteIssueComment(conv.id)
+ break
+ case 'mr':
+ deleteComment(conv.id)
+ break
+ default:
+ return
+ }
}
- const handleMenuClick: MenuProps['onClick'] = ({ key }) => {
- if (key === '3') {
- handleDelete()
- }
- };
-
- const items: MenuProps['items'] = [
- {
- label: 'Edit',
- key: '1',
- disabled: true
- },
- {
- label: 'Hide',
- key: '2',
- disabled: true
- },
- {
- type: 'divider',
- },
- {
- label: 'Delete',
- key: '3',
- danger: true,
- }
- ];
- const menuProps = {
- items,
- onClick: handleMenuClick,
- };
+ const handleMenuClick: MenuProps['onClick'] = ({ key }) => {
+ if (key === '3') {
+ handleDelete()
+ }
+ }
- const time = formatDistance(fromUnixTime(conv.created_at), new Date(), { addSuffix: true });
+ const items: MenuProps['items'] = [
+ {
+ label: 'Edit',
+ key: '1',
+ disabled: true
+ },
+ {
+ label: 'Hide',
+ key: '2',
+ disabled: true
+ },
+ {
+ type: 'divider'
+ },
+ {
+ label: 'Delete',
+ key: '3',
+ danger: true
+ }
+ ]
+ const menuProps = {
+ items,
+ onClick: handleMenuClick
+ }
- return (
-
-
-
- }>
-
-
- )
+ const time = formatDistance(fromUnixTime(conv.created_at), new Date(), { addSuffix: true })
+ return (
+
+
+
+ }
+ >
+
+
+ )
}
-export default Comment;
\ No newline at end of file
+export default Comment
diff --git a/moon/apps/web/components/Sidebar/SidebarIssue.tsx b/moon/apps/web/components/Sidebar/SidebarIssue.tsx
new file mode 100644
index 000000000..a7db85a04
--- /dev/null
+++ b/moon/apps/web/components/Sidebar/SidebarIssue.tsx
@@ -0,0 +1,22 @@
+import { ChatBubblePlusIcon } from '@gitmono/ui/Icons'
+import router from 'next/router'
+
+import { useScope } from '@/contexts/scope'
+
+import { SidebarLink } from './SidebarLink'
+
+export function SidebarIssue() {
+ const { scope } = useScope()
+
+ return (
+ <>
+ }
+ />
+ >
+ )
+}
diff --git a/moon/apps/web/components/Sidebar/SidebarOrgSwitcher.tsx b/moon/apps/web/components/Sidebar/SidebarOrgSwitcher.tsx
index 9cec56853..0afcfddd1 100644
--- a/moon/apps/web/components/Sidebar/SidebarOrgSwitcher.tsx
+++ b/moon/apps/web/components/Sidebar/SidebarOrgSwitcher.tsx
@@ -1,13 +1,12 @@
import { useRef, useState } from 'react'
-import { Reorder } from 'framer-motion'
-import { useAtomValue } from 'jotai'
-import { useRouter } from 'next/router'
-import { isMacOs } from 'react-device-detect'
-
import { PublicOrganization } from '@gitmono/types'
import { Avatar, LayeredHotkeys, Link, PlusIcon, Tooltip } from '@gitmono/ui'
import { useIsDesktopApp } from '@gitmono/ui/src/hooks'
import { cn } from '@gitmono/ui/src/utils'
+import { Reorder } from 'framer-motion'
+import { useAtomValue } from 'jotai'
+import { useRouter } from 'next/router'
+import { isMacOs } from 'react-device-detect'
import { OrganizationSwitchHoverCard } from '@/components/InboxItems/OrganizationSwitchHoverCard'
import { sidebarCollapsedAtom } from '@/components/Layout/AppLayout'
@@ -48,28 +47,30 @@ export function SidebarOrgSwitcher() {
onReorder={onReorder}
className='flex flex-col gap-3'
>
- {memberships?.map(({ id, organization }, index) => (
- setDraggingId(id)}
- onDragEnd={() => {
- setDraggingId(undefined)
- reorder.mutate(organizationMembershipIds)
- }}
- className={cn('group/reorder-item relative', {
- 'opacity-60': draggingId === id,
- 'pointer-events-none': !!draggingId
- })}
- >
-
-
- ))}
+ {memberships
+ ?.filter((m) => m.organization !== null)
+ ?.map(({ id, organization }, index) => (
+ setDraggingId(id)}
+ onDragEnd={() => {
+ setDraggingId(undefined)
+ reorder.mutate(organizationMembershipIds)
+ }}
+ className={cn('group/reorder-item relative', {
+ 'opacity-60': draggingId === id,
+ 'pointer-events-none': !!draggingId
+ })}
+ >
+
+
+ ))}
+
diff --git a/moon/apps/web/hooks/issues/useDeleteIssueComment.ts b/moon/apps/web/hooks/issues/useDeleteIssueComment.ts
new file mode 100644
index 000000000..f328ce383
--- /dev/null
+++ b/moon/apps/web/hooks/issues/useDeleteIssueComment.ts
@@ -0,0 +1,18 @@
+import { DeleteApiIssueCommentDeleteData, RequestParams } from '@gitmono/types/generated'
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+
+import { legacyApiClient } from '@/utils/queryClient'
+
+export function useDeleteIssueComment(id: string, params?: RequestParams) {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationKey: legacyApiClient.v1.deleteApiIssueCommentDelete().baseKey,
+ mutationFn: (convId) => legacyApiClient.v1.deleteApiIssueCommentDelete().request(convId, params),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: legacyApiClient.v1.getApiIssueDetail().requestKey(id)
+ })
+ }
+ })
+}
diff --git a/moon/apps/web/hooks/issues/useGetIssueDetail.ts b/moon/apps/web/hooks/issues/useGetIssueDetail.ts
new file mode 100644
index 000000000..4b06108e0
--- /dev/null
+++ b/moon/apps/web/hooks/issues/useGetIssueDetail.ts
@@ -0,0 +1,59 @@
+import { useQuery } from '@tanstack/react-query'
+
+import { legacyApiClient } from '@/utils/queryClient'
+
+interface conversations {
+ id: number
+ user_id: number
+ conv_type: string
+ comment: string
+ created_at: number
+ updated_at: number
+}
+
+interface raw {
+ id: number
+ link: string
+ title: string
+ status: string
+ open_timestamp: number
+ conversations: conversations[]
+}
+
+interface issueDetail {
+ status: string
+ conversations: { id: number; user_id: number; conv_type: string; comment: string; created_at: number }[]
+ 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: ['issueDetail', 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
+ }
+
+ return {
+ err_message,
+ data: converted,
+ req_result
+ }
+ },
+
+ enabled: !!id
+ })
+}
diff --git a/moon/apps/web/hooks/issues/useGetIssueLists.ts b/moon/apps/web/hooks/issues/useGetIssueLists.ts
new file mode 100644
index 000000000..dfe3e2f9b
--- /dev/null
+++ b/moon/apps/web/hooks/issues/useGetIssueLists.ts
@@ -0,0 +1,10 @@
+import { PageParamsStatusParams, PostApiIssueListData, RequestParams } from '@gitmono/types/generated'
+import { useMutation } from '@tanstack/react-query'
+
+import { legacyApiClient } from '@/utils/queryClient'
+
+export function useGetIssueLists() {
+ return useMutation({
+ mutationFn: ({ data, params }) => legacyApiClient.v1.postApiIssueList().request(data, params)
+ })
+}
diff --git a/moon/apps/web/hooks/issues/usePostIssueClose.ts b/moon/apps/web/hooks/issues/usePostIssueClose.ts
new file mode 100644
index 000000000..4f57d83a6
--- /dev/null
+++ b/moon/apps/web/hooks/issues/usePostIssueClose.ts
@@ -0,0 +1,10 @@
+import { PostApiIssueCloseData, RequestParams } from '@gitmono/types/generated'
+import { useMutation } from '@tanstack/react-query'
+
+import { legacyApiClient } from '@/utils/queryClient'
+
+export function usePostIssueClose() {
+ return useMutation({
+ mutationFn: ({ link, params }) => legacyApiClient.v1.postApiIssueClose().request(link, params)
+ })
+}
diff --git a/moon/apps/web/hooks/issues/usePostIssueComment.ts b/moon/apps/web/hooks/issues/usePostIssueComment.ts
new file mode 100644
index 000000000..eb5d43090
--- /dev/null
+++ b/moon/apps/web/hooks/issues/usePostIssueComment.ts
@@ -0,0 +1,14 @@
+import { PostApiIssueCommentData, RequestParams, SaveCommentRequest } from '@gitmono/types/generated'
+import { useMutation } from '@tanstack/react-query'
+
+import { legacyApiClient } from '@/utils/queryClient'
+
+export function usePostIssueComment() {
+ return useMutation<
+ PostApiIssueCommentData,
+ Error,
+ { link: string; data: SaveCommentRequest; params?: RequestParams }
+ >({
+ mutationFn: ({ link, data, params }) => legacyApiClient.v1.postApiIssueComment().request(link, data, params)
+ })
+}
diff --git a/moon/apps/web/hooks/issues/usePostIssueReopen.ts b/moon/apps/web/hooks/issues/usePostIssueReopen.ts
new file mode 100644
index 000000000..0cbc9f95a
--- /dev/null
+++ b/moon/apps/web/hooks/issues/usePostIssueReopen.ts
@@ -0,0 +1,10 @@
+import { PostApiIssueReopenData, RequestParams } from '@gitmono/types/generated'
+import { useMutation } from '@tanstack/react-query'
+
+import { legacyApiClient } from '@/utils/queryClient'
+
+export function usePostIssueReopen() {
+ return useMutation({
+ mutationFn: ({ link, params }) => legacyApiClient.v1.postApiIssueReopen().request(link, params)
+ })
+}
diff --git a/moon/apps/web/hooks/issues/usePostIssueSubmit.ts b/moon/apps/web/hooks/issues/usePostIssueSubmit.ts
new file mode 100644
index 000000000..2f0af6c02
--- /dev/null
+++ b/moon/apps/web/hooks/issues/usePostIssueSubmit.ts
@@ -0,0 +1,10 @@
+import { NewIssue, PostApiIssueNewData } from '@gitmono/types/generated'
+import { useMutation } from '@tanstack/react-query'
+
+import { legacyApiClient } from '@/utils/queryClient'
+
+export function usePostIssueSubmit() {
+ return useMutation({
+ mutationFn: ({ data }) => legacyApiClient.v1.postApiIssueNew().request(data)
+ })
+}
diff --git a/moon/apps/web/next.config.js b/moon/apps/web/next.config.js
index 11395c52f..9be480de0 100644
--- a/moon/apps/web/next.config.js
+++ b/moon/apps/web/next.config.js
@@ -101,6 +101,7 @@ const ContentSecurityPolicy = Object.keys(cspResourcesByDirective).reduce((prevP
/** @type {import('next').NextConfig} */
const moduleExports = {
+ output: 'standalone',
experimental: {
// https://nextjs.org/docs/messages/import-esm-externals
esmExternals: 'loose',
diff --git a/moon/apps/web/pages/[org]/issue/[id]/index.tsx b/moon/apps/web/pages/[org]/issue/[id]/index.tsx
new file mode 100644
index 000000000..50152a831
--- /dev/null
+++ b/moon/apps/web/pages/[org]/issue/[id]/index.tsx
@@ -0,0 +1,40 @@
+import { GetServerSideProps } from 'next'
+
+import IssueDetailPage from '@/components/Issues/IssueDetailPage'
+import { AppLayout } from '@/components/Layout/AppLayout'
+import { AuthAppProviders } from '@/components/Providers/AuthAppProviders'
+import { PageWithLayout } from '@/utils/types'
+
+export const getServerSideProps: GetServerSideProps = async ({ query }) => {
+ if (!query.id) {
+ return {
+ redirect: {
+ destination: `/${query.org}/issue`,
+ permanent: false
+ }
+ }
+ }
+ return {
+ props: {
+ id: query.id
+ }
+ }
+}
+
+const OrganizationIssueDetailPage: PageWithLayout = ({ id }) => {
+ return (
+ <>
+
+ >
+ )
+}
+
+OrganizationIssueDetailPage.getProviders = (page, pageProps) => {
+ return (
+
+ {page}
+
+ )
+}
+
+export default OrganizationIssueDetailPage
diff --git a/moon/apps/web/pages/[org]/issue/index.tsx b/moon/apps/web/pages/[org]/issue/index.tsx
new file mode 100644
index 000000000..5b93ac2e1
--- /dev/null
+++ b/moon/apps/web/pages/[org]/issue/index.tsx
@@ -0,0 +1,22 @@
+import IssuePage from '@/components/Issues/IssuePage'
+import { AppLayout } from '@/components/Layout/AppLayout'
+import { AuthAppProviders } from '@/components/Providers/AuthAppProviders'
+import { PageWithLayout } from '@/utils/types'
+
+const OrganizationIssuePage: PageWithLayout = () => {
+ return (
+ <>
+
+ >
+ )
+}
+
+OrganizationIssuePage.getProviders = (page, pageProps) => {
+ return (
+
+ {page}
+
+ )
+}
+
+export default OrganizationIssuePage
diff --git a/moon/apps/web/pages/[org]/issue/new/index.tsx b/moon/apps/web/pages/[org]/issue/new/index.tsx
new file mode 100644
index 000000000..6b4c53305
--- /dev/null
+++ b/moon/apps/web/pages/[org]/issue/new/index.tsx
@@ -0,0 +1,22 @@
+import IssueNewPage from '@/components/Issues/IssueNewPage'
+import { AppLayout } from '@/components/Layout/AppLayout'
+import { AuthAppProviders } from '@/components/Providers/AuthAppProviders'
+import { PageWithLayout } from '@/utils/types'
+
+const OrganizationIssueNewPage: PageWithLayout = () => {
+ return (
+ <>
+
+ >
+ )
+}
+
+OrganizationIssueNewPage.getProviders = (page, pageProps) => {
+ return (
+
+ {page}
+
+ )
+}
+
+export default OrganizationIssueNewPage
diff --git a/moon/apps/web/pages/[org]/mr/[id].tsx b/moon/apps/web/pages/[org]/mr/[id].tsx
index 2059aa385..5bdfbf703 100644
--- a/moon/apps/web/pages/[org]/mr/[id].tsx
+++ b/moon/apps/web/pages/[org]/mr/[id].tsx
@@ -111,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;