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
414 changes: 332 additions & 82 deletions moon/apps/web/components/Issues/IssueNewPage.tsx

Large diffs are not rendered by default.

24 changes: 22 additions & 2 deletions moon/apps/web/components/Issues/IssuesContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ import { formatDistance, fromUnixTime } from 'date-fns'
import { useAtom } from 'jotai'

import { SyncOrganizationMember as Member, PostApiIssueListData } from '@gitmono/types/generated'
import { Button, ChatBubbleIcon, CheckCircleFilledFlushIcon, ChevronDownIcon, OrderedListIcon } from '@gitmono/ui'
import {
Button,
ChatBubbleIcon,
CheckCircleFilledFlushIcon,
ChevronDownIcon,
CircleFilledCloseIcon,
OrderedListIcon
} from '@gitmono/ui'
import { Link } from '@gitmono/ui/Link'

// import { MenuItem } from '@gitmono/ui/Menu'
Expand Down Expand Up @@ -285,6 +292,19 @@ export function IssuesContent({ searching }: Props) {
return <EmptySearchResults />
}

const getStatusIcon = (status: string) => {
const normalizedStatus = status.toLowerCase()

switch (normalizedStatus) {
case 'open':
return <CircleFilledCloseIcon color='#f44613' />
case 'closed':
return <CheckCircleFilledFlushIcon color='#378f50' size={16} />
default:
return null
}
}

