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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .github/workflows/web-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,6 @@ jobs:
working-directory: ./moon
run: pnpm run lint

- name: Build Next.js application
working-directory: ./moon
run: pnpm run build

- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
Expand Down
25 changes: 20 additions & 5 deletions moon/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
FROM node:20-alpine AS runner
# 构建阶段
FROM node:20-alpine AS builder

WORKDIR /app

ENV PORT=3000

# 安装 pnpm(保持一致版本)
RUN corepack enable && corepack prepare pnpm@9.7.1 --activate

# 拷贝构建产物和依赖
# 安装依赖
COPY . .
RUN pnpm install --frozen-lockfile

# 构建:递归构建nextjs中的所有依赖
RUN pnpm run build

# 运行阶段
FROM node:20-alpine AS runner
WORKDIR /app
ENV PORT=3000

# 启用 pnpm
RUN corepack enable && corepack prepare pnpm@9.7.1 --activate

COPY --from=builder /app/apps/web/.next/standalone ./
COPY --from=builder /app/apps/web/.next/static ./.next/static
COPY --from=builder /app/apps/web/public ./public

# 启动 SSR 服务
WORKDIR /app/apps/web
CMD ["pnpm", "start"]
CMD ["node", "server.js"]

27 changes: 27 additions & 0 deletions moon/apps/web/components/Catalyst/Heading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import clsx from 'clsx'

type HeadingProps = { level?: 1 | 2 | 3 | 4 | 5 | 6 } & React.ComponentPropsWithoutRef<
'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
>

export function Heading({ className, level = 1, ...props }: HeadingProps) {
let Element: `h${typeof level}` = `h${level}`

return (
<Element
{...props}
className={clsx(className, 'text-2xl/8 font-semibold text-zinc-950 sm:text-xl/8 dark:text-white')}
/>
)
}

export function Subheading({ className, level = 2, ...props }: HeadingProps) {
let Element: `h${typeof level}` = `h${level}`

return (
<Element
{...props}
className={clsx(className, 'text-base/7 font-semibold text-zinc-950 sm:text-sm/6 dark:text-white')}
/>
)
}
230 changes: 230 additions & 0 deletions moon/apps/web/components/Issues/IssueDetailPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
'use client'

import React, { useCallback, useEffect, useState } from 'react'
// import { CloseCircleOutlined, CommentOutlined } from '@ant-design/icons'
import { Button, Card, Flex, Space, Tabs, TabsProps, Timeline } from 'antd'
import { useRouter } from 'next/router'
import toast from 'react-hot-toast'

import Comment from '@/components/MrView/MRComment'
import RichEditor from '@/components/MrView/rich-editor/RichEditor'
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 { apiErrorToast } from '@/utils/apiErrorToast'

interface IssueDetail {
status: string
conversations: Conversation[]
title: string
}
interface Conversation {
id: number
user_id: number
conv_type: string
comment: string
created_at: number
}

interface detailRes {
err_message: string
data: IssueDetail
req_result: boolean
}

