@@ -2,8 +2,10 @@ import { Button } from "@ui/Button";
22import { Tooltip } from "@ui/Tooltip" ;
33import { Checkbox } from "@ui/Checkbox" ;
44import { Modal } from "@ui/Modal" ;
5+ import { TextInput } from "@ui/TextInput" ;
6+ import { LoadingLogo } from "@ui/Loading" ;
57import difference from "lodash/difference" ;
6- import React , { useState } from "react" ;
8+ import React , { useState , useEffect } from "react" ;
79import type {
810 TeamResponse ,
911 ProjectMemberRoleResponse ,
@@ -13,20 +15,22 @@ import type {
1315} from "generatedApi" ;
1416import Link from "next/link" ;
1517import { useHasProjectAdminPermissions } from "api/roles" ;
18+ import { usePaginatedProjects } from "api/projects" ;
1619import sortBy from "lodash/sortBy" ;
1720import { TeamMemberLink } from "elements/TeamMemberLink" ;
21+ import { PaginationControls } from "elements/PaginationControls" ;
22+ import { useDebounce } from "react-use" ;
23+ import { useGlobalLocalStorage } from "@common/lib/useGlobalLocalStorage" ;
1824import { ProjectLink } from "./AuditLogItem" ;
1925
2026export function MemberProjectRolesModal ( {
2127 team,
22- projects,
2328 member,
2429 projectRoles,
2530 onUpdateProjectRoles,
2631 onClose,
2732} : {
2833 team : TeamResponse ;
29- projects : ProjectDetails [ ] ;
3034 member : TeamMemberResponse ;
3135 projectRoles : ProjectMemberRoleResponse [ ] ;
3236 onUpdateProjectRoles : ( body : UpdateProjectRolesArgs ) => Promise < undefined > ;
@@ -42,6 +46,73 @@ export function MemberProjectRolesModal({
4246
4347 const [ isSaving , setIsSaving ] = useState ( false ) ;
4448
49+ // Pagination and search state
50+ const [ projectQuery , setProjectQuery ] = useState ( "" ) ;
51+ const [ debouncedQuery , setDebouncedQuery ] = useState ( "" ) ;
52+ const [ currentCursor , setCurrentCursor ] = useState < string | undefined > (
53+ undefined ,
54+ ) ;
55+ const [ cursorHistory , setCursorHistory ] = useState < ( string | undefined ) [ ] > ( [
56+ undefined ,
57+ ] ) ;
58+ const [ pageSize , setPageSize ] = useGlobalLocalStorage ( "projectsPageSize" , 25 ) ;
59+
60+ // Debounce search query (300ms delay)
61+ useDebounce (
62+ ( ) => {
63+ setDebouncedQuery ( projectQuery ) ;
64+ } ,
65+ 300 ,
66+ [ projectQuery ] ,
67+ ) ;
68+
69+ // Fetch paginated projects with debounced query
70+ const paginatedData = usePaginatedProjects (
71+ team . id ,
72+ {
73+ cursor : currentCursor ,
74+ q : debouncedQuery . trim ( ) || undefined ,
75+ } ,
76+ 30000 ,
77+ ) ;
78+
79+ const projects = paginatedData ?. items ?? [ ] ;
80+ const hasMore = paginatedData ?. pagination . hasMore ?? false ;
81+ const nextCursor = paginatedData ?. pagination . nextCursor ;
82+ const isLoading = paginatedData === undefined ;
83+
84+ // Calculate current page range for display
85+ const currentPageNumber = cursorHistory . length ;
86+
87+ const handleNextPage = ( ) => {
88+ if ( nextCursor ) {
89+ setCursorHistory ( ( prev ) => [ ...prev , currentCursor ] ) ;
90+ setCurrentCursor ( nextCursor ) ;
91+ }
92+ } ;
93+
94+ const handlePrevPage = ( ) => {
95+ if ( cursorHistory . length > 1 ) {
96+ const newHistory = [ ...cursorHistory ] ;
97+ newHistory . pop ( ) ;
98+ setCursorHistory ( newHistory ) ;
99+ setCurrentCursor ( newHistory [ newHistory . length - 1 ] ) ;
100+ }
101+ } ;
102+
103+ const handlePageSizeChange = ( newPageSize : number ) => {
104+ setPageSize ( newPageSize ) ;
105+ // Reset to first page when page size changes
106+ setCurrentCursor ( undefined ) ;
107+ setCursorHistory ( [ undefined ] ) ;
108+ } ;
109+
110+ // Reset cursor when debounced search query changes
111+ useEffect ( ( ) => {
112+ setCurrentCursor ( undefined ) ;
113+ setCursorHistory ( [ undefined ] ) ;
114+ } , [ debouncedQuery ] ) ;
115+
45116 const closeWithConfirmation = ( ) => {
46117 if ( addedProjects . length > 0 || removedProjects . length > 0 ) {
47118 // eslint-disable-next-line no-alert
@@ -101,9 +172,43 @@ export function MemberProjectRolesModal({
101172 }
102173 } }
103174 >
104- < div className = "scrollbar max-h-[60vh] overflow-auto" >
105- { sortBy ( projects , ( project ) => project . name . toLocaleLowerCase ( ) ) . map (
106- ( project ) => (
175+ { /* Search input */ }
176+ < TextInput
177+ placeholder = "Search projects"
178+ value = { projectQuery }
179+ onChange = { ( e ) => setProjectQuery ( e . target . value ) }
180+ type = "search"
181+ id = "Search projects in modal"
182+ isSearchLoading = { isLoading && debouncedQuery === projectQuery }
183+ />
184+
185+ { /* Loading state */ }
186+ { projects . length === 0 && isLoading && (
187+ < div className = "my-12 flex flex-col items-center gap-2" >
188+ < LoadingLogo />
189+ </ div >
190+ ) }
191+
192+ { /* Empty search results */ }
193+ { projects . length === 0 && ! isLoading && debouncedQuery . trim ( ) && (
194+ < div className = "my-12 flex animate-fadeInFromLoading flex-col items-center gap-2 text-content-secondary" >
195+ No projects match your search.
196+ </ div >
197+ ) }
198+
199+ { /* Empty state - no projects */ }
200+ { projects . length === 0 && ! isLoading && ! debouncedQuery . trim ( ) && (
201+ < div className = "my-12 flex flex-col items-center gap-2 text-content-secondary" >
202+ This team doesn't have any projects yet.
203+ </ div >
204+ ) }
205+
206+ { /* Project list */ }
207+ { projects . length > 0 && (
208+ < div className = "scrollbar max-h-[40vh] overflow-auto" >
209+ { sortBy ( projects , ( project ) =>
210+ project . name . toLocaleLowerCase ( ) ,
211+ ) . map ( ( project ) => (
107212 < ProjectRoleItem
108213 key = { project . id }
109214 project = { project }
@@ -112,10 +217,25 @@ export function MemberProjectRolesModal({
112217 newProjectRoles = { newProjectRoles }
113218 setNewProjectRoles = { setNewProjectRoles }
114219 />
115- ) ,
116- ) }
117- </ div >
118- < p className = "text-xs text-content-secondary" >
220+ ) ) }
221+ </ div >
222+ ) }
223+
224+ { /* Bottom pagination controls with page size */ }
225+ { projects . length > 0 && (
226+ < PaginationControls
227+ showPageSize
228+ isCursorBasedPagination
229+ currentPage = { currentPageNumber }
230+ hasMore = { hasMore }
231+ pageSize = { pageSize }
232+ onPageSizeChange = { handlePageSizeChange }
233+ onPreviousPage = { handlePrevPage }
234+ onNextPage = { handleNextPage }
235+ canGoPrevious = { cursorHistory . length > 1 }
236+ />
237+ ) }
238+ < p className = "mt-1 text-xs text-content-secondary" >
119239 Pro-tip! You can manage the Project Admin role for multiple members at
120240 the same time on the{ " " }
121241 < Link
0 commit comments