From 9144c3b317223798c557c7f1fcf74e7fef3e936c Mon Sep 17 00:00:00 2001 From: liuyangjuncong20202570 Date: Tue, 17 Jun 2025 17:21:12 +0800 Subject: [PATCH] feat(UI) completion of issue page&fix the bug of navigation --- .../web/components/Issues/IssueDetailPage.tsx | 2 +- .../apps/web/components/Issues/IssueIndex.tsx | 15 +- moon/apps/web/components/Issues/IssueList.tsx | 393 +++++++++++++----- .../web/components/Issues/IssuesContent.tsx | 253 ++++++++++- .../apps/web/components/Issues/Pagenation.tsx | 2 +- .../web/components/Issues/utils/consts.ts | 39 ++ .../Issues/utils/generateAllMenuItems.ts | 37 ++ .../web/components/Issues/utils/store.tsx | 6 +- .../apps/web/pages/[org]/issue/[id]/index.tsx | 2 +- moon/apps/web/pages/[org]/issue/new/index.tsx | 6 +- moon/apps/web/pages/_app.tsx | 5 +- moon/packages/ui/src/Menu/types.ts | 3 +- 12 files changed, 643 insertions(+), 120 deletions(-) create mode 100644 moon/apps/web/components/Issues/utils/consts.ts create mode 100644 moon/apps/web/components/Issues/utils/generateAllMenuItems.ts diff --git a/moon/apps/web/components/Issues/IssueDetailPage.tsx b/moon/apps/web/components/Issues/IssueDetailPage.tsx index c11166e77..6dc997f19 100644 --- a/moon/apps/web/components/Issues/IssueDetailPage.tsx +++ b/moon/apps/web/components/Issues/IssueDetailPage.tsx @@ -78,7 +78,7 @@ export default function IssueDetailPage({ id }: { id: string }) { useEffect(() => { fetchDetail() - }, [fetchDetail]) + }, [fetchDetail, id]) const [_loadings, setLoadings] = useState([]) const router = useRouter() diff --git a/moon/apps/web/components/Issues/IssueIndex.tsx b/moon/apps/web/components/Issues/IssueIndex.tsx index c5581a98b..813355631 100644 --- a/moon/apps/web/components/Issues/IssueIndex.tsx +++ b/moon/apps/web/components/Issues/IssueIndex.tsx @@ -1,3 +1,5 @@ +'use client' + import { useState } from 'react' import { useAtom } from 'jotai' import { useRouter } from 'next/router' @@ -32,7 +34,6 @@ export const IssueIndex = () => { return ( <> - @@ -45,11 +46,10 @@ export const IssueIndex = () => { - + - @@ -101,7 +101,14 @@ export const NewIssueButton = () => { const { scope } = useScope() return ( - ) diff --git a/moon/apps/web/components/Issues/IssueList.tsx b/moon/apps/web/components/Issues/IssueList.tsx index c9ea398c5..ab5f0274f 100644 --- a/moon/apps/web/components/Issues/IssueList.tsx +++ b/moon/apps/web/components/Issues/IssueList.tsx @@ -1,38 +1,38 @@ -import { ReactNode } from 'react' -import { formatDistance, fromUnixTime } from 'date-fns' -import { useAtom, useAtomValue } from 'jotai' -import { atomFamily } from 'jotai/utils' -import { useRouter } from 'next/router' +import React, { forwardRef, memo, ReactNode, useRef, useState } from 'react' +import { useAtomValue } from 'jotai' +import { SyncOrganizationMember } from '@gitmono/types/index' import { Button, - ChatBubbleIcon, - CheckCircleFilledFlushIcon, ChevronDownIcon, + cn, Command, ConditionalWrap, + LazyLoadingSpinner, + SearchIcon, useCommand } from '@gitmono/ui' +import { DropdownMenu } from '@gitmono/ui/DropdownMenu' +import { MenuItem } from '@gitmono/ui/Menu' -import { Item } from '@/components/Issues/IssuesContent' +import { Label } from '@/components/Issues/IssuesContent' import { darkModeAtom } from '@/components/Issues/utils/store' import { SubjectCommand } from '@/components/Subject/SubjectCommand' import { BreadcrumbTitlebar } from '@/components/Titlebar/BreadcrumbTitlebar' -import { useScope } from '@/contexts/scope' -import { atomWithWebStorage } from '@/utils/atomWithWebStorage' +import { MemberAvatar } from '../MemberAvatar' import { IssueIndexTabFilter } from './IssueIndex' -interface Props { - Issuelists: Item[] +export function IssueList({ + Issuelists, + header, + children +}: { + Issuelists: T[] hideProject?: boolean -} - -export function IssueList({ Issuelists }: Props) { - const { scope } = useScope() - const router = useRouter() - // const filter = useAtomValue(filterAtom(scope)) - // const sort = useAtomValue(sortAtom({ scope, filter })) + header?: React.ReactNode + children?: (issue: T[]) => React.ReactNode +}) { const needsCommandWrap = !useCommand() const isDark = useAtomValue(darkModeAtom) @@ -40,8 +40,7 @@ export function IssueList({ Issuelists }: Props) { <> {!isDark ? (
- - + {header} ( @@ -50,22 +49,7 @@ export function IssueList({ Issuelists }: Props) { )} > - {Issuelists.map((i) => { - return ( - } - rightIcon={} - onClick={() => router.push(`/${scope}/issue/${i.link}`)} - > -
- {i.link} · {i.owner} {i.status}{' '} - {formatDistance(fromUnixTime(i.open_timestamp), new Date(), { addSuffix: true })} -
-
- ) - })} + {children?.(Issuelists)}
) : ( @@ -75,83 +59,302 @@ export function IssueList({ Issuelists }: Props) { ) } -type IssuePickerType = 'Author' | 'Labels' | 'Projects' | 'Milestones' | 'Assignees' | 'Types' +interface ListBannerProps { + pickerTypes: string[] + children?: (p: string) => React.ReactNode +} -const filterAtomFamily = atomFamily((sign: IssuePickerType) => - atomWithWebStorage(`${sign}:picker`, sign) +export const ListBanner = forwardRef( + ({ pickerTypes, children }: ListBannerProps, ref) => { + return ( + <> +
+ +
{c}
}> + +
+
{c}
}> + {pickerTypes.map((p) => { + return {children?.(p)} + })} +
+
+
+ + ) + } ) -const ListPicker = ({ Sign }: { Sign: T }) => { - const [_filter, setFilter] = useAtom(filterAtomFamily(Sign)) +ListBanner.displayName = 'ListBanner' - // TODO - // logic of onClick will change later - // storage will store the specific value from backend when chose the options +export const DropdownItemwithAvatar = ({ + member, + classname +}: { + member: SyncOrganizationMember + classname?: string +}) => { return ( - <> - - +
+ + {member.user.display_name} + {member.user.username} +
) } -export const ListBanner = () => { - // TODO: Authors, Labels, Projects, Milestones, Assignees need to be stored in storgae in future - const pickerTypes: IssuePickerType[] = ['Author', 'Labels', 'Projects', 'Milestones', 'Assignees', 'Types'] +export const DropdownItemwithLabel = ({ classname, label }: { classname?: string; label: Label }) => { + return ( +
+
+ {label.name} + {label.remarks} +
+ ) +} +export const DropdownOrder = ({ + name, + dropdownArr, + dropdownItem, + onOpen, + open, + inside +}: { + name: string + dropdownArr: MenuItem[] + dropdownItem?: MenuItem[] + onOpen?: (open: boolean) => void + open?: boolean + inside?: React.ReactNode +}) => { return ( <> - -
{c}
}> - -
-
{c}
}> - {pickerTypes.map((p) => ( - - ))} -
-
+ Sort by

+ // className: 'sticky top-0 z-50 bg-white' + }, + ...dropdownArr, + { type: 'separator' }, + { + type: 'item', + disabled: true, + label:

Order

+ // className: 'sticky top-0 z-50 bg-white' + }, + ...(dropdownItem as MenuItem[]) + ]} + trigger={ + + } + /> ) } -export const ListItem = ({ - title, - children, - leftIcon, - rightIcon, - onClick +// dropdownArr是不一样的,其他一样 +export const Dropdown = ({ + name, + dropdownArr, + dropdownItem, + isChosen, + onOpen, + open, + inside }: { - title: string - children?: ReactNode - leftIcon?: ReactNode - rightIcon?: ReactNode - onClick?: () => void + name: string + dropdownArr: MenuItem[] + dropdownItem?: MenuItem[] + isChosen: boolean + onOpen?: (open: boolean) => void + open?: boolean + inside?: React.ReactNode }) => { + const [query, setQuery] = useState('') + // const { scope } = useScope() + // const [sort] = useAtom(sortAtom({ scope, filter: 'sortPicker' })) + const isSearching = query.length > 0 + const ref = useRef(null) + + const DropdownSearch = () => ( +
+ + {isSearching ? } /> : } + + setQuery(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Escape') { + setQuery('') + ref.current?.blur() + } else if (e.key === 'Enter') { + e.preventDefault() + e.stopPropagation() + } + }} + /> +
+ ) + return ( <> -
-
-
{leftIcon}
-
{ - e.stopPropagation() - onClick?.() - }} - className='inner flex flex-col hover:cursor-pointer' - > - {title} - {children} -
-
-
-
{rightIcon}
-
-
+ {isChosen ? ( + Filter by {name}

+ // className: 'sticky top-0 z-50 bg-white' + }, + { + type: 'item', + label: , + onSelect: (e) => e.preventDefault() + // className: 'sticky top-10 z-50 bg-white' + }, + { type: 'separator' }, + ...dropdownArr + ]} + trigger={ + + } + /> + ) : ( + Filter by {name}

, + disabled: true + // className: 'sticky top-0 z-50 bg-white pt-4' + }, + { + type: 'item', + label: , + onSelect: (e) => e.preventDefault() + // className: 'sticky top-10 z-50 bg-white pt-4' + }, + { type: 'separator' }, + { type: 'heading', label: 'Group assignees' }, + ...(dropdownItem as MenuItem[]), + { type: 'separator' }, + { type: 'heading', label: 'Suggestions' }, + ...dropdownArr + ]} + trigger={ + + } + /> + )} ) } + +export const ListItem = memo( + ({ + title, + children, + leftIcon, + rightIcon, + onClick + }: { + title: string + children?: ReactNode + leftIcon?: ReactNode + rightIcon?: ReactNode + onClick?: () => void + }) => { + return ( + <> +
+
+
{leftIcon}
+
{ + e.stopPropagation() + onClick?.() + }} + className='inner flex flex-col hover:cursor-pointer' + > + {title} + {children} +
+
+
+
{rightIcon}
+
+
+ + ) + } +) +ListItem.displayName = 'ListItem' diff --git a/moon/apps/web/components/Issues/IssuesContent.tsx b/moon/apps/web/components/Issues/IssuesContent.tsx index fa2d0bf56..c2850b239 100644 --- a/moon/apps/web/components/Issues/IssuesContent.tsx +++ b/moon/apps/web/components/Issues/IssuesContent.tsx @@ -1,19 +1,35 @@ -import React, { useCallback, useEffect, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' import { useInfiniteQuery } from '@tanstack/react-query' +import { formatDistance, fromUnixTime } from 'date-fns' import { useAtom } from 'jotai' -import { PostApiIssueListData } from '@gitmono/types/generated' +import { SyncOrganizationMember as Member, PostApiIssueListData } from '@gitmono/types/generated' +import { Button, ChatBubbleIcon, CheckCircleFilledFlushIcon, ChevronDownIcon, OrderedListIcon } from '@gitmono/ui' +import { Link } from '@gitmono/ui/Link' + +// import { MenuItem } from '@gitmono/ui/Menu' import { EmptySearchResults } from '@/components/Feed/EmptySearchResults' -import { IssueList } from '@/components/Issues/IssueList' -import { filterAtom } from '@/components/Issues/utils/store' +import { + Dropdown, + DropdownItemwithAvatar, + DropdownItemwithLabel, + DropdownOrder, + IssueList, + ListBanner, + ListItem +} from '@/components/Issues/IssueList' +import { filterAtom, sortAtom } from '@/components/Issues/utils/store' import { useScope } from '@/contexts/scope' import { useGetIssueLists } from '@/hooks/issues/useGetIssueLists' +import { useSyncedMembers } from '@/hooks/useSyncedMembers' import { apiErrorToast } from '@/utils/apiErrorToast' +import { atomWithWebStorage } from '@/utils/atomWithWebStorage' import { IndexPageInstantLoading } from '../IndexPages/components' -import { IssueIndexEmptyState } from './IssueIndex' import { Pagination } from './Pagenation' +import { orderTags, tags } from './utils/consts' +import { generateAllMenuItems, MenuConfig } from './utils/generateAllMenuItems' interface Props { getIssues?: ReturnType> @@ -31,9 +47,17 @@ export interface Item { updated_at: number } +export interface Label { + id: string + name: string + color: string + remarks: string + checked: boolean +} + export function IssuesContent({ searching }: Props) { - // TODO:rebuild bu useInfiniteQuery const { mutate: issueLists } = useGetIssueLists() + const { scope } = useScope() const [pageSize, _setPageSize] = useState(10) @@ -46,6 +70,182 @@ export function IssuesContent({ searching }: Props) { const [numTotal, setNumTotal] = useState(0) + const [sort, setSort] = useAtom(sortAtom({ scope, filter: 'sortPicker' })) + + const orderAtom = useMemo(() => atomWithWebStorage(`${scope}:issue-order`, 'Newest'), [scope]) + + const [order, _setOrder] = useAtom(orderAtom) + + const { members } = useSyncedMembers() + + const MemberConfig: MenuConfig[] = [ + { + key: 'Author', + isChosen: (item) => item.user.id === sort['Author'], + onSelectFactory: (item: Member) => (e: Event) => { + e.preventDefault() + if (item.user.id === sort['Author']) { + fetchData(1, pageSize) + setSort({ + ...sort, + Author: '' + }) + } else { + setIssueList(issueList.filter((i) => i.link === sort['Author'])) + setSort({ + ...sort, + Author: item.user.id + }) + } + }, + className: 'overflow-hidden', + labelFactory: (item: Member) => + }, + { + key: 'Assignees', + isChosen: (item: Member) => item.user.id === sort['Assignees'], + onSelectFactory: (item: Member) => (e: Event) => { + e.preventDefault() + if (item.user.id === sort['Assignees']) { + fetchData(1, pageSize) + + setSort({ + ...sort, + Assignees: '' + }) + } else { + setIssueList(issueList.filter((i) => i.link === sort['Assignees'])) + setSort({ + ...sort, + Assignees: item.user.id + }) + } + }, + className: 'overflow-hidden', + labelFactory: (item: Member) => + } + ] + + const LabelConfig: MenuConfig