return (
<>
{/* TODO:Searching logic need to be completed */}
Expand Down Expand Up @@ -315,7 +335,7 @@ export function IssuesContent({ searching }: Props) {
<ListItem
key={i.link}
title={i.title}
leftIcon={<CheckCircleFilledFlushIcon color='#378f50' size={16} />}
leftIcon={getStatusIcon(i.status)}
rightIcon={<RightAvatar member={members[0]} />}
>
<div className='text-xs text-[#59636e]'>
Expand Down
20 changes: 20 additions & 0 deletions moon/apps/web/components/Issues/utils/extractText.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react'

export function extractTextArray(node: React.ReactNode): string[] {
if (typeof node === 'string' || typeof node === 'number') {
return [node.toString()]
}

if (Array.isArray(node)) {
// 递归展开所有节点,返回扁平化的字符串数组
return node.flatMap(extractTextArray)
}

if (React.isValidElement(node)) {
// 递归提取 React 元素的 children
return extractTextArray(node.props.children)
}

// 其他情况(null, undefined, boolean, function)
return []
}
22 changes: 22 additions & 0 deletions moon/apps/web/components/Issues/utils/mergeDuplicate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ItemInput } from '@primer/react/lib/deprecated/ActionList'

export function mergeAndDeduplicate(
prev: ItemInput[],
selected: ItemInput[],
prevId: string,
selectId: string
): ItemInput[] {
const updatedPrev = prev.map((item) => ({
...item,
groupId: prevId
}))

const updatedSelected = selected.map((item) => ({
...item,
groupId: selectId
}))

const mergedArray = [...updatedPrev, ...updatedSelected]

return Array.from(new Map(mergedArray.map((item) => [item.text, item])).values())
}
8 changes: 8 additions & 0 deletions moon/apps/web/components/Issues/utils/pickWithReflectDeep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const pickWithReflect = <T extends Object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> => {
const result = {} as Pick<T, K>

keys.forEach((i) => {
result[i] = Reflect.get(obj, i)
})
return result
}
174 changes: 169 additions & 5 deletions moon/apps/web/components/MrView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,38 @@ import React, { useCallback, useEffect, useState } from 'react'
import { formatDistance, fromUnixTime } from 'date-fns'
import { useAtom } from 'jotai'

import { ChatBubbleIcon, CheckCircleFilledFlushIcon, CircleFilledCloseIcon, ClockIcon } from '@gitmono/ui'
import { SyncOrganizationMember as Member } from '@gitmono/types/generated'
import {
Button,
ChatBubbleIcon,
CheckCircleFilledFlushIcon,
ChevronDownIcon,
CircleFilledCloseIcon,
ClockIcon
} from '@gitmono/ui'
import { Link } from '@gitmono/ui/Link'
import { cn } from '@gitmono/ui/src/utils'

import { IssueIndexTabFilter as MRIndexTabFilter } from '@/components/Issues/IssueIndex'
import { ListBanner, ListItem as MrItem, IssueList as MrList } from '@/components/Issues/IssueList'
import {
Dropdown,
DropdownItemwithAvatar,
DropdownItemwithLabel,
ListBanner,
ListItem as MrItem,
IssueList as MrList
} from '@/components/Issues/IssueList'
import { useScope } from '@/contexts/scope'
import { usePostMrList } from '@/hooks/usePostMrList'
import { useSyncedMembers } from '@/hooks/useSyncedMembers'
import { apiErrorToast } from '@/utils/apiErrorToast'

import { IndexPageContainer, IndexPageContent } from '../IndexPages/components'
import { Label } from '../Issues/IssuesContent'
import { Pagination } from '../Issues/Pagenation'
import { filterAtom } from '../Issues/utils/store'
import { tags } from '../Issues/utils/consts'
import { generateAllMenuItems, MenuConfig } from '../Issues/utils/generateAllMenuItems'
import { filterAtom, sortAtom } from '../Issues/utils/store'
import { Heading } from './catalyst/heading'

interface MrInfoItem {
Expand Down Expand Up @@ -95,7 +114,7 @@ export default function MrView() {

switch (normalizedStatus) {
case 'open':
return <CircleFilledCloseIcon color='#378f50' />
return <CircleFilledCloseIcon color='#f44613' />
case 'closed':
return <ClockIcon size={16} />
case 'merged':
Expand Down Expand Up @@ -124,6 +143,144 @@ export default function MrView() {
}
}

const [sort, setSort] = useAtom(sortAtom({ scope, filter: 'sortPickerMR' }))
const { members } = useSyncedMembers()

const MemberConfig: MenuConfig<Member>[] = [
{
key: 'Author',
isChosen: (item) => item.user.id === sort['Author'],
onSelectFactory: (item: Member) => (e: Event) => {
e.preventDefault()
if (item.user.id === sort['Author']) {
loadMrList()
setSort({
...sort,
Author: ''
})
} else {
setMrList(mrList.filter((i) => i.link === sort['Author']))

Copilot AI Jun 29, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filtering logic for the 'Author' selection uses sort['Author'] as the filter criterion, which may be incorrect. Consider filtering based on the currently selected member's id (i.e. item.user.id) to ensure accurate results.

Suggested change
setMrList(mrList.filter((i) => i.link === sort['Author']))
setMrList(mrList.filter((i) => i.user.id === sort['Author']))

Copilot uses AI. Check for mistakes.
setSort({
...sort,
Author: item.user.id
})
}
},
className: 'overflow-hidden',
labelFactory: (item: Member) => <DropdownItemwithAvatar member={item} classname='text-sm' />
},
{
key: 'Assignees',
isChosen: (item: Member) => item.user.id === sort['Assignees'],
onSelectFactory: (item: Member) => (e: Event) => {
e.preventDefault()
if (item.user.id === sort['Assignees']) {
loadMrList()

setSort({
...sort,
Assignees: ''
})
} else {
setMrList(mrList.filter((i) => i.link === sort['Assignees']))
setSort({
...sort,
Assignees: item.user.id
})
}
},
className: 'overflow-hidden',
labelFactory: (item: Member) => <DropdownItemwithAvatar member={item} classname='text-sm' />
}
]

const LabelConfig: MenuConfig<Label>[] = [
{
key: 'Labels',
isChosen: (item) => sort['Labels']?.includes(item.id),

onSelectFactory: (item: Label) => (e: Event) => {
e.preventDefault()
if (sort['Labels']?.includes(item.id)) {
// fetchData(1, pageSize)
// sort['Labels'] contains the id of each labels which are chosed
setSort({
...sort,
Labels: (sort['Labels'] as string[]).filter((i) => i !== item.id)
})
} else {
// setIssueList(issueList.filter((i) => i.link === sort['Labels']))
setSort({
...sort,
// make sure labels must be an array of string
Labels: [...((sort['Labels'] as string[]) ?? []), item.id]
})
}
},
className: 'overflow-hidden',
labelFactory: (item: Label) => <DropdownItemwithLabel label={item} />
}
]

const handleOpen = (open: boolean) => {
if (open) {
// open: do nothing
} else {
// close: fetch data from labels array
}
}

const member = generateAllMenuItems(members, MemberConfig)

const labels = generateAllMenuItems(tags, LabelConfig)

const ListHeaderItem = (p: string) => {
switch (p) {
case 'Author':
return (
<Dropdown
isChosen={sort['Author'] === ''}
key={p}
name={p}
dropdownArr={member?.get('Author').all}
dropdownItem={member?.get('Author').chosen}
/>
)
case 'Assignees':
return (
<Dropdown
isChosen={sort['Assignees'] === ''}
key={p}
name={p}
dropdownArr={member?.get('Assignees').all}
dropdownItem={member?.get('Assignees').chosen}
/>
)
case 'Labels':
return (
<Dropdown
onOpen={(open) => handleOpen(open)}
isChosen={!sort['Labels']?.length}
key={p}
name={p}
dropdownArr={labels?.get('Labels').all}
dropdownItem={labels?.get('Labels').chosen}
/>
)
default:
return (
<>
<Button size='sm' variant={'plain'} tooltipShortcut={p}>
<div className='flex items-center justify-center'>
{p}
<ChevronDownIcon />
</div>
</Button>
</>
)
}
}

return (
<div className='m-4'>
<Heading>Merge Request</Heading>
Expand All @@ -133,7 +290,14 @@ export default function MrView() {
<MrList
isLoading={isLoading}
Issuelists={mrList}
header={<ListBanner pickerTypes={[]} tabfilter={<MRIndexTabFilter part='mr' />} />}
header={
<ListBanner
pickerTypes={['Author', 'Labels', 'Projects', 'Milestones', 'Assignees', 'Types']}
tabfilter={<MRIndexTabFilter part='mr' />}
>
{(p) => ListHeaderItem(p)}
</ListBanner>
}
>
{(issueList) => {
return issueList.map((i) => (
Expand Down
3 changes: 2 additions & 1 deletion moon/apps/web/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ const moduleExports = {
'@gitmono/config',
'@gitmono/regex',
'@gitmono/editor',
'react-tweet'
'react-tweet',
'@primer/react'
],
reactStrictMode: true,
images: {
Expand Down
3 changes: 3 additions & 0 deletions moon/apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
"@next/bundle-analyzer": "catalog:",
"@octokit/auth-app": "catalog:",
"@octokit/core": "catalog:",
"@primer/primitives": "^10.7.0",
"@primer/react": "^37.27.0",
"@radix-ui/react-accordion": "catalog:",
"@radix-ui/react-dialog": "catalog:",
"@radix-ui/react-hover-card": "catalog:",
Expand Down Expand Up @@ -123,6 +125,7 @@
"react-wrap-balancer": "catalog:",
"remeda": "catalog:",
"slugify": "catalog:",
"styled-components": "^6.1.19",
"tippy.js": "catalog:",
"use-debounce": "catalog:",
"use-sound": "catalog:",
Expand Down
9 changes: 8 additions & 1 deletion moon/apps/web/pages/[org]/issue/new/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { BaseStyles, ThemeProvider } from '@primer/react'
import { useRouter } from 'next/router'

import '@primer/primitives/dist/css/functional/themes/light.css'

import IssueNewPage from '@/components/Issues/IssueNewPage'
import { AppLayout } from '@/components/Layout/AppLayout'
import { AuthAppProviders } from '@/components/Providers/AuthAppProviders'
Expand All @@ -18,7 +21,11 @@ const OrganizationIssueNewPage: PageWithLayout<any> = () => {
OrganizationIssueNewPage.getProviders = (page, pageProps) => {
return (
<AuthAppProviders {...pageProps}>
<AppLayout {...pageProps}>{page}</AppLayout>
<ThemeProvider>
<BaseStyles>
<AppLayout {...pageProps}>{page}</AppLayout>
</BaseStyles>
</ThemeProvider>
</AuthAppProviders>
)
}
Expand Down
Loading