Skip to content

Commit 4375701

Browse files
authored
feat: add label page (#1249)
* refactor(UI): add `colord` dependency * feat(UI): add labels page
1 parent dd1d252 commit 4375701

7 files changed

Lines changed: 252 additions & 5 deletions

File tree

moon/apps/web/components/Issues/IssueSearch.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,7 @@ export default function IssueSearch() {
3838
}}
3939
/>
4040
{/* <IndexSearchInput query={query} setQuery={setQuery} isSearchLoading={isSearchLoading} /> */}
41-
<Button variant='primary' size={'base'}>
42-
Labels
43-
</Button>
41+
<LabelsButton />
4442
<NewIssueButton />
4543
</BreadcrumbTitlebar>
4644
</>
@@ -58,3 +56,15 @@ export const NewIssueButton = () => {
5856
</Link>
5957
)
6058
}
59+
60+
const LabelsButton = () => {
61+
const { scope } = useScope()
62+
63+
return (
64+
<Link href={`/${scope}/labels`}>
65+
<Button variant='primary' size={'base'}>
66+
Labels
67+
</Button>
68+
</Link>
69+
)
70+
}

moon/apps/web/components/Issues/utils/store.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,7 @@ export const issueOpenCurrentPage = atomWithWebStorage('IssueOpencurrentPage', 1
3535
export const issueCloseCurrentPage = atomWithWebStorage('IssueClosecurrentPage', 1)
3636

3737
export const mrOpenCurrentPage = atomWithWebStorage('MROpencurrentPage', 1)
38-
export const mrCloseCurrentPage = atomWithWebStorage('MRClosecurrentPage', 1)
38+
export const mrCloseCurrentPage = atomWithWebStorage('MRClosecurrentPage', 1)
39+
40+
export const labelsOpenCurrentPage = atomWithWebStorage('LabelsOpenCurrentPage', 1)
41+
export const labelsCloseCurrentPage = atomWithWebStorage('LabelsCloseCurrentPage', 1)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// hooks/usePostLabelList.ts
2+
import { PageParamsString, PostApiLabelListData, RequestParams } from '@gitmono/types'
3+
import { legacyApiClient } from '@/utils/queryClient'
4+
import { useMutation } from '@tanstack/react-query'
5+
6+
export function usePostLabelList() {
7+
return useMutation<PostApiLabelListData, Error, { data: PageParamsString, params?: RequestParams }>({
8+
mutationFn: ({ data, params }) => legacyApiClient.v1.postApiLabelList().request(data, params)
9+
})
10+
}

moon/apps/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"@vercel/og": "catalog:",
6262
"@vercel/speed-insights": "catalog:",
6363
"clsx": "catalog:",
64+
"colord": "catalog:",
6465
"cookies-next": "catalog:",
6566
"copy-to-clipboard": "catalog:",
6667
"d3-selection": "catalog:",
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import React, { AwaitedReactNode, JSX, ReactElement, ReactNode, ReactPortal, useCallback, useEffect, useState } from 'react'
2+
import { colord } from 'colord'
3+
import { useRouter } from 'next/router'
4+
import { useDebounce } from 'use-debounce'
5+
6+
import { PostApiLabelListData } from '@gitmono/types'
7+
import {Button, cn, LazyLoadingSpinner, SearchIcon} from '@gitmono/ui'
8+
9+
import { IndexPageContainer, IndexPageContent } from '@/components/IndexPages/components'
10+
import { IssueList as LabelList, ListItem } from '@/components/Issues/IssueList'
11+
import { Pagination } from '@/components/Issues/Pagenation'
12+
import { labelsOpenCurrentPage } from '@/components/Issues/utils/store'
13+
import { AppLayout } from '@/components/Layout/AppLayout'
14+
import { Heading } from '@/components/MrView/catalyst/heading'
15+
import AuthAppProviders from '@/components/Providers/AuthAppProviders'
16+
import { BreadcrumbTitlebar } from '@/components/Titlebar/BreadcrumbTitlebar'
17+
import { useScope } from '@/contexts/scope'
18+
import { usePostLabelList } from '@/hooks/usePostLabelList'
19+
import { apiErrorToast } from '@/utils/apiErrorToast'
20+
21+
type ItemsType = NonNullable<PostApiLabelListData['data']>['items']
22+
23+
function LabelsPage() {
24+
const router = useRouter()
25+
const { scope } = useScope()
26+
27+
const [query, setQuery] = useState("")
28+
const [queryDebounced] = useDebounce(query, 150)
29+
const [isLoading, setIsLoading] = useState(false)
30+
const [isSearchLoading, setIsSearchLoading] = useState(false)
31+
32+
const handleQuery = () => {
33+
setIsSearchLoading(true)
34+
setShowLabelList(
35+
() =>
36+
labelList.filter((i) =>
37+
i.name.toLowerCase().includes(queryDebounced.toLowerCase())))
38+
setIsSearchLoading(false)
39+
}
40+
41+
const [labelList, setLabelList] = useState<ItemsType>([])
42+
const [showLabelList, setShowLabelList] = useState<ItemsType>([])
43+
const [numTotal, setNumTotal] = useState(0)
44+
const [page, setPage] = useState(1)
45+
const [per_page] = useState(20)
46+
const { mutate: fetchLabelList } = usePostLabelList()
47+
const fetchLabels = useCallback(() => {
48+
setIsLoading(true)
49+
50+
fetchLabelList(
51+
{
52+
data: {
53+
additional: 'string',
54+
pagination: {
55+
page,
56+
per_page
57+
}
58+
}
59+
},
60+
{
61+
onSuccess: (response) => {
62+
const data = response.data
63+
64+
setLabelList(data?.items ?? [])
65+
setShowLabelList(data?.items ?? [])
66+
setNumTotal(data?.total ?? 0)
67+
},
68+
onError: apiErrorToast,
69+
onSettled: () => setIsLoading(false)
70+
}
71+
)
72+
}, [page, per_page, fetchLabelList])
73+
74+
useEffect(() => {
75+
fetchLabels()
76+
}, [fetchLabels])
77+
78+
return (
79+
<>
80+
<div className='m-4'>
81+
<Heading>Labels</Heading>
82+
<br />
83+
84+
<IndexPageContainer>
85+
<IndexPageContent id='/[org]/labels' className={cn('@container', '3xl:max-w-6xl max-w-6xl')}>
86+
<BreadcrumbTitlebar className='justify-between pl-3 pr-3'>
87+
<div className='relative flex flex-1 flex-row items-center gap-2 overflow-hidden rounded-md border border-gray-300 px-2 py-1 focus-within:border-blue-500 focus-within:ring-1 focus-within:ring-blue-500'>
88+
<input
89+
className='flex-1 border-none bg-transparent p-0 text-sm outline-none ring-0 focus:ring-0'
90+
placeholder='Search...'
91+
role='searchbox'
92+
autoComplete='off'
93+
autoCorrect='off'
94+
spellCheck={false}
95+
type='text'
96+
value={query}
97+
onChange={(e) => {
98+
setQuery(e.target.value)
99+
}}
100+
onKeyDown={(e) => {
101+
if (e.key === 'Enter') {
102+
e.preventDefault()
103+
e.stopPropagation()
104+
handleQuery()
105+
}
106+
}}
107+
/>
108+
<span className='text-tertiary flex h-5 w-5 items-center justify-center'>
109+
<div className='border-l !border-l-[#d1d9e0]'>
110+
<Button variant='plain' className='rounded-none bg-[#f6f8fa]' tooltip='search'>
111+
{isSearchLoading ? <LazyLoadingSpinner fallback={<SearchIcon />} /> : <SearchIcon />}
112+
</Button>
113+
</div>
114+
</span>
115+
</div>
116+
</BreadcrumbTitlebar>
117+
<LabelList
118+
isLoading={isLoading}
119+
Issuelists={showLabelList}
120+
header={
121+
<BreadcrumbTitlebar className='justify-between bg-gray-100 pl-3 pr-3'>
122+
<span className='p-2 font-medium'>{numTotal} labels</span>
123+
</BreadcrumbTitlebar>
124+
}
125+
>
126+
{(labels) => {
127+
return labels.map((label) => {
128+
const fontColor = colord(label.color).lighten(0.5).toHex()
129+
130+
return (
131+
<ListItem
132+
key={label.id}
133+
title={''}
134+
onClick={() => router.push(`/${scope}/issue?q=label:${label.name}`)}
135+
>
136+
<div className='flex items-center gap-2'>
137+
<div
138+
style={{
139+
backgroundColor: label.color,
140+
color: fontColor,
141+
border: `1px solid ${fontColor}`,
142+
borderRadius: '16px',
143+
padding: '2px 8px',
144+
fontSize: '12px',
145+
fontWeight: '500',
146+
justifyContent: 'center',
147+
textAlign: 'center'
148+
}}
149+
>
150+
{label.name}
151+
</div>
152+
<div className='flex-1 text-center'>
153+
<span className='text-gray-500'>description: {label.description}</span>
154+
</div>
155+
</div>
156+
</ListItem>
157+
)
158+
})
159+
}}
160+
</LabelList>
161+
{numTotal > per_page && (
162+
<Pagination
163+
totalNum={numTotal}
164+
pageSize={per_page}
165+
onChange={setPage}
166+
currentPage={labelsOpenCurrentPage}
167+
/>
168+
)}
169+
</IndexPageContent>
170+
</IndexPageContainer>
171+
</div>
172+
</>
173+
)
174+
}
175+
176+
LabelsPage.getProviders = (
177+
page:
178+
| string
179+
| number
180+
| boolean
181+
| ReactElement
182+
| Iterable<ReactNode>
183+
| ReactPortal
184+
| Promise<AwaitedReactNode>
185+
| null
186+
| undefined,
187+
pageProps: JSX.IntrinsicAttributes & { children?: ReactNode }
188+
) => {
189+
return (
190+
<AuthAppProviders {...pageProps}>
191+
<AppLayout {...pageProps}>{page}</AppLayout>
192+
</AuthAppProviders>
193+
)
194+
}
195+
196+
export default LabelsPage

0 commit comments

Comments
 (0)