Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 9ea107b

Browse files
committed
add pagination and filtering for the dropdown
1 parent f851353 commit 9ea107b

File tree

1 file changed

+221
-40
lines changed

1 file changed

+221
-40
lines changed

‎client/packages/lowcoder/src/pages/common/profileDropdown.tsx

Lines changed: 221 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ import { showSwitchOrg } from "@lowcoder-ee/pages/common/customerService";
3131
import { checkIsMobile } from "util/commonUtils";
3232
import { selectSystemConfig } from "redux/selectors/configSelectors";
3333
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";
3537
const { Item } = Menu;
3638

3739
const 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+
234276
type 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

Comments
 (0)