@@ -31,7 +31,9 @@ import { showSwitchOrg } from "@lowcoder-ee/pages/common/customerService";
3131import { checkIsMobile } from "util/commonUtils" ;
3232import { selectSystemConfig } from "redux/selectors/configSelectors" ;
3333import type { ItemType } from "antd/es/menu/interface" ;
34-
34+ import { Pagination } from "antd" ;
35+ import { debounce } from "lodash" ;
36+ import UserApi from "api/userApi" ;
3537const { Item } = Menu ;
3638
3739const ProfileDropdownContainer = styled . div `
@@ -231,6 +233,46 @@ const StyledDropdown = styled(Dropdown)`
231233 align-items: end;
232234` ;
233235
236+
237+ const PaginationContainer = styled . div `
238+ padding: 12px 16px;
239+ border-top: 1px solid #f0f0f0;
240+ display: flex;
241+ justify-content: center;
242+
243+ .ant-pagination {
244+ margin: 0;
245+
246+ .ant-pagination-item {
247+ min-width: 24px;
248+ height: 24px;
249+ line-height: 22px;
250+ font-size: 12px;
251+ margin-right: 4px;
252+ }
253+
254+ .ant-pagination-prev,
255+ .ant-pagination-next {
256+ min-width: 24px;
257+ height: 24px;
258+ line-height: 22px;
259+ margin-right: 4px;
260+ }
261+
262+ .ant-pagination-item-link {
263+ font-size: 11px;
264+ }
265+ }
266+ ` ;
267+ const LoadingSpinner = styled . div `
268+ display: flex;
269+ align-items: center;
270+ justify-content: center;
271+ padding: 16px;
272+ color: #8b8fa3;
273+ font-size: 13px;
274+ ` ;
275+
234276type DropDownProps = {
235277 onClick ?: ( text : string ) => void ;
236278 user : User ;
@@ -246,9 +288,107 @@ export default function ProfileDropdown(props: DropDownProps) {
246288 const settingModalVisible = useSelector ( isProfileSettingModalVisible ) ;
247289 const sysConfig = useSelector ( selectSystemConfig ) ;
248290 const dispatch = useDispatch ( ) ;
249- const [ searchTerm , setSearchTerm ] = useState ( "" ) ;
250- const [ dropdownVisible , setDropdownVisible ] = useState ( false ) ;
251291
292+ // Local state for pagination and search
293+ const [ searchTerm , setSearchTerm ] = useState ( "" ) ;
294+ const [ dropdownVisible , setDropdownVisible ] = useState ( false ) ;
295+ const [ currentPageWorkspaces , setCurrentPageWorkspaces ] = useState < Org [ ] > ( [ ] ) ;
296+ const [ currentPage , setCurrentPage ] = useState ( 1 ) ;
297+ const [ totalCount , setTotalCount ] = useState ( 0 ) ;
298+ const [ isLoading , setIsLoading ] = useState ( false ) ;
299+ const [ isSearching , setIsSearching ] = useState ( false ) ;
300+
301+ const pageSize = 10 ;
302+
303+ // Determine which workspaces to show
304+ const displayWorkspaces = useMemo ( ( ) => {
305+ if ( searchTerm . trim ( ) ) {
306+ return currentPageWorkspaces ; // Search results
307+ }
308+ if ( currentPage === 1 ) {
309+ return workspaces . items ; // First page from Redux
310+ }
311+ return currentPageWorkspaces ; // Other pages from API
312+ } , [ searchTerm , currentPage , workspaces . items , currentPageWorkspaces ] ) ;
313+
314+ // Update total count based on context
315+ useEffect ( ( ) => {
316+ if ( searchTerm . trim ( ) ) {
317+ // Keep search result count
318+ return ;
319+ }
320+ if ( currentPage === 1 ) {
321+ setTotalCount ( workspaces . totalCount ) ;
322+ }
323+ } , [ searchTerm , currentPage , workspaces . totalCount ] ) ;
324+
325+ // Fetch workspaces for specific page
326+ const fetchWorkspacesPage = async ( page : number , search ?: string ) => {
327+ setIsLoading ( true ) ;
328+ try {
329+ const response = await UserApi . getMyOrgs ( page , pageSize , search ) ;
330+ if ( response . data . success ) {
331+ const apiData = response . data . data ;
332+ const transformedItems = apiData . data . map ( item => ( {
333+ id : item . orgId ,
334+ name : item . orgName ,
335+ } ) ) ;
336+
337+ setCurrentPageWorkspaces ( transformedItems as Org [ ] ) ;
338+ setTotalCount ( apiData . total ) ;
339+ }
340+ } catch ( error ) {
341+ console . error ( 'Error fetching workspaces:' , error ) ;
342+ setCurrentPageWorkspaces ( [ ] ) ;
343+ } finally {
344+ setIsLoading ( false ) ;
345+ }
346+ } ;
347+
348+ // Handle page change
349+ const handlePageChange = ( page : number ) => {
350+ setCurrentPage ( page ) ;
351+ if ( page === 1 && ! searchTerm . trim ( ) ) {
352+ // Use Redux data for first page when not searching
353+ setCurrentPageWorkspaces ( [ ] ) ;
354+ } else {
355+ // Fetch from API for other pages or when searching
356+ fetchWorkspacesPage ( page , searchTerm . trim ( ) || undefined ) ;
357+ }
358+ } ;
359+
360+
361+ // Debounced search function
362+ const debouncedSearch = useMemo (
363+ ( ) => debounce ( async ( term : string ) => {
364+ if ( ! term . trim ( ) ) {
365+ setCurrentPage ( 1 ) ;
366+ setCurrentPageWorkspaces ( [ ] ) ;
367+ setTotalCount ( workspaces . totalCount ) ;
368+ setIsSearching ( false ) ;
369+ return ;
370+ }
371+
372+ setIsSearching ( true ) ;
373+ setCurrentPage ( 1 ) ;
374+ await fetchWorkspacesPage ( 1 , term ) ;
375+ setIsSearching ( false ) ;
376+ } , 300 ) ,
377+ [ workspaces . totalCount ]
378+ ) ;
379+
380+
381+
382+ // Reset state when dropdown closes
383+ useEffect ( ( ) => {
384+ if ( ! dropdownVisible ) {
385+ setCurrentPageWorkspaces ( [ ] ) ;
386+ setCurrentPage ( 1 ) ;
387+ setSearchTerm ( "" ) ;
388+ setTotalCount ( workspaces . totalCount ) ;
389+ setIsSearching ( false ) ;
390+ }
391+ } , [ dropdownVisible , workspaces . totalCount ] ) ;
252392
253393
254394
@@ -292,12 +432,15 @@ export default function ProfileDropdown(props: DropDownProps) {
292432 setDropdownVisible ( false ) ;
293433 } ;
294434
295- const handleSearchChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
296- setSearchTerm ( e . target . value ) ;
297- } ;
435+ // Handle search input change
436+ const handleSearchChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
437+ const value = e . target . value ;
438+ setSearchTerm ( value ) ;
439+ debouncedSearch ( value ) ;
440+ } ;
298441
299442 const dropdownContent = (
300- < ProfileDropdownContainer onClick = { e => e . stopPropagation ( ) } >
443+ < ProfileDropdownContainer onClick = { ( e ) => e . stopPropagation ( ) } >
301444 { /* Profile Section */ }
302445 < ProfileSection onClick = { handleProfileClick } >
303446 < ProfileImage source = { avatarUrl } userName = { username } side = { 40 } />
@@ -310,48 +453,86 @@ export default function ProfileDropdown(props: DropDownProps) {
310453 < ProfileRole > { OrgRoleInfo [ currentOrgRoleId ] . name } </ ProfileRole >
311454 ) }
312455 </ ProfileInfo >
313- { ! checkIsMobile ( window . innerWidth ) && < EditIcon style = { { color : '#8b8fa3' } } /> }
456+ { ! checkIsMobile ( window . innerWidth ) && (
457+ < EditIcon style = { { color : "#8b8fa3" } } />
458+ ) }
314459 </ ProfileSection >
315460
316461 { /* Workspaces Section */ }
317- { workspaces . items && workspaces . items . length > 0 && showSwitchOrg ( props . user , sysConfig ) && (
318- < WorkspaceSection >
319- < SectionHeader > { trans ( "profile.switchOrg" ) } </ SectionHeader >
320-
321- { workspaces . items . length > 3 && (
322- < SearchContainer >
323- < StyledSearchInput
324- placeholder = "Search workspaces..."
325- value = { searchTerm }
326- onChange = { handleSearchChange }
327- prefix = { < SearchIcon style = { { color : '#8b8fa3' } } /> }
328- size = "small"
329- />
330- </ SearchContainer >
331- ) }
462+ { workspaces . items &&
463+ workspaces . items . length > 0 &&
464+ showSwitchOrg ( props . user , sysConfig ) && (
465+ < WorkspaceSection >
466+ < SectionHeader > { trans ( "profile.switchOrg" ) } </ SectionHeader >
467+
468+ { workspaces . items . length > 3 && (
469+ < SearchContainer >
470+ < StyledSearchInput
471+ placeholder = "Search workspaces..."
472+ value = { searchTerm }
473+ onChange = { handleSearchChange }
474+ prefix = { < SearchIcon style = { { color : "#8b8fa3" } } /> }
475+ size = "small"
476+ />
477+ </ SearchContainer >
478+ ) }
332479
333- < WorkspaceList >
334- { filteredOrgs . length > 0 ? (
335- filteredOrgs . map ( ( org : Org ) => (
336- < WorkspaceItem
337- key = { org . id }
338- isActive = { currentOrgId === org . id }
339- onClick = { ( ) => handleOrgSwitch ( org . id ) }
340- >
341- < WorkspaceName title = { org . name } > { org . name } </ WorkspaceName >
342- { currentOrgId === org . id && < ActiveIcon /> }
343- </ WorkspaceItem >
344- ) )
345- ) : (
346- < EmptyState > No workspaces found</ EmptyState >
480+ { /* Workspaces List */ }
481+ < WorkspaceList >
482+ { isSearching || isLoading ? (
483+ < LoadingSpinner >
484+ < PackUpIcon
485+ style = { {
486+ animation : "spin 1s linear infinite" ,
487+ marginRight : "8px" ,
488+ } }
489+ />
490+ { isSearching ? "Searching..." : "Loading..." }
491+ </ LoadingSpinner >
492+ ) : displayWorkspaces . length > 0 ? (
493+ displayWorkspaces . map ( ( org : Org ) => (
494+ < WorkspaceItem
495+ key = { org . id }
496+ isActive = { currentOrgId === org . id }
497+ onClick = { ( ) => handleOrgSwitch ( org . id ) }
498+ >
499+ < WorkspaceName title = { org . name } > { org . name } </ WorkspaceName >
500+ { currentOrgId === org . id && < ActiveIcon /> }
501+ </ WorkspaceItem >
502+ ) )
503+ ) : (
504+ < EmptyState >
505+ { searchTerm . trim ( )
506+ ? "No workspaces found"
507+ : "No workspaces available" }
508+ </ EmptyState >
509+ ) }
510+ </ WorkspaceList >
511+
512+ { /* Pagination */ }
513+ { totalCount > pageSize && ! isSearching && ! isLoading && (
514+ < PaginationContainer >
515+ < Pagination
516+ current = { currentPage }
517+ total = { totalCount }
518+ pageSize = { pageSize }
519+ size = "small"
520+ showSizeChanger = { false }
521+ showQuickJumper = { false }
522+ showTotal = { ( total , range ) =>
523+ `${ range [ 0 ] } -${ range [ 1 ] } of ${ total } `
524+ }
525+ onChange = { handlePageChange }
526+ simple = { totalCount > 100 }
527+ />
528+ </ PaginationContainer >
347529 ) }
348- </ WorkspaceList >
349530 < CreateWorkspaceItem onClick = { handleCreateOrg } >
350531 < AddIcon />
351532 { trans ( "profile.createOrg" ) }
352533 </ CreateWorkspaceItem >
353- </ WorkspaceSection >
354- ) }
534+ </ WorkspaceSection >
535+ ) }
355536
356537 { /* Actions Section */ }
357538 < ActionsSection >
0 commit comments