@@ -31,7 +31,9 @@ import { showSwitchOrg } from "@lowcoder-ee/pages/common/customerService";
31
31
import { checkIsMobile } from "util/commonUtils" ;
32
32
import { selectSystemConfig } from "redux/selectors/configSelectors" ;
33
33
import type { ItemType } from "antd/es/menu/interface" ;
34
-
34
+ import { Pagination } from "antd" ;
35
+ import { debounce } from "lodash" ;
36
+ import UserApi from "api/userApi" ;
35
37
const { Item } = Menu ;
36
38
37
39
const ProfileDropdownContainer = styled . div `
@@ -231,6 +233,46 @@ const StyledDropdown = styled(Dropdown)`
231
233
align-items: end;
232
234
` ;
233
235
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
+
234
276
type DropDownProps = {
235
277
onClick ?: ( text : string ) => void ;
236
278
user : User ;
@@ -246,9 +288,107 @@ export default function ProfileDropdown(props: DropDownProps) {
246
288
const settingModalVisible = useSelector ( isProfileSettingModalVisible ) ;
247
289
const sysConfig = useSelector ( selectSystemConfig ) ;
248
290
const dispatch = useDispatch ( ) ;
249
- const [ searchTerm , setSearchTerm ] = useState ( "" ) ;
250
- const [ dropdownVisible , setDropdownVisible ] = useState ( false ) ;
251
291
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 ] ) ;
252
392
253
393
254
394
@@ -292,12 +432,15 @@ export default function ProfileDropdown(props: DropDownProps) {
292
432
setDropdownVisible ( false ) ;
293
433
} ;
294
434
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
+ } ;
298
441
299
442
const dropdownContent = (
300
- < ProfileDropdownContainer onClick = { e => e . stopPropagation ( ) } >
443
+ < ProfileDropdownContainer onClick = { ( e ) => e . stopPropagation ( ) } >
301
444
{ /* Profile Section */ }
302
445
< ProfileSection onClick = { handleProfileClick } >
303
446
< ProfileImage source = { avatarUrl } userName = { username } side = { 40 } />
@@ -310,48 +453,86 @@ export default function ProfileDropdown(props: DropDownProps) {
310
453
< ProfileRole > { OrgRoleInfo [ currentOrgRoleId ] . name } </ ProfileRole >
311
454
) }
312
455
</ ProfileInfo >
313
- { ! checkIsMobile ( window . innerWidth ) && < EditIcon style = { { color : '#8b8fa3' } } /> }
456
+ { ! checkIsMobile ( window . innerWidth ) && (
457
+ < EditIcon style = { { color : "#8b8fa3" } } />
458
+ ) }
314
459
</ ProfileSection >
315
460
316
461
{ /* 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
+ ) }
332
479
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 >
347
529
) }
348
- </ WorkspaceList >
349
530
< CreateWorkspaceItem onClick = { handleCreateOrg } >
350
531
< AddIcon />
351
532
{ trans ( "profile.createOrg" ) }
352
533
</ CreateWorkspaceItem >
353
- </ WorkspaceSection >
354
- ) }
534
+ </ WorkspaceSection >
535
+ ) }
355
536
356
537
{ /* Actions Section */ }
357
538
< ActionsSection >
0 commit comments