export default function IssueDetailPage({ id }: { id: string }) {
const [editorState, setEditorState] = useState('')
const [login, setLogin] = useState(false)
const [info, setInfo] = useState<IssueDetail>({
status: '',
conversations: [],
title: ''
})

const [buttonLoading, setButtonLoading] = useState<{ [key: string]: boolean }>({
comment: false,
close: false,
reopen: false
})

const setLoading = (key: string, value: boolean) => {
setButtonLoading((prev) => ({ ...prev, [key]: value }))
}

const { mutate: closeIssue } = usePostIssueClose()

const { mutate: reopenIssue } = usePostIssueReopen()

const { mutate: saveComment } = usePostIssueComment()

const { data: issueDetailObj, error, isError, refetch } = useGetIssueDetail(id)

const applyDetailData = (detail: detailRes | undefined) => {
if (!detail || !detail.req_result) return
setInfo({
title: detail.data.title,
status: detail.data.status,
conversations: detail.data.conversations
})
}

const fetchDetail = useCallback(() => {
if (error || isError) return
applyDetailData(issueDetailObj)
setLogin(true)
}, [issueDetailObj, error, isError])

useEffect(() => {
fetchDetail()
}, [fetchDetail])

const [_loadings, setLoadings] = useState<boolean[]>([])
const router = useRouter()

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 close_issue = useCallback(() => {
setLoading('close', true)
set_to_loading(3)
closeIssue(
{ link: id },
{
onSuccess: () => {
router.push(`/${router.query.org}/issue`)
cancel_loading(3)
},
onError: apiErrorToast,
onSettled: () => setLoading('close', false)
}
)
}, [id, router, closeIssue])

const reopen_issue = useCallback(() => {
setLoading('reopen', true)
set_to_loading(3)
reopenIssue(
{ link: id },
{
onSuccess: () => {
router.push(`/${router.query.org}/issue`)
},
onError: apiErrorToast,
onSettled: () => setLoading('reopen', false)
}
)
}, [id, router, reopenIssue])

const save_comment = useCallback(
(comment: string) => {
if (JSON.parse(comment).root.children[0].children.length === 0) {
toast.error('comment can not be empty!')
return
}
setLoading('comment', true)
set_to_loading(3)
saveComment(
{ link: id, data: { content: comment } },
{
onSuccess: async () => {
setEditorState('')
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 = <CommentOutlined />
children = <Comment conv={conv} id={id} whoamI='issue' />
break
case 'Closed':
// icon = <CloseCircleOutlined />
children = conv.comment
}

const element = {
dot: icon,
children: children
}

return element
})

const tab_items: TabsProps['items'] = [
{
key: '1',
label: 'Conversation',
children: (
<Space direction='vertical' style={{ width: '100%' }}>
<Timeline items={conv_items} />
{info && info.status === 'open' && (
<>
<h1>Add a comment</h1>
<RichEditor setEditorState={setEditorState} />
<Flex gap='small' justify={'flex-end'}>
<Button
// loading={motivation === 'close' ? loadings[3] : undefined}
loading={buttonLoading.close}
disabled={!login}
onClick={() => close_issue()}
>
Close issue
</Button>
<Button
loading={buttonLoading.comment}
disabled={editorState === '' || !login}
onClick={() => save_comment(editorState)}
>
Comment
</Button>
</Flex>
</>
)}
{info && info.status === 'closed' && (
<Flex gap='small' justify={'flex-end'}>
<Button loading={buttonLoading.reopen} disabled={!login} onClick={() => reopen_issue()}>
Reopen issue
</Button>
</Flex>
)}
</Space>
)
}
]

return (
<Card className='max-h-64 overflow-y-auto' title={info.title + ' #' + id}>
<Tabs defaultActiveKey='1' items={tab_items} />
</Card>
)
}
87 changes: 87 additions & 0 deletions moon/apps/web/components/Issues/IssueNewPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
'use client'

import { useCallback, useState } from 'react'
import { Button, Flex, Input, Space } from 'antd/lib'
import { useRouter } from 'next/router'
import toast from 'react-hot-toast'

import RichEditor from '@/components/MrView/rich-editor/RichEditor'
import { usePostIssueSubmit } from '@/hooks/issues/usePostIssueSubmit'
import { apiErrorToast } from '@/utils/apiErrorToast'

export default function IssueNewPage() {
const [editorState, setEditorState] = useState('')
const [title, setTitle] = useState('')
const [loadings, setLoadings] = useState<boolean[]>([])
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 (
<>
<div className='container p-10'>
<Space direction='vertical' style={{ width: '100%' }}>
<h1>
Add a title
<Input
aria-label='title'
name='title'
placeholder='Title'
value={title}
onChange={(e) => setTitle(e.target.value)}
></Input>
</h1>
</Space>
<Space direction='vertical' style={{ width: '100%' }}>
<h1>Add a description</h1>
<RichEditor setEditorState={setEditorState} />
<Flex justify={'flex-end'}>
<Button loading={loadings[3]} onClick={() => submit(editorState)}>
Submit New Issue
</Button>
</Flex>
</Space>
</div>
</>
)
}
Loading
You are viewing a condensed version of this merge commit. You can view the full changes